Dans cet article, je vais aborder un sujet qui est bien documenté pour Solr, mais moins pour elasticsearch : le développement et l’installation d’un token filter. La structure du projet Maven doit permettre de packager le token filter aussi bien pour Lucene seul, que pour Solr ou elasticsearch.
Que sont des analyzers ou des token filters ?
Lors du processus d’indexation et de recherche dans Lucene, les données indexées ainsi que les requêtes utilisateurs sont analysées. L’analyse des données est généralement identique lors de l’indexation et de la recherche dans un champ donné (par exemple un titre, un auteur, une date ou position géographique).
L’analyse consiste à appliquer un traitement sur les données ou la requête en fonction des caractéristiques du contenu du champ. Par exemple, s’il s’agit d’un titre, le contenu est généralement tokenisé, puis les tokens sont traités selon la langue (retrait des mots vides, ajout des synonymes, stemmisation). Le processus d’indexation est réalisé par :
- 0 ou plusieurs “char filter”
- 1 tokenizer
- 0 ou plusieurs “token filter”
Les “char filters” ont pour rôle de supprimer ou remplacer des caractères (Mapping char filter) ou des ensembles de caractères dans le flux de données (HTMLStrip char filter ou PatternReplace char filter).
Les tockenizers ont pour rôle de découper (ou pas) le flux de données en termes (généralement les mots d’une phrase).
Les “token filters” ont pour rôle de traitter les tokens produits par le tokenizer pour appliquer des règles liées par exemple à la langue des données (suppression des mots vides, stemmisation, …).
Il est fréquent de devoir mettre en place des token filters “custom” adaptés à des besoins spécifiques ou pour améliorer le fonctionnement de Solr. Il existe de nombreux exemples de token filter “custom” développés par la communauté. Pour l’exemple, j’utiliserai le “auto-phrase-tokenfilter” (https://github.com/bejean/auto-phrase-tokenfilter).
Ce token filter a pour objectif de recréer des expressions en regroupant les mots qu’il aurait été préférable de ne pas découper via le tokenizer car ensembles, ils représentent des concepts concrets. Par exemple, des groupe de termes : “New-York”, “New-York city”, “New-Zealand”, “San Francisco”, « New-Jersey ». Ne pas découper ces concepts permet d’obtenir des résultats de recherche plus précis.
Comme la plupart des exemples que l’on trouve, ce token filter est conçu pour fonctionner non seulement avec Lucene seul, mais également avec Solr. Par contre, il n’est pas prévu pour fonctionner dans elasticsearch. Le but de cet article est d’expliquer comment utiliser ces token filters dans elasticsearch.
Fonctionnement d’un token filter dans Lucene
Un token filter est une classe java. Il s’agit pour l’exemple de la classe AutoPhrasingTokenFilter. Le but ici n’est pas d’expliquer comment est implémenté un token filter mais comment il est “appelé” par l’application qui l’utilise. Voici donc une version simplifiée de la classe pour ce qui concerne son instanciation.
public class AutoPhrasingTokenFilter extends TokenFilter { // The list of auto-phrase character strings private CharArrayMap phraseMap; // If true - emit single tokens as well as auto-phrases private boolean emitSingleTokens; private Character replaceWhitespaceWith = null; public AutoPhrasingTokenFilter( TokenStream input, CharArraySet phraseSet, boolean emitSingleTokens ) { super(input); // Convert to CharArrayMap by iterating the char[] strings and // putting them into the CharArrayMap with Integer of the number // of tokens in the map: need this to determine when a phrase match is completed. this.phraseMap = convertPhraseSet( phraseSet ); this.emitSingleTokens = emitSingleTokens; } public void setReplaceWhitespaceWith( Character replaceWhitespaceWith ) { this.replaceWhitespaceWith = replaceWhitespaceWith; } ... }
On voit que la classe dispose d’un constructeur avec pour paramètre le flux de données à traiter (un flux de tokens) et des paramètres tels que les phrases que l’on souhaite conserver, un paramètre indiquant si les token avant regroupement doivent être également envoyés dans le flux de sortie. Il y a également une méthode pour positionner un paramètre pour indiquer avec quel caractère joindre les termes regroupés.
Une application java appelle donc ce constructeur et éventuellement la méthode lors de la création de l’objet.
AutoPhrasingTokenFilter autoPhraseFilter = new AutoPhrasingTokenFilter( input, phraseSets, emitSingleTokens ); if (replaceWhitespaceWith != null) { autoPhraseFilter.setReplaceWhitespaceWith( new Character( replaceWhitespaceWith.charAt( 0 )) ); }
Fonctionnement d’un token filter dans Solr
Pour utiliser un token filter dans un traitement d’analyse, Solr s’appuie sur le type du champ qu’il traite et sur son paramétrage dans le fichier shema.xml
<fieldType name="text_autophrase" class="solr.TextField" positionIncrementGap="100"> <analyzer> <tokenizer class="solr.StandardTokenizerFactory"/> <filter class="solr.StopFilterFactory" ignoreCase="true" words="stopwords.txt" enablePositionIncrements="true" /> <filter class="solr.LowerCaseFilterFactory"/> <filter class="com.lucidworks.analysis.AutoPhrasingTokenFilterFactory" phrases="autophrases.txt" includeTokens="true" /> <filter class="solr.PorterStemFilterFactory"/> </analyzer> </fieldType>
Pour instancier un objet AutoPhrasingTokenFilter, Solr a besoin d’une factory qui dispose en entrée du paramétrage du token filter pour le type en question.
public class AutoPhrasingTokenFilterFactory extends TokenFilterFactory implements ResourceLoaderAware { private CharArraySet phraseSets; private final String phraseSetFiles; private final boolean ignoreCase; private final boolean emitSingleTokens; private String replaceWhitespaceWith = null; public AutoPhrasingTokenFilterFactory(Map<String, String> initArgs) { super( initArgs ); phraseSetFiles = get(initArgs, "phrases"); ignoreCase = getBoolean( initArgs, "ignoreCase", false); emitSingleTokens = getBoolean( initArgs, "includeTokens", false ); String replaceWhitespaceArg = initArgs.get( "replaceWhitespaceWith" ); if (replaceWhitespaceArg != null) { replaceWhitespaceWith = replaceWhitespaceArg; } } @Override public void inform(ResourceLoader loader) throws IOException { if (phraseSetFiles != null) { phraseSets = getWordSet(loader, phraseSetFiles, ignoreCase); } } @Override public TokenStream create( TokenStream input ) { AutoPhrasingTokenFilter autoPhraseFilter = new AutoPhrasingTokenFilter( input, phraseSets, emitSingleTokens ); if (replaceWhitespaceWith != null) { autoPhraseFilter.setReplaceWhitespaceWith( new Character( replaceWhitespaceWith.charAt( 0 )) ); } return autoPhraseFilter; } }
Le constructeur reçoit la configuration du schéma comme paramètre d’entrée, la fonction “create” retourne un objet AutoPhrasingTokenFilter créé avec ces paramètres.
Je ne rentre pas dans le détail de l’utilisation et l’installation d’un token filter dans Solr. Il faut mettre le ou les jar contenant les classes du token filter et de sa factory dans le classpath et définir un type dans schema.xml.
Fonctionnement d’un token filter dans elasticsearch
Le principe est identique que dans Solr. Il y a bien une factory mais elle est différente de celle de Solr car elle reçoit les paramètres sous une autre forme. De plus, comme toutes extensions de elasticsearch, un token filter est ce que l’on appelle un plugin (https://www.elastic.co/guide/en/elasticsearch/plugins/5.1/intro.html). Il faut donc mettre en place ce plugin. Dans notre cas, le plugin fournit une factory qui elle même va créer le token filter. Pour terminer, on a besoin d’un assembly qui va décrire comment packager le plugin (les classes, les dépendances, la définition du plugin) pour pouvoir l’installer dans elasticsearch.
Pour elasticsearch, on a donc :
- un plugin (une classe java) et son fichier de définition
- une configuration de l’analyzer pour l’index de travail
- une factory
- le token filter
- un assembly qui va décrire comment packager le plugin (les classes, les dépendances, la définition du plugin)
Le plugin : AnalysisAutoPhrasingPlugin.java
Il s’agit d’une classe très simple dans le cadre d’un token filter mais qui dépend du type de plugin.
package org.elasticsearch.plugin.analysis.autophrasing; ... public class AnalysisAutoPhrasingPlugin extends Plugin implements AnalysisPlugin { @Override public Map<String, AnalysisProvider> getTokenFilters() { return singletonMap("autophrasing", AutoPhrasingTokenFilterFactory::new); } }
Le fichier de définition du plugin : plugin-descriptor.properties
Il décrit le plugin : nom, version de elasticsearch compatible et nom de la classe du plugin.
description=AutoPhrasingTokenFilter version=1.0 name=autophrasing classname=org.elasticsearch.plugin.analysis.autophrasing.AnalysisAutoPhrasingPlugin java.version=1.8 elasticsearch.version=5.0.1
La définition de l’analyzer dans l’index de travail
On crée un index avec un analyzer utilisant le token filter et indiquant quels sont les paramètres.
curl -X PUT 'localhost:9200/test_autophrasing' -d ' { "settings" : { "number_of_shards" : 1, "number_of_replicas" : 1, "analysis": { "analyzer": { "my_autophrasing_analyzer": { "type": "custom", "tokenizer": "my_standard", "filter": ["autophrasing", "my_stop"] } }, "tokenizer": { "my_standard": { "type": "standard", "max_token_length": 5 } }, "filter": { "autophrasing" : { "type": "autophrasing", "phrases_path":"dictionary.txt" }, "my_stop": { "type": "stop", "stopwords": ["and", "is", "the"] } } } } }'
La factory : AutoPhrasingTokenFilter
Si on compare la factory elasticsearch à la factory Solr, on constate que la principale différence est dans les paramètres d’appel du constructeur et la manipulation de ces paramètres.
package org.elasticsearch.index.analysis.autophrasing; ... import com.lucidworks.analysis.autophrasing.AutoPhrasingTokenFilter; public class AutoPhrasingTokenFilterFactory extends AbstractTokenFilterFactory { private CharArraySet phraseSets; private final String phraseSetFiles; private final boolean ignoreCase; private final boolean emitSingleTokens; private String replaceWhitespaceWith = null; public AutoPhrasingTokenFilterFactory(IndexSettings indexSettings, Environment env, String name, Settings settings) { super(indexSettings, name, settings); phraseSetFiles= settings.get( "phrases_path" ); this.ignoreCase = settings.getAsBoolean("ignoreCase", false); this.emitSingleTokens = settings.getAsBoolean("includeTokens", false ); String replaceWhitespaceArg = settings.get( "replaceWhitespaceWith" ); if (replaceWhitespaceArg != null) { replaceWhitespaceWith = replaceWhitespaceArg; } if (phraseSetFiles != null) { List pathLoadedPhrases = Analysis.getWordList(env, settings, "phrases"); if (pathLoadedPhrases != null) { phraseSets = new CharArraySet(pathLoadedPhrases, ignoreCase); } } } @Override public TokenStream create(TokenStream tokenStream) { AutoPhrasingTokenFilter autoPhraseFilter = new AutoPhrasingTokenFilter( tokenStream, phraseSets, emitSingleTokens ); if (replaceWhitespaceWith != null) { autoPhraseFilter.setReplaceWhitespaceWith( new Character( replaceWhitespaceWith.charAt( 0 )) ); } return autoPhraseFilter; } }
Le token filter Lucene
Ce toke filter est exactement le même que celui de Solr
L’assembly : plugin.xml
L’assembly permet à Maven de savoir quoi assembler dans la package du plugin (jar du plugin et des dépendances, fichier de description du plugin).
<?xml version="1.0"?> <assembly> <id>plugin</id> <formats> <format>zip</format> </formats> <includeBaseDirectory>false</includeBaseDirectory> <files> <file> <source>${project.basedir}/src/main/resources/plugin-descriptor.properties</source> <outputDirectory>elasticsearch</outputDirectory> <filtered>true</filtered> </file> </files> <dependencySets> <dependencySet> <outputDirectory>/elasticsearch/</outputDirectory> <useProjectArtifact>true</useProjectArtifact> <useTransitiveFiltering>true</useTransitiveFiltering> <excludes> <exclude>org.elasticsearch:elasticsearch</exclude> <exclude>org.apache.lucene:*</exclude> <exclude>org.apache.logging.log4j:*</exclude> </excludes> </dependencySet> <dependencySet> <outputDirectory>elasticsearch</outputDirectory> <useProjectArtifact>true</useProjectArtifact> <useTransitiveFiltering>true</useTransitiveFiltering> <includes> <include>com.lucidworks:lucene-auto-phrase-tokenfilter</include> </includes> </dependencySet> </dependencySets> </assembly>
Structure du projet Maven et organisation des fichiers sources
Pour un Token filter devant fonctionner à la fois sous Solr et elasticsearch, il y a donc 3 composants et donc 3 modules Maven dans un projet père: le token filter Lucene, la factory pour Solr et la factory pour elasticsearch. La copie d’écran ci-dessous illustre cette organisation en un projet Maven constitué de 3 modules avec une arborescence hiérarchique et les 4 fichiers pom.xml.
Le projet principal : solr-es-auto-phrase-tokenfilter
Il est uniquement constitué du fichier pom.xml qui référence les 3 modules et définit les versions des librairies utilisées par les modules.
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>fr.eolya</groupId> <artifactId>solr-es-auto-phrase-tokenfilter</artifactId> <version>0.0.1-SNAPSHOT</version> <packaging>pom</packaging> <name>solr-es-auto-phrase-tokenfilter</name> <description>solr-es-auto-phrase-tokenfilter</description> <modules> <module>solr-auto-phrase-tokenfilter</module> <module>es-auto-phrase-tokenfilter</module> <module>lucene-auto-phrase-tokenfilter</module> </modules> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <lucene.version>6.3.0</lucene.version> <junit.version>4.12</junit.version> <slf4j.version>1.6.4</slf4j.version> <log4j.version>2.6.2</log4j.version> <elasticsearch.version>5.1.1</elasticsearch.version> </properties> </project>
Le projet token filter Lucene : lucene-auto-phrase-tokenfilter
Dans le cas de ce filtre, il est constitué de la seule classe nécessaire au filtre et également de sa classe de tests.
Le fichier pom.xml est simple avec principalement la définition des dépendances.
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>fr.eolya</groupId> <artifactId>solr-es-auto-phrase-tokenfilter</artifactId> <version>0.0.1-SNAPSHOT</version> </parent> <groupId>com.lucidworks</groupId> <artifactId>lucene-auto-phrase-tokenfilter</artifactId> <name>lucene-auto-phrase-tokenfilter</name> <description>lucene-auto-phrase-tokenfilter</description> <dependencies> <dependency> <groupId>org.apache.lucene</groupId> <artifactId>lucene-core</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.slf4j</groupId> <artifactId>slf4j-simple</artifactId> <version>${slf4j.version}</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>${junit.version}</version> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.5.1</version> <configuration> <fork>true</fork> <source>1.8</source> <target>1.8</target> </configuration> </plugin> </plugins> </build> </project>
Le projet token filter Solr: solr-auto-phrase-tokenfilter
Il est également constitué de la seule classe nécessaire à la factory et de sa classe de tests.
Le fichier pom.xml est simple également avec principalement la définition des dépendances. On remarque la dépendance avec le token filter Lucene.
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>fr.eolya</groupId> <artifactId>solr-es-auto-phrase-tokenfilter</artifactId> <version>0.0.1-SNAPSHOT</version> </parent> <groupId>com.lucidworks</groupId> <artifactId>solr-auto-phrase-tokenfilter</artifactId> <name>solr-auto-phrase-tokenfilter</name> <description>solr-auto-phrase-tokenfilter</description> <dependencies> <dependency> <groupId>com.lucidworks</groupId> <artifactId>lucene-auto-phrase-tokenfilter</artifactId> <version>0.0.1-SNAPSHOT</version> </dependency> <dependency> <groupId>org.apache.lucene</groupId> <artifactId>lucene-core</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.solr</groupId> <artifactId>solr-core</artifactId> <version>${lucene.version}</version> </dependency> <dependency> <groupId>org.apache.lucene</groupId> <artifactId>lucene-test-framework</artifactId> <version>${lucene.version}</version> <scope>test</scope> </dependency> <dependency> <groupId>org.apache.solr</groupId> <artifactId>solr-test-framework</artifactId> <version>${lucene.version}</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>${junit.version}</version> <scope>test</scope> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-simple</artifactId> <version>${slf4j.version}</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.5.1</version> <configuration> <fork>true</fork> <source>1.8</source> <target>1.8</target> </configuration> </plugin> </plugins> </build> </project>
Le projet token filter elasticsearch: es-auto-phrase-tokenfilter
Sa structure est plus complexe que pour Solr car outre la classe de la factory, il est constitué de la classe du plugin, du fichier de description du plugin et du fichier de description de l’assembly.
Le fichier pom.xml et plus complexe avec notamment le plugin Maven de construction de l’assembly “maven-assembly-plugin” et des règles d’exclusion dans les dépendances du fait de la grande sensibilité d’elasticsearch au conflits de librairies “Jar Hell” (https://dzone.com/articles/jar-hell-made-easy). On remarque également la dépendance avec le token filter Lucene.
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>fr.eolya</groupId> <artifactId>solr-es-auto-phrase-tokenfilter</artifactId> <version>0.0.1-SNAPSHOT</version> </parent> <groupId>org.elasticsearch</groupId> <artifactId>es-auto-phrase-tokenfilter</artifactId> <name>es-auto-phrase-tokenfilter</name> <description>es-auto-phrase-tokenfilter</description> <dependencies> <dependency> <groupId>com.lucidworks</groupId> <artifactId>lucene-auto-phrase-tokenfilter</artifactId> <version>0.0.1-SNAPSHOT</version> <exclusions> <exclusion> <groupId>org.slf4j</groupId> <artifactId>slf4j-simple</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.apache.lucene</groupId> <artifactId>lucene-core</artifactId> <version>${lucene.version}</version> </dependency> <dependency> <groupId>org.apache.lucene</groupId> <artifactId>lucene-analyzers-common</artifactId> <version>${lucene.version}</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>${junit.version}</version> <scope>test</scope> </dependency> <dependency> <groupId>org.elasticsearch</groupId> <artifactId>elasticsearch</artifactId> <version>${elasticsearch.version}</version> </dependency> <dependency> <groupId>org.elasticsearch.test</groupId> <artifactId>framework</artifactId> <version>${elasticsearch.version}</version> <scope>test</scope> <exclusions> <exclusion> <groupId>org.hamcrest</groupId> <artifactId>hamcrest-all</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>${slf4j.version}</version> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-simple</artifactId> <version>${slf4j.version}</version> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.5.1</version> <configuration> <fork>true</fork> <source>1.8</source> <target>1.8</target> </configuration> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-assembly-plugin</artifactId> <version>2.5.5</version> <configuration> <appendAssemblyId>false</appendAssemblyId> <outputDirectory>${project.build.directory}/releases/</outputDirectory> <descriptors> <descriptor>${basedir}/src/main/assemblies/plugin.xml</descriptor> </descriptors> </configuration> <executions> <execution> <phase>package</phase> <goals> <goal>single</goal> </goals> </execution> </executions> </plugin> </plugins> <resources> <resource> <directory>src/main/resources</directory> <filtering>true</filtering> <includes> <include>*.properties</include> </includes> </resource> <resource> <directory>src/main/resources</directory> <filtering>false</filtering> <excludes> <exclude>*.properties</exclude> </excludes> </resource> </resources> </build> </project>
Le résultat du build du projet est comme pour le le token filter Lucene et la factory Solr un fichier jar qui contient les différentes classe et le fichier de description du plugin, mais également l’assemblage sous la forme d’un fichier zip qui sera déployé dans elasticsearch : le plugin !
Le contenu de l’assemblage (es-auto-phrase-tokenfilter-0.0.1-SNAPSHOT.zip).
On y retrouve tout ce qui est nécessaire au fonctionnement du plugin et qui sera déployé dans le répertoire plugins de elasticsearch au moyen de la commande “elasticsearch-plugin”.
# cd /opt/elasticsearch-5.0.1 # sudo bin/elasticsearch-plugin install file:///path/to/es-auto-phrase-tokenfilter.zip
Dans cet article, j’ai décrit le principe de fonctionnement d’un plugin de type token filer dans elasticsearch ainsi qu’une organisation modulaire d’un projet eclipse afin de générer un token filter à destination à la fois de Lucene, de Solr et de elasticsearch.