Apache Lucene: búsqueda libre para tu sitio web

Al pensar hoy en día en un motor de búsqueda, el primero que viene a la mente es Google. Los operadores de sitios web recurren también al motor de búsqueda personalizada (CSE, del inglés Custom Search Engine) del gigante de Internet para ofrecer a los usuarios una función de búsqueda de su propio contenido rápida y sencilla. Claro está que esta no es la única opción existente y para muchos ni siquiera la mejor. Una alternativa es Lucene: un proyecto gratuito de código abierto de Apache, que ofrece a los visitantes una función de búsqueda de texto completo.

Numerosas compañías han integrado Apache Lucene tanto online como offline. Hasta hace unos años, Wikipedia utilizaba Lucene como función de búsqueda, si bien ahora ha pasado a Solr, que a su vez se basa en el primero. Asimismo la búsqueda en Twitter funciona completamente a través de Apache Lucene. El proyecto de Doug Cutting, que comenzó a finales de la década de 1990 como un pasatiempo, se ha convertido en un software del que millones de personas se benefician a diario.

¿Qué es Lucene?

Lucene es una biblioteca de programas gratuita y de código abierto que cualquier persona puede utilizar y modificar y que está publicada por la Apache Software Foundation. Aunque en sus inicios Lucene estaba escrito completamente en Java, ahora también se puede usar en otros lenguajes de programación. Además Apache Solr y Elasticsearch sirven como poderosas extensiones que dotan a la función de búsqueda de más posibilidades.

Lucene permite la búsqueda de texto completo, es decir, se trata de un programa que busca en un conjunto de documentos de texto uno o más términos definidos por el usuario. De este hecho se deduce que Lucene no solo se utiliza en el contexto de la World Wide Web, si bien en este las funciones de búsqueda constituyen un elemento omnipresente. También se puede recurrir a Lucene en la búsqueda de archivos, bibliotecas o incluso en el ordenador de sobremesa y, además de aplicarse en documentos HTML, también trabaja con correo electrónico o incluso con archivos PDF.

El índice es un elemento decisivo en el proceso de búsqueda con Lucene y se considera el corazón del programa: en él se almacenan todos los términos de todos los documentos. Este tipo de índice invertido consiste principalmente en una tabla donde se guarda la posición de cada término. No obstante, para poder crear un índice, primero es necesaria la extracción de todos los términos de todos los documentos, proceso que cada usuario puede configurar individualmente. En la configuración, los desarrolladores establecen qué campos quieren incluir en el índice. Pero ¿a qué se refieren estos campos?

Los objetos con los que Lucene trabaja son documentos de cualquier tipo. Estos, desde el punto de vista del propio programa, contienen una serie de campos y estos incluyen, por ejemplo, el nombre del autor, el título del documento o el nombre del archivo. Cada campo tiene una designación y un valor únicos. Por ejemplo, el campo llamado title puede tener el valor "Manual de usuario de Apache Lucene". Al crear el índice, el usuario puede decidir qué metadatos quiere incluir.

En la indexación de los documentos, tiene lugar también lo que se conoce como tokenization. Para una máquina, un documento es ante todo un conjunto de información. Incluso cuando se pasa del nivel de los bits al de contenido legible para los humanos, un documento sigue estando compuesto por una serie de signos: letras, puntuación, espacios, etc.

A partir de estos datos, se crean con los segmentos de tokenization los términos (en su mayoría palabras) que finalmente van a poder buscarse. La forma más sencilla de ejecutar este tipo de tokenización es mediante el método de espacio en blanco: un término termina cuando se encuentra un espacio en blanco. Sin embargo, este método pierde su utilidad cuando un término está compuesto por varias palabras separadas, como puede ser el caso de "sin embargo". Para solucionarlo, se recurre a diccionarios adicionales que se pueden implementar también en el código Lucene.

Al analizar los datos de los que forma parte el proceso de tokenization, Lucene también ejecuta un proceso de normalización. Esto significa que los términos se convierten a una forma estandarizada, de modo que, por ejemplo, todas las mayúsculas se escriben en minúsculas. Además, Apache Lucene introduce una clasificación mediante una serie de algoritmos, por ejemplo, a través de la medida tf-idf. Como usuario, es probable que primero desees obtener los resultados más relevantes o recientes, lo que es posible gracias a los algoritmos del motor de búsqueda.

Para que los usuarios puedan encontrar lo que buscan, deben introducir un término o términos de búsqueda en una línea de texto. Dicho término recibe el nombre de query (consulta) en el contexto de Apache Lucene. Esta entrada no solo debe estar formada por una palabra o conjunto de ellas, sino que también puede contener comodines y operadores boleanos como AND, OR o + y -, entre otros. El denominado QueryParser, una clase dentro de la biblioteca del programa, traduce la entrada en una solicitud de búsqueda concreta para el motor de búsqueda. Los desarrolladores también pueden configurar el QueryParser de modo que se adapte exactamente a las necesidades del usuario.

Lo que caracterizó al lanzamiento de Lucene fue la indexación incremental. Antes de este programa solo era posible la indexación por bloques, que permitía indexar únicamente índices completos. No obstante, con la indexación incremental es posible actualizar un índice, de modo que se pueden añadir o eliminar entradas individuales.

¿Apache Lucene vs. Google y cía.?

La pregunta parece lógica: ¿para qué crear tu propio motor de búsqueda cuando ya hay otros como Google o Bing disponibles? Por supuesto, esta pregunta no tiene fácil respuesta y, en definitiva, depende de la aplicación que cada usuario quiera hacer de un motor. Pero una cosa hay que tenerla clara: cuando hablamos de Lucene como motor de búsqueda, se está haciendo uso solo de una denominación simplificada.

De hecho, Apache Lucene es una biblioteca de recuperación de información. Lucene es, por lo tanto, un sistema con el que se puede encontrar información. También lo son Google y otros motores de búsqueda, si bien estos se limitan a la información en Internet. Lucene se puede usar en cualquier escenario y es posible configurarlo para que se adapte a las necesidades de cada usuario, por ejemplo, al integrarlo en otras aplicaciones.

En resumen

Apache Lucene no es, al contrario que los buscadores web, un software listo para usar: para obtener un beneficio de las opciones del sistema, primero es necesario programar el propio buscador. En nuestro tutorial de Lucene te mostraremos los primeros pasos.

Lucene, Solr y Elasticsearch ¿en qué se diferencian?

Sobre todo los menos expertos suelen preguntarse cuáles son las diferencias entre Apache Lucene, Apache Solr y Elasticsearch. Los dos últimos se basan en Lucene: el producto original es un motor de búsqueda. Solr y Elasticsearch se presentan como servidores de búsqueda completos que amplían aún más el alcance de Lucene.

Nota

Si únicamente necesitas una función de búsqueda para tu sitio web, es mejor recurrir a Solr o Elasticsearch, pues estos sistemas están específicamente diseñados para su aplicación en la web.

Apache Lucene: tutorial de instalación y configuración

En la versión original, Lucene está basado en Java, lo que permite utilizar el motor de búsqueda para diferentes plataformas online y offline siempre que sepas cómo hacerlo. A continuación, te explicamos cómo crear paso a paso tu propio motor de búsqueda con Apache Lucene.

Nota

Este tutorial utiliza Lucene basado en Java. El código se probó en la versión 7.3.1 de Lucene y la versión 8 de JDK. Se trabaja con Eclipse en Ubuntu. Cada uno de estos pasos puede cambiar si se utilizan otros entornos de desarrollo o sistemas operativos diferentes.

Instalación

Para poder trabajar con Apache Lucene, has de tener Java instalado en tu sistema. Al igual que Lucene, es posible descargar el software Java Development Kit (JDK) de forma gratuita desde la página web oficial de Oracle. También debes instalar un entorno de desarrollo que te permita trabajar con el código de Lucene. Muchos desarrolladores utilizan Eclipse, si bien hay otras ofertas disponibles de código abierto. Finalmente puedes descargar Lucene de la página del proyecto. Elige la versión principal del programa.

Con todo, no es necesario que instales Lucene, basta con abrir el archivo descargado en la ubicación deseada. A continuación, crea un nuevo proyecto en Eclipse o en otro entorno de desarrollo y añade Lucene como biblioteca. Para este ejemplo, utilizamos tres bibliotecas, todas ellas incluidas en el paquete de instalación:

  • …/lucene-7.3.1/core/lucene-core-7.3.1.jar
  • …/lucene-7.3.1/queryparser/lucene-queryparser-7.3.1.jar
  • …/lucene-7.3.1/analysis/common/lucene-analyzers-common-7.3.1.jar

Si estás utilizando una versión diferente o has modificado la estructura de los directorios, debes adaptar la información en consecuencia.

Consejo

Si careces de los conocimientos básicos de Java y de programación en general, los siguientes pasos pueden resultarte difíciles. Sin embargo, si ya cuentas con experiencia en este lenguaje de programación, trabajar con Lucene es una buena forma de desarrollar tus habilidades.

Indexación

El núcleo de un motor de búsqueda basado en Lucene es el índice. Sin un índice no se puede ofrecer una función de búsqueda. Es por eso que el primer paso es crear una clase en Java para la indexación.

Pero antes de construir el mecanismo de indexación real, creamos dos clases que pueden servir de ayuda con el resto, pues posteriormente tanto la clase de índice como la clase de búsqueda recurren a ellas.

package tutorial;
public class LuceneConstants {
    public static final String CONTENTS = "contents";
    public static final String FILE_NAME = "filename";
    public static final String FILE_PATH = "filepath";
    public static final int MAX_SEARCH = 10;
}

Estos datos serán importantes más adelante cuando se trate de definir los campos con exactitud.

package tutorial;
import java.io.File;
import java.io.FileFilter;
public class TextFileFilter implements FileFilter {
    @Override
    public boolean accept(File pathname) {
        return pathname.getName().toLowerCase().endsWith(".txt");
    }
}

Con ello se implementa un filtro que lee los documentos correctamente. Ya aquí se puede ver que este motor de búsqueda solo va a funcionar con los archivos txt. Este ejemplo ignorará el resto de formatos.

Nota

Para empezar con una clase, importa primero otras clases, que bien forman ya parte de la instalación de Java o están disponibles a través de la integración de bibliotecas externas.

Es ahora cuando puedes crear la clase real para la indexación.

package tutorial;
import java.io.File;
import java.io.FileFilter;
import java.io.FileReader;
import java.io.IOException;
import java.nio.file.Paths;
import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.TextField;
import org.apache.lucene.index.CorruptIndexException;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.IndexWriterConfig;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.FSDirectory;
public class Indexer {
    private IndexWriter writer;
    public Indexer(String indexDirectoryPath) throws IOException {
        Directory indexDirectory = 
            FSDirectory.open(Paths.get(indexDirectoryPath));
        StandardAnalyzer analyzer = new StandardAnalyzer();
        IndexWriterConfig iwc = new IndexWriterConfig(analyzer);
        writer = new IndexWriter(indexDirectory, iwc);
    }
    public void close() throws CorruptIndexException, IOException {
        writer.close();
    }
    private Document getDocument(File file) throws IOException {
        Document document = new Document();
        TextField contentField = new TextField(LuceneConstants.CONTENTS, new FileReader(file));
        TextField fileNameField = new TextField(LuceneConstants.FILE_NAME,
            file.getName(),TextField.Store.YES);
        TextField filePathField = new TextField(LuceneConstants.FILE_PATH,
            file.getCanonicalPath(),TextField.Store.YES);
        document.add(contentField);
        document.add(fileNameField);
        document.add(filePathField);
        return document;
    }    
    private void indexFile(File file) throws IOException {
        System.out.println("Indexing "+file.getCanonicalPath());
        Document document = getDocument(file);
        writer.addDocument(document);
    }
    public int createIndex(String dataDirPath, FileFilter filter) 
        throws IOException {
        File[] files = new File(dataDirPath).listFiles();
        for (File file : files) {
            if(!file.isDirectory()
                && !file.isHidden()
                && file.exists()
                && file.canRead()
                && filter.accept(file)
            ){
                indexFile(file);
            }
        }
        return writer.numDocs();
    }
} 

En el transcurso del código se ejecutan varios pasos: el IndexWriter se configura con el StandardAnalyzer. Lucene ofrece diferentes clases para análisis, todas ellas se encuentran en la biblioteca correspondiente.

Consejo

En la documentación de Apache Lucene puedes encontrar todas las clases que se incluyen en la descarga.

Además, el programa lee los archivos y establece los campos para la indexación. Al final del código, se crean los archivos de índice.

Función de búsqueda

Claro está que el índice por sí solo no sirve de nada. Es por eso que también es necesario establecer una función de búsqueda.

package tutorial;
import java.io.IOException;
import java.nio.file.Paths;
import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.apache.lucene.document.Document;
import org.apache.lucene.index.CorruptIndexException;
import org.apache.lucene.index.DirectoryReader;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.queryparser.classic.ParseException;
import org.apache.lucene.queryparser.classic.QueryParser;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.ScoreDoc;
import org.apache.lucene.search.TopDocs;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.FSDirectory;
public class Searcher {
	
   IndexSearcher indexSearcher;
   QueryParser queryParser;
   Query query;
   
   public Searcher(String indexDirectoryPath) 
      throws IOException {
      Directory indexDirectory = 
         FSDirectory.open(Paths.get(indexDirectoryPath));
      IndexReader reader = DirectoryReader.open(indexDirectory);
      indexSearcher = new IndexSearcher(reader);
      queryParser = new QueryParser(LuceneConstants.CONTENTS,
         new StandardAnalyzer());
   }
   
   public TopDocs search( String searchQuery) 
      throws IOException, ParseException {
      query = queryParser.parse(searchQuery);
      return indexSearcher.search(query, LuceneConstants.MAX_SEARCH);
   }
   public Document getDocument(ScoreDoc scoreDoc) 
      throws CorruptIndexException, IOException {
      return indexSearcher.doc(scoreDoc.doc);	
   }
}

En el código hay dos clases importadas por Lucene que cuentan con una importancia particular: IndexSearcher y QueryParser. Mientras la primera realiza la búsqueda en el índice, QueryParser es responsable de transformar la consulta de búsqueda a un tipo de información que la máquina pueda entender.

Es ahora cuando dispones tanto de una clase para indexar como de otra clase para realizar búsquedas en el índice, pero todavía no puedes realizar una consulta específica con ninguna de ellas. Por esto motivo requieres una quinta clase.

package tutorial;
import java.io.IOException;
import org.apache.lucene.document.Document;
import org.apache.lucene.queryparser.classic.ParseException;
import org.apache.lucene.search.ScoreDoc;
import org.apache.lucene.search.TopDocs;
public class LuceneTester {
	
   String indexDir = "/home/Index/";
   String dataDir = "/home/Data/";
   Indexer indexer;
   Searcher searcher;
   public static void main(String[] args) {
      LuceneTester tester;
      try {
         tester = new LuceneTester();
         tester.createIndex();
         tester.search("YourSearchTerm");
      } catch (IOException e) {
         e.printStackTrace();
      } catch (ParseException e) {
         e.printStackTrace();
      }
   }
   private void createIndex() throws IOException {
      indexer = new Indexer(indexDir);
      int numIndexed;
      long startTime = System.currentTimeMillis();	
      numIndexed = indexer.createIndex(dataDir, new TextFileFilter());
      long endTime = System.currentTimeMillis();
      indexer.close();
      System.out.println(numIndexed+" File indexed, time taken: "
         +(endTime-startTime)+" ms");		
   }
   private void search(String searchQuery) throws IOException, ParseException {
      searcher = new Searcher(indexDir);
      long startTime = System.currentTimeMillis();
      TopDocs hits = searcher.search(searchQuery);
      long endTime = System.currentTimeMillis();
   
      System.out.println(hits.totalHits +
         " documents found. Time :" + (endTime - startTime));
      for(ScoreDoc scoreDoc : hits.scoreDocs) {
         Document doc = searcher.getDocument(scoreDoc);
            System.out.println("File: "
            + doc.get(LuceneConstants.FILE_PATH));
      }  
   }
}

En estas clases finales has de adaptar al menos tres entradas, ya que aquí se especifican las rutas que llevan a los documentos originales y a los archivos de índice y se indica la palabra o palabras de búsqueda.

  • String indexDir: aquí se inserta la ruta al directorio donde se desean almacenar los archivos de índice.
  • String dataDir: el código fuente exige la ruta al directorio en el que se almacenan los documentos a buscar.
  • tester.search: introduce el término de búsqueda.

Dado que en los tres casos se trata de una cadena de caracteres (strings), las expresiones han de incluirse entre comillas. Para indicar las rutas se usan barras oblicuas normales en lugar de barras inversas, también en Windows.

Para testar el programa, copia algunos archivos de texto plano en el directorio establecido como dataDir. Tienes que asegurarte de que los archivos seleccionados tienen la extensión ".txt". A continuación inicia el programa: en Eclipse, por ejemplo, clica en el botón con la flecha verde en la barra de menú.

Nota

El código de programa presentado es solo una demo del funcionamiento de Lucene. Por ejemplo, en este programa falta una interfaz gráfica de usuario: has de introducir el término de búsqueda directamente en el código fuente y solo puedes obtener el resultado a través de la consola.

Sintaxis de las consultas en Lucene

La mayoría de los motores de búsqueda, también los más conocidos de la web, no solo se limitan a la búsqueda de un solo término. Con determinados métodos es posible encadenar términos, buscar frases o excluir palabras concretas. Apache Lucene también ofrece estas posibilidades: Lucene Query Syntax permite buscar expresiones complejas, incluso en diferentes campos.

  • Un solo término: se introduce sin más modificaciones. A diferencia de Google, Lucene asume que el término introducido está escrito correctamente, por lo que si el usuario introduce un error ortográfico, obtendrá un resultado negativo. Ejemplo: “Coche”.
  • Frase: las frases son secuencias de palabras establecidas. En este caso, no solo son importantes los términos individuales introducidos, sino también el orden en que aparecen. Ejemplo: "Mi coche es rojo".
  • Consulta con comodines: en la consulta puedes reemplazar uno o más caracteres con diferentes marcadores de posición. Los caracteres comodines pueden usarse tanto al final como en la mitad de un término, pero nunca al principio.
    • ?: El signo de interrogación representa un carácter. Ejemplo: co?he.
    • *: El asterisco sustituye a un número infinito de caracteres. Por ejemplo, puedes buscar otras formas de un término, como el plural. Ejemplo: Coche*.
  • Consulta de expresiones regulares: con las expresiones regulares puedes buscar al mismo tiempo varios términos que comparten una serie de similitudes. A diferencia de los comodines, con estos marcadores se definen exactamente las divergencias que hay tener en cuenta. Para ello, se recurre a las barras oblicuas y a los corchetes. Ejemplo: /[mp]adre/.
  • Consulta fuzzy searches: este tipo de consulta se utiliza cuando se quiere tener tolerancia a errores. Se utiliza la distancia de Damerau-Levenshtein, fórmula que evalúa las similitudes, para establecer cómo de grande puede ser la desviación. Usa la virgulilla para indicarlo. Se admiten distancias de 0 a 2. Ejemplo: Coche~1.
  • Búsquedas de proximidad: cuando quieras permitir una aproximación en las frases, utiliza también la virgulilla. Por ejemplo, puedes especificar que deseas buscar dos términos incluso si hay otras 5 palabras entre ellos. Ejemplo: "Auto rojo"~5.
  • Búsquedas por rango: en este tipo de consulta se busca entre dos términos en un área concreta. Si bien esta búsqueda no tiene mucho sentido para contenido general, puede ser muy útil para consultar ciertos campos, como los autores o los títulos. La clasificación funciona de acuerdo con un orden lexicográfico. Se recurre a los corchetes para indicar un área de inclusión y a las llaves para indicar un área de exclusión, en ambos casos determinadas por los dos términos indicados en la búsqueda. Estos términos están delimitados con “TO” (a, hacia). Ejemplo: [Allende TO Borges] o {Byron TO Shelley}
  • Boosting: Lucene brinda la oportunidad de dar más relevancia en la búsqueda a determinados términos o frases, lo que influye en la clasificación de los resultados. Para indicar este parámetro de búsqueda se utiliza el acento circunflejo seguido del valor al que se le quiera dar más relevancia. Ejemplo: Coche^2 rojo.
  • Operadores booleanos: los operadores lógicos sirven para establecer conexiones entre términos en una consulta. Los operadores han de estar siempre escritos en mayúsculas para que Lucene no los considere términos de búsqueda normales.
    • AND: con AND, ambos términos deben estar presentes en el documento para que este se muestre como resultado. En lugar del término AND se puede usar también dos signos et (&&). Ejemplo: Coche && rojo.
    • OR: con OR, situado entre dos términos de búsqueda, se indica que para que se muestre un documento como resultado al menos uno de los términos indicados ha de aparecer en él. Además de OR, se puede usar||, si bien también es posible no introducir operador alguno. Al fin y al cabo, la consulta con OR es una búsqueda estándar formada por dos o más términos. Ejemplo: Coche rojo.
    • +: el signo más se utiliza para establecer un caso específico de conector OR. Si colocas el símbolo directamente delante de una palabra, se establece que dicho término debe aparecer, mientras que el otro es opcional. Ejemplo: +Auto rojo
    • NOT: La asociación con NOT excluye ciertos términos o frases de la búsqueda. También puedes recurrir a un signo de exclamación o emplazar un signo menos justo antes del término que desea excluir. No debes utilizar el operador NOT con un solo término o frase. Ejemplo: Auto rojo – azul
  • Grouping: Utiliza los paréntesis para agrupar términos dentro de una consulta. Es de gran utilidad para crear entradas más complejas, por ejemplo, para vincular un término con otros dos que se rijan por un criterio de búsqueda diferente: Auto AND (rojo OR azul).
  • Escaping Special Characters: para utilizar como términos de búsqueda aquellos caracteres que se emplean en la sintaxis de Lucene, combínalos mediante barra invertida. Así, puedes insertar, por ejemplo, un signo de interrogación en una consulta de búsqueda sin que el analizador lo interprete como un comodín: "¿Dónde está mi coche\?".

Apache Lucene: ventajas y desventajas

Lucene es una potente herramienta para configurar una función de búsqueda en la web, en archivos o en aplicaciones. Los seguidores de Lucene prefieren utilizar el programa para crear mediante indexación motores de búsqueda de gran velocidad, que además pueden adaptarse a sus necesidades. Al ser un proyecto de código abierto, Lucene no solo está disponible de forma gratuita, sino que también dispone de una gran comunidad que lo desarrolla sin pausa.

Puedes usarlo en Java, pero también en PHP, Python y otros lenguajes de programación. De ello se deduce una única desventaja: es necesario contar con conocimientos en programación. Es por eso que Apache Lucene no es apto para todos. Si solo necesitas una función de búsqueda para tu web, posiblemente prefieras optar por otras ofertas.

Ventajas Desventajas
Disponible para diferentes lenguajes de programación Requiere conocimientos en programación
Código abierto  
Rápido y ligero  
Búsqueda clasificada  
Posibilidad de búsquedas complejas  
Muy flexible