diff --git a/pkg/apis/dashboard/v0alpha1/search.go b/pkg/apis/dashboard/v0alpha1/search.go index f569fda081e..96afdc447ff 100644 --- a/pkg/apis/dashboard/v0alpha1/search.go +++ b/pkg/apis/dashboard/v0alpha1/search.go @@ -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 { diff --git a/pkg/apis/dashboard/v0alpha1/zz_generated.openapi.go b/pkg/apis/dashboard/v0alpha1/zz_generated.openapi.go index 6aa20485d9f..cb73d61dc29 100644 --- a/pkg/apis/dashboard/v0alpha1/zz_generated.openapi.go +++ b/pkg/apis/dashboard/v0alpha1/zz_generated.openapi.go @@ -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"}, }, diff --git a/pkg/registry/apis/dashboard/search.go b/pkg/registry/apis/dashboard/search.go index 5b92813eaea..16e43feaed0 100644 --- a/pkg/registry/apis/dashboard/search.go +++ b/pkg/registry/apis/dashboard/search.go @@ -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", diff --git a/pkg/services/dashboards/service/dashboard_service.go b/pkg/services/dashboards/service/dashboard_service.go index 5f680cfac6d..0531772ec57 100644 --- a/pkg/services/dashboards/service/dashboard_service.go +++ b/pkg/services/dashboards/service/dashboard_service.go @@ -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 diff --git a/pkg/storage/unified/resource/document.go b/pkg/storage/unified/resource/document.go index cea910f81c4..4ca6933b1bb 100644 --- a/pkg/storage/unified/resource/document.go +++ b/pkg/storage/unified/resource/document.go @@ -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") diff --git a/pkg/storage/unified/resource/table.go b/pkg/storage/unified/resource/table.go index abcbecfbec0..a58cf2dd13a 100644 --- a/pkg/storage/unified/resource/table.go +++ b/pkg/storage/unified/resource/table.go @@ -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 diff --git a/pkg/storage/unified/search/bleve.go b/pkg/storage/unified/search/bleve.go index c07ec260f82..ec4d2d64b8e 100644 --- a/pkg/storage/unified/search/bleve.go +++ b/pkg/storage/unified/search/bleve.go @@ -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) -}