Skip to content

storage: introduce search interface with scoring and filtering#18497

Open
roidelapluie wants to merge 1 commit intoprometheus:mainfrom
roidelapluie:roidelapluie/search_api_part1
Open

storage: introduce search interface with scoring and filtering#18497
roidelapluie wants to merge 1 commit intoprometheus:mainfrom
roidelapluie:roidelapluie/search_api_part1

Conversation

@roidelapluie
Copy link
Copy Markdown
Member

@roidelapluie roidelapluie commented Apr 9, 2026

Which issue(s) does the PR fix:

Release notes for end users (ALL commits must be considered).

Reviewers should verify clarity and quality.

NONE

@roidelapluie roidelapluie force-pushed the roidelapluie/search_api_part1 branch 3 times, most recently from f3f783f to 48c2b3f Compare April 13, 2026 14:26
@roidelapluie roidelapluie requested a review from aknuds1 April 14, 2026 08:57
@roidelapluie roidelapluie force-pushed the roidelapluie/search_api_part1 branch from 48c2b3f to 1324caa Compare April 14, 2026 15:16
@roidelapluie roidelapluie marked this pull request as ready for review April 14, 2026 15:17
@roidelapluie roidelapluie force-pushed the roidelapluie/search_api_part1 branch from 1324caa to d87f4a3 Compare April 15, 2026 07:18
Copy link
Copy Markdown
Contributor

@aknuds1 aknuds1 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Directionally good, but my review uncovered some issues and refactoring opportunities.

Comment thread storage/generic.go
Comment on lines +139 to +154
switch v := gq.(type) {
case *mergeGenericQuerier:
var searchers []Searcher
for _, q := range v.queriers {
searchers = append(searchers, collectSearchers(q)...)
}
return searchers
case *genericQuerierAdapter:
if s, ok := v.q.(Searcher); ok {
return []Searcher{s}
}
default:
if s, ok := gq.(Searcher); ok {
return []Searcher{s}
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest a refactor to simplify:

Suggested change
switch v := gq.(type) {
case *mergeGenericQuerier:
var searchers []Searcher
for _, q := range v.queriers {
searchers = append(searchers, collectSearchers(q)...)
}
return searchers
case *genericQuerierAdapter:
if s, ok := v.q.(Searcher); ok {
return []Searcher{s}
}
default:
if s, ok := gq.(Searcher); ok {
return []Searcher{s}
}
}
if m, ok := gq.(*mergeGenericQuerier); ok {
var searchers []Searcher
for _, q := range m.queriers {
searchers = append(searchers, collectSearchers(q)...)
}
return searchers
}
if s, ok := searcherFromGenericQuerier(gq); ok {
return []Searcher{s}
}

Comment thread storage/generic.go
// ApplySearchHints filters, sorts, and limits a slice of string values according to hints,
// returning scored SearchResult entries. The caller must pass a non-nil hints value.
func ApplySearchHints(values []string, hints *SearchHints) []SearchResult {
var results []SearchResult
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest preallocating as the upper bound is known:

Suggested change
var results []SearchResult
results := make([]SearchResult, 0, len(values))

Comment thread storage/generic.go
Comment thread storage/generic.go
}

// mergeSearchSets merges results from multiple Searcher calls, deduplicating by value
// and taking the maximum score for duplicates. Returns a SearchResultSet.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// and taking the maximum score for duplicates. Returns a SearchResultSet.
// and taking the maximum score for duplicates. When hints.CompareFunc is nil, results
// are sorted alphabetically by value. Returns a SearchResultSet.

Comment thread storage/generic.go
warns.Merge(rs.Warnings())
if err := rs.Err(); err != nil {
_ = rs.Close()
return ErrSearchResultSet(err)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Retain warnings:

Suggested change
return ErrSearchResultSet(err)
return errSearchResultSetWithWarnings(err, warns)

Comment thread tsdb/index/postings.go
Comment on lines +176 to -183
// p.lvs[name] is a shared, append-only slice; clone before returning so
// callers cannot observe future appends or corrupt the internal state.
if hints != nil && hints.Limit > 0 && len(values) > hints.Limit {
values = values[:hints.Limit]
return slices.Clone(values[:hints.Limit])
}

// The slice from p.lvs[name] is shared between all readers, and it is append-only.
// Since it's shared, we need to make a copy of it before returning it to make
// sure that no caller modifies the original one by sorting it or filtering it.
// Since it's append-only, we can do this while not holding the mutex anymore.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please revert these changes, they are purely cosmetical and I don't see the revision making things better?

Comment thread tsdb/querier.go
labelHints.Limit = hints.Limit
}

values, err := q.index.SortedLabelValues(ctx, name, labelHints, matchers...)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's optimize:

Suggested change
values, err := q.index.SortedLabelValues(ctx, name, labelHints, matchers...)
var values []string
var err error
if hints.CompareFunc != nil {
// Custom ordering will re-sort, so skip the index-level sort.
values, err = q.index.LabelValues(ctx, name, labelHints, matchers...)
} else {
values, err = q.index.SortedLabelValues(ctx, name, labelHints, matchers...)
}

Comment thread tsdb/querier_test.go
require.NoError(t, rs.Err())
require.NoError(t, rs.Close())
return got
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test blockBaseQuerier.LabelNames with limit:

Suggested change
}
func TestBlockBaseQuerierLabelNamesLimit(t *testing.T) {
ctx := t.Context()
q := newBlockBaseQuerierForSearch(t,
labels.FromStrings("env", "prod", "job", "api"),
labels.FromStrings("region", "eu"),
)
t.Run("no limit returns all names", func(t *testing.T) {
names, _, err := q.LabelNames(ctx, nil)
require.NoError(t, err)
require.Len(t, names, 3)
})
t.Run("limit truncates results", func(t *testing.T) {
names, _, err := q.LabelNames(ctx, &storage.LabelHints{Limit: 1})
require.NoError(t, err)
require.Len(t, names, 1)
})
}

Comment thread tsdb/querier_test.go
}

func TestBlockBaseQuerierSearchLabelNames(t *testing.T) {
ctx := context.Background()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Nit]

Suggested change
ctx := context.Background()
ctx := t.Context()

Comment thread tsdb/querier_test.go
got := collectSearchResultSet(t, rs)
require.Empty(t, got)
})
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
}
t.Run("CompareFunc sorts via unsorted LabelValues path", func(t *testing.T) {
// When CompareFunc is set, SearchLabelValues uses the unsorted
// LabelValues index method. Verify results are sorted by CompareFunc.
rs := q.SearchLabelValues(ctx, "env", &storage.SearchHints{
CompareFunc: reverseAlphaComparator{},
})
got := collectSearchResultSet(t, rs)
require.Len(t, got, 3)
gotValues := make([]string, len(got))
for i, r := range got {
gotValues[i] = r.Value
}
require.Equal(t, []string{"staging", "prod", "dev"}, gotValues)
})
}
// reverseAlphaComparator sorts SearchResults in reverse alphabetical order by value.
type reverseAlphaComparator struct{}
func (reverseAlphaComparator) Compare(a, b storage.SearchResult) int {
return cmp.Compare(b.Value, a.Value)
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants