Căutare text integral a dialogurilor cu Apache Lucene: un tutorial

Publicat: 2022-03-11

Apache Lucene este o bibliotecă Java folosită pentru căutarea textului integral al documentelor și se află în centrul serverelor de căutare precum Solr și Elasticsearch. Poate fi, de asemenea, încorporat în aplicații Java, cum ar fi aplicațiile Android sau backend-urile web.

Deși opțiunile de configurare ale Lucene sunt extinse, ele sunt destinate utilizării de către dezvoltatorii de baze de date pe un corpus generic de text. Dacă documentele dvs. au o structură specifică sau un tip de conținut, puteți profita de oricare dintre ele pentru a îmbunătăți calitatea căutării și capacitatea de interogare.

căutare text integral cu apache lucene

Ca exemplu al acestui tip de personalizare, în acest tutorial Lucene vom indexa corpus Proiectului Gutenberg, care oferă mii de cărți electronice gratuite. Știm că multe dintre aceste cărți sunt romane. Să presupunem că suntem interesați în mod special de dialogul din aceste romane. Nici Lucene, nici Elasticsearch, nici Solr nu furnizează instrumente ieșite din cutie pentru a identifica conținutul ca dialog. De fapt, ei vor arunca semnele de punctuație în primele etape ale analizei textului, ceea ce contravine a fi capabil să identifice părți ale textului care sunt dialog. Prin urmare, personalizarea noastră trebuie să înceapă în aceste etape incipiente.

Piese din conducta de analiză Apache Lucene

Analiza Lucene JavaDoc oferă o bună imagine de ansamblu asupra tuturor părților mobile din conducta de analiză a textului.

La un nivel înalt, vă puteți gândi la pipeline de analiză ca consumând un flux brut de caractere la început și producând „termeni”, corespunzători aproximativ cuvintelor, la sfârșit.

Conducta de analiză standard poate fi vizualizată astfel:

Conducta de analiză Lucene

Vom vedea cum să personalizăm această conductă pentru a recunoaște regiunile de text marcate prin ghilimele duble, pe care le voi numi dialog, și apoi vom crește potrivirile care apar atunci când căutăm în acele regiuni.

Citirea Personajelor

Când documentele sunt adăugate inițial la index, caracterele sunt citite dintr-un Java InputStream și astfel pot proveni din fișiere, baze de date, apeluri de servicii web etc. Pentru a crea un index pentru Proiectul Gutenberg, descarcăm cărțile electronice și creați o mică aplicație pentru a citi aceste fișiere și a le scrie în index. Crearea unui index Lucene și citirea fișierelor sunt căi bine parcurse, așa că nu le vom explora prea mult. Codul esențial pentru producerea unui index este:

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

Putem vedea că fiecare carte electronică va corespunde unui singur Document Lucene, astfel încât, mai târziu, rezultatele căutării noastre vor fi o listă de cărți care se potrivesc. Store.YES indică faptul că stocăm câmpul titlu , care este doar numele fișierului. Cu toate acestea, nu dorim să stocăm corpul cărții electronice, deoarece nu este necesar la căutare și ar pierde doar spațiu pe disc.

Citirea efectivă a fluxului începe cu addDocument . IndexWriter trage jetoane de la capătul conductei. Această tragere continuă înapoi prin conductă până când prima etapă, Tokenizer , citește din InputStream .

De asemenea, rețineți că nu închidem fluxul, deoarece Lucene se ocupă de asta pentru noi.

Tokenizarea personajelor

Lucene StandardTokenizer aruncă semnele de punctuație și, prin urmare, personalizarea noastră va începe aici, deoarece trebuie să păstrăm ghilimele.

Documentația pentru StandardTokenizer vă invită să copiați codul sursă și să îl adaptați nevoilor dvs., dar această soluție ar fi inutil de complexă. În schimb, vom extinde CharTokenizer , care vă permite să specificați caractere de „acceptat”, unde cele care nu sunt „acceptate” vor fi tratate ca delimitatori între jetoane și aruncate. Deoarece ne interesează cuvintele și citatele din jurul lor, Tokenizerul nostru personalizat este pur și simplu:

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

Având în vedere un flux de intrare de [He said, "Good day".] , jetoanele produse ar fi [He] , [said] , ["Good] , [day"]

Observați modul în care ghilimele sunt intercalate în jetoane. Este posibil să scrieți un Tokenizer care să producă jetoane separate pentru fiecare cotație, dar Tokenizer este, de asemenea, preocupat de detalii grele, ușor de înșurubat, cum ar fi tamponarea și scanarea, așa că cel mai bine este să păstrați Tokenizer simplu și să curățați flux de simboluri mai departe în conductă.

Împărțirea jetoanelor folosind filtre

După tokenizer vine o serie de obiecte TokenFilter . Rețineți, de altfel, acel filtru este un nume puțin greșit, deoarece un TokenFilter poate adăuga, elimina sau modifica jetoane.

Multe dintre clasele de filtrare oferite de Lucene se așteaptă la cuvinte unice, așa că nu va fi util ca simbolurile noastre mixte de cuvinte și ghilimele să curgă în ele. Astfel, următoarea personalizare a tutorialului nostru Lucene trebuie să fie introducerea unui filtru care va curăța rezultatul QuotationTokenizer .

Această curățare va implica producerea unui simbol suplimentar de citat de început dacă citatul apare la începutul unui cuvânt sau a unui simbol de citat de final dacă citatul apare la sfârșit. Vom lăsa deoparte manipularea cuvintelor ghilimele simple pentru simplitate.

Crearea unei subclase TokenFilter implică implementarea unei metode: incrementToken . Această metodă trebuie să apeleze incrementToken pe filtrul anterior din conductă și apoi să manipuleze rezultatele apelului respectiv pentru a efectua orice lucru de care este responsabil filtrul. Rezultatele incrementToken sunt disponibile prin intermediul obiectelor Attribute , care descriu starea curentă a procesării token-ului. După ce implementarea incrementToken revine, este de așteptat ca atributele să fi fost manipulate pentru a configura tokenul pentru următorul filtru (sau indexul dacă ne aflăm la capătul conductei).

Atributele care ne interesează în acest moment sunt:

  • CharTermAttribute : Conține un buffer char[] care conține caracterele jetonului curent. Va trebui să manipulăm acest lucru pentru a elimina citatul sau pentru a produce un simbol de citat.

  • TypeAttribute : Conține „tipul” simbolului curent. Deoarece adăugăm ghilimele de început și de sfârșit în fluxul de simboluri, vom introduce două tipuri noi folosind filtrul nostru.

  • OffsetAttribute : Lucene poate stoca opțional referințe la locația termenilor din documentul original. Aceste referințe sunt numite „compensații”, care sunt doar indici de început și de sfârșit în fluxul de caractere original. Dacă schimbăm tamponul din CharTermAttribute pentru a indica doar un subșir al jetonului, trebuie să ajustam aceste decalaje în consecință.

S-ar putea să vă întrebați de ce API-ul pentru manipularea fluxurilor de jetoane este atât de complicat și, în special, de ce nu putem face ceva de genul String#split pe jetoanele primite. Acest lucru se datorează faptului că Lucene este proiectat pentru indexare de mare viteză, cu o supraîncărcare redusă, prin care tokenizatoarele și filtrele încorporate pot rula rapid gigaocteți de text folosind doar megaocteți de memorie. Pentru a realiza acest lucru, în timpul tokenizării și filtrării se efectuează puține alocări sau deloc, astfel încât instanțele de Attribute menționate mai sus sunt destinate a fi alocate o singură dată și reutilizate. Dacă tokenizatoarele și filtrele dvs. sunt scrise în acest fel și își minimizează propriile alocări, puteți personaliza Lucene fără a compromite performanța.

Având toate acestea în minte, să vedem cum să implementăm un filtru care ia un token, cum ar fi ["Hello] , și produce cele două jetoane, ["] și [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);

Începem prin a obține referințe la unele dintre atributele pe care le-am văzut mai devreme. Sufixăm numele câmpurilor cu „Attr”, astfel încât să fie clar mai târziu când ne vom referi la ele. Este posibil ca unele implementări Tokenizer să nu ofere aceste atribute, așa că folosim addAttribute pentru a obține referințele noastre. addAttribute va crea o instanță de atribut dacă lipsește, în caz contrar, luați o referință partajată la atributul acelui tip. Rețineți că Lucene nu permite mai multe instanțe ale aceluiași tip de atribut simultan.

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

Deoarece filtrul nostru va introduce un nou token care nu a fost prezent în fluxul original, avem nevoie de un loc pentru a salva starea acelui token între apelurile la incrementToken . Deoarece împărțim un jeton existent în două, este suficient să cunoaștem doar compensațiile și tipul noului jeton. Avem, de asemenea, un steag care ne spune dacă următorul apel la incrementToken va emite acest token suplimentar. Lucene oferă de fapt o pereche de metode, captureState și restoreState , care vor face acest lucru pentru tine. Dar aceste metode implică alocarea unui obiect State și pot fi, de fapt, mai complicate decât simpla gestionare a acelei stări, așa că vom evita să le folosim.

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

Ca parte a evitării agresive a alocării, Lucene poate reutiliza instanțe de filtrare. În această situație, este de așteptat ca un apel de reset să pună filtrul înapoi în starea sa inițială. Așa că aici, pur și simplu resetam câmpurile suplimentare de token.

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

Acum ajungem la părțile interesante. Când implementarea noastră a incrementToken este apelată, avem posibilitatea de a nu apela incrementToken în etapa anterioară a conductei. Făcând acest lucru, introducem efectiv un nou jeton, deoarece nu extragem un jeton din Tokenizer .

În schimb, apelăm advanceToExtraToken pentru a configura atributele pentru jetonul nostru suplimentar, setăm emitExtraToken la false pentru a evita această ramură la apelul următor și apoi returnăm true , ceea ce indică faptul că un alt token este disponibil.

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

Restul incrementToken va face unul dintre cele trei lucruri diferite. Amintiți-vă că termBufferAttr este folosit pentru a inspecta conținutul jetonului care trece prin conductă:

  1. Dacă am ajuns la sfârșitul fluxului de simboluri (adică hasNext este fals), am terminat și pur și simplu ne întoarcem.

  2. Dacă avem un simbol de mai mult de un caracter și unul dintre acele caractere este un citat, împărțim simbolul.

  3. Dacă simbolul este un citat solitar, presupunem că este un citat final. Pentru a înțelege de ce, rețineți că ghilimelele de început apar întotdeauna în stânga unui cuvânt (adică, fără semne de punctuație intermediare), în timp ce ghilimele de sfârșit pot urma semnelor de punctuație (cum ar fi în propoziție, [He told us to "go back the way we came."] ). În aceste cazuri, citatul final va fi deja un simbol separat și, prin urmare, trebuie doar să îi setăm tipul.

splitTermQuoteFirst și splitTermWordFirst vor seta atribute pentru a face din simbolul curent fie un cuvânt, fie un citat și vor configura câmpurile „extra” pentru a permite cealaltă jumătate să fie consumată mai târziu. Cele două metode sunt similare, așa că ne vom uita doar la 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); }

Deoarece dorim să împărțim acest token cu citatul care apare mai întâi în flux, trunchiem tamponul setând lungimea la unul (adică, un caracter; și anume, citatul). Ajustăm offset-urile în consecință (adică indicând citatul din documentul original) și, de asemenea, setăm tipul să fie un citat de pornire.

prepareExtraTerm va seta câmpurile extra* și va seta emitExtraToken la true. Este apelat cu decalaje îndreptate spre simbolul „extra” (adică, cuvântul care urmează ghilimelei).

Întregul QuotationTokenFilter este disponibil pe GitHub.

În afară de aceasta, în timp ce acest filtru produce doar un token suplimentar, această abordare poate fi extinsă pentru a introduce un număr arbitrar de jetoane suplimentare. Înlocuiți câmpurile extra* cu o colecție sau, mai bine, cu o matrice cu lungime fixă, dacă există o limită a numărului de jetoane suplimentare care pot fi produse. Consultați SynonymFilter și clasa sa internă PendingInput pentru un exemplu în acest sens.

Consumul de jetoane de cotație și dialogul de marcare

Acum, că am făcut tot acest efort pentru a adăuga acele citate în fluxul de simboluri, le putem folosi pentru a delimita secțiuni de dialog în text.

Întrucât scopul nostru final este de a ajusta rezultatele căutării în funcție de faptul dacă termenii fac sau nu parte din dialog, trebuie să atașăm metadate la acești termeni. Lucene oferă PayloadAttribute în acest scop. Sarcinile utile sunt matrice de octeți care sunt stocate alături de termeni din index și pot fi citite ulterior în timpul unei căutări. Aceasta înseamnă că flag-ul nostru va ocupa în mod risipitor un octet întreg, astfel încât încărcături suplimentare ar putea fi implementate ca indicatori de biți pentru a economisi spațiu.

Mai jos este un nou filtru, DialoguePayloadTokenFilter , care este adăugat chiar la sfârșitul conductei de analiză. Acesta atașează sarcina utilă indicând dacă jetonul face sau nu parte din dialog.

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

Deoarece acest filtru trebuie să mențină doar o singură bucată de stare, withinDialogue , este mult mai simplu. Un citat de început indică faptul că ne aflăm acum într-o secțiune de dialog, în timp ce un citat de final indică faptul că secțiunea de dialog s-a încheiat. În ambele cazuri, indicativul de cotație este eliminat prin efectuarea unui al doilea apel la incrementToken , astfel încât, de fapt, indicativele de cotare de început sau de sfârșit nu trec niciodată din această etapă în conductă.

De exemplu, DialoguePayloadTokenFilter va transforma fluxul de simboluri:

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

în acest nou flux:

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

Legarea jetoanelor și a filtrelor împreună

Un Analyzer este responsabil pentru asamblarea conductei de analiză, de obicei prin combinarea unui Tokenizer cu o serie de TokenFilter . Analyzer pot defini, de asemenea, modul în care acea conductă este reutilizată între analize. Nu trebuie să ne facem griji pentru asta, deoarece componentele noastre nu necesită nimic, cu excepția unui apel la reset() între utilizări, ceea ce Lucene va face întotdeauna. Trebuie doar să facem asamblarea prin implementarea 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); } }

După cum am văzut mai devreme, filtrele conțin o referință la etapa anterioară în conductă, așa că le instanțiem. De asemenea, introducem câteva filtre de la StandardAnalyzer : LowerCaseFilter și StopFilter . Aceste două trebuie să vină după QuotationTokenFilter pentru a se asigura că orice ghilimele au fost separate. Putem fi mai flexibili în plasarea DialoguePayloadTokenFilter , deoarece oriunde după QuotationTokenFilter va funcționa. L-am pus după StopFilter pentru a evita pierderea timpului injectând sarcina utilă de dialog în cuvintele stop care vor fi în cele din urmă eliminate.

Iată o vizualizare a noii noastre conducte în acțiune (minus acele părți ale conductei standard pe care le-am eliminat sau le-am văzut deja):

Noua vizualizare a conductei în apache lucene

DialogueAnalyzer poate fi folosit acum ca orice alt Analyzer de stoc, iar acum putem construi indexul și trece la căutare.

Căutare text integral al dialogului

Dacă am fi vrut să căutăm doar dialogul, am fi putut pur și simplu să aruncăm toate jetoanele în afara unui citat și am fi terminat. În schimb, lăsând intacte toate jetoanele originale, ne-am oferit flexibilitatea fie de a efectua interogări care iau în considerare dialogul, fie de a trata dialogul ca orice altă parte a textului.

Elementele de bază ale interogării unui index Lucene sunt bine documentate. Pentru scopurile noastre, este suficient să știm că interogările sunt compuse din obiecte Term lipite împreună cu operatori precum MUST sau SHOULD , împreună cu documente de potrivire bazate pe acești termeni. Documentele care se potrivesc sunt apoi punctate pe baza unui obiect de Similarity configurabil, iar acele rezultate pot fi ordonate după scor, filtrate sau limitate. De exemplu, Lucene ne permite să facem o interogare pentru primele zece documente care trebuie să conțină ambii termeni [hello] și [world] .

Personalizarea rezultatelor căutării pe baza dialogului se poate face prin ajustarea scorului unui document în funcție de sarcina utilă. Primul punct de extensie pentru aceasta va fi în Similarity , care este responsabil pentru cântărirea și notarea termenilor de potrivire.

Similaritate și punctaj

Interogările vor folosi, în mod implicit, DefaultSimilarity , care ponderează termenii în funcție de frecvența cu care apar într-un document. Este un bun punct de extensie pentru ajustarea greutăților, așa că îl extindem și pentru a nota documentele pe baza sarcinii utile. Metoda DefaultSimilarity#scorePayload este furnizată în acest scop:

 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 punctează pur și simplu încărcăturile utile non-dialog ca zero. Deoarece fiecare Term poate fi asociat de mai multe ori, va avea potențial mai multe scoruri de sarcină utilă. Interpretarea acestor scoruri până la implementarea Query .

Acordați o atenție deosebită BytesRef care conține sarcina utilă: trebuie să verificăm octetul de la offset , deoarece nu putem presupune că matricea de octeți este aceeași sarcină utilă pe care am stocat-o mai devreme. Când citiți indexul, Lucene nu va pierde memorie alocand o matrice de octeți separată doar pentru apelul la scorePayload , așa că obținem o referință într-o matrice de octeți existentă. Când se codifică în funcție de API-ul Lucene, merită să rețineți că performanța este prioritatea, cu mult înaintea confortului dezvoltatorului.

Acum că avem noua noastră implementare Similarity , aceasta trebuie apoi setată pe IndexSearcher folosit pentru a executa interogări:

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

Întrebări și termeni

Acum că IndexSearcher -ul nostru poate nota încărcături utile, trebuie de asemenea să construim o interogare care să țină cont de sarcina utilă. PayloadTermQuery poate fi folosit pentru a potrivi un singur Term , verificând, de asemenea, încărcăturile utile ale acelor potriviri:

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

Această interogare se potrivește cu termenul [hello] din câmpul body (reamintim că aici punem conținutul documentului). De asemenea, trebuie să furnizăm o funcție pentru a calcula scorul final al sarcinii utile din toate potrivirile pe termen, așa că AveragePayloadFunction , care face media tuturor scorurilor de sarcină utilă. De exemplu, dacă termenul [hello] apare de două ori în interiorul dialogului și o dată în afara dialogului, scorul final al sarcinii utile va fi ²⁄₃. Acest scor final al sarcinii utile este înmulțit cu cel oferit de DefaultSimilarity pentru întregul document.

Folosim o medie deoarece am dori să subliniem rezultatele căutării în care mulți termeni apar în afara dialogului și să producem un scor de zero pentru documentele fără niciun termen în dialog.

De asemenea, putem compune mai multe obiecte PayloadTermQuery folosind un BooleanQuery dacă dorim să căutăm mai mulți termeni conținuti în dialog (rețineți că ordinea termenilor este irelevantă în această interogare, deși alte tipuri de interogare sunt conștiente de poziție):

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

Când această interogare este executată, putem vedea cum funcționează împreună structura interogării și implementarea similarității:

conducta de analiză a dialogului lucene

Execuția interogării și explicația

Pentru a executa interogarea, o IndexSearcher :

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

Obiectele Collector sunt folosite pentru a pregăti colecția de documente potrivite.

colectorii pot fi alcătuiți pentru a realiza o combinație de sortare, limitare și filtrare. Pentru a obține, de exemplu, primele zece documente cu scoruri care conțin cel puțin un termen în dialog, combinăm TopScoreDocCollector și PositiveScoresOnlyCollector . Luarea numai a scorurilor pozitive asigură că potrivirile cu scorul zero (adică cele fără termeni în dialog) sunt filtrate.

Pentru a vedea această interogare în acțiune, o putem executa, apoi folosim IndexSearcher#explain pentru a vedea cum au fost punctate documentele individuale:

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

Aici, repetăm ​​ID-urile documentelor din TopDocs obținute prin căutare. De asemenea, folosim IndexSearcher#doc pentru a prelua câmpul de titlu pentru afișare. Pentru interogarea noastră "hello" , rezultă:

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

Deși rezultatul este încărcat cu jargon, putem vedea cum a fost utilizată implementarea noastră personalizată Similarity în punctare și cum MaxPayloadFunction a produs un multiplicator de 1.0 pentru aceste potriviri. Acest lucru implică faptul că sarcina utilă a fost încărcată și marcată, iar toate meciurile "Hello" au avut loc în dialog, astfel încât aceste rezultate sunt chiar în partea de sus, unde ne așteptăm.

De asemenea, merită subliniat că indexul pentru Proiectul Gutenberg, cu încărcături utile, are o dimensiune de aproape patru gigaocteți și, totuși, pe mașina mea modestă de dezvoltare, interogările apar instantaneu. Nu am sacrificat nicio viteză pentru a ne atinge obiectivele de căutare.

Încheierea

Lucene este o bibliotecă de căutare de text complet puternică, construită pentru scop, care preia un flux brut de caractere, le grupează în simboluri și le păstrează ca termeni într-un index. Poate interoga rapid acel index și poate oferi rezultate clasate și oferă oportunități ample de extindere, menținând în același timp eficiența.

Utilizând Lucene direct în aplicațiile noastre sau ca parte a unui server, putem efectua căutări de text complet în timp real pe gigaocteți de conținut. În plus, prin intermediul analizei și punctajului personalizat, putem profita de caracteristicile specifice domeniului din documentele noastre pentru a îmbunătăți relevanța rezultatelor sau a interogărilor personalizate.

Listele complete de coduri pentru acest tutorial Lucene sunt disponibile pe GitHub. Repo conține două aplicații: LuceneIndexerApp pentru construirea indexului și LuceneQueryApp pentru efectuarea de interogări.

Corpusul Proiectului Gutenberg, care poate fi obținut ca imagine de disc prin BitTorrent, conține o mulțime de cărți care merită citite (fie cu Lucene, fie doar la modă veche).

Indexare fericită!