Перейти к основному содержимому

Введение в Apache Lucene

· 7 мин. чтения

1. Обзор

Apache Lucene — это полнотекстовый поисковый движок, который можно использовать с различными языками программирования.

В этой статье мы попытаемся понять основные концепции библиотеки и создать простое приложение.

2. Настройка Мавена

Для начала давайте сначала добавим необходимые зависимости:

<dependency>        
<groupId>org.apache.lucene</groupId>
<artifactId>lucene-core</artifactId>
<version>7.1.0</version>
</dependency>

Последнюю версию можно найти здесь .

Также для разбора наших поисковых запросов нам понадобится:

<dependency>
<groupId>org.apache.lucene</groupId>
<artifactId>lucene-queryparser</artifactId>
<version>7.1.0</version>
</dependency>

Проверьте наличие последней версии здесь .

3. Основные концепции

3.1. Индексация

Проще говоря, Lucene использует «обратное индексирование» данных — вместо сопоставления страниц с ключевыми словами ключевые слова сопоставляются со страницами точно так же, как глоссарий в конце любой книги.

Это позволяет быстрее получать ответы при поиске, поскольку поиск выполняется по индексу, а не по тексту напрямую.

3.2. Документы

Здесь документ представляет собой набор полей, и каждое поле имеет связанное с ним значение.

Индексы обычно состоят из одного или нескольких документов, а результаты поиска представляют собой наборы наиболее подходящих документов.

Это не всегда простой текстовый документ, это также может быть таблица базы данных или коллекция.

3.3. Поля

Документы могут иметь данные поля, где поле обычно представляет собой ключ, содержащий значение данных:

title: Goodness of Tea
body: Discussing goodness of drinking herbal tea...

Обратите внимание, что здесь title и body являются полями, и их можно искать вместе или по отдельности.

3.4. Анализ

Анализ преобразует данный текст в более мелкие и точные единицы для облегчения поиска.

Текст проходит различные операции по извлечению ключевых слов, удалению общеупотребительных слов и знаков препинания, переводу слов в нижний регистр и т. д.

Для этого есть несколько встроенных анализаторов:

  1. StandardAnalyzer — анализирует на основе базовой грамматики, удаляет стоп-слова, такие как «a», «an» и т. д. Также преобразует в нижний регистр
  2. SimpleAnalyzer — разбивает текст на основе символов без букв и преобразует в нижний регистр.
  3. WhiteSpaceAnalyzer — разбивает текст на основе пробелов

Мы также можем использовать и настраивать больше анализаторов.

3.5. Идет поиск

После того, как индекс построен, мы можем искать этот индекс, используя Query и IndexSearcher. Результатом поиска обычно является набор результатов, содержащий извлеченные данные.

Обратите внимание, что IndexWritter отвечает за создание индекса, а IndexSearcher — за поиск в индексе.

3.6. Синтаксис запроса

Lucene предоставляет очень динамичный и простой в написании синтаксис запросов.

Чтобы найти свободный текст, мы просто использовали текстовую строку в качестве запроса.

Для поиска текста в определенном поле мы будем использовать:

fieldName:text

eg: title:tea

Диапазон поиска:

timestamp:[1509909322,1572981321]

Мы также можем искать с помощью подстановочных знаков:

dri?nk

будет искать один символ вместо подстановочного знака «?»

d*k

ищет слова, начинающиеся с «d» и заканчивающиеся на «k», с несколькими символами между ними.

uni*

найдет слова, начинающиеся с «uni».

Мы также можем комбинировать эти запросы и создавать более сложные запросы. И включите логический оператор, такой как И, НЕ, ИЛИ:

title: "Tea in breakfast" AND "coffee"

Подробнее о синтаксисе запросов здесь .

4. Простое приложение

Давайте создадим простое приложение и проиндексируем некоторые документы.

Во-первых, мы создадим индекс в памяти и добавим в него несколько документов:

...
Directory memoryIndex = new RAMDirectory();
StandardAnalyzer analyzer = new StandardAnalyzer();
IndexWriterConfig indexWriterConfig = new IndexWriterConfig(analyzer);
IndexWriter writter = new IndexWriter(memoryIndex, indexWriterConfig);
Document document = new Document();

document.add(new TextField("title", title, Field.Store.YES));
document.add(new TextField("body", body, Field.Store.YES));

writter.addDocument(document);
writter.close();

Здесь мы создаем документ с TextField и добавляем их в индекс с помощью IndexWriter. Третий аргумент в конструкторе TextField указывает, должно ли также сохраняться значение поля.

Анализаторы используются для разделения данных или текста на куски, а затем отфильтровывают из них стоп-слова. Стоп-слова — это такие слова, как «а», «ам», «есть» и т. д. Они полностью зависят от данного языка.

Далее создадим поисковый запрос и поищем в индексе добавленный документ:

public List<Document> searchIndex(String inField, String queryString) {
Query query = new QueryParser(inField, analyzer)
.parse(queryString);

IndexReader indexReader = DirectoryReader.open(memoryIndex);
IndexSearcher searcher = new IndexSearcher(indexReader);
TopDocs topDocs = searcher.search(query, 10);
List<Document> documents = new ArrayList<>();
for (ScoreDoc scoreDoc : topDocs.scoreDocs) {
documents.add(searcher.doc(scoreDoc.doc));
}

return documents;
}

В методе search() второй целочисленный аргумент указывает, сколько лучших результатов поиска он должен вернуть.

Теперь давайте проверим это:

@Test
public void givenSearchQueryWhenFetchedDocumentThenCorrect() {
InMemoryLuceneIndex inMemoryLuceneIndex
= new InMemoryLuceneIndex(new RAMDirectory(), new StandardAnalyzer());
inMemoryLuceneIndex.indexDocument("Hello world", "Some hello world");

List<Document> documents
= inMemoryLuceneIndex.searchIndex("body", "world");

assertEquals(
"Hello world",
documents.get(0).get("title"));
}

Здесь мы добавляем в индекс простой документ с двумя полями «заголовок» и «тело», а затем пытаемся найти его с помощью поискового запроса.

6. Запросы Lucene

Поскольку мы уже знакомы с основами индексации и поиска, давайте копнем немного глубже.

В предыдущих разделах мы рассмотрели базовый синтаксис запроса и то, как преобразовать его в экземпляр Query с помощью метода QueryParser.

Lucene также предоставляет различные конкретные реализации:

6.1. TermQuery

Термин — это базовая единица для поиска, содержащая имя поля вместе с текстом для поиска.

TermQuery — самый простой из всех запросов, состоящий из одного термина:

@Test
public void givenTermQueryWhenFetchedDocumentThenCorrect() {
InMemoryLuceneIndex inMemoryLuceneIndex
= new InMemoryLuceneIndex(new RAMDirectory(), new StandardAnalyzer());
inMemoryLuceneIndex.indexDocument("activity", "running in track");
inMemoryLuceneIndex.indexDocument("activity", "Cars are running on road");

Term term = new Term("body", "running");
Query query = new TermQuery(term);

List<Document> documents = inMemoryLuceneIndex.searchIndex(query);
assertEquals(2, documents.size());
}

6.2. ПрефиксЗапрос

Чтобы найти документ со словом «начинается с»:

@Test
public void givenPrefixQueryWhenFetchedDocumentThenCorrect() {
InMemoryLuceneIndex inMemoryLuceneIndex
= new InMemoryLuceneIndex(new RAMDirectory(), new StandardAnalyzer());
inMemoryLuceneIndex.indexDocument("article", "Lucene introduction");
inMemoryLuceneIndex.indexDocument("article", "Introduction to Lucene");

Term term = new Term("body", "intro");
Query query = new PrefixQuery(term);

List<Document> documents = inMemoryLuceneIndex.searchIndex(query);
assertEquals(2, documents.size());
}

6.3. Подстановочный знак

Как следует из названия, мы можем использовать подстановочные знаки «*» или «?». для поиска:

// ...
Term term = new Term("body", "intro*");
Query query = new WildcardQuery(term);
// ...

6.4. Фразезапрос

Он используется для поиска последовательности текстов в документе:

// ...
inMemoryLuceneIndex.indexDocument(
"quotes",
"A rose by any other name would smell as sweet.");

Query query = new PhraseQuery(
1, "body", new BytesRef("smell"), new BytesRef("sweet"));

List<Document> documents = inMemoryLuceneIndex.searchIndex(query);
// ...

Обратите внимание, что первый аргумент конструктора PhraseQuery называется slop и представляет собой расстояние в количестве слов между сопоставляемыми терминами.

6.5. нечеткий запрос

Мы можем использовать это при поиске чего-то похожего, но не обязательно идентичного:

// ...
inMemoryLuceneIndex.indexDocument("article", "Halloween Festival");
inMemoryLuceneIndex.indexDocument("decoration", "Decorations for Halloween");

Term term = new Term("body", "hallowen");
Query query = new FuzzyQuery(term);

List<Document> documents = inMemoryLuceneIndex.searchIndex(query);
// ...

Мы попытались найти текст «Хэллоуин», но с ошибкой в написании «хэллоуин».

6.6. логический запрос

Иногда нам может понадобиться выполнить сложный поиск, объединяя два или более разных типа запросов:

// ...
inMemoryLuceneIndex.indexDocument("Destination", "Las Vegas singapore car");
inMemoryLuceneIndex.indexDocument("Commutes in singapore", "Bus Car Bikes");

Term term1 = new Term("body", "singapore");
Term term2 = new Term("body", "car");

TermQuery query1 = new TermQuery(term1);
TermQuery query2 = new TermQuery(term2);

BooleanQuery booleanQuery
= new BooleanQuery.Builder()
.add(query1, BooleanClause.Occur.MUST)
.add(query2, BooleanClause.Occur.MUST)
.build();
// ...

7. Сортировка результатов поиска

Мы также можем сортировать документы результатов поиска по определенным полям:

@Test
public void givenSortFieldWhenSortedThenCorrect() {
InMemoryLuceneIndex inMemoryLuceneIndex
= new InMemoryLuceneIndex(new RAMDirectory(), new StandardAnalyzer());
inMemoryLuceneIndex.indexDocument("Ganges", "River in India");
inMemoryLuceneIndex.indexDocument("Mekong", "This river flows in south Asia");
inMemoryLuceneIndex.indexDocument("Amazon", "Rain forest river");
inMemoryLuceneIndex.indexDocument("Rhine", "Belongs to Europe");
inMemoryLuceneIndex.indexDocument("Nile", "Longest River");

Term term = new Term("body", "river");
Query query = new WildcardQuery(term);

SortField sortField
= new SortField("title", SortField.Type.STRING_VAL, false);
Sort sortByTitle = new Sort(sortField);

List<Document> documents
= inMemoryLuceneIndex.searchIndex(query, sortByTitle);
assertEquals(4, documents.size());
assertEquals("Amazon", documents.get(0).getField("title").stringValue());
}

Мы попытались отсортировать полученные документы по полям заголовков, которые являются названиями рек. Логический аргумент конструктора SortField предназначен для изменения порядка сортировки.

8. Удалить документы из индекса

Попробуем удалить некоторые документы из индекса на основе заданного Term:

// ...
IndexWriterConfig indexWriterConfig = new IndexWriterConfig(analyzer);
IndexWriter writer = new IndexWriter(memoryIndex, indexWriterConfig);
writer.deleteDocuments(term);
// ...

Мы проверим это:

@Test
public void whenDocumentDeletedThenCorrect() {
InMemoryLuceneIndex inMemoryLuceneIndex
= new InMemoryLuceneIndex(new RAMDirectory(), new StandardAnalyzer());
inMemoryLuceneIndex.indexDocument("Ganges", "River in India");
inMemoryLuceneIndex.indexDocument("Mekong", "This river flows in south Asia");

Term term = new Term("title", "ganges");
inMemoryLuceneIndex.deleteDocument(term);

Query query = new TermQuery(term);

List<Document> documents = inMemoryLuceneIndex.searchIndex(query);
assertEquals(0, documents.size());
}

9. Заключение

Эта статья была кратким введением в начало работы с Apache Lucene. Также мы выполнили различные запросы и отсортировали полученные документы.

Как всегда, код примеров можно найти на Github .