NOTE: Apart from
(and even then it's questionable, I'm Scottish). These are machine translated in languages I don't read. If they're terrible please contact me.
You can see how this translation was done in this article.
Tuesday, 20 August 2024
//Less than a minute
Αναζήτηση περιεχομένου είναι ένα κρίσιμο μέρος οποιασδήποτε ιστοσελίδας heavy περιεχόμενο. Βελτιώνει την ανακάλυψη και την εμπειρία του χρήστη. Σε αυτό το άρθρο θα καλύψω πώς πρόσθεσα πλήρη αναζήτηση κειμένου για αυτό το site
Επόμενα μέρη σε αυτή τη σειρά:
Υπάρχουν αρκετοί τρόποι για να κάνεις πλήρη αναζήτηση κειμένου, συμπεριλαμβανομένου
Σε αυτό το blog μετακόμισα πρόσφατα στη χρήση Postgres για τη βάση δεδομένων μου. Postgres έχει ένα πλήρες χαρακτηριστικό αναζήτησης κειμένου που είναι πολύ ισχυρό και (κάτι) εύκολο στη χρήση. Είναι επίσης πολύ γρήγορο και μπορεί να χειριστεί περίπλοκα ερωτήματα με ευκολία.
Όταν χτίζετε yout DbContext
Μπορείτε να προσδιορίσετε ποια πεδία έχουν ενεργοποιημένη την πλήρη αναζήτηση κειμένου.
Postgres χρησιμοποιεί την έννοια των διανυσματικών φορέων αναζήτησης για την επίτευξη γρήγορης, αποδοτικής πλήρους αναζήτησης κειμένου. Ένα διάνυσμα αναζήτησης είναι μια δομή δεδομένων που περιέχει τις λέξεις σε ένα έγγραφο και τις θέσεις τους. Ουσιαστικά η προσύνθεση του φορέα αναζήτησης για κάθε σειρά στη βάση δεδομένων επιτρέπει στα Postgres να ψάχνουν για λέξεις στο έγγραφο πολύ γρήγορα. Χρησιμοποιεί δύο ειδικούς τύπους δεδομένων για την επίτευξη αυτού:
Επιπλέον προσφέρει μια συνάρτηση κατάταξης που σας επιτρέπει να βαθμολογήσετε τα αποτελέσματα με βάση το πόσο καλά ταιριάζουν με το ερώτημα αναζήτησης. Αυτό είναι πολύ ισχυρό και σας επιτρέπει να παραγγείλετε τα αποτελέσματα από τη συνάφεια. PostgreSQL αναθέτει μια κατάταξη στα αποτελέσματα με βάση τη σημασία. Η σχέση υπολογίζεται εξετάζοντας παράγοντες όπως η εγγύτητα των όρων αναζήτησης μεταξύ τους και πόσο συχνά εμφανίζονται στο έγγραφο. Οι λειτουργίες ts_rank ή ts_rank_cd χρησιμοποιούνται για τον υπολογισμό αυτής της κατάταξης.
Μπορείτε να διαβάσετε περισσότερα για τα πλήρη χαρακτηριστικά αναζήτησης κειμένου του Postgres Ορίστε.
Το πακέτο-πλαίσιο για την οντότητα Postgres Ορίστε. παρέχει ισχυρή υποστήριξη για την πλήρη αναζήτηση κειμένου. Σας επιτρέπει να προσδιορίσετε ποια πεδία είναι πλήρη ευρετήρια κειμένου και πώς να τα ρωτήσετε.
Για να το κάνουμε αυτό προσθέτουμε συγκεκριμένους τύπους δεικτών στους Φορείς μας, όπως ορίζονται στο 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");
...
Εδώ προσθέτουμε ένα πλήρες ευρετήριο κειμένου στο Title
και PlainTextContent
πεδία μας BlogPostEntity
. Προσδιορίζουμε επίσης ότι ο δείκτης θα πρέπει να χρησιμοποιήσει το GIN
τύπος ευρετηρίου και ο english
γλώσσα. Αυτό είναι σημαντικό καθώς λέει στον Postgres πώς να ευρετηριάσει τα δεδομένα και ποια γλώσσα να χρησιμοποιήσει για να αντλεί και να σταματήσει τις λέξεις.
Αυτό είναι προφανώς ένα ζήτημα για το blog μας, καθώς έχουμε πολλές γλώσσες. Δυστυχώς προς το παρόν, απλά χρησιμοποιώ το... english
γλώσσα για όλες τις θέσεις. Αυτό είναι κάτι που πρέπει να αντιμετωπίσω στο μέλλον αλλά προς το παρόν λειτουργεί αρκετά καλά.
Προσθέτουμε επίσης έναν δείκτη στο Category
οντότητα:
modelBuilder.Entity<CategoryEntity>(entity =>
{
entity.HasIndex(b => b.Name).HasMethod("GIN").IsTsVectorExpressionIndex("english");;
...
Κάνοντας αυτό το Postgres δημιουργεί ένα διάνυσμα αναζήτησης για κάθε σειρά στη βάση δεδομένων. Αυτό το διάνυσμα περιέχει τις λέξεις στο Title
και PlainTextContent
χωράφια. Μπορούμε στη συνέχεια να χρησιμοποιήσουμε αυτό το διάνυσμα για να αναζητήσουμε λέξεις στο έγγραφο.
Αυτό μεταφράζεται σε μια λειτουργία to_tsvector στο SQL που παράγει το διάνυσμα αναζήτησης για τη σειρά. Στη συνέχεια μπορούμε να χρησιμοποιήσουμε τη λειτουργία ts_rank για να ταξινομήσουμε τα αποτελέσματα με βάση τη σχέση.
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
Εφαρμόστε αυτό ως μετανάστευση στη βάση δεδομένων μας και είμαστε έτοιμοι να ξεκινήσουμε την έρευνα.
Για να ψάξουμε θα χρησιμοποιήσουμε το EF.Functions.ToTsVector
και EF.Functions.WebSearchToTsQuery
λειτουργίες για τη δημιουργία ενός φορέα αναζήτησης και ερώτησης. Στη συνέχεια, μπορούμε να χρησιμοποιήσουμε το Matches
λειτουργία για να αναζητήσετε το ερώτημα στο φορέα αναζήτησης.
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();
Η λειτουργία EF.Functions.WebSearchTsQuery δημιουργεί το ερώτημα για τη σειρά που βασίζεται στην κοινή σύνταξη μηχανών αναζήτησης Web.
SELECT websearch_to_tsquery('english', '"sad cat" or "fat rat"');
websearch_to_tsquery
-----------------------------------
'sad' <-> 'cat' | 'fat' <-> 'rat'
Σε αυτό το παράδειγμα μπορείτε να δείτε ότι αυτό δημιουργεί ένα ερώτημα που αναζητά τις λέξεις "λυπημένη γάτα" ή "χοντρός αρουραίος" στο έγγραφο. Αυτό είναι ένα ισχυρό χαρακτηριστικό που μας επιτρέπει να ψάξουμε για περίπλοκα ερωτήματα με ευκολία.
Όπως αναφέρεται befpre αυτές οι μέθοδοι παράγουν τόσο το διάνυσμα αναζήτησης όσο και το ερώτημα για τη σειρά. Στη συνέχεια, χρησιμοποιούμε το Matches
λειτουργία για να αναζητήσετε το ερώτημα στο φορέα αναζήτησης. Μπορούμε επίσης να χρησιμοποιήσουμε το Rank
λειτουργία για να ταξινομήσει τα αποτελέσματα από τη σχετικότητα.
Όπως μπορείτε να δείτε αυτό δεν είναι ένα απλό ερώτημα, αλλά είναι πολύ ισχυρό και μας επιτρέπει να ψάξουμε για λέξεις στο Title
, PlainTextContent
και Category
πεδία μας BlogPostEntity
και να ταξινομήσει αυτά από τη σημασία.
Για να χρησιμοποιήσετε αυτά (στο μέλλον) μπορούμε να δημιουργήσουμε ένα απλό τελικό σημείο WebAPI που παίρνει ένα ερώτημα και επιστρέφει τα αποτελέσματα. Αυτό είναι ένα απλό χειριστήριο που παίρνει μια ερώτηση και επιστρέφει τα αποτελέσματα:
[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);
}
Μια εναλλακτική προσέγγιση στη χρήση αυτών των "απλή" TsVector Indices είναι να χρησιμοποιήσετε μια στήλη που παράγεται για να αποθηκεύσετε το Vector Αναζήτηση και στη συνέχεια να χρησιμοποιήσετε αυτό για να αναζητήσετε. Αυτή είναι μια πιο περίπλοκη προσέγγιση, αλλά επιτρέπει την καλύτερη απόδοση.
Εδώ τροποποιούμε το BlogPostEntity
για την προσθήκη ειδικού τύπου στήλης:
[DatabaseGenerated(DatabaseGeneratedOption.Computed)]
public NpgsqlTsVector SearchVector { get; set; }
Αυτή είναι μια υπολογισμένη στήλη που παράγει το διάνυσμα αναζήτησης για τη σειρά. Στη συνέχεια, μπορούμε να χρησιμοποιήσουμε αυτή τη στήλη για να αναζητήσουμε λέξεις στο έγγραφο.
Στη συνέχεια, δημιουργήσαμε αυτόν τον δείκτη εντός του ορισμού της οντότητας μας (ακόμη και για επιβεβαίωση, αλλά αυτό μπορεί επίσης να μας επιτρέψει να έχουμε πολλαπλές γλώσσες καθορίζοντας μια στήλη γλώσσας για κάθε θέση).
entity.Property(b => b.SearchVector)
.HasComputedColumnSql("to_tsvector('english', coalesce(\"Title\", '') || ' ' || coalesce(\"PlainTextContent\", ''))", stored: true);
Θα δείτε εδώ ότι χρησιμοποιούμε HasComputedColumnSql
για να καθορίσετε ρητά τη λειτουργία PostGreSQL για τη δημιουργία του φορέα αναζήτησης. Επίσης, προσδιορίζουμε ότι η στήλη είναι αποθηκευμένη στη βάση δεδομένων. Αυτό είναι σημαντικό καθώς λέει στον Postgres να αποθηκεύσει το φορέα αναζήτησης στη βάση δεδομένων. Αυτό μας επιτρέπει να ψάξουμε για λέξεις στο έγγραφο χρησιμοποιώντας το φορέα αναζήτησης.
Στη βάση δεδομένων αυτό το δημιούργησε για κάθε σειρά, τα οποία είναι τα "λεξικά" στο έγγραφο και τις θέσεις τους:
"'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 ...
Στη συνέχεια, μπορούμε να χρησιμοποιήσουμε αυτή τη στήλη για να αναζητήσουμε λέξεις στο έγγραφο. Μπορούμε να χρησιμοποιήσουμε το Matches
λειτουργία για να αναζητήσετε το ερώτημα στο φορέα αναζήτησης. Μπορούμε επίσης να χρησιμοποιήσουμε το Rank
λειτουργία για να ταξινομήσει τα αποτελέσματα από τη σχετικότητα.
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();
Θα δείτε εδώ ότι χρησιμοποιούμε επίσης έναν διαφορετικό κατασκευαστή ερωτημάτων EF.Functions.ToTsQuery("english", query + ":*")
που μας επιτρέπει να προσφέρουμε μια λειτουργικότητα τύπου TypeAhead (όπου μπορούμε να πληκτρολογήσουμε π.χ. "Γάτα" και "γάτα," "γάτες," "κάμπια" κ.λπ.).
Επιπλέον, μας επιτρέπει να απλοποιήσουμε το κύριο blog post ερώτημα για να ψάξουμε μόνο για το ερώτημα στο SearchVector
στήλη. Αυτό είναι ένα ισχυρό χαρακτηριστικό που μας επιτρέπει να ψάξουμε για λέξεις στο Title
, PlainTextContent
. Εξακολουθούμε να χρησιμοποιούμε το δείκτη που δείξαμε παραπάνω για την CategoryEntity
.
x.Categories.Any(c =>
EF.Functions.ToTsVector("english", c.Name)
.Matches(EF.Functions.ToTsQuery("english", query + ":*")))
Στη συνέχεια, χρησιμοποιούμε το Rank
λειτουργία για να ταξινομήσει τα αποτελέσματα από τη συνάφεια με βάση το ερώτημα.
x.SearchVector.Rank(EF.Functions.ToTsQuery("english", query + ":*")))
Αυτό μας επιτρέπει να χρησιμοποιήσουμε το τελικό σημείο ως εξής, όπου μπορούμε να περάσουμε στα πρώτα γράμματα μιας λέξης και να πάρουμε πίσω όλες τις θέσεις που ταιριάζουν με αυτή τη λέξη:
Μπορείτε να δείτε το API σε δράση εδώ Ψάξτε για το /api/SearchApi
. (Σημείωση; Έχω ενεργοποιήσει Swagger για αυτό το site, ώστε να μπορείτε να δείτε το API σε δράση, αλλά τις περισσότερες από τις ώρες που αυτό θα πρέπει να προορίζεται για την ανάπτυξη ()).
Στο μέλλον θα προσθέσω ένα χαρακτηριστικό TypeAhead στο πλαίσιο αναζήτησης στην ιστοσελίδα που χρησιμοποιεί αυτή τη λειτουργία.
Μπορείτε να δείτε ότι είναι δυνατόν να αποκτήσετε ισχυρή λειτουργία αναζήτησης χρησιμοποιώντας Postgres και Πλαίσιο οντότητας. Ωστόσο, έχει πολυπλοκότητες και περιορισμούς για τους οποίους πρέπει να λογοδοτήσουμε (όπως το θέμα της γλώσσας). Στο επόμενο μέρος θα καλύψω πώς θα το κάνουμε αυτό χρησιμοποιώντας OpenSearch - η οποία είναι έχει ένα τόνο περισσότερο στημένο αλλά είναι πιο ισχυρό και κλιμακωτή.