
This blog is written by AI for SEO
Semantic Search Over Internal Documents Is Not Enough
A developer at a mid-sized tech company ships a RAG assistant intended to be the new company brain. They index thousands of pages from Notion, Jira, and Slack into a vector database. On day one, a senior engineer asks, Who is currently responsible for the database migration project? The assistant returns a three-paragraph summary of what the migration is, but it fails to name a person. The semantic search found the project overview doc, but it missed the Slack thread where the lead was assigned three days ago because the vector similarity score for that thread was slightly lower than the doc.
This failure is not about bad embeddings or poor chunking strategies. It is a fundamental architectural mismatch. Internal knowledge is not a collection of isolated text snippets. It is a dense web of relationships between people, projects, codebases, and shifting priorities. Semantic search over internal documents fails because it ignores the edges between these entities. When the answer to a query lives in the relationship between two nodes rather than inside a single text chunk, naive vector search hits a ceiling that no amount of prompt engineering can fix.
Why Internal Knowledge Disappoints Vector Search (And It's Not the Embeddings)
Most developers assume that if their RAG system hallucinates or misses context, they need a better embedding model or a more complex chunking strategy. They swap an older embedding model for something newer from OpenAI, Cohere or Voyage. They try recursive character splitting and small-to-big retrieval. It helps at the margin. The core problem stays. Internal data is fundamentally different from the public internet data these models were trained on. Public data is often self-contained. A Wikipedia article about a historical event usually contains the necessary context within its own borders.
Internal documents are different. They are highly fragmented and depend on outside context. A technical specification doc might refer to a project by a code name like Project X, while the budget spreadsheet calls it the Q3 Infrastructure Initiative. Semantic search over internal documents treats these as two distinct clusters in vector space. Unless the text explicitly links them, the vector engine sees no connection. This leads to the orphaned chunk problem. The retriever finds a relevant piece of information but lacks the relational context to verify if it is still true or who wrote it. You are not searching for similar text. You are searching for facts distributed across a graph of dependencies. A vector database is a point lookup tool in a world that requires pathfinding. This is why AI agent memory architecture must move beyond simple embeddings.
What Happens When the Answer Lives in an Edge, Not a Chunk?
Consider the query, Which services will be affected if we deprecate the legacy auth module? To answer this, an AI agent must identify the legacy auth module, find its dependencies, look up the teams that own those dependencies, and check their current project status. This is a multi-step traversal. In a standard vector setup, the LLM has to perform multiple round trips to the database. It searches for auth module, then searches for services using auth, then searches for teams. Each step introduces noise and increases the chance of a retrieval failure.
The real answer lives in the edges. In a graph-vector database like HelixDB, the auth module is a node. The services are other nodes. The relationship between them is an edge labeled DEPENDS_ON. Finding the affected services is a graph traversal, so it returns the same answer every time instead of the closest thing it could find. Relying on semantic similarity to find dependencies is a gamble. Two services might be semantically similar (both are Go microservices) but have zero functional relationship. Conversely, a frontend dashboard and a backend database might be semantically distant but tightly coupled. Vector search cannot distinguish between similarity and relationship. When you treat internal knowledge as a flat list of vectors, you lose the logical structure that makes the information useful to a human engineer.
How Do You Search When One Person Has Four Different Names?
Internal knowledge is plagued by the entity aliasing problem. A single person might be David Miller in the HR system, d.miller on GitHub, Dave in a Slack thread, and user_882 in the production logs. A vector search for David Miller will likely miss a critical bug report where he was tagged as Dave. Cross-encoder models and re-ranking can help, but they still rely on the initial retrieval set being accurate. If the top 20 chunks do not contain the alias, the re-ranker is useless.
Graphs solve this by resolving those identities to one entity node. The aliases, emails and handles become properties on that node, or their own Identity nodes hanging off it by an ALIAS_OF edge, and the mentions become edges from the systems they came from:
import {
BatchCondition, g, writeBatch, NodeRef, SourcePredicate,
} from "@helix-db/helix-db";
const link = writeBatch()
.varAs(
"person",
g()
.nWithLabelWhere("Person", SourcePredicate.eq("email", "david.miller@example.com"))
.limit(1),
)
.varAs("handle", g().addN("Identity", { system: "slack", value: "dave" }))
.varAsIf(
"edge",
BatchCondition.varNotEmpty("person"),
g().n(NodeRef.var("handle")).addE("ALIAS_OF", NodeRef.var("person"), {}),
)
.returning(["person", "handle", "edge"]);That lookup is the part to get right, because the naive version fails quietly. nWithLabelWhere returns a stream, not a promise of exactly one row. If no Person matches that email, addE receives an empty source, succeeds without creating anything, and the Identity node still commits as an orphan. If two people share the email, you get two ALIAS_OF edges. Atomicity is not the thing protecting you here: one request is one transaction, and it will commit an unattached Identity quite happily, because "every alias belongs to exactly one person" is your invariant, not the engine's. So enforce it with a uniqueness index on the lookup property, IndexSpec.nodeUniqueEquality("Person", "email"), gate the edge with varAsIf on the lookup having found something, and return all three bindings so the caller can tell an attached alias from an orphaned one.
Resolve the alias once and every mention of that person is one hop away, whichever system wrote it. That is what stops the fragmented recall a chunk-only pipeline produces, and it is what makes a scoped search possible later: once an entity is resolved, it is somewhere to start a traversal from.
How Do You Stop an Agent Answering From a Superseded Document?
Internal document stores are messy. You likely have five versions of the same onboarding guide, three different API specs for the same service, and a dozen v2_final_final.pdf files. When an AI agent performs semantic search over internal documents, it often retrieves the most semantically relevant chunk, which might be from a document deprecated three years ago. Scoring by recency is a common hack, but it is a blunt instrument. A 2023 document about the company's core values might still be valid, while a 2026 document about a specific sprint goal is already obsolete.
Recency is not a global score. It is a property of the relationship between two documents. A document node can carry a SUPERSEDES edge pointing at the version it replaced, a validUntil timestamp, or a BELONGS_TO_SPRINT relationship that tells you when it stopped mattering. You model that history yourself, with timestamped nodes and edges, the same way you would on any engine.
What makes reading it back cheap is the range index. Put one on the timestamp property and you get gt, gte, lt, lte and between, plus ordered scans in either direction, so everything written between the Q2 kickoff and the code freeze is an indexed scan over a sorted range, and the last twenty revisions of a spec is a descending scan you can stop early. Combine that with a hop across the SUPERSEDES edge and the agent reads the version that is still current, rather than the one that happened to score highest.
What the Retrieval Gap Looks Like in Practice: GraphRAG vs. Naive RAG on Multi-Hop Queries
To visualize the gap, imagine a multi-hop query: Who wrote the documentation for the service that handles payments? In a naive RAG setup, the retriever looks for payments service and documentation. It might find the payments API docs. Then the LLM has to extract the author from the text. If the author is not explicitly named in the chunk, the process stops. The LLM might hallucinate an author or say the information is missing. Every extra round trip is another chance for the right chunk not to come back, and the chain is only as good as its weakest hop.
With GraphRAG, the process is structured. The engine identifies the Payments Service node. It follows an edge to the Documentation node. It follows another edge to the Person node labeled as AUTHOR_OF. The system retrieves the exact entity, even if the person's name never appeared in the same text chunk as the word payments. This approach reduces the search space and improves accuracy. You can follow the guide on how to build a GraphRAG pipeline to see how this transition works. The difference is the shift from finding something that looks like the answer to traversing the path that leads to the answer.
The Architecture That Actually Works: Graph + Vector in One Query
The usual answer to all of this is a second database, and often a third: a vector store for the embeddings, a graph database for the relationships, your application database underneath. Every retrieval crosses all of them. The real cost there is not the latency or the invoice, it is the synchronization. Ingesting one document becomes two writes that have to succeed together, and when one lands and the other does not, your company brain is quietly wrong and nothing tells you.
HelixDB is a graph-vector database written from scratch in Rust that does graph traversal, vector ANN and BM25 full-text search in one engine and one ACID transaction. Vectors are properties on nodes and edges, so there is no second index to keep in step with the first. One write, one source of truth, and one query that traverses the relationships and ranks by similarity at the same time.
How Does HelixDB Scope a Vector Search to a Graph Traversal?
Because vectors are properties on nodes and edges, a similarity search can be chained onto a traversal instead of running against the whole index. The documented order is graph traversal, then exact candidate membership, then vector ranking, then top k. The traversal membership is authoritative: a result outside the candidate set cannot come back.
In practice that means the query starts where you know the answer lives, not in the global index:
import {
g, readBatch, defineParams, param, SourcePredicate,
} from "@helix-db/helix-db";
const params = defineParams({
team: param.string(),
query_vector: param.array(param.f32()),
limit: param.i64(),
});
const recall = readBatch()
.varAs(
"hits",
g()
.nWithLabelWhere("Team", SourcePredicate.eq("name", params.team))
.out("OWNS")
.vectorSearchWith("Document", "embedding", params.query_vector, params.limit)
.valueMap(["$id", "title", "$distance"]),
)
.returning(["hits"]);
const request = recall.toQueryRequest(
params,
{ team: "Security", query_vector: queryVector, limit: 10n },
{ queryName: "documents_for_team" },
);Two honest caveats, because the guarantee is narrower than it sounds. Exact membership does not mean the engine compares every candidate embedding one by one; approximate structures still do the ranking, and the output is validated against the traversal set. And a scoped search can return fewer than k rows, bounded by the number of unique candidates, which is the scope working rather than a bug.
The reason this is not the same as searching everything and adding a where clause is stated in HelixDB's own filtering guide: the high scorers you exclude afterwards have already consumed the source top k, so you are left with fewer eligible results than you asked for. Ask for the ten closest documents across fifty thousand and then keep only the security team's, and you can easily get none. Full-text chains the same way, with textSearchWith and a BM25 $score in the same documented order, so a hybrid query is scoped once and both halves obey it.
One thing scoping is not: it is a retrieval scope, not an authorization layer. It decides which candidates get ranked. Who is allowed to see what still belongs in your application.
What Else Comes in the Same Engine?
HelixDB also ships native MCP support, so an agent can discover the database as a tool and walk the graph itself, step by step, without a human in the loop.
Storage is a startup flag rather than a product tier. The open source build runs fully in memory, on disk, or against S3-compatible object storage, and it is the same engine, the same SDKs and the same endpoint in all three, so you can prototype in memory and ship on object storage without touching application code. The run modes are documented in full, and Helix Cloud is the managed version of the object-storage mode.
The licence matters if the company brain is something you ship rather than something you run internally. HelixDB is Apache-2.0. Among the actively developed engines a team usually shortlists alongside it, that is unusual: Memgraph and SurrealDB are both under the Business Source License, converting to Apache on a 2030 change date, and FalkorDB is under the Server Side Public License. Kuzu is MIT, but its repository has been archived since October 2025, and LadybugDB, the successor project in the same C++ lineage, is MIT and active.
Conclusion
The limitations of semantic search over internal documents are a feature of the architecture, not a bug in the models. Vector search is a powerful discovery tool, but it lacks the logical connective tissue required to understand a complex organization. As agents move from answering questions to doing work, what they need is memory that knows how things are connected, not just what they sound like. Duct-taping a vector store onto a graph database is a temporary fix that becomes permanent technical debt. HelixDB puts the graph, the vectors and full-text search in one Rust engine and one transaction. If your RAG pipeline keeps failing on basic ownership and dependency queries, stop tweaking your embeddings and start building a real knowledge graph. The repo is at github.com/HelixDB/helix-db if you want to read the engine or star it.