Ricerca full-text di dialoghi con Apache Lucene: un tutorial

Pubblicato: 2022-03-11

Apache Lucene è una libreria Java utilizzata per la ricerca full-text di documenti ed è il fulcro di server di ricerca come Solr ed Elasticsearch. Può anche essere incorporato in applicazioni Java, come app Android o back-end web.

Sebbene le opzioni di configurazione di Lucene siano estese, sono destinate all'uso da parte degli sviluppatori di database su un corpus di testo generico. Se i tuoi documenti hanno una struttura o un tipo di contenuto specifico, puoi sfruttare entrambi per migliorare la qualità della ricerca e la capacità di query.

ricerca del testo completo con apache lucene

Come esempio di questo tipo di personalizzazione, in questo tutorial di Lucene indicizzeremo il corpus di Project Gutenberg, che offre migliaia di e-book gratuiti. Sappiamo che molti di questi libri sono romanzi. Supponiamo di essere particolarmente interessati al dialogo all'interno di questi romanzi. Né Lucene, Elasticsearch né Solr forniscono strumenti pronti all'uso per identificare i contenuti come dialoghi. In effetti, elimineranno la punteggiatura nelle prime fasi dell'analisi del testo, il che è contrario alla capacità di identificare parti del testo che sono dialoghi. Quindi è in queste prime fasi che deve iniziare la nostra personalizzazione.

Pezzi della pipeline di analisi Apache Lucene

L'analisi Lucene JavaDoc fornisce una buona panoramica di tutte le parti mobili nella pipeline di analisi del testo.

Ad un livello elevato, puoi pensare alla pipeline di analisi come a consumare un flusso grezzo di caratteri all'inizio e produrre "termini", approssimativamente corrispondenti a parole, alla fine.

La pipeline di analisi standard può essere visualizzata come tale:

Gasdotto di analisi Lucene

Vedremo come personalizzare questa pipeline per riconoscere le regioni di testo contrassegnate da virgolette, che chiamerò dialogo, e quindi aumentare le corrispondenze che si verificano durante la ricerca in quelle regioni.

Leggere i personaggi

Quando i documenti vengono inizialmente aggiunti all'indice, i caratteri vengono letti da un InputStream Java, quindi possono provenire da file, database, chiamate di servizi Web, ecc. Per creare un indice per Project Gutenberg, scarichiamo gli e-book e creare una piccola applicazione per leggere questi file e scriverli nell'indice. La creazione di un indice Lucene e la lettura di file sono percorsi ben percorsi, quindi non li esploreremo molto. Il codice essenziale per produrre un indice è:

 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);

Possiamo vedere che ogni e-book corrisponderà a un singolo Document Lucene, quindi, in seguito, i nostri risultati di ricerca saranno un elenco di libri corrispondenti. Store.YES indica che memorizziamo il campo del titolo , che è solo il nome del file. Tuttavia, non vogliamo archiviare il corpo dell'ebook, poiché non è necessario durante la ricerca e sprecherebbe solo spazio su disco.

La lettura effettiva del flusso inizia con addDocument . IndexWriter estrae i token dalla fine della pipeline. Questo pull procede indietro attraverso la pipe fino a quando la prima fase, Tokenizer , legge da InputStream .

Tieni inoltre presente che non chiudiamo il flusso, poiché Lucene lo gestisce per noi.

Personaggi di tokenizzazione

Il Lucene StandardTokenizer elimina la punteggiatura, quindi la nostra personalizzazione inizierà qui, poiché dobbiamo preservare le virgolette.

La documentazione per StandardTokenizer ti invita a copiare il codice sorgente e ad adattarlo alle tue esigenze, ma questa soluzione sarebbe inutilmente complessa. Estenderemo CharTokenizer , che permette di specificare i caratteri da “accettare”, dove quelli che non vengono “accettati” verranno trattati come delimitatori tra i token e gettati via. Dal momento che siamo interessati alle parole e alle citazioni che le circondano, il nostro Tokenizer personalizzato è semplicemente:

 public class QuotationTokenizer extends CharTokenizer { @Override protected boolean isTokenChar(int c) { return Character.isLetter(c) || c == '"'; } }

Dato un flusso di input di [He said, "Good day".] , i token prodotti sarebbero [He] , [said] , ["Good] , [day"]

Nota come le virgolette sono intervallate all'interno dei token. È possibile scrivere un Tokenizer che produca token separati per ogni citazione, ma Tokenizer si occupa anche di dettagli complicati e facili da rovinare come il buffering e la scansione, quindi è meglio mantenere il tuo Tokenizer semplice e ripulire il flusso di token più avanti nella pipeline.

Dividere i token usando i filtri

Dopo il tokenizer arriva una serie di oggetti TokenFilter . Nota, per inciso, quel filtro è un nome un po' improprio, poiché un TokenFilter può aggiungere, rimuovere o modificare i token.

Molte delle classi di filtri fornite da Lucene prevedono parole singole, quindi non è possibile che i nostri token misti di parole e virgolette fluiscano in esse. Pertanto, la prossima personalizzazione del nostro tutorial di Lucene deve essere l'introduzione di un filtro che ripulirà l'output di QuotationTokenizer .

Questa pulizia comporterà la produzione di un token di virgoletta iniziale extra se la citazione appare all'inizio di una parola, o un token di virgoletta finale se la citazione appare alla fine. Metteremo da parte la gestione delle singole parole citate per semplicità.

La creazione di una sottoclasse TokenFilter implica l'implementazione di un metodo: incrementToken . Questo metodo deve chiamare incrementToken sul filtro precedente nella pipe e quindi manipolare i risultati di tale chiamata per eseguire il lavoro di cui è responsabile il filtro. I risultati di incrementToken sono disponibili tramite gli oggetti Attribute , che descrivono lo stato corrente dell'elaborazione del token. Dopo la nostra implementazione di incrementToken restituisce, ci si aspetta che gli attributi siano stati manipolati per impostare il token per il filtro successivo (o l'indice se siamo alla fine della pipe).

Gli attributi che ci interessano a questo punto della pipeline sono:

  • CharTermAttribute : contiene un buffer char[] contenente i caratteri del token corrente. Dovremo manipolarlo per rimuovere la quotazione o per produrre un token di quotazione.

  • TypeAttribute : contiene il "tipo" del token corrente. Poiché stiamo aggiungendo virgolette di inizio e fine al flusso di token, introdurremo due nuovi tipi utilizzando il nostro filtro.

  • OffsetAttribute : Lucene può facoltativamente memorizzare riferimenti alla posizione dei termini nel documento originale. Questi riferimenti sono chiamati "offset", che sono solo indici di inizio e fine nel flusso di caratteri originale. Se cambiamo il buffer in CharTermAttribute in modo che punti solo a una sottostringa del token, dobbiamo regolare questi offset di conseguenza.

Ti starai chiedendo perché l'API per la manipolazione dei flussi di token è così contorta e, in particolare, perché non possiamo semplicemente fare qualcosa come String#split sui token in entrata. Questo perché Lucene è progettato per l'indicizzazione ad alta velocità e a basso sovraccarico, in base alla quale i tokenizer e i filtri integrati possono masticare rapidamente gigabyte di testo utilizzando solo megabyte di memoria. Per ottenere ciò, durante la tokenizzazione e il filtraggio vengono eseguite poche o nessuna allocazione, quindi le istanze di Attribute sopra menzionate devono essere allocate una volta e riutilizzate. Se i tuoi tokenizzatori e filtri sono scritti in questo modo e riduci al minimo le proprie allocazioni, puoi personalizzare Lucene senza compromettere le prestazioni.

Con tutto ciò in mente, vediamo come implementare un filtro che accetta un token come ["Hello] e produce i due token, ["] e [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);

Iniziamo ottenendo riferimenti ad alcuni degli attributi che abbiamo visto in precedenza. Diamo un suffisso ai nomi dei campi con "Attr", quindi sarà chiaro in seguito quando ci riferiremo ad essi. È possibile che alcune implementazioni di Tokenizer non forniscano questi attributi, quindi utilizziamo addAttribute per ottenere i nostri riferimenti. addAttribute creerà un'istanza di attributo se manca, altrimenti prenderà un riferimento condiviso all'attributo di quel tipo. Si noti che Lucene non consente più istanze dello stesso tipo di attributo contemporaneamente.

 private boolean emitExtraToken; private int extraTokenStartOffset, extraTokenEndOffset; private String extraTokenType;

Poiché il nostro filtro introdurrà un nuovo token che non era presente nel flusso originale, abbiamo bisogno di una posizione in cui salvare lo stato di quel token tra le chiamate a incrementToken . Poiché stiamo dividendo in due un token esistente, è sufficiente conoscere solo gli offset e il tipo del nuovo token. Abbiamo anche un flag che ci dice se la prossima chiamata a incrementToken emetterà questo token aggiuntivo. Lucene in realtà fornisce un paio di metodi, captureState e restoreState , che lo faranno per te. Ma questi metodi implicano l'allocazione di un oggetto State e possono effettivamente essere più complicati della semplice gestione di quello stato da soli, quindi eviteremo di usarli.

 @Override public void reset() throws IOException { emitExtraToken = false; extraTokenStartOffset = -1; extraTokenEndOffset = -1; extraTokenType = null; super.reset(); }

Come parte della sua aggressiva elusione dell'allocazione, Lucene può riutilizzare le istanze dei filtri. In questa situazione, si prevede che una chiamata per il reset riporti il ​​filtro allo stato iniziale. Quindi qui, ripristiniamo semplicemente i nostri campi token extra.

 @Override public boolean incrementToken() throws IOException { if (emitExtraToken) { advanceToExtraToken(); emitExtraToken = false; return true; } ...

Ora arriviamo ai pezzi interessanti. Quando viene chiamata la nostra implementazione di incrementToken , abbiamo l'opportunità di non chiamare incrementToken nella fase precedente della pipeline. In questo modo, introduciamo effettivamente un nuovo token, perché non stiamo estraendo un token dal Tokenizer .

Invece, chiamiamo advanceToExtraToken per impostare gli attributi per il nostro token aggiuntivo, impostiamo emitExtraToken su false per evitare questo ramo alla chiamata successiva, quindi restituiamo true , che indica che è disponibile un altro token.

 @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; }

Il resto di incrementToken farà una delle tre cose diverse. Ricordiamo che termBufferAttr è usato per ispezionare il contenuto del token che arriva attraverso la pipe:

  1. Se abbiamo raggiunto la fine del flusso di token (cioè hasNext è false), abbiamo finito e torniamo semplicemente.

  2. Se abbiamo un token di più di un carattere e uno di quei caratteri è una virgoletta, dividiamo il token.

  3. Se il token è una virgoletta solitaria, assumiamo che sia una virgoletta finale. Per capire perché, nota che le virgolette iniziali appaiono sempre a sinistra di una parola (cioè senza punteggiatura intermedia), mentre le virgolette finali possono seguire la punteggiatura (come nella frase, [He told us to "go back the way we came."] ). In questi casi, la virgoletta finale sarà già un token separato, quindi dobbiamo solo impostarne il tipo.

splitTermQuoteFirst e splitTermWordFirst imposteranno gli attributi per rendere il token corrente una parola o una citazione e imposteranno i campi "extra" per consentire all'altra metà di essere consumata in seguito. I due metodi sono simili, quindi esamineremo solo 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); }

Poiché vogliamo dividere questo token con la virgoletta che appare per prima nello stream, tronchiamo il buffer impostando la lunghezza su uno (cioè, un carattere; vale a dire, la virgoletta). Regoliamo gli offset di conseguenza (cioè puntando alla citazione nel documento originale) e impostiamo anche il tipo in modo che sia una citazione iniziale.

prepareExtraTerm imposterà i campi extra* e imposterà emitExtraToken su true. Viene chiamato con offset che puntano al token "extra" (cioè la parola che segue la virgoletta).

L'intero QuotationTokenFilter è disponibile su GitHub.

Per inciso, mentre questo filtro produce solo un token extra, questo approccio può essere esteso per introdurre un numero arbitrario di token extra. Basta sostituire i campi extra* con una collezione o, meglio ancora, un array di lunghezza fissa se c'è un limite al numero di token extra che possono essere prodotti. Vedi SynonymFilter e la sua classe interna PendingInput per un esempio di questo.

Consumo di gettoni citazione e dialogo di marcatura

Ora che abbiamo fatto tutto questo sforzo per aggiungere quelle citazioni al flusso di token, possiamo usarle per delimitare sezioni di dialogo nel testo.

Poiché il nostro obiettivo finale è modificare i risultati della ricerca in base al fatto che i termini facciano parte del dialogo o meno, è necessario allegare metadati a tali termini. Lucene fornisce PayloadAttribute per questo scopo. I payload sono array di byte archiviati insieme ai termini nell'indice e possono essere letti in seguito durante una ricerca. Ciò significa che il nostro flag occuperà inutilmente un intero byte, quindi è possibile implementare payload aggiuntivi come flag di bit per risparmiare spazio.

Di seguito è riportato un nuovo filtro, DialoguePayloadTokenFilter , che viene aggiunto alla fine della pipeline di analisi. Allega il payload indicando se il token fa o meno parte del dialogo.

 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; } }

Dal momento che questo filtro ha solo bisogno di mantenere un singolo pezzo di stato, all'interno di withinDialogue , è molto più semplice. Una citazione iniziale indica che ora siamo all'interno di una sezione del dialogo, mentre una citazione finale indica che la sezione del dialogo è terminata. In entrambi i casi, il token di quotazione viene eliminato effettuando una seconda chiamata a incrementToken , quindi in effetti, i token di inizio quotazione o fine quota non scorrono mai oltre questa fase della pipeline.

Ad esempio, DialoguePayloadTokenFilter trasformerà il flusso di token:

 [the], [program], [printed], ["], [hello], [world], ["]`

in questo nuovo flusso:

 [the][0], [program][0], [printed][0], [hello][1], [world][1]

Collegamento di token e filtri insieme

Un Analyzer è responsabile dell'assemblaggio della pipeline di analisi, in genere combinando un Tokenizer con una serie di TokenFilter . Analyzer possono anche definire come quella pipeline viene riutilizzata tra le analisi. Non dobbiamo preoccuparcene poiché i nostri componenti non richiedono nient'altro che una chiamata a reset() tra gli usi, cosa che Lucene farà sempre. Abbiamo solo bisogno di fare l'assemblaggio implementando 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); } }

Come abbiamo visto in precedenza, i filtri contengono un riferimento alla fase precedente nella pipeline, quindi è così che li istanziamo. Inseriamo anche alcuni filtri da StandardAnalyzer : LowerCaseFilter e StopFilter . Questi due devono venire dopo QuotationTokenFilter per garantire che tutte le virgolette siano state separate. Possiamo essere più flessibili nel posizionamento di DialoguePayloadTokenFilter , poiché ovunque dopo QuotationTokenFilter andrà bene. Lo inseriamo dopo StopFilter per evitare di perdere tempo a iniettare il payload del dialogo in parole di arresto che alla fine verranno rimosse.

Ecco una visualizzazione della nostra nuova pipeline in azione (meno le parti della pipeline standard che abbiamo rimosso o già visto):

Nuova visualizzazione della pipeline in apache lucene

DialogueAnalyzer ora può essere utilizzato come qualsiasi altro Analyzer di azioni e ora possiamo creare l'indice e passare alla ricerca.

Ricerca full-text del dialogo

Se volessimo cercare solo i dialoghi, avremmo potuto semplicemente scartare tutti i token al di fuori di una citazione e avremmo finito. Invece, lasciando intatti tutti i token originali, ci siamo dati la flessibilità di eseguire query che tengano conto del dialogo o di trattare il dialogo come qualsiasi altra parte del testo.

Le basi per interrogare un indice Lucene sono ben documentate. Per i nostri scopi, è sufficiente sapere che le query sono composte da oggetti Term incollati insieme a operatori come MUST o SHOULD , insieme a documenti di corrispondenza basati su tali termini. I documenti corrispondenti vengono quindi valutati in base a un oggetto Similarity configurabile e tali risultati possono essere ordinati per punteggio, filtrati o limitati. Ad esempio, Lucene ci consente di eseguire una query per i primi dieci documenti che devono contenere entrambi i termini [hello] e [world] .

La personalizzazione dei risultati di ricerca in base al dialogo può essere eseguita regolando il punteggio di un documento in base al carico utile. Il primo punto di estensione per questo sarà in Similarity , che è responsabile della pesatura e del punteggio dei termini di corrispondenza.

Somiglianza e punteggio

Le query utilizzeranno, per impostazione predefinita, DefaultSimilarity , che pondera i termini in base alla frequenza con cui si verificano in un documento. È un buon punto di estensione per regolare i pesi, quindi lo estendiamo anche per valutare i documenti in base al carico utile. Il metodo DefaultSimilarity#scorePayload viene fornito a questo scopo:

 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 valuta semplicemente come zero i payload non di dialogo. Poiché ogni Term può essere abbinato più volte, avrà potenzialmente più punteggi di carico utile. L'interpretazione di questi punteggi fino all'implementazione della Query .

Presta molta attenzione al BytesRef contenente il payload: dobbiamo controllare il byte a offset , poiché non possiamo presumere che l'array di byte sia lo stesso payload che abbiamo memorizzato in precedenza. Durante la lettura dell'indice, Lucene non sprecherà memoria allocando un array di byte separato solo per la chiamata a scorePayload , quindi otteniamo un riferimento in un array di byte esistente. Quando si codifica contro l'API Lucene, vale la pena tenere presente che le prestazioni sono la priorità, ben prima della comodità degli sviluppatori.

Ora che abbiamo la nostra nuova implementazione di Similarity , deve quindi essere impostata su IndexSearcher utilizzato per eseguire le query:

 IndexSearcher searcher = new IndexSearcher(... reader for index ...); searcher.setSimilarity(new DialogueAwareSimilarity());

Domande e termini

Ora che il nostro IndexSearcher può assegnare un punteggio ai payload, dobbiamo anche costruire una query che tenga conto del payload. PayloadTermQuery può essere utilizzato per abbinare un singolo Term controllando anche i payload di tali corrispondenze:

 PayloadTermQuery helloQuery = new PayloadTermQuery(new Term("body", "hello"), new AveragePayloadFunction());

Questa query corrisponde al termine [hello] all'interno del campo del corpo (ricorda che è qui che mettiamo il contenuto del documento). Dobbiamo anche fornire una funzione per calcolare il punteggio del carico utile finale da tutte le corrispondenze dei termini, quindi AveragePayloadFunction , che calcola la media di tutti i punteggi del carico utile. Ad esempio, se il termine [hello] compare due volte all'interno del dialogo e all'esterno del dialogo una volta, il punteggio del carico utile finale sarà ²⁄₃. Questo punteggio finale del carico utile viene moltiplicato per quello fornito da DefaultSimilarity per l'intero documento.

Utilizziamo una media perché vorremmo ridurre l'enfasi sui risultati di ricerca in cui molti termini compaiono al di fuori del dialogo e produrre un punteggio pari a zero per i documenti senza alcun termine in dialogo.

Possiamo anche comporre diversi oggetti PayloadTermQuery usando una BooleanQuery se vogliamo cercare più termini contenuti nel dialogo (notare che l'ordine dei termini è irrilevante in questa query, sebbene altri tipi di query siano consapevoli della posizione):

 PayloadTermQuery worldQuery = new PayloadTermQuery(new Term("body", "world"), new AveragePayloadFunction()); BooleanQuery query = new BooleanQuery(); query.add(helloQuery, Occur.MUST); query.add(worldQuery, Occur.MUST);

Quando questa query viene eseguita, possiamo vedere come la struttura della query e l'implementazione della somiglianza funzionano insieme:

pipeline di analisi del dialogo Lucene

Esecuzione e spiegazione della query

Per eseguire la query, la passiamo a IndexSearcher :

 TopScoreDocCollector collector = TopScoreDocCollector.create(10); searcher.search(query, new PositiveScoresOnlyCollector(collector)); TopDocs topDocs = collector.topDocs();

Gli oggetti da Collector vengono utilizzati per preparare la raccolta di documenti corrispondenti.

i collettori possono essere composti per ottenere una combinazione di smistamento, limitazione e filtraggio. Per ottenere, ad esempio, i primi dieci documenti di punteggio che contengono almeno un termine nel dialogo, combiniamo TopScoreDocCollector e PositiveScoresOnlyCollector . Prendere solo punteggi positivi assicura che le corrispondenze con punteggio zero (cioè quelle senza termini nel dialogo) vengano filtrate.

Per vedere questa query in azione, possiamo eseguirla, quindi utilizzare IndexSearcher#explain per vedere come sono stati valutati i singoli documenti:

 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)); }

Qui, iteriamo sugli ID documento nei TopDocs ottenuti dalla ricerca. Usiamo anche IndexSearcher#doc per recuperare il campo del titolo per la visualizzazione. Per la nostra domanda di "hello" , questo risulta in:

 --- 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() ...

Sebbene l'output sia carico di gergo, possiamo vedere come la nostra implementazione di Similarity personalizzata è stata utilizzata nel punteggio e come MaxPayloadFunction prodotto un moltiplicatore di 1.0 per queste corrispondenze. Ciò implica che il carico utile è stato caricato e segnato e tutte le partite di "Hello" si sono verificate nel dialogo, quindi questi risultati sono proprio in alto dove li aspettavamo.

Vale anche la pena sottolineare che l'indice per Project Gutenberg, con i payload, raggiunge una dimensione di quasi quattro gigabyte, eppure sulla mia modesta macchina di sviluppo, le query si verificano istantaneamente. Non abbiamo sacrificato alcuna velocità per raggiungere i nostri obiettivi di ricerca.

Avvolgendo

Lucene è una potente libreria di ricerca full-text creata appositamente che prende un flusso grezzo di caratteri, li raggruppa in token e li mantiene come termini in un indice. Può interrogare rapidamente quell'indice e fornire risultati classificati e offre ampie opportunità di estensione pur mantenendo l'efficienza.

Utilizzando Lucene direttamente nelle nostre applicazioni o come parte di un server, possiamo eseguire ricerche di testo completo in tempo reale su gigabyte di contenuto. Inoltre, tramite analisi e punteggi personalizzati, possiamo sfruttare le funzionalità specifiche del dominio nei nostri documenti per migliorare la pertinenza dei risultati o delle query personalizzate.

Gli elenchi di codici completi per questo tutorial di Lucene sono disponibili su GitHub. Il repository contiene due applicazioni: LuceneIndexerApp per la creazione dell'indice e LuceneQueryApp per l'esecuzione di query.

Il corpus di Project Gutenberg, che può essere ottenuto come immagine disco tramite BitTorrent, contiene molti libri che vale la pena leggere (o con Lucene, o semplicemente alla vecchia maniera).

Buona indicizzazione!