Configuración del entorno de desarrollo para un plugin personalizado de Elasticsearch

Imagen

En los últimos meses he dedicado la mayor parte de mi tiempo a construir un motor de búsqueda con Elasticsearch. El equipo se decidió por esta tecnología por sus APIs REST sencillas, su naturaleza distribuida, su velocidad y su escalabilidad.

Empecé prototipando en Python. Portar la mayoría de los componentes de la implementación a Elasticsearch fue sencillo; sin embargo, hay un modelo de ranking personalizado que no está disponible en el módulo de similitud. La complejidad del algoritmo es alta. Aplicarlo a todos los documentos recuperados en una búsqueda tomaría una eternidad.

La funcionalidad de rescore fue la solución al problema. El modelo de rescore se aplica únicamente a los primeros N documentos que resultan de una consulta inicial. Esto último permite al usuario definir un tamaño de ventana que cumpla con sus requerimientos de tiempo de respuesta, recall y precisión. La decisión final fue crear un plugin personalizado sobre el módulo de rescore de Elasticsearch.

Los retos

Implementar un plugin personalizado requiere conocer cómo funciona Elasticsearch y cuáles son sus componentes. Las tareas iniciales que ayudaron a terminar a tiempo una primera prueba de concepto, seguida del despliegue en producción, fueron:

  1. Definir un conjunto de herramientas para navegar por el código, resaltar errores, autocorregir, dar formato al código y depurar.
  2. Entender las clases y los métodos que conforman el plugin y la importancia de cada uno de ellos.

Tomando en cuenta los puntos anteriores, voy a dividir la información en dos artículos. En este artículo explico el proceso de selección de un IDE y la configuración del proyecto para trabajar en él de manera efectiva. En una publicación adicional explicaré las clases y los métodos más importantes de un plugin personalizado y cómo modificarlos para crear un plugin de rescore sencillo.

Las herramientas

Los ingredientes principales para construir el plugin de forma eficiente son un entorno de desarrollo integrado (IDE) y Docker.

IDE

El IDE que utilicé para desarrollar el plugin es Visual Studio Code (VS Code). Me gustó el hecho de que sea un editor de código abierto y que exista una gran comunidad que le da mantenimiento. Otra opción es Intellij IDEA. Hay una versión gratuita que tiene un subconjunto de funcionalidades, pero tienes que pagar por la versión completa.

Técnicamente VS Code es solo un editor, pero puedes extender su funcionalidad instalando plugins. Los plugins que usé para desarrollar el plugin son:

En caso de que también seas nuevo desarrollando en Java, la documentación Java in Visual Studio Code es útil para entender las capacidades del IDE y obtener información adicional.

La estructura del proyecto

Un plugin de Elasticsearch puede vivir de forma independiente en un repositorio. El plugin de Gradle build-tools se encarga de las dependencias de Elasticsearch. La estructura del proyecto se ve así:

├── build.gradle
└── src
    ├── main
    │   └── java
    ├── test
    │   └── java
    └── yamlRestTest
        ├── java
        └── resources
            └── rest-api-spec

El archivo build.gradle contiene las especificaciones del proyecto. src/main y src/test son la implementación y las pruebas unitarias. La carpeta yamlRestTest tiene la definición de las pruebas de integración.

Dependencias:

  • Open JDK 15
  • Gradle 6.7.1

El enfoque de esta publicación es configurar el proyecto. Partimos del plugin de rescore de ejemplo del repositorio de Elasticsearch. No hace falta conservar todos los archivos. Solo necesitamos copiar el plugin de rescore a su propia carpeta. Asegúrate de hacer checkout a la versión 7.10.0 antes de copiar la carpeta.

$ git clone https://github.com/elastic/elasticsearch.git ~/elasticsearch
$ cd ~/elasticsearch
$ git checkout v7.10.0
$ cp -r ~/plugins/examples/rescore ~/rescore
$ cd ~/rescore

Al final de este artículo encontrarás un repositorio de GitHub que contiene el proyecto. Puedes revisar los commits para analizar paso a paso cómo se llegó al proyecto final.

Configurando el proyecto

Después de abrir el proyecto con VS Code, se genera automáticamente un nuevo archivo .vscode/settings.json. Si no es así, puedes crearlo tú mismo. Dentro de este archivo tienes que especificar la ruta a las carpetas de OpenJDK y Gradle.

{
    "java.home": "/path/to/jdk-15.0.1",
    "java.import.gradle.home": "/path/to/gradle-6.7.1",
}

Si intentas hacer el build del proyecto de Java, no va a funcionar. Antes necesitamos instalar las dependencias de Elasticsearch. Así que, al inicio del build.gradle, agrega el siguiente objeto buildscript:

buildscript {
  ext {
    version_es = "7.10.0"
  }

repositories {
    mavenCentral()
    jcenter()
  }

dependencies {
    classpath "org.elasticsearch.gradle:build-tools:${version_es}" 
  }
}

El último paso es crear un archivo vacío NOTICE.txt, ya que es necesario para compilar el plugin. Si te interesa conocer el contenido ideal de este archivo puedes leer Assembling LICENSE and NOTICE files — Apache Infrastructure. Sin embargo, puede quedar vacío.

Para verificar que el proceso de configuración es correcto, ejecuta las pruebas unitarias y de integración:

$ gradle test yamlRestTest

=======================================
Elasticsearch Build Hamster says Hello!
  Gradle Version        : 6.7.1
  OS Info               : Linux 4.19.128-microsoft-standard (amd64)
  JDK Version           : 15 (OpenJDK)
  JAVA_HOME             : /opt/java/jdk-15.0.1
  Random Testing Seed   : 6B35F021BB7E61CB
  In FIPS 140 mode      : false
=======================================

BUILD SUCCESSFUL in 10s
12 actionable tasks: 3 executed, 9 up-to-date

Instalar el plugin

Todas las pruebas pasaron, lo cual es una buena señal de que el plugin compila y se ejecuta como se espera. Para ejecutar algunas consultas necesitamos instalarlo. El proceso es simple, basta con ensamblarlo:

$ gradle assemble

El paquete ensamblado se guarda en ./build/distributions/example-rescore.zip. El siguiente es el comando para instalarlo; si quieres más detalles puedes seguir el enlace a la documentación oficial.

$ ./bin/elasticsearch-plugin install file:///path/to/plugin.zip

El proceso de crear el archivo zip, instalar el plugin e iniciar Elasticsearch se puede automatizar. Creé un Dockerfile con cada una de las etapas para preparar y ejecutar Elasticsearch. La automatización reduce todo a ejecutar (asegúrate de clonar este proyecto antes de correr el comando):

$ docker-compose up --build cache

Para confirmar que el plugin se instaló correctamente, basta con llamar al endpoint:

$ curl -X GET http://localhost:9201/_cat/plugins

Debería imprimir la información del plugin.

Insertar datos y buscar

La Bulk API realiza múltiples operaciones de indexado o borrado en una sola llamada. Aquí hay un ejemplo para insertar algunos registros:

$ curl -X POST http://localhost:9201/_bulk -H 'Content-Type: application/json' -d '
{ "index" : { "_index" : "test", "_id" : "1" } }
{ "field1" : "value1", "field2": 1.2 }
{ "index" : { "_index" : "test", "_id" : "2" } }
{ "field1" : "value2", "field2": 1.1 }
{ "index" : { "_index" : "test", "_id" : "3" } }
{ "field1" : "value3", "field2": 5 }
'

Finalmente, ya podemos usar el plugin. Un ejemplo de una consulta que aplica el modelo de rescore a todos los documentos se ve así:

$ curl -X GET http://localhost:9201/test/_search?pretty=true -H 'Content-Type: application/json' -d '
{
   "rescore": {
      "example": {
         "factor": 2,
         "factor_field":  "field2"
      }
   }
}'

El cálculo del rescore es el valor del atributo field2 multiplicado por el factor. Como se mencionó antes, podemos definir una primera consulta que aplique un modelo de ranking rápido para ordenar los documentos y después hacerles rescore. Solo hay que agregar una query al inicio del body.

$ curl -X GET http://localhost:9201/test/_search?pretty=true -H 'Content-Type: application/json' -d '
{
   "query": {
      "match": {
         "field1": "value1"
      }
   },
   "rescore": {
      "example": {
         "factor": 2,
         "factor_field": "field2"
      }
   }
}'

Cómo depurar

La depuración es uno de los procesos más críticos en el desarrollo de software. Es importante para entender el código y para encontrar comportamientos incorrectos.

Crea la configuración en el archivo .vscode/launch.json. Esta configuración le indica al IDE que se conecte a una JVM remota mapeada al puerto 5005.

{
    "version": "0.2.0",
    "configurations": [
        {
            "type": "java",
            "name": "Debug",
            "request": "attach",
            "hostName": "localhost",
            "port": 5005
        }
    ]
}

Para depurar Elasticsearch, necesitas iniciarlo en modo debug:

$ ./bin/elasticsearch -Xdebug -Xrunjdwp:server=y,transport=dt_socket,address=*:5005,suspend=y

El servidor espera hasta que el IDE se conecte. Por alguna razón no arranca en el primer intento, así que haz clic dos o tres veces en el botón de iniciar la depuración.

También hay otra configuración que automatiza el arranque del servidor de Elasticsearch en modo depuración. En el mismo proyecto, el archivo docker-compose.debug.yml tiene la definición. La forma de ejecutarlo es:

$ docker-compose -f docker-compose.yml -f docker-compose.debug.yml up --build cache

Algunos recursos adicionales sobre depuración en VS Code:

Conclusión

En este artículo se describen paso a paso las acciones que hay que seguir para configurar un proyecto de un plugin de Elasticsearch. También se explica cómo insertar algunos documentos en la base de datos y cómo buscar usando el plugin.

Repositorio

ocampor/plugin-example

← Volver al blog