Pencarian Teks Lengkap Dialog dengan Apache Lucene: Sebuah Tutorial

Diterbitkan: 2022-03-11

Apache Lucene adalah perpustakaan Java yang digunakan untuk pencarian teks lengkap dokumen, dan merupakan inti dari server pencarian seperti Solr dan Elasticsearch. Itu juga dapat disematkan ke dalam aplikasi Java, seperti aplikasi Android atau backend web.

Sementara opsi konfigurasi Lucene sangat luas, mereka dimaksudkan untuk digunakan oleh pengembang basis data pada kumpulan teks umum. Jika dokumen Anda memiliki struktur atau tipe konten tertentu, Anda dapat memanfaatkannya untuk meningkatkan kualitas pencarian dan kemampuan kueri.

pencarian teks lengkap dengan apache lucene

Sebagai contoh penyesuaian semacam ini, dalam tutorial Lucene ini kita akan mengindeks korpus Project Gutenberg, yang menawarkan ribuan e-book gratis. Kita tahu bahwa banyak dari buku-buku ini adalah novel. Misalkan kita secara khusus tertarik pada dialog dalam novel-novel ini. Baik Lucene, Elasticsearch, maupun Solr tidak menyediakan alat out-of-the-box untuk mengidentifikasi konten sebagai dialog. Bahkan, mereka akan membuang tanda baca pada tahap awal analisis teks, yang bertentangan dengan kemampuan mengidentifikasi bagian teks yang merupakan dialog. Oleh karena itu, pada tahap awal inilah penyesuaian kami harus dimulai.

Bagian dari Pipa Analisis Apache Lucene

Analisis Lucene JavaDoc memberikan gambaran yang baik tentang semua bagian yang bergerak dalam saluran analisis teks.

Pada tingkat tinggi, Anda dapat menganggap saluran analisis sebagai mengonsumsi aliran karakter mentah di awal dan menghasilkan "istilah", kira-kira sesuai dengan kata-kata, di akhir.

Pipa analisis standar dapat divisualisasikan sebagai berikut:

Pipa analisis Lucene

Kita akan melihat bagaimana menyesuaikan saluran ini untuk mengenali wilayah teks yang ditandai dengan tanda kutip ganda, yang akan saya sebut dialog, dan kemudian menemukan kecocokan yang terjadi saat mencari di wilayah tersebut.

Membaca Karakter

Saat dokumen awalnya ditambahkan ke indeks, karakter dibaca dari Java InputStream, sehingga bisa berasal dari file, database, panggilan layanan web, dll. Untuk membuat indeks untuk Project Gutenberg, kami mengunduh e-book, dan buat aplikasi kecil untuk membaca file-file ini dan menulisnya ke file index. Membuat indeks Lucene dan membaca file adalah jalur yang dilalui dengan baik, jadi kami tidak akan banyak menjelajahinya. Kode penting untuk menghasilkan indeks adalah:

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

Kita dapat melihat bahwa setiap e-book akan sesuai dengan satu Document Lucene sehingga, nanti, hasil pencarian kita akan menjadi daftar buku yang cocok. Store.YES menunjukkan bahwa kami menyimpan bidang judul , yang hanya merupakan nama file. Kami tidak ingin menyimpan isi ebook, bagaimanapun, karena tidak diperlukan saat mencari dan hanya akan membuang-buang ruang disk.

Pembacaan aliran yang sebenarnya dimulai dengan addDocument . IndexWriter menarik token dari ujung pipa. Tarikan ini kembali melalui pipa sampai tahap pertama, Tokenizer , membaca dari InputStream .

Perhatikan juga bahwa kami tidak menutup aliran, karena Lucene menangani ini untuk kami.

Karakter Tokenisasi

Lucene StandardTokenizer membuang tanda baca, dan penyesuaian kami akan dimulai di sini, karena kami perlu mempertahankan tanda kutip.

Dokumentasi untuk StandardTokenizer mengundang Anda untuk menyalin kode sumber dan menyesuaikannya dengan kebutuhan Anda, tetapi solusi ini tidak perlu rumit. Sebagai gantinya, kami akan memperluas CharTokenizer , yang memungkinkan Anda menentukan karakter untuk "diterima", di mana karakter yang tidak "diterima" akan diperlakukan sebagai pembatas antara token dan dibuang. Karena kami tertarik dengan kata-kata dan kutipan di sekitarnya, Tokenizer khusus kami hanyalah:

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

Diberikan aliran input [He said, "Good day".] , token yang dihasilkan adalah [He] , [said] , ["Good] , [day"]

Perhatikan bagaimana tanda kutip diselingi dalam token. Dimungkinkan untuk menulis Tokenizer yang menghasilkan token terpisah untuk setiap kutipan, tetapi Tokenizer juga memperhatikan detail yang rumit dan mudah dikacaukan seperti buffering dan pemindaian, jadi yang terbaik adalah menjaga Tokenizer Anda tetap sederhana dan membersihkan aliran token lebih jauh di dalam pipa.

Memisahkan Token Menggunakan Filter

Setelah tokenizer muncul serangkaian objek TokenFilter . Perhatikan, kebetulan, filter itu sedikit keliru, karena TokenFilter dapat menambahkan, menghapus, atau memodifikasi token.

Banyak dari kelas filter yang disediakan oleh Lucene mengharapkan kata-kata tunggal, jadi tidak perlu jika token kata-dan-kutipan campuran kami mengalir ke dalamnya. Jadi, penyesuaian tutorial Lucene kami selanjutnya adalah pengenalan filter yang akan membersihkan output QuotationTokenizer .

Pembersihan ini akan melibatkan produksi token kutipan awal tambahan jika kutipan muncul di awal kata, atau token kutipan akhir jika kutipan muncul di akhir. Kami akan mengesampingkan penanganan kata-kata yang dikutip tunggal untuk kesederhanaan.

Membuat subkelas TokenFilter melibatkan penerapan satu metode: incrementToken . Metode ini harus memanggil incrementToken pada filter sebelumnya dalam pipa, dan kemudian memanipulasi hasil panggilan tersebut untuk melakukan pekerjaan apa pun yang menjadi tanggung jawab filter. Hasil dari incrementToken tersedia melalui objek Attribute , yang menjelaskan status pemrosesan token saat ini. Setelah implementasi pengembalian incrementToken kami, diharapkan atribut telah dimanipulasi untuk menyiapkan token untuk filter berikutnya (atau indeks jika kami berada di ujung pipa).

Atribut yang kami minati pada titik ini dalam pipeline adalah:

  • CharTermAttribute : Berisi buffer char[] yang menyimpan karakter token saat ini. Kita perlu memanipulasi ini untuk menghapus kutipan, atau untuk menghasilkan token kutipan.

  • TypeAttribute : Berisi "jenis" dari token saat ini. Karena kami menambahkan tanda kutip awal dan akhir ke aliran token, kami akan memperkenalkan dua jenis baru menggunakan filter kami.

  • OffsetAttribute : Lucene secara opsional dapat menyimpan referensi ke lokasi istilah dalam dokumen asli. Referensi ini disebut "offset", yang hanya merupakan indeks awal dan akhir ke dalam aliran karakter asli. Jika kita mengubah buffer di CharTermAttribute untuk menunjuk ke substring token saja, kita harus menyesuaikan offset ini.

Anda mungkin bertanya-tanya mengapa API untuk memanipulasi aliran token sangat berbelit-belit dan, khususnya, mengapa kita tidak bisa melakukan sesuatu seperti String#split pada token yang masuk. Ini karena Lucene dirancang untuk pengindeksan berkecepatan tinggi dan overhead rendah, di mana tokenizer dan filter bawaan dapat dengan cepat mengunyah teks dalam gigabyte sementara hanya menggunakan memori megabyte. Untuk mencapai hal ini, sedikit atau tidak ada alokasi yang dilakukan selama tokenisasi dan pemfilteran, sehingga instance Attribute yang disebutkan di atas dimaksudkan untuk dialokasikan sekali dan digunakan kembali. Jika tokenizer dan filter Anda ditulis dengan cara ini, dan meminimalkan alokasinya sendiri, Anda dapat menyesuaikan Lucene tanpa mengurangi kinerja.

Dengan semua itu, mari kita lihat bagaimana menerapkan filter yang mengambil token seperti ["Hello] , dan menghasilkan dua token, ["] dan [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);

Kita mulai dengan mendapatkan referensi ke beberapa atribut yang kita lihat sebelumnya. Kami memberi akhiran nama bidang dengan "Attr" sehingga akan jelas nanti ketika kami merujuknya. Ada kemungkinan bahwa beberapa implementasi Tokenizer tidak menyediakan atribut ini, jadi kami menggunakan addAttribute untuk mendapatkan referensi kami. addAttribute akan membuat instance atribut jika tidak ada, jika tidak, ambil referensi bersama ke atribut jenis itu. Perhatikan bahwa Lucene tidak mengizinkan beberapa instance dari tipe atribut yang sama sekaligus.

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

Karena filter kami akan memperkenalkan token baru yang tidak ada di aliran asli, kami memerlukan tempat untuk menyimpan status token tersebut di antara panggilan ke incrementToken . Karena kita membagi token yang ada menjadi dua, cukup mengetahui offset dan jenis token baru saja. Kami juga memiliki tanda yang memberi tahu kami apakah panggilan berikutnya ke incrementToken akan memancarkan token tambahan ini. Lucene sebenarnya menyediakan sepasang metode, captureState dan restoreState , yang akan melakukan ini untuk Anda. Tetapi metode ini melibatkan alokasi objek State , dan sebenarnya bisa lebih rumit daripada sekadar mengelola status itu sendiri, jadi kami akan menghindari menggunakannya.

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

Sebagai bagian dari penghindaran alokasi yang agresif, Lucene dapat menggunakan kembali instans filter. Dalam situasi ini, diharapkan panggilan untuk reset akan mengembalikan filter ke keadaan awalnya. Jadi di sini, kami hanya mengatur ulang bidang token ekstra kami.

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

Sekarang kita masuk ke bagian yang menarik. Saat implementasi incrementToken kami dipanggil, kami memiliki kesempatan untuk tidak memanggil incrementToken pada tahap awal pipeline. Dengan demikian, kami secara efektif memperkenalkan token baru, karena kami tidak menarik token dari Tokenizer .

Sebagai gantinya, kami memanggil advanceToExtraToken untuk menyiapkan atribut untuk token tambahan kami, menyetel emitExtraToken ke false untuk menghindari cabang ini pada panggilan berikutnya, lalu mengembalikan true , yang menunjukkan bahwa token lain tersedia.

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

Sisa dari incrementToken akan melakukan salah satu dari tiga hal yang berbeda. Ingatlah bahwa termBufferAttr digunakan untuk memeriksa konten token yang masuk melalui pipa:

  1. Jika kita telah mencapai akhir aliran token (yaitu hasNext salah), kita selesai dan cukup kembali.

  2. Jika kami memiliki token lebih dari satu karakter, dan salah satu karakter tersebut adalah kutipan, kami membagi token tersebut.

  3. Jika token adalah kutipan tunggal, kami menganggap itu adalah kutipan akhir. Untuk memahami alasannya, perhatikan bahwa tanda kutip awal selalu muncul di sebelah kiri kata (yaitu, tanpa tanda baca perantara), sedangkan tanda kutip akhir dapat mengikuti tanda baca (seperti dalam kalimat, [He told us to "go back the way we came."] ). Dalam kasus ini, kutipan akhir sudah menjadi token terpisah, jadi kita hanya perlu mengatur jenisnya.

splitTermQuoteFirst dan splitTermWordFirst akan mengatur atribut untuk membuat token saat ini menjadi kata atau kutipan, dan mengatur bidang "ekstra" untuk memungkinkan separuh lainnya digunakan nanti. Kedua metode tersebut serupa, jadi kita hanya akan melihat 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); }

Karena kami ingin membagi token ini dengan kutipan yang muncul di aliran terlebih dahulu, kami memotong buffer dengan menyetel panjangnya menjadi satu (yaitu, satu karakter; yaitu, kutipan). Kami menyesuaikan offset yang sesuai (yaitu menunjuk ke kutipan dalam dokumen asli) dan juga mengatur jenisnya menjadi kutipan awal.

prepareExtraTerm akan menyetel bidang extra* dan menyetel emitExtraToken ke true. Ini disebut dengan offset yang menunjuk pada token "ekstra" (yaitu, kata yang mengikuti kutipan).

Keseluruhan QuotationTokenFilter tersedia di GitHub.

Selain itu, sementara filter ini hanya menghasilkan satu token tambahan, pendekatan ini dapat diperluas untuk memperkenalkan sejumlah token tambahan yang berubah-ubah. Ganti saja bidang extra* dengan koleksi atau, lebih baik lagi, larik dengan panjang tetap jika ada batasan jumlah token tambahan yang dapat diproduksi. Lihat SynonymFilter dan kelas dalam PendingInput untuk contoh ini.

Mengkonsumsi Token Kutipan dan Menandai Dialog

Sekarang setelah kita melakukan semua upaya untuk menambahkan kutipan tersebut ke aliran token, kita dapat menggunakannya untuk membatasi bagian dialog dalam teks.

Karena tujuan akhir kami adalah menyesuaikan hasil penelusuran berdasarkan apakah istilah merupakan bagian dari dialog atau tidak, kami perlu melampirkan metadata ke istilah tersebut. Lucene menyediakan PayloadAttribute untuk tujuan ini. Payload adalah array byte yang disimpan di samping istilah dalam indeks, dan dapat dibaca nanti selama pencarian. Ini berarti bahwa flag kita akan menghabiskan seluruh byte secara sia-sia, sehingga muatan tambahan dapat diimplementasikan sebagai flag bit untuk menghemat ruang.

Di bawah ini adalah filter baru, DialoguePayloadTokenFilter , yang ditambahkan ke bagian paling akhir dari pipeline analisis. Itu melampirkan muatan yang menunjukkan apakah token adalah bagian dari dialog atau tidak.

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

Karena filter ini hanya perlu mempertahankan satu status, withinDialogue , ini jauh lebih sederhana. Kutipan awal menunjukkan bahwa kita sekarang berada dalam bagian dialog, sedangkan kutipan akhir menunjukkan bahwa bagian dialog telah berakhir. Dalam kedua kasus tersebut, token kutipan dibuang dengan melakukan panggilan kedua ke incrementToken , jadi pada dasarnya, token awal kutipan atau akhir kutipan tidak pernah mengalir melewati tahap ini dalam pipa.

Misalnya, DialoguePayloadTokenFilter akan mengubah aliran token:

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

ke aliran baru ini:

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

Mengikat Tokenizer dan Filter Bersama

Sebuah Analyzer bertanggung jawab untuk merakit pipa analisis, biasanya dengan menggabungkan Tokenizer dengan serangkaian TokenFilter s. Analyzer s juga dapat menentukan bagaimana pipeline itu digunakan kembali di antara analisis. Kami tidak perlu khawatir tentang itu karena komponen kami tidak memerlukan apa pun kecuali panggilan ke reset() di antara penggunaan, yang akan selalu dilakukan Lucene. Kita hanya perlu melakukan perakitan dengan mengimplementasikan 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); } }

Seperti yang kita lihat sebelumnya, filter berisi referensi kembali ke tahap sebelumnya dalam pipeline, jadi begitulah cara kita membuat instance mereka. Kami juga memasukkan beberapa filter dari StandardAnalyzer : LowerCaseFilter dan StopFilter . Keduanya harus muncul setelah QuotationTokenFilter untuk memastikan bahwa setiap kutipan telah dipisahkan. Kami dapat lebih fleksibel dalam penempatan DialoguePayloadTokenFilter , karena di mana saja setelah QuotationTokenFilter akan melakukannya. Kami meletakkannya setelah StopFilter untuk menghindari membuang-buang waktu menyuntikkan muatan dialog ke dalam kata-kata berhenti yang pada akhirnya akan dihapus.

Berikut adalah visualisasi dari pipeline baru kami yang sedang beraksi (dikurangi bagian-bagian dari pipeline standar yang telah kami hapus atau sudah kami lihat):

Visualisasi pipa baru di Apache lucene

DialogueAnalyzer sekarang dapat digunakan seperti Analyzer saham lainnya, dan sekarang kita dapat membangun indeks dan melanjutkan pencarian.

Pencarian Teks Lengkap Dialog

Jika kita hanya ingin mencari dialog, kita bisa saja membuang semua token di luar kutipan dan kita akan selesai. Alih-alih, dengan membiarkan semua token asli tetap utuh, kami memberi diri kami fleksibilitas untuk melakukan kueri yang memperhitungkan dialog, atau memperlakukan dialog seperti bagian teks lainnya.

Dasar-dasar kueri indeks Lucene didokumentasikan dengan baik. Untuk tujuan kami, cukup untuk mengetahui bahwa kueri terdiri dari objek Term yang disatukan dengan operator seperti MUST atau SHOULD , bersama dengan dokumen yang cocok berdasarkan persyaratan tersebut. Dokumen yang cocok kemudian diberi skor berdasarkan objek Similarity yang dapat dikonfigurasi, dan hasil tersebut dapat diurutkan berdasarkan skor, difilter, atau dibatasi. Misalnya, Lucene memungkinkan kita untuk melakukan kueri untuk sepuluh dokumen teratas yang harus berisi istilah [hello] dan [world] .

Menyesuaikan hasil pencarian berdasarkan dialog dapat dilakukan dengan menyesuaikan skor dokumen berdasarkan payload. Titik ekstensi pertama untuk ini akan berada di Similarity , yang bertanggung jawab untuk menimbang dan menilai istilah yang cocok.

Kesamaan dan Skor

Kueri akan, secara default, menggunakan DefaultSimilarity , yang memberi bobot pada istilah berdasarkan frekuensi kemunculannya dalam dokumen. Ini adalah titik ekstensi yang baik untuk menyesuaikan bobot, jadi kami memperluasnya untuk juga menilai dokumen berdasarkan muatan. Metode DefaultSimilarity#scorePayload disediakan untuk tujuan ini:

 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 hanya menilai muatan non-dialog sebagai nol. Karena setiap Term dapat dicocokkan beberapa kali, itu berpotensi memiliki beberapa skor muatan. Penafsiran skor ini hingga implementasi Query .

Perhatikan baik- BytesRef yang berisi payload: kita harus memeriksa byte di offset , karena kita tidak dapat berasumsi bahwa array byte adalah payload yang sama dengan yang kita simpan sebelumnya. Saat membaca indeks, Lucene tidak akan membuang-buang memori dengan mengalokasikan larik byte terpisah hanya untuk panggilan ke scorePayload , jadi kita mendapatkan referensi ke larik byte yang ada. Saat melakukan pengkodean terhadap Lucene API, perlu diingat bahwa kinerja adalah prioritas, jauh di atas kenyamanan pengembang.

Sekarang kita memiliki implementasi Similarity baru, itu kemudian harus disetel pada IndexSearcher yang digunakan untuk mengeksekusi kueri:

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

Pertanyaan dan Ketentuan

Sekarang setelah IndexSearcher kami dapat menilai muatan, kami juga harus membuat kueri yang sadar akan muatan. PayloadTermQuery dapat digunakan untuk mencocokkan satu Term sambil juga memeriksa muatan kecocokan tersebut:

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

Kueri ini cocok dengan istilah [hello] di dalam bidang isi (ingat bahwa ini adalah tempat kami meletakkan konten dokumen). Kami juga harus menyediakan fungsi untuk menghitung skor muatan akhir dari semua kecocokan istilah, jadi kami memasukkan AveragePayloadFunction , yang rata-rata semua skor muatan. Misalnya, jika istilah [hello] muncul dua kali di dalam dialog dan di luar dialog satu kali, skor muatan akhir adalah ²⁄₃. Skor muatan akhir ini dikalikan dengan yang disediakan oleh DefaultSimilarity untuk seluruh dokumen.

Kami menggunakan rata-rata karena kami ingin tidak menekankan hasil pencarian di mana banyak istilah muncul di luar dialog, dan untuk menghasilkan skor nol untuk dokumen tanpa istilah dalam dialog sama sekali.

Kami juga dapat membuat beberapa objek PayloadTermQuery menggunakan BooleanQuery jika kami ingin mencari beberapa istilah yang terdapat dalam dialog (perhatikan bahwa urutan istilah tidak relevan dalam kueri ini, meskipun jenis kueri lainnya sadar posisi):

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

Ketika kueri ini dijalankan, kita dapat melihat bagaimana struktur kueri dan implementasi kesamaan bekerja bersama:

pipa analisis dialog lucene

Eksekusi dan Penjelasan Kueri

Untuk mengeksekusi kueri, kami menyerahkannya ke IndexSearcher :

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

Benda Collector digunakan untuk menyiapkan koleksi dokumen yang cocok.

kolektor dapat disusun untuk mencapai kombinasi penyortiran, pembatasan, dan penyaringan. Untuk mendapatkan, misalnya, sepuluh dokumen penilaian teratas yang berisi setidaknya satu istilah dalam dialog, kami menggabungkan TopScoreDocCollector dan PositiveScoresOnlyCollector . Mengambil hanya skor positif memastikan bahwa skor nol cocok (yaitu, mereka yang tidak memiliki istilah dalam dialog) disaring.

Untuk melihat kueri ini beraksi, kita dapat menjalankannya, lalu menggunakan IndexSearcher#explain untuk melihat bagaimana masing-masing dokumen dinilai:

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

Di sini, kami mengulangi ID dokumen di TopDocs yang diperoleh dengan pencarian. Kami juga menggunakan IndexSearcher#doc untuk mengambil bidang judul untuk ditampilkan. Untuk kueri "hello" kami, ini menghasilkan:

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

Meskipun output sarat dengan jargon, kita dapat melihat bagaimana penerapan Similarity kustom kita digunakan dalam penilaian, dan bagaimana MaxPayloadFunction menghasilkan pengganda 1.0 untuk kecocokan ini. Ini menyiratkan bahwa muatan telah dimuat dan dicetak, dan semua kecocokan "Hello" terjadi dalam dialog, sehingga hasil ini berada tepat di atas tempat yang kami harapkan.

Perlu juga ditunjukkan bahwa indeks untuk Project Gutenberg, dengan muatan, berukuran hampir empat gigabyte, namun pada mesin pengembangan sederhana saya, kueri muncul secara instan. Kami tidak mengorbankan kecepatan apa pun untuk mencapai tujuan pencarian kami.

Membungkus

Lucene adalah pustaka pencarian teks lengkap yang dibuat untuk tujuan yang kuat yang mengambil aliran karakter mentah, menggabungkannya ke dalam token, dan mempertahankannya sebagai istilah dalam indeks. Itu dapat dengan cepat menanyakan indeks itu dan memberikan hasil peringkat, dan memberikan banyak peluang untuk ekstensi sambil mempertahankan efisiensi.

Dengan menggunakan Lucene langsung di aplikasi kami, atau sebagai bagian dari server, kami dapat melakukan pencarian teks lengkap secara real-time melalui gigabyte konten. Selain itu, melalui analisis dan penilaian khusus, kami dapat memanfaatkan fitur khusus domain dalam dokumen kami untuk meningkatkan relevansi hasil atau kueri khusus.

Daftar kode lengkap untuk tutorial Lucene ini tersedia di GitHub. Repo berisi dua aplikasi: LuceneIndexerApp untuk membangun indeks, dan LuceneQueryApp untuk melakukan kueri.

Korpus Proyek Gutenberg, yang dapat diperoleh sebagai gambar disk melalui BitTorrent, berisi banyak buku yang layak dibaca (baik dengan Lucene, atau hanya dengan cara kuno).

Selamat mengindeks!