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
Saturday, 27 December 2025
In Parte 1, abbiamo esplorato il motivo per cui GraphRAG è importante . Ora lasciate' costruire un GraphRAG minimamente viabile con tre modalità di estrazione:
| Mode | Calls LLM | Perfetto per M | |
|---|---|---|---|
| Heuristica (defaultM SK1 | 0 per blocco SSK4 Indicettazione veloce, Markdown strutturato | ||
| Ibrido | 1 per ogni documento | Balance tra velocità e qualità S | |
| LLM |
Tutti i modi usano:
Navigation delle serie:
Code: Mostlylucid.GraphRag su 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 usa un'archiviazione separata per i vettori M SK1LanceDB),entità (ParquetMSC4 e relazioni SSK5di più ParquetMST6 DuckDB semplifica questo
.duckdb file per tutto.DuckDB non è una base di dati grafica - e cheM SK1 è il punto. Questo non è 't NeoM SK1j. I travertili sono superficiali , SQLMST4 basati MST5 e deliberatiM ST6 Questa restrizione mantiene il sistema debuggerabile e economicoMst7
Il schema usa Si uniscono i tavoli per la provenienza. - possiamo fare una domanda " quali frammenti menzionano l'entità X?" direttamenteM SK3
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
}
Decisione di progettazione chiave: No. VARCHAR[] per la provenienza. Coinvolgere i tavoli (entity_mentions, relationship_mentions) permette di fare ricerche efficienti come " ottenere tutti i frammenti che menzionano Docker".
L'indice HNSW di DuckDB' si attiva solo con 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
Utilizzando array_cosine_similarity non userà l'indice. - ha vintoM SK1 non ha attivato l'indice HNSW. Su corpora non--trivialeMSC4 questo trasforma una domanda indexata di ~5ms in una scansione completa della tavola.
Questo è il punto in cui ci diffondiamo dall'approccio di Microsoft' LLM-per-passi d'estrazione di scarto usata in Microsoft's riferimento Pipeline GraphRAG, che usiamo Extrazione statistica basata su IDF-. L'obiettivo è non essere un'entità perfetta. stabile, corpusM SK1 segnali relativi che non richiedono un LLM per produrre. Questo corrisponde a un certo richiamo al determinismo,Auditabilità, e costi prevedibili- una scelta deliberata per le corporazioni tecniche:
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
L'approccio naïvo è un metodo hardcode. HashSet<string> KnownTech = { "Docker", "Kubernetes", ... }. Questa interruzione per:
IDF (Frequenza inversa del documentoM SK1 Risolve questo statisticamente. Un termineM SK1s IDF è:
$$\text{IDFM SK2tMSC3 = \logMST6fracMSV7NMSS8dfM SV9t\MSV10
Dove:
High IDF = termine raro = probabile che un'entitàM SK1 "Docker" apparsa in SSK4 dei frammenti 100 abbia un IDF più alto di SSK6ilSNK7 apparso in \SSK8 dei S100.
Per saperne di più su TF-IDF e BM25, consultate il mio post su ricerca ibrida con BM25.
La struttura di marcazione ci dice cosa è importante.
## Docker SetupEntità )`docker-compose`Entità )[Docker](https://docker.com)) → entità + relazione// 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);
}
}
I link Markdown forniscono Esplicito relationships that don't require LLM inference:
// 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);
}
I nomi di entità come "Docker Compose", "dockerM SK3composeMSC4 e "DokerComposeSSK6 dovrebbero essere unitiMST7 Noi usiamo Inserzioni BERT per rilevare la somiglianza semantica:
// 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);
}
}
}
Questo passo è O(nM SK1 all'interno di un gruppo di candidati limitato, ma i conteggi dei candidati sono limitati da filtri IDF e segnali strutturali - non dimensione del corpusMSC4 Per i dettagli sulle inserzioni BERTMNK5 consultate La ricerca semantica con ONNX e BERT.
Il CLI supporta tre modi di estrazione attraverso --extraction-mode:
dotnet run --project Mostlylucid.GraphRag -- index ./Markdown --extraction-mode heuristic
Usa i segnali strutturali IDF + per la rilevazione delle entità, con classificazione opzionale di lotti LLMM SK2 Zero per le chiamate di LLM su -. - solo ~1 chiamate per 50 entità per classificare il tipo.
dotnet run --project Mostlylucid.GraphRag -- index ./Markdown --extraction-mode hybrid
La migliore di entrambi i mondi:
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
Per i documenti 5 con i frammenti 62, il modo ibrido fa Le chiamate 5 LLM (vs 124 per il modo completo LLMM SK2 ottenete:
dotnet run --project Mostlylucid.GraphRag -- index ./Markdown --extraction-mode llm
L'approccio completo di Microsoft GraphRAG: 2 LLM chiama per blocco (estrazione di entità +estramento di relazione). più costosoM SK3 ma di migliore qualità per testi non strutturatiMSC4
| Mode | Calls LLM | Perfetto per M |
|---|---|---|
| Heuristica | ||
| Ibrido | 1 per documento | Balance della copertura e della qualità S |
| LLM |
Per la documentation tecnica, cominciate con ibrido. mode. E vi dà relazioni semantiche senza il costo per l'uno. Heuristica per velocità pura, o Llm per il testo narrativo.
La ricerca ibrida combina due approcci complementari:
Denso (BERTM SK1 Capisce il significato. "Container di dockerM SK2 corrisponde "containerizzazioneMSC4 Sparse (BMM SK1 Corre i termini precisi. "HNSWM SK2 solo corrisponde "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) valuta i documenti sulla base della frequenza del termine di domanda. La formulaMSC4
$$\ testoM SK1 punteggio_ \textMSC4IDFMST5qMSSK6i+) ==\cdot MSSK9fracMSV10fM SV11q=MSV12i\MSV13 DMSS14 ≥MSV15cdo SSV16kM Sv17 ≤MSV18 ±MSV19f+MSV20q~MSV21i^MSV22 D\MSS23 + kMsv25 +M SV26c dot RMSV27 ++MSV28 b MMSV29 b
Principali intuizioni:
Per la completa implementazione del BM25, see ,. La ricerca e l'indexazione ibride.
RRF fonde classificazioni da diversi sistemi di rilevamento. Ogni posizione di classifica ottiene un punteggio:
$$\testM SK1RRF}(dMSC3 = | | 5 | somma_{r \in RM SK2 \frac{1}{k M+ rMSC6dMST7
Dove $kM SK1 ( tipicamente 60) impedisce l'eccesso di sovrapeso del risultato più alto. Documenti presenti in Entrambi. I ranking sono migliorati:
// 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;
}
Esempio: Un documento classificato M SK1 in densità e #3 in scarsa
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;
}
Questo classificatore è deliberatamente semplice - ed è facile da sostituire con un piccolo modello di intenzione più tardi. Se nessuna entità corrisponde a , il sistema si degrada pulito per ottenere una ripresa pura ibridaM SK3
# 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
Per i post di blog 100 (~500 frammenti, | Documenti
| Operazione | MSFT GraphRAG | Heuristice MSSK3 Hybride | LLM \ | |||||||
|---|---|---|---|---|---|---|---|---|---|
| Extrazione delle entità | 1,000 richiami | \0 | ♫ | 0 ♫ | |||||
| Riparazione dei documenti | - | | 4 | 5 | 6 | chiamate | 7 | 8 | 9 | |
| Classificazione | Incluso | S~4 lotto M | - | ~4 | lotto | ||||
| Esempi comuni | ~20 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | |
| Totali chiamate LLM | ~1,020 | ~24 | ~120 | ~1,024 | |||||
| Calità delle relazioni | Semantici | Co-Occurrenza | Semantiche | ||||||
| Costo (gpt -4oM SK2mini) | ~$5-10 | ~$0.15 | ~$0.75 | ~$5-10 | |||||
| Costo (OllamaM SK1 | NM SK1A |
Il modo ibrido è il punto preferito. per la maggior parte del contenuto tecnico: si ottengono relazioni semantiche M SK1non solo co-occurrenceMSC3 a ~10% di MSFTMST5costa di .
Ordinazione ristretta-di-estimazione di grandezzaM SK2costa esatta dipende dalla dimensione del pezzo e dalla forma improvvisa
| Aspetto | Heuristico MSFT GraphRAG SSK5 | |||||||
|---|---|---|---|---|---|---|---|---|
| Detezione delle entità | IDF | + | Structure | LLM per un pezzo | ||||
| Relazioni | Co-occurrenza | LLMM SK4inferita M | Co-occurenza ♫ | LLSMSC8inferrata R | ||||
| Calls LLM (100 docs) | ~24 | ♫ | ~120 ♪ | |||||
| Calità delle relazioni | Bassa | alta | bassa SMK4 alta | |||||
| Funziona offline | Sì | Sì | ||||||
| Perfetto per | Velocità-Critificabile | Recommended | Compatto di sequenza | testo non strutturato |
Conceptualmente, questo è lo stesso tubo di costruzione. DocSummarizer: costruire prima la struttura, poi lasciare che lo descriva un LLMM SK2
Dove questo si rompe: Un testo narrativa o di fantascienza senza marcatura strutturale. Relazioni implicite senza segnale lexico. Nomi di entità molto ambigui che richiedono conoscenza del mondo per essere disinambiguatiM SK2 Per questi casi , usare il modo LLM o MicrosoftMST4 l'approccio completoMst5
L'implementazione è minimale - ~2,000 linee attraverso questi file:
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/
© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.