LanceDB Vector Backend
Implementation status (2026-08-25): π© Selectable from config. The backend is fully built and trait-conformant β
LanceVectorStorebehind#[cfg(feature="lancedb")](src/vector_lance.rs), allKnowledgeVectorStoremethods includingonly_if()predicate pushdown, theVectorSearchDelegatewrapper, andquipu migrate-vectors(src/migration.rs) β and since quipu-lv7 the binaries readvector.backendand install it at open (src/config/vector_backend.rs;src/cli_open.rsfor everyquipusubcommand,src/server/base.rsforquipu-server).Store::vector_store()already preferred a local backend over the built-in table, so selecting it is all search, resolution, auto-embed and the MCP/REST search tools needed.Two things to know before turning it on:
- The shipped release binaries are NOT built with the feature.
lancedbis deliberately outside thefullbundle β protoc plus the whole datafusion tree is a real cost for a backend most deployments do not use. A binary built without it refusesbackend = "lancedb"at startup, naming the rebuild, rather than falling back to the SQLite table: a deployment that has runquipu migrate-vectorswould otherwise have every search answered out of the store it migrated away from. Build withcargo build --features full,lancedbto ship it.- It needs a Tokio runtime.
quipu-serveris#[tokio::main]; the CLI enters one for the whole dispatch when the configured backend requires it.(This banner previously read βcode-complete but inert in the shipped binariesβ β accurate when it was written, and the shape that rots into a false affordance if left:
set_local_vector_backendhad zero non-test callers andvector.backendwas set-but-not-read, somigrate-vectorsmoved embeddings into a store nothing then selected.)
Quipu supports two vector storage backends: the default SQLite backend and
an optional LanceDB backend for production workloads. Both implement the
KnowledgeVectorStore trait.
Dual-Backend Architecture
ββββββββββββββββββββββββββββ
β KnowledgeVectorStore β
β (trait) β
ββββββββββ¬ββββββββββββββββββ
β
ββββββββββββββββ΄βββββββββββββββ
β β
ββββββββββ΄βββββββββ ββββββββββββ΄βββββββββββ
β SQLite (default)β β LanceDB (optional) β
β Brute-force β β ANN + pushdown β
β cosine sim β β Arrow columnar β
βββββββββββββββββββ βββββββββββββββββββββββ
| Aspect | SQLite | LanceDB |
|---|---|---|
| Storage format | f32 BLOB in vectors table | Arrow RecordBatch columns |
| Search algorithm | Brute-force cosine similarity | Approximate nearest neighbor |
| Predicate pushdown | No (5x oversampling fallback) | Yes (only_if() clause) |
| Complexity | O(n) scan | O(log n) with filter |
| Metadata columns | entity_id, text, valid_from, valid_to | + entity_type, source_episode |
| Async requirement | None | Tokio runtime required |
| Feature flag | Always available | lancedb feature |
Enabling LanceDB
Two steps, and both are required β the feature compiles the backend, the config key selects it.
1. Build with the feature.
cargo build --features full,lancedb
2. Select it in .bobbin/config.toml.
[quipu.vector]
backend = "lancedb"
lancedb_path = ".bobbin/quipu/quipu-vectors"
Moving existing embeddings across first is one command:
quipu migrate-vectors --from sqlite --to lancedb --dry-run # see the count
quipu migrate-vectors --from sqlite --to lancedb
Selecting the backend on a directory that has never been written creates the empty table, so a fresh deployment does not have to migrate first.
As a library dependency
Add the lancedb feature flag:
[dependencies]
quipu = { git = "https://github.com/scbrown/quipu", features = ["lancedb"] }
Or build from source:
cargo build --features lancedb
The KnowledgeVectorStore Trait
Both backends implement this trait (defined in src/vector.rs):
#![allow(unused)]
fn main() {
pub trait KnowledgeVectorStore {
fn embed_entity(&self, entity_id: i64, text: &str,
embedding: &[f32], valid_from: &str) -> Result<()>;
fn close_embedding(&self, entity_id: i64, valid_to: &str) -> Result<()>;
fn vector_search(&self, query: &[f32], limit: usize,
valid_at: Option<&str>) -> Result<Vec<VectorMatch>>;
fn vector_search_filtered(&self, query: &[f32], limit: usize,
filter: Option<&str>,
valid_at: Option<&str>) -> Result<Vec<VectorMatch>>;
fn vector_count(&self) -> Result<usize>;
}
}
The Store::vector_store() method returns &dyn KnowledgeVectorStore,
so calling code is backend-agnostic.
Delegated Vector Search
When Quipu is used as a Bobbin dependency, embeddings are rebuildable derived
data that belong in the index layer (Bobbin), not the durable knowledge layer
(Quipu). The VectorSearchDelegate trait enables this separation:
#![allow(unused)]
fn main() {
pub trait VectorSearchDelegate: Send + Sync {
fn vector_search(&self, query: &[f32], limit: usize,
valid_at: Option<&str>) -> Result<Vec<VectorMatch>>;
fn vector_search_filtered(&self, query: &[f32], limit: usize,
filter: Option<&str>,
valid_at: Option<&str>) -> Result<Vec<VectorMatch>>;
fn text_search(&self, query: &str, limit: usize,
valid_at: Option<&str>) -> Result<Vec<VectorMatch>>;
fn vector_count(&self) -> Result<usize>;
}
}
When a delegate is set via Store::set_vector_search_delegate():
- Search forwards to delegate:
vector_store()returns a wrapper that routes all search calls to the delegate - Auto-embedding is skipped: the transact hook does not generate embeddings (Bobbin owns the embedding lifecycle)
- Write methods are no-ops:
embed_entity()andclose_embedding()on the delegated store silently succeed without writing
When no delegate is set (standalone mode), Quipu falls back to its own SQLite or LanceDB vectors as before.
Hybrid Search with Predicate Pushdown
The quipu_hybrid_search tool uses a three-phase approach:
Phase 1 β Extract pushdown filter. Simple SPARQL type patterns
(?s a <TypeIRI>) are converted to a SQL filter string like
entity_type = 'TypeIRI'.
Phase 2 β Vector search with filter. The filter is passed to
vector_search_filtered():
- LanceDB: applies the filter during ANN search (
only_if()clause), so only matching vectors are scanned - SQLite: ignores the filter and oversamples by 5x, relying on post-filtering
Phase 3 β Post-filter by SPARQL candidates. The full SPARQL query executes independently, and vector results are intersected with SPARQL results for consistency.
SPARQL: SELECT ?s WHERE { ?s a <Person> }
β
βββΊ Extract type filter: entity_type = 'Person'
β
βββΊ Vector search with pushdown (LanceDB)
β or oversample 5x (SQLite)
β
βββΊ Post-filter: intersect with SPARQL candidates
β
βΌ
Ranked results
Embedding Dimensions
All backends use 384-dimensional float32 vectors, compatible with the
all-MiniLM-L6-v2 model. When running as a Bobbin subsystem, the shared
ONNX embedding pipeline provides vectors automatically.
Temporal Awareness
Both backends track valid_from and valid_to for each embedding:
- Current embeddings have
valid_to = NULL - Expired embeddings are excluded from searches unless
valid_atis specified - Time-travel queries (
valid_at) return embeddings active at that timestamp