Сегодня многие разрабатывают enterprise-приложения на Java с использованием spring boot. В ходе проектов часто возникают задачи по созданию поисковых систем разной сложности. Например, если вы разрабатываете систему, хранящую данные о пользователях и книгах, то рано или поздно в ней может потребоваться поиск по имени/фамилии пользователя, по названиям/аннотациям для книг.

В этом посте я вкратце расскажу об инструментах, которые могут помочь в решении таких задач. А затем представлю демо-проект поискового сервиса, где реализована более интересная и сложная фича — синхронизация сущностей, БД и поискового индекса. На примере этого демо-проекта вы сможете познакомиться с Hibernate Search — удобным способом общения с полнотекстовыми индексами Solr, Lucene, ElasticSearch.
Среди инструментов для развертывания поисковых механизмов я бы выделил три.
Lucene — это java-библиотека, предоставляющая низкоуровневый интерфейс денормализованной базы данных с возможностью полнотекстового поиска. С ее помощью можно создавать индексы и наполнять их записями (документами). Подробней о Lucene можно почитать здесь.
Solr — это конечный программный продукт на основе Lucene, полнотекстовая база данных, самостоятельный отдельный веб-сервер. Имеет http-интерфейс для индексации и полнотекстовых запросов, позволяет индексировать документы и вести поиск по ним. У Solr есть простой API и встроенный UI, что избавляет пользователя от ручных манипуляций над индексами. На Хабре выходил хороший сравнительный анализ Solr и Lucene.
ElasticSearch — более современный аналог Solr. В его основе также лежит Apache Lucene. По сравнению с Solr, ElasticSearch выдерживает более высокие нагрузки при индексации документов и поэтому может быть использован для индексации лог-файлов. В сети можно найти подробную таблицу со сравнением Solr и ElasticSearch.
Это, конечно, не полный список, выше я выбрал лишь те системы, которые заслуживают наибольшего внимания. Систем для организации поиска очень много. PostgreSQL имеет возможности полнотекстового поиска; не стоит забывать и о Sphinx.
Переходим к главному. Для надежного/консистентного хранения данных обычно используется RDB (реляционная база данных). Она обеспечивает транзакционность в соответствии с принципами ACID. Для работы поисковой системы используется индекс, в который нужно добавлять сущности и те поля таблиц, по которым будет производиться поиск. То есть когда новый объект попадает в систему, его необходимо сохранить и в реляционную базу данных, и в полнотекстовый индекс.
Если внутри вашего приложения не организована транзакционность таких изменений, то могут возникнуть разного рода рассинхронизации. Например, вы производите выборку из БД, а в индексе этого объекта нет. Или наоборот: в индексе есть запись об объекте, а из RDB он был удален.
Решить эту проблему можно разными способами. Вы можете вручную организовывать транзакционность изменений при помощи механизмов JTA и Spring Transaction Management. А можете пойти более интересным путем — использовать Hibernate Search, который сделает все это сам. По умолчанию используется Lucene, хранящий данные индекса внутри файловой системы, в общем виде настраивается подключение к индексу. При старте системы вы запускаете метод синхронизации startAndWait(), и во время работы системы записи будут сохраняться в RDB и индексе.
Чтобы проиллюстрировать это решение, я подготовил демо-проект с Hibernate Search. Мы создадим сервис, содержащий методы для чтения, обновления и поиска пользователей. Он может лечь в основу внутренней базы данных с возможностью полнотекстового поиска по имени, фамилии или другим мета-данным. Для взаимодействия с реляционными базами данных используем фреймворк Spring Data Jpa.
Начнем с класса-сущности для представления пользователя:
Все стандартно, обозначаем entity всеми необходимыми аннотациями для spring data. При помощи Entity указываем сущность, при помощи Table указываем табличку в БД. Аннотация Indexed указывает, что сущность индексируемая и будет попадать в полнотекстовый индекс.
JPA-Репозиторий, необходимый для CRUD-операций над пользователями в базе данных:
Сервис для работы с пользователями, UserService.java:
FindAll достает всех пользователей непосредственно из БД. Search использует компонент userSearch для извлечения пользователей из индекса. Компонент для работы с поисковым индексом пользователей:
REST-контроллер, UserController.java:
Используем два метода, для извлечения из БД и поиска по строке.
Перед работой приложения необходимо провести инициализацию индекса, делаем это при помощи ApplicationListener'a.
Для теста использовали PostgreSQL:
И наконец,
Приведенное демо — простой пример использования технологии Hibernate Search, с помощью которого можно понять как подружить Apache Lucene и Spring Data Jpa. При необходимости проекты на основе этого демо можно подключить к Apache Solr или ElasticSearch. Потенциальное направление развития проекта — это поиск по крупным индексам (>10 ГБ) и замер производительности в них. Можно создавать конфигурации для ElasticSearch или более сложные конфигурации индексов, изучая возможности Hibernate Search на более глубоком уровне.
Полезные ссылки:

В этом посте я вкратце расскажу об инструментах, которые могут помочь в решении таких задач. А затем представлю демо-проект поискового сервиса, где реализована более интересная и сложная фича — синхронизация сущностей, БД и поискового индекса. На примере этого демо-проекта вы сможете познакомиться с Hibernate Search — удобным способом общения с полнотекстовыми индексами Solr, Lucene, ElasticSearch.
Среди инструментов для развертывания поисковых механизмов я бы выделил три.
Lucene — это java-библиотека, предоставляющая низкоуровневый интерфейс денормализованной базы данных с возможностью полнотекстового поиска. С ее помощью можно создавать индексы и наполнять их записями (документами). Подробней о Lucene можно почитать здесь.
Solr — это конечный программный продукт на основе Lucene, полнотекстовая база данных, самостоятельный отдельный веб-сервер. Имеет http-интерфейс для индексации и полнотекстовых запросов, позволяет индексировать документы и вести поиск по ним. У Solr есть простой API и встроенный UI, что избавляет пользователя от ручных манипуляций над индексами. На Хабре выходил хороший сравнительный анализ Solr и Lucene.
ElasticSearch — более современный аналог Solr. В его основе также лежит Apache Lucene. По сравнению с Solr, ElasticSearch выдерживает более высокие нагрузки при индексации документов и поэтому может быть использован для индексации лог-файлов. В сети можно найти подробную таблицу со сравнением Solr и ElasticSearch.
Это, конечно, не полный список, выше я выбрал лишь те системы, которые заслуживают наибольшего внимания. Систем для организации поиска очень много. PostgreSQL имеет возможности полнотекстового поиска; не стоит забывать и о Sphinx.
Основная проблема
Переходим к главному. Для надежного/консистентного хранения данных обычно используется RDB (реляционная база данных). Она обеспечивает транзакционность в соответствии с принципами ACID. Для работы поисковой системы используется индекс, в который нужно добавлять сущности и те поля таблиц, по которым будет производиться поиск. То есть когда новый объект попадает в систему, его необходимо сохранить и в реляционную базу данных, и в полнотекстовый индекс.
Если внутри вашего приложения не организована транзакционность таких изменений, то могут возникнуть разного рода рассинхронизации. Например, вы производите выборку из БД, а в индексе этого объекта нет. Или наоборот: в индексе есть запись об объекте, а из RDB он был удален.
Решить эту проблему можно разными способами. Вы можете вручную организовывать транзакционность изменений при помощи механизмов JTA и Spring Transaction Management. А можете пойти более интересным путем — использовать Hibernate Search, который сделает все это сам. По умолчанию используется Lucene, хранящий данные индекса внутри файловой системы, в общем виде настраивается подключение к индексу. При старте системы вы запускаете метод синхронизации startAndWait(), и во время работы системы записи будут сохраняться в RDB и индексе.
Чтобы проиллюстрировать это решение, я подготовил демо-проект с Hibernate Search. Мы создадим сервис, содержащий методы для чтения, обновления и поиска пользователей. Он может лечь в основу внутренней базы данных с возможностью полнотекстового поиска по имени, фамилии или другим мета-данным. Для взаимодействия с реляционными базами данных используем фреймворк Spring Data Jpa.
Начнем с класса-сущности для представления пользователя:
import org.hibernate.search.annotations.Field import org.hibernate.search.annotations.Indexed import javax.persistence.Entity import javax.persistence.Id import javax.persistence.Table @Entity @Table(name = "users") @Indexed internal data class User( @Id val id: Long, @Field val name: String, @Field val surname: String, @Field val phoneNumber: String)
Все стандартно, обозначаем entity всеми необходимыми аннотациями для spring data. При помощи Entity указываем сущность, при помощи Table указываем табличку в БД. Аннотация Indexed указывает, что сущность индексируемая и будет попадать в полнотекстовый индекс.
JPA-Репозиторий, необходимый для CRUD-операций над пользователями в базе данных:
internal interface UserRepository: JpaRepository<User, Long>
Сервис для работы с пользователями, UserService.java:
import org.springframework.stereotype.Service import javax.transaction.Transactional @Service @Transactional internal class UserService(private val userRepository: UserRepository, private val userSearch: UserSearch) { fun findAll(): List<User> { return userRepository.findAll() } fun search(text: String): List<User> { return userSearch.searchUsers(text) } fun saveUser(user: User): User { return userRepository.save(user) } }
FindAll достает всех пользователей непосредственно из БД. Search использует компонент userSearch для извлечения пользователей из индекса. Компонент для работы с поисковым индексом пользователей:
@Repository @Transactional internal class UserSearch(@PersistenceContext val entityManager: EntityManager) { fun searchUsers(text: String): List<User> { //извлекаем fullTextEntityManager, используя entityManager val fullTextEntityManager = org.hibernate.search.jpa.Search.getFullTextEntityManager(entityManager) // создаем запрос при помощи Hibernate Search query DSL val queryBuilder = fullTextEntityManager.searchFactory .buildQueryBuilder().forEntity(User::class.java).get() //обозначаем поля, по которым необходимо произвести поиск val query = queryBuilder .keyword() .onFields("name") .matching(text) .createQuery() //оборачиваем Lucene Query в Hibernate Query object val jpaQuery: FullTextQuery = fullTextEntityManager.createFullTextQuery(query, User::class.java) //возвращаем список сущностей return jpaQuery.resultList.map { result -> result as User }.toList() } }
REST-контроллер, UserController.java:
import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RestController import java.util.* @RestController internal class UserController(private val userService: UserService) { @GetMapping("/users") fun getAll(): List<User> { return userService.findAll() } @GetMapping("/users/search") fun search(text: String): List<User> { return userService.search(text) } @PostMapping("/users") fun insertUser(@RequestBody user: User): User { return userService.saveUser(user) } }
Используем два метода, для извлечения из БД и поиска по строке.
Перед работой приложения необходимо провести инициализацию индекса, делаем это при помощи ApplicationListener'a.
package ru.rti import org.hibernate.search.jpa.Search import org.springframework.boot.context.event.ApplicationReadyEvent import org.springframework.context.ApplicationListener import org.springframework.stereotype.Component import javax.persistence.EntityManager import javax.persistence.PersistenceContext import javax.transaction.Transactional @Component @Transactional class BuildSearchService( @PersistenceContext val entityManager: EntityManager) : ApplicationListener<ApplicationReadyEvent> { override fun onApplicationEvent(event: ApplicationReadyEvent?) { try { val fullTextEntityManager = Search.getFullTextEntityManager(entityManager) fullTextEntityManager.createIndexer().startAndWait() } catch (e: InterruptedException) { println("An error occurred trying to build the search index: " + e.toString()) } } }
Для теста использовали PostgreSQL:
spring.datasource.url=jdbc:postgresql:users spring.datasource.username=postgres spring.datasource.password=postgres spring.datasource.driver-class-name=org.postgresql.Driver spring.datasource.name=users
И наконец,
build.gradle:buildscript { ext.kotlin_version = '1.2.61' ext.spring_boot_version = '1.5.15.RELEASE' repositories { jcenter() } dependencies { classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-allopen:$kotlin_version" classpath "org.springframework.boot:spring-boot-gradle-plugin:$spring_boot_version" classpath "org.jetbrains.kotlin:kotlin-noarg:$kotlin_version" } } apply plugin: 'kotlin' apply plugin: "kotlin-spring" apply plugin: "kotlin-jpa" apply plugin: 'org.springframework.boot' noArg { invokeInitializers = true } jar { baseName = 'gs-rest-service' version = '0.1.0' } repositories { jcenter() } dependencies { compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" compile 'org.springframework.boot:spring-boot-starter-web' compile 'org.springframework.boot:spring-boot-starter-data-jpa' compile group: 'postgresql', name: 'postgresql', version: '9.1-901.jdbc4' compile group: 'org.hibernate', name: 'hibernate-core', version: '5.3.6.Final' compile group: 'org.hibernate', name: 'hibernate-search-orm', version: '5.10.3.Final' compile group: 'com.h2database', name: 'h2', version: '1.3.148' testCompile('org.springframework.boot:spring-boot-starter-test') }
Приведенное демо — простой пример использования технологии Hibernate Search, с помощью которого можно понять как подружить Apache Lucene и Spring Data Jpa. При необходимости проекты на основе этого демо можно подключить к Apache Solr или ElasticSearch. Потенциальное направление развития проекта — это поиск по крупным индексам (>10 ГБ) и замер производительности в них. Можно создавать конфигурации для ElasticSearch или более сложные конфигурации индексов, изучая возможности Hibernate Search на более глубоком уровне.
Полезные ссылки:
- Этот проект на GitHub
- Как использовать Kotlin совместно со Spring Data Jpa
- Использование Kotlin и Spring boot
- Демо-проект Kotlin и Spring boot на Github
- Spring Data Jpa и Kotlin — maven configuration
- Kotlin compiler plugins — Jpa и Spring
- Интеграция Spring boot и Hibernate Search (+Github)
- Совместимость версий Hibernate Search
