Quelques notes sur Elasticsearch version 2.4 (en production sur ssodnet.imcce.fr). A vérifier si elles sont toujours d'actualité en version 5.

Généralités

Si un champ n'a pas de mapping associé alors c'est celui déterminé par ES qui est utilisé (dynamic mapping). Donc si on ne veut pas indexer ou analyser un champ, il faut le dire explicitement.

Fonctionnement des analyzers

Un analyzer est défini par un tokenizer et 0 ou plusieurs token filters. L'objectif du tokenizer est de découper une chaîne en plusieurs tokens. Les token filters traîtent (transforment) chaque token obtenu.

Les analyzers sont utilisés à deux niveaux : lors de l'indexation et lors de la recherche. Ils peuvent être différents mais il faut faire attention à ce qu'ils soient cohérents.

Pour définir un analyzer on doit se poser ce genre de questions :

Test des fonctionnalités

Pour avoir des sorties cURL plus lisibles on peut utiliser l'outil python mjson ou ajouter le paramètre pretty=true à l'URI.

curl curl_parameters | python -mjson.tool

Vérifier la configuration du mapping

Cette étape est nécesaire pour s'assurer que les champs ont bien le mapping défini.

http://ssodnet.imcce.fr:9200/ssodnet/_mapping

API _analyze

Tester un analyzer

curl 'http://ssodnet.imcce.fr:9200/ssodnet/_analyze?analyzer={ANALYZER}&pretty=true' -d "string"

Exemple :

curl 'http://ssodnet.imcce.fr:9200/ssodnet/_analyze?analyzer=search_is_analyzer&pretty=true' -d "P/2000 Haléo"

Tester un tokenizer

curl 'http://ssodnet.imcce.fr:9200/ssodnet/_analyze?tokenizer={TOKENIZER}&pretty=true' -d "string"

Exemple :

curl 'http://ssodnet.imcce.fr:9200/ssodnet/_analyze?tokenizer=standard&pretty=true' -d "P/2000 Haléo"

Tester les token filters

curl 'http://ssodnet.imcce.fr:9200/ssodnet/_analyze?tokenizer={TOKENIZER}&filters={TF1},{TF2}&pretty=true' -d "string"

Exemple :

curl 'http://ssodnet.imcce.fr:9200/ssodnet/_analyze?tokenizer=standard&filters=lowercase,asciifolding&pretty=true' -d "P/2000 Haléo"

Tester comment un champ est analysé

Cela permet de vérifier que le configuration du mapping est correcte.

curl 'http://ssodnet.imcce.fr:9200/ssodnet/_analyze?field={FIELD}&pretty=true' -d "string"

Exemples :

curl 'http://ssodnet.imcce.fr:9200/ssodnet/_analyze?field=name.raw&pretty=true' -d "P/2000 Haléo"
curl 'http://ssodnet.imcce.fr:9200/ssodnet/_analyze?field=name.sayt&pretty=true' -d "P/2000 Haléo"

Si le champ est inconnu c'est l'analyzer standard qui sera utilisé. Pour les champs analysés de plusieurs façons différentes il faut bien indiquer le suffixe sinon l'analyzer standard sera utilisé.

API _validate

Permet de voir comment la recherche est effectuée.

curl 'http://ssodnet.imcce.fr:9200/ssodnet/sso/_validate/query?rewrite=true&pretty=true' -d '
{"query": {
   "multi_match": {
      "query": "1950 TT2",
      "type": "best_fields",
      "fields": ["name.raw^2", "name", "aliases.raw^2", "aliases"],
      "operator": "and"
   }
}}'
curl 'http://ssodnet.imcce.fr:9200/ssodnet/sso/_validate/query?rewrite=true&pretty=true' -d '
{"query": {
   "query_string": {
      "query": "1950 TT2",
      "fields" : ["name.raw", "name", "aliases.raw", "aliases"]
   }
}}'

API _search

Highlight

curl 'http://ssodnet.imcce.fr:9200/ssodnet/sso/_search?pretty=true' -d '
{"query": {
   "multi_match": {
      "query": "2001 L",
      "fields": ["name.sayt"]
   }},
   "highlight": {
      "fields": {
         "name.sayt": {}
      }
}}'
curl 'http://ssodnet.imcce.fr:9200/ssodnet/sso/_search?pretty=true' -d '
{"query": {
   "multi_match": {
      "query": "C/2003",
      "fields": ["name.raw^2", "name.sayt", "aliases.raw^2", "aliases.sayt"]
   }},
   "highlight": {
      "fields": {
         "name.raw": {},
          "name.sayt": {},
          "aliases.raw": {},
          "aliases.sayt": {}
      }
}}'

Quand on highlight sur un champ multi-valué le highlight est sur l'intégralité de la chaîne (semble être le cas tout le temps). Dans l'exemple précédent les champs aliases.* sont multi-valués alors que name.* non.

Recherche

Recherche exacte - query

Dans ce cas on recherche exactement un ou plusieurs tokens. C'est à dire que tous les tokens doivent apparaître dans les champs demandés (ici toutes les déclinaisons de name et aliases avec un boost si la chaîne recherchée a la même valeur que celle du champ). La recherche présentée dans l'exemple n'est pas sensible à la casse (cf. mapping ssodnet).

curl 'http://ssodnet.imcce.fr:9200/ssodnet/sso/_search?pretty=true' -d '
{"query": {
   "multi_match": {
      "query": "1950 TT2",
      "type": "best_fields",
      "fields": ["name.raw^2", "name", "aliases.raw^2", "aliases"],
      "operator": "and"
   }
}}'

La recherche de type multi_match est analysée avant d'être executée :

On peut aussi ajouter un filtre pour affiner cette recherche (ici sur le champ type). Ce filtre est effectué au moyen d'une recherche de type term. La chaîne n'est donc pas analysée avant d'être exécutée. Elle est donc sensible à la casse.

curl 'http://ssodnet.imcce.fr:9200/ssodnet/sso/_search?pretty=true' -d '
{"query": {
   "bool": {
      "must": [
         {"multi_match": {
            "query": "io",
            "fields": ["name.raw^2", "name", "aliases.raw^2", "aliases"],
            "operator": "and"
         }}
      ],
      "filter": [
         {"term": {"type": "Asteroid"}}
      ]
   }
}}'

Pour filtrer les données selon le type (dans cet exemple), on se place dans un context filter car on ne veut pas obtenir de résultats qui ne correspondent pas au type demandé. Si on avait utilisé une clause match, la requête aurait aussi retournée des ressources du mauvais type mais avec un score faible (donc en fin de liste).

Recherche avancée - query_string

Dans ce cas on peut exprimer sa propre requête et non une simple liste de tokens. La syntaxe est documentée ici. On peut aussi utiliser une version simplifiée qui n'est pas sensible aux erreurs dans la saisie de la requête.

curl 'http://ssodnet.imcce.fr:9200/ssodnet/sso/_search?pretty=true' -d '
{"query": {
   "query_string": {
      "query": "1950 tt2",
      "fields": ["name.raw", "name", "aliases.raw", "aliases"]
   }
}}'
curl 'http://ssodnet.imcce.fr:9200/ssodnet/sso/_search?pretty=true' -d '
{"query": {
   "query_string": {
      "query": "\"1950 tt2\"",
      "fields": ["name.raw", "name", "aliases.raw", "aliases"]
   }
}}'

L'utilisation de l'API _validate montre pourquoi ces deux exemples ne retournent pas le même résultat.

curl 'http://ssodnet.imcce.fr:9200/ssodnet/sso/_validate/query?rewrite=true&pretty=true' -d '
{"query": {
   "query_string": {
      "query": "1950 tt2",
      "fields": ["name.raw", "name", "aliases.raw", "aliases"]
   }
}}'

explanation : +((name.raw:1950 | name:1950 | aliases.raw:1950 | aliases:1950) (name.raw:tt2 | name:tt2 | aliases.raw:tt2 | aliases:tt2))

curl 'http://ssodnet.imcce.fr:9200/ssodnet/sso/_validate/query?rewrite=true&pretty=true' -d '
{"query": {
   "query_string": {
      "query": "\"1950 tt2\"",
      "fields": ["name.raw", "name", "aliases.raw", "aliases"]
   }
}}'

explanation : +(name.raw:1950 tt2 | name:\"1950 tt2\" | aliases.raw:1950 tt2 | aliases:\"1950 tt2\")

dfs_query_then_fetch

Si le classement des résultats retournés semble étrange (c-à-d que le calcul du score ne correspond pas à ce que la logique voudrait), c'est peut être dû au sharding.

Cf. https://www.elastic.co/blog/understanding-query-then-fetch-vs-dfs-query-then-fetch

Application à SsODNet/Quaero

Les champs présents dans les ressources sont principalement de type string:

Les différents désignations pour un objet se trouvent dans les champs name et aliases.

On propose 3 façons de faire des recherches dans SsODNet :

Cas d'utilisation :

Analyzers

Pour répondre aux cas d'utilisation du service, on défini plusieurs analyzers :

Cf. définition du service.

Recherche

/sso

Dans ce cas d'utilisation l'utilisateur entre une liste de tokens et s'attend à obtenir comme résultat les sso où tous les tokens sont trouvés dans les champs name ou aliases.

Comme le champ aliases est multi-valué on peut obtenir des sso dont un des alias contient un token et un autre alias contient un autre token. Par exemple une recherche de 2000 JW8 retourne un sso avec un alias 2000 AB174 et un alias 1997 JW8.

Comme on recherche dans plusieurs champs, on utilise une recherche de type multi_match et on attribue un poids supplémentaire.

Dans cette recherche le même analyzer est utilisé pour l'indexation et la recherche.

Cf Recherche exacte - query

/sso/search

Dans ce cas d'utilisation une recherche est possible sur aucun ou des champs donnés. Les contraintes sont alors exprimées sous forme de clé:valeur. Si la clé n'est pas indiquée alors les tokens sont recherchés dans les attributs name et aliases (car le _all est désactivé).

Cas d'une recherche sur corot-1 :

Le résultat obtenu pour une recherche sans préciser d'attribut est dû au fait que les champs utilisés dans ce cas sont name, aliases, name.raw et aliases.raw. Or l'analyzer utilisé pour name indexe ce champ comme (corot-1, corot, 1) et c'est le même analyzer utilisé pour la recherche.

Cf. Recherche avancée - query_string

TBD utiliser un analyzer qui ne fait rien au moment de la query pour chercher exactement le token entré ?

/sso/instant-search

Dans ce cas on veut pouvoir faire une saisie avec autocomplétion à partir du début de la chaîne ou d'un de ses tokens.

L'analyzer utilisé lors de le recherche est différent de celui de utilisé lors de l'indexation.

L'analyzer à l'indexation doit :

L'analyzer à la recherche doit uniquement :

Deux approches sont possibles : match_phrase_prefix ou edgengram. Comme c'est plus performant de découper les tokens lors de l'indexation on utilise un token filter de type edgeNGram (cf. 1 et 2).

Choix de la taille du edgeNGram :