# GraphRAG частина 2: Minimal Viable Graph RAG

<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 має значення . Тепер давайте МSK2 побудуємо **мінімальний життєздатний граф РАГ** з трьома режимами видобутку:

| tryb | LLM дзвінки МSK2 найкращий для МSK3
|------|-----------|----------|
| **Герістичні** (передбачуванийM SK1 МSK2 \0 на шматок МSK4 швидке indeksування , структурне віднімання M SK6
| **Гібрид** МSK0 1 за документ МSK2 Баланс швидкості та якості |
| **ЛЛМ** МSK0 2 на шматок МSK2 Максимальна якість об 'єкта |

Усі режими використовуються

- **Дук-ДБ** для об 'єднаного зберігання графу ( вектори МSK1 в одному файлі
- **БММСК0 МSK1 Гібридний пошук BERT** за допомогою синтезу РРФ
- **Оллама** для синтезу та опціональної класифікації пакетів (нуль вартості APIM SK1

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

- [Частина 1: GraphRAG Fundamentals](/blog/graphrag-knowledge-graphs-for-rag)
- **Частина 2: Minimal Viable GraphRAG** (таков artykułМSK1

> **Код:** [Найяскравіший.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
```

## Чому DuckDB?

Microsoft's GraphRAG використовує відокремлене зберігання векторів (LanceDBM SK2 об 'єктів МSK3Parquet), та стосунків

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

**DuckDB - це не графічна база даних -, а те, що** Це не "'", а Neo . МSK1 , j , . . Траверзалі є плиткими. МSK3 , SQL , МSK4 , базовані на МSK5 і навмисно обмірковані.

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

skeма використовує **об 'єднати таблички для провінції** - ми можемо задати питання ", які фрагменти вказують на об 'єкт 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[]` для походження**. Приєднуйтесь до столів МSK1`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` не буде використовувати індекс** - він вигравM SK1 не запускає індексу HNSW . На не-- банальних корпораціях МSK4 це перетворює МSK5 ms indeksованого запиту на повну таблицю сканування .

## Видобуток

Ось де ми відокремлені від підхіду Microsoft' **LLM-per-переходи до видобутку** застосовується в Microsoft's референції Pipeline GraphRAG, ми використовуємо **Статистика з IDF-**. Метою є не ідеальні об 'єкти, а ціла *стабільні, корпусM SK1 відносні сигнали* що не потребують 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, не має hardcoded lists?

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

- Нові технології ( ви ' вам потрібно обновити список
- Домен-спеціальні терміни M SK1різноманітний корпус = різні об 'єкти МSK3
- Відхилення та варіації

**IDF (Взамінна частота документуM SK1** обчислює це статистично.

МСК0текстМSK1IDF}(t) M SK4 ♫ ♫ МSK5лог ♫ \frac ♫

Де:

- МSK0N$ M SK2 kopā
- $dfM SK1t)$ = документи, що містять термін МSK4t+$

Високое ІДФ = **рідкісний термін** = швидше за все, те, що об 'єкт МSK1 МSK2 Докер ", який з' являється в мSK4 з chunks M100, має вищий рівень IDF, ніж МСК6 те МПС7, що з 'являється у MPС8 з МУС9

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

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

Структура позначки вказує нам, що

- **Заголовки** (`## Docker Setup`МSK0 → одиниця
- **Код в лінії** (`` `docker-compose` ``МSK0 → одиниця
- **Ліки** (`[Docker](https://docker.com)`МSK0 → одиниця МSK2 стосунки

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

### Видобуток зв 'язку ( ВисокоM SK1Досконалість стосунків)

Ссылки на позначку **яскраво** відносини, які не потребують LLM висновку

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

### Дедупликація за допомогою BERT Embeddings

Назви об 'єктів, такі як "Docker Compose", "dockerM SK3composeMSC4 і МSK5DokerComposeCNS6 мають бути з' єднаніCns7 Ми використовуємо **Вбудова BERT** для виявлення семантичного подібності:

```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(nM SK1 в межах обмеженого набору кандидатів ,, але кількість кандидатів обмежена IDF-фільтруванням та структурними сигналами MSC3 не розміром корпусу. Для подібного опису BERT вбудованийMNK5 читайте [семантичні пошуки з компанією ONNX та BERT](/blog/semantic-search-with-onnx-and-qdrant).

## Моди видобутку

CLI підтримує три види видобутку за допомогою `--extraction-mode`:

### Хирологічний режим (DefaultM SK1

```bash
dotnet run --project Mostlylucid.GraphRag -- index ./Markdown --extraction-mode heuristic
```

Використовуючи IDF + структурні сигнали для виявлення об 'єктівM SK1 з опціональною класифікаціям пакетів LLM. **Нуль за дзвінки LLM на "-".** - лише МSK1 дзвінок на один МSK2 podmiot для класифікації типу .

### Гібридний режим (РекомендуєтьсяM SK1

```bash
dotnet run --project Mostlylucid.GraphRag -- index ./Markdown --extraction-mode hybrid
```

Найкращий з обох світів:

1. **Хирологічна діагностика**: ІДФ МSK1 структурні сигнали знаходять кандидатів на учасника
2. **Поліпшення LLM**: Один дзвінок на *документ* підтверджує сутності і виділяє семантичні відносини

```mermaid
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 Ви отримуєте

- Детерміністичне покрытие об 'єктів геуристики
- LLM-видобуток стосунків якості МSK1семантикаM SK2 не лише коMSC3колишнє)
- описи та підтверджені типи

### У trybie LLM (Microsoft-StyleM SK2

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

Для технічної документації, почати з **гібрид** режим. Дає вам семантичні відносини без **гуристичний** для чистої швидкості, або **Лльм** для наративного тексту.

## Гібридний пошук: BM25 M SK2 BERT

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

**Dense (BERTM SK1** Усвідомлює значення. M SK1Докерні контейнери " збігає МSK3контейнеризація МSK4
**Набір запасних частин (BMM SK1** Matches exact terms. M SK1HNSW" only matches "HN SWMSC4

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

БММСК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 \

Ключові інтуїції:

- **Термин ІДФ**: рідкісні слова мають більше значення
- **Насилення TF**: Слова, що з 'являються 10x, не є МSK2t МSK3x більш значущими за မ်SK4x
- **Нормалізація довжини**: Довгі документи не отримують несправедливої переваги

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

### Реципрочний рейтинг термоядерного синтезу (RRFM SK1

РР об 'єднує рейтинги з різних систем пошукуМСК0 Кожна позиція в рейтингі отримує рахунокМSK1

МСК0текстМSK1RRF}(dM SK3 MСК4 \\сум_МSK0r \in RM SK2 МSK3frac{1}{k + rMSC6dMST7

Там, де $kM SK1 (звичайно МSK3 запобігає перетягуванню верхнього результату **обидві** рейтинги покращуються:

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

До прикладу: Документ, в якому M SK1 порівнюється з щільним і #3 з вузьким

- Dense: M SK1 | = | | МSK3
- Набір запасних частин: M SK1
- **Об 'єднані: M SK1** (вище, ніж одиниці

## Моди запитів

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

Цей класифікатор навмисно простий - і легко замінити маленькою моделью на інтенцію пізніше . Якщо жодні об 'єкти не співпадають МSK2 система очищено деградує до чистого гібридного відтворення

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

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

```bash
# 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
```

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

### Попит

```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 блогових постів

МСК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](/blog/docsummarizer-tool): спочатку збудувати конструкцію , потім дозволити 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/`](https://github.com/scottgal/mostlylucidweb/tree/main/Mostlylucid.GraphRag)

## Подібні статті

- [Частина 1: GraphRAG Fundamentals](/blog/graphrag-knowledge-graphs-for-rag) - Чому графи знань покращують РАG
- [Гібридні пошуки з BM25](/blog/rag-hybrid-search-and-indexing) - Глубокий подив на BMM SK1 показники та індексування
- [Семантичні пошуки з комп 'ютерами ONNX та BERT](/blog/semantic-search-with-onnx-and-qdrant) - Введення BERT в .NET
- [Програмне забезпечення DocSummarizer](/blog/docsummarizer-tool) - Моє інструмент для підсумування документу, який ділиться кодом з цим

## Внешні ресурси

- [Поширення VSS DuckDB](https://duckdb.org/docs/extensions/vss.html) - Поиск вектора HNSW
- [Алгоритм Лейдена](https://arxiv.org/pdf/1810.08473.pdf) - Community detection
- [РРФ Папер](https://plg.uwaterloo.ca/~gvcormac/cormacksigir09-rrf.pdf) - Об 'єднання рангів