mirror of
https://github.com/grafana/grafana.git
synced 2025-07-31 15:42:55 +08:00
Search: Explain scores (#98316)
This commit is contained in:
@ -64,10 +64,10 @@ type DashboardHit struct {
|
||||
Folder string `json:"folder,omitempty"`
|
||||
// Stick untyped extra fields in this object (including the sort value)
|
||||
Field *common.Unstructured `json:"field,omitempty"`
|
||||
// Explain the score (if possible)
|
||||
Explain *common.Unstructured `json:"explain,omitempty"`
|
||||
// When using "real" search, this is the score
|
||||
Score float64 `json:"score,omitempty"`
|
||||
// Explain the score (if possible)
|
||||
Explain *common.Unstructured `json:"explain,omitempty"`
|
||||
}
|
||||
|
||||
type FacetResult struct {
|
||||
|
@ -268,12 +268,6 @@ func schema_pkg_apis_dashboard_v0alpha1_DashboardHit(ref common.ReferenceCallbac
|
||||
Ref: ref("github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1.Unstructured"),
|
||||
},
|
||||
},
|
||||
"explain": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "Explain the score (if possible)",
|
||||
Ref: ref("github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1.Unstructured"),
|
||||
},
|
||||
},
|
||||
"score": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "When using \"real\" search, this is the score",
|
||||
@ -281,6 +275,12 @@ func schema_pkg_apis_dashboard_v0alpha1_DashboardHit(ref common.ReferenceCallbac
|
||||
Format: "double",
|
||||
},
|
||||
},
|
||||
"explain": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "Explain the score (if possible)",
|
||||
Ref: ref("github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1.Unstructured"),
|
||||
},
|
||||
},
|
||||
},
|
||||
Required: []string{"resource", "name", "title"},
|
||||
},
|
||||
|
@ -223,6 +223,7 @@ func (s *SearchHandler) DoSearch(w http.ResponseWriter, r *http.Request) {
|
||||
Query: queryParams.Get("query"),
|
||||
Limit: int64(limit),
|
||||
Offset: int64(offset),
|
||||
Explain: queryParams.Has("explain") && queryParams.Get("explain") != "false",
|
||||
Fields: []string{
|
||||
"title",
|
||||
"folder",
|
||||
|
@ -2,6 +2,7 @@ package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
@ -10,18 +11,18 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/grafana/authlib/claims"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"go.opentelemetry.io/otel"
|
||||
"golang.org/x/exp/slices"
|
||||
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
k8sUser "k8s.io/apiserver/pkg/authentication/user"
|
||||
k8sRequest "k8s.io/apiserver/pkg/endpoints/request"
|
||||
"k8s.io/client-go/dynamic"
|
||||
|
||||
"golang.org/x/exp/slices"
|
||||
|
||||
"github.com/grafana/authlib/claims"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend/gtime"
|
||||
|
||||
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
||||
"github.com/grafana/grafana/pkg/apimachinery/utils"
|
||||
"github.com/grafana/grafana/pkg/apis/dashboard/v0alpha1"
|
||||
@ -46,8 +47,6 @@ import (
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/storage/unified/resource"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
k8sUser "k8s.io/apiserver/pkg/authentication/user"
|
||||
k8sRequest "k8s.io/apiserver/pkg/endpoints/request"
|
||||
)
|
||||
|
||||
var (
|
||||
@ -1356,6 +1355,27 @@ func ParseResults(result *resource.ResourceSearchResponse, offset int64) *v0alph
|
||||
return nil
|
||||
}
|
||||
|
||||
titleIDX := 0
|
||||
folderIDX := 1
|
||||
tagsIDX := -1
|
||||
scoreIDX := 0
|
||||
explainIDX := 0
|
||||
|
||||
for i, v := range result.Results.Columns {
|
||||
switch v.Name {
|
||||
case resource.SEARCH_FIELD_EXPLAIN:
|
||||
explainIDX = i
|
||||
case resource.SEARCH_FIELD_SCORE:
|
||||
scoreIDX = i
|
||||
case "title":
|
||||
titleIDX = i
|
||||
case "folder":
|
||||
folderIDX = i
|
||||
case "tags":
|
||||
tagsIDX = i
|
||||
}
|
||||
}
|
||||
|
||||
sr := &v0alpha1.SearchResults{
|
||||
Offset: offset,
|
||||
TotalHits: result.TotalHits,
|
||||
@ -1364,28 +1384,21 @@ func ParseResults(result *resource.ResourceSearchResponse, offset int64) *v0alph
|
||||
Hits: make([]v0alpha1.DashboardHit, len(result.Results.Rows)),
|
||||
}
|
||||
|
||||
titleRow := 0
|
||||
folderRow := 1
|
||||
tagsRow := -1
|
||||
for i, row := range result.Results.GetColumns() {
|
||||
if row.Name == "title" {
|
||||
titleRow = i
|
||||
} else if row.Name == "folder" {
|
||||
folderRow = i
|
||||
} else if row.Name == "tags" {
|
||||
tagsRow = i
|
||||
}
|
||||
}
|
||||
|
||||
for i, row := range result.Results.Rows {
|
||||
hit := &v0alpha1.DashboardHit{
|
||||
Resource: row.Key.Resource, // folders | dashboards
|
||||
Name: row.Key.Name, // The Grafana UID
|
||||
Title: string(row.Cells[titleRow]),
|
||||
Folder: string(row.Cells[folderRow]),
|
||||
Title: string(row.Cells[titleIDX]),
|
||||
Folder: string(row.Cells[folderIDX]),
|
||||
}
|
||||
if tagsRow != -1 && row.Cells[tagsRow] != nil {
|
||||
_ = json.Unmarshal(row.Cells[tagsRow], &hit.Tags)
|
||||
if tagsIDX > 0 && row.Cells[tagsIDX] != nil {
|
||||
_ = json.Unmarshal(row.Cells[tagsIDX], &hit.Tags)
|
||||
}
|
||||
if explainIDX > 0 && row.Cells[explainIDX] != nil {
|
||||
_ = json.Unmarshal(row.Cells[explainIDX], &hit.Explain)
|
||||
}
|
||||
if scoreIDX > 0 && row.Cells[scoreIDX] != nil {
|
||||
_, _ = binary.Decode(row.Cells[scoreIDX], binary.BigEndian, &hit.Score)
|
||||
}
|
||||
|
||||
sr.Hits[i] = *hit
|
||||
|
@ -343,6 +343,16 @@ func StandardSearchFields() SearchableDocumentFields {
|
||||
Type: ResourceTableColumnDefinition_INT64,
|
||||
Description: "created timestamp", // date?
|
||||
},
|
||||
{
|
||||
Name: SEARCH_FIELD_EXPLAIN,
|
||||
Type: ResourceTableColumnDefinition_OBJECT,
|
||||
Description: "Explain why this result matches (depends on the engine)",
|
||||
},
|
||||
{
|
||||
Name: SEARCH_FIELD_SCORE,
|
||||
Type: ResourceTableColumnDefinition_DOUBLE,
|
||||
Description: "The search score",
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
panic("failed to initialize standard search fields")
|
||||
|
@ -123,6 +123,9 @@ func NewTableBuilder(cols []*ResourceTableColumnDefinition) (*TableBuilder, erro
|
||||
}
|
||||
var err error
|
||||
for i, v := range cols {
|
||||
if v == nil {
|
||||
return nil, fmt.Errorf("invalid field definitions")
|
||||
}
|
||||
if table.lookup[v.Name] != nil {
|
||||
table.hasDuplicateNames = true
|
||||
continue
|
||||
|
@ -2,6 +2,7 @@ package search
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
@ -413,7 +414,11 @@ func toBleveSearchRequest(req *resource.ResourceSearchRequest, access authz.Acce
|
||||
}
|
||||
}
|
||||
|
||||
queries = append(queries, newTextQuery(req))
|
||||
// Add a text query
|
||||
if req.Query != "" && req.Query != "*" {
|
||||
searchrequest.Fields = append(searchrequest.Fields, resource.SEARCH_FIELD_SCORE)
|
||||
queries = append(queries, bleve.NewFuzzyQuery(req.Query))
|
||||
}
|
||||
|
||||
if access != nil {
|
||||
// TODO AUTHZ!!!!
|
||||
@ -581,19 +586,27 @@ func (b *bleveIndex) hitsToTable(selectFields []string, hits search.DocumentMatc
|
||||
}
|
||||
|
||||
for i, f := range fields {
|
||||
if f.Name == resource.SEARCH_FIELD_ID {
|
||||
var v any
|
||||
switch f.Name {
|
||||
case resource.SEARCH_FIELD_ID:
|
||||
row.Cells[i] = []byte(match.ID)
|
||||
continue
|
||||
}
|
||||
|
||||
// QUICK QUICK... more options yes
|
||||
v := match.Fields[f.Name]
|
||||
if v != nil {
|
||||
// Encode the value to protobuf
|
||||
row.Cells[i], err = encoders[i](v)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error encoding (row:%d/col:%d) %v %w", rowID, i, v, err)
|
||||
case resource.SEARCH_FIELD_SCORE:
|
||||
row.Cells[i], err = encoders[i](match.Score)
|
||||
|
||||
case resource.SEARCH_FIELD_EXPLAIN:
|
||||
if match.Expl != nil {
|
||||
row.Cells[i], err = json.Marshal(match.Expl)
|
||||
}
|
||||
default:
|
||||
v := match.Fields[f.Name]
|
||||
if v != nil {
|
||||
// Encode the value to protobuf
|
||||
row.Cells[i], err = encoders[i](v)
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error encoding (row:%d/col:%d) %v %w", rowID, i, v, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -644,12 +657,3 @@ func newResponseFacet(v *search.FacetResult) *resource.ResourceSearchResponse_Fa
|
||||
}
|
||||
return f
|
||||
}
|
||||
|
||||
func newTextQuery(req *resource.ResourceSearchRequest) query.Query {
|
||||
if req.Query == "" || req.Query == "*" {
|
||||
return bleve.NewMatchAllQuery()
|
||||
}
|
||||
// TODO: wildcard query?
|
||||
// return bleve.NewWildcardQuery(req.Query)
|
||||
return bleve.NewFuzzyQuery(req.Query)
|
||||
}
|
||||
|
Reference in New Issue
Block a user