DocSummarizer Part 5 - lucidRAGM SK2 Multi-Document RAG Web Application (Svenska (Swedish))

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

Thursday, 01 January 2026

//

8 minute read

Det här är Del 5 av DocSummarizer-serien , och den ' är också toppen på GraphRAG-serien och Semantiska sökserien. 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 | källa: GitHub

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

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

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

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

Denna enda linje gör mycket arbete (se DocSummarizer Part 1):

  • 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,

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

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

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.

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

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.

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

[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

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

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

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:

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

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

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

Docker

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

Finding related posts...
logo

© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.