- PVSM.RU - https://www.pvsm.ru -

Как начать работать с Hibernate Search

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

Как начать работать с Hibernate Search - 1

В этом посте я вкратце расскажу об инструментах, которые могут помочь в решении таких задач. А затем представлю демо-проект поискового сервиса, где реализована более интересная и сложная фича — синхронизация сущностей, БД и поискового индекса. На примере этого демо-проекта вы сможете познакомиться с Hibernate Search — удобным способом общения с полнотекстовыми индексами Solr, Lucene, ElasticSearch.

Среди инструментов для развертывания поисковых механизмов я бы выделил три.

Lucene [1] — это java-библиотека, предоставляющая низкоуровневый интерфейс денормализованной базы данных с возможностью полнотекстового поиска. С ее помощью можно создавать индексы и наполнять их записями (документами). Подробней о Lucene можно почитать здесь [2].

Solr [3] — это конечный программный продукт на основе Lucene, полнотекстовая база данных, самостоятельный отдельный веб-сервер. Имеет http-интерфейс для индексации и полнотекстовых запросов, позволяет индексировать документы и вести поиск по ним. У Solr есть простой API и встроенный UI, что избавляет пользователя от ручных манипуляций над индексами. На Хабре выходил хороший сравнительный анализ [4] Solr и Lucene.

ElasticSearch [5] — более современный аналог Solr. В его основе также лежит Apache Lucene. По сравнению с Solr, ElasticSearch выдерживает более высокие нагрузки при индексации документов и поэтому может быть использован для индексации лог-файлов. В сети можно найти подробную таблицу [6] со сравнением Solr и ElasticSearch.

Это, конечно, не полный список, выше я выбрал лишь те системы, которые заслуживают наибольшего внимания. Систем для организации поиска очень много. PostgreSQL имеет возможности полнотекстового поиска; не стоит забывать и о Sphinx.

Основная проблема

Переходим к главному. Для надежного/консистентного хранения данных обычно используется RDB (реляционная база данных). Она обеспечивает транзакционность в соответствии с принципами ACID. Для работы поисковой системы используется индекс, в который нужно добавлять сущности и те поля таблиц, по которым будет производиться поиск. То есть когда новый объект попадает в систему, его необходимо сохранить и в реляционную базу данных, и в полнотекстовый индекс.

Если внутри вашего приложения не организована транзакционность таких изменений, то могут возникнуть разного рода рассинхронизации. Например, вы производите выборку из БД, а в индексе этого объекта нет. Или наоборот: в индексе есть запись об объекте, а из RDB он был удален.

Решить эту проблему можно разными способами. Вы можете вручную организовывать транзакционность изменений при помощи механизмов JTA и Spring Transaction Management [7]. А можете пойти более интересным путем — использовать Hibernate Search, который сделает все это сам. По умолчанию используется Lucene, хранящий данные индекса внутри файловой системы, в общем виде настраивается подключение к индексу. При старте системы вы запускаете метод синхронизации startAndWait(), и во время работы системы записи будут сохраняться в RDB и индексе.

Чтобы проиллюстрировать это решение, я подготовил демо-проект с Hibernate Search. Мы создадим сервис, содержащий методы для чтения, обновления и поиска пользователей. Он может лечь в основу внутренней базы данных с возможностью полнотекстового поиска по имени, фамилии или другим мета-данным. Для взаимодействия с реляционными базами данных используем фреймворк Spring Data Jpa [8].

Начнем с класса-сущности для представления пользователя:

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 [9] указываем сущность, при помощи Table [10] указываем табличку в БД. Аннотация Indexed [11] указывает, что сущность индексируемая и будет попадать в полнотекстовый индекс.

JPA-Репозиторий, необходимый для CRUD-операций над пользователями в базе данных:

@Transactional(Transactional.TxType.MANDATORY)
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 на более глубоком уровне.

Полезные ссылки:

Автор: xqnmqx

Источник [20]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/java/298235

Ссылки в тексте:

[1] Lucene: http://lucene.apache.org/

[2] здесь: http://www.lucenetutorial.com/lucene-in-5-minutes.html

[3] Solr: http://lucene.apache.org/solr/

[4] сравнительный анализ: https://habr.com/post/30594/

[5] ElasticSearch: https://www.elastic.co/products/elasticsearch

[6] таблицу: http://solr-vs-elasticsearch.com/

[7] Spring Transaction Management: https://docs.spring.io/spring/docs/4.2.x/spring-framework-reference/html/transaction.html

[8] Spring Data Jpa: https://projects.spring.io/spring-data-jpa/

[9] Entity: https://habr.com/users/entity/

[10] Table: https://habr.com/users/table/

[11] Indexed: https://habr.com/users/indexed/

[12] Этот проект на GitHub: https://github.com/xqnmqx/simple-search-system

[13] Как использовать Kotlin совместно со Spring Data Jpa: https://blog.codecentric.de/en/2017/06/kotlin-spring-working-jpa-data-classes/

[14] Использование Kotlin и Spring boot: https://kotlinlang.org/docs/tutorials/spring-boot-restful.html

[15] Демо-проект Kotlin и Spring boot на Github: https://github.com/JetBrains/kotlin-examples/tree/master/tutorials/spring-boot-restful

[16] Spring Data Jpa и Kotlin — maven configuration: https://github.com/dnno/spring-kotlin-jpa/blob/master/pom.xml#L110

[17] Kotlin compiler plugins — Jpa и Spring: https://kotlinlang.org/docs/reference/compiler-plugins.html

[18] Интеграция Spring boot и Hibernate Search: http://blog.netgloo.com/2014/11/23/spring-boot-and-hibernate-search-integration/

[19] Github: https://github.com/netgloo/spring-boot-samples/tree/master/spring-boot-hibernate-search

[20] Источник: https://habr.com/post/428578/?utm_campaign=428578