Back to "RAG für Implementer: :-Semantische Suche in Aktion"

This is a viewer only at the moment see the article on how this works.

To update the preview hit Ctrl-Alt-R (or ⌘-Alt-R on Mac) or Enter to refresh. The Save icon lets you save the markdown file to disk

This is a preview from the server running through my markdig pipeline

AI-Article Alpine.js ASP.NET HTMX Hybrid Search RAG Semantic Search

RAG für Implementer: :-Semantische Suche in Aktion

Tuesday, 25 November 2025

Einleitung

Teil der RAG-Serie Dies ist Teil der Suchfunktionen von 4-b und UI-:-

Im TeilWir bauten die Stiftung, ONNX-Embeddings und Qdrant-Vektorspeicher. Echte Suche UI -, einschließlich autokompletter Typahead-Hybridsuche zur Kombination von semantischer Semantik, vollwertigem -, Text, , und fortgeschrittenem Filtern

Dieser Artikel deckt die tatsächliche Suche Erfahrung Benutzer interagieren mit auf diesem Blog .

Die Sucherfahrung des Typeahead

Das Suchfeld an der Spitze dieser Website bietet sofortige Suche.-.as.-.syou.-. Type results...s hier.'.s wie es funktioniert.

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

Der Typ der Alpine-.-Js-Baugruppe

Das Suchfeld verwendet Alpen-.-js für reaktive Benutzeroberfläche ohne schwere JavaScript-Frameworks...... Hier ist die Komponente....

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 = '';
        }
    }
}

Wesentliche Merkmale

  1. Entblößte EingabeVerzögerung verhindert Hämmern des Servers
  2. MindestlängeBenötigte Zeichen
  3. TastaturnavigationPfeiltasten für die Zugänglichkeit eingeben
  4. HTMX-IntegrationErgebnisse verwenden HTMX für eine reibungslose Navigation

Das Suchfeld 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>

Warum? x-on:click.outside? Klicken außerhalb der Dropdown schließt es Standard UX-Muster für Autocomplete

Die Such-API

Das /api/search/{query} Der Endpunkt treibt den Typahead an... Hier ist der Controller.

[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");
        }
    }
}

Wichtige Design-Entscheidungen

  1. Merkmalskennzeichen: semanticSearchConfig.Enabled können Sie semantische Suche umschalten
  2. Ausgabe-CachingDer Cache des 1-hour reduziert die Serverlast für häufige Abfragen
  3. AnalyseverfolgungJede Suche wird verfolgt hilft Benutzerverhalten zu verstehen
  4. Anmutige ErniedrigungFallt zurück zu PostgreSQL, wenn semantische Suche fehlschlägt

Hybride Suche mit reziproker Rang Fusion

Die wahre Magie ist hybride Suche Wir verwenden reziproke Rank Fusion, um sie fair zu verschmelzen.

Warum hybride Suche?

Unterschiedliche Suchansätze haben unterschiedliche Stärken

Suchtyp Stärke Stärke Schwäche |------------|-----------|------------| | Semantisch Synonyme Bezeichnungen ,, was bedeutet: Konzepte | Kann genaue Phrasen vermissen | Vollständiger Text Nicht synonym verstehend

Beispiel: Auf der Suche nach einem Container-Deployment

  • Semantische Funde:-Docker Tutorials",-Gubernetes-Führer" - verwandte Konzepte)-Docken
  • Vollständiger --Text findet ,: , Beiträge mit genau ," ,Container-Deployment , " ,
  • Hybrid bekommt das Beste von beidem

Der RRF-Algorithmus

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

Die Formel „:" score = Σ(1 / (k + rank))

Worum es geht

  • k = 60 Um zu verhindern, dass frühe Ränge dominieren
  • rank Position in dieser Suchmethode Ergebnisse von '.

Warum RRF funktioniert

  • Ergebnisse in beides Quellen Score höher
  • Keine einzige Quelle kann dominieren
  • Kein komplexes Tuning erforderlich

Durchführung

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

Wichtige Durchführungsdetails

  1. Parallele AusführungBeide Suchanfragen laufen gleichzeitig.Task.WhenAll)
  2. Fall: Nicht empfindliches DedupSlugs normalisiert mit ToLowerInvariant()
  3. WerteakkumulationGleicher Beitrag in beiden Quellen erhält Punkte hinzugefügt
  4. Top ErgebnisseGenug für Typahead, ,, nicht überwältigend

Volltext Suche Zurück

Wenn semantische Suche deaktiviert ist oder fehlschlägt, fallen wir zurück zu PostgreSQL full--.

Abfragebearbeitung

Die Volltextsuche behandelt zwei unterschiedliche Fälle

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
}

Einheitliches Wort Verwendet Platzhalter-Präfixsuche docker:* Mehrere Wörter ("-Docker-Container verwendet PostgreSQL-'-Websuchsyntax

PostgreSQL-Abfragen

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

Warum? WebSearchToTsQuery? Es behandelt natürliche Sprachabfragen wie Google .: .

  • "docker containers" Suche nach beiden Wörtern
  • docker OR kubernetes BESCHÄFTIGUNG UND UMWELT
  • docker -compose Nicht enthalten

Für weitere Informationen über PostgreSQL Full---Textsuche lesen Sie bitte Volltextsuche mit Postgres.

Die vollständige Suche

Jenseits von typeahead-,'s eine vollständige Suchresultate Seite mit erweitertem Filtern

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

Suchcontroller

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

Datumsbereich-Optionen

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

Verwandte Beiträge mit Lazy Laden

Jeder Blog-Post zeigt semantisch ähnliche Beiträge in einem zusammenklappbaren Panel, das HTMX für faules Laden verwendet.

<!-- 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>

Warum Verzögern Sie 500-ms-?- Der Hauptinhalt lädt zuerst ,, , dann verwandte Pfosten laden im Hintergrund , . , Benutzer sehen Inhalt sofort

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

Zwischenspeicher der 2--StundeVerknüpfte Beiträge don-'nicht häufig ändern , so aggressives Caching ist sicher , .

Eine DaisyUI-Komponente mit radialem Fortschritt, der Ähnlichkeitsergebnisse zeigt

@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>
}

Der radiale Fortschritt zeigt Ähnlichkeit wie ein Prozentsatz an, der Benutzern hilft zu verstehen, wie verwandt jeder Beitrag ist.

API-Referenz

Art der Suche

GET /api/search/{query}
Beschreibung des Parameters , Typ ,
query Zeichenkette (Ausgangszeichen)Auszugszeichen Austrittswörter (Ausstiegszeichen 2Aushangszeichen

Ansprechrate List<SearchResults>

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

Caching-: 1, Stunden,, variiert nach Abfragen

Semantische Suche

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

Parameter Typ Typ Typ Standard Beschreibung Beschreibung |-----------|------|---------|-------------| | query Bitte geben Sie die Zeichenfolge an, in der die Zeichenkette angegeben ist. | limit Maximale Ergebnisse

Ansprechrate List<SearchResult> mit Ähnlichkeitswerten

Verwandte Beiträge

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

Parameter Typ Typ Typ Standard Beschreibung Beschreibung |-----------|------|---------|-------------| | slug Benötigte Zeichenfolge Bewünschte Zeichenkette Befragte Zeichenkette | language Bitte geben Sie an, ob dies der Fall ist. | limit Max verwandte Beiträge

Ansprechrate List<SearchResult> nach Ähnlichkeit sortiert

Caching-: 2, Stunden,, variiert je nach Schnecke und Sprache

Volle Suche mit Filtern

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

Parameter Typ Typ Typ Standard Beschreibung Beschreibung |-----------|------|---------|-------------| | query Bitte geben Sie die Zeichenfolge an, in der die Zeichenkette angegeben ist. | page Anzahl der Seiten | pageSize Ergebnisse pro Seite | language Filtern nach Sprache | dateRange Letzte Woche, Letzte Monat, Letztes Jahr,Benutzerdefinierte | startDate DatumZeitpunkt Zeitpunkt null Beginndatum Zeitpunkt mit DatumBereich =Custom-) | endDate DatumZeitpunkt Zeitpunkt null Enddatum Zeitpunkt mit Datumsbereich=Custom-)

Leistungstipps

Caching-Strategie

// 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" })]

Entschuldung

Immer debounce Benutzereingabe, um übermäßige API-Aufrufe zu verhindern

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

300-ms ist eine gute Balance schnell genug, um sich responsiv zu fühlen, langsam genug, Serverlast zu reduzieren

Faules Laden

Verwendung von HTMX-' load delay: Trigger für nicht-kritische Inhalte

hx-trigger="load delay:500ms"

Dies stellt sicher, dass der Hauptinhalt vor sekundären Inhalt Lasten sichtbar ist.

Was kommt als nächstes?

Dieser Artikel behandelte die Sucherfahrung Wie Benutzer mit semantischer Suche interagieren Produktionseinsatz inklusive automatischer Indexierung und dem Hintergrunddienst ,, , weiter zu ,: ,

Teil-Hybridsuche Produktionsintegrationsmuster

  • FileSystemWatcher für die Echtzeit-Indizierung
  • Hintergrundservice für Start-Indizierung
  • Hash-Erkennung von Inhalten für inkrementelle Updates

Aufkommen

Verwandte Artikel

Verwendete Technologien

Vollständiger Code

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

  • Mostlylucid/API/SearchApi.cs Typeahead API
  • Mostlylucid/Controllers/SearchController.cs Vollständige Suchseite
  • Mostlylucid.Services/Blog/BlogSearchService.cs Hybride Suchlogik
  • Mostlylucid/src/js/typeahead.js Komponente „Alpine-."
logo

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