Введение в Apache Lucene

Введение в Apache Lucene

1. обзор

Apache Lucene - это система полнотекстового поиска, которую можно использовать на разных языках программирования.

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

2. Maven Setup

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


    org.apache.lucene
    lucene-core
    7.1.0

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

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


    org.apache.lucene
    lucene-queryparser
    7.1.0

Проверьте последнюю версиюhere.

3. Основные понятия

3.1. индексирование

Проще говоря, Lucene использует «инвертированную индексацию» данных -instead of mapping pages to keywords, it maps keywords to pages, как глоссарий в конце любой книги.

Это позволяет ускорить поиск ответов при поиске по индексу вместо прямого поиска по тексту.

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 предоставляет очень динамичный и простой в написании синтаксис запроса.

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

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

fieldName:text

eg: title:tea

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

timestamp:[1509909322,1572981321]

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

dri?nk

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

d*k

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

uni*

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

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

title: "Tea in breakfast" AND "coffee"

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

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 указывает, должно ли значение поля также сохраняться или нет .

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

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

public List 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 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 documents
      = inMemoryLuceneIndex.searchIndex("body", "world");

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

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

6. Lucene запросы

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

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

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

6.1. TermQueryс

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

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 documents = inMemoryLuceneIndex.searchIndex(query);
    assertEquals(2, documents.size());
}

6.2. PrefixQueryс

Для поиска документа по слову «начинается с»:

@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 documents = inMemoryLuceneIndex.searchIndex(query);
    assertEquals(2, documents.size());
}

6.3. WildcardQueryс

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

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

6.4. PhraseQueryс

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

// ...
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 documents = inMemoryLuceneIndex.searchIndex(query);
// ...

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

6.5. FuzzyQueryс

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

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

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

List documents = inMemoryLuceneIndex.searchIndex(query);
// ...

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

6.6. BooleanQueryс

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

// ...
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 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 documents = inMemoryLuceneIndex.searchIndex(query);
    assertEquals(0, documents.size());
}

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

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

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