Drawer 04 · Storage & schema
Where the collection is shelved
There is no external datastore. Vectors sit in a FAISS flat index beside a pickled metadata map; repository records, two embedding caches and the repository summaries each get their own SQLite file; keyword shards belong to Zoekt; and the incremental-indexing manifest is a JSON file written relative to the process working directory.
04.1Shelf map
CREATE TABLE statements.
04.2The vector store in detail
def _init_index(self) -> None:
# Start with flat index (exact search)
# Will upgrade to HNSW when threshold is reached
self._index = faiss.IndexFlatIP(self.dimensions) # inner product = cosine on unit vectors
if self.use_gpu:
try:
res = faiss.StandardGpuResources()
self._index = faiss.index_cpu_to_gpu(res, 0, self._index)
except Exception as e:
logger.warning(f"GPU not available for FAISS: {e}")
HNSW_THRESHOLD is
declared at line 54 and referenced nowhere else in the tree; no code path
constructs IndexHNSWFlat or any quantised variant. Search is exact
inner product over every stored vector, always. The project's own notes in
claude.md flag this as a known gap and place it in the backlog in
TASKS.md with the annotation “only needed at 50k+ vectors”.
| Operation | What actually happens | Site |
|---|---|---|
| add_batch() | Vectors are L2-normalised in place and appended; metadata rows written to two dicts. Returns the count, never fails loudly if FAISS is absent — it returns 0. | :248 |
| add() | On an existing id it appends a second vector and repoints the metadata; the superseded vector stays in the index and can still be returned, then dropped because its row is no longer in _idx_to_doc. |
:216 |
| search() | Over-fetches — k = limit × 5 when filtering by repository, limit × 2 otherwise — then filters in Python and stops at limit. A repository with few chunks in a large index can be starved. |
:326 |
| delete_by_repo() | Metadata-only. The vectors remain; only the mapping is removed. Reported document counts drop, index.ntotal does not. |
:399 |
| reinitialize() | Unlinks faiss.index and metadata.pkl and starts empty. Triggered automatically when an embedding provider with a different width is configured. |
:135 |
| close() | Not defined. main.py:262 calls vector_store.close() on shutdown inside a try, so the resulting AttributeError is caught and logged as a warning. The final save on shutdown does not occur. |
main.py:260 |
04.3Concurrency and durability
-
The index is per process.
FAISSVectorStoreholds the index in memory and writes whole files. Running./scripts/start.sh prodstarts four uvicorn workers, each with its own copy loaded at boot and each writing the same two paths. There is no lock and no shared-memory path. -
SQLite access is synchronous and connection-per-call.
RepoManageropenssqlite3.connect(), runs one statement and closes, insideasync defmethods (repo_manager.py:189, 218, 336, 359, 423).aiosqliteis a declared dependency but is never imported. -
The embedding cache commits per row.
EmbeddingCache.set()issues anINSERT OR REPLACEand acommit()for each entry (embedding_cache.py:212). This only bites on the query path, because document embedding passesuse_cache=False. -
The sidecar is a pickle. Loading
metadata.pklreconstructsVectorDocumentinstances, so the file is bound to the class's import path and to the Python that wrote it. A failed load falls through to_init_index()(vector_store_faiss.py:108-110) — a corrupt sidecar silently empties the catalogue rather than refusing to start. -
Zoekt shards are named by directory basename.
index_name = project_path.nameat bothzoekt.py:100andzoekt.py:174. Two clones calledapishare one shard directory, andclear_index()called with no argument removes the entirezoekt/tree.
04.4Configuration surface
Settings come from a single pydantic-settings class with the prefix
COMPUTE_ and an optional .env file. A second, unrelated
set of variables is read directly from os.environ deeper in the
tree; those do not take the prefix.
| Variable | Default | Read by | Mechanism |
|---|---|---|---|
| COMPUTE_HOST | 0.0.0.0 | config.py:13 | Settings |
| COMPUTE_PORT | 8080 | config.py:14 | Settings |
| COMPUTE_WORKERS | 1 | config.py:15 | Settings |
| COMPUTE_CORS_ORIGINS | ["*"] | config.py:19 | Settings |
| COMPUTE_DATA_PATH | /app/data | config.py:37 | Settings |
| COMPUTE_INDEX_PATH | /app/indexes | config.py:38 | Settings |
| COMPUTE_MAX_CONCURRENT_TASKS | 10 | config.py:25 | Settings — logged at startup, never enforced |
| COMPUTE_EMBEDDING_MODEL | BAAI/bge-base-en-v1.5 | config.py:29 | Settings — shadowed below |
| EMBEDDING_MODEL | BAAI/bge-m3 | main.py:96 | os.environ, checked first |
| EMBEDDING_PROVIDER | local | main.py:122 | os.environ |
| EMBEDDING_API_KEY | — | main.py:123 | os.environ |
| RERANKER_MODEL | BAAI/bge-reranker-v2-m3 | reranker.py:74 | os.environ, read at class definition |
| COHERE_API_KEY | — | reranker.py:479 | os.environ, decides the reranker |
| OLLAMA_URL | http://localhost:11434 | chat.py:28 | os.environ |
| OLLAMA_MODEL | qwen2.5-coder:14b | chat.py:29 | os.environ |
| CONTEXT_MODEL | llama3.2:3b | contextual_retrieval.py:48 | os.environ, read at class definition |
| ANTHROPIC_API_KEY | — | chat.py:34 | os.environ, header takes precedence |
| GITHUB_TOKEN | — | repo_manager.py:265 | os.environ, header takes precedence |
Settings.embedding_model defaults to
BAAI/bge-base-en-v1.5, but main.py:96 reads the bare
EMBEDDING_MODEL environment variable first and only falls back to
the setting, whose own default is a third value. Setting
COMPUTE_EMBEDDING_MODEL works only when EMBEDDING_MODEL
is unset. Class-level reads in reranker.py and
contextual_retrieval.py happen at import, so those two cannot be
changed by a .env file loaded later.
04.5Declared but unused dependencies
Six runtime dependencies in pyproject.toml have no import anywhere
in the package. Three of them are the official SDKs for services the code does
call — over raw httpx instead.
- anthropic ≥ 0.21.0
- Claude is called by hand-rolled
POSTto/v1/messageswith ananthropic-versionheader (chat.py:376). - voyageai ≥ 0.3.0
- Voyage is called via
httpx.AsyncClient(base_url="https://api.voyageai.com/v1")(cloud_embeddings.py:130). - openai ≥ 1.0.0
- OpenAI embeddings likewise, through a raw client at
cloud_embeddings.py:275. - aiosqlite ≥ 0.19.0
- All four SQLite users are synchronous
sqlite3. - anyio ≥ 4.0.0
- No direct import; present transitively through Starlette regardless.
- python-multipart ≥ 0.0.6
- No endpoint accepts form data or file uploads.
cohere is the exception: it is imported and used for real
(reranker.py:37), guarded so that its absence downgrades to the
local cross-encoder rather than breaking the import.