12. Recherche d’Information - TP ElasticSearch : pertinence

Cette séance de travaux pratiques est à réaliser à la suite de la précédente (voir chapitre Recherche d’Information - TP ElasticSearch). En particulier, le lancement d’ElasticSearch et le chargement des données sont identiques. On travaille sur l’index contenant les données de 5000 films (environ).

Vous devriez notamment pouvoir relancer votre instance es1 avec la commande suivante (conservez ou enlevez le -i selon que vous souhaitez avoir votre serveur en mode interactif ou non) :

sudo docker start -i es1

Ouvrez ensuite un nouveau terminal.

Elasticsearch et la pertinence

Elasticsearch utilise Lucene, et sa manière d’implémenter le score repose d’abord sur la Practical Scoring Function de Lucene. En regardant cette page, vous devriez constater que ce score repose sur les notions de tf et d’idf que vous connaissez, mais qu’il y a des modifications importantes. Notamment, de nouveaux paramètres font leur apparition, l’idf est élevé au carré et le tf est la racine carré du nombre d’occurrence d’un terme dans le document.

Une première notion de score

Nous pouvons observer avec l’API explain d’Elasticsearch le calcul du score pour un film donné et pour une requête donnée.

Prenons par exemple la requête life sur le titre, et regardons quel score est calculé pour le film des Monty Python, “Life of Brian” (La vie de Brian, en français).

Utilisez la requête suivante dans l’adresse :

movies/movie/2232/_explain

Et celle-ci dans la partie “document” :

{
  "query": {
    "match": {
      "fields.title": "life"
    }
  }
}

Vous devez obtenir un score de 3.0772645.

Avec la documentation (cf lien supra), reconstituez les détails du calcul de ce score, en regardant tf, idf et fieldNorm.

Correction

tf = 1, et sqrt(1) = 1. tf(freq=1.0), with freq of:”

idf idf(docFreq=27, maxDocs=4850). 27 documents sur 4850 contiennent life. Et 1 + ln(4850/(27 + 1)) = 6.154529

“description”: “fieldNorm(doc=679)” “value”: 0.5 : Il y a 3 termes dans “Life of Brian”, et cette fieldNorm est l’inverse de la racine carrée de ce nombre. 1/sqrt(3) = 0.577. Stocké sur un octet donc arrondi à 0.5

Si on multiplie 1 * 6.154529 * 0.5, on obtient 3.077264. Multiplié par la queryWeight de 0.99999994, on obtient le score attendu (problème de chiffres significatifs).

Boosting

Quand on effectue des recherches sur plus d’un champ, il peut rapidement devenir pertinent de donner davantage de poids à l’un ou l’autre de ces champs, de façon à améliorer les résultats de recherche. Par exemple, il peut être tentant d’indiquer qu’une correspondance (match) dans le titre d’un document vaut 2 fois plus qu’une correspondance dans n’importe quel autre champ. C’est ce que l’on appelle en anglais le boosting, cela autorise la modification du score calculé par Elasticsearch en vue de rendre les résultats plus pertinents. Il existe de nombreuses manières d’ajuster les paramètres entrant dans le score, nous allons en aborder quelques unes.

Essayez la commande suivante et observez la position d’American Grafiti dans le classement, avec et sans l’option “boost”. Que se passe-t-il ?

{
  "_source": {
    "includes": [
      "*.title"
    ],
    "excludes": [
      "*.actors*",
      "fields.genres",
      "fields.directors"
    ]
  },
  "query": {
    "bool": {
      "should": [
        {
          "match": {
            "fields.title": {
              "query": "Star Wars",
              "boost": 4
            }
          }
        },
        {
          "match": {
            "fields.directors": {
              "query": "George Lucas"
            }
          }
        }
      ]
    }
  }
}

On peut aussi associer du boosting positif à certaines valeurs de certains champs, tout en rejetant vers le bas du classement des documents qui contiennent certaines valeurs pour d’autres champs. Ici, on ne récupère que les films dont le titre contient Star Wars, mais l’on pondère négativement (negative boost) le réalisateur JJ Abrams, dont le film doit apparaître en queue de classement :

{
  "_source": {
    "includes": [
      "*.title"
    ],
    "excludes": [
      "*.actors*"
    ]
  },
  "query": {
    "boosting": {
      "positive": {
        "query": {
          "match_phrase": {
            "fields.title": {
              "query": "Star Wars",
              "boost": 2
            }
          }
        }
      },
      "negative": {
        "match": {
          "fields.directors": "Abrams"
        }
      },
      "negative_boost": 0.5
    }
  }
}

Si les documents contiennent des valeurs numériques comme la popularité (les likes d’un statut de réseau social, le nombre d’achats d’un produit donné), ou une note, il est possible d’utiliser cet indicateur pour pondérer les documents. On utilise pour cela field_value_factor (attention, il semble qu’Elasticsearch ait un problème si au moins un des documents n’a pas le champ utilisé pour ce boosting). Avec nos documents, nous pouvons proposer une pondération avec la note (ce qui revient à ordonner par rating) :

{
  "_source": {
    "includes": [
      "*.title",
      "*.rating"
    ],
    "excludes": [
      "*.actors*"
    ]
  },
  "query": {
    "function_score": {
      "query": {
        "match_phrase": {
          "fields.directors": {
            "query": "Sergio Leone"
          }
        }
      },
      "functions": [
        {
          "field_value_factor": {
            "field": "fields.rating"
          }
        }
      ]
    }
  }
}

Regardez notamment les scores qui sont maintenant calculés. Il est bien entendu possible d’ajouter de nombreux paramètres, pour modifier la façon dont est utilisée cette valeur de note (multiplication, addition, max, min, etc.). Il est même possible, avec script_score de calculer vos propres valeurs et d’en tenir ensuite compte dans le calcul du score (voir https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-function-score-query.html#function-script-score pour les détails).

Regardons pour finir des fonctions assez utiles, les fonctions de décroissance. Celles-ci s’appliquent à une valeur d’un document selon une échelle “glissante”. L’idée est d’ajuster la pertinence des documents en fonction, par exemple : - de leur ancienneté - de leur distance (géographique) - de leur écart de prix

Cette pondération peut être linéaire, exponentielle, ou gaussienne. Voir https://www.elastic.co/guide/en/elasticsearch/reference/current/images/decay_2d.png pour une illustration du fonctionnement de cette pondération.

Dans nos documents, on dispose d’un champ date précis, donnant le jour de sortie des films. On peut, avec la requête suivante, trouver les films qui sont sortis peu de temps avant ou après “Grand prix” de Frankenheimer, sorti le 21 décembre 1966.

{
  "_source": {
    "includes": [
      "*.title",
      "*.release_date",
      "*.year"
    ],
    "excludes": [
      "*.actors*",
      "*.genres"
    ]
  },
  "query": {
    "function_score": {
      "query": {
        "exists": {
          "field": "fields.release_date"
        }
      },
      "functions": [
        {
          "gauss": {
            "fields.release_date": {
              "origin": "1966-12-21T00:00:00Z",
              "scale": "30d",
              "offset": "1d",
              "decay": 0.5
            }
          }
        }
      ]
    }
  }
}

À vous de jouer

Proposer les requêtes DSL pour organiser les résultats de la façon souhaitée :

  1. les films de James Cameron en pondérant négativement ceux qui durent plus de deux heures

    Correction

    {
            "_source": {
                    "includes": [
                            "fields.title",
                            "fields*.rating",
                            "fields.running_time_secs"
                    ],
                    "excludes": [
                            "fields*.actors*",
                            "fields*.genres"
                    ]
            },
            "query": {
                    "function_score": {
                            "query": {
                                    "match_phrase": {
                                            "fields.directors": {
                                                    "query": "James Cameron"
                                            }
                                    }
                            },
                            "functions": [
                                    {
                                            "exp": {
                                                    "fields.running_time_secs": {
                                                            "origin": "7200",
                                                            "scale": "200",
                                                            "decay": 0.5
                                                    }
                                            }
                                    }
                            ]
                    }
            }
    }
    
  2. les meilleures comédies romantiques

    Correction

    {
      "size": 25,
      "_source": {
        "includes": [
          "fields.title",
          "fields*.genres"
        ],
        "excludes": [
          "fields*.actors*",
          "fields*.rating",
          "fields.running_time_secs"
        ]
      },
      "query": {
        "bool": {
          "must": [
            {
              "match": {
                "fields.genres": "Romance"
              }
            }
          ]
        }
      },
      "sort": [
        {
          "fields.rating": {
            "order": "desc"
          }
        }
      ]
    }
    
  3. les films réalisés par Clint Eastwood, en affichant d’abord ceux dans lesquels il joue

    Correction

    {
            "size": 21,
            "_source": {
                    "includes": [
                            "*.title",
                            "*.actors",
                            "*.directors"
                    ],
                    "excludes": [
                            "*.genres"
                    ]
            },
            "query": {
                    "bool": {
                            "must": {
                                    "match_phrase": {
                                            "fields.directors": {
                                                    "query": "Clint Eastwood"
                                            }
                                    }
                            },
                            "should": {
                                    "match_phrase": {
                                            "fields.actors": {
                                                    "query": "Clint Eastwood",
                                                    "boost": 4
                                            }
                                    }
                            }
                    }
            }
    }
    
  4. les films de Sergio Leone, en les ordonnant du plus récent au plus ancien (deux requêtes possibles, avec et sans boosting)

    Correction

    Sans boosting

    {
      "size": 25,
      "_source": {
        "includes": [
          "fields.title",
          "fields*.directors",
          "fields*.release_date"
        ],
        "excludes": [
          "fields*.actors*",
          "fields*.rating",
          "fields*.genres",
          "fields.running_time_secs"
        ]
      },
      "query": {
        "bool": {
          "must": [
            {
              "match_phrase": {
                "fields.directors": "Sergio Leone"
              }
            }
          ]
        }
      },
      "sort": [
        {
          "fields.release_date": {
            "order": "desc"
          }
        }
      ]
    }
    

    Avec boosting

    {
      "_source": {
        "includes": [
          "*.title",
          "*.release_date"
        ],
        "excludes": [
          "*.actors*"
        ]
      },
      "query": {
        "function_score": {
          "query": {
            "match_phrase": {
              "fields.directors": {
                "query": "Sergio Leone"
              }
            }
          },
          "functions": [
            {
              "field_value_factor": {
                "field": "fields.release_date"
              }
            }
          ]
        }
      }
    }
    
  5. les films du genre Western, en pondérant négativement ceux réalisés par Sergio Leone

    Correction

           {
           "size": 75,
           "_source": {
            "includes": [
                   "*.title"
            ],
            "excludes": [
                   "*.actors*"
            ]
           },
           "query": {
            "boosting": {
                   "positive": {
                    "query": {
                           "match_phrase": {
                            "fields.genres": {
                                   "query": "Western"
                            }
                           }
                    }
                   },
                   "negative": {
                    "match_phrase": {
                           "fields.directors": "Sergio Leone"
                    }
                   },
                   "negative_boost": 0.5
            }
           }
    }
    
  6. les films de sport autour de la boxe, et assez courts. Indice : la durée du film se trouve dans fields.running_time_secs.

    Correction

    {
      "_source": {
        "includes": [
          "*.title",
          "fields.plot",
          "*.genres",
          "fields.running_time_secs"
        ],
        "excludes": [
          "*.actors*"
        ]
      },
      "query": {
        "function_score": {
          "query": {
            "bool": {
              "must": [
                {
                  "exists": {
                    "field": "fields.running_time_secs"
                  }
                },
                {
                  "exists": {
                    "field": "fields.genres"
                  }
                },
                {
                  "match": {
                    "fields.genres": {
                      "query": "Sport"
                    }
                  }
                },
                {
                  "exists": {
                    "field": "fields.plot"
                  }
                }
              ],
              "should": [
                {
                  "wildcard": {
                    "fields.plot": "box*"
                  }
                },
                {
                  "range": {
                    "fields.running_time_secs": {
                      "lte": 7200
                    }
                  }
                }
              ]
            }
          }
        }
      }
    }
    
  7. les films qui sont sortis dans les 15 jours avant ou après Lost in Translation de Sofia Coppola

    Correction

    {
            "size": 25,
            "_source": {
                    "includes": [
                            "fields.title",
                            "fields.rating",
                            "fields.release_date"
                    ],
                    "excludes": [
                            "fields*.actors*",
                            "fields*.genres"
                    ]
            },
            "query": {
                    "function_score": {
                            "query": {
                                    "exists": {
                                            "field": "fields.release_date"
                                    }
                            },
                            "functions": [
                                    {
                                            "gauss": {
                                                    "fields.release_date": {
                                                            "origin": "2003-08-29",
                                                            "scale": "15d",
                                                            "decay": 0.5
                                                    }
                                            }
                                    }
                            ]
                    }
            }
    }