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

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

<!-- category -- ASP.NET, Semantic Search, Alpine.js, HTMX, Hybrid Search, RAG, AI-Article -->
# Εισαγωγή

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

- [Μέρος 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β: Σημαντική Αναζήτηση σε Δράση** (αυτό το άρθρο) - Στοιχεία τύπου, υβριδική αναζήτηση και UI
- [Μέρος 5: Υβριδική Αναζήτηση & Auto-Indexing](/blog/rag-hybrid-search-and-indexing) - Τρόποι ενσωμάτωσης στην παραγωγή
- [Μέρος 6: GraphRAG](/blog/graphrag-knowledge-graphs-for-rag) - Γραφήματα γνώσης για την κατανόηση του επιπέδου του σώματος

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

Αυτό το άρθρο καλύπτει την πραγματική εμπειρία αναζήτησης οι χρήστες αλληλεπιδρούν με αυτό το blog.

[TOC]

# Η Εμπειρία Αναζήτησης ΤύπουName

Το πλαίσιο αναζήτησης στην κορυφή αυτού του ιστότοπου παρέχει άμεση αναζήτηση-όπως-σας-τύπου αποτελέσματα.

```mermaid
sequenceDiagram
    participant U as User
    participant A as Alpine.js
    participant API as SearchApi
    participant H as HybridSearch
    participant S as Semantic Search
    participant P as PostgreSQL

    U->>A: Types "docker"
    A->>A: Debounce 300ms
    A->>API: GET /api/search/docker
    API->>H: HybridSearchAsync("docker")
    par Parallel Search
        H->>S: SearchAsync("docker", 20)
        H->>P: GetSearchResultForComplete("docker")
    end
    S-->>H: Semantic results (by meaning)
    P-->>H: Full-text results (by keywords)
    H->>H: Apply RRF scoring
    H-->>API: Combined results
    API-->>A: JSON results
    A->>U: Display dropdown
```

## Συνιστώσα τύπου Alpine.js

Το πλαίσιο αναζήτησης χρησιμοποιεί [Alpine.js](https://alpinejs.dev/) για αντιδραστικό UI χωρίς βαριά πλαίσια JavaScript. Εδώ είναι το συστατικό:

```javascript
export function typeahead() {
    return {
        query: '',
        results: [],
        highlightedIndex: -1, // Tracks keyboard navigation

        search() {
            // Minimum 2 characters to trigger search
            if (this.query.length < 2) {
                this.results = [];
                this.highlightedIndex = -1;
                return;
            }

            fetch(`/api/search/${encodeURIComponent(this.query)}`, {
                method: 'GET',
                headers: { 'Content-Type': 'application/json' }
            })
            .then(response => {
                if (response.ok) return response.json();
                return Promise.reject(response);
            })
            .then(data => {
                this.results = data;
                this.highlightedIndex = -1;
                // Process HTMX attributes in results
                this.$nextTick(() => {
                    htmx.process(document.getElementById('searchresults'));
                });
            })
            .catch((response) => {
                console.log("Error fetching search results");
            });
        },

        // Keyboard navigation
        moveDown() {
            if (this.highlightedIndex < this.results.length - 1) {
                this.highlightedIndex++;
            }
        },

        moveUp() {
            if (this.highlightedIndex > 0) {
                this.highlightedIndex--;
            }
        },

        selectHighlighted() {
            if (this.highlightedIndex >= 0 && this.highlightedIndex < this.results.length) {
                this.selectResult(this.highlightedIndex);
            }
        },

        selectResult(selectedIndex) {
            // Click the HTMX link to navigate
            let links = document.querySelectorAll('#searchresults a');
            links[selectedIndex].click();
            this.results = [];
            this.highlightedIndex = -1;
            this.query = '';
        }
    }
}
```

**Βασικά χαρακτηριστικά:**

1. **Αποκαλυμμένη είσοδος**: 300ms καθυστέρηση αποτρέπει το σφυροκόπημα του διακομιστή
2. **Ελάχιστο μήκος**: Τουλάχιστον 2 χαρακτήρες που απαιτούνται
3. **Πλοήγηση πληκτρολογίου**: Κλειδιά βέλους + Εισάγετε για προσβασιμότητα
4. **HTMX Integration**: Αποτελέσματα χρήσης HTMX για ομαλή πλοήγηση

## Το πλαίσιο αναζήτησης HTML

```html
<div x-data="window.mostlylucid.typeahead()"
     class="relative"
     x-on:click.outside="results = []">

    <label class="input input-sm bg-white dark:bg-custom-dark-bg input-bordered flex items-center gap-2">
        <input
            type="text"
            x-model="query"
            x-on:input.debounce.300ms="search"
            x-on:keydown.down.prevent="moveDown"
            x-on:keydown.up.prevent="moveUp"
            x-on:keydown.enter.prevent="selectHighlighted"
            placeholder="Search..."
            class="border-0 grow input-sm text-black dark:text-white bg-transparent w-full"/>
        <i class="bx bx-search"></i>
    </label>

    <!-- Dropdown Results -->
    <ul x-show="results.length > 0"
        id="searchresults"
        class="absolute z-10 my-2 w-full bg-white dark:bg-custom-dark-bg border rounded-lg shadow-lg">
        <template x-for="(result, index) in results" :key="result.slug">
            <li :class="{'bg-blue-light dark:bg-blue-dark': index === highlightedIndex}"
                class="cursor-pointer text-sm p-2 m-2 hover:bg-blue-light dark:hover:bg-blue-dark">
                <a hx-boost="true"
                   hx-target="#contentcontainer"
                   hx-swap="innerHTML show:window:top"
                   :href="result.url"
                   x-text="result.title"></a>
            </li>
        </template>
    </ul>
</div>
```

**Γιατί; `x-on:click.outside`?** Κάνοντας κλικ έξω από την πτώση κλείνει - πρότυπο UX μοτίβο για autocomplete.

# Η αναζήτηση API

Η `/api/search/{query}` Εδώ είναι το χειριστήριο:

```csharp
[ApiController]
[Route("api")]
public class SearchApi(
    BlogSearchService searchService,
    UmamiBackgroundSender umamiBackgroundSender,
    ISemanticSearchService semanticSearchService,
    SemanticSearchConfig semanticSearchConfig) : ControllerBase
{
    private const int RrfConstant = 60; // Reciprocal Rank Fusion constant

    [HttpGet]
    [Route("search/{query}")]
    [OutputCache(Duration = 3600, VaryByQueryKeys = new[] { "query" })]
    public async Task<Results<JsonHttpResult<List<SearchResults>>, BadRequest<string>>> Search(string query)
    {
        using var activity = Log.Logger.StartActivity("Search {query}", query);
        try
        {
            var host = Request.Host.Value;
            List<SearchResults> output;

            // Use hybrid search if semantic search is enabled
            if (semanticSearchConfig.Enabled)
            {
                output = await HybridSearchAsync(query, host);
            }
            else
            {
                // Fallback to full-text search only
                output = await FullTextSearchAsync(query, host);
            }

            // Track search event for analytics
            var encodedQuery = HttpUtility.UrlEncode(query);
            await umamiBackgroundSender.Track("searchEvent", new UmamiEventData { { "query", encodedQuery } });

            return TypedResults.Json(output);
        }
        catch (Exception e)
        {
            Log.Error(e, "Error in search");
            return TypedResults.BadRequest("Error in search");
        }
    }
}
```

**Σημαντικές αποφάσεις σχεδιασμού:**

1. **Σημαία χαρακτηριστικών**: `semanticSearchConfig.Enabled` Σας επιτρέπει να ενεργοποιήσετε σημασιολογική αναζήτηση
2. **Αποθήκευση εξόδου**: 1-ωρη κρύπτη μειώνει το φορτίο του διακομιστή για κοινά ερωτήματα
3. **Ανίχνευση ανάλυσης**: Κάθε αναζήτηση παρακολουθείται (βοηθά στην κατανόηση της συμπεριφοράς των χρηστών)
4. **Ευχάριστη Αποδόμηση**: Πέφτει πίσω στο PostgreSQL εάν η σημασιολογική αναζήτηση αποτύχει

# Υβριδική αναζήτηση με αμοιβαία Rank Fusion

Η πραγματική μαγεία είναι **υβριδική αναζήτηση** Χρησιμοποιούμε αμοιβαία Rank Fusion (RRF) για να τα συγχωνεύσουμε δίκαια.

## Γιατί το Υβριδικό Ψάξιμο;

Διαφορετικές προσεγγίσεις αναζήτησης έχουν διαφορετικές δυνάμεις:

Τύπος αναζήτησης Δυνατότητες Αδυναμίες
|------------|-----------|------------|
| **Σημαντική** Συνώνυμα, έννοια, έννοιες Μπορεί να χάσει ακριβείς φράσεις
| **Πλήρες κείμενο** Ακριβές λέξεις-κλειδιά, τεχνικοί όροι ~ Δεν συνώνυμο κατανόηση ~

**Παράδειγμα:** Αναζήτηση για "προώθηση"

- Σημασιολογικά ευρήματα: "Docker tutorials," "Kubernetes guides" (σχετικές έννοιες)
- Full-text founds: Posts containing exactly "putchment"
- Το Hybrid παίρνει το καλύτερο και από τα δύο!

## Ο Αλγόριθμος του RRF

```mermaid
flowchart TB
    subgraph Semantic[Semantic Results]
        S1[Docker Containers - 0.92]
        S2[Kubernetes Basics - 0.87]
        S3[Container Security - 0.81]
    end

    subgraph FullText[Full-Text Results]
        F1[Container Security - rank 1]
        F2[Docker Containers - rank 2]
        F3[CI/CD Pipelines - rank 3]
    end

    subgraph RRF[RRF Scores]
        R1[Container Security = 0.0327]
        R2[Docker Containers = 0.0325]
        R3[Kubernetes Basics = 0.0161]
        R4[CI/CD Pipelines = 0.0159]
    end

    subgraph Final[Final Ranking]
        O1[Container Security]
        O2[Docker Containers]
        O3[Kubernetes Basics]
        O4[CI/CD Pipelines]
    end

    S1 --> R2
    S2 --> R3
    S3 --> R1
    F1 --> R1
    F2 --> R2
    F3 --> R4
    R1 --> O1
    R2 --> O2
    R3 --> O3
    R4 --> O4
```

**Ο τύπος:** `score = Σ(1 / (k + rank))`

Όπου:

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

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

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

## Εφαρμογή

```csharp
private async Task<List<SearchResults>> HybridSearchAsync(string query, string host)
{
    // Run both searches in parallel
    var fullTextTask = GetFullTextResultsAsync(query);
    var semanticTask = semanticSearchService.SearchAsync(query, limit: 20);

    await Task.WhenAll(fullTextTask, semanticTask);

    var fullTextResults = await fullTextTask;
    var semanticResults = await semanticTask;

    // Apply Reciprocal Rank Fusion to combine results
    var rrfScores = new Dictionary<string, (double Score, string Title, string Slug)>();

    // Score full-text results
    for (int i = 0; i < fullTextResults.Count; i++)
    {
        var (title, slug) = fullTextResults[i];
        var key = slug.ToLowerInvariant();
        var rrfScore = 1.0 / (RrfConstant + i + 1);

        if (rrfScores.TryGetValue(key, out var existing))
        {
            rrfScores[key] = (existing.Score + rrfScore, title, slug);
        }
        else
        {
            rrfScores[key] = (rrfScore, title, slug);
        }
    }

    // Score semantic results
    for (int i = 0; i < semanticResults.Count; i++)
    {
        var result = semanticResults[i];
        var key = result.Slug.ToLowerInvariant();
        var rrfScore = 1.0 / (RrfConstant + i + 1);

        if (rrfScores.TryGetValue(key, out var existing))
        {
            rrfScores[key] = (existing.Score + rrfScore, existing.Title, existing.Slug);
        }
        else
        {
            rrfScores[key] = (rrfScore, result.Title, result.Slug);
        }
    }

    // Sort by combined RRF score and return top results
    return rrfScores.Values
        .OrderByDescending(x => x.Score)
        .Take(15)
        .Select(x => new SearchResults(
            x.Title.Trim(),
            x.Slug,
            Url.ActionLink("Show", "Blog", new { x.Slug }, "https", host)))
        .ToList();
}
```

**Βασικά στοιχεία εφαρμογής:**

1. **Παράλληλη εκτέλεση**: Και οι δύο έρευνες εκτελούνται ταυτόχρονα (`Task.WhenAll`)
2. **Περίπτωση αναίσθητης καθυστέρησης**: Slugs normalized με `ToLowerInvariant()`
3. **Συγκέντρωση βαθμολογίαςName**: Ίδια θέση και στις δύο πηγές παίρνει τα αποτελέσματα προστίθενται
4. **Κορυφαία 15 Αποτελέσματα**: Αρκετά για το typeproof, όχι συντριπτικό

# Full-Text Search Fallback

Όταν η σημασιολογική αναζήτηση είναι απενεργοποιημένη ή αποτυγχάνει, επιστρέφουμε στην αναζήτηση πλήρους κειμένου PostgreSQL.

## Χειρισμός ερωτήσεων

Η αναζήτηση πλήρους κειμένου χειρίζεται δύο περιπτώσεις διαφορετικά:

```csharp
private async Task<List<(string Title, string Slug)>> GetFullTextResultsAsync(string query)
{
    if (!query.Contains(' '))
        return await searchService.GetSearchResultForComplete(query);  // Wildcard
    else
        return await searchService.GetSearchResultForQuery(query);     // Web search
}
```

**Μία λέξη** ("docker"): Χρήση αναζήτησης προθεμάτων Wildcard `docker:*`
**Πολλαπλές λέξεις** ("docker containers"): Χρησιμοποιεί τη σύνταξη αναζήτησης ιστού του PostgreSQL

## Ερωτήματα PostgreSQL

```csharp
// Single word with wildcard
private IQueryable<BlogPostEntity> QueryForWildCard(string query)
{
    return context.BlogPosts
        .Include(x => x.Categories)
        .Include(x => x.LanguageEntity)
        .AsNoTracking()
        .Where(x =>
            !x.IsHidden
            && (x.ScheduledPublishDate == null || x.ScheduledPublishDate <= now)
            && (x.SearchVector.Matches(EF.Functions.ToTsQuery("english", query + ":*"))
                || x.Categories.Any(c =>
                    EF.Functions.ToTsVector("english", c.Name)
                        .Matches(EF.Functions.ToTsQuery("english", query + ":*"))))
            && x.LanguageEntity.Name == "en")
        .OrderByDescending(x =>
            x.SearchVector.Rank(EF.Functions.ToTsQuery("english", query + ":*")));
}

// Multiple words with web search
private IQueryable<BlogPostEntity> QueryForSpaces(string processedQuery)
{
    return context.BlogPosts
        .Where(x =>
            x.SearchVector.Matches(EF.Functions.WebSearchToTsQuery("english", processedQuery))
            || x.Categories.Any(c =>
                EF.Functions.ToTsVector("english", c.Name)
                    .Matches(EF.Functions.WebSearchToTsQuery("english", processedQuery))))
        .OrderByDescending(x =>
            x.SearchVector.Rank(EF.Functions.WebSearchToTsQuery("english", processedQuery)));
}
```

**Γιατί; `WebSearchToTsQuery`?** Χειρίζεται ερωτήματα φυσικής γλώσσας όπως το Google:

- `"docker containers"` → αναζητήσεις και για τις δύο λέξεις
- `docker OR kubernetes` → Boolean OR
- `docker -compose` → αποκλείει το "σύνθετο"

Για περισσότερες πληροφορίες σχετικά με Αναζήτηση πλήρους κειμένου PostgreSQL, δείτε [Πλήρης αναζήτηση κειμένου με Postgres](/blog/textsearchingpt1).

# Η Πλήρης Σελίδα Αναζήτησης

Πέρα από το typenaward, υπάρχει μια πλήρης σελίδα αποτελεσμάτων αναζήτησης με προηγμένο φιλτράρισμα:

```mermaid
flowchart LR
    subgraph SearchPage[Search Page]
        A[Query Input] --> B{Filters}
        B --> C[Language Filter]
        B --> D[Date Range Filter]
        C --> E[Search Results]
        D --> E
        E --> F[Paginated List]
    end

    style A stroke:#10b981,stroke-width:2px
    style B stroke:#6366f1,stroke-width:2px
    style E stroke:#ec4899,stroke-width:2px
    style F stroke:#8b5cf6,stroke-width:2px
```

## SearchController

```csharp
[Route("search")]
public class SearchController(
    BaseControllerService baseControllerService,
    BlogSearchService searchService,
    ISemanticSearchService semanticSearchService,
    ILogger<SearchController> logger)
    : BaseController(baseControllerService, logger)
{
    [HttpGet]
    [Route("")]
    [OutputCache(Duration = 3600, VaryByQueryKeys = new[] { "query", "page", "pageSize", "language", "dateRange", "startDate", "endDate" })]
    public async Task<IActionResult> Search(
        string? query,
        int page = 1,
        int pageSize = 10,
        string? language = null,
        DateRangeOption dateRange = DateRangeOption.AllTime,
        DateTime? startDate = null,
        DateTime? endDate = null,
        [FromHeader] bool pagerequest = false)
    {
        // Calculate date range based on option
        var (calculatedStartDate, calculatedEndDate) = CalculateDateRange(dateRange, startDate, endDate);

        // Get available languages for the filter dropdown
        var availableLanguages = await searchService.GetAvailableLanguagesAsync();

        if (string.IsNullOrEmpty(query?.Trim()))
        {
            var emptyModel = new SearchResultsModel { /* ... */ };
            if (Request.IsHtmx()) return PartialView("SearchResults", emptyModel);
            return View("SearchResults", emptyModel);
        }

        var searchResults = await searchService.HybridSearchWithPagingAsync(
            query,
            language,
            calculatedStartDate,
            calculatedEndDate,
            page,
            pageSize);

        // Build response model...
        if (pagerequest && Request.IsHtmx())
            return PartialView("_SearchResultsPartial", searchModel.SearchResults);
        if (Request.IsHtmx())
            return PartialView("SearchResults", searchModel);
        return View("SearchResults", searchModel);
    }
}
```

## Επιλογές εύρους ημερομηνίας

```csharp
public enum DateRangeOption
{
    AllTime,
    LastWeek,
    LastMonth,
    LastYear,
    Custom
}

private static (DateTime? StartDate, DateTime? EndDate) CalculateDateRange(
    DateRangeOption dateRange, DateTime? startDate, DateTime? endDate)
{
    var now = DateTime.UtcNow;
    return dateRange switch
    {
        DateRangeOption.LastWeek => (now.AddDays(-7), now),
        DateRangeOption.LastMonth => (now.AddMonths(-1), now),
        DateRangeOption.LastYear => (now.AddYears(-1), now),
        DateRangeOption.Custom => (startDate, endDate),
        _ => (null, null) // AllTime - no date filter
    };
}
```

# Σχετικές Δημοσιεύσεις με Lazy Loading

Κάθε δημοσίευση στο blog δείχνει σημασιολογικά παρόμοιες θέσεις σε ένα colapsible πάνελ. Αυτό χρησιμοποιεί HTMX για τεμπέλικη φόρτωση:

```html
<!-- In blog post view -->
<div class="print:hidden"
     hx-get="/search/related/@Model.Slug/@Model.Language"
     hx-trigger="load delay:500ms"
     hx-swap="innerHTML">
    <!-- Loading placeholder -->
    <div class="mt-8 mb-8 text-center opacity-50">
        <span class="loading loading-spinner loading-md"></span>
        <p class="text-sm mt-2">Finding related posts...</p>
    </div>
</div>
```

**Γιατί να καθυστερήσουμε 500μ;** Το κύριο περιεχόμενο φορτώνει πρώτα, στη συνέχεια σχετικές θέσεις φορτίο στο παρασκήνιο. Οι χρήστες δείτε το περιεχόμενο αμέσως.

## Σχετικές Δημοσιεύσεις Endpoint

```csharp
[HttpGet]
[Route("related/{slug}/{language}")]
[OutputCache(Duration = 7200, VaryByRouteValueNames = new[] {"slug", "language"})]
public async Task<IActionResult> RelatedPosts(string slug, string language, int limit = 5)
{
    var results = await semanticSearchService.GetRelatedPostsAsync(slug, language, limit);

    if (Request.IsHtmx())
    {
        return PartialView("_RelatedPosts", results);
    }

    return Json(results);
}
```

**Κρύπτη 2 ωρών**: Οι σχετικές θέσεις δεν αλλάζουν συχνά, οπότε το επιθετικό caching είναι ασφαλές.

## Η Σχετική Συστατικό Δημοσιεύσεις

Ένα στοιχείο κατάρρευσης DaisyUI με ακτινική πρόοδο που δείχνει βαθμολογίες ομοιότητας:

```html
@model List<SearchResult>

@if (Model != null && Model.Any())
{
    <div class="mt-8 mb-8">
        <div class="collapse collapse-arrow bg-base-200">
            <input type="checkbox" class="peer" />
            <div class="collapse-title text-xl font-medium">
                <i class='bx bx-brain text-2xl mr-2'></i>
                Related Posts
                <span class="badge badge-secondary badge-sm ml-2">@Model.Count</span>
            </div>
            <div class="collapse-content">
                <div class="divider mt-0"></div>
                <div class="space-y-2">
                    @foreach (var post in Model)
                    {
                        <div class="card bg-base-100 shadow-sm hover:shadow-md transition-shadow">
                            <div class="card-body p-4">
                                <div class="flex items-start justify-between">
                                    <div class="flex-1">
                                        <a hx-boost="true"
                                           hx-target="#contentcontainer"
                                           asp-action="Show"
                                           asp-controller="Blog"
                                           asp-route-slug="@post.Slug"
                                           asp-route-language="@post.Language"
                                           class="card-title text-base hover:text-secondary">
                                            @post.Title
                                        </a>

                                        @if (post.Categories?.Any() == true)
                                        {
                                            <div class="flex flex-wrap gap-1 mt-2">
                                                @foreach (var category in post.Categories.Take(3))
                                                {
                                                    <span class="badge badge-outline badge-sm">@category</span>
                                                }
                                            </div>
                                        }

                                        <div class="flex items-center gap-3 mt-2 text-sm opacity-70">
                                            <span>
                                                <i class='bx bx-calendar'></i>
                                                @post.PublishedDate.ToString("MMM dd, yyyy")
                                            </span>
                                            <span>
                                                <i class='bx bx-planet'></i>
                                                @post.Language.ToUpper()
                                            </span>
                                        </div>
                                    </div>

                                    <!-- Similarity Score -->
                                    <div class="flex flex-col items-end ml-4">
                                        <div class="radial-progress text-primary text-xs"
                                             style="--value:@(post.Score * 100); --size:3rem; --thickness:3px;"
                                             role="progressbar">
                                            @((post.Score * 100).ToString("F0"))%
                                        </div>
                                        <span class="text-xs opacity-60 mt-1">similarity</span>
                                    </div>
                                </div>
                            </div>
                        </div>
                    }
                </div>
            </div>
        </div>
    </div>
}
```

**Η ακτινική πρόοδος** δείχνει ομοιότητα ως ποσοστό (- 100%), βοηθώντας τους χρήστες να κατανοήσουν πώς σχετίζεται κάθε θέση.

# Αναφορά API

## Αναζήτηση επόμενου τύπου

```
GET /api/search/{query}
```

~ Παράμετρος ~ Τύπος ~ Description ~
|-----------|------|-------------|
| `query` &gt; συμβολοσειρά (path) &gt; Αναζήτηση όρος (min 2 χαρακτήρες) &gt;

**Απάντηση:** `List<SearchResults>`

```json
[
  {
    "title": "Docker Containers Explained",
    "slug": "docker-containers",
    "url": "https://example.com/blog/docker-containers"
  }
]
```

**Φύλαξη:** 1 ώρα, ποικίλλει ανάλογα με την ερώτηση

## Σημασιολογική Αναζήτηση

```
GET /search/semantic?query={query}&limit={limit}
```

□ Παράμετρος &amp; Τύπος &amp; &amp; &amp; &amp; &gt; &gt; &gt; &gt; &gt; &lt; &gt; &gt; &gt; &gt; &gt; &gt; &gt; &gt; &gt; &gt; &gt; &gt; &gt; &gt; &gt; &gt; &gt; &gt; &lt; &gt; &lt; &gt; &gt; &gt; &gt; &lt; &gt; &gt; &gt; &lt; &gt; &gt; &gt; &gt; &gt; &lt; &gt; &gt; &gt; &gt; &gt; &gt; &gt; &gt; &gt; &gt; &lt; &gt; &lt; &gt; &gt; &gt; &gt; &gt; &gt; &gt; &lt; &gt; &gt; &gt; &gt; &lt; &gt; &lt; &gt; &gt; &gt; &gt; &lt; &gt; &gt; &gt; &gt; &lt; &gt; &lt; &gt; &lt; &lt; &gt; &lt; &gt; &lt; &lt; &lt; &lt; &lt; &lt; &lt; &gt; &gt; &gt; &gt; &gt; &gt; &gt; &gt; &gt; &gt; &gt; &gt; &gt;
|-----------|------|---------|-------------|
| `query` Ο όρος "αναζήτηση" είναι ο όρος "αναζήτηση"
| `limit` ~ int ~ 10 ~ Max results ~

**Απάντηση:** `List<SearchResult>` με βαθμολογίες ομοιότητας

## Σχετικές Δημοσιεύσεις

```
GET /search/related/{slug}/{language}?limit={limit}
```

□ Παράμετρος &amp; Τύπος &amp; &amp; &amp; &amp; &gt; &gt; &gt; &gt; &gt; &lt; &gt; &gt; &gt; &gt; &gt; &gt; &gt; &gt; &gt; &gt; &gt; &gt; &gt; &gt; &gt; &gt; &gt; &gt; &lt; &gt; &lt; &gt; &gt; &gt; &gt; &lt; &gt; &gt; &gt; &lt; &gt; &gt; &gt; &gt; &gt; &lt; &gt; &gt; &gt; &gt; &gt; &gt; &gt; &gt; &gt; &gt; &lt; &gt; &lt; &gt; &gt; &gt; &gt; &gt; &gt; &gt; &lt; &gt; &gt; &gt; &gt; &lt; &gt; &lt; &gt; &gt; &gt; &gt; &lt; &gt; &gt; &gt; &gt; &lt; &gt; &lt; &gt; &lt; &lt; &gt; &lt; &gt; &lt; &lt; &lt; &lt; &lt; &lt; &lt; &gt; &gt; &gt; &gt; &gt; &gt; &gt; &gt; &gt; &gt; &gt; &gt; &gt;
|-----------|------|---------|-------------|
| `slug` Η συμβολοσειρά είναι απαραίτητη για το blog post slug
| `language` Η συμβολοσειρά είναι απαραίτητη, κωδικός γλώσσας (εν, es, κ.λπ.)
| `limit` Οι θέσεις που σχετίζονται με το μέγ

**Απάντηση:** `List<SearchResult>` ταξινομήθηκε κατά ομοιότητα

**Φύλαξη:** 2 ώρες, ποικίλει ανά γυμνοσάλιαγκα και γλώσσα

## Πλήρης αναζήτηση με φίλτρα

```
GET /search?query={query}&page={page}&pageSize={pageSize}&language={language}&dateRange={dateRange}&startDate={startDate}&endDate={endDate}
```

□ Παράμετρος &amp; Τύπος &amp; &amp; &amp; &amp; &gt; &gt; &gt; &gt; &gt; &lt; &gt; &gt; &gt; &gt; &gt; &gt; &gt; &gt; &gt; &gt; &gt; &gt; &gt; &gt; &gt; &gt; &gt; &gt; &lt; &gt; &lt; &gt; &gt; &gt; &gt; &lt; &gt; &gt; &gt; &lt; &gt; &gt; &gt; &gt; &gt; &lt; &gt; &gt; &gt; &gt; &gt; &gt; &gt; &gt; &gt; &gt; &lt; &gt; &lt; &gt; &gt; &gt; &gt; &gt; &gt; &gt; &lt; &gt; &gt; &gt; &gt; &lt; &gt; &lt; &gt; &gt; &gt; &gt; &lt; &gt; &gt; &gt; &gt; &lt; &gt; &lt; &gt; &lt; &lt; &gt; &lt; &gt; &lt; &lt; &lt; &lt; &lt; &lt; &lt; &gt; &gt; &gt; &gt; &gt; &gt; &gt; &gt; &gt; &gt; &gt; &gt; &gt;
|-----------|------|---------|-------------|
| `query` Ο όρος "αναζήτηση" είναι ο όρος "αναζήτηση"
| `page` □ int 
| `pageSize` Αpiοτελέσματα ανά σελίδα
| `language` Φίλτρο από τη γλώσσα
| `dateRange` Όλο το χρόνο, τελευταίος χρόνος, τελευταίος μήνας, τελευταίος χρόνος, συνήθεια
| `startDate` ΗμερομηνίαTime null Ημερομηνία έναρξης (με ημερομηνίαRange=Custom)
| `endDate` ΗμερομηνίαTime null Ημερομηνία λήξης (με την ημερομηνίαRange=Προσοχή)

# Συμβουλές απόδοσης

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

```csharp
// Typeahead - 1 hour (queries are repeated often)
[OutputCache(Duration = 3600, VaryByQueryKeys = new[] { "query" })]

// Related posts - 2 hours (rarely change)
[OutputCache(Duration = 7200, VaryByRouteValueNames = new[] {"slug", "language"})]

// Full search - 1 hour (many filter combinations)
[OutputCache(Duration = 3600, VaryByQueryKeys = new[] { "query", "page", "pageSize", "language", "dateRange", "startDate", "endDate" })]
```

## Αποκήρυξη

Πάντα να απενεργοποιήσετε την είσοδο του χρήστη για να αποτρέψετε υπερβολικές κλήσεις API:

```html
x-on:input.debounce.300ms="search"
```

300ms είναι μια καλή ισορροπία - αρκετά γρήγορη για να αισθάνονται ανταποκρίνεται, αρκετά αργή ώστε να μειώσει το φορτίο του διακομιστή.

## Τεμπέλικη φόρτωση

Χρήση HTMX `load delay:` ενεργοποίηση για μη κρίσιμο περιεχόμενο:

```html
hx-trigger="load delay:500ms"
```

Αυτό εξασφαλίζει ότι το κύριο περιεχόμενο είναι ορατό πριν από τα δευτερεύοντα φορτία περιεχομένου.

# Ποιο είναι το επόμενο;

Το άρθρο αυτό κάλυψε το **Εμπειρία αναζήτησης** - πώς οι χρήστες αλληλεπιδρούν με σημασιολογική αναζήτηση. **ανάπτυξη της παραγωγής** συμπεριλαμβανομένης της αυτόματης ευρετηρίασης και της υπηρεσίας υποβάθρου, εξακολουθούν να:

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

- FileSystemWatcher για ευρετηρίαση σε πραγματικό χρόνο
- Background service for startup indexing
- Ανίχνευση χασίς περιεχομένου για αυξανόμενες ενημερώσεις

# Πόροι

## Σχετικά άρθρα

- [Μέρος 4α: Εφαρμογή ONNX & Qdrant](/blog/semantic-search-with-onnx-and-qdrant) - Το θεμέλιο που βασίζεται αυτό το άρθρο
- [Πλήρης αναζήτηση κειμένου με PostgreSQL](/blog/textsearchingpt1) - Ρύθμιση αναζήτησης πλήρους κειμένου PostgreSQL
- [Πλήρης αναζήτηση κειμένου - Το Typenaward](/blog/textsearchingpt11) - Αυθεντική εφαρμογή του επόμενου τύπου

## Χρησιμοποιούμενες τεχνολογίες

- [Alpine.js](https://alpinejs.dev/) - Ελαφρύ αντιδραστικό JavaScript
- [HTMX](https://htmx.org/) - HTML πάνω από το καλώδιο
- [DaisyUI](https://daisyui.com/) - Βιβλιοθήκη συστατικών TailwindCSS
- [Ανομοιόμορφη Συσσωρευμένη Συσσωρευμένη Συσσωρευμένη](https://plg.uwaterloo.ca/~gvcormac/cormacksigir09-rrf.pdf) - Το χαρτί αλγόριθμου RRF

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

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

- `Mostlylucid/API/SearchApi.cs` - API τύπου agreece
- `Mostlylucid/Controllers/SearchController.cs` - Πλήρης σελίδα αναζήτησης
- `Mostlylucid.Services/Blog/BlogSearchService.cs` - Υβριδική λογική αναζήτησης
- `Mostlylucid/src/js/typeahead.js` - Alpine.js component