Búsquedas full-text en MongoDB de la mano de Elasticsearch

En este artículo integraremos MongoDB con Elasticsearch para realizar búsquedas full-text utilizando un plugin de mongoose.

En la versión 2.4 de MongoDB se introdujeron las búsquedas full-text (también llamadas text indexes). El problema es que hasta la versión 2.6 (que salió hace unos días), desaconsejaban su uso en produción ya que no era una funcionalidad estable. Esta funcionalidad, similar a la que ofrece MySQL mediante los índices FULLTEXT, es bastante limitada y si queremos búsquedas más robustas y completas debemos acudir a Sphinx, Solr o, como en este caso, a Elasticsearch.

Puesta a punto de Elasticsearch

Al igual que Solr, Elasticsearch está basado en el motor de búsquedas Apache Lucene. En este artículo no vamos a explicar como utilizar Elasticsearch ni todo su potencial, simplemente como integrarlo con MongoDB para sincronizar nuestras bases de datos de tal manera que dispongamos de búsquedas full-text en MongoDB gracias a Elasticsearch.

Para empezar, instalamos Elasticsearch según esté disponible. En Debian/Ubuntu estará disponible en APT, en mi caso (gentoo), está disponible mediante Portage.

Posiblemente tengamos que copiar/renombrar los ficheros .sample para usar la configuración por defecto. Tras ello arrancamos el servicio Elasticsearch y todo listo, ya tenemos Elasticsearch funcionando.

Front-ends para Elasticsearch

Algo que recomiendo encarecidamente es usar un front-end para visualizar, gestionar y monitorizar nuestro clúster. Se pueden instalar como plugins de una manera muy sencilla. Los tres que recomiendo, por orden de preferencia personal son:

El matrimonio de Elasticsearch y MongoDB

Integración de Elasticsearch con MongoDB
Integración de Elasticsearch con MongoDB

Integrando Elasticsearch en nuestro modelo

En este caso como estamos utilizando Node.js con mongoose, vamos a utilizar un plugin llamado Mongoosastic.

Para indicar a mongoose que vamos a utilizar Elasticsearch, tenemos que requerir el módulo e insertar el plugin. Añadimos a nuestro modelo las líneas destacadas:

import mongoose from 'mongoose'
import mongoosastic from 'mongoosastic'

var User = mongoose.Schema({
  name: String,
  email: String,
  city: String,
})

User.plugin(mongoosastic)

const model = mongoose.model('User', User)

Sencillo, ¿verdad?

Por defecto todos los campos serán indexados (algo que no es buena idea), así que para elegir qué campos queremos indexar añadiremos es_indexed. También podemos dar prioridad al campo mediante el parámetro es_boost. De tal manera que quedaría así:

import mongoose from 'mongoose'
import mongoosastic from 'mongoosastic'

var User = mongoose.Schema({
  name: { type: String, es_indexed: true, es_boost: 2.0 },
  email: String,
  city: { type: String, es_indexed: true },
})

User.plugin(mongoosastic)

const model = mongoose.model('User', User)

Esto daría mayor prioridad al campo name en las búsquedas.

Asociando el índice a mongoose

¡Un momento!, tenemos el índice pero no tenemos mapeadas nuestras bases de datos entre sí. Esto es una operación que sólo se ejecuta la primera vez y sirve para asociar nuestros campos en Elasticsearch con los campos de mongoose. Para ello simplemente añadimos createMapping a nuestro código (después podemos comentarlo o borrarlo):

import mongoose from 'mongoose'
import mongoosastic from 'mongoosastic'

var User = mongoose.Schema({
  name: { type: String, es_indexed: true, es_boost: 2.0 },
  email: String,
  city: { type: String, es_indexed: true },
})

User.plugin(mongoosastic)

const model = mongoose.model('User', User)

model.createMapping((err, mapping) => {
  if (err) {
    console.err(err)
  } else {
    console.log('Mapeo creado!')
    console.log(mapping)
  }
})

Sincronizando una colección existente

Si tenemos una colección en MongoDB que ya tiene documentos debemos sincronizar nuestras bases de datos. Esto se hace utilizando el método synchronize. También podemos comentar o borrar estas líneas una vez ejecutadas:

const stream = model.synchronize()
let count = 0

stream.on('data', (err, doc) => {
  count++
})

stream.on('close', () => {
  console.log(`Indexados ${count} documentos!`);
})

stream.on('error', err => {
  console.error(err);
})

Cómo realizar búsquedas full-text

Para realizar una búsqueda full-text en Elasticsearch a través de mongoose usaremos el método search:

User.search({ query: 'juan' }, (err, results) => {
  // Aquí los resultados
})

Los resultados que obtendremos serán un objeto de Elasticsearch. Si queremos un objeto de MongoDB con todos los campos de los documentos encontrados (no sólo los campos indexados) debemos usar la opción hydrate:

User.search({ query: 'juan' }, { hydrate: true }, (err, results) => {
  // Aquí los resultados
})

Si queremos que por defecto nuestras consultas siempre devuelvan el objeto de MongoDB, modificaremos nuestro modelo para que incluya la opción:

User.plugin(mongoosastic, { hydrate: true })

Y aquí termina el artículo sobre la integración de Elasticsearch en MongoDB. Recomiendo consultar la documentación de Elasticsearch para realizar consultas más complejas y la documentación de Mongoosastic para ver todas las opciones y mapeos disponibles de este gran plugin.

Compartir en

Facebook Twitter Google+ LinkedIn