solutions · rag

RAG without
the sync stack.

The row, its dense embedding and its sparse postings commit in one atomic write. Retrieval runs dense and sparse in parallel and fuses by rank, on one snapshot — so a returned vector can never point at a row that no longer matches.

hybrid retrieval
> POST /v1/tenants/:t/vector/search
{
  "k": 5,
  "dense":   { "query": [0.12, …], "metric": "cosine" },
  "sparse":  { "tokens": { "warranty": 0.81, "claim": 0.63 } },
  "where":   "tier IN ('premium','enterprise')",
  "min_score": 0.7
}

rrf fusion          dense ∥ sparse → one ranked list
  1  doc_912          dense #1 · sparse #3
  2  doc_204          dense #4 · sparse #1
  3  doc_338          sparse #2

one snapshot · every hit's row is current
the failure mode this kills

Two stores. One source of truth, at most.

A customer updates a row in the relational store. A sync job re-embeds it and writes to the vector store. Between those two writes there is a window where retrieval surfaces the old vector pointing at the new row — and the model composes a confidently-wrong answer from context that was never true.

This is the most common RAG failure mode in production, and it produces the "sometimes the LLM hallucinates" reports postmortems struggle to explain. It's not your model. It's your stack.

THE SYNC STACK row store embed worker vector store drift window — old vector, new row ORIGINCHAIN row dense vector sparse postings one atomic write · no window
how originchain does it

One upsert. Three artifacts. Zero pipelines.

Every shape lives on the same substrate — the database indexes the dense vector and the BM25-style sparse postings in the same append that writes the row. The sync job you didn't build can't fall behind.

01
One upsert carries everything

The row, its dense embedding and its sparse postings arrive in a single request. There is no separate embed-and-forward pipeline to build, monitor, or replay.

row + dense + sparse, one request
02
One append commits all three

All three artifacts land on the same write-ahead log in a single append. Either every index sees the new version, or none of them do — by construction, not by best-effort.

one WAL append, all or nothing
03
Readers see one version

A concurrent retrieval cannot observe the row without its vector, or the old vector next to the new row. The half-written state is structurally impossible to read.

no observable half-state
one upsert, three artifacts
> POST /v1/tenants/:t/rows/docs
> { "title": "warranty policy v2",
>   "body": "…",
>   "embedding": […] }

committed atomically
  row        docs/d_912           
  dense      hnsw entry           
  sparse     term postings        

a concurrent retrieval sees all three — or none
retrieval

Dense and sparse in parallel. Then fuse by rank.

The two retrievers make different mistakes: dense search catches semantic matches the keyword side misses; BM25-style sparse catches exact terms — SKUs, acronyms, product codes — the embedding model never saw. OriginChain runs both in one query and fuses the ranked lists with Reciprocal Rank Fusion, by rank rather than raw score, because the two scores live on incomparable scales.

A SQL where filter rides the same plan and the same snapshot, and min_score keeps the floor honest. How the fusion works →

reciprocal rank fusion
RRF(d) = Σ  1 / (k + rank_i(d))
       i ∈ retrievers

with k = 60 (the Cormack default)

in both lists → rises to the top
in one list  → still contributes
k damps low-ranked hits
why the substrate matters here

Three guarantees your reranker gets for free.

Atomic upsert

Row + dense + sparse commit together in one atomic write. Retrieval can't return a vector pointing at a row that no longer matches.

one writeall shapes or none
Hybrid by default

Semantic + BM25-style sparse + a SQL filter in one query. One plan, one consistent snapshot — the filter and both retrievers read the same instant.

dense ∥ sparseone snapshot
min_score threshold

Empty result is a real result. Below the threshold, you get nothing back instead of a confidently-wrong nearest neighbour for the model to cite.

min_scoreempty is honest
the short path

/ask orchestrates RAG by default.

If you'd rather not compose the retrieval yourself, /ask runs the whole loop — translate, hybrid retrieve, re-rank, generate — against the same consistent snapshot.

01
Translate

POST the user's question to /ask. A foundation model compiles it to a retrieval plan — the LLM is the compiler, not the runtime, so the plan passes the same security boundary as any other query.

question → compiled plan
02
Retrieve + re-rank

The plan runs hybrid retrieval — dense and sparse fused by rank — then re-ranks. Every step reads the same consistent snapshot, so the context handed to the model is internally coherent.

hybrid retrieve · re-rank
03
Generate, auditable

The answer is grounded in rows the engine actually returned. Add ?explain=true to see the exact plan, and override the retrieval plan yourself if the LLM picks wrong.

?explain=true → the plan
at a glance
1
atomic write for row + dense + sparse
0
sync jobs, reconcilers, drift windows
0.96
recall@10, high_recall mode at 100k
109 ms
p99 at that recall
11,700
QPS, dense HNSW on SIFT-1M

Retrieval numbers from the published architecture guide.

next

From signup to your first hybrid query, in the walkthrough.