<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0">
  <channel>
    <title><![CDATA[Python - Elasticsearch Labs]]></title>
    <description><![CDATA[Articles and tutorials from the Search team at Elastic]]></description>
    <copyright><![CDATA[© 2026. Elasticsearch B.V. All Rights Reserved]]></copyright>
    <image>
      <title><![CDATA[Python - Elasticsearch Labs]]></title>
      <url>https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt1121c0bf0e8a6e65/6a88da6340a1841030ef456f/search-labs-thumbnail.png</url>
      <link>https://www.elastic.co/fr/search-labs/blog/category/python-programming</link>
    </image>
    <link>https://www.elastic.co/fr/search-labs/blog/category/python-programming</link>
    <atom:link href="https://www.elastic.co/fr/search-labs/rss/category/python-programming.xml" rel="self" type="application/rss+xml"/>
    <language><![CDATA[fr]]></language>
    <lastBuildDate>Sun, 11 Oct 2026 18:45:50 GMT</lastBuildDate>
  <item>
    <title><![CDATA[Recherche multimodale de sommets avec Elasticsearch et SigLIP-2 ]]></title>
    <description><![CDATA[Apprenez à mettre en œuvre la recherche multimodale texte-image et image-image en utilisant les encastrements SigLIP-2 et la recherche vectorielle Elasticsearch kNN. Objectif du projet : trouver des photos du sommet du mont Ama Dablam prises lors d'un trekking dans l'Everest.]]></description>
    <content:encoded><![CDATA[<p>Avez-vous déjà voulu rechercher votre album photo par signification ? Essayez des requêtes telles que "montrez-moi mes photos où je porte une veste bleue et suis assis sur un banc", "montrez-moi des photos du mont Everest" ou "saké et sushi". Prenez une tasse de café (ou votre boisson préférée) et poursuivez votre lecture. Dans ce blog, nous vous montrons comment créer une application de recherche hybride multimodale. Multimodale signifie que l'application peut comprendre et rechercher différents types d'entrées - texte, images et audio - et pas seulement des mots. Hybride signifie qu'il combine des techniques telles que la correspondance de mots-clés, la recherche vectorielle kNN et la géolocalisation pour fournir des résultats plus précis.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltdfa1ec1ccd450e94/6a17da751d1b8308ee93e344/0ec6bbb45013846b59ee00d2bf73ee2182ee7392-1920x1080.gif" alt="Bibliothèque de différentes photos de sommets de montagne de la randonnée du Mont Everest." /><p>Pour ce faire, nous utilisons le logiciel SigLIP-2 de Google pour générer des encastrements vectoriels pour les images et le texte, et les stocker dans la base de données vectorielles Elasticsearch. Au moment de la requête, nous convertissons l'entrée de la recherche, texte ou image, en encastrements et exécutons des recherches vectorielles kNN rapides pour extraire les résultats. Cette configuration permet une recherche efficace de texte à image et d'image à image. Une interface utilisateur Streamlit donne vie à ce projet en nous fournissant un frontend qui nous permet non seulement d'effectuer une recherche textuelle pour trouver et afficher les photos correspondantes de l'album, mais aussi d'identifier le sommet de la montagne à partir de l'image téléchargée et d'afficher d'autres photos de cette montagne dans l'album photo.
Nous présentons également les mesures que nous avons prises pour améliorer la précision des recherches, ainsi que des conseils et astuces pratiques. Pour une exploration plus approfondie, nous fournissons un <a href="https://github.com/navneet83/multimodal-mountain-peak-search">dépôt GitHub</a> et un <a href="https://github.com/navneet83/multimodal-mountain-peak-search/blob/main/notebooks/multimodal_mountain_peak_search.ipynb">carnet de notes Colab</a>.</p><h2>Comment cela a commencé</h2><p>Ce billet a été inspiré par un enfant de 10 ans qui m'a demandé de lui montrer toutes les photos du mont Ama Dablam prises lors de mon trek au camp de base de l'Everest. En parcourant l'album photo, on m'a également demandé d'identifier plusieurs autres pics montagneux, dont certains que je n'arrivais pas à nommer.</p><p>Cela m'a donné l'idée d'un projet amusant de vision par ordinateur. Ce que nous voulions réaliser :</p><ul><li><p>trouver des images d'un sommet de montagne par son nom</p></li><li><p>deviner le nom du sommet d'une montagne à partir d'une image et trouver des sommets similaires dans l'album photo</p></li><li><p>faire fonctionner les requêtes conceptuelles<em>(personne</em>, <em>rivière</em>, <em>drapeaux de prière</em>, <em>etc.)</em></p></li></ul><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltf82df9d7005fc3fe/6a17da78abe0f2e77bdfe8b9/e9d0d720a9b565d5b749bdc915068852d4f157ad-1200x1600.png" alt="Mont Ama Dablam " /><h2>Assembler l'équipe de rêve : SigLIP-2, Elasticsearch &amp; Streamlit</h2><p>Il est rapidement apparu que pour que cela fonctionne, nous devions transformer à la fois le texte ("Ama Dablam") et les images (photos de mon album) en vecteurs pouvant être comparés de manière significative, c'est-à-dire dans le même espace vectoriel. Une fois cette étape franchie, la recherche se résume à "trouver les voisins les plus proches".</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt5f80b69a9d5bd28a/6a17da7a4b055ddd1243209e/20e6f8b7d4fa48414f407ec200adbe00ee28d517-1536x1024.png" alt="SigLIP-2, Elasticsearch &amp; Streamlit- dream team." /><p>Pour générer des enregistrements d'images, nous utilisons un<a href="https://huggingface.co/blog/vlms-2025"> encodeur multilingue</a> vision-langage, de sorte qu'une photo d'une montagne et une phrase comme "Ama Dablam" se retrouvent dans le même espace vectoriel.</p><p><a href="https://huggingface.co/blog/siglip2"><strong>SigLIP-2</strong></a>, récemment publié par Google, s'inscrit parfaitement dans ce cadre. Il peut générer des enchâssements sans formation spécifique à une tâche (un réglage <strong>zéro)</strong> et fonctionne bien pour notre cas d'utilisation : des photos non étiquetées et des pics avec des noms et des langues différents. Parce qu'il est formé à la correspondance texte ↔ image, une photo de montagne prise lors d'une randonnée et un court texte d'incitation se retrouvent proches en tant qu'ancrages, même lorsque la langue ou l'orthographe de la requête varient.</p><p>SigLIP-2 offre un excellent rapport qualité-vitesse, prend en charge plusieurs résolutions d'entrée et fonctionne à la fois avec le CPU et le GPU. SigLIP-2 est conçu pour être plus résistant aux photos prises en extérieur que les modèles précédents tels que le CLIP original. Lors de nos tests, SigLIP-2 a toujours produit des résultats fiables. Il est également très bien supporté, ce qui en fait un choix évident pour ce projet.</p><p>Ensuite, nous avons besoin d'une base de données vectorielle pour stocker les encastrements et effectuer des recherches puissantes. Il devrait permettre non seulement la recherche par cosinus kNN sur des images intégrées, mais aussi l'application de filtres de géographie et de texte en une seule requête. Elasticsearch convient bien ici : il gère très bien les vecteurs (HNSW kNN sur les champs dense_vector), prend en charge la recherche hybride qui combine le texte, les vecteurs et les requêtes géographiques, et propose d'emblée le filtrage et le tri. Il est également évolutif horizontalement, ce qui permet de passer facilement d'une poignée de photos à des milliers. Le client officiel <a href="https://www.elastic.co/docs/reference/elasticsearch/clients/python">Elasticsearch Python</a> simplifie la plomberie et s'intègre parfaitement au projet. Enfin, nous avons besoin d'un frontal léger où nous pouvons saisir des requêtes de recherche et afficher les résultats. Pour une démonstration rapide, basée sur Python, Streamlit est une solution idéale. Il fournit les primitives dont nous avons besoin - le téléchargement de fichiers, une grille d'images réactive et des menus déroulants pour le tri et la géolocalisation. Il est facile de le cloner et de l'exécuter localement, et il fonctionne également dans un cahier Colab.</p><h2>Implémentation</h2><h3>Conception et stratégie d'indexation Elasticsearch</h3><p>Nous utiliserons deux indices pour ce projet : <code>peaks_catalog</code> et <code>photos</code>.</p><h4>Index du catalogue des pics</h4><p>Cet index constitue un catalogue compact des principaux sommets visibles pendant le trek du camp de base de l'Everest. Chaque document de cet index correspond à un sommet de montagne, comme le mont Everest. Pour chaque document relatif à un pic montagneux, nous stockons les noms/alias, les coordonnées facultatives de latitude et de longitude, ainsi qu'un vecteur prototype unique construit en mélangeant les messages-guides SigLIP-2 (+ images de référence facultatives).</p><p><strong>Mappage de l'index :</strong></p><p>Champ d'application</p><p>Type</p><p>Exemple</p><p>Objectif/Notes</p><p>Vecteur/Indexation</p><p>id</p><p>mot-clé</p><p>ama-dablam</p><p>Slug/id stable</p><p>-</p><p>noms</p><p>texte + sous-champ mot-clé</p><p>["Ama Dablam","Amadablam"]</p><p>Alias / noms multilingues ; names.raw pour les filtres exacts</p><p>-</p><p>latlon</p><p>geo_point</p><p>{"lat":27.8617,"lon":86.8614}</p><p>Coordonnées GPS du pic sous la forme d'une combinaison latitude/longitude (facultatif)</p><p>-</p><p>elev_m</p><p>entier</p><p>6812</p><p>Élévation (facultatif)</p><p>-</p><p>texte_embed</p><p>dense_vector</p><p>768</p><p>Prototype mixte (invites et éventuellement 1 à 3 images de référence) pour ce pic</p><p>index:true, similarité :"cosine", index_options :{type:"hnsw", m:16, ef_construction:128}</p><p>Cet index est principalement utilisé pour des recherches d'image à image, telles que l'identification de sommets de montagne à partir d'images. Nous utilisons également cet index pour améliorer les résultats de recherche texte-image.</p><p>En résumé, le site <code>peaks_catalog</code> transforme la question "Quelle est cette montagne ?" en un problème ciblé de plus proche voisin, séparant efficacement la compréhension conceptuelle des complexités des données d'image.</p><p><strong>Stratégie d'indexation pour l'index peaks_catalog : </strong>Nous commençons par créer une liste des sommets les plus importants visibles lors de la randonnée EBC. Pour chaque pic, nous stockons sa position géographique, son nom, ses synonymes et son altitude dans un <a href="https://github.com/navneet83/multimodal-mountain-peak-search/blob/main/data/peaks.yaml">fichier yaml</a>. L'étape suivante consiste à <a href="https://github.com/navneet83/multimodal-mountain-peak-search/blob/main/scripts/embed_and_index_photos.py#L351">générer l'intégration</a> pour chaque pic et à la stocker dans le champ <code>text_embed</code>. Afin de générer des encastrements robustes, nous utilisons la technique suivante :</p><ul><li><p>Créer un prototype de texte en utilisant :</p><ul><li><p>noms des sommets</p></li><li><p>l'<a href="https://github.com/navneet83/multimodal-mountain-peak-search/blob/main/scripts/embed_and_index_photos.py#L301">ensemble des invites</a> (utilisation de plusieurs invites différentes pour tenter de répondre à la même question), par exemple :</p><ul><li><p>"photo naturelle du sommet de la montagne {name} dans l'Himalaya, Népal".</p></li><li><p>"{name} sommet emblématique de la région du Khumbu, paysage alpin"</p></li><li><p>"{name} sommet montagneux, neige, ligne de crête rocheuse"</p></li></ul></li><li><p><a href="https://github.com/navneet83/multimodal-mountain-peak-search/blob/main/scripts/embed_and_index_photos.py#L333">anti-concept</a> optionnel (indiquant à SigLIP-2 ce qu'il ne faut pas faire) : soustraire un petit vecteur pour "peinture, illustration, affiche, carte, logo" afin de privilégier les photos réelles.</p></li></ul></li><li><p><a href="https://github.com/navneet83/multimodal-mountain-peak-search/blob/main/scripts/embed_and_index_photos.py#L388C13-L388C29">Créer éventuellement un prototype d'image</a> si des images de référence du pic sont fournies.</p></li></ul><p>Nous <a href="https://github.com/navneet83/multimodal-mountain-peak-search/blob/main/scripts/embed_and_index_photos.py#L392">fusionnons ensuite les prototypes de texte et d'image</a> pour générer l'intégration finale. Enfin, le document est <a href="https://github.com/navneet83/multimodal-mountain-peak-search/blob/main/scripts/embed_and_index_photos.py#L396">indexé</a> avec tous les champs obligatoires :</p>def l2norm(v: np.ndarray) -&gt; np.ndarray:
    return v / (np.linalg.norm(v) + 1e-12)
def compute_blended_peak_vec(
        emb: Siglip2,
        names: List[str],
        peak_id: str,
        peaks_images_root: str,
        alpha_text: float = 0.5,
        max_images: int = 3,
) -&gt; Tuple[np.ndarray, int, int, List[str]]:
    """
    Build blended vector for a single peak.

    Returns:
      vec           : np.ndarray (L2-normalized)
      found_count   : number of reference images discovered
      used_count    : number of references used (&lt;= max_images)
      used_filenames: list of filenames used (for logging)
    """
    # 1) TEXT vector
    tv = embed_text_blend(emb, names)

    # 2) IMAGE refs: prefer folder by id; fallback to slug of the primary name
    root = Path(peaks_images_root)
    candidates = [root / peak_id]
    if names:
        candidates.append(root / slugify(names[0]))

    all_refs: List[Path] = []
    for c in candidates:
        if c.exists() and c.is_dir():
            all_refs = list_ref_images(c)
            if all_refs:
                break

    found = len(all_refs)
    used_list = all_refs[:max_images] if (max_images and found &gt; max_images) else all_refs
    used = len(used_list)

    img_v = embed_image_mean(emb, used_list) if used_list else None

    # 3) Blend TEXT and IMAGE vectors, clamp alpha to [0,1]
    a = max(0.0, min(1.0, float(alpha_text)))
    vec = l2norm(tv if img_v is None else (a * tv + (1.0 - a) * img_v)).astype("float32")
    return vec, found, used, [p.name for p in used_list]<p>Exemple de document provenant de l'index <code>peaks_catalog</code>:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt1219f5d0e39b512c/6a17da7c57726263161bcace/bc05fbd0c4f8d721d5170c28a3884a9eda80bb7d-1210x1132.png" alt="Un exemple de document provenant de l'index peaks_catalog dans Elasticsearch." /><h4>Index des photos</h4><p>Cet index primaire contient des informations détaillées sur toutes les photos de l'album. Chaque document représente une seule photo, contenant les informations suivantes :</p><ul><li><p>Chemin d'accès relatif à la photo dans l'album photo. Cette option permet de visualiser l'image correspondante ou de charger l'image dans l'interface de recherche.</p></li><li><p>Informations sur le GPS et l'heure de la photo.</p></li><li><p>Vecteur dense pour le codage d'images généré par SigLIP-2.</p></li><li><p><code>predicted_peaks</code> qui nous permet de filtrer par nom de pic.

<strong>Cartographie de l'index</strong></p></li></ul><p>Champ d'application</p><p>Type</p><p>Exemple</p><p>Objectif/Notes</p><p>Vecteur / Indexation</p><p>chemin</p><p>mot-clé</p><p>data/images/IMG_1234.HEIC</p><p>Comment l'interface utilisateur ouvre la vignette/l'image complète</p><p>-</p><p>clip_image</p><p>dense_vector</p><p>768</p><p>Intégration d'images SigLIP-2</p><p>index:true, similarité :"cosine", index_options :{type:"hnsw", m:16, ef_construction:128}</p><p>pics_prédits</p><p>mot-clé</p><p>["ama-dablam","pumori"]</p><p>Les suppositions Top-K au moment de l'indexation (filtre / facette UX bon marché)</p><p>-</p><p>GPS</p><p>geo_point</p><p>{"lat":27.96,"lon":86.83}</p><p>permet d'utiliser des filtres géographiques</p><p>-</p><p>heure de la prise de vue</p><p>date</p><p>2023-10-18T09:41:00Z</p><p>temps de capture : tri/filtre</p><p>-</p><p><strong>Stratégie d'indexation pour l'index des photos : </strong>Pour chaque photo de l'album, nous procédons comme suit :
Extraire les informations sur les images <code>shot_time</code> et <code>gps</code> <a href="https://github.com/navneet83/multimodal-mountain-peak-search/blob/main/scripts/embed_and_index_photos.py#L526">à partir des métadonnées de l'image</a>.</p><ul><li><p><a href="https://github.com/navneet83/multimodal-mountain-peak-search/blob/main/scripts/embed_and_index_photos.py#L511">Intégration d'images SigLIP-2</a>: passage de l'image dans le modèle et normalisation L2 du vecteur. Stocker l'intégration dans le champ <code>clip_image</code>.</p></li><li><p><a href="https://github.com/navneet83/multimodal-mountain-peak-search/blob/main/scripts/embed_and_index_photos.py#L519">Prédire les pics</a> et les stocker dans le champ <code>predicted_peaks</code>. Pour ce faire, nous prenons d'abord le vecteur image de la photo généré à l'étape précédente, puis nous effectuons une recherche rapide par kNN sur le champ text_embed de l'index <code>peaks_catalog</code>. Nous conservons les 3-4 premiers sommets et ignorons les autres.</p></li><li><p>Nous calculons le champ <code>_id</code> en effectuant un <a href="https://github.com/navneet83/multimodal-mountain-peak-search/blob/main/scripts/embed_and_index_photos.py#L509">hachage</a> du nom de l'image et du chemin d'accès. Cela permet de s'assurer qu'il n'y a pas de doublons après plusieurs exécutions.</p></li></ul><p>Une fois que nous avons déterminé tous les champs de la photo, les documents photo sont indexés par lots à l'aide de l'indexation <a href="https://github.com/navneet83/multimodal-mountain-peak-search/blob/main/scripts/embed_and_index_photos.py#L530">en bloc :</a></p>def bulk_index_photos(
        es: Elasticsearch,
        images_root: str,
        photos_index: str = "photos",
        peaks_index: str = "peaks_catalog",
        topk_predicted: int = 5,
        batch_size: int = 200,
        refresh: str = "false",
) -&gt; None:
    """Walk a folder of images, embed + enrich, and bulk index to Elasticsearch."""
    root = Path(images_root)
    if not root.exists():
        raise SystemExit(f"Images root not found: {images_root}")

    emb = Siglip2()
    batch: List[Dict[str, Any]] = []
    n_indexed = 0

    for p in iter_images(root):
        rel = relpath_within(root, p)
        _id = id_for_path(rel)

        # 1) Image embedding (and reuse it for predicted_peaks)
        try:
            with Image.open(p) as im:
                ivec = emb.image_vec(im.convert("RGB")).astype("float32")
        except (UnidentifiedImageError, OSError) as e:
            print(f"[skip] {rel} — cannot embed: {e}")
            continue

        # 2) Predict top-k peak names
        try:
            top_names = predict_peaks(es, ivec.tolist(), peaks_index=peaks_index, k=topk_predicted)
        except Exception as e:
            print(f"[warn] predict_peaks failed for {rel}: {e}")
            top_names = []

        # 3) EXIF enrichment (safe)
        gps = get_gps_decimal(str(p))
        shot = get_shot_time(str(p))

        # 4) Build doc and stage for bulk
        doc = {"path": rel, "clip_image": ivec.tolist(), "predicted_peaks": top_names}
        if gps:
            doc["gps"] = gps
        if shot:
            doc["shot_time"] = shot

        batch.append(
            {"_op_type": "index", "_index": photos_index, "_id": _id, "_source": doc}
        )

        # 5) Periodic flush
        if len(batch) &gt;= batch_size:
            helpers.bulk(es, batch, refresh=refresh)
            n_indexed += len(batch)
            print(f"[photos] indexed {n_indexed} (last: {rel})")
            batch.clear()

    # Final flush
    if batch:
        helpers.bulk(es, batch, refresh=refresh)
        n_indexed += len(batch)
        print(f"[photos] indexed {n_indexed} total.")

    print("[done] photos indexing")<p>Exemple de document de l'index des photos :</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt744b7e6326937cfc/6a17da7e6df731d3040a0da8/1dc1406ac2a97440b6804838795b3c2205c4c6b2-1080x1234.png" alt="Un exemple de document provenant de l'index des photos dans Elasticsearch." /><p>En résumé, l'index des photos est le magasin rapide, filtrable et prêt pour le kNN de toutes les photos de l'album. Sa cartographie est volontairement minimale - juste assez de structure pour permettre une recherche rapide, un affichage propre et une répartition des résultats dans l'espace et dans le temps. Cet index sert aux deux types de recherche. Le script Python permettant de créer les deux indices est disponible <a href="https://github.com/navneet83/multimodal-mountain-peak-search/blob/main/scripts/create_indices.py">ici.</a></p><p>La visualisation des cartes Kibana ci-dessous affiche les documents de l'album photo sous forme de points verts et les pics montagneux de l'index <code>peaks_catalog</code> sous forme de triangles rouges, les points verts correspondant bien au sentier de randonnée du camp de base de l'Everest.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltb5bf016e8d9c3e84/6a17da80be608681f10045e6/1c75d0ed0ce53d28a94bf2f47a354e25581d2baf-1600x1402.png" alt="Visualisation de cartes Kibana affichant les documents de l'album photo sous forme de points verts et les sommets de l'index peaks_catalog sous forme de triangles rouges, les points verts correspondant bien au sentier de trekking du camp de base de l'Everest." /><h2>Cas d'utilisation de la recherche</h2><p><strong>Recherche par nom (texte-image) :</strong> Cette fonction permet aux utilisateurs de localiser des photos de sommets de montagne (et même des concepts abstraits comme les "drapeaux de prière") à l'aide de requêtes textuelles. Pour ce faire, l'entrée texte est <a href="https://github.com/navneet83/multimodal-mountain-peak-search/blob/main/scripts/query_by_peak_name.py#L87C5-L87C20">convertie en un vecteur texte</a> à l'aide de SigLIP-2. Pour la génération de vecteurs de texte robustes, nous utilisons la même stratégie que pour la création d'enchâssements de texte dans l'index <code>peaks_catalog</code>: <a href="https://github.com/navneet83/multimodal-mountain-peak-search/blob/main/scripts/query_by_peak_name.py#L104">combinaison de l'</a> entrée texte avec un petit <a href="https://github.com/navneet83/multimodal-mountain-peak-search/blob/main/scripts/query_by_peak_name.py#L100">ensemble d'invites</a>, soustraction d'un<a href="https://github.com/navneet83/multimodal-mountain-peak-search/blob/main/scripts/query_by_peak_name.py#L103"> vecteur anti-concept</a> mineur et application de la <a href="https://github.com/navneet83/multimodal-mountain-peak-search/blob/main/scripts/query_by_peak_name.py#L104">normalisation L2</a> pour produire le vecteur d'interrogation final. Une <a href="https://github.com/navneet83/multimodal-mountain-peak-search/blob/main/scripts/query_by_peak_name.py#L140">requête</a> kNN est ensuite exécutée sur le champ <code>photos.clip_image</code> pour récupérer les pics les plus proches, sur la base de la similarité cosinusoïdale pour trouver les images les plus proches. Il est possible de rendre les résultats de la recherche plus pertinents en appliquant des <a href="https://github.com/navneet83/multimodal-mountain-peak-search/blob/main/scripts/query_by_peak_name.py#L152">filtres</a> géographiques et de date, et/ou un filtre de terme <code>photos.predicted_peaks</code> dans le cadre de la requête (voir les exemples de requêtes ci-dessous). Cela permet d'exclure les sommets qui ressemblent à d'autres et qui ne sont pas visibles lors de la randonnée.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt9bb9abf5ce64fcbb/6a17da81e8fbce20db3a17da/b5fac28ffdbedb820505365ca07df125cd01b939-946x370.png" alt="Comment fonctionne la recherche multimodale par nom (texte-image) dans Elasticsearch." /><p><strong>Requête Elasticsearch avec filtre géographique :</strong></p>POST photos/_search
{
  "knn": {
    "field": "clip_image",
    "query_vector": [ ... ],
    "k": 60,
    "num_candidates": 2000
  },
  "query": {
    "bool": {
      "filter": [
        { "geo_bounding_box": { "gps": { "top_left": "...", "bottom_right": "..." } } }
      ]
    }
  },
  "_source": ["path","predicted_peaks","gps","shot_time"]
}

Response (first two documents):
{
 "hits": {
   "total": {
     "value": 56,
     "relation": "eq"
   },
   "max_score": 0.5779596,
   "hits": [
     {
       "_index": "photos",
       "_id": "d01da3a1141981486c3493f6053c79e92a788463",
       "_score": 0.5779596,
       "_source": {
         "path": "IMG_2738.HEIC",
         "predicted_peaks": [
           "Pumori",
           "Kyajo Ri",
           "Khumbila",
           "Nangkartshang",
           "Kongde Ri"
         ],
         "gps": {
           "lat": 27.97116388888889,
           "lon": 86.82331111111111
         },
         "shot_time": "2023-11-03T08:07:13"
       }
     },
     {
       "_index": "photos",
       "_id": "c79d251f07adc5efaedc53561110a7fd78e23914",
       "_score": 0.5766071,
       "_source": {
         "path": "IMG_2761.HEIC",
         "predicted_peaks": [
           "Kyajo Ri",
           "Makalu",
           "Baruntse",
           "Cho Oyu",
           "Khumbila"
         ],
         "gps": {
           "lat": 27.975558333333332,
           "lon": 86.82515
         },
         "shot_time": "2023-11-03T08:51:08"
       }
     }
}<p><strong>Recherche par image (image à image) :</strong> Cette fonction permet d'identifier une montagne sur une photo et de trouver d'autres images de cette même montagne dans l'album photo. Lorsqu'une image est téléchargée, elle est traitée par l'encodeur d'images SigLIP-2 pour générer un <a href="https://github.com/navneet83/multimodal-mountain-peak-search/blob/main/scripts/identify_from_picture_find_similar_peaks.py#L228">vecteur d'image</a>. Une <a href="https://github.com/navneet83/multimodal-mountain-peak-search/blob/main/scripts/identify_from_picture_find_similar_peaks.py#L234">recherche kNN</a> est ensuite effectuée sur le champ <code>peaks_catalog.text_embed</code> pour identifier les noms de pics qui correspondent le mieux. Ensuite, un <a href="https://github.com/navneet83/multimodal-mountain-peak-search/blob/main/scripts/identify_from_picture_find_similar_peaks.py#L257">vecteur de texte est généré</a> à partir des noms de pics correspondants, et une autre <a href="https://github.com/navneet83/multimodal-mountain-peak-search/blob/main/scripts/identify_from_picture_find_similar_peaks.py#L263">recherche kNN</a> est effectuée sur l'index des photos pour localiser les images correspondantes.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltab9d16333e2a9e69/6a17da827f6f155448c099cc/3a3d5635bee7a222b95529dd7f9fbee016381610-1226x550.png" alt="Comment fonctionne la recherche multimodale par image (image à image) dans Elasticsearch." /><p><strong>Requête Elasticsearch :</strong></p><p>Étape 1 : Trouver les noms de pics correspondants</p>GET peaks_catalog/_search
{
 "knn": {
   "field": "text_embed",
   "query_vector": [...image-vector... ],
   "k": 3,
   "num_candidates": 500
 },
 "_source": [
   "id",
   "names",
   "latlon",
   "text_embed"
 ]
}


Response (first two documents):
{
 "took": 2,
 "timed_out": false,
 "_shards": {
   "total": 1,
   "successful": 1,
   "skipped": 0,
   "failed": 0
 },
 "hits": {
   "total": {
     "value": 3,
     "relation": "eq"
   },
   "max_score": 0.58039916,
   "hits": [
     {
       "_index": "peaks_catalog",
       "_id": "pumori",
       "_score": 0.58039916,
       "_source": {
         "id": "pumori",
         "names": [
           "Pumori",
           "Pumo Ri"
         ],
         "latlon": {
           "lat": 28.01472,
           "lon": 86.82806
         },
         "text_embed": [
                  ... embeddings...
         ]
       }
     },
     {
       "_index": "peaks_catalog",
       "_id": "kyajo-ri",
       "_score": 0.57942784,
       "_source": {
         "id": "kyajo-ri",
         "names": [
           "Kyajo Ri",
           "Kyazo Ri"
         ],
         "latlon": {
           "lat": 27.909167,
           "lon": 86.673611
         },
         "text_embed": [
           ... embeddings...
         ]
       }
     }
   ]
 }
}<p>Étape 2 : Effectuer une recherche dans l'index <code>photos</code> pour trouver les images correspondantes (même requête que dans le cas d'utilisation de la recherche texte-image) :</p>POST photos/_search
{
 "knn": {
   "field": "clip_image",
   "query_vector": [ ...image-vector... ],
   "k": 30,
   "num_candidates": 2000
 },
 "_source": [
   "path",
   "gps",
   "shot_time",
   "predicted_peaks",
   "clip_image"
 ],
 "query": {
   "bool": {
     "filter": [
       {
         "term": {
           "predicted_peaks": "Pumori"
         }
       }
     ]
   }
 }
}


Response (first two documents):
{
 "hits": {
   "total": {
     "value": 56,
     "relation": "eq"
   },
   "max_score": 0.5779596,
   "hits": [
     {
       "_index": "photos",
       "_id": "d01da3a1141981486c3493f6053c79e92a788463",
       "_score": 0.5779596,
       "_source": {
         "path": "IMG_2738.HEIC",
         "predicted_peaks": [
           "Pumori",
           "Kyajo Ri",
           "Khumbila",
           "Nangkartshang",
           "Kongde Ri"
         ],
         "gps": {
           "lat": 27.97116388888889,
           "lon": 86.82331111111111
         },
         "shot_time": "2023-11-03T08:07:13"
       }
     },
     {
       "_index": "photos",
       "_id": "c79d251f07adc5efaedc53561110a7fd78e23914",
       "_score": 0.5766071,
       "_source": {
         "path": "IMG_2761.HEIC",
         "predicted_peaks": [
           "Kyajo Ri",
           "Makalu",
           "Baruntse",
           "Cho Oyu",
           "Khumbila"
         ],
         "gps": {
           "lat": 27.975558333333332,
           "lon": 86.82515
         },
         "shot_time": "2023-11-03T08:51:08"
       }
     }
}<h2>Streamlit UI</h2><p>Pour réunir le tout, nous avons créé une interface utilisateur Streamlit simple qui nous permet de réaliser les deux cas d'utilisation de la recherche. La barre de gauche affiche une liste déroulante de pics (agrégés à partir de <code>photos.predicted_peaks</code>) avec des cases à cocher et un filtre mini-carte/géo. En haut, il y a un champ de <strong>recherche par nom</strong> et un bouton d'<strong>identification à partir du</strong> téléchargement d'une photo. Le volet central présente une grille de vignettes réactive indiquant les scores kNN, les badges de pic prédit et les heures de capture. Chaque image comporte un bouton " <strong>Voir l'image"</strong> qui permet d'obtenir un aperçu en pleine résolution.</p><p><strong>Recherchez en téléchargeant une image :</strong> Nous prédisons le pic et trouvons les pics correspondants dans l'album photo.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltd1fb2b0304a310d2/6a17da8425daab7cda08a0fa/dca540cbf5279e6d6102c5a0c0351ddd4ac91cda-1600x1112.png" alt="Une interface utilisateur simple et claire qui permet d'effectuer une recherche multimodale texte-image et image-image pour les pics du mont Ama Dablam." /><p><strong>Recherche par texte</strong>: Trouver les sommets correspondants dans l'album à partir d'un texte.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt496c1ae8f7886320/6a17da86abe0f2da48dfe8bd/b1e8618db746cd49ea4962d3dc73031387b975dd-1600x1166.png" alt="Comment rechercher un sommet du mont Everest avec la recherche par texte dans la bibliothèque des sommets de montagne." /><h2>Conclusion</h2><p>Tout a commencé par <em>la possibilité de voir les </em><em> photos de</em><em><strong>l'Ama Dablam.</strong></em> s'est transformé en un petit système de <strong>recherche multimodale</strong> fonctionnel. Nous avons pris des photos brutes de trek, les avons transformées en <strong>encastrements SigLIP-2</strong> et avons utilisé <strong>Elasticsearch</strong> pour effectuer un <strong>kNN</strong> rapide sur les vecteurs, ainsi que des filtres géo/temporels simples pour faire remonter à la surface les bonnes images en fonction de leur <em>signification</em>. En cours de route, nous avons séparé les préoccupations en deux indices : un minuscule <code>peaks_catalog</code> de prototypes mélangés (pour l'identification) et un index évolutif <code>photos</code> de vecteurs d'images et d'EXIF (pour la recherche). Il est pratique, reproductible et facile à étendre.</p><p>Si vous souhaitez l'accorder, vous pouvez jouer avec quelques paramètres :</p><ul><li><p><strong>Paramètres de temps de recherche :</strong> <code>k</code> (nombre de voisins à récupérer) et <code>num_candidates</code> (étendue de la recherche avant la notation finale). Ces paramètres sont abordés dans le blog <a href="https://www.elastic.co/search-labs/blog/elasticsearch-knn-and-num-candidates-strategies">ici.</a></p></li><li><p><strong>Paramètres de temps d'indexation :</strong> <code>m</code> (connectivité du graphique) et <code>ef_construction</code> (précision du temps de construction par rapport à la mémoire). Pour les requêtes, expérimentez avec <code>ef_search</code> également - une valeur plus élevée signifie généralement un meilleur rappel avec un certain compromis en termes de latence. Consultez <a href="https://www.elastic.co/search-labs/blog/hnsw-graph">ce blog</a> pour plus de détails sur ces paramètres.</p></li></ul><p>A l'avenir, des modèles natifs/rerankers pour la recherche <strong>multimodale</strong> et <strong>multilingue</strong> seront bientôt intégrés à l'écosystème Elastic, ce qui devrait rendre la recherche d'images/de textes et le classement hybride encore plus performants.<a href="https://ir.elastic.co/news/news-details/2025/Elastic-Completes-Acquisition-of-Jina-AI-a-Leader-in-Frontier-Models-for-Multimodal-and-Multilingual-Search/default.aspx?utm_source=chatgpt.com"> ir.elastic.co+1</a></p><p>Si vous souhaitez essayer vous-même :</p><ul><li><p><strong>GitHub repo</strong> <a href="https://github.com/navneet83/multimodal-mountain-peak-search"><em>: https://github.com/navneet83/multimodal-mountain-peak-search</em></a></p></li><li><p><strong>Colab quickstart</strong> <a href="https://github.com/navneet83/multimodal-mountain-peak-search/blob/main/notebooks/multimodal_mountain_peak_search.ipynb">: https://github.com/navneet83/multimodal-mountain-peak-search/blob/main/notebooks/multimodal_mountain_peak_search.ipynb</a></p></li></ul><p>Notre voyage s'achève donc et il est temps de prendre l'avion du retour. J'espère que cela vous a été utile et si vous le cassez (ou l'améliorez), j'aimerais savoir ce que vous avez changé.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltdce2fff1569d2a8b/6a17da894b055dd1f24320a2/d324d1e1472f1bfbd8f25747f57bdeeb9c7f16b2-1600x1200.png" alt="" />]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/multimodal-search-siglip-2-elasticsearch</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/multimodal-search-siglip-2-elasticsearch</guid>
    <category><![CDATA[Base vectorielle]]></category>
    <category><![CDATA[Recherche hybride]]></category>
    <category><![CDATA[IA]]></category>
    <category><![CDATA[Python]]></category>
    <dc:creator><![CDATA[Navneet Kumar]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltccb66279debb05f9/6a17da8b63baffe228741b15/ffcf93358a7c5dadcea82faf3de460bf060d003c-1600x1200.png" length="0" type="image/png"/>
    <pubDate>Tue, 04 Nov 2025 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Formation de modèles LTR dans Elasticsearch avec des listes de jugement basées sur les données de comportement des utilisateurs]]></title>
    <description><![CDATA[Apprenez à utiliser les données UBI pour créer des listes de jugement afin d'automatiser la formation de vos modèles LTR (Learning to Rank) dans Elasticsearch.]]></description>
    <content:encoded><![CDATA[<p>L'une des principales difficultés rencontrées lors de l'utilisation de modèles d'<a href="https://www.elastic.co/docs/solutions/search/ranking/learning-to-rank-ltr"><em><strong>apprentissage par classement</strong></em></a> est la création d'une <a href="https://www.elastic.co/search-labs/blog/judgment-lists"><em><strong>liste de jugements de</strong></em></a> haute qualité pour entraîner le modèle. Traditionnellement, ce processus implique une évaluation <em><strong>manuelle</strong></em> de la pertinence des requêtes et des documents afin d'attribuer une note à chacun d'entre eux. Il s'agit d'un processus lent, peu évolutif et difficile à maintenir (imaginez que vous deviez mettre à jour à la main une liste comportant des centaines d'entrées).</p><p>Et si nous pouvions utiliser les interactions réelles des utilisateurs avec notre application de recherche pour créer ces données d'entraînement ? L'utilisation des données <a href="https://www.elastic.co/search-labs/blog/elasticsearch-plugin-user-behavior-insights"><em><strong>UBI</strong></em></a> nous permet justement de le faire. Créer un système automatique capable de capturer et d'utiliser nos recherches, nos clics et autres interactions pour générer une liste de jugement. Ce processus peut être mis à l'échelle et répété beaucoup plus facilement qu'une interaction manuelle et tendrait à donner de meilleurs résultats. Dans ce blog, nous allons explorer comment nous pouvons interroger les données UBI stockées dans Elasticsearch pour calculer des signaux significatifs afin de générer un ensemble de données d'entraînement pour un modèle <a href="https://www.elastic.co/search-labs/blog/elasticsearch-learning-to-rank-introduction"><em><strong>LTR</strong></em></a>.</p><p><em><strong>L'expérience complète est disponible </strong></em><a href="https://github.com/Alex1795/elastic-ltr-judgement_list-blog.git"><em><strong>ici</strong></em></a><em><strong>.</strong></em></p><h2>Pourquoi les données UBI peuvent-elles être utiles pour entraîner votre modèle LTR ?</h2><p>Les données UBI offrent plusieurs avantages par rapport à une annotation manuelle :</p><ul><li><p><strong>Le volume :</strong> Étant donné que les données UBI proviennent d'interactions réelles, nous pouvons collecter beaucoup plus de données que nous ne pouvons en générer manuellement. Cela suppose que nous ayons suffisamment de trafic pour générer ces données, bien entendu.</p></li><li><p><strong>L'intention de l'utilisateur réel :</strong> Traditionnellement, une liste de jugement manuel provient d'une évaluation experte des données disponibles. D'autre part, les données UBI reflètent le comportement réel des utilisateurs. Cela signifie que nous pouvons générer de meilleures données de formation qui amélioreront la précision de notre système de recherche, car elles sont basées sur la façon dont les utilisateurs interagissent réellement avec votre contenu et y trouvent de la valeur, plutôt que sur des hypothèses théoriques sur ce qui devrait être pertinent.</p></li><li><p><strong>Mises à jour permanentes :</strong> Les listes de jugement doivent être actualisées au fil du temps. Si nous les créons à partir des données de l'UBI, nous pouvons disposer de données actualisées qui se traduisent par des listes de jugement mises à jour.</p></li><li><p><strong>Rentabilité :</strong> Sans les frais généraux liés à la création manuelle d'une liste de jugement, le processus peut être répété efficacement un nombre illimité de fois.</p></li><li><p><strong>Distribution naturelle des requêtes</strong>: Les données UBI représentent les requêtes réelles des utilisateurs, ce qui peut entraîner des changements plus profonds. Par exemple, nos utilisateurs utilisent-ils le langage naturel pour effectuer des recherches dans notre système ? Si c'est le cas, nous pourrions vouloir mettre en œuvre une recherche sémantique ou une approche de recherche hybride.</p></li></ul><p>Elle s'accompagne toutefois de quelques mises en garde :</p><ul><li><p><strong>Amplification des biais : </strong>Un contenu populaire est plus susceptible de recevoir des clics, simplement parce qu'il est plus exposé. Cela pourrait donc avoir pour effet d'amplifier les articles les plus populaires et d'étouffer les meilleures options.</p></li><li><p><strong>Couverture incomplète : </strong>Le nouveau contenu n'a pas d'interactions, il peut donc être difficile pour lui de figurer en bonne place dans les résultats. Les requêtes rares peuvent également ne pas comporter suffisamment de points de données pour créer des données de formation significatives.</p></li><li><p><strong>Variations saisonnières :</strong> Si vous vous attendez à ce que le comportement de l'utilisateur change radicalement au fil du temps, les données historiques risquent de ne pas vous donner beaucoup d'indications sur ce qui constitue un bon résultat.</p></li><li><p><strong>Ambiguïté de la tâche :</strong> Un clic ne garantit pas toujours que l'utilisateur a trouvé ce qu'il cherchait.</p></li></ul><h2>Calcul des notes</h2><h3>Grades pour la formation LTR</h3><p>Pour former les modèles LTR, nous devons fournir une représentation numérique de la pertinence d'un document par rapport à une requête. Dans notre application, ce nombre est un score continu allant de 0,0 à 5,0+, les scores les plus élevés indiquant une plus grande pertinence.</p><p>Pour illustrer le fonctionnement de ce système de classement, prenons l'exemple suivant, créé manuellement :</p><p>Requête</p><p>Contenu du document</p><p>Grade</p><p>Explication</p><p>"meilleure recette de pizza"</p><p>"Recette de pâte à pizza italienne authentique avec photos étape par étape"</p><p>4.0</p><p>Très pertinent, exactement ce que l'utilisateur recherche</p><p>"meilleure recette de pizza"</p><p>"Histoire de la pizza en Italie"</p><p>1.0</p><p>Un peu dans le sujet, il s'agit de pizza mais ce n'est pas une recette.</p><p>"meilleure recette de pizza"</p><p>"Recette de pizza en 15 minutes pour les débutants"</p><p>3.0</p><p>C'est un bon résultat, mais il ne s'agit peut-être pas de la "meilleure" recette.</p><p>"meilleure recette de pizza"</p><p>"Guide d'entretien des voitures"</p><p>0.0</p><p>Pas du tout pertinent, sans aucun rapport avec la requête</p><p>Comme nous pouvons le voir ici, la note est une représentation numérique de la pertinence d'un document par rapport à notre exemple de requête "meilleure recette de pizza". Grâce à ces scores, notre modèle LTR peut apprendre quels documents devraient être présentés plus haut dans les résultats.</p><p>La manière de calculer les notes est au cœur de notre ensemble de données de formation. Il existe <a href="https://www.elastic.co/search-labs/blog/judgment-lists">plusieurs approches</a> pour y parvenir, chacune ayant ses propres forces et faiblesses. Par exemple, nous pourrions attribuer un score binaire de 1 pour pertinent, 0 pour non pertinent, ou nous pourrions simplement compter le nombre de clics dans un document résultant pour chaque requête.</p><p>Dans cet article de blog, nous utiliserons une approche différente, en <em><strong>prenant en compte le comportement de l'utilisateur en tant qu'entrée et en calculant un nombre de notes en tant que sortie.</strong></em> Nous corrigerons également le biais qui pourrait résulter du fait que les résultats les plus élevés ont tendance à être davantage cliqués, quelle que soit la pertinence du document.</p><h2>Calcul des notes - algorithme COEC</h2><p>L'algorithme COEC<a href="https://www.wsdm-conference.org/2010/proceedings/docs/p351.pdf">(Clicks over Expected Clicks</a>) est une méthodologie permettant de calculer les notes de jugement à partir des clics des utilisateurs.
Comme nous l'avons indiqué précédemment, les utilisateurs ont tendance à cliquer sur les résultats les mieux positionnés, même si le document n'est pas le plus pertinent par rapport à la requête ; c'est ce que l'on appelle le <a href="https://eugeneyan.com/writing/position-bias/">biais de position.</a> L'idée centrale de l'utilisation de l'algorithme COEC est que tous les clics n'ont pas la même importance ; un clic sur un document en position 10 indique que ce document est beaucoup plus pertinent pour la requête qu'un clic sur un document en position 1. Pour citer le document de recherche sur l'algorithme COEC (lien ci-dessus) :</p><p><em>"Il est bien connu que le taux de clics (CTR) des résultats de recherche ou des publicités diminue de manière significative en fonction de la position des résultats."</em></p><p>Pour en savoir plus sur les préjugés de position <a href="https://www.researchgate.net/publication/200110550_An_experimental_comparison_of_click_position-bias_models">, cliquez ici.</a></p><p>Pour résoudre ce problème avec l'algorithme COEC, nous suivons les étapes suivantes :</p><p><strong>1. Établir des valeurs de référence pour les positions :</strong> Nous calculons le taux de clics (CTR) pour chaque position de recherche de 1 à 10. Cela signifie que nous déterminons le pourcentage d'utilisateurs qui cliquent généralement sur la position 1, la position 2, etc. Cette étape permet de prendre en compte les préférences naturelles des utilisateurs en matière de position.

Nous calculons le CTR en utilisant :</p><p>Où ?</p><p>p = Position. De 1 à 10
Cp = Nombre total de clics (sur n'importe quel document) à la position p sur l'ensemble des requêtes
Ip = Nombre total d'impressions : Nombre de fois qu'un document est apparu à la position p sur l'ensemble des requêtes.</p><p>Dans ce cas, nous nous attendons à ce que les positions les plus élevées donnent lieu à davantage de clics.</p><p><strong>2.</strong> <strong>Calculer les clics attendus (EC)</strong>:</p><p>Cette mesure détermine le nombre de clics qu'un document "aurait dû" recevoir sur la base des positions dans lesquelles il est apparu et du CTR pour ces positions. Nous calculons la CE à l'aide de :</p><p>Où ?</p><p>Qd = Toutes les requêtes dans lesquelles le document d est apparu
pos(d,q)= Position du document d dans les résultats de la requête q</p><p>3. <strong>Compter les clics réels : </strong>Nous comptons le nombre total de clics qu'un document a reçus pour toutes les requêtes où il est apparu, ci-après dénommé <strong>A(d).</strong></p><p>4. <strong>Calculer le score COEC :</strong> Il s'agit du rapport entre les clics réels (A(d)) et les clics attendus (EC(d)) :</p><p>Cette métrique normalise le biais de position de la manière suivante :</p><ul><li><p>Un score de 1,0 signifie que le document s'est comporté exactement comme prévu compte tenu des positions dans lesquelles il est apparu.</p></li><li><p>Un score supérieur à 1,0 signifie que le document a obtenu de meilleurs résultats que prévu en examinant ses positions. Ce document est donc plus pertinent pour la requête.</p></li><li><p>Un score inférieur à 1,0 signifie que le document a obtenu de moins bons résultats que prévu, compte tenu de ses positions. Ce document est donc moins pertinent pour la requête.</p></li></ul><p><em><strong>Le résultat final est une note qui reflète ce que les utilisateurs recherchent, en tenant compte des attentes basées sur la position, extraites des interactions réelles avec notre système de recherche.</strong></em></p><h2>Mise en œuvre technique</h2><p>Nous allons créer un script pour créer une liste de jugements afin d'entraîner un modèle LTR.</p><p>L'entrée de ce script est constituée des données UBI indexées dans Elastic (requêtes et événements).</p><p>Le résultat est une liste de jugements dans un fichier CSV généré à partir de ces documents UBI en utilisant l'algorithme COEC. Cette liste de jugements peut être utilisée avec <a href="https://www.elastic.co/search-labs/blog/elasticsearch-learning-to-rank-introduction">Eland</a> pour extraire les caractéristiques pertinentes et former un modèle LTR.</p><h3>Démarrage rapide</h3><p>Pour générer une liste de jugement à partir de l'échantillon de données présenté dans ce blog, vous pouvez suivre les étapes suivantes :</p><p>1. Clonez le référentiel :</p>git clone https://github.com/Alex1795/elastic-ltr-judgement_list-blog.git  
cd elastic-ltr-judgement_list-blog<p>2. Installer les bibliothèques nécessaires</p><p>Pour ce script, nous avons besoin des bibliothèques suivantes :</p><ul><li><p><em>pandas</em>: pour sauvegarder la liste des jugements</p></li><li><p><em>elasticsearch</em>: Pour obtenir les données UBI à partir de notre déploiement Elastic</p></li></ul><p>Nous avons également besoin de Python 3.11</p>pip install -r requirements.txt<p>3. Mettez à jour les variables d'environnement pour votre déploiement Elastic dans un <a href="https://github.com/Alex1795/elastic-ltr-judgement_list-blog/blob/main/.env-example"> fichier .env</a></p><ul><li><p>ES_HOST</p></li><li><p>API_KEY</p></li></ul><p>Pour ajouter les variables d'environnement, utilisez :</p>source .env<p>4. Créez les index ubi_queries et ubi_events, et téléchargez les échantillons de données. Exécutez le fichier setup.py :</p>python setup.py<p>5. Exécutez le script Python :</p>python judgement_list-generator.py<p>Si vous suivez ces étapes, vous devriez voir apparaître un nouveau fichier appelé judgment_list.csv qui ressemble à ceci :</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt94317eda8f7af194/6a170aa46f7f04542f914821/2531090131ac9fe3e4e1d79de9d156fc47a7825a-782x531.png" alt="" /><p>Ce script calcule les notes en appliquant l'algorithme COEC évoqué précédemment à l'aide de la fonction <strong>calculate_relevance_grade()</strong> présentée ci-dessous.</p><h2>Architecture des données</h2><h3>Requêtes Ubi</h3><p>Notre index des requêtes UBI contient des informations sur les requêtes exécutées dans notre système de recherche. Il s'agit d'un document type :</p>{
          "client_id": "client_002",
          "query": "italian pasta recipes",
          "query_attributes": {
            "search_type": "recipe",
            "category": "food",
            "cuisine": "italian"
          },
          "query_id": "q002",
          "query_response_id": "qr002",
          "query_response_object_ids": [
            "doc_011",
            "doc_012",
            "doc_013",
            "doc_014",
            "doc_015",
            "doc_016",
            "doc_017",
            "doc_018",
            "doc_019",
            "doc_020"
          ],
          "timestamp": "2024-08-14T11:15:00Z",
          "user_query": "italian pasta recipes"
        }<p>Ici, nous pouvons voir les données de l'utilisateur (client_id), les résultats de la requête (query_response_object_ids) et la requête elle-même (timestamp, user_query).</p><h3>Événements de clics Ubi</h3><p>Notre index ubi_events contient des données à chaque fois qu'un utilisateur a cliqué sur un document dans les résultats. Il s'agit d'un document type :</p>{
          "action_name": "click",
          "application": "recipe_search",
          "client_id": "client_001",
          "event_attributes": {
            "object": {
              "description": "Authentic Italian Pizza Dough Recipe with Step-by-Step Photos",
              "device": "desktop",
              "object_id": "doc_001",
              "position": {
                "ordinal": 1,
                "page_depth": 1
              },
              "user": {
                "city": "New York",
                "country": "USA",
                "ip": "192.168.1.100",
                "location": {
                  "lat": 40.7128,
                  "lon": -74.006
                },
                "region": "NY"
              }
            }
          },
          "message": "User clicked on document doc_001",
          "message_type": "click",
          "query_id": "q001",
          "timestamp": "2024-08-14T10:31:00Z",
          "user_query": "best pizza recipe"
        }<h2>Script de génération de listes d'arrêts</h2><h3>Aperçu général du script</h3><p>Ce script automatise la génération de la liste de jugement en utilisant les données UBI des requêtes et des événements de clics stockés dans Elasticsearch. Il exécute ces tâches :</p><ul><li><p>Récupère et traite les données UBI dans Elasticsearch.</p></li><li><p>Corréler les événements UBI avec ses requêtes.</p></li><li><p>Calcule le CTR pour chaque position.</p></li><li><p>Calcule le nombre de clics attendus (EC) pour chaque document.</p></li><li><p>Compte les clics réels pour chaque document.</p></li><li><p>Calcule le score COEC pour chaque paire requête-document.</p></li><li><p>Génère une liste de jugements et l'écrit dans un fichier CSV.</p></li></ul><p>Passons en revue chaque fonction :</p><h3>connect_to_elasticsearch()</h3>def connect_to_elasticsearch(host, api_key):
    """Create and return Elasticsearch client"""
    try:
        es = Elasticsearch(
            hosts=[host],
            api_key=api_key,
            request_timeout=60
        )
        # Test the connection
        if es.ping():
            print(f"✓ Successfully connected to Elasticsearch at {host}")
            return es
        else:
            print("✗ Failed to connect to Elasticsearch")
            return None
    except Exception as e:
        print(f"✗ Error connecting to Elasticsearch: {e}")
        return None<p>Cette fonction renvoie un objet client Elasticsearch en utilisant l'hôte et la clé api.</p><h3>fetch_ubi_data()</h3>def fetch_ubi_data(es_client: Elasticsearch, queries_index: str, events_index: str,
                   size: int = 10000) -&gt; Tuple[List[Dict], List[Dict]]:
    """
    Fetch UBI queries and events data from Elasticsearch indices.

    Args:
        es_client: Elasticsearch client
        queries_index: Name of the UBI queries index
        events_index: Name of the UBI events index
        size: Maximum number of documents to fetch

    Returns:
        Tuple of (queries_data, events_data)
    """
    logger.info(f"Fetching data from {queries_index} and {events_index}")

    # Fetch queries with error handling
    try:
        queries_response = es_client.search(
            index=queries_index,
            body={
                "query": {"match_all": {}},
                "size": size
            }
        )
        queries_data = [hit['_source'] for hit in queries_response['hits']['hits']]
        logger.info(f"Fetched {len(queries_data)} queries")

    except Exception as e:
        logger.error(f"Error fetching queries from {queries_index}: {e}")
        raise

    # Fetch events (only click events for now) with error handling
    try:
        events_response = es_client.search(
            index=events_index,
            body={
                "query": {
                    "term": {"message_type.keyword": "CLICK_THROUGH"}
                },
                "size": size
            }
        )
        events_data = [hit['_source'] for hit in events_response['hits']['hits']]
        logger.info(f"Fetched {len(events_data)} click events")

    except Exception as e:
        logger.error(f"Error fetching events from {events_index}: {e}")
        raise

    logger.info(f"Data fetch completed successfully - Queries: {len(queries_data)}, Events: {len(events_data)}")

    return queries_data, events_data<p>Cette fonction est la couche d'extraction des données ; elle se connecte à Elasticsearch pour récupérer les requêtes UBI à l'aide d'une requête match_all et filtre les événements UBI pour obtenir uniquement les événements "CLICK_THROUGH".</p><h3>process_ubi_data()</h3>def process_ubi_data(queries_data: List[Dict], events_data: List[Dict]) -&gt; pd.DataFrame:
    """
    Process UBI data and generate judgment list.

    Args:
        queries_data: List of query documents from UBI queries index
        events_data: List of event documents from UBI events index

    Returns:
        DataFrame with judgment list (qid, docid, grade, keywords)
    """
    logger.info("Processing UBI data to generate judgment list")

    # Group events by query_id
    clicks_by_query = {}
    for event in events_data:
        query_id = event['query_id']
        if query_id not in clicks_by_query:
            clicks_by_query[query_id] = {}

        # Extract clicked document info
        object_id = event['event_attributes']['object']['object_id']
        position = event['event_attributes']['object']['position']['ordinal']

        clicks_by_query[query_id][object_id] = {
            'position': position,
            'timestamp': event['timestamp']
        }

    judgment_list = []

    # Process each query
    for query in queries_data:
        query_id = query['query_id']
        user_query = query['user_query']
        document_ids = query['query_response_object_ids']

        # Get clicks for this query
        query_clicks = clicks_by_query.get(query_id, {})

        # Generate judgment for each document shown
        for doc_id in document_ids:
            grade = calculate_relevance_grade(doc_id, query_clicks, document_ids, queries_data, events_data)

            judgment_list.append({
                'qid': query_id,
                'docid': doc_id,
                'grade': grade,
                'query': user_query
            })

    df = pd.DataFrame(judgment_list)
    logger.info(f"Generated {len(df)} judgment entries for {df['qid'].nunique()} unique queries")

    return df<p>Cette fonction gère la génération de la liste des jugements. Il commence à traiter les données UBI en associant les événements UBI et les requêtes. Il appelle ensuite la fonction calculate_relevance_grade() pour chaque paire document-requête afin d'obtenir les entrées de la liste de jugement. Enfin, il renvoie la liste résultante sous la forme d'un cadre de données pandas.</p><h3>calculer_la_relevance_de_la_grade()</h3>def calculate_relevance_grade(document_id: str, clicks_data: Dict,
                              query_response_ids: List[str], all_queries_data: List[Dict] = None,
                              all_events_data: List[Dict] = None) -&gt; float:
    """
    Calculate COEC (Click Over Expected Clicks) relevance score for a document.

    Args:
        document_id: ID of the document
        clicks_data: Dictionary of clicked documents with their positions for current query
        query_response_ids: List of document IDs shown in search results (ordered by position)
        all_queries_data: All queries data for calculating position CTR averages
        all_events_data: All events data for calculating position CTR averages

    Returns:
        COEC relevance score (continuous value, typically 0.0 to 5.0+)
    """

    # If no global data provided, fall back to simple position-based grading
    if all_queries_data is None or all_events_data is None:
        logger.warning("No global data provided, falling back to position-based grading")
        # Simple fallback logic
        if document_id in clicks_data:
            position = clicks_data[document_id]['position']
            if position &gt; 3:
                return 4.0
            elif position &gt;= 1 and position &lt;= 3:
                return 3.0
        if document_id in query_response_ids:
            position = query_response_ids.index(document_id) + 1
            if position &lt;= 5:
                return 2.0
            elif position &gt;= 6 and position &lt;= 10:
                return 1.0
        return 0.0

    # Calculate rank-aggregated click-through rates
    position_ctr_averages = {}
    position_impression_counts = {}
    position_click_counts = {}

    # Initialize counters
    for pos in range(1, 11):  # Positions 1-10
        position_impression_counts[pos] = 0
        position_click_counts[pos] = 0

    # Count impressions (every document shown contributes)
    for query in all_queries_data:
        for i, doc_id in enumerate(query['query_response_object_ids'][:10]):  # Top 10 positions
            position = i + 1
            position_impression_counts[position] += 1

    # Count clicks by position
    for event in all_events_data:
        if event.get('action_name') == 'click':
            position = event['event_attributes']['object']['position']['ordinal']
            if position &lt;= 10:
                position_click_counts[position] += 1

    # Calculate average CTR per position
    for pos in range(1, 11):
        if position_impression_counts[pos] &gt; 0:
            position_ctr_averages[pos] = position_click_counts[pos] / position_impression_counts[pos]
        else:
            position_ctr_averages[pos] = 0.0

    # Calculate expected clicks for this specific document
    expected_clicks = 0.0

    # Count how many times this document appeared at each position for any query
    for query in all_queries_data:
        if document_id in query['query_response_object_ids']:
            position = query['query_response_object_ids'].index(document_id) + 1
            if position &lt;= 10:
                expected_clicks += position_ctr_averages[position]

    # Count total actual clicks for this document across all queries
    actual_clicks = 0
    for event in all_events_data:
        if (event.get('action_name') == 'click' and
                event['event_attributes']['object']['object_id'] == document_id):
            actual_clicks += 1

    # Calculate COEC score
    if expected_clicks &gt; 0:
        coec_score = actual_clicks / expected_clicks
    else:
        coec_score = 0.0

    logger.debug(
        f"Document {document_id}: {actual_clicks} clicks / {expected_clicks:.3f} expected = {coec_score:.3f} COEC")

    return coec_score<p>Il s'agit de la fonction qui met en œuvre l'algorithme COEC. Il calcule le CTR pour chaque position, puis compare les clics réels pour une paire document-requête, et enfin calcule le score COEC réel pour chacun d'entre eux.</p><h3>generate_judgment_statistics()</h3>def generate_judgment_statistics(df: pd.DataFrame) -&gt; Dict:
    """Generate statistics about the judgment list."""
    stats = {
        'total_judgments': len(df),
        'unique_queries': df['qid'].nunique(),
        'unique_documents': df['docid'].nunique(),
        'grade_distribution': df['grade'].value_counts().to_dict(),
        'avg_judgments_per_query': len(df) / df['qid'].nunique() if df['qid'].nunique() &gt; 0 else 0,
        'queries_with_clicks': len(df[df['grade'] &gt; 1]['qid'].unique()),
        'click_through_rate': len(df[df['grade'] &gt; 1]) / len(df) if len(df) &gt; 0 else 0
    }
    return stats<p>Il génère des statistiques utiles à partir de la liste de jugement, telles que le nombre total de requêtes, le nombre total de documents uniques ou la distribution des notes. Il s'agit d'une information purement informative qui ne modifie pas la liste de jugement qui en résulte.</p><h2>Résultats et impact</h2><p>Si vous suivez les instructions de la section Démarrage rapide, vous devriez obtenir un fichier CSV contenant une liste de jugements de 320 entrées (vous pouvez voir un <a href="https://github.com/Alex1795/elastic-ltr-judgement_list-blog/blob/main/judgment_list.csv">exemple de sortie</a> dans le répertoire). Avec ces champs :</p><ul><li><p>qid : identifiant unique de la requête</p></li><li><p>docid : identifiant unique du document résultant</p></li><li><p>note : la note calculée pour la paire requête-document</p></li><li><p>requête : La requête de l'utilisateur</p></li></ul><p> Examinons les résultats de la requête "recettes italiennes" :</p><p>qid</p><p>docid</p><p>grade</p><p>Requête</p><p>q1-italien-recettes</p><p>recette_pasta_basics</p><p>0.0</p><p>Recettes italiennes</p><p>q1-italien-recettes</p><p>recette_pizza_margherita</p><p>3.333333</p><p>Recettes italiennes</p><p>q1-italien-recettes</p><p>guide_recette_risotto_guide</p><p>10.0</p><p>Recettes italiennes</p><p>q1-italien-recettes</p><p>recette_french_croissant</p><p>0.0</p><p>Recettes italiennes</p><p>q1-italien-recettes</p><p>recette_paella_espagnole</p><p>0.0</p><p>Recettes italiennes</p><p>q1-italien-recettes</p><p>recette_greek_moussaka</p><p>1.875</p><p>Recettes italiennes</p><p>Nous pouvons voir dans les résultats que pour la requête "recettes italiennes" :</p><ul><li><p>La recette de risotto est sans aucun doute le meilleur résultat pour la requête, recevant 10 fois plus de clics que prévu</p></li><li><p>La pizza Margherita est également un excellent résultat.</p></li><li><p>La mousaka grecque (surprenante) est également un bon résultat et se comporte mieux que ne le laisse supposer sa position dans les résultats. Cela signifie que quelques utilisateurs recherchant des recettes italiennes ont été intéressés par cette recette à la place. Ces utilisateurs sont peut-être intéressés par les plats méditerranéens en général. En fin de compte, cela nous indique qu'il pourrait s'agir d'un bon résultat à présenter dans le cadre des deux autres "meilleurs" matches dont nous avons parlé plus haut.</p></li></ul><h2>Conclusion</h2><p>L'utilisation des données UBI nous permet d'automatiser l'apprentissage des modèles LTR, en créant des listes de jugement de haute qualité à partir de nos propres utilisateurs. En utilisant l'algorithme COEC pour générer les notes, nous tenons compte des biais inhérents tout en reflétant ce qu'un utilisateur considère comme un meilleur résultat. La méthode décrite ici peut être appliquée à des cas d'utilisation réels afin d'offrir une meilleure expérience de recherche qui évolue en fonction des tendances d'utilisation réelles.</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/training-learning-to-rank-models-elasticsearch-ubi-data</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/training-learning-to-rank-models-elasticsearch-ubi-data</guid>
    <category><![CDATA[Python]]></category>
    <category><![CDATA[Elastic Cloud Hosted]]></category>
    <dc:creator><![CDATA[Alexander Dávila]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt037eb2f4d380fe65/6a170aa67d8d67397170e6e6/762bf09c28829d626d42c2cfadc719e1dd618d1b-1536x1024.png" length="0" type="image/png"/>
    <pubDate>Wed, 15 Oct 2025 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Recherche géospatiale Elasticsearch avec ES|QL]]></title>
    <description><![CDATA[Recherche géospatiale dans le langage de requête Elasticsearch (ES|QL). Elasticsearch possède de puissantes fonctionnalités de recherche géospatiale, qui sont désormais intégrées à ES|QL pour une facilité d'utilisation et une familiarité avec l'OGC considérablement améliorées.]]></description>
    <content:encoded><![CDATA[<p>Elasticsearch dispose depuis de nombreuses années de puissantes <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/geospatial-analysis.html">fonctionnalités de recherche et d'analyse géospatiales</a>, mais l'API était très différente de celle à laquelle les utilisateurs de SIG étaient habitués. L'année dernière, nous avons <a href="https://www.elastic.co/search-labs/blog/esql-piped-query-language-goes-ga">ajouté le langage d'interrogation ES|QL</a>, un langage d'interrogation par pipeline aussi facile, voire plus facile, que SQL. Il est particulièrement adapté aux cas d'utilisation de la recherche, de la sécurité et de l'observabilité dans lesquels Elastic excelle. Nous avons également ajouté la prise en charge de la recherche et de l'analyse géospatiales dans ES|QL, ce qui facilite grandement son utilisation, en particulier pour les utilisateurs issus des communautés SQL ou <a href="https://en.wikipedia.org/wiki/Geographic_information_system">GIS</a>.</p><p>Elasticsearch 8.12 et 8.13 ont apporté une prise en charge de base des types géospatiaux à ES|QL. Cette fonction a été considérablement améliorée par l'ajout de capacités de recherche géospatiale dans la version 8.14. Plus important encore, ce support a été conçu pour se conformer étroitement à la norme <a href="https://en.wikipedia.org/wiki/Simple_Features">Simple Feature Access de</a> l'<a href="https://en.wikipedia.org/wiki/Open_Geospatial_Consortium">Open Geospatial Consortium (OGC)</a> utilisée par d'autres bases de données spatiales comme PostGIS, ce qui le rend beaucoup plus facile à utiliser pour les experts SIG familiarisés avec ces normes.</p><p>Dans ce blog, nous vous montrerons comment utiliser ES|QL pour effectuer des recherches géospatiales, et comment il se compare aux équivalents SQL et Query DSL. Nous vous montrerons également comment utiliser ES|QL pour effectuer des jointures spatiales et comment visualiser les résultats dans Kibana Maps. Notez que toutes les fonctionnalités décrites ici figurent dans l'aperçu technique de "" , et nous serions ravis de recevoir vos commentaires sur la manière dont nous pouvons les améliorer.</p><h2>Recherche de données géospatiales</h2><p>Commençons par un exemple de requête :</p>FROM airport_city_boundaries
| WHERE ST_INTERSECTS(
      city_boundary,
      "POLYGON((109.4 18.1, 109.6 18.1, 109.6 18.3, 109.4 18.3, 109.4 18.1))"::geo_shape
  )
| KEEP abbrev, airport, region, city, city_location
<p>Cette opération permet de rechercher tous les polygones de limite de ville qui recoupent un polygone de recherche rectangulaire autour de l'aéroport international de Sanya Phoenix (SYX).</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt3897df6bed5d6061/6a17d7c2abe0f29eccdfe861/e48bac8f246c8842f2ea97ddd54910045262aeb1-1440x808.png" alt="Recherche géospatiale ESQL" /><p>Dans un échantillon de données d'aéroports, de villes et de limites de villes, cette recherche trouve le polygone d'intersection et renvoie les champs souhaités à partir du document correspondant :</p><p>abbrev</p><p>aéroport</p><p>région</p><p>ville</p><p>ville_location</p><p>SYX</p><p>Sanya Phoenix Int'l</p><p>天涯区</p><p>Sanya</p><p>POINT(109.5036 18.2533)</p><p>C'était facile ! Comparez maintenant cela au DSL Elasticsearch Query classique pour la même requête :</p>GET /airport_city_boundaries/_search
{
  "_source": ["abbrev", "airport", "region", "city", "city_location"],
  "query": {
    "geo_shape": {
      "city_boundary": {
        "shape": {
          "type": "polygon",
          "coordinates" : [[
            [109.4, 18.1],
            [109.6, 18.1],
            [109.6, 18.3],
            [109.4, 18.3],
            [109.4, 18.1]
          ]]
        }
      }
    }
  }
}
<p>Les deux requêtes sont raisonnablement claires dans leur intention, mais la requête ES|QL ressemble beaucoup à SQL. La même requête dans PostGIS se présente comme suit :</p>SELECT abbrev, airport, region, city, city_location
FROM airport_city_boundaries
WHERE ST_INTERSECTS(
    city_boundary,
    'SRID=4326;POLYGON((109.4 18.1, 109.6 18.1, 109.6 18.3, 109.4 18.3, 109.4 18.1))'::geometry
);
<p>Reprenez l'exemple de ES|QL. C'est un peu la même chose, non ?</p>FROM airport_city_boundaries
| WHERE ST_INTERSECTS(
      city_boundary,
      "POLYGON((109.4 18.1, 109.6 18.1, 109.6 18.3, 109.4 18.3, 109.4 18.1))"::geo_shape
  )
| KEEP abbrev, airport, region, city, city_location
<p>Nous avons constaté que les utilisateurs existants de l'API Elasticsearch trouvent ES|QL beaucoup plus facile à utiliser. Nous pensons que les utilisateurs de SQL, en particulier ceux de Spatial SQL, trouveront que ES|QL est très proche de ce qu'ils ont l'habitude de voir.</p><h4>Pourquoi pas SQL ?</h4><p>Qu'en est-il d'Elasticsearch SQL ? Il existe depuis un certain temps et possède des fonctions géospatiales. Cependant, Elasticsearch SQL a été écrit comme une enveloppe au-dessus de l'API de requête originale, ce qui signifie que seules les requêtes qui pouvaient être transposées vers l'API originale étaient prises en charge. ES|QL n'a pas cette limitation. Le fait qu'il s'agisse d'une pile entièrement nouvelle permet de nombreuses optimisations qui n'étaient pas possibles avec SQL. Nos benchmarks montrent qu'ES|QL est <a href="https://elasticsearch-benchmarks.elastic.co/#tracks/esql/nightly/default/6M">très souvent plus rapide que l'API Query</a>, en particulier avec les agrégations !</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt18bda964c8b24e36/6a17d7c3e3179155d22d568a/b8b6c2b2e45850d832805ed1e71e522f4955f53c-1440x813.png" alt="polygone-intersection-benchmark" /><h2>Différences avec SQL</h2><p>Il ressort clairement de l'exemple précédent que ES|QL est quelque peu similaire à SQL, mais qu'il existe des différences importantes. Par exemple, ES|QL est un langage d'interrogation par pipeline, qui commence par une commande source telle que FROM et enchaîne toutes les commandes suivantes à l'aide du caractère de pipeline |. Il est ainsi très facile de comprendre comment chaque commande reçoit un tableau de données et effectue une action sur ce tableau, comme le filtrage avec <code>WHERE</code>, l'ajout de colonnes avec <code>EVAL</code>, ou l'exécution d'agrégations avec <code>STATS</code>. Plutôt que de commencer par <code>SELECT</code> pour définir les colonnes de sortie finales, il peut y avoir une ou plusieurs commandes <code>KEEP</code>, la dernière spécifiant les résultats de sortie finaux. Cette structure simplifie le raisonnement sur la requête.</p><p>Si l'on se concentre sur la commande <code>WHERE</code> dans l'exemple ci-dessus, on constate qu'elle ressemble beaucoup à l'exemple PostGIS :</p><p><em>ES|QL</em></p>WHERE ST_INTERSECTS(
    city_boundary,
    "POLYGON((109.4 18.1, 109.6 18.1, 109.6 18.3, 109.4 18.3, 109.4 18.1))"::geo_shape
)
<p><em>PostGIS</em></p>WHERE ST_INTERSECTS(
    city_boundary,
    'SRID=4326;POLYGON((109.4 18.1, 109.6 18.1, 109.6 18.3, 109.4 18.3, 109.4 18.1))'::geometry
)
<p>Outre la différence entre les caractères de citation des chaînes, la plus grande différence réside dans la façon dont nous transformons la chaîne de caractères en un type spatial. Dans PostGIS, nous utilisons le suffixe <code>::geometry</code>, tandis que dans ES|QL, nous utilisons le suffixe <code>::geo_shape</code>. En effet, ES|QL fonctionne au sein d'Elasticsearch, et l'opérateur de conversion de type <code>::</code> peut être utilisé pour convertir une chaîne en l'un des <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/esql-limitations.html#_supported_types">types ES|QL pris en charge</a>, dans ce cas, un <code>geo_shape</code>. En outre, les types <code>geo_shape</code> et <code>geo_point</code> dans Elasticsearch impliquent le système de coordonnées spatiales connu sous le nom de WGS84, plus communément désigné par le numéro SRID 4326. Dans PostGIS, cela doit être explicite, d'où l'utilisation du préfixe <code>SRID=4326;</code> pour la chaîne WKT. Si ce préfixe est supprimé, le SRID sera fixé à 0, ce qui est plus proche des types Elasticsearch <code>cartesian_point</code> et <code>cartesian_shape</code>, qui ne sont pas liés à un système de coordonnées spécifique.</p><p>ES|QL et PostGIS fournissent également une syntaxe de fonction de conversion de type :</p><p><em>ES|QL</em></p>WHERE ST_INTERSECTS(
    city_boundary,
    TO_GEOSHAPE("POLYGON((109.4 18.1, 109.6 18.1, 109.6 18.3, 109.4 18.3, 109.4 18.1))")
)
<p><em>PostGIS</em></p>WHERE ST_INTERSECTS(
    city_boundary,
    ST_SetSRID(
      ST_GeomFromText('POLYGON((109.4 18.1, 109.6 18.1, 109.6 18.3, 109.4 18.3, 109.4 18.1))'),
      4326
    )
)
<h2>Fonctions de l'OGC</h2><p>Elasticsearch 8.14 introduit les quatre fonctions de recherche spatiale de l'OGC suivantes :</p><p>ES|QL</p><p>PostGIS</p><p>Description</p><p>ST_INTERSECTS</p><p>ST_Intersects</p><p>Retourne true si deux géométries se croisent, et false dans le cas contraire.</p><p>ST_DISJOINT</p><p>ST_Disjoint</p><p>Retourne true si deux géométries ne se croisent pas, et false dans le cas contraire. L'inverse de ST_INTERSECTS.</p><p>ST_CONTAINS</p><p>ST_Contains</p><p>Retourne true si une géométrie en contient une autre, et false sinon.</p><p>ST_WITHIN</p><p>ST_Within</p><p>Retourne true si une géométrie est à l'intérieur d'une autre, et false dans le cas contraire. L'inverse de ST_CONTAINS.</p><p>Ces fonctions se comportent de manière similaire à leurs homologues PostGIS et sont utilisées de la même manière. Par exemple, <code>ST_INTERSECTS</code> renvoie un message vrai si deux géométries se croisent et un message faux dans le cas contraire. Si vous suivez les liens de documentation dans le tableau ci-dessus, vous remarquerez que tous les exemples ES|QL se trouvent dans une clause <code>WHERE</code> après une clause <code>FROM</code>, alors que tous les exemples PostGIS utilisent des géométries littérales. En fait, les deux plates-formes permettent d'utiliser les fonctions dans n'importe quelle partie de la requête où elles sont utiles.</p><p>Le premier exemple dans la documentation PostGIS pour <code>ST_INTERSECTS</code> est le suivant :</p>SELECT ST_Intersects(
    'POINT(0 0)'::geometry,
    'LINESTRING ( 2 0, 0 2 )'::geometry
);
<p>L'équivalent en ES|QL serait le suivant :</p>ROW ST_INTERSECTS(
    "POINT(0 0)"::geo_point,
    "LINESTRING ( 2 0, 0 2 )"::geo_shape
)
<p>Notez que nous n'avons pas spécifié le SRID dans l'exemple de PostGIS. En effet, dans PostGIS, lorsque l'on utilise le type <code>geometry</code>, tous les calculs sont effectués sur un système de coordonnées planaires et, par conséquent, si les deux géométries ont le même SRID, le SRID n'a pas d'importance. Dans Elasticsearch, c'est également le cas pour la plupart des fonctions, mais il existe des exceptions où <code>geo_shape</code> et <code>geo_point</code> utilisent des calculs sphériques, comme nous le verrons dans le prochain blog sur la recherche de distance spatiale.</p><h2>ES|QL polyvalence</h2><p>Nous avons donc vu ci-dessus des exemples d'utilisation de fonctions spatiales dans les clauses <code>WHERE</code> et dans les commandes <code>ROW</code>. Où cela aurait-il un sens ? Un endroit très utile est la commande <code>EVAL</code>. Cette commande permet d'évaluer une expression et de renvoyer le résultat. Par exemple, déterminons si les centroïdes de tous les aéroports regroupés par nom de pays se trouvent à l'intérieur d'une frontière délimitant le pays :</p>FROM airports
| EVAL in_uk = ST_INTERSECTS(location, TO_GEOSHAPE("POLYGON((1.2305 60.8449, -1.582 61.6899, -10.7227 58.4017, -7.1191 55.3291, -7.9102 54.2139, -5.4492 54.0078, -5.2734 52.3756, -7.8223 49.6676, -5.0977 49.2678, 0.9668 50.5134, 2.5488 52.1065, 2.6367 54.0078, -0.9668 56.4625, 1.2305 60.8449))"))
| EVAL in_iceland = ST_INTERSECTS(location, TO_GEOSHAPE("POLYGON ((-25.4883 65.5312, -23.4668 66.7746, -18.4131 67.4749, -13.0957 66.2669, -12.3926 64.4159, -20.1270 62.7346, -24.7852 63.3718, -25.4883 65.5312))"))
| EVAL within_uk = ST_WITHIN(location, TO_GEOSHAPE("POLYGON((1.2305 60.8449, -1.582 61.6899, -10.7227 58.4017, -7.1191 55.3291, -7.9102 54.2139, -5.4492 54.0078, -5.2734 52.3756, -7.8223 49.6676, -5.0977 49.2678, 0.9668 50.5134, 2.5488 52.1065, 2.6367 54.0078, -0.9668 56.4625, 1.2305 60.8449))"))
| EVAL within_iceland = ST_WITHIN(location, TO_GEOSHAPE("POLYGON ((-25.4883 65.5312, -23.4668 66.7746, -18.4131 67.4749, -13.0957 66.2669, -12.3926 64.4159, -20.1270 62.7346, -24.7852 63.3718, -25.4883 65.5312))"))
| STATS centroid = ST_CENTROID_AGG(location), count=COUNT() BY in_uk, in_iceland, within_uk, within_iceland
| SORT count ASC
<p>Les résultats sont attendus, les centroïdes des aéroports britanniques sont situés à l'intérieur de la frontière britannique, mais pas à l'intérieur de la frontière islandaise, et vice versa :</p><p>centroïde</p><p>Compte</p><p>in_uk</p><p>en_pays</p><p>within_uk</p><p>à l'intérieur de la patrie</p><p>POINT (-21.946634463965893 64.13187285885215)</p><p>1</p><p>faux</p><p>vrai</p><p>faux</p><p>vrai</p><p>POINT (-2.597342072712148 54.33551226578214)</p><p>17</p><p>vrai</p><p>faux</p><p>vrai</p><p>faux</p><p>POINT (0.04453958108176276 23.74658354606057)</p><p>873</p><p>faux</p><p>faux</p><p>faux</p><p>faux</p><p>En fait, ces fonctions peuvent être utilisées dans n'importe quelle partie de la requête où leur signature a un sens. Ils prennent tous deux arguments, qui sont soit un objet spatial littéral, soit un champ d'un type spatial, et renvoient tous une valeur booléenne. Il est important de noter que le système de référence des coordonnées (CRS) des géométries doit correspondre, sinon une erreur sera renvoyée. Cela signifie que vous ne pouvez pas mélanger les types <code>geo_shape</code> et <code>cartesian_shape</code> dans le même appel de fonction. Vous pouvez toutefois mélanger les types <code>geo_point</code> et <code>geo_shape</code>, car le type <code>geo_point</code> est un cas particulier du type <code>geo_shape</code>, et tous deux partagent le même système de référence de coordonnées. La documentation relative à chacune des fonctions définies ci-dessus énumère les combinaisons de types prises en charge.</p><p>En outre, chaque argument peut être un littéral spatial ou un champ, dans l'un ou l'autre ordre. Vous pouvez même spécifier deux champs, deux littéraux, un champ et un littéral, ou un littéral et un champ. La seule condition est que les types soient compatibles. Par exemple, cette requête compare deux champs dans le même index :</p>FROM airport_city_boundaries
| EVAL in_city = ST_INTERSECTS(city_location, city_boundary)
| STATS count=COUNT(*) BY in_city
| SORT count ASC
| EVAL cardinality = CASE(count &lt; 10, "very few", count &lt; 100, "few", "many")
| KEEP cardinality, count, in_city
<p>La requête demande essentiellement si la ville est située à l'intérieur des limites de la ville, ce qui devrait généralement être le cas, mais il y a toujours des exceptions :</p><p>cardinalité</p><p>Compte</p><p>dans_la_ville</p><p>peu</p><p>29</p><p>faux</p><p>nombreux</p><p>740</p><p>vrai</p><p>Une question bien plus intéressante serait de savoir si l'emplacement de l'aéroport se trouve à l'intérieur des limites de la ville desservie par l'aéroport. Cependant, l'emplacement de l'aéroport se trouve dans un index différent de celui qui contient les limites de la ville. Il faut donc trouver une méthode pour interroger efficacement les données de ces deux index distincts et les mettre en corrélation.</p><h2>Jointures spatiales</h2><p>ES|QL ne prend pas <code>JOIN</code> en charge les commandes, mais vous pouvez réaliser un cas spécial de jointure à l'aide de la <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/esql-commands.html#esql-enrich"><code>ENRICH</code></a><a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/esql-commands.html#esql-enrich"> </a>commande, qui se comporte de la même manière qu'une "jointure gauche" en SQL. Cette commande s'apparente à une "jointure gauche" en SQL, vous permettant d'enrichir les résultats d'un index avec des données d'un autre index sur la base d'une relation spatiale entre les deux ensembles de données.</p><p>Par exemple, enrichissons les résultats d'un tableau d'aéroports avec des informations supplémentaires sur la ville qu'ils desservent en trouvant la limite de la ville qui contient l'emplacement de l'aéroport, puis effectuons quelques statistiques sur les résultats :</p>FROM airports
| ENRICH city_boundaries ON city_location WITH airport, region, city_boundary
| MV_EXPAND city_boundary
| EVAL boundary_wkt_length = LENGTH(TO_STRING(city_boundary))
| STATS centroid = ST_CENTROID_AGG(location), count = COUNT(city_location), min_wkt = MIN(boundary_wkt_length), max_wkt = MAX(boundary_wkt_length) BY region
| SORT count DESC
| LIMIT 5
<p>Cette méthode permet d'obtenir les 5 régions qui comptent le plus d'aéroports, ainsi que le centroïde de tous les aéroports qui ont des régions correspondantes et la longueur de la représentation WKT des limites des villes à l'intérieur de ces régions :</p><p>centroïde</p><p>Compte</p><p>min_wkt</p><p>max_wkt</p><p>région</p><p>POINT (-32.56093470960719 32.598117914802714)</p><p>90</p><p>207</p><p>207</p><p>nul</p><p>POINT (-73.94515332765877 40.70366442203522)</p><p>9</p><p>438</p><p>438</p><p>Ville de New York</p><p>POINT (-83.10398317873478 42.300230911932886)</p><p>9</p><p>473</p><p>473</p><p>Détroit</p><p>POINT (-156.3020245861262 20.176383580081165)</p><p>5</p><p>307</p><p>803</p><p>Hawaï</p><p>POINT (-73.88902732171118 45.57078813901171)</p><p>4</p><p>837</p><p>837</p><p>Montréal</p><p>Alors, que s'est-il vraiment passé ? Où s'est produite la supposée <code>JOIN</code>? L'essentiel de la question réside dans la commande <code>ENRICH</code>:</p>FROM airports
| ENRICH city_boundaries ON city_location WITH airport, region, city_boundary
<p>Cette commande demande à Elasticsearch d'enrichir les résultats extraits de l'index <code>airports</code> et d'effectuer une jointure <code>intersects</code> entre le champ <code>city_location</code> de l'index original et le champ <code>city_boundary</code> de l'index <code>airport_city_boundaries</code>, que nous avons utilisé dans quelques exemples plus tôt. Mais certaines de ces informations ne sont pas clairement visibles dans cette requête. Ce que nous voyons, c'est le nom d'une politique d'enrichissement <code>city_boundaries</code>, et l'information manquante est encapsulée dans cette définition de politique.</p>{
  "geo_match": {
    "indices": "airport_city_boundaries",
    "match_field": "city_boundary",
    "enrich_fields": ["city", "airport", "region", "city_boundary"]
  }
}
<p>Ici, nous pouvons voir qu'il effectuera une requête <code>geo_match</code> (<code>intersects</code> est la valeur par défaut), que le champ à comparer est <code>city_boundary</code> et que les champs <code>enrich_fields</code> sont les champs que nous voulons ajouter au document d'origine. L'un de ces champs, le <code>region</code>, a été utilisé comme clé de regroupement pour la commande <code>STATS</code>, ce que nous n'aurions pas pu faire sans cette capacité de "jointure à gauche". Pour plus d'informations sur les politiques d'enrichissement, voir la <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/enrich-setup.html">documentation d'enrichissement</a>. En lisant ces documents, vous remarquerez qu'ils décrivent l'utilisation des index d'enrichissement pour enrichir les données au moment de l'indexation, en configurant les pipelines d'ingestion. Cela n'est pas nécessaire pour ES|QL, car la commande <code>ENRICH</code> fonctionne au moment de la requête. Il suffit de préparer l'index d'enrichissement avec les données et la politique d'enrichissement nécessaires, puis d'utiliser la commande <code>ENRICH</code> dans vos requêtes ES|QL.</p><p>Vous pouvez également remarquer que la région la plus fréquemment trouvée est <code>null</code>. Qu'est-ce que cela peut signifier ? Rappelez-vous que j'ai comparé cette commande à une "jointure gauche" en SQL, ce qui signifie que si aucune limite de ville correspondante n'est trouvée pour un aéroport, l'aéroport est toujours renvoyé, mais avec les valeurs <code>null</code> pour les champs de l'index <code>airport_city_boundaries</code>. Il s'avère que 89 aéroports n'ont pas trouvé de correspondance avec <code>city_boundary</code>, et un aéroport avec une correspondance où le champ <code>region</code> était <code>null</code>. Cela a conduit à un décompte de 90 aéroports sans <code>region</code> dans les résultats. Un autre détail intéressant est la nécessité de la commande <code>MV_EXPAND</code>. Cela est nécessaire car la commande <code>ENRICH</code> peut renvoyer plusieurs résultats pour chaque ligne d'entrée, et <code>MV_EXPAND</code> permet de séparer ces résultats en plusieurs lignes, une pour chaque résultat. Cela explique également pourquoi "Hawaii" affiche des résultats différents de <code>min_wkt</code> et <code>max_wkt</code>: il y avait plusieurs régions portant le même nom mais ayant des limites différentes.</p><h2>Cartes Kibana</h2><p>Kibana a ajouté la prise en charge de Spatial ES|QL dans l'application Maps. Cela signifie que vous pouvez désormais utiliser ES|QL pour rechercher des données géospatiales dans Elasticsearch et visualiser les résultats sur une carte.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt91922eb4ef43d290/6a16f7161949f737bfe7a7b5/bd78470bd8a4bc60f0db7006bd804b8fe87e2fea-1440x683.png" alt="Couches Kibana ES|QL" /><p>Il existe une nouvelle option de couche dans le menu d'ajout de couches, appelée "ES|QL". Comme toutes les fonctions géospatiales décrites jusqu'à présent, il s'agit d'un aperçu technique "" . Cette option permet d'ajouter une couche à la carte en fonction des résultats d'une requête ES|QL. Par exemple, vous pouvez ajouter une couche à la carte qui montre tous les aéroports du monde.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt5185ef7461d7e81d/6a16f718839dfa1559dcfcb1/1dd28d3d0509f92d26b0bb5320a2925f7a54c5d9-1440x736.png" alt="Kibana ES|QL - Aéroports" /><p>Vous pouvez également ajouter une couche qui montre les polygones de l'index <code>airport_city_boundaries</code> ou, mieux encore, la requête complexe <code>ENRICH</code> ci-dessus qui génère des statistiques sur le nombre d'aéroports dans chaque région.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt51ee3543bb811d98/6a16f71a8b73cbbe63189df1/679a0a401faa613c7cedddd07c64f614ac2b7144-1440x727.png" alt="Kibana ES|QL - Statistiques par région" /><h2>Et ensuite ?</h2><p>Vous avez peut-être remarqué que dans deux des exemples ci-dessus, nous avons inséré une autre fonction spatiale <code>ST_CENTROID_AGG</code>. Il s'agit d'une fonction d'agrégation utilisée dans la commande <code>STATS</code> et de la première des nombreuses fonctions d'analyse spatiale que nous prévoyons d'ajouter à ES|QL. Nous en parlerons sur notre blog lorsque nous en aurons plus à montrer !</p><p>Avant cela, nous souhaitons vous parler d'une fonctionnalité particulièrement intéressante sur laquelle nous avons travaillé : la possibilité d'effectuer des recherches spatiales par distance, l'une des fonctionnalités de recherche spatiale les plus utilisées d'Elasticsearch. Pouvez-vous imaginer à quoi pourrait ressembler la syntaxe des recherches à distance ? Peut-être similaire à une fonction de l'OGC ? Restez à l'écoute du prochain blog de cette série pour le découvrir !</p><p>Alerte au spoiler : Elasticsearch 8.15 vient d'être publié, et la recherche de distance spatiale avec ES|QL est incluse !</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/esql-geospatial-search-part-one</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/esql-geospatial-search-part-one</guid>
    <category><![CDATA[Python]]></category>
    <category><![CDATA[ES|QL]]></category>
    <dc:creator><![CDATA[Craig Taverner]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltd05627be20e89dfb/6a17d7c6414c640256944fdb/de886289dcb56494920875303b622b030b9b810f-720x420.jpg" length="0" type="image/jpeg"/>
    <pubDate>Mon, 12 Aug 2024 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Évaluation de la pertinence des recherches, partie 1 - Le benchmark BEIR]]></title>
    <description><![CDATA[Apprenez à évaluer votre système de recherche dans le cadre d'une meilleure compréhension du benchmark BEIR, grâce à des conseils et des techniques pour améliorer vos processus d'évaluation des recherches.]]></description>
    <content:encoded><![CDATA[<p>Ce document est le premier d'une série d'articles de blog traitant de la manière d'évaluer vos propres systèmes de recherche dans le cadre d'une meilleure compréhension du benchmark BEIR. Nous vous présenterons des conseils et des techniques spécifiques pour améliorer vos processus d'évaluation des recherches dans le contexte d'une meilleure compréhension du BEIR. Nous aborderons également les pièges courants qui affectent la fiabilité de l'évaluation. Enfin, nous soulignerons l'importance des LLM comme nouvel outil puissant pour les ingénieurs en recherche et illustrerons par des exemples leur utilisation pour faciliter l'évaluation.</p><h2>Comprendre le benchmark BEIR dans l'évaluation de la pertinence des recherches</h2><p>Pour améliorer un système, il faut pouvoir en mesurer l'efficacité. Dans le contexte de la recherche, <a href="https://arxiv.org/abs/2104.08663">BEIR</a> (ou, de manière équivalente, la section Récupération du classement <a href="https://huggingface.co/spaces/mteb/leaderboard">MTEB</a>) est considéré comme le "Saint Graal" de la communauté de recherche d'informations, et cela n'a rien d'étonnant. C'est un indice de référence très bien structuré avec des ensembles de données variés couvrant différentes tâches. Plus précisément, les domaines suivants sont abordés :</p><ul><li><p>Récupération d'arguments (ArguAna, Touche2020)</p></li><li><p>QA en domaine ouvert (HotpotQA, Natural Questions, FiQA)</p></li><li><p>Récupération par passes (MSMARCO)</p></li><li><p>Récupération des questions en double (Quora, CQADupstack)</p></li><li><p>Vérification des faits (FEVER, Climate-FEVER, Scifact)</p></li><li><p>Récupération d’informations biomédicales (TREC-COVID, NFCorpus, BioASQ)</p></li><li><p>Récupération d'entités (DBPedia)</p></li><li><p>Prédiction de citation (SCIDOCS)</p></li></ul><p>Cet benchmark fournit une statistique unique, nDCG@10, qui indique dans quelle mesure un système fait correspondre les documents les plus pertinents pour chaque exemple de tâche dans les premiers résultats qu'il renvoie. Pour un système de recherche avec lequel un humain interagit, la pertinence des résultats en haut de liste est essentielle. Cependant, l'évaluation d'une recherche comporte de nombreuses nuances qu'une simple statistique récapitulative ne permet pas de saisir.</p><h2>Structure d'un ensemble de données BEIR</h2><p>Chaque benchmark comporte trois artefacts :</p><ul><li><p>le corpus ou les documents à récupérer</p></li><li><p>les requêtes</p></li><li><p>les jugements de pertinence pour les requêtes (alias <code>qrels</code>).</p></li></ul><p>Les jugements de pertinence sont fournis sous forme de score égal ou supérieur à zéro. Les scores non nuls indiquent que le document est partiellement pertinent par rapport à la requête.</p><p>Ensemble de données</p><p>Taille du corpus</p><p>Nb. de requêtes dans l'ensemble de test</p><p>#qrels étiquetés positivement</p><p>#qrels égal à zéro</p><p>#Doublons dans le corpus</p><p>Arguana</p><p>8 674</p><p>1 406</p><p>1 406</p><p>0</p><p>96</p><p>Climate-FEVER</p><p>5 416 593</p><p>1 535</p><p>4 681</p><p>0</p><p>0</p><p>DBPedia</p><p>4 635 922</p><p>400</p><p>15 286</p><p>28 229</p><p>0</p><p>FEVER</p><p>5 416 568</p><p>6 666</p><p>7 937</p><p>0</p><p>0</p><p>FiQA-2018</p><p>57 638</p><p>648</p><p>1 706</p><p>0</p><p>0</p><p>HotpotQA</p><p>5 233 329</p><p>7 405</p><p>14 810</p><p>0</p><p>0</p><p>Questions naturelles</p><p>2 681 468</p><p>3 452</p><p>4 021</p><p>0</p><p>16 781</p><p>NFCorpus</p><p>3 633</p><p>323</p><p>12 334</p><p>0</p><p>80</p><p>Quora</p><p>522 931</p><p>10 000</p><p>15 675</p><p>0</p><p>1 092</p><p>SCIDOCS</p><p>25 657</p><p>1 000</p><p>4 928</p><p>25 000</p><p>2</p><p>SciFact</p><p>5 183</p><p>300</p><p>339</p><p>0</p><p>0</p><p>Touche2020</p><p>382 545</p><p>49</p><p>932</p><p>1 982</p><p>5 357</p><p>TREC-COVID</p><p>171 332</p><p>50</p><p>24 763</p><p>41 663</p><p>0</p><p>MSMARCO</p><p>8 841 823</p><p>6 980</p><p>7 437</p><p>0</p><p>324</p><p>CQADupstack (somme)</p><p>457 199</p><p>13 145</p><p>23 703</p><p>0</p><p>0</p><p><strong>Tableau 1</strong> : Statistiques de l'ensemble de données. Les chiffres ont été calculés sur la partie test des ensembles de données (<code>dev</code> pour <code>MSMARCO</code>).</p><p>Le <strong>tableau 1</strong> présente quelques statistiques issues des ensembles de données qui composent le benchmark <code>BEIR</code>, telles que le nombre de documents dans le corpus, le nombre de requêtes dans l'ensemble de données de test et le nombre de paires positives/négatives (requête, document) dans le fichier <code>qrels</code>. Un simple coup d'œil aux données nous permet de déduire immédiatement ce qui suit :</p><ul><li><p>La plupart des ensembles de données ne contiennent aucune relation négative dans le fichier <code>qrels</code>, c'est-à-dire des scores nuls, ce qui indiquerait explicitement que les documents ne sont pas pertinents pour la requête donnée.</p></li><li><p>Le nombre moyen de relations entre documents par requête (<code>#qrels</code> / <code>#queries</code>) varie de 1,0 dans le cas de <code>ArguAna</code> à 493,5 (<code>TREC-COVID</code>), mais avec une valeur de <code>&lt;</code>5 dans la majorité des cas.</p></li><li><p>Certains ensembles de données comportent des documents en double dans le corpus, ce qui, dans certains cas, peut conduire à une évaluation incorrecte, c'est-à-dire lorsqu'un document est considéré comme pertinent par rapport à une requête, alors que son doublon ne l'est pas. Par exemple, dans <code>ArguAna</code>, nous avons identifié 96 cas de paires de documents en double, avec un seul document par paire marqué comme pertinent pour une requête. En agrandissant la liste initiale des qrels pour inclure également les doublons, nous avons observé une augmentation relative d'environ 1 % du score <code>nDCG@10</code> en moyenne.</p></li></ul>{
  "_id": "test-economy-epiasghbf-pro02b",
  "title": "economic policy international africa society gender house believes feminisation",
  "text": "Again employment needs to be contextualised with …",
  "metadata": {}
}
{
  "_id": "test-society-epiasghbf-pro02b",
  "title": "economic policy international africa society gender house believes feminisation",
  "text": "Again employment needs to be contextualised with …",
  "metadata": {}
}
<p><strong>Exemple de paires dupliquées dans ArguAna. Dans le fichier qrels, seul la première semble pertinente (en tant que contre-argument) par rapport à la requête ("test-economy-epiasghbf-pro02a")</strong></p><p>Lors de la comparaison des modèles sur le classement MTEB, il est tentant de se concentrer sur la qualité moyenne de la récupération. Il s'agit d'un bon indicateur de la qualité globale du modèle, mais cela ne prédit pas nécessairement ses performances dans votre cas d'utilisation. Comme les résultats sont présentés par ensemble de données, il est important de comprendre le degré de pertinence des différents ensembles de données par rapport à votre tâche de recherche et de réévaluer les modèles en ne retenant que les plus pertinents. Pour une analyse plus approfondie, vous pouvez également vérifier le chevauchement thématique entre les différents corpus. Répartir les mesures de qualité par thème permet une évaluation beaucoup plus fine de leurs forces et faiblesses particulières.</p><p>Une remarque importante ici est que lorsqu'un document n'est pas marqué dans le fichier <code>qrels</code>, considéré par défaut comme non pertinent pour la requête. Nous approfondissons un peu plus ce domaine et recueillons des éléments de preuve pour éclairer davantage la question suivante : "À quelle fréquence un évaluateur est-il confronté à des paires (requête, document) pour lesquelles il n'existe aucune information de référence ?" Ceci est important, car lorsque seul un balisage superficiel est disponible (et que tous les documents pertinents ne sont donc pas étiquetés comme tels), un système de recherche d'informations peut être considéré comme pire qu'un autre simplement parce qu'il "choisit" de faire apparaître différents documents pertinents (mais non marqués). Il s'agit d'un piège courant dans la création d'ensembles de données d'évaluation de haute qualité, en particulier pour les grands ensembles de données. Pour être réalisable, l'étiquetage manuel se concentre généralement sur les meilleurs résultats renvoyés par le système actuel, ce qui peut entraîner l'omission de documents pertinents situés dans ses angles morts. Il est donc généralement préférable de consacrer davantage de ressources à un balisage plus complet d'un nombre restreint de requêtes plutôt qu'à un balisage superficiel et général.</p><h2>Utilisation du benchmark BEIR pour l'évaluation de la pertinence des recherches</h2><p>Pour initier notre analyse, nous mettons en œuvre le scénario suivant (voir le <a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/evaluating-search-relevance-part-1/retrieve-and-rerank.ipynb">notebook</a>) :</p><ol><li><p>Tout d'abord, nous chargeons le corpus de chaque ensemble de données dans un index Elasticsearch.</p></li><li><p>Pour chaque requête dans l'ensemble de test, nous récupérons les 100 meilleurs documents avec BM25.</p></li><li><p>Nous reclassons les documents récupérés à l'aide de divers modèles de reclassement SOTA.</p></li><li><p>Enfin, nous rapportons le "taux de jugement" pour les 10 meilleurs documents provenant des étapes 2 (après récupération) et 3 (après reclassement). En d'autres termes, nous calculons le pourcentage moyen des 10 meilleurs documents qui ont un score dans le fichier <code>qrels</code>.</p></li></ol><p>Voici la liste des modèles de reclassement que nous avons utilisés :</p><ul><li><p><a href="https://docs.cohere.com/reference/rerank">Cohere</a> <code>rerank-english-v2.0</code> et <code>rerank-english-v3.0</code></p></li><li><p><a href="https://huggingface.co/BAAI/bge-reranker-base">BGE-base</a></p></li><li><p><a href="https://huggingface.co/mixedbread-ai/mxbai-rerank-xsmall-v1">mxbai-rerank-xsmall-v1</a></p></li><li><p><a href="https://huggingface.co/cross-encoder/ms-marco-MiniLM-L-6-v2">MiniLM-L-6-v2</a></p></li></ul><p></p><p>Récupération</p><p>Reranking</p><p></p><p></p><p></p><p></p><p>Ensemble de données</p><p>BM25 (%)</p><p>Cohere Rerank v2 (%)</p><p>Cohere Rerank v3 (%)</p><p>BGE-base (%)</p><p>mxbai-rerank-xsmall-v1 (%)</p><p>MiniLM-L-6-v2 (%)</p><p>Arguana</p><p>7,54</p><p>4,87</p><p>7,87</p><p>4,52</p><p>4,53</p><p>6,84</p><p>Climate-FEVER</p><p>5,75</p><p>6,24</p><p>8,15</p><p>9,36</p><p>7,79</p><p>7,58</p><p>DBPedia</p><p>61,18</p><p>60,78</p><p>64,15</p><p>63,9</p><p>63,5</p><p>67,62</p><p>FEVER</p><p>8,89</p><p>9,97</p><p>10,08</p><p>10,19</p><p>9,88</p><p>9,88</p><p>FiQa-2018</p><p>7,02</p><p>11,02</p><p>10,77</p><p>8,43</p><p>9,1</p><p>9,44</p><p>HotpotQA</p><p>12,59</p><p>14,5</p><p>14,76</p><p>15.1</p><p>14,02</p><p>14,42</p><p>Questions naturelles</p><p>5,94</p><p>8.84</p><p>8,71</p><p>8,37</p><p>8,14</p><p>8,34</p><p>NFCorpus</p><p>31,67</p><p>32,9</p><p>33,91</p><p>30,63</p><p>32,77</p><p>32,45</p><p>Quora</p><p>12,2</p><p>10,46</p><p>13,04</p><p>11,26</p><p>12,58</p><p>12,78</p><p>SCIDOCS</p><p>8,62</p><p>9,41</p><p>9,71</p><p>8,04</p><p>8,79</p><p>8,52</p><p>SciFact</p><p>9,07</p><p>9,57</p><p>9,77</p><p>9,3</p><p>9,1</p><p>9,17</p><p>Touche2020</p><p>38,78</p><p>30,41</p><p>32,24</p><p>33,06</p><p>37,96</p><p>33,67</p><p>TREC-COVID</p><p>92,4</p><p>98,4</p><p>98,2</p><p>93,8</p><p>99,6</p><p>97,4</p><p>MSMARCO</p><p>3,97</p><p>6,00</p><p>6,03</p><p>6,07</p><p>5,47</p><p>6,11</p><p>CQADupstack (moy.)</p><p>5,47</p><p>6,32</p><p>6,87</p><p>5,89</p><p>6,22</p><p>6,16</p><p><strong>Tableau 2</strong> : Taux de jugement par paires (ensemble de données, reranker) calculé sur les 10 premiers documents récupérés/reclassés</p><p>D'après le <strong>tableau 2</strong>, à l'exception de <code>TREC-COVID</code> (&gt;90 % de couverture), <code>DBPedia</code> (~65 %), <code>Touche2020</code> et <code>nfcorpus</code> (~35%), nous constatons que la majorité des ensembles de données ont un taux d'étiquetage compris entre 5 % et un peu plus de 10 % après l'extraction ou le reclassement. Cela ne veut pas dire que tous ces documents banalisés sont pertinents, mais il se peut qu'un sous-ensemble d'entre eux, en particulier ceux placés en tête de liste, soit positif.</p><p>L'avènement des modèles de langage réglés pour les instructions générales nous offre un nouvel outil puissant capable d'automatiser potentiellement l'évaluation de la pertinence. Ces méthodes sont généralement beaucoup trop gourmandes en ressources de calcul pour être utilisées en ligne dans le cadre de la recherche, mais nous nous intéressons ici à l'évaluation hors ligne. Nous utilisons ces méthodes dans ce qui suit pour examiner les indices montrant que certains ensembles de données BEIR souffrent d'un balisage superficiel.</p><p>Afin d'approfondir cette hypothèse, nous avons décidé de nous concentrer sur MSMARCO et de sélectionner un sous-ensemble de 100 requêtes, ainsi que les 5 documents les mieux reclassés (avec Cohere v2) qui ne sont actuellement pas marqués comme pertinents. Nous avons suivi deux voies d'évaluation différentes : premièrement, nous avons utilisé un prompt soigneusement ajusté (nous y reviendrons dans un article ultérieur) pour préparer le modèle <a href="https://huggingface.co/microsoft/Phi-3-mini-4k-instruct">Phi-3-mini-4k</a> récemment publié afin de prédire la pertinence (ou non) d'un document par rapport à la requête. En parallèle, ces cas ont également été étiquetés manuellement afin d'évaluer également le taux d'accord entre la sortie du LLM et le jugement humain. Dans l'ensemble, nous pouvons tirer les deux conclusions suivantes :</p><ul><li><p>Le taux de concordance entre les réponses LLM et les jugements humains était proche de 80 %, ce qui semble suffisant comme point de départ dans cette direction.</p></li><li><p>Dans 57,6 % des cas (sur la base du jugement humain), les documents renvoyés se sont avérés réellement pertinents par rapport à la requête. Autrement dit : pour 100 requêtes, 107 documents ont été jugés pertinents, mais au moins 0,576 × 5 × 100 = 288 documents supplémentaires sont réellement pertinents !</p></li></ul><p>Voici quelques exemples issus de l'ensemble de données <code>MSMARCO</code>/<code>dev</code> qui contiennent la requête, le document positif annoté (de <code>qrels</code>) et un faux négatif dû à un balisage incomplet :</p><p>1{^&gt;er &lt;^}exemple :</p>{
  "query":
    {
        "_id": 155234,
        "text": "do bigger tires affect gas mileage"
    },
  "positive_doc":
    {
        "_id": 502713,
        "text": "Tire Width versus Gas Mileage. Tire width is one of the only tire size factors that can influence gas mileage in a positive way. For example, a narrow tire will have less wind resistance, rolling resistance, and weight; thus increasing gas mileage.",
    },
    "negative_doc":
    {
        "_id": 7073658,
        "text": "Tire Size and Width Influences Gas Mileage. There are two things to consider when thinking about tires and their effect on gas mileage; one is wind resistance, and the other is rolling resistance. When a car is driving at higher speeds, it experiences higher wind resistance; this means lower fuel economy."
    }
}
<p>2{^&gt;e &lt;^}exemple :</p>{
  "query":
    {
        "_id": 300674,
        "text": "how many years did william bradford serve as governor of plymouth colony?"
    },
  "positive_doc":
    {
        "_id": 7067032,
        "text": "http://en.wikipedia.org/wiki/William_Bradford_(Plymouth_Colony_governor) William Bradford (c.1590 \u00e2\u0080\u0093 1657) was an English Separatist leader in Leiden, Holland and in Plymouth Colony was a signatory to the Mayflower Compact. He served as Plymouth Colony Governor five times covering about thirty years between 1621 and 1657."
    },
    "negative_doc":
    {
        "_id": 2495763,
        "text": "William Bradford was the governor of Plymouth Colony for 30 years. The colony was founded by people called Puritans. They were some of the first people from England to settle in what is now the United States. Bradford helped make Plymouth the first lasting colony in New England."
    }
}
<p>L'évaluation manuelle de requêtes spécifiques comme celle-ci est une technique généralement utile pour comprendre la qualité de la recherche qui complète les mesures quantitatives comme le nDCG@10. Si vous disposez d'un ensemble représentatif de requêtes que vous exécutez systématiquement lorsque vous modifiez la recherche, cela vous fournit des informations qualitatives importantes sur l'évolution des performances, informations invisibles dans les statistiques. Par exemple, cela vous donne beaucoup plus d'informations sur les faux résultats renvoyés par la recherche : cela peut vous aider à repérer les erreurs évidentes dans les résultats obtenus, les classes d'erreurs connexes, telles que la mauvaise interprétation de la terminologie spécifique au domaine, etc.</p><p>Notre résultat est en accord avec les recherches pertinentes concernant l'évaluation de <code>MSMARCO</code>. Par exemple, <a href="https://arxiv.org/pdf/2109.00062">Arabzadeh et al.</a> suivent une procédure similaire où ils emploient des travailleurs du crowdsourcing pour effectuer des jugements de préférences : entre autres, ils montrent que, dans de nombreux cas, les documents retournés par les modules de reclassement sont préférés à ceux du fichier MSMARCO <code>qrels</code>. Un autre élément de preuve est fourni par les auteurs de l'outil de reclassement <a href="https://arxiv.org/pdf/2010.08191">RocketQA</a>, qui indiquent que plus de 70 % des documents reclassés ont été jugés pertinents après inspection manuelle.</p><p> Mise à jour – 9 septembre : Après une réévaluation minutieuse de l'ensemble de données, nous avons identifié 15 autres cas de documents pertinents, portant leur nombre total de 273 à 288</p><h2>Principaux points à retenir et prochaines étapes</h2><ul><li><p>La quête d'une meilleure référence est sans fin, car elle est cruciale pour le benchmarking et la comparaison de modèles. Les LLM peuvent aider dans certains domaines d'évaluation s'ils sont utilisés avec prudence et réglés avec des instructions appropriées.</p></li><li><p>Plus généralement, étant donné que les critères de référence ne seront jamais parfaits, il peut être préférable de passer d'une simple comparaison de scores à des techniques plus robustes permettant de saisir les différences statistiquement significatives. Les travaux de <a href="https://arxiv.org/pdf/2109.00062">Arabzadeh et al.</a> fournit un bon exemple de cette méthode où, d'après leurs résultats, ils construisent des intervalles de confiance à 95 % indiquant des différences significatives (ou non) entre les différentes exécutions. Dans le <a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/evaluating-search-relevance-part-1/retrieve-and-rerank.ipynb">notebook</a> joint, nous proposons une implémentation des intervalles de confiance en utilisant le <a href="https://en.wikipedia.org/wiki/Bootstrapping_(statistics)">bootstrapping</a>.</p></li><li><p>Du point de vue de l’utilisateur final, il est utile de réfléchir à l'alignement des tâches lors de la lecture des résultats de benchmark. Par exemple, pour un ingénieur en IA qui conçoit un pipeline RAG et sait que le cas d'utilisation le plus courant consiste à assembler de multiples informations provenant de différentes sources, il serait plus pertinent d'évaluer les performances de son modèle de recherche sur des ensembles de données QA multisauts comme HotpotQA plutôt que sur la moyenne globale du benchmark BEIR.</p></li></ul><p>Dans l'<a href="https://www.elastic.co/search-labs/blog/evaluating-search-relevance-part-2">article de blog suivant</a>, nous approfondirons l'utilisation de Phi-3 en tant que juge LLM et le processus de réglage pour prédire la pertinence.</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/evaluating-search-relevance-part-1</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/evaluating-search-relevance-part-1</guid>
    <category><![CDATA[Recherche ML]]></category>
    <category><![CDATA[Python]]></category>
    <dc:creator><![CDATA[Thanos Papaoikonomou,Thomas Veasey]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt9d9912c8d4187096/6a1704f5b0367d30e672bc17/54a6e5197f5721b36fc65f27387d29803ed35589-721x420.jpg" length="0" type="image/jpeg"/>
    <pubDate>Tue, 16 Jul 2024 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Plagiat par l'IA : Détection de plagiat avec Elasticsearch]]></title>
    <description><![CDATA[Voici comment vérifier le plagiat de l'IA à l'aide d'Elasticsearch, en se concentrant sur les cas d'utilisation avec les modèles NLP et Vector Search.]]></description>
    <content:encoded><![CDATA[<p>Le plagiat peut être <strong>direct</strong>, c'est-à-dire qu'il consiste à copier des parties ou la totalité du contenu, ou <strong>paraphrasé</strong>, c'est-à-dire qu'il consiste à reformuler le travail de l'auteur en changeant certains mots ou certaines phrases.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltb6e139760e56ec98/6a171147dc55de0ad2e00edf/5d7073187fda829438aeec8d3a1194a5bea2ba57-1440x347.png" alt="" /><p>Il existe une distinction entre l'inspiration et la paraphrase. Il est possible de lire un contenu, de s'en inspirer, puis d'explorer l'idée avec ses propres mots, même si l'on arrive à une conclusion similaire.</p><p>Si le plagiat est un sujet de discussion depuis longtemps, l'accélération de la production et de la publication de contenus lui a donné toute sa pertinence et constitue un défi permanent.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltc2af1678cb04506b/6a171149cf4f251c2ab2d257/a0b9a98d729db09dae0a79315c001e6763c12704-1400x1016.png" alt="" /><p>Ce défi ne se limite pas aux livres, à la recherche universitaire ou aux documents judiciaires, pour lesquels des contrôles de plagiat sont fréquemment effectués. Elle peut également s'étendre aux journaux et même aux médias sociaux.</p><p>Avec l'abondance d'informations et l'accès facile à la publication, comment le plagiat peut-il être contrôlé de manière efficace et à grande échelle ?</p><p>Les universités, les organismes publics et les entreprises utilisent divers outils, mais si une simple <a href="https://www.elastic.co/search-labs/lexical-and-semantic-search-with-elasticsearch">recherche lexicale</a> permet de détecter efficacement le plagiat direct, la principale difficulté réside dans l'identification du <strong>contenu paraphrasé</strong>.</p><h2>Détection du plagiat avec l'IA générative</h2><p>L'IA générative pose un nouveau défi. Le contenu généré par l'IA est-il considéré comme du plagiat lorsqu'il est copié ?</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt8a1ed1b6fa3f1a56/6a17114b4a531bc40736aa69/9345b28d6d27c37469bc38e823c41780b4eabfe5-1440x875.png" alt="" /><p>Les <a href="https://openai.com/"></a> <a href="https://openai.com/policies/terms-of-use">conditions d'utilisation</a> de l'OpenAI, par exemple, précisent que l'OpenAI ne revendiquera pas de droits d'auteur sur le contenu généré par l'API pour les utilisateurs. Dans ce cas, les personnes utilisant leur IA générative peuvent utiliser le contenu généré comme elles le souhaitent, sans le citer.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltbc2e08ab5b808e38/6a17114dab7f086cbadb9f93/e1f415f69a247666f02ddc81468944920c874cd7-968x814.png" alt="" /><p>Toutefois, l'acceptation de l'utilisation de l'IA générative pour améliorer l'efficacité reste un sujet de discussion.</p><p>Afin de contribuer à la détection du plagiat, OpenAI a développé un <a href="https://huggingface.co/roberta-base-openai-detector">modèle de détection</a>, mais a reconnu par la suite que sa précision n'était pas suffisamment élevée.</p><p><em>"Nous pensons que cette précision n'est pas suffisante pour une détection autonome et qu'elle doit être associée à des approches basées sur les métadonnées, au jugement humain et à l'éducation du public pour être plus efficace."</em></p><p>Le défi persiste ; cependant, grâce à la disponibilité d'outils supplémentaires, il existe désormais davantage d'options pour détecter le plagiat, même dans les cas de paraphrases et de contenu d'IA.</p><h2>Détecter le plagiat avec Elasticsearch</h2><p>C'est pourquoi, dans ce blog, nous explorons un autre cas d'utilisation des modèles de traitement du langage naturel (NLP) et de la recherche vectorielle, la détection du plagiat, au-delà de la recherche de métadonnées.</p><p>Nous le démontrons à l'aide d'<a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/plagiarism-detection-with-elasticsearch/plagiarism_detection_es.ipynb">exemples en Python</a>, où nous utilisons un <a href="https://sbert.net/datasets/emnlp2016-2018.json">ensemble de données</a> de <a href="https://www.sbert.net/">SentenceTransformers</a> contenant des articles liés au TAL. Nous vérifions que les résumés ne sont pas plagiés en effectuant une "similarité textuelle sémantique" en tenant compte de l'intégration des "résumés" générée à l'aide d'un <a href="https://huggingface.co/sentence-transformers/all-mpnet-base-v2">modèle d'intégration de texte</a> importé au préalable dans Elasticsearch. En outre, pour identifier le contenu généré par l'IA - le plagiat par l'IA, un <a href="https://huggingface.co/roberta-base-openai-detector">modèle NLP</a> développé par OpenAI a également été importé dans Elasticsearch.</p><p>L'image suivante illustre le flux de données :</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt0464f88d3ed12070/6a17114fab7f084905db9f97/1ad89c98a2f42a497548ca3947749bad54ec1172-1440x880.png" alt="" /><p>Au cours du processus d <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/ingest.html">'ingestion</a> avec un <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/inference-processor.html">processeur d'inférence</a>, le paragraphe "abstrait" est mis en correspondance avec un vecteur à 768 dimensions, le "vecteur_abstrait.valeur_prédite".</p><p>Cartographie :</p>"abstract_vector.predicted_value": { # Inference results field
"type": "dense_vector", 
"dims": 768, # model embedding_size
"index": "true", 
"similarity": "dot_product" # When indexing vectors for approximate kNN search, you need to specify the similarity function for comparing the vectors.
<p>La similarité entre les représentations vectorielles est mesurée à l'aide d'une métrique de similarité vectorielle, définie à l'aide du <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/dense-vector.html#dense-vector-params">paramètre</a>"similarité".</p><p><a href="https://en.wikipedia.org/wiki/Cosine_similarity">Cosinus</a> est la métrique de similarité par défaut, calculée comme '(1 + cosinus(query, vector)) / 2'. À moins que vous ne deviez préserver les vecteurs originaux et que vous ne puissiez pas les normaliser à l'avance, la façon la plus efficace d'effectuer une similarité en cosinus est de normaliser tous les vecteurs à une longueur unitaire. Cela permet d'éviter d'effectuer des calculs supplémentaires sur la longueur du vecteur pendant la recherche, et d'utiliser plutôt 'dot_product'.</p><p>Dans ce même pipeline, un autre processeur d'inférence contenant le <a href="https://huggingface.co/roberta-base-openai-detector">modèle de classification de texte</a> détecte si le contenu est "vrai", probablement écrit par des humains, ou "faux", probablement écrit par une IA, en ajoutant la valeur "openai-detector.predicted_value" à chaque document.</p><p>Pipeline d'ingestion :</p>client.ingest.put_pipeline( 
    id="plagiarism-checker-pipeline",
    processors = [
    {
      "inference": { #for ml models - to infer against the data that is being ingested in the pipeline
        "model_id": "roberta-base-openai-detector", #text classification model id
        "target_field": "openai-detector", # Target field for the inference results
        "field_map": { #Maps the document field names to the known field names of the model.
        "abstract": "text_field" # Field matching our configured trained model input. 
        }
      }
    },
    {
      "inference": {
        "model_id": "sentence-transformers__all-mpnet-base-v2", #text embedding model id
        "target_field": "abstract_vector", # Target field for the inference results
        "field_map": {
        "abstract": "text_field" # Field matching our configured trained model input. Typically for NLP models, the field name is text_field.
        }
      }
    }
    
  ]
)
<p>Au moment de la requête, le même modèle d'intégration de texte est également utilisé pour générer la représentation vectorielle de la requête "texte_modèle" dans un objet "query_vector_builder".</p><p>Une recherche par k-voisins les plus proches (kNN) permet de trouver les k vecteurs les plus proches du vecteur de la requête, mesurés par la métrique de similarité.</p><p>Le _score de chaque document est dérivé de la similarité, ce qui garantit qu'un score plus élevé correspond à un meilleur classement. Cela signifie que le document est plus similaire d'un point de vue sémantique. Par conséquent, nous imprimons trois possibilités : si le score &gt; est de 0,9, nous considérons qu'il s'agit d'une "forte similarité" ; si &lt; est de 0,7, il s'agit d'une "faible similarité" ; sinon, il s'agit d'une "similarité modérée". Vous avez la possibilité de définir différentes valeurs seuils pour déterminer quel niveau de _score est considéré comme du plagiat ou non, en fonction de votre cas d'utilisation.</p><p>En outre, une classification du texte est effectuée afin de vérifier si la requête contient des éléments générés par l'IA.</p><p>Requête :</p>from elasticsearch import Elasticsearch
from elasticsearch.client import MlClient

#duplicated text - direct plagiarism test

model_text = 'Understanding and reasoning about cooking recipes is a fruitful research direction towards enabling machines to interpret procedural text. In this work, we introduce RecipeQA, a dataset for multimodal comprehension of cooking recipes. It comprises of approximately 20K instructional recipes with multiple modalities such as titles, descriptions and aligned set of images. With over 36K automatically generated question-answer pairs, we design a set of comprehension and reasoning tasks that require joint understanding of images and text, capturing the temporal flow of events and making sense of procedural knowledge. Our preliminary results indicate that RecipeQA will serve as a challenging test bed and an ideal benchmark for evaluating machine comprehension systems. The data and leaderboard are available at http://hucvl.github.io/recipeqa.'

response = client.search(index='plagiarism-checker', size=1,
    knn={
        "field": "abstract_vector.predicted_value",
        "k": 9,
        "num_candidates": 974,
        "query_vector_builder": { #The 'all-mpnet-base-v2' model is also employed to generate the vector representation of the query in a 'query_vector_builder' object.
            "text_embedding": {
                "model_id": "sentence-transformers__all-mpnet-base-v2",
                "model_text": model_text
            }
        }
    }
)

for hit in response['hits']['hits']:
    score = hit['_score']
    title = hit['_source']['title']
    abstract = hit['_source']['abstract']
    openai = hit['_source']['openai-detector']['predicted_value']
    url = hit['_source']['url']

    if score &gt; 0.9:
        print(f"\nHigh similarity detected! This might be plagiarism.")
        print(f"\nMost similar document: '{title}'\n\nAbstract: {abstract}\n\nurl: {url}\n\nScore:{score}\n\n")

        if openai == 'Fake':
            print("This document may have been created by AI.\n")

    elif score &lt; 0.7:
        print(f"\nLow similarity detected. This might not be plagiarism.")

        if openai == 'Fake':
            print("This document may have been created by AI.\n")

    else:
        print(f"\nModerate similarity detected.")
        print(f"\nMost similar document: '{title}'\n\nAbstract: {abstract}\n\nurl: {url}\n\nScore:{score}\n\n")

        if openai == 'Fake':
            print("This document may have been created by AI.\n")

ml_client = MlClient(client)

model_id = 'roberta-base-openai-detector' #open ai text classification model

document = [
    {
        "text_field": model_text
    }
]

ml_response = ml_client.infer_trained_model(model_id=model_id, docs=document)

predicted_value = ml_response['inference_results'][0]['predicted_value']

if predicted_value == 'Fake':
    print("\nNote: The text query you entered may have been generated by AI.\n")
<p>Sortie :</p>High similarity detected! This might be plagiarism.

Most similar document: 'RecipeQA: A Challenge Dataset for Multimodal Comprehension of Cooking Recipes'

Abstract: Understanding and reasoning about cooking recipes is a fruitful research direction towards enabling machines to interpret procedural text. In this work, we introduce RecipeQA, a dataset for multimodal comprehension of cooking recipes. It comprises of approximately 20K instructional recipes with multiple modalities such as titles, descriptions and aligned set of images. With over 36K automatically generated question-answer pairs, we design a set of comprehension and reasoning tasks that require joint understanding of images and text, capturing the temporal flow of events and making sense of procedural knowledge. Our preliminary results indicate that RecipeQA will serve as a challenging test bed and an ideal benchmark for evaluating machine comprehension systems. The data and leaderboard are available at[ http://hucvl.github.io/recipeqa](http://hucvl.github.io/recipeqa).

url:[http://aclweb.org/anthology/D18-1166](http://aclweb.org/anthology/D18-1166)

Score:1.0
<p>Dans cet exemple, après avoir utilisé l'une des valeurs "abstract" de notre ensemble de données comme requête textuelle "model_text", le plagiat a été identifié. Le score de similarité est de 1,0, ce qui indique un niveau élevé de similarité - <strong>un plagiat direct</strong>. La requête et le document vectorisés n'ont pas été reconnus comme des contenus générés par l'IA, ce qui était attendu.</p><p>Requête :</p>#similar text - paraphrase plagiarism test 

model_text = 'Comprehending and deducing information from culinary instructions represents a promising avenue for research aimed at empowering artificial intelligence to decipher step-by-step text. In this study, we present CuisineInquiry, a database for the multifaceted understanding of cooking guidelines. It encompasses a substantial number of informative recipes featuring various elements such as headings, explanations, and a matched assortment of visuals. Utilizing an extensive set of automatically crafted question-answer pairings, we formulate a series of tasks focusing on understanding and logic that necessitate a combined interpretation of visuals and written content. This involves capturing the sequential progression of events and extracting meaning from procedural expertise. Our initial findings suggest that CuisineInquiry is poised to function as a demanding experimental platform.'
<p>Sortie :</p>High similarity detected! This might be plagiarism.

Most similar document: 'RecipeQA: A Challenge Dataset for Multimodal Comprehension of Cooking Recipes'

Abstract: Understanding and reasoning about cooking recipes is a fruitful research direction towards enabling machines to interpret procedural text. In this work, we introduce RecipeQA, a dataset for multimodal comprehension of cooking recipes. It comprises of approximately 20K instructional recipes with multiple modalities such as titles, descriptions and aligned set of images. With over 36K automatically generated question-answer pairs, we design a set of comprehension and reasoning tasks that require joint understanding of images and text, capturing the temporal flow of events and making sense of procedural knowledge. Our preliminary results indicate that RecipeQA will serve as a challenging test bed and an ideal benchmark for evaluating machine comprehension systems. The data and leaderboard are available at[ http://hucvl.github.io/recipeqa](http://hucvl.github.io/recipeqa).

url:[http://aclweb.org/anthology/D18-1166](http://aclweb.org/anthology/D18-1166)

Score:0.9302529

Note: The text query you entered may have been generated by AI.
<p>En mettant à jour la requête "texte_modèle" avec un texte généré par l'IA qui transmet le même message tout en minimisant la répétition de mots similaires, la similarité détectée était toujours élevée, mais le score était de 0,9302529 au lieu de 1,0 - <strong>plagiat de paraphrase</strong>. On s'attendait également à ce que cette requête, générée par l'IA, soit détectée.</p><p>Enfin, si l'on considère la requête textuelle "model_text" comme un texte sur Elasticsearch, qui n'est pas un résumé de l'un de ces documents, la similarité détectée était de 0,68991005, ce qui indique une faible similarité selon les valeurs seuils considérées.</p><p>Requête :</p>#different text - not a plagiarism

model_text = 'Elasticsearch provides near real-time search and analytics for all types of data.'
<p>Sortie :</p>Low similarity detected. This might not be plagiarism.
<p>Bien que le plagiat ait été correctement identifié dans la requête de texte générée par l'IA, ainsi que dans les cas de paraphrase et de contenu directement copié, la navigation dans le paysage de la détection du plagiat implique la reconnaissance de divers aspects.</p><p>Dans le contexte de la détection de contenu généré par l'IA, nous avons exploré un modèle qui apporte une contribution précieuse. Toutefois, il est essentiel de reconnaître les limites inhérentes à la détection autonome, ce qui nécessite l'incorporation d'autres méthodes pour améliorer la précision.</p><p>La variabilité introduite par le choix des modèles d'intégration de texte est un autre élément à prendre en considération. Différents modèles, formés avec des ensembles de données distincts, aboutissent à des niveaux de similarité variables, ce qui souligne l'importance des enchâssements de texte générés.</p><p>Enfin, dans ces exemples, nous avons utilisé le résumé du document. Cependant, la détection du plagiat porte souvent sur des documents volumineux, ce qui rend essentielle la prise en compte de la longueur du texte. Il est fréquent que le texte dépasse la limite de jetons d'un modèle, ce qui nécessite une segmentation en morceaux avant de construire des embeddings. Une <a href="https://www.elastic.co/guide/en/elasticsearch/reference/8.11/knn-search.html#nested-knn-search">approche pratique</a> consiste à utiliser des structures imbriquées avec dense_vector.</p><h2>Conclusion</h2><p>Dans ce blog, nous avons abordé les défis liés à la détection du plagiat, en particulier dans les contenus paraphrasés et générés par l'IA, et comment la similarité textuelle sémantique et la classification des textes peuvent être utilisées à cette fin.</p><p>En combinant ces méthodes, nous avons fourni un exemple de détection du plagiat dans lequel nous avons réussi à identifier le contenu généré par l'IA, le plagiat direct et le plagiat paraphrastique.</p><p>L'objectif premier était d'établir un système de filtrage qui simplifie la détection, mais l'évaluation humaine reste essentielle pour la validation.</p><p>Si vous souhaitez en savoir plus sur la similarité textuelle sémantique et le NLP, nous vous encourageons à consulter les liens suivants :</p><ul><li><p><a href="https://www.elastic.co/what-is/semantic-search">Qu'est-ce que la recherche sémantique ?</a></p></li><li><p><a href="https://www.elastic.co/what-is/natural-language-processing">Qu'est-ce que le traitement du langage naturel (NLP) ?</a></p></li><li><p><a href="https://www.elastic.co/search-labs/blog/lexical-and-semantic-search-with-elasticsearch">Recherche lexicale et sémantique avec Elasticsearch</a></p></li><li><p><a href="https://www.elastic.co/search-labs/blog/chunking-via-ingest-pipelines">Chunking Large Documents via Ingest pipelines plus nested vectors equals easy passage search (en anglais)</a></p></li></ul>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/ai-plagiarism-checker-with-elasticsearch</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/ai-plagiarism-checker-with-elasticsearch</guid>
    <category><![CDATA[Base vectorielle]]></category>
    <category><![CDATA[Python]]></category>
    <dc:creator><![CDATA[Priscilla Parodi]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt68a5bc2434a9b03b/6a1711510e2e49a09641a22a/83e05cd4f81799fbb7b7950ed87600e825ec81e9-1024x1024.png" length="0" type="image/png"/>
    <pubDate>Tue, 19 Dec 2023 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Recherche lexicale et sémantique avec Elasticsearch]]></title>
    <description><![CDATA[Dans ce blog, nous allons explorer différentes approches de la recherche d'informations à l'aide d'Elasticsearch, en nous concentrant sur la recherche lexicale et sémantique.]]></description>
    <content:encoded><![CDATA[<p>La recherche est le processus de localisation des informations les plus pertinentes sur la base de votre requête de recherche ou de requêtes combinées, et les résultats de recherche pertinents sont les documents qui correspondent le mieux à ces requêtes. Bien qu'il existe plusieurs défis et méthodes associés à la recherche, l'objectif final reste le même : <strong>trouver la meilleure réponse possible à votre question</strong>.</p><p>Compte tenu de cet objectif, dans cet article de blog, nous allons explorer différentes approches de la recherche d'informations à l'aide d'Elasticsearch, en nous concentrant plus particulièrement sur la recherche de texte : <strong>recherche lexicale et sémantique</strong>.</p><h2>Produits requis</h2><p>Pour ce faire, nous fournirons des exemples Python qui démontrent divers scénarios de recherche sur un <a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/lexical-and-semantic-search-with-elasticsearch/products-ecommerce.json">ensemble de données</a> généré pour simuler des informations sur des produits de commerce électronique.</p><p>Cet ensemble de données contient plus de 2 500 produits, chacun accompagné d'une description. Ces produits sont classés en 76 catégories de produits distinctes, chaque catégorie contenant un nombre variable de produits, comme indiqué ci-dessous :</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt34415151c00ced2b/6a17d8710b0bed6c9bdd342c/4104466050f3024b6bcaf382da2a702650f62227-1440x708.png" alt="" /><p><em>Visualisation du treemap - les 22 premières valeurs de category.keyword (catégories de produits)</em></p><p>Pour l'installation, vous aurez besoin de</p><ul><li><p>Python 3.6 ou version ultérieure</p></li><li><p>Le <a href="https://www.elastic.co/guide/en/elasticsearch/client/python-api/current/index.html">client Elastic Python</a></p></li><li><p>Déploiement d'Elastic 8.8 ou d'une version ultérieure, avec un nœud d'apprentissage automatique de 8 Go de mémoire</p></li><li><p>Le modèle <a href="https://www.elastic.co/guide/en/machine-learning/8.9/ml-nlp-elser.html">Elastic Learned Sparse EncodeR</a> qui est préchargé dans Elastic a été installé et démarré sur votre déploiement.</p></li></ul><p>Nous utiliserons Elastic Cloud, une <a href="https://cloud.elastic.co/registration?onboarding_token=vectorsearch&amp;cta=cloud-registration&amp;tech=trial&amp;plcmt=article%20content&amp;pg=search-labs">version d'essai gratuite est disponible</a>.</p><p>Outre les requêtes de recherche présentées dans cet article de blog, un <a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/lexical-and-semantic-search-with-elasticsearch/ecommerce_dense_sparse_project.ipynb">carnet Python</a> vous guidera dans les processus suivants :</p><ul><li><p>Établir une connexion à notre déploiement Elastic en utilisant le client Python</p></li><li><p>Charger un modèle d'intégration de texte dans le cluster Elasticsearch</p></li><li><p>Créer un index avec des correspondances pour indexer les vecteurs de caractéristiques et les vecteurs denses.</p></li><li><p>Créer un pipeline d'ingestion avec des processeurs d'inférence pour l'intégration et l'expansion du texte</p></li></ul><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt0e95406ece8f08d9/6a17d8731d1b83d32e93e2f4/54a9a490a3b0cf1b9c2228bee8eddd3f566bd435-1418x1102.png" alt="" /><h2>Recherche lexicale - recherche éparse</h2><p>La manière classique dont les documents sont classés en fonction de leur pertinence par Elasticsearch sur la base d'une requête textuelle utilise l'implémentation Lucene du modèle <a href="https://en.wikipedia.org/wiki/Okapi_BM25">BM25</a>, un <strong>modèle clairsemé pour la recherche lexicale</strong>. Cette méthode suit l'approche traditionnelle de la recherche de texte, en recherchant des correspondances de termes exacts.</p><p>Pour rendre cette recherche possible, Elasticsearch convertit les données des <strong>champs de texte</strong> en un format de recherche en effectuant une analyse de texte.</p><p>L'<strong>analyse de texte</strong> est effectuée par un<a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/analyzer-anatomy.html"> analyseur</a>, un ensemble de règles régissant le processus d'extraction des mots-clés pertinents pour la recherche. Un analyseur doit avoir exactement un<a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-tokenizers.html"> tokenizer</a>. Le tokenizer reçoit un flux de caractères et le décompose en tokens individuels (généralement des mots individuels), comme dans l'exemple ci-dessous :</p><h3>Balisage des chaînes de caractères pour la recherche lexicale</h3>#Performs text analysis on a string and returns the resulting tokens.

# Define the text to be analyzed
text = "Comfortable furniture for a large balcony"

# Define the analyze request
request_body = {
  "analyzer": "standard",
  "text": text
}

# Perform the analyze request
response = client.indices.analyze(analyzer=request_body["analyzer"], text=request_body["text"])

# Extract and display the analyzed tokens
tokens = [token["token"] for token in response["tokens"]]
print("Analyzed Tokens:", tokens)
<p>Sortie</p>Analyzed Tokens: ['comfortable', 'furniture', 'for', 'a', 'large', 'balcony']
<p>Dans cet exemple, nous utilisons l'analyseur par défaut, l'analyseur <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-standard-analyzer.html">standard</a>, qui fonctionne bien pour la plupart des cas d'utilisation car il fournit une tokenisation basée sur la grammaire anglaise. La tokenisation permet de faire correspondre des termes individuels, mais chaque token est toujours mis en correspondance de manière littérale.</p><p>Si vous souhaitez personnaliser votre expérience de recherche, vous pouvez choisir un autre<a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-analyzers.html"> analyseur intégré</a>. Par exemple, en mettant à jour le code pour utiliser l'<a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-stop-analyzer.html">analyseur de mots v</a> ides, il décomposera le texte en jetons à partir de tout caractère autre qu'une lettre, avec la possibilité de supprimer les mots vides.</p>...
# Define the analyze request
request_body = {
  "analyzer": "stop",
  "text": text
}
...
<p>Sortie</p>Analyzed Tokens: ['comfortable', 'furniture', 'large', 'balcony']
<p>Lorsque les analyseurs intégrés ne répondent pas à vos besoins, vous pouvez créer un <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-custom-analyzer.html">analyseur personnalisé</a>, qui utilise la combinaison appropriée de zéro ou plusieurs <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-charfilters.html">filtres de caractères</a>, un <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-tokenizers.html">tokenizer</a> et zéro ou plusieurs <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-tokenfilters.html">filtres de jetons</a>.</p>"analyzer":  {

  "my_analyzer": {

    "type": "custom", #For custom analyzers, use a type of custom or omit the type parameter.

    "tokenizer": "standard", #Built-in or customized tokenizer

    "filter": ["lowercase", "synonym"] #Built-in or customized token filters
  }
}
<p>Dans l'exemple ci-dessus, qui combine un tokenizer et des filtres de jetons, le texte sera mis en minuscules par le <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-lowercase-tokenfilter.html">filtre de minuscules</a> avant d'être traité par le <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-synonym-tokenfilter.html#:~:text=Elasticsearch%20will%20use%20the%20token,applied%20to%20the%20synonym%20entries.">filtre de jetons de synonymes</a>.</p><h2>Correspondance lexicale</h2><p><a href="https://www.elastic.co/blog/practical-bm25-part-2-the-bm25-algorithm-and-its-variables">BM25</a> mesurera la pertinence des documents par rapport à une requête de recherche donnée sur la base de la fréquence des termes et de leur importance.</p><p>Le code ci-dessous effectue une recherche de <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-match-query.html">correspondance</a>, recherchant jusqu'à deux documents en tenant compte des valeurs du champ <em>"description"</em> de l'index <em>"ecommerce-search"</em> et de la requête de recherche. <strong>"</strong><em><strong>Meubles confortables pour un grand balcon</strong></em><strong>"</strong>.</p><p>L'affinement des critères pour qu'un document soit considéré comme correspondant à cette requête peut améliorer la précision. Cependant, les résultats plus spécifiques sont assortis d'une plus faible tolérance aux variations.</p># BM25

response = client.search(size=2,
index="ecommerce-search",
query= {
  "match": {
    "description" : {  
      "query": "Comfortable furniture for a large balcony",
      "analyzer": "stop"
    }
  }
}
)

hits = response['hits']['hits']

if not hits:
  print("No matches found")

else:
  for hit in hits:
    score = hit['_score']
    product = hit['_source']['product']
    category = hit['_source']['category']
    description = hit['_source']['description']
    print(f"\nScore: {score}\nProduct: {product}\nCategory: {category}\nDescription: {description}\n")
<p>Sortie</p>Score: 15.607948
Product: Barbie Dreamhouse
Category: Toys
Description: is a classic Barbie playset with multiple rooms, furniture, a large balcony, a pool, and accessories. It allows kids to create their dream Barbie world.

Score: 9.137739
Product: Comfortable Rocking Chair
Category: Indoor Furniture
Description: enjoy relaxing moments with this comfortable rocking chair. Its smooth motion and cushioned seat make it an ideal piece of furniture for unwinding.
<p>En analysant le résultat, le résultat le plus pertinent est le produit "<em>Barbie Dreamhouse</em>", dans la catégorie "<em>Toys</em>", et sa description est très pertinente car elle inclut les termes "<em>furniture</em>", "<em>large"</em> et <em>"balcony</em>", c'est le seul produit avec 3 termes dans la description qui correspondent à la requête de recherche, le produit est également le seul avec le terme <em>"balcony"</em> dans la description.</p><p>Le deuxième produit le plus pertinent est un "<em>Comfortable Rocking Chair</em>" catégorisé en "<em>Indoor Furniture</em>" et sa description inclut les termes "<em>comfortable</em>" et "<em>furniture</em>". Seuls 3 produits dans l'ensemble de données correspondent à au moins 2 termes de cette requête de recherche, et ce produit est l'un d'entre eux.</p><p><em>"Comfortable"</em> apparaît dans la description de 105 produits et <em>"furniture"</em> dans la description de 4 produits avec 4 catégories différentes : <em>Jouets</em>, <em>Mobilier d'intérieur, Mobilier d'extérieur et 'Dog and Cat Supplies &amp; Toys'.</em></p><p>Comme vous pouvez le constater, le produit le plus pertinent par rapport à la requête est un jouet et le deuxième produit le plus pertinent est un meuble d'intérieur. Si vous souhaitez obtenir des informations détaillées sur le calcul du score afin de savoir pourquoi ces documents correspondent, vous pouvez attribuer la valeur true au paramètre <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/search-explain.html"><em>explain</em></a> __query.</p><p>Bien que les deux résultats soient les plus pertinents, compte tenu à la fois du nombre de documents et de l'occurrence des termes dans cet ensemble de données, l'intention derrière la requête "<em>Meubles confortables pour un grand</em>balcon" est de rechercher des meubles pour un grand balcon réel, à l'exclusion, entre autres, des jouets et des meubles d'intérieur.</p><p>La recherche lexicale est relativement <strong>simple et rapide</strong>, mais elle présente des limites car il n'est pas toujours possible de connaître tous les termes et synonymes possibles sans nécessairement connaître l'intention et les requêtes de l'utilisateur. Un phénomène courant dans l'utilisation du langage naturel est l'<strong>inadéquation du vocabulaire</strong>. <a href="https://dl.acm.org/doi/abs/10.1145/32206.32212">% L es recherches</a> montrent qu'en moyenne, dans<strong>80 % des cas,</strong> des personnes différentes (experts dans le même domaine) nomment la même chose différemment.</p><p>Ces limites nous incitent à rechercher d'autres modèles de notation qui intègrent des connaissances sémantiques. Les modèles basés sur des transformateurs, qui excellent dans le traitement de jetons d'entrée séquentiels tels que le langage naturel, capturent le sens sous-jacent de votre recherche en prenant en compte les représentations mathématiques des documents et des requêtes. Cela permet une représentation vectorielle dense et contextuelle du texte, qui alimente la <strong>recherche sémantique</strong>, un moyen raffiné de trouver du contenu pertinent.</p><h2>Recherche sémantique - recherche dense</h2><p>Dans ce contexte, après avoir converti vos données en valeurs vectorielles significatives, l'algorithme de recherche <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/knn-search.html">k-nearest neighbor (kNN</a> ) est utilisé pour trouver les représentations vectorielles dans un ensemble de données qui sont les plus similaires à un vecteur d'interrogation. Elasticsearch prend en charge deux méthodes de recherche kNN, le <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/knn-search.html#exact-knn">kNN exact par force brute</a> et le <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/knn-search.html#approximate-knn">kNN approximatif</a>, également connu sous le nom d'ANN.</p><p>Le kNN brutal garantit des résultats précis, mais ne s'adapte pas bien aux grands ensembles de données. Le kNN approximatif trouve efficacement les voisins les plus proches approximatifs en sacrifiant une partie de la précision pour améliorer les performances.</p><p>Grâce à la prise en charge par Lucene de la recherche kNN et des index vectoriels denses, Elasticsearch tire parti de l'algorithme Hierarchical Navigable Small World (HNSW), qui démontre d'excellentes performances de recherche sur une variété d'<a href="http://ann-benchmarks.com/">ensembles de données de référence.</a> Une recherche kNN approximative peut être effectuée en Python à l'aide du code d'exemple ci-dessous.</p><h3>Recherche sémantique avec kNN approximatif</h3># KNN - approximate kNN

response = client.search(index='ecommerce-search', size=2,
knn={
  "field": "description_vector.predicted_value",
  "k": 50, # Number of nearest neighbors to return as top hits.
#The optimal value of k is dependent on the data. It can vary in different scenarios.

  "num_candidates": 500, # Number of nearest neighbor candidates to consider per shard.

#Increasing num_candidates tends to improve the accuracy of the final k results.

  "query_vector_builder": { # Object indicating how to build a query_vector. kNN search enables you to perform semantic search by using a previously deployed text embedding model, the steps for this process are demonstrated in the Python notebook.
    "text_embedding": { 
      "model_id": "sentence-transformers__all-mpnet-base-v2", # Text embedding model id
      "model_text": "Comfortable furniture for a large balcony" # Query
    }
  }
}
)

for hit in response['hits']['hits']:
        
  score = hit['_score']
  product = hit['_source']['product']
  category = hit['_source']['category']
  description = hit['_source']['description']
  print(f"\nScore: {score}\nProduct: {product}\nCategory: {category}\nDescription: {description}\n")
<p>Ce bloc de code utilise le kNN d'Elasticsearch pour renvoyer jusqu'à deux produits dont la description est similaire à la requête vectorisée (query_vector_build) de "<em>Meubles confortables pour un grand balcon</em>" en tenant compte de l'intégration du champ "<em>description</em>" dans l'ensemble de données sur les produits.</p><p>Les encastrements de produits ont été préalablement générés dans un pipeline d'ingestion avec un processeur d'inférence contenant le logiciel <em>"</em><a href="https://huggingface.co/sentence-transformers/all-mpnet-base-v2"><em>all-mpnet-base-v2</em></a><em>"</em> pour déduire les données qui étaient ingérées dans le pipeline.</p><p>Ce modèle a été choisi sur la base de l'évaluation des modèles pré-entraînés à l'aide de <em>"</em><a href="https://github.com/UKPLab/sentence-transformers/blob/master/docs/package_reference/sentence_transformer/evaluation.md"><em>sentence_transformers.evaluation</em></a><em>"</em> où différentes classes sont utilisées pour évaluer un modèle pendant la formation. Le modèle "all-mpnet-base-v2" a démontré la meilleure performance moyenne selon le <a href="https://www.sbert.net/docs/pretrained_models.html">classement Sentence-Transformers</a> et a également obtenu une position favorable sur le <a href="https://huggingface.co/spaces/mteb/leaderboard">Massive Text Embedding Benchmark (MTEB)</a> Leaderboard. Le modèle pré-entraîné<a href="https://huggingface.co/microsoft/mpnet-base"> microsoft/mpnet-base</a> et affiné sur un ensemble de données de 1B paires de phrases, il cartographie les phrases dans un espace vectoriel dense de 768 dimensions.</p><p>Par ailleurs, il existe de nombreux autres modèles qui peuvent être utilisés, en particulier ceux qui sont adaptés aux données spécifiques à votre domaine.</p><p>Sortie</p>Score: 0.79207325
Product: Patio Sofa Set with Ottoman
Category: Outdoor Furniture
Description: is a versatile and comfortable patio sofa set, including a sofa, ottoman, and coffee table, great for outdoor lounging.

Score: 0.7836937
Product: Patio Sofa Set with Canopy
Category: Outdoor Furniture
Description: is a luxurious and comfortable patio sofa set with a canopy, providing shade and style for outdoor lounging.
<p><em>Le résultat peut varier en fonction du modèle choisi, des</em> <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/knn-search.html#knn-search-filter-example"><em>filtres</em></a> <em>et de la</em> <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/knn-search.html#tune-approximate-knn-for-speed-accuracy"><em>syntonisation approximative de kNN</em></a><em>.</em></p><p>Les résultats de la recherche kNN se trouvent tous deux dans la catégorie "<em>Outdoor Furniture</em>", même si le mot "<em>outdoor</em>" n'a pas été explicitement mentionné dans la requête, ce qui souligne l'importance de la compréhension sémantique dans le contexte.</p><p>La recherche vectorielle dense offre plusieurs avantages :</p><ul><li><p>Permettre la recherche sémantique</p></li><li><p>Évolutivité pour traiter de très grands ensembles de données</p></li><li><p>Flexibilité pour traiter un large éventail de types de données</p></li></ul><p>Cependant, la <strong>recherche vectorielle dense présente également ses propres difficultés :</strong></p><ul><li><p>Choisir le bon modèle d'intégration pour votre cas d'utilisation</p></li><li><p>Une fois le modèle choisi, il peut être nécessaire de l'affiner pour optimiser les performances sur un ensemble de données spécifiques au domaine, un processus qui nécessite la participation d'experts du domaine.</p></li><li><p>En outre, l'indexation de vecteurs à haute dimension peut être très coûteuse sur le plan informatique</p></li></ul><h2>Recherche sémantique - récupération de données éparses apprises</h2><p>Explorons une autre approche : la récupération éparse apprise, une autre façon d'effectuer une recherche sémantique.</p><p>En tant que modèle clairsemé, il utilise l'index inversé basé sur Lucene d'Elasticsearch, qui bénéficie de décennies d'optimisations. Toutefois, cette approche va au-delà du simple ajout de synonymes avec des fonctions de notation lexicale telles que BM25. Au lieu de cela, il incorpore des associations apprises en utilisant une connaissance plus approfondie de la langue pour optimiser la pertinence.</p><p>En élargissant les requêtes de recherche pour inclure des termes pertinents qui ne sont pas présents dans la requête originale, l' encodeur de vecteurs <a href="https://www.elastic.co/guide/en/machine-learning/current/ml-nlp-elser.html">épar s Elastic Learned Sparse Encoder</a> <strong>améliore les encastrements de vecteurs</strong> épars, comme vous pouvez le voir dans l'exemple ci-dessous.</p><h3>Recherche de vecteurs épars à l'aide d'un codeur épars à apprentissage élastique</h3># Elastic Learned Sparse Encoder

response = client.search(index='ecommerce-search', size=2,
query={
  "text_expansion": {
    "ml.tokens": {
      "model_id":"elser_model",
      "model_text":"Comfortable furniture for a large balcony"                
    }
  }
}
)

for hit in response['hits']['hits']:

  score = hit['_score']
  product = hit['_source']['product']
  category = hit['_source']['category']
  description = hit['_source']['description']
  print(f"\nScore: {score}\nProduct: {product}\nCategory: {category}\nDescription: {description}\n")
<p>Sortie</p>Score: 14.405318
Product: Garden Lounge Set with Side Table
Category: Garden Furniture
Description: is a comfortable and stylish garden lounge set, including a sofa, chairs, and a side table for outdoor relaxation.

Score: 14.281318
Product: Rattan Patio Conversation Set
Category: Outdoor Furniture
Description: is a stylish and comfortable outdoor furniture set, including a sofa, two chairs, and a coffee table, all made of durable rattan material.
<p>Les résultats dans ce cas incluent la catégorie "<em>Garden Furniture</em>", qui offre des produits assez similaires à "<em>Outdoor Furniture</em>".</p><p>En analysant "ml.tokens", le champ "rank_features" contenant les tokens générés par Learned Sparse Retrieval, il devient évident que parmi les différents tokens générés, il y a des termes qui, bien que ne faisant pas partie de la requête de recherche, sont toujours pertinents dans leur signification, tels que "<em>relax</em>" (confortable), "<em>sofa</em>" (meuble) et "<em>outdoor</em>" (balcon).</p><p>L'image ci-dessous présente certains de ces termes en regard de la requête, avec et sans expansion des termes.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt7e869985347b19a7/6a17d875e31791dc2c2d56a1/dd86607fce6137d843a3ec002390eaa988b432f9-1440x502.png" alt="" /><p>Comme nous l'avons observé, ce modèle permet une recherche contextuelle et contribue à atténuer le problème d'inadéquation du vocabulaire tout en fournissant des résultats plus faciles à interpréter. Il peut même être plus performant que les modèles à vecteurs denses lorsqu'aucun recyclage spécifique au domaine n'est appliqué.</p><h2>Recherche hybride : résultats pertinents grâce à la combinaison de la recherche lexicale et sémantique</h2><p>En matière de recherche, il n'existe pas de solution universelle. Chacune de ces méthodes de recherche a ses avantages, mais aussi ses inconvénients. En fonction du cas d'utilisation, la meilleure option peut changer. Souvent, les meilleurs résultats obtenus par les différentes méthodes de recherche peuvent être complémentaires. C'est pourquoi, pour améliorer la pertinence, nous chercherons à combiner les points forts de chaque méthode.</p><p>Il existe plusieurs façons de mettre en œuvre une <strong>recherche hybride</strong>, notamment la combinaison linéaire, en attribuant un poids à chaque score, et la fusion des rangs réciproques (RRF), où il n'est pas nécessaire de spécifier un poids.</p><h3>Elasticsearch : le meilleur des deux mondes avec la recherche lexicale et sémantique</h3># BM25 + Elastic Learned Sparse Encoder (Linear Combination)

response = client.search(index='ecommerce-search', size=2,

query= {
  "bool": {
    "should": [
    {
      "match": {
        "description" : {  
          "query": "A dining table and comfortable chairs for a large balcony",
          "boost": 1
        }
      }
    },                   
    {
      "text_expansion": {
        "ml.tokens": {
          "model_id": "elser_model",
          "model_text": "A dining table and comfortable chairs for a large balcony",
          "boost": 1
        }
      }
     }
    ]
  }
}
)

# The boost value is 1 for the text expansion and match query. This means that the relevance score of the results of these queries are not boosted. You can specify a boost value to give a weight to each score in the sum. The scores will be calculated as: score = boost value * match_score + boost value * text_expansion_score

for hit in response['hits']['hits']:

  score = hit['_score']
  product = hit['_source']['product']
  category = hit['_source']['category']
  description = hit['_source']['description']
  print(f"\nScore: {score}\nProduct: {product}\nCategory: {category}\nDescription: {description}\n")
<p>Dans ce code, nous avons effectué une recherche hybride avec deux requêtes ayant la valeur " Une<em>table à manger et des chaises confortables pour un grand balcon</em>". Au lieu d'utiliser "<em>furniture</em>" comme terme de recherche, nous spécifions ce que nous recherchons, et les deux recherches prennent en compte les mêmes valeurs de champ, "description". Le classement est déterminé par une combinaison linéaire avec un poids égal pour les notes BM25 et ELSER.</p><p>Sortie</p>Score: 31.628141
Product: Garden Dining Set with Swivel Rockers
Category: Garden Furniture
Description: is a functional and comfortable garden dining set, including a table and chairs with swivel rockers for easy movement.

Score: 31.334227
Product: Garden Dining Set with Swivel Chairs
Category: Garden Furniture
Description: is a functional and comfortable garden dining set, including a table and chairs with swivel seats for convenience.
<p>Dans le code ci-dessous, nous utiliserons la même valeur pour la requête, mais nous combinerons les scores de BM25 (paramètre de requête) et de kNN (paramètre knn) en utilisant la méthode de fusion par rang réciproque pour combiner et classer les documents.</p># BM25 + KNN (RRF)

response = client.search(index='ecommerce-search', size=2,
query={
  "bool": {
    "should": [
    {
      "match": {
        "description": {
        "query": "A dining table and comfortable chairs for a large balcony"
        }
      }
    }
    ]
  }
},
knn={
  "field": "description_vector.predicted_value",
  "k": 50,
  "num_candidates": 500,
  "query_vector_builder": {
    "text_embedding": {
      "model_id": "sentence-transformers__all-mpnet-base-v2",
      "model_text": "A dining table and comfortable chairs for a large balcony"
    }
  }
},
rank={
  "rrf": { # Reciprocal rank fusion
    "window_size": 50, # This value determines the size of the individual result sets per query.
    "rank_constant": 20 # This value determines how much influence documents in individual result sets per query have over the final ranked result set.
  }
}
)

for hit in response['hits']['hits']:
        
  rank = hit['_rank']
  category = hit['_source']['category']
  product = hit['_source']['product']
  description = hit['_source']['description']
  print(f"\nRank: {rank}\nProduct: {product}\nCategory: {category}\nDescription: {description}\n")
<p><em>La fonctionnalité RRF est en avant-première technique. La syntaxe sera probablement modifiée avant l'AG.</em></p><p>Sortie</p>Rank: 1
Product: Patio Dining Set with Bench
Category: Outdoor Furniture
Description: is a spacious and functional patio dining set, including a dining table, chairs, and a bench for additional seating.

Rank: 2
Product: Garden Dining Set with Swivel Chairs
Category: Garden Furniture
Description: is a functional and comfortable garden dining set, including a table and chairs with swivel seats for convenience.
<p>Ici, nous pourrions également utiliser différents champs et valeurs ; certains de ces exemples sont disponibles dans le <a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/lexical-and-semantic-search-with-elasticsearch/ecommerce_dense_sparse_project.ipynb">Python notebook</a>.</p><p>Comme vous pouvez le constater, Elasticsearch vous offre le meilleur des deux mondes : la recherche lexicale traditionnelle et la recherche vectorielle, qu'elle soit éparse ou dense, pour atteindre votre objectif <strong>et trouver la meilleure réponse possible à votre question.</strong></p><p>Si vous souhaitez continuer à vous familiariser avec les approches mentionnées ici, ces blogs peuvent vous être utiles :</p><ul><li><p><a href="https://www.elastic.co/blog/improving-information-retrieval-elastic-stack-hybrid">Améliorer la recherche d'informations dans la pile élastique : Recherche hybride</a></p></li><li><p><a href="https://www.elastic.co/blog/vector-search-elasticsearch-rationale">Recherche vectorielle dans Elasticsearch : La raison d'être de la conception</a></p></li><li><p><a href="https://www.elastic.co/blog/lexical-ai-powered-search-elastic-vector-database">Comment obtenir le meilleur de la recherche lexicale et de la recherche alimentée par l'IA avec la base de données vectorielle d'Elastic ?</a></p></li><li><p><a href="https://www.elastic.co/blog/may-2023-launch-sparse-encoder-ai-model">Présentation d'Elastic Learned Sparse Encoder : Le modèle d'IA d'Elastic pour la recherche sémantique</a></p></li><li><p><a href="https://www.elastic.co/blog/may-2023-launch-information-retrieval-elasticsearch-ai-model">Améliorer la recherche d'information dans la pile Elastic : Présentation d'Elastic Learned Sparse Encoder, notre nouveau modèle de recherche d'information</a></p></li></ul><p>Elasticsearch fournit une base de données vectorielle, ainsi que tous les outils dont vous avez besoin pour construire une recherche vectorielle :</p><ul><li><p><a href="https://www.elastic.co/elasticsearch/vector-database">Base de données vectorielle</a>Elasticsearch</p></li><li><p>Cas d'utilisation de la <a href="https://www.elastic.co/enterprise-search/vector-search">recherche vectorielle</a> avec Elastic</p></li></ul><h2>Conclusion</h2><p>Dans ce billet de blog, nous avons exploré différentes approches de la recherche d'informations à l'aide d'Elasticsearch, en nous concentrant plus particulièrement sur la recherche textuelle, lexicale et sémantique. Pour le démontrer, nous avons fourni des exemples Python illustrant différents scénarios de recherche à l'aide d'un ensemble de données contenant des informations sur des produits de commerce électronique.</p><p>Nous avons passé en revue la recherche lexicale classique avec BM25 et discuté de ses avantages et de ses défis, tels que l'inadéquation du vocabulaire. Nous avons souligné l'importance d'intégrer des connaissances sémantiques pour résoudre ce problème. En outre, nous avons abordé la recherche vectorielle dense, qui permet une recherche sémantique, et les défis associés à cette méthode de recherche, notamment le coût de calcul lors de l'indexation de vecteurs à haute dimension.</p><p>D'autre part, nous avons mentionné que les vecteurs épars se compriment exceptionnellement bien. Ainsi, nous avons discuté de l'encodeur Learned Sparse Encoder d'Elastic, qui étend les requêtes de recherche pour inclure des termes pertinents qui ne sont pas présents dans la requête originale.</p><p>Il n'existe pas de solution unique en matière de recherche. Chaque méthode de recherche a ses avantages et ses inconvénients. C'est pourquoi nous avons également abordé le concept de recherche hybride.</p><p>Comme vous pouvez le constater, avec Elasticsearch, vous pouvez bénéficier du meilleur des deux mondes : la recherche lexicale traditionnelle et la recherche vectorielle !</p><p>Prêt à commencer ? Consultez le <a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/lexical-and-semantic-search-with-elasticsearch/ecommerce_dense_sparse_project.ipynb">cahier Python</a> disponible et commencez un <a href="https://cloud.elastic.co/registration?onboarding_token=vectorsearch&amp;cta=cloud-registration&amp;tech=trial&amp;plcmt=article%20content&amp;pg=search-labs">essai gratuit d'Elastic Cloud.</a></p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/lexical-and-semantic-search-with-elasticsearch</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/lexical-and-semantic-search-with-elasticsearch</guid>
    <category><![CDATA[Base vectorielle]]></category>
    <category><![CDATA[Python]]></category>
    <category><![CDATA[Langages de requête]]></category>
    <dc:creator><![CDATA[Priscilla Parodi]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltbd7e961f594005e7/6a17d80f033c8d981f6bb009/d240bfef29e9d432069059b312dd044eb76eec6c-1440x840.png" length="0" type="image/png"/>
    <pubDate>Tue, 03 Oct 2023 00:00:00 GMT</pubDate>
  </item>
  </channel>
</rss>