Apache Lucene ile Diyalogların Tam Metin Araması: Bir Eğitim

Yayınlanan: 2022-03-11

Apache Lucene, belgelerin tam metin araması için kullanılan bir Java kitaplığıdır ve Solr ve Elasticsearch gibi arama sunucularının merkezinde yer alır. Ayrıca Android uygulamaları veya web arka uçları gibi Java uygulamalarına da yerleştirilebilir.

Lucene'nin yapılandırma seçenekleri kapsamlı olsa da, genel bir metin topluluğu üzerinde veritabanı geliştiricileri tarafından kullanılmak üzere tasarlanmıştır. Belgelerinizin belirli bir yapısı veya içeriği varsa, arama kalitesini ve sorgulama yeteneğini geliştirmek için ikisinden de yararlanabilirsiniz.

apache lucene ile tam metin arama

Bu tür özelleştirmeye bir örnek olarak, bu Lucene eğitiminde, binlerce ücretsiz e-kitap sunan Project Gutenberg'in külliyatını indeksleyeceğiz. Bu kitapların çoğunun roman olduğunu biliyoruz. Bu romanlardaki diyaloglarla özellikle ilgilendiğimizi varsayalım. Ne Lucene, ne Elasticsearch ne de Solr, içeriği diyalog olarak tanımlamak için kullanıma hazır araçlar sağlamaz. Aslında, metin analizinin en erken aşamalarında noktalama işaretlerini atacaklar, bu da metnin diyalog olan kısımlarını tanımlayabilmekle ters düşüyor. Bu nedenle, özelleştirmemizin başlaması gereken yer bu erken aşamalardır.

Apache Lucene Analiz Hattının Parçaları

Lucene analizi JavaDoc, metin analizi boru hattındaki tüm hareketli parçalara iyi bir genel bakış sağlar.

Yüksek düzeyde, analiz hattını başlangıçta ham bir karakter akışı tüketerek ve sonunda kabaca kelimelere karşılık gelen “terimler” üreterek düşünebilirsiniz.

Standart analiz hattı şu şekilde görselleştirilebilir:

Lucene analiz hattı

Bu ardışık düzeni, diyalog diyeceğim çift tırnakla işaretlenmiş metin bölgelerini tanımak için nasıl özelleştireceğimizi ve ardından bu bölgelerde arama yaparken meydana gelen eşleşmeleri nasıl artıracağımızı göreceğiz.

Karakter Okuma

Belgeler ilk olarak dizine eklendiğinde, karakterler Java InputStream'den okunur ve böylece dosyalardan, veritabanlarından, web hizmeti çağrılarından vb. gelebilirler. Project Gutenberg için bir dizin oluşturmak için e-kitapları indiririz ve bu dosyaları okumak ve dizine yazmak için küçük bir uygulama oluşturun. Bir Lucene indeksi oluşturmak ve dosyaları okumak iyi seyahat edilmiş yollar, bu yüzden onları fazla keşfetmeyeceğiz. Bir indeks üretmek için gerekli kod şudur:

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

Her e-kitabın tek bir Lucene Document karşılık geleceğini görebiliriz, bu nedenle daha sonra arama sonuçlarımız eşleşen kitapların bir listesi olacaktır. Store.YES , yalnızca dosya adı olan başlık alanını depoladığımızı belirtir. Ancak e-kitabın gövdesini saklamak istemiyoruz, çünkü arama yaparken gerekli değildir ve yalnızca disk alanını boşa harcar.

Akışın gerçek okuması addDocument ile başlar. IndexWriter , işlem hattının sonundan belirteçleri çeker. Bu çekme, ilk aşama olan Tokenizer InputStream okumasına kadar boru boyunca geriye doğru ilerler.

Ayrıca akışı kapatmadığımızı da unutmayın, çünkü bunu bizim için Lucene hallediyor.

Tokenleştirme Karakterleri

Lucene StandardTokenizer noktalama işaretlerini ortadan kaldırır ve bu nedenle, alıntıları korumamız gerektiğinden özelleştirmemiz burada başlayacaktır.

StandardTokenizer belgeleri sizi kaynak kodunu kopyalamaya ve ihtiyaçlarınıza göre uyarlamaya davet ediyor, ancak bu çözüm gereksiz yere karmaşık olacaktır. Bunun yerine, "kabul" edilecek karakterleri belirtmenize izin veren CharTokenizer genişleteceğiz, burada "kabul edilmeyen" olanlar belirteçler arasında sınırlayıcı olarak değerlendirilecek ve atılacaktır. Sözcükler ve etraflarındaki alıntılarla ilgilendiğimiz için özel Tokenizer'ımız basitçe:

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

[He said, "Good day".] giriş akışı verildiğinde, üretilen jetonlar [He] , [said] , ["Good] , [day"] olacaktır.

Alıntıların belirteçler içinde nasıl serpiştirildiğine dikkat edin. Her teklif için ayrı jetonlar üreten bir Tokenizer yazmak mümkündür, ancak Tokenizer ayrıca arabelleğe alma ve tarama gibi basit, vidalanması kolay ayrıntılarla da ilgilenir, bu nedenle Tokenizer basit tutmak ve temizlemek en iyisidir. jeton akışı boru hattında daha da ilerler.

Filtreleri Kullanarak Jetonları Bölme

Belirteç oluşturucudan sonra bir dizi TokenFilter nesnesi gelir. Bu arada, bir TokenFilter belirteçleri ekleyebileceği, kaldırabileceği veya değiştirebileceği için bu filtrenin biraz yanlış adlandırma olduğunu unutmayın.

Lucene tarafından sağlanan filtre sınıflarının çoğu tek sözcükler bekler, bu nedenle karışık sözcük ve alıntı belirteçlerimizin bunlara akması gerekmez. Bu nedenle, Lucene öğreticimizin bir sonraki özelleştirmesi, QuotationTokenizer çıktısını temizleyecek bir filtrenin tanıtımı olmalıdır.

Bu temizleme, alıntı bir kelimenin başında görünüyorsa ekstra bir başlangıç ​​alıntı jetonunun veya alıntı sonunda görünüyorsa bir bitiş alıntı jetonunun üretilmesini içerecektir. Basitlik için tek alıntı kelimelerin işlenmesini bir kenara bırakacağız.

Bir TokenFilter alt sınıfı oluşturmak, bir yöntemin uygulanmasını içerir: incrementToken . Bu yöntem, borudaki önceki filtrede incrementToken çağırmalı ve ardından filtrenin sorumlu olduğu her türlü işi gerçekleştirmek için bu çağrının sonuçlarını değiştirmelidir. incrementToken sonuçlarına, token işlemenin mevcut durumunu açıklayan Attribute nesneleri aracılığıyla erişilebilir. incrementToken dönüşlerini uygulamamızdan sonra, bir sonraki filtre için belirteci (veya borunun sonundaysak dizini) ayarlamak üzere özniteliklerin manipüle edilmesi beklenir.

Boru hattının bu noktasında ilgilendiğimiz nitelikler şunlardır:

  • CharTermAttribute : Geçerli simgenin karakterlerini tutan bir char[] arabelleği içerir. Teklifi kaldırmak veya bir teklif belirteci üretmek için bunu değiştirmemiz gerekecek.

  • TypeAttribute : Geçerli belirtecin "türünü" içerir. Belirteç akışına başlangıç ​​ve bitiş alıntıları eklediğimiz için filtremizi kullanarak iki yeni tür tanıtacağız.

  • OffsetAttribute : Lucene, isteğe bağlı olarak, orijinal belgedeki terimlerin konumuna referansları saklayabilir. Bu referanslara, orijinal karakter akışında sadece başlangıç ​​ve bitiş indeksleri olan “ofsetler” denir. CharTermAttribute içindeki arabelleği, belirtecin yalnızca bir alt dizesine işaret edecek şekilde değiştirirsek, bu ofsetleri buna göre ayarlamamız gerekir.

Belirteç akışlarını manipüle etmek için kullanılan API'nin neden bu kadar karmaşık olduğunu ve özellikle neden gelen belirteçlerde String#split gibi bir şey yapamadığımızı merak ediyor olabilirsiniz. Bunun nedeni, Lucene'nin yüksek hızlı, düşük maliyetli indeksleme için tasarlanmış olmasıdır; bu sayede yerleşik belirteçler ve filtreler, yalnızca megabaytlarca bellek kullanırken gigabaytlarca metni hızla çiğneyebilir. Bunu başarmak için, belirteçleştirme ve filtreleme sırasında çok az tahsis yapılır veya hiç tahsis edilmez ve bu nedenle yukarıda belirtilen Attribute örneklerinin bir kez tahsis edilmesi ve yeniden kullanılması amaçlanır. Belirteçleriniz ve filtreleriniz bu şekilde yazılırsa ve kendi tahsislerini en aza indirirseniz, performanstan ödün vermeden Lucene'i özelleştirebilirsiniz.

Tüm bunları göz önünde bulundurarak, ["Hello] gibi bir jeton alan ve ["] ve [Hello] gibi iki jeton üreten bir filtrenin nasıl uygulanacağını görelim:

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

Daha önce gördüğümüz bazı niteliklere referanslar alarak başlıyoruz. Alan adlarına "Attr" ekliyoruz, böylece daha sonra bunlara atıfta bulunduğumuzda netleşecek. Bazı Tokenizer uygulamalarının bu öznitelikleri sağlamaması mümkündür, bu nedenle referanslarımızı almak için addAttribute kullanıyoruz. addAttribute , eksikse bir öznitelik örneği oluşturur, aksi takdirde bu tür özniteliğe paylaşılan bir başvuru alır. Lucene'in aynı öznitelik türünün birden çok örneğine aynı anda izin vermediğini unutmayın.

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

Filtremiz, orijinal akışta olmayan yeni bir belirteç tanıtacağından, bu belirtecin durumunu incrementToken çağrıları arasında kaydedecek bir yere ihtiyacımız var. Mevcut bir jetonu ikiye böldüğümüz için, yeni jetonun sadece ofsetlerini ve türünü bilmek yeterlidir. Ayrıca, incrementToken yapılan bir sonraki çağrının bu ekstra belirteci yayınlayıp yaymayacağını bize söyleyen bir bayrağımız var. Lucene aslında bunu sizin için yapacak olan CaptureState ve captureState restoreState çift yöntem sağlar. Ancak bu yöntemler, bir State nesnesinin tahsis edilmesini içerir ve aslında bu durumu kendiniz yönetmekten daha aldatıcı olabilir, bu yüzden onları kullanmaktan kaçınacağız.

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

Lucene, ayırmadan agresif şekilde kaçınmasının bir parçası olarak filtre örneklerini yeniden kullanabilir. Bu durumda, bir reset çağrısının filtreyi ilk durumuna geri getirmesi beklenir. Yani burada, sadece ekstra jeton alanlarımızı sıfırlıyoruz.

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

Şimdi ilginç kısımlara geliyoruz. incrementToken uygulamamız çağrıldığında, boru hattının önceki aşamasında incrementToken çağırmama fırsatımız var. Bunu yaparak, etkin bir şekilde yeni bir belirteç tanıtıyoruz çünkü Tokenizer bir belirteç çekmiyoruz.

Bunun yerine, ekstra simgemizin özniteliklerini ayarlamak için emitExtraToken advanceToExtraToken olarak ayarlarız ve ardından başka bir belirtecin kullanılabilir olduğunu gösteren true değerini döndürürüz.

 @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 geri kalanı üç farklı şeyden birini yapacak. termBufferAttr borudan gelen belirtecin içeriğini incelemek için kullanıldığını hatırlayın:

  1. Belirteç akışının sonuna ulaştıysak (yani hasNext yanlıştır), işimiz biter ve basitçe geri döneriz.

  2. Birden fazla karakterden oluşan bir simgemiz varsa ve bu karakterlerden biri bir alıntıysa, belirteci böleriz.

  3. Belirteç tek bir alıntıysa, bunun bir son alıntı olduğunu varsayıyoruz. Nedenini anlamak için, başlangıç ​​tırnaklarının her zaman bir kelimenin solunda göründüğünü (yani, ara noktalama işaretleri olmadan), bitiş tırnaklarının noktalama işaretlerini takip edebileceğini unutmayın (örneğin, cümlede, [He told us to "go back the way we came."] ). Bu durumlarda, bitiş alıntısı zaten ayrı bir belirteç olacaktır ve bu nedenle yalnızca türünü ayarlamamız gerekir.

splitTermQuoteFirst ve splitTermWordFirst , geçerli belirteci bir sözcük veya bir alıntı yapmak için öznitelikleri ayarlayacak ve diğer yarısının daha sonra tüketilmesine izin vermek için "ekstra" alanları ayarlayacaktır. İki yöntem benzerdir, bu yüzden sadece 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); }

Bu belirteci, akışta önce görünen alıntı ile bölmek istediğimiz için, uzunluğu bir olarak ayarlayarak arabelleği kısaltırız (yani, bir karakter; yani alıntı). Ofsetleri buna göre ayarlıyoruz (yani orijinal belgedeki alıntıyı işaret ederek) ve ayrıca türü bir başlangıç ​​alıntısı olarak ayarladık.

hazırlıkExtraTerm, extra* alanları ayarlayacak ve prepareExtraTerm emitExtraToken olarak ayarlayacaktır. "Ekstra" jetona (yani, alıntıdan sonraki kelimeye) işaret eden ofsetlerle çağrılır.

QuotationTokenFilter tamamı GitHub'da mevcuttur.

Bir kenara, bu filtre yalnızca bir ekstra jeton üretirken, bu yaklaşım isteğe bağlı sayıda ekstra jeton eklemek için genişletilebilir. extra* alanları bir koleksiyonla veya daha iyisi, üretilebilecek fazladan jeton sayısında bir sınır varsa sabit uzunluklu bir diziyle değiştirin. Bunun bir örneği için SynonymFilter ve PendingInput iç sınıfına bakın.

Alıntı Belirteçlerini Tüketme ve Diyalog İşaretleme

Artık bu alıntıları jeton akışına eklemek için tüm bu çabayı sarf ettiğimize göre, bunları metindeki diyalog bölümlerini sınırlamak için kullanabiliriz.

Nihai hedefimiz, arama sonuçlarını terimlerin diyaloğun parçası olup olmadığına göre ayarlamak olduğundan, bu terimlere meta veri eklememiz gerekir. Lucene, bu amaç için PayloadAttribute sağlar. Yükler, dizinde terimlerin yanında depolanan ve daha sonra bir arama sırasında okunabilen bayt dizileridir. Bu, bayrağımızın bir baytın tamamını boş yere kaplayacağı anlamına gelir, bu nedenle yerden tasarruf etmek için ek yükler bit bayrakları olarak uygulanabilir.

Aşağıda, analiz hattının en sonuna eklenen yeni bir filtre olan DialoguePayloadTokenFilter bulunmaktadır. Belirtecin diyaloğun bir parçası olup olmadığını belirten yükü ekler.

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

Bu filtrenin withinDialogue içinde yalnızca tek bir durum parçasını koruması gerektiğinden, çok daha basittir. Bir başlangıç ​​alıntısı, artık bir diyalog bölümünde olduğumuzu belirtirken, bir bitiş alıntısı, diyalog bölümünün sona erdiğini gösterir. Her iki durumda da, teklif belirteci, incrementToken ikinci bir çağrı yapılarak atılır, bu nedenle aslında, alıntı başlangıç ​​veya bitiş belirteçleri boru hattında bu aşamayı asla geçmez.

Örneğin, DialoguePayloadTokenFilter belirteç akışını dönüştürecektir:

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

bu yeni akışa:

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

Belirteçleri ve Filtreleri Birbirine Bağlama

Bir Analyzer , tipik olarak bir Tokenizer bir dizi TokenFilter s ile birleştirerek analiz hattını bir araya getirmekten sorumludur. Analyzer , bu ardışık düzenin analizler arasında nasıl yeniden kullanılacağını da tanımlayabilir. Bu konuda endişelenmemize gerek yok çünkü bileşenlerimiz, Lucene'in her zaman yapacağı gibi, kullanımlar arasında reset() çağrısı dışında hiçbir şeye ihtiyaç duymaz. Montajı Analyzer#createComponents(String) uygulayarak yapmamız yeterli:

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

Daha önce gördüğümüz gibi, filtreler boru hattındaki önceki aşamaya bir referans içerir, bu yüzden onları bu şekilde somutlaştırırız. Ayrıca StandardAnalyzer birkaç filtreyi de kaydırıyoruz: LowerCaseFilter ve StopFilter . Bu ikisi, alıntıların ayrıldığından emin olmak için QuotationTokenFilter sonra gelmelidir. QuotationTokenFilter sonra herhangi bir yerde yapacağından, DialoguePayloadTokenFilter yerleştirmemizde daha esnek olabiliriz. Diyalog yükünü, nihayetinde kaldırılacak olan durdurma sözcüklerine enjekte ederek zaman kaybetmemek için StopFilter koyduk.

İşte yeni işlem hattımızın eylem halindeki bir görselleştirmesi (standart işlem hattının kaldırdığımız veya zaten gördüğümüz kısımları hariç):

Apache lucene'de yeni boru hattı görselleştirmesi

DialogueAnalyzer artık diğer herhangi bir hisse senedi Analyzer gibi kullanılabilir ve şimdi indeksi oluşturup aramaya geçebiliriz.

Diyalog Tam Metin Arama

Sadece diyalog aramak isteseydik, bir alıntının dışındaki tüm belirteçleri atabilirdik ve işimiz bitmiş olurdu. Bunun yerine, tüm orijinal belirteçleri olduğu gibi bırakarak, kendimize diyaloğu dikkate alan sorguları gerçekleştirme veya diyaloğu metnin herhangi bir parçası gibi ele alma esnekliği verdik.

Bir Lucene indeksini sorgulamanın temelleri iyi belgelenmiştir. Bizim amaçlarımız için, sorguların MUST veya SHOULD gibi operatörlerle birbirine yapıştırılmış Term nesnelerinden ve bu terimlere dayalı eşleşme belgelerinden oluştuğunu bilmek yeterlidir. Eşleşen belgeler daha sonra yapılandırılabilir bir Similarity nesnesine dayalı olarak puanlanır ve bu sonuçlar puan, filtrelenmiş veya sınırlı olarak sıralanabilir. Örneğin, Lucene, [hello] ve [world] terimlerinin her ikisini de içermesi gereken ilk on belge için bir sorgu yapmamızı sağlar.

Arama sonuçlarını diyaloğa göre özelleştirme, bir belgenin yüküne göre puanını ayarlayarak yapılabilir. Bunun için ilk uzatma noktası, eşleştirme terimlerinin tartılmasından ve puanlamasından sorumlu olan Similarity olacaktır.

Benzerlik ve Puanlama

Sorgular, varsayılan olarak, terimleri bir belgede ne sıklıkta ortaya çıktıklarına göre ağırlıklandıran DefaultSimilarity kullanır. Ağırlıkları ayarlamak için iyi bir uzatma noktasıdır, bu nedenle belgeleri yüke dayalı olarak puanlayacak şekilde genişletiyoruz. DefaultSimilarity#scorePayload yöntemi bu amaç için sağlanmıştır:

 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 , diyalog dışı yükleri basitçe sıfır olarak puanlar. Her Term birden çok kez eşleştirilebileceğinden, potansiyel olarak birden çok yük puanına sahip olacaktır. Bu puanların yorumlanması, Query uygulamasına kadar.

Yükü içeren BytesRef çok dikkat edin: bayt dizisinin daha önce depoladığımız aynı yük olduğunu varsayamadığımız için baytı offset konumunda kontrol etmeliyiz. Dizini okurken, Lucene yalnızca scorePayload çağrısı için ayrı bir bayt dizisi ayırarak belleği boşa harcamaz, bu nedenle mevcut bir bayt dizisine bir başvuru alırız. Lucene API'sine karşı kodlama yaparken, geliştirici rahatlığının çok ötesinde performansın öncelikli olduğunu akılda tutmakta fayda var.

Artık yeni Similarity uygulamamıza sahip olduğumuza göre, bunun sorguları yürütmek için kullanılan IndexSearcher ayarlanması gerekir:

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

Sorgular ve Şartlar

Artık IndexSearcher yükleri puanlayabildiğine göre, yük farkında olan bir sorgu da oluşturmamız gerekiyor. PayloadTermQuery , tek bir Term eşleştirmek ve aynı zamanda bu eşleşmelerin yüklerini kontrol etmek için kullanılabilir:

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

Bu sorgu, gövde alanındaki [hello] terimiyle eşleşir (belgenin içeriğini buraya koyduğumuzu hatırlayın). Ayrıca, tüm terim eşleşmelerinden nihai yük puanını hesaplamak için bir işlev sağlamalıyız, bu nedenle, tüm yük puanlarının ortalamasını alan OrtalamaPayloadFunction AveragePayloadFunction . Örneğin, [hello] terimi diyalog içinde iki kez ve diyalog dışında bir kez geçerse, nihai yük puanı ²⁄₃ olacaktır. Bu nihai yük puanı, tüm belge için DefaultSimilarity tarafından sağlanan puanla çarpılır.

Bir ortalama kullanıyoruz çünkü diyalog dışında birçok terimin göründüğü arama sonuçlarını vurgulamak ve diyalogda herhangi bir terim içermeyen belgeler için sıfır puan vermek istiyoruz.

Diyalogda yer alan birden çok terimi aramak istiyorsak, bir BooleanQuery kullanarak birkaç PayloadTermQuery nesnesi de oluşturabiliriz (diğer sorgu türleri konum farkında olsa da, bu sorguda terimlerin sırasının alakasız olduğunu unutmayın):

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

Bu sorgu yürütüldüğünde, sorgu yapısı ve benzerlik uygulamasının birlikte nasıl çalıştığını görebiliriz:

lucene diyalog analizi boru hattı

Sorgu Yürütme ve Açıklama

Sorguyu yürütmek için IndexSearcher teslim ediyoruz:

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

Collector nesneleri, eşleşen belgelerin koleksiyonunu hazırlamak için kullanılır.

toplayıcılar, sıralama, sınırlama ve filtrelemenin bir kombinasyonunu elde etmek için oluşturulabilir. Örneğin, diyalogda en az bir terim içeren ilk on puanlama belgesini almak için TopScoreDocCollector ve PositiveScoresOnlyCollector birleştiriyoruz. Yalnızca pozitif puanlar almak, sıfır puan eşleşmelerinin (yani, diyalogda terim olmayanlar) filtrelenmesini sağlar.

Bu sorguyu çalışırken görmek için yürütebiliriz, ardından tek tek belgelerin nasıl puanlandığını görmek için IndexSearcher#explain kullanabiliriz:

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

Burada, aramayla elde edilen TopDocs belge kimlikleri üzerinde yineleniriz. Ayrıca, görüntülenmek üzere başlık alanını almak için IndexSearcher#doc kullanırız. "hello" için bunun sonucu:

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

Çıktı jargonla dolu olsa da, puanlamada özel Similarity uygulamamızın nasıl kullanıldığını ve MaxPayloadFunction bu eşleşmeler için nasıl 1.0 çarpanı ürettiğini görebiliriz. Bu, yükün yüklendiği ve puanlandığı ve tüm "Hello" eşleşmelerinin diyalogda gerçekleştiği anlamına gelir ve bu nedenle bu sonuçlar, beklediğimiz yerde en üsttedir.

Ayrıca, faydalı yüklerle Project Gutenberg endeksinin yaklaşık dört gigabayt boyutuna ulaştığını ve yine de mütevazı geliştirme makinemde sorguların anında gerçekleştiğini belirtmekte fayda var. Arama hedeflerimize ulaşmak için hiçbir hızdan ödün vermedik.

Toplama

Lucene, ham bir karakter akışını alan, onları belirteçler halinde bir araya getiren ve onları bir dizinde terimler olarak sürdüren güçlü, amaca yönelik olarak oluşturulmuş bir tam metin arama kitaplığıdır. Bu dizini hızlı bir şekilde sorgulayabilir ve sıralanmış sonuçlar sağlayabilir ve verimliliği korurken genişletme için bol fırsat sağlar.

Lucene'i doğrudan uygulamalarımızda veya bir sunucunun parçası olarak kullanarak, gigabaytlarca içerik üzerinden gerçek zamanlı olarak tam metin aramaları yapabiliriz. Ayrıca, özel analiz ve puanlama yoluyla, sonuçların veya özel sorguların alaka düzeyini artırmak için belgelerimizdeki alana özgü özelliklerden yararlanabiliriz.

Bu Lucene öğreticisi için tam kod listeleri GitHub'da mevcuttur. Depo iki uygulama içerir: dizin oluşturmak için LuceneIndexerApp ve sorguları gerçekleştirmek için LuceneQueryApp .

BitTorrent aracılığıyla bir disk görüntüsü olarak elde edilebilen Project Gutenberg'in külliyatı, okumaya değer pek çok kitap içeriyor (ya Lucene ile ya da sadece eski usul).

Mutlu indeksleme!