The read path
How a query is routed to an owner, planned at one snapshot, served from cache or the bucket, and merged with the unindexed tail.
Every engine read has the same formula:
result = the durable state at the manifest's applied offset ∪ the tail (applied, requested]
The durable state is the collection's manifest: Lance, splits and delete bitmaps, served from cache or the bucket. The tail is the records already in the log but not yet in that manifest. This page follows a hybrid query through both.
Step by step
1. Route to the owner. Any gateway accepts the read. In a cluster, it forwards the read to the collection's owner, chosen by rendezvous hashing over live query nodes, because the owner is the node most likely to have the collection warm. The forwarded request carries the consistency level and token. If the owner answers Unavailable or Timeout, or cannot be reached, the gateway runs the read itself. A read is forwarded at most once. Ownership is a hint, so a stale route costs latency, never correctness. See cluster mode.
2. Resolve one snapshot. The coordinator picks, for each object the query touches, a manifest version and its applied offsets. Every operator in the query reads that one snapshot, so results are repeatable within a query, even if a commit lands halfway through it.
3. Plan. The request becomes a DataFusion plan with Loams' own operators: a filter bitmap, AnnExec for vectors, TantivySearchExec for BM25, SparseExec, FusionExec, DocFetchExec. The optimizer pushes predicates on indexed fields into bitmaps, picks pre- or post-filtering for the vector search from the filter's estimated selectivity, and delays fetching documents until the final top k. See hybrid retrieval.
4. Read bytes from the fastest layer. Each operator asks for byte ranges of immutable objects: Lance pages, split postings, a manifest. Each range is served by the first layer that has it:
| Layer | Where | On a miss |
|---|---|---|
| H2 hot structure | An HNSW artifact or pinned split on this node | Fall back to the durable index (Lance IVF, the split on the bucket) |
| H1 range cache | RAM | Try the next tier |
| H1 range cache | NVMe (designed tier) | Range GET from the bucket |
| Bucket | Range GET, coalesced into large reads | Cached on the way back |
Nothing ever needs invalidating, because the objects never change.
5. Merge the tail. For each partition, the read needs the records after the manifest's applied offset, up to the target: the current high watermark for a strong read, the token for an at_least read. The node holds a live tail index in memory for its collections; when that cannot serve the range (a pinned read, an overflowed tail), it builds a range tail from the log, cached per manifest and target. The overlay rule: the latest operation on key k in the tail, at offset o, overrides the durable document when o is at or past the applied offset. Tail documents are scored by brute force for vectors and by the in-memory Tantivy index for text, with BM25 statistics computed over both halves together.
6. Fetch and answer. Documents for the final top k are fetched from Lance, or from the tail. The response carries its read token and a performance block: timings, manifest version, tail records, rows scanned per retriever, cache hit ratio and object store requests.
Consistency levels
| Level | Tail merged | Latency |
|---|---|---|
strong (default) | Up to the high watermark at the start of the read | Low; may need a linearizable metastore read |
at_least a token | Up to the token | Low |
eventual | Only what this node already holds; reports what it missed | Lowest |
| pinned | Up to the pin's token, at a named manifest | Low while the manifest is retained |
strong asks the metastore for the high watermark with a linearizable read. On a learner node that is a round trip to the leader for a read index, then a wait until the local replica has applied it.
SQL and other surfaces
The same path serves SQL through Flight SQL, the Postgres and MySQL wire listeners and the REST SQL route: DataFusion plans the SQL, collections appear as tables, and table functions such as vector_search and text_search call the retrievers. The Qdrant and Elasticsearch surfaces translate their requests into the same logical query.
Why it is shaped like this
- Strong by default, without waiting for indexes. Merging the tail is cheaper and more predictable than waiting for a link, and it makes freshness independent of indexing lag.
- Correctness never depends on a cache or an owner. Any layer, any node, same answer. Tests run queries with the hot tier on and off and compare.
- Fetch late. Retrievers move row ids and scores; only the final results pay for document bytes.
What it costs
- Cold reads are slow. A collection no node has cached pays bucket round trips: the design target is half a second to a second at the median, against 5 to 20 ms warm. These are targets from reference systems, not measured Loams numbers.
- The tail costs memory. Its size is bounded by write backpressure; a bulk load that overrides the budget can make strong reads build range tails or answer
Unavailable. - Owner hot spots. One very hot collection lands on one owner until replication of hot collections is raised;
--replicationsets it by hand today.
Status
Available Forwarding to owners, snapshot resolution, the hybrid operators, the H0 and H1 caches in RAM, HNSW artifacts and pinned splits, live and range tails, all four consistency levels and the performance block ship in Loams 0.1.0. Distributed execution across nodes for large scans, the NVMe tier of H1 behind a flag and bounded-load routing are planned.
Go deeper
- Consistency tokens, the hot tier, hybrid retrieval.
- The write path.
- Design: 05 Query engine, 04 Hot tier (§5 and §6), decision log (D15, D77, D79, D107, D108).
- Source:
crates/loams-query,crates/loams-cache,crates/loams-hot.