Эта статья является частью нашего академического курса под названием Apache Lucene Fundamentals .
В этом курсе вы познакомитесь с Lucene. Вы поймете, почему такая библиотека важна, а затем узнаете, как работает поиск в Lucene. Кроме того, вы узнаете, как интегрировать Lucene Search в ваши собственные приложения, чтобы обеспечить надежные возможности поиска. Проверьте это здесь !
Содержание
1. Введение
В этом уроке нашего курса мы собираемся исследовать основные механизмы запросов, предлагаемые Lucene. Как вы помните из вводного урока, Lucene не отправляет необработанный текст для поиска в указатель. Для этого он использует Query
Objects. В этом уроке мы рассмотрим все важные компоненты, которые выстраиваются в очередь, чтобы преобразовать написанные человеком поисковые фразы в репрезентативные структуры, такие как Queries
.
2. Класс запроса
Класс Query
— это открытый абстрактный класс, который представляет запрос к индексу. В этом разделе мы рассмотрим наиболее важные подклассы запросов, которые вы можете использовать для выполнения специализированных запросов.
2.1 TermQuery
Это самый простой и простой запрос, который вы можете выполнить для индекса Lucene. Вы просто ищете Documents
которые содержат одно слово в определенном Field
.
Базовый конструктор TermQuery
определяется следующим образом: public TermQuery(Term t)
. Как вы помните из первого урока, Term
состоит из двух частей:
- Название
Field
в котором находится этот термин. - Фактическое значение термина, которое в подавляющем большинстве случаев представляет собой одно слово, полученное в результате анализа некоторого простого текста.
Итак, если вы хотите создать TermQuery, чтобы найти все Documents
, содержащие слово "good"
в их Field
"content"
, вот как вы можете это сделать
1
|
TermQuery termQuery = new TermQuery( new Term( "content" , "good" )); |
Мы можем использовать это для поиска слова «статический» в нашем ранее созданном индексе:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
String q = "static" Directory directory = FSDirectory.open(indexDir); IndexReader indexReader = DirectoryReader.open(directory); IndexSearcher searcher = new IndexSearcher(indexReader); Analyzer analyzer = new StandardAnalyzer(Version.LUCENE_46); TermQuery termQuery = new TermQuery( new Term( "content" ,q)); TopDocs topDocs =searcher.search(termQuery, maxHits); ScoreDoc[] hits = topDocs.scoreDocs; for (ScoreDoc hit : hits) { int docId = hit.doc; Document d = searcher.doc(docId); System.out.println(d.get( "fileName" ) + " Score :" + hit.score); } System.out.println( "Found " + hits.length); |
Выход этого будет:
1
2
3
4
5
6
7
8
|
C:\\Users\\nikos\\Desktop\\LuceneFolders\\LuceneHelloWorld\\SourceFiles\\Product.java Score :0.29545835 C:\\Users\\nikos\\Desktop\\LuceneFolders\\LuceneHelloWorld\\SourceFiles\\SimpleSearcher.java Score :0.27245367 C:\\Users\\nikos\\Desktop\\LuceneFolders\\LuceneHelloWorld\\SourceFiles\\PropertyObject.java Score :0.24368995 C:\\Users\\nikos\\Desktop\\LuceneFolders\\LuceneHelloWorld\\SourceFiles\\SimpleIndexer.java Score :0.14772917 C:\\Users\\nikos\\Desktop\\LuceneFolders\\LuceneHelloWorld\\SourceFiles\\TestSerlvet.java Score :0.14621398 C:\\Users\\nikos\\Desktop\\LuceneFolders\\LuceneHelloWorld\\SourceFiles\\ShoppingCartServlet.java Score :0.13785185 C:\\Users\\nikos\\Desktop\\LuceneFolders\\LuceneHelloWorld\\SourceFiles\\MyServlet.java Score :0.12184498 Found 7 |
Как видите, семь моих исходных файлов содержали ключевое слово "static"
. Вот и все. Естественно, если вы попытаетесь добавить другое слово в строку запроса, поиск вернет 0 результатов. Например, если вы установите строку запроса в:
1
|
String q = "private static" |
Выход будет:
1
|
Found 0 |
Теперь я знаю, что "private static"
присутствует во многих моих исходных файлах. Но, как вы помните, мы использовали StandarAnalyzer
для обработки простого текста, полученного из наших файлов, в процессе индексации. StandardAnalyzer
разбивает текст на отдельные слова, поэтому каждый Term
содержит одно слово. Вы можете не маркировать индексированное Field
. Но я бы посоветовал вам делать это в Fields
которые содержат метаинформацию о нашем Документе, например, в заголовке или авторе, а не в полях, содержащих его содержимое. Например, если вы решите не маркировать и не проиндексировать поле с именем 'author'
и значением 'James Wilslow'
, Field
'author'
будет содержать только один Term
со значением 'James Wilslow'
в целом. Если бы вы сделали токенизацию Field
, оно содержало бы два термина, один со значением 'James'
а другой со значением 'Wilslow'
.
2.2 PhraseQuery
С PhraseQuery
вы можете искать Documents
которые содержат определенную последовательность слов, или фраз.
Вы можете создать PhraseQuery
следующим образом:
1
|
PhraseQuery phraseQuery = new PhraseQuery(); |
И тогда вы можете добавить Terms
к нему. Например, если вы хотите найти Documents
которые содержат фразу «private static» в их поле «content», вы можете сделать это так:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
|
PhraseQuery phraseQuery = new PhraseQuery(); phraseQuery.add( new Term( "content" , "private" )); phraseQuery.add( new Term( "content" , "static" )); TopDocs topDocs =searcher.search(phraseQuery, maxHits); ScoreDoc[] hits = topDocs.scoreDocs; for (ScoreDoc hit : hits) { int docId = hit.doc; Document d = searcher.doc(docId); System.out.println(d.get( "fileName" ) + " Score :" + hit.score); } System.out.println( "Found " + hits.length); |
Выход будет:
1
2
3
4
5
6
7
8
|
C:\\Users\\nikos\\Desktop\\LuceneFolders\\LuceneHelloWorld\\SourceFiles\\Product.java Score :0.54864377 C:\\Users\\nikos\\Desktop\\LuceneFolders\\LuceneHelloWorld\\SourceFiles\\PropertyObject.java Score :0.45251375 C:\\Users\\nikos\\Desktop\\LuceneFolders\\LuceneHelloWorld\\SourceFiles\\SimpleSearcher.java Score :0.45251375 C:\\Users\\nikos\\Desktop\\LuceneFolders\\LuceneHelloWorld\\SourceFiles\\TestSerlvet.java Score :0.27150828 C:\\Users\\nikos\\Desktop\\LuceneFolders\\LuceneHelloWorld\\SourceFiles\\ShoppingCartServlet.java Score :0.25598043 C:\\Users\\nikos\\Desktop\\LuceneFolders\\LuceneHelloWorld\\SourceFiles\\MyServlet.java Score :0.22625688 C:\\Users\\nikos\\Desktop\\LuceneFolders\\LuceneHelloWorld\\SourceFiles\\SimpleIndexer.java Score :0.22398287 Found 7 |
Document
превращает его в результаты, только если Field
содержит слова "private"
и "static"
последовательно и в том же порядке.
Так что если вы измените приведенный выше код примерно так:
1
2
|
phraseQuery.add( new Term( "content" , "private" )); phraseQuery.add( new Term( "content" , "final" )); |
Ты получишь :
1
|
Found 0 |
Это потому, что, хотя мои исходные файлы содержат оба слова, они не являются последовательными. Чтобы немного изменить это поведение, вы можете добавить PhraseQuery
в PhraseQuery
. Когда вы добавляете пометку 1, вы позволяете не более одного слова вмешиваться между словами в вашей фразе. Когда вы добавляете помет 2, вы допускаете не более 2 слов между словами в фразе.
Интересно: «На самом деле наклон — это расстояние редактирования, где единицы соответствуют перемещениям терминов в фразе запроса вне позиции. Например, для переключения порядка двух слов требуется два хода (первый шаг помещает слова друг на друга), поэтому, чтобы разрешить перестановку фраз, наклон должен быть не менее двух ».
Итак, если мы сделаем:
1
2
3
4
5
6
|
PhraseQuery phraseQuery = new PhraseQuery(); phraseQuery.add( new Term( "content" , "private" )); phraseQuery.add( new Term( "content" , "final" )); phraseQuery.setSlop( 2 ); |
Результат нашего поиска даст:
1
2
3
4
5
6
7
|
C:\\Users\\nikos\\Desktop\\LuceneFolders\\LuceneHelloWorld\\SourceFiles\\Product.java Score :0.38794976 C:\\Users\\nikos\\Desktop\\LuceneFolders\\LuceneHelloWorld\\SourceFiles\\PropertyObject.java Score :0.31997555 C:\\Users\\nikos\\Desktop\\LuceneFolders\\LuceneHelloWorld\\SourceFiles\\SimpleSearcher.java Score :0.31997555 C:\\Users\\nikos\\Desktop\\LuceneFolders\\LuceneHelloWorld\\SourceFiles\\TestSerlvet.java Score :0.19198532 C:\\Users\\nikos\\Desktop\\LuceneFolders\\LuceneHelloWorld\\SourceFiles\\ShoppingCartServlet.java Score :0.18100551 C:\\Users\\nikos\\Desktop\\LuceneFolders\\LuceneHelloWorld\\SourceFiles\\MyServlet.java Score :0.15998778 C:\\Users\\nikos\\Desktop\\LuceneFolders\\LuceneHelloWorld\\SourceFiles\\SimpleIndexer.java Score :0.15837982 |
Важно отметить, что документы, содержащие фразы, более близкие к точной фразе запроса, получат более высокие оценки.
2.3 BooleanQuery
BooleanQuery
— более выразительный и мощный инструмент, поскольку вы можете комбинировать несколько запросов вместе с логическими предложениями. BoleanQuery может быть заполнен BooleanClauses. BooleanClause состоит из Query
и той роли, которую этот Query
должен играть в булевом поиске.
Чтобы быть более конкретным, логическое предложение может играть следующие роли в запросе:
-
MUST
: Это довольноexplenatory
.Document
попадает в список результатов, если и только если он содержит это предложение. -
MUST NOT
: Это прямо противоположный случай. Обязательно, чтобы Документ попал в список результатов, чтобы не содержать этот пункт. -
SHOULD
: Это относится к предложениям, которые могут встречаться вDocument
, но им не обязательно включать его, чтобы получить его результаты.
Если у вас есть логический запрос только с предложениями SHOULD, результаты соответствуют хотя бы одному из предложений. Это похоже на классический логический оператор ИЛИ, но его не так просто использовать правильно.
Теперь давайте посмотрим несколько примеров. Давайте найдем исходные файлы, которые содержат слово «string», но не содержат слово «int».
01
02
03
04
05
06
07
08
09
10
11
|
TermQuery termQuery = new TermQuery( new Term( "content" , "string" )); TermQuery termQuery2 = new TermQuery( new Term( "content" , "int" )); BooleanClause booleanClause1 = new BooleanClause(termQuery, BooleanClause.Occur.MUST); BooleanClause booleanClause2 = new BooleanClause(termQuery2, BooleanClause.Occur.MUST_NOT); BooleanQuery booleanQuery = new BooleanQuery(); booleanQuery.add(booleanClause1); booleanQuery.add(booleanClause2); TopDocs topDocs =searcher.search(booleanQuery, maxHits); |
Вот результат:
1
2
3
4
5
|
C:\\Users\\nikos\\Desktop\\LuceneFolders\\LuceneHelloWorld\\SourceFiles\\SimpleEJB.java Score :0.45057273 C:\\Users\\nikos\\Desktop\\LuceneFolders\\LuceneHelloWorld\\SourceFiles\\PropertyObject.java Score :0.39020744 C:\\Users\\nikos\\Desktop\\LuceneFolders\\LuceneHelloWorld\\SourceFiles\\ShoppingCartServlet.java Score :0.20150226 C:\\Users\\nikos\\Desktop\\LuceneFolders\\LuceneHelloWorld\\SourceFiles\\TestSerlvet.java Score :0.13517183 Found 4 |
Теперь давайте попробуем найти все документы, содержащие слово «nikos» и фразу «httpservletresponse response». В следующем фрагменте вы можете увидеть, как вы можете избежать создания экземпляров BooleanClause
, делая вашу работу более компактной.
01
02
03
04
05
06
07
08
09
10
11
12
|
TermQuery termQuery = new TermQuery( new Term( "content" , "nikos" )); PhraseQuery phraseQuery = new PhraseQuery(); phraseQuery.add( new Term( "content" , "httpservletresponse" )); phraseQuery.add( new Term( "content" , "response" )); BooleanQuery booleanQuery = new BooleanQuery(); booleanQuery.add(phraseQuery,BooleanClause.Occur.MUST); booleanQuery.add(termQuery,BooleanClause.Occur.MUST); TopDocs topDocs =searcher.search(booleanQuery, maxHits); |
Это результат:
1
2
|
C:\\Users\\nikos\\Desktop\\LuceneFolders\\LuceneHelloWorld\\SourceFiles\\ShoppingCartServlet.java Score :0.3148332 Found 1 |
Давайте найдем все Документы, которые содержат слово «int» или слово «nikos». Как вы можете представить, вы должны каким-то образом использовать спецификацию СЛЕДУЕТ:
1
2
3
4
5
6
7
8
9
|
TermQuery termQuery = new TermQuery( new Term( "content" , "int" )); TermQuery termQuery2 = new TermQuery( new Term( "content" , "nikos" )); BooleanQuery booleanQuery = new BooleanQuery(); booleanQuery.add(termQuery,BooleanClause.Occur.SHOULD); booleanQuery.add(termQuery2,BooleanClause.Occur.SHOULD); TopDocs topDocs =searcher.search(booleanQuery, maxHits); |
Это было не слишком сложно, но немного сложнее создать более сложные дизъюнктивные запросы. Это не всегда просто, как вы должны использовать правильно.
Например, давайте попробуем найти все Документы, которые содержат слово «nikos» и фразу «httpservletresponse response» или содержат слово «int». Можно написать что-то вроде этого:
01
02
03
04
05
06
07
08
09
10
11
12
13
|
TermQuery termQuery = new TermQuery( new Term( "content" , "nikos" )); PhraseQuery phraseQuery = new PhraseQuery(); phraseQuery.add( new Term( "content" , "httpservletresponse" )); phraseQuery.add( new Term( "content" , "response" )); BooleanQuery booleanQuery = new BooleanQuery(); booleanQuery.add(phraseQuery,BooleanClause.Occur.MUST); booleanQuery.add(termQuery,BooleanClause.Occur.MUST); booleanQuery.add( new TermQuery( new Term( "content" , "int" )),BooleanClause.Occur.SHOULD); TopDocs topDocs =searcher.search(booleanQuery, maxHits); |
Но запрос не даст желаемых результатов. Помните, что результаты этого запроса, как мы его "nikos"
, ДОЛЖНЫ содержать слово "nikos"
и ДОЛЖНЫ содержать фразу "httpservletresponse response"
одновременно. Но это не то, что вы хотите. Вы хотите документы, которые содержат слово nikos и фразу "httpservletresponse response"
, но вы также хотите, чтобы документы содержали слово "int"
независимо, независимо от того, содержат ли они другие пункты. Справедливости ради, приведенный выше логический запрос немного ошибочен. Потому что в прямом логическом синтаксисе вы бы никогда не написали что-то вроде: A И B ИЛИ C. Вы должны написать (A И B) ИЛИ C. Или A И (B ИЛИ C). Увидеть разницу?
Поэтому вы должны написать запрос, который вам нравится: («nikos» И «httpservletresponse response») ИЛИ «int».
Вы можете сделать это, комбинируя BooleanQueries
вместе. Используя приведенный выше строгий синтаксис, не очень сложно представить, как это будет происходить:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
|
TermQuery termQuery = new TermQuery( new Term( "content" , "nikos" )); PhraseQuery phraseQuery = new PhraseQuery(); phraseQuery.add( new Term( "content" , "httpservletresponse" )); phraseQuery.add( new Term( "content" , "response" )); // (A AND B) BooleanQuery conjunctiveQuery = new BooleanQuery(); conjunctiveQuery.add(termQuery,BooleanClause.Occur.MUST); conjunctiveQuery.add(phraseQuery,BooleanClause.Occur.MUST); BooleanQuery disjunctiveQuery = new BooleanQuery(); // (A AND B) OR C disjunctiveQuery.add(conjunctiveQuery,BooleanClause.Occur.SHOULD); disjunctiveQuery.add( new TermQuery( new Term( "content" , "int" )),BooleanClause.Occur.SHOULD); TopDocs topDocs =searcher.search(disjunctiveQuery, maxHits); |
Это краткое руководство, BooleanQuery
вы можете следовать при построении логических запросов с использованием класса BooleanQuery
:
- X И Y
- X ИЛИ Y
- X И (НЕ Y)
- (X И Y) ИЛИ Z
- (X ИЛИ Y) И Z
- X ИЛИ (НЕ Z)
1
2
3
|
BooleanQuery bool = new BooleanQuery(); bool.add(X,BooleanClause.Occur.MUST); bool.add(Y,BooleanClause.Occur.MUST); |
1
2
3
|
BooleanQuery bool = new BooleanQuery(); bool.add(X,BooleanClause.Occur.SHOULD); bool.add(Y,BooleanClause.Occur.SHOULD); |
1
2
3
|
BooleanQuery bool = new BooleanQuery(); bool.add(X,BooleanClause.Occur.MUST); bool.add(Y,BooleanClause.Occur.MUST_NOT); |
1
2
3
4
5
6
7
8
|
BooleanQuery conj = new BooleanQuery(); conj.add(X,BooleanClause.Occur.MUST); conj.add(Y,BooleanClause.Occur.MUST); BooleanQuery disj = new BooleanQuery(); disj.add(conj,BooleanClause.Occur.SHOULD) disj.add(Z,BooleanClause.Occur.SHOULD) |
1
2
3
4
5
6
7
8
|
BooleanQuery conj = new BooleanQuery(); conj.add(X,BooleanClause.Occur.SHOULD); conj.add(Y,BooleanClause.Occur.SHOULD); BooleanQuery disj = new BooleanQuery(); disj.add(conj,BooleanClause.Occur.MUST) disj.add(Z,BooleanClause.Occur.MUST) |
1
2
3
4
5
6
7
|
BooleanQuery neg = new BooleanQuery(); neg.add(Z,BooleanClause.Occur.MUST_OT); BooleanQuery disj = new BooleanQuery(); disj.add(neg,BooleanClause.Occur.SHOULD) disj.add(X,BooleanClause.Occur.SHOULD) |
Выше можно использовать для создания все более сложных логических запросов.
2.4 WildcardQuery
Как следует из названия, вы можете использовать класс WildcardQuery для выполнения подстановочных запросов, используя ‘*’ или ‘?’ персонажи. Например, если вы хотите искать документы, содержащие термины, начинающиеся с «ni», за которыми следует любая другая последовательность символов, вы можете выполнить поиск «ni *». Если вы хотите найти термины, которые начинаются с «jamie», за которым следует (любой) один символ, вы можете найти «jamie?». Просто как тот. Естественно, WildcardQueries
неэффективны, потому что при поиске может потребоваться много разных терминов для поиска совпадений. Как правило, рекомендуется избегать использования подстановочного знака в начале слова, например «* abcde».
Давайте посмотрим на пример:
1
2
|
Query wildcardQuery = new WildcardQuery( new Term( "content" , "n*os" )); TopDocs topDocs =searcher.search(wildcardQuery, maxHits); |
И
1
2
|
Query wildcardQuery = new WildcardQuery( new Term( "content" , "niko?" )); TopDocs topDocs =searcher.search(wildcardQuery, maxHits); |
2.5 RegexpQuery
Используя RegexpQuery
, вы можете выполнять быстрые запросы регулярных выражений, которые оцениваются с помощью очень быстрой автоматической реализации Lucene. Вот пример
1
2
3
|
Query regexpQuery = new RegexpQuery( new Term( "content" , "n[a-z]+" )); TopDocs topDocs =searcher.search(regexpQuery, maxHits); |
2.6 TermRangeQuery
Этот подкласс Query полезен при выполнении запросов диапазона на строковых терминах. Например, вы можете искать термины между словами «abc» и «xyz». Сравнение слов выполняется с помощью Byte.compareTo(Byte)
. Это может оказаться особенно полезным для запросов диапазона в метаданных ваших документов, таких как заголовки и даже даты (в случае дат следует использовать DateTools
).
Вот как вы можете найти все документы, созданные за последнюю неделю:
1
2
3
4
5
6
7
|
Calendar c = Calendar.getInstance(); c.add(Calendar.DATE, - 7 ); Date lastWeek = c.getTime(); Query termRangeQuery = TermRangeQuery.newStringRange( "date" , DateTools.dateToString( new Date(), DateTools.Resolution.DAY),DateTools.dateToString(lastWeek, DateTools.Resolution.DAY), true , true ); |
Конечно, вы должны быть осторожны при индексации поля «дата». К нему также необходимо применить DateTools.dateToString
и указать, что это поле не нужно анализировать (чтобы оно не разбивалось на токены и не разбивалось на слова).
2.7 NumberRangeQuery
Это для выполнения запросов числового диапазона. Представьте, что у вас есть поле «wordcount», в котором хранится количество слов этого документа, и вы хотите получить документы, содержащие от 2000 до 10000 слов:
1
|
Query numericRangeQuery = NumericRangeQuery.newIntRange( "wordcount" , 2000 , 10000 , true , true ); |
Логические аргументы диктуют, что верхний и нижний пределы включены в диапазон.
2.8 FuzzyQuery
Это очень интересный подкласс запросов. Этот запрос оценивает термины в соответствии с мерами близости, такими как хорошо известное расстояние Дамерау-Левенштейна. Это найдет слова, которые лексикографически близки. Если вы хотите выполнить интенсивное лексикографическое приложение, например, словарь или функцию предложения слов «вы имели в виду», вы можете использовать SpellChecker
API .
Давайте посмотрим, как вы можете выполнить нечеткий запрос с ошибочным написанием строки:
1
|
Query fuzzyQuery = new FuzzyQuery( new Term( "content" , "srng" )); |