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"` Folder string `json:"folder,omitempty"`
// Stick untyped extra fields in this object (including the sort value) // Stick untyped extra fields in this object (including the sort value)
Field *common.Unstructured `json:"field,omitempty"` 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 // When using "real" search, this is the score
Score float64 `json:"score,omitempty"` Score float64 `json:"score,omitempty"`
// Explain the score (if possible)
Explain *common.Unstructured `json:"explain,omitempty"`
} }
type FacetResult struct { 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"), 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": { "score": {
SchemaProps: spec.SchemaProps{ SchemaProps: spec.SchemaProps{
Description: "When using \"real\" search, this is the score", 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", 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"}, Required: []string{"resource", "name", "title"},
}, },

@ -223,6 +223,7 @@ func (s *SearchHandler) DoSearch(w http.ResponseWriter, r *http.Request) {
Query: queryParams.Get("query"), Query: queryParams.Get("query"),
Limit: int64(limit), Limit: int64(limit),
Offset: int64(offset), Offset: int64(offset),
Explain: queryParams.Has("explain") && queryParams.Get("explain") != "false",
Fields: []string{ Fields: []string{
"title", "title",
"folder", "folder",

@ -2,6 +2,7 @@ package service
import ( import (
"context" "context"
"encoding/binary"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
@ -10,18 +11,18 @@ import (
"time" "time"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/grafana/authlib/claims"
"github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus"
"go.opentelemetry.io/otel" "go.opentelemetry.io/otel"
"golang.org/x/exp/slices"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1" v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema" "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" "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-plugin-sdk-go/backend/gtime"
"github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/apimachinery/identity"
"github.com/grafana/grafana/pkg/apimachinery/utils" "github.com/grafana/grafana/pkg/apimachinery/utils"
"github.com/grafana/grafana/pkg/apis/dashboard/v0alpha1" "github.com/grafana/grafana/pkg/apis/dashboard/v0alpha1"
@ -46,8 +47,6 @@ import (
"github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/storage/unified/resource" "github.com/grafana/grafana/pkg/storage/unified/resource"
"github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/util"
k8sUser "k8s.io/apiserver/pkg/authentication/user"
k8sRequest "k8s.io/apiserver/pkg/endpoints/request"
) )
var ( var (
@ -1356,6 +1355,27 @@ func ParseResults(result *resource.ResourceSearchResponse, offset int64) *v0alph
return nil 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{ sr := &v0alpha1.SearchResults{
Offset: offset, Offset: offset,
TotalHits: result.TotalHits, TotalHits: result.TotalHits,
@ -1364,28 +1384,21 @@ func ParseResults(result *resource.ResourceSearchResponse, offset int64) *v0alph
Hits: make([]v0alpha1.DashboardHit, len(result.Results.Rows)), 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 { for i, row := range result.Results.Rows {
hit := &v0alpha1.DashboardHit{ hit := &v0alpha1.DashboardHit{
Resource: row.Key.Resource, // folders | dashboards Resource: row.Key.Resource, // folders | dashboards
Name: row.Key.Name, // The Grafana UID Name: row.Key.Name, // The Grafana UID
Title: string(row.Cells[titleRow]), Title: string(row.Cells[titleIDX]),
Folder: string(row.Cells[folderRow]), 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 { if scoreIDX > 0 && row.Cells[scoreIDX] != nil {
_ = json.Unmarshal(row.Cells[tagsRow], &hit.Tags) _, _ = binary.Decode(row.Cells[scoreIDX], binary.BigEndian, &hit.Score)
} }
sr.Hits[i] = *hit sr.Hits[i] = *hit

@ -343,6 +343,16 @@ func StandardSearchFields() SearchableDocumentFields {
Type: ResourceTableColumnDefinition_INT64, Type: ResourceTableColumnDefinition_INT64,
Description: "created timestamp", // date? 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 { if err != nil {
panic("failed to initialize standard search fields") panic("failed to initialize standard search fields")

@ -123,6 +123,9 @@ func NewTableBuilder(cols []*ResourceTableColumnDefinition) (*TableBuilder, erro
} }
var err error var err error
for i, v := range cols { for i, v := range cols {
if v == nil {
return nil, fmt.Errorf("invalid field definitions")
}
if table.lookup[v.Name] != nil { if table.lookup[v.Name] != nil {
table.hasDuplicateNames = true table.hasDuplicateNames = true
continue continue

@ -2,6 +2,7 @@ package search
import ( import (
"context" "context"
"encoding/json"
"fmt" "fmt"
"log/slog" "log/slog"
"os" "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 { if access != nil {
// TODO AUTHZ!!!! // TODO AUTHZ!!!!
@ -581,20 +586,28 @@ func (b *bleveIndex) hitsToTable(selectFields []string, hits search.DocumentMatc
} }
for i, f := range fields { 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) row.Cells[i] = []byte(match.ID)
continue
}
// QUICK QUICK... more options yes case resource.SEARCH_FIELD_SCORE:
v := match.Fields[f.Name] row.Cells[i], err = encoders[i](match.Score)
if v != nil {
// Encode the value to protobuf case resource.SEARCH_FIELD_EXPLAIN:
row.Cells[i], err = encoders[i](v) if match.Expl != nil {
if err != nil { row.Cells[i], err = json.Marshal(match.Expl)
return nil, fmt.Errorf("error encoding (row:%d/col:%d) %v %w", rowID, i, v, err) }
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 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