Skip to main content
AI coding agents work better when they can search a codebase semantically (“find the authentication logic”) rather than just by filename or grep. A code index backed by Turso can combine full-text search over identifiers with vector similarity search over embeddings, all in a single embedded database. This guide covers the schema and queries for building a code indexer with both FTS and vector search.

Schema

How it fits together

  • codebases registers each project root. A single database can index multiple codebases.
  • chunks stores individual semantic units extracted from source files — functions, structs, classes, impl blocks, etc. Each chunk has a name, signature, code snippet, line range, and optionally a vector embedding. The chunk_key is a unique identifier (e.g. file_path::kind::name) for upsert operations.
  • indexed_files tracks which files have been indexed and their content hashes, enabling incremental re-indexing — only changed files are re-processed.

Connecting

The FTS index features require an experimental flag at connection time:
The experimental: ["index_method"] flag enables Turso’s USING fts index syntax and the fts_match() / fts_score() functions.

Indexing files

Upserting chunks

When a file is parsed, upsert its chunks. Setting embedding = NULL on conflict forces re-embedding when content changes:

Tracking indexed files

Skipping unchanged files

Before parsing a file, check if it has changed:
If the hash matches, skip it entirely.

Storing embeddings

Embeddings can be stored as int8-quantized vectors using Turso’s vector8() function, which reduces storage from 1,536 bytes (float32, 384 dims) to 395 bytes:
The parameter for vector8() is a JSON-stringified float array. Turso handles the quantization internally. Find chunks that need embedding:
Turso provides an FTS index with weighted BM25 scoring that is separate from SQLite’s fts5 virtual tables.

Creating the FTS index

Create an FTS table per codebase and populate it from the chunks:
The weights parameter controls BM25 scoring — here, matches in the function/type name are weighted 5x and signature matches 3x.

Searching with FTS

Then fetch the full chunk data:
For semantic/natural language queries, use vector cosine distance:
The score is 1 - distance (cosine similarity from cosine distance). For the best results, combine both approaches using Reciprocal Rank Fusion (RRF). Run the FTS and vector queries separately, then merge results in application code:

Removing stale files

When files are deleted from the codebase, clean up their chunks and records:
After removing stale data, rebuild the FTS table to keep the index consistent:

Key design points

  • Incremental indexing via file hashes means only changed files are re-parsed and re-embedded, making updates fast even on large codebases.
  • Two search modes — FTS for identifier/keyword lookup and vector search for semantic/natural language queries — cover different use cases. Hybrid search combines both.
  • Weighted FTS (name=5.0, signature=3.0) biases results toward function and type names, which is what developers usually search for.
  • Int8 quantized vectors via vector8() reduce storage by 75% compared to float32 with minimal impact on search quality.
  • Everything runs in a single embedded database file with no external services.

Example

codemogger is an MCP server that implements this pattern as a code indexer for AI coding agents.