Kitabı oku: «Desarrollo de motores de búsqueda utilizando herramientas open source», sayfa 4
2.3 ARQUITECTURA DE APACHE LUCENE
El proceso de indexación que utiliza Apache Lucene requiere analizar los objetos implicados en dicho proceso (figura 2.1):
Figura 2.1 Arquitectura de Apache Lucene.
• Directory. El directorio representa la ubicación de un índice de Lucene. Es una clase abstracta que permite a sus subclases almacenar el índice como mejor consideren. Podríamos obtener una implementación concreta para almacenar archivos en un directorio dentro del sistema de archivos, y pasar dicho objeto a su vez al constructor de IndexWriter.
• IndexWriter. IndexWriter es el componente central del proceso de indexación. Esta clase crea un nuevo índice o abre uno existente, y añade, elimina o actualiza documentos en el índice. Puede pensar en IndexWriter como en un objeto que le da acceso de escritura al índice pero que no le permite realizar búsquedas. IndexWriter necesita un lugar para almacenar su índice, y para eso sirve la clase Directory.
• Analyzer. Antes de indexar el texto, este se pasa por un analizador. El analizador, especificado en el constructor IndexWriter, se encarga de extraer esos tokens del texto y eliminar aquellas palabras que no sean relevantes. Analyzer es una clase abstracta y Lucene viene con varias implementaciones de la misma. Algunas de estas implementaciones se ocupan de omitir las stopwords y otras se encargan de la conversión de tokens a letras minúsculas. Para un desarrollador que integra Lucene en una aplicación, en ocasiones, la elección de los analizadores es un elemento crítico del diseño de la aplicación.
• Document. Un documento representa una colección de campos. Un documento es, simplemente, un contenedor para múltiples campos donde el campo es la clase que contiene el contenido textual que posteriormente se indexará en el índice. Los campos de un documento representan el documento o metadatos asociados con ese documento. Los metadatos (autor, título, tema y fecha de modificación) se indexan y se almacenan por separado como campos de un documento.
• Field. Cada documento en un índice contiene uno o más campos con un determinado nombre, incorporados en una clase llamada Field (campo). Cada campo tiene un nombre y un valor correspondiente, y un conjunto de opciones que controlan cómo Lucene indexará el valor del campo. Un documento puede tener más de un campo con el mismo nombre. En este caso, los valores de los campos se añaden, durante el proceso de indexación, en el orden en que se añadieron al documento.
Lucene, en principio, solo trata con texto y números. De esta forma, el núcleo de Lucene maneja los tipos java.lang.String para cadenas texto y java. io.Reader para lectura de ficheros y los tipos numéricos nativos (como int o float). Aunque varios tipos de documentos se pueden indexar y hacer búsquedas, procesarlos no es tan sencillo como procesar contenido puramente textual o numérico.
En nuestro indexador, para cada archivo de texto que encontramos creamos una nueva instancia de la clase Documento, la rellenamos con campos y metadatos y, finalmente, añadimos ese documento al índice, indexando finalmente el archivo.
La biblioteca Lucene ofrece posibilidades prácticamente ilimitadas para desarrollar sus propias aplicaciones de búsqueda. El proceso de desarrollo se centra en los dos pasos clave para buscar por índice invertido: primero indexar documentos y posteriormente realizar búsquedas sobre los índices creados.
La clase Java org.apache.lucene.index.IndexWriter es responsable de crear y modificar índices. El constructor de la clase org.apache.lucene.index.IndexWriter acepta dos argumentos: un objeto de la clase org.apache. lucene.store.Directory, que representa un directorio donde almacenar los documentos, y otro objeto tipo configuración de la clase org.apache.lucene.index.IndexWriterConfig.
Posteriormente, con el método IndexWriter.addDocument() podemos añadir un documento al índice. El método close() guarda los cambios en el directorio indicado y cierra los archivos temporales que pudieran quedar abiertos.
Un documento consta de un número de campos que Lucene define con la clase org.apache.lucene.document.Field. Estos campos no siguen un esquema definido, ya que cada documento dentro de un índice puede contener campos de diferentes tipos y contenidos. En la práctica, los campos sirven para clasificar el contenido y hacer que se pueda buscar por separado. Los desarrolladores usan el método add() para añadir este contenido a un objeto del tipo Document. Para los casos más comunes, Lucene proporciona una serie de subclases apropiadas para diferentes tipos de datos, como TextField, IntField y DoubleField.
El siguiente ejemplo crea un documento y lo almacena en el índice utilizando el método addDocument() del objeto IndexWriter:
/* Creando un objeto IndexWriter * /
Analyzer analyzer = new EnglishAnalyzer(VERSION. Lucene_40);
IndexWriterConfig config = new IndexWriterConfig(analyzer);
Directory dir = FSDirectory.open(new File(“/home/user/lucene/index”));
IndexWriter writer = new IndexWriter(dir, config);
/* Nuestro documento a almacenar*/
Field textField = new TextField(“text”, “My document”, Field.Store.YES);
Document documento = new Document();
documento.add(textField);
/* Añadir el documento al índice*/
writer.addDocument(documento);
writer.close();
En el código anterior vemos cómo se configura el objeto IndexWriterConfig, donde se ha utilizado un objeto de la clase org.apache.lucene.analysis. Analyzer, que se encarga del análisis de los textos que se van añadiendo al índice. En el ejemplo anterior se ha empleado el analizador para textos en inglés con la clase org.apache.lucene.analysis.en.EnglishAnalyzer.
2.3.1 Proceso de tokenización y búsqueda en Apache Lucene
El analizador comentado desempeña un papel importante para la estructura del índice porque, entre otras cosas, se encarga del proceso de “tokenización”, es decir, de la división del texto en los componentes representados por sus propios tokens de entrada en el índice invertido.
Este proceso es una tarea bastante compleja porque debe diferenciar correctamente entre signos de puntuación y caracteres especiales como partes de un token (como en una dirección web o de correo electrónico) y caracteres independientes (como un punto al final de una oración).
Además, existen numerosas formas de optimizar el índice en el análisis; por ejemplo, se pueden eliminar palabras como “y” —que son bastante frecuentes pero de poca importancia— con la ayuda de los filtros que se encuentran en org.apache.lucene.analysis.TokenFilter.
Los analizadores y tokenizadores son componentes importantes del proceso de indexación porque determinan qué puede buscar un usuario. Para ayudar a evitar cualquier confusión de los diferentes algoritmos de análisis y tokenización, cuando se realiza una consulta sobre alguno de los índices normalmente se lleva acabo utilizando el mismo analizador que el empleado para indexar los documentos.
String queryString = new String(“texto_buscar”);
int maxHits = 100;
/* Crear los objetos IndexReader y IndexSearcher */
Analyzer analyzer = new EnglishAnalyzer(VERSION. Lucene_40);
Directory dir = FSDirectory.open(new File(“/home/user/lucene/index”));
IndexReader reader = DirectoryReader.open(dir);
IndexSearcher searcher = new IndexSearcher(reader);
/* Crear y ejecutar una query */
StandardQueryParser parser = new StandardQueryParser(analyzer);
Query query = parser.parse(queryString);
TopDocs topdocs = searcher.search(query, maxHits);
/* Leer los documentos que se han encontrado */
ScoreDoc[] docs = topdocs.scoreDocs;
for (ScoreDoc documento : docs){
System.out.println(documento.doc+»\t»+documento.score);
}
reader.close();
En el ejemplo anterior, en primera instancia creamos los objetos IndexReader e IndexSearchers. La clase org.apache.lucene.index.IndexReader es responsable de leer un índice a partir de un objeto de la clase org.apache.lucene.index. DirectoryReader, que contiene el directorio donde se encuentra almacenado nuestro índice. El constructor de la clase org.apache.lucene.search.Index-Searcher recibe dicho objeto IndexReader como parámetro en su constructor.
Respecto al proceso de búsqueda, la clase IndexSearcher será la encargada de ejecutar las consultas de búsqueda utilizando un objeto IndexReader. Un objeto de la clase org.apache.lucene.queryparser.flexible.standard. StandardQueryParser genera dichos objetos de consulta a partir de una cadena de consulta y un analizador. Alternativamente, los desarrolladores pueden implementar su propio analizador de consultas para construir los diferentes tipos de consultas y objetos de búsqueda de forma personalizada.
Las búsquedas pasan por el método search() del objeto IndexSearcher. El resultado se almacena en un objeto de la clase org.apache.lucene.search. TopDocs, que contiene los documentos encontrados, así como información sobre el número de documentos y la relevancia de cada uno de ellos.
2.4 TRABAJAR CON APACHE LUCENE
Antes de crear un índice y empezar a trabajar con Lucene, es necesario descargar el framework y crear un proyecto para empezar a trabajar. Lucene está disponible en la página web http://lucene.apache.org, en concreto dentro de la sección Apache Lucene Core (http://lucene.apache.org/core). En la sección Download encontrará disponible para descargar la última versión disponible. Una vez descargado el archivo, lo descomprimimos en una carpeta y extraemos los ficheros que vamos a necesitar para incorporarlos a nuestro proyecto.
2.4.1 Configuración del entorno
Lucene está construido en Java, por lo que es necesario disponer de un sistema operativo compatible con Java con el JDK instalado. Se recomienda usar una versión del JDK compatible con la última versión de Lucene (figura 2.2). Una forma de incluir Lucene en un proyecto de Java es a través de un repositorio Maven. Aquí hay una descripción de dependencia de Maven de muestra para incluir en la biblioteca lucene-core. Puede encontrar el repositorio oficial de Lucene en http://mvnrepository.com/artifact/org.apache.lucene.
Figura 2.2 Librería de Apache Lucene en el repositorio de Java.
Para configurar Lucene en Maven, es necesario añadir la siguiente configuración en el fichero de dependencias pom.xml del proyecto. La versión con la que vamos a trabajar es la publicada en el repositorio de Maven, la 8.6.3.
https://mvnrepository.com/artifact/org.apache.lucene/lucene-core/8.6.3
<!-- https://mvnrepository.com/artifact/org.apache.lucene/lucene-core -->
<dependency>
<groupId>org.apache.lucene</groupId>
<artifactId>lucene-core</artifactId>
<version>8.6.3</version>
</dependency>
Los pasos para crear un proyecto desde Eclipse son:
• Creamos un nuevo proyecto haciendo clic en File -> New-> Maven Project (figura 2.3).
Figura 2.3 Crear nuevo proyecto con Maven desde Eclipse.
• Introducimos un nombre para el artefecto y definimos la versión (figura 2.4).
Figura 2.4 Nombre y versión del artefacto Maven.
El siguiente paso consiste en añadir en el fichero pom.xml las dependencias correspondientes a la API de Lucene.
También podríamos configurar el proyecto añadiendo las librerías al Build Path y haciendo clic en Add External Archives. Podríamos añadir las siguientes librerías (figura 2.5):
Figura 2.5 Librerías para el Java Build Path.
2.4.2 Crear un índice
El núcleo de un motor de búsqueda basado en Lucene es el índice. Por eso, el primer paso es crear una clase en Java para la indexación. La creación de un índice constituye el punto de partida para el trabajo con Lucene. Una vez generado, se le irá añadiendo la información relevante de los distintos documentos seleccionados para el análisis.
Antes de empezar con el ejemplo, vamos a crear una clase en un proyecto de Eclipse. Para ello, haga clic con el botón derecho sobre el nombre del proyecto y, en el menú desplegable, haga clic en New>Class. En la ventana emergente, en este ejemplo escribiremos “LuceneCrearIndice”, seleccionando también las casillas “Public static void main” e “Inherited abstract method”.
El programa que se muestra a continuación, LuceneCrearIndice.java, construye un índice, para lo cual simplemente crea un objeto de la clase IndexWriter que busca e indexa los documentos que se encuentran en una carpeta específica.
En esta clase creamos nuestro índice, junto con las funciones para generar un documento en el índice. En este caso, empleamos la clase IndexWriter del paquete org.apache.lucene.index, que se configura con un objeto de la clase ClasicAnalyzer. Lucene ofrece diferentes clases para análisis; todas ellas se encuentran dentro del paquete org.apache.lucene.analysis.
El siguiente código lo podemos encontrar en el fichero LuceneCrearIndice. java dentro del repositorio:
En el código anterior tendríamos que modificar la rutas por otras que apunten a nuestro equipo. Para cada archivo encontrado en dicha carpeta creamos un objeto tipo Document; estos serán los objetos que realmente se indexarán en el índice. A esta variable le añadimos objetos de tipo Field, que son los campos que queremos indexar y a través de los cuales podremos realizar búsquedas.
Lucene proporciona la clase FSDirectory para indicarle la ruta donde crear el índice en el sistema de archivos. El parámetro indexPath representa la ubicación del directorio; si no existe el directorio, Lucene lo creará de forma automática.
Directory directory = FSDirectory.open(Paths. get(indexPath));
Lucene indexa objetos tipo Document propios de la API de Lucene. Recorre el directorio buscando archivos con extensión txt y con permisos de lectura. En este caso, a cada variable Document le añadimos dos variables Field: una con el nombre del archivo y otra con su contenido. Una vez añadidos los campos a la variable Document, el documento se añade al índice con la instrucción indice.add(doc).
En el ejemplo anterior, lo que hemos hecho ha sido indexar tantos documentos como teníamos en la ruta definida en la variable directorioDocumentos. Esta podría ser la salida:
Indexing /home/linux/documentos/java.txt
Indexing /home/linux/documentos/python.txt
Indexing /home/linux/documentos/php.txt
Indexing /home/linux/documentos/lucene.txt
Indexing /home/linux/documentos/golang.txt
El número de documentos indexados es 5
Al crear el índice, Lucene genera los segmentos para ir añadiendo documentos; cada segmento es un índice completamente independiente a partir del cual realizar búsquedas por separado.
2.4.3 Crear y escribir documentos en un índice
El siguiente código muestra un ejemplo de cómo añadir un documento a un índice. Para ello, primero se inicializan una serie de objetos de las clases Analyzer, Directory, IndexWriterConfig e IndexWriter. Una vez que se obtiene un objeto IndexWriter, se crea un nuevo documento de la clase Document con un TextField personalizado. Por último, el documento se añade al IndexWriter a través del método addDocument(doc).
El ejemplo lo podemos encontrar en el fichero LuceneCrearIndiceDocumentos.java:
Hemos visto cómo se obtiene un IndexWriter inicializado con los objetos Analyzer e IndexWriterConfig. El comportamiento de inicialización predeterminado funciona bien la mayoría de las veces. Sin embargo, puede haber situaciones en las que necesite un control más preciso durante la secuencia de inicialización. Por ejemplo, cuando el comportamiento predeterminado crea un nuevo índice y no existe un índice previo. Esto puede no ser ideal en un entorno de producción donde siempre debe existir un índice.
La clase IndexWriter expone el método addDocument(doc) que le permite añadir un documento al índice. IndexWriter añadirá dicho documento al índice especificado por el directorio. Además, es importante realizar la llamada al método indexWriter.close() al final para que se confirmen todos los cambios, y cerrar el índice para que ningún otro proceso pueda escribir sobre el mismo.
Para que los documentos queden registrados en el índice, además, hay que aplicar el método commit() del objeto IndexWriter; así, los cambios quedarán reflejados en el índice. Al final, el objetivo es añadir documentos al índice a través del objeto IndexWriter, cuya documentación podemos encontrar en:
https://lucene.apache.org/core/8_6_3/core/org/apache/lucene/index/IndexWriter.html
En el código anterior hemos utilizado el método addDocument() para añadir documentos a un índice. Este método proporciona dos constructores (uno donde se pasa por un parámetro un objeto Document y otro donde, además, se pasa por el parámetro un objeto Analyzer):
• addDocument(Document): este método permite añadir el documento utilizando el analizador predeterminado para la tokenización que se indicó al crear el IndexWriter.
• addDocument(Document, Analyzer): permite añadir el documento utilizando el analizador proporcionado para la tokenización. Para que las búsquedas funcionen correctamente, necesita el analizador empleado en el momento de la búsqueda para “hacer coincidir” los tokens producidos por los analizadores en el momento de la indexación.
2.5 REALIZAR BÚSQUEDAS EN APACHE LUCENE
Tras revisar el proceso de indexación de documentos en Lucene, continuamos con la implementación del proceso de búsqueda. Hay que tener en cuenta que la indexación es un proceso necesario para que los textos puedan buscarse.
Si consideramos un escenario de búsqueda donde ya tenemos un índice construido y funcionando en nuestro clúster, el objetivo será buscar todos aquellos documentos relacionados con el índice de Lucene que coincidan con un término de búsqueda. Lucene tiene la capacidad de obtener documentos con solo introducir el término Lucene en el índice, y devolver todos los documentos relacionados. Un término en Lucene contiene dos elementos: el valor y el campo en el que aparece.
2.5.1 Obtención de un IndexSearcher
Para realizar una búsqueda concreta necesitamos apoyarnos en la clase IndexSearcher, que proporciona la base para realizar búsquedas sobre el índice. La clase IndexSearcher es la puerta de entrada para buscar en un índice, en lo que respecta a Lucene. Un IndexSearcher toma un objeto IndexReader y realiza una búsqueda a través de este objeto.
IndexReader se comunica con el índice físicamente y devuelve los resultados. A continuación, se describe un fragmento de código que muestra cómo obtener un objeto de la clase IndexSearcher:
Directory directory = getDirectory();
IndexReader indexReader = DirectoryReader. open(directory);
IndexSearcher indexSearcher = new IndexSearcher(indexReader);
Para que los usuarios puedan realizar búsquedas y obtener lo que buscan, deben introducir un término o términos de búsqueda como parámetro de entrada. 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 booleanos como AND, OR o + y -, entre otros.
Esta consulta la vamos a construir con un objeto de la clase QueryParser, una clase dentro de Lucene que permite traducir la entrada en una solicitud de búsqueda concreta para el motor de búsqueda. Los desarrolladores también pueden configurar QueryParser de modo que se adapte exactamente a las necesidades del usuario. A continuación, pasamos a analizar cómo realizar una búsqueda. Para ello, necesitamos crear un objeto Query con un QueryParser.