Register now for early access to concurrent writes in the Turso Cloud. Join the waitlist

Guest Post

Building a Retrieval Pipeline with Turso and Voyage AI

A retrieval-plus-memory loop for AI agents that collapses the usual multi-service RAG stack into one embeddable Turso database and the Voyage AI API.

Cover image for Building a Retrieval Pipeline with Turso and Voyage AI

Modern AI systems are converging on a pattern: each agent, workflow, or tenant owns its own local state instead of contending for rows in one shared database. That is hard to serve with a conventional client-server database, where scaling means giving a single instance more CPU and more replicas. It maps cleanly onto Turso, where a database is a file that can be created on demand, replicated to the edge, or synced onto a device. Turso is SQLite-compatible and ships native vector search, so embeddings live in the same table as your relational data.

But state is only half of what an agent needs. The other half is the ability to find the right thing in a pile of documents, messages, and past decisions, and that comes down to retrieval quality. This is where Voyage AI fits. Voyage builds the embedding and reranking models that decide what an agent sees, and they lead the Retrieval Embedding Benchmark (RTEB) across general, multilingual, and domain-specific tasks. Retrieval accuracy is not a rounding error: on RTEB, the gap between the strongest and weakest models runs close to 50%, and that gap is the difference between an agent that grounds its answers and one that hallucinates.

Put them together and you get a retrieval-plus-memory loop that collapses the usual multi-service RAG architecture into one embeddable database and one retrieval API:

This post walks through the data model, the embedding and search path, reranking, the agent memory loop, and the performance and cost characteristics that make the combination work, using the Voyage AI Embedding and Reranking API (endpoint https://ai.mongodb.com/v1) as the model layer.

#Turso and Voyage AI

Turso gives you storage where vectors, relational data, and agent state live in the same database:

  • Native vector search, no extension to install, built into the engine.
  • File-shaped databases that fit per-agent, per-tenant, and per-workflow patterns, create thousands cheaply instead of contending on one instance.
  • Embedded replicas for microsecond, offline-capable reads on-device or at the edge.
  • SQLite compatibility, so your vectors sit next to the WHERE-clause columns you already query.

Voyage AI gives you the retrieval accuracy that determines whether the right context reaches the model:

  • Best-in-class accuracy. State-of-the-art results on RTEB across general, multilingual, code, finance, and legal retrieval.
  • A shared embedding space (Voyage 4 series). voyage-4-large, voyage-4, voyage-4-lite, and the open-weight voyage-4-nano all produce compatible embeddings. You can index your corpus once with voyage-4-large for maximum fidelity and run high-volume queries with voyage-4-lite for lower latency and cost, no re-indexing, no second copy of your data. That asymmetric setup is a natural fit for agent memory, where reads dwarf writes.
  • Storage that scales down, not just up. Every Voyage 4 model supports Matryoshka dimensions (256 / 512 / 1024 / 2048) and quantization (int8, binary). Dropping from 2048 to 512 dimensions, or from float32 to binary, shrinks what Turso has to store and scan by 4–32x, and because Turso stores vectors as typed blobs (F32_BLOB, F16_BLOB, 1-bit with a Hamming metric), you can match the model's output directly to a cheaper column type. Retrieval cost is dominated by vector storage and scan; this is the lever that controls it.
  • Specialized models when you need them. voyage-context-4 embeds each chunk with its surrounding document context for long, chunked corpora; voyage-code-3 for repositories; voyage-multimodal-3.5 for interleaved text, images, and video; plus finance and legal models.
  • Instruction-following reranking. rerank-2.5 (and the faster rerank-2.5-lite) re-score query–document pairs for a precision second stage, and accept natural-language instructions in the query, for example, "prefer documents that cite primary sources", so you can steer relevance without retraining anything.

The clean mental model: Voyage decides what's relevant; Turso stores it, remembers it, and serves it fast.

#Data Modeling and Schema Design

Turso supports vector columns natively. A vector column is a typed BLOB under the hood, declared alongside ordinary SQL columns and manipulated through built-in vector functions:

CREATE TABLE documents (
    id         TEXT PRIMARY KEY,
    content    TEXT NOT NULL,
    -- F32_BLOB dimension must match voyage-4 default of 1024 dims
    embedding  F32_BLOB(1024) NOT NULL,
    tags       TEXT,
    created_at INTEGER
);

-- DiskANN approximate-nearest-neighbor index for scalable search
CREATE INDEX documents_idx
    ON documents (libsql_vector_idx(embedding, 'metric=cosine'));

Key points:

  • tags and created_at are plain columns you can filter on with normal WHERE clauses alongside a vector search, no join and no separate metadata store.
  • The F32_BLOB(1024) dimension must match the dimension your embedding model returns (see output_dimension below). If you use Matryoshka to store 512-dim vectors, declare F32_BLOB(512).
  • The libsql_vector_idx index is what makes nearest-neighbor search scale; without it, Turso falls back to an exact linear scan, which is fine for small tables but not for a growing corpus. The DiskANN index is currently available in libSQL, the SQLite fork that preceded the Turso rewrite; exact scan runs everywhere.

#Embedding Generation with Voyage AI

Call the Embedding and Reranking API's /v1/embeddings endpoint. Authenticate with a model API key using the Bearer format:

import json
import requests

VOYAGE_BASE = "https://ai.mongodb.com/v1"
# created in the Atlas console
MODEL_API_KEY = "YOUR_MODEL_API_KEY"
HEADERS = {
    "Authorization": f"Bearer {MODEL_API_KEY}",
    "Content-Type": "application/json",
}

payload = {
    "model": "voyage-4-large",
    "input": ["Turso ships native vector search out of the box"],
    # use "document" when embedding content to store
    "input_type": "document",
    # Matryoshka: 256 / 512 / 1024 / 2048
    "output_dimension": 1024,
    # or "binary" to cut storage further
    "output_dtype": "float",
}

resp = requests.post(
    f"{VOYAGE_BASE}/embeddings", json=payload, headers=HEADERS
)
resp.raise_for_status()

vector = resp.json()["data"][0]["embedding"]

A couple of details worth calling out:

  • input_type matters. Set it to "document" when embedding content you're storing and "query" when embedding a search query. Voyage prepends the right retrieval-oriented prompt internally, which measurably improves accuracy. Don't omit it.
  • output_dimension and output_dtype are your cost knobs. Lower dimensions and quantized dtypes shrink storage and speed up scans in Turso, with minimal accuracy loss thanks to Matryoshka and quantization-aware training. Just keep the Turso column type in sync.
  • The returned embedding is a plain list of floats. Insert it with Turso's vector32() function, which converts a JSON-style array literal into the blob storage format:

The vector returned is a plain list of floats. Insert it using Turso's vector32() function, which converts a JSON-style array literal into the BLOB storage format:

INSERT INTO documents (id, content, embedding, tags, created_at)
VALUES (?1, ?2, vector32(?3), ?4, ?5);

Where ?3 is bound to a JSON string like '[0.01, -0.02, ...]' (i.e. json.dumps(vector) on the Python side).

#Vector Search Query

Query the DiskANN index with the vector_top_k table-valued function, which returns the k approximate nearest neighbors by rowid:

Nearest-neighbor search in Turso goes through a distance function like vector_distance_cos():

# voyage-4-lite: cheaper query-side model, same embedding space
q = requests.post(
    f"{VOYAGE_BASE}/embeddings",
    json={"model": "voyage-4-lite",
          "input": ["How does Turso handle concurrent writes"],
          "input_type": "query",
          "output_dimension": 1024},
    headers=HEADERS,
).json()["data"][0]["embedding"]
SELECT d.id, d.content
FROM vector_top_k('documents_idx', vector32(?1), 20) AS knn
JOIN documents d ON d.rowid = knn.id;

Because the vectors live in the same table as everything else, you can combine similarity search with ordinary filters. For small tables you can even skip the index and filter inline:

SELECT id, content
FROM documents
WHERE tags LIKE '%support%'
ORDER BY vector_distance_cos(embedding, vector32(?1))
LIMIT 20;

Note the asymmetric setup: documents were embedded with voyage-4-large, the query with voyage-4-lite. Because both live in the Voyage 4 shared embedding space, the vectors are directly comparable, you get large-model index quality with lite-model query economics.

#Reranking with Voyage AI

A first-stage vector search optimizes for recall; a reranker optimizes for precision. Pass the query and the top candidates to /v1/rerank and let rerank-2.5 re-score them before they reach the LLM:

rerank = requests.post(
    f"{VOYAGE_BASE}/rerank",
    json={
        "model": "rerank-2.5",
        # instruction-following query
        "query": "How does Turso handle concurrent writes? "
                 "Prefer documents that describe the MVCC engine.",
        "documents": [row["content"] for row in results],
        "top_k": 5,
    },
    headers=HEADERS,
).json()

# results sorted by relevance; each item has an
# index into the input documents
top = [results[item["index"]] for item in rerank["data"]]

Rerankers are specialized models that take the query and the document and determines a score for each pairing. This enables the application to rank the results and only feed the most relevant results to the LLM before generating a response. The reranker reads the query and each document together (cross-attention), so it catches relevance that pure vector similarity misses. The instruction embedded in the query above steers the ranking without any retraining (for example, "prefer documents that cite primary sources"). Only the top few reranked documents go into the prompt, which keeps context tight and generation grounded.

#The Agent Memory Loop

Putting it together, a single agent turn looks like this:

  1. Agent receives a query.
  2. Embed the query with Voyage (input_type="query").
  3. Retrieve nearest neighbors from Turso via vector_top_k.
  4. Rerank the candidates with Voyage rerank-2.5.
  5. Send the top results as context to the LLM.
  6. LLM produces output.
  7. Embed and write new memory/state back to Turso (input_type="document").
  8. Index the new knowledge so the next turn can retrieve it.

State and knowledge both live in the same Turso database; Voyage supplies the accuracy on the way in and the way out.

#MVCC and Concurrent Writes

One of the main innovations of the Turso engine over SQLite is the ability to support concurrent writes. That happens through an MVCC mode that lifts SQLite's single-writer limitation. It needs to be enabled as a journal mode, and transactions are explicitly marked as concurrent:

PRAGMA journal_mode = 'mvcc';

BEGIN CONCURRENT;
INSERT INTO documents (id, content, embedding, tags, created_at)
VALUES (?1, ?2, vector32(?3), ?4, ?5);
COMMIT;

MVCC mode allows writes to different rows without any conflicts. Conflicting concurrent writes happen if they happen in the same row, and surface as SQLITE_BUSY at commit time (a write-write conflict), and your application should retry the transaction.

In practice, embedding updates are not expected to touch the same row. MVCC mode is recommended to allow the database to ingest at high throughput.

There is one caveat, and it ties back to the schema we started with. MVCC is the newest part of the engine, and the DiskANN vector index (the vector_top_k path) does not run under it yet. In practice you pick per workload: enable MVCC for high-throughput concurrent ingest and search with an exact vector_distance_cos() scan (the brute-force query shown earlier, which needs no index), or keep the DiskANN index for approximate search and ingest without concurrent writes. For agent memory, where each corpus is small, file-shaped, and write-heavy, exact scan under MVCC is often the right default, and it costs little at that scale. The two paths are converging as the engine matures.

#Performance Considerations

  • Latency. Embedding generation is usually the slowest step, not the database query. Batch embedding calls, the /v1/embeddings endpoint accepts a list of inputs in one request (up to 1,000).
  • Storage and scan cost. This is where the pairing pays off. Use Voyage's Matryoshka dimensions and quantization to store smaller vectors, and match them to Turso's typed vector columns (F32_BLOB, F16_BLOB, or 1-bit with a Hamming metric). Cutting 2048-dim float32 down to 512-dim, or to binary, is a 4–32x reduction in what Turso stores and scans, with little accuracy loss.
  • Shape your databases. File-shaped Turso databases favor many small, fast per-agent or per-tenant databases over one shared instance under contention. Embedded replicas push reads to the edge or on-device.
  • Split model roles. Index with voyage-4-large, query with voyage-4-lite, rerank with rerank-2.5, accuracy where it counts, economy where it scales.

#Why This Architecture Works

Turso keeps vectors, relational data, and agent state in one file-shaped, SQLite-compatible database with native ANN search and edge/on-device reach. Voyage supplies the retrieval accuracy: a shared embedding space, dimension and quantization control that directly lowers storage cost, specialized models, and instruction-following reranking. Together they form a retrieval-plus-memory loop that's simple to reason about and fast enough for real agent workloads, without a separate vector service to run and sync.

#Get Started

You can build the full loop above in a few minutes:

  1. Create a Turso database. Sign up and spin one up from the Turso dashboard, then create your table and DiskANN vector index.
  2. Get a Voyage model API key. Generate one in the MongoDB Atlas console, you get 200 million free tokens on the latest models to start, with simple token-based pricing after that. Point your client at https://ai.mongodb.com/v1.
  3. Clone the starter. Grab the turso-voyage-agent-memory starter repo, a runnable reference app that embeds, indexes, searches, reranks, and writes memory back, wired exactly as shown here.
  4. Ship it. Swap in your own corpus, tune output_dimension and the model split for your accuracy/cost target, and deploy.