Hoy en día, muchos están desarrollando aplicaciones Java empresariales mediante el arranque por resorte. En el curso de los proyectos, a menudo surgen tareas para crear motores de búsqueda de diversa complejidad. Por ejemplo, si está desarrollando un sistema que almacena datos sobre usuarios y libros, tarde o temprano puede requerir una búsqueda por nombre de usuario / apellido, por nombre / anotación de libros.

En esta publicación, hablaré brevemente sobre las herramientas que pueden ayudar a resolver tales problemas. Y luego presentaré un proyecto de demostración de un servicio de búsqueda, donde se implementa una característica más interesante y compleja: sincronización de entidades, DB e índice de búsqueda. Usando el ejemplo de este proyecto de demostración, puede familiarizarse con Hibernate Search, una forma conveniente de comunicarse con los índices de texto completo Solr, Lucene, ElasticSearch.
Entre las herramientas para implementar motores de búsqueda, destacaría tres.
Lucene es una biblioteca de Java que proporciona una interfaz de base de datos desnormalizada de búsqueda de texto completo de bajo nivel. Con su ayuda, puede crear índices y llenarlos con registros (documentos). Lea más sobre Lucene
aquí .
Solr es el último producto de software basado en Lucene, una base de datos de texto completo, un servidor web independiente y separado. Tiene una interfaz http para indexación y consultas de texto completo, le permite indexar documentos y buscarlos. Solr tiene una API simple y una interfaz de usuario integrada, lo que elimina la necesidad de manipulación manual de índices. En Habré hubo un buen
análisis comparativo de Solr y Lucene.
ElasticSearch es un análogo más moderno de Solr. También se basa en Apache Lucene. En comparación con Solr, ElasticSearch puede soportar cargas más altas al indexar documentos y, por lo tanto, puede usarse para indexar archivos de registro. En la red puede encontrar una
tabla detallada que compara Solr y ElasticSearch.
Esto, por supuesto, no es una lista completa; arriba, elegí solo aquellos sistemas que merecen la mayor atención. Hay muchos sistemas para organizar una búsqueda. PostgreSQL tiene capacidades de búsqueda de texto completo; No te olvides de Sphinx.
Problema principal
Pasamos a lo principal. Para un almacenamiento de datos confiable / consistente, generalmente se usa RDB (base de datos relacional). Proporciona transaccionalidad de acuerdo con los principios de ACID. Para que el motor de búsqueda funcione, se utiliza un índice para agregar entidades y aquellos campos de las tablas mediante los cuales se realizará la búsqueda. Es decir, cuando un nuevo objeto ingresa al sistema, debe guardarse tanto en la base de datos relacional como en el índice de texto completo.
Si la naturaleza transaccional de dichos cambios no está organizada dentro de su aplicación, pueden ocurrir varios tipos de desincronización. Por ejemplo, selecciona de una base de datos, pero este objeto no está en el índice. O viceversa: hay un registro de objeto en el índice y se eliminó de RDB.
Hay varias formas de resolver este problema. Puede organizar manualmente los cambios transaccionales utilizando los mecanismos JTA y
Spring Transaction Management . O puede ir de una manera más interesante: use Hibernate Search, que lo hará todo por sí mismo. De manera predeterminada, se utiliza Lucene, que almacena datos de índice dentro del sistema de archivos, en general, se configura la conexión al índice. Cuando se inicia el sistema, inicia el método de sincronización startAndWait (), y mientras el sistema se está ejecutando, los registros se almacenarán en el RDB y el índice.
Para ilustrar esta solución, preparé un proyecto de demostración con Hibernate Search. Crearemos un servicio que contenga métodos para leer, actualizar y buscar usuarios. Puede formar la base de una base de datos interna con la posibilidad de búsqueda de texto completo por nombre, apellido u otros metadatos. Para interactuar con bases de datos relacionales, utilizamos el
marco Spring Data Jpa .
Comencemos con la clase de entidad para representar al usuario:
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)
Todo es estándar, denotamos entidad con todas las anotaciones necesarias para los datos de primavera. Usando
Entity, indicamos la entidad, usando
Table indicamos la etiqueta en la base de datos.
La anotación
indexada indica que una entidad es indexable y caerá en un índice de texto completo.
Se requiere repositorio JPA para operaciones CRUD en usuarios de la base de datos:
internal interface UserRepository: JpaRepository<User, Long>
Servicio para trabajar con usuarios, 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 obtiene todos los usuarios directamente de la base de datos. La búsqueda utiliza el componente userSearch para recuperar usuarios del índice. Componente para trabajar con el índice de búsqueda de usuarios:
@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() } }
Controlador 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) } }
Utilizamos dos métodos para extraer de la base de datos y buscar por cadena.
Antes de que se ejecute la aplicación, es necesario inicializar el índice, lo hacemos usando ApplicationListener.
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()) } } }
Para la prueba usamos 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
Finalmente,
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') }
Esta demostración es un ejemplo simple del uso de la tecnología de búsqueda de Hibernate, con la que puede comprender cómo hacer amigos Apache Lucene y Spring Data Jpa. Si es necesario, los proyectos basados en esta demostración se pueden conectar a Apache Solr o ElasticSearch. Una posible dirección para el proyecto es buscar índices grandes (> 10 GB) y medir el rendimiento en ellos. Puede crear configuraciones para ElasticSearch o configuraciones de índice más complejas explorando las posibilidades de Hibernate Search a un nivel más profundo.
Enlaces utiles: