Modifier le fichier

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 :

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 possibleValeur DéfautCommentaire
cluster.nameStringcoreNom du cluster
nameString${cluster.name}.node.defaultNom du noeud
index.number_of_shardsint1Nombre de shard de l'index par défaut
index.number_of_replicasint0Nombre de replicas de l'index par défaut
http.enabledbooleantrueActivation du HTTP par défaut
client.transport.sniffbooleantrueHeartbeat du client
discovery.zen.ping.multicast.enabledbooleantrueHeartbeat 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
  • 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 possibleValeur DéfautCommentaire
search.inclusion_objetChaîne de caractères : Code d'objet séparés par des , (exemple : CODE_OBJET1,CODE_OBJET2)videListe des codes objets a indexer (exécutée avant search.exclusion_objet). Si vide alors tous les objets sont pris en compte.
search.exclusion_objetChaîne de caractères : Code d'objet séparés par des ,videListe des objets à exclure de l'indéxation
search.restriction_etatChaîne de caractères : Code des états séparés par des ,0005,0006noms des états à exclure au moment de l'indexation
search.read.scroll.timeoutint10Durée du scroll Opensearch
search.read.scroll.timeout.unitTimeUnitMINUTESUnité de la durée du scroll Opensearch
search.batch.indexation.commit-intervalint50commit-interval pour le job de réindexation complet
search.batch.indexation.throttle-limitint4nombre maximum d'exécution en parallèle pour le job de réindexation complet (nombre de thread)
search.batch.indexation.skip-limitint10000nombre maximum d'éléments ignorés pour le job de réindexation complet
batch.indexation.pagesizeint10nombre de documents récupéré par le reader pour le job de réindexation complet
search.batch.indexation.fiche.metatag.commit-intervalint10commit-interval pour le job de réindexation suite à la modification d'une fiche
search.batch.indexation.fiche.metatag.skip-limitint10000nombre maximum d'éléments ignorés pour le job de réindexation suite à la modification d'une fiche
search.batch.indexation.user.commit-intervalint10commit-interval pour le job de réindexation suite à la modification d'un utilisateur
search.batch.indexation.user.skip-limitint10000nombre maximum d'éléments ignorés pour le job de réindexation suite à la modification d'un utilisateur
search.batch.indexation.rubrique.commit-intervalint10commit-interval pour le job de réindexation suite à la modification d'une rubrique
search.batch.indexation.rubrique.skip-limitint10000nombre maximum d'éléments ignorés pour le job de réindexation suite à la modification d'une rubrique
search.batch.indexation.label.commit-intervalint10commit-interval pour le job de réindexation suite à la modification d'un label
search.batch.indexation.label.skip-limitint10000nombre maximum d'éléments ignorés pour le job de réindexation suite à la modification d'un label
search.batch.indexation.media.commit-intervalint10commit-interval pour le job de réindexation suite à la modification d'un média
search.batch.indexation.media.skip-limitint10000nombre 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.

  1. 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.
  1. Appel du service de recherche :
  • Le service de recherche (FulltextSearchFactory) est appelé pour obtenir un ServiceSearcher approprié.
  • Si un ServiceSearcher est trouvé, il exécute la recherche et obtient un SearchResultBean.
  1. Initialisation du contexte de recherche :
  • La méthode initContext initialise un SearchContext avec les informations de la requête, les résultats de la recherche, et d'autres métadonnées.
  1. Gestion des résultats de recherche :
  • Si des résultats de recherche sont obtenus, le servlet parcourt les AbstractSearchHandler disponibles pour trouver celui qui peut gérer la requête et la réponse.
  • Le AbstractSearchHandler approprié est utilisé pour construire et retourner un ModelAndView.
  1. Gestion des erreurs :
  • Si aucun ServiceSearcher ou résultat de recherche n'est trouvé, une exception est levée.