# RAG για Εφαρμοστές: Υβριδική αναζήτηση και αυτόματη ευρετηρίαση

<datetime class="hidden">2025-11-22T12:00</datetime>

<!-- category -- ASP.NET, Semantic Search, ONNX, Qdrant, Machine Learning, Vector Search, RAG, AI-Article -->
# Εισαγωγή

**Μέρος της σειράς RAG:** Αυτό είναι το Μέρος 5 - πρότυπα ενσωμάτωσης στην παραγωγή:

- [Μέρος 1: Προέλευση των ΚΓΠΕ και θεμελιώδεις αρχές](/blog/rag-primer) - Τι είναι οι προσθήκες, γιατί έχουν σημασία
- [Μέρος 2: RAG Αρχιτεκτονική και Εσωτερικά](/blog/rag-architecture) - Τσουνγκ, μαρκαδόρος, διανυσματικές βάσεις δεδομένων
- [Μέρος 3: Οι ΚΓΠΕ στην πράξη](/blog/rag-practical-applications) - Κατασκευή πλήρων συστημάτων RAG
- [Μέρος 4α: Εφαρμογή ONNX & Qdrant](/blog/semantic-search-with-onnx-and-qdrant) - ΚΜΕ-φιλικό θεμέλιο σημασιολογικής αναζήτησης
- [Μέρος 4β: Σημαντική Αναζήτηση σε Δράση](/blog/semantic-search-in-action) - Typenaward, υβριδική αναζήτηση, και UI συστατικά
- **Μέρος 5: Υβριδική Αναζήτηση & Auto-Indexing** (αυτό το άρθρο) - Τρόποι ενσωμάτωσης στην παραγωγή
- [Μέρος 6: GraphRAG](/blog/graphrag-knowledge-graphs-for-rag) - Γραφήματα γνώσης για την κατανόηση του επιπέδου του σώματος

Το [Μέρος 4α](/blog/semantic-search-with-onnx-and-qdrant), φτιάξαμε τα θεμέλια: ONNX embeddings και Qdrant αποθήκευσης. [Μέρος 4β](/blog/semantic-search-in-action), καλύψαμε την αναζήτηση UI και υβριδική εφαρμογή αναζήτησης . Τώρα θα το κάνουμε έτοιμο με την παραγωγή **αυτόματη ευρετηρίαση** (Ενημέρωση περιεχομένου μηδενικού αφής) μέσω FileSystemWatcher.

[TOC]

# Υβριδική Αναζήτηση: Best of Both Worlds

Σημασιολογική αναζήτηση είναι ισχυρή αλλά παραδοσιακή αναζήτηση πλήρους κειμένου εξακολουθεί να υπερέχει με ακριβείς φράσεις και τεχνικούς όρους. **Η λύση;** Χρησιμοποίησε και τα δύο.

**Γιατί το Υβρίδιο;** Διαφορετικές προσεγγίσεις έχουν διαφορετικές δυνάμεις:

- **PostgreSQL πλήρες κείμενο** ([Καλύπτεται εδώ.](/blog/textsearchingpt1)): Ακριβείς αγώνες, τεχνικοί όροι, χειριστές Boolean
- **Semantic Vector Search**: Σημασία, πλαίσιο, συνώνυμα, εννοιολογικά σχετικό περιεχόμενο

## Αμοιβαίες Rank Fusion (RRF)

Χρησιμοποιούμε **Ανομοιόμορφη Συσσωρευμένη Συσσωρευμένη Συσσωρευμένη** για να συνδυάσετε τα αποτελέσματα από πολλαπλές πηγές αναζήτησης:

```mermaid
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:** `score = Σ(1 / (k + rank))`

- `k` = 60 (σταθερή για την πρόληψη της κυριαρχίας στις πρώτες τάξεις)
- `rank` = θέση στα αποτελέσματα της εν λόγω μεθόδου αναζήτησης
- Α p i ο τ ε λ έ σ α τα **και τα δύο** πηγές παίρνουν αποτελέσματα από κάθε προσθήκη μαζί

**Γιατί λειτουργεί το RRF:**

- **Απομίμηση**: Το ίδιο αποτέλεσμα και στις δύο πηγές είναι υψηλότερο
- **Δίκαιη σχέση**: Καμία μέθοδος αναζήτησης δεν κυριαρχεί άδικα
- **Ακρίβεια**: Δεν απαιτείται περίπλοκος συντονισμός

## Εφαρμογή

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

> **Σημείωση:** Αυτό δείχνει σημασιολογική αναζήτηση μόνο. Στην παραγωγή, εκτελέστε την αναζήτηση πλήρους κειμένου PostgreSQL παράλληλα και περιλαμβάνουν αυτά τα αποτελέσματα στον υπολογισμό RRF.

## Ενσωμάτωση

Εάν έχετε ήδη εφαρμόσει την αναζήτηση πλήρους κειμένου PostgreSQL ([όπως καλύπτεται εδώ](/blog/textsearchingpt1)), η προσθήκη σημασιολογικής αναζήτησης είναι απλή:

```csharp
// Program.cs
services.AddSemanticSearch(configuration);
services.AddSingleton<IHybridSearchService, HybridSearchService>();
```

```csharp
[HttpGet("search/hybrid")]
public async Task<IActionResult> HybridSearch(string query, string language = "en")
{
    var results = await _hybridSearchService.SearchAsync(query, language);
    return PartialView("_SearchResults", results);
}
```

# Αυτόματη ευρετηρίαση με τον Παρατηρητή συστήματος αρχείων

Το πιο ισχυρό χαρακτηριστικό: **αυτόματη ευρετηρίαση**. Αποθήκευση μιας ανάρτησης blog, είναι αμέσως αναζητήσιμο - καμία χειροκίνητη παρέμβαση.

## Πώς Λειτουργεί

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

**Βασική απόφαση σχεδιασμού:** Μόνο αρχεία ευρετηρίου στην **κεντρικός κατάλογος Markdown**, όχι υποκατηγορίες (`translated/`, `drafts/`, `comments/`). Αυτό διατηρεί το δείκτη αναζήτησης καθαρό.

## Ενσωμάτωση Παρατηρητών αρχείωνName

Το blog έχει ήδη ένα `MarkdownDirectoryWatcherService`Το επεκτείνουμε για να ενεργοποιήσουμε σημασιολογική ευρετηρίαση:

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

## Διαγραφή χειρισμού

Όταν διαγράφεται μια θέση, αφαιρέστε την από τον σημασιολογικό δείκτη:

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

# Υπηρεσία υποβάθρου για αρχική ευρετηρίαση

Κατά την έναρξη της λειτουργίας της, υπάρχει ένας δείκτης υπηρεσιών υποβάθρου που δεν υπάρχει ακόμη στο Qdrant:

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

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

**Αυτό εξασφαλίζει:**

1. **Τεμπέλης φόρτωσης μοντέλου** - Λήψεις κατά την πρώτη χρήση, δεν εμποδίζουν την εκκίνηση
2. **Επιφανειοδραστική ευρετηρίαση** - Μόνο νέες/αλλαγμένες θέσεις που επαναδείχθηκαν (μέσω του περιεχομένου χασίς)
3. **Μόνον ο κύριος κατάλογος** - Τα σχέδια και τα μεταφρασμένα αρχεία δεν μολύνουν το ευρετήριο

# Τι φτιάξαμε;

Στα μέρη 4α, 4β και 5, έχουμε τώρα:

- ✅ **Φιλική με CPU σημασιολογική αναζήτηση** - Δεν απαιτείται GPU
- ✅ **Ανακάλυψη σχετικών θέσεων** - Σημασιολογικά παρόμοιο περιεχόμενο
- ✅ **Αναζήτηση φυσικής γλώσσας** - Βρείτε με την έννοια, όχι μόνο λέξεις-κλειδιά
- ✅ **Υβριδική αναζήτηση** - Το καλύτερο σημασιολογικό + πλήρες κείμενο
- ✅ **Αυτόματη ευρετηρίαση** - Ενημερώσεις για το περιεχόμενο μηδενικής αφής
- ✅ **Αυτοεξυπηρετούμενος** - Τα δεδομένα σας παραμένουν στον διακομιστή σας

**Μελλοντικές βελτιώσεις:**

- **Αναζήτηση ειδήσεων κατηγορίας** - Ενισχύστε τα αποτελέσματα από συγκεκριμένες κατηγορίες
- **Πολυγλωσσικές Ενσωμάτωση** - Γλωσσικά ειδικά μοντέλα προσθήκης
- **Ενσωμάτωση OpenSearch** - Προσθήκη OpenSearch στο υβριδικό μείγμα ([δείτε το άρθρο μου OpenSearch](/blog/textsearchingpt3))

# Συμπέρασμα

Αυτό ολοκληρώνει την πρακτική εφαρμογή της σημασιολογικής αναζήτησης τύπου RAG. [Μέρος 4α](/blog/semantic-search-with-onnx-and-qdrant) (ιδρυση) και [Μέρος 4β](/blog/semantic-search-in-action) (search UI), έχετε όλα τα απαραίτητα για να προσθέσετε έξυπνη αναζήτηση στην εφαρμογή σας .NET - τρέχει εξ ολοκλήρου σε CPU, με μηδενικό επιπλέον κόστος.

## Συνεχίστε να μαθαίνετε

- **[ΜΕΤΡΗΣΗ Μέρος 1: Προέλευση και Θεμελιώδη Στοιχεία](/blog/rag-primer)** - Η θεωρία πίσω από την ενσωμάτωση
- **[RAG Μέρος 2: Αρχιτεκτονική και Εσωτερικά](/blog/rag-architecture)** - Βαθιά κατάδυση στα συστήματα RAG
- **[ΜΕΤΡΗΣΗ Μέρος 3: Πρακτικές εφαρμογές](/blog/rag-practical-applications)** - Πλήρη RAG με ενσωμάτωση LLM
- **[Μέρος 4α: Εφαρμογή ONNX & Qdrant](/blog/semantic-search-with-onnx-and-qdrant)** - Ίδρυμα: παρεμβολές και διανυσματική αποθήκευση
- **[Μέρος 4β: Σημαντική Αναζήτηση σε Δράση](/blog/semantic-search-in-action)** - Typenaward, υβριδική αναζήτηση, και UI
- **[Αναζήτηση πλήρους κειμένου με PostgreSQL](/blog/textsearchingpt1)** - Το πλήρες κείμενο της υβριδικής αναζήτησης

## Πόροι

### Βάσεις δεδομένων Qdrant & Vector

- [Αυτό-Hosted Vector Databases με Qdrant](/blog/self-hosted-vector-databases-qdrant) - Βαθιά κατάδυση σε έννοιες Qdrant, HNSW ευρετήρια, φιλτράρισμα, και C# πελάτη
- [Υβρίδιο Qdrant Αναζήτηση](https://qdrant.tech/documentation/concepts/hybrid-queries/) - Ιθαγενής υβριδική υποστήριξη Qdrant του

### Υβριδική αναζήτηση

- [Αντίστοιχο χαρτί σύντηξης Rank](https://plg.uwaterloo.ca/~gvcormac/cormacksigir09-rrf.pdf) - Ο αλγόριθμος RRF

### Παρακολούθηση συστήματος αρχείων

- [Κατηγορία συστήματος αρχείωνWatcherName](https://learn.microsoft.com/en-us/dotnet/api/system.io.filesystemwatcher) - .NET τεκμηρίωση
- [BackgroundService Class](https://learn.microsoft.com/en-us/dotnet/api/microsoft.extensions.hosting.backgroundservice) - Hosted services in ASP.NET Core

### Πλήρεςς κωδικός

Όλος ο κωδικός είναι διαθέσιμος στη διεύθυνση: [github.com/scottgal/κυρίως διαυγής ιστός](https://github.com/scottgal/mostlylucidweb)

- `Mostlylucid.SemanticSearch/` - Κεντρική σημασιολογική βιβλιοθήκη αναζήτησης
- `Mostlylucid/Blog/WatcherService/` - Παρατηρητής αρχείων με σημασιολογική ευρετηρίαση