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
Monday, 15 December 2025
Welkom bij Deel 9! In vorige delen hebben we een robuust RAG-systeem gebouwd dat markdown blogberichten verwerkt en ze doorzoekbaar maakt via semantische inbeddingen. Nu is het tijd om onze mogelijkheden uit te breiden met Aankoppelen - een krachtige documentverwerkingsbibliotheek die PDF's, DOCX en andere formaten kan inslikken, waardoor onze Advocaat GPT echt uitgebreid is.
OPMERKING: Dit maakt deel uit van mijn experimenten met AI (ondersteund opstellen) + mijn eigen bewerking. Zelfde stem, zelfde pragmatisme; gewoon snellere vingers.
Niet dat ik dit nodig heb voor de blog (het is allemaal markdown), maar voor de voltooiing dacht ik dat ik zou laten zien hoe om gemakkelijk document inname mogelijkheden toe te voegen aan uw RAG-pijpleiding. Dit is vooral handig als je een systeem dat nodig heeft om juridische documenten, contracten, of andere zakelijke documenten te verwerken.
Moderne juridische praktijken hebben betrekking op een verscheidenheid aan documentformaten die verder gaan dan alleen tekst. Advocaten moeten verwijzen naar:
Docling stelt onze Advocaat GPT in staat om deze formaten te verwerken en ze doorzoekbaar te maken, waardoor een echt uitgebreide kennisbasis ontstaat die weerspiegelt hoe moderne advocatenkantoren AI gebruiken.
Aankoppelen is een open-source document processing toolkit van IBM die:
Docling Serve is de makkelijkste manier om Docling als dienst uit te voeren.
# Using the official container image from Quay.io
docker run -p 5001:5001 quay.io/docling-project/docling-serve
# Or with the UI enabled for testing
docker run -p 5001:5001 -e DOCLING_SERVE_ENABLE_UI=1 quay.io/docling-project/docling-serve
Beschikbare Container Afbeeldingen:
Afbeeldingen Afbeeldingen Beschrijving Grootte
|-------|-------------|------|
| quay.io/docling-project/docling-serve Basisafbeelding (PyPI-pakketten)
| quay.io/docling-project/docling-serve-cpu Alleen voor CPU's... ~4.4GB...
| quay.io/docling-project/docling-serve-cu126 CUDA 12.6 voor GPU 10GB
| quay.io/docling-project/docling-serve-cu128 CUDA 12.8 voor GPU . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Eindpunten:
http://localhost:5001http://localhost:5001/docshttp://localhost:5001/ui (indien ingeschakeld)Voor de productie, voeg Docling aan uw bestaande docker-compose.yml:
services:
docling:
image: quay.io/docling-project/docling-serve:latest
ports:
- "5001:5001"
environment:
- DOCLING_SERVE_ENABLE_UI=0
- DOCLING_SERVE_MAX_WORKERS=4
volumes:
- docling_cache:/root/.cache
restart: unless-stopped
volumes:
docling_cache:
Alvorens te integreren met C#, laten we de API testen:
# Convert a PDF from URL
curl -X 'POST' \
'http://localhost:5001/v1/convert/source' \
-H 'accept: application/json' \
-H 'Content-Type: application/json' \
-d '{
"sources": [{"kind": "http", "url": "https://arxiv.org/pdf/2501.17887"}],
"options": {
"to_formats": ["md"]
}
}'
# Convert a local file (upload)
curl -X 'POST' \
'http://localhost:5001/v1/convert/file' \
-H 'accept: application/json' \
-F '[email protected]'
Aangezien Docling een Python service is, integreren we via HTTP. Laten we een robuuste C# client bouwen.
using System.Text.Json.Serialization;
namespace Mostlylucid.BlogLLM.Core.Models
{
/// <summary>
/// Request to convert documents from URLs or base64 content
/// </summary>
public class DoclingConvertRequest
{
[JsonPropertyName("sources")]
public List<DoclingSource> Sources { get; set; } = new();
[JsonPropertyName("options")]
public DoclingOptions? Options { get; set; }
}
public class DoclingSource
{
[JsonPropertyName("kind")]
public string Kind { get; set; } = "http"; // "http", "base64", "file"
[JsonPropertyName("url")]
public string? Url { get; set; }
[JsonPropertyName("base64")]
public string? Base64Content { get; set; }
[JsonPropertyName("filename")]
public string? Filename { get; set; }
}
public class DoclingOptions
{
[JsonPropertyName("to_formats")]
public List<string> ToFormats { get; set; } = new() { "md" }; // "md", "json", "text"
[JsonPropertyName("ocr")]
public bool Ocr { get; set; } = true;
[JsonPropertyName("table_mode")]
public string TableMode { get; set; } = "accurate"; // "fast", "accurate"
}
/// <summary>
/// Response from Docling conversion
/// </summary>
public class DoclingConvertResponse
{
[JsonPropertyName("document")]
public DoclingDocument? Document { get; set; }
[JsonPropertyName("status")]
public string Status { get; set; } = string.Empty;
[JsonPropertyName("errors")]
public List<string>? Errors { get; set; }
}
public class DoclingDocument
{
[JsonPropertyName("md_content")]
public string? MarkdownContent { get; set; }
[JsonPropertyName("json_content")]
public object? JsonContent { get; set; }
[JsonPropertyName("text_content")]
public string? TextContent { get; set; }
[JsonPropertyName("metadata")]
public DoclingMetadata? Metadata { get; set; }
}
public class DoclingMetadata
{
[JsonPropertyName("filename")]
public string? Filename { get; set; }
[JsonPropertyName("page_count")]
public int? PageCount { get; set; }
[JsonPropertyName("file_type")]
public string? FileType { get; set; }
}
/// <summary>
/// Our internal document model
/// </summary>
public class ProcessedDocument
{
public string DocumentId { get; set; } = Guid.NewGuid().ToString();
public string FileName { get; set; } = string.Empty;
public string OriginalFormat { get; set; } = string.Empty;
public string MarkdownContent { get; set; } = string.Empty;
public string? TextContent { get; set; }
public DateTime ProcessedDate { get; set; } = DateTime.UtcNow;
public string[] Categories { get; set; } = Array.Empty<string>();
public int? PageCount { get; set; }
public string ContentHash { get; set; } = string.Empty;
}
}
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Mostlylucid.BlogLLM.Core.Models;
using System.Net.Http.Json;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
namespace Mostlylucid.BlogLLM.Core.Services
{
public class DoclingClientOptions
{
public string BaseUrl { get; set; } = "http://localhost:5001";
public int TimeoutSeconds { get; set; } = 300; // 5 minutes for large documents
public bool EnableOcr { get; set; } = true;
public string TableMode { get; set; } = "accurate";
}
public class DoclingClient : IDisposable
{
private readonly HttpClient _httpClient;
private readonly ILogger<DoclingClient> _logger;
private readonly DoclingClientOptions _options;
public DoclingClient(
HttpClient httpClient,
ILogger<DoclingClient> logger,
IOptions<DoclingClientOptions> options)
{
_httpClient = httpClient;
_logger = logger;
_options = options.Value;
_httpClient.BaseAddress = new Uri(_options.BaseUrl);
_httpClient.Timeout = TimeSpan.FromSeconds(_options.TimeoutSeconds);
}
/// <summary>
/// Convert a document from a URL
/// </summary>
public async Task<ProcessedDocument?> ConvertFromUrlAsync(
string url,
string[]? categories = null,
CancellationToken cancellationToken = default)
{
_logger.LogInformation("Converting document from URL: {Url}", url);
var request = new DoclingConvertRequest
{
Sources = new List<DoclingSource>
{
new() { Kind = "http", Url = url }
},
Options = new DoclingOptions
{
ToFormats = new List<string> { "md", "text" },
Ocr = _options.EnableOcr,
TableMode = _options.TableMode
}
};
return await SendConversionRequestAsync(request, categories, cancellationToken);
}
/// <summary>
/// Convert a local file
/// </summary>
public async Task<ProcessedDocument?> ConvertFileAsync(
string filePath,
string[]? categories = null,
CancellationToken cancellationToken = default)
{
if (!File.Exists(filePath))
{
_logger.LogError("File not found: {FilePath}", filePath);
return null;
}
_logger.LogInformation("Converting local file: {FilePath}", filePath);
// Read file and convert to base64
var fileBytes = await File.ReadAllBytesAsync(filePath, cancellationToken);
var base64Content = Convert.ToBase64String(fileBytes);
var fileName = Path.GetFileName(filePath);
var request = new DoclingConvertRequest
{
Sources = new List<DoclingSource>
{
new()
{
Kind = "base64",
Base64Content = base64Content,
Filename = fileName
}
},
Options = new DoclingOptions
{
ToFormats = new List<string> { "md", "text" },
Ocr = _options.EnableOcr,
TableMode = _options.TableMode
}
};
return await SendConversionRequestAsync(request, categories, cancellationToken);
}
/// <summary>
/// Convert a file using multipart form upload (more efficient for large files)
/// </summary>
public async Task<ProcessedDocument?> ConvertFileUploadAsync(
string filePath,
string[]? categories = null,
CancellationToken cancellationToken = default)
{
if (!File.Exists(filePath))
{
_logger.LogError("File not found: {FilePath}", filePath);
return null;
}
_logger.LogInformation("Uploading and converting file: {FilePath}", filePath);
try
{
using var fileStream = File.OpenRead(filePath);
using var content = new MultipartFormDataContent();
using var streamContent = new StreamContent(fileStream);
var fileName = Path.GetFileName(filePath);
content.Add(streamContent, "files", fileName);
var response = await _httpClient.PostAsync(
"/v1/convert/file",
content,
cancellationToken);
if (!response.IsSuccessStatusCode)
{
var errorContent = await response.Content.ReadAsStringAsync(cancellationToken);
_logger.LogError("Docling conversion failed: {StatusCode} - {Error}",
response.StatusCode, errorContent);
return null;
}
var result = await response.Content.ReadFromJsonAsync<DoclingConvertResponse>(
cancellationToken: cancellationToken);
return MapToProcessedDocument(result, fileName, categories);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error uploading file to Docling: {FilePath}", filePath);
return null;
}
}
private async Task<ProcessedDocument?> SendConversionRequestAsync(
DoclingConvertRequest request,
string[]? categories,
CancellationToken cancellationToken)
{
try
{
var response = await _httpClient.PostAsJsonAsync(
"/v1/convert/source",
request,
cancellationToken);
if (!response.IsSuccessStatusCode)
{
var errorContent = await response.Content.ReadAsStringAsync(cancellationToken);
_logger.LogError("Docling conversion failed: {StatusCode} - {Error}",
response.StatusCode, errorContent);
return null;
}
var result = await response.Content.ReadFromJsonAsync<DoclingConvertResponse>(
cancellationToken: cancellationToken);
var filename = request.Sources.FirstOrDefault()?.Filename
?? request.Sources.FirstOrDefault()?.Url
?? "unknown";
return MapToProcessedDocument(result, filename, categories);
}
catch (HttpRequestException ex)
{
_logger.LogError(ex, "HTTP error calling Docling API");
return null;
}
catch (TaskCanceledException ex)
{
_logger.LogError(ex, "Docling conversion timed out");
return null;
}
catch (Exception ex)
{
_logger.LogError(ex, "Unexpected error calling Docling API");
return null;
}
}
private ProcessedDocument? MapToProcessedDocument(
DoclingConvertResponse? response,
string filename,
string[]? categories)
{
if (response?.Document == null)
{
_logger.LogWarning("Docling returned empty document");
return null;
}
var markdownContent = response.Document.MarkdownContent ?? string.Empty;
return new ProcessedDocument
{
DocumentId = Guid.NewGuid().ToString(),
FileName = Path.GetFileName(filename),
OriginalFormat = Path.GetExtension(filename).TrimStart('.').ToLower(),
MarkdownContent = markdownContent,
TextContent = response.Document.TextContent,
ProcessedDate = DateTime.UtcNow,
Categories = categories ?? Array.Empty<string>(),
PageCount = response.Document.Metadata?.PageCount,
ContentHash = ComputeHash(markdownContent)
};
}
private static string ComputeHash(string content)
{
using var sha256 = SHA256.Create();
var bytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(content));
return Convert.ToBase64String(bytes);
}
public void Dispose()
{
_httpClient?.Dispose();
}
}
}
Laten we nu Docling integreren met onze bestaande RAG-pijpleiding:
using Microsoft.Extensions.Logging;
using Mostlylucid.BlogLLM.Core.Models;
namespace Mostlylucid.BlogLLM.Core.Services
{
public class DocumentIngestionService
{
private readonly ILogger<DocumentIngestionService> _logger;
private readonly DoclingClient _doclingClient;
private readonly MarkdownParserService _markdownParser;
private readonly ChunkingService _chunker;
private readonly BatchEmbeddingService _embedder;
private readonly QdrantVectorStore _vectorStore;
public DocumentIngestionService(
ILogger<DocumentIngestionService> logger,
DoclingClient doclingClient,
MarkdownParserService markdownParser,
ChunkingService chunker,
BatchEmbeddingService embedder,
QdrantVectorStore vectorStore)
{
_logger = logger;
_doclingClient = doclingClient;
_markdownParser = markdownParser;
_chunker = chunker;
_embedder = embedder;
_vectorStore = vectorStore;
}
/// <summary>
/// Process a document file and add to vector store
/// </summary>
public async Task<bool> ProcessDocumentAsync(
string filePath,
string[]? categories = null,
CancellationToken cancellationToken = default)
{
try
{
_logger.LogInformation("Processing document: {FilePath}", filePath);
// Step 1: Convert with Docling
var document = await _doclingClient.ConvertFileUploadAsync(
filePath, categories, cancellationToken);
if (document == null)
{
_logger.LogError("Failed to convert document: {FilePath}", filePath);
return false;
}
return await ProcessConvertedDocumentAsync(document, cancellationToken);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error processing document {FilePath}", filePath);
return false;
}
}
/// <summary>
/// Process a document from URL and add to vector store
/// </summary>
public async Task<bool> ProcessDocumentFromUrlAsync(
string url,
string[]? categories = null,
CancellationToken cancellationToken = default)
{
try
{
_logger.LogInformation("Processing document from URL: {Url}", url);
// Step 1: Convert with Docling
var document = await _doclingClient.ConvertFromUrlAsync(
url, categories, cancellationToken);
if (document == null)
{
_logger.LogError("Failed to convert document from URL: {Url}", url);
return false;
}
return await ProcessConvertedDocumentAsync(document, cancellationToken);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error processing document from URL {Url}", url);
return false;
}
}
private async Task<bool> ProcessConvertedDocumentAsync(
ProcessedDocument document,
CancellationToken cancellationToken)
{
_logger.LogInformation("Document converted: {FileName} ({PageCount} pages)",
document.FileName, document.PageCount ?? 0);
// Step 2: Parse the markdown content
var post = _markdownParser.ParseMarkdownFromContent(
document.MarkdownContent,
document.FileName);
post.Categories = document.Categories;
// Step 3: Chunk the content
var chunks = _chunker.ChunkBlogPost(post);
_logger.LogInformation("Created {ChunkCount} chunks from {FileName}",
chunks.Count, document.FileName);
if (chunks.Count == 0)
{
_logger.LogWarning("No chunks created for document: {FileName}", document.FileName);
return false;
}
// Step 4: Generate embeddings
var progress = new Progress<int>(processed =>
{
_logger.LogDebug("Embedded {Processed}/{Total} chunks",
processed, chunks.Count);
});
await _embedder.GenerateEmbeddingsAsync(chunks, progress, cancellationToken);
// Step 5: Store in vector database
await _vectorStore.UpsertChunksAsync(chunks);
_logger.LogInformation("Successfully processed document: {FileName} ({ChunkCount} chunks)",
document.FileName, chunks.Count);
return true;
}
/// <summary>
/// Batch process multiple documents
/// </summary>
public async Task<(int success, int failed)> ProcessDocumentBatchAsync(
IEnumerable<string> filePaths,
string[]? categories = null,
CancellationToken cancellationToken = default)
{
int success = 0;
int failed = 0;
foreach (var filePath in filePaths)
{
if (cancellationToken.IsCancellationRequested)
break;
var result = await ProcessDocumentAsync(filePath, categories, cancellationToken);
if (result)
success++;
else
failed++;
}
_logger.LogInformation("Batch processing complete: {Success} succeeded, {Failed} failed",
success, failed);
return (success, failed);
}
}
}
using Microsoft.Extensions.DependencyInjection;
using Mostlylucid.BlogLLM.Core.Services;
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddDoclingServices(
this IServiceCollection services,
Action<DoclingClientOptions>? configureOptions = null)
{
// Configure options
if (configureOptions != null)
{
services.Configure(configureOptions);
}
else
{
services.Configure<DoclingClientOptions>(options =>
{
options.BaseUrl = "http://localhost:5001";
options.TimeoutSeconds = 300;
options.EnableOcr = true;
});
}
// Register HttpClient with configuration
services.AddHttpClient<DoclingClient>((serviceProvider, client) =>
{
var options = serviceProvider
.GetRequiredService<IOptions<DoclingClientOptions>>().Value;
client.BaseAddress = new Uri(options.BaseUrl);
client.Timeout = TimeSpan.FromSeconds(options.TimeoutSeconds);
});
// Register services
services.AddScoped<DocumentIngestionService>();
return services;
}
}
Voeg toe aan uw appsettings.json:
{
"Docling": {
"BaseUrl": "http://localhost:5001",
"TimeoutSeconds": 300,
"EnableOcr": true,
"TableMode": "accurate"
}
}
// PDF Processing
await documentIngestionService.ProcessDocumentAsync(
"C:\\documents\\client_contract.pdf",
new[] { "legal", "contracts", "client-agreements" });
// DOCX Processing
await documentIngestionService.ProcessDocumentAsync(
"C:\\documents\\motion_to_dismiss.docx",
new[] { "legal", "briefs", "motions" });
// URL Processing (great for public documents)
await documentIngestionService.ProcessDocumentFromUrlAsync(
"https://arxiv.org/pdf/2501.17887",
new[] { "research", "ai", "docling" });
// Batch Processing
var files = Directory.GetFiles("C:\\documents\\legal", "*.pdf");
var (success, failed) = await documentIngestionService.ProcessDocumentBatchAsync(
files,
new[] { "legal", "batch-import" });
Console.WriteLine($"Processed {success} files, {failed} failures");
public class DocumentWatcherService : BackgroundService
{
private readonly IServiceProvider _serviceProvider;
private readonly ILogger<DocumentWatcherService> _logger;
private FileSystemWatcher? _watcher;
private readonly string _watchPath;
public DocumentWatcherService(
IServiceProvider serviceProvider,
ILogger<DocumentWatcherService> logger,
IConfiguration configuration)
{
_serviceProvider = serviceProvider;
_logger = logger;
_watchPath = configuration["DocumentWatch:Path"] ?? "C:\\documents\\incoming";
}
protected override Task ExecuteAsync(CancellationToken stoppingToken)
{
if (!Directory.Exists(_watchPath))
{
Directory.CreateDirectory(_watchPath);
}
_watcher = new FileSystemWatcher(_watchPath)
{
NotifyFilter = NotifyFilters.FileName | NotifyFilters.LastWrite,
IncludeSubdirectories = false
};
// Watch for common document types
_watcher.Filters.Add("*.pdf");
_watcher.Filters.Add("*.docx");
_watcher.Filters.Add("*.doc");
_watcher.Filters.Add("*.pptx");
_watcher.Created += OnFileCreated;
_watcher.EnableRaisingEvents = true;
_logger.LogInformation("Watching for documents in: {Path}", _watchPath);
return Task.CompletedTask;
}
private async void OnFileCreated(object sender, FileSystemEventArgs e)
{
_logger.LogInformation("New document detected: {FileName}", e.Name);
// Wait for file to be fully written
await Task.Delay(1000);
using var scope = _serviceProvider.CreateScope();
var ingestionService = scope.ServiceProvider
.GetRequiredService<DocumentIngestionService>();
await ingestionService.ProcessDocumentAsync(e.FullPath);
}
public override void Dispose()
{
_watcher?.Dispose();
base.Dispose();
}
}
Typische grootte van het document Bezig met het inbedden van tijd |--------------|-------------|-------------------|----------------| Eenvoudige PDF (1-5 pagina's) 100KB-500KB Incompleet PDF (20+ pagina's) 10-30 seconden . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Gescande PDF met OCR 1-10MB 30-120 seconden 2-5 seconden . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . DOCX-document 50KB-500KB 1e seconden 0.3-0.8 seconden PPTX Presentatie 1-20MB 5-30 seconden
docling-serve-cu126 of docling-serve-cu128)fast tabelmodus wanneer de nauwkeurigheid van de tabel niet kritiek isWe hebben Docling succesvol geïntegreerd in ons Advocaat GPT systeem, waardoor:
Dit breidt onze kennisbasis verder uit dan blogberichten om juridische documenten, contracten, onderzoekspapieren en andere belangrijke materialen op te nemen.
© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.