Apache Lucene е библиотека на Java, използвана за пълнотекстово търсене на документи и е в основата на сървърите за търсене като Solr и Elasticsearch . Той може също да бъде вграден в Java приложения, като приложения за Android или уеб бекендове.
Въпреки че опциите за конфигурация на Lucene са обширни, те са предназначени за използване от разработчици на бази данни върху общ корпус от текст. Ако документите ви имат специфична структура или тип съдържание, можете да се възползвате от двете, за да подобрите качеството на търсене и възможностите за заявки.
Като пример за този вид персонализация, в този урок по Lucene ще индексираме корпуса на Проект Гутенберг , която предлага хиляди безплатни електронни книги. Знаем, че много от тези книги са романи. Да предположим, че се интересуваме особено от диалог в рамките на тези романи. Нито Lucene, Elasticsearch, нито Solr предлагат готови инструменти за идентифициране на съдържанието като диалог. Всъщност те ще изхвърлят пунктуацията на най-ранните етапи от анализа на текста, което противоречи на възможността да се идентифицират части от текста, които са диалог. Следователно в тези ранни етапи трябва да започне нашето персонализиране.
The Анализ на луцен JavaDoc осигурява добър преглед на всички движещи се части в конвейера за анализ на текст.
На високо ниво можете да мислите за конвейера за анализ като за консумиране на суров поток от символи в началото и създаване на „термини“, приблизително съответстващи на думи, в края.
Стандартният тръбопровод за анализ може да бъде визуализиран като такъв:
Ще видим как да персонализираме този конвейер, за да разпознава региони от текст, маркирани с двойни кавички, които ще нарека диалог, и след това ще извадим съвпадения, които се появяват при търсене в тези региони.
Когато документите първоначално се добавят към индекса, символите се четат от Java InputStream , и така те могат да идват от файлове, бази данни, обаждания към уеб услуги и др. За да създадем индекс за Project Gutenberg, изтегляме електронните книги и създаваме малко приложение, което да чете тези файлове и да ги записва в индекса. Създаването на индекс на Lucene и четенето на файлове са добре изминати пътища, така че няма да ги изследваме много. Основният код за създаване на индекс е:
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);
Виждаме, че всяка електронна книга ще съответства на един Луцен Document
така че по-късно резултатите от търсенето ни ще бъдат списък със съответстващи книги. Store.YES
показва, че съхраняваме заглавие поле, което е само името на файла. Не искаме да съхраняваме тяло на електронната книга, тъй като тя не е необходима при търсене и само би загубила дисково пространство.
Действителното отчитане на потока започва с addDocument
. IndexWriter
изтегля жетони от края на тръбопровода. Това изтегляне продължава през тръбата до първия етап, Tokenizer
, чете от InputStream
.
Също така имайте предвид, че не затваряме потока, тъй като Lucene се справя с това вместо нас.
Луценът StandardTokenizer изхвърля пунктуацията и така нашата персонализация ще започне тук, тъй като трябва да запазим кавичките.
Документацията за StandardTokenizer
ви кани да копирате изходния код и да го съобразите с вашите нужди, но това решение би било ненужно сложно. Вместо това ще разширим CharTokenizer
, което ви позволява да посочите символи за 'приемане', при което тези, които не са 'приети', ще бъдат третирани като разделители между жетоните и изхвърлени. Тъй като се интересуваме от думи и цитати около тях, нашият персонализиран токенизатор е просто:
public class QuotationTokenizer extends CharTokenizer { @Override protected boolean isTokenChar(int c) return Character.isLetter(c) }
При входящ поток от [He said, 'Good day'.]
, произведените токени ще бъдат [He]
, [said]
, ['Good]
, [day']
как да проектираме целева страница
Обърнете внимание как кавичките са разпръснати в рамките на жетоните. Възможно е да се напише Tokenizer
който произвежда отделни символи за всяка кавичка, но Tokenizer
се занимава и с неподходящи, лесни за извиване детайли, като буфериране и сканиране, така че е най-добре да запазите Tokenizer
опростете и почистете потока от символи по-нататък по тръбопровода.
След токенизатора идва поредица от TokenFilter
обекти. Имайте предвид, между другото, че филтър е малко погрешно наименование, като TokenFilter
може да добавя, премахва или променя жетони.
Много от класовете на филтри, предоставени от Lucene, очакват единични думи, така че няма да е възможно нашите смесени символи на думи и цитати да се вливат в тях. Следователно следващото персонализиране на нашия урок за Lucene трябва да бъде въвеждането на филтър, който ще изчисти изхода на QuotationTokenizer
Това почистване ще включва производството на допълнителна начална оферта знак, ако кавичката се появява в началото на дума или крайна оферта знак, ако кавичката се появи в края. Ще оставим настрана боравенето с единични цитирани думи за простота.
Създаване на TokenFilter
подкласът включва прилагане на един метод: incrementToken
. Този метод трябва да извиква incrementToken
върху предишния филтър в тръбата и след това манипулирайте резултатите от това повикване, за да извършите каквато и да е работа, за която отговаря филтърът. Резултатите от incrementToken
са достъпни чрез Attribute
обекти, които описват текущото състояние на обработка на токени. След нашето внедряване на incrementToken
връща, се очаква, че атрибутите са били манипулирани за настройка на маркера за следващия филтър (или индекса, ако сме в края на тръбата).
Атрибутите, които ни интересуват в този момент от конвейера, са:
CharTermAttribute
: Съдържа char[]
буфер, съдържащ символите на текущия маркер. Ще трябва да манипулираме това, за да премахнем котировката или да създадем котировка.
TypeAttribute
: Съдържа 'типа' на текущия маркер. Тъй като добавяме начални и крайни кавички към потока на маркера, ще въведем два нови типа с помощта на нашия филтър.
OffsetAttribute
: Lucene може по избор да съхранява препратки към местоположението на термините в оригиналния документ. Тези препратки се наричат „отмествания“, които са само начални и крайни индекси в оригиналния поток от символи. Ако сменим буфера в CharTermAttribute
за да посочим само един подниз на маркера, трябва съответно да коригираме тези отмествания.
Може би се чудите защо API за манипулиране на потоци с маркери е толкова объркан и по-специално защо не можем просто да направим нещо като String#split
върху входящите жетони. Това е така, защото Lucene е проектиран за високоскоростно индексиране с ниски режийни разходи, при което вградените токенизатори и филтри могат бързо да дъвчат през гигабайта текст, като използват само мегабайта памет. За да се постигне това, по време на токенизация и филтриране се правят малко или никакви разпределения и така Attribute
споменатите по-горе екземпляри са предназначени да бъдат разпределени веднъж и използвани повторно. Ако вашите токенизатори и филтри са написани по този начин и минимизират собствените им разпределения, можете да персонализирате Lucene, без да нарушавате производителността.
Имайки предвид всичко това, нека да видим как да приложим филтър, който взема маркер като ['Hello]
и произвежда двата маркера, [']
и [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);
Започваме с получаване на препратки към някои от атрибутите, които видяхме по-рано. Суфиксираме имената на полетата с „Attr“, за да стане ясно по-късно, когато се позоваваме на тях. Възможно е някои Tokenizer
реализациите не предоставят тези атрибути, затова използваме addAttribute
за да получите нашите референции. addAttribute
ще създаде екземпляр на атрибут, ако той липсва, в противен случай вземете споделена препратка към атрибута от този тип. Обърнете внимание, че Lucene не позволява няколко екземпляра от един и същи тип атрибут наведнъж.
private boolean emitExtraToken; private int extraTokenStartOffset, extraTokenEndOffset; private String extraTokenType;
Тъй като нашият филтър ще въведе нов маркер, който не присъства в оригиналния поток, имаме нужда от място, за да запазим състоянието на този маркер между извикванията към incrementToken
Тъй като разделяме съществуващ маркер на две, достатъчно е да знаем само изместванията и типа на новия маркер. Имаме и флаг, който ни казва дали следващото повикване към incrementToken
ще излъчва този допълнителен знак. Луценът всъщност осигурява двойка методи, captureState
и restoreState
, които ще направят това вместо вас. Но тези методи включват разпределяне на State
обект и всъщност може да бъде по-сложно, отколкото просто да управлявате това състояние сами, така че ще избягваме да ги използваме.
@Override public void reset() throws IOException { emitExtraToken = false; extraTokenStartOffset = -1; extraTokenEndOffset = -1; extraTokenType = null; super.reset(); }
Като част от агресивното си избягване на разпределение, Lucene може да използва повторно филтърни копия. В тази ситуация се очаква повикване към reset
ще върне филтъра в първоначалното му състояние. Така че тук ние просто нулираме нашите допълнителни полета за маркери.
@Override public boolean incrementToken() throws IOException { if (emitExtraToken) { advanceToExtraToken(); emitExtraToken = false; return true; } ...
Сега стигаме до интересните битове. Когато нашето изпълнение на incrementToken
се нарича, имаме възможност да не обадете се incrementToken
на по-ранния етап от тръбопровода. По този начин ние ефективно въвеждаме нов маркер, защото не дърпаме маркер от Tokenizer
.
Вместо това извикваме advanceToExtraToken
за да настроите атрибутите за нашия допълнителен маркер, задайте emitExtraToken
на false, за да се избегне този клон при следващото повикване и след това се върне true
, което показва, че е наличен друг маркер.
@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
ще направи едно от трите различни неща. Спомнете си, че termBufferAttr
се използва за проверка на съдържанието на маркера, идващ през тръбата:
Ако сме достигнали до края на потока с маркери (т.е. hasNext
е false), сме готови и просто се връщаме.
Ако имаме знак от повече от един знак и един от тези символи е кавичка, ние разделяме символа.
tdd и bdd в agile
Ако маркерът е единичен цитат, предполагаме, че е краен цитат. За да разберете защо, обърнете внимание, че началните кавички винаги се появяват отляво на думата (т.е. без междинни пунктуационни знаци), докато завършващите кавички могат да следват пунктуацията (например в изречението, [He told us to 'go back the way we came.']
). В тези случаи крайният цитат вече ще бъде отделен знак и затова трябва само да зададем неговия тип.
splitTermQuoteFirst
и splitTermWordFirst
ще зададе атрибути, за да направи текущия маркер или дума, или кавичка, и ще настрои полетата „допълнителни“, за да позволи другата половина да бъде консумирана по-късно. Двата метода са сходни, така че ще разгледаме само 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); }
Тъй като искаме да разделим този маркер с кавичката, която се появява първо в потока, ние съкращаваме буфера, като задаваме дължината на един (т.е. един знак; а именно цитата). Съответно коригираме отместванията (т.е. соченето на цитата в оригиналния документ) и също така задаваме типа да бъде начална оферта.
prepareExtraTerm
ще зададе extra*
полета и задайте emitExtraToken
до вярно. Извиква се с отмествания, сочещи към „допълнителния“ маркер (т.е. думата, следваща цитата).
Цялото на QuotationTokenFilter
е на разположение на GitHub .
Освен това, докато този филтър произвежда само един допълнителен маркер, този подход може да бъде разширен, за да въведе произволен брой допълнителни символи. Просто заменете extra*
полета с колекция или, още по-добре, масив с фиксирана дължина, ако има ограничение за броя на допълнителните символи, които могат да бъдат произведени. Вижте SynonymFilter
и неговите PendingInput
вътрешен клас за пример за това.
Сега, след като се насочихме към всички тези усилия, за да добавим тези кавички към потока с маркери, можем да ги използваме, за да разграничим разделите на диалога в текста.
Тъй като нашата крайна цел е да коригираме резултатите от търсенето въз основа на това дали термините са част от диалога или не, трябва да прикачим метаданни към тези термини. Луценът осигурява PayloadAttribute
за тази цел. Полезните товари са байтови масиви, които се съхраняват заедно с термини в индекса и могат да бъдат прочетени по-късно по време на търсене. Това означава, че нашият флаг ще заема разточително цял байт, така че допълнителни полезни товари могат да бъдат внедрени като битови флагове, за да се спести място.
По-долу има нов филтър, DialoguePayloadTokenFilter
, който е добавен в самия край на конвейера за анализ. Той прикачва полезния товар, указвайки дали маркерът е част от диалога.
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; } }
Тъй като този филтър трябва да поддържа само едно състояние, withinDialogue
, той е много по-опростен. Стартова кавичка показва, че вече сме в рамките на част от диалога, докато крайната оферта показва, че секцията на диалога е приключила. И в двата случая котировъчният маркер се отхвърля чрез второ повикване към incrementToken
, така че всъщност, начална оферта или крайна оферта символите никога не преминават през този етап в тръбопровода.
Например, DialoguePayloadTokenFilter
ще преобразува потока от символи:
[the], [program], [printed], ['], [hello], [world], [']`
в този нов поток:
[the][0], [program][0], [printed][0], [hello][1], [world][1]
An Analyzer
е отговорен за сглобяването на тръбопровода за анализ, обикновено чрез комбиниране на Tokenizer
с поредица от TokenFilter
s. Analyzer
s могат също да определят как този тръбопровод се използва повторно между анализите. Не е нужно да се притесняваме за това, тъй като нашите компоненти не изискват нищо освен повикване към reset()
между употребите, което Lucene винаги ще прави. Просто трябва да направим сглобяването, като приложим 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); } }
Както видяхме по-рано, филтрите съдържат препратка към предишния етап в конвейера, така че по този начин ги създаваме. Също така плъзгаме няколко филтъра от StandardAnalyzer
: LowerCaseFilter
и StopFilter
. Тези две трябва да дойдат след QuotationTokenFilter
за да се гарантира, че всякакви кавички са разделени. Можем да бъдем по-гъвкави при поставянето на DialoguePayloadTokenFilter
, тъй като навсякъде след QuotationTokenFilter
ще го направя. Поставяме го след StopFilter
за да се избегне загубата на време за инжектиране на полезния товар на диалога стоп думи които в крайна сметка ще бъдат премахнати.
Ето визуализация на новия ни тръбопровод в действие (минус онези части от стандартния тръбопровод, които сме премахнали или вече сме виждали):
raspberry pi като сървър
DialogueAnalyzer
вече може да се използва като всеки друг запас Analyzer
ще бъде и сега можем да изградим индекса и да преминем към търсене.
Ако искахме само да търсим диалог, можехме просто да изхвърлим всички символи извън офертата и щяхме да свършим. Вместо това, като оставихме всички оригинални символи непокътнати, ние си дадохме гъвкавостта или да изпълняваме заявки, които отчитат диалога, или да третираме диалога като всяка друга част от текста.
Основите на заявките за индекс на Луцен са добре документирани . За нашите цели е достатъчно да знаем, че заявките са съставени от Term
обекти, залепени заедно с оператори като MUST
или SHOULD
, заедно с документи за съвпадение въз основа на тези условия. След това съответстващите документи се оценяват въз основа на конфигурируем Similarity
обект и тези резултати могат да бъдат подредени по оценка, филтрирани или ограничени. Например, Lucene ни позволява да направим заявка за първите десет документа, които трябва да съдържат и двата термина [hello]
и [world]
.
Персонализирането на резултатите от търсенето въз основа на диалога може да се извърши чрез коригиране на оценката на документа въз основа на полезния товар. Първата точка за удължаване за това ще бъде в Similarity
, която отговаря за претеглянето и оценяването на съответстващите условия.
Заявките по подразбиране ще използват DefaultSimilarity
, което претегля термините въз основа на това колко често се появяват в документ. Това е добра точка за удължаване за регулиране на тежестите, така че ние я разширяваме, за да вкарваме и документи на базата на полезен товар. Методът DefaultSimilarity#scorePayload
е предвидено за тази цел:
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
просто оценява полезния товар без диалог като нула. Като всеки Term
може да се съчетава няколко пъти, потенциално ще има множество резултати от полезен товар. Тълкуването на тези оценки до Query
изпълнение.
Обърнете голямо внимание на BytesRef
съдържащ полезния товар: трябва да проверим байта при offset
, тъй като не можем да приемем, че масивът от байтове е същият полезен товар, който сме съхранявали по-рано. Когато чете индекса, Lucene няма да губи памет, заделяйки отделен байтов масив само за извикване към scorePayload
, така че получаваме препратка към съществуващ байтов масив. Когато кодирате срещу API на Lucene, струва си да имате предвид, че производителността е приоритет, много преди удобството на разработчика.
Сега, когато имаме нашите нови Similarity
изпълнението, след това трябва да бъде зададено на IndexSearcher
използвани за изпълнение на заявки:
IndexSearcher searcher = new IndexSearcher(... reader for index ...); searcher.setSimilarity(new DialogueAwareSimilarity());
Сега, когато нашите IndexSearcher
можем да вкараме полезен товар, ние също трябва да изградим заявка, която да е наясно с полезния товар. PayloadTermQuery
може да се използва за съвпадение на един Term
като същевременно проверявате полезния товар на тези съвпадения:
PayloadTermQuery helloQuery = new PayloadTermQuery(new Term('body', 'hello'), new AveragePayloadFunction());
Тази заявка съответства на термина [hello]
в рамките на тяло поле (припомнете си, че тук поставяме съдържанието на документа). Трябва също така да предоставим функция за изчисляване на крайния резултат на полезния товар от всички мачове, така че включваме AveragePayloadFunction
, която осреднява всички резултати от полезния товар. Например, ако терминът [hello]
се случва вътре в диалога два пъти и отвън веднъж, крайният резултат от полезния товар ще бъде ²⁄₃. Този краен резултат на полезния товар се умножава с този, предоставен от DefaultSimilarity
за целия документ.
Използваме средна стойност, защото бихме искали да деакцентираме резултатите от търсенето, когато много термини се появяват извън диалога, и да създадем нулева оценка за документи без никакви термини в диалога.
Можем също да съставим няколко PayloadTermQuery
обекти, използващи BooleanQuery
ако искаме да търсим множество термини, съдържащи се в диалога (имайте предвид, че редът на термините е ирелевантен в тази заявка, макар че други типове заявки са съобразени с позицията):
PayloadTermQuery worldQuery = new PayloadTermQuery(new Term('body', 'world'), new AveragePayloadFunction()); BooleanQuery query = new BooleanQuery(); query.add(helloQuery, Occur.MUST); query.add(worldQuery, Occur.MUST);
Когато тази заявка се изпълни, можем да видим как структурата на заявката и внедряването на сходство работят заедно:
За да изпълним заявката, ние я предаваме на IndexSearcher
:
TopScoreDocCollector collector = TopScoreDocCollector.create(10); searcher.search(query, new PositiveScoresOnlyCollector(collector)); TopDocs topDocs = collector.topDocs();
Collector
обектите се използват за подготовка на колекцията от съответстващи документи.
колекторите могат да бъдат съставени, за да се постигне комбинация от сортиране, ограничаване и филтриране. За да получим, например, десетте най-важни документи, които съдържат поне един термин в диалог, ние комбинираме TopScoreDocCollector
и PositiveScoresOnlyCollector
. Взимането само на положителни резултати гарантира, че съвпаденията с нулеви оценки (т.е. тези без термини в диалог) се филтрират.
За да видим тази заявка в действие, можем да я изпълним, след което да използваме IndexSearcher#explain
за да видите как се оценяват отделни документи:
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)); }
Тук прелистваме идентификаторите на документите в TopDocs
получени при търсенето. Ние също използваме IndexSearcher#doc
за извличане на заглавното поле за показване. За нашата заявка за 'hello'
това води до:
--- 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() ...
Въпреки че изходът е натоварен с жаргон, можем да видим как нашите потребителски Similarity
внедряването е използвано при оценяването и как MaxPayloadFunction
произведе множител от 1.0
за тези мачове. Това означава, че полезният товар е бил зареден и отбелязан, както и всички мачове от 'Hello'
възникнали в диалог и затова тези резултати са точно на върха, където ги очакваме.
Също така си струва да се отбележи, че индексът на Project Gutenberg, с полезни товари, достига почти четири гигабайта по размер, и въпреки това на моята скромна машина за разработка, запитванията се появяват мигновено. Не сме жертвали никаква скорост, за да постигнем целите си за търсене.
Lucene е мощна, вградена за целта библиотека за пълнотекстово търсене, която приема суров поток от символи, обединява ги в символи и ги запазва като термини в индекс. Той може бързо да запитва този индекс и да предоставя класирани резултати и предоставя широка възможност за разширяване, като същевременно поддържа ефективност.
Използвайки Lucene директно в нашите приложения или като част от сървър, ние можем да извършваме търсене на пълен текст в реално време над гигабайта съдържание. Освен това, чрез персонализиран анализ и оценка, можем да се възползваме от специфичните за домейна функции в нашите документи, за да подобрим уместността на резултатите или персонализираните заявки.
кое от следните е добре запознат с html5, javascript и css?
Пълните списъци с кодове за този урок по Lucene са на разположение на GitHub . Репото съдържа две приложения: LuceneIndexerApp
за изграждане на индекса и LuceneQueryApp
за изпълнение на заявки.
Корпусът на проект Гутенберг, който може да бъде получен като образ на диск чрез BitTorrent , съдържа много книги, които си струва да се прочетат (или с луцен, или просто по старомодния начин).
Честито индексиране!