On this page
Updated 2026-10-18 for Loams 0.1.0
Changed: graphs are now Loams Graph (GQL on Grafeo, D634, D740) with experimental graph expansion, not mapped graphs; streaming subscribe and idempotent gRPC produce ship; engine auth ships; the House reverses "no ClickHouse surface" (D347); the Kafka gateway is deferred to Iggy's (D408); statuses follow the 0.1.0 table.
The retrieval engine is the oldest part of Loams and the part that exists most completely. It combines vector, sparse and full-text retrieval in one Rust binary over one bucket. Beside it, Loams Graph stores property graphs queried with GQL, and in 0.1.0 a search can expand one or two hops into a graph (experimental). This post covers its data model, how a write and a read move through it, the protocols it speaks, and the pieces that are still designs: Kafka-compatible streams and Iceberg tables. The engine page in Learn is the short version.
Five kinds of objects
A namespace holds five kinds of objects. Four ship in Loams 0.1.0.
| Object | What it is | Durable format | Status |
|---|---|---|---|
| Stream | A partitioned, ordered, offset-addressed log | Log segments on object storage | Native produce, fetch and streaming subscribe |
| Collection | Documents by primary key with text, keyword and numeric fields, dense and sparse vectors | One Lance dataset plus Tantivy splits, bound by one manifest | Available |
| Link | A declared, continuously maintained materialization, such as stream → collection | Its applied offset, committed with each target commit | Available |
| Graph | A stored property graph queried with GQL (Loams Graph), on an embedded Grafeo engine | The graph's own log, plus snapshots in the bucket | Available |
| Table | A columnar, schema'd table for analytics | Apache Iceberg | Planned |
The idea that ties them together is that the log is the spine (the Opinions post makes the full case). Every write, whatever protocol it arrives on, lands in a stream first. A write to a collection goes to that collection's implicit stream. Collections, graphs and tables are materializations of streams, kept up to date by links that record their applied offset atomically with every commit. That gives exactly-once materialization, and it gives every write a consistency token that means something in any read on any object.
The write path
- 01Batch and PUTA log node batches records from many partitions and namespaces into one WAL object and writes it to the bucket.
- 02CommitThe metastore records the object and assigns dense offsets per partition.
- 03AcknowledgeThe client gets a consistency token: stream, partition and offset.
- 04ApplyA worker folds the records into Lance fragments and a Tantivy split, and commits a new manifest with its applied offset.
A collection write is admitted only while the collection's unapplied backlog is under a budget (a million records or 128 MiB by default). Past it, the write gets HTTP 429 or gRPC RESOURCE_EXHAUSTED with a Retry-After. That backpressure is what keeps strong reads cheap: the backlog a read may need to merge always fits in memory.
Each collection is one Lance dataset for documents and vectors, plus a set of Tantivy splits for full text and typed-field indexes. A chain of immutable manifests binds them, and a compare-and-swap on the manifest pointer is the only commit point. We wrote about that in One manifest, two formats. Upserts and deletes go through a primary-key index on SlateDB, which maps a key to where its current row lives, and through per-split delete bitmaps. The "What Loams is built on" series covers each of those projects.
The read path
Every read compiles to an Apache DataFusion plan, whichever protocol it came in on. Loams adds its own physical operators:
| Operator | What it does |
|---|---|
AnnExec | Vector top-k: the hot HNSW graph if the collection is hot, otherwise Lance's IVF index, plus a brute-force pass over the tail |
TantivySearchExec | BM25 top-k over the manifest's splits and the tail, with block-max WAND pruning |
SparseExec | Exact scoring of sparse vectors (the SPLADE and BM42 kind) |
FilterBitmapExec | Filters on indexed fields, as a bitmap that the retrievers intersect |
FusionExec | Reciprocal-rank fusion, weighted scores or distribution-based fusion |
TailMergeExec | Merges not-yet-indexed writes from the log with the durable results, honouring upserts and deletes |
DocFetchExec | Fetches the winning documents from Lance by stable row id |
A hybrid query is one plan: filter bitmap → (vector search ‖ BM25) → fusion → fetch. Nothing crosses a network hop between retrieval and fusion, and the planner sees the whole query.
Consistency. By default a read is strong: it sees every write acknowledged before it began. It does that by reading the durable state at the manifest's applied offset and merging the log tail after it. A read that carries a consistency token is guaranteed to see that write, on any node. eventual skips the tail for lower latency.
Scores that do not depend on layout. BM25 statistics are computed over every live document in every split plus the tail, not per split, and deleted or shadowed documents never count. So a document's score is the same before and after a split merge, and on a hot node or a cold one.
The hot tier
Stateless compute over object storage is only fast with caching. Each query node owns the collections that rendezvous hashing assigns to it, and keeps four layers of derived state for them:
- H0: manifests, split footers and file metadata, in RAM.
- H1: byte ranges of durable objects in a RAM and NVMe cache (foyer). Objects are immutable, so the cache never needs invalidation.
- H2: acceleration structures, today HNSW graphs built with qdrant-edge, Qdrant's index code packaged as a library. The graphs are built from a manifest and stored back in the bucket, so another node can load them instead of rebuilding.
- H3: the in-memory tail of records already in the log but not yet indexed.
A differential test runs the same queries with the hot tier on and off and requires identical results.
The wire APIs
Loams takes over the role each system plays in an AI stack, not every feature of its protocol. The footprint is deliberately narrow.
| Surface | Scope | Status |
|---|---|---|
| Native REST | Collections, the hybrid query, SQL with search table functions | Available |
| Qdrant REST and gRPC | Qdrant's API for collections and points, sparse vectors included; the official Python client's suites run in CI | Available |
| Arrow Flight SQL | SQL queries and DoPut bulk ingest, for any language with an ADBC driver | Available |
| Elasticsearch subset | Document APIs, _bulk, _search, core Query DSL, knn, hybrid RRF and filter writes | Available |
| MCP and analytics helpers | Collection tools for agents, to_arrow() and to_polars() | Planned |
| Native Connect / gRPC / gRPC-Web | Namespace, collection, document and typed hybrid Search RPCs | Available |
| Streaming gRPC | Idempotent produce and streaming subscribe | Available |
| OTLP logs | Log ingest from OpenTelemetry shippers | Planned |
| Postgres and MySQL wire | Optional read-only listeners over collections | Available |
| Kafka wire protocol | Kafka clients producing to and consuming from streams | Planned |
There is no Neo4j Bolt surface. Graphs are reached through GQL over loams.graph.v1, and from retrieval through the expand stage of a search and SQL table functions such as graph_expand. When this post was written there was no ClickHouse surface either; D347 reversed that for the House only, and Loams House (experimental in 0.1.0) speaks ClickHouse's HTTP interface on chDB. Analytics on Loams tables still go through Iceberg and Flight SQL. The Elasticsearch surface is scoped by external conformance suites, not by feature parity: no Kibana, no Painless, no full Query DSL.
The same retrieval is reachable from SQL, with the function names Spice uses:
SELECT id, _score
FROM rrf(
vector_search('memories', [0.12, 0.34, 0.56], 'embedding', 100),
text_search('memories', 'quarterly revenue', 'body', 100)
)
LIMIT 10;Auth. When this post was written none of these listeners had authentication. In 0.1.0 engine auth is available: Tailscale identity by default, OIDC, hashed API keys, or loopback-only (LOAMS_AUTH_MODE). Roles, per-namespace authorization and tenant quotas are still planned.
What is still design
Graph expansion for GraphRAG
GraphRAG, LightRAG and Cognee retrieve with a narrow shape: seed by vector or BM25, expand one or two hops, rerank. Running a separate graph database for that one step adds a silo and an ETL path. Loams’ design keeps expansion in the query engine. When this post was written, graphs were to be mapped over collections and tables with CSR and CSC sidecars (D44). That changed: D634 and D740 made Loams Graph a stored property graph, queried with GQL and executed by an embedded Grafeo engine, with its truth in its own log and the bucket. Expansion now runs against a Loams Graph: the expand stage of a search (REST and Connect) and the SQL table functions graph_expand, graph_neighbors, graph_degree and graph_shortest_path. It is experimental in 0.1.0, and the SDKs do not expose it yet. Adapters for LightRAG and the LlamaIndex property graph are still planned.
Streams as a product
The log exists and carries every write. Native HTTP produce and fetch, CloudEvents ingest, and idempotent produce with streaming subscribe over gRPC ship in 0.1.0. OTLP ingest and Kafka wire compatibility remain planned, so RisingWave, Flink, Spark, Kafka Connect and Debezium can read and write Loams streams unchanged. One change since this post: D408 defers a Kafka gateway inside Loams in favour of contributing to Apache Iggy's Kafka gateway, rather than building a second front end (see the Kafka wire protocol). Segments already use the Kafka RecordBatch v2 layout internally, and a test checks that the kafka-protocol crate decodes Loams’ batches and that Loams decodes its batches.
Iceberg tables
Tables for AI analytics (LLM calls and costs, traces, evals) will be Apache Iceberg tables in your bucket, catalogued in Lakekeeper, so DuckDB, Trino, Spark, ClickHouse and Snowflake read them without Loams. Loams adds keyed tables (one live row per key, through deletion vectors), a hot tier for Iceberg (a file index and sorted projections on NVMe) and a real-time tail, so a query sees rows seconds after they are written while external engines see snapshots at commit cadence. The Iceberg deep dive covers what that needs upstream.
Where this stands
| Part | Status |
|---|---|
| Log, WAL on object storage, links, primary-key index, GC | Available |
| Collections: Lance and Tantivy under one manifest, upserts, deletes, sparse vectors | Available |
| DataFusion query engine: vector, BM25, sparse, fusion, tail merge, strong reads | Available |
| Hot tier: foyer cache, HNSW artifacts, pinned splits, affinity routing, backpressure | Available |
| Native REST, SQL table functions, Flight SQL with bulk ingest, Qdrant API | Available |
Elasticsearch subset, collection/document/Search RPCs, and seven SDKs published as 0.1.0 | Available |
| MCP and analytics helpers | Planned |
| Native HTTP streams, CloudEvents, idempotent produce and streaming subscribe over gRPC | Available |
| Engine auth (Tailscale identity, OIDC, API keys) and Loams Graph | Available |
| Graph expansion in the retrieval plan, Loams House | Experimental |
| Roles and tenancy, OTLP, Iceberg and Kafka wire | Planned |
We have not published performance numbers, and will not until the exit benchmarks (BEIR relevance against Elasticsearch BM25, recall against Qdrant, the framework suites) run against a release. The next post moves off the bucket, to the part of Loams that needs real transactions: the reactive database on TiKV.