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

    Сегодня многие разрабатывают 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.

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

    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 на более глубоком уровне.

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

    Ростелеком
    126,00
    Компания
    Поделиться публикацией

    Комментарии 11

      +1
      При необходимости проекты на основе этого демо можно подключить к… ElasticSearch


      у меня для вас плохие новости. зачем буханко-троллейбус, когда уже всё есть?
        0
        Spring Data Elasticsearch — хорошая штука, но предоставляет другой уровень абстракции, а также имеет ряд существенных недостатков. В случае работы с ним придется создавать отдельный репозиторий и самому дописывать туда записи, несмотря на то что есть удобные штуки типа:
        docs.spring.io/spring-data/elasticsearch/docs/current/reference/html/#core.domain-events (Поправьте меня если чего не увидел в доке). В hibernate search практически не пришлось чего-то делать для того чтобы записи попали в index, удобно. Также он работает только с ES, который ест 2GB памяти при минимальных настройках, т.е. для чего-то небольшого не подойдет.

        PS. Случалось его использовать в 2017 (версию 2.*.*), в проде, количество багов и недоработок расстраивало, возможно к концу 2018 все изменилось). jira.spring.io/projects/DATAES
          0
          + расстроила совместимость версий spring-data-elasticsearch: spring data ES version matrix

          С 5-й версией он до сих пор не работает. Совместимость hibernate search: hibernate search version matrix
            0
            Если не секрет, сколько документов в индексе? Хотя бы порядок?
              0
              На основе hibernate search в проде ничего нет. Как раз и возник интерес попробовать. Индексы в системах с Solr'ом и Es не очень крупные, содержат десятки миллионов документов.
          0
          Этот код будет работать на чистом Постгресе, больше ничего не нужно?
            0
            Работает на чистом постгресе, только надо выполнить ddl скрипт.
            PS. Заметил старый ddl в описании, поправлю.
            +1
            Поправьте ссылку в тексте «почитать про lucene», укажите, что там рассматривает версия от 2012 года. Актуальная версия практически не совместима со старой архитектурой.

            What's New
            Nov 15 2012 - GitHub repo now available for HelloLucene.
            Nov 4 2012 - Updated code and examples to Lucene 4.0.0.
            Mar 3 2012 - Expanded on Lucene Query syntax.
            Jan 26 2012 - Updated code to Lucene 3.5.0.
            Oct 1 2011 - Redesigned the site, and incorporated the Disqus commenting system. Updated links to Lucene 3.4.0.
            Sep 16 2011 - Lucene 3.4.0 released.

            PS: вот буквально на прошлой неделе имплантировали Lucene в наш проект. Подробной документации по актуальным версиям (даже для 5.х/6.х) очень мало, практически нет. Пришлось читать про solr, там поболе
              0
              Спасибо за замечание! Добавлю раздел про совместимость версий в статью. В документации есть хорошая матрица: hibernate.org/search/releases
              0
              @Transactional(Transactional.TxType.MANDATORY) не нужен в интерфейсе, ибо они неявно и так создаются(транзакции) имплементацией jpa репозиториев.

              P.S. интереснее было бы прочитать именно про применение full-text search'a по разным критериям которых мало в документации — поиск по дистанции, поиск для русских слов и т.п.
                0
                @Transactional(Transactional.TxType.MANDATORY) не нужен в интерфейсе, ибо они неявно и так создаются(транзакции) имплементацией jpa репозиториев.

                Спасибо! Код поправил.

                P.S. интереснее было бы прочитать именно про применение full-text search'a по разным критериям которых мало в документации — поиск по дистанции, поиск для русских слов и т.п.

                Понял что добавить в следующую статью.

              Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

              Самое читаемое