This is a viewer only at the moment see the article on how this works.
To update the preview hit Ctrl-Alt-R (or ⌘-Alt-R on Mac) or Enter to refresh. The Save icon lets you save the markdown file to disk
This is a preview from the server running through my markdig pipeline
Tuesday, 30 December 2025
Це Частина 4 з серії DocSummarizer. Погляньте Частина 1 для архітектури, Частина 2 для інструмента CLI, або Частина 3 для глибоких занурень на вбудовах.
Гарна частина RAG - це МСК0 , а не ЛЛМ . ".". Це МСК2 - це все, що було до LМ , і так далі.
Ви, мабуть, бачили цю модель. МСК1 - розділити документи на шматочки. МSK2 - генерувати вбудовані додатки. , - зберігати їх у векторній базі даних.
Це - багато інфраструктури, перш ніж написати один ряд коду застосування.
DocSummarizer.Core обробляє все це в одному пакеті - доступний для обох систем: .NET та Node
DocSummarizer.Core є по суті здатність документувати: детерміністична структура по-перше
Ось що робить DocSummarizer: doesn't do:
flowchart TB
subgraph INPUT["Input (Your Document)"]
DOC[/"PDF / DOCX / Markdown / HTML / URL"/]
end
subgraph DOCSUMMARIZER["DocSummarizer.Core (Deterministic)"]
direction TB
PARSE["Parse & Structure"]
SEGMENT["Segment by Semantics"]
EMBED["Generate Embeddings<br/>(ONNX - Local)"]
SCORE["Compute Salience"]
CITE["Assign Citation IDs"]
PARSE --> SEGMENT
SEGMENT --> EMBED
EMBED --> SCORE
SCORE --> CITE
end
subgraph OUTPUT["Output (ExtractionResult)"]
SEGMENTS[/"Segments[]<br/>• Original text (verbatim)<br/>• float[384] embedding<br/>• Salience score<br/>• StartChar / EndChar<br/>• Section context"/]
end
subgraph YOURS["Your Code"]
STORE[("Vector Store<br/>(Qdrant / pgvector / etc)")]
end
subgraph QUERY["Query Time (Later)"]
Q["User Question"]
RETRIEVE["Retrieve Top-K"]
LLM["LLM Synthesis"]
ANS["Answer + Citations"]
Q --> RETRIEVE
RETRIEVE --> LLM
LLM --> ANS
end
DOC --> PARSE
CITE --> SEGMENTS
SEGMENTS --> STORE
STORE --> RETRIEVE
style DOCSUMMARIZER stroke:#27ae60,stroke-width:3px
style YOURS stroke:#3498db,stroke-width:2px
style QUERY stroke:#9b59b6,stroke-width:2px
style LLM stroke:#e74c3c,stroke-width:2px
Що робить DocSummarizer (зелена скринькаM SK1
Що ви робите (блакитна скринькаM SK1
Що відбувається в часі запиту (пурпурна скринькаM SK1
Ключеві ідеї: LLM ( червона межа МSK1 залучена лише в час пошуку МSK2 Введення є повністю детерміністичним - один і той самий документ завжди виробляє ті самі сегменти
Бонус на репродукцію: Детерміністичне втягування означає, що ви можете переглянути схему RAG0, індикцію ,, дифференцію МSK2 і деbugувати її так само, як будь-який інший будівельний артефакт.
Для RAG, ви хочете справжні речення з ваших документів. - не LLM.
LLM з 'являється пізніше , у часі запиту, для синтезу відповіді з отриманих фрагментів МSK2 Але самі фрагменти мають бути дослівним джерелом M SK3 Це те, що makes citations meaningful .
DocSummarizer's ExtractSegmentsAsync дає вам саме це: оригіналні текстові сегменти з вбудовами, готовий до відтворенняM SK2 Ніяких LLM, що брали участь у поглинанні .
Ось що ви отримуєте з ним dotnet add package:
dotnet add package Mostlylucid.DocSummarizer
Ні Pythonа. Ні зовнішніх APIM SK1 Ні складної настановки . Працює в офлайні після першого завантаження моделі МSK3
Найпростіший застосовний випадок - виділення сегментів з вбудовами, готових для вашого векторного магазину
using Microsoft.Extensions.DependencyInjection;
using Mostlylucid.DocSummarizer;
// Setup DI
var services = new ServiceCollection();
services.AddDocSummarizer();
var provider = services.BuildServiceProvider();
var summarizer = provider.GetRequiredService<IDocumentSummarizer>();
// Extract segments with embeddings
string markdown = File.ReadAllText("document.md");
var extraction = await summarizer.ExtractSegmentsAsync(markdown);
foreach (var segment in extraction.AllSegments)
{
Console.WriteLine($"[{segment.Type}] {segment.SectionTitle}");
Console.WriteLine($" ID: {segment.Id}");
Console.WriteLine($" Salience: {segment.SalienceScore:F2}");
Console.WriteLine($" Embedding: float[{segment.Embedding?.Length}]");
Console.WriteLine($" Text: {segment.Text[..Math.Min(80, segment.Text.Length)]}...");
}
Виход:
[Heading] Introduction
ID: a1b2c3d4e5f6g7h8_h_0
Salience: 0.85
Embedding: float[384]
Text: This document describes the architecture of our new microservices platform...
[Sentence] Introduction
ID: a1b2c3d4e5f6g7h8_s_1
Salience: 0.72
Embedding: float[384]
Text: The system is designed to handle 10,000 requests per second with sub-100ms...
МSK0 - це воно, МSK1 - без оркестрування, МСК2 - без пропону, М СК3 - без думки, M СК4 - лише сегменти з вбудовами та провинцією, М S К5 - готовий до вашої векторної бази даних
Кожен сегмент's Id збудований з ID-документу плюс тип та індекс: {docId}_{type}_{index}.
Ви можете запропонувати власний документ ID, або дозволити DocSummarizer обчислити один із контенту hash:
// Option 1: Provide your own ID (useful for tracking documents in your system)
var extraction = await summarizer.ExtractSegmentsAsync(markdown, documentId: "contract-2024-001");
// Segments get IDs like: contract_2024_001_s_0, contract_2024_001_h_1, ...
// Option 2: Auto-generated from content hash (default)
var extraction = await summarizer.ExtractSegmentsAsync(markdown);
// Segments get IDs like: a1b2c3d4e5f6g7h8_s_0, a1b2c3d4e5f6g7h8_h_1, ...
// Same document = same hash = same IDs (deterministic)
Чому це важливо для РАГ:
[s42] завжди вирішує до того ж джерела текстуКожен виділений сегмент містить все, що вам потрібно для RAG:
public class Segment
{
string Id; // Unique ID: "mydoc_s_42" (for citations)
string Text; // The actual content
SegmentType Type; // Sentence, Heading, ListItem, CodeBlock, Quote, TableRow
int Index; // 0-based order in document
// Source location tracking
int StartChar; // Character offset where segment starts
int EndChar; // Character offset where segment ends
int? PageNumber; // Page number (for PDFs)
int? LineNumber; // Line number (for text/markdown)
// Section context
string SectionTitle; // "Introduction" - immediate heading
string HeadingPath; // "Chapter 1 > Introduction > Overview"
int HeadingLevel; // 1-6 (heading depth)
// Computed during extraction
float[] Embedding; // 384-dim vector (default model)
double SalienceScore; // 0-1 importance score
string ContentHash; // Stable hash for citation tracking across re-indexing
// For retrieval (set during query)
double QuerySimilarity; // Similarity to the query
double RetrievalScore; // Combined score: similarity + salience
string Citation { get; } // Auto-generated: "[s42]", "[h3]", etc.
}
І Id це ключ до відстеження цитат. Коли ваш LLM виводить [s42], ви можете повернути його до точного місця джерела за допомогою StartChar/EndChar.
Бонус: І ExtractionResult включає в себе допоміжні методи для відтворення цитацій:
var extraction = await summarizer.ExtractSegmentsAsync(markdown);
// Fast O(1) lookups
var segment = extraction.GetSegment("mydoc_s_42");
var segmentByIdx = extraction.GetSegmentByIndex(42);
// Find segment at a character position
var segmentAtPos = extraction.GetSegmentAtPosition(5432);
// Get all segments on page 5 (for PDFs)
var pageSegments = extraction.GetSegmentsOnPage(5);
// Get source location for highlighting
var location = extraction.GetSourceLocation("mydoc_s_42");
// Returns: StartChar, EndChar, LineNumber, PageNumber, SectionTitle, HeadingPath
// Extract highlighted text with context
var highlight = extraction.GetHighlightedText(originalMarkdown, "mydoc_s_42", contextChars: 50);
Console.WriteLine(highlight.ToHtml()); // <span class="highlight">...</span>
Console.WriteLine(highlight.ToMarkdown()); // **...**
DocSummarizer дає вбудовані додатки. Використовуйте будь-яку векторну базу даних, яку хочетеM SK1
var points = extraction.AllSegments.Select((s, i) => new PointStruct
{
Id = (ulong)i,
Vectors = s.Embedding,
Payload =
{
["text"] = s.Text,
["section"] = s.SectionTitle,
["salience"] = s.SalienceScore,
["segment_id"] = s.Id,
["start_char"] = s.StartChar,
["end_char"] = s.EndChar
}
}).ToList();
await qdrantClient.UpsertAsync("documents", points);
foreach (var segment in extraction.Segments)
{
await connection.ExecuteAsync(
@"INSERT INTO documents (segment_id, text, heading, salience, embedding)
VALUES (@id, @text, @heading, @salience, @embedding::vector)",
new {
id = segment.Id,
text = segment.Text,
heading = segment.SectionTitle,
salience = segment.SalienceScore,
// NOTE: String interpolation is for demo simplicity only.
// For production, use NpgsqlParameter with Vector type for better
// performance and to avoid culture-dependent decimal separators.
embedding = $"[{string.Join(",", segment.Embedding)}]"
});
}
Не хочете керувати окремою базою даних
services.AddDocSummarizer(options =>
{
// In-memory (fastest, no persistence)
options.BertRag.VectorStore = VectorStoreBackend.InMemory;
// DuckDB (embedded file-based, default)
options.BertRag.VectorStore = VectorStoreBackend.DuckDB;
// Qdrant (external server)
options.BertRag.VectorStore = VectorStoreBackend.Qdrant;
options.Qdrant.Host = "localhost";
options.Qdrant.Port = 6334;
});
Більшість RAG-систем провалюються не тому, що вбудова є поганою,, а тому що всі фрагменти вважаються однаково важливими.
flowchart LR
subgraph DOC["Document"]
H1["# Title"]
P1["First paragraph<br/>(intro)"]
H2["## Methods"]
P2["Technical details..."]
P3["More details..."]
H3["## Results"]
P4["Key findings here"]
H4["## Appendix"]
P5["Reference data..."]
end
subgraph SCORES["Salience Scores"]
S1["0.95"]
S2["0.85"]
S3["0.70"]
S4["0.65"]
S5["0.60"]
S6["0.80"]
S7["0.30"]
end
H1 --> S1
P1 --> S2
H2 --> S3
P2 --> S4
P3 --> S5
P4 --> S6
P5 --> S7
style S1 stroke:#27ae60,stroke-width:3px
style S2 stroke:#27ae60,stroke-width:2px
style S6 stroke:#27ae60,stroke-width:2px
style S7 stroke:#e74c3c,stroke-width:2px
речення у абстрактному значенні важливіше, ніж речення в приписі.
// Get the top 20% most salient segments
var topSegments = extraction.Segments
.OrderByDescending(s => s.SalienceScore)
.Take((int)(extraction.Segments.Count * 0.2));
Різовики слизистості:
| Фактор МSK1 Ефект МSK2 | |
|---|---|
| МSK0 Позиция | В вступі МSK2сумовні речення отримують більше балів |
| Близькість заголовку | Перші речення після заголовків - це теорічні речення |
| МSK0 Длина | Дуже короткі сегменти |
| МSK0 Тип секції | Абстракт МSK2Введення збільшено , Референції |
| Тип наповнення МSK1 Блоки коду МSK2 цитати , листи з різним значенням |
Це означає, що ваше відтворення (similarity * salience) замість простої схожості.
DocSummarizer auto- визначає тип dokumentu, використовуючи гуристику на основі контентуM SK1
var extraction = await summarizer.ExtractSegmentsAsync(markdown);
// Document type detected from content
Console.WriteLine($"Type: {extraction.DocumentType}"); // Technical, Narrative, Legal, etc.
Console.WriteLine($"Confidence: {extraction.Confidence}"); // High, Medium, Low
класифікація впливає на пошук: Шкала глибини відтворення з ентропією dokumentu , не зашифрований ВерхнийKM SK2 Розмовні документи МSK3 вигадка МSK4 історії М SK5 отримують МСК6 x підштовхування до числа відтворення, тому що їм потрібен більше контексту М СК7 Технический документ з чіткою структурою потребує менше М S К8
Геуристичні дослідження на :
Якщо геризтика непевна, DocSummarizer може повернутися до швидкої класифікації LLM, використовуючи модель "sentinel". Це вимагає того, щоб Ollama працювала локально з маленькою моделлю, наприклад tinyllama. Увімкніть його за допомогою конфігурації
services.AddDocSummarizer(options =>
{
options.Ollama.BaseUrl = "http://localhost:11434";
options.Ollama.Model = "tinyllama";
});
// Then use with LLM fallback enabled
var extraction = await summarizer.ExtractSegmentsAsync(markdown, useLlmFallback: true);
Для більшості документів , геуристика сама по собі є достатньо точною - зворотній зв 'язок LLM існує для крайних випадків МSK2
Тут – це повний індекс – МСК1, МСК2, труба запиту, МSK3. Ми – МSK4 – збудуємо її за три кроки
sequenceDiagram
participant User
participant App as Your App
participant DS as DocSummarizer
participant VS as Vector Store
participant LLM
Note over DS: INGESTION (No LLM)
App->>DS: ExtractSegmentsAsync(markdown)
DS->>DS: Parse structure
DS->>DS: Split into segments
DS->>DS: Generate embeddings (ONNX)
DS->>DS: Compute salience
DS-->>App: ExtractionResult
App->>VS: Store segments + vectors
Note over LLM: QUERY TIME (LLM involved)
User->>App: "What about X?"
App->>DS: EmbedAsync(question)
DS-->>App: float[384]
App->>VS: Search(vector, topK=5)
VS-->>App: Top segments
App->>LLM: Question + Context
LLM-->>App: Answer with [citations]
App-->>User: Answer
public class SimpleRagService
{
private readonly IDocumentSummarizer _summarizer;
// In-memory segment store - maps "docId:segmentId" to the full segment
private readonly Dictionary<string, ExtractedSegment> _segments = new();
// In-memory vector index - pairs of (id, embedding vector)
private readonly List<(string Id, float[] Vector)> _index = new();
В процесі виробництва ви використовуєте реальну векторну базу даних: (QdrantM SK2 pgvector, і т.д.
public async Task IndexAsync(string markdown, string docId)
{
// Extract segments with embeddings - this is where DocSummarizer does the work
var extraction = await _summarizer.ExtractSegmentsAsync(markdown);
// Store each segment and its vector
foreach (var segment in extraction.Segments)
{
// Composite key: document + segment for citation tracking
var id = $"{docId}:{segment.SegmentId}";
// Keep the full segment for retrieval
_segments[id] = segment;
// Add to vector index for similarity search
_index.Add((id, segment.Embedding));
}
}
Зверніть увагу, ми зберігаємо справжня текст dokumentu, не підсумкиM SK1
public async Task<string> QueryAsync(string question, int topK = 5)
{
// Embed the question using the same model as documents
// This ensures vectors are in the same space
var embedding = await _summarizer.EmbedAsync(question);
// Find top-K most similar segments
var results = _index
.Select(x => (x.Id, Similarity: CosineSimilarity(embedding, x.Vector)))
.OrderByDescending(x => x.Similarity)
.Take(topK)
.Select(x => _segments[x.Id])
.ToList();
// Build context with citation markers
// The LLM can reference [chunk-3] and we can trace it back
var context = string.Join("\n\n", results.Select(s =>
$"[{s.SegmentId}] {s.Text}"));
return context; // Send this + the question to your LLM
}
Повернений контекст містить справжній текст документа з ідеями сегменту. Ваша LLM про prompt може виглядати так, як
Answer the question based on the following context.
Cite sources using the [chunk-N] markers.
Context:
{context}
Question: {question}
private static float CosineSimilarity(float[] a, float[] b)
{
float dot = 0, normA = 0, normB = 0;
for (int i = 0; i < a.Length; i++)
{
dot += a[i] * b[i];
normA += a[i] * a[i];
normB += b[i] * b[i];
}
return dot / (MathF.Sqrt(normA) * MathF.Sqrt(normB));
}
}
DocSummarizer включає VectorMath.CosineSimilarity() якщо ви не хочете писати це самостійно
DocSummarizer також відкриває IEmbeddingService безпосередньо, якщо вам потрібно вмонтувати пошуки окремо від повної схеми підсумування.
За замовчанням AllMiniLmL6V2 - швидко , маленьке МSK2 хороша якість M SK3 Вибирати в залежності від ваших потреб
services.AddDocSummarizer(options =>
{
options.Onnx.EmbeddingModel = OnnxEmbeddingModel.BgeBaseEnV15;
});
| МSK0 Модель | Dims | Max Tokens | МSK3 | Rozmiar | Примітки | |||
|---|---|---|---|---|---|---|---|---|
AllMiniLmL6V2 МSK0 384 |
||||||||
BgeSmallEnV15 МSK0 384 ♫ ♫ МSK2 ♫ |
||||||||
BgeBaseEnV15 МSK0 768 ♫ ♫ МSK2 ♫ |
||||||||
JinaEmbeddingsV2BaseEn МSK0 768 ♫ ♫ МSK2 ♫ |
Моделі автоматизовано- завантажити з HuggingFace при першій користуванні . Далі запускає навантаження з диска M SK2
Наблюдайте за вашими трубопроводами RAG в процесі виробництва:
services.AddOpenTelemetry()
.WithTracing(tracing => tracing
.AddSource("Mostlylucid.DocSummarizer")
.AddSource("Mostlylucid.DocSummarizer.Ollama")
.AddSource("Mostlylucid.DocSummarizer.WebFetcher")
.AddOtlpExporter())
.WithMetrics(metrics => metrics
.AddMeter("Mostlylucid.DocSummarizer")
.AddMeter("Mostlylucid.DocSummarizer.Ollama")
.AddMeter("Mostlylucid.DocSummarizer.WebFetcher")
.AddPrometheusExporter());
Ключові метрики:
docsummarizer.summarizations - Перерахунки запитівdocsummarizer.summarization.duration - Час обробки в msdocsummarizer.document.size - розміри документівdocsummarizer.ollama.embed.requests - Введення API дзвінківDocSummarizer обробляє багато форматів документу з розумним виявленням та обробкою.
Ці формати оброблені наtively - не потрібні документування чи інші послугиM SK1
| Формат МSK1 Поширення МSK2 Обробка | ||
|---|---|---|
Замітка МSK1 .md, .markdown |
Розшифровано за допомогою Маркдіґа МSK1 структуру зберігають МSK2 | |
Plain Text МSK1 .txt, .text |
Подібно до абзаців | |
| МSK0 HTML | .html, .htm |
СанітаризованийM SK1 перетворений на Markdown |
Архиви ЗIP МSK1 .zip |
Витягує текстові файли , автоматичний МSK2 розпізнає формат Гутенберґа |
Простий текст отримує розумне оброблення: Коли немає заголовків з відмітками, , chunker переключається на абзац МSK2 базується на розрізі \ . Він виявляє структуру dokumentu у найпростішому розумінні. МSK4 якщо у вашому тексті є чіткі прориви абзацу , вони стають межами блока
// Plain text works the same way
var plainText = File.ReadAllText("notes.txt");
var extraction = await summarizer.ExtractSegmentsAsync(plainText);
// Chunks split by paragraphs, embeddings generated
Для PDF, DOCXM SK1 PPTX, XLSXMSC3 і зображеннях (OCRMska5 M Ska6 додайте DoclingMske7
docker run -d -p 5001:5001 quay.io/docling-project/docling-serve
services.AddDocSummarizer(options =>
{
options.Docling.BaseUrl = "http://localhost:5001";
});
// PDF, DOCX, PPTX, images all work
var pdfBytes = await File.ReadAllBytesAsync("document.pdf");
var extraction = await summarizer.ExtractSegmentsAsync(pdfBytes, "document.pdf");
Docling зберігає структуру dokumentu - заголовкиM SK1 таблиці , листи просуваються як правильне Markdown. Це означає, що краще відокремлення, ніж видобуток сирого тексту
| Формат МSK1 Поширення МSK2 Замітки | ||
|---|---|---|
PDF МSK1 .pdf |
Текст МSK1 макет зачуваний МSK2 таблички конвертовані | |
Слово МSK1 .docx |
Полне форматування МSK1 заголовки МSK2 листи | |
| PowerPoint | .pptx |
Сліпи стають розділами МSK1 |
| Excel | .xlsx |
Переведені таблички МSK1 |
Фото МSK1 .png, .jpg, .tiff |
ОCR через Docling |
Подивіться Частина 1 для більшої інформації про інтеграцію Docling, або Мультиконвертація формату dokumentu для глибоких погружений.
| Проблема | DocSummarizer Solution МSK2 |
|---|---|
| Спілкування на семантичних кордонах МSK1 Розриви на заголовкахM SK2 вміст, пов 'язаний з групами МSK3 | |
| Токенізація за модель | Використовує правильний токенізер для кожної моделі ONNX МSK2 |
| Вбудова пакету МSK1 Конфигурований розмір пакета МSK2 пам 'ять - ефективний | |
| Переслідування цитат | Кожен сегмент стає унікальним SegmentId |
| Конвертація формату МSK1 Марка-даун МSK2 HTML , PDF | |
| Скачаймо модельM SK1Cachering | Auto-downloadsМSK4 Cache in ~/.docsummarizer |
До цікавої частини трубопроводу RAG потрібна інфраструктура.
dotnet add package Mostlylucid.DocSummarizer
Вимикання завершено. Збудувати вашу RAG програмуM SK1
© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.