Overview
Multi-model embedding support for Worlds: normalized chunk_vectors table, model switching, and async backfill. This is the post-beta design — the July 31 private beta ships with one model per world.
Current design (beta)
- Single
vector F32_BLOB column on chunks table
embedding_model TEXT on worlds_metadata identifies which model produced vectors
- On model switch, synchronous
client.reindex() re-embeds all chunks
- One model per world, no rollback without full reindex
Problem
Users will eventually want to:
- Switch embedding models without losing search availability
- Run queries against a specific model's vector space
- Compare search quality across models on the same data
- Roll back to a previous model if quality degrades
The current single-vector design can't support any of these without a full reindex.
Proposed design
Normalized chunk_vectors table
CREATE TABLE chunk_vectors (
chunk_id INTEGER NOT NULL,
model TEXT NOT NULL,
vector F32_BLOB NOT NULL,
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
PRIMARY KEY (chunk_id, model),
FOREIGN KEY (chunk_id) REFERENCES chunks(id) ON DELETE CASCADE
);
Replace the vector column on chunks with this normalized table. The idx_chunks_vector index moves here, scoped per model.
Per-model vector indexes
-- Partial index per active model (created dynamically on model registration)
CREATE INDEX idx_cv_vector_{model} ON chunk_vectors(vector)
WHERE model = '{model}';
Or use a single index with model-aware query:
CREATE INDEX idx_cv_vector ON chunk_vectors(vector, model);
Query routing
vector_top_k scoped to model:
SELECT chunk_id, row_number() OVER (...) AS rank_number
FROM vector_top_k('idx_cv_vector_{model}', vector32(?), ?)
Or filter in a CTE:
WITH model_vectors AS (
SELECT chunk_id, vector FROM chunk_vectors WHERE model = ?
),
vec_matches AS (
SELECT chunk_id AS rowid, row_number() OVER (...) AS rank_number
FROM vector_top_k('idx_cv_vector', vector32(?), ?)
WHERE chunk_id IN (SELECT chunk_id FROM model_vectors)
)
Model switching flow
- User sets
embedding_model on world via PATCH /worlds/{id}
- API stores new model name but does NOT immediately reindex
- New chunks get vectors for the new model
- Background reindex job (triggered by API or CLI) backfills old chunks:
- Embed old chunks with new model
- Insert rows into
chunk_vectors with new model name
- Mark model as "active" when backfill completes
- During backfill, search uses old model's index (no downtime)
- After backfill, switch
vector_top_k to new model's index
- Optionally delete old model's vectors after validation period
Import path changes
Current: embed once, insert into chunks.vector.
New: embed once, insert into chunk_vectors with model = embedding_model.
Reindex path changes
Current: rebuildLibsqlSearchIndexFromQuads() re-embeds all chunks.
New: rebuildLibsqlSearchIndexFromQuads() embeds into chunk_vectors for active model.
Search path changes
Current: vector_top_k('idx_chunks_vector', ...) over chunks.vector.
New: vector_top_k('idx_cv_vector_{model}', ...) over chunk_vectors.vector WHERE model = ?.
Write amplification
- Import: No change (one embedding call per chunk)
- Model switch + backfill: N embedding calls for N existing chunks (one-time cost)
- Multi-model queries: No write amplification (queries read from existing vectors)
Open questions
- Should we support model registration metadata (
embedding_models table) or keep embedding_model as a free-text field?
- Should backfill be automatic (triggered by
embedding_model change) or manual (CLI/API command)?
- How long do we keep old model vectors after switching? (Validation period)
- Should
SearchRequest accept a model override for cross-model quality comparison?
- What's the storage budget? (32-dim TFJS vs 1536-dim OpenAI = ~48x per vector)
Migration path
The v0 schema has chunks.vector. The migration:
- Create
chunk_vectors table
- Migrate existing vectors:
INSERT INTO chunk_vectors (chunk_id, model, vector) SELECT id, embedding_model, vector FROM chunks WHERE vector IS NOT NULL
- Drop
chunks.vector column
- Drop
idx_chunks_vector index
- Create per-model partial indexes on
chunk_vectors
Related
- worlds-api#2 (inference cost offset research — beta one-model design)
- worlds-api MemoryProfile PR (adds
embedding_model to world metadata)
- Wazoo Beta deadline: July 31 (this is post-beta)
Overview
Multi-model embedding support for Worlds: normalized
chunk_vectorstable, model switching, and async backfill. This is the post-beta design — the July 31 private beta ships with one model per world.Current design (beta)
vector F32_BLOBcolumn onchunkstableembedding_model TEXTonworlds_metadataidentifies which model produced vectorsclient.reindex()re-embeds all chunksProblem
Users will eventually want to:
The current single-vector design can't support any of these without a full reindex.
Proposed design
Normalized chunk_vectors table
Replace the
vectorcolumn onchunkswith this normalized table. Theidx_chunks_vectorindex moves here, scoped per model.Per-model vector indexes
Or use a single index with model-aware query:
Query routing
vector_top_kscoped to model:Or filter in a CTE:
Model switching flow
embedding_modelon world viaPATCH /worlds/{id}chunk_vectorswith new model namevector_top_kto new model's indexImport path changes
Current: embed once, insert into
chunks.vector.New: embed once, insert into
chunk_vectorswithmodel = embedding_model.Reindex path changes
Current:
rebuildLibsqlSearchIndexFromQuads()re-embeds all chunks.New:
rebuildLibsqlSearchIndexFromQuads()embeds intochunk_vectorsfor active model.Search path changes
Current:
vector_top_k('idx_chunks_vector', ...)overchunks.vector.New:
vector_top_k('idx_cv_vector_{model}', ...)overchunk_vectors.vector WHERE model = ?.Write amplification
Open questions
embedding_modelstable) or keepembedding_modelas a free-text field?embedding_modelchange) or manual (CLI/API command)?SearchRequestaccept amodeloverride for cross-model quality comparison?Migration path
The v0 schema has
chunks.vector. The migration:chunk_vectorstableINSERT INTO chunk_vectors (chunk_id, model, vector) SELECT id, embedding_model, vector FROM chunks WHERE vector IS NOT NULLchunks.vectorcolumnidx_chunks_vectorindexchunk_vectorsRelated
embedding_modelto world metadata)