Search: Fix title filter overmatching (#102547)

* fix issues with over matching
* search wildcard for backward compatibility
This commit is contained in:
Scott Lepper
2025-03-21 09:57:20 -04:00
committed by GitHub
parent 8a8b1a0743
commit 2aae523c3f
6 changed files with 403 additions and 62 deletions
+88 -14
View File
@@ -159,7 +159,7 @@ func (b *bleveBackend) BuildIndex(ctx context.Context,
var index bleve.Index
build := true
mapper, err := getBleveMappings(fields)
mapper, err := GetBleveMappings(fields)
if err != nil {
return nil, err
}
@@ -660,8 +660,14 @@ func (b *bleveIndex) toBleveSearchRequest(ctx context.Context, req *resource.Res
}
}
// Add a text query
if req.Query != "" && req.Query != "*" {
if len(req.Query) > 1 && strings.Contains(req.Query, "*") {
// wildcard query is expensive - should be used with caution
wildcard := bleve.NewWildcardQuery(req.Query)
queries = append(queries, wildcard)
}
if req.Query != "" && !strings.Contains(req.Query, "*") {
// Add a text query
searchrequest.Fields = append(searchrequest.Fields, resource.SEARCH_FIELD_SCORE)
// There are multiple ways to match the query string to documents. The following queries are ordered by priority:
@@ -789,6 +795,11 @@ var textSortFields = map[string]string{
const lowerCase = "phrase"
// termField fields to use termQuery for filtering
var termFields = []string{
resource.SEARCH_FIELD_TITLE,
}
// Convert a "requirement" into a bleve query
func requirementQuery(req *resource.Requirement, prefix string) (query.Query, *resource.ErrorResult) {
switch selection.Operator(req.Operator) {
@@ -797,16 +808,14 @@ func requirementQuery(req *resource.Requirement, prefix string) (query.Query, *r
return query.NewMatchAllQuery(), nil
}
if len(req.Values[0]) == 1 {
q := query.NewMatchQuery(filterValue(req.Key, req.Values[0]))
q.FieldVal = prefix + req.Key
return q, nil
if len(req.Values) == 1 {
filter := filterValue(req.Key, req.Values[0])
return newQuery(req.Key, filter, prefix), nil
}
conjuncts := []query.Query{}
for _, v := range req.Values {
q := query.NewMatchQuery(filterValue(req.Key, v))
q.FieldVal = prefix + req.Key
q := newQuery(req.Key, filterValue(req.Key, v), prefix)
conjuncts = append(conjuncts, q)
}
@@ -822,15 +831,13 @@ func requirementQuery(req *resource.Requirement, prefix string) (query.Query, *r
return query.NewMatchAllQuery(), nil
}
if len(req.Values) == 1 {
q := query.NewMatchQuery(filterValue(req.Key, req.Values[0]))
q.FieldVal = prefix + req.Key
q := newQuery(req.Key, filterValue(req.Key, req.Values[0]), prefix)
return q, nil
}
disjuncts := []query.Query{}
for _, v := range req.Values {
q := query.NewMatchQuery(filterValue(req.Key, v))
q.FieldVal = prefix + req.Key
q := newQuery(req.Key, filterValue(req.Key, v), prefix)
disjuncts = append(disjuncts, q)
}
@@ -841,7 +848,8 @@ func requirementQuery(req *resource.Requirement, prefix string) (query.Query, *r
var mustNotQueries []query.Query
for _, value := range req.Values {
mustNotQueries = append(mustNotQueries, bleve.NewMatchQuery(filterValue(req.Key, value)))
q := newQuery(req.Key, filterValue(req.Key, value), prefix)
mustNotQueries = append(mustNotQueries, q)
}
boolQuery.AddMustNot(mustNotQueries...)
@@ -856,6 +864,55 @@ func requirementQuery(req *resource.Requirement, prefix string) (query.Query, *r
)
}
// newQuery will create a query that will match the value or the tokens of the value
func newQuery(key string, value string, prefix string) query.Query {
if value == "*" {
return bleve.NewMatchAllQuery()
}
if strings.Contains(value, "*") {
// wildcard query is expensive - should be used with caution
return bleve.NewWildcardQuery(value)
}
delimiter, ok := hasTerms(value)
if slices.Contains(termFields, key) && ok {
return newTermsQuery(key, value, delimiter, prefix)
}
q := bleve.NewMatchQuery(value)
q.SetField(prefix + key)
return q
}
// newTermsQuery will create a query that will match on term or tokens
func newTermsQuery(key string, value string, delimiter string, prefix string) query.Query {
tokens := strings.Split(value, delimiter)
// won't match with ending space
value = strings.TrimSuffix(value, " ")
q := bleve.NewTermQuery(value)
q.SetField(prefix + key)
cq := newMatchAllTokensQuery(tokens, key, prefix)
return bleve.NewDisjunctionQuery(q, cq)
}
// newMatchAllTokensQuery will create a query that will match on all tokens
func newMatchAllTokensQuery(tokens []string, key string, prefix string) query.Query {
cq := bleve.NewConjunctionQuery()
for _, token := range tokens {
_, ok := hasTerms(token)
if ok {
tq := bleve.NewTermQuery(token)
tq.SetField(prefix + key)
cq.AddQuery(tq)
continue
}
mq := bleve.NewMatchQuery(token)
mq.SetField(prefix + key)
cq.AddQuery(mq)
}
return cq
}
// filterValue will convert the value to lower case if the field is a phrase field
func filterValue(field string, v string) string {
if strings.HasSuffix(field, lowerCase) {
@@ -1068,3 +1125,20 @@ func (q *permissionScopedQuery) Searcher(ctx context.Context, i index.IndexReade
return filteringSearcher, nil
}
// hasTerms - any value that will be split into multiple tokens
var hasTerms = func(v string) (string, bool) {
for _, c := range TermCharacters {
if strings.Contains(v, c) {
return c, true
}
}
return "", false
}
// TermCharacters characters that will be used to determine if a value is split into tokens
var TermCharacters = []string{
" ", "-", "_", ".", ",", ":", ";", "?", "!", "@", "#", "$", "%", "^", "&", "*", "(", ")", "+",
"=", "{", "}", "[", "]", "|", "\\", "/", "<", ">", "~", "`",
"'", "\"",
}
+8 -8
View File
@@ -9,7 +9,7 @@ import (
"github.com/grafana/grafana/pkg/storage/unified/resource"
)
func getBleveMappings(fields resource.SearchableDocumentFields) (mapping.IndexMapping, error) {
func GetBleveMappings(fields resource.SearchableDocumentFields) (mapping.IndexMapping, error) {
mapper := bleve.NewIndexMapping()
err := RegisterCustomAnalyzers(mapper)
@@ -31,22 +31,22 @@ func getBleveDocMappings(_ resource.SearchableDocumentFields) *mapping.DocumentM
}
mapper.AddFieldMappingsAt(resource.SEARCH_FIELD_NAME, nameMapping)
// for sorting by title full phrase
titlePhraseMapping := bleve.NewKeywordFieldMapping()
titlePhraseMapping.Store = false // already stored in title
mapper.AddFieldMappingsAt(resource.SEARCH_FIELD_TITLE_PHRASE, titlePhraseMapping)
// for searching by title - uses an edge ngram token filter
titleSearchMapping := bleve.NewTextFieldMapping()
titleSearchMapping.Analyzer = TITLE_ANALYZER
titleSearchMapping.Store = false // already stored in title
mapper.AddFieldMappingsAt(resource.SEARCH_FIELD_TITLE_NGRAM, titleSearchMapping)
// mapping for title to search on words/tokens larger than the ngram size
titleWordMapping := bleve.NewTextFieldMapping()
titleWordMapping.Analyzer = standard.Name
titleWordMapping.Store = true
mapper.AddFieldMappingsAt(resource.SEARCH_FIELD_TITLE, titleWordMapping)
// for filtering/sorting by title full phrase
titlePhraseMapping := bleve.NewKeywordFieldMapping()
titleSearchMapping.Store = false // already stored in title
mapper.AddFieldMappingsAt(resource.SEARCH_FIELD_TITLE_PHRASE, titlePhraseMapping)
// NOTE: this causes 3 title fields in the response
mapper.AddFieldMappingsAt(resource.SEARCH_FIELD_TITLE, titleWordMapping, titleSearchMapping, titlePhraseMapping)
descriptionMapping := &mapping.FieldMapping{
Name: resource.SEARCH_FIELD_DESCRIPTION,
@@ -1,4 +1,4 @@
package search
package search_test
import (
"fmt"
@@ -9,10 +9,11 @@ import (
"github.com/grafana/grafana/pkg/apimachinery/utils"
"github.com/grafana/grafana/pkg/storage/unified/resource"
"github.com/grafana/grafana/pkg/storage/unified/search"
)
func TestDocumentMapping(t *testing.T) {
mappings, err := getBleveMappings(nil)
mappings, err := search.GetBleveMappings(nil)
require.NoError(t, err)
data := resource.IndexableDocument{
Title: "title",
@@ -48,5 +49,5 @@ func TestDocumentMapping(t *testing.T) {
fmt.Printf("DOC: fields %d\n", len(doc.Fields))
fmt.Printf("DOC: size %d\n", doc.Size())
require.Equal(t, 16, len(doc.Fields))
require.Equal(t, 17, len(doc.Fields))
}
@@ -0,0 +1,131 @@
package search_test
import (
"context"
"fmt"
"os"
"runtime"
"testing"
"time"
"github.com/grafana/grafana/pkg/storage/unified/resource"
"github.com/stretchr/testify/require"
)
func setupIndex() (resource.ResourceIndex, string) {
// size := 1000000 // TODO: 200k documents standard size?
size := 200000
// batchSize := 1000 slower 8s (for 200k documents) - 34s (for 1M documents)
// batchSize := 10000 // faster 5s (for 200k documents) - 27s (for 1M documents)
batchSize := 100000 // fasterer 3.5s (for 200k documents) - 27s (for 1M documents)
writer := newTestWriter(size, batchSize)
return newTestDashboardsIndex(nil, 1, int64(size), int64(batchSize), writer)
}
const maxAllowedTime = 20 * time.Millisecond // Reasonable (can vary per env) performance threshold per query (e.g., 20ms)
const maxAllowedAllocMB = 1 // 1MB memory per operation
const maxAllowedAlloc = maxAllowedAllocMB * 1024 * 1024
const verbose = false
// BenchmarkBleveQuery measures the time, mem, cpu to execute a search query
// changes the the indexer settings can cause unforeseen performance issues ( for example: using wildcard queries )
// this will fail if the stats exceed the "normal" thresholds
func BenchmarkBleveQuery(b *testing.B) {
var memStatsStart runtime.MemStats
var memStatsAfterIndex runtime.MemStats
runtime.ReadMemStats(&memStatsStart)
testIndex, testIndexDir := setupIndex()
defer func() {
err := os.RemoveAll(testIndexDir)
if err != nil {
fmt.Printf("Error removing index directory: %v\n", err)
}
}()
runtime.ReadMemStats(&memStatsAfterIndex)
allocDiff := memStatsAfterIndex.Alloc - memStatsStart.Alloc
logVerbose(fmt.Sprintf("Memory allocated for index: %d bytes", allocDiff))
searchRequest := newQueryByTitle("name99999")
b.ResetTimer() // Reset timer before benchmarking
b.ReportAllocs() // Track memory allocations
for i := 0; i < b.N; i++ {
start := time.Now() // Start timer
var memStatsBefore, memStatsAfter runtime.MemStats
runtime.ReadMemStats(&memStatsBefore)
_, err := testIndex.Search(context.Background(), nil, searchRequest, nil)
elapsed := time.Since(start) // Calculate elapsed time
runtime.ReadMemStats(&memStatsAfter)
allocDiff := (memStatsAfter.Alloc - memStatsBefore.Alloc)
if memStatsAfter.Alloc < memStatsBefore.Alloc {
// This can happen due to memory being freed after the search operation
allocDiff = 0 // don't care if it goes down
}
logVerbose(fmt.Sprintf("Memory allocated for query: %d bytes", allocDiff))
require.NoError(b, err)
// Fail if query takes longer than maxAllowedTime
if elapsed > maxAllowedTime {
b.Fatalf("Query too slow: %v (limit: %v)", elapsed, maxAllowedTime)
}
// Check memory allocation limit
if allocDiff > maxAllowedAlloc {
b.Fatalf("Excessive memory usage: %d mb (limit: %d mb)", allocDiff, maxAllowedAllocMB)
}
}
}
func newTestWriter(size int, batchSize int) IndexWriter {
key := &resource.ResourceKey{
Namespace: "default",
Group: "dashboard.grafana.app",
Resource: "dashboards",
}
return func(index resource.ResourceIndex) (int64, error) {
total := time.Now()
start := time.Now()
for i := range size {
name := fmt.Sprintf("name%d", i)
err := index.Write(&resource.IndexableDocument{
RV: int64(i),
Name: name,
Key: &resource.ResourceKey{
Name: name,
Namespace: key.Namespace,
Group: key.Group,
Resource: key.Resource,
},
Title: name + "-title",
})
if err != nil {
return 0, err
}
// show progress for every batch
if i%batchSize == 0 && verbose {
fmt.Printf("Indexed %d documents\n", i)
end := time.Now()
fmt.Printf("Time taken for indexing batch: %s\n", end.Sub(start))
start = time.Now()
}
}
end := time.Now()
logVerbose(fmt.Sprintf("Indexed %d documents in %s", size, end.Sub(total)))
return 0, nil
}
}
func logVerbose(msg string) {
if verbose {
fmt.Println(msg)
}
}
+166 -32
View File
@@ -1,4 +1,4 @@
package search
package search_test
import (
"context"
@@ -16,8 +16,11 @@ import (
"github.com/grafana/grafana/pkg/services/store/kind/dashboard"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/storage/unified/resource"
"github.com/grafana/grafana/pkg/storage/unified/search"
)
const threshold = 9999
func TestCanSearchByTitle(t *testing.T) {
key := &resource.ResourceKey{
Namespace: "default",
@@ -26,7 +29,7 @@ func TestCanSearchByTitle(t *testing.T) {
}
t.Run("when query is empty, sort documents by title instead of search score", func(t *testing.T) {
index := newTestDashboardsIndex(t)
index, _ := newTestDashboardsIndex(t, threshold, 2, 2, noop)
err := index.Write(&resource.IndexableDocument{
RV: 1,
Name: "name1",
@@ -53,7 +56,7 @@ func TestCanSearchByTitle(t *testing.T) {
require.NoError(t, err)
// search for phrase
query := newQuery("")
query := newTestQuery("")
res, err := index.Search(context.Background(), nil, query, nil)
require.NoError(t, err)
require.Equal(t, int64(2), res.TotalHits)
@@ -61,7 +64,7 @@ func TestCanSearchByTitle(t *testing.T) {
})
t.Run("will boost phrase match query over match query results", func(t *testing.T) {
index := newTestDashboardsIndex(t)
index, _ := newTestDashboardsIndex(t, threshold, 2, 2, noop)
err := index.Write(&resource.IndexableDocument{
RV: 1,
Name: "name1",
@@ -88,7 +91,7 @@ func TestCanSearchByTitle(t *testing.T) {
require.NoError(t, err)
// search for phrase
query := newQuery("want hello")
query := newTestQuery("want hello")
res, err := index.Search(context.Background(), nil, query, nil)
require.NoError(t, err)
require.Equal(t, int64(2), res.TotalHits)
@@ -96,7 +99,7 @@ func TestCanSearchByTitle(t *testing.T) {
})
t.Run("will prioritize matches", func(t *testing.T) {
index := newTestDashboardsIndex(t)
index, _ := newTestDashboardsIndex(t, threshold, 2, 2, noop)
err := index.Write(&resource.IndexableDocument{
RV: 1,
Name: "name1",
@@ -122,7 +125,7 @@ func TestCanSearchByTitle(t *testing.T) {
})
require.NoError(t, err)
query := newQuery("New dash")
query := newTestQuery("New dash")
res, err := index.Search(context.Background(), nil, query, nil)
require.NoError(t, err)
require.Equal(t, int64(2), res.TotalHits)
@@ -130,7 +133,7 @@ func TestCanSearchByTitle(t *testing.T) {
})
t.Run("will boost exact match query over match phrase query results", func(t *testing.T) {
index := newTestDashboardsIndex(t)
index, _ := newTestDashboardsIndex(t, threshold, 2, 2, noop)
err := index.Write(&resource.IndexableDocument{
RV: 1,
Name: "name1",
@@ -157,7 +160,7 @@ func TestCanSearchByTitle(t *testing.T) {
require.NoError(t, err)
// search for exact match
query := newQuery("we want hello")
query := newTestQuery("we want hello")
res, err := index.Search(context.Background(), nil, query, nil)
require.NoError(t, err)
require.Equal(t, int64(2), res.TotalHits)
@@ -165,7 +168,7 @@ func TestCanSearchByTitle(t *testing.T) {
})
t.Run("title with numbers will match document", func(t *testing.T) {
index := newTestDashboardsIndex(t)
index, _ := newTestDashboardsIndex(t, threshold, 2, 2, noop)
err := index.Write(&resource.IndexableDocument{
RV: 1,
Name: "name1",
@@ -180,20 +183,65 @@ func TestCanSearchByTitle(t *testing.T) {
require.NoError(t, err)
// search for prefix of title with mix of chars and numbers
query := newQuery("A12")
query := newQueryByTitle("A12")
res, err := index.Search(context.Background(), nil, query, nil)
require.NoError(t, err)
require.Equal(t, int64(1), res.TotalHits)
// search for whole title
query = newQuery("A123456")
query = newQueryByTitle("A123456")
res, err = index.Search(context.Background(), nil, query, nil)
require.NoError(t, err)
require.Equal(t, int64(1), res.TotalHits)
// case insensive search for partial title
query = newQueryByTitle("a1234")
res, err = index.Search(context.Background(), nil, query, nil)
require.NoError(t, err)
require.Equal(t, int64(1), res.TotalHits)
})
t.Run("title will match escaped characters", func(t *testing.T) {
index, _ := newTestDashboardsIndex(t, threshold, 2, 2, noop)
err := index.Write(&resource.IndexableDocument{
RV: 1,
Name: "name1",
Key: &resource.ResourceKey{
Name: "aaa",
Namespace: key.Namespace,
Group: key.Group,
Resource: key.Resource,
},
Title: "what\"s up",
})
require.NoError(t, err)
err = index.Write(&resource.IndexableDocument{
RV: 2,
Name: "name2",
Key: &resource.ResourceKey{
Name: "name2",
Namespace: key.Namespace,
Group: key.Group,
Resource: key.Resource,
},
Title: "what\"s that",
})
require.NoError(t, err)
query := newQueryByTitle("what\"s up")
res, err := index.Search(context.Background(), nil, query, nil)
require.NoError(t, err)
require.Equal(t, int64(1), res.TotalHits)
query = newQueryByTitle("what\"s")
res, err = index.Search(context.Background(), nil, query, nil)
require.NoError(t, err)
require.Equal(t, int64(2), res.TotalHits)
})
t.Run("title search will match document", func(t *testing.T) {
index := newTestDashboardsIndex(t)
index, _ := newTestDashboardsIndex(t, threshold, 2, 2, noop)
err := index.Write(&resource.IndexableDocument{
RV: 1,
Name: "name1",
@@ -208,50 +256,50 @@ func TestCanSearchByTitle(t *testing.T) {
require.NoError(t, err)
// search by entire phrase
query := newQuery("I want to say a wonderfully Hello to the WORLD! Hello-world")
query := newTestQuery("I want to say a wonderfully Hello to the WORLD! Hello-world")
res, err := index.Search(context.Background(), nil, query, nil)
require.NoError(t, err)
require.Equal(t, int64(1), res.TotalHits)
// search for word at start
query = newQuery("hello")
query = newTestQuery("hello")
res, err = index.Search(context.Background(), nil, query, nil)
require.NoError(t, err)
require.Equal(t, int64(1), res.TotalHits)
// search for word larger than ngram max size
query = newQuery("wonderfully")
query = newQueryByTitle("wonderfully")
res, err = index.Search(context.Background(), nil, query, nil)
require.NoError(t, err)
require.Equal(t, int64(1), res.TotalHits)
// search for word at end
query = newQuery("world")
query = newQueryByTitle("world")
res, err = index.Search(context.Background(), nil, query, nil)
require.NoError(t, err)
require.Equal(t, int64(1), res.TotalHits)
// can search for word substring anchored at start of word (edge ngram)
query = newQuery("worl")
query = newQueryByTitle("worl")
res, err = index.Search(context.Background(), nil, query, nil)
require.NoError(t, err)
require.Equal(t, int64(1), res.TotalHits)
// can search for multiple, non-consecutive words in title
query = newQuery("hello world")
query = newQueryByTitle("hello world")
res, err = index.Search(context.Background(), nil, query, nil)
require.NoError(t, err)
require.Equal(t, int64(1), res.TotalHits)
// can search for a term with a hyphen
query = newQuery("hello-world")
query = newQueryByTitle("hello-world")
res, err = index.Search(context.Background(), nil, query, nil)
require.NoError(t, err)
require.Equal(t, int64(1), res.TotalHits)
})
t.Run("title search will NOT match documents", func(t *testing.T) {
index := newTestDashboardsIndex(t)
index, _ := newTestDashboardsIndex(t, threshold, 2, 2, noop)
err := index.Write(&resource.IndexableDocument{
RV: 1,
Name: "name1",
@@ -290,26 +338,79 @@ func TestCanSearchByTitle(t *testing.T) {
require.NoError(t, err)
// word that doesn't exist
query := newQuery("cats")
query := newQueryByTitle("cats")
res, err := index.Search(context.Background(), nil, query, nil)
require.NoError(t, err)
require.Equal(t, int64(0), res.TotalHits)
// string shorter than 3 chars (ngam min)
query = newQuery("ma")
query = newQueryByTitle("ma")
res, err = index.Search(context.Background(), nil, query, nil)
require.NoError(t, err)
require.Equal(t, int64(0), res.TotalHits)
// substring that doesn't exist
query = newQuery("A01")
query = newQueryByTitle("A01")
res, err = index.Search(context.Background(), nil, query, nil)
require.NoError(t, err)
require.Equal(t, int64(0), res.TotalHits)
})
t.Run("title search with character will match one document", func(t *testing.T) {
index, _ := newTestDashboardsIndex(t, threshold, 2, 2, noop)
err := index.Write(&resource.IndexableDocument{
RV: 1,
Name: "name1",
Key: &resource.ResourceKey{
Name: "aaa",
Namespace: key.Namespace,
Group: key.Group,
Resource: key.Resource,
},
Title: "foo",
})
require.NoError(t, err)
for i, v := range search.TermCharacters {
err = index.Write(&resource.IndexableDocument{
RV: int64(i),
Name: fmt.Sprintf("name%d", i),
Key: &resource.ResourceKey{
Name: fmt.Sprintf("name%d", i),
Namespace: key.Namespace,
Group: key.Group,
Resource: key.Resource,
},
Title: fmt.Sprintf(`test foo%d%sbar`, i, v),
})
require.NoError(t, err)
}
for i, v := range search.TermCharacters {
title := fmt.Sprintf(`test foo%d%sbar`, i, v)
query := newQueryByTitle(title)
res, err := index.Search(context.Background(), nil, query, nil)
require.NoError(t, err)
if res.TotalHits != 1 {
fmt.Printf("i: %d, v: %s, title: %s", i, v, title)
}
require.Equal(t, int64(1), res.TotalHits)
// can search for a title with a term character suffix
title = fmt.Sprintf(`foo%d%s`, i, v)
query = newQueryByTitle(title)
res, err = index.Search(context.Background(), nil, query, nil)
require.NoError(t, err)
if res.TotalHits != 1 {
fmt.Printf("i: %d, v: %s, title: %s", i, v, title)
}
require.Equal(t, int64(1), res.TotalHits)
}
})
}
func newQuery(query string) *resource.ResourceSearchRequest {
func newTestQuery(query string) *resource.ResourceSearchRequest {
return &resource.ResourceSearchRequest{
Options: &resource.ListOptions{
Key: &resource.ResourceKey{
@@ -323,7 +424,21 @@ func newQuery(query string) *resource.ResourceSearchRequest {
}
}
func newTestDashboardsIndex(t *testing.T) resource.ResourceIndex {
func newQueryByTitle(query string) *resource.ResourceSearchRequest {
return &resource.ResourceSearchRequest{
Options: &resource.ListOptions{
Key: &resource.ResourceKey{
Namespace: "default",
Group: "dashboard.grafana.app",
Resource: "dashboards",
},
Fields: []*resource.Requirement{{Key: "title", Operator: "=", Values: []string{query}}},
},
Limit: 100000,
}
}
func newTestDashboardsIndex(t TB, threshold int64, size int64, batchSize int64, writer IndexWriter) (resource.ResourceIndex, string) {
key := &resource.ResourceKey{
Namespace: "default",
Group: "dashboard.grafana.app",
@@ -332,17 +447,18 @@ func newTestDashboardsIndex(t *testing.T) resource.ResourceIndex {
tmpdir, err := os.MkdirTemp("", "grafana-bleve-test")
require.NoError(t, err)
backend, err := NewBleveBackend(BleveOptions{
backend, err := search.NewBleveBackend(search.BleveOptions{
Root: tmpdir,
FileThreshold: 9999, // use in-memory for tests
FileThreshold: threshold, // use in-memory for tests
BatchSize: int(batchSize),
}, tracing.NewNoopTracerService(), featuremgmt.WithFeatures(featuremgmt.FlagUnifiedStorageSearchPermissionFiltering), nil)
require.NoError(t, err)
rv := int64(10)
ctx := identity.WithRequester(context.Background(), &user.SignedInUser{Namespace: "ns"})
info, err := DashboardBuilder(func(ctx context.Context, namespace string, blob resource.BlobSupport) (resource.DocumentBuilder, error) {
return &DashboardDocumentBuilder{
info, err := search.DashboardBuilder(func(ctx context.Context, namespace string, blob resource.BlobSupport) (resource.DocumentBuilder, error) {
return &search.DashboardDocumentBuilder{
Namespace: namespace,
Blob: blob,
Stats: make(map[string]map[string]int64), // empty stats
@@ -355,10 +471,16 @@ func newTestDashboardsIndex(t *testing.T) resource.ResourceIndex {
Namespace: key.Namespace,
Group: key.Group,
Resource: key.Resource,
}, 2, rv, info.Fields, func(index resource.ResourceIndex) (int64, error) { return 0, nil })
}, size, rv, info.Fields, writer)
require.NoError(t, err)
return index
return index, tmpdir
}
type IndexWriter func(index resource.ResourceIndex) (int64, error)
var noop IndexWriter = func(index resource.ResourceIndex) (int64, error) {
return 0, nil
}
// helper to check which tokens are generated by an analyzer
@@ -399,3 +521,15 @@ func debugIndexedTerms(index bleve.Index, field string) {
}
}
}
// TB is an interface that works for both *testing.T and *testing.B
type TB interface {
Log(args ...interface{})
Logf(format string, args ...interface{})
Error(args ...interface{})
Errorf(format string, args ...interface{})
Fatal(args ...interface{})
Fatalf(format string, args ...interface{})
Helper()
FailNow()
}
+6 -5
View File
@@ -1,4 +1,4 @@
package search
package search_test
import (
"context"
@@ -13,6 +13,7 @@ import (
"github.com/grafana/grafana/pkg/services/store/kind/dashboard"
"github.com/grafana/grafana/pkg/storage/unified/resource"
"github.com/grafana/grafana/pkg/storage/unified/search"
)
func doSnapshotTests(t *testing.T, builder resource.DocumentBuilder, kind string, key *resource.ResourceKey, names []string) {
@@ -52,14 +53,14 @@ func TestDashboardDocumentBuilder(t *testing.T) {
Resource: "dashboards",
}
info, err := DashboardBuilder(func(ctx context.Context, namespace string, blob resource.BlobSupport) (resource.DocumentBuilder, error) {
return &DashboardDocumentBuilder{
info, err := search.DashboardBuilder(func(ctx context.Context, namespace string, blob resource.BlobSupport) (resource.DocumentBuilder, error) {
return &search.DashboardDocumentBuilder{
Namespace: namespace,
Blob: blob,
Stats: map[string]map[string]int64{
"aaa": {
DASHBOARD_ERRORS_LAST_1_DAYS: 1,
DASHBOARD_ERRORS_LAST_7_DAYS: 1,
search.DASHBOARD_ERRORS_LAST_1_DAYS: 1,
search.DASHBOARD_ERRORS_LAST_7_DAYS: 1,
},
},
DatasourceLookup: dashboard.CreateDatasourceLookup([]*dashboard.DatasourceQueryResult{{