The previous vector-storage migration plan is retired.
It had three problems for this repository:
- licensing risk for non-open-source packaging
- incorrect assumptions about filtered top-k behavior
- quantized-index lifecycle complexity for a mutable corpus
libSQL is a better fit because it stays in-process, keeps SQLite file-format
and API compatibility, and provides built-in vector types, distance functions,
and vector indexes.
This plan adopts libSQL as a local embedded library only.
It does not adopt:
- Turso Cloud
- sync URLs
- auth tokens
- embedded replicas
- background network activity
The migration must preserve the repository constraints:
- local-first and offline by default
- deterministic builds with pinned revisions
- no hidden network access
- exact retrieval remains the default behavior
- public Godot API stays small and Variant-compatible
RAG APIs are currently unused.
This migration does not need to preserve RAG API compatibility across:
- Godot method names
- retrieval option keys
- schema details
- persisted corpus format
- support for legacy retrieval modes that complicate the backend
The libSQL migration should prefer the smallest coherent API surface for the new backend, even when that is a breaking change, as long as docs and tests are updated in the same change.
- Replace the bundled SQLite engine with a pinned local
libSQLbuild. - Keep the storage layer using the SQLite-compatible
sqlite3_*C API. - Move exact cosine retrieval into SQL so we stop loading the whole corpus into memory for scoring.
- Add libSQL vector indexes as an optional acceleration path, not as the only retrieval path.
- Preserve current filtering semantics for
source_ids, exclusions, and metadata filters. - Avoid new main-thread blocking during migration or index maintenance.
- No Turso Cloud integration.
- No remote replication or sync.
- No dependency on preview
libsql_*client APIs in the hot path. - No silent change from exact retrieval to approximate retrieval.
- No carrying forward unused RAG surface area just for compatibility.
Current RAG docs describe retrieval as exact dense search with filtering, deduplication, and optional MMR.
The libSQL migration must preserve that default:
- exact search remains the default
- filtering happens before the final top-k result is chosen
- deduplication and MMR remain in C++
- ANN is opt-in and may overfetch, but must fall back to exact search when it cannot satisfy the current filtered contract
Goal: prove the local-only integration shape before committing the repo to the dependency swap.
Deliverables
- Build a pinned
libSQLrevision as a local static dependency. - Verify the repository can continue using
sqlite3_open,sqlite3_prepare_v2,sqlite3_step, and friends unchanged. - Verify local-file operation with no remote URL, token, or sync settings.
- Verify libSQL vector SQL on a local DB:
vector32(...)vector_distance_cos(...)vector_distance_l2(...)libsql_vector_idx(...)vector_top_k(...)
- Verify index maintenance behavior on insert, update, and delete.
- Determine the exact vector storage shape to use in the schema:
- whether
F32_BLOBis sufficient - or whether the chosen revision requires
F32_BLOB(<dim>)
- whether
- Verify this on the minimum CI matrix:
- Ubuntu
- macOS
- Windows
Decision rule
- If the local embedded engine path is stable and SQLite-compatible on all target platforms, continue.
- If the implementation would require repo-wide dependence on preview client bindings or remote-only features, stop and keep SQLite.
Goal: swap the underlying embedded engine from bundled SQLite to pinned
local libSQL.
- Add
thirdparty/libsqlas a pinned submodule or pinned vendored source tree. - Record the exact revision in docs and CHANGELOG.
- Do not mix this dependency update with unrelated work.
The current repo aliases the bundled SQLite build as SQLite::SQLite3.
Keep that alias, but make it point to the local libSQL-backed target instead.
This keeps most store code and test code unchanged while the engine changes underneath.
- Add a new static target for the chosen libSQL core.
- Expose the same include surface currently used by
src/core/rag. - Preserve:
SQLite::SQLite3- out-of-source builds
compile_commands.json- warnings-as-errors behavior
- Do not enable remote protocol or sync-specific features in the runtime dependency path.
The local embedded engine path is lower risk for this repo because:
- the current code already uses
sqlite3_* - the repo already treats SQLite as an embedded library
- this avoids coupling core runtime behavior to preview client-SDK surfaces
- it keeps the migration mostly in
CMakeLists.txtand the RAG SQL layer
Goal: move the chunk embedding column to a libSQL vector-aware schema.
Do not assume the current raw float BLOB layout can be reinterpreted in-place as a libSQL vector column.
The safe migration is to rebuild stored embeddings into the new format.
Bump kSchemaVersion from 1 to 2.
Replace the raw embedding column with a libSQL vector-aware column:
CREATE TABLE chunks(
chunk_id TEXT PRIMARY KEY,
source_id TEXT NOT NULL,
source_version TEXT NOT NULL,
title TEXT NOT NULL,
source_path TEXT NOT NULL,
normalized_text TEXT NOT NULL,
display_text TEXT NOT NULL,
metadata_blob TEXT NOT NULL,
chunk_index INTEGER NOT NULL,
byte_start INTEGER NOT NULL,
byte_end INTEGER NOT NULL,
char_start INTEGER NOT NULL,
char_end INTEGER NOT NULL,
token_count INTEGER NOT NULL,
embedding_fingerprint TEXT NOT NULL,
embedding_dimensions INTEGER NOT NULL,
embedding_normalized INTEGER NOT NULL,
vector_metric TEXT NOT NULL,
pooling_type INTEGER NOT NULL,
embedding_vec F32_BLOB NOT NULL,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
FOREIGN KEY(source_id) REFERENCES sources(source_id) ON DELETE CASCADE
);If the pinned libSQL revision requires a dimension-qualified declaration for indexed vectors, resolve that in Phase 0 and use the verified type spelling in the schema implementation.
Do not run a full re-embedding migration inside open().
Instead:
- preserve
sourcesandsource_metadata - create the schema-v2 tables
- mark embeddings as stale in
rag_meta - rebuild chunk embeddings asynchronously from stored source text
Because sources.normalized_text is already persisted, the repo can rebuild
deterministically without downloading anything or requiring original source
files to still exist.
Add:
embedding_storage_formatann_index_readyann_index_metric
Suggested values:
embedding_storage_format=libsql_f32ann_index_ready=0|1ann_index_metric=cosine
Goal: write embeddings in libSQL vector format during ingestion.
src/core/rag/sqlite_corpus_store.cpp
Replace raw embedding BLOB inserts with libSQL vector conversion at write time.
The implementation chosen in Phase 0 must be one of:
- bind a validated libSQL-native vector blob directly
- call
vector32(?)on a bound value that the pinned libSQL revision accepts - as a last resort, call
vector32(?)on a generated JSON array string during ingestion only
The migration should prefer option 1 or 2.
Do not add JSON serialization inside retrieval hot loops.
At insert time, validate:
- embedding dimensions match
embedding_dimensions - metric and normalization metadata match corpus state
- zero-length embeddings are rejected as storage corruption
Goal: keep exact retrieval as the default while moving cosine scoring into SQL.
Exact SQL retrieval fixes the biggest problem in the current implementation: we no longer need to load every candidate embedding into memory just to compute query similarity.
It also preserves filtered top-k semantics because filters stay in the main SQL query instead of being applied after a separate ANN primitive has already chosen neighbors.
Add a cosine exact-search method to CorpusStore:
struct VectorSearchHit {
std::string chunk_id;
float distance = 0.0f;
};
[[nodiscard]] virtual Error exact_vector_search(
const std::vector<float> &query_vector,
const RetrievalOptions &options,
std::vector<VectorSearchHit> &out_hits) const = 0;Keep fetch_chunks_by_ids() for metadata and optional embedding fetches used by
MMR.
SELECT
c.chunk_id,
vector_distance_cos(c.embedding_vec, ?1) AS distance
FROM chunks AS c
WHERE 1=1
[AND source filters]
[AND metadata filters]
ORDER BY distance ASC, c.source_id ASC, c.chunk_index ASC
LIMIT ?2;Where:
?1is the query embedding encoded as a libSQL vector?2iscandidate_k
Always include a stable tie-break after distance so tests stay deterministic.
For the default retrieval mode:
- embed query
- call
store.exact_vector_search(...) - fetch metadata for the selected chunk ids
- convert cosine distance to similarity with
1.0f - distance - keep deduplication, reranking, and MMR behavior unchanged
libSQL’s documented vector surface exposes cosine and L2 distance.
Because RAG API compatibility is not required, the first libSQL migration should intentionally narrow the RAG surface to cosine retrieval only.
Rules:
- remove
dotfrom the RAG-facing config and docs - keep one exact cosine retrieval path
- keep one optional cosine ANN path
- do not ship a separate compatibility fallback for
dot
This simplifies the backend, tests, and docs considerably.
Goal: add libSQL vector indexes as an opt-in acceleration path.
libSQL vector indexes are approximate nearest-neighbor indexes. They are useful, but they are not equivalent to the current exact retrieval contract.
Create an index only for cosine corpora:
CREATE INDEX idx_chunks_embedding_ann
ON chunks(libsql_vector_idx(embedding_vec));If the chosen revision needs explicit settings:
CREATE INDEX idx_chunks_embedding_ann
ON chunks(libsql_vector_idx(embedding_vec, 'metric=cosine'));Add to RetrievalOptions:
bool use_ann_search = false;Default stays false.
SELECT c.chunk_id, v.distance
FROM vector_top_k('idx_chunks_embedding_ann', ?1, ?2) AS v
JOIN chunks AS c ON c.rowid = v.id
WHERE 1=1
[AND source filters]
[AND metadata filters]
ORDER BY v.distance ASC;Because vector_top_k(...) chooses neighbors before outer filters are applied,
ANN retrieval must not assume the first query is sufficient.
Implementation rule:
- overfetch from ANN using a bounded multiplier
- apply normal filters
- if filtered hits are still insufficient, fall back to exact SQL retrieval
This preserves current behavior without silently returning too few or wrong results.
Unlike the retired earlier plan, libSQL vector indexes are part of the engine and are updated automatically with base-table writes.
Still verify this explicitly in tests for:
- insert
- delete
- rebuild
- clear
Expose use_ann_search in the retrieval options dictionary.
Document clearly:
false= exact searchtrue= approximate cosine search with exact fallback when needed to satisfy filtering semantics
Goal: keep layering clean while supporting the two retrieval paths.
Add:
exact_vector_search(...)fetch_chunks_by_ids(...)ensure_ann_index(...)drop_ann_index(...)is_ann_index_ready(...)
Do not expose libSQL-specific types outside the store implementation.
Refactor retrieval into two modes:
cosine + exactcosine + ann
Keep:
- overlap suppression
- reranker hook
- MMR
- existing
RetrievalStatsfields
Add:
search_modewith values likeexact_sqlandann_sqlann_fallback_used
Keep existing counts meaningful:
scanned_chunksshould represent the number of rows considered by the chosen mode when it is knowablecandidate_chunksremains post-threshold, pre-dedup
Goal: expose the smallest Godot-facing API that matches the new backend.
Do not add network or sync-related API.
Godot-facing changes are allowed to be breaking if they simplify the surface.
Add only:
- retrieval option
use_ann_search - optional maintenance method if explicit index creation is required:
rebuild_vector_index_async()
Keep index build and rebuild work off the main thread.
If vector_metric remains exposed at all, reduce it to a single supported
value:
cosine
Prefer removing the option entirely if that produces a cleaner API.
open() must not attempt:
- remote connection
- sync
- full corpus re-embedding migration
- eager ANN rebuild
If a corpus needs migration, surface that as:
stale_embeddings = true- actionable error details
- async rebuild path
Goal: verify local-only correctness, exact-search parity, and cross-platform build behavior.
-
test_libsql_local_store.cpp- open local DB
- schema init
- source and chunk writes
- exact cosine query ordering
-
test_rag_exact_sql_retriever.cpp- exact cosine retrieval with source filters
- exact cosine retrieval with metadata filters
- deterministic tie-break ordering
-
test_rag_ann_retriever.cpp- ANN path returns results
- ANN overfetch + fallback preserves filtered semantics
- ANN path remains opt-in and deterministic under test fixtures
-
test_rag_migration.cpp- schema-v1 DB opens under libSQL
- stale state reported
- async rebuild produces schema-v2 vector rows
Update tests/integration/test_rag_pipeline.cpp to cover:
- local libSQL open
- upsert -> retrieve exact path
- optional ANN retrieval smoke test
- migration failure path
Update docs/RAG_EVALUATION.md to report both:
- exact SQL retrieval latency
- ANN retrieval latency and recall against exact baseline
ANN evaluation must report recall against exact retrieval instead of claiming a fixed quality number from upstream docs.
Update:
- dependency list: bundled SQLite -> libSQL
- retrieval section: exact SQL cosine path
- optional ANN index path and fallback rules
Document:
use_ann_search- exact vs approximate behavior
- cosine-only retrieval
- any removed RAG options as breaking changes
Update feature text to say:
- local embedded libSQL backend
- exact cosine retrieval in SQL
- optional ANN acceleration for cosine corpora
- no cloud dependency
Under [Unreleased]:
- Added local libSQL backend for RAG storage
- Changed exact cosine retrieval to run in SQL
- Added optional ANN retrieval path for cosine corpora
- Added schema-v2 vector storage and async migration flow
Ship this in reviewable increments:
- Phase 0 spike and dependency pin
- build integration with
SQLite::SQLite3backed by libSQL - schema-v2 vector storage and stale-migration path
- exact cosine SQL retrieval
- optional ANN index path
- docs, evaluation, and cleanup
Do not call the migration complete until all of the following are true:
- local libSQL build works on Ubuntu, macOS, and Windows
- no runtime path requires network access
- exact retrieval remains the default behavior
- filtered retrieval semantics are preserved
- the final RAG API surface is documented and intentionally smaller
- ANN is clearly opt-in
- schema migration avoids new main-thread blocking
- docs and CHANGELOG match the shipped API and runtime behavior