What you'll build: a persistent research memory bank on DataK3 that answers real questions over ArXiv papers — filtering by metadata, ranking by meaning, and citing the exact source PDF.
What you'll learn:
- How a VECTOR column (or one
recipe install) gives a bucket semantic search — no separate vector DB. - The narrow → rank → resolve retrieval loop that powers a research agent.
- How to run every step two ways — the
dodilCLI, or by prompting an agent through the DODIL MCP.
This is DataK3's reference workload. There's a working implementation —
davidmiheev/research-agent ("Atlas") — a small
Python agent on Dodil Ignite that keeps all of its long-term memory in DataK3 with semantic vector
search. This tutorial generalizes the pattern behind it.
NOTE
Connect the DODIL MCP once — see the two-minute setup. Add the dodil MCP server to Claude Code, Cursor, or VS Code and
you can drive every step by chatting. Each step shows both: the CLI tab, and an Ask your
agent tab with the exact MCP tools the agent calls and the reply you'd get back.
Problem
The end user here is a research engineer (or the agent working on their behalf) who needs to answer
precise questions over a growing pile of ArXiv PDFs — "what did 2026 cs.CL papers say about expanding
transformer context windows?" — and cite the exact source. The naive stack bolts an object store to a
separate vector DB to a separate warehouse, then hand-writes CDC glue to keep the PDF, its embeddings,
and its metadata in sync; embeddings silently drift the moment a paper is replaced, and "filter by
category then search semantics" becomes a cross-system join in app code. The payoff of doing it on
one DataK3 bucket: the embeddings live in a VECTOR column right next to the metadata (a merge-keyed
table) and the raw PDF (an object), and one question flows narrow → rank → resolve without any
inter-system plumbing — so a research agent gets durable, citable long-term memory instead of a brittle
three-system pipeline.
Prerequisites
- A DODIL organization, and either the
dodilCLI authenticated (dodil auth login) or the DODIL MCP connected to your agent. - A sample paper to ingest — e.g.
2603.07890.pdf.
Nothing else to turn on: a new bucket has its SQL/vector tables engine ready by default (there is no engine to enable), and it's created with an internal object source already wired for ingestion.
Scenario
A user asks for recent breakthroughs in "transformer context windows." A capable agent should:
- Use SQL to filter papers categorized
cs.CLand published in 2026. - Use Vector search to locate the paragraphs that actually discuss context-window expansion.
- Pull the source PDF from S3 to cite exact findings.
That single question touches structured filtering, fuzzy semantic retrieval, and heavy-payload fetch — the three things DataK3 unifies over one copy of the rows.
How the three pillars map
| Pillar | Holds | Example keys / columns |
|---|---|---|
| S3 (object store) | Raw PDFs and LaTeX source; the durable "truth" you cite from. | papers/2026/2603.07890.pdf |
| Vector | Chunked + embedded paper text. | paper_chunks.embedding VECTOR(2048) |
| SQL (table) | Publication metadata for strict filtering and rollups. | papers(arxiv_id, title, primary_category, published, s3_key) |
The agent's loop becomes: narrow with SQL → rank with Vector → resolve with S3.
Step 1 — Create the bucket
Create a DataK3 bucket called blog-research for an ArXiv memory bank.
data_bucket_createDone — created bucket blog-research (status ACTIVE). Its SQL/vector tables engine is ready and an internal object source is already wired for ingestion.
export BUCKET=blog-research
dodil data bucket create "$BUCKET" --description "ArXiv research memory bank"Step 2 — Give the bucket semantic search
A vector isn't a separate store any more — it's a VECTOR(<dim>) column on a table, right beside
your other columns. We'll keep chunked paper text in a paper_chunks table whose embedding column
holds 2048-dim jina-embeddings-v4 vectors. That's the path this tutorial searches, and the one you
control end-to-end.
In blog-research, create a paper_chunks table keyed on chunk_id with body text and a VECTOR(2048) embedding column.
data_table_createCreated paper_chunks (PRIMARY KEY chunk_id) with columns chunk_id, arxiv_id, body, embedding VECTOR(2048). Ready for jina-embeddings-v4 vectors.
dodil data table create paper_chunks -b "$BUCKET" \
--columns-json '[
{"name":"chunk_id","type":"string"},
{"name":"arxiv_id","type":"string"},
{"name":"body","type":"string"},
{"name":"embedding","type":"VECTOR(2048)"}
]' \
--merge-key chunk_idWant DataK3 to auto-embed every PDF you upload? Install the document-rag recipe
One recipe wires the auto-embed pipeline — an embed collection plus the ingest rule that feeds it — in one command, so anything you drop under the folder is chunked and embedded for you (async). It's the hands-off alternative to embedding chunks yourself.
On blog-research, install the document-rag recipe so PDFs under papers/ get embedded for semantic search.
data_recipe_installInstalled document-rag on blog-research (2 steps): added the papers_docs collection (text_embedding_index) and the papers_docs_rule ingest rule for **/*.pdf,**/*.docx,**/*.md,**/*.txt. Any doc you upload now embeds automatically.
# One command: embed collection + ingest rule, in the right order (no vector store to provision)
dodil data recipe install document-rag -b "$BUCKET" --name-prefix papers --folder papers/
# Preview exactly what it provisions first (optional)
dodil data recipe show document-ragAuto-embed runs asynchronously as objects land, which is great in production but slow to demo — so the main path above embeds a handful of chunks directly, which is instant and fully under your control.
Step 3 — Create the metadata table
Create a papers table keyed on arxiv_id, so re-inserting the same paper upserts instead of
duplicating. A PRIMARY KEY (merge-key) is required — writes are keyed, so re-runs and shard
retries upsert idempotently.
Create a papers table in blog-research keyed on arxiv_id with columns for title, primary_category, published, and s3_key.
data_table_createCreated the papers table with PRIMARY KEY (arxiv_id). Re-inserting the same paper now upserts the row instead of duplicating it.
dodil data table create papers -b "$BUCKET" \
--columns-json '[
{"name":"arxiv_id","type":"string"},
{"name":"title","type":"string"},
{"name":"primary_category","type":"string"},
{"name":"published","type":"string"},
{"name":"s3_key","type":"string"}
]' \
--merge-key arxiv_idTIP
Never write JSON null or an empty string "" into a key column on an upsert — both read back as
null and silently drop the row on the next read. Use a real value (here every paper has a real
arxiv_id), or a non-empty sentinel like "n/a".
Step 4 — Ingest a paper
Upload the PDF to the object store, embed a chunk of its text with jina-embeddings-v4 into
paper_chunks, and record the metadata row so SQL can filter it. Three writes, one bucket — no CDC glue
between them.
Upload ./2603.07890.pdf to blog-research at papers/2026/2603.07890.pdf, embed its abstract into paper_chunks with jina-embeddings-v4, and add its metadata row to papers.
data_object_create→ignite_models_embed→data_table_upsertUploaded 2603.07890.pdf (status created). Embedded the abstract (2048-dim, jina-embeddings-v4) and upserted chunk c2 into paper_chunks (wal_written: true). Upserted the papers row for 2603.07890 (cs.CL, 2026-03-22). All three are visible to reads immediately.
# (a) raw PDF → object store
dodil data object create ./2603.07890.pdf -b "$BUCKET" --key papers/2026/2603.07890.pdf
# (b) embed a chunk with jina-embeddings-v4 (2048-dim) and upsert it as a VECTOR literal
VEC=$(dodil ignite models embed jina-embeddings-v4 \
--input "Streaming attention keeps a bounded KV cache and evicts stale keys, letting the model process million-token contexts with constant memory." \
-o json | python3 -c 'import sys,json;print("["+",".join(f"{x:.6f}" for x in json.load(sys.stdin)["data"]["data"][0]["embedding"])+"]")')
dodil data table upsert paper_chunks -b "$BUCKET" \
--row "{\"chunk_id\":\"c2\",\"arxiv_id\":\"2603.07890\",\"body\":\"Streaming attention keeps a bounded KV cache...\",\"embedding\":\"$VEC\"}"
# (c) metadata row → SQL can filter it
dodil data table upsert papers -b "$BUCKET" \
--row '{"arxiv_id":"2603.07890","title":"Streaming Attention for Million-Token Contexts","primary_category":"cs.CL","published":"2026-03-22","s3_key":"papers/2026/2603.07890.pdf"}'TIP
Prefer the hands-off recipe path from Step 2? Then you only do (a) and (c) — upload the PDF and write
the metadata row; the ingest rule embeds the object for you. Kick discovery now instead of waiting for
the next scan with dodil data ingest trigger-discovery -b "$BUCKET" --source "$SRC" (the
internalSourceId from bucket create, or dodil data source list -b "$BUCKET").
Step 5 — Answer the question (narrow → rank → resolve)
SQL narrows the candidates, Vector ranks by meaning, and S3 resolves the exact citation. data vsearch
embeds the query text with the same model (jina-embeddings-v4) and returns a cosine distance
score — lower is closer.
In blog-research, find 2026 cs.CL papers about expanding transformer context windows, rank the passages by meaning, and give me a citable link to the top paper's PDF.
data_sql→data_vsearch→data_object_urlSQL narrowed to 2 cs.CL 2026 papers: 2601.01234 and 2603.07890. Ranking their chunks by meaning, the closest is chunk c2 → “Streaming Attention for Million-Token Contexts” (arXiv 2603.07890, cs.CL, 2026-03-22) at cosine distance 0.427 (c1/2601.01234 was 0.477). Citable PDF (valid 1h): https://object.uk-lon-1.dodil.io/blog-research/papers/2026/2603.07890.pdf?X-K3-Token=988d34…&X-K3-Expires=2026-09-01T09:59:40+03:00
# (a) SQL narrows the search space
dodil data sql -b "$BUCKET" \
"SELECT arxiv_id, title, s3_key FROM papers
WHERE primary_category = 'cs.CL' AND published >= '2026-01-01'
ORDER BY published"
# (b) Vector ranks by meaning (query embedded with the same jina-embeddings-v4 model)
dodil data vsearch -b "$BUCKET" -t paper_chunks --column embedding \
--text "expanding transformer context window length" \
--model jina-embeddings-v4 --metric cosine --top-k 4
# (c) S3 resolves the exact citation — a pre-signed URL for the cited PDF
dodil data object url papers/2026/2603.07890.pdf -b "$BUCKET" --expires 3600NOTE
The MCP turns a CLI gap into a strength. data vsearch doesn't take metadata filters as flags, so
on the CLI the "SQL-then-vector" handoff is done by hand (query papers first, keep the returned
arxiv_ids). You can fuse both in one SQL statement — data sql "… WHERE primary_category='cs.CL' ORDER BY embedding <=> '[…]' LIMIT 1" with the query embedding as a literal vector — but the
easiest path is to ask an agent: it just does the handoff — table query, constrain the vector hits,
resolve the PDF — from a single prompt.
Query it — one bucket, drop-in clients
The same rows answer by content (SQL) and by meaning (vector) with no ETL and no second copy — and any
Postgres/pgvector client points straight at the bucket. data connect prints the endpoints (DB name =
bucket, credential = your login token), so psql or a notebook runs the exact same
SELECT … ORDER BY embedding <=> '[…]' KNN.
dodil data connect "$BUCKET" -o psql # postgresql://…@pg.uk-lon-1.dodil.io:5432/blog-researchDataK3 vs. the three-system stack
| Concern | Three-system stack (S3 + vector DB + warehouse) | DataK3 |
|---|---|---|
| Keeping PDF, embeddings, and metadata in sync | Custom CDC/pipeline glue; embeddings drift when a paper is replaced | Object, VECTOR column, and metadata row live in one bucket |
| "Filter by category, then search semantics" | Cross-system join in app code | SQL narrows, Vector ranks, same rows |
| Citing the exact source | Separate object-store credentials + URL signing | object url presigned link in the same API |
Troubleshooting
- A recipe auto-embed job failed. If you took the recipe path, poll
dodil data ingest jobs -b "$BUCKET" -o jsonand re-trigger withdodil data ingest trigger --retry-failed. Treat a failed job as needing an explicit retry. - A KNN over a big table is slow the first time. The default request timeout is 30s; give a cold,
large
paper_chunksroom with--timeout(e.g.--timeout 120s). Once warm, queries return in well under a second. - Metadata-filtered vector search isn't a one-liner on the CLI. Run the SQL query first and keep the
returned
arxiv_ids, fuse it into onedata sql … ORDER BY embedding <=> '[…]'statement, or just prompt an agent (Step 5). - Embeddings look stale after a model change. A
VECTORcolumn is pinned to whatever model produced its vectors: switching models means re-embedding the column (recompute + upsert) so query and stored vectors share a space. Don't mixjina-embeddings-v4vectors with another model's.
Test
Verified end-to-end against a live blog-research bucket on 2026-09-01. Reads are strong
(read-your-writes) by default, so the metadata row, the embedded chunk, and the uploaded PDF are visible
immediately — no compaction needed.
export BUCKET=blog-research
# The bucket exists
dodil data bucket get "$BUCKET"
# ran: status BUCKET_STATUS_ACTIVE, internalSourceId present
# The merge-keyed metadata rows upserted (not duplicated)
dodil data sql -b "$BUCKET" \
"SELECT arxiv_id, title, s3_key FROM papers WHERE primary_category='cs.CL' AND published >= '2026-01-01' ORDER BY published"
# ran → 2 rows: 2601.01234 (Ring Attention), 2603.07890 (Streaming Attention)
# The source PDF landed in the object store and a citable URL resolves
dodil data object url papers/2026/2603.07890.pdf -b "$BUCKET" --expires 3600
# ran → https://object.uk-lon-1.dodil.io/blog-research/papers/2026/2603.07890.pdf?X-K3-Token=… (valid 1h)
# Vector search ranks the embedded passages by meaning (cosine distance, lower = closer)
dodil data vsearch -b "$BUCKET" -t paper_chunks --column embedding \
--text "expanding transformer context window length" --model jina-embeddings-v4 --metric cosine --top-k 4
# ran → c3 0.331, c2 0.427, c1 0.477, c4 0.505 — within the cs.CL-2026 set {c1,c2}, c2 (2603.07890) winsLive-run notes: the bucket, both tables, four jina-embeddings-v4 embeddings (2048-dim), the metadata
upserts, the uploaded PDF, the SQL narrow, the data vsearch rank, and the presigned object url all
ran for real and their outputs are captured above. The document-rag recipe path (Step 2 details) is
shown as code — data recipe show document-rag confirmed it provisions the collection + ingest rule
(no vector store), but its async auto-embed is slower to demo than embedding chunks directly, which is
why the searched path is the manual VECTOR(2048) column.
One-shot
Give an agent (with the DODIL MCP connected) this prompt to reproduce the whole tutorial end-to-end:
Stand up an ArXiv research memory bank on DataK3 and answer a question over it, all in one bucket.
1. Create a DataK3 bucket named blog-research (description "ArXiv research memory bank").
2. Create a paper_chunks table, merge-keyed on chunk_id, with columns chunk_id, arxiv_id,
body (string), and embedding VECTOR(2048).
3. Create a papers table, merge-keyed on arxiv_id, with string columns arxiv_id, title,
primary_category, published, and s3_key.
4. Upload ./2603.07890.pdf to key papers/2026/2603.07890.pdf. Embed its abstract with
jina-embeddings-v4 and upsert it into paper_chunks (chunk_id c2, arxiv_id 2603.07890).
Upsert the papers row: 2603.07890, "Streaming Attention for Million-Token Contexts",
cs.CL, 2026-03-22, papers/2026/2603.07890.pdf.
5. Answer: "find 2026 cs.CL papers about expanding transformer context windows, rank the
passages by meaning, and give me a citable link to the top paper's PDF." Do the handoff:
query papers (primary_category='cs.CL' AND published >= '2026-01-01'), run a data vsearch
on paper_chunks with jina-embeddings-v4, then resolve a 1h pre-signed object url for the
top PDF.
Report the top arxiv_id, its cosine distance, and the citable URL.Ship it — wrap the loop in a public app
This recipe is a query-and-retrieval loop, not a deployed service. To put it behind a URL your users — or their agents — can call, wrap the loop in an Ignite app and ship it the way every DODIL app ships: DODIL git → CI → a scanned image in the registry → a public endpoint, with versioning and one-command rollback. That full lifecycle is its own tutorial: Ship a DODIL App.
Conclusion
You built a research memory bank where a single question flows narrow → rank → resolve across one
DataK3 bucket — the embeddings sit in a VECTOR column right next to the metadata and the raw PDF, no
separate vector DB required. Every step runs the same whether you type the CLI or ask your agent.
Next steps:
- Agent Cache — cache the agent's prompt→answer pairs to cut model spend.
- Code & Dev Memory — the same memory pattern for engineering work.
davidmiheev/research-agent— the live "Atlas" implementation.