Streaming labels values PR 1 - #15233
Conversation
Wire-decoupled input shape for BuildFilter. Field names mirror the HTTP URL-param contract from Prometheus PR #18573 (case_sensitive polarity, fuzz_threshold int 0-100, default fuzz_alg subsequence) so the eventual HTTP layer is a verbatim translation.
Score semantics match Prometheus PR #18573: prefix=1.0, substring=0.9. Case-sensitive option matches the HTTP URL-param polarity.
Wraps Prometheus strutil.JaroWinklerMatcher. Same semantics as Prometheus PR #18573's FuzzyFilter, minus the RWMutex (Mimir's per-goroutine construction avoids per-Accept lock acquisitions).
Score semantics match Prometheus PR #18573: prefix override to 1.0, score 0 always rejected. Wraps Prometheus strutil.SubsequenceMatcher and stores the pattern locally because the matcher does not expose a Pattern accessor.
Equivalent of Prometheus PR #18573's orSearchesFilter. Combines per-term filters with OR semantics, returning the max score across accepting children. Short-circuits at score 1.0.
Translates Params into a storage.Filter that mirrors Prometheus PR #18573's composition: per-term substring-then-fuzzy fallback, multi-term OR-max. FuzzThreshold (int 0-100) divided by 100 to match the matcher's float [0,1] unit. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Mimir convention: every Go file in pkg/ carries the AGPL-3.0-only SPDX header on line 1. Restore it to the four files added in the preceding commits.
Wraps a single child SearchResultSet with a producer goroutine that pre-fetches into a bounded buffered channel. Used by the store-gateway to parallelise per-block scans before Prometheus's pairwise merge. Value strings are strings.Cloned at the channel boundary.
Field shape mirrors Prometheus PR #18573's HTTP URL-param contract (case_sensitive polarity, int32 fuzz_threshold 0-100, default fuzz_alg SUBSEQUENCE) so the eventual HTTP handler is a verbatim translation. RPCs are reachable but unused; querier-side caller and HTTP route arrive in later PRs. Adds stub implementations on *Ingester, *ProfilingWrapper, and *ActivityTrackerWrapper to satisfy the API interface, and updates the client-package test mock. Full logic arrives in Tasks 9/10.
Replaces the placeholder stubs with real server implementations.
Builds a storage.Filter from the wire SearchFilter (matching the
Prometheus PR #18573 contract), opens the per-tenant TSDB querier,
type-asserts to storage.Searcher, and streams batches of {Value,
Score} via the new gRPC RPCs. Shared helpers buildSearchHints /
protoToParams / protoToOrdering / streamSearchResults factored out.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Mirrors the ingester proto shape (SearchFilter with case_sensitive, int32 fuzz_threshold 0-100, default fuzz_alg SUBSEQUENCE; streaming SearchResultBatch). Field shape mirrors Prometheus PR #18573. Adds Unimplemented stubs on BucketStore (and any wrappers) to keep the build green; full server logic arrives in the next task.
Replaces the placeholder stubs with real per-block fan-out implementations. Mirrors the existing BucketStore.LabelNames / LabelValues structure: errgroup over blocks, util.MergeSlices to dedup+sort across blocks, then storage.ApplySearchHints applies the filter, ordering, and limit before streaming batches. Deviation from the original plan: per-block concurrentSearchResultSet + pairwiseMergeSearchSets is replaced with the slice-based merge because Prometheus's pairwise merge primitives are package-private.
Three behavioural updates to track upstream's revised search filter
contract:
- FilterContains: non-prefix substring scoring changes from flat 0.9
to position-decay (score = 1 - 0.9*idx/maxIdx, range [0.1, 1.0))
so earlier match positions score higher.
- BuildFilter per-term composition mirrors Prometheus buildSearchFilter:
* FuzzAlgSubsequence yields a single FilterSubsequence per term
(no substring fallback; the filter's prefix override still
scores prefix matches at 1.0).
* FuzzAlgJaroWinkler with threshold 0 yields a single FilterContains
(substring only — no fuzzy when threshold is unset).
* FuzzAlgJaroWinkler with threshold > 0 yields the existing
substring-then-fuzzy fallback.
- The universal substring-then-fuzzy chain that wrapped every term is
removed; substring-only is reachable explicitly via JaroWinkler
threshold 0.
Tests updated to assert the new position-decay scoring (with explicit
edge cases at the latest possible position scoring 0.1) and the new
per-alg composition.
Not adopted: Prometheus's caseFoldingFilter and memoizingFilter
wrappers — their use cases are covered today by per-filter case
handling and pre-merge dedup at the store-gateway.
gogoproto's default for repeated nested messages emits []*Result. For this hot-path streaming RPC that means one heap allocation per emitted result — at searchBatchSize=256 and N batches per request, hundreds of small allocations per request go straight to the GC. Adding [(gogoproto.nullable) = false] on the repeated Result field in both protos changes the generated slice to []SearchResultBatch_Result (value semantics, no per-element heap allocation, better cache locality). Mirrors the existing convention for hot-path repeated messages in this codebase (e.g. LabelMatcher in storepb).
…m/grafana/mimir into streaming-labels-values-utilities
streamSearchResults previously discarded the SearchResultSet's Warnings(). Per spec invariant 5 (annotations propagate at gRPC boundaries), the ingester must forward annotations produced by the in-process Searcher to the caller. Wire change: add 'repeated string warnings = 2' to SearchResultBatch in both ingester and storegateway protos. The field is omitempty so the wire footprint is unchanged in the no-warnings case. Server change: streamSearchResults converts annotations.Annotations to []string via warningsToStrings and attaches them to the final batch. If iteration produced no results but did produce warnings, a single empty-results batch is sent so the warnings reach the caller. Warnings are suppressed when rs.Err is non-nil — the error takes precedence on the gRPC return. Storegateway side: only the wire field is added in this commit; the SG implementation has no warning source today (it uses util.MergeSlices + storage.ApplySearchHints rather than a Searcher), so streamBucketSearchResults is unchanged. Future SG-level warnings (e.g. limit truncation) can populate the existing wire field.
The originally-planned consumer of this primitive — BucketStore search wrapping each block's storage.Searcher result before Prometheus's pairwiseMergeSearchSets — was replaced during execution with a slice-based merge (util.MergeSlices + storage.ApplySearchHints on []string), because pairwiseMergeSearchSets is package-private upstream and the BucketStore does not own tsdb.BlockReader instances. That deviation removed PR #1's only consumer. Per CLAUDE.md, primitives without a consumer should not land. The type and its tests will arrive in PR #2 with their actual consumer (cross-replica fan-out at the querier, where Prometheus's pull-based merge would otherwise serialise replicas).
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Autofix Details
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Test assertion is vacuously true on empty slice
- Updated the test to generate searchBatchSize results so a mid-iteration batch is sent and warnings suppression is meaningfully asserted instead of vacuously passing.
Or push these changes by commenting:
@cursor push d8289ba067
Preview (d8289ba067)
diff --git a/pkg/ingester/ingester_search_test.go b/pkg/ingester/ingester_search_test.go
--- a/pkg/ingester/ingester_search_test.go
+++ b/pkg/ingester/ingester_search_test.go
@@ -231,18 +231,19 @@
func TestStreamSearchResultsPropagatesErrInsteadOfWarnings(t *testing.T) {
want := errors.New("boom")
- rs := &fakeSearchResultSet{
- results: []storage.SearchResult{{Value: "a", Score: 1.0}},
- err: want,
- warns: addAnnotation(nil, "should not appear"),
+ // Generate enough results to trigger at least one mid-iteration batch send.
+ results := make([]storage.SearchResult, searchBatchSize)
+ for i := 0; i < searchBatchSize; i++ {
+ results[i] = storage.SearchResult{Value: "a", Score: 1.0}
}
+ rs := &fakeSearchResultSet{results: results, err: want, warns: addAnnotation(nil, "should not appear")}
var sent []*client.SearchResultBatch
send := func(b *client.SearchResultBatch) error {
sent = append(sent, b)
return nil
}
require.ErrorIs(t, streamSearchResults(rs, send), want)
- // The single result was sent before iteration ended; the trailer batch
+ // A batch was sent before iteration ended; the trailer batch
// (which would carry warnings) is suppressed because rs.Err is non-nil.
for _, b := range sent {
assert.Empty(t, b.Warnings, "warnings must not be sent when iteration errored")You can send follow-ups to the cloud agent here.
Folds two iterations of staff-review feedback into a single commit on
top of the original PR work.
Wire shape and semantics
* Rename SearchFilter.case_sensitive → case_insensitive in both the
ingester and store-gateway protos so the proto3 zero value matches
Prometheus's HTTP case_sensitive=true URL-param default. Regenerate
the .pb.go files; update the server-side translators to invert the
bit (CaseSensitive: !wf.CaseInsensitive) and the test fixtures
accordingly. Update doc.go to spell out the wire-vs-Params polarity.
streaminglabelvalues optimisations and validation
* Add a private caseFoldingFilter that wraps the OR root for
case-insensitive searches; per-term filters are now built
case-sensitive against a pre-lowered term so the value is folded
exactly once per Accept call instead of once per leaf. Mirrors
Prometheus PR #18573's caseFoldingFilter. Behavioural test via a
recordingFilter proves the wrapper folds once and feeds every leaf
the lowered value.
* Validate Params before the empty-Terms early-return in BuildFilter
so out-of-range FuzzThreshold or unknown FuzzAlg surface as errors
even when no terms are supplied. Regression test locks this in.
Store-gateway server hardening
* Add TODO comments above each util.MergeSlices call noting the
per-block materialisation OOM hazard and the metrics-reuse
ambiguity, so the next-PR querier author scopes the limit pushdown
and the metrics relabelling.
* Wire activity tracking and tracing on the SearchLabelNames /
SearchLabelValues wrappers: g.tracker.Insert/Delete in gateway.go
and spanlogger.New in bucket_stores.go via new
spanSearchLabel{Names,Values}Server context wrappers next to
spanSeriesServer.
* Promote StoreGateway.tracker from *activitytracker.ActivityTracker
to a private activityTracker interface so tests can inject a fake.
The production *ActivityTracker satisfies it implicitly; typed-nil
pointers wrapped in the interface continue to call the nil-safe
concrete methods, so no public API changes.
* Emit a one-element "results truncated" warning on the trailer batch
when the limit clips on an unfiltered search. Suppress on filtered
searches (we cannot tell post-filter count without a second
iteration) — false-negative documented at the helper.
* Stream loops (streamSearchResults at the ingester,
streamBucketSearchResults at the SG) check ctx.Err() before
iteration begins and at every batch boundary so cancellation is
honoured even when the underlying iterator is not yet ctx-aware.
* Drop the duplicate params.Validate() call in buildBucketSearchHints
— BuildFilter now validates unconditionally.
Ingester error mapping
* Validate the wire request ahead of the deferred
mapReadErrorToErrorWithStatus and tag the error with
codes.InvalidArgument explicitly, matching the SG-side mapping
(the deferred mapper would otherwise re-tag every error as
codes.Internal, including user-input ones). Regression test asserts
status.FromError on a request with FuzzThreshold=200 returns
codes.InvalidArgument.
Tests
* New regression coverage for the proto rename, BuildFilter
validation order, codes.InvalidArgument mapping, batching boundary,
context cancellation, multi-block dedup (covered by the existing
6-block fixture), limit-truncation warning emission,
caseFoldingFilter wrapping behaviour, activity-tracker contract on
the SG search wrappers (with a 0-based recordingActivityTracker
fake mirroring production indexing), and the spanlogger contract
via tracetest.NewInMemoryExporter swapped in for the duration of
the test.
|
Could not push Autofix changes. The PR branch has conflicting changes. |
…l.ErrorToStatus Mimir's faillint policy bans google.golang.org/grpc/status.FromError (and gogo/status.FromError) because neither honours wrapped errors; the codebase standardises on github.com/grafana/dskit/grpcutil.ErrorToStatus which does. CI surfaced two test-only callers introduced by the staff- review commit (ingester_search_test.go and bucket_search_test.go). Drop-in replacement: same (*status.Status, bool) return signature.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Autofix Details
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Duplicate proto-to-domain conversion logic across packages
- Centralized shared wire-to-domain conversion helpers in streaminglabelvalues and refactored both packages to use them.
Or push these changes by commenting:
@cursor push e749d6cdba
Preview (e749d6cdba)
diff --git a/pkg/ingester/ingester_search.go b/pkg/ingester/ingester_search.go
--- a/pkg/ingester/ingester_search.go
+++ b/pkg/ingester/ingester_search.go
@@ -128,24 +128,21 @@
if wf == nil {
return nil, nil
}
- alg := streaminglabelvalues.FuzzAlgSubsequence
- if wf.FuzzAlg == client.FUZZ_ALG_JARO_WINKLER {
- alg = streaminglabelvalues.FuzzAlgJaroWinkler
- }
- return streaminglabelvalues.NewParams(wf.Terms, !wf.CaseInsensitive, alg, int(wf.FuzzThreshold))
+ return streaminglabelvalues.ParamsFromWire(
+ wf.Terms,
+ wf.CaseInsensitive,
+ wf.FuzzAlg == client.FUZZ_ALG_JARO_WINKLER,
+ int(wf.FuzzThreshold),
+ )
}
// protoToOrdering maps the wire SearchOrdering enum onto the Prometheus
// storage.Ordering enum.
func protoToOrdering(o client.SearchOrdering) storage.Ordering {
- switch o {
- case client.ORDER_BY_VALUE_DESC:
- return storage.OrderByValueDesc
- case client.ORDER_BY_SCORE_DESC:
- return storage.OrderByScoreDesc
- default:
- return storage.OrderByValueAsc
- }
+ return streaminglabelvalues.OrderingFromWire(
+ o == client.ORDER_BY_VALUE_DESC,
+ o == client.ORDER_BY_SCORE_DESC,
+ )
}
// searchResultSender is the minimal interface satisfied by both
diff --git a/pkg/storegateway/bucket_search.go b/pkg/storegateway/bucket_search.go
--- a/pkg/storegateway/bucket_search.go
+++ b/pkg/storegateway/bucket_search.go
@@ -214,23 +214,20 @@
if wf == nil {
return nil, nil
}
- alg := streaminglabelvalues.FuzzAlgSubsequence
- if wf.FuzzAlg == storepb.FUZZ_ALG_JARO_WINKLER {
- alg = streaminglabelvalues.FuzzAlgJaroWinkler
- }
- return streaminglabelvalues.NewParams(wf.Terms, !wf.CaseInsensitive, alg, int(wf.FuzzThreshold))
+ return streaminglabelvalues.ParamsFromWire(
+ wf.Terms,
+ wf.CaseInsensitive,
+ wf.FuzzAlg == storepb.FUZZ_ALG_JARO_WINKLER,
+ int(wf.FuzzThreshold),
+ )
}
// storepbToOrdering maps the wire SearchOrdering enum onto storage.Ordering.
func storepbToOrdering(o storepb.SearchOrdering) storage.Ordering {
- switch o {
- case storepb.ORDER_BY_VALUE_DESC:
- return storage.OrderByValueDesc
- case storepb.ORDER_BY_SCORE_DESC:
- return storage.OrderByScoreDesc
- default:
- return storage.OrderByValueAsc
- }
+ return streaminglabelvalues.OrderingFromWire(
+ o == storepb.ORDER_BY_VALUE_DESC,
+ o == storepb.ORDER_BY_SCORE_DESC,
+ )
}
// streamBucketSearchResults sends results in batches of searchBatchSize via
diff --git a/pkg/streaminglabelvalues/wire.go b/pkg/streaminglabelvalues/wire.go
new file mode 100644
--- /dev/null
+++ b/pkg/streaminglabelvalues/wire.go
@@ -1,0 +1,33 @@
+// SPDX-License-Identifier: AGPL-3.0-only
+//
+// Helper conversions for translating wire-level search options into
+// streaminglabelvalues and Prometheus domain types. Kept here to avoid
+// duplicating the same logic across multiple gRPC servers.
+package streaminglabelvalues
+
+import (
+ "github.com/prometheus/prometheus/storage"
+)
+
+// ParamsFromWire constructs Params from wire-level fields.
+// - caseInsensitive matches the wire polarity and is inverted internally.
+// - useJaroWinkler selects the Jaro-Winkler fuzzy algorithm; subsequence is default.
+func ParamsFromWire(terms []string, caseInsensitive bool, useJaroWinkler bool, fuzzThreshold int) (*Params, error) {
+ alg := FuzzAlgSubsequence
+ if useJaroWinkler {
+ alg = FuzzAlgJaroWinkler
+ }
+ return NewParams(terms, !caseInsensitive, alg, fuzzThreshold)
+}
+
+// OrderingFromWire maps booleans derived from a wire enum onto storage.Ordering.
+// Prefer score desc when both flags are set (defensive default).
+func OrderingFromWire(isValueDesc bool, isScoreDesc bool) storage.Ordering {
+ if isScoreDesc {
+ return storage.OrderByScoreDesc
+ }
+ if isValueDesc {
+ return storage.OrderByValueDesc
+ }
+ return storage.OrderByValueAsc
+}You can send follow-ups to the cloud agent here.
| return storage.OrderByScoreDesc | ||
| default: | ||
| return storage.OrderByValueAsc | ||
| } |
There was a problem hiding this comment.
Duplicate proto-to-domain conversion logic across packages
Low Severity
storepbToParams/storepbToOrdering in bucket_search.go and protoToParams/protoToOrdering in ingester_search.go contain identical domain-mapping logic (fuzz algorithm translation, ordering translation) duplicated across two packages. The shared streaminglabelvalues package already exists and could host the common ordering-to-storage.Ordering mapping and the fuzz-algorithm conversion, avoiding divergence risk if new enum values are added to only one copy.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 11c63dd. Configure here.
Verbatim port of Prometheus's package-private pairwiseMergeSearchSets plus the surrounding mergingSearchResultSet, limitSearchResultSet, and compareSearchResults helpers (see vendor/.../storage/generic.go). Exposed so Mimir components that fan out to multiple Searcher sources can stream merged, ordered, deduped results without materialising the full set first. First consumer in the next commit: the store-gateway's per-block fan-out in pkg/storegateway/bucket_search.go. The same primitive will serve cross-replica fan-out in the next PR. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…Sets Restructure SearchLabelNames / SearchLabelValues so each per-block goroutine applies the filter, ordering, and request limit against its own slice (per-goroutine filter construction — filters cache term runes lazily and are not concurrency-safe). The per-block SearchResultSets are then streamed through a pairwise k-way merge that respects the requested ordering, deduplicates across blocks, and stops after the request limit — without materialising the merged set. Drops the unfilteredTruncationWarning helper: the new pipeline never sees the pre-truncation total, so the warning could no longer be accurate. Reviewer didn't ask for it; revisit when the HTTP layer needs reliable truncation signalling. Drops the TODO comments at the merge step — they are now resolved. streamBucketSearchResults takes a storage.SearchResultSet iterator instead of a materialised []storage.SearchResult. Addresses ldufr's review comment on PR #15233 (apply hints in the goroutine; k-way merge across iterators to stream results). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
There are 2 total unresolved issues (including 1 from previous review).
Autofix Details
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Duplicated
warningsToStringsfunction across two packages- Extracted the helper to pkg/storage.WarningsToStrings and updated ingester and storegateway to use it, removing both duplicates.
Or push these changes by commenting:
@cursor push 9d8497c3f6
Preview (9d8497c3f6)
diff --git a/pkg/ingester/ingester_search.go b/pkg/ingester/ingester_search.go
--- a/pkg/ingester/ingester_search.go
+++ b/pkg/ingester/ingester_search.go
@@ -9,11 +9,11 @@
"github.com/grafana/dskit/tenant"
"github.com/prometheus/prometheus/model/labels"
"github.com/prometheus/prometheus/storage"
- "github.com/prometheus/prometheus/util/annotations"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"github.com/grafana/mimir/pkg/ingester/client"
+ mimirstorage "github.com/grafana/mimir/pkg/storage"
"github.com/grafana/mimir/pkg/streaminglabelvalues"
)
@@ -179,7 +179,7 @@
if err := rs.Err(); err != nil {
return err
}
- batch.Warnings = warningsToStrings(rs.Warnings())
+ batch.Warnings = mimirstorage.WarningsToStrings(rs.Warnings())
if len(batch.Results) > 0 || len(batch.Warnings) > 0 {
if err := send(batch); err != nil {
return err
@@ -188,15 +188,4 @@
return nil
}
-// warningsToStrings flattens annotations into a string slice for wire transport.
-// Returns nil for empty input so the proto field is omitted on the wire.
-func warningsToStrings(a annotations.Annotations) []string {
- if len(a) == 0 {
- return nil
- }
- out := make([]string, 0, len(a))
- for _, w := range a {
- out = append(out, w.Error())
- }
- return out
-}
+
diff --git a/pkg/storage/warnings.go b/pkg/storage/warnings.go
new file mode 100644
--- /dev/null
+++ b/pkg/storage/warnings.go
@@ -1,0 +1,19 @@
+// SPDX-License-Identifier: AGPL-3.0-only
+
+package storage
+
+import "github.com/prometheus/prometheus/util/annotations"
+
+// WarningsToStrings flattens annotations into a string slice for wire transport.
+// Returns nil for empty input so the proto field is omitted on the wire.
+func WarningsToStrings(a annotations.Annotations) []string {
+ if len(a) == 0 {
+ return nil
+ }
+ out := make([]string, 0, len(a))
+ for _, w := range a {
+ out = append(out, w.Error())
+ }
+ return out
+}
+
diff --git a/pkg/storegateway/bucket_search.go b/pkg/storegateway/bucket_search.go
--- a/pkg/storegateway/bucket_search.go
+++ b/pkg/storegateway/bucket_search.go
@@ -10,7 +10,6 @@
"github.com/grafana/dskit/runutil"
"github.com/pkg/errors"
"github.com/prometheus/prometheus/storage"
- "github.com/prometheus/prometheus/util/annotations"
"golang.org/x/sync/errgroup"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
@@ -251,22 +250,11 @@
if err := rs.Err(); err != nil {
return err
}
- batch.Warnings = warningsToStrings(rs.Warnings())
+ batch.Warnings = mimirstorage.WarningsToStrings(rs.Warnings())
if len(batch.Results) > 0 || len(batch.Warnings) > 0 {
return send(batch)
}
return nil
}
-// warningsToStrings flattens annotations into a string slice for wire transport.
-// Returns nil for empty input so the proto field is omitted on the wire.
-func warningsToStrings(a annotations.Annotations) []string {
- if len(a) == 0 {
- return nil
- }
- out := make([]string, 0, len(a))
- for _, w := range a {
- out = append(out, w.Error())
- }
- return out
-}
+You can send follow-ups to the cloud agent here.
| out = append(out, w.Error()) | ||
| } | ||
| return out | ||
| } |
There was a problem hiding this comment.
Duplicated warningsToStrings function across two packages
Low Severity
The warningsToStrings function is identically defined in both pkg/ingester/ingester_search.go and pkg/storegateway/bucket_search.go. Both convert annotations.Annotations to []string with the exact same logic. This could live in a shared utility package (e.g., pkg/streaminglabelvalues or pkg/storage) to avoid maintaining two copies.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit f69c271. Configure here.
|
@ldufr - I have addressed your comments so far - do you have any other review comments to add? Thanks! |
PairwiseMergeSearchSets and its helpers are a verbatim port of package-private code in vendored Prometheus (vendor/github.com/prometheus/prometheus/storage/generic.go). Record that with the standard provenance comment matching the convention used elsewhere in the repo (e.g. pkg/streamingpromql/operators/ selectors/extend_range_vector.go). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The package-level notes (Prometheus PR #18573 link, case_sensitive polarity, concurrency contract) live on Params and the filter types, which is where readers actually look. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
filters.go, params.go, and filters_test.go are new files whose design mirrors Prometheus PR #18573 (filter implementations, URL-param shape, score semantics). Add the standard Provenance-includes-* header attributing PR #18573 as the design source. ingester.proto and storepb/rpc.proto already carry Cortex/Thanos provenance for the file's origin; the new SearchFilter / SearchOrdering types we added are modelled on Prometheus PR #18573. Add a second Provenance-includes-location + copyright line per the multi-source pattern used elsewhere in the repo (e.g. cmd/metaconvert/main.go). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Provenance-includes-location must point at code, not pull requests. The previous commit's prometheus/prometheus#18573 links are removed from filters.go, params.go, filters_test.go (where they were the only attribution) and from ingester.proto and storepb/rpc.proto (where they sat alongside Cortex/Thanos file-origin provenance). mergesearch.go's blob/main/storage/generic.go provenance is unaffected (it points at real code) and remains in place. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous round used the PR URL itself for provenance, which is not a code location. Replace with SHA-pinned blob URLs into the PR's head commit (e8e25eb09e41bf295e0c9e847cd27cf9016a553a), matching the existing Mimir pattern for SHA-pinned upstream provenance (see storage/remote/otlptranslator/* attributions on main). Mapping: - filters.go → web/api/v1/search_filters.go - params.go → web/api/v1/search.go - filters_test.go → web/api/v1/search_filters_test.go - ingester.proto → web/api/v1/search.go (second location alongside Cortex origin) - storepb/rpc.proto → web/api/v1/search.go (second location alongside Thanos origin) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
There are 3 total unresolved issues (including 2 from previous reviews).
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Unused index reader in SearchLabelValues block fan-out
- Removed the unnecessary indexReader creation and close in SearchLabelValues since blockLabelValues manages its own reader.
Or push these changes by commenting:
@cursor push 7fb125ab8b
Preview (7fb125ab8b)
diff --git a/pkg/storegateway/bucket_search.go b/pkg/storegateway/bucket_search.go
--- a/pkg/storegateway/bucket_search.go
+++ b/pkg/storegateway/bucket_search.go
@@ -135,11 +135,7 @@
)
s.blockSet.filter(req.Start, req.End, nil, func(b *bucketBlock) {
- // indexReader is created here (outside the goroutine) to hold the block open.
- indexr := b.indexReader(nil)
g.Go(func() error {
- defer runutil.CloseWithLogOnErr(s.logger, indexr, "search label values")
-
b.ensureIndexHeaderLoaded(gctx, stats)
result, err := blockLabelValues(gctx, b, s.postingsStrategy, s.maxSeriesPerBatch, req.Label, matchers, s.logger, stats)You can send follow-ups to the cloud agent here.
Reviewed by Cursor Bugbot for commit 144e481. Configure here.
|
|
||
| b.ensureIndexHeaderLoaded(gctx, stats) | ||
|
|
||
| result, err := blockLabelValues(gctx, b, s.postingsStrategy, s.maxSeriesPerBatch, req.Label, matchers, s.logger, stats) |
There was a problem hiding this comment.
Unused index reader in SearchLabelValues block fan-out
Low Severity
In SearchLabelValues, indexr is created via b.indexReader(nil) and closed in the goroutine, but it is never passed to blockLabelValues — unlike SearchLabelNames, where indexr is created with s.postingsStrategy and actually used by blockLabelNames. The nil strategy argument and the unused reader suggest this was copied from SearchLabelNames without adapting it to match blockLabelValues's different signature, which takes the block directly.
Reviewed by Cursor Bugbot for commit 144e481. Configure here.
There was a problem hiding this comment.
Updated with better comment
| @@ -0,0 +1,282 @@ | |||
| // SPDX-License-Identifier: AGPL-3.0-only | |||
| // Provenance-includes-location: https://github.com/prometheus/prometheus/blob/e8e25eb09e41bf295e0c9e847cd27cf9016a553a/web/api/v1/search_filters.go | |||
There was a problem hiding this comment.
Note to reviewers - I am doing these links since the PR where this is introduced in Prometheus has not yet merged. I will update these once Prometheus PRs are merged.
|
@ldufr I fixed those couple of comments - can I get you to give the review stamp again please |
#### What this PR does Second PR of the streaming label/value search feature (6-PR stack). Builds on #15233 (the ingester and store-gateway streaming `SearchLabelNames` / `SearchLabelValues` RPCs) by adding the calling side: distributor fan-out and querier-side adapters. Note - this PR will likely require GEM updates (implement SearchLabelNames / SearchLabelValues on whatever satisfies pkg/querier.Distributor). Note - a changelog has not been recorded since there are no user-facing changes or live code paths. #### Which issue(s) this PR fixes or relates to Fixes #<issue number> #### Checklist - [x] Tests updated. - [ ] Documentation added. - [ ] `CHANGELOG.md` updated - the order of entries should be `[CHANGE]`, `[FEATURE]`, `[ENHANCEMENT]`, `[BUGFIX]`. If changelog entry is not needed, please add the `changelog-not-needed` label to the PR. - [ ] [`about-versioning.md`](https://github.com/grafana/mimir/blob/main/docs/sources/mimir/configure/about-versioning.md) updated with experimental features. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Introduces new streaming search code paths with concurrent fan-out, context-cancellation handling, and a changed store-gateway stream wire contract (mandatory header batch), which could cause leaks or partial results if edge cases are missed. > > **Overview** > Adds end-to-end support for streaming `SearchLabelNames`/`SearchLabelValues` results by fanning out to ingesters and store-gateways and merging results as `storage.SearchResultSet`s (dedup + ordering + limit) while preserving leaf scores. > > On the ingester path, the distributor now opens quorum-based streaming RPCs, wraps each stream in a bounded prefetching `ConcurrentSearchResultSet`, and adds explicit cleanup to avoid leaking survivor streams when any fan-out branch fails. > > On the store-gateway path, streaming search now **always** emits a header-only batch containing `response_hints.queried_blocks`, supports request scoping via block-matchers hints, and the querier adds adapters that read/validate the header for consistency checking, cancel peer streams on non-retriable errors, and surface warnings correctly. > > Replaces the local `PairwiseMergeSearchSets` implementation with `storage.MergeSearchResultSets`, and adds extensive unit tests covering merge behavior, warnings under early termination, context cancellation, retry/protocol-violation handling, and metadata propagation. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit bb35558. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
#### What this PR does This is the third PR in the streaming labels/values search API stack. See prometheus/proposals#74. This PR builds upon #15233 and #15301 This PR wires the querier-side multi-source fan-out that consumes the work from the above PRs. Reachable but unused - there is no Mimir-internal caller from the HTTP layer — the new methods are exercised only by unit tests in this PR. The user-visible HTTP endpoints will come in a subsequent PR. #### Which issue(s) this PR fixes or relates to Fixes #<issue number> #### Checklist - [x] Tests updated. - [ ] Documentation added. - [ ] `CHANGELOG.md` updated - the order of entries should be `[CHANGE]`, `[FEATURE]`, `[ENHANCEMENT]`, `[BUGFIX]`. If changelog entry is not needed, please add the `changelog-not-needed` label to the PR. - [ ] [`about-versioning.md`](https://github.com/grafana/mimir/blob/main/docs/sources/mimir/configure/about-versioning.md) updated with experimental features. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Adds new querier-level `SearchLabelNames`/`SearchLabelValues` fan-out and limit-clamping logic plus wrapper passthroughs; while covered by unit tests, it touches core query orchestration and could affect search correctness, ordering, and per-tenant limits. > > **Overview** > Adds `multiQuerier.SearchLabelNames`/`SearchLabelValues` to **fan out streaming label/values search across distributor and block-store**, then merge/deduplicate ordered `storage.SearchResultSet`s while enforcing per-tenant `Limit` via `MaxLabelNamesLimit`/`MaxLabelValuesLimit` (including clamp warnings). > > Extends `memoryTrackingQuerier` and `lazyquery.LazyQuerier` with **search pass-through methods** that type-assert for search support and otherwise return `ErrSearchResultSet`, intentionally skipping additional memory tracking on the streaming search path. Comprehensive unit tests cover pass-through behavior, error propagation, ordering/dedup, empty-range short-circuit, and limit-clamp warnings. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit fabc599. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
#### What this PR does This is the 4th PR in the streaming labels/values search API stack. See prometheus/proposals#74. This PR builds upon #15233, #15301 and #15347 This PR adds three experimental NDJSON-streaming HTTP endpoints on the querier; * /api/v1/search/metric_names — searches values of the __name__ label * /api/v1/search/label_names — searches for label names * /api/v1/search/label_values — searches for values of a single label The HTTP endpoints have been placed behind a feature flag ( -querier.experimental-search-api-enabled ) and this defaults to false. Note - in a subsequent PR will be support for the metadata query param to decorate metric names, additional documentation, bechmarks and updates to dashboard mixins. #### Which issue(s) this PR fixes or relates to Fixes #<issue number> #### Checklist - [x] Tests updated. - [ ] Documentation added. - [x] `CHANGELOG.md` updated - the order of entries should be `[CHANGE]`, `[FEATURE]`, `[ENHANCEMENT]`, `[BUGFIX]`. If changelog entry is not needed, please add the `changelog-not-needed` label to the PR. - [ ] [`about-versioning.md`](https://github.com/grafana/mimir/blob/main/docs/sources/mimir/configure/about-versioning.md) updated with experimental features. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > New query surface can drive expensive label scans across ingesters/store-gateways; mitigated by default-off flag, limits, and streaming, but multi-tenant federation fan-out increases blast radius when enabled. > > **Overview** > Adds **experimental** Prometheus-style **streaming label/value search** on the querier: NDJSON endpoints at `/api/v1/search/{metric_names,label_names,label_values}`, gated by **`-querier.experimental-search-api-enabled`** (default off). When disabled, callers get **404** `feature_not_enabled`. > > The new **`search_handler`** layer parses query params (fuzz, sort, limits, `match[]` OR-union), opens a **`mimirSearcher`**, streams batched NDJSON with success/error trailers, and sets **`has_more`** using a limit+1 probe plus typed **`MaxLimitError`** clamp warnings. **`MaxLimitError`** is refactored so handlers can read which limit was enforced without parsing messages. > > Routes are wired in **`pkg/api`** and **`handlers.go`** with usage stats. **Tenant federation** **`merge_queryable`** fans out **`SearchLabelNames` / `SearchLabelValues`**, merges result sets, and special-cases synthetic **`__tenant_id__`** (and retain-prefix) label values like existing **`LabelValues`**. > > Changelog and config docs flag the feature as experimental. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit bc74dda. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
#### What this PR does This is the fifth PR in the streaming label/value search API implementation. See also #15233, #15301, #15347, #15349. This PR focuses on adding support for the `include_metadata` enrichment on results from ` /api/v1/search/metric_names`. This feature allows for metric metadata to be included in the metric name results. This allows for a client to avoid needing to make a separate API call to retrieve metadata records. It should be noted that in Mimir, only the ingesters maintain metric metadata information - and only if it has been included in the remote write. This metadata information is only maintained in the ingesters for a short period of time (10 minutes). To maintain simplicity, this implementation only decorates in the most recent metadata record for a metric. This is regardless of any time range submitted in the search. In the future if Mimir was persist or maintain metadata for longer this implementation choice could be reviewed. No metadata is included if the search time range only requires store-gateways to full-fill the request. This is intentional since "old" metrics which have not been seen (metadata record pushed) by an ingester in the last 10 minutes will not have a metadata in Mimir. #### Which issue(s) this PR fixes or relates to Fixes #<issue number> #### Checklist - [x] Tests updated. - [ ] Documentation added. - [x] `CHANGELOG.md` updated - the order of entries should be `[CHANGE]`, `[FEATURE]`, `[ENHANCEMENT]`, `[BUGFIX]`. If changelog entry is not needed, please add the `changelog-not-needed` label to the PR. - [ ] [`about-versioning.md`](https://github.com/grafana/mimir/blob/main/docs/sources/mimir/configure/about-versioning.md) updated with experimental features. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Additive, flag-gated experimental API behavior with ingester-only enrichment; no auth or ingestion path changes. > > **Overview** > Adds optional **`include_metadata`** to the experimental streaming search API so **`/api/v1/search/metric_names`** can return **type**, **help**, and **unit** on each metric without a separate metadata call. > > The flag is parsed on the querier HTTP handler, carried on **`streaminglabelvalues.Params`**, and sent to ingesters as **`include_metadata`** on **`SearchLabelValues`** when searching **`__name__`**. Ingesters attach **`MetricMetadata`** per batch (most recent in-memory record per metric on that replica); the distributor maps wire metadata into **`storage.SearchResult`**, and the metric-names NDJSON builder emits the extra fields. **Store-gateway–only** searches stay un-enriched by design. > > Also tightens search plumbing: shared **`parseBoolParam`**, **`buildSearchHints`** rejects negative limits and clamps huge values, and dev **`mimir.yaml`** turns on **`experimental_search_api_enabled`**. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 1b2c64f. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
#### What this PR does This is the 6th PR in the streaming label/value search API implementation. See also #15233, #15301, #15347, #15349 and #15364. This PR adds a README.md to assist in documenting this implementation, benchmark test files and some enhancements found whilst running the newly added benchmarks. No changelog has been recorded since these are internal implementation, testing and documentation changes with no user facing modifications. #### Benchmarks: legacy `LabelValues` vs new `SearchLabelValues` Apple M4 Pro, `go test -benchmem -benchtime=5s -count=3`, medians of 3 runs reported. ##### Ingester (`pkg/ingester`) `BenchmarkIngester_LegacyVsSearchLabelValues` — in-process, no RPC framing. | Cardinality | Path | ns/op | B/op | allocs/op | Δ time | Δ B/op | Δ allocs | | ----------- | ------ | ----------: | ----------: | ---------: | -------- | -------- | -------- | | 4 199 | legacy | 211 794 | 90 418 | 4 209 | | | | | 4 199 | new | 216 027 | 223 578 | 21 | +2% | +147% | **−99.5%** | | 1 000 003 | legacy | 149 987 546 | 23 942 547 | 1 000 161 | | | | | 1 000 003 | new | 130 728 720 | 48 140 526 | 158 | **−13%** | +101% | **−99.98%** | **Observations** - At 4 199 values: parity on wall-clock, but allocations drop from 4 209 → 21 (≈200×). - At 1 000 003 values: new path is 13% faster, uses 2× the memory, but allocates **6 330× fewer objects** (158 vs 1 000 161). The allocation reduction is due to the batch buffer reuse and synchronous-send which make skipping the clone safe. ``` q, err := db.Querier(...) defer q.Close() ... rs := searcher.SearchLabelValues(ctx, ...) // result set holds yolo strings defer rs.Close() // return streamSearchResults(ctx, rs, stream.Send, ...) // synchronous; blocks until all batches sent ``` The increase in B/op is due to the wrapping of each record in a `SearchResult{Value, Score}`. ##### Store-gateway (`pkg/storegateway`) `BenchmarkBucketStoreSearchLabelValuesVsLabelValues` — bucket store, cold index cache (`worstCaseFetchedDataStrategy`). | Cardinality | Path | ns/op | B/op | allocs/op | Δ time | Δ B/op | Δ allocs | | ----------- | ------ | -------------: | ----------: | ----------: | -------- | ------ | -------- | | 1 000 | legacy | 21 346 956 | 13 931 796 | 61 668 | | | | | 1 000 | new | 21 368 149 | 14 004 859 | 61 681 | +0.1% | +0.5% | +0.02% | | 1 000 000 | legacy | 32 330 190 178 | 3 482 e6 | 51 001 391 | | | | | 1 000 000 | new | 29 567 861 292 | 3 569 e6 | 51 009 234 | **−9%** | +2.5% | +0.02% | **Observations** - At 1 000 values: parity. - At 1 000 000 values: new path is ~9% faster on time. Memory and allocation count are essentially unchanged because the bulk of the work at the store-gateway layer is in the shared TSDB postings/chunk walk. Although the same SearchResult wrapper is present it is lost in the other SG B/op noise. #### Which issue(s) this PR fixes or relates to Fixes #<issue number> #### Checklist - [x] Tests updated. - [x] Documentation added. - [ ] `CHANGELOG.md` updated - the order of entries should be `[CHANGE]`, `[FEATURE]`, `[ENHANCEMENT]`, `[BUGFIX]`. If changelog entry is not needed, please add the `changelog-not-needed` label to the PR. - [ ] [`about-versioning.md`](https://github.com/grafana/mimir/blob/main/docs/sources/mimir/configure/about-versioning.md) updated with experimental features. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > No user-facing API or behavior changes beyond optional micro-optimizations on an experimental, flag-gated search path; risk is mainly benchmark/doc churn and regression in edge fast paths (single tenant/block, cancel lifecycle). > > **Overview** > Adds **`docs/internal/streaming-label-value-search/README.md`**, an internal guide for the experimental streaming label/value search stack (NDJSON HTTP endpoints, data flow, merge layers, wire batching, benchmarks). > > **Benchmark coverage** is added across ingester, distributor, store-gateway, tenant federation, and HTTP handler tests (including legacy vs new parity benches and distributor merge/dedup scenarios). > > **Performance tweaks** found while benchmarking: HTTP **`search_handler`** pools NDJSON batch envelopes, splits score vs no-score record types to avoid `*float64` allocs, and writes a prebuilt success trailer; **distributor** stores stream cancel funcs on `ingesterSearchResultSet` instead of per-ingester closures; **tenant federation** uses `tenantJobsForSearch` (skip map work when no id-label matcher) and a single-tenant job bypass; **store-gateway** skips `MergeSearchResultSets` when only one block matches. Ingester/store-gateway sources link to the new README. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 3990db9. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->



What this PR does
This is the first PR for the implementation of a new streaming labels/values search API. See prometheus/proposals#74.
This PR leverages Prometheus vendored storage interface changes and search/filter utilities.
For reference - noting not all of these are merged as yet.
This PR focuses on the Searcher implementation which will run on the Ingester and Store-Gateways. The changes are at the gRPC boundary into these components.
Once this PR is merged, the next PR will focus on the Querier and fan-out of the search requests to the changes made in this PR.
TBD - should a change log be recorded for non-user facing change. None of the changes in this PR will be inline to any user operation at this time.
Which issue(s) this PR fixes or relates to
Fixes #
Checklist
CHANGELOG.mdupdated - the order of entries should be[CHANGE],[FEATURE],[ENHANCEMENT],[BUGFIX]. If changelog entry is not needed, please add thechangelog-not-neededlabel to the PR.about-versioning.mdupdated with experimental features.Note
Medium Risk
Introduces new gRPC streaming APIs and non-trivial merge/ordering logic on the read path; while largely additive, it touches core ingester/store-gateway query surfaces and could impact correctness or performance under load.
Overview
Adds a new streaming labels/values search API across ingester and store-gateway, including new gRPC methods
SearchLabelNames/SearchLabelValues, request/response protos (SearchFilter,SearchOrdering,SearchResultBatch), and regenerated client/server stubs.Implements the ingester-side
storage.Searcherintegration with batched streaming, input validation mapped toInvalidArgument, warning propagation, and context-cancellation checks; wires the new RPCs through activity tracking and profiling wrappers.Implements the store-gateway search path by running per-block searches concurrently, applying per-block filter/order/limit, and streaming a deduplicated k-way merge via a new
PairwiseMergeSearchSetsutility (ported from Prometheus), plus wrappers for tracing, activity tracking, and client error wrapping; adds comprehensive tests for batching, ordering, limits, warnings, errors, and cancellation.Reviewed by Cursor Bugbot for commit 3e5862c. Bugbot is set up for automated code reviews on this repo. Configure here.