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

Простая реализация тегов с помощью Elasticsearch

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

1. Обзор

Тегирование — это распространенный шаблон проектирования, который позволяет нам классифицировать и фильтровать элементы в нашей модели данных.

В этой статье мы реализуем тегирование с помощью Spring и Elasticsearch. Мы будем использовать Spring Data и Elasticsearch API.

Прежде всего, мы не собираемся описывать основы получения Elasticsearch и Spring Data — вы можете изучить их здесь .

2. Добавление тегов

Простейшая реализация тегирования — это массив строк. Мы можем реализовать это, добавив новое поле в нашу модель данных следующим образом:

@Document(indexName = "blog", type = "article")
public class Article {

// ...

@Field(type = Keyword)
private String[] tags;

// ...
}

Обратите внимание на использование типа поля Ключевое слово . Нам нужны только точные совпадения наших тегов для фильтрации результата. Это позволяет нам использовать похожие, но отдельные теги, такие как elasticsearchIsAwesome и elasticsearchIsTerrible .

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

3. Создание запросов

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

3.1. Поиск тегов

Новое поле тега , которое мы создали в нашей модели, такое же, как и любое другое поле в нашем индексе. Мы можем искать любую сущность, которая имеет определенный тег, например:

@Query("{\"bool\": {\"must\": [{\"match\": {\"tags\": \"?0\"}}]}}")
Page<Article> findByTagUsingDeclaredQuery(String tag, Pageable pageable);

В этом примере для создания нашего запроса используется репозиторий данных Spring, но мы можем так же быстро использовать шаблон Rest для запроса кластера Elasticsearch вручную.

Точно так же мы можем использовать Elasticsearch API:

boolQuery().must(termQuery("tags", "elasticsearch"));

Предположим, что мы используем следующие документы в нашем индексе:

[
{
"id": 1,
"title": "Spring Data Elasticsearch",
"authors": [ { "name": "John Doe" }, { "name": "John Smith" } ],
"tags": [ "elasticsearch", "spring data" ]
},
{
"id": 2,
"title": "Search engines",
"authors": [ { "name": "John Doe" } ],
"tags": [ "search engines", "tutorial" ]
},
{
"id": 3,
"title": "Second Article About Elasticsearch",
"authors": [ { "name": "John Smith" } ],
"tags": [ "elasticsearch", "spring data" ]
},
{
"id": 4,
"title": "Elasticsearch Tutorial",
"authors": [ { "name": "John Doe" } ],
"tags": [ "elasticsearch" ]
},
]

Теперь мы можем использовать этот запрос:

Page<Article> articleByTags 
= articleService.findByTagUsingDeclaredQuery("elasticsearch", PageRequest.of(0, 10));

// articleByTags will contain 3 articles [ 1, 3, 4]
assertThat(articleByTags, containsInAnyOrder(
hasProperty("id", is(1)),
hasProperty("id", is(3)),
hasProperty("id", is(4)))
);

3.2. Фильтрация всех документов

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

Допустим, мы хотим вернуть все статьи, отфильтрованные по любому тегу, выбранному пользователем:

@Query("{\"bool\": {\"must\": " +
"{\"match_all\": {}}, \"filter\": {\"term\": {\"tags\": \"?0\" }}}}")
Page<Article> findByFilteredTagQuery(String tag, Pageable pageable);

И снова мы используем Spring Data для построения объявленного нами запроса.

Следовательно, используемый нами запрос разбит на две части. Запрос оценки — это первый термин, в данном случае match_all . Следующий запрос фильтра сообщает Elasticsearch, какие результаты следует отбросить.

Вот как мы используем этот запрос:

Page<Article> articleByTags =
articleService.findByFilteredTagQuery("elasticsearch", PageRequest.of(0, 10));

// articleByTags will contain 3 articles [ 1, 3, 4]
assertThat(articleByTags, containsInAnyOrder(
hasProperty("id", is(1)),
hasProperty("id", is(3)),
hasProperty("id", is(4)))
);

Важно понимать, что хотя это возвращает те же результаты, что и в нашем примере выше, этот запрос будет работать лучше.

3.3. Фильтрация запросов

Иногда поиск возвращает слишком много результатов, чтобы их можно было использовать. В этом случае неплохо предоставить механизм фильтрации, который может повторно выполнить тот же поиск, только с более суженными результатами.

Вот пример, когда мы сужаем количество статей, написанных автором, до статей с определенным тегом:

@Query("{\"bool\": {\"must\": " + 
"{\"match\": {\"authors.name\": \"?0\"}}, " +
"\"filter\": {\"term\": {\"tags\": \"?1\" }}}}")
Page<Article> findByAuthorsNameAndFilteredTagQuery(
String name, String tag, Pageable pageable);

Опять же, Spring Data делает всю работу за нас.

Давайте также посмотрим, как построить этот запрос самостоятельно:

QueryBuilder builder = boolQuery().must(
nestedQuery("authors", boolQuery().must(termQuery("authors.name", "doe")), ScoreMode.None))
.filter(termQuery("tags", "elasticsearch"));

Мы можем, конечно, использовать эту же технику для фильтрации любого другого поля в документе. Но теги особенно хорошо подходят для этого варианта использования.

Вот как использовать приведенный выше запрос:

SearchQuery searchQuery = new NativeSearchQueryBuilder().withQuery(builder)
.build();
List<Article> articles =
elasticsearchTemplate.queryForList(searchQuery, Article.class);

// articles contains [ 1, 4 ]
assertThat(articleByTags, containsInAnyOrder(
hasProperty("id", is(1)),
hasProperty("id", is(4)))
);

4. Контекст фильтра

Когда мы создаем запрос, нам нужно различать контекст запроса и контекст фильтра. У каждого запроса в Elasticsearch есть контекст запроса, поэтому мы должны привыкнуть к ним.

Не каждый тип запроса поддерживает контекст фильтра. Поэтому, если мы хотим фильтровать теги, нам нужно знать, какие типы запросов мы можем использовать.

Логический запрос имеет два способа доступа к контексту фильтра . Первый параметр filter — это тот, который мы использовали выше. Мы также можем использовать параметр must_not для активации контекста.

Следующий тип запроса, который мы можем отфильтровать, — Constant_score . Это полезно, когда вы хотите заменить контекст запроса результатами фильтра и присвоить каждому результату одинаковую оценку.

Последний тип запроса, который мы можем фильтровать на основе тегов, — это агрегация фильтров . Это позволяет нам создавать группы агрегации на основе результатов нашего фильтра. Другими словами, мы можем сгруппировать все статьи по тегу в нашем результате агрегирования.

5. Расширенная маркировка

До сих пор мы говорили только о тегировании с использованием самой простой реализации. Следующим логическим шагом является создание тегов, которые сами являются парами ключ-значение . Это позволило бы нам усовершенствовать наши запросы и фильтры.

Например, мы могли бы изменить наше поле тега на это:

@Field(type = Nested)
private List<Tag> tags;

Затем мы просто изменим наши фильтры, чтобы использовать типы вложенных запросов .

Как только мы поймем, как использовать пары ключ-значение , останется сделать небольшой шаг к использованию сложных объектов в качестве нашего тега. Не многим реализациям потребуется полный объект в качестве тега, но хорошо знать, что у нас есть эта опция, если она нам потребуется.

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

В этой статье мы рассмотрели основы реализации тегов с помощью Elasticsearch.

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