# RAG για Εφαρμόστες: CPU-Friendly Σημασιολογική Αναζήτηση με ONNX και Qdrant

<datetime class="hidden">2025-11-25T11:00</datetime>

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

**Μέρος της σειράς RAG:** Αυτό είναι το μέρος 4α - βασική εφαρμογή:

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

Τα μέρη 1-3 εξηγούνται *Γιατί;* Σημαντική αναζήτηση λειτουργεί. Αυτό το άρθρο δείχνει *Πώς* να οικοδομήσουμε τα θεμέλια - a **0 - κόστος, φιλική προς τον ΚΜΕ εφαρμογή** χρήση ONNX Runtime και Qdrant. [Μέρος 4β](/blog/semantic-search-in-action) καλύπτει την εφαρμογή αναζήτησης UI και υβριδικής αναζήτησης, και [Μέρος 5](/blog/rag-hybrid-search-and-indexing) καλύπτει την παραγωγή auto-indexing.

**Η πρόκληση:** Οι περισσότερες σημασιολογικές λύσεις αναζήτησης απαιτούν ακριβή υποδομή GPU ή δαπανηρές υπηρεσίες διαχείρισης.

**Η Λύση:** Ένα πλήρως λειτουργικό σημασιολογικό σύστημα αναζήτησης που λειτουργεί εξ ολοκλήρου σε CPU, χρησιμοποιώντας δωρεάν εργαλεία ανοικτού κώδικα. Αυτή είναι η ακριβής ρύθμιση που εκτελείται σε αυτό το blog - μηδέν επιπλέον κόστος πέρα από την υπάρχουσα φιλοξενία.

[TOC]

# Βασικές έννοιες

Οι έννοιες αυτές καλύπτονται σε βάθος [Σειρά RAG](/blog/rag-primer), αλλά εδώ είναι ό, τι πρέπει να ξέρετε για αυτή την εφαρμογή:

## Ενσωμάτωση: Κείμενο ως αριθμοί

Οι προσθήκες είναι φορείς (αγγελία των αριθμών) που αιχμαλωτίζουν το *Σημασία* Παρόμοιες έννοιες παράγουν παρόμοιους φορείς - αυτή είναι η μαγεία.

```mermaid
graph TD
    A["Text: 'The cat sat on the mat'"] --> B[Embedding Model]
    B --> C["Vector: [0.25, -0.18, 0.91, ... 384 more numbers]"]
    D["Text: 'A feline rested on the carpet'"] --> B
    B --> E["Vector: [0.27, -0.16, 0.89, ... similar numbers!]"]

    C -.Similar vectors = similar meaning.-> E

    style A stroke:#10b981,stroke-width:2px
    style D stroke:#10b981,stroke-width:2px
    style B stroke:#6366f1,stroke-width:3px
    style C stroke:#f59e0b,stroke-width:2px
    style E stroke:#f59e0b,stroke-width:2px
```

**Βασική διορατικότητα:** Έτσι μπορούμε να βρούμε "σχετικό" περιεχόμενο - στην κυριολεξία μετράμε την απόσταση μεταξύ των νοημάτων!

### Κατανόηση της ομοιότητας συνημίτονο

[Ομοιότητα συνημίτονου](https://en.wikipedia.org/wiki/Cosine_similarity) Μετρά τη γωνία μεταξύ δύο διανυσμάτων - αν δείχνουν σε παρόμοιες κατευθύνσεις, είναι σημασιολογικά παρόμοια:

```mermaid
flowchart LR
    subgraph "Vector Space (simplified to 2D)"
        direction TB
        A["'Docker tutorial'"] -.-> B((0.85))
        C["'Container deployment'"] -.-> B
        D["'Cooking recipes'"] -.-> E((0.12))
        A -.-> E
    end

    B --> F["High Similarity<br/>Related content!"]
    E --> G["Low Similarity<br/>Different topics"]

    style A stroke:#10b981,stroke-width:2px
    style C stroke:#10b981,stroke-width:2px
    style D stroke:#f59e0b,stroke-width:2px
    style B stroke:#22c55e,stroke-width:3px
    style E stroke:#ef4444,stroke-width:3px
    style F stroke:#22c55e,stroke-width:2px
    style G stroke:#ef4444,stroke-width:2px
```

Ο τύπος: `similarity = (A · B) / (||A|| × ||B||)` - αλλά αφού L2-κανονίζουμε τους φορείς μας, απλοποιεί μόνο το προϊόν κουκκίδα!

## Τι είναι το ONNX;

[ONNX (Open Neural Network Exchange)](https://onnx.ai/) είναι μια ανοιχτή τυποποιημένη μορφή για τα μοντέλα εκμάθησης μηχανών που τους επιτρέπει να τρέχουν αποτελεσματικά σε διάφορες πλατφόρμες. Σκεφτείτε το σαν ένα καθολικό μεταφραστή για τα μοντέλα AI. [ONNX Runtime](https://onnxruntime.ai/) είναι η μηχανή υψηλής απόδοσης της Microsoft που εκτελεί αυτά τα μοντέλα.

**Γιατί ONNX για μας περίπτωση χρήσης:**

- Λειτουργεί σε CPU (δεν χρειάζεται GPU!) - δείτε το [ONNX Runtime CPU πάροχος εκτέλεσης docs](https://onnxruntime.ai/docs/execution-providers/CPU-Execution-Provider.html)
- Πολύ πιο γρήγορα από το να τρέχει μοντέλα σε Python
- Μικρότερο αποτύπωμα μνήμης
- Ενσωμάτωση απρόσκοπτα με .NET μέσω [Microsoft.ML.OnnxRuntime NuGet πακέτο](https://www.nuget.org/packages/Microsoft.ML.OnnxRuntime)
- Υποστηρίξεις [βελτιστοποιήσεις γραφήματος](https://onnxruntime.ai/docs/performance/model-optimizations/graph-optimizations.html) για γρηγορότερα συμπεράσματα

```mermaid
flowchart LR
    subgraph "ONNX Inference Pipeline"
        A[Raw Text] --> B[Tokenizer]
        B --> C["Tokens: [CLS] the cat sat [SEP]"]
        C --> D[Token IDs: 101 1996 4937 2068 102]
        D --> E[ONNX Runtime]
        E --> F[384-dim Vector]
        F --> G[L2 Normalize]
        G --> H[Final Embedding]
    end

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

## Τι είναι το Qdrant;

[QdrantCity name (optional, probably does not need a translation)](https://qdrant.tech/) είναι μια ανοιχτή-πηγή διανυσματική βάση δεδομένων - βασικά μια βάση δεδομένων βελτιστοποιημένη για την αποθήκευση και την αναζήτηση αυτών των διανυσμάτων ενσωμάτωση. Για μια βαθιά κατάδυση στις έννοιες του Qdrant, διαμόρφωση, και C# ενσωμάτωση, βλέπε [Αυτό-Hosted Vector Databases με Qdrant](/blog/self-hosted-vector-databases-qdrant). Ενώ εσύ *θα μπορούσε* αποθηκεύουν φορείς στο PostgreSQL, Qdrant είναι σκοπός-χτίστηκε για αυτό και προσφέρει:

- Αστραπιαία αναζήτηση ομοιότητας με τη χρήση [Αλγόριθμος HNSW](https://qdrant.tech/documentation/concepts/indexing/#vector-index)
- [Φίλτρο μεταδεδομένα](https://qdrant.tech/documentation/concepts/filtering/) - αποτελέσματα φίλτρου από τα πεδία φορτίου
- Ευκαμψία σε εκατομμύρια φορείς με [κατανεμημένη ανάπτυξη](https://qdrant.tech/documentation/guides/distributed_deployment/)
- Χαμηλή χρήση πόρων - τρέχει άνετα σε μέτρια υλικό
- Αυτο-ξεχωριστή με Docker - δείτε το [Qdrant Docker quickstart](https://qdrant.tech/documentation/quick-start/)
- [gRPC και REST APIs](https://qdrant.tech/documentation/interfaces/) για την ολοκλήρωση
- Native .NET υποστήριξη μέσω [Qdrant. Πελάτης NuGet πακέτο](https://www.nuget.org/packages/Qdrant.Client)

```mermaid
flowchart TB
    subgraph "Qdrant Vector Storage"
        direction TB
        A[Collection: blog_posts] --> B[Point 1]
        A --> C[Point 2]
        A --> D[Point N...]

        B --> B1["Vector: [0.12, -0.08, ...]"]
        B --> B2["Payload: {slug, title, language}"]

        C --> C1["Vector: [0.25, 0.14, ...]"]
        C --> C2["Payload: {slug, title, language}"]
    end

    subgraph "Vector Search"
        E[Query Vector] --> F[HNSW Index]
        F --> G[Cosine Similarity]
        G --> H[Top K Results]
    end

    style A stroke:#ef4444,stroke-width:3px
    style B stroke:#8b5cf6,stroke-width:2px
    style C stroke:#8b5cf6,stroke-width:2px
    style D stroke:#8b5cf6,stroke-width:2px
    style B1 stroke:#f59e0b,stroke-width:2px
    style B2 stroke:#10b981,stroke-width:2px
    style C1 stroke:#f59e0b,stroke-width:2px
    style C2 stroke:#10b981,stroke-width:2px
    style E stroke:#6366f1,stroke-width:2px
    style F stroke:#ec4899,stroke-width:3px
    style G stroke:#ec4899,stroke-width:2px
    style H stroke:#10b981,stroke-width:2px
```

# Αρχιτεκτονική επισκόπηση

Εδώ είναι πώς το σημασιολογικό μας σύστημα αναζήτησης ταιριάζει μεταξύ τους:

```mermaid
flowchart TB
    subgraph "Content Ingestion"
        A[Blog Post Markdown] --> B[Extract Plain Text]
        B --> C[ONNX Embedding Service]
        C --> D[Generate 384-dim Vector]
        D --> E[Qdrant Vector Store]
    end

    subgraph "Search Flow"
        F[User Query] --> G[ONNX Embedding Service]
        G --> H[Generate Query Vector]
        H --> I[Qdrant Search]
        E -.Vector Similarity.-> I
        I --> J[Ranked Results]
    end

    subgraph "Related Posts"
        K[Current Blog Post] --> L[Get Post Vector from Qdrant]
        L --> M[Find Similar Vectors]
        E -.->M
        M --> N[Top 5 Related Posts]
    end

    style A stroke:#10b981,stroke-width:2px
    style B stroke:#10b981,stroke-width:2px
    style C stroke:#6366f1,stroke-width:3px
    style D stroke:#f59e0b,stroke-width:2px
    style E stroke:#ef4444,stroke-width:3px
    style F stroke:#10b981,stroke-width:2px
    style G stroke:#6366f1,stroke-width:3px
    style H stroke:#f59e0b,stroke-width:2px
    style I stroke:#ef4444,stroke-width:2px
    style J stroke:#8b5cf6,stroke-width:2px
    style K stroke:#10b981,stroke-width:2px
    style L stroke:#ef4444,stroke-width:2px
    style M stroke:#ef4444,stroke-width:2px
    style N stroke:#8b5cf6,stroke-width:2px
```

**Η ροή στα απλά αγγλικά:**

1. **Ευρεσιτεχνία**: Όταν γράφετε ένα blog post, το μετατρέπουμε σε φορέα και το αποθηκεύουμε στο Qdrant
2. **Αναζήτηση**: Όταν κάποιος ψάχνει, μετατρέπουμε την ερώτησή του σε ένα διάνυσμα και βρίσκουμε παρόμοια διανυσματικά μέσα στο Qdrant
3. **Σχετικές Δημοσιεύσεις**: Για οποιοδήποτε blog post, μπορούμε να βρούμε άλλες θέσεις με παρόμοια διανυσματικά μέσα

# Δομή έργου

Δημιουργήσαμε μια καθαρή, αρθρωτή δομή:

```
Mostlylucid.SemanticSearch/
├── Config/
│   └── SemanticSearchConfig.cs      # Configuration settings
├── Models/
│   ├── BlogPostDocument.cs          # Document model for indexing
│   └── SearchResult.cs               # Search result model
├── Services/
│   ├── IEmbeddingService.cs         # Embedding interface
│   ├── OnnxEmbeddingService.cs      # ONNX-based embeddings
│   ├── IVectorStoreService.cs       # Vector store interface
│   ├── QdrantVectorStoreService.cs  # Qdrant implementation
│   ├── ISemanticSearchService.cs    # High-level search interface
│   └── SemanticSearchService.cs     # Orchestration service
├── Extensions/
│   └── ServiceCollectionExtensions.cs  # DI registration
├── download-models.sh               # Model download script
└── README.md
```

# Εφαρμογή

## Βήμα 1: Ρύθμιση του Έργου

Πρώτα, δημιουργήστε τη νέα βιβλιοθήκη:

```bash
dotnet new classlib -n Mostlylucid.SemanticSearch -f net9.0
dotnet sln add Mostlylucid.SemanticSearch
```

Προσθέστε τα απαραίτητα πακέτα NuGet:

```bash
cd Mostlylucid.SemanticSearch
dotnet add package Microsoft.Extensions.Logging.Abstractions
dotnet add package Microsoft.ML.OnnxRuntime --version 1.21.1
dotnet add package Qdrant.Client --version 1.14.0
dotnet add reference ../Mostlylucid.Shared/Mostlylucid.Shared.csproj
```

## Βήμα 2: Ρύθμιση

Ας φτιάξουμε το μάθημα διαμόρφωσης. `IConfigSection` μοτίβο που χρησιμοποιείται σε όλη την Κυρίως διαυγή:

```csharp
using Mostlylucid.Shared.Config;

namespace Mostlylucid.SemanticSearch.Config;

/// <summary>
/// Configuration for semantic search functionality
/// </summary>

public class SemanticSearchConfig : IConfigSection
{
    public static string Section => "SemanticSearch";

    /// <summary>
    /// Enable or disable semantic search
    /// </summary>

    public bool Enabled { get; set; } = true;

    /// <summary>
    /// Qdrant server URL (e.g., http://localhost:6333)
    /// </summary>

    public string QdrantUrl { get; set; } = "http://localhost:6333";

    /// <summary>
    /// Optional read-only API key for Qdrant (used for search operations)
    /// </summary>

    public string? ReadApiKey { get; set; }

    /// <summary>
    /// Optional read-write API key for Qdrant (used for indexing operations)
    /// </summary>

    public string? WriteApiKey { get; set; }

    /// <summary>
    /// Collection name in Qdrant for blog posts
    /// </summary>

    public string CollectionName { get; set; } = "blog_posts";

    /// <summary>
    /// Path to the ONNX embedding model file
    /// </summary>

    public string EmbeddingModelPath { get; set; } = "models/all-MiniLM-L6-v2.onnx";

    /// <summary>
    /// Path to the tokenizer vocabulary file
    /// </summary>

    public string VocabPath { get; set; } = "models/vocab.txt";

    /// <summary>
    /// Embedding vector size (384 for all-MiniLM-L6-v2)
    /// </summary>

    public int VectorSize { get; set; } = 384;

    /// <summary>
    /// Number of related posts to return
    /// </summary>

    public int RelatedPostsCount { get; set; } = 5;

    /// <summary>
    /// Minimum similarity score (0-1) for related posts
    /// </summary>

    public float MinimumSimilarityScore { get; set; } = 0.5f;

    /// <summary>
    /// Number of search results to return
    /// </summary>

    public int SearchResultsCount { get; set; } = 10;
}
```

**Γιατί να διαχωρίσουμε τα πλήκτρα API;** Ασφάλεια! Το κλειδί ανάγνωσης σας μπορεί να χρησιμοποιηθεί σε δημόσια σημεία αναζήτησης, ενώ το κλειδί εγγραφής σας παραμένει server-side μόνο για διοικητικές λειτουργίες.

Προσθέστε αυτό στο δικό σας `appsettings.json`:

```json
{
  "SemanticSearch": {
    "Enabled": false,
    "QdrantUrl": "http://localhost:6333",
    "ReadApiKey": "",
    "WriteApiKey": "",
    "CollectionName": "blog_posts",
    "EmbeddingModelPath": "models/all-MiniLM-L6-v2.onnx",
    "VocabPath": "models/vocab.txt",
    "VectorSize": 384,
    "RelatedPostsCount": 5,
    "MinimumSimilarityScore": 0.5,
    "SearchResultsCount": 10
  }
}
```

## Βήμα 3: Η Υπηρεσία Ενσωμάτωσης ONNX

Χρησιμοποιούμε το μοντέλο all-MiniLM-L6-v2, το οποίο είναι ειδικά σχεδιασμένο για σημασιολογικές λειτουργίες ομοιότητας και λειτουργεί αποτελεσματικά σε CPU.

**Γιατί αυτό το μοντέλο;**

- Μικρό μέγεθος (~90MB)
- Γρήγορο συμπέρασμα για CPU (~50-100ms ανά ενσωμάτωση)
- Καλής ποιότητας παρεμβολές (384 διαστάσεις)
- Εκπαιδεύτηκε σε πάνω από 1 δισεκατομμύριο ζεύγη προτάσεων

Εδώ είναι η πλήρης εφαρμογή:

```csharp
using Microsoft.Extensions.Logging;
using Microsoft.ML.OnnxRuntime;
using Microsoft.ML.OnnxRuntime.Tensors;
using Mostlylucid.SemanticSearch.Config;
using System.Text.RegularExpressions;

namespace Mostlylucid.SemanticSearch.Services;

public class OnnxEmbeddingService : IEmbeddingService, IDisposable
{
    private readonly ILogger<OnnxEmbeddingService> _logger;
    private readonly SemanticSearchConfig _config;
    private readonly InferenceSession? _session;
    private readonly Dictionary<string, int> _vocabulary;
    private readonly SemaphoreSlim _semaphore = new(1, 1);
    private bool _disposed;

    private const int MaxSequenceLength = 256;
    private const string PadToken = "[PAD]";
    private const string UnkToken = "[UNK]";
    private const string ClsToken = "[CLS]";
    private const string SepToken = "[SEP]";

    public OnnxEmbeddingService(
        ILogger<OnnxEmbeddingService> logger,
        SemanticSearchConfig config)
    {
        _logger = logger;
        _config = config;
        _vocabulary = new Dictionary<string, int>();

        if (!_config.Enabled)
        {
            _logger.LogInformation("Semantic search is disabled");
            return;
        }

        try
        {
            // Check if model file exists
            if (!File.Exists(_config.EmbeddingModelPath))
            {
                _logger.LogWarning("Embedding model not found at {Path}. Semantic search will be disabled.",
                    _config.EmbeddingModelPath);
                return;
            }

            // Load vocabulary if it exists
            if (File.Exists(_config.VocabPath))
            {
                LoadVocabulary(_config.VocabPath);
            }

            // Create ONNX session with CPU execution provider
            var sessionOptions = new SessionOptions
            {
                ExecutionMode = ExecutionMode.ORT_SEQUENTIAL,
                GraphOptimizationLevel = GraphOptimizationLevel.ORT_ENABLE_ALL
            };

            _session = new InferenceSession(_config.EmbeddingModelPath, sessionOptions);
            _logger.LogInformation("ONNX embedding model loaded successfully from {Path}",
                _config.EmbeddingModelPath);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Failed to initialize ONNX embedding service");
        }
    }

    private void LoadVocabulary(string vocabPath)
    {
        var lines = File.ReadAllLines(vocabPath);
        for (int i = 0; i < lines.Length; i++)
        {
            var token = lines[i].Trim();
            if (!string.IsNullOrEmpty(token))
            {
                _vocabulary[token] = i;
            }
        }
        _logger.LogInformation("Loaded vocabulary with {Count} tokens", _vocabulary.Count);
    }

    public async Task<float[]> GenerateEmbeddingAsync(string text, CancellationToken cancellationToken = default)
    {
        if (_session == null || !_config.Enabled)
        {
            return new float[_config.VectorSize];
        }

        if (string.IsNullOrWhiteSpace(text))
        {
            return new float[_config.VectorSize];
        }

        // Use semaphore to prevent concurrent ONNX inference (not thread-safe)
        await _semaphore.WaitAsync(cancellationToken);
        try
        {
            return await Task.Run(() => GenerateEmbedding(text), cancellationToken);
        }
        finally
        {
            _semaphore.Release();
        }
    }

    private float[] GenerateEmbedding(string text)
    {
        try
        {
            // Tokenize the input text
            var tokens = Tokenize(text);

            // Create input tensors for ONNX model
            var inputIds = CreateInputTensor(tokens, "input_ids");
            var attentionMask = CreateAttentionMaskTensor(tokens.Length);
            var tokenTypeIds = CreateTokenTypeIdsTensor(tokens.Length);

            // Run inference
            var inputs = new List<NamedOnnxValue>
            {
                NamedOnnxValue.CreateFromTensor("input_ids", inputIds),
                NamedOnnxValue.CreateFromTensor("attention_mask", attentionMask),
                NamedOnnxValue.CreateFromTensor("token_type_ids", tokenTypeIds)
            };

            using var results = _session!.Run(inputs);

            // Extract the output tensor (sentence embedding)
            var output = results.First().AsTensor<float>();
            var embedding = output.ToArray();

            // Normalize the vector (L2 normalization)
            return NormalizeVector(embedding);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error generating embedding for text: {Text}",
                text[..Math.Min(100, text.Length)]);
            return new float[_config.VectorSize];
        }
    }

    private List<int> Tokenize(string text)
    {
        // Simple whitespace + punctuation tokenization
        var tokens = new List<int>();

        // Add [CLS] token at the start
        if (_vocabulary.TryGetValue(ClsToken, out var clsId))
            tokens.Add(clsId);

        // Tokenize the text
        var words = Regex.Split(text.ToLowerInvariant(), @"(\W+)")
            .Where(w => !string.IsNullOrWhiteSpace(w))
            .Take(MaxSequenceLength - 2); // Leave room for [CLS] and [SEP]

        foreach (var word in words)
        {
            if (_vocabulary.Count > 0)
            {
                if (_vocabulary.TryGetValue(word, out var tokenId))
                    tokens.Add(tokenId);
                else if (_vocabulary.TryGetValue(UnkToken, out var unkId))
                    tokens.Add(unkId);
            }
            else
            {
                // Fallback: use hash code as token ID
                tokens.Add(Math.Abs(word.GetHashCode()) % 30000);
            }
        }

        // Add [SEP] token at the end
        if (_vocabulary.TryGetValue(SepToken, out var sepId))
            tokens.Add(sepId);

        return tokens;
    }

    private Tensor<long> CreateInputTensor(List<int> tokens, string name)
    {
        var length = Math.Min(tokens.Count, MaxSequenceLength);
        var tensorData = new long[1, MaxSequenceLength];

        for (int i = 0; i < length; i++)
        {
            tensorData[0, i] = tokens[i];
        }

        // Pad the rest
        var padId = _vocabulary.TryGetValue(PadToken, out var id) ? id : 0;
        for (int i = length; i < MaxSequenceLength; i++)
        {
            tensorData[0, i] = padId;
        }

        return new DenseTensor<long>(tensorData, new[] { 1, MaxSequenceLength });
    }

    private Tensor<long> CreateAttentionMaskTensor(int actualLength)
    {
        var length = Math.Min(actualLength, MaxSequenceLength);
        var tensorData = new long[1, MaxSequenceLength];

        for (int i = 0; i < length; i++)
        {
            tensorData[0, i] = 1; // Attend to actual tokens
        }

        return new DenseTensor<long>(tensorData, new[] { 1, MaxSequenceLength });
    }

    private Tensor<long> CreateTokenTypeIdsTensor(int actualLength)
    {
        var tensorData = new long[1, MaxSequenceLength];
        // All zeros for single sentence
        return new DenseTensor<long>(tensorData, new[] { 1, MaxSequenceLength });
    }

    private float[] NormalizeVector(float[] vector)
    {
        // L2 normalization
        var sumOfSquares = vector.Sum(v => v * v);
        var magnitude = MathF.Sqrt(sumOfSquares);

        if (magnitude > 0)
        {
            for (int i = 0; i < vector.Length; i++)
            {
                vector[i] /= magnitude;
            }
        }

        return vector;
    }

    public void Dispose()
    {
        if (_disposed) return;

        _session?.Dispose();
        _semaphore?.Dispose();
        _disposed = true;

        GC.SuppressFinalize(this);
    }
}
```

**Κύρια σημεία για τους κατώτερους devs:**

1. **Τοκενοποίηση**: Σπάζουμε το κείμενο σε μικρότερα κομμάτια (tokens) που το μοντέλο μπορεί να καταλάβει
2. **Τενσέρ**: Αυτές είναι πολυδιάστατες συστοιχίες με τις οποίες λειτουργούν τα μοντέλα ONNX
3. **Μάσκα Προσοχής**: Tells the model which parts of the input are actual content vs. padding
4. **L2 Ομαλοποίηση**: Κάνει όλους τους φορείς να έχουν το ίδιο "μήκος," ώστε να μπορούμε να τους συγκρίνουν δίκαια
5. **ΣεμαφόρCity name (optional, probably does not need a translation)**: Εξασφαλίζει την ασφάλεια των νημάτων (το ONNX δεν είναι ασφαλές από προεπιλογή)

## Βήμα 4: Qdrant Vector Store

Τώρα ας εφαρμόσουμε την διανυσματική αποθήκευση και αναζήτηση:

```csharp
using Microsoft.Extensions.Logging;
using Mostlylucid.SemanticSearch.Config;
using Mostlylucid.SemanticSearch.Models;
using Qdrant.Client;
using Qdrant.Client.Grpc;

namespace Mostlylucid.SemanticSearch.Services;

public class QdrantVectorStoreService : IVectorStoreService
{
    private readonly ILogger<QdrantVectorStoreService> _logger;
    private readonly SemanticSearchConfig _config;
    private readonly QdrantClient? _client;
    private bool _collectionInitialized;

    public QdrantVectorStoreService(
        ILogger<QdrantVectorStoreService> logger,
        SemanticSearchConfig config)
    {
        _logger = logger;
        _config = config;

        if (!_config.Enabled)
        {
            _logger.LogInformation("Semantic search is disabled");
            return;
        }

        try
        {
            var uri = new Uri(_config.QdrantUrl);
            var host = uri.Host;
            var port = uri.Port > 0 ? uri.Port : 6334; // Default gRPC port

            _client = new QdrantClient(host, port, https: uri.Scheme == "https");
            _logger.LogInformation("Connected to Qdrant at {Host}:{Port}", host, port);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Failed to connect to Qdrant at {Url}", _config.QdrantUrl);
        }
    }

    public async Task InitializeCollectionAsync(CancellationToken cancellationToken = default)
    {
        if (_client == null || !_config.Enabled || _collectionInitialized)
            return;

        try
        {
            var collections = await _client.ListCollectionsAsync(cancellationToken);
            var collectionExists = collections.Any(c => c.Name == _config.CollectionName);

            if (!collectionExists)
            {
                _logger.LogInformation("Creating collection {CollectionName}", _config.CollectionName);

                await _client.CreateCollectionAsync(
                    collectionName: _config.CollectionName,
                    vectorsConfig: new VectorParams
                    {
                        Size = (ulong)_config.VectorSize,
                        Distance = Distance.Cosine // Cosine similarity for semantic search
                    },
                    cancellationToken: cancellationToken
                );

                _logger.LogInformation("Collection {CollectionName} created successfully", _config.CollectionName);
            }

            _collectionInitialized = true;
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Failed to initialize collection {CollectionName}", _config.CollectionName);
            throw;
        }
    }

    public async Task<List<SearchResult>> FindRelatedPostsAsync(
        string slug,
        string language,
        int limit = 5,
        CancellationToken cancellationToken = default)
    {
        if (_client == null || !_config.Enabled)
            return new List<SearchResult>();

        try
        {
            // Find the document by slug and language
            var scrollResults = await _client.ScrollAsync(
                collectionName: _config.CollectionName,
                filter: new Filter
                {
                    Must =
                    {
                        new Condition
                        {
                            Field = new FieldCondition
                            {
                                Key = "slug",
                                Match = new Match { Keyword = slug }
                            }
                        },
                        new Condition
                        {
                            Field = new FieldCondition
                            {
                                Key = "language",
                                Match = new Match { Keyword = language }
                            }
                        }
                    }
                },
                limit: 1,
                cancellationToken: cancellationToken
            );

            var point = scrollResults.FirstOrDefault();
            if (point == null)
            {
                _logger.LogWarning("Post {Slug} ({Language}) not found in vector store", slug, language);
                return new List<SearchResult>();
            }

            // Use the document's vector to find similar posts
            var searchResults = await _client.SearchAsync(
                collectionName: _config.CollectionName,
                vector: point.Vectors.Vector.Data.ToArray(),
                limit: (ulong)(limit + 1), // +1 because the first result will be the post itself
                scoreThreshold: _config.MinimumSimilarityScore,
                cancellationToken: cancellationToken
            );

            // Filter out the original post and return top N similar posts
            return searchResults
                .Where(r => r.Payload["slug"].StringValue != slug || r.Payload["language"].StringValue != language)
                .Take(limit)
                .Select(result => new SearchResult
                {
                    Slug = result.Payload["slug"].StringValue,
                    Title = result.Payload["title"].StringValue,
                    Language = result.Payload["language"].StringValue,
                    Categories = result.Payload.TryGetValue("categories", out var cats)
                        ? cats.ListValue.Values.Select(v => v.StringValue).ToList()
                        : new List<string>(),
                    Score = result.Score,
                    PublishedDate = DateTime.Parse(result.Payload["published_date"].StringValue)
                })
                .ToList();
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Failed to find related posts for {Slug} ({Language})", slug, language);
            return new List<SearchResult>();
        }
    }

    // ... Additional methods for IndexDocument, Search, Delete, etc.
}
```

**Τι συμβαίνει εδώ:**

1. **Απόσταση συνημίτονου**: Χρησιμοποιούμε ομοιότητα συνημίτονου, η οποία είναι ιδανική για τη σύγκριση ομαλοποιημένων διανυσμάτων
2. **Αποθήκευση μεταδεδομένα**: Το Qdrant μας επιτρέπει να αποθηκεύουμε επιπλέον δεδομένα (πληρωμή) παράλληλα με τους φορείς
3. **Φίλτρο**: Μπορούμε να φιλτράρουμε τα αποτελέσματα με μεταδεδομένα πριν συγκρίνουμε τους φορείς
4. **Κατώτατο όριο βαθμολογίας**: Μόνο αποτελέσματα επιστροφής πάνω από μια ορισμένη βαθμολογία ομοιότητας

## Βήμα 5: Η Υπηρεσία Ενορχήστρωσης

Αυτή η υπηρεσία υψηλού επιπέδου συνδέει τα πάντα μεταξύ τους:

```csharp
using Microsoft.Extensions.Logging;
using Mostlylucid.SemanticSearch.Config;
using Mostlylucid.SemanticSearch.Models;
using System.Security.Cryptography;
using System.Text;

namespace Mostlylucid.SemanticSearch.Services;

public class SemanticSearchService : ISemanticSearchService
{
    private readonly ILogger<SemanticSearchService> _logger;
    private readonly SemanticSearchConfig _config;
    private readonly IEmbeddingService _embeddingService;
    private readonly IVectorStoreService _vectorStoreService;

    public SemanticSearchService(
        ILogger<SemanticSearchService> logger,
        SemanticSearchConfig config,
        IEmbeddingService embeddingService,
        IVectorStoreService vectorStoreService)
    {
        _logger = logger;
        _config = config;
        _embeddingService = embeddingService;
        _vectorStoreService = vectorStoreService;
    }

    public async Task IndexPostAsync(BlogPostDocument document, CancellationToken cancellationToken = default)
    {
        if (!_config.Enabled)
            return;

        try
        {
            // Prepare text for embedding: combine title and content
            // We give more weight to the title by including it twice
            var textToEmbed = $"{document.Title}. {document.Title}. {document.Content}";

            // Truncate to reasonable length (embedding models have token limits)
            const int maxLength = 2000;
            if (textToEmbed.Length > maxLength)
            {
                textToEmbed = textToEmbed[..maxLength];
            }

            // Generate embedding
            var embedding = await _embeddingService.GenerateEmbeddingAsync(textToEmbed, cancellationToken);

            // Compute content hash if not provided
            if (string.IsNullOrEmpty(document.ContentHash))
            {
                document.ContentHash = ComputeContentHash(document.Content);
            }

            // Store in vector database
            await _vectorStoreService.IndexDocumentAsync(document, embedding, cancellationToken);

            _logger.LogInformation("Indexed post {Slug} ({Language})", document.Slug, document.Language);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Failed to index post {Slug} ({Language})", document.Slug, document.Language);
        }
    }

    public async Task<List<SearchResult>> SearchAsync(
        string query,
        int limit = 10,
        CancellationToken cancellationToken = default)
    {
        if (!_config.Enabled || string.IsNullOrWhiteSpace(query))
            return new List<SearchResult>();

        try
        {
            // Generate embedding for the search query
            var queryEmbedding = await _embeddingService.GenerateEmbeddingAsync(query, cancellationToken);

            // Search in vector store
            var results = await _vectorStoreService.SearchAsync(
                queryEmbedding,
                Math.Min(limit, _conken);

            _logger.LogDebug("Search for '{Query}' returned {Count} results", query, results.Count);

            return results;
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Search failed for query '{Query}'", query);
            return new List<SearchResult>();
        }
    }

    public async Task<List<SearchResult>> GetRelatedPostsAsync(
        string slug,
        string language,
        int limit = 5,
        CancellationToken cancellationToken = default)
    {
        if (!_config.Enabled)
            return new List<SearchResult>();

        try
        {
            var results = await _vectorStoreService.FindRelatedPostsAsync(
                slug,
                language,
                Math.Min(limit, _config.RelatedPostsCount),
                cancellationToken);

            _logger.LogDebug("Found {Count} related posts for {Slug} ({Language})",
                results.Count, slug, language);

            return results;
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Failed to get related posts for {Slug} ({Language})", slug, language);
            return new List<SearchResult>();
        }
    }

    private string ComputeContentHash(string content)
    {
        using var sha256 = SHA256.Create();
        var bytes = Encoding.UTF8.GetBytes(content);
        var hashBytes = sha256.ComputeHash(bytes);
        return Convert.ToBase64String(hashBytes);
    }
}
```

## Βήμα 6: Ρύθμιση εγχύσεων εξάρτησης

Καταχωρήστε τα πάντα στο δοχείο DI:

```csharp
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Mostlylucid.SemanticSearch.Config;
using Mostlylucid.SemanticSearch.Services;
using Mostlylucid.Shared.Config;

namespace Mostlylucid.SemanticSearch.Extensions;

public static class ServiceCollectionExtensions
{
    public static void AddSemanticSearch(
        this IServiceCollection services,
        IConfiguration configuration)
    {
        // Bind configuration using POCO pattern
        services.ConfigurePOCO<SemanticSearchConfig>(
            configuration.GetSection(SemanticSearchConfig.Section));

        // Register services as singletons for efficiency
        services.AddSingleton<IEmbeddingService, OnnxEmbeddingService>();
        services.AddSingleton<IVectorStoreService, QdrantVectorStoreService>();
        services.AddSingleton<ISemanticSearchService, SemanticSearchService>();
    }
}
```

Σε σας `Program.cs`:

```csharp
using Mostlylucid.SemanticSearch.Extensions;
using Mostlylucid.SemanticSearch.Services;

// Add services
services.AddSemanticSearch(config);

// Initialize after building the app
using (var scope = app.Services.CreateScope())
{
    var semanticSearch = scope.ServiceProvider.GetRequiredService<ISemanticSearchService>();
    await semanticSearch.InitializeAsync();
}
```

# Εγκατάσταση υποδομής

## Docker Compose για Qdrant

Δημιουργήστε ένα ξεχωριστό docker-compose αρχείο για σημασιολογικές υπηρεσίες αναζήτησης:

```yaml
version: '3.8'

services:
  qdrant:
    image: qdrant/qdrant:latest
    container_name: mostlylucid-qdrant
    restart: unless-stopped
    ports:
      - "6333:6333"  # HTTP API
      - "6334:6334"  # gRPC API
    volumes:
      - qdrant_storage:/qdrant/storage
    environment:
      - QDRANT__SERVICE__HTTP_PORT=6333
      - QDRANT__SERVICE__GRPC_PORT=6334
    networks:
      - mostlylucid_network
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:6333/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 40s

volumes:
  qdrant_storage:
    driver: local

networks:
  mostlylucid_network:
    name: mostlylucidweb_app_network
    external: true
```

Ξεκίνα με:

```bash
docker-compose -f semantic-search-docker-compose.yml up -d
```

## Κατεβάστε το Μοντέλο Ενσωμάτωσης

Χρησιμοποιούμε το [Όλα-MiniLM-L6-v2](https://huggingface.co/sentence-transformers/all-MiniLM-L6-v2) μοντέλο από το Hugging Face's [Μετασχηματιστές ποινή](https://www.sbert.net/) βιβλιοθήκη. Αυτό το μοντέλο είναι ειδικά εκπαιδευμένο σε σημασιολογικές εργασίες ομοιότητας και παράγει 384-διαστατικές καταχωρήσεις.

### Αυτόματη λήψη (Συνιστάται)

Η υπηρεσία κατεβάζει αυτόματα το μοντέλο από το Hugging Face κατά την πρώτη εκτέλεση, αν δεν υπάρχει:

```csharp
// In OnnxEmbeddingService.cs
private const string ModelUrl = "https://huggingface.co/sentence-transformers/all-MiniLM-L6-v2/resolve/main/onnx/model.onnx";
private const string VocabUrl = "https://huggingface.co/sentence-transformers/all-MiniLM-L6-v2/resolve/main/vocab.txt";

public async Task EnsureInitializedAsync(CancellationToken cancellationToken = default)
{
    if (_initialized || !_config.Enabled) return;

    // Download model if not exists
    if (!File.Exists(_config.EmbeddingModelPath))
    {
        _logger.LogInformation("Downloading ONNX embedding model to {Path}...", _config.EmbeddingModelPath);
        await DownloadFileAsync(ModelUrl, _config.EmbeddingModelPath, cancellationToken);
    }

    // Download vocab if not exists
    if (!File.Exists(_config.VocabPath))
    {
        _logger.LogInformation("Downloading vocabulary file to {Path}...", _config.VocabPath);
        await DownloadFileAsync(VocabUrl, _config.VocabPath, cancellationToken);
    }

    // Initialize ONNX session...
}
```

Αυτό είναι ιδιαίτερα χρήσιμο κατά την ανάπτυξη με Docker - μπορείτε να χαρτογραφήσετε έναν όγκο για τον κατάλογο μοντέλων:

```yaml
volumes:
  - ./mlmodels:/app/mlmodels  # Model persists across container restarts
```

### Χειροκίνητη λήψη

Εναλλακτικά, μπορείτε να κατεβάσετε με το χέρι:

```bash
chmod +x Mostlylucid.SemanticSearch/download-models.sh
./Mostlylucid.SemanticSearch/download-models.sh
```

Ή απευθείας από το Hugging Face:

```bash
mkdir -p mlmodels
curl -L https://huggingface.co/sentence-transformers/all-MiniLM-L6-v2/resolve/main/onnx/model.onnx -o mlmodels/all-MiniLM-L6-v2.onnx
curl -L https://huggingface.co/sentence-transformers/all-MiniLM-L6-v2/resolve/main/vocab.txt -o mlmodels/vocab.txt
```

Αυτή η λήψη:

- `all-MiniLM-L6-v2.onnx` (~90MB) [Μοντέλο προσθήκης ONNX-εξαγωγής](https://huggingface.co/sentence-transformers/all-MiniLM-L6-v2/tree/main/onnx)
- `vocab.txt` (~230KB) [WordPiece λεξιλόγιο πέναλτι](https://huggingface.co/sentence-transformers/all-MiniLM-L6-v2/blob/main/vocab.txt)

# Εκτιμήσεις Απόδοσης

## Ενσωμάτωση της Γενιάς

- **Επιδόσεις CPU**~50-100ms ανά ενσωμάτωση σε ένα σύγχρονο CPU
- **Βελτιστοποίηση**: Χρησιμοποιούμε ένα semaphore για να αποτρέψουμε ταυτόχρονα το συμπέρασμα ONNX
- **Παρτίδα**: Για τον μαζικό ευρετηρίαση, θέσεις διεργασιών σε παρτίδες των 10-20

## Αναζήτηση διάνυσμα

- **Ταχύτητα αναζήτησης**: <10ms για συλλογές έως 100K φορείς
- **Χρήση μνήμης**~1KB ανά διανυσματικό φορέα (με μεταδεδομένα)
- **Ευκαμψία**: Το Qdrant μπορεί να χειριστεί εκατομμύρια διανυσματικά συστήματα σε μέτριο υλικό

## Στρατηγική σύλληψης

Χρησιμοποιούμε ASP.NET Core caching εξόδου:

```csharp
[OutputCache(Duration = 7200, VaryByRouteValueNames = new[] {"slug", "language"})]
```

Αυτή η κρύπτη σχετίζεται με θέσεις για 2 ώρες, μειώνοντας σημαντικά το φορτίο.

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

Σε αυτό το σημείο έχετε ένα πλήρες, λειτουργικό σημασιολογικό ίδρυμα αναζήτησης:

- ✅ **Ενσωμάτωση ONNX** - CPU φιλικό, auto-downloads από Hugging πρόσωπο
- ✅ **Αποθήκευση διάνυσμα QdrantName** - Γρήγορη αναζήτηση ομοιότητας με το φιλτράρισμα μεταδεδομένα
- ✅ **Σχετικές θέσεις** - Βρείτε σημασιολογικά παρόμοιο περιεχόμενο
- ✅ **Αναζήτηση API** - Φυσικά γλωσσικά ερωτήματα
- ✅ **Δείκτης περιεχομένου** - Store blog posts as vectors

**Αυτό είναι το ακριβές setup τρέχει σε αυτό το blog** - Μηδέν GPU, μηδενικό επιπλέον κόστος.

# Επόμενος: Σημαντική Αναζήτηση σε Δράση

Το [Μέρος 4β: Σημαντική Αναζήτηση σε Δράση](/blog/semantic-search-in-action), καλύπτουμε:

- **Αναζήτηση επόμενου τύπου** - Πώς η αναζήτηση-όπως-σας-τύπου λειτουργεί με Alpine.js
- **Υβριδική αναζήτηση** - Συνδυάζοντας σημασιολογικό + PostgreSQL πλήρες κείμενο με αμοιβαία Rank Fusion
- **Αναζήτηση API** - Πλήρης τεκμηρίωση API με φίλτρα
- **Σχετικές Δημοσιεύσεις UI** - DaisyUI συστατικά με HTMX τεμπέλη φόρτωση
- **Προηγμένα φίλτρα** - Γλώσσα και εύρος ημερομηνίας φιλτραρίσματος

**Συνεχίστε να [Μέρος 4β](/blog/semantic-search-in-action) για την αναζήτηση UI και υβριδική εφαρμογή αναζήτησης.**

Τότε [Μέρος 5: Υβριδική Αναζήτηση & Auto-Indexing](/blog/rag-hybrid-search-and-indexing) καλύπτει τα πρότυπα ενσωμάτωσης στην παραγωγή.

# Πόροι

## Τεκμηρίωση ONNX

- [Επίσημη ιστοσελίδα ONNX](https://onnx.ai/) - Το ανοικτό πρότυπο για τα μοντέλα ML
- [ONNX Runtime](https://onnxruntime.ai/) - Η μηχανή υψηλών επιδόσεων της Microsoft
- [ONNX Runtime .NET API](https://onnxruntime.ai/docs/api/csharp-api.html) - C# API docs
- [Απόδοση Runtime ONNX](https://onnxruntime.ai/docs/performance/tune-performance/threading.html) - Οδηγός βελτιστοποίησης

## Τεκμηρίωση Qdrant

- [Αυτό-Hosted Vector Databases με Qdrant](/blog/self-hosted-vector-databases-qdrant) - Βαθιά κατάδυση σε έννοιες Qdrant και C# πελάτη
- [Επίσημοι Γιατροί του Qdrant](https://qdrant.tech/documentation/) - Κύριο κέντρο τεκμηρίωσης
- [Γρήγορη εκκίνηση QdrantName](https://qdrant.tech/documentation/quick-start/) - Ξεκινάμε.
- [Qdrant Διανυσματική ευρετηρίαση](https://qdrant.tech/documentation/concepts/indexing/#vector-index) - Αλγόριθμος HNSW
- [Qdrant .NET Πελάτης](https://github.com/qdrant/qdrant-dotnet) - Επίσημη Εφημερίδα .NET SDK

## Ενσωμάτωση μοντέλων

- [Όλα-MiniLM-L6-v2](https://huggingface.co/sentence-transformers/all-MiniLM-L6-v2) - Το μοντέλο που χρησιμοποιούμε.
- [Μετασχηματιστές ποινή](https://www.sbert.net/) - Semantic enbeddings βιβλιοθήκη

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

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

- `Mostlylucid.SemanticSearch/` - Κεντρική σημασιολογική βιβλιοθήκη αναζήτησης