Tutorial de Apache Lucene 7 desde cero

25/04/2018

Las bibliotecas digitales contienen millones de ejemplares con infinidad de textos. Muchos son los avances que hemos visto desde la revolución tecnológica desde los años 80. Numerosas librerías y programas se han desarrollado para el procesamiento de los textos almacenados por multitud de repositorios digitales. Por ejemplo, en la Biblioteca Virtual Miguel de Cervantes podemos encontrar el buscador de concordancias, el buscador diacrónico o el buscador de sonetos del Siglo de Oro, que ofrecen diferentes funcionalidades. Todas ellas se basan en la tecnología de Apache Lucene, una API de código abierto para recuperación de información utilizado para la creación de motores de búsqueda.

Lucene ha ido evolucionando desde las primeras versiones 3.6 hasta la actual 7.2, ampliando la funcionalidad y mejorando la eficiencia y tiempos de respuesta. Si buscamos por Internet encontraremos infinidad de ejemplos relativos a las versiones 3, 4, 5, 6 y 7. Sin embrago, de las últimas versiones la información escasea en cierta medida. Es por ello que desde nuestra fundación hemos intentado dar un paso adelante para ofrecer un pequeño tutorial que sirva de ejemplo para favorecer el uso de Lucene en vuestros proyectos.

Desde la web de apache podemos encontrar la documentación de la última versión 7.3.0. En ella se encuentran los ejemplos básicos de indexación y búsqueda que nos dan una idea de cómo utilizar esta librería. Sin embargo, si queremos hacer algo más complejo como por ejemplo utilizar sinónimos y resaltar resultados, la búsqueda de tutoriales e información puede ser más complicada.

En este tutorial vamos a ver un pequeño ejemplo de cómo utilizar el texto de una obra. En primer lugar, vamos a extraer el contenido de la obra para enriquecer cada palabra con su lemma y categoría gramatical. a continuación, vamos a crear un fichero con cada una de estas palabras siguiendo el modelo de Lucene para utilizarlo como un fichero de sinónimos. De esta forma podremos buscar todos los verbos de la obra o el número de ocurrencias del lemma hacer.

El texto seleccionado en nuestro caso de ejemplo es Doña perfecta de Benito Pérez Galdós.

dona-perfecta-perez-galdos
dona-perfecta-perez-galdos

 

En primer lugar, necesitamos automatizar el proceso de categorización y lematización de las palabras. La librería de Stanford CoreNLP permite realizar diferentes operaciones como las mencionadas, e incluso el reconocimiento de entidades como explicamos en nuestro post. Sin embargo, la lematización no funciona para el idioma español como se puede observar en la siguiente tabla.

Afortunadamente, existen otras alternativas como por ejemplo FreeLing. Para poder empezar a trabajar con FreeLing es necesario su instalación. Una vez instalado podemos ejecutar el siguiente comando:

echo "he logrado instalar el programa" | analyze -f /usr/local/share/freeling/config/es.cfg --output xml

Obteniendo como resultado el siguiente fichero xml:

<sentence id="1">
 <token id="t1.1" begin="0" end="2" form="he" lemma="haber" tag="VAIP1S0" ctag="VAI" pos="verb" type="auxiliary" mood="indicative" tense="present" person="1" num="singular" >
 </token>
 <token id="t1.2" begin="3" end="10" form="logrado" lemma="lograr" tag="VMP00SM" ctag="VMP" pos="verb" type="main" mood="participle" num="singular" gen="masculine" >
 </token>
 <token id="t1.3" begin="11" end="19" form="instalar" lemma="instalar" tag="VMN0000" ctag="VMN" pos="verb" type="main" mood="infinitive" >
 </token>
 <token id="t1.4" begin="20" end="22" form="el" lemma="el" tag="DA0MS0" ctag="DA" pos="determiner" type="article" gen="masculine" num="singular" >
 </token>
 <token id="t1.5" begin="23" end="31" form="programa" lemma="programa" tag="NCMS000" ctag="NC" pos="noun" type="common" gen="masculine" num="singular" >
 </token>
</sentence>

Como se puede observar, FreeLing nos devuelve para cada palabra o token su posición, su lema, su categoría gramatical…

Si ejecutamos el comando analyzer de FreeLing para el texto de Galdós obtenemos un fichero como el siguiente:

<sentence id="1">
  <token id="t1.1" begin="0" end="6" form="Cuando" lemma="cuando" tag="CS" ctag="CS" pos="conjunction" type="subordinating" >
  </token>
  <token id="t1.2" begin="7" end="9" form="el" lemma="el" tag="DA0MS0" ctag="DA" pos="determiner" type="article" gen="masculine" num="singular" >
  </token>
  <token id="t1.3" begin="10" end="14" form="tren" lemma="tren" tag="NCMS000" ctag="NC" pos="noun" type="common" gen="masculine" num="singular" >
  </token>
  <token id="t1.4" begin="15" end="20" form="mixto" lemma="mixto" tag="AQ0MS00" ctag="AQ" pos="adjective" type="qualificative" gen="masculine" num="singular" >
  </token>
  <token id="t1.5" begin="21" end="32" form="descendente" lemma="descendente" tag="AQ0CS00" ctag="AQ" pos="adjective" type="qualificative" gen="common" num="singular" >
  </token>
  <token id="t1.6" begin="32" end="33" form="," lemma="," tag="Fc" ctag="Fc" pos="punctuation" type="comma" >
  </token>
  <token id="t1.7" begin="34" end="38" form="núm." lemma="núm." tag="VMIP3S0" ctag="VMI" pos="verb" type="main" mood="indicative" tense="present" person="3" num="singular" >
  </token>
  <token id="t1.8" begin="39" end="41" form="65" lemma="65" tag="Z" ctag="Z" pos="number" >
  </token>
  <token id="t1.9" begin="42" end="43" form="(" lemma="(" tag="Fpa" ctag="Fpa" pos="punctuation" type="parenthesis" punctenclose="open" >
  </token>
  <token id="t1.10" begin="43" end="45" form="no" lemma="no" tag="RN" ctag="RN" pos="adverb" type="negative" >
  </token>
  <token id="t1.11" begin="46" end="48" form="es" lemma="ser" tag="VSIP3S0" ctag="VSI" pos="verb" type="semiauxiliary" mood="indicative" tense="present" person="3" num="singular" >
  </token>
  <token id="t1.12" begin="49" end="56" form="preciso" lemma="preciso" tag="AQ0MS00" ctag="AQ" pos="adjective" type="qualificative" gen="masculine" num="singular" >
  </token>...

El resultado completo lo podéis consultar en el siguiente enlace.

Para poder crear un fichero de sinónimos, debemos utilizar la sintaxis de lucene. Para ello hemos creado la siguiente plantilla xslt para especificar los sinónimos. El resultado tiene la siguiente forma:

ab => ab, tag#NCFS000, ctag#NC, pos#noun, lemma#ab
abajo => abajo, tag#RG, ctag#RG, pos#adverb, lemma#abajo
abandonado => abandonado, tag#VMP00SM, ctag#VMP, pos#verb, lemma#abandonar
abandonados => abandonados, tag#VMP00PM, ctag#VMP, pos#verb, lemma#abandonar
abandonar => abandonar, tag#VMN0000, ctag#VMN, pos#verb, lemma#abandonar
Abandonaron => Abandonaron, tag#VMIS3P0, ctag#VMI, pos#verb, lemma#abandonar
abandone => abandone, tag#VMSP3S0, ctag#VMS, pos#verb, lemma#abandonar
abandones => abandones, tag#VMSP2S0, ctag#VMS, pos#verb, lemma#abandonar
abandono => abandono, tag#NCMS000, ctag#NC, pos#noun, lemma#abandono
abandonó => abandonó, tag#VMIS3S0, ctag#VMI, pos#verb, lemma#abandonar

La parte que nos interesa son los literales que comienzan por pos# y lemma#. En este pequeño ejemplo, y gracias a la funcionalidad de sinónimos de Lucene, si buscamos lemma#abandonar deberíamos obtener abandonados, abandonado, abandonaron, abandone…Para eliminar los duplicados hemos utilizado el comando sort -u sobre el fichero resultante de la plantilla XSLT. De esta forma nos aseguramos que no hay duplicados.

A continuación, comenzamos a trabajar con Lucene. Lo primero que vamos a hacer es crear un proyecto Maven en nuestro IDE preferido. En el fichero pom.xml vamos a especificar las siguientes dependencias:

<properties>
  <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  <compileSource>1.8</compileSource>
  <lucene.version>7.3.0</lucene.version>
</properties>

<dependencies>
  <dependency>
    <groupId>org.apache.lucene</groupId>
    <artifactId>lucene-core</artifactId>
    <version>${lucene.version}</version>
  </dependency>

  <dependency>
    <groupId>org.apache.lucene</groupId>
    <artifactId>lucene-queryparser</artifactId>
    <version>${lucene.version}</version>
  </dependency>

  <dependency>
    <groupId>org.apache.lucene</groupId>
    <artifactId>lucene-analyzers-common</artifactId>
    <version>${lucene.version}</version>
  </dependency>

  <dependency>
    <groupId>org.apache.lucene</groupId>
    <artifactId>lucene-highlighter</artifactId>
    <version>${lucene.version}</version>
  </dependency>
.....

Y de esta forma tan sencilla, ya podemos empezar a trabajar con Lucene 🙂

Añadimos el siguiente código:

public class TestSynonyms {

	static IndexWriter writer;
	static RAMDirectory dir;
	static String field = "contents";

	@BeforeClass
	public static void setup() throws Exception {
		dir = new RAMDirectory();
		Analyzer analyzer = new SynonymIndexAnalyzer();
		IndexWriterConfig config = new IndexWriterConfig(analyzer);
		writer = new IndexWriter(dir, config);

		String docsPath = "src/main/resources/txtfiles";
		final Path docDir = Paths.get(docsPath);
		IndexFiles.indexDocs(writer, docDir);

		writer.forceMerge(1);
		writer.commit();
		writer.close();
	}

	@Test
	public void testAutomata() throws Exception {
		IndexReader reader = DirectoryReader.open(dir);
		IndexSearcher searcher = new IndexSearcher(reader);

		SynonymSearchAnalyzer analyzer = new SynonymSearchAnalyzer();

		TokenStreamToTermAutomatonQuery q = new TokenStreamToTermAutomatonQuery();
		TermAutomatonQuery autQuery = q.toQuery("contents",
				analyzer.tokenStream("contents", "pos#noun pos#adjective"));

		System.out.println("aut:" + autQuery);

		TopDocs hits = searcher.search(autQuery, 10);

		SimpleHTMLFormatter htmlFormatter = new SimpleHTMLFormatter();
		Highlighter highlighter = new Highlighter(htmlFormatter, new QueryScorer(autQuery));
		for (int i = 0; i < hits.totalHits; i++) {
			int id = hits.scoreDocs[i].doc;
			Document doc = searcher.doc(id);

			// Term vector
			String text = doc.get("contents");
			TokenStream tokenStream = TokenSources.getAnyTokenStream(searcher.getIndexReader(), hits.scoreDocs[i].doc,
					"contents", new SynonymSearchAnalyzer());
			TextFragment[] frag = highlighter.getBestTextFragments(tokenStream, text, false, 10);
			for (int j = 0; j < frag.length; j++) {
				if ((frag[j] != null) && (frag[j].getScore() > 0)) {
					System.out.println((frag[j].toString()));
				}
			}
			System.out.println("-------------");
		}

		reader.close();
	}

 

Siguiendo la documentación de Lucene sobre sinónimos y la nueva calse FlattenGraphFilter, en concreto:

However, if you use this during indexing, you must follow it with FlattenGraphFilter to squash tokens on top of one another like SynonymFilter, because the indexer can’t directly consume a graph. To get fully correct positional queries when your synonym replacements are multiple tokens, you should instead apply synonyms using this TokenFilter at query time and translate the resulting graph to a TermAutomatonQuery e.g. using TokenStreamToTermAutomatonQuery.

La indexación utiliza SynonymGraphFilter y FlattenGraphFilter, mientras que la búsqueda solo usa el primero de ellos. El código completo lo podéis consultar en GitHub.

El resultado del test anterior para la búsqueda de la cadena «pos#noun pos#adjective» es el siguiente:

Cuando el <B>tren</B> <B>mixto</B> descendente, núm. 65 (no es preciso nombrar la línea), se detuvo en la pequeña
 caballero con la palabra en la boca. Vio este que se acercaba otro empleado con un <B>farol</B> <B>pendiente</B> de
 ondulaciones luminosas. La luz caía sobre el piso del andén, formando un <B>zig-zag</B> <B>semejante</B> al que describe la
 Volviose y vio una oscura masa de <B>paño</B> <B>pardo</B> sobre sí misma revuelta y por cuyo principal pliegue
 asomaba el avellanado <B>rostro</B> <B>astuto</B> de un labriego castellano. Fijose en la desgarbada estatura que
 <B>terciopelo</B> <B>viejo</B> resplandecían; vio la <B>mano</B> <B>morena</B> y acerada que empuñaba una vara verde, y el ancho
 marchar... La jaca corre como el viento. Me parece que el señor D. José ha de <B>ser</B> <B>buen</B> jinete. Verdad
 el macho cuyo freno debía regir un joven zagal de <B>piernas</B> <B>listas</B> y fogosa sangre, cargaría el
 producían <B>ecos</B> <B>profundos</B> bajo tierra. Al entrar en el túnel del kilómetro 172, lanzó el vapor por el
 silbato, y un <B>aullido</B> <B>estrepitoso</B> resonó en los aires. El túnel, echando por su negra boca un hálito

En el ejemplo se puede observar cómo se realiza el resaltado (con etiquetas HTML <B>) en las ocurrencias del texto.

Esperamos que os guste nuestro pequeño tutorial y os ayude a poner en funcionamiento vuestras ideas para reutilizar el texto de nuestros obras 🙂