# GraphRAG Partie 2: Minimum viable

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

<!-- category -- ASP.NET, GraphRAG, DuckDB, Vector Search, Machine Learning, Knowledge Graphs -->
À l'intérieur [Partie 1](/blog/graphrag-knowledge-graphs-for-rag), 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:**

- [Partie 1: GraphRAG Fundamentals](/blog/graphrag-knowledge-graphs-for-rag)
- **Partie 2: GraphRAG minimal viable** (le présent articleM SK1

> **Code:** [Mostlylucid.GraphRag](https://github.com/scottgal/mostlylucidweb/tree/main/Mostlylucid.GraphRag) sur GitHub

[TOC]

## Aperçu de l'architecture

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

## 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?"

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

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

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

**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.

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

### 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](/blog/rag-hybrid-search-and-indexing).

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

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

### 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'

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

### 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:

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

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](/blog/semantic-search-with-onnx-and-qdrant).

## Modes d'extraction

Le CLI appuie trois modes d'extraction par `--extraction-mode`:

### Mode héuristique (Default)

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

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

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

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

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

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

### 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_{iM SK1n} \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](/blog/rag-hybrid-search-and-indexing).

### 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:

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

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

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

### Classification de la demande

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

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

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

### Demande de renseignements

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

### Statistiques

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

## 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](/blog/docsummarizer-tool): 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/`](https://github.com/scottgal/mostlylucidweb/tree/main/Mostlylucid.GraphRag)

## Postes connexes

- [Partie 1: GraphRAG Fundamentals](/blog/graphrag-knowledge-graphs-for-rag) - Pourquoi les graphiques de connaissance améliorent le RAG
- [Recherche hybride avec BM25](/blog/rag-hybrid-search-and-indexing) - Une profonde plongée dans le BMM SK1 la cotation et l’indexation
- [Recherche sémantique avec ONNX et BERT](/blog/semantic-search-with-onnx-and-qdrant) - Emplacements BERT dans le .NET
- [Outil DocSummarizer](/blog/docsummarizer-tool) - Mon outil de synthèse du document qui partage le code avec ce

## Ressources externes

- [Extension DuckDB VSS](https://duckdb.org/docs/extensions/vss.html) Recherche par vecteur - HNSW
- [Document d'algorithme de Leiden](https://arxiv.org/pdf/1810.08473.pdf) - Détection communautaire
- [Papier RRF](https://plg.uwaterloo.ca/~gvcormac/cormacksigir09-rrf.pdf) - Fusion de rang réciproque