# DocSummarizer Part 5 - lucidRAGM SK2 Multi-Document RAG Web Application

<!--category-- AI, LLM, RAG, C#, HTMX, GraphRAG, Semantic Search, DuckDB -->
<datetime class="hidden">2026-01-01T18:00</datetime>

Il s'agit de **Partie 5** de la série DocSummarizer, et elleM SK1 est également le sommet du [GraphRAG](/blog/graphrag-minimum-viable-implementation) et [Série de recherche sémantique](/blog/semantic-search-with-onnx-and-qdrant). NousM SK1combinons tout en une application Web déployable.

> 🚨🚨 ARTICLE DE PREVIEW 🚨🚨 Je travaille encore sur quelques kinks et j’ajouterai des fonctionnalitésM SK2 Mais le noyau est bien fait et fonctionne bien. J’attends les mises à jour au cours des prochaines semainesMSC4 Il sera à lucidRAGMST5comMst6 J'ajouterais ici des screenshots une fois que je terminerai la conceptionM st8

> **Le but de la construction d’infrastructures RAG est de les utiliser pour quelque chose réel.**

Au cours des dernières semaines, nous avons construit

- **DocSummarizer** - Parsage de documents, fragmentation sémantique , Emplacements ONNX
- **GraphRAG** - Extraction d'entitésM SK1 Graphiques de connaissances, Détection communautaire
- **Recherche sémantique** Recherche hybride BERT avec fusion RRF

Maintenant, nous les connectons à **lucidRAG** - une application Web autonome pour répondre à des questions de documents multiples avec la visualisation d'un graphique de connaissances .

**Site Web:** [lucidrag.com](https://lucidrag.com) | **Source:** [GitHub](https://github.com/scottgal/mostlylucidweb/tree/main/Mostlylucid.RagDocuments)

[TOC]

## Que fait lucidRAG

Uploader les documents. poser des questionsM SK1 Obtenir des réponses avec des citations et un graphique de connaissance montrant comment les concepts se connectent.

**Principales caractéristiques:**

- **Multi- téléchargement de documents** avec le drag-etM SK1droppe
- **RAG Agentique** - Décomposition de la demande et autodestruction de celle-ciM SK1correction déterministe dans la structure, cycle de vie d’une seule demandeMSC5
- **Visualisation du graphique des connaissances** - Voir les relations des entités
- **Vue des preuves** - Sentence-sources citatives de niveau
- **Déploiement autonome** - Unité exécutable ou Docker

**Constraints de conception:**

- Aucune dépendance cloud pour l'indexation
- Prétraitement déterministe (chunking, emplacementM SK2 extraction d'entités )
- État de vecteur reconstruitable à partir des documents source
- LLMs utilisés *seulement* pour la synthèse des réponses sur les preuves récupérées

> À aucun moment, les LLM ne sont utilisés pour la mise en bloc, embedding, extraction de l’entitéM SK2 ou stockage - uniquement pour synthétiser des réponses sur celles qui ont été retiréesMska4 citationM Ska5 preuves corroborées Mska6

## Pourquoi combiner la recherche par vecteur + Graphiques de connaissances?

La recherche par vecteur ne fonctionne que pour certains types de demandes:

| Type de demande | Problème de recherche par vecteur | Résolution des graphiques M|
|------------|----------------------|----------------|
| CrossM SK1document | "Comment X se rapporte-t-il à Y?
| EntitéM SK1centrique | "Qu'en est-il de Docker?" \| Traversement d'un graphe à partir d'une entité ?
| Sommaires globaux | MSC2Thèmes principauxMSC3 | | 4 | Détection communautaire | |

lucidRAG utilise à la fois les vecteurs: pour l'exactitudeM SK1 les graphiques pour le contexte . Les demandes de graphe sont fondées sur la profondeur-limitée MNK4max MNK5 hopsMMK6 et portées aux documents récupérés afin d'empêcher une traversation sans limites sur des grandes corporationsMRK7

## Aperçu de l'architecture

L’application couche trois projets que nous avons déjà construits, ' et ,. **[StyloFlow](/blog/styloflow-signal-driven-workflows)** - un signal - moteur de travail dirigé:

```
lucidRAG
├── Controllers/Api/    # REST endpoints
├── Services/           # Business logic
│   ├── DocumentProcessingService   # Wraps DocSummarizer
│   ├── EntityGraphService          # Wraps GraphRAG
│   └── Background/                 # Async queue processing (StyloFlow waves)
└── Views/              # HTMX + Alpine.js UI
```

**Pourquoi StyloFlow?** Au lieu de pipelines hardcoded, chaque étape de traitement est un "onde" qui émet des signauxM SK3 Les ondes fonctionnent lorsque leurs conditions de déclenchement correspondent à celles de l’exécution parallèle. [StyloFlow:Signal-Orchestrement du flux de travail dirigé](/blog/styloflow-signal-driven-workflows) pour plus de détails sur la mise en œuvre.

## Le pipeline de traitement

Lorsque vous téléchargez un document, il passe par trois étapes :

### Étape 1: Déchargement et enchaînement

Le point final du téléchargement valide le fichier, calcule un hash de contenu pour la déduplication, et l'échelonne pour le traitement arrière-plan :

```csharp
public async Task<Guid> QueueDocumentAsync(Stream fileStream, string fileName)
{
    // Compute hash to detect duplicates
    var contentHash = ComputeHash(fileStream);

    var existing = await _db.Documents
        .FirstOrDefaultAsync(d => d.ContentHash == contentHash);
    if (existing != null)
        return existing.Id; // Already processed
```

La clé de l’incertitude: on achemine d’abordM SK1 on sauvegarde plus tard. Cela évite le gaspillage du temps de traitement lors des téléchargements répétés .

```csharp
    // Save to disk, create DB record
    var docId = Guid.NewGuid();
    await SaveFileToDiskAsync(fileStream, docId, fileName);

    // Queue for background processing
    await _queue.EnqueueAsync(new DocumentProcessingJob(docId, filePath));

    return docId;
}
```

### Étape 2: Configuration et emplacement

Le processeur arrière-plan prend les documents en queue et les lance par DocSummarizer:

```csharp
var result = await _summarizer.SummarizeFileAsync(job.FilePath, progressChannel);
```

Cette ligne unique fait beaucoup de travail (voir [DocSummarizer Part 1](/blog/building-a-document-summarizer-with-rag)):

- Parsez la structure du document (PDF, DOCXM SK2 Markdown )
- Split into semantic chunks respecting headings
- Générer des embeddings ONNX pour chaque tronçon
- Entreposer les vecteurs dans DuckDB avec indexation HNSW

### Étape 3: Extraction des entités

Après la fragmentation,, nous extraisons des entités en utilisant l’approche héuristique de GraphRAG'

```csharp
var segments = await _vectorStore.GetDocumentSegmentsAsync(documentId);
var entityResult = await _entityGraph.ExtractAndStoreEntitiesAsync(documentId, segments);
```

Cette méthode utilise les cotes IDF et les signaux structurels plutôt que par les appels -chunk LLM - voir [GraphRAG Partie 2](/blog/graphrag-minimum-viable-implementation) pour plus de détails.

## Voies limitées pour la pression arrière

Une mise en oeuvre naïve utiliserait des queues non limitées, risqueant d’être détournéesM SK1de -craques de mémoire pendant les inondations d’uploads. Nous utilisons des canaux limités avec des limites explicites de capacitéMSC4

```csharp
private readonly Channel<DocumentProcessingJob> _queue =
    Channel.CreateBounded<DocumentProcessingJob>(new BoundedChannelOptions(100)
    {
        FullMode = BoundedChannelFullMode.Wait
    });
```

Quand la liste est remplie, `Wait` mode bloque les nouveaux écritures jusqu’à ce que l’espace soit ouvert. Nous ajoutons un délai pour que les utilisateurs obtiennent une erreur claire au lieu d’en suspendre

```csharp
using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(ct);
timeoutCts.CancelAfter(TimeSpan.FromMinutes(5));

try {
    await _queue.Writer.WriteAsync(job, timeoutCts.Token);
} catch (OperationCanceledException) when (!ct.IsCancellationRequested) {
    throw new InvalidOperationException("Queue full. Try again later.");
}
```

## Per-Excès de temps des documents

Les documents de grande taille peuvent prendre quelques minutes à traiter. Mais un document bloqué ne devrait pas bloquer l’ensemble de la queue . Chaque document a sa propre période d’interruption

```csharp
while (!stoppingToken.IsCancellationRequested)
{
    var job = await _queue.DequeueAsync(stoppingToken);

    // 30-minute timeout per document
    using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(stoppingToken);
    timeoutCts.CancelAfter(TimeSpan.FromMinutes(30));

    try {
        await ProcessDocumentAsync(job, timeoutCts.Token);
    } catch (OperationCanceledException) when (!stoppingToken.IsCancellationRequested) {
        await MarkDocumentFailedAsync(job.DocumentId, "Processing timed out");
    }
}
```

Le symbole relié nous garantit que nous respectons toujours la fermeture de l'application tout en ajoutant la limite du document per-.

## Épuration des canaux de progrès

Chaque document de traitement reçoit un canal d’avancement pour les mises à jour SSE. Mais si l’utilisateur ferme son navigateur à la mi-parcoursMSC1upload, ce canal devient orphelinM SK3 Nous suivons le temps de création et nous nettoyons périodiquement :

```csharp
private readonly ConcurrentDictionary<Guid, ProgressChannelEntry> _progressChannels = new();

public int CleanupAbandonedChannels()
{
    var cutoff = DateTimeOffset.UtcNow - TimeSpan.FromHours(1);
    var cleaned = 0;

    foreach (var kvp in _progressChannels.Where(x => x.Value.CreatedAt < cutoff))
    {
        if (_progressChannels.TryRemove(kvp.Key, out var entry))
        {
            entry.Channel.Writer.TryComplete();
            cleaned++;
        }
    }
    return cleaned;
}
```

A `PeriodicTimer` appele cela tous les 15 minutes dans le processeur de fond

## Entreposage: DuckDB + PostgreSQLM SK2SQLite

Nous utilisons deux bases de données à des fins différentes:

**PostgreSQL/SQLite (EF CoreM SK2** stocke les métadonnées du document - ce qui existeM SK1 le statut de traitement , les relations. Ces données sont durables et vérifiables

**DuckDB** stores les vecteurs et le graphique de l'entité. Il ' est éphémère M SK2 vous pouvez le reconstruire à partir des documents source. Cette séparation signifie que la corruption du stockage des vecteurs ne se produit pas

```csharp
// Metadata in PostgreSQL
public class DocumentEntity
{
    public Guid Id { get; set; }
    public string Name { get; set; }
    public string ContentHash { get; set; }
    public DocumentStatus Status { get; set; }
}

// Vectors in DuckDB (managed by DocSummarizer)
// Entities in DuckDB (managed by GraphRAG)
```

## L'API de chat

Les questions s’écoulent dans le pipeline de recherche agentique:

```csharp
[HttpPost]
public async Task<IActionResult> ChatAsync([FromBody] ChatRequest request)
{
    // 1. Get or create conversation for memory
    var conversation = await GetOrCreateConversationAsync(request.ConversationId);

    // 2. Search with hybrid retrieval
    var searchResult = await _search.SearchAsync(request.Query, new SearchOptions
    {
        TopK = 10,
        IncludeGraphData = request.IncludeGraphData
    });
```

Le service de recherche traite la décomposition des demandes si nécessaire, puis synthétise une réponse:

```csharp
    // 3. Generate answer with LLM
    var answer = await _summarizer.SummarizeAsync(
        request.Query,
        searchResult.Segments,
        new SummarizeOptions { IncludeCitations = true });

    // 4. Save to conversation history
    await SaveToConversationAsync(conversation.Id, request.Query, answer);

    return Ok(new ChatResponse
    {
        Answer = answer.Text,
        Sources = answer.Citations,
        GraphData = searchResult.GraphData
    });
}
```

## L’UI: HTMX + AlpineM SK2js

L'interface est une seule page avec des documents à gauche, chat à droite:

```
┌──────────────────┬─────────────────────────────────────┐
│  📁 Documents    │  💬 Chat                            │
│  ─────────────   │  [Answer] [Evidence] [Graph]       │
│  [+ Upload]      │                                     │
│  📄 api-docs.pdf │  Q: How does auth work?            │
│  📝 readme.md    │  A: JWT tokens stored... [1][2]    │
│  ─────────────   │                                     │
│  🕸️ Graph: 168   │  ┌─────────────────────────────┐   │
│                  │  │ Ask about your documents... │   │
└──────────────────┴──┴─────────────────────────────┴───┘
```

Alpine.js gère l'étatM SK1 HTMX gère les mises à jour de la liste des documents:

```javascript
function ragApp() {
    return {
        messages: [],
        isTyping: false,

        async sendMessage() {
            const query = this.currentMessage.trim();
            this.messages.push({ role: 'user', content: query });
            this.isTyping = true;

            const result = await fetch('/api/chat', {
                method: 'POST',
                body: JSON.stringify({ query })
            }).then(r => r.json());

            this.messages.push({
                role: 'assistant',
                content: result.answer,
                sources: result.sources
            });
            this.isTyping = false;
        }
    };
}
```

## Mode de démonstration

Pour des déploiements publics comme lucidrag.com, mode de démonstration désactive les téléchargements et utilise le contenu pré-achevéM SK2 . Le mode de démonstration existe pour rendre les déploiements publics sécuritaires

```csharp
public class DemoModeConfig
{
    public bool Enabled { get; set; } = false;
    public string ContentPath { get; set; } = "./demo-content";
    public string BannerMessage { get; set; } = "Demo Mode: Pre-loaded RAG articles";
}
```

A `DemoContentSeeder` service arrière-plan surveille le répertoire de contenu et traite les fichiers sortants:

```csharp
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
    if (!_config.DemoMode.Enabled) return;

    await SeedExistingContentAsync();
    StartFileWatcher(_config.DemoMode.ContentPath);
}
```

Cela vous permet de mettre à jour le contenu de la démonstration en copient simplement les fichiers - aucun restart n'est nécessaire.

## Activation de lucidRAG

### Standalone (Sans dépendance)

```bash
dotnet run --project Mostlylucid.RagDocuments -- --standalone
```

Utilise SQLite + DuckDB localement. Ouvrir `http://localhost:5080`.

### Docker

```yaml
services:
  lucidrag:
    build: .
    ports: ["5080:8080"]
    depends_on: [postgres, ollama]
```

## Ce qui fonctionne réellement

| Composante | Source MSC2 But MSC3
|-----------|--------|---------|
| Parsage des documents | DocSummarizer | PDFM SK3 DOCX, Marquage ≥|
| Intégrations ONNX | DocSummarizer
| Extraction d'entités | GraphRAG | IDF M+ Signaux structurels P|
| Recherche hybride | Les deux MSC2 BMMSC3 SMC4 BERT avec RRF S|
| Traitement asynchrone | Nouveaux CNS2 Canaux limitésCns3 Interruptions de temps DNS4
| L'interface Web | Nouveau | HTMX + AlpineMSC4js S|

### Coût

**Aucun coût de l'API** pour l'indexation - les embeddings sont ONNX, les entités sont héuristesM SK2 vous payez seulement la synthèse de LLM au moment de la demande , et cela fonctionne avec Ollama localMSC4

## Articles connexes

- [DocSummarizer Part 1 - Architecture](/blog/building-a-document-summarizer-with-rag)
- [DocSummarizer Part 4 - Pipelines RAG](/blog/docsummarizer-rag-pipeline)
- [GraphRAG Partie 2 - Mise en œuvre](/blog/graphrag-minimum-viable-implementation)
- [Recherche sémantique avec ONNX](/blog/semantic-search-with-onnx-and-qdrant)