Простая реализация тегов с Elasticsearch

Простая реализация тегов с Elasticsearch

1. обзор

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

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

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

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

The simplest implementation of tagging is an array of strings. Мы можем реализовать это, добавив новое поле в нашу модель данных следующим образом:

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

    // ...

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

    // ...
}

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

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

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

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

3.1. Поиск по тегам

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

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

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

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

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
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. Фильтрация всех документов

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

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

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

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

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

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

Page
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
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
articles = elasticsearchTemplate.queryForList(searchQuery, Article.class); // articles contains [ 1, 4 ] assertThat(articleByTags, containsInAnyOrder( hasProperty("id", is(1)), hasProperty("id", is(4))) );

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

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

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

The bool query has two ways to access the Filter Context. Первый параметрfilter - это тот, который мы использовали выше. Мы также можем использовать параметрmust_not для активации контекста.

The next query type we can filter is constant_score. Это полезно, когда вы хотите заменить контекст запроса результатами фильтра и назначить каждому результату одинаковую оценку.

The final query type that we can filter based on tags is the filter aggregation. Это позволяет нам создавать группы агрегации на основе результатов нашего фильтра. Другими словами, мы можем сгруппировать все статьи по тегу в нашем результате агрегации.

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

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

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

@Field(type = Nested)
private List tags;

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

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

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

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

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

Related