This is a viewer only at the moment see the article on how this works.
To update the preview hit Ctrl-Alt-R (or ⌘-Alt-R on Mac) or Enter to refresh. The Save icon lets you save the markdown file to disk
This is a preview from the server running through my markdig pipeline
GraphRAG Partie 2: Minimum viable
Saturday, 27 December 2025
À l'intérieur Partie 1, nous avons étudié les raisons pour lesquelles le GraphRAG est important . Maintenant, let’s GraphRAG minimum viable avec trois modes d'extraction:
| Modèle | Mode | appels LLM MSC2 le meilleur pour SMC3 |
|---|---|---|
| Héuristique (défaultM SK1 | ≥0 par chunk | Indexage rapide, rajustement structuré M |
| Hybride | 1 par document MSC2 Équilibre de vitesse et qualité MSC3 | |
| LLM | 2 par entité | Qualité maximale de l'entité ≥ |
Tous les modes utilisent:
- DuckDB pour stockage unifié (vectors + graphe dans un seul fichierM SK2
- BM25 M SK1 Recherche hybride BERT par le biais de la fusion RRF
- Ollama pour la synthèse et la classification de lots facultative (coûts d’API zéroM SK1
Navigation de série:
- Partie 1: GraphRAG Fundamentals
- Partie 2: GraphRAG minimal viable (le présent articleM SK1
Code: Mostlylucid.GraphRag sur GitHub
Aperçu de l'architecture
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
Pourquoi DuckDB?
Microsoft's GraphRAG utilise un stockage séparé pour les vecteurs M SK1LanceDB), entités (ParquetMSC4 et relations SSK5suite ParquetMST6 DuckDB simplifie ce document
- Unité
.duckdbDossier pour tout - Recherche de vecteurs natifs HNSW par l'intermédiaire d'une extension VSS
- SQL pour la recherche vectorielle et le traversage graphique
- Complexité de déploiement zéro
DuckDB n'est pas une base de données graphique - et queM SK1 est le point. Ce n’est pas le cas.
Système de stockage
Le schéma utilise joindre les tableaux de provenance - nous pouvons faire une recherche "dont les chunks mentionnent directement l'entité 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
}
Décision de conception clé: non VARCHAR[] pour la provenance. Join tables (entity_mentions, relationship_mentions) permet d'effectuer des recherches efficaces comme "obtenir tous les tronçons mentionnant Docker".
Vector Search: Le HNSW vous a trouvé
L'indice HNSW de DuckDB' ne déclenche que avec 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
Utilisation array_cosine_similarity n'utilisera pas l'index - il a gagné’t déclenche l’indice HNSW . Sur une corporation non-trivialeM SK3, , cela transforme une question indexée en une analyse de tableaux completsMSC6
Extraction d'entités
C’est là que nous diverrons de l’approche Microsoft ' . Au lieu LLM-per-passes d'extraction à coupe utilisés dans la référence de Microsoft' GraphRAG pipeline, que nous utilisons Extraction statistique basée sur IDF-. L’objectif n’est pas de constituer des entités parfaites, mais de les transformer en stable, corpusM SK1 signaux relatifs qui n’exigent pas un LLM pour produire une émission de gaz à effet de serre, ce qui fait appel au déterminisme, à la vérifiabilité et aux coûts prévisibles.
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
Pourquoi IDF, ne contient pas de listes codées
L'approche naïve est une approche HashSet<string> KnownTech = { "Docker", "Kubernetes", ... }. Cette interruption s’applique àM SK1
- Nouvelles technologies (vous'il faut mettre à jour la listeM SK2
- Domaine-expressions spécifiques M SK1corps différent =entités différentes)
- Précipitations et variations
IDF (Fréquence inversée du documentM SK1 Le terme 's IDF est :
$$\texteMSC1IDF}(tM SK3 =
Où:
- $NM SK1 = totaux
- Documents contenant le terme \(t\)
IDF élevé = expression rare = vraisemblablement une entitéM SK1 "Docker" apparaissant dans les chunks SSK4 de 100 a des IDF plus élevés que S"leMSC7 appartenant dans les ESK8 de
Pour de plus amples renseignements sur TF-IDF et BM25, voir mon article sur recherche hybride avec BM25.
Signales structurelles
La structure de marquage nous indique ce qui est important
- Intitulé (
## Docker Setup) - Code en ligne (
`docker-compose`) - Liens (
[Docker](https://docker.com)) → entité + 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);
}
}
Extraction de liens (HighM SK1Quality Relationships)
Les liens de marquage fournissent d'une manière explicite relations qui n’exigent pas l’inférence LLM'
// 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);
}
Déduplication via BERT Embeddings
Les noms d'entités tels que "Docker Compose", "dockerM SK3composeMSC4 et MSC5DokerComposeSSK6 devraient être fusionnésMST7 Nous utilisons Emplacements BERT pour détecter la similitude sémantique:
// 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);
}
}
}
Cette étape est O(nM SK1 à l'intérieur d'un ensemble de candidats limités , mais les cotes des candidats sont limitées par le filtrage IDF et les signaux structurels - non la taille du corpusMNK4 Pour plus de détails sur les emplacements BERTMRK5 voir Recherche sémantique avec ONNX et BERT.
Modes d'extraction
Le CLI appuie trois modes d'extraction par --extraction-mode:
Mode héuristique (Default)
dotnet run --project Mostlylucid.GraphRag -- index ./Markdown --extraction-mode heuristic
Utilise des signaux structurels IDF + pour la détection de l'entitéM SK1 avec classification facultative du lot LLM. Numéro par -appels de LLM en souffrance - seulement ~1 appel par entité pour la classification de typeM SK3
Mode hybride (Recommandé)
dotnet run --project Mostlylucid.GraphRag -- index ./Markdown --extraction-mode hybrid
Le meilleur des deux mondes:
- Détection héuristique: IDF + les signaux structurels trouvent des candidats à l’entité
- Amélioration de LLM: Un appel par Documents Valider les entités et extraire des relations sémantiques
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
Pour les documents 5 avec des fragments 62, le mode hybride 5 appels LLM (vs 124 pour le mode LLM complet). Vous obtiendrez
- La couverture d'entités déterministes à partir de l'héuristique
- LLM-extraction des relations de qualité M SK1semantique , non seulement coMSC3occurrence)
- Descriptions et types validés
Mode LLM (Microsoft-StyleM SK2
dotnet run --project Mostlylucid.GraphRag -- index ./Markdown --extraction-mode llm
Approche Microsoft GraphRAG intégrale: 2 Les appels de LLM par chunk ( Extraction d'entités + extraction de rapports). Le texte le plus coûteuxM SK3 mais la qualité la plus élevée pour un texte non structuréMSC4
Quand utiliser chacune
| Modèle | Mode | appels LLM MSC2 le meilleur pour SMC3 |
|---|---|---|
| Héuristique | ~1 par SSK2 entités MSC3 Indexage rapideMSc4 bienMScs5 rajustement structuré MSc6 | |
| Hybride | 1 par document MSC2 Solde de la couverture et qualité MSC3 | |
| LLM | 2 par chunk | Prose non structurée, qualité maximale M |
Pour la documentation technique, débutez par hybride mode. Il vous donne des relations sémantiques sans le coût perM SK1chunk. Retournez à Heuristic pour la vitesse pure, ou Llm pour le texte narratif.
Recherche hybride: BMM SK1 + BERT
La recherche hybride combine deux approches complémentaires:
Densité (BERTM SK1 Comprend la signification. M SK1Conteneurs de docker" correspondent "conteneurisationMSC4 Pièces de rechange (BMM SK1 Correspond aux termes exacts. M SK1HNSW" seulement correspond "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
Qu'est-ce que BM25?
BM25 M SK1Meilleure correspondance 25) classe les documents en fonction de la fréquence du terme d'interrogatoire. La formuleMSC4
$$\texteM SK1points de vue}(D ,Q_ \texteMSC4IDFMNK5qMRK6i+) \cdot \ \fracMEK10fMMK11q~_i=, D&) ♪\cdo (k+MNK17 | | + ─f MNK20q |MNK21i\MNK22 D &) ==+ k=MNK25 ++MNK26cdon == (1 ≥MNK28 b ==M+ b
Principaux intuitions:
- Définition du terme IDF: Les mots rares sont plus importants.
- TF saturation: Parole apparaissante 10x n'est pasM SK2t 10x plus pertinente que SSK4x
- Normalisation de la longueur: Les documents longs ne bénéficient pas d’un avantage injuste
Pour une mise en œuvre intégrale du BM25, voir Recherche hybride et indexation.
Fusion de rang réciproque (RRFM SK1
RRF fusionne les rangs provenant de différents systèmes de recherche. Chaque poste de rang reçoit une noteM SK1
$$\texteM SK1RRF}(dMSC3 = ≥\ somme_{r \in RM SK2 \frac{1}{k M+ rMSC6dMST7
Lorsque $kM SK1 (typiquement 60) empêche l’excès de poids du résultat supérieur. Documents apparaissant dans les deux les rangs sont renforcés:
// 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;
}
Exemple: Un document classé M SK1 en densité et #3 en épargne
- Densité
- Pièces de rechange: M SK1 ≥= ≤0.0159$
- Combinés: M SK1 (plus élevé que l'un ou l'autre seul
Modes de demande
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
Classification de la demande
// 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;
}
Ce classeur est délibérément simple - et facile à remplacer par un petit modèle d'intention ultérieurement . Si aucune entité ne se prête à ce type de requête, le système décroît nettement jusqu'à une récupération hybride pure.
Utilisation du CLI
Indexage
# 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 │
└───────────────┴───────┘
Demande de renseignements
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)
Statistiques
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
Comparaison des coûts
Pour 100 les articles de blog (~500 chunks, ~100 documentsM SK4
| Opération | MSFT GraphRAG | |||||||
|---|---|---|---|---|---|---|---|---|
| Extraction de l'entité | 1,000 appels MSC3 \0 | ♫ | MSC6 \ | \ | MSC8 | appels | ||
| Amélioration des documents | 2 | 3 | 4 | 5 | 6 | appels | ||
| Classification | Inclue MSC2 MSC3 lots SMC4 | 5 | 6 | 7 | lots | 8 | ||
| Sommaires communautaires | MSC2 | 3 | 4 | 5 | 6 | 7 | ||
| Total des appels LLM | ~1,020 | ~24 | ~120 | ~1,024 | ||||
| Qualité des relations | Spécifique | Co-occurrence | Smécifique | |||||
| Coût (gpt -4oM SK2mini) | ~$5-10 | ~$0.15 | ~$0.75 | ~$5-10 | ||||
| Coût (OllamaM SK1 | NM SK1A | $0 | 4 | 5 | 6 | 7 | 8 |
Le mode hybride est le bon endroit pour la plupart des contenus techniques: vous obtiendrez les relations sémantiques M SK1non seulement co-occurrenceMSC3 à ~10% de MSFTMST5coût
Ordonnance brute-de-estimation de l’magnitude ;le coût exact dépend de la taille du tronçon et de sa forme immédiate
Comptes compensatoires
| Aspect | Heuristic MSFT GraphRAG S | ||||
|---|---|---|---|---|---|
| Détection de l'entité | IDF + structure | IFD SMK4 structure | |||
| Relations | Co-occurrence | LLMM SK4inferred | Co~-occurence ♫ | LLSMSC8inférred ♫ | |
| Les appels de LLM (100 Dossiers) | ~24 | ♫ | ~120 ♪ | ||
| Qualité de la relation | Faible | ||||
| Fonctionne en mode offline | Oui MSC2 Oui | ||||
| Le meilleur pour | La vitesseM SK2Critique | Recommandation | Commutateur d'ancienneté | Texte non structuré ≥ |
Conceptuellement, il s’agit de la même conduite que DocSummarizer: bâtir la structure d’abord , puis laisser un LLM l’énoncer
Où cela se décompose: Texte de fiction ou narratif sans marquage structurel. Relations implicites sans signaux lexiques . Noms d'entités très ambigus qui nécessitent des connaissances mondiales pour être désambigués. Dans ces casM SK3 utilisez le mode LLM ou MicrosoftMSC4 l'approche complète
Code
La mise en œuvre est minimale : - ~2,000 lignes à travers ces fichiers
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
Source: Mostlylucid.GraphRag/
Postes connexes
- Partie 1: GraphRAG Fundamentals - Pourquoi les graphiques de connaissance améliorent le RAG
- Recherche hybride avec BM25 - Une profonde plongée dans le BMM SK1 la cotation et l’indexation
- Recherche sémantique avec ONNX et BERT - Emplacements BERT dans le .NET
- Outil DocSummarizer - Mon outil de synthèse du document qui partage le code avec ce
Ressources externes
- Extension DuckDB VSS Recherche par vecteur - HNSW
- Document d'algorithme de Leiden - Détection communautaire
- Papier RRF - Fusion de rang réciproque