developerlover.com

Indexar ficheros de texto en Sphinx con Node.js

En el post Buscador con Sphinx y MySQL, comentábamos que Sphinx puede leer contenido de diferentes fuentes (bases de datos, archivos de texto, archivos HTML, buzones de correo, etc).

Hoy vamos a explicar cómo indexar ficheros de texto en Sphinx, y para ello nos vamos a ayudar de un script en Node.js (Más adelante veremos para qué necesitamos este script).

Y una vez tengamos los ficheros indexados, enviaremos consultas a Sphinx que nos devolverá aquellos más relevantes junto con su fecha de creación y ruta de localización.

¿Cómo funciona la indexación de ficheros de texto con Sphinx?

Para empezar, y cómo decíamos más arriba, Sphinx puede leer de diferentes fuentes o sources.

Esto en Sphinx se traduce a “tipos de fuente”, y en el fichero de configuración se indican en el bloque source a través del atributo type. Sus valores pueden ser: mysql, pgsql, mssql, xmlpipe2, tsvpipe y odbc.

El que vamos a utilizar para indexar ficheros de texto es el tipo xmlpipe2. ¿Pero cómo funciona?

Básicamente, con este tipo de fuente, Sphinx recibe un XML con un formato determinado e indexa su contenido. ¿Pero cómo le enviamos ese XML? ¿Y cómo lo generamos?

Esto se consigue gracias a que Sphinx es capaz de leer la salida que produzca un comando (que le indicamos a través del atributo xmlpipe_command en el fichero de configuración), y ese comando puede ser cualquiera que se nos ocurra y que genere una salida con un XML válido para Sphinx (un poco más abajo veremos cómo es este formato).

Por ejemplo, un comando válido (suponiendo que el fichero libros.xml contenga un XML válido para Sphinx) podría ser:

$ cat libros.xml

Y en nuestro caso, el comando que vamos a usar va a ser la ejecución de un script de Node.js, que recibirá como parámetro un directorio, leerá todos sus ficheros, y generará un XML válido para Sphinx con el contenido de cada fichero de texto que encuentre:

$ nodejs get_sphinx_xml_content.js dir=/testing_sphinx_xmlpipe/libros

¿Qué necesitamos para la indexación de ficheros de texto con Sphinx?

Para empezar, necesitamos tener instalado Sphinx y Node.js (un poco más abajo veremos cómo hacerlo en Ubuntu) y unos ficheros de texto para indexar.

Pero de momento, simplemente vamos a crearnos el siguiente árbol de directorios y ficheros:

testing_sphinx_xmlpipe
| - - libros
|     | - - aventura
|     |     ` – – Don-Quijote-de-la-Mancha.txt
|     | - - poesía
|     |     ` – – Veinte-poemas-de-amor-y-una-canción-desesperada.txt
|     ` – – tragicomedia
|           ` – – La-Celestina.txt
| - - nodejs
|     | - - get_sphinx_xml_content.js
|     ` – – package.json
` - - sphinx
      ` – – sphinx.conf

Yo lo voy a crear en el directorio raíz /. Recordad que si cambiáis las rutas, más adelante deberéis hacer ajustes.

Y también vamos a aprovechar para añadir algo de contenido a los “libros”:

/testing_sphinx_xmlpipe/libros/aventura/Don-Quijote-de-la-Mancha.txt

En un lugar de la Mancha, de cuyo nombre no quiero acordarme, no ha mucho tiempo que vivía un hidalgo de los de lanza en astillero, adarga antigua, rocín flaco y galgo corredor.

/testing_sphinx_xmlpipe/libros/poesía/Veinte-poemas-de-amor-y-una-canción-desesperada.txt

Me gustas cuando callas porque estás como ausente,
y me oyes desde lejos, y mi voz no te toca.

/testing_sphinx_xmlpipe/libros/tragicomedia/La-Celestina.txt

Entrando Calisto en una huerta en pos de un halcón suyo, halló ahí a Melibea, de cuyo amor preso, comenzole de hablar.

¿Cómo vamos a indexar ficheros de texto en Sphinx con Node.js?

Pues lo vamos a hacer realizando los siguientes pasos:

  1. Instalar Sphinx y Node.js.
  2. Crear un script de Node.js, que se va a encargar de leer recursivamente todos los sub-directorios y ficheros de un directorio (que le vamos a pasar como parámetro), y generar un XML válido para Sphinx con el contenido de los ficheros de texto que encuentre.
  3. Configurar Sphinx para que lea la salida del script de Node.js e indexe los contenidos.

¿Y cómo es el formato del XML que necesita Sphinx?

Copio el ejemplo de la documentación oficial de Sphinx:

<?xml version="1.0" encoding="utf-8"?>
<sphinx:docset>

    <sphinx:schema>
        <sphinx:field name="subject"/>
        <sphinx:field name="content"/>
        <sphinx:attr name="published" type="timestamp"/>
        <sphinx:attr name="author_id" type="int" bits="16" default="1"/>
    </sphinx:schema>

    <sphinx:document id="1234">
        <content>this is the main content <![CDATA[[and this <cdata> entry must be handled properly by xml parser lib]]></content>
        <published>1012325463</published>
        <subject>note how field/attr tags can be in <b class="red">randomized</b> order</subject>
        <misc>some undeclared element</misc>
    </sphinx:document>

    <sphinx:document id="1235">
        <subject>another subject</subject>
        <content>here comes another document, and i am given to understand, that in-document field order must not matter, sir</content>
        <published>1012325467</published>
    </sphinx:document>

    <!-- ... even more sphinx:document entries here ... -->

    <sphinx:killlist>
        <id>1234</id>
        <id>4567</id>
    </sphinx:killlist>

</sphinx:docset>
  • sphinx:docset: Obligatorio. Es el elemento de nivel superior obligatorio para Sphinx y que incluirá al resto de elementos.
  • sphinx:schema: Opcional. Este elemento es opcional, pero si se añade, debe indicarse como primer elemento dentro de sphinx:docset. Contiene las declaraciones de campos y atributos. Si está presente, anula las declaraciones hechas en el archivo de configuración.
  • sphinx:document: Obligatorio. Es el utilizado para indicar cada uno de los documentos que van a ser indexados. Puede haber tantos como se necesiten y cada uno deberá tener un atributo ID único, y contener a su vez un número de elementos arbitrario con sus respectivos nombres y contenido.
  • sphinx:killlist: Opcional. Contiene una serie de elementos id, cuyo contenido es el ID de los documentos para ser puestos en la kill-list de este índice.

Más información acerca del formato XML.

Instalación de Sphinx en Ubuntu

$ sudo apt-get update
$ sudo apt-get install sphinxsearch

Para comprobar la instalación ejecutamos:

$ search

Y si todo ha ido bien, nos devolverá información del paquete, su versión y diferentes opciones aceptadas por el comando.

Para obtener más información sobre la instalación, o instalar Sphinx en otros sistemas operativos, consultar el manual de instalación oficial de Sphinx.

Instalación de Node.js y NPM en Ubuntu

$ sudo apt-get update
$ sudo apt-get install nodejs npm

Para comprobar la instalación ejecutamos:

$ nodejs

Y si todo ha ido bien, entraremos en la consola de Node.js. Para salir ejecutamos:

> .exit

Para obtener más información sobre la instalación, o instalar Node.js en otros sistemas operativos, consultar la página de descarga de Node.js.

Script de Node.js

Lo primero de todo es añadir el contenido a los ficheros que hemos creado en el directorio /testing_sphinx_xmlpipe/nodejs.

/testing_sphinx_xmlpipe/nodejs/package.json
{
    "name"        : "sphinx-xml-generator",
    "version"     : "0.1.0",
    "description" : "Generate a valid XML for Sphinx with the content of a directory text files.",
    "author"      : "Pablo Domínguez",
    "homepage"    : "http://developerlover.com",
    "keywords"    : [
        "sphinx",
        "nodejs",
        "developerlover"
    ],
    "engineStrict": true,
    "dependencies": {
        "xmlbuilder": "2.6.4",
        "walk": "2.3.9",
        "istextorbinary": "1.0.2"
    }
}
/testing_sphinx_xmlpipe/nodejs/get_sphinx_xml_content.js
// Inluir módulo fs
var fs = require('fs');

// Inluir módulo walk
var walk = require('walk');

// Inluir módulo xmlbuilder
var xmlbuilder = require('xmlbuilder');

// Inluir módulo istextorbinary
var istextorbinary = require('istextorbinary')

// Comprobar que se pasa el argumento requerido
if (typeof process.argv[2] === 'undefined') {

    // Finalizar ejecución con error si no se pasa el argumento requerido
    process.exit(1);

} else {

    // Leemos el argumento dir con formato obligatorio dir=/some/directory
    var dir_argument = process.argv[2].split("=");
    var directory = dir_argument[1];

    // Comprobamos que el directorio no es un valor vacío
    if (typeof directory === 'undefined' || directory === '') {
        process.exit(1);
    }

    // Leer el directorio
    fs.stat(directory, function (err, stats) {

        // Lanzar excepción si se produce algún error
        if (err) throw err;

        // Comprobar que el argumento pasado es un directorio
        if (stats.isDirectory()) {

            // Creamos el docset del XML
            var docset = xmlbuilder.create('sphinx:docset');

            // Añadimos la estructura al XML
            var schema = docset.ele('sphinx:schema');
            schema.ele('sphinx:field', {'name': 'content'});
            schema.ele('sphinx:attr', {'name': 'file', 'type': 'string'});
            schema.ele('sphinx:attr', {'name': 'created', 'type': 'timestamp'});

            // Cargar el directorio en la variable walker con el módulo walk
            var walker = walk.walk(directory, { followLinks: false });

            // Evento que se ejecuta por cada fichero detectado
            walker.on('file', function(root, stat, next) {

                // Crear variable file con la ruta completa y el nombre del fichero
                var file = root + '/' + stat.name;

                // Leer fichero
                fs.readFile(file, 'utf-8', function (err, fileContent) {

                    // Lanzar excepción si se produce algún error
                    if (err) throw err;

                    // Leer información del fichero
                    fs.stat(file, function (err, stats) {

                        // Ver si el fichero es de texto o binario
                        var fileIsText = istextorbinary.isTextSync(file);

                        // Comprobar que es un fichero de texto para incluirlo en el XML
                        if (fileIsText) {

                            // Extraer el timestamp de la fecha de creación del fichero
                            var timestamp = new Date(stats.ctime).getTime()/1000|0;

                            // Generar el XML con el contenido del fichero
                            var document = docset.ele('sphinx:document', {'id': stats.ino});
                            document.ele('file', {}, file);
                            document.ele('content', {}, fileContent)
                            document.ele('created', {}, timestamp);

                        }

                        // Continuar con el siguiente fichero
                        next();

                    });

                });

            });

            // Mostrar XML al finalizar la lectura de todos los ficheros
            walker.on('end', function() {
                xml = docset.end({pretty: true});
                console.log(xml);
            });

        } else {

            // Finalizar ejecución con error si el argumento pasado no es un directorio
            process.exit(1);

        }

    });

}

Con el contenido añadido, tenemos que ejecutar en el directorio /testing_sphinx_xmlpipe/nodejs el comando:
$ npm install

De esta forma, instalaremos en el directorio /testing_sphinx_xmlpipe/nodejs/node_modules los módulos o dependencias que necesita nuestro script y que previamente hemos indicado en el fichero package.json.

Las dependencias son:

  • xmlbuilder: Nos ayudará con la generación del XML.
  • walk: Lo utilizaremos para leer todos los ficheros de un directorio de forma recursiva.
  • istextorbinary: Que nos servirá para comprobar si un fichero es de texto.

Una vez instaladas todas las dependencias ya tendremos la parte más importante, el script de Node.js que lee todos los ficheros de texto de un directorio y los convierte a un XML válido para Sphinx.

Vamos a probar el comando (que más adelante añadiremos a la configuración de Sphinx). Si no hemos modificado las rutas indicadas al inicio del post, el comando debería ser:

$ nodejs /testing_sphinx_xmlpipe/nodejs/get_sphinx_xml_content.js dir=/testing_sphinx_xmlpipe/libros

Y si todo ha ido bien deberíamos ver el XML:

<?xml version="1.0"?>
<sphinx:docset>
  <sphinx:schema>
    <sphinx:field name="content"/>
    <sphinx:attr name="file" type="string"/>
    <sphinx:attr name="created" type="timestamp"/>
  </sphinx:schema>
  <sphinx:document id="6090492">
    <file>/testing_sphinx_xmlpipe/libros/tragicomedia/La-Celestina.txt</file>
    <content>Entrando Calisto en una huerta en pos de un halcón suyo, halló ahí a Melibea, de cuyo amor preso, comenzole de hablar.</content>
    <created>1436124791</created>
  </sphinx:document>
  <sphinx:document id="6089711">
    <file>/testing_sphinx_xmlpipe/libros/poesía/Veinte-poemas-de-amor-y-una-canción-desesperada.txt</file>
    <content>Me gustas cuando callas porque estás como ausente,
y me oyes desde lejos, y mi voz no te toca.</content>
    <created>1436124792</created>
  </sphinx:document>
  <sphinx:document id="6081144">
    <file>/testing_sphinx_xmlpipe/libros/aventura/Don-Quijote-de-la-Mancha.txt</file>
    <content>En un lugar de la Mancha, de cuyo nombre no quiero acordarme, no ha mucho tiempo que vivía un hidalgo de los de lanza en astillero, adarga antigua, rocín flaco y galgo corredor.</content>
    <created>1436124792</created>
  </sphinx:document>
</sphinx:docset>

Ahora sólo nos queda configurar Sphinx para que lea la salida del comando.

Configuración de Sphinx

En este punto no nos vamos a complicar demasiado, vamos a usar un fichero de configuración con las opciones mínimas, pero utilizando el tipo de fuente xmlpipe2 que hemos visto al principio.

Vamos a añadir el contenido:

/testing_sphinx_xmlpipe/sphinx/sphinx.conf
source books
{
    type                = xmlpipe2
    xmlpipe_command     = /usr/bin/nodejs /testing_sphinx_xmlpipe/nodejs/get_sphinx_xml_content.js dir=/testing_sphinx_xmlpipe/libros
}

index books
{
    source              = books
    charset_type        = utf-8
    path                = /var/lib/sphinxsearch/data/books
}

indexer
{
    mem_limit           = 32M
}

searchd
{
    listen              = 9312
    log                 = /var/log/sphinxsearch/searchd.log
    query_log           = /var/log/sphinxsearch/query.log
    pid_file            = /var/run/sphinxsearch/searchd.pid
}

# --eof--

Y como podéis observar, en el bloque source le estamos indicando el tipo de fuente del que hablábamos (xmlpipe2) y el comando que ejecuta nuestro script de Node.js.

Vamos a realizar la indexación ejecutando el siguiente comando:

$ sudo indexer -c /testing_sphinx_xmlpipe/sphinx/sphinx.conf --all

Y si todo ha ido bien deberíamos ver algo así:

Sphinx 2.0.4-id64-release (r3135)
Copyright (c) 2001-2012, Andrew Aksyonoff
Copyright (c) 2008-2012, Sphinx Technologies Inc (http://sphinxsearch.com)
 
using config file '/testing_sphinx_xmlpipe/sphinx/sphinx.conf'...
indexing index 'books'...
collected 3 docs, 0.0 MB
sorted 0.0 Mhits, 100.0% done
total 3 docs, 395 bytes
total 0.003 sec, 117037 bytes/sec, 888.88 docs/sec
total 3 reads, 0.000 sec, 0.3 kb/call avg, 0.0 msec/call avg
total 9 writes, 0.000 sec, 0.2 kb/call avg, 0.0 msec/call avg

Ahora sólo nos queda comprobar que nuestros ficheros de texto se han indexado correctamente enviando alguna consulta a Sphinx.

Consultar los ficheros indexados

Para esta parte voy a utilizar la línea de comandos, pero recordad que se pueden enviar consultas a Sphinx de múltiples formas.

Y ahora, para comprobar si nuestros libros se han indexado correctamente vamos a enviar la siguiente búsqueda a Sphinx:

$ search -c /testing_sphinx_xmlpipe/sphinx/sphinx.conf -i books -e "Calisto | Melibea"

Este comando busca aquellos ficheros que contengan las palabras “Calisto” o “Melibea”, y como resultado nos debería mostrar:

Sphinx 2.0.4-id64-release (r3135)
Copyright (c) 2001-2012, Andrew Aksyonoff
Copyright (c) 2008-2012, Sphinx Technologies Inc (http://sphinxsearch.com)
 
using config file '/testing_sphinx_xmlpipe/sphinx/sphinx.conf'...
index 'books': query 'Calisto | Melibea ': returned 1 matches of 1 total in 0.000 sec
 
displaying matches:
1. document=7136967, weight=1680, file=/testing_sphinx_xmlpipe/libros/tragicomedia/La-Celestina.txt, created=Thu Jul 9 21:54:56 2015
 
words:
1. 'calisto': 1 documents, 1 hits
2. 'melibea': 1 documents, 1 hits

Para lanzar las consultas recomiendo utilizar la sintaxis de consulta extendida (a través del parámetro -e) ya que permite más versatilidad.

Más información acerca de la sintaxis de consulta extendida de Sphinx.

Categorías: Node.js, Sphinx

« Crear backup de base de datos MySQL excluyendo tablas o su contenido

2 Comentarios

  1. Hola,

    Quería agradecerte por brindar esta información, que la verdad esta muy buena y creo que me será de gran utilidad.

    Estoy por emprender un proyecto y tengo la idea de utilizar sphinx.

    Gracias por el aporte.

Deja un comentario

Your email address will not be published.

Copyright © 2017 developerlover.com

Up ↑