Back to "Πλήρης αναζήτηση κειμένου (Pt 1)"

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

Entity Framework Postgres

Πλήρης αναζήτηση κειμένου (Pt 1)

Tuesday, 20 August 2024

Εισαγωγή

Αναζήτηση περιεχομένου είναι ένα κρίσιμο μέρος οποιασδήποτε ιστοσελίδας heavy περιεχόμενο. Βελτιώνει την ανακάλυψη και την εμπειρία του χρήστη. Σε αυτό το άρθρο θα καλύψω πώς πρόσθεσα πλήρη αναζήτηση κειμένου για αυτό το site

Επόμενα μέρη σε αυτή τη σειρά:

Προσεγγίσεις

Υπάρχουν αρκετοί τρόποι για να κάνεις πλήρη αναζήτηση κειμένου, συμπεριλαμβανομένου

  1. Απλά αναζητώντας μια δομή δεδομένων μνήμης (όπως μια λίστα), αυτό είναι σχετικά απλό να εφαρμοστεί αλλά δεν κλιμακώνεται καλά. Επιπλέον δεν υποστηρίζει περίπλοκες ερωτήσεις χωρίς πολλή δουλειά.
  2. Χρησιμοποιώντας μια βάση δεδομένων όπως SQL Server ή Postgres. Ενώ αυτό λειτουργεί και έχει υποστήριξη από σχεδόν όλους τους τύπους βάσεων δεδομένων δεν είναι πάντα η καλύτερη λύση για πιο περίπλοκες δομές δεδομένων ή περίπλοκα ερωτήματα? Ωστόσο, είναι αυτό που αυτό το άρθρο θα καλύψει.
  3. Χρησιμοποιώντας μια ελαφριά τεχνολογία αναζήτησης, όπως ΛουσένοCity name (optional, probably does not need a translation) ή SQLite FTS. Αυτό είναι ένα μεσαίο έδαφος μεταξύ των δύο παραπάνω λύσεων. Είναι πιο περίπλοκο από το να ψάχνεις μια λίστα αλλά λιγότερο περίπλοκη από μια πλήρη λύση βάσης δεδομένων. Ωστόσο, είναι ακόμα αρκετά περίπλοκο να εφαρμοστεί (ειδικά για την κατάποση δεδομένων) και δεν κλιμακώνεται καθώς και μια πλήρης λύση αναζήτησης. Στην πραγματικότητα πολλές άλλες τεχνολογίες αναζήτησης Χρησιμοποιήστε Lucene κάτω από την κουκούλα για Είναι καταπληκτικές δυνατότητες αναζήτησης διανυσματικών στοιχείων.
  4. Χρησιμοποιώντας μια μηχανή αναζήτησης όπως ElasticSearch, OpenSearch ή Azure Search. Αυτή είναι η πιο πολύπλοκη και εντατική λύση πόρων αλλά και η πιο ισχυρή. Είναι επίσης το πιο κλιμακωτό και μπορεί να χειριστεί περίπλοκα ερωτήματα με ευκολία. Θα πάω σε βασανιστικό βάθος την επόμενη εβδομάδα περίπου για το πώς να αυτο-ξεχωρίσω, να ρυθμίσω και να χρησιμοποιήσω το OpenSearch από το C#.

Αναζήτηση πλήρους κειμένου βάσης δεδομένων με Postgres

Σε αυτό το blog μετακόμισα πρόσφατα στη χρήση Postgres για τη βάση δεδομένων μου. Postgres έχει ένα πλήρες χαρακτηριστικό αναζήτησης κειμένου που είναι πολύ ισχυρό και (κάτι) εύκολο στη χρήση. Είναι επίσης πολύ γρήγορο και μπορεί να χειριστεί περίπλοκα ερωτήματα με ευκολία.

Όταν χτίζετε yout DbContext Μπορείτε να προσδιορίσετε ποια πεδία έχουν ενεργοποιημένη την πλήρη αναζήτηση κειμένου.

Postgres χρησιμοποιεί την έννοια των διανυσματικών φορέων αναζήτησης για την επίτευξη γρήγορης, αποδοτικής πλήρους αναζήτησης κειμένου. Ένα διάνυσμα αναζήτησης είναι μια δομή δεδομένων που περιέχει τις λέξεις σε ένα έγγραφο και τις θέσεις τους. Ουσιαστικά η προσύνθεση του φορέα αναζήτησης για κάθε σειρά στη βάση δεδομένων επιτρέπει στα Postgres να ψάχνουν για λέξεις στο έγγραφο πολύ γρήγορα. Χρησιμοποιεί δύο ειδικούς τύπους δεδομένων για την επίτευξη αυτού:

  • TSVector: Ένας ειδικός τύπος δεδομένων PostgreSQL που αποθηκεύει μια λίστα με lexemes (σκέψου το ως διάνυσμα λέξεων). Είναι η έκδοση του εγγράφου που χρησιμοποιείται για γρήγορη αναζήτηση.
  • TSQuery: Ένας άλλος ειδικός τύπος δεδομένων που αποθηκεύει το ερώτημα αναζήτησης, το οποίο περιλαμβάνει τους όρους αναζήτησης και τους λογικούς χειριστές (όπως ΚΑΙ, Ή, ΟΧΙ).

Επιπλέον προσφέρει μια συνάρτηση κατάταξης που σας επιτρέπει να βαθμολογήσετε τα αποτελέσματα με βάση το πόσο καλά ταιριάζουν με το ερώτημα αναζήτησης. Αυτό είναι πολύ ισχυρό και σας επιτρέπει να παραγγείλετε τα αποτελέσματα από τη συνάφεια. 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

Εφαρμόστε αυτό ως μετανάστευση στη βάση δεδομένων μας και είμαστε έτοιμοι να ξεκινήσουμε την έρευνα.

Αναζήτηση

Δείκτης TsVector

Για να ψάξουμε θα χρησιμοποιήσουμε το 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

Για να χρησιμοποιήσετε αυτά (στο μέλλον) μπορούμε να δημιουργήσουμε ένα απλό τελικό σημείο 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);
    }

Δημιουργία στήλης και TypeAhead

Μια εναλλακτική προσέγγιση στη χρήση αυτών των "απλή" 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 ...

SearchAPI

Στη συνέχεια, μπορούμε να χρησιμοποιήσουμε αυτή τη στήλη για να αναζητήσουμε λέξεις στο έγγραφο. Μπορούμε να χρησιμοποιήσουμε το 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 σε δράση, αλλά τις περισσότερες από τις ώρες που αυτό θα πρέπει να προορίζεται για την ανάπτυξη ()).

API

Στο μέλλον θα προσθέσω ένα χαρακτηριστικό TypeAhead στο πλαίσιο αναζήτησης στην ιστοσελίδα που χρησιμοποιεί αυτή τη λειτουργία.

Συμπέρασμα

Μπορείτε να δείτε ότι είναι δυνατόν να αποκτήσετε ισχυρή λειτουργία αναζήτησης χρησιμοποιώντας Postgres και Πλαίσιο οντότητας. Ωστόσο, έχει πολυπλοκότητες και περιορισμούς για τους οποίους πρέπει να λογοδοτήσουμε (όπως το θέμα της γλώσσας). Στο επόμενο μέρος θα καλύψω πώς θα το κάνουμε αυτό χρησιμοποιώντας OpenSearch - η οποία είναι έχει ένα τόνο περισσότερο στημένο αλλά είναι πιο ισχυρό και κλιμακωτή.

logo

©2024 Scott Galloway