# Volltextsuche

<!--category-- Postgres, Entity Framework -->
<datetime class="hidden">2024-08-20T12:40</datetime>

# Einleitung

Die Suche nach Inhalt ist ein kritischer Teil jedes Inhaltes schwere Website.Es verbessert die Auffindbarkeit und Benutzererfahrung...In diesem Beitrag deckt I'.ll, wie ich Volltext auf der Suche nach dieser Website hinzugefügt

Nächste Teile in dieser Serie

- [Suchfeld mit Postgres](/blog/textsearchingpt11)
- [Einführung in OpenSearch](/blog/textsearchingpt2)
- [Offene Suche mit C-#](/blog/textsearchingpt3)

[TOC]

# Ansätze

Es gibt eine Reihe von Möglichkeiten, um Volltextsuche zu tun, einschließlich

1. Suchen Sie einfach eine in Speicherdatenstruktur wie eine Liste, ist dies relativ einfach zu implementieren, aber doesn't scale well -.. Zusätzlich unterstützt es komplexe Abfragen ohne viel Arbeit.
2. Verwendung einer Datenbank wie SQL Server oder Postgres-., Während dies funktioniert und hat Unterstützung von fast allen Datenbanktypen es ist nicht immer die beste Lösung für komplexere Datenstrukturen oder komplexe Abfragen, aber es ist ', was in diesem Artikel behandelt wird
3. Verwendung einer leichten Suchtechnologie wie [Lucene](https://lucenenet.apache.org/) oder SQLite FTS... Dies ist ein Mittelboden zwischen den beiden oben genannten Lösungen.MS K1. Es ist komplexer als nur die Suche nach einer Liste, aber weniger komplex als eine vollständige Datenbanklösung... Jedoch...;. es ist immer noch ziemlich kompliziert, um ( zu implementieren, besonders für die Aufnahme von Daten.MSC7. und doesn-Skala sowie eine volle Suchlösung. [verwenden Lucene unter der Haube für ](https://www.elastic.co/search-labs/blog/elasticsearch-lucene-vector-database-gains) es ist erstaunlichen Vektor-Suchfunktionen
4. Die Verwendung einer Suchmaschine wie ElasticSearch-,-OpenSearch oder Azure Search-.-Dies ist die komplexeste, ressourcenintensive Lösung, aber auch die leistungsstärkste und skalierbarste .-It-'-Lösung und kann komplexe Abfragen mit Leichtigkeit bearbeiten..-I'-ll gehen in der nächsten Woche oder so weiter, wie man sich selbst konfiguriert und verwendet OpenSearch von C-#.-

# Datenbank Volltextsuche mit Postgres

In diesem Blog I'®ve vor kurzem auf die Verwendung von Postgres für meine Datenbank verschoben..® Postgres hat eine Volltext-Suchfunktion, die sehr leistungsstark ist und einfach zu bedienen ist.MSC4® Es ist auch sehr schnell und kann komplexe Abfragen mit Leichtigkeit handhaben..

Wenn du baust `DbContext` Sie können festlegen, welche Felder Volltext-Suchfunktion aktiviert haben.

Postgres verwendet das Konzept der Suchvektoren, um schnell zu erreichen.,-effizientes Volltextsuchen..-Ein Suchvektor ist eine Datenstruktur, die die Wörter in einem Dokument und ihre Positionen enthält.MSC2-Im Wesentlichen vorkomputiert der Suchvaktor für jede Zeile in der Datenbank ermöglicht es Postgres, sehr schnell nach Worten im Dokument zu suchen.
Es verwendet zwei spezielle Datentypen, um dies zu erreichen

- Ein spezieller PostgreSQL-Datentyp, der eine Liste von Lexemen speichert.Denken Sie daran als Vektor von Wörtern.Es ist die indizierte Version des Dokuments, das für schnelle Suche verwendet wird.
- Ein anderer spezieller Datentyp, der die Suchanfrage speichert, die die Suchbegriffe und logischen Operatoren enthält, wie AND-,.

Zusätzlich bietet es eine Rangfolge-Funktion, die Ihnen erlaubt, die Ergebnisse zu ordnen, basierend darauf, wie gut sie mit der Suchanfrage übereinstimmen.Dies ist sehr leistungsstark und ermöglicht es Ihnen, die Resultate nach Relevanz zu bestellen.
PostgreSQL weist den Ergebnissen auf der Grundlage von Relevanz ein Ranking zu, wobei die Relevanz unter Berücksichtigung von Faktoren wie der Nähe der Suchbegriffe zueinander und ihrer Häufigkeit im Dokument berechnet wird.
Die Funktionen ts ,_ , oder ts -_ ,rank , _ ,cd werden verwendet, um dieses Ranking zu berechnen.

Lesen Sie mehr über die Volltextsuche von Postgres [Hierher](https://www.postgresql.org/docs/current/textsearch.html)

## Rahmen für die Einrichtung

Das Postgres Entity Framework Paket [Hierher](https://www.npgsql.org/efcore/mapping/full-text-search.html?tabs=pg12%2Cv5) bietet leistungsstarke Unterstützung für die Volltextsuche.Es erlaubt Ihnen, zu spezifizieren, welche Felder Volltextindexiert sind und wie Sie sie abfragen können.

Um dies zu tun, fügen wir spezifische Indextypen zu unseren Entities nach Definition in `DbContext`:

```csharp
   modelBuilder.Entity<BlogPostEntity>(entity =>
        {
            entity.HasIndex(x => new { x.Slug, x.LanguageId });
            entity.HasIndex(x => x.ContentHash).IsUnique();
            entity.HasIndex(x => x.PublishedDate);

                entity.HasIndex(b => new { b.Title, b.PlainTextContent})
                .HasMethod("GIN")
                .IsTsVectorExpressionIndex("english");
  ...
```

Hier wird ein Volltextindex zum `Title` und `PlainTextContent` Bereiche unserer `BlogPostEntity`Wir erklären auch, dass der Index sollte die `GIN` Indextyp und der `english` Dies ist wichtig, da es Postgres sagt, wie man die Daten indexiert und welche Sprache verwendet wird, um Wörter zu erzeugen und zu stoppen.

Dies ist offensichtlich ein Problem für unseren Blog, da wir mehrere Sprachen haben. `english` Sprache für alle Beiträge Dieses ist etwas, das I'.ll in der Zukunft adressieren muß, aber jetzt funktioniert es gut genug.

Wir fügen auch einen Index zu unserem `Category` entity-:-

```csharp
     modelBuilder.Entity<CategoryEntity>(entity =>
        {
            entity.HasIndex(b => b.Name).HasMethod("GIN").IsTsVectorExpressionIndex("english");;
...

```

Durch diese Postgres erzeugt ein Suchvektor für jede Zeile in der Datenbank. Dieser Vektor enthält die Wörter in der `Title` und `PlainTextContent` Wir können diesen Vektor dann verwenden, um nach Wörtern im Dokument zu suchen.

Dies übersetzt sich in eine Funktion in SQL, die den Suchvektor für die Zeile erzeugt.Wir können dann mit der Funktion ts._«rank die Ergebnisse auf Basis von Relevanz ordnen.

```postgresql
SELECT to_tsvector('english', 'a fat  cat sat on a mat - it ate a fat rats');
to_tsvector
-----------------------------------------------------
'ate':9 'cat':3 'fat':2,11 'mat':7 'rat':12 'sat':4
```

Wenden Sie dies als Migration auf unsere Datenbank an und wir sind bereit zu suchen.

# Suchen

## TsVector Index

Zur Suche verwenden wir die `EF.Functions.ToTsVector` und `EF.Functions.WebSearchToTsQuery` Funktionen, um einen Suchvektor und eine Abfrage zu erstellen. `Matches` Funktion zur Suche nach der Abfrage im Suchvektor

```csharp
  var posts = await context.BlogPosts
            .Include(x => x.Categories)
            .Include(x => x.LanguageEntity)
            .Where(x =>
                EF.Functions.ToTsVector("english", x.Title + " " + x.PlainTextContent)
                    .Matches(EF.Functions.WebSearchToTsQuery("english", query)) // Search in title and content
                && x.Categories.Any(c =>
                    EF.Functions.ToTsVector("english", c.Name)
                        .Matches(EF.Functions.WebSearchToTsQuery("english", query))) // Search in categories
                && x.LanguageEntity.Name == "en") // Filter by language
            .OrderByDescending(x =>
                EF.Functions.ToTsVector("english", x.Title + " " + x.PlainTextContent)
                    .Rank(EF.Functions.WebSearchToTsQuery("english", query))) // Rank by relevance
            .Select(x => new { x.Title, x.Slug })
            .ToListAsync();
       
```

Die Funktion EF-.-Funktion .-WebSearchToTsQuery erzeugt die Abfrage für die Zeile basierend auf der gemeinsamen Syntax der Web Search Engine.

```postgresql
SELECT websearch_to_tsquery('english', '"sad cat" or "fat rat"');
       websearch_to_tsquery
-----------------------------------
 'sad' <-> 'cat' | 'fat' <-> 'rat'
```

In diesem Beispiel können Sie sehen, dass dies eine Abfrage generiert, die nach den Wörtern sucht." sad cat-" oder "-fat rat-" im document-.-Dies ist ein leistungsfähiges Feature, das es uns ermöglicht, komplexe Abfragen mit Leichtigkeit zu suchen.

Wie angegeben, erzeugen diese Methoden sowohl den Suchvektor als auch die Abfrage für die Zeile. `Matches` Funktion, um nach der Abfrage im Suchvektor zu suchen `Rank` Funktion, um die Ergebnisse nach Relevanz zu ordnen

Wie Sie sehen können, ist dies eine einfache Abfrage aber es ist sehr leistungsfähig und ermöglicht uns, nach Wörtern in der Suche `Title`, `PlainTextContent` und `Category` Bereiche unserer `BlogPostEntity` und ordne diese nach Relevanz.

## WebAPI

Um diese zu verwenden, können wir in Zukunft einen einfachen WebAPI-Endpunkt erstellen, der eine Abfrage annimmt und die Ergebnisse zurückgibt.

```csharp
[ApiController]
[Route("api/[controller]")]
public class SearchApi(IMostlylucidDbContext context) : ControllerBase
{
    [HttpGet]
    public async Task<JsonHttpResult<List<SearchResults>>> Search(string query)
    {;

        var posts = await context.BlogPosts
            .Include(x => x.Categories)
            .Include(x => x.LanguageEntity)
            .Where(x =>
                EF.Functions.ToTsVector("english", x.Title + " " + x.PlainTextContent)
                    .Matches(EF.Functions.WebSearchToTsQuery("english", query)) // Search in title and content
                && x.Categories.Any(c =>
                    EF.Functions.ToTsVector("english", c.Name)
                        .Matches(EF.Functions.WebSearchToTsQuery("english", query))) // Search in categories
                && x.LanguageEntity.Name == "en") // Filter by language
            .OrderByDescending(x =>
                EF.Functions.ToTsVector("english", x.Title + " " + x.PlainTextContent)
                    .Rank(EF.Functions.WebSearchToTsQuery("english", query))) // Rank by relevance
            .Select(x => new { x.Title, x.Slug })
            .ToListAsync();
        
        var output = posts.Select(x => new SearchResults(x.Title.Trim(), x.Slug)).ToList();
        
        return TypedResults.Json(output);
    }

```

## Erzeugte Säule und TypeAhead

Ein alternativer Ansatz für die Verwendung dieser Simple-Indizes ist es, eine generierte Spalte zu verwenden, um den Suchvektor zu speichern und diese dann zur Suche zu benutzen.
Hier ändern wir unsere `BlogPostEntity` zum Hinzufügen einer speziellen Art von Spalte

```csharp
   [DatabaseGenerated(DatabaseGeneratedOption.Computed)]
    public NpgsqlTsVector SearchVector { get; set; }
```

Dies ist eine berechnete Spalte, die den Suchvektor für die Zeile erzeugt.Wir können diese Spalte dann verwenden, um nach Wörtern im Dokument zu suchen.

Wir stellen dann diesen Index in unserer Entity Definition ein, um zu bestätigen, aber dies kann uns auch erlauben, mehrere Sprachen zu haben, indem wir eine Sprachspalte für jeden Beitrag angeben.

```csharp
   entity.Property(b => b.SearchVector)
                .HasComputedColumnSql("to_tsvector('english', coalesce(\"Title\", '') || ' ' || coalesce(\"PlainTextContent\", ''))", stored: true);
```

Sie sehen hier, dass wir verwenden `HasComputedColumnSql` zur expliziten Angabe der PostGreSQL-Funktion, um den Suchvektor zu generieren.Wir legen auch fest, dass die Spalte in der Datenbank gespeichert ist...Dies ist wichtig, da es Postgres anweist, den Suchvaktor in der Datenbanken zu speichern.

In der Datenbank generierte dies für jede Zeile, die das -' ilexemes .' . im Dokument und ihre Positionen sind.

```csharp
"'1992':464 '1996':468 '20':480 '200':115 '2007':426 '2009':428 '2012':88 '2015':397 '2018':370 '2020':372 '2021':288,327,329,399 '2022':196,243,245,290 '2024':156,158,198 '25':21,477,486,522 '3d':346 '6':203,256 '8':179,485 '90':120,566 'ab':282 'access':221 'accomplish':14 'achiev':118 'across':60 'adapt':579 'advanc':134 'applic':168,316,526 'apr':155,197 'architect':83,97,159 'architectur':307,337 ...
```

### SuchAPI

Wir können dann diese Spalte verwenden, um nach Wörtern im Dokument zu suchen. `Matches` Funktion, um nach der Abfrage im Suchvektor zu suchen `Rank` Funktion, um die Ergebnisse nach Relevanz zu ordnen

```csharp
       var posts = await context.BlogPosts
            .Include(x => x.Categories)
            .Include(x => x.LanguageEntity)
            .Where(x =>
                // Search using the precomputed SearchVector
                x.SearchVector.Matches(EF.Functions.ToTsQuery("english", query + ":*")) // Use precomputed SearchVector for title and content
                && x.Categories.Any(c =>
                    EF.Functions.ToTsVector("english", c.Name)
                        .Matches(EF.Functions.ToTsQuery("english", query + ":*"))) // Search in categories
                && x.LanguageEntity.Name == "en") // Filter by language
            .OrderByDescending(x =>
                // Rank based on the precomputed SearchVector
                x.SearchVector.Rank(EF.Functions.ToTsQuery("english", query + ":*"))) // Use precomputed SearchVector for ranking
            .Select(x => new { x.Title, x.Slug })
            .ToListAsync();
```

Hier sehen Sie, dass wir auch einen anderen Abfragekonstruktor verwenden. `EF.Functions.ToTsQuery("english", query + ":*")` die uns erlaubt, eine TypeAhead-Funktionalität anzubieten, wo wir e....g.h und erhalten 'h und ',h geben können.',h

Zusätzlich ermöglicht es uns, die Haupt-Blog-Post-Abfrage zu vereinfachen, um nur nach der Abfrage in der `SearchVector` Dies ist eine leistungsstarke Funktion, die uns erlaubt, nach Wörtern in der Suche `Title`, `PlainTextContent`Wir verwenden immer noch den Index, den wir oben für die `CategoryEntity`.

```csharp
x.Categories.Any(c =>
                    EF.Functions.ToTsVector("english", c.Name)
                        .Matches(EF.Functions.ToTsQuery("english", query + ":*"))) 
```

Wir benutzen dann die `Rank` Funktion, um die Ergebnisse nach Relevanz basierend auf der Abfrage zu ordnen

```csharp
 x.SearchVector.Rank(EF.Functions.ToTsQuery("english", query + ":*")))
```

Dies lässt uns den Endpunkt wie folgt verwenden: ,, , wo wir in den ersten Buchstaben eines Wortes passieren können und zurück alle Beiträge, die mit diesem Wort übereinstimmen , : ,

Sie können die [API in Aktion hier](https://www.mostlylucid.net/swagger/index.html) suchen für die `/api/SearchApi`Anmerkung: ; ist aktivierter Swagger für diese Seite, so dass Sie die API in Aktion sehen können, aber die meiste Zeit sollte dies für ` vorbehalten sein.

![API](searchapi.png?width=900&format=webp&quality=50)

In Zukunft I'®ll fügen Sie eine TypeAhead-Funktion in das Suchfeld auf der Website, die diese Funktionalität verwendet

# Schlussfolgerung

Sie können sehen, dass es möglich ist, leistungsstarke Suchfunktionalität mit Postgres und Entity Framework zu erhalten... Jedoch hat es Komplexitäten und Einschränkungen, die wir brauchen, um für ( wie das Sprach-Ding zu berücksichtigen.).. Im nächsten Teil deckt IM'.ll ab, wie wir dies mithilfe von OpenSearch machen.MSC6. Das hat eine Tonne mehr Setup, ist aber leistungsfähiger und skalierbar.