RAG för implementatorer: Hybridsökning och automatisk indexering (Svenska (Swedish))

RAG för implementatorer: Hybridsökning och automatisk indexering

Saturday, 22 November 2025

//

8 minute read

Inledning

En del av RAG-serien: Detta är del 5 - produktionsintegrationsmönster:

Till Häfte 4a, Vi byggde grunden: ONNX inbäddningar och Qdrant lagring. Häfte 4b, Vi täckte sökningen UI och hybrid sökning genomförande. Nu ska vi göra det produktionsklara med Automatisk indexering (noll-touch innehållsuppdateringar) via FileSystemWatcher.

Hybridsökning: Det bästa av båda världarna

Semantisk sökning är kraftfull men traditionell fulltextsökning utmärker sig fortfarande på exakta fraser och tekniska termer. Vad är lösningen? Använd båda.

Varför Hybrid? Olika metoder har olika starka sidor:

  • PostgreSQL fulltext (omfattas här: Exakta matcher, tekniska termer, Booleska operatörer
  • Sök efter semantisk vektor: Betydelse, sammanhang, synonymer, begreppsmässigt relaterat innehåll

Reciprocal Rank Fusion (RRF)

Vi använder Reciprocal Rank Fusion (ciprocal Rank Fusion) För att kombinera resultat från flera sökkällor:

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

RRF-formeln: score = Σ(1 / (k + rank))

  • k = 60 (konstant för att förhindra att tidiga led dominerar)
  • rank = position i sökmetodens resultat
  • Resultat i båda källor få poäng från varje tillsatt tillsammans

Varför RRF fungerar:

  • Avledning: Samma resultat i båda källor poäng högre
  • Rättvisa: Ingen sökmetod dominerar orättvist
  • Enkelhet: Ingen komplicerad inställning krävs

Genomförande

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

Anmärkning: Detta visar endast semantisk sökning. I produktionen, kör PostgreSQL fulltextsökning parallellt och inkludera dessa resultat i RRF beräkning.

Integrering

Om du redan har implementerat PostgreSQL fulltextsökning (som omfattas här), att lägga till semantisk sökning är enkelt:

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

Automatisk indexering med File System Watcher

Den mest kraftfulla funktionen: Automatisk indexeringSpara ett blogginlägg, det är omedelbart sökbart - ingen manuell intervention.

Hur den fungerar

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

Beslut om nyckelutformning: Endast indexfiler i main Markdown- katalog, inte underkataloger (translated/, drafts/, comments/. . Detta håller sökindex ren.

Integration av filväktare

Bloggen har redan en MarkdownDirectoryWatcherService. Vi utvidgar den till att utlösa semantisk indexering:

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

Hantering av avvikelser

När ett inlägg tas bort, ta bort det från semantiskt 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);
    }
}

Bakgrundstjänst för inledande indexering

Vid start indexerar en bakgrundstjänst befintliga tjänster som ännu inte finns i 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);
        }
    }
}

Detta säkerställer följande:

  1. Lasig laddningsmodell - Nedladdningar vid första användning, inte blockering av start
  2. Inkrementell indexering - Endast nya/ändrade inlägg omindexerade (via innehållshash)
  3. Endast huvudkatalog - Utkast och översatta filer förorenar inte indexet

Vad vi har byggt

Över delarna 4a, 4b och 5 har vi nu:

  • CPU-vänlig semantisk sökning - Ingen GPU krävs
  • Relaterade inlägg upptäckt - Semantiskt liknande innehåll
  • Sök på naturligt språk - Hitta med mening, inte bara nyckelord.
  • Hybridsökning - Bäst av semantisk + fulltext
  • Automatisk indexering - Noll-touch innehållsuppdateringar
  • Självvärdig - Dina data stannar på din server

Framtida förbättringar:

  • Kategori-medveten sökning - Öka resultaten från specifika kategorier
  • Flerspråkiga sängkläder - Språkspecifika inbäddningsmodeller
  • Integrering av OpenSearch - Lägg till OpenSearch i hybridblandningen (se min OpenSearch artikel)

Slutsatser

Detta kompletterar det praktiska genomförandet av RAG-stil semantisk sökning. Häfte 4a (grundläggning) och Häfte 4b (sök UI), du har allt som behövs för att lägga till intelligent sökning till din .NET-applikation - körs helt på CPU, till noll extra kostnad.

Fortsätt att lära

Resurser

Qdrant- och vektordatabaser

Hybridsökning

Filsystemets bevakning

Fullständig kod

Alla koder finns på: github.com/scottgal/mestlylucidweb

  • Mostlylucid.SemanticSearch/ - Kärnan semantiskt sökbibliotek
  • Mostlylucid/Blog/WatcherService/ - File Watcher med semantisk indexering
Finding related posts...
logo

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