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

Det här är **Del 5** av DocSummarizer-serien , och den ' är också toppen på [GraphRAG-serien](/blog/graphrag-minimum-viable-implementation) och [Semantiska sökserien](/blog/semantic-search-with-onnx-and-qdrant). Vi, ', kombinerar allting till en användbar webbapp.

> 🚨🚨 PREVIEW ARTICLE | 🚨🚨 | Jag håller fortfarande på att utveckla några kinks och lägga till gränssnitt |. | Men kärnan är klar och fungerar bra |

> **Hela poängen med att bygga RAG-infrastruktur är att använda den för något verkligt.**

De senaste veckorna har vi byggt

- **DocSummarizer** - Dokumentparsering , semantisk chunkning, ONNX-inbäddar
- **GraphRAG** - Enhetens extraktion , kunskapsgrafer
- **Semantiska sökningar** - BMM SK1 + BERT hybridsökning med RRF fuktion

Nu kopplar vi ihop dem till **lucidRAG** - en självständig webapplikation för multi-documentans svar med kunskapsgrafvisualisering

**Website:** [lucidrag.com](https://lucidrag.com) | **källa:** [GitHub](https://github.com/scottgal/mostlylucidweb/tree/main/Mostlylucid.RagDocuments)

[TOC]

## Vad lucidRAG gör

Uploada dokument. ställa frågor. få svar med citat och en kunskapsgraf som visar hur koncept hör ihop

**Nyckelfunktionen:**

- **Multi-document Upload** med drag
- **Agentisk RAG** - Fragmentation och självfördelning av sökningar -korrigation begränsad, deterministisk i strukturen
- **Vetskapsgrafvisualisering** - Se enhetens relationer
- **Evidens syn** - mening -nivåcitet
- **Enkel användning** - Enkel utförbar eller Docker

**Designkonsträngningar:**

- Ingen moln beroende för indexering
- Deterministisk förprocessing (
- Ombyggbar vektor tillstånd från källdokument
- LLMs som används *bara* för svarssynteesi över uppfunna bevis

> I inga fall används LLM för att bryta ihop data, att inbädda, att extrahera föremål, att utvinna föremål eller att lagra dem, bara för att syntetisera svar över de som tagits fram, att citera, att lägga till bevis,

## Varför kombinera vektorsökningen + kunskapsgrafer?

Enkel vektorsökning bryts ner för vissa query-typer:

| söktyp | Vektorsökningsproblem | | | Graphlösning |
|------------|----------------------|----------------|
| CrossM SK1dokument | " Hur relaterar X till Y?" | | | Beståndsdelar som länkar mellan dokument
| EntitydM SK1centrisk | | | " Vad sägs om Docker? ♫ ?" ♫ | МSK5 ♫ Graph traversal from entity ♫
| Globala sammanfattningar

lucidRAG använder både: vektorer för precision , grafer för kontexten . Graphberättelserna är djupa- begränsade M SK4 max 2 hopsMska6 och skalade till sökta dokument för att förhindra obegränsad traversal på stora strukturerMske7

## Arkitektur överblick

Applayern visar tre projekt som vi redan har byggt, ' och ,. **[StyloFlow](/blog/styloflow-signal-driven-workflows)** - en signal

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

**Varför StyloFlow?** Istället för hårdkoderade pipeliner, är varje bearbetande steg en | " | våg " | som släpper ut signaler . | Flöder körs när de utlösande villkoren matchar |, | vilket gör det möjligt att utföra parallellt ♫ ( | inbäddar ♫+ | väsenextraktion ♫ [StyloFlow: Signal](/blog/styloflow-signal-driven-workflows) för implementeringsdetails.

## Processeringskedjan

När du laddar upp ett dokument

### Etappe 1: Upload och kö

Uploadens slutpunkt bekräftar filen, beräknar ett innehållshash för deduplikeringM SK1 och sätter upp queues för bakgrundsprocessing:

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

Den viktigaste insikten: vi harh först , spara senare. Det här förhindrar att slösa bort processningstid på kopierade laddade upp

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

### Etappe 2: Spelande och inbäddar

Den bakgrundsprocessören plockar upp alla dokument i kö och kör dem genom DocSummarizer:

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

Denna enda linje gör mycket arbete (se [DocSummarizer Part 1](/blog/building-a-document-summarizer-with-rag)):

- Föreläsning av dokumentstrukturen (PDF, DOCXM SK2 Markdown )
- Splittra in i semantiska chunks som respekterar opskrifte
- Generera ONNX inbäddar för varje stycke
- Speichera vektorer i DuckDB med HNSW indexering

### Etappe 3: Enhetens extraktion

Efter att ha hamnat, extraherar vi enheter med hjälp av GraphRAG,

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

Detta använder IDF-värdering och struktursignaler snarare än per -chunk LLM sändningar - se [GraphRAG-delen 2](/blog/graphrag-minimum-viable-implementation) för detaljer.

## Avgränsade kanaler för baktryck

En naiv implementering skulle använda obegränsade köer, som riskerar att ta sig ut- avM SK2 minneskracker under laddade uppfloderMSC3 Vi använder begränsade kanaler med explizit kapacitetgränser :

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

När köen fyller upp `Wait` mode blockerar nya skrivar tills utrymmet öppnas. Vi lägger till ett avbrott så att användaren får en tydlig fel istället för att hänga

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

Stora dokument kan ta minuter att processa.

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

Den länkade token säkerställer att vi fortfarande respekterar avstängning av applikationen medan vi lägger till per

## "Progress Channel Cleanup"

Varje bearbetande dokument får en framstegkanal för SSE-uppdateringar. Men om en användare slutar sin webblader mitt i tidenM SK1upload, så blir kanalen föräldralös . Vi följer skapadestid och städar upp periodisktMSC4

```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` kallar detta varenda 15 minut i bakgrundsprocessorn

## Storage: DuckDB + PostgreSQLM SK2SQLite

Vi använder två databaser för olika syften:

**PostgreSQL/SQLite (EF Core** lagrar dokumentets metadata - vad som existerar , bearbetande status, relationerM SK3 Den här datan är hållbar och efterfrågabarMSC4

**DuckDB** lagrar vektorer och enhetens graf.

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

## Chat API

Frågor strömmar genom den agentiska sökningskedjan:

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

söktjänsterna hanterar sökfrakturen om det behövs, och sedan syntetisera ett svar

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

## UI: HTMX + AlpineM SK2js

Interfejsen är en enda sida med dokument till vänster

```
┌──────────────────┬─────────────────────────────────────┐
│  📁 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 sköter tillståndet ; HTMX hanterar dokumentlistans uppdaterade versioner :

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

## Demo-modi

För offentliga användningsområden som lucidrag.com, avbryter laddade upp och använder förinlägset - laddad innehåll M SK3 Demo-läger existerar för att göra offentliga tolkningar säkrare MSC4 deterministiska , och billiga utan speciellaM SK6 kodspår

```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` bakgrundstjänsterna tittar på innehållskatalogen och bearbetar alla avlägsna filer:

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

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

Det här låter dig uppdatera demoinnehåll genom att bara kopiera plikter - ingen återstart krävs.

## Running lucidRAG

### Standalone ( Inga beroenden

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

Använder SQLite + DuckDB lokalt . Open `http://localhost:5080`.

### Docker

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

## Vad som egentligen körs

| Komponent | | | källa | | | syfte | МSK3
|-----------|--------|---------|
| Dokumentparsering | DocSummarizer
| ONNX-inbäddar | DocSummarizer | | | Lokala | МSK3 | inga API-klyver |
| Enhetens extraktion | | | GraphRAG | | | IDF | МSK3 | Struktursignaler ||
| Hybridsökning | Båda | BM25 + BERT med RRF M|
| Async-processing | Nya ♫ ♫ | ♫ Begränsade kanaler ♫
| webbgränssnitt ♫ ♫ | ♫ nytt ♫

### kostnaderna

**Null API-kostnader** för indexering - inbäddar är ONNXM SK1 enheter är heuristiska . Du betalar bara för LLM-synthes vid querytimet, och det fungerar med lokal Ollama

## Related Articles

- [DocSummarizer Part 1 - Arkitektur](/blog/building-a-document-summarizer-with-rag)
- [DocSummarizer Part 4 - RAG Pipelines](/blog/docsummarizer-rag-pipeline)
- [GraphRAG-delen 2 - Implementering](/blog/graphrag-minimum-viable-implementation)
- [Semantiska sökningar med ONNX](/blog/semantic-search-with-onnx-and-qdrant)