В Частина 1, ми дослідили, чому GraphRAG має значення . Тепер давайте МSK2 побудуємо мінімальний життєздатний граф РАГ з трьома режимами видобутку:
| tryb | LLM дзвінки МSK2 найкращий для МSK3 |
|---|---|
| Герістичні (передбачуванийM SK1 МSK2 \0 на шматок МSK4 швидке indeksування , структурне віднімання M SK6 | |
| Гібрид МSK0 1 за документ МSK2 Баланс швидкості та якості | |
| ЛЛМ МSK0 2 на шматок МSK2 Максимальна якість об 'єкта |
Усі режими використовуються
Навігація серії:
Код: Найяскравіший.GraphRag на 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 використовує відокремлене зберігання векторів (LanceDBM SK2 об 'єктів МSK3Parquet), та стосунків
.duckdb файл для всьогоDuckDB - це не графічна база даних -, а те, що Це не "'", а Neo . МSK1 , j , . . Траверзалі є плиткими. МSK3 , SQL , МSK4 , базовані на МSK5 і навмисно обмірковані.
skeма використовує об 'єднати таблички для провінції - ми можемо задати питання ", які фрагменти вказують на об 'єкт 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
}
Головне рішення щодо дизайну: ні VARCHAR[] для походження. Приєднуйтесь до столів МSK1entity_mentions, relationship_mentions) дозволяє виконувати ефективні питання, такі як " отримати всі фрагменти, які вказують на Докер
Індекс HNSW DuckDB' триггерує лише з 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
Використовуючи array_cosine_similarity не буде використовувати індекс - він вигравM SK1 не запускає індексу HNSW . На не-- банальних корпораціях МSK4 це перетворює МSK5 ms indeksованого запиту на повну таблицю сканування .
Ось де ми відокремлені від підхіду Microsoft' LLM-per-переходи до видобутку застосовується в Microsoft's референції Pipeline GraphRAG, ми використовуємо Статистика з IDF-. Метою є не ідеальні об 'єкти, а ціла стабільні, корпусM SK1 відносні сигнали що не потребують LLM для виробництва
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
Наївний підхід HashSet<string> KnownTech = { "Docker", "Kubernetes", ... }. Це розрив дляM SK1
IDF (Взамінна частота документуM SK1 обчислює це статистично.
МСК0текстМSK1IDF}(t) M SK4 ♫ ♫ МSK5лог ♫ \frac ♫
Де:
Високое ІДФ = рідкісний термін = швидше за все, те, що об 'єкт МSK1 МSK2 Докер ", який з' являється в мSK4 з chunks M100, має вищий рівень IDF, ніж МСК6 те МПС7, що з 'являється у MPС8 з МУС9
Щоб дізнатися більше про TF-IDF та BMM SK1, погляньте на мій пост: пошук гібридом з BM25.
Структура позначки вказує нам, що
## Docker SetupМSK0 → одиниця`docker-compose`МSK0 → одиниця[Docker](https://docker.com)МSK0 → одиниця МSK2 стосунки// 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);
}
}
Ссылки на позначку яскраво відносини, які не потребують 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);
}
Назви об 'єктів, такі як "Docker Compose", "dockerM SK3composeMSC4 і МSK5DokerComposeCNS6 мають бути з' єднаніCns7 Ми використовуємо Вбудова BERT для виявлення семантичного подібності:
// 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);
}
}
}
Цей крок є O(nM SK1 в межах обмеженого набору кандидатів ,, але кількість кандидатів обмежена IDF-фільтруванням та структурними сигналами MSC3 не розміром корпусу. Для подібного опису BERT вбудованийMNK5 читайте семантичні пошуки з компанією ONNX та BERT.
CLI підтримує три види видобутку за допомогою --extraction-mode:
dotnet run --project Mostlylucid.GraphRag -- index ./Markdown --extraction-mode heuristic
Використовуючи IDF + структурні сигнали для виявлення об 'єктівM SK1 з опціональною класифікаціям пакетів LLM. Нуль за дзвінки LLM на "-". - лише МSK1 дзвінок на один МSK2 podmiot для класифікації типу .
dotnet run --project Mostlylucid.GraphRag -- index ./Markdown --extraction-mode hybrid
Найкращий з обох світів:
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
Для 5 документів з 62 шматочками МSK2 гібридний режим робить 5 LLM дзвінки (vs МSK1 для повного LLM-модуM SK2 Ви отримуєте
dotnet run --project Mostlylucid.GraphRag -- index ./Markdown --extraction-mode llm
Полний підхід Microsoft GraphRAG: 2 LLM дзвінків на транзакцію ( видобуток властивості + виdobуток стосунків МSK2 Найдорожча МSK3 але найвища якість для неструктурованого тексту
| tryb | LLM дзвінки МSK2 найкращий для МSK3 |
|---|---|
| Герістичні МSK0 ~1 на МSK2 одиниці | швидке індексування мSK4 добре M- структурований спад МСК6 |
| Гібрид МSK0 1 за документ МSK2 Сбаланс coverage and quality | |
| ЛЛМ МSK0 2 на клаптику МSK2 Неструктурована проза , найвища якість မ်SK4 |
Для технічної документації, почати з гібрид режим. Дає вам семантичні відносини без гуристичний для чистої швидкості, або Лльм для наративного тексту.
Гібридний пошук об 'єднує два komplementarne підходи:
Dense (BERTM SK1 Усвідомлює значення. M SK1Докерні контейнери " збігає МSK3контейнеризація МSK4 Набір запасних частин (BMM SK1 Matches exact terms. M SK1HNSW" only matches "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
БММСК0 МSK1 Найкращий матч 25) оцінює документи на основі частоти терміну запиту МSK3 Формула МСК4
МSK0текст{оцінка_МСК0iМSK1n} МSK3текстМСК4IDFМ СК5qMСК6i+MССК7 \ MСК8cdot \ МСК9fracM СК10fMSК11q\ М СК12i=MС К13 DМ S К14 | \ \M С К15cdo \
Ключові інтуїції:
Для повного втілення програми BM25 см. гібридний пошук і індексування.
РР об 'єднує рейтинги з різних систем пошукуМСК0 Кожна позиція в рейтингі отримує рахунокМSK1
МСК0текстМSK1RRF}(dM SK3 MСК4 \сум_МSK0r \in RM SK2 МSK3frac{1}{k + rMSC6dMST7
Там, де $kM SK1 (звичайно МSK3 запобігає перетягуванню верхнього результату обидві рейтинги покращуються:
// 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;
}
До прикладу: Документ, в якому M SK1 порівнюється з щільним і #3 з вузьким
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;
}
Цей класифікатор навмисно простий - і легко замінити маленькою моделью на інтенцію пізніше . Якщо жодні об 'єкти не співпадають МSK2 система очищено деградує до чистого гібридного відтворення
# 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
Для 100 блогових постів
| МСК0 Операція | MSFT GraphRAG МSK2 Хирічна | Гібридна СМСК4 ЛЛМ МСК5 | ||
|---|---|---|---|---|
| Видобуток об 'єктів МSK1 МSK2 дзвінки | ||||
| покращення документів МSK1 МSK2 | ||||
| Класифікація МSK1 Включно МSK2 ~4 партія СМС4 СМС5 МС6 МС7 партію SМС8 | ||||
| Підсумки спільноти МSK1 МSK2 | MSК4 MSК5 МСК6 СМСК7 СМСК8 SМСК9 | |||
| Кількість дзвінків LLM | ~1,020 | ~24 | ~120 | ~1,024 |
| Якість стосунків | Семантика МSK1 Ко МSK2 Пробність | Сематика | ||
| Вартість (gptM SK1o -mini) | ~$5-10 | ~$0.15 | ~$0.75 | ~$5-10 |
| Вартість (OllamaM SK1 МSK0 N/A M SK2 $0 ♫ ♫ МSK4 ♫ |
Гібридний режим - це чудове місце для більшої кількості технічного контенту: ви отримуєте семантичні відносини M SK1 не лише співвідносини -occurrence МSK3 на МSK4 MSFT
Широкий замовлення-зM SK1оцінка величини; точний вартість залежить від розміру та швидкої форми
| Аспект МSK1 Хірістичний МSK2 Гібридний | ЛЛМ မ်SK4 MSFT GraphRAG M | |||
|---|---|---|---|---|
| Виявление об 'єктів | ІДФ МSK2 структура МSK3 ІдФ \ + структура | |||
| Зв 'язки МSK1 КоМСК2колишнє МСК3 ЛЛММ СК4непоширене МСК5 Ко\МСК6колише MSК7 Л ЛММСк8непотрібне MSК9 | ||||
| Подібні LLM дзвінки | ||||
| Якість стосунків | ||||
| Працює бездротово МSK1 Так МSK2 Так | ||||
| МSK0 Найкраще для | Швидше МSK2критичне | Рекомендується | Погадний компат | Неструктурований текст МSK2 |
Концептуально, це той самий трубопровод, що і DocSummarizer: спочатку збудувати конструкцію , потім дозволити LLM її розповісти
Де це руйнується: Фіксійний чи оповідковий текст без структурного маркеру. Невизначені стосунки без ліксичного сигналу . Надмірно двозначні назви об 'єктів, які потребують світових знань для неоднозначності
Втілення є мінімальним: - ~2,000 лінії по всіх цих файлах
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
Источник: Mostlylucid.GraphRag/
© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.