Skip to content

Streaming labels values PR 1 - #15233

Merged
tcp13equals2 merged 35 commits into
mainfrom
streaming-labels-values-utilities
May 19, 2026
Merged

Streaming labels values PR 1#15233
tcp13equals2 merged 35 commits into
mainfrom
streaming-labels-values-utilities

Conversation

@tcp13equals2

@tcp13equals2 tcp13equals2 commented May 5, 2026

Copy link
Copy Markdown
Contributor

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

  • 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 updated 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.Searcher integration with batched streaming, input validation mapped to InvalidArgument, 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 PairwiseMergeSearchSets utility (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.

tcp13equals2 and others added 15 commits April 25, 2026 10:53
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.
@tcp13equals2 tcp13equals2 added the changelog-not-needed PRs that don't need a CHANGELOG.md entry label May 5, 2026
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).
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).
@tcp13equals2
tcp13equals2 marked this pull request as ready for review May 11, 2026 04:39
@tcp13equals2
tcp13equals2 requested a review from a team as a code owner May 11, 2026 04:39

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.

Create PR

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.

Comment thread pkg/ingester/ingester_search_test.go
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.
@tcp13equals2

Copy link
Copy Markdown
Contributor Author

@cursor push d8289ba

@cursor

cursor Bot commented May 11, 2026

Copy link
Copy Markdown

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.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.

Create PR

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
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 11c63dd. Configure here.

tcp13equals2 and others added 2 commits May 14, 2026 15:37
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>

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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 warningsToStrings function across two packages
    • Extracted the helper to pkg/storage.WarningsToStrings and updated ingester and storegateway to use it, removing both duplicates.

Create PR

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
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit f69c271. Configure here.

@tcp13equals2

Copy link
Copy Markdown
Contributor Author

@ldufr - I have addressed your comments so far - do you have any other review comments to add? Thanks!

tcp13equals2 and others added 5 commits May 14, 2026 16:02
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>

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

There are 3 total unresolved issues (including 2 from previous reviews).

Fix All in Cursor

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.

Create PR

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)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 144e481. Configure here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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
ldufr previously approved these changes May 17, 2026
Comment thread pkg/ingester/client/ingester.proto Outdated
@tcp13equals2

Copy link
Copy Markdown
Contributor Author

@ldufr I fixed those couple of comments - can I get you to give the review stamp again please

@tcp13equals2 tcp13equals2 changed the title Streaming labels values utilities Streaming labels values PR 1 May 18, 2026
@tcp13equals2
tcp13equals2 merged commit 53c04e8 into main May 19, 2026
83 checks passed
@tcp13equals2
tcp13equals2 deleted the streaming-labels-values-utilities branch May 19, 2026 00:13
@tcp13equals2 tcp13equals2 mentioned this pull request May 22, 2026
4 tasks
tcp13equals2 added a commit that referenced this pull request May 22, 2026
#### 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>
tcp13equals2 added a commit that referenced this pull request May 22, 2026
#### 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>
@tcp13equals2 tcp13equals2 mentioned this pull request May 25, 2026
4 tasks
tcp13equals2 added a commit that referenced this pull request May 28, 2026
#### 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>
tcp13equals2 added a commit that referenced this pull request May 28, 2026
#### 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>
tcp13equals2 added a commit that referenced this pull request Jun 4, 2026
#### 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 -->
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

changelog-not-needed PRs that don't need a CHANGELOG.md entry

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants