Vol tekst zoeken (Pt 1)
Inleiding
De zoektocht naar inhoud is een cruciaal onderdeel van elke inhoudszwakke website. Het verhoogt ontdekbaarheid en gebruikerservaring. In deze post zal ik bespreken hoe ik de volledige tekst zoek op deze site heb toegevoegd.
Volgende onderdelen in deze reeks:
benaderingen
Er zijn een aantal manieren om een volledige tekst te zoeken.
- Alleen zoeken naar een in geheugendatastructuur ( zoals een lijst), dit is relatief eenvoudig om te implementeren, maar het schaalt niet goed.
- Met behulp van een database zoals SQL Server of Postgres. Hoewel dit werkt en van bijna alle databasetypes ondersteund is, is het niet altijd de beste oplossing voor complexere datastructuren of complexe vragen.
- Met een lichtgewichtszoektechnologie zoals Lucene of SQLite FTS. Dit is een middelpunt tussen de twee eerdere oplossingen. Het is meer complex dan alleen maar een lijst zoeken, maar minder complex dan een volledige database-oplossingsmethode. Gebruik Lucene onder de kap. het's verbazingwekkende vector-zoekcapaciteiten.
- Gebruik van een zoekmachine zoals ElasticSearch, OpenSearch of Azure SearchM SK1 Dit is de meest complexe & middelenintensieve oplossing, maar ook de krachtigste . Het is ook de meest schaalbare en kan complexe vragen gemakkelijk aanpakkenMskaal5 IkMske6 ga in de volgende week of zo in de ongelooflijke diepte gaan hoe je jezelf kunt configureren en OpenSearch gebruiken vanuit CMsko9
Databasis Volle Tekszoek met Postgres
In deze blog ben ik recentelijk overgestapt naar Postgres voor mijn database.. Postgres heeft een heel krachtige tekst zoekfunctie en is gemakkelijk te gebruiken.
Als je een gebouw bouwt DbContext Je kan bepalen welke velden de volledige tekstzoek-functie hebben..
Postgres gebruikt het concept van zoekvektoren om snel te zoeken, efficiënte Volle tekstzoeken. Een zoekvector is een datastructuur die de woorden in een document en hun posities bevat. Het gebruikt twee speciale datatypes om dit te bereiken:
- TSVector: Een speciaal PostgreSQL-datatype dat een lijst van lexemen beherbergt. (vat het als een vector van woorden.
- TSQuery: Een ander speciaal datatype dat de zoekvraag beherbergt, waarin de zoektermen en logische operatoren inbegrepen zijn M SK2 zoals AND , ODERMSC4 NICHT).
Het biedt ook een rangorde-functie waarmee je de resultaten kunt rangschikken op basis van hoe goed ze overeenkomen met de zoekvraag. Dit is erg krachtig en je kunt de resultaten volgens relevantie orderen. PostgreSQL geeft een rangorde aan de resultaten op basis van relevantie. Relevance wordt berekend door factoren te beschouwen zoals de nabijheid van de zoektermen tot elkaar en hoe vaak ze in het document voorkomen. De cd-functies ts_rank of ts _rank_cd worden gebruikt om deze rangorde te berekenen.
Je kan meer lezen over de volledige tekst zoekfuncties van Postgres. Hier.
Eenheidsraamwerk
Het Postgres-Entiteitsraamwerkpakket. Hier. Het biedt krachtige ondersteuning voor volledige tekstzoeken. Het stelt je in staat om te bepalen welke velden volledige tekst geindexeerd zijn en hoe ze te ondervragen.
Om dit te doen voegen we specifieke indextypes toe aan onze Eenheden zoals gedefinieerd in 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");
...
Hier voegen we een volledige tekstindex toe. Title en PlainTextContent We hebben een groot aantal landbouwgebieden in ons land ontwikkeld. BlogPostEntity. Wij' speciferen ook dat de index de GIN Indextype en de english taal. Dit is belangrijk omdat het Postgres vertelt hoe de data te indexeren en welke taal te gebruiken om woorden te stoppen.
Dit is duidelijk een probleem voor ons blog omdat we meerdere talen hebben. english Taal voor alle posten. Dit is iets dat ik in de toekomst moet aanpakken, maar het werkt nu goed genoeg.
We voegen ook een index toe. Category eenheid:
modelBuilder.Entity<CategoryEntity>(entity =>
{
entity.HasIndex(b => b.Name).HasMethod("GIN").IsTsVectorExpressionIndex("english");;
...
Door dit te doen genereert Postgres een zoekvector voor elke rij in de database. Deze vector bevat de woorden in de Title en PlainTextContent fields. Dan kunnen we deze vektor gebruiken om woorden in het document te zoeken.
Dit vertaalt zich tot een to_tsvector-functie in SQL die de zoekvektor voor de rij genereert.
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
Wend dit aan als een migratie naar onze database en we' zijn klaar om te beginnen met zoeken
Op zoek
TsVector Index
Om te zoeken gebruiken we de EF.Functions.ToTsVector en EF.Functions.WebSearchToTsQuery functiones om een zoekvector en vraag te creëren. We kunnen dan de Matches functione om te zoeken naar de vraag in de zoekvector.
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();
De EF.Functions.WebSearchToTsQuery-functie genereert de zoekopdracht voor de rij gebaseerd op de gemeenschappelijke Webzoekmachinesyntax.
SELECT websearch_to_tsquery('english', '"sad cat" or "fat rat"');
websearch_to_tsquery
-----------------------------------
'sad' <-> 'cat' | 'fat' <-> 'rat'
In dit voorbeeld kun je zien dat dit een zoekopdracht genereert die zoekt naar de woorden "sad cat" of "fat ratM SK3 in het document.
Zoals gezegd befpre genereren deze methodes zowel het zoekvector als de zoekvraag voor de rij. We gebruiken dan de Matches functione om te zoeken naar de vraag in de zoekvector. We kunnen ook het Rank functione om de resultaten te rangschikken volgens relevantie.
Zoals je ziet is dit geen simpele zoekopdracht, maar het is erg krachtig en stelt ons in staat om woorden te zoeken. Title, PlainTextContent en Category We hebben een groot aantal landbouwgebieden in ons land ontwikkeld. BlogPostEntity En rangschikken ze volgens relevantie.
WebAPI
Om deze in de toekomst te gebruiken, kunnen we een eenvoudig WebAPI-endpunt maken dat een zoekopdracht neemt en de resultaten teruggeeft.
[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);
}
Gegenereerde kolom en TypeAhead
Een alternatieve aanpak voor het gebruik van deze 'simple' TsVector Indices is om een gegenereerde kolom te gebruiken om de zoekvector op te slaan en dan dit te gebruiken tot zoeken.
Hier kunnen we onze BlogPostEntity om een speciaal type kolom te voegen:
[DatabaseGenerated(DatabaseGeneratedOption.Computed)]
public NpgsqlTsVector SearchVector { get; set; }
Dit is een berekende kolom die de zoekvector genereert voor de rij. Dan kunnen we deze kolom gebruiken om woorden in het document te zoeken.
Dan zetten we deze index in onze entiteitsdefinitie ( nog te bevestigen, maar dit kan ons ook toestaan om meerdere talen te hebben door een taalcolumn voor elke post te speciferen.
entity.Property(b => b.SearchVector)
.HasComputedColumnSql("to_tsvector('english', coalesce(\"Title\", '') || ' ' || coalesce(\"PlainTextContent\", ''))", stored: true);
Je zal hier zien wat we gebruiken. HasComputedColumnSql We speciferen ook dat de kolom in de database is opgeslagen. Dit is belangrijk omdat het Postgres vertelt om de zoekvector in de databasis op te slaan. Dit stelt ons in staat om woorden in het document te zoeken met behulp van de zoekvektor .
In de database werd dit gegenereerd voor elke rij,, die de M SK1lexemes' in het document en hun posities zijn.
"'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 ...
SoektogAPI
Dan kunnen we deze kolom gebruiken om woorden in het document te zoeken. Matches functione om te zoeken naar de vraag in de zoekvector. We kunnen ook het Rank functione om de resultaten te rangschikken volgens relevantie.
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();
Je'l Hier zie je dat we ook een ander query constructor gebruiken. EF.Functions.ToTsQuery("english", query + ":*") Dit stelt ons in staat om een TypeAhead-type-functie te bieden: (waar we e.gM SK2 'catMSC4 kunnen typen en 'cat+', 'cats+', |'katerpillar+ ' enz.
Additionally laat het ons de belangrijkste blogpostvraag vereenvoudigen om alleen maar te zoeken naar de vraag in de SearchVector column. Dit is een krachtige functie die ons in staat stelt om woorden te zoeken. Title, PlainTextContent. We gebruiken nog steeds de index die we eerder lieten zien. CategoryEntity.
x.Categories.Any(c =>
EF.Functions.ToTsVector("english", c.Name)
.Matches(EF.Functions.ToTsQuery("english", query + ":*")))
Dan gebruiken we de Rank functione om de resultaten te rangschikken volgens relevantie op basis van de vraag.
x.SearchVector.Rank(EF.Functions.ToTsQuery("english", query + ":*")))
Dit laat ons het eindpunt gebruiken als volgt, waar we in de eerste paar letters van een woord kunnen doorgaan en alle posten terugkrijgen die met dat woord overeenkomen.
Je kan de API in actie hier. Kijk naar de /api/SearchApi. (Note; IkM SK3 heb Swagger voor deze site geactiveerd zodat je de API in actie kunt zien, maar het moet meestal voor `IsDevelopmentMSC5 gereserveerd worden.

In de toekomst voeg ik een TypeAhead-feature toe aan de zoekbox op de site die deze functionaliteit gebruikt.
In slotting:
Je kunt zien dat het mogelijk is om krachtige zoekfuncties te krijgen met Postgres en Entity Framework. Maar het heeft complexiteit en beperkingen waar we rekening mee moeten houden ( zoals de taal-kwestieM SK3 In de volgende deel zal ik bespreken hoe we dit doen met OpenSearch - die een ton meer opstelling heeft, maar krachtiger en schaalbaarder is.