Skip to content

Concepts

The query lifecycle

What happens between a query string and a result frame — lowering, planning, pruning, dispatch, merge, streaming, and the failure behavior at each stage.


A search in YoloSearch passes through a fixed sequence of phases. Each one has a defined failure behavior. The response header and trailer report execution metadata.

The phases

  query string
      │  parse + lower (client-side by default)
      v
  structured request ──────────────────> digest
      │
      v
  coordinator
      │  1. validate; resolve whether exactness is provable
      │  2. pin exactly one catalog generation
      │  3. prune segments
      │  4. plan bounded work units and a merge topology
      │  5. select workers; asynchronously hydrate
      v
  workers ──> ordered local runs + score bounds
      │
      v
  merge tree (bounded fan-in, bound propagation)
      │  6. prove the exact prefix
      │  7. stream the tail
      v
  header ─> result frames ─> progress ─> trailer (the receipt)

1. Lowering, and the digest

search and explain accept two grammars — Lucene and CQP — that lower to one abstract syntax tree. Lowering is the single producer of the structured request, so equivalent canonical trees produce the same digest: a SHA-256 over the canonical AST.

The digest is computed over the AST, not the lowered request, so it is independent of any schema. explain prints it, and it is how you confirm that two spellings are the same query. [word="foo"] and foo share a digest, because a metacharacter-free, case-sensitive CQP value normalizes to a literal. +foo and foo do not, because a lone required clause is a different tree from a lone optional one.

The CLI lowers client-side, so a typo lands a caret rendering before any round trip:

error: <message>
  (hint naming the spelling that works)
  title:new york
            ^^^^  byte 11
yolosearch: query refused

A query may also travel to the server as a typed DSL string, which the server lowers against the index schema exactly as the client would. A refusal comes back as INVALID_ARGUMENT carrying the same caret rendering — the one server error with a multi-line body.

Recognized-but-unsupported syntax is refused by name, never reinterpreted. Treating foo~2 as the term foo~2 would quietly answer a different query.

2. Pinning a generation

The coordinator pins exactly one catalog generation for the query's lifetime. Everything downstream — the segment set, the liveness bitmaps, the schema each segment resolves names under — is fixed at this point, which is what makes a long-running stream coherent.

If a worker is named a generation it has not observed, it resolves that generation once and builds the engine only if the generation-overlap window has not expired. A coordinator may refresh and replan once when such a refusal arrives before its merge frontier has emitted anything; it never replans after emission.

3. Pruning

Routing and segment preparation can skip work when metadata rules out a match. The available checks include:

Signal Proves
Time and field extrema No document in range
terms.bloom The segment cannot contain a term
filters.postings Exact postings for low-cardinality filters
Vector centroids Select IVF lists for ANN retrieval; this is candidate selection and can omit matches
Term dictionaries The term is absent from this field

Every Bloom filter here is one-sided. A negative skips work; a positive keeps the exact path. Exact exclusion checks preserve the answer. ANN list selection has a different guarantee: results are exact within the retrieved candidates.

4. Planning work units

The coordinator plans segment work units and a merge topology. Fan-in and per-stage buffering have explicit limits. Total planning work and allocation still depend on the number of active segments.

In the M3 qualification fixtures with 1, 8, 64, and 512 segments, connection count stayed at 9, coordinator buffered frames capped at 8, and merge buffered frames capped at 8. At 512 segments merge streams reached 144 and peak goroutines 2,158. These measurements describe those fixtures; they do not establish constant memory use for arbitrary fleet sizes or query mixes.

Segments are assigned to subtrees as disjoint contiguous ranges in ascending catalog segment-ID order. That ordering preserves tie order without including per-candidate segment identity in frames. See ranking and exactness.

5. Selecting workers and getting bytes

Placement picks a worker per work unit: the union of both zone aggregators' candidates, warm claimants ordered by rendezvous hash for stability, the top two compared by free lane slots, advancing on RESOURCE_EXHAUSTED. The chosen worker performs the authoritative admission check, which closes the observe-then-consume race without consensus.

The worker then resolves its storage mode per segment. Under AUTO, a cold segment still executes through remote blocks while full admission is queued in the background.

Failed work can be retried before emission. The current coordinator does not launch speculative duplicates of slow work units.

Queries run in lanes — INTERACTIVE, STREAMING, BACKGROUND — with separate admission slots, to separate their admission budgets. CPU, storage, and network resources can still be shared. A worker advertises per-lane free slots as part of its capacity frame.

6. Proving the prefix

Within a segment, workers use block-max WAND and MaxScore, and return an ordered local run, the highest remaining unseen score bound, cost counters, and the score-bound version used.

A bounded-fan-in merge tree (default 32) with one lookahead frame per child combines runs. A node's advertised bound is the maximum over its children's current bounds and its own buffered-but-unemitted candidates. Before emitting a candidate, the merge fetches any unbuffered, unfinished child whose bound is unknown or at least that candidate's score. Equality blocks because an unseen candidate could win on the public-ID tie break.

For large K the coordinator pages: it requests bounded pages with bounds, raises the global threshold, and asks only competitive segments for more.

7. Streaming the answer

Search is unary-request / server-streaming. The response stream is a sequence of five message kinds:

Message Carries
SearchHeader Query ID, pinned catalog generation, plan digest, scorer version, exactness, total segments, storage decision, tail ordering, effective vector parameters, any segments whose stored lane cannot answer the projection
ResultFrame Delta- or bit-packed public IDs, optional packed scores, result ordinal, ordering class, score range, conservative ordering error, result count
DocumentFrame Stored documents for a result ordinal, each with its key, mutation version, and stored values — or marked unavailable
SearchProgress Segments completed and skipped, candidates evaluated, highest unseen score bound
SearchTrailer Total emitted, segments completed and skipped, object bytes, cache bytes, completeness, exactness, storage decision, tail ordering, maximum ordering error

The default target frame size is 256 KiB. Requests can select a target from 64 KiB through 1 MiB. Distributed work uses fixed lane defaults: 256 KiB for interactive work and 1 MiB for streaming work. Frame size does not adapt to observed send blocking or compression ratio.

Backpressure is end-to-end and uses no separate mechanism: gRPC HTTP/2 flow control. A blocked public send stops the root merger, which stops requesting child pages, which eventually blocks leaf workers. Only a small bounded number of frames sit queued between stages.

The M3 benchmark recorded 20.1M IDs/s at 20.01 bytes per ID for a four-worker, 64-segment in-process fleet on an 8-CPU Ryzen 7 4700U. Its send-blocked fraction was 0.010; first-frame times were 17.8 ms interactive and 21.8 ms streaming. These are historical fixture measurements, not current production capacity guarantees. The engine repository records the run in bench/receipts/m3-remote-9c1e8967e5e6.json and docs/milestones/m3-evidence.md.

Failure behavior

The qualification tests exercise failures before and after result emission. Retry behavior depends on the emission state and available replacement workers.

Fault Behavior
Worker killed before its unit emitted Eligible for retry on a survivor before emission
Worker killed after emission The public stream returns an error
Every worker killed Returns an error
Merge node killed before emission Retried on its peer
The only merge node killed Returns an error
Coordinator killed The stream ends; the client must submit a new query after recovery
Every placement aggregator killed Selection uses configured fallback workers
Local cache deleted entirely Same results reconstructed from objects alone
Segment data, manifest, or commit marker corrupted Refused by every storage mode once the cache is cold

No fault yields a successful trailer over missing work. The emission frontier is what makes this decidable: before the root has emitted, a failed unit can be retried transparently; after it has emitted, the stream must report an error if it can no longer maintain the ordering guarantee.

Error codes

Client-side, by exit code:

  • 2 — an invocation error: unknown command or flag, bad flag value, wrong argument count, missing required flag. It never dials the server.
  • 1 — everything else: a refused query, a rejected push, a server that is not there.
  • 0 — success, and --help anywhere.

A client error names the operation, the server's message, the gRPC code in upper snake case, and a remedy naming a flag or environment variable:

yolosearch: stats: connection refused [UNAVAILABLE]; is a server listening at 127.0.0.1:9500? start one with `yolosearch serve`, or name it with --server (env YOLOSEARCH_CLIENT_SERVER)

Server-side refusals arrive as gRPC status codes with a consistent split:

  • INVALID_ARGUMENT — a query the engine cannot compile: a regex expansion over query.regex_max_expansions (default 256), a filter on a field that is not filterable, a nesting depth over query.max_ast_depth (default 32).
  • FAILED_PRECONDITION — a segment lacking the lane a query needs: positions for a phrase, attribute lanes for a filter.

Reading a receipt

The header and trailer together are the receipt. Four fields answer most questions:

  • complete (trailer) — whether every planned unit contributed. A successful stream with complete=false is not possible for missing work; the stream fails instead.
  • exactness — GLOBAL_EXACT, EXACT_WITHIN_CANDIDATES, or APPROXIMATE. ANN vector retrieval over a nonexhaustive candidate set yields the middle value.
  • storage_decision — the selected and effective mode per the AUTO policy, which is how you tell an asynchronous cold admission from a hydration wait.
  • object_bytes and cache_bytes — where this query's bytes came from.

Also check projection_unavailable_segments in the header: it names segments whose stored lane cannot answer the requested projection, because they are format v1 or predate the field becoming stored. Missing projected values do not change the reported ranking exactness.

Next