Apache Lucene είναι μια βιβλιοθήκη Java που χρησιμοποιείται για την αναζήτηση πλήρους κειμένου εγγράφων και βρίσκεται στον πυρήνα των διακομιστών αναζήτησης όπως Solr και Ελαστική αναζήτηση . Μπορεί επίσης να ενσωματωθεί σε εφαρμογές Java, όπως εφαρμογές Android ή web backend.
Ενώ οι επιλογές διαμόρφωσης του Lucene είναι εκτενείς, προορίζονται για χρήση από προγραμματιστές βάσεων δεδομένων σε ένα γενικό σώμα κειμένου. Εάν τα έγγραφά σας έχουν συγκεκριμένη δομή ή τύπο περιεχομένου, μπορείτε να επωφεληθείτε είτε για τη βελτίωση της ποιότητας αναζήτησης και της δυνατότητας ερωτήματος.
Ως παράδειγμα αυτού του είδους προσαρμογής, σε αυτό το σεμινάριο Lucene θα ευρετηριάσουμε το σώμα του Έργο Gutenberg , το οποίο προσφέρει χιλιάδες δωρεάν ηλεκτρονικά βιβλία. Γνωρίζουμε ότι πολλά από αυτά τα βιβλία είναι μυθιστορήματα. Ας υποθέσουμε ότι ενδιαφερόμαστε ιδιαίτερα για το διάλογος μέσα σε αυτά τα μυθιστορήματα. Ούτε η Lucene, η Elasticsearch, ούτε η Solr παρέχουν εξωχρηματιστηριακά εργαλεία για τον προσδιορισμό του περιεχομένου ως διαλόγου. Στην πραγματικότητα, θα απομακρύνουν τα σημεία στίξης στα πρώτα στάδια της ανάλυσης κειμένου, κάτι που έρχεται σε αντίθεση με τη δυνατότητα εντοπισμού τμημάτων του κειμένου που είναι διάλογος. Επομένως, σε αυτά τα πρώτα στάδια πρέπει να ξεκινήσει η προσαρμογή μας.
ο Ανάλυση Lucene JavaDoc παρέχει μια καλή επισκόπηση όλων των κινούμενων μερών στον αγωγό ανάλυσης κειμένου.
Σε υψηλό επίπεδο, μπορείτε να θεωρήσετε τον αγωγό ανάλυσης ότι καταναλώνει μια ακατέργαστη ροή χαρακτήρων στην αρχή και παράγει «όρους», που αντιστοιχούν περίπου στις λέξεις, στο τέλος.
Ο τυπικός αγωγός ανάλυσης μπορεί να είναι οπτικοποιείται ως έχει:
Θα δούμε πώς να προσαρμόσετε αυτόν τον αγωγό για να αναγνωρίσετε περιοχές κειμένου που επισημαίνονται με διπλά εισαγωγικά, τα οποία θα ονομάσω διάλογο και, στη συνέχεια, θα συναντήσουμε αγώνες που εμφανίζονται κατά την αναζήτηση σε αυτές τις περιοχές.
Όταν τα έγγραφα προστίθενται αρχικά στο ευρετήριο, οι χαρακτήρες διαβάζονται από μια Java InputStream και έτσι μπορούν να προέρχονται από αρχεία, βάσεις δεδομένων, κλήσεις υπηρεσίας ιστού κ.λπ. Για να δημιουργήσουμε ένα ευρετήριο για το Project Gutenberg, κατεβάζουμε τα ηλεκτρονικά βιβλία και δημιουργούμε μια μικρή εφαρμογή για να διαβάσουμε αυτά τα αρχεία και να τα γράψουμε στο ευρετήριο. Η δημιουργία ευρετηρίου Lucene και η ανάγνωση αρχείων είναι καλές διαδρομές, οπότε δεν θα τα εξερευνήσουμε πολύ. Ο βασικός κώδικας για την παραγωγή ενός ευρετηρίου είναι:
IndexWriter writer = ...; BufferedReader reader = new BufferedReader(new InputStreamReader(... fileInputStream ...)); Document document = new Document(); document.add(new StringField('title', fileName, Store.YES)); document.add(new TextField('body', reader)); writer.addDocument(document);
Μπορούμε να δούμε ότι κάθε ηλεκτρονικό βιβλίο θα αντιστοιχεί σε ένα μόνο Lucene Document
Έτσι, αργότερα, τα αποτελέσματα αναζήτησης θα είναι μια λίστα με αντίστοιχα βιβλία. Store.YES
δείχνει ότι αποθηκεύουμε το τίτλος πεδίο, το οποίο είναι μόνο το όνομα αρχείου. Δεν θέλουμε να αποθηκεύσουμε το σώμα του ebook, ωστόσο, καθώς δεν είναι απαραίτητο κατά την αναζήτηση και θα χάσει μόνο χώρο στο δίσκο.
Η πραγματική ανάγνωση της ροής ξεκινά με addDocument
. Το IndexWriter
τραβάει μάρκες από το τέλος του αγωγού. Αυτό το τράβηγμα προχωρά πίσω από το σωλήνα μέχρι το πρώτο στάδιο, το Tokenizer
, να διαβάζεται από το InputStream
.
Σημειώστε επίσης ότι δεν κλείνουμε τη ροή, καθώς η Lucene το χειρίζεται για εμάς.
Η Λουκένια StandardTokenizer πετάει τα σημεία στίξης, και έτσι η προσαρμογή μας θα ξεκινήσει εδώ, καθώς πρέπει να διατηρήσουμε τα αποσπάσματα.
Η τεκμηρίωση για StandardTokenizer
σας προσκαλεί να αντιγράψετε τον πηγαίο κώδικα και να τον προσαρμόσετε στις ανάγκες σας, αλλά αυτή η λύση θα ήταν άσκοπα περίπλοκη. Αντ 'αυτού, θα επεκτείνουμε το CharTokenizer
, το οποίο σας επιτρέπει να καθορίσετε χαρακτήρες για 'αποδοχή', όπου αυτοί που δεν είναι 'αποδεκτοί' θα αντιμετωπίζονται ως οριοθέτες μεταξύ των διακριτικών και θα απορριφθούν. Δεδομένου ότι μας ενδιαφέρει τα λόγια και τα αποσπάσματα γύρω τους, το προσαρμοσμένο Tokenizer μας είναι απλά:
public class QuotationTokenizer extends CharTokenizer { @Override protected boolean isTokenChar(int c) return Character.isLetter(c) }
Λαμβάνοντας υπόψη μια ροή εισόδου [He said, 'Good day'.]
, τα διακριτικά που παράγονται θα είναι [He]
, [said]
, ['Good]
, [day']
σε ποια γλώσσα είναι γραμμένα τα discord bots
Σημειώστε πώς διανέμονται τα εισαγωγικά μέσα στα διακριτικά. Είναι δυνατό να γράψετε ένα Tokenizer
που παράγει ξεχωριστά διακριτικά για κάθε προσφορά, αλλά Tokenizer
ασχολείται επίσης με τις εύκολες και εύκολες λεπτομέρειες, όπως η αποθήκευση σε προσωρινή μνήμη και η σάρωση, επομένως είναι καλύτερο να διατηρήσετε το Tokenizer
απλή και καθαρίστε τη ροή του διακριτικού κατά μήκος του αγωγού.
Μετά το tokenizer έρχεται μια σειρά TokenFilter
αντικείμενα. Σημειώστε, παρεμπιπτόντως, ότι φίλτρο είναι λίγο λάθος, ως TokenFilter
μπορεί να προσθέσει, να αφαιρέσει ή να τροποποιήσει μάρκες.
Πολλές από τις τάξεις φίλτρων που παρέχονται από τη Lucene αναμένουν μεμονωμένες λέξεις, οπότε δεν θα πρέπει να ρέουν τα μικτά κουπόνια λέξεων-και-προσφορών. Επομένως, η επόμενη προσαρμογή του φροντιστηρίου Lucene πρέπει να είναι η εισαγωγή ενός φίλτρου που θα καθαρίζει την έξοδο του QuotationTokenizer
.
Αυτός ο καθαρισμός θα περιλαμβάνει την παραγωγή ενός επιπλέον αρχική προσφορά διακριτικό εάν το απόσπασμα εμφανίζεται στην αρχή μιας λέξης, ή ένα τελικό απόσπασμα διακριτικό εάν το απόσπασμα εμφανίζεται στο τέλος. Θα αφήσουμε στην άκρη το χειρισμό μεμονωμένων λέξεων για απλότητα.
Δημιουργία a TokenFilter
Η υποκατηγορία περιλαμβάνει την εφαρμογή μίας μεθόδου: incrementToken
. Αυτή η μέθοδος πρέπει να καλέσει incrementToken
στο προηγούμενο φίλτρο του σωλήνα και, στη συνέχεια, χειριστείτε τα αποτελέσματα αυτής της κλήσης για να εκτελέσετε οποιαδήποτε εργασία είναι υπεύθυνη για το φίλτρο. Τα αποτελέσματα του incrementToken
είναι διαθέσιμα μέσω Attribute
αντικείμενα, τα οποία περιγράφουν την τρέχουσα κατάσταση της επεξεργασίας διακριτικών. Μετά την εφαρμογή μας του incrementToken
επιστρέφει, αναμένεται ότι τα χαρακτηριστικά έχουν χειριστεί για να ρυθμίσετε το διακριτικό για το επόμενο φίλτρο (ή το ευρετήριο εάν είμαστε στο τέλος του σωλήνα).
Τα χαρακτηριστικά που μας ενδιαφέρουν σε αυτό το σημείο στον αγωγό είναι:
CharTermAttribute
: Περιέχει ένα char[]
buffer που κρατά τους χαρακτήρες του τρέχοντος διακριτικού. Θα πρέπει να το χειριστούμε για να αφαιρέσουμε την προσφορά ή για να δημιουργήσουμε ένα διακριτικό προσφοράς.
TypeAttribute
: Περιέχει τον 'τύπο' του τρέχοντος διακριτικού. Επειδή προσθέτουμε εισαγωγικά και εισαγωγικά στη ροή διακριτικών, θα παρουσιάσουμε δύο νέους τύπους χρησιμοποιώντας το φίλτρο μας.
OffsetAttribute
: Το Lucene μπορεί προαιρετικά να αποθηκεύει αναφορές στη θέση των όρων στο αρχικό έγγραφο. Αυτές οι αναφορές ονομάζονται 'offsets', οι οποίοι είναι απλά δείκτες έναρξης και λήξης στην αρχική ροή χαρακτήρων. Εάν αλλάξουμε το buffer σε CharTermAttribute
για να δείξουμε μόνο ένα υπόστρωμα του διακριτικού, πρέπει να προσαρμόσουμε αυτά τα αντισταθμιστικά ανάλογα.
Ίσως αναρωτιέστε γιατί το API για χειρισμό ροών διακριτικών είναι τόσο περίπλοκο και, συγκεκριμένα, γιατί δεν μπορούμε απλώς να κάνουμε κάτι σαν String#split
στα εισερχόμενα διακριτικά. Αυτό συμβαίνει επειδή το Lucene έχει σχεδιαστεί για ευρετηρίαση υψηλής ταχύτητας και χαμηλής επιβάρυνσης, με το οποίο τα ενσωματωμένα tokenizer και τα φίλτρα μπορούν να μασήσουν γρήγορα gigabytes κειμένου, ενώ χρησιμοποιούν μόνο megabytes μνήμης. Για να επιτευχθεί αυτό, γίνονται λίγες ή καθόλου κατανομές κατά τη διάρκεια του tokenization και του φιλτραρίσματος, και έτσι το Attribute
περιπτώσεις που αναφέρονται παραπάνω προορίζονται να διατεθούν μία φορά και να επαναχρησιμοποιηθούν. Εάν τα tokenizer και τα φίλτρα σας είναι γραμμένα με αυτόν τον τρόπο και ελαχιστοποιούν τις δικές τους κατανομές, μπορείτε να προσαρμόσετε το Lucene χωρίς να υπονομεύσετε την απόδοση.
μέχρι το 2025 τα ρομπότ θα υπάρχουν σε κάθε οικιακό διαφημιστικό σποτ
Έχοντας όλα αυτά υπόψη, ας δούμε πώς να εφαρμόσουμε ένα φίλτρο που παίρνει ένα διακριτικό όπως ['Hello]
και παράγει τα δύο διακριτικά, [']
και [Hello]
:
public class QuotationTokenFilter extends TokenFilter { private static final char QUOTE = '''; public static final String QUOTE_START_TYPE = 'start_quote'; public static final String QUOTE_END_TYPE = 'end_quote'; private final OffsetAttribute offsetAttr = addAttribute(OffsetAttribute.class); private final TypeAttribute typeAttr = addAttribute(TypeAttribute.class); private final CharTermAttribute termBufferAttr = addAttribute(CharTermAttribute.class);
Ξεκινάμε με τη λήψη αναφορών σε ορισμένα από τα χαρακτηριστικά που είδαμε νωρίτερα. Καταθέτουμε τα ονόματα των πεδίων με το 'Attr', ώστε να είναι σαφές αργότερα όταν αναφερόμαστε σε αυτά. Είναι πιθανό ότι ορισμένα Tokenizer
οι υλοποιήσεις δεν παρέχουν αυτά τα χαρακτηριστικά, επομένως χρησιμοποιούμε addAttribute
για να λάβετε τις αναφορές μας. addAttribute
θα δημιουργήσει μια παρουσία χαρακτηριστικού εάν λείπει, αλλιώς τραβήξτε μια κοινή αναφορά στο χαρακτηριστικό αυτού του τύπου. Σημειώστε ότι το Lucene δεν επιτρέπει ταυτόχρονα πολλαπλές παρουσίες του ίδιου τύπου χαρακτηριστικού.
private boolean emitExtraToken; private int extraTokenStartOffset, extraTokenEndOffset; private String extraTokenType;
Επειδή το φίλτρο μας θα παρουσιάσει ένα νέο διακριτικό που δεν υπήρχε στην αρχική ροή, χρειαζόμαστε ένα μέρος για να αποθηκεύσουμε την κατάσταση αυτού του διακριτικού μεταξύ των κλήσεων προς incrementToken
. Επειδή χωρίζουμε ένα υπάρχον διακριτικό σε δύο, αρκεί να γνωρίζουμε μόνο τις αντισταθμίσεις και τον τύπο του νέου διακριτικού. Έχουμε επίσης μια σημαία που μας λέει αν η επόμενη κλήση προς incrementToken
θα εκπέμπει αυτό το επιπλέον διακριτικό. Το Lucene παρέχει στην πραγματικότητα ένα ζευγάρι μεθόδων, captureState
και restoreState
, το οποίο θα το κάνει αυτό για εσάς. Αλλά αυτές οι μέθοδοι περιλαμβάνουν την κατανομή ενός State
αντικείμενο και μπορεί στην πραγματικότητα να είναι πιο δύσκολο από το να διαχειριστείτε μόνοι σας αυτήν την κατάσταση, οπότε θα αποφύγουμε τη χρήση τους.
@Override public void reset() throws IOException { emitExtraToken = false; extraTokenStartOffset = -1; extraTokenEndOffset = -1; extraTokenType = null; super.reset(); }
Ως μέρος της επιθετικής αποφυγής της κατανομής, το Lucene μπορεί να επαναχρησιμοποιήσει τις παρουσίες φίλτρου. Σε αυτήν την περίπτωση, αναμένεται μια κλήση στο reset
θα επαναφέρει το φίλτρο στην αρχική του κατάσταση. Εδώ λοιπόν, επαναφέρουμε απλώς τα επιπλέον πεδία διακριτικών.
@Override public boolean incrementToken() throws IOException { if (emitExtraToken) { advanceToExtraToken(); emitExtraToken = false; return true; } ...
Τώρα φτάνουμε στα ενδιαφέροντα κομμάτια. Όταν η εφαρμογή μας incrementToken
καλείται, έχουμε την ευκαιρία να δεν κλήση incrementToken
στο προηγούμενο στάδιο του αγωγού. Με αυτόν τον τρόπο, παρουσιάζουμε αποτελεσματικά ένα νέο διακριτικό, επειδή δεν τραβάμε ένα διακριτικό από το Tokenizer
.
Αντ 'αυτού, καλούμε advanceToExtraToken
για να ρυθμίσετε τα χαρακτηριστικά για το επιπλέον διακριτικό μας, ορίστε emitExtraToken
στο false για να αποφύγετε αυτόν τον κλάδο στην επόμενη κλήση και, στη συνέχεια, επιστρέψτε true
, το οποίο υποδεικνύει ότι υπάρχει άλλο διακριτικό.
@Override public boolean incrementToken() throws IOException { ... (emit extra token) ... boolean hasNext = input.incrementToken(); if (hasNext) { char[] buffer = termBufferAttr.buffer(); if (termBuffer.length() > 1) { if (buffer[0] == QUOTE) { splitTermQuoteFirst(); } else if (buffer[termBuffer.length() - 1] == QUOTE) { splitTermWordFirst(); } } else if (termBuffer.length() == 1) { if (buffer[0] == QUOTE) { typeAttr.setType(QUOTE_END_TYPE); } } } return hasNext; }
Το υπόλοιπο του incrementToken
θα κάνει ένα από τα τρία διαφορετικά πράγματα. Θυμηθείτε ότι termBufferAttr
χρησιμοποιείται για την επιθεώρηση του περιεχομένου του διακριτικού που περνά μέσω του σωλήνα:
Αν φτάσαμε στο τέλος της ροής διακριτικών (δηλαδή hasNext
είναι ψευδής), τελειώσαμε και απλώς επιστρέφουμε.
Εάν έχουμε ένα διακριτικό περισσότερων από έναν χαρακτήρων και ένας από αυτούς τους χαρακτήρες είναι ένα απόσπασμα, χωρίζουμε το διακριτικό.
Εάν το διακριτικό είναι απόμερο απόσπασμα, υποθέτουμε ότι είναι τελικό απόσπασμα. Για να καταλάβετε γιατί, σημειώστε ότι τα αρχικά εισαγωγικά εμφανίζονται πάντα στα αριστερά μιας λέξης (δηλαδή, χωρίς ενδιάμεση στίξη), ενώ τα τελικά εισαγωγικά μπορούν να ακολουθούν σημεία στίξης (όπως στην πρόταση, [He told us to 'go back the way we came.']
). Σε αυτές τις περιπτώσεις, το τελικό απόσπασμα θα είναι ήδη ένα ξεχωριστό διακριτικό και γι 'αυτό χρειάζεται μόνο να ορίσουμε τον τύπο του.
splitTermQuoteFirst
και splitTermWordFirst
θα ορίσει χαρακτηριστικά για να κάνει το τρέχον διακριτικό είτε μια λέξη είτε μια προσφορά, και θα ρυθμίσει τα «επιπλέον» πεδία για να επιτρέψει στο άλλο μισό να καταναλωθεί αργότερα. Οι δύο μέθοδοι είναι παρόμοιες, οπότε θα δούμε μόνο splitTermQuoteFirst
:
private void splitTermQuoteFirst() { int origStart = offsetAttr.startOffset(); int origEnd = offsetAttr.endOffset(); offsetAttr.setOffset(origStart, origStart + 1); typeAttr.setType(QUOTE_START_TYPE); termBufferAttr.setLength(1); prepareExtraTerm(origStart + 1, origEnd, TypeAttribute.DEFAULT_TYPE); }
Επειδή θέλουμε να χωρίσουμε αυτό το διακριτικό με το απόσπασμα να εμφανίζεται πρώτα στη ροή, περικόπτουμε το buffer ρυθμίζοντας το μήκος σε ένα (δηλαδή, έναν χαρακτήρα, δηλαδή, το απόσπασμα). Προσαρμόζουμε ανάλογα τις αντισταθμίσεις (δηλ. Δείχνοντας την προσφορά στο πρωτότυπο έγγραφο) και ορίζουμε επίσης τον τύπο ως αρχική προσφορά.
prepareExtraTerm
θα ορίσει το extra*
πεδία και σύνολο emitExtraToken
σε αλήθεια. Ονομάζεται με όφσετ που δείχνουν το 'επιπλέον' διακριτικό (δηλαδή, τη λέξη που ακολουθεί την προσφορά).
Το σύνολο των QuotationTokenFilter
είναι διαθέσιμο στο GitHub .
Εκτός αυτού, ενώ αυτό το φίλτρο παράγει μόνο ένα επιπλέον διακριτικό, αυτή η προσέγγιση μπορεί να επεκταθεί για να εισαγάγει έναν αυθαίρετο αριθμό επιπλέον διακριτικών. Απλώς αντικαταστήστε το extra*
πεδία με μια συλλογή ή, ακόμη καλύτερα, μια συστοιχία σταθερού μήκους εάν υπάρχει όριο στον αριθμό των επιπλέον διακριτικών που μπορούν να παραχθούν. Βλέπω SynonymFilter
και του PendingInput
εσωτερική τάξη για ένα παράδειγμα αυτού.
Τώρα που έχουμε καταβάλει κάθε προσπάθεια για να προσθέσουμε αυτά τα αποσπάσματα στη ροή διακριτικών, μπορούμε να τα χρησιμοποιήσουμε για να οριοθετήσουμε τμήματα διαλόγου στο κείμενο.
πίνακας διάθεσης στο σχεδιασμό μόδας
Δεδομένου ότι ο τελικός μας στόχος είναι να προσαρμόσουμε τα αποτελέσματα αναζήτησης με βάση το εάν οι όροι αποτελούν μέρος του διαλόγου ή όχι, πρέπει να επισυνάψουμε μεταδεδομένα σε αυτούς τους όρους. Το Lucene παρέχει PayloadAttribute
για το σκοπό αυτό. Τα ωφέλιμα φορτία είναι πίνακες byte που αποθηκεύονται παράλληλα με όρους στο ευρετήριο και μπορούν να διαβαστούν αργότερα κατά τη διάρκεια μιας αναζήτησης. Αυτό σημαίνει ότι η σημαία μας θα καταλαμβάνει άσκοπα ένα ολόκληρο byte, οπότε θα μπορούσαν να εφαρμοστούν επιπλέον ωφέλιμα φορτία ως σημαίες bit για εξοικονόμηση χώρου.
Ακολουθεί ένα νέο φίλτρο, DialoguePayloadTokenFilter
, το οποίο προστίθεται στο τέλος του αγωγού ανάλυσης. Συνδέει το ωφέλιμο φορτίο που δείχνει εάν το διακριτικό είναι μέρος του διαλόγου.
public class DialoguePayloadTokenFilter extends TokenFilter { private final TypeAttribute typeAttr = getAttribute(TypeAttribute.class); private final PayloadAttribute payloadAttr = addAttribute(PayloadAttribute.class); private static final BytesRef PAYLOAD_DIALOGUE = new BytesRef(new byte[] { 1 }); private static final BytesRef PAYLOAD_NOT_DIALOGUE = new BytesRef(new byte[] { 0 }); private boolean withinDialogue; protected DialoguePayloadTokenFilter(TokenStream input) { super(input); } @Override public void reset() throws IOException { this.withinDialogue = false; super.reset(); } @Override public boolean incrementToken() throws IOException { boolean hasNext = input.incrementToken(); while(hasNext) { boolean isStartQuote = QuotationTokenFilter .QUOTE_START_TYPE.equals(typeAttr.type()); boolean isEndQuote = QuotationTokenFilter .QUOTE_END_TYPE.equals(typeAttr.type()); if (isStartQuote) { withinDialogue = true; hasNext = input.incrementToken(); } else if (isEndQuote) { withinDialogue = false; hasNext = input.incrementToken(); } else { break; } } if (hasNext) { payloadAttr.setPayload(withinDialogue ? PAYLOAD_DIALOGUE : PAYLOAD_NOT_DIALOGUE); } return hasNext; } }
Δεδομένου ότι αυτό το φίλτρο χρειάζεται μόνο να διατηρήσει ένα μόνο κομμάτι της κατάστασης, withinDialogue
, είναι πολύ πιο απλό. Ένα απόσπασμα έναρξης δείχνει ότι βρισκόμαστε τώρα σε ένα τμήμα του διαλόγου, ενώ ένα τελικό απόσπασμα δείχνει ότι το τμήμα του διαλόγου έχει τελειώσει. Και στις δύο περιπτώσεις, το διακριτικό προσφοράς απορρίπτεται κάνοντας μια δεύτερη κλήση στο incrementToken
, οπότε στην πραγματικότητα, αρχική προσφορά ή τελικό απόσπασμα Τα διακριτικά δεν ρέουν ποτέ πέρα από αυτό το στάδιο στο στάδιο της προετοιμασίας.
Για παράδειγμα, DialoguePayloadTokenFilter
θα μεταμορφώσει τη ροή διακριτικών:
[the], [program], [printed], ['], [hello], [world], [']`
σε αυτήν τη νέα ροή:
[the][0], [program][0], [printed][0], [hello][1], [world][1]
Ένα Analyzer
είναι υπεύθυνη για τη συναρμολόγηση του αγωγού ανάλυσης, συνήθως συνδυάζοντας ένα Tokenizer
με μια σειρά TokenFilter
s. Analyzer
s μπορούν επίσης να ορίσουν πώς επαναχρησιμοποιείται αυτός ο αγωγός μεταξύ των αναλύσεων. Δεν χρειάζεται να ανησυχούμε για αυτό, καθώς τα συστατικά μας δεν απαιτούν τίποτα εκτός από μια κλήση προς reset()
μεταξύ χρήσεων, τις οποίες θα κάνει πάντα η Lucene. Πρέπει απλώς να κάνουμε τη συναρμολόγηση εφαρμόζοντας Analyzer#createComponents(String)
:
public class DialogueAnalyzer extends Analyzer { @Override protected TokenStreamComponents createComponents(String fieldName) { QuotationTokenizer tokenizer = new QuotationTokenizer(); TokenFilter filter = new QuotationTokenFilter(tokenizer); filter = new LowerCaseFilter(filter); filter = new StopFilter(filter, StopAnalyzer.ENGLISH_STOP_WORDS_SET); filter = new DialoguePayloadTokenFilter(filter); return new TokenStreamComponents(tokenizer, filter); } }
Όπως είδαμε νωρίτερα, τα φίλτρα περιέχουν μια αναφορά στο προηγούμενο στάδιο του αγωγού, έτσι ώστε να τα δημιουργούμε. Ολισθαίνουμε επίσης σε μερικά φίλτρα από StandardAnalyzer
: LowerCaseFilter
και StopFilter
. Αυτά τα δύο πρέπει να ακολουθήσουν QuotationTokenFilter
για να διασφαλιστεί ότι έχουν διαχωριστεί τα αποσπάσματα. Μπορούμε να είμαστε πιο ευέλικτοι στην τοποθέτηση των DialoguePayloadTokenFilter
, αφού οπουδήποτε μετά την QuotationTokenFilter
θα κάνω. Το βάζουμε μετά StopFilter
για να αποφευχθεί η σπατάλη χρόνου για την έγχυση του ωφέλιμου διαλόγου σταματήστε τα λόγια που τελικά θα αφαιρεθεί.
Ακολουθεί μια οπτικοποίηση του νέου αγωγού μας σε δράση (μείον εκείνα τα μέρη του τυπικού αγωγού που έχουμε αφαιρέσει ή έχουμε ήδη δει):
DialogueAnalyzer
μπορεί τώρα να χρησιμοποιηθεί ως οποιοδήποτε άλλο απόθεμα Analyzer
θα ήταν, και τώρα μπορούμε να δημιουργήσουμε το ευρετήριο και να προχωρήσουμε στην αναζήτηση.
Εάν θέλαμε να ψάξουμε μόνο τον διάλογο, θα μπορούσαμε απλώς να απορρίψουμε όλα τα διακριτικά εκτός μιας αναφοράς και θα είχαμε τελειώσει. Αντ 'αυτού, αφήνοντας άθικτα όλα τα αρχικά διακριτικά, δώσαμε στους εαυτούς μας την ευελιξία είτε να εκτελέσουμε ερωτήματα που λαμβάνουν υπόψη τον διάλογο είτε να αντιμετωπίσουμε τον διάλογο όπως οποιοδήποτε άλλο μέρος του κειμένου.
Τα βασικά ερωτήματα για ένα ευρετήριο Lucene είναι καλά τεκμηριωμένο . Για τους σκοπούς μας, αρκεί να γνωρίζουμε ότι τα ερωτήματα αποτελούνται από Term
αντικείμενα κολλημένα μαζί με τελεστές όπως MUST
ή SHOULD
, μαζί με έγγραφα αντιστοίχισης που βασίζονται σε αυτούς τους όρους. Στη συνέχεια, τα αντίστοιχα έγγραφα βαθμολογούνται με βάση ένα διαμορφώσιμο Similarity
αντικείμενο και αυτά τα αποτελέσματα μπορούν να ταξινομηθούν κατά βαθμολογία, φιλτράρισμα ή περιορισμό. Για παράδειγμα, η Lucene μας επιτρέπει να κάνουμε ένα ερώτημα για τα δέκα κορυφαία έγγραφα που πρέπει να περιέχουν και τους δύο όρους [hello]
και [world]
.
Η προσαρμογή των αποτελεσμάτων αναζήτησης βάσει διαλόγου μπορεί να γίνει προσαρμόζοντας τη βαθμολογία ενός εγγράφου με βάση το ωφέλιμο φορτίο. Το πρώτο σημείο επέκτασης για αυτό θα είναι στο Similarity
, το οποίο είναι υπεύθυνο για τη ζύγιση και τη βαθμολόγηση όρων αντιστοίχισης.
Τα ερωτήματα θα χρησιμοποιούν, από προεπιλογή, DefaultSimilarity
, που σταθμίζουν τους όρους με βάση τη συχνότητα που εμφανίζονται σε ένα έγγραφο. Είναι ένα καλό σημείο επέκτασης για την προσαρμογή των βαρών, γι 'αυτό το επεκτείνουμε και για να βαθμολογούμε έγγραφα με βάση το ωφέλιμο φορτίο. Η μέθοδος DefaultSimilarity#scorePayload
παρέχεται για το σκοπό αυτό:
public final class DialogueAwareSimilarity extends DefaultSimilarity { @Override public float scorePayload(int doc, int start, int end, BytesRef payload) { if (payload.bytes[payload.offset] == 0) { return 0.0f; } return 1.0f; } }
DialogueAwareSimilarity
απλώς βαθμολογεί τα ωφέλιμα φορτία χωρίς διάλογο ως μηδέν. Όπως το καθένα Term
μπορεί να αντιστοιχιστεί πολλές φορές, θα έχει δυνητικά πολλές βαθμολογίες ωφέλιμου φορτίου. Η ερμηνεία αυτών των βαθμολογιών έως το Query
εκτέλεση.
Δώστε ιδιαίτερη προσοχή στο BytesRef
που περιέχει το ωφέλιμο φορτίο: πρέπει να ελέγξουμε το byte στο offset
, καθώς δεν μπορούμε να υποθέσουμε ότι ο πίνακας byte είναι το ίδιο ωφέλιμο φορτίο που αποθηκεύσαμε νωρίτερα. Κατά την ανάγνωση του ευρετηρίου, το Lucene δεν πρόκειται να σπαταλήσει τη μνήμη εκχωρώντας έναν ξεχωριστό πίνακα byte μόνο για την κλήση στο scorePayload
, οπότε λαμβάνουμε μια αναφορά σε έναν υπάρχοντα πίνακα byte. Όταν κωδικοποιείτε το Lucene API, πρέπει να έχετε κατά νου ότι η απόδοση είναι η προτεραιότητα, πολύ μπροστά από την ευκολία του προγραμματιστή.
Τώρα που έχουμε το νέο μας Similarity
εφαρμογή, τότε πρέπει να οριστεί στο IndexSearcher
χρησιμοποιείται για την εκτέλεση ερωτημάτων:
IndexSearcher searcher = new IndexSearcher(... reader for index ...); searcher.setSimilarity(new DialogueAwareSimilarity());
Τώρα που το IndexSearcher
μπορεί να βαθμολογήσει ωφέλιμα φορτία, πρέπει επίσης να δημιουργήσουμε ένα ερώτημα που να γνωρίζει το ωφέλιμο φορτίο. PayloadTermQuery
μπορεί να χρησιμοποιηθεί για να ταιριάξει ένα μόνο Term
ελέγχοντας επίσης τα ωφέλιμα φορτία αυτών των αγώνων:
PayloadTermQuery helloQuery = new PayloadTermQuery(new Term('body', 'hello'), new AveragePayloadFunction());
Αυτό το ερώτημα ταιριάζει με τον όρο [hello]
μέσα στο σώμα πεδίο (θυμηθείτε ότι εδώ βάζουμε το περιεχόμενο του εγγράφου). Πρέπει επίσης να παρέχουμε μια συνάρτηση για τον υπολογισμό της τελικής βαθμολογίας ωφέλιμου φορτίου από όλους τους αγώνες όρου, οπότε συνδέουμε το Για παράδειγμα, εάν ο όρος AveragePayloadFunction
εμφανίζεται εντός του διαλόγου δύο φορές και εκτός του διαλόγου μία φορά, η τελική βαθμολογία ωφέλιμου φορτίου θα είναι ⁄₃ 2. Αυτή η τελική βαθμολογία ωφέλιμου φορτίου πολλαπλασιάζεται με αυτήν που παρέχεται από [hello]
για ολόκληρο το έγγραφο.
Χρησιμοποιούμε έναν μέσο όρο γιατί θα θέλαμε να υπογραμμίσουμε τα αποτελέσματα αναζήτησης όπου πολλοί όροι εμφανίζονται εκτός του διαλόγου και να παράγουμε μηδενική βαθμολογία για έγγραφα χωρίς όρους σε διάλογο.
Μπορούμε επίσης να συνθέσουμε πολλά DefaultSimilarity
αντικείμενα που χρησιμοποιούν ένα PayloadTermQuery
εάν θέλουμε να αναζητήσουμε πολλούς όρους που περιέχονται στο διάλογο (λάβετε υπόψη ότι η σειρά των όρων δεν σχετίζεται με αυτό το ερώτημα, αν και άλλοι τύποι ερωτημάτων γνωρίζουν τη θέση):
BooleanQuery
Όταν εκτελείται αυτό το ερώτημα, μπορούμε να δούμε πώς λειτουργούν μαζί η δομή του ερωτήματος και η εφαρμογή ομοιότητας:
Για να εκτελέσουμε το ερώτημα, το παραδίδουμε στο PayloadTermQuery worldQuery = new PayloadTermQuery(new Term('body', 'world'), new AveragePayloadFunction()); BooleanQuery query = new BooleanQuery(); query.add(helloQuery, Occur.MUST); query.add(worldQuery, Occur.MUST);
:
IndexSearcher
TopScoreDocCollector collector = TopScoreDocCollector.create(10); searcher.search(query, new PositiveScoresOnlyCollector(collector)); TopDocs topDocs = collector.topDocs();
τα αντικείμενα χρησιμοποιούνται για την προετοιμασία της συλλογής των αντίστοιχων εγγράφων.
οι συλλέκτες μπορούν να δημιουργηθούν για να επιτύχουν έναν συνδυασμό ταξινόμησης, περιορισμού και φιλτραρίσματος. Για να λάβουμε, για παράδειγμα, τα δέκα κορυφαία έγγραφα που περιέχουν τουλάχιστον έναν όρο σε διάλογο, συνδυάζουμε Collector
και TopScoreDocCollector
. Η λήψη μόνο θετικών βαθμολογιών διασφαλίζει ότι οι αντιστοιχίσεις μηδενικής βαθμολογίας (δηλαδή, αυτοί που δεν έχουν όρους διαλόγου) φιλτράρονται.
Για να δούμε αυτό το ερώτημα σε δράση, μπορούμε να το εκτελέσουμε και μετά να χρησιμοποιήσουμε το PositiveScoresOnlyCollector
για να δείτε πώς βαθμολογούνται μεμονωμένα έγγραφα:
IndexSearcher#explain
Εδώ, επαναλαμβάνουμε τα αναγνωριστικά εγγράφων στο for (ScoreDoc result : topDocs.scoreDocs) { Document doc = searcher.doc(result.doc, Collections.singleton('title')); System.out.println('--- document ' + doc.getField('title').stringValue() + ' ---'); System.out.println(this.searcher.explain(query, result.doc)); }
αποκτήθηκε από την αναζήτηση. Χρησιμοποιούμε επίσης TopDocs
για να ανακτήσετε το πεδίο τίτλου για προβολή. Για το ερώτημά μας για IndexSearcher#doc
, αυτό έχει ως αποτέλεσμα:
'hello'
Αν και η έξοδος είναι γεμάτη με ορολογία, μπορούμε να δούμε πώς το έθιμο μας --- Document whelv10.txt --- 0.072256625 = (MATCH) btq, product of: 0.072256625 = weight(body:hello in 7336) [DialogueAwareSimilarity], result of: 0.072256625 = fieldWeight in 7336, product of: 2.345208 = tf(freq=5.5), with freq of: 5.5 = phraseFreq=5.5 3.1549776 = idf(docFreq=2873, maxDocs=24796) 0.009765625 = fieldNorm(doc=7336) 1.0 = AveragePayloadFunction.docScore() --- Document daved10.txt --- 0.061311778 = (MATCH) btq, product of: 0.061311778 = weight(body:hello in 6873) [DialogueAwareSimilarity], result of: 0.061311778 = fieldWeight in 6873, product of: 3.3166249 = tf(freq=11.0), with freq of: 11.0 = phraseFreq=11.0 3.1549776 = idf(docFreq=2873, maxDocs=24796) 0.005859375 = fieldNorm(doc=6873) 1.0 = AveragePayloadFunction.docScore() ...
Η εφαρμογή χρησιμοποιήθηκε στη βαθμολογία και πώς το Similarity
παρήγαγε πολλαπλασιαστή του MaxPayloadFunction
για αυτούς τους αγώνες. Αυτό συνεπάγεται ότι το ωφέλιμο φορτίο φορτώθηκε και σημειώθηκε, και όλοι οι αγώνες του 1.0
συνέβη στον διάλογο και έτσι αυτά τα αποτελέσματα βρίσκονται στην κορυφή όπου τα περιμένουμε.
Αξίζει επίσης να επισημανθεί ότι ο δείκτης για το Project Gutenberg, με ωφέλιμο φορτίο, φτάνει σε περίπου τέσσερα gigabyte σε μέγεθος, και όμως στη μέτρια μηχανή ανάπτυξης μου, τα ερωτήματα εμφανίζονται αμέσως. Δεν έχουμε θυσιάσει καμία ταχύτητα για την επίτευξη των στόχων αναζήτησης.
Το Lucene είναι μια ισχυρή, ενσωματωμένη βιβλιοθήκη αναζήτησης πλήρους κειμένου που λαμβάνει μια ακατέργαστη ροή χαρακτήρων, τις ομαδοποιεί σε διακριτικά και τους διατηρεί ως όρους σε ένα ευρετήριο. Μπορεί γρήγορα να ζητήσει αυτό το ευρετήριο και να παρέχει αποτελέσματα κατάταξης και παρέχει άφθονες ευκαιρίες για επέκταση διατηρώντας παράλληλα την αποδοτικότητα.
Χρησιμοποιώντας το Lucene απευθείας στις εφαρμογές μας ή ως τμήμα διακομιστή, μπορούμε να πραγματοποιήσουμε αναζητήσεις πλήρους κειμένου σε πραγματικό χρόνο σε gigabyte περιεχομένου. Επιπλέον, μέσω προσαρμοσμένης ανάλυσης και βαθμολόγησης, μπορούμε να εκμεταλλευτούμε τις δυνατότητες που αφορούν συγκεκριμένους τομείς στα έγγραφά μας για να βελτιώσουμε τη συνάφεια των αποτελεσμάτων ή των προσαρμοσμένων ερωτημάτων.
αντ' αυτού χρησιμοποιήστε έναν διακομιστή παραγωγής wsgi
Οι πλήρεις καταχωρίσεις κώδικα για αυτό το σεμινάριο Lucene είναι διαθέσιμο στο GitHub . Το repo περιέχει δύο εφαρμογές: 'Hello'
για τη δημιουργία του ευρετηρίου και LuceneIndexerApp
για την εκτέλεση ερωτημάτων.
Το σώμα του έργου Gutenberg, το οποίο μπορεί να αποκτηθεί ως εικόνα δίσκου μέσω του BitTorrent , περιέχει πολλά βιβλία που αξίζουν ανάγνωση (είτε με τον Lucene, είτε με τον παλιομοδίτικο τρόπο).
Καλή ευρετηρίαση!