I Del 1, vi undersökte varför GraphRAG är viktigt . Låt oss nu bygga minimalt hållbara GraphRAG med tre extraktionsmodi:
| Modus | LLM-sändningar | bäst för | |||||
|---|---|---|---|---|---|---|---|
| Heuristik (default) | 3 | per chunk | snabb indexering | ||||
| Hybrid | 1 per dokument | Tillsvarighet mellan hastighet och kvalitet | |||||
| LLM | 2 per stycke | Max. entitetskvalité |
Alla former använder:
Navigationsserien:
Kod: Mostlylucid.GraphRag på GitHub
flowchart LR
subgraph Indexing
MD[Markdown Files] --> CH[Chunker]
CH --> EMB[BERT Embeddings]
CH --> EXT[Entity Extractor]
EXT --> |heuristics + links| ENT[Entities]
ENT --> REL[Relationships]
REL --> COM[Communities]
end
subgraph Storage
EMB --> DB[(DuckDB)]
ENT --> DB
REL --> DB
COM --> DB
end
subgraph Query
Q[Query] --> CLASS{Classify}
CLASS --> |local| HS[Hybrid Search]
CLASS --> |global| CS[Community Search]
CLASS --> |drift| BOTH[Both + Synthesis]
HS --> LLM[Ollama]
CS --> LLM
BOTH --> LLM
end
DB --> HS
DB --> CS
style MD stroke:#22c55e,stroke-width:2px
style DB stroke:#3b82f6,stroke-width:2px
style LLM stroke:#a855f7,stroke-width:2px
Microsoft's GraphRAG använder separat data lagring för vektorer (LanceDB), enheter | (Parquet |), | och relationer ♫ ( | mer Parquet | МSK6 | DuckDB förenklar det här ♫
.duckdb file for everythingDuckDB är inte en grafdatabas - och att ' är punkten Det här är "'", Neo , 4, j, ., Traversal är flyktiga , ,, SQL, -, baserade på , och avsiktliga . Den begränsningen gör att systemet är nedbrytbart och billigt.
Schematet använder anslut till provenances tabeller - kan vi fråga " vilka chunk som nämner enhet X
erDiagram
documents ||--o{ chunks : contains
chunks ||--o{ entity_mentions : has
chunks ||--o{ relationship_mentions : has
entities ||--o{ entity_mentions : mentioned_in
entities ||--o{ relationships : source
entities ||--o{ relationships : target
relationships ||--o{ relationship_mentions : mentioned_in
communities ||--o{ community_members : contains
entities ||--o{ community_members : belongs_to
chunks {
varchar id PK
varchar document_id FK
text text
float[] embedding
}
entities {
varchar id PK
varchar name
varchar type
int mention_count
}
entity_mentions {
varchar entity_id FK
varchar chunk_id FK
}
Nyckeldesign lösning: nej VARCHAR[] för ursprunget. Join tables (entity_mentions, relationship_mentions) gör det möjligt att utföra effektiva queryer som " får alla chunks som nämner Docker
DuckDB's HNSW-index triggar bara med array_cosine_distance + ORDER BY + LIMIT:
// GraphRagDb.cs - SearchChunksAsync
cmd.CommandText = $"""
SELECT id, document_id, text, chunk_index,
array_cosine_distance(embedding, $1::FLOAT[{_dim}]) as distance
FROM chunks
WHERE embedding IS NOT NULL
ORDER BY distance
LIMIT $2
""";
// Convert distance to similarity: 1.0f - distance
Att använda array_cosine_similarity kommer inte använda indexet - det vann’ inte utlösade HNSW-indexet . På icke-- triviella företagen M SK4 blir detta en indexerad query i ms till en komplett tabellscan
Det är här vi skiljer oss från Microsofts "'" -metod, LLM-per-spända extraktionspasser som används i Microsofts referens GraphRAG Pipeline Statistisk extraktion baserat på IDF-. målet är att stabila, korpus som inte kräver en LLM för att producera
flowchart TB
subgraph "Phase 1: Signal Collection"
TEXT[All Chunks] --> IDF[Compute IDF Scores]
TEXT --> STRUCT[Structural Signals]
STRUCT --> HEAD[Headings]
STRUCT --> CODE[Inline Code]
STRUCT --> LINKS[Links]
IDF --> RARE[High-IDF = Rare Terms]
RARE --> CAND[Candidates]
HEAD --> CAND
CODE --> CAND
LINKS --> |explicit rels| LINKREL[Link Relationships]
end
subgraph "Phase 2: Dedup"
CAND --> EMBED[BERT Embeddings]
EMBED --> SIM[Similarity > 0.85]
SIM --> MERGE[Merge Duplicates]
end
subgraph "Phase 3: Classify"
MERGE --> LLM{LLM Available?}
LLM --> |yes| BATCH[Single Batch Call]
LLM --> |no| HEUR[Heuristic Types]
end
style IDF stroke:#f59e0b,stroke-width:2px
style BATCH stroke:#a855f7,stroke-width:2px
Den naiva metoden är HashSet<string> KnownTech = { "Docker", "Kubernetes", ... }. Detta bryter förM SK1
IDF (Inverse dokumentfrekvens) löser det statistiskt.
$$, text, {, IDF, }(, t, ), =, | | , log, M SK6, frak, MST7, N, MSP8, df, MSV9, t och MST10
Var:
Hög IDF = ovanlig termin = antagligen en enhetM SK1 "Docker" som visas i 5 av m100 chunks har högre IDF än "den " som syns i \100 av
För mer information om TF-IDF och BMM SK1, se min artikel på hybridsökning med BM25.
Markdown-strukturen säger oss vad
## Docker Setup) → enhet`docker-compose`) → enhet[Docker](https://docker.com)) → enhet + relation// EntityExtractor.cs - structural signal extraction
private void ExtractStructuralEntities(string chunk, string chunkId)
{
// Headings: ## Docker Compose Setup → "Docker Compose Setup"
foreach (Match m in Regex.Matches(chunk, @"^#{1,3}\s+(.+)$", RegexOptions.Multiline))
{
var heading = m.Groups[1].Value.Trim();
AddCandidate(heading, chunkId, weight: 2.0); // Higher weight
}
// Inline code: `docker-compose` → "docker-compose"
foreach (Match m in Regex.Matches(chunk, @"`([^`]+)`"))
{
AddCandidate(m.Groups[1].Value, chunkId, weight: 1.5);
}
}
Markdown- länkar erbjuder uppenbart relationer som inte kräver LLM-förklaringar
// EntityExtractor.cs - ExtractLinks
foreach (Match m in Regex.Matches(chunk, @"\[([^\]]+)\]\((/blog/[^)]+)\)"))
{
var linkText = m.Groups[1].Value; // "semantic search"
var slug = m.Groups[2].Value; // "/blog/semantic-search-with-qdrant"
yield return new Relationship(linkText, $"blog:{slug}", "references", chunkId);
}
Beståndsdels namn som "Docker Compose", "dockerM SK3composeMSC4 och ♫ "DokerCompose ♫ should be merged ♫ BERT-bäddar för att upptäcka semantisk likhet:
// EntityExtractor.cs - DeduplicateAsync
var embeddings = await _embedder.EmbedBatchAsync(candidates.Select(c => c.Name), ct);
for (int i = 0; i < candidates.Count; i++)
{
for (int j = i + 1; j < candidates.Count; j++)
{
var similarity = CosineSimilarity(embeddings[i], embeddings[j]);
if (similarity > 0.85)
{
// Merge into canonical entity (keep higher mention count)
canonical.MentionCount += duplicate.MentionCount;
canonical.ChunkIds.UnionWith(duplicate.ChunkIds);
}
}
}
Det här steget är O(nM SK1 inom en begränsad kandidatgrupp, , men kandidatkolorna är begränsade av IDF-filtrering och struktursignaler. - inte korpusstorlek, . För mer information om BERT-inbäddar, semantiska sökningar med ONNX och BERT.
CLI supporterar tre extraktionsmodi via --extraction-mode:
dotnet run --project Mostlylucid.GraphRag -- index ./Markdown --extraction-mode heuristic
Användar IDF + struktursignaler för enhetens detektion , med optionell LLM-skikt klassificering . Noll per -chunk LLM-sändningar - endast ~1 uppgift per | 50 | enhet för typklassificering
dotnet run --project Mostlylucid.GraphRag -- index ./Markdown --extraction-mode hybrid
Den bästa av båda världarna:
flowchart LR
subgraph "Per Document"
CHUNKS[Document Chunks] --> HEUR[Heuristic Extraction]
HEUR --> CAND[30 Candidates]
CAND --> LLM[Single LLM Call]
LLM --> ENT[Validated Entities]
LLM --> REL[Semantic Relationships]
end
style HEUR stroke:#22c55e,stroke-width:2px
style LLM stroke:#a855f7,stroke-width:2px
För 5-documenter med 62 chunks, gör hybridmodiet 5 LLM-sändningar (vs 124 för full LLM-modus
dotnet run --project Mostlylucid.GraphRag -- index ./Markdown --extraction-mode llm
Hela Microsoft GraphRAG-metoden: 2 LLM ringer per chunk ( entitetsextraktion + relations extraktion). Den dyrasteM SK3 men den högsta kvaliteten för ostrukturerad textMSC4
| Modus | LLM-sändningar | bäst för | |||||||
|---|---|---|---|---|---|---|---|---|---|
| Heuristik | ~1 per 50 enhet | snabb indexering | , | bra | МSK5 | strukturerad nedgång | |||
| Hybrid | 1 per dokument | Tillsvarighet i tillämpning och kvalitet | |||||||
| LLM | 2 per stycke ♫ ♫ | ♫ Ostrukturerad prosa ♫ |
För teknisk dokumentation, börja med hybrida mode. Det ger dig semantiska relationer utan att behöva heuristisk för ren hastighet, eller Llm för berättelsetext.
Hybridsökning kombinerar två komplementära tillvägagångssätt
Dense (BERTM SK1 Förstår meningen. M SK1Dockercontainer" matchar "containeriseringMSC4 Sparse (BMM SK1 Matchar exakta termer. M SK1HNSW" bara matchar "HN SWMSC4
flowchart LR
Q[Query] --> BERT[BERT Embedding]
Q --> BM25[BM25 Tokenize]
BERT --> DENSE[Dense Search<br/>HNSW Index]
BM25 --> SPARSE[Sparse Search<br/>TF-IDF Scoring]
DENSE --> RRF[RRF Fusion]
SPARSE --> RRF
RRF --> TOP[Top K Results]
TOP --> ENR[Enrich with<br/>Entities + Rels]
style RRF stroke:#f59e0b,stroke-width:2px
BM25 M SK1Best Match 25) räknar ut dokument baserat på frågaterm frekvens
$$_ \textMSC4IDFMska5qM Ska6i+Mska7
Nyckelinsikter:
För en full implementation av BM, 25 och ,, se hybridsökning och indexering.
RRF sammanfogar betyg från olika söksystem. Varje rangposition får en poäng
$$, text, {, RRF, }(, d, M SK3, =,_{r \in RM SK2 \frac{1}{k + rMska6dMske7
Där $kM SK1 ( typiskt 60) förhindrar övervikt på toppresultatet båda rangorde blir ökande:
// SearchService.cs - RRF fusion
const int k = 60;
foreach (var (chunk, rank) in denseResults.Select((c, i) => (c, i)))
scores[chunk.Id] = 1.0 / (k + rank + 1);
foreach (var (chunk, rank) in sparseResults.Select((c, i) => (c, i)))
{
var rrfScore = 1.0 / (k + rank + 1);
if (scores.TryGetValue(chunk.Id, out var existing))
scores[chunk.Id] = existing + rrfScore; // Boost for appearing in both!
else
scores[chunk.Id] = rrfScore;
}
exempel: Ett dokument som rangerade M SK1 i täthet och #3 i sparsamhet
flowchart TB
Q[Query] --> CLASS[Classify Query]
CLASS --> |"How do I use X?"| LOCAL[Local Search]
CLASS --> |"What are the themes?"| GLOBAL[Global Search]
CLASS --> |"How does X relate to Y?"| DRIFT[DRIFT Search]
LOCAL --> HS[Hybrid Search] --> CTX1[Chunk + Entity Context]
GLOBAL --> CS[Community Summaries] --> MAP[Map-Reduce]
DRIFT --> BOTH[Local + Communities] --> SYN[Synthesize]
CTX1 --> LLM[LLM Answer]
MAP --> LLM
SYN --> LLM
style LOCAL stroke:#22c55e,stroke-width:2px
style GLOBAL stroke:#3b82f6,stroke-width:2px
style DRIFT stroke:#a855f7,stroke-width:2px
// QueryEngine.cs
private static QueryMode ClassifyQuery(string query)
{
var q = query.ToLowerInvariant();
if (q.Contains("main theme") || q.Contains("summarize") || q.Contains("overview"))
return QueryMode.Global;
if (q.Contains("relate") || q.Contains("connect") || q.Contains("compare"))
return QueryMode.Drift;
return QueryMode.Local;
}
Den här klassifieren är medvetet enkel - och lätt att ersätta med ett litet syftesmodell senare . Om inga enheter matchar , så degraderar systemet rent till en ren hybrid sökning
# Heuristic mode (default) - fast, no per-chunk LLM
dotnet run --project Mostlylucid.GraphRag -- index ./test-markdown
# LLM mode - Microsoft-style classification
dotnet run --project Mostlylucid.GraphRag -- index ./test-markdown --extraction-mode llm
GraphRAG Indexer
Source: test-markdown
Database: graphrag.duckdb
Model: llama3.2:3b
Extraction: Heuristic (IDF + signals)
Initializing...
Indexing docker-development-deep-dive.md: 0%
Indexing docker-swarm-cluster-guide.md: 40%
Indexing dockercomposedevdeps.md: 80%
Indexing complete: 100%
Classifying entities...: 0%
Extracted 168 entities, 315 rels (4 LLM calls): 100%
Found 10 communities: 100%
Summarizing c_0_2 (12 entities): 20%
Summarizing c_0_8 (4 entities): 80%
────────────────── Indexing Complete ───────────────────
┌───────────────┬───────┐
│ Metric │ Count │
├───────────────┼───────┤
│ Documents │ 5 │
│ Chunks │ 62 │
│ Entities │ 168 │
│ Relationships │ 312 │
│ Communities │ 10 │
└───────────────┴───────┘
dotnet run --project Mostlylucid.GraphRag -- query "How do I use Docker Compose?"
──────────────────── Local Search ────────────────────
Query: How do I use Docker Compose?
╭─Answer────────────────────────────────────────────────╮
│ To run the services defined in the │
│ devdeps-docker-compose.yml file, you need to run the │
│ following command in the same directory as the file: │
│ │
│ docker compose -f .\devdeps-docker-compose.yml up -d │
│ │
│ This command will start the containers in detached │
│ mode. │
╰───────────────────────────────────────────────────────╯
Related Entities: Docker, container, services, image
Sources: 5 chunks (top score: 0.016)
dotnet run --project Mostlylucid.GraphRag -- stats
─────────────── GraphRAG Database Stats ────────────────
┌───────────────┬───────┐
│ Metric │ Count │
├───────────────┼───────┤
│ Documents │ 5 │
│ Chunks │ 62 │
│ Entities │ 168 │
│ Relationships │ 312 │
│ Communities │ 10 │
└───────────────┴───────┘
Database size: 7.76 MB
För 100 bloggposter
| Operation | MSFT GraphRAG | Heuristisk | МSK3 | Hybrid | LLM ♫ | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Enhetens extraktion | 1,000 | anslutningar | МSK3 | m0 | M | m0 | M | 1,000 | Anslutningar | ||||
| Dokumentupphöjning ♫ | ♫ ♫ | ||||||||||||
| Klassificering | Inbegript | 2 | 3 | lots | 4 | 5 | 6 | 7 | lot | 8 | |||
| Samhällens sammanfattningar | 2 | 3 | 4 | 5 | 6 | 7 | 8 | ||||||
| Tilltal av LLM sändningar | ~1,020 | ~24 | ~120 | ~1,024 | |||||||||
| Attitydkvalitet | Semantik | Ko- uppträffande | Sementik ♫ ♫ | ♫ Semantika ♫ | |||||||||
| kostnaderna (gpt-4o -miniM SK3 | ~$5-10 | ~$0.15 | ~$0.75 | ~$5-10 | |||||||||
| kostnaderna (OllamaM SK1 | NM SK1A |
Hybridmodiet är den bästa punkten för de flesta tekniska innehållet
Den grova inställningen, -, av -, storleksskattning, ;, är beroende på skalan och form.
| Aspect | Heuristiska | Hybrida | LLM | MSFT GraphRAG | |||
|---|---|---|---|---|---|---|---|
| Bestämning av enheten | IDF ♫ ♫ + ♫ struktur ♫ | ||||||
| Attityder | Ko | - | uppkomst | МSK3 | LLM | ||
| LLM-sändningar | |||||||
| Attitydkvalitet | lågt ♫ ♫ | ♫ högt ♫ | |||||
| Fungerar offline | Ja | Ja | |||||
| bäst för | hastighet Recommended | Gamla kompat | Ostrukturerad text |
Conceptuellt, är detta samma rör som DocSummarizer: bygg konstruktion först , sedan låter en LLM berätta det
Var det bryts ner Sannings- eller berättelsetext utan strukturell markup. Itydliga relationer utan lexisk signal
Implementationen är minimal - | ~2,000 | linjer över dessa plikter
Mostlylucid.GraphRag/
├── Storage/GraphRagDb.cs # DuckDB with HNSW + provenance
├── Services/EmbeddingService.cs # ONNX BERT wrapper
├── Services/OllamaClient.cs # LLM client
├── Extraction/
│ ├── IEntityExtractor.cs # Extractor interface
│ ├── EntityExtractor.cs # Heuristic mode
│ ├── HybridEntityExtractor.cs # Hybrid mode (recommended)
│ └── LlmEntityExtractor.cs # Full LLM mode
├── Search/SearchService.cs # BM25 + BERT hybrid
├── Graph/CommunityDetector.cs # Leiden + summarization
├── Query/QueryEngine.cs # Local/Global/DRIFT
├── Indexing/MarkdownIndexer.cs # Chunking
├── GraphRagPipeline.cs # Orchestration
├── Models.cs # Shared types + ExtractionMode enum
└── Program.cs # CLI
källa: Mostlylucid.GraphRag/
© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.