DEV Community

Cover image for VectorAI DB Doesn't Embed Your Text, and That's the Point
Greg Mate
Greg Mate

Posted on

VectorAI DB Doesn't Embed Your Text, and That's the Point

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"}}
Enter fullscreen mode Exit fullscreen mode

Get the dimension wrong and it says so plainly:

Upsert failed: Dimension mismatch for vector '': expected 1536, got 3
Enter fullscreen mode Exit fullscreen mode

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/answer already 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)

Collapse
 
crdtcto profile image
Kane Lim

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.