Module de recherche avec Opensearch
Ce module gère l'ensemble du mécanisme permettant de mettre en place le moteur de recherche sur le produit K-Sup.
Le moteur de recherche est basé sur l'utilisation du serveur Opensearch dans la version 2.4.0
Dans ce module vous trouverez :
- La configuration de la communication avec le serveur de données (Opensearch).
- Le mécanisme d'indexation des données.
- Les méthodes pour constituer les requêtes et les exécuter.
Les notes suivantes décrivent le fonctionnement général intégré à K-Sup. Vous trouverez aussi des informations pour surcharger certaines parties du comportement.
Infrastucture et intégration d'Opensearch
Fonctionnement
Fonctionnement général
Le moteur Opensearch est un serveur RESTful nativement scalable. Ce moteur expose des API REST pour échanger avec lui. K-Sup va interagir avec ce moteur en utilisant les différentes API REST exposées.
Opensearch utilise une base de données NoSQL orientée document. Les objets manipulés sont donc des documents.
API Java
Opensearch propose de plus des librairies Java permettant de manipuler facilement les objets échangés :
- Pour la constitution des requêtes de recherche : query, agrégation, highlight
- Pour l'utilisation des outils exposés : percolateur, santé du serveur ...
- Pour la manipulation des objets retournés : réponse, highlight ...
Deux librairies sont publiées :
- Java Low Level REST Client : permettant la communication via HTTP avec le serveur
- Java High Level REST Client : surcouche de la librairie précédente, mais exposant les APIs métier.
Nativement, K-Sup utilise un moteur de recherche mono-serveur (configuré avec un seul shard), mais il est possible de modifier ce comportement pour communiquer avec un cluster Opensearch par exemple ( ou pour ajouter des shards ou des réplicas).
Afin de fonctionner, le serveur Opensearch nécessite l'installation de plugins :
- ingest-attachment : plugin utilisé pour l'indexation de pièce jointe (utilisation de Tika).
Environnement de production
L'installation d'un serveur Opensearch est décrite dans la documentation se trouvant sur le site de la documentation K-Sup : Serveur Opensearch.
Attention cette documentation n'est pas à jour car pointe sur un environnement 2.4.6 (alors que la présente version est en 7.9).
Environnement de développement
Afin de pouvoir utiliser la recherche sur un poste de développement, il faut avoir a disposition un serveur Opensearch déployé.
Si vous avez besoin de déployer un serveur sur votre poste, vous pouvez utiliser le conteneur docker suivant : ksup-docker-opensearch. Vous trouverez sur le repository la documentation d'utilisation de ce conteneur.
Déclaration du serveur sur un poste K-Sup
Dans les fichiers de properties de votre projet, il faut déclarer le serveur Opensearch cible. Pour se faire, il faut créer un fichier env_opensearch.properties dans le répertoire storage de votre application et déclarer à minima la propriété suivante search.nodes pour indiquer le hostname du serveur Opensearch ainsi que son port :
search.nodes=localhost:32787
Modification du comportement K-Sup
Propriétés exposées
| Clé | Valeur possible | Valeur Défaut | Commentaire |
|---|---|---|---|
| cluster.name | String | core | Nom du cluster |
| name | String | ${cluster.name}.node.default | Nom du noeud |
| index.number_of_shards | int | 1 | Nombre de shard de l'index par défaut |
| index.number_of_replicas | int | 0 | Nombre de replicas de l'index par défaut |
| http.enabled | boolean | true | Activation du HTTP par défaut |
| client.transport.sniff | boolean | true | Heartbeat du client |
| discovery.zen.ping.multicast.enabled | boolean | true | Heartbeat du client |
Indexation
Fonctionnement
Les documents présents dans Opensearch sont sérialisés au format json par K-Sup et sont ensuite poussés via l'API du serveur Opensearch. Seules les fiches sont aujourd'hui sérialisées au travers de l'objet FicheIndexDocument. Cet objet permet lors de la sérialisation, de dénormaliser le modèle objet des fiches afin de stocker en plus des données propre de la fiche :
- Les différentes rubriques sur lesquelles la fiche est visible : rubrique de rattachement et multi-publication.
- Les plugins liés à la fiche
- Les ressources attachées : médias issus de la médiathèque ou non.
- Différentes informations techniques
- Code de la fiche.
- ID du metatag lié à la fiche.
- Type de fiche.
- Date de publication de la fiche.
- Si la fiche est visible en front offic (en ligne, dans un site actif ...).
- Les contrôles d'accès : Champ utilisé pour effectuer avec une regexp la vérification des droits sur la fiche.
- Liste des champs utilisés pour les agrégations transverses entre chaque fiche (thématique).
L'indexation des fiches est effectuée de différentes manières :
- au fil de l'eau lors de l'enregistrement d'un contenu.
- si un contenu modifié est référencé dans d'autres contenus, ces contenus parents seront mis à jour afin d'avoir les données en phase. Ces traitements sont réalisés via des jobs SpringBatch. Les contenus écoutés de cette manière sont :
- Les fiches enfant : fiche structure par exemple.
- Les libellés.
- Les utilisateurs.
- Les rubriques.
- Les traitements d'indexation au fil de l'eau sont réalisés via un bus de message déclenché lors d'évènements métiers (enregistrement). Ces messages sont gérés via le framework Spring Integration
- si un contenu modifié est référencé dans d'autres contenus, ces contenus parents seront mis à jour afin d'avoir les données en phase. Ces traitements sont réalisés via des jobs SpringBatch. Les contenus écoutés de cette manière sont :
- de manière globale lors de l'exécution du script automatisé indexerJobModule Indexation complète des contenus.
Afin de sérialiser les objets et leurs attributs avec le bon type (Texte, date, booléen ...), Opensearch utilise des templates. Il faut donc décrire comment chaque champ doit être indexé.
Opensearch met à disposition 2 types de templates :
- index_template : template utilisé pour décrire le mapping d'un template
- component_template : fragment de template mutualisé entre différents index_template.
Dans K-Sup il y a autant d'index que de langues (et donc autant de index_template). Les templates sont chargés à la création de chaque index.
Description des templates
Les templates décrivant l'indexation des données K-Sup sont placés dans le répertoire /resources/com/kosmos/search/mapping. Le répertoire index_template contient les fichiers décrivant les indexes et le répertoire component_template contient les fragments.
Description des fichier index_template
Les fichiers index_template décrivent les analyzers utilisés pour les différentes langues. Il y a un ainsi fichier par langue. Ces templates vont décrire les analyzer utilisés pour chaque langue :
- Tokenizer utilisés
- Filtres spécifiques à la langue : stemming, stop words ...
Les analyzer de chaque langue sont toujours écrits de la même manière :
- un analyzer nommé analyzer_core_xx décrivant l'analyze par défaut des données stockées.
- un analyzer nommé analyzer_core_raw_xx décrivant l'analyze des données avec peu de traitement.
- Les tokens seront ensuite stockés avec un chemin préfixé par .raw*.
- un analyzer nommé analyzer_core_raw_stemmer_xx l'analyze des données sans stemming.
- un analyzer nommé analyzer_core_ngram_xx décrivant l'analyze des données avec l'utilisation du filtre edgengram.
- Les tokens seront ensuite stockés avec un chemin préfixé par .ngram.
Plusieurs mapping sont ensuite décrits dans ce fichier
Champ fulltext
Afin de limiter le nombre de champs sur lesquels est effectuée la recherche un champ nommé fullText est créé en plus des champs de la fiche. Ce champ va contenir toutes valeurs de tous les champs candidats à la recherche fullText. Dans l'index, nous aurons donc l'ensemble des champs et ce champ. Il sera possible de requêter ainsi de manière indifférente sur ce champ ou sur un champ individuel (pour ajouter un boost particulier sur un champ par exemple).
Mapping dynamique
Un mapping dynamique est aussi présent pour détailler comment sérializer les champs de type String de l'application.
Ce mapping dynamique doit décrire :
- le champ par défaut utilisant l'analyzer analyzer_core_xx
- Ce champ doit aussi contenir l'instruction pour copier les données dans le champ full_text.
- un champ raw utilisant l'analyzer analyzer_core_xx
- un champ ngram utilisant l'analyzer analyzer_core_ngram_xx
Afin d'effectuer les highlights avec l'algorithme (plus performant), il est nécessaire de mettre l'attribut
"term_vector": "with_positions_offsets"pour chaque analyzer.
Description des fichiers component_template
Ces fichiers contiennent les fragments mutualisés entre les différentes langues. Ils décrivent notamment le mapping de certains champs comme ceux que l'on souhaite exclure du mapping par défaut :
- code de fiche
- date ...
Indexation par batch
Lors de certaines modifications des batchs sont déclenchés afin de mettre à jour les données présentes dans les indexes Opensearch. Ces batches réalisés avec Spring-Batch sont déclarés dans le fichier search-batch-job.xml. Pour chaque élément modifié une requête est effectuée pour récupérer l'ensemble des éléments liés à modifier. Ces requêtes sont déclarées en utilisant un template mustache. Les items lus sont ensuite modifiés et de nouveau sérialisés dans le serveur Opensearch. Exemple de requête :
<bean id="searchFicheMetatagItemReader" parent="searchAbstractItemReader" scope="step">
<property name="template">
<bean class="org.opensearch.script.mustache.SearchTemplateRequest">
<property name="script">
<value>
{
"query":{
"query_string" : {
"fields": [
"*.fiche_id_metatag"
],
"query": "{{idFicheMetatag}}"
}
}
}
</value>
</property>
<property name="scriptType" value="INLINE"/>
<property name="scriptParams">
<map>
<entry key="idFicheMetatag" value="#{jobParameters['job.id_fiche_metatag']}" />
</map>
</property>
</bean>
</property>
</bean>
Indexation des dépendances
Lors de l'indexation d'un contenu, les dépendances du contenu seront aussi sérialisées (libellé, fiche, rubrique, utilisateur). Ces serialisations sont déclenchées par l'ajout d'annotation sur les attributs des beans décrivant les objets (héritant de AbstractFicheBean)
Sérialisation des utilisateurs
La sérialisation est activée en ajoutant l'annotation @User sur l'attribut.
@User
protected String codeValidation = StringUtils.EMPTY;
Une fois sérialisé, un utilisateur est de la forme suivante :
"codeValidation": {
"user_birthday": null,
"user_ldap_code": "",
"user_mail": "admin@xxx.fr",
"user_first_name": "Administrateur",
"user_code": "admin",
"user_id": 1,
"user_name": "K-Sup",
"user_gender": null
},
Sérialisation des rubriques
La sérialisation est activée en ajoutant l'annotation @Rubrique sur l'attribut.
@Rubrique
protected String codeRubrique = StringUtils.EMPTY;
Une fois sérialisé, un utilisateur est de la forme suivante :
"codeRubrique": [
{
"rubrique_category": "0000",
"rubrique_value": "Fonctionnalités éditoriales",
"rubrique_code": "1404583540039",
"rubrique_id": 16
}
],
Sérialisation des libellés
La sérialisation est activée en ajoutant l'annotation @Label sur l'attribut.
@Label(type = "04")
protected String thematique = null;
Cette anotation prend 3 paramètres :
- type : Définit le type de Libelle (issu de la table LIBELLE).
- strategy : Stratégie de mapping permettant d'associer le code de libellé avec une valeur. Il y a deux valeurs possibles :
- ServiceLabelStrategy (stratégie par défaut) : stratégie utilisée pour les libellés stockés en base de données.
- DatLabelStrategy : stratégie utilisée pour les libellés stockés dans les fichiers .dat. Cette propriété est obligatoire si on utilise la stratégie DatLabelStrategy.
- contexte : Contexte d'extension (extension dans laquelle se trouve le libellé)
Une fois sérialisé, un libellé est de la forme suivante :
"thematique": [
{
"label_value": "Thématique 1",
"label_id": 1000035,
"label_code": "THEM"
},
{
"label_value": "Administratif",
"label_id": 1000010,
"label_code": "01"
},
{
"label_value": "Sports-Loisirs",
"label_id": 1000014,
"label_code": "05"
}
],
Sérialisation des fiches
La sérialisation est activée en ajoutant l'annotation @Fiche sur l'attribut.
@Fiche(contexte = "ofin", nomObjet = "annuaireksup")
private String codeResponsable = null;
Cette annotation prend 2 paramètres :
- nomObjet : nom de l'objet cible du rattachement.
- contexte : contexte de l'extension dans laquelle se trouve l'objet cible. Afin de ne pas sérialiser l'ensemble de la base de données, la profondeur de sérialisation est paramétrable et est limitée par défaut à 2 niveaux de profondeur. Pour les données écartées, leurs identifiants sont sérialisés (code, langue, état).
La fiche structure d'une fiche est sérialisée. Mais la fiche structure parente de la fiche structure ne l'est pas. Seules ses identifiants sont sérialisée.
Indexation des toolboxes
Afin de sérialiser les toolboxes, il faut que l'annotation @GetterAnnotation soit placé sur le getter de l'attribut souhaité.
@GetterAnnotation(isToolBox = true)
protected String getResume() {
return this.resume;
}
Cette annotation indique que le contenu indexé provient d'une toolbox, il va donc interpréter les tags de toolbox et le code HTML au lieu de les mettre en brut. Lors de la serialisation, les balises HTML sont filtrées et ne sont donc pas présentes dans le document Opensearch. La sérialisation est effectuée par la classe ToolboxSerializer.
Indexation des plugins
Les plugins sont indexés dans le champ plugins de la fiche. Les données sont sérialisées sont à intégrer dans un bean héritant de PluginIndexableBean généré par la méthode generatePluginIndex du Plugin (héritant de IPluginFiche). L'implémentation par défaut de DefaultPluginFiche return null. Il faut donc implémenter un traitement spécifique pour chaque plugin en surchargeant la méthode generatePluginBean. Le bean PluginBean indexé peut utiliser les annotations de sérialisation (@Label, @User, ...) afin de sérialiser des objets liés comme pour les fiches.
Indexation des medias
L'indexation des médias est réalisé lors de la sérialisation des fiches. Tous les médias sont sérialisés dans le champ resources de la fiche.
Ajouter un champ additionnel à l'indexation d'une fiche
Il est possible d'indexer des données additionnelles lors de l'indexation d'une fiche en déclarant un bean de type AdditionalSearchField.
Lors de l'indexation d'une fiche, l'ensemble des beans de type AdditionalSearchField sont récupérés et ajoutés dans un objet additionalProperties.[TYPE_FICHE] dans l'index de la fiche.
"additionalProperties": {
"formation": {
"libelleAffichable": "Master Sciences du Médicament"
}
}
Pour créer une donnée additionnelle, il faut créer une classe qui étende AdditionalSearchField.
La classe doit surcharger la méthode serialize pour définir le contenu du champ.
Exemple AdditionalFieldLibelleAffichable :
public class AdditionalFieldLibelleAffichable extends AdditionalSearchField {
private static final long serialVersionUID = -7342698426096788996L;
@Override
public void serialize(final AbstractFicheBean abstractFicheBean, final JsonGenerator gen, final SerializerProvider serializers) throws IOException {
final ServiceFiche serviceFiche = ServiceManager.getServiceForBean((abstractFicheBean.getClass()));
if (serviceFiche != null) {
gen.writeStringField(getFieldName(), serviceFiche.getLibelleAffichable(abstractFicheBean));
}
}
}
Puis ajouter la déclaration du bean dans le contexte du projet en déclarant le nom que portera le champ dans l'index :
<bean id="additionalFieldLibelleAffichable" class="com.kosmos.search.index.mapper.AdditionalFieldLibelleAffichable">
<property name="fieldName" value="libelleAffichable"/>
</bean>
Modification du comportement K-Sup
Propriétés exposées
| Clé | Valeur possible | Valeur Défaut | Commentaire |
|---|---|---|---|
| search.inclusion_objet | Chaîne de caractères : Code d'objet séparés par des , (exemple : CODE_OBJET1,CODE_OBJET2) | vide | Liste des codes objets a indexer (exécutée avant search.exclusion_objet). Si vide alors tous les objets sont pris en compte. |
| search.exclusion_objet | Chaîne de caractères : Code d'objet séparés par des , | vide | Liste des objets à exclure de l'indéxation |
| search.restriction_etat | Chaîne de caractères : Code des états séparés par des , | 0005,0006 | noms des états à exclure au moment de l'indexation |
| search.read.scroll.timeout | int | 10 | Durée du scroll Opensearch |
| search.read.scroll.timeout.unit | TimeUnit | MINUTES | Unité de la durée du scroll Opensearch |
| search.batch.indexation.commit-interval | int | 50 | commit-interval pour le job de réindexation complet |
| search.batch.indexation.throttle-limit | int | 4 | nombre maximum d'exécution en parallèle pour le job de réindexation complet (nombre de thread) |
| search.batch.indexation.skip-limit | int | 10000 | nombre maximum d'éléments ignorés pour le job de réindexation complet |
| batch.indexation.pagesize | int | 10 | nombre de documents récupéré par le reader pour le job de réindexation complet |
| search.batch.indexation.fiche.metatag.commit-interval | int | 10 | commit-interval pour le job de réindexation suite à la modification d'une fiche |
| search.batch.indexation.fiche.metatag.skip-limit | int | 10000 | nombre maximum d'éléments ignorés pour le job de réindexation suite à la modification d'une fiche |
| search.batch.indexation.user.commit-interval | int | 10 | commit-interval pour le job de réindexation suite à la modification d'un utilisateur |
| search.batch.indexation.user.skip-limit | int | 10000 | nombre maximum d'éléments ignorés pour le job de réindexation suite à la modification d'un utilisateur |
| search.batch.indexation.rubrique.commit-interval | int | 10 | commit-interval pour le job de réindexation suite à la modification d'une rubrique |
| search.batch.indexation.rubrique.skip-limit | int | 10000 | nombre maximum d'éléments ignorés pour le job de réindexation suite à la modification d'une rubrique |
| search.batch.indexation.label.commit-interval | int | 10 | commit-interval pour le job de réindexation suite à la modification d'un label |
| search.batch.indexation.label.skip-limit | int | 10000 | nombre maximum d'éléments ignorés pour le job de réindexation suite à la modification d'un label |
| search.batch.indexation.media.commit-interval | int | 10 | commit-interval pour le job de réindexation suite à la modification d'un média |
| search.batch.indexation.media.skip-limit | int | 10000 | nombre maximum d'éléments ignorés pour le job de réindexation suite à la modification d'un média |
Ajout de template
Ajout d'une langue
Si votre projet nécessite le besoin d'ajouter une langue, il suffit d'ajouter un fichier nommé template_xx.json dans le répertoire /resources/com/kosmos/search/mapping/index_template de votre application.
Il faudra ensuite décrire l'ensemble des éléments nécessaires à la description des mappings.
Pour cela il faut s'inspirer des templates existant : template_fr.json.
Ajout d'un template spécifique
Si votre projet a besoin de décrire un template spécifique pour un objet que vous avez à sérialiser, il faut ajouter un fichier dans le répertoire : /resources/com/kosmos/search/mapping/component_template de votre application.
Ajout de serializer spécifique
Lors de la sérialisation des objets, il est possible de déclarer un serializer spécifique afin d'effectuer un traitement particulier. Il faut alors l'enregistrer dans le fichier de contexte de l'extension : Création du serializer :
public class CustomIndexSerializer extends StdSerializer<CustomIndexableBean> {
public CustomIndexSerializer() {
super(CustomIndexableBean.class);
}
/**
* Sérialisation des données.
*/
@Override
public void serialize(final CustomIndexableBean customBean, final JsonGenerator jgen, final SerializerProvider serializers) throws IOException, JsonProcessingException {
jgen.writeStartObject();
//TODO serialize specific data
jgen.writeEndObject();
}
}
Déclaration du serializer dans le fichier de contexte de l'extension (ExtensionContext.xml) :
<bean id="customModule" class="com.fasterxml.jackson.databind.module.SimpleModule">
<constructor-arg type="java.lang.String" value="customModule"/>
<property name="serializers">
<bean id="agendaSimpleSerializer" class="com.fasterxml.jackson.databind.module.SimpleSerializers">
<constructor-arg>
<list>
<bean id="customStdSerialize" class="com.kosmos.agenda.index.CustomIndexSerializer"/>
</list>
</constructor-arg>
</bean>
</property>
</bean>
<!-- Ajout du module de serialisation aux serializers du core -->
<bean id="coreSearchMapper" class="com.kportal.core.context.MergedModulesJacksonBean">
<property name="idBeanToMerge" value="coreSearchMapper"/>
<property name="idExtensionToMerge" value="core"/>
<property name="modules">
<list>
<ref bean="customModule"/>
</list>
</property>
</bean>
Indexation d'une fiche spécifique
Pour qu'une fiche soit indexable, il faut :
- que l'objet FicheUniv représentant la fiche soit indexable. Il faut vérifier que l'attribut isIndexable ne soit pas à false
@FicheAnnotation(isIndexable = false)(il est à true par défaut). - ajouter les différentes annotations (@Label, @User ...) sur les attributs du bean héritant de AbstractFicheBean afin d'ajouter la sérialisation des objets liés.
Indexation des plugins
Pour indexer les plugins, il faut :
- créer un objet héritant IndexablePluginBean détaillant les champs à sérialiser.
- implémenter la méthode generatePluginIndex dans la classe héritant de IPluginFiche du plugin. Cette méthode alimente le bean IndexablePluginBean.
Recherche
Documentation du controllerSearchServlet
Introduction
La SearchServlet dans le package com.kosmos.search.query.servlet est un contrôleur Spring qui gère les requêtes de recherche.
Elle exécute la recherche et interroge les différents AbstractSearchHandler pour déterminer lequel peut prendre en charge la construction de la vue.
Fonctionnement
Méthode doGet
Cette méthode est appelée pour traiter les requêtes GET.
- Génération des options de recherche :
- Les options de recherche sont générées à partir des paramètres de la requête en utilisant
searchOptionFeeder.
- Appel du service de recherche :
- Le service de recherche (
FulltextSearchFactory) est appelé pour obtenir unServiceSearcherapproprié. - Si un
ServiceSearcherest trouvé, il exécute la recherche et obtient unSearchResultBean.
- Initialisation du contexte de recherche :
- La méthode
initContextinitialise unSearchContextavec les informations de la requête, les résultats de la recherche, et d'autres métadonnées.
- Gestion des résultats de recherche :
- Si des résultats de recherche sont obtenus, le servlet parcourt les
AbstractSearchHandlerdisponibles pour trouver celui qui peut gérer la requête et la réponse. - Le
AbstractSearchHandlerapproprié est utilisé pour construire et retourner unModelAndView.
- Gestion des erreurs :
- Si aucun
ServiceSearcherou résultat de recherche n'est trouvé, une exception est levée.