Améliorez la pertinence de vos résultats ElasticSearch grâce au score

Améliorez la pertinence de vos résultats ElasticSearch grâce au score.

  1. ElasticSearch
  2. Une histoire de score
  3. Indexation
  4. Requêter
  5. Conclusion

ElasticSearch

ElasticSearch est un moteur de recherche très puissant mais relativement simple à mettre en place et à intégrer grâce à son API RESTful. Des bibliothèques telles que le client PHP Elastica et le bundle Symfony FOSElasticaBundle facilitent encore plus son intégration. Néanmoins la configuration fine du moteur de recherche reste assez complexe et peut faire peur au premier abord.

Je ne vais pas parler de la configuration serveur et infrastructure d'ElasticSearch qui touche plus aux performances et à la sécurité de l'outil mais plutôt m'attarder sur la configuration du moteur de recherche en lui-même, de ce qui impactera la pertinence de vos résultats.

Deux choses vont impacter les résultats de vos recherches : l'indexation de vos données et vos requêtes de recherche. Ce sont donc ces deux points qui vont être abordés dans cet article.

Une histoire de score

Lors d'une recherche ElasticSearch, un score est calculé pour chaque document du résultat. Ce score est censé représenter la pertinence du document afin de pouvoir ordonner les résultats. Néanmoins il ne représente que la pertinence des résultats face aux paramètres de la recherche et d'indexation.

Pour calculer ce score, ElasticSearch va s'appuyer sur trois critères :

  • La fréquence du terme recherché dans le document. Plus le terme est fréquent, plus son poids sera élevé.
  • La fréquence inverse du terme à travers tous les documents. Plus le terme est fréquent, moins il aura de poids.
  • La longueur du champ. Plus le champ est grand, plus le poids sera faible ; inversement, plus le champ est petit, plus le poids sera élevé.

Par defaut, ElasticSearch combine ces 3 règles pour obtenir un score, mais certaines peuvent être désactivées si elles ne vous semblent pas correspondre à vos données. Pour plus d'informations sur le calcul du score, lisez la théorie du score sur le site d'ElasticSearch.

Ces règles permettent déjà d'avoir une bonne notion de pertinence mais restent assez simples et ne prennent pas en compte le métier de vos données. Pour ajouter plus de logique dans les scores, vous devrez introduire vos propres règles qui influenceront voire remplaceront le score.

Indexation

L'indexation est la première étape lorsque qu'il s'agit d'optimiser la pertinence de son moteur de recherche. Car c'est grâce aux données indéxées qu'ElasticSearch va calculer les scores.

Typage

ElasticSearch propose un large choix de types pour vos données. Il a de nombreux types spéciaux qui n'existent pas dans les languages de programmation tels que geo_point ou ip. Il est important de typer correctement ses données car ElasticSearch dispose de traitements optimisés pour chaque type.

Analyser

L'analyser est chargé d'examiner les données a indéxer afin de les stocker de la façon la plus optimale pour les recherches. Cette partie est très importante car des données mal indéxées ne permettront pas une recherche pertinente. Il faut donc choisir avec soin l'analyser pour chaque type de donnée que vous souhaitez indéxer. Ce choix est d'autant plus important pour les données complexes telles qu'un texte.

ElasticSearch propose plusieurs analysers configurables. Chaque analyzer est une combinaison d'un tokeniser chargé de découper votre donnée en tokens, de char filters chargés de filtrer les caractères et de token filters chargés de filtrer les tokens.

Vous pouvez également créer votre propre analyzer en combinant vous-même tokeniser, char filters et token filters. Pour configurer un moteur de recherche efficace, il est donc recommandé de choisir ou créer un analyser adapté à chacune des données indéxées.

Par exemple, pour obtenir une indexation efficace d'un texte, il existe quelques filtres très importants à mettre en place :

  • stemmer : Permet une analyse linguistique de votre texte basée sur les racines des mots dans une langue donnée (une recherche sur le mot "collection" trouvera ainsi les mots "collectionner" ou "collectionneur" par exemple).
  • stop : Permet de filtrer les stop words, c'est-à-dire les mots de liaison qui ne sont pas porteurs de sens et qui ne feraient que polluer l'index (en français par exemple : "de", "en", "à", "le", "la", ...).
  • keyword_marker : Permet d'indiquer des mots clés à considérer comme un seul token et non comme plusieurs mots (par exemple "service worker" ou "sous domaine" sont des mots clés).
  • lowercase : Permet de tout indexer en lowercase afin de ne pas être sensible à la casse.

Il existe bien évidemment des analysers par langue déjà prêts à l'emploi, mais l'idée est de vous montrer qu'il est important de bien indiquer à ElasticSearch comment analyser vos données.

Boost

Vous pouvez ajouter dans votre mapping des boost sur certaines propriétés afin de privilégier automatiquement ces propriétés lors du calcul de pertinence.

{
  "mappings": {
    "article": {
      "properties": {
        "title": {
          "type": "text",
          "boost": 3
        },
        "content": {
          "type": "text"
        }
      }
    }
  }
}
fos_elastica:
  indexes:
    app:
      types:
        article:
          mappings:
            title:   { analyzer: my_analyzer, boost: 3 }
            content: { analyzer: my_analyzer }

Dans cet exemple, le titre aura 3 fois plus de poids que le contenu lors du calcul de pertinence.

Attention

Les boosts indiqués au mapping ne fonctionneront que sur les requêtes de type term. Pour les requêtes de type range ou match par exemple, il faudra préciser les boosts dans la requête comme expliqué dans la suite de l'article.

Requêter

Analyzer

Pour utiliser votre analyser lors de la recherche, vous devez le préciser dans votre requête. Vous pouvez compléter votre requête avec les options fuzziness et minimum_should_match.

minimum_should_match permet d'indiquer le pourcentage minimum de votre recherche qui doit être trouvé dans vos documents.

fuzziness permet de rechercher des termes malgré des fautes de frappe (inversion de lettre, lettre manquante, ...) en utilisant la Distance de Levenshtein.

Les scores des résultats seront bien évidemment impactés par ces options.

{
  "query": {
    "bool": {
      "must": [
        { "match": {
          "title": {
            "query": "Foobar",
            "analyser": "my_analyser",
            "fuzziness": "AUTO",
            "minimum_should_match": "70%"
          }
        }}
      ]
    }
  }
}
setFieldQuery('title', $search)
    ->setFieldAnalyzer('title', 'my_analyzer')
    ->setFieldFuzziness('title', 'AUTO')
    ->setFieldMinimumShouldMatch('title', '70%')
);

Boost

Les boost permettent également d'augmenter le poids d'une clause de votre rêquete. Plus le boost est élevé, plus votre clause pèsera sur le score.

Dans l'exemple suivant, nous faisons une recherche de la chaine Foobar sur un document ayant un titre et un contenu. Grâce aux boost nous pouvons donner plus d'importance aux titres qu'aux contenus.

{
  "query": {
    "bool": {
      "should": [
        { "match": {
          "title": { "query": "Foobar", "boost": 5 }
        }},
        { "match": {
          "content": { "query": "Foobar", "boost": 2 }
        }}
      ]
    }
  }
}
addShould((new Query\Match())
    ->setFieldQuery('title', $search)
    ->setFieldBoost('title', 5)
);

$query->addShould((new Query\Match())
    ->setFieldQuery('content', $search)
    ->setFieldBoost('content', 2)
);

Vous pouvez également utiliser plusieurs boost sur la même propriété mais avec plusieurs valeurs afin d'augmenter le score par palier.

{
  "query" : {
    "bool" : {
      "should" : [
        { "range" : {
          "publishedAt" : { "boost" : 5, "gte" : "<1 month ago>" }
        }},
        { "range" : {
          "publishedAt" : { "boost" : 4, "gte" : "<2 months ago>" }
        }},
        { "range" : {
          "publishedAt" : { "boost" : 3, "gte" : "<3 months ago>" }
        }}
      ]
    }
  }
}
addShould((new Query\Range('publishedAt', [
    'boost' => 5,
    'gte'   => (new \DateTime('-1 month'))->format('c'),
])));

$query->addShould((new Query\Range('publishedAt', [
    'boost' => 4,
    'gte'   => (new \DateTime('-2 months'))->format('c'),
])));

$query->addShould((new Query\Range('publishedAt', [
    'boost' => 3,
    'gte'   => (new \DateTime('-3 months'))->format('c'),
])));

Les fonctions de score

Les fonctions de score permettent de modifier le score de vos résultats.

Il existe plusieurs types de fonctions de score :

  • script_score
  • weight
  • random_score
  • field_value_factor
  • decay functions

Je vais surtout détailler les fonctions script et decay car ce sont celles qui permettent le plus d'implémenter une logique de pertinence. Pour les autres vous pouvez lire la documentation sur les fonctions de score.

Les scripts de score

Les scripts de score (script_score) vous permettent de modifier le score de vos résultats à partir d'un script ou d'une formule de votre choix. Vous avez accès au document dont vous modifiez le score et pouvez donc utiliser l'une de ses propriétés dans le calcul. _score est une variable qui contient le score original.

{
    "script_score" : {
        "script" : {
          "lang": "painless",
          "inline": "_score * doc['my_numeric_field'].value"
        }
    }
}
addScriptScoreFunction(
    new \Elastica\Script("_score * doc['my_numeric_field'].value")
);

$score->setQuery($bool);

$query = new Query($score);

Vous pouvez ainsi utiliser une valeur ou une formule métier pour calculer la pertinence de vos résultats.

Facteur

Cette fonction de score (field_value_factor) vous permet d'appliquer un facteur de multiplication (factor), une valeur par defaut (missing) ainsi qu'une fonction mathématique (modifier) à une propriété de votre document. Plusieurs fonctions mathématiques sont disponibles (log, sqrt, ln, ...).

{
    "field_value_factor": {
        "field": "rate",
        "factor": 1.1,
        "modifier": "sqrt",
        "missing": 1
    }
}
addFieldValueFactorFunction(
    'rate',
    1.1,
    Query\FunctionScore::FIELD_VALUE_FACTOR_MODIFIER_SQRT,
    1
);

$score->setQuery($bool);

$query = new Query($score);

Dans cet exemple, la pertinence d'un résultat repose sur la note du document via la formule suivante : sqrt(1.1 * doc.rate).

Les fonctions de décroissance

Les fonctions de décroissance (decay function) sont une autre méthode pour modifier le score de vos résultats. Elles se basent sur des fonctions mathématiques pour réduire le score de vos résultats.

{
    "DECAY_FUNCTION": {
        "FIELD_NAME": {
              "origin": "2017-04-24",
              "offset": "1d",
              "scale": "5d",
              "decay": 0.5
        }
    }
}
addDecayFunction(
    Query\FunctionScore::DECAY_LINEAR,
    'publishedAt',
    '2017-04-24',
    '5d',
    '1d',
    0.90
);

$score->setQuery($bool);

$query = new Query($score);

Chaque fonction de décroissance est caratérisée par les propriétés origin, offset, scale et decay.

  • origin est la valeur centrale à partir de laquelle sera calculée la distance de vos résultats. D'une manière générale, plus vos résultats s'éloigneront de cette valeur centrale, plus le score sera réduit.
  • offset est la distance à partir de laquelle s'appliquera votre fonction de décroissance. Avant cette distance le score ne sera pas modifié.
  • scale est la valeur à laquelle votre fonction de décroissance appliquera la réduction souhaitée.
  • decay est la valeur de réduction de score souhaitée (pourcentage de 0 à 1).

Dans l'exemple ci-dessus, la valeur centrale est le 24 avril 2017 et on souhaite qu'à 6 jours (1 jour d'offset + 5 jours de scale) de cette date, soit le 18 et le 30 avril, le score soit réduit de moitié. La réduction du score des autres résultats sera calculée par la fonction de décroissance choisie.

Il existe 3 fonctions de décroissances, linéaire, exponentielle et gaussienne.

La fonction linéaire est une droite, la décroissance est proportionelle à la distance. Avec la fonction exponentielle, la décroissance est très forte au début et diminue rapidement avec la distance jusqu'à tendre vers zero. Avec la fonction gaussienne, la décroissance est également très forte au début mais diminue moins rapidement.

Decay functions

Les fonctions de décroissance peuvent être appliquées sur des valeurs numériques, des dates (offset et scale sont alors exprimés en durée : 5h ou 1d par exemple) ou des géopoints (offset et scale sont alors exprimés en distance : 100m ou 5km par exemple).

Conclusion

Avec toutes ces fonctionnalités, vous dévriez être capables de gérer la pertinence de votre moteur de recherche assez finement. Attention néanmoins, cet article n'est pas exhaustif, ElasticSearch propose bien d'autres possibilités.

L'important est de ne pas se limiter à la configuration de base et d'adapter l'algorithme de score à vos données et vos besoins.