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
Recherche de texte complet (Pt 1)
Tuesday, 20 August 2024
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:
Approches
Il existe un certain nombre de façons d’effectuer une recherche en texte complet, y compris
- 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
- 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.
- Utilisation d'une technologie de recherche léger comme Lucène 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 il'sa formidable capacité de recherche par vecteur.
- 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
Cadre d'entité
Le paquet Cadre des entités Postgres ici 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:
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 BlogPostEntityNous 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é:
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.
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.
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.
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.
[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:
[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).
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 :
"'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.
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.
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.
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 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()).

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