# DocSummarizer Part 5 - lucidRAG: MultiM SK3Document RAG Web Application

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

Це **Частина 5** з серії DocSummarizer – ,, а це – МSK1. Це також вершина [Серії GraphRAG](/blog/graphrag-minimum-viable-implementation) і [Серія Semantic Search](/blog/semantic-search-with-onnx-and-qdrant). Ми ' поєднаємо все в веб-приложений, який можна застосувати

> 🚨🚨 АКТУАЛЬНЫЙ АRTICLE МSK1 Ми все ще працюємо над деякими кінками та додаємо додаткиM SK2 Але ядро зроблено і працює добре. Сподіваюсь на поновлення протягом наступних кількох тижнівMSC4 Це буде на lucidRAGMST5comMst6 ЯM stsк7 Я додам скриншоти тут, як тільки я вичерплю дизайнMSt8

> **Весь сенс створення інфраструктури РАG - використати її для чогось реального.**

За останні кілька тижнів ми створили

- **DocSummarizer** - Дослідження документівM SK1 семантичне відокремлення, Вбудова на NX
- **GraphRAG** - Extraction Entity , knowledge graphsM SK2 community detection
- **Семантичні пошуки** - BMM SK1 + Гібридний пошук BERT з синтезом РРФ

Тепер ми підключимо їх до **lucidRAG** - автономна веб-аplikaція для багатостороннього -розпитів на питання з допомогою візуалізації графів знань

**Вебсайт:** [lucidrag.com](https://lucidrag.com) | **Источник:** [GitHub](https://github.com/scottgal/mostlylucidweb/tree/main/Mostlylucid.RagDocuments)

[TOC]

## Що робить lucidRAG

завантажити документи. Задавати питанняM SK1 отримати відповіді з цитатами та графом знань, що показує, як концепти пов 'язані

**Головні характеристики:**

- **Завантаження документу Multi-** з перетягуванням
- **Агентська РАГ** - Декомпозиція та самовідтворення запиту МSK1 Koreкція МSK2 обмежені , детерміністична структура
- **Візуалізація графів знань** - Погляньте на взаємозв 'язки podmiotів
- **Погляньмо на докази** - реченняM SK1за citations на рівні
- **Саморозгортання** - Одноразовий інтерактивний або Докер

**Дизайнові обмеження:**

- Немає залежності від хмар для індексування
- Детерміністичне попереднє оброблення ( підштовхування МSK1 введення МSK2 видобуток об 'єктів )
- Перетворити векторний стан з вихідних документів
- Використововані LLM *лише* для синтезу відповідей за отримані докази

> В жодному випадку LLM не використовується для відокремлення, введення, вилучення об 'єктівM SK2 або зберігання MSC3 лише для синтезу відповідей над отриманимиMSL4 цитатаMKL5 підтверджений доказMCL6

## Чому об 'єднати пошук векторів + Графи знаньM SK1

Вektorний пошук розривається лише для певних типів запитів:

| Тип запиту МSK1 Проблема з пошуком векторів МSK2 Вигадка графа |
|------------|----------------------|----------------|
| КрестніM SK1документ МSK2 МSK3Як X пов 'язано з Y?" \| Об' єднання об 'єктів між Docs |
| Об 'єктM SK1центричний | МSK3 Як щодо Докера?" \| Перехід графу від об' єкта МSK6
| Глобальні підсумки МSK1 МSK2Главні темиM SK3 ♫ | Детективність в суспільстві ♫ МSK5 ♫

lucidRAG використовує як вектори : для точності, так і графи , для контексту, МSK2 Попити на графи мають глибину

## Уявлення про архітектуру

Ця програма поєднує три проекти, які ми вже збудували: **[StyloFlow](/blog/styloflow-signal-driven-workflows)** - сигнал - двигун робочого потоку МSK2

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

**Чому StyloFlow?** Замість закодованих трубок , кожен етап обробки - це МSK1 хвиля МSK2, яка випромінює сигнали . хвиле запускаються тоді, коли умови їх триггерування співпадають | МSK4 | що дає можливість паралельного виконання | ( | вмонтування |+ | видобуток об 'єктів [StyloFlow: Сигнель-Організація потоку роботи](/blog/styloflow-signal-driven-workflows) для подібних деталей.

## Переробка трубопроводу

Коли ви завантажуєте документ, він проходить через три етапи:

### Стадія 1: Завантаження і черга

Даний кінцевий пункт завантаження підтверджує файл, обчислює хаш контенту для відокремленняM SK1 і ставить його в чергах для обробки фона:

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

Найголовніша ідея: ми хашуємо першими , заощаджуємо пізніше МSK2 Це запобігає марнуванню часу на обробку подібних завантажень

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

### Стадія 2: Заряди та вбудова

Пізніше обробник підбирає заліковані документи і запускає їх через DocSummarizer:

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

Ця одна лінія робить багато роботи (гляньте [DocSummarizer Part 1](/blog/building-a-document-summarizer-with-rag)):

- Розшифруйте структуру dokumentu (PDF, DOCXM SK2 Markdown )
- Розділити на семантичні шматочки стосовно заголовків
- Створити вбудовані додатки для кожного шматка
- Поміщайте вектори в DuckDB з індексуванням HNSW

### Стадія Extraction Entity 3:

Після відокремлення, ми виділяємо об 'єкти, використовуючи геріатичний підхід GraphRAG '

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

Це використовує IDF оцінку та структурні сигнали, а не за один -chunk LLM дзвінок - patrz [GraphRAG - частина 2](/blog/graphrag-minimum-viable-implementation) для деталей.

## Обмежені канали для зворотнього тиску

Наївна інсталяція використовувала б безмежні черги.

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

Коли черга заповнюється, `Wait` режим блокує нові записи доки простір не відкривається. Ми додаємо тайм-аут, щоб користувачі отримали чітку помилку замість того, щоб сповісити:

```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-Timeouts документу

Великі документи можуть займати хвилини, щоб їх обробити. Але застряглий документ має не блокувати всю чергу ' each document gets its own timeout МSK3

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

Поєднаний символ гарантує, що ми все ще дотримуємося замкнутої програми, додаючи limit для документу per-document limitM SK1

## Процедура очистки каналів

Кожен документ, що обробляється, отримує канал прогресу для оновлення SSE. Але якщо пользователь закриває свій браузер всерединіMSC1upload, цей канал стає сиротоюM SK3 Ми відстежуємо час створення і регулярно очищаємоМSK4

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

А `PeriodicTimer` називає це кожні 15 хвилини в фоновому процесорі

## Storage: DuckDB + PostgreSQLM SK2SQLite

Ми використовуємо дві бази даних для різних цілей:

**PostgreSQL/SQLite (EF CoreM SK2** зберігає метадані dokumentu - те, що існує , статус обробки МSK2 відносини

**Дук-ДБ** зберігає вектори і граф об 'єкту. Це ' ефемерний M SK2 ви можете побудувати його з джерельних документів. Це відокремлення означає, що уektorи зберігають коррупцію не

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

## API чату

Питання протікають через канал агентичного пошуку:

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

Поиск регулює декомпозицію запиту, якщо необхідно, потім синтезує відповідьM SK1

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

В інтерфейсі є одна сторінка з документами ліворуч, чат праворучМSK1

```
┌──────────────────┬─────────────────────────────────────┐
│  📁 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 управляє состоянием; HTMX обробляє поновлення списку документівM SK2

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

## Модій демонстрації

Для публічних застосувань, таких як lucidrag.comM SK1 режим демонстрації блокує завантаження та використовує до-завантаження контентуMSC3 tryb демонстрації існує для того, щоб зробити громадські застосування безпечнимиMST4 детерміністичнимиM ST5 і дешевими без спеціальногоM st6 кодових шляхівMst7

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

А `DemoContentSeeder` служба фона спостерігає за змістом каталогу і обробляє будь-які відкинуті файли:

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

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

Це дає вам змогу оновити зміст демонстрації, просто копіюючи файли - не потрібно перезапускатиM SK1

## Працюючий lucidRAG

### Самотній (Не має жодних залежностіM SK1

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

Використовуючи SQLite + DuckDB локально. Open `http://localhost:5080`.

### Докер

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

## Що насправді відбувається

МSK0 Komponenт | Źródło МSK2 Задача |
|-----------|--------|---------|
| Дослідження Dokumentів МSK1 DocSummarizer | PDFM SK3 DOCX, Замітка МSK5
| вбудова на NX | DocSummarizer МSK2 локальний
| Видобуток об 'єктів
| Гібридні пошуки МSK1 Обидві МSK2 BMM SK3 + BERT з РРФ мSK5
| Асинхове оброблення МSK1 Нові МSK2 Замкнуті канали , відхилення часу
| Веб інтерфейс МSK1 Новое МSK2 HTMX + Альпіjski MSК4 js МСК5

### Ціна

**Нуль هزینه API** для індексування - вбудова - ONNX , об 'єкти - эвристичні МSK2 Ви платите тільки за синтез LLM в часі запиту МSK3 і це працює з локальною Олламі

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

- [DocSummarizer Part 1 МSK1 Архітектура](/blog/building-a-document-summarizer-with-rag)
- [DocSummarizer Part 4 - РАG Pipelines](/blog/docsummarizer-rag-pipeline)
- [GraphRAG частина 2 - Втілення](/blog/graphrag-minimum-viable-implementation)
- [Семантичні пошуки з компанією ONNX](/blog/semantic-search-with-onnx-and-qdrant)