Most AI agents fail on data, not on the model. They retrieve the wrong document, leak a record the user should not see, or let a language model invent a number that sounds right. We built eight hands-on NebulaDB Labs and a working Visa AI Agent to show how to get that layer right. This post walks through what we built, what we measured, and what we learned. The full technical guide has every query, plan and result.
The idea: put the agent's data, search and model calls in one place
Cloud databases such as Google's AlloyDB AI have made a clear case: an AI application is easier to build when the database does the vector search, the embeddings and the model calls, close to the operational data. We agree with the idea. NebulaDB is our open-source take on it, a single Rust binary you can run on your own hardware:
- Documents with metadata, embedded on write by a pluggable embedder (we run
nomic-embed-text, 768 dimensions). - Vector, keyword (BM25) and hybrid search through one endpoint.
- SQL with meaning:
semantic_match(text, '…')next to ordinary filters such asmin_age <= 55, over REST or the Postgres wire protocol. - An LLM gateway (
/api/v1/ai/chat) that routes chat, reasoning and summarising to the model you configure. - An MCP server, so agents such as Claude can call SQL, search and RAG as tools.
Our production instance holds 8.3 million documents. The Labs run on that same server, each in its own small bucket.
Eight Labs, eight businesses, one database
Each Lab teaches one idea in two to five minutes and runs against a real NebulaDB. There are no recordings and no canned numbers. You pick a business "pack" (a school trust, a care group, a logistics team chat, a newsroom, a B2B supplier, small UK businesses with tax notes, Indian farmers asking in Hindi and Punjabi, or a Thai visa agency) and the Lab seeds that pack's documents on first use. All the data is invented.
- Meaning vs words: the same question through BM25, vector and hybrid search, side by side.
- Model shoot-out: hit@1, hit@5 and MRR for each retriever over a labelled question set.
- Filters that matter: semantic search plus metadata filters in one SQL statement, with
EXPLAIN ANALYZE. - RAG, step by step: safety rules, then retrieval, then numbered sources, then the same model asked twice, once without the sources and once with them.
- Reason over the data: a multi-step plan in which every step is a live SQL query.
- When to say no: refusal rules, per-user access filters and relevance thresholds, scored on a confusion matrix.
- Scale it up: an HNSW
efsweep that trades latency against agreement. - See the space: embeddings projected to 2-D in your browser.
What we measured: hybrid search is not always the winner
On 8 November 2026 we ran every pack's labelled questions against production NebulaDB and recorded hit@1, the share of questions whose correct document came back first:
| Business pack | BM25 | Vector | Hybrid |
|---|---|---|---|
| School trust and university | 59% | 74% | 89% |
| Care homes | 62% | 88% | 73% |
| Thai visa agency | 42% | 71% | 65% |
| Small businesses and tax | 58% | 62% | 77% |
| Farmers (English, Hindi, Punjabi) | 42% | 32% | 48% |
Two lessons came out of this. First, the best retriever depends on the business: hybrid search won for the school and the tax pack, while plain vector search won for care homes and visas. Measure before you choose. Second, an English-centric embedder struggles with Hindi and Punjabi, and no amount of prompt engineering fixes that. The fix is a multilingual embedder. These question sets are small (26 to 55 questions each), so treat them as smoke tests, not benchmarks.
The Visa AI Agent: let code decide, let the model write
The Visa AI Agent answers questions for "Lotus Gate", a fictional Thai visa agency, using 138 documents: 32 visa-type entries across 21 routes, plus rules, processes, fees, offices and private client case notes. Its design rule is simple: the language model never decides what to query. Small local models do not follow tool-calling instructions reliably, so plain code does four jobs:
- It reads the applicant's age, savings, income and purpose from the message.
- It runs the safety rules first. A request for a fake bank statement gets a fixed reply, and no model is called.
- It chooses the tools and writes the SQL itself, such as this real Visa Finder query:
SELECT * FROM lab_visa
WHERE semantic_match(text, 'I''m 55, British, with 900k THB savings and want to retire in Chiang Mai')
AND kind = 'visa_type'
AND min_age <= 55
AND min_funds_thb <= 900000
AND array_contains(purpose, 'retire')
LIMIT 200
On production this returned exactly three routes in about 100 ms: the 90-day Non-O for retirement, the one-year Non-O-A, and Thailand Privilege. The visa cards on screen are built from those SQL rows, not from the model's prose.
- It checks the answer. Every number in the model's reply must appear in a source.
That last check earned its place. With the production 3-billion-parameter model, one answer recommended the right visa but also "worked out" a monthly income of 75,000 THB by dividing the savings by 12. No source says that. Across four runs, two contained a figure that appears in no source, and the check flagged both. Given the same sources, a 30-billion-parameter model wrote a cited, correct comparison of five routes. The SQL, the cards and the guards were identical in every run. Only the writing quality changed.
Private data stays in the database
An unfiltered hybrid search for "I want to retire in Chiang Mai, I am 55" returned, at rank two, a private case note about another client. Retrieval is about relevance, not permissions. In NebulaDB the fix is a filter inside the query, AND client_id = 'LG-1042', so rows outside the signed-in user's scope never leave the database. Anonymous visitors only ever see public documents. We learned this the hard way: the agent's first rule search was plain hybrid search, and we caught that case note in a guest's evidence while writing this post. One client_id = 'public' predicate fixed it.
When the government changes the rules
Visa rules change often, and that is the strongest argument for RAG over fine-tuning. The RAG Lab's "The rules changed" panel re-ingests a simulated change to an official page, under the same document id, and asks the question again. The next answer cites the new rule, and no model is retrained. In production this becomes a scheduled crawler. It hashes each official page, upserts the changed text with its source and as_of date, and lets the agent cite the update on the next question.
Try it, then build your own
- The Labs: showcase.nebuladb.net/#/labs. Sign in with a Google account.
- The Visa AI Agent: showcase.nebuladb.net/#/tab/visaagent.
- The code: github.com/bwalia/nebuladb, including the Labs under
apps/showcase/src/labs/. - The deep dive: Building AI Agents with NebulaDB: the technical guide.
Want an agent like this over your own documents, with your own access rules, on hardware you control? Talk to us and we will start with a Lab-style pack of your data and measure retrieval before writing a single prompt.
The visa content in the demo is fictional and simplified. Rules change often, so verify with Thai Immigration or a Thai embassy. This is not legal advice.