# Recherche de texte complet (Pt 1)

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

# Introduction

La recherche de contenu est un élément essentiel d’un site Web lourd de contenu.. Il améliore la découverte et l’expérience des utilisateurs.

Autres pièces de cette série:

- [Case de recherche avec Postgres](/blog/textsearchingpt11)
- [Introduction à OpenSearch](/blog/textsearchingpt2)
- [Recherche ouverte avec C#](/blog/textsearchingpt3)

[TOC]

# Approches

Il existe un certain nombre de façons d’effectuer une recherche en texte complet, y compris

1. Just Searching an in memory data structure (like a listM SK1 this is relatively simple to implement but doesn't scale wellMSC3 Additionally it doesn 't support complex queries without a lot of work
2. Utilisation d'une base de données comme SQL Server ou Postgres. Bien que cela fonctionne et qu'il bénéficie du soutien de presque tous les types de bases de données, ce n'est pas toujours la meilleure solution pour des structures de données plus complexes ou des demandes complexes.
3. Utilisation d'une technologie de recherche léger comme [Lucène](https://lucenenet.apache.org/) ou SQLite FTS. Il s'agit d'un terrain intermédiaire entre les deux solutions susmentionnées . C'est plus complexe que la simple recherche d'une liste mais moins complexe qu'une solution de base de données complète M SK3 Cependant ; c'est encore assez complexe pour mettre en œuvre ( particulièrement pour ingérer des données MSC7 et ne fait pas de l'échelle, ni n'est-il une solution de recherche complète? [utiliser Lucene sous le cap pour ](https://www.elastic.co/search-labs/blog/elasticsearch-lucene-vector-database-gains) il'sa formidable capacité de recherche par vecteur.
4. Utilisation d’un moteur de recherche comme ElasticSearch, OpenSearch ou Azure Search. C’est la solution la plus complexe M SK2 à forte intensité de ressources, mais aussi la plus puissante . Il est également la plus scalable et peut traiter les demandes complexes avec facilitéMST5 JeMSC6 je vais aller dans une profondeur excruciatrice au cours de la semaine prochaine ou de l’autre sur la façon d’autoconfigurer et d’utiliser OpenSearch depuis CMSSK9

# Recherche du texte complet de la base de données avec Postgres

Dans ce blog, j’ai récemment choisi Postgres pour ma base de données.

Lorsque vous construisez une usine `DbContext` vous pouvez spécifier quels champs ont une fonction de recherche complète de texte activée.

Postgres utilise le concept de vecteurs de recherche pour obtenir des résultats rapides, Recherche efficace du texte complet. Un vecteur de recherche est une structure de données qui contient les mots d’un document et leurs positions.
Il utilise deux types de données spéciaux pour réaliser cet objectif:

- TSVector: Un type de données PostgreSQL spécial qui stocke une liste de lexèmes (considérez-le comme un vecteur de motsM SK2 Il s'agit d'une version indexée du document utilisé pour la recherche rapide .
- TSQuery: Un autre type de données spécial qui stocke la demande de recherche, incluant les termes de recherche et les opérateurs logiques M SK2comme ET , OUMST4 NONMst5

En outre, il vous offre une fonction de classement qui vous permet de classer les résultats en fonction du bien-fondé avec lequel ils correspondent à la demande de recherche. C'est très puissant et vous permet d'ordonner les résultats selon leur pertinence.
PostgreSQL assigne un classement des résultats en fonction de la pertinence. La pertinency est calculée en tenant compte de facteurs tels que la proximité entre les termes de recherche et le nombre de fois qu’ils apparaissent dans le document.
Les fonctions ts_rank ou ts _rankM SK2cd sont utilisées pour calculer ce classement.

Vous pouvez lire plus sur les fonctions de recherche du texte complet d'Postgres [ici](https://www.postgresql.org/docs/current/textsearch.html)

## Cadre d'entité

Le paquet Cadre des entités Postgres [ici](https://www.npgsql.org/efcore/mapping/full-text-search.html?tabs=pg12%2Cv5) fournit un support puissant pour la recherche de texte complet. vous permet d'indiquer quels champs sont indexés et comment les interroger.

Pour ce faire, nous ajoutons des types d'index spécifiques à nos Entités tel que défini dans `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");
  ...
```

Nous 'donnons ici un index de texte complet au `Title` et `PlainTextContent` de nos champs `BlogPostEntity`Nous spécifions également que l’indice doit utiliser `GIN` le type d'index et la `english` langage. C’est important car il indique à Postgres comment indexer les données et quelle langue utiliser pour arrêter ou mettre fin aux mots.

Il s’agit évidemment d’un problème pour notre blog car nous avons plusieurs langues. Malheureusement pour l’instant I'm simplement en utilisant le `english` langage pour tous les postes. C’est quelque chose que je devrais aborder dans l’avenir, mais pour le moment il fonctionne assez bien.

Nous ajoutons également un index à notre `Category` entité:

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

```

En faisant cela, Postgres génère un vecteur de recherche pour chaque rang dans la base de données. Ce vecteur contient les mots `Title` et `PlainTextContent` fields. Nous pouvons alors utiliser ce vecteur pour rechercher des mots dans le document.

Cela se traduit par une fonction de to_tsvector dans SQL qui génère le vecteur de recherche pour la ligne.

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

Appliquez-le comme migration à notre base de données et nous'nous sommes prêts à commencer la recherche

# Recherche

## Index de TsVector

Pour la recherche, nous utiliserons le `EF.Functions.ToTsVector` et `EF.Functions.WebSearchToTsQuery` fonctions pour créer un vecteur de recherche et une demande. Nous pouvons ensuite utiliser `Matches` fonction pour rechercher la demande dans le vecteur de recherche.

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

La fonction EF.Functions.WebSearchToTsQuery génère la demande pour la ligne basée sur la syntaxe commune du moteur de recherche Web.

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

Dans cet exemple, vous pouvez voir que cela génère une demande qui recherche les mots "sad catM SK1 ou "fat rat" dans le documentMSC4 C'est une fonctionnalité puissante qui nous permet de rechercher facilement des questions complexes.

Comme indiqué dans befpre, ces méthodes génèrent à la fois le vecteur de recherche et la demande pour la ligne. Nous utilisons ensuite `Matches` . Nous pouvons également utiliser la fonction `Rank` fonction pour classer les résultats selon la pertinence.

Comme vous pouvez le voir, ce n’est pas une simple demande mais il est très puissant et nous permet de rechercher des mots dans `Title`, `PlainTextContent` et `Category` de nos champs `BlogPostEntity` et les classer par pertinence.

## WebAPI

Pour utiliser ces (à l’avenir ), nous pouvons créer un simple point final WebAPI qui prend une demande et retourne les résultats.

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

```

## Colonne et TypeAhead générés

Une approche alternative à l’utilisation de ces indices TsVector est d’utiliser une colonne générée pour stocker le vecteur de recherche puis d’en utiliser la recherche.
Nous modifions ici notre `BlogPostEntity` pour ajouter un type spécial de colonne:

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

Il s'agit d'une colonne calculée qui génère le vecteur de recherche pour la ligne.

Nous avons ensuite créé cet index à l’intérieur de notre définition d’entité (pour confirmer, mais cela peut aussi nous permettre d’avoir plusieurs langues en spécifiant une colonne linguistique pour chaque poste).

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

Vous verrez ici que nous utilisons `HasComputedColumnSql` à spécifier explicitement la fonction PostGreSQL pour générer le vecteur de recherche. Nous spécifions également que la colonne est entreposée dans la base de données. C'est important parce qu'elle dit à Postgres d'entreposer le vector de recherche dans la Base de données

Dans la base de données, cela a été généré pour chaque rangée, qui sont les M SK1lexemes' dans le document et leurs positions :

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

### API de recherche

Nous pouvons alors utiliser cette colonne pour rechercher des mots dans le document. `Matches` . Nous pouvons également utiliser la fonction `Rank` fonction pour classer les résultats selon la pertinence.

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

Vous'l je vois ici que nous utilisons également un constructeur de demande différent `EF.Functions.ToTsQuery("english", query + ":*")` ce qui nous permet d'offrir une fonctionnalité de type TypeAhead (dans laquelle nous pouvons écrire e.gM SK2 'cat\' et obtenir 'cat+', 'cats=', ≥'caterpillar ' etc.

En outre, il nous permet de simplifier la recherche principale sur le blog post pour simplement chercher la demande dans `SearchVector` column. C'est une fonctionnalité puissante qui nous permet de rechercher des mots dans les `Title`, `PlainTextContent`. Nous utilisons toujours l’indice que nous avons montré ci-dessus pour `CategoryEntity`.

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

Nous utilisons ensuite `Rank` fonction pour classer les résultats par pertinence en fonction de la demande.

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

Cela nous permet d’utiliser le point final comme suit, où nous pouvons passer dans les premières lettres d’un mot et obtenir de nouveau tous les postes qui correspondent à ce mot:

Vous pouvez consulter le [L'API en action ici](https://www.mostlylucid.net/swagger/index.html) rechercher les `/api/SearchApi`. Swagger pour ce site a été activé afin que vous puissiez voir l'API en action, mais la plupart du temps cela devrait être réservé à `IsDevelopment()).

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

À l’avenir, I' ajoutera une fonction TypeAhead à la case de recherche du site qui utilise cette fonctionnalité.

# En conclusion

Vous pouvez voir qu’il est possible de procurer une fonctionnalité de recherche puissante à l’aide du Postgres et du Cadre d’entités.