Search: Explain scores (#98316)

pull/98534/head
Ryan McKinley 6 months ago committed by GitHub
parent bfa56bcf08
commit d1d7c0850f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 4
      pkg/apis/dashboard/v0alpha1/search.go
  2. 12
      pkg/apis/dashboard/v0alpha1/zz_generated.openapi.go
  3. 1
      pkg/registry/apis/dashboard/search.go
  4. 59
      pkg/services/dashboards/service/dashboard_service.go
  5. 10
      pkg/storage/unified/resource/document.go
  6. 3
      pkg/storage/unified/resource/table.go
  7. 44
      pkg/storage/unified/search/bleve.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 {

@ -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 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 tagsRow != -1 && row.Cells[tagsRow] != nil {
_ = json.Unmarshal(row.Cells[tagsRow], &hit.Tags)
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,20 +586,28 @@ 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)
}

Loading…
Cancel
Save