# Частина GraphRAG 2: Мінімальний непрозорий GraphRAG (Необов' язкові виклики для LLM)

<datetime class="hidden">2025-12-27T14:00</datetime>

<!-- category -- ASP.NET, GraphRAG, DuckDB, Vector Search, Machine Learning, Knowledge Graphs -->
Вхід [Частина 1](/blog/graphrag-knowledge-graphs-for-rag)Ми з'ясували, чому GraphRAG має значення. **мінімально життєздатний GraphRAG, який працює без окремих викликів LLM** "Прагматичний, автономний-перший, і досить дешевий, щоб працювати на ноутбуку:

- **DuckDB** для уніфікованого зберігання (вводів + графік у окремому файлі)
- **IDF + структурні сигнали** для видобування сутностей (без LLM на шматок)
- **BM25 + BERT- гібридний пошук** за допомогою RRF- синтезу
- **Ольямаjapan. kgm** для синтезу (нульові витрати API)

**Навігація серією:**

- [Частина 1: Основи GraphRAG](/blog/graphrag-knowledge-graphs-for-rag)
- **Частина 2: Мінімальний плавний GraphRAG** (Ця стаття)

> **Код:** [traphRag threelucid. GraphRag](https://github.com/scottgal/mostlylucidweb/tree/main/Mostlylucid.GraphRag) на GitHub

[TOC]

## Огляд архітектури

```mermaid
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
```

## Почему ДакDB?

Microsoft' GraphRAG використовує окреме сховище для векторів (LanceDB), об' єктів (паралети), та взаємозв' язків (більше Parquet). DuckDB спрощує це:

- Одинарна `.duckdb` файл для всього
- Пошук за вектором HNSW за допомогою додатка VSS
- SQL для обох векторних пошуків і побудови графів
- Складність нульового розгортання

**ДакБ - це не база даних графів, і в цьому суть.** Це не Neo4j. Traversals неглибокі, базовані на SQL і обдумані.

## Схема зберігання

Використання схеми **Приєднатися до таблиць для перевірки** - мы можем спросить "который кусок упоминает объект X?" прям так:

```mermaid
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[]` для перевірки**. Приєднуйтесь до таблиць?`entity_mentions`, `relationship_mentions`) увімкнути такі ефективні запити, як "знайдіть всі шматки, що згадують Докера."

### Пошук векторів: Плівка HNSW

Індекс HNSW від DuckDB викликає лише з `array_cosine_distance` + `ORDER BY` + `LIMIT`:

```csharp
// 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` не використовувати індекс** На нетривіальній корпорі, це перетворює запит 5мс у секунди.

## Видобування сутності

Тут ми розходимося з підходом Microsoft замість **2 LLM виклики на шматок**, ми використовуємо **Базоване на IDF статистичне видобування**Мета не ідеальна. *стійкі, корпус- відносні сигнали* для цього не потрібен LLM:

```mermaid
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
```

### Чому IDF, не закодовані списки?

Наївний підхід запрограмований. `HashSet<string> KnownTech = { "Docker", "Kubernetes", ... }`. Переривання для:

- Нові технології (вам потрібно оновити список)
- Особливості доменів (різні корпуси = різні об' єкти)
- Помилкове написання та варіації

**IDF (Зворотна частота документа)** Розв' язує це статистично. IDF терміна:

$$\ text{ IDF}}} {лог) =\log\frac{ N} {df} $$

Де:

- $N$ = загальні документи (унці)
- $df}t) $$ документи, що містять термін $t$

Високий IDF = **рідкісний термін** =, ймовірно, елемент. " Docker " з' явиться у 5/ 100 шматках має вищий IDF, ніж " IDF " з' явиться у 100/ 100.

Більше про TF-IDF і BM25, див. мій допис [гібридний пошук з BM25](/blog/rag-hybrid-search-and-indexing).

### Структурні сигнали

Структура розмітки показує, що є важливим:

- **Заголовок** (`## Docker Setup`) → суттю
- **Вбудований код** (`` `docker-compose` ``) → суттю
- **Посилання** (`[Docker](https://docker.com)`) → сутність + взаємозв' язки

```csharp
// 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);
    }
}
```

### Видобування зв' язку (високі зв' язки)

Помітки посилань **явний** взаємозв' язки, які не вимагають LLMCEction:

```csharp
// 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 " і " DockerCombose " мають бути об' єднані. Ми використовуємо **Вбудовування БЕРТ** для визначення семантичної подібності:

```csharp
// 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'n2 всередині набору кандидатів, але значення кандидата обмежено фільтрацією IDF та структурними крапками } Не розміром copus. Подробиці щодо вбудовування BERT, дивіться [Семантичний пошук з ONNX і BERT](/blog/semantic-search-with-onnx-and-qdrant).

## Hybrid Search: BM25 + BERT

Гибридний пошук комбінує два додаткові підходи:

**Щільність (НРТ):** Знаходить значення. " Контейнери " відповідають " конфігурації ."
**Спаз (BM25):** Пошук точних термінів. " HNSW " відповідає лише " HNSW ."

```mermaid
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?

BM25 (найкращий збіг 25) оформляє результати документів на основі частоти терміну запиту. Формула:

$$\текст {скрип}} Д, Q) =\ sum_{i=1}^{ n}\text{ IDF}}} {q_ i)\cdot\frac{ f} i, D) \cdot (k_ 1 + 1) } f}q_ i, D) + k_ 1\cdot (1 - b + b\cdot {drac{ d}%} $$$

Інтуїції ключа:

- **Ключ IDF**: Rare words matter more ("HNSW" > "The")
- **Насиченість TF**: Слово, що з'являється у 10х, не більше 10х, ніж 1х
- **Нормалізація тривалості**: Довгі документи не отримують несправедливої користі

Повноцінна реалізація BM25, див. [гібридний пошук і індексування](/blog/rag-hybrid-search-and-indexing).

### Reciprocal Rank Fusion (RF)

Рейтинги об' єднання RRF з різних систем отримання даних. Кожна позиція у рангах отримує рахунок:

$$\ text{ RRF} {d) =\sum_{r\ in R} \frac{ 1}Кл + r=d)} $$

Де за k$ (зазвичай 60 доларів) можна уникнути надмірної ваги результату. **обидва** Рейтинги підвищуються:

```csharp
// 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;
}
```

Приклад: шапка документа # 1 у щільності і # 3 у розрідженні:

- Щільність: $1/⇩60+1) = 0, 0164$
- Спара: 1 / +60+3) = 0, 0159$
- **Комбіновано: 0. 02323** (вище, ніж будь-хто один)

## Режими запиту

```mermaid
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
```

### Класифікація запиту

```csharp
// 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;
}
```

Цей класифікатор спеціально простий } і його легко замінити маленькою моделлю  меті пізніше. Якщо нічого не співпадає, система повертається до чистого гібридного отримання.

## Використання CLI

### Індексування

```bash
dotnet run --project Mostlylucid.GraphRag -- index ./test-markdown
```

```text
GraphRAG Indexer
  Source: test-markdown
  Database: graphrag.duckdb
  Model: llama3.2:3b

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 relationships: 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    │
└───────────────┴───────┘
```

### Запит

```bash
dotnet run --project Mostlylucid.GraphRag -- query "How do I use Docker Compose?"
```

```text
──────────────────── 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)
```

### Стани

```bash
dotnet run --project Mostlylucid.GraphRag -- stats
```

```text
─────────────── GraphRAG Database Stats ────────────────
┌───────────────┬───────┐
│ Metric        │ Count │
├───────────────┼───────┤
│ Documents     │     5 │
│ Chunks        │    62 │
│ Entities      │   168 │
│ Relationships │   312 │
│ Communities   │    10 │
└───────────────┴───────┘

Database size: 7.76 MB
```

## Порівняння вартості

Для 100 дописів блогу (~500 шматків):

♪
|-----------|-------------------|---------------------|
дрімає 2 × 500 = 1.000 викликів LLM = 01 (необов'язкова дужка) +
Перед нами - Космос (безкоштовність)
Дівчата мають значення +20.
| **Загальний індексування** 1,020- целей ~20- циклів
| **Вартість (gpt- 4o- mini)** | ~$5-10 | ~$0.10 |
| **Вартість (Оллама)** Пн/А' о. о. amount in units (optional, rarely needs a translation)

**Від одного до двох порядків величини дешевше** у структурованому технічному змісті (заголовок з заголовками, блоками коду і посиланнями).

## Торгові міниunit synonyms for matching user input

♪Цього разу ♪ LLM-Heavy GraphRAG ♪
|---------------------|-------------------|
 щас, дешеве індексування* } Сутність сутностей}
Д. д. д. д. д. д. д. д.:
♪ So-occurensions ♪ Семантичні взаємозв'язки ♪
♪Output's API access's application

> **Там, де це розривається:** Фіктивний або обліковий текст без структурної розмітки. Неявні взаємозв' язки без лексикального сигналу. Високо двозначні назви сутностей, які вимагають від світових знань, щоб їх розрізнити.

## Код

Реалізація мінімальна - 11 файлів, ~ 1500 рядків:

```
Mostlylucid.GraphRag/
├── Storage/GraphRagDb.cs        # DuckDB with HNSW + provenance
├── Services/EmbeddingService.cs # ONNX BERT wrapper
├── Services/OllamaClient.cs     # LLM client
├── Extraction/EntityExtractor.cs # Heuristic + link extraction
├── 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
└── Program.cs                   # CLI
```

**Джерело:** [`Mostlylucid.GraphRag/`](https://github.com/scottgal/mostlylucidweb/tree/main/Mostlylucid.GraphRag)

Ця реалізація ділиться своєю основною філософією з [DocSummarizer](/blog/docsummarizer-tool): спочатку вийняти детермінатичну структуру, нехай LLM пояснює її.

## Супутні повідомлення

- [Частина 1: Основи GraphRAG](/blog/graphrag-knowledge-graphs-for-rag) - Чому графіки знань покращують RAG
- [Hybrid Search with BM25](/blog/rag-hybrid-search-and-indexing) - Глибше пірнання в BM25 оцінці та індексування.
- [Семантичний пошук з ONNX і BERT](/blog/semantic-search-with-onnx-and-qdrant) - БЕРТ вбудовується в НЕТ
- [Інструмент DocSummarizer](/blog/docsummarizer-tool) - Мій інструмент резюме документів, який використовує код з цим

## Зовнішні ресурси

- [Розширення VSS DuckDB](https://duckdb.org/docs/extensions/vss.html) - Векторний пошук HNSW
- [Папір Лейдена- Алгоритма](https://arxiv.org/pdf/1810.08473.pdf) - Виявлення спільноти
- [Папір RRF](https://plg.uwaterloo.ca/~gvcormac/cormacksigir09-rrf.pdf) - Реципрокальне фузіонування.