Loams v0.1.0 is out: run the whole stack yourself with Docker Compose. See the roadmap

Blog/The Loams platform

The engine: hybrid retrieval, streams, Iceberg and the wire APIs

How Loams combines vector, sparse and full-text retrieval on a bucket, with native and compatibility APIs, streams and the next graph/analytics integration steps.

The engine: hybrid retrieval, streams, Iceberg and the wire APIs
On this page
  1. Five kinds of objects
  2. The write path
  3. The read path
  4. The hot tier
  5. The wire APIs
  6. What is still design
  7. Graph expansion for GraphRAG
  8. Streams as a product
  9. Iceberg tables
  10. Where this stands

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.

ObjectWhat it isDurable formatStatus
StreamA partitioned, ordered, offset-addressed logLog segments on object storageNative produce, fetch and streaming subscribe
CollectionDocuments by primary key with text, keyword and numeric fields, dense and sparse vectorsOne Lance dataset plus Tantivy splits, bound by one manifestAvailable
LinkA declared, continuously maintained materialization, such as stream → collectionIts applied offset, committed with each target commitAvailable
GraphA stored property graph queried with GQL (Loams Graph), on an embedded Grafeo engineThe graph's own log, plus snapshots in the bucketAvailable
TableA columnar, schema'd table for analyticsApache IcebergPlanned

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

  1. 01Batch and PUTA log node batches records from many partitions and namespaces into one WAL object and writes it to the bucket.
  2. 02CommitThe metastore records the object and assigns dense offsets per partition.
  3. 03AcknowledgeThe client gets a consistency token: stream, partition and offset.
  4. 04ApplyA worker folds the records into Lance fragments and a Tantivy split, and commits a new manifest with its applied offset.
A write is acknowledged once it is durable in the bucket. Indexing happens later and never blocks a read that asks for the write.

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:

OperatorWhat it does
AnnExecVector top-k: the hot HNSW graph if the collection is hot, otherwise Lance's IVF index, plus a brute-force pass over the tail
TantivySearchExecBM25 top-k over the manifest's splits and the tail, with block-max WAND pruning
SparseExecExact scoring of sparse vectors (the SPLADE and BM42 kind)
FilterBitmapExecFilters on indexed fields, as a bitmap that the retrievers intersect
FusionExecReciprocal-rank fusion, weighted scores or distribution-based fusion
TailMergeExecMerges not-yet-indexed writes from the log with the durable results, honouring upserts and deletes
DocFetchExecFetches 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.

SurfaceScopeStatus
Native RESTCollections, the hybrid query, SQL with search table functionsAvailable
Qdrant REST and gRPCQdrant's API for collections and points, sparse vectors included; the official Python client's suites run in CIAvailable
Arrow Flight SQLSQL queries and DoPut bulk ingest, for any language with an ADBC driverAvailable
Elasticsearch subsetDocument APIs, _bulk, _search, core Query DSL, knn, hybrid RRF and filter writesAvailable
MCP and analytics helpersCollection tools for agents, to_arrow() and to_polars()Planned
Native Connect / gRPC / gRPC-WebNamespace, collection, document and typed hybrid Search RPCsAvailable
Streaming gRPCIdempotent produce and streaming subscribeAvailable
OTLP logsLog ingest from OpenTelemetry shippersPlanned
Postgres and MySQL wireOptional read-only listeners over collectionsAvailable
Kafka wire protocolKafka clients producing to and consuming from streamsPlanned

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

PartStatus
Log, WAL on object storage, links, primary-key index, GCAvailable
Collections: Lance and Tantivy under one manifest, upserts, deletes, sparse vectorsAvailable
DataFusion query engine: vector, BM25, sparse, fusion, tail merge, strong readsAvailable
Hot tier: foyer cache, HNSW artifacts, pinned splits, affinity routing, backpressureAvailable
Native REST, SQL table functions, Flight SQL with bulk ingest, Qdrant APIAvailable
Elasticsearch subset, collection/document/Search RPCs, and seven SDKs published as 0.1.0Available
MCP and analytics helpersPlanned
Native HTTP streams, CloudEvents, idempotent produce and streaming subscribe over gRPCAvailable
Engine auth (Tailscale identity, OIDC, API keys) and Loams GraphAvailable
Graph expansion in the retrieval plan, Loams HouseExperimental
Roles and tenancy, OTLP, Iceberg and Kafka wirePlanned

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.

More from the blog