Register now for early access to concurrent writes in the Turso Cloud. Join the waitlist
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.

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 gives you storage where vectors, relational data, and agent state live in the same database:
WHERE-clause columns you already query.Voyage AI gives you the retrieval accuracy that determines whether the right context reaches the model:
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.The clean mental model: Voyage decides what's relevant; Turso stores it, remembers it, and serves it fast.
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.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).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.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:
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).
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.
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.
Putting it together, a single agent turn looks like this:
input_type="query").vector_top_k.input_type="document").State and knowledge both live in the same Turso database; Voyage supplies the accuracy on the way in and the way out.
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.
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.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.
You can build the full loop above in a few minutes:
output_dimension and the model split for your accuracy/cost target, and deploy.