---
title: "From RAG to Knowledge Graphs: Preserving Context in Retrieval"
description: Compare keyword search, vectors, and knowledge graphs. Trace one-hop candidate expansion in ChatCrystal and identify boundaries relationships cannot prove.
image: "https://zengliangyi.com/og/blog/en/rag-and-knowledge-graphs.png"
---

“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](/en/projects/chatcrystal) at commit [`7642768`](https://github.com/ZengLiangYi/ChatCrystal/tree/764276814e5c5a33ecbfeae63754f74b9261a3a8).

## 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](https://arxiv.org/abs/2005.11401) 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.

## Related meaning does not imply the same cause

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](/en/blog/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:



```text
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](https://github.com/ZengLiangYi/ChatCrystal/blob/764276814e5c5a33ecbfeae63754f74b9261a3a8/server/src/services/relations.ts) 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](https://github.com/ZengLiangYi/ChatCrystal/blob/764276814e5c5a33ecbfeae63754f74b9261a3a8/server/src/services/embedding.ts) 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](https://github.com/ZengLiangYi/ChatCrystal/blob/764276814e5c5a33ecbfeae63754f74b9261a3a8/server/src/routes/notes.ts) 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.



```text
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](/en/blog/agent-knowledge-memory-loop) for using that material and recording verified lessons, or [local-first data boundaries](/en/blog/local-first-ai-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.          曾亮懿       October 3, 2026      3 min read

#ChatCrystal

#Semantic search

#RAG](/en/blog/semantic-search-workflow)    [##  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.          曾亮懿       October 7, 2026      4 min read

#ChatCrystal

#Node.js

#SQLite](/en/blog/ai-conversation-format-divergence)    [##  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.          曾亮懿       October 7, 2026      4 min read

#ChatCrystal

#Node.js

#SQLite](/en/blog/ai-conversation-management-options)

##  From writing to code

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

[Visit my GitHub](https://github.com/ZengLiangYi)

```json
{
  "@context": "https://schema.org",
  "@type": "WebSite",
  "name": "ZengLiangYi Blog",
  "alternateName": [
    "Zeng Liangyi",
    "ZengLiangYi",
    "zengliangyi.com"
  ],
  "url": "https://zengliangyi.com",
  "description": "曾亮懿的个人网站。记录 AI Native 探索、开发者工具与全栈产品实践。"
}
{
  "@context": "https://schema.org",
  "@type": "BlogPosting",
  "headline": "From RAG to Knowledge Graphs: Preserving Context in Retrieval",
  "description": "Compare keyword search, vectors, and knowledge graphs. Trace one-hop candidate expansion in ChatCrystal and identify boundaries relationships cannot prove.",
  "url": "https://zengliangyi.com/en/blog/rag-and-knowledge-graphs/",
  "image": "https://zengliangyi.com/images/blog/rag-and-knowledge-graphs.svg",
  "datePublished": "2026-06-05T00:45:00.000Z",
  "dateModified": "2026-10-23T00:00:00.000Z",
  "author": {
    "@type": "Person",
    "name": "曾亮懿",
    "url": "https://zengliangyi.com/en/about"
  },
  "publisher": {
    "@type": "Organization",
    "name": "ZengLiangYi Blog",
    "logo": {
      "@type": "ImageObject",
      "url": "https://zengliangyi.com/favicon-96x96.png"
    }
  },
  "mainEntityOfPage": {
    "@type": "WebPage",
    "@id": "https://zengliangyi.com/en/blog/rag-and-knowledge-graphs/"
  }
}
{
  "@context": "https://schema.org",
  "@type": "BreadcrumbList",
  "itemListElement": [
    {
      "@type": "ListItem",
      "position": 1,
      "name": "Home",
      "item": "https://zengliangyi.com/en"
    },
    {
      "@type": "ListItem",
      "position": 2,
      "name": "Blog",
      "item": "https://zengliangyi.com/en/blog"
    },
    {
      "@type": "ListItem",
      "position": 3,
      "name": "From RAG to Knowledge Graphs: Preserving Context in Retrieval",
      "item": "https://zengliangyi.com/en/blog/rag-and-knowledge-graphs/"
    }
  ]
}
```
