RAG für Implementer-Hybridsuche und automatische Indexierung (Deutsch (German))

RAG für Implementer-Hybridsuche und automatische Indexierung

Saturday, 22 November 2025

//

8 minute read

Einleitung

Teil der RAG-Serie Dies ist Teil der Produktionsintegrationsmuster von :

Im TeilWir bauten die Stiftung... ONNX Einbettungen und Qdrant-Speicher. Teil BWir deckten die Suche UI und hybride Suchimplementierung ab. automatische Indexierung Über FileSystemWatcher: Aktualisierung des Inhalts von -Touch

Hybride Suche Das Beste aus beiden Welten

Semantische Suche ist leistungsstark, aber traditionelle Voll---Textsuche zeichnet sich immer noch bei genauen Phrasen und Fachbegriffen aus Die Lösung Beides verwenden

Warum Hybride? Unterschiedliche Ansätze haben unterschiedliche Stärken

  • PostgreSQL Volltext (hier abgedecktGenaue Übereinstimmungen mit den technischen Bedingungen der ,
  • Semantische VektorsucheBedeutung: ,; Kontext: MS K2; Synonyme: konzeptuell verwandte Inhalte

Reziproker Rang Fusion

Wir benutzen Gegenseitigkeitsrang Fusion Ergebnisse aus mehreren Suchquellen kombinieren

flowchart TB
    A[User Query: 'docker containers'] --> B[PostgreSQL Full-Text Search]
    A --> C[Semantic Vector Search]

    B --> D["Results:<br/>1. 'Docker Basics' (rank 1)<br/>2. 'Containerizing Apps' (rank 2)<br/>3. 'Docker Compose' (rank 3)"]
    C --> E["Results:<br/>1. 'Containerizing Apps' (rank 1)<br/>2. 'Kubernetes Guide' (rank 2)<br/>3. 'Docker Basics' (rank 3)"]

    D --> F[RRF Algorithm]
    E --> F

    F --> G["Combined Results:<br/>1. 'Containerizing Apps'<br/>   (1/61 + 1/62 = 0.0328)<br/>2. 'Docker Basics'<br/>   (1/61 + 1/63 = 0.0322)<br/>3. 'Docker Compose'<br/>   (1/63 = 0.0159)"]

    style A stroke:#10b981,stroke-width:2px
    style B stroke:#3b82f6,stroke-width:2px
    style C stroke:#6366f1,stroke-width:2px
    style D stroke:#3b82f6,stroke-width:2px
    style E stroke:#6366f1,stroke-width:2px
    style F stroke:#ec4899,stroke-width:4px
    style G stroke:#8b5cf6,stroke-width:2px

Die RRF-Formel score = Σ(1 / (k + rank))

  • k ,= , um zu verhindern, dass die frühen Ränge dominieren
  • rank Position in dieser Suchmethode Ergebnisse
  • Ergebnisse in beides Quellen erhalten Ergebnisse von jedem zusammen addiert

Warum RRF funktioniert

  • DeduplikationGleiches Ergebnis in beiden Quellen punktet höher
  • FairnessKeine Suchmethode dominiert unfair
  • EinfachheitKeine komplexe Abstimmung erforderlich

Durchführung

public class HybridSearchService : IHybridSearchService
{
    private readonly ISemanticSearchService _semanticSearchService;
    private const int RrfConstant = 60;

    public async Task<List<SearchResult>> SearchAsync(
        string query,
        string language = "en",
        int limit = 10,
        CancellationToken cancellationToken = default)
    {
        // Execute both searches in parallel
        var semanticResults = await _semanticSearchService.SearchAsync(
            query, limit * 2, cancellationToken);

        // Filter by language and apply RRF
        var filteredResults = semanticResults
            .Where(r => r.Language == language)
            .ToList();

        return ApplyReciprocalRankFusion(filteredResults)
            .Take(limit)
            .ToList();
    }

    private List<SearchResult> ApplyReciprocalRankFusion(List<SearchResult> results)
    {
        var rrfScores = new Dictionary<string, RrfScore>();

        for (int i = 0; i < results.Count; i++)
        {
            var result = results[i];
            var key = $"{result.Slug}_{result.Language}";

            if (!rrfScores.ContainsKey(key))
                rrfScores[key] = new RrfScore { Result = result };

            // RRF formula: 1 / (k + rank)
            rrfScores[key].Score += 1.0 / (RrfConstant + i + 1);
        }

        return rrfScores.Values
            .OrderByDescending(x => x.Score)
            .Select(x => x.Result)
            .ToList();
    }
}

Anmerkung: Dies zeigt die semantische Suche nur in der Produktion.,. Ausführen der PostgreSQL-Volltextsuche parallel und schließen diese Ergebnisse in die RRF-Berechnung ein.

Integration

Wenn Sie bereits PostgreSQL Volltextsuche implementiert habenwie hier behandeltDas Hinzufügen semantischer Suche ist einfach.

// Program.cs
services.AddSemanticSearch(configuration);
services.AddSingleton<IHybridSearchService, HybridSearchService>();
[HttpGet("search/hybrid")]
public async Task<IActionResult> HybridSearch(string query, string language = "en")
{
    var results = await _hybridSearchService.SearchAsync(query, language);
    return PartialView("_SearchResults", results);
}

Automatische Indexierung mit File System Watcher

Die leistungsstärkste Funktion automatische IndexierungSpeichern Sie einen Blog-Post , , , es ist sofort durchsuchbar , ohne manuelle Intervention , . ,

Wie es funktioniert

flowchart TB
    A[Save Markdown File] --> B[FileSystemWatcher Detects Change]
    B --> C{File in Main Directory?}
    C -->|Yes| D[Save to Database]
    C -->|No| E[Save to Database Only]
    D --> F[Create BlogPostDocument]
    F --> G[Generate Embedding via ONNX]
    G --> H[Store in Qdrant]
    H --> I[Post Searchable Immediately]

    style A stroke:#10b981,stroke-width:2px
    style B stroke:#f59e0b,stroke-width:2px
    style C stroke:#ec4899,stroke-width:3px
    style D stroke:#3b82f6,stroke-width:2px
    style E stroke:#6b7280,stroke-width:2px
    style F stroke:#8b5cf6,stroke-width:2px
    style G stroke:#6366f1,stroke-width:3px
    style H stroke:#ef4444,stroke-width:2px
    style I stroke:#10b981,stroke-width:2px

Schlüsselentwurfsentscheidung Nur Indexdateien in der Hauptmarkdown-VerzeichnisNicht Unterverzeichnissetranslated/, drafts/, comments/Dies hält den Suchindex sauber.

Integration des Dateibeobachters

Der Blog hat bereits eine MarkdownDirectoryWatcherServiceWir erweitern es, um semantische Indexierung auszulösen.

// In MarkdownDirectoryWatcherService.cs
private async Task OnChangedAsync(WaitForChangedResult e)
{
    if (e.Name == null) return;

    await retryPolicy.ExecuteAsync(async () =>
    {
        var savedModel = await blogService.SavePost(slug, language, markdown);

        // Index ONLY if file is in main directory (no path separators in name)
        if (!e.Name.Contains(Path.DirectorySeparatorChar) &&
            !e.Name.Contains(Path.AltDirectorySeparatorChar))
        {
            await IndexPostForSemanticSearchAsync(scope, savedModel, language);
        }
    });
}

private async Task IndexPostForSemanticSearchAsync(
    IServiceScope scope,
    BlogPostDto post,
    string language)
{
    var semanticSearchService = scope.ServiceProvider.GetService<ISemanticSearchService>();
    if (semanticSearchService == null) return; // Not configured

    var document = new BlogPostDocument
    {
        Id = $"{post.Slug}_{language}",
        Slug = post.Slug,
        Title = post.Title,
        Content = post.PlainTextContent,
        Language = language,
        Categories = post.Categories?.ToList() ?? new List<string>(),
        PublishedDate = post.PublishedDate
    };

    await semanticSearchService.IndexPostAsync(document);
    _logger.LogInformation("Indexed {Slug} ({Language}) in semantic search", post.Slug, language);
}

Umgang mit Delektionen

Wenn ein Beitrag gelöscht wird, entfernen Sie es aus dem semantischen Index.

private async Task OnDeletedAsync(WaitForChangedResult e)
{
    await blogService.Delete(slug, language);

    // Delete from semantic search ONLY if file was in main directory
    if (!e.Name.Contains(Path.DirectorySeparatorChar) &&
        !e.Name.Contains(Path.AltDirectorySeparatorChar))
    {
        var semanticSearchService = scope.ServiceProvider.GetService<ISemanticSearchService>();
        await semanticSearchService?.DeletePostAsync(slug, language);
    }
}

Hintergrunddienst für die erste Indexierung

Beim Start indexiert ein Hintergrunddienst vorhandene Posts noch nicht in Qdrant-:

flowchart TB
    A[Application Starts] --> B[Wait 10 seconds]
    B --> C[Initialize Semantic Search]
    C --> D{Model Exists?}
    D -->|No| E[Download from Hugging Face]
    D -->|Yes| F[Load ONNX Model]
    E --> F
    F --> G[Scan Main Markdown Directory]
    G --> H{For Each .md File}
    H --> I[Compute Content Hash]
    I --> J{Hash Changed?}
    J -->|Yes| K[Generate Embedding]
    J -->|No| L[Skip - Already Indexed]
    K --> M[Store in Qdrant]
    M --> H
    L --> H
    H -->|Done| N[Indexing Complete]

    style A stroke:#10b981,stroke-width:2px
    style C stroke:#6366f1,stroke-width:2px
    style E stroke:#f59e0b,stroke-width:2px
    style F stroke:#6366f1,stroke-width:3px
    style G stroke:#8b5cf6,stroke-width:2px
    style J stroke:#ec4899,stroke-width:3px
    style K stroke:#6366f1,stroke-width:2px
    style M stroke:#ef4444,stroke-width:2px
    style N stroke:#10b981,stroke-width:2px
public class SemanticIndexingBackgroundService : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        // Wait for app to be ready
        await Task.Delay(TimeSpan.FromSeconds(10), stoppingToken);

        // Initialize (downloads model if needed)
        await _semanticSearchService.InitializeAsync(stoppingToken);

        // Get all posts from main directory only
        var markdownFiles = Directory.GetFiles(
            _markdownConfig.MarkdownPath,
            "*.md",
            SearchOption.TopDirectoryOnly);  // NOT subdirectories

        foreach (var file in markdownFiles)
        {
            var needsIndexing = await _semanticSearchService.NeedsReindexingAsync(
                slug, language, contentHash, stoppingToken);

            if (needsIndexing)
                await _semanticSearchService.IndexPostAsync(document, stoppingToken);
        }
    }
}

Dies stellt sicher, dass die

  1. Lazy Modell Laden Downloads beim ersten Einsatz nicht blockieren
  2. Inkrementelle Indexierung Nur neue, geänderte Beiträge
  3. Nur Hauptverzeichnis Dokumente und übersetzte Dateien verschmutzen den Index

Was wir gebaut haben

Über Teile hinweg haben wir jetzt :

  • ✅ CPU.-.freundliche semantische Suche Keine GPU erforderlich
  • ✅ Related posts Entdeckung Semantähnliche Inhalte
  • ✅ Natürliche Sprachsuche Suche nach Bedeutungen nicht nur Schlüsselwörter
  • ✅ Hybride Suche Das Beste aus semantischem Text
  • ✅ Automatische Indexierung Aktualisierungen des Inhalts von --Zero---Touch
  • ✅ Selbstbewirtet Ihre Daten bleiben auf Ihrem Server

Zukünftige Verbesserungen

  • Kategorie: --Bewusste Suche Ergebnisse aus bestimmten Kategorien steigern
  • Mehrsprachige Einbettungen Sprache, -Besondere Einbettungsmodelle
  • OpenSearch Integration OpenSearch zum Hybrid-Mix hinzufügensiehe meinen OpenSearch Artikel)

Schlußfolgerung

Dies vervollständigt die praktische Umsetzung der semantischen Suche von RAG--, kombiniert mit Teil (-Foundation, )- und Teil B Such-UI-),e haben Sie alles, was benötigt wird, um intelligente Suche zu Ihrer Anwendung hinzuzufügen. -e läuft komplett auf CPU, ohne zusätzliche Kosten.

Lerne weiter

Aufkommen

Qdrant-Vektordatenbanken

Hybride Suche

Dateisystem beobachten

Vollständiger Code

Alle Codes sind verfügbar unter: github-.-com./-scottgal./-mostlylucidweb

  • Mostlylucid.SemanticSearch/ Kernsemantische Suchbibliothek
  • Mostlylucid/Blog/WatcherService/ Dateibeobachter mit semantischer Indexierung
Finding related posts...
logo

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