Architecture
Available

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.

One read: routed to the owner, planned at one snapshot, served from the fastest layer, merged with the tail.

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:

LayerWhereOn a miss
H2 hot structureAn HNSW artifact or pinned split on this nodeFall back to the durable index (Lance IVF, the split on the bucket)
H1 range cacheRAMTry the next tier
H1 range cacheNVMe (designed tier)Range GET from the bucket
BucketRange GET, coalesced into large readsCached 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

LevelTail mergedLatency
strong (default)Up to the high watermark at the start of the readLow; may need a linearizable metastore read
at_least a tokenUp to the tokenLow
eventualOnly what this node already holds; reports what it missedLowest
pinnedUp to the pin's token, at a named manifestLow 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; --replication sets 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

On this page