Building a small local knowledge base on top of Actian VectorAI DB, the actual challenge wasn't the database. It was easy to accidentally fake semantic search, and just as easy to reach for automation tools, or an LLM, in the wrong place. Here's the split that fixed it, plus the grounded answer step I added on top of search. The full build is on GitHub.
Two ways to fake it
The first pass at this stored a single note and searched for that same note. It "worked," in the sense that the top result was always right. That doesn't prove anything about semantic search: with one point in the database, of course it wins.
The other early version chained four n8n HTTP Request nodes straight into the database. That's not using n8n for anything; it's a curl command dressed up as a workflow.
Both come from the same mistake: not being clear on what each tool is actually for.
| Layer | Job | Not its job |
|---|---|---|
Embedding model (all-MiniLM-L6-v2) |
Turn text into a 384-number vector | Storage, search, answering |
| Actian VectorAI DB | Store vectors + payload, run nearest-neighbor search | Embed text, call an LLM, talk to Slack |
| n8n | Trigger a search from Slack, a form, a webhook | Own the vectors |
| Optional LLM | Answer a question using only the retrieved excerpt text | See vectors, talk to VectorAI directly |
Once that's the frame, the fix is straightforward: give the embedding model a real corpus, let VectorAI DB just be the store, and only bring n8n in as a trigger.
Proving search actually works
Six notes: five on-topic, one deliberate distractor about campus cafe hours. If a search demo can't tell "how do I start the database" from "what does the cafe serve," it isn't doing much. Real queries against the running container:
| Query | Expected top result | Score |
|---|---|---|
| How do I start the database with Docker? | Start VectorAI DB | 0.466 |
| What's the prize track at PEC Hacks? | PEC Hacks 4.0 track | 0.668 |
| How many vectors can I store for free? | Community Edition limits | 0.481 |
| Does VectorAI embed text for me? | Embeddings are your job | 0.619 |
| What does the campus cafe serve? | Campus cafe hours | 0.582 |
No cafe question beat a Docker or pricing question. These scores are cosine similarity, not percentages: treat 0.466 as "clearly the best match among the options," not "47% confident."
A brand-new note, added through the UI and searched for right away, also came back correctly. VectorAI DB's upsert blocks until the point is indexed by default, so that's a real upsert-then-retrieve, not a cached result.
The split, in practice
The embedding model does the language understanding, not the database. VectorAI DB never sees a search string; every insert and every search is a float array, and its length has to match whatever size the collection was created with:
PUT /collections/kb
Content-Type: application/json
{"vectors":{"size":384,"distance":"Cosine"}}
Get the dimension wrong and it says so plainly:
Upsert failed: Dimension mismatch for vector '': expected 1536, got 3
That error shows up fast if you swap embedding models without recreating the collection.
The payload holds the human-readable text. Each vector carries a title and text alongside it, so a search result comes back readable, not just an ID and a score to look up elsewhere.
n8n only makes sense as a trigger. What's actually built is three nodes: a webhook, an HTTP call to the app's /api/search, and a response back. On its own, that's not doing much more than curl.
The value shows up when the trigger changes and nothing else does. Swap the webhook for a Slack trigger or a form submission, and the same search logic still runs; only the front door changes. The Slack version isn't built for this demo, so there's nothing to show running yet, but that's the actual case for n8n here: it makes adding a second or third way to trigger the same search cheap.
Adding a grounded answer
Ranked hits are still excerpts, and most people want a sentence, not a list. /api/answer reuses the exact same search, then hands the top few excerpts to an LLM with one rule: answer only from what search returned, and say the knowledge base doesn't cover it rather than guess.
Worth being precise about what this is and isn't. VectorAI DB still only ever sees float vectors; it has no idea an LLM exists. The app calls search, gets back title/text payloads, and only those payloads go into the prompt, never the vectors, never anything outside them. Without an API key, the endpoint doesn't error; it returns the same ranked hits with "answer": null, so the base demo has zero dependency on an LLM being available.
The check that mattered: asking something the seeded notes don't cover, or a cafe question when the answer should come from the database, and confirming the model says it can't find that rather than inventing something plausible-sounding. If retrieval is wrong or the corpus doesn't have the answer, generation shouldn't be able to paper over that.
What the pattern generalizes to
The six-note demo is a stand-in. Swapping in a different corpus doesn't touch the plumbing:
- A team FAQ or handbook, searched from Slack instead of a browser tab.
- Support tickets, where the payload is the past resolution instead of a note body.
- A product catalog or pricing sheet, so "what's included in the Pro tier" finds the right snippet even if it's worded differently.
- Agent memory: chat turns as points, search before answering.
- Grounded answers on any of the above:
/api/answeralready does this for the six-note demo, and the same prompt-and-refuse pattern carries over.
Different corpus, sometimes a different trigger. Same architecture.
One gotcha worth knowing
Each point's ID is a hash of the note's text, so re-saving a note with the same wording overwrites the same point. Edit the wording, even slightly, and it's treated as a new point: the old version doesn't get cleaned up, it just sits there, still searchable, still a possible top result. Decide early whether edits should replace or version, and delete the stale point explicitly if you want replace.
This is small and local on purpose. It hasn't been pushed anywhere near the 5,000-vector ceiling on Community Edition, and there's no auth or TLS in front of it, so it's not production-ready as-is. Fork the repo, swap the corpus, pick a trigger, and the pattern holds.
Top comments (1)
This is a strong example of keeping the RAG stack honest by giving each component one clear responsibility.
The most important distinction here is that vector search is retrieval, not understanding. Your embedding model converts text and queries into the same vector space; VectorAI DB performs the nearest-neighbor search; the payload preserves the source text; and the LLM, when present, operates only on retrieved evidence. That separation makes the system much easier to reason about and debug.
I especially like the deliberate distractor in the evaluation corpus. A semantic-search demo with one document can hide retrieval failures, while even a small adversarial corpus forces you to validate whether the embedding space actually separates relevant from irrelevant content. Also, calling cosine similarity a ranking signal rather than a confidence percentage is exactly right.
The /api/answer design is another important detail. Retrieval should establish the evidence boundary before generation begins. If the corpus doesn't contain the answer, the generator should abstain instead of converting retrieval uncertainty into a confident hallucination. In a larger system, I'd take that one step further with measurable retrieval metrics such as Recall@k/MRR, minimum similarity thresholds, and an evaluation set containing both answerable and unanswerable queries.
The stale-vector issue from content-hash IDs is also worth highlighting. Once documents become mutable, I'd generally separate document identity from content identity and maintain explicit versioning/deletion semantics. Otherwise, a seemingly harmless edit can silently create duplicate historical knowledge that continues participating in retrieval.
And I agree with your n8n point: orchestration only becomes valuable when it actually orchestrates something. Keeping the retrieval service independent from the trigger means Slack, webhooks, forms, scheduled jobs, or other interfaces can reuse the same deterministic search layer.
Overall, this is a very practical architecture: embed → retrieve → constrain evidence → generate → evaluate. The simplicity is actually the strength.
If you're interested in discussing production-grade RAG architecture, evaluation, or building this into a reusable knowledge platform, I'd be happy to connect for a longer-term technical exchange.