Предисловие
Всем привет! Это моя первая статья на Хабре, и в ней я хочу рассказать о том, как мы можем интегрировать Elasticsearch в наше приложение на Spring Boot. Этот проект предназначен для ознакомления с технологиями и служит скорее шпаргалкой/пособием или же фундаментом для дальнейшего погружения в тему.
Введение
В эру огромных объемов данных осуществление эффективного поиска является ключевым фактором для успешного бизнеса. К счастью, разработчикам сегодня доступны инструменты, которые могут справиться с такими задачами, одним из которых является Elasticsearch.
Elasticsearch - это мощный и гибкий open-source инструмент, который позволяет создавать быстрые и масштабируемые системы для поиска и анализа данных. Он представляет собой распределенную поисковую и аналитическую систему, которая может интегрироваться с многими современными технологиями.
В этой статье мы рассмотрим, как интегрировать Elasticsearch в Spring Boot приложение для поиска пассажиров в контексте авиаперевозок. Это полезный пример, который поможет нам детально изучить, как Elasticsearch может быть использован в реальных проектах.
Будем изучать основные концепции Elasticsearch и его преимущества, а также подробно рассмотрим проект, использующий Elasticsearch для выполнения запросов на основе данных о пассажирах. Вы также узнаете, как написать код на Java для реализации функциональности поиска пассажиров с помощью Elasticsearch.
Итак, пристегните ремни, дорогие читатели, мы начинаем наше путешествие в мире Elasticsearch и Spring Boot!
Что такое Elasticsearch и для чего он необходим
Elasticsearch и его преимущества
Elasticsearch - это свободно распространяемый поисковый движок, разработанный на основе Apache Lucene. Он может использоваться для поиска, агрегирования и анализа большого объема данных. Elasticsearch способен интегрироваться с различными приложениями, используя RESTful API или различные клиентские библиотеки.
Один из ключевых принципов работы Elasticsearch заключается в том, что он разделен на несколько узлов (называемых узлами кластера), каждый из которых содержит фрагменты данных, хранящиеся в индексах. При запросе Elasticsearch отправляет запрос на все узлы кластера, а затем обрабатывает и объединяет результаты. Этот подход позволяет Elasticsearch равномерно распределять нагрузку на узлы и масштабировать систему при необходимости.
Основные преимущества Elasticsearch для поиска и анализа данных
Elasticsearch предоставляет множество преимуществ для поиска и анализа данных. Некоторые из них:
Масштабируемость: Elasticsearch способен эффективно масштабироваться до очень больших объемов данных.
Высокая скорость и производительность: Elasticsearch обеспечивает быстрый и эффективный поиск данных, что делает его подходящим для приложений с высокими нагрузками и большим объемом данных.
Низкая латентность: благодаря тому, что Elasticsearch распределенный и использует индексирование в реальном времени, он способен отвечать на запросы пользователя с низкой латентностью.
Полнотекстовый поиск: Elasticsearch предоставляет интеллектуальный полнотекстовый поиск, который позволяет быстро и эффективно находить соответствия в документах.
Гибкие запросы: в Elasticsearch существует ряд мощных запросов, позволяющих быстро и удобно получать информацию из индекса.
Различия между SQL и Elasticsearch
SQL и Elasticsearch имеют различные подходы к работе с данными. SQL является декларативным языком, который используется для запросов к реляционным базам данных, в то время как Elasticsearch - это документ-ориентированная база данных с декларативным и императивным API.
Различия между SQL и Elasticsearch заключаются в том, как они взаимодействуют с данными. SQL работает с реляционными структурами данных, в то время как Elasticsearch основан на документах, содержащих много полей. Elasticsearch позволяет проводить поиск по строкам данных, а SQL - по столбцам.
Кроме того, Elasticsearch использует многие мощные функции поиска, агрегации и анализа, которые не доступны в SQL. Elasticsearch также предоставляет более низкоуровневый доступ к данным, потому что он напрямую работает с индексами, в то время как SQL запросы обрабатывают базу данных через промежуточный язык SQL.
Перейдем к примерам из реальной жизни
Вышеперечисленные преимущества могут звучать немного сухо, поэтому давайте проиллюстрируем, как конкретный пример может показать, что мы можем получить преимущества от этой технологии:
Допустим, у нас есть таблица с именами пользователей в нашей абстрактной базе данных. Наша социальная сеть требует поиска друзей пользователей.
Давайте найдем нашего друга Александра Александрова. Похоже, что это довольно просто, верно?
select * from profile where full_name ='Александр Александров'
А теперь представим что мы крайне популярный пользователь социальной сети и мы случайно забыли фамилию нашего нашего друга (с кем не бывает)
select * from profile where full_name like 'Александр %'
Окей,все еще не так уж и плохо. Александра мы, возможно, найдем
Но что, если искомый Александр переехал, скажем, в Америку, и его имя написано в другой транскрипции или он решил переименоваться в "Саню" - как нам теперь найти его? Какова будет скорость поиска?
Конечно, вы можете быть гениальным разработчиком SQL или иметь умение щелкать алгоритмы текстового поиска, как семечки (а если у вас это получается, то вы большой молодец!), но использование инструментов, которые могут решить эти проблемы за вас, гораздо более экономически выгодно для продукта.
И здесь Elasticsearch приходит на помощь!
Чтобы найти друга Александра в такой ситуации, мы можем использовать мощные возможности Elasticsearch для поиска по неполному соответствию вводимых данных. В данном случае, мы могли бы создать индекс в Elasticsearch, содержащий информацию об именах друзей нашей абстрактной социальной сети, и использовать поисковый запрос для поиска друга Александра не только по полному соответствию имени, но и по частичному совпадению, транслитерации, креативным вариантам написания и т.д.
Если использовать Elasticsearch, мы также можем использовать фильтры, чтобы ограничить результаты поиска до тех пользователей, которые могут быть потенциальными друзьями или искомым другом. Все это позволит нам эффективно искать друзей, несмотря на неполноту или ошибки в вводе данных, что может очень сильно помочь пользователям нашей социальной сети.
Как говорится, "А ну, Александры, давайте сюда!"
### нашли Александров
GET profile/_search
{
"query": {
"match": {
"name": "Александр"
}
}
}
"Бам!"
"А ну вылазьте, Сани"
### нашли Сань
### В этом запросе мы используем оператор "fuzzy",
### который позволяет выполнить нечеткий поиск.
### Значение "fuzziness" определяет, насколько сильно мы можем изменять запрос,
### чтобы выполнить поиск по похожим словам.
GET profile/_search
{
"query": {
"fuzzy": {
"name": {
"value": "Александр",
"fuzziness": 2
}
}
}
}
"Бам!"
"Второй"
### создали фонетические фильтры для поиска с разной транскрипцией для поиска Alexandr`ов
"analyzer": {
"my_analyzer": {
"tokenizer": "standard",
"filter": [
"standard",
"my_phonetic_english",
"my_phonetic_cyrillic"
]
}
},
"filter": {
"my_phonetic_cyrillic": {
"type": "phonetic",
"encoder": "beider_morse",
"rule_type" : "approx",
"name_type" : "generic",
"languageset" : ["cyrillic"]
},
"my_phonetic_english": {
"type": "phonetic",
"encoder": "beider_morse",
"rule_type" : "approx",
"name_type" : "generic",
"languageset" : ["english"]
}
"Бам!"
Выглядит как настоящее приключение, не правда ли? Давайте узнаем больше о том, как использовать Elasticsearch и Spring Boot для разработки решений в следующих разделах статьи.
Приступим к коду
Давайте немного отвлечемся от поиска Александров и перейдем непосредственно к реализации нашего проекта. Я возьму за основу следующую бизнес-модель - поиск пассажиров в контексте авиаперевозок.
Наша задача:
Подключиться к основной базе продукта и актуализировать базу Elasticsearch на ее основе.
Интегрировать Elasticsearch с Spring Boot.
Разработать API (REST, UI) для поиска пассажиров по имени.
Задач не так уж много, но выполнение их поможет нам научиться взаимодействовать с Elasticsearch.
В проекте будут использоваться следующие технологии:
Java 11
Spring Boot 2.3.6
Elasticsearch 7.10.1
PostgreSQL 12.16
Kibana 7.10.1
Logstash-OSS 7.9.1
Maven
Docker
Возможно, в этом списке есть технологии, которые пока что неизведанны, но мы остановимся на каждой из них и рассмотрим, чем они могут быть полезны для нашего проекта.
Настройка окружения
Интегрировать наше Spring Boot приложение с Elasticsearch не получится без самого Elasticsearch (логично). Поэтому получим мы его в видео образа с помощью Docker, а настроим все необходимое с помощью docker-compose:
version: '3.3'
services:
db:
image: postgres:12.16
container_name: postgres
ports:
- "5433:5432"
environment:
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=postgres
- POSTGRES_DB=demo
- config.support_escapes=true
volumes:
- ./pg_data:/var/lib/postgresql/data
- ./dump:/docker-entrypoint-initdb.d
odfe-node:
image: elasticsearch:7.10.1
logging:
driver: "json-file"
options:
max-size: "1000m"
max-file: "10"
container_name: odfe-node
environment:
- discovery.type=single-node
- node.name=odfe-node
- discovery.seed_hosts=odfe-node
- bootstrap.memory_lock=true
- xpack.security.enabled=false
- "ES_JAVA_OPTS=-Xms4096m -Xmx4096m"
ulimits:
memlock:
soft: -1
hard: -1
nofile:
soft: 65536
hard: 65536
volumes:
- ./elasticsearch_data:/usr/share/elasticsearch/data
ports:
- "9200:9200"
- "9600:9600"
networks:
- odfe-net
kibana:
image: kibana:7.10.1
logging:
driver: "json-file"
options:
max-size: "100m"
max-file: "3"
container_name: odfe-kibana
ports:
- "5601:5601"
expose:
- "5601"
environment:
ELASTICSEARCH_URL: http://odfe-node:9200
ELASTICSEARCH_HOSTS: http://odfe-node:9200
networks:
- odfe-net
logstash:
user: root
image: docker.elastic.co/logstash/logstash-oss:7.9.1
logging:
driver: "json-file"
options:
max-size: "100m"
max-file: "3"
ports:
- "5044:5044"
depends_on:
- db
- odfe-node
environment:
- PIPELINE_WORKERS=1
- PIPELINE_BATCH_SIZE=125
- PIPELINE_BATCH_DELAY=50
volumes:
- ./conf/logstash.conf:/usr/share/logstash/pipeline/logstash.conf
- ./logstash_data:/usr/share/logstash/data
- ./conf/postgresql-42.6.0.jar:/usr/share/logstash/postgresql-42.6.0.jar
networks:
- odfe-net
networks:
odfe-net:
volumes:
odfe-data:
db:
logstash:
Круто? Круто! А что здесь вообще происходит?
Давайте кратко:
docker-compose.yml
файл описывает среду, необходимую для запуска приложения.
Среда включает в себя:
PostgreSQL контейнер для хранения данных
Elasticsearch контейнер для поиска и хранения данных
Kibana контейнер для визуализации Elasticsearch данных
Logstash контейнер для сбора, обработки и отправки данных в Elasticsearch (в нашем конкретном примере мы используем данный инструмент для актуализации индекса Elasticsearch нашими данными из PostgreSQL)
Описание сервисов
PostgreSQL
image: postgres:12.16
- используемый образ для контейнера PostgreSQLcontainer_name: postgres
- имя контейнераports: - "5433:5432"
- прокси-порт, который привязывает порт на хостовой машине к порту внутри контейнераenvironment
- переменные окружения, необходимые для настройки контейнера-
volumes
- подключенные директории на хостовой машине/pg_data
- директория для хранения данных PostgreSQL/dump
- директория, которая используется для инициализации базы данных при запуске контейнера
Elasticsearch
image: elasticsearch:7.10.1
- используемый образ для контейнера Elasticsearchcontainer_name: odfe-node
- имя контейнераlogging
- настройки логгирования контейнераenvironment
- переменные окружения, необходимые для настройки контейнераulimits
- ограничения ресурсов для контейнера, настройка системных ограничений на контейнер Elasticsearch-
volumes
- подключенные директории на хостовой машине/elasticsearch_data
- директория для хранения данных Elasticsearch
ports
- прокси-порт, который привязывает порт на хостовой машине к порту внутри контейнераnetworks
- подключенные сети
Kibana
image: kibana:7.10.1
- используемый образ для контейнера Kibanacontainer_name: odfe-kibana
- имя контейнераlogging
- настройки логгирования контейнераports
- прокси-порт, который привязывает порт на хостовой машине к порту внутри контейнераexpose
- соотносит порты и IP-адреса, чтобы они стали доступными другим контейнерамenvironment
- переменные окружения, необходимые для настройки контейнераnetworks
- подключенные сети
Logstash
image:
docker.elastic.co/logstash/logstash-oss:7.9.1 - используемый образ для контейнера Logstashlogging
- настройки логгирования контейнераports
- прокси-порт, который привязывает порт на хостовой машине к порту внутри контейнера
Отлично, с этим разобрались. Теперь двигаемся непосредственно к проекту.
Как уже ранее говорилось, Logstash нам нужен для актуализации индекса Elasticsearch нашими данными из PostgreSQL (но его возможности нам этом не заканчиваются).
input {
jdbc {
jdbc_driver_library => "/usr/share/logstash/postgresql-42.6.0.jar"
jdbc_driver_class => "org.postgresql.Driver"
jdbc_connection_string => "jdbc:postgresql://host.docker.internal:5433/demo"
jdbc_user => "postgres"
jdbc_password => "postgres"
schedule => "*/10 * * * *"
statement => "SELECT ticket_no, book_ref, passenger_id, passenger_name FROM bookings.tickets"
}
}
output {
stdout {
codec => rubydebug
}
}
output {
elasticsearch {
hosts => ["odfe-node:9200"]
index => "tickets"
doc_as_upsert => true
action => "update"
document_id => "%{ticket_no}"
}
}
Этот файл конфигурации Logstash позволяет осуществлять периодический импорт данных из PostgreSQL в Elasticsearch.
В блоке "input" мы используем JDBC input plugin, который позволяет нам осуществлять чтение данных из базы данных. Мы указываем путь к драйверу JDBC, который нужен для подключения к PostgreSQL, а также указываем параметры подключения - имя базы данных, имя пользователя и пароль. Затем мы указываем расписание выполнения запроса (каждые 10 минут) и запрос на чтение данных из таблицы "bookings.tickets".
В блоке "output" мы указываем два выхода для сохранения полученных данных. Первый блок позволяет выводить полученные данные на консоль Logstash для отладки, используя codec "rubydebug". Второй блок позволяет отправлять полученные данные в Elasticsearch для дальнейшего поиска и анализа. Мы указываем хосты Elasticsearch и индекс, в который мы будем сохранять данные. Кроме того, мы используем параметры "doc_as_upsert", "action" и "document_id", чтобы Elasticsearch корректно обрабатывал документы при выполнении индексации.
Реализация приложения
Зависимости
Не забываем, что для взаимодействия нам необходимо подтянуть зависимости. В моем случае я использую maven. Перечислим основные:
Сам Spring Boot:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.6.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
Интеграция с Elasticsearch:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
Инструменты для предоставления API:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
Держим связь с Postgresql:
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
Движок шаблонов(UI):
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
Конфигурация
@Configuration
@EnableElasticsearchRepositories(basePackages
= "com.example.elasticsearch.repository")
@ComponentScan(basePackages = { "com.example.elasticsearch" })
public class ElasticsearchClientConfig extends
AbstractElasticsearchConfiguration {
@Override
@Bean
public RestHighLevelClient elasticsearchClient() {
RestClientBuilder builder = RestClient.builder(
new HttpHost("localhost", 9200))
.setRequestConfigCallback(
requestConfigBuilder -> requestConfigBuilder
.setConnectionRequestTimeout(0));
return new RestHighLevelClient(builder);
}
}
Данный класс представляет собой конфигурацию для подключения и использования Elasticsearch в приложении на Spring Boot.
Аннотация @Configuration
сообщает Spring о том, что данный класс представляет конфигурации для приложения.
Аннотация @EnableElasticsearchRepositories
сообщает Spring о том, что данный класс будет использоваться для настройки репозиториев Elasticsearch, в которых мы будем хранить и извлекать данные из нашего индекса.
Аннотация @ComponentScan
сообщает Spring о том, какие компоненты нужно сканировать в поисках бинов. В данном случае Spring будет сканировать пакет "com.example.elasticsearch".
Класс ElasticsearchClientConfig
также наследует AbstractElasticsearchConfiguration
, который предоставляет базовую конфигурацию для подключения к Elasticsearch. Метод elasticsearchClient()
переопределяет эту базовую конфигурацию и создает экземпляр клиента RestHighLevelClient
для общения с Elasticsearch.
В методе elasticsearchClient()
мы создаем объект RestClientBuilder
, который используется для создания RestHighLevelClient
. Мы указываем HttpHost
и порт, на котором работает Elasticsearch.
В дополнение к этому, мы указываем setRequestConfigCallback
, чтобы сконфигурировать тайм-аут для соединения. В данном случае мы устанавливаем его в 0, что означает, что тайм-аут для соединения не ограничен.
Маппим данные
@Getter
@Setter
@Document(indexName = "tickets")
@ToString
public class Ticket {
@Id
private String ticket_no;
@Field(type = FieldType.Text, name = "book_ref")
private String bookRef;
@Field(type = FieldType.Text, name = "passenger_id")
private String passengerId;
@Field(type = FieldType.Text, name = "passenger_name")
private String passengerName;
}
Этот класс представляет модель данных билета на авиарейс в контексте использования Elasticsearch для индексации и поиска данных.
Аннотация @Document
указывает, что объекты класса Ticket будут храниться в формате документов в Elasticsearch в индексе "tickets".
Аннотация @Getter
и @Setter
генерируют для каждого поля класса соответствующие методы get и set, что позволяет обращаться к полям объекта через методы, а не напрямую к полям класса.
Аннотация @Id
указывает, что поле "ticket_no" является уникальным идентификатором документа в Elasticsearch.
Аннотация @Field
определяет свойства поля, которые будут использоваться при индексации документов в Elasticsearch. Здесь мы используем тип FieldType.Text
для индексации поля в виде текста и указываем имя этого поля в Elasticsearch с помощью параметра name
. Например, поле passenger_id
будет индексироваться в Elasticsearch с именем passenger_id
. Это позволяет нам более гибко управлять, как поля будут индексироваться в Elasticsearch.
Таким образом, этот класс Ticket представляет модель данных, которую мы будем использовать для индексации билетов в Elasticsearch. Он содержит поля для хранения информации о билете, аннотации для управления индексацией полей в Elasticsearch и методы для удобства работы с объектами этого класса.
Сервисы по поиску
В целых ознакомления я реализовал 2 простых сервиса, чья задача проста - искать пользователя по имени\фамилии. Различие сервисов состоит лишь в подходах:
TicketServiceWithRepo
- используем для поиска репозитории (ElasticsearchRepository
);
TicketServiceWithNativeQuery
- конструируем запрос самостоятельно.
TicketServiceWithRepo
@Service
@RequiredArgsConstructor
public class TicketServiceWithRepo {
private final TicketRepository ticketRepository;
private final TicketMapper ticketMapper;
public Ticket saveIndex(SaveTicketIndexRequest request){
return ticketRepository.save(ticketMapper.ticketDtoToTicket(request.getTicketDto()));
}
public List<Ticket> saveBulkIndex(SaveTicketBulkIndexRequest request) {
List<Ticket> tickets = request
.getTickets()
.stream()
.map(ticketDto -> ticketMapper.ticketDtoToTicket(ticketDto))
.collect(Collectors.toList());
return (List<Ticket>) ticketRepository.saveAll(tickets);
}
public FindByNameContainingResponse findByNameContaining(String name){
List<TicketDto> ticketDtos = ticketRepository
.findByPassengerNameContaining(name)
.stream()
.map(ticket -> ticketMapper.ticketToTicketDto(ticket))
.collect(Collectors.toList());
return FindByNameContainingResponse
.builder()
.tickets(ticketDtos)
.build();
}
}
Поля ticketRepository
и ticketMapper
инициализируются через конструктор. ticketRepository
это интерфейс, реализующий Spring Data Elasticsearch CRUD-операции. ticketMapper
это отдельный класс, который используется для маппинга объектов между DTO и сущностями, чтобы передать их в репозиторий или вернуть в ответ на запрос от клиента.
Метод saveIndex
сохраняет билет в Elasticsearch. Он принимает объект SaveTicketIndexRequest
и после маппинга через ticketMapper
сохраняет результат с помощью ticketRepository
. Данный метод возвращает объект Ticket, который соответствует сохраненному документу в Elasticsearch.
Метод saveBulkIndex
сохраняет несколько билетов в Elasticsearch одновременно. Он принимает объект SaveTicketBulkIndexRequest
, извлекает список билетов, формирует список объектов Ticket
при помощи маппера и сохраняет все билеты в Elasticsearch через ticketRepository
. Этот метод также возвращает список объектов Ticket, которые соответствуют сохраненным документам в Elasticsearch.
Метод findByNameContaining
ищет билеты в Elasticsearch по имени пассажира, содержащему данную строку name
. Он обращается к репозиторию Elasticsearch, используя метод findByPassengerNameContaining
, который возвращает список документов, удовлетворяющих этому условию. Затем результат преобразуется в список объектов TicketDto
через маппер ticketMapper
и возвращается как часть объекта типа FindByNameContainingResponse
, который содержит в себе список найденных билетов.
Таким образом, класс TicketServiceWithRepo
представляет собой сервисный слой приложения для выполнения операций индексации и поиска данных в Elasticsearch с использованием Spring Data Elasticsearch. Он использует Spring IoC-контейнер для инъекции зависимостей, маппер для преобразования между различными типами объектов, и репозиторий Elasticsearch для выполнения CRUD-операций.
TicketRepository
Класс репозитория, тут Spring все сделает за нас
@Repository
public interface TicketRepository extends ElasticsearchRepository<Ticket, String> {
List<Ticket> findByPassengerNameContaining(String name);
}
TicketServiceWithNativeQuery
@Service
@RequiredArgsConstructor
public class TicketServiceWithNativeQuery {
private static final String TICKET_INDEX = "tickets";
private final ElasticsearchOperations elasticsearchOperations;
public List<Ticket> processSearch(String passengerName) {
QueryBuilder queryBuilder =
QueryBuilders
.matchQuery("passenger_name", passengerName)
.fuzziness(Fuzziness.AUTO);
Query searchQuery = new NativeSearchQueryBuilder()
.withFilter(queryBuilder)
.withPageable(PageRequest.of(0, 5))
.build();
SearchHits<Ticket> productHits =
elasticsearchOperations
.search(searchQuery,
Ticket.class,
IndexCoordinates.of(TICKET_INDEX));
List<Ticket> ticketMatches = new ArrayList<>();
productHits.forEach(searchHit -> {
ticketMatches.add(searchHit.getContent());
});
return ticketMatches;
}
public List<String> fetchSuggestions(String query) {
QueryBuilder queryBuilder = QueryBuilders
.wildcardQuery("passenger_name", query+"*");
Query searchQuery = new NativeSearchQueryBuilder()
.withFilter(queryBuilder)
.withPageable(PageRequest.of(0, 5))
.build();
SearchHits<Ticket> searchSuggestions =
elasticsearchOperations.search(searchQuery,
Ticket.class,
IndexCoordinates.of(TICKET_INDEX));
List<String> suggestions = new ArrayList<>();
searchSuggestions.getSearchHits().forEach(searchHit->{
suggestions.add(searchHit.getContent().getPassengerName());
});
return suggestions;
}
}
Класс TicketServiceWithNativeQuery
является сервисом для работы с Elasticsearch в проекте. Он содержит два метода: processSearch
и fetchSuggestions
, которые выполняют поиск и предложения в индексе Elasticsearch для заданного запроса в соответствии с предоставленными параметрами.
Метод processSearch
принимает параметр passengerName
, который является строкой, содержащей имя пассажира. Затем метод конструирует объект QueryBuilder
, который соответствует поисковому запросу Elasticsearch, с использованием matchQuery
для настройки поиска с опечатками, и fuzziness
для задания автоматической настройки опечаток. Затем метод инициирует запрос к Elasticsearch с использованием объекта NativeSearchQueryBuilder
и передает созданный QueryBuilder
в фильтр для выполнения поиска. Метод возвращает список типа Ticket
, который содержит все найденные билеты.
Метод fetchSuggestions
принимает параметр query
, который является строкой запроса для предложений. Затем метод создает объект QueryBuilder
, который соответствует поисковому запросу Elasticsearch, с использованием wildcardQuery
для настройки поиска Elasticsearch с применением маски и PageRequest
для выполнения запроса первых пяти результатов. Затем метод инициирует запрос к Elasticsearch с использованием объекта NativeSearchQueryBuilder
и передает созданный QueryBuilder
в фильтр для выполнения поиска. Метод возвращает список String
, который содержит все найденные предложения.
Контроллеры
наше приложение предоставляет 2 интерфейса: UI и REST
UIController
@Controller
public class UIController {
@GetMapping("/search")
public String home(Model model) {
return "search";
}
}
тут мы отдаем по эндпоитну /search заготовленную страницу search.html
TicketController
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/v1/tickets")
public class TicketController {
private final TicketServiceWithRepo ticketServiceWithRepo;
private final TicketServiceWithNativeQuery ticketServiceWithNativeQuery;
@PostMapping("/repo")
public ResponseEntity saveIndexWithRepo(@RequestBody SaveTicketIndexRequest saveTicketIndexRequest) {
ticketServiceWithRepo.saveIndex(saveTicketIndexRequest);
return ResponseEntity.ok().build();
}
@PostMapping("/repo/bulk")
public ResponseEntity saveIndexWithRepo(@RequestBody SaveTicketBulkIndexRequest saveTicketBulkIndexRequest) {
ticketServiceWithRepo.saveBulkIndex(saveTicketBulkIndexRequest);
return ResponseEntity.ok().build();
}
@GetMapping("/repo")
public ResponseEntity<FindByNameContainingResponse> findByNameContainingWithRepo
(@RequestParam("name") String name) {
FindByNameContainingResponse findByNameContainingResponse =
ticketServiceWithRepo.findByNameContaining(name);
return ResponseEntity.ok().body(findByNameContainingResponse);
}
@GetMapping("/native")
@ResponseBody
public List<Ticket> fetchByNameOrDesc(@RequestParam(value = "q", required = false)
String query) {
List<Ticket> tickets = ticketServiceWithNativeQuery.processSearch(query) ;
return tickets;
}
@GetMapping("/native/suggestions")
@ResponseBody
public List<String> fetchSuggestions(@RequestParam(value = "q", required = false) String query) {
List<String> suggests = ticketServiceWithNativeQuery.fetchSuggestions(query);
return suggests;
}
}
Данный класс TicketController
представляет собой контроллер API, который реализует RESTful запросы HTTP для работы с объектами Ticket
. Контроллер использует Spring Framework для управления запросами и ответами, а также для взаимодействия с сервисами для обработки запросов.
Аннотация @RestController
описывает класс как контроллер, который работает с RESTful API, а конкретно – контроллер, который всегда возвращает ответ в формате JSON.
Аннотация @RequiredArgsConstructor
сгенерирует конструктор для зависимостей класса. Это означает, что поля ticketServiceWithRepo
и ticketServiceWithNativeQuery
будут автоматически инициализироваться с помощью конструктора, без необходимости явно его задавать.
Аннотация @RequestMapping
определяет базовый URI, который будет использоваться для обработки всех запросов в контроллере. URI в данном случае - "/api/v1/tickets".
Методы saveIndexWithRepo
и saveIndexWithRepo
реализуют POST-запросы в API и соответственно сохраняют индексы последовательно и в булковом режиме, используя сервисы ticketServiceWithRepo
. Они принимают GET-запрос для поиска объектов Ticket
, содержащих заданный текст в имени используя сервис ticketServiceWithRepo
.
Методы fetchByNameOrDesc
и fetchSuggestions
реализуют GET-запросы в API и получают все объекты Ticket
из индекса, соответствующие заданному запросу, используя сервис ticketServiceWithNativeQuery
. Метод fetchSuggestions
возвращает список предложений, основанных на введенном пользователем запросе.
Поехали!
Теперь, когда у нас готова инфраструктура и само приложение, мы можем запустить все элементы и посмотреть к чему пришли.
Запускаем контейнеры:
*Если вы из России, то тут может понадобиться включить vpn из-за региональных ограничений у некоторых образов
docker-compose up -d
Проверяем:
docker-compose ps
Если все отлично, то увидим:
Name Command State Ports
----------------------------------------------------------------------------------------
elasticsearch-spring-demo_db_1 docker-entrypoint.sh postgres Up 0.0.0.0:5433->5432/tcp
elasticsearch-spring-demo_kibana_1 /usr/local/bin/dumb-init ... Up 0.0.0.0:5601->5601/tcp
elasticsearch-spring-demo_logstash_1 /usr/local/bin/docker-entr ... Up 0.0.0.0:5044->5044/tcp
elasticsearch-spring-demo_odfe-node_1 /usr/local/bin/docker-entr ... Up 0.0.0.0:9200->9200/tcp, 0.0.0.0:9600->9600/tcp
Что происходит после запуска:
PostrgreSql наполняется данными из дампа
Logstash переносит необходимые данные в Elasticsearch
Kibana отображает данные, составляет графики и дашборды (если у вас есть необходимость в этом)
Проверим наполнение данных:
Поиграемся с диаграммами и сделаем, например, пончик:
Запускаем само приложение:
mvn spring-boot:run
Перейдем на эндпоинт UI и найдем пассажира:
Отлично! Теперь проверим REST API:
###our requst to add element to ticket index (repo)
POST http://localhost:8080/api/v1/tickets/repo
Content-Type: application/json
{
"ticketDto": {
"ticket_no": "12345",
"bookRef": "ABC123",
"passengerId": "P123",
"passengerName": "John Doe"
}
}
Теперь проверим поиск:
Запрос:
###our requst to get tickets by containing name (repo)
GET http://localhost:8080/api/v1/tickets/repo?name=MAKSIM
Content-Type: application/json
Ответ:
{
"tickets": [
{
"ticket_no": "0005432384059",
"bookRef": "3873F4",
"passengerId": "0917 258591",
"passengerName": "MAKSIM PAVLOV"
},
{
"ticket_no": "0005432384107",
"bookRef": "519855",
"passengerId": "4591 778259",
"passengerName": "VALENTINA MAKSIMOVA"
},
{
"ticket_no": "0005432384206",
"bookRef": "3AAC78",
"passengerId": "2284 578245",
"passengerName": "MAKSIM MEDVEDEV"
},......
Как отлично, когда все Максимы на месте!
Заключение
В этой статье мы рассмотрели интеграцию Elasticsearch и Spring Boot в проектах Java в приложении для работы с данными авиарейсов.
Мы начали с разбора архитектуры Elasticsearch и его ключевых компонентов, в том числе индексов, типов документов, запросов и агрегаций.
Затем мы обсудили различные подходы к интеграции Elasticsearch и Spring Boot. Мы рассмотрели использование Spring Data Elasticsearch для выполнения CRUD-операций и Spring Elasticsearch для определения запросов в стиле Спринг и выполнения агрегационных запросов.
Мы также рассмотрели использование нативного API Elasticsearch для выполнения запросов в Spring Boot с помощью класса ElasticsearchOperations
.
В качестве примера мы построили простенькое приложение на Spring Boot, которое использует Elasticsearch для индексации и поиска билетов на авиарейсы.
Надеюсь, данная статья помогла вам разобраться в интеграции Elasticsearch и Spring Boot, а также облегчит процесс разработки Java-приложений, связанных с работой с данными. Благодарю вас за прочтение до конца! Пожалуйста, оставляйте свои комментарии, пожелания и возражения - это будет большим опытом для меня!
С исходным кодом можно ознакомиться тут.
Комментарии (13)
ciplenok57
11.10.2023 09:29Крутой материал, спасибо!
Если собирать контейнеры из России, то сборка упадет на образе logstash, так как docker.elastic.co блочит подключения. Либо это локальная проблемаХотелось бы подобнее узнать про связку pg + elastic. Исходя из описания мы записываем в pg, потом у нас идет перенос данных в elstic. Но в примере из гита мы пишем напрямую в elastic. В целом, нужен ли тут pg, почему бы не писать данные сразу в elastic?
IrlkKvch Автор
11.10.2023 09:29да, подключение действительно блочит, для подгрузки можно воспользоваться vpn (забыл указать это в статье, вскоре дополню. Спасибо за замечание!)
"Хотелось бы подобнее узнать про связку pg + elastic. Исходя из описания мы записываем в pg, потом у нас идет перенос данных в elstic. Но в примере из гита мы пишем напрямую в elastic. В целом, нужен ли тут pg, почему бы не писать данные сразу в elastic? " - в конкретном примере у нас происходит обновление данных в соответствии с pg (например , если у нас cqrs: pg - основная база, elastic - база для чтения (засовываем и компонуем туда все самое необходимое)). В примере из гита импорт данных в эластик идет из дампа и обновляется по крону - это лишь абстрактный пример для понимания принципов взаимодействия и возможностей плавной интеграции с pg
ciplenok57
11.10.2023 09:29Спасибо!
Чтобы не включать VPN, можно воспользоваться образом из докерхаба
https://hub.docker.com/r/opensearchproject/logstash-oss-with-opensearch-output-pluginDonAlPAtino
11.10.2023 09:29А что надо в docker-compose поменять чтобы он завелся? Конфиг у него где-то в другом месте лежит?
ciplenok57
11.10.2023 09:29+1Можно просто склонить репу с гита и в композе поменять образ logstash на
image: opensearchproject/logstash-oss-with-opensearch-output-plugin:latest
В гите есть все конфиги
DonAlPAtino
11.10.2023 09:29Собственно не завелось. Падало с can't connect to postgress. Вот я и спросил... Скаченный через VPN logtash сработал без вопросов
IrlkKvch Автор
11.10.2023 09:29обновил статью и гит
DonAlPAtino
11.10.2023 09:292023-10-12T12:32:44,758][ERROR][logstash.inputs.jdbc ][main] Unable to connect to database. Tried 1 times {:message=>"Java::OrgPostgresqlUtil::PSQLException: The connection attempt failed.", :exception=>Sequel::DatabaseConnectionError, :cause=>#<Java::OrgPostgresqlUtil::PSQLException: The connection attempt failed.>, :backtrace=>["org.postgresql.core.v3.ConnectionFactoryImpl.openConnectionImpl(org/postgresql/core/v3/ConnectionFactoryImpl.java:354)", "org.postgresql.core.ConnectionFactory.openConnection(org/postgresql/core/ConnectionFactory.java:54)", "org.postgresql.jdbc.PgConnection.(org/postgresql/jdbc/PgConnection.java:263)", "org.postgresql.Driver.makeConnection(org/postgresql/Driver.java:443)", "org.postgresql.Driver.connect(org/postgresql/Driver.java:297)
c docker.elastic.co/logstash/logstash-oss:7.9.1 все работает. Винда, docker desktop
artemnekrasov18
Кайфанул, спасибо????