mirror of
https://github.com/grafana/grafana.git
synced 2025-08-01 12:23:10 +08:00
Search: Explain scores (#98316)
This commit is contained in:
@ -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 tagsRow != -1 && row.Cells[tagsRow] != nil {
|
if tagsIDX > 0 && row.Cells[tagsIDX] != nil {
|
||||||
_ = json.Unmarshal(row.Cells[tagsRow], &hit.Tags)
|
_ = 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
|
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,19 +586,27 @@ 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)
|
|
||||||
}
|
|
||||||
|
Reference in New Issue
Block a user