Back to "GraphRAG Partie 2: Minimum viable"

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

ASP.NET DuckDB GraphRAG Knowledge Graphs Machine Learning Vector Search

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:

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é .duckdb Dossier 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:

  1. Détection héuristique: IDF + les signaux structurels trouvent des candidats à l’entité
  2. 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

Ressources externes

logo

© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.