Skip to content
曾亮懿 ZengLiangYi Blog

From RAG to Knowledge Graphs: Preserving Context in Retrieval

Compare keyword search, vectors, and knowledge graphs. Trace one-hop candidate expansion in ChatCrystal and identify boundaries relationships cannot prove.

曾亮懿

曾亮懿

5 min read
Table of contents Show Hide

“I remember discussing this, but cannot find it” is a familiar problem when organizing AI conversations. Finding similar content raises another question: is this note a solution, a prerequisite, or a conclusion that was later overturned?

Vector search and note graphs each address part of these problems. They can work together, but they do not form a simple upgrade path in which vectors obsolete keywords and graphs obsolete RAG. This article examines their connection and limits in ChatCrystal at commit 7642768.

Separate retrieval from generation

Approach Useful question Boundary to consider
Literal matching Which record contains this error code, symbol, or path? Different wording can hide relevant results
Full-text retrieval Which documents match these terms, and how should they rank? Tokenization, language, and ranking configuration matter
Vector search Which content is nearby in the representation space? Similarity establishes neither correctness nor causality
Relationship graph Which connections between notes have been recorded? Edges can be missing, outdated, or incorrectly extracted
RAG How can retrieved material support a generated answer? Context assembly, generation, and evidence checks remain necessary

The original RAG paper combines retrieved external information with language generation. Returning vector-search results alone does not perform that generation step. Graph relationships can also contribute material for generation.

Keyword search remains useful for locating ECONNREFUSED or a function name exactly. Full-text retrieval should not be reduced to mechanical matching either: its capabilities depend on tokenization, synonyms, and ranking configuration. In particular, an arbitrary default tokenizer cannot be assumed to segment Chinese as intended.

A search for “memory leak investigation” may surface OOM errors, unreleased objects, and large file batches. These are related clues, not synonyms. An OOM can result from a legitimate working set exceeding its memory budget; proximity between notes does not establish a leak.

Vector retrieval supplies candidates when the original wording is unknown. Reading them is still necessary to establish whether their environment, trigger, version, and validation method apply to the current problem.

ChatCrystal assembles selected note fields into index text, chunks it, generates vectors, and embeds queries for retrieval. The vector path in its Vectra LocalIndex scores candidates by similarity; it should not be described as an HNSW or LSH index. See local vector database selection for the storage decision.

The application also boosts literal matches, filters low scores, and deduplicates direct results by note. A literal match can raise a result’s score, so returned score values are not always raw vector similarities.

This graph connects notes

ChatCrystal’s graph primarily connects notes. It does not automatically extract every function, module, and call relationship from a codebase. That distinction determines which questions it can help answer.

Its relationship types include CAUSED_BY, LEADS_TO, RESOLVED_BY, SIMILAR_TO, CONTRADICTS, DEPENDS_ON, EXTENDS, and REFERENCES. Some have direction. Consider this constructed example:

Note A: Cache invalidation triggers duplicate requests
Note B: Share a Promise to collapse concurrent requests

A --RESOLVED_BY--> B

Following the edge backward can reveal which problem B solves, but it does not mean B is resolved by A. Likewise, similarity expressed by SIMILAR_TO cannot replace the causal evidence required by CAUSED_BY.

Automatically discovered edges are candidate conclusions

The relationship-discovery implementation first finds candidates through semantic search, falling back to recent notes when no candidates are available. It passes candidate information and new-note content to a language model, constrains relationship types, and filters target IDs and confidence.

It therefore does not compare a new note with every existing note. When candidate retrieval misses a record, the model never gets to assess that connection. The implementation also truncates summaries to limit prompt size, so important conditions may be absent from the model’s context.

Model-reported confidence is a filtering signal, not a calibrated probability of truth. Even a high-scoring CAUSED_BY edge needs checking against the notes and original material. A graph makes connections visible while adding hypotheses that need maintenance.

Current search expansion is one hop

The semantic-search implementation first obtains direct note matches. With relationship expansion enabled, it follows adjacent edges, adds eligible linked notes, and discounts their scores.

The loop visits direct results; it does not recursively expand newly added neighbors. This is direct retrieval plus one-hop expansion, not an arbitrary multi-hop reasoning engine. Linked results carry a viaRelation marker, but that marker alone does not reconstruct the complete directed edge; fetch relationship records when needed. Expanded results have an empty chunkText, so their content also needs a separate fetch.

The entry point matters too. The HTTP search endpoint enables expansion with expand=true. The MCP tool search_knowledge exposes only query and limit and currently calls search without expansion. By contrast, recall_for_task controls it through options.include_relations, which defaults to enabled.

Question
↓ generate query vector
Direct note matches
↓ optional one-hop expansion
Direct matches + linked notes
↓ caller reads and selects
Context for answer generation

The final step belongs to the client or agent workflow using the results. A retrieval service returning material does not imply that every search also calls a language model to generate an answer.

Evaluate whether expansion helps

Start with a small set of tasks whose answers you know. For each task, compare direct retrieval with relationship expansion. Record whether expansion finds a necessary prerequisite, introduces irrelevant notes, and increases reading time.

Include cases where a relationship helps, where it is outdated, and where similar notes reach opposite conclusions. Evaluate whether the material improves the current decision rather than counting graph edges.

If answers are generated from those results, also check whether they retain sources, present hypotheses as facts, and acknowledge insufficient evidence. Observe retrieval and generation quality separately; one fluent answer does not establish that the whole system is correct.

In a personal knowledge base, keywords support exact lookup, vectors discover candidates, and relationships help inspect context. See agent knowledge-base design for using that material and recording verified lessons, or local-first data boundaries for the model-call data flow.

Share:

Related Posts

Semantic Search: Candidate Deduplication and Graph Context

Trace query vectors, candidate growth, lexical boosts, and relation expansion in ChatCrystal, distinguishing list filtering from semantic retrieval.

曾亮懿 曾亮懿
3 min read
#ChatCrystal #Semantic search #RAG

AI Conversation Formats: JSONL Events vs SQLite Snapshots

Compare 5 AI adapter formats: separate JSONL events from SQLite keys, and inspect indexed, orphan, and unknown-version chat transcripts in memory stores.

曾亮懿 曾亮懿
4 min read
#ChatCrystal #Node.js #SQLite

How Claude Code, Cursor, Copilot, and Trae Store Chats on Disk

Compare Claude Code, Cursor, Copilot, and Trae storage paths. Distinguish raw copies from exports, and verify migration retention with test manifests.

曾亮懿 曾亮懿
4 min read
#ChatCrystal #Node.js #SQLite

From writing to code

Explore ChatCrystal, developer tools and daily learning notes on my GitHub. Questions about the implementation are welcome.