Apache Lucene ile Diyalogların Tam Metin Araması: Bir Eğitim
Yayınlanan: 2022-03-11Apache 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.
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:
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 birchar[]
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:
Belirteç akışının sonuna ulaştıysak (yani
hasNext
yanlıştır), işimiz biter ve basitçe geri döneriz.Birden fazla karakterden oluşan bir simgemiz varsa ve bu karakterlerden biri bir alıntıysa, belirteci böleriz.
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ç):
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:
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!