# 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 , و هو ' أيضاً الذروة من [سلسلة GraphRAG](/blog/graphrag-minimum-viable-implementation) و [سلسلة البحث الدلالي](/blog/semantic-search-with-onnx-and-qdrant). نحن' ندمج كل شيء في تطبيق على الانترنت قابل للتطبيق

> 🚨🚨 أRTICLE PREVIEW 🚨🚨 مازلت أعمل على بعض اللاصق وإضافة الخصائص. لكن الجوهر قد تم عمله و يعمل بشكل جيدM SK3 نتوقع تحديثات خلال الأسابيع القليلة القادمةMSC4 ستكون في lucidRAGMST5comMst6 سأقوم بإضافة شاشات الشاشة هنا بمجرد أن أخرج التصميمM st8

> **الفكرة الأساسية لبناء بنية تحتية RAG هي استخدامها لشيء حقيقي**

خلال الأسابيع القليلة الماضية

- **DocSummarizer** - تحليل الوثائق, تجزئة لغويةM SK2 مداخلات أونكس
- **GraphRAG** - إستخراج الكائناتM SK1 رسومات المعرفة, كشف المجتمع
- **البحث الدلالي** - BM25 + بحث بيرت هجين مع الإنصهار RRF

الآن نربطهم معاً في **lucidRAG** - تطبيق ويب مستقل لعدة -إجابات على أسئلة مستندة مع رسم بياني للمعرفة

**الموقع الإلكتروني:** [lucidrag.com](https://lucidrag.com) | **المصدر:** [GitHub](https://github.com/scottgal/mostlylucidweb/tree/main/Mostlylucid.RagDocuments)

[TOC]

## ما يفعله lucidRAG

تحمّل الملفات. أسأل الأسئلة. تحصل على الإجابات مع الاستشهادات ورسم بياني للمعرفة يوضح كيف ترتبط المفاهيمM SK2

**الخصائص الرئيسية:**

- **رفع وثيقة متعددة-** مع سحب-and-drop
- **RAG الوكيميائي** - تفكيك الطلبية ونفسها- اصلاح ( مقيدةM SK3 ديناميكية في البنيةMSC4 دورة حياة الطلبية الواحدة)
- **رسم بياني للمعرفة** - شاهد علاقات الكيان
- **وجهة نظر الأدلة** - جملة-إقتباسات المصدر على مستوى
- **نشر مستقل** - واحد يمكن تنفيذه أو دوكر

**قيود التصميم:**

- لا يعتمد السحاب على التصنيف
- المعالجة الإفتراضية المسبقة (التقاط , الدمج | , | استخراج الكائنات |)
- يمكن إعادة بناء حالة الفيكتور من مستندات المصدر
- LLMs المستخدمة *فقط* لتحليل الإجابة على الأدلة المستخرجة

> في أي نقطة لا تستخدم الـ LLMs لتجزئة .

## لماذا تدمج البحث فيكتوري + رسمات المعرفة?

البحث الفيكتوري لوحده يكسر بعض أنواع الشؤال:

| نوع الសំណួរ | مشكلة البحث فيكتوري | | | حل الرسوم ||
|------------|----------------------|----------------|
| متقاطعة-وثيقة | | | " كيف يرتبط X بالـ YM SK4 ♫| ترابط الكيانات عبر الملاحظات |
| الكيان- مركزي | " ماذا عن الدوكرM SK4 ♫| تمرير الرسم البياني من الكيان МSK6
| ملخصات عالمية | "المواضيع الرئيسية?" ♫| الكشف المجتمعي МSK5

يستعمل lucidRAG كلاً من الـ: و , و الرسوم البيانية . و الأسئلة البيانية هي عمق- محدودة( اعظمي" ♫2" متداخلة) و مخصصة للقيام بالبحث في المستجدات لمنع التنقل دون حدود على المجسمات الكبيرة

## نظرة عامة على العمارة

طبقة التطبيق ثلاثة مشاريع قمنا ببنائها بالفعل **[تدفق Stylo](/blog/styloflow-signal-driven-workflows)** - إشارة

```
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?** بدلاً من خطوط أنابيب موزعة بدقة , كل مرحلة المعالجة هي " موجه ," التي تبعث إشارات , . الموجات تعمل عندما تتطابق ظروف التنبيه ., تسمح بالتنفيذ المتوازن . [StyloFlow:Signal-Driven Workflow Orchestration](/blog/styloflow-signal-driven-workflows) من أجل تفاصيل التنفيذ.

## خط المعالجة

عندما تقوم بتحميل مستندة , تتدفق خلال ثلاث مراحل :

### المرحلة 1: تحميل و الاصطفاء

النقطة النهائية للتحميل تتحقق من الملف , تقوم بحساب هاش المحتوى لفك النسخ , وتصطفه لعمليات الخلفية

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

الفكرة الرئيسية: نخترق أولاً, نحفظ لاحقاً . هذا يمنع هدر وقت المعالجة عند تحميلات متكررةM SK3

```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 1](/blog/building-a-document-summarizer-with-rag)):

- Parse the document structure (PDF , DOCX, MarkdownM SK3
- تقسم إلى أجزاء سيمانسية فيما يخص الرؤوس
- صنع مداخلات أونكس لكل قطعة
- تخزين ال vectors في DuckDB مع HNSW إحصاء

### المرحلة 3: استخراج الكائنات

بعد تجزئة , نستخرج الكائنات باستخدام GraphRAG ' المقاربة الهيuristics

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

هذا يستخدم تقييم IDF و الإشارات الهيكلية بدلاً من إتصالات -chunk LLM - أنظر [GraphRAG Part 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- أوقات استراحة مستندة

الوثائق الكبيرة يمكن أن تستغرق دقائق من المعالجة.

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

الرموز المترابطة تضمن أننا لازلنا نحترم إيقاف تطبيقنا بينما نضيف الحد لكل - "document limit"

## مسح قناة التقدم

كل وثيقة معالجة تحصل على قناة تقدم ل تحديثات الSSE. لكن إذا أغلق أحد المستخدمين شاشتهم في منتصف الـ-upload, فإن ذلك المسار يصبح ناضجًا

```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 دقائق في المعالج الخلفي.

## زيرمه: DuckDB + PostgreSQLM SK2SQLite

نستخدم قاعدة بيانات لأغراض مختلفة:

**PostgreSQL/SQLite (EF CoreM SK2** يحفظ بيانات المجازة للوثيقة - ما هو موجود , حالة المعالجة, العلاقاتM SK3 هذه البيانات مستدامة وقابلة للبحث

**DuckDB** يحفظ الفيكتورات والرسم البياني للكائنات..

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

## واجهة الدردشة

تتدفق الأسئلة من خلال خط البحث الأجذري:

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

خدمة البحث تتعامل مع تحلل الطلب إذا كان ذلك ضروريا, ثم تقوم بتحليل الإجابة:

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

الواجهة هي صفحة واحدة مع مستندات على اليسار

```
┌──────────────────┬─────────────────────────────────────┐
│  📁 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.com, tryb العرض يحجب تحميلات ويستخدم ما سبقM SK2محتويات تم تحميلهاMSC3 نظام العرض موجود لجعل تطبيقات العامة آمنةMST4منطقيّة MST5 و رخيصة بدون خاصيةM ST6طريق رمزية صغيرةM st7

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

هذا يسمح لك بتحميل المحتوى التجريبي ببساطة عن طريق نسخ الملفات - لا حاجة لإعادة البداية

## تشغيل lucidRAG

### مستقلة (لا إختلافات )

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

يستخدم SQLite + DuckDB محليا. Open `http://localhost:5080`.

### الدوكر

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

## ما يجري في الواقع

| مكون | مصدر | هدف |
|-----------|--------|---------|
| تحليل الوثائق | DocSummarizer | PDFM SK3 DOCX, العلامة المنخفضة |
| مداخلات الـONNX | DocSummarizer | محلية, لا توجد مفاتيح API ||
| إستخراج الكائنات | GraphRAG | | | IDF |+ | الإشارات الهيكلية |
| البحث الهجين | كلاهما S| BM25 + بريت مع RRF M|
| معالجة غير متجانسة | | | 新 || | القنوات المحدودة | МSK3 | وقفات زمنية |
| UI للويب | جديد | HTMX | | + | Alpine |. |js |

### التكلفة

**صفر من تكاليف API** من أجل التصنيف - المداخلات هي ONNX, الكائنات هيوريسيةM SK2 تدفع فقط لsynthesis LLM في وقت الاستطلاع , وهذا يعمل مع Ollama المحلية

## المقالات المرتبطة

- [أجزاء DocSummarizer 1 - العمارة](/blog/building-a-document-summarizer-with-rag)
- [أجزاء DocSummarizer 4 - أنابيب RAG](/blog/docsummarizer-rag-pipeline)
- [GraphRAG الجزء 2 - تنفيذ](/blog/graphrag-minimum-viable-implementation)
- [البحث الدلالي مع أونكس](/blog/semantic-search-with-onnx-and-qdrant)