<?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/cn/search-labs/blog/category/python-programming</link>
    </image>
    <link>https://www.elastic.co/cn/search-labs/blog/category/python-programming</link>
    <atom:link href="https://www.elastic.co/cn/search-labs/rss/category/python-programming.xml" rel="self" type="application/rss+xml"/>
    <language><![CDATA[cn]]></language>
    <lastBuildDate>Sun, 11 Oct 2026 18:43:40 GMT</lastBuildDate>
  <item>
    <title><![CDATA[利用 Elasticsearch 和 SigLIP-2 对山峰进行多模式搜索 ]]></title>
    <description><![CDATA[了解如何使用 SigLIP-2 嵌入和 Elasticsearch kNN 向量搜索实现文本到图像和图像到图像的多模态搜索。项目重点：寻找珠峰徒步旅行中拍摄的阿玛达布拉姆峰照片。]]></description>
    <content:encoded><![CDATA[<p>您是否曾想过按含义搜索相册？试着询问 "给我看我穿着蓝色夹克坐在长椅上的照片"、"给我看珠穆朗玛峰的照片 "或 "清酒和寿司"。喝杯咖啡（或您最喜欢的饮料），继续阅读。在本博客中，我们将向您展示如何构建多模态混合搜索应用程序。多模态是指应用程序可以理解和搜索不同类型的输入（文本、图像和音频），而不仅仅是文字。混合式意味着它结合了关键词匹配、kNN 向量搜索和地理围栏等技术，以提供更清晰的结果。</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltdfa1ec1ccd450e94/6a17da751d1b8308ee93e344/0ec6bbb45013846b59ee00d2bf73ee2182ee7392-1920x1080.gif" alt="来自珠穆朗玛峰徒步旅行的不同山峰照片库。" /><p>为此，我们使用谷歌的 SigLIP-2 为图像和文本生成矢量嵌入，并将其存储在 Elasticsearch 矢量数据库中。在查询时，我们将搜索输入（文本或图像）转换为嵌入，并运行快速的 kNN 向量搜索来检索结果。这种设置可实现高效的文本到图像和图像到图像搜索。Streamlit 用户界面为我们提供了一个前端，不仅可以进行基于文本的搜索，从相册中查找并查看匹配的照片，还可以从上传的图片中识别山峰，并查看相册中该山峰的其他照片，从而使该项目栩栩如生。我们还介绍了为提高搜索准确性而采取的措施，以及实用技巧和窍门。为便于进一步探索，我们提供了<a href="https://github.com/navneet83/multimodal-mountain-peak-search">GitHub 存储库</a>和<a href="https://github.com/navneet83/multimodal-mountain-peak-search/blob/main/notebooks/multimodal_mountain_peak_search.ipynb">Colab 笔记本</a>。</p><h2>如何开始</h2><p>这篇博文的灵感来自于一个 10 岁的孩子，他让我给他们看我在珠峰大本营徒步旅行时拍摄的阿玛达布拉姆山的所有照片。在翻阅相册时，我还被要求辨认其他几座山峰，其中一些我还叫不出名字。</p><p>这让我想到，这可以成为一个有趣的计算机视觉项目。我们的目标</p><ul><li><p>按名称查找山峰图片</p></li><li><p>从图片中猜测山峰名称，并在相册中找到类似的山峰</p></li><li><p>让概念查询发挥作用<em>（人</em>、<em>河流</em>、<em>祈祷旗</em> <em>等）</em></p></li></ul><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltf82df9d7005fc3fe/6a17da78abe0f2e77bdfe8b9/e9d0d720a9b565d5b749bdc915068852d4f157ad-1200x1600.png" alt="阿玛-达布拉姆山 " /><h2>组建梦之队：SigLIP-2、Elasticsearch&amp; Streamlit</h2><p>很快我们就发现，要想实现这一目标，我们需要将文字（"阿玛达布拉姆"）和图像（我相册中的照片）都转化为可以进行有意义比较的矢量，即在同一个矢量空间中。一旦我们做到了这一点，搜索就只是 "寻找最近的邻居"。</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt5f80b69a9d5bd28a/6a17da7a4b055ddd1243209e/20e6f8b7d4fa48414f407ec200adbe00ee28d517-1536x1024.png" alt="SigLIP-2、Elasticsearch&amp; Streamlit--梦之队。" /><p>为了生成图像嵌入，我们使用了多语言<a href="https://huggingface.co/blog/vlms-2025"> 视觉语言编码器</a>，因此山峰的照片和 "Ama Dablam "这样的短语会出现在同一个向量空间中。</p><p>谷歌最近发布的<a href="https://huggingface.co/blog/siglip2"><strong>SigLIP-2</strong></a> 在这方面非常适合。它可以在没有特定任务训练的情况下生成嵌入式（<strong>零镜头</strong>设置），并能很好地适用于我们的使用案例：未标记的照片和具有不同名称和语言的山峰。由于它是针对文本与图像匹配进行训练的，因此即使查询语言或拼写不同，徒步旅行中的山峰图片和简短的文字提示最终也能接近嵌入。</p><p>SigLIP-2 在质量与速度之间实现了很好的平衡，支持多种输入分辨率，并可在 CPU 和 GPU 上运行。SigLIP-2 在设计上比以前的型号（如最初的 CLIP）更适合户外拍摄。在我们的测试中，SigLIP-2 始终能生成可靠的结果。此外，它还得到了很好的支持，因此是本项目的不二之选。</p><p>接下来，我们需要一个向量数据库来存储嵌入和强力搜索。它不仅应支持对图像嵌入进行余弦 kNN 搜索，还应在单个查询中应用地理围栏和文本过滤器。Elasticsearch 在这方面非常适合：它能很好地处理向量（在 dense_vector 字段上使用 HNSW kNN），支持结合文本、向量和地理查询的混合搜索，并提供开箱即用的过滤和排序功能。它还可以横向扩展，因此很容易从少量照片扩展到数千张照片。<a href="https://www.elastic.co/docs/reference/elasticsearch/clients/python"></a>最后，我们需要一个轻量级前端，以便输入搜索查询并查看结果。对于基于 Python 的快速演示，Streamlit 非常适合。它提供了我们所需的基本功能--文件上传、响应式图像网格以及用于排序和地理围栏的下拉菜单。它很容易克隆并在本地运行，也可以在 Colab 笔记本中使用。</p><h2>实施</h2><h3>Elasticsearch 索引设计和索引策略</h3><p>我们将在这个项目中使用两个索引：<code>peaks_catalog</code> 和<code>photos</code> 。</p><h4>峰值_目录索引</h4><p>该索引是珠峰大本营徒步旅行期间可看到的著名山峰的简明目录。该索引中的每份文件都对应一座山峰，如珠穆朗玛峰。对于每个山峰文档，我们都会存储名称/别名、可选的经纬度坐标以及由 SigLIP-2 文本提示（+ 可选的参考图片）混合而成的单一原型向量。</p><p><strong>索引映射：</strong></p><p>现场</p><p>类型</p><p>示例</p><p>目的/说明</p><p>矢量/索引</p><p>本我</p><p>关键词</p><p>阿玛-达布拉姆</p><p>稳定的弹头/ID</p><p>-</p><p>姓名</p><p>文本 + 关键字子字段</p><p>["Ama Dablam","Amadablam"]</p><p>别名/多语言名称；names.raw 用于精确筛选</p><p>-</p><p>纬纶</p><p>地理点</p><p>{"lat":27.8617,"lon":86.8614}</p><p>以经纬度组合形式显示的山顶 GPS 坐标（可选）</p><p>-</p><p>海拔_m</p><p>整数</p><p>6812</p><p>海拔（可选）</p><p>-</p><p>嵌入文本</p><p>dense_vector</p><p>768</p><p>该山峰的混合原型（提示和可选的 1-3 幅参考图片</p><p>index:true, similarity:"cosine", index_options：{type:"hnsw", m:16, ef_construction:128}</p><p>该索引主要用于图像到图像的搜索，例如从图像中识别山峰。我们还使用该索引来增强文本到图片的搜索结果。</p><p>总之，<code>peaks_catalog</code> 将问题""这是什么山？" "转化为一个重点突出的 "最近邻问题"，有效地将概念理解与图像数据的复杂性分离开来。</p><p><strong>peaks_catalog 索引的索引策略： </strong>首先，我们创建了一份在 EBC 徒步旅行中可见的最突出山峰的列表。对于每个山峰，我们都会在<a href="https://github.com/navneet83/multimodal-mountain-peak-search/blob/main/data/peaks.yaml">yaml 文件</a>中存储其地理位置、名称、同义词和海拔高度。下一步是<a href="https://github.com/navneet83/multimodal-mountain-peak-search/blob/main/scripts/embed_and_index_photos.py#L351"> 生成</a> 每个峰值的 嵌入 值，并将其存储在<code>text_embed</code> 字段中。为了生成稳健的嵌入，我们使用了以下技术：</p><ul><li><p>创建文本原型：</p><ul><li><p>山峰名称</p></li><li><p><a href="https://github.com/navneet83/multimodal-mountain-peak-search/blob/main/scripts/embed_and_index_photos.py#L301">提示组合</a>（使用多个不同的提示来尝试回答同一个问题），例如</p><ul><li><p>"尼泊尔喜马拉雅山脉山峰的自然照片{name} "</p></li><li><p>"{name} 昆布地区的地标性山峰，高山景观"</p></li><li><p>"{name} 山顶，积雪，岩石山脊线"</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">反概念</a>（告诉 SigLIP-2 什么不能匹配）：为 "绘画、插图、海报、地图、徽标 "减去一个小矢量，这样我们就偏向于真实照片。</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">创建图像原型</a>。</p></li></ul><p>然后，我们<a href="https://github.com/navneet83/multimodal-mountain-peak-search/blob/main/scripts/embed_and_index_photos.py#L392">混合文本和图像原型</a>，生成最终的嵌入。最后，文件将被<a href="https://github.com/navneet83/multimodal-mountain-peak-search/blob/main/scripts/embed_and_index_photos.py#L396">索引到</a>所有必填字段：</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><code>peaks_catalog</code> 索引中的文件样本：</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt1219f5d0e39b512c/6a17da7c57726263161bcace/bc05fbd0c4f8d721d5170c28a3884a9eda80bb7d-1210x1132.png" alt="来自 Elasticsearch 中 peaks_catalog 索引的示例文档。" /><h4>照片索引</h4><p>该主索引存储相册中所有照片的详细信息。每份文档代表一张照片，包含以下信息：</p><ul><li><p>相册中照片的相对路径。可用于查看匹配图像或在搜索用户界面中加载图像。</p></li><li><p>图片的 GPS 和时间信息。</p></li><li><p>SigLIP-2 生成的图像编码密集矢量。</p></li><li><p><code>predicted_peaks</code> 可让我们根据峰名进行筛选。<strong>索引映射</strong></p></li></ul><p>现场</p><p>类型</p><p>示例</p><p>目的/说明</p><p>矢量/索引</p><p>路径</p><p>关键词</p><p>data/images/IMG_1234.HEIC</p><p>用户界面如何打开缩略图/全图</p><p>-</p><p>剪贴图片</p><p>dense_vector</p><p>768</p><p>SigLIP-2 图像嵌入</p><p>index:true, similarity:"cosine", index_options：{type:"hnsw", m:16, ef_construction:128}</p><p>预测峰值</p><p>关键词</p><p>["ama-dablam","pumori"]</p><p>索引时的 Top-K 猜想（廉价用户体验过滤器/面）</p><p>-</p><p>全球定位系统</p><p>地理点</p><p>{"lat":27.96,"lon":86.83}</p><p>启用地理筛选器</p><p>-</p><p>拍摄时间</p><p>date</p><p>2023-10-18T09:41:00Z</p><p>捕捉时间：排序/过滤</p><p>-</p><p><strong>照片索引的索引策略： </strong>对于相册中的每张照片，我们会采取以下措施：
<a href="https://github.com/navneet83/multimodal-mountain-peak-search/blob/main/scripts/embed_and_index_photos.py#L526">从图像元数据中</a>提取图像<code>shot_time</code> 和<code>gps</code> 信息。</p><ul><li><p><a href="https://github.com/navneet83/multimodal-mountain-peak-search/blob/main/scripts/embed_and_index_photos.py#L511">SigLIP-2 图像嵌入</a>：通过模型传递图像并对向量进行 L2 归一化。将嵌入内容存储在<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">预测峰值</a>并将其存储在<code>predicted_peaks</code> 字段中。为此，我们首先获取上一步生成的照片图像向量，然后针对<code>peaks_catalog</code> 索引中的 text_embed 字段快速运行 kNN 搜索。我们保留顶部的 3-4 个山峰，忽略其余的。</p></li><li><p>我们通过对图片名称和路径进行<a href="https://github.com/navneet83/multimodal-mountain-peak-search/blob/main/scripts/embed_and_index_photos.py#L509">散列</a>计算<code>_id</code> 字段。这可以确保我们在多次运行后不会出现重复。</p></li></ul><p>一旦我们确定了照片的所有字段，就会使用<a href="https://github.com/navneet83/multimodal-mountain-peak-search/blob/main/scripts/embed_and_index_photos.py#L530"> 批量 索引对照片文件进行</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>照片索引中的样本文件：</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt744b7e6326937cfc/6a17da7e6df731d3040a0da8/1dc1406ac2a97440b6804838795b3c2205c4c6b2-1080x1234.png" alt="来自 Elasticsearch 照片索引的样本文件。" /><p>总之，照片索引是相册中所有照片的快速、可过滤、kNN 就绪存储。它的映射结构非常简单，只需足够的结构就能快速检索、清晰显示，并按空间和时间对结果进行切分。该索引可同时满足这两种搜索用途。创建这两个索引的 Python 脚本可在<a href="https://github.com/navneet83/multimodal-mountain-peak-search/blob/main/scripts/create_indices.py">此处</a>找到。</p><p>下面的 Kibana 地图可视化将相册中的文档显示为绿色圆点，将<code>peaks_catalog</code> 索引中的山峰显示为红色三角形，其中绿色圆点与珠峰大本营徒步路线非常吻合。</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltb5bf016e8d9c3e84/6a17da80be608681f10045e6/1c75d0ed0ce53d28a94bf2f47a354e25581d2baf-1600x1402.png" alt="Kibana 地图可视化显示相册中的文件为绿色圆点，peaks_catalog 索引中的山峰为红色三角形，其中绿色圆点与珠峰大本营徒步路线非常吻合。" /><h2>搜索用例</h2><p><strong>按名称搜索（文本到图像）：</strong>该功能可让用户使用文本查询查找山峰照片（甚至是 "祈祷旗 "等抽象概念）。为此，使用 SigLIP-2 将文本输入<a href="https://github.com/navneet83/multimodal-mountain-peak-search/blob/main/scripts/query_by_peak_name.py#L87C5-L87C20">转换为文本向量</a>。为了生成稳健的文本向量，我们采用了与在<code>peaks_catalog</code> 索引中创建文本嵌入相同的策略：<a href="https://github.com/navneet83/multimodal-mountain-peak-search/blob/main/scripts/query_by_peak_name.py#L104"> 将</a> 文本输入与小型 提示集合<a href="https://github.com/navneet83/multimodal-mountain-peak-search/blob/main/scripts/query_by_peak_name.py#L100"> 相结合</a> ，减去次要的<a href="https://github.com/navneet83/multimodal-mountain-peak-search/blob/main/scripts/query_by_peak_name.py#L103"> 反概念向量</a> ，并应用<a href="https://github.com/navneet83/multimodal-mountain-peak-search/blob/main/scripts/query_by_peak_name.py#L104"> L2 归一化 生成最终的查询向量。</a>然后在<code>photos.clip_image</code> 字段上执行 kNN<a href="https://github.com/navneet83/multimodal-mountain-peak-search/blob/main/scripts/query_by_peak_name.py#L140">查询</a>，根据余弦相似度检索匹配度最高的峰值，从而找到最接近的图像。作为查询的一部分，还可选择应用地理和日期<a href="https://github.com/navneet83/multimodal-mountain-peak-search/blob/main/scripts/query_by_peak_name.py#L152">筛选器</a>和/或<code>photos.predicted_peaks</code> 术语筛选器来提高搜索结果的相关性（见下文查询示例）。这有助于排除在徒步过程中看不到的相似山峰。</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt9bb9abf5ce64fcbb/6a17da81e8fbce20db3a17da/b5fac28ffdbedb820505365ca07df125cd01b939-946x370.png" alt="如何在 Elasticsearch 中通过名称（文本到图像）进行多模式搜索。" /><p><strong>带有地理过滤器的 Elasticsearch 查询：</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>按图像搜索（图像到图像）：</strong>通过该功能，我们可以识别照片中的某座山，并在相册中查找该座山的其他图像。图像上传后，将由 SigLIP-2 图像编码器处理，生成<a href="https://github.com/navneet83/multimodal-mountain-peak-search/blob/main/scripts/identify_from_picture_find_similar_peaks.py#L228">图像矢量</a>。然后在<code>peaks_catalog.text_embed</code> 字段上进行<a href="https://github.com/navneet83/multimodal-mountain-peak-search/blob/main/scripts/identify_from_picture_find_similar_peaks.py#L234">kNN 搜索</a>，以确定最匹配的峰值名称。随后，根据这些匹配的山峰名称<a href="https://github.com/navneet83/multimodal-mountain-peak-search/blob/main/scripts/identify_from_picture_find_similar_peaks.py#L257"> 生成</a> 一个 文本向量 ，并在照片索引中进行另一次<a href="https://github.com/navneet83/multimodal-mountain-peak-search/blob/main/scripts/identify_from_picture_find_similar_peaks.py#L263"> kNN 搜索</a> ，以找到相应的照片。</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltab9d16333e2a9e69/6a17da827f6f155448c099cc/3a3d5635bee7a222b95529dd7f9fbee016381610-1226x550.png" alt="Elasticsearch 如何通过图像进行多模式搜索（图像到图像）。" /><p><strong>Elasticsearch 查询：</strong></p><p>第 1 步：找到匹配的山峰名称</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>第 2 步：在<code>photos</code> 索引上进行搜索，找到匹配的图片（与文本到图片搜索用例中的查询相同）：</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>流光 UI</h2><p>为了将所有功能整合在一起，我们创建了一个简单的 Streamlit 用户界面，让我们可以同时执行两种搜索用例。左侧栏显示可滚动的峰值列表（从<code>photos.predicted_peaks</code> 中汇总），并带有复选框和小地图/地理过滤器。顶部有一个<strong>按姓名搜索</strong>框和一个<strong>从照片</strong>上传识别按钮。中心窗格采用响应式缩略图网格，显示 kNN 分数、预测峰值徽章和捕获时间。每张图片都有一个<strong>查看图片</strong>按钮，用于全分辨率预览。</p><p><strong>通过上传图片进行搜索：</strong>我们会预测峰值，并从相册中找到匹配的峰值。</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltd1fb2b0304a310d2/6a17da8425daab7cda08a0fa/dca540cbf5279e6d6102c5a0c0351ddd4ac91cda-1600x1112.png" alt="这是一个简单的流光式用户界面，可通过文本到图像和图像到图像的多模态搜索方式搜索阿玛达布拉姆山峰。" /><p><strong>文本搜索</strong>从文本中查找相册中匹配的峰值</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt496c1ae8f7886320/6a17da86abe0f2da48dfe8bd/b1e8618db746cd49ea4962d3dc73031387b975dd-1600x1166.png" alt="如何在山峰库中通过文本搜索珠穆朗玛峰。" /><h2>结论</h2><p><em>我们能看看 </em><em><strong>阿玛-达布拉姆</strong></em><em> 的照片吗？</em>变成了一个可运行的小型<strong>多模态搜索</strong>系统。我们采集了原始的徒步旅行照片，将其转化为<strong>SigLIP-2 嵌入</strong>，并使用<strong>Elasticsearch</strong>对向量进行快速的<strong>kNN</strong>处理，再加上简单的地理/时间过滤器，根据<em>意义</em>浮现出正确的图像。在此过程中，我们将两个索引的关注点分开：一个是混合原型的小<code>peaks_catalog</code> （用于识别），另一个是图像向量和 EXIF 的可扩展<code>photos</code> 索引（用于检索）。它实用、可复制、易扩展。</p><p>如果您想对其进行调整，有几项设置可供使用：</p><ul><li><p><strong>查询时间设置：</strong> <code>k</code> （您希望返回多少个邻居）和<code>num_candidates</code> （最终评分前的搜索范围）。这些设置将在<a href="https://www.elastic.co/search-labs/blog/elasticsearch-knn-and-num-candidates-strategies">此处的</a>博客中讨论。</p></li><li><p><strong>索引时间设置：</strong> <code>m</code> （图形连接性）和<code>ef_construction</code> （构建时间精度与内存）。对于查询，也可以尝试使用<code>ef_search</code> --更高通常意味着更高的召回率，但需要权衡一定的延迟。有关这些设置的更多详情，请参阅<a href="https://www.elastic.co/search-labs/blog/hnsw-graph">本博客</a>。</p></li></ul><p>展望未来，用于<strong>多模态</strong>和<strong>多语言</strong>搜索的本地模型/路由器即将登陆<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"> Elastic</a>生态系统，这将使图像/文本检索和混合排名功能更加强大。</p><p>如果你想亲自尝试一下：</p><ul><li><p><strong>GitHub 代码库</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 快速入门</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>我们的旅程就此结束，是时候飞回去了。希望这对你有帮助，如果你改动（或改进）了它，我很乐意听听你的改动。</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[向量数据库]]></category>
    <category><![CDATA[混合搜索]]></category>
    <category><![CDATA[AI]]></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[使用基于用户行为数据的判断列表在 Elasticsearch 中训练 LTR 模型]]></title>
    <description><![CDATA[了解如何使用 UBI 数据创建判断列表，以便在 Elasticsearch 中自动训练学习排名 (LTR) 模型。]]></description>
    <content:encoded><![CDATA[<p>使用<a href="https://www.elastic.co/docs/solutions/search/ranking/learning-to-rank-ltr"><em><strong>"学习-排名 "</strong></em></a>模型的一大挑战是创建一个高质量的<a href="https://www.elastic.co/search-labs/blog/judgment-lists"><em><strong>判断列表</strong></em></a>来训练模型。传统上，这一过程需要对查询与文档的相关性进行<em><strong>人工</strong></em>评估，为每个文档分配一个等级。这是一个缓慢的过程，不能很好地扩展，也很难维护（想象一下要手工更新一个有数百个条目的列表）。</p><p>现在，如果我们可以使用真实用户与搜索应用程序的交互来创建这些训练数据，会怎样呢？使用<a href="https://www.elastic.co/search-labs/blog/elasticsearch-plugin-user-behavior-insights"><em><strong>UBI</strong></em></a>数据可以让我们做到这一点。创建一个自动系统，捕捉并利用我们的搜索、点击和其他互动来生成判断列表。与人工交互相比，这一过程更容易扩展和重复，而且往往会产生更好的结果。在本博客中，我们将探讨如何查询存储在 Elasticsearch 中的 UBI 数据，以计算有意义的信号，从而为<a href="https://www.elastic.co/search-labs/blog/elasticsearch-learning-to-rank-introduction"><em><strong>LTR</strong></em></a>模型生成训练数据集。</p><p><em><strong>您可以 </strong></em><a href="https://github.com/Alex1795/elastic-ltr-judgement_list-blog.git"><em><strong>在这里</strong></em></a>找到完整的实验 <em><strong>。</strong></em></p><h2>为什么 UBI 数据有助于训练 LTR 模型？</h2><p>与人工标注相比，UBI 数据具有多项优势：</p><ul><li><p><strong>数量：</strong>鉴于 UBI 数据来自真实的互动，我们可以收集到比人工生成的数据多得多的数据。当然，前提是我们有足够的流量来生成这些数据。</p></li><li><p><strong>真实用户意图：</strong>传统上，人工判断列表来自专家对可用数据的评估。另一方面，UBI 数据反映了真实的用户行为。这意味着我们可以生成更好的训练数据，从而提高搜索系统的准确性，因为这些数据是基于用户如何与您的内容进行实际互动并从中发现价值的，而不是对相关内容的理论假设。</p></li><li><p><strong>不断更新：</strong>判断列表需要不断刷新。如果我们根据 UBI 数据创建它们，我们就可以获得最新的数据，从而更新判断列表。</p></li><li><p><strong>成本效益高：</strong>无需手动创建判断列表，可多次高效重复这一过程。</p></li><li><p><strong>自然查询分布</strong>：UBI 数据代表真实的用户查询，可推动更深层次的变化。例如，用户是否使用自然语言在我们的系统中进行搜索？如果是这样，我们可能需要采用语义搜索或混合搜索方法。</p></li></ul><p>不过也有一些警告：</p><ul><li><p><strong>偏见放大： </strong>热门内容更容易获得点击，因为它的曝光率更高。因此，这最终可能会放大热门项目，可能会淹没更好的选择。</p></li><li><p><strong>覆盖面不全： </strong>新内容缺乏互动，因此可能难以在结果中占据重要位置。罕见查询也可能缺乏足够的数据点来创建有意义的训练数据。</p></li><li><p><strong>季节性变化：</strong>如果您预期用户行为会随时间发生巨大变化，那么历史数据可能无法告诉您什么是好结果。</p></li><li><p><strong>任务模糊：</strong>点击并不总能保证用户找到想要的东西。</p></li></ul><h2>成绩计算</h2><h3>LTR 培训的成绩</h3><p>为了训练 LTR 模型，我们需要提供一些数字来表示文档与查询的相关程度。在我们的实施过程中，这个数字是一个连续的分数，从 0.0 到 5.0+，分数越高表示相关性越高。</p><p>为了说明这个分级系统是如何运行的，请看这个手动创建的示例：</p><p>查询</p><p>文件内容</p><p>等级</p><p>说明</p><p>"最佳比萨饼配方"</p><p>"正宗意大利比萨面团配方及步骤图片"</p><p>4.0</p><p>高度相关，正是用户正在寻找的内容</p><p>"最佳比萨饼配方"</p><p>"意大利比萨的历史"</p><p>1.0</p><p>与主题有点吻合，是关于披萨的，但不是食谱</p><p>"最佳比萨饼配方"</p><p>"适合初学者的 15 分钟快速比萨食谱"</p><p>3.0</p><p>这是个不错的结果，但也许还达不到 "最佳 "配方的标准。</p><p>"最佳比萨饼配方"</p><p>"汽车保养指南"</p><p>0.0</p><p>完全不相关，与查询完全无关</p><p>正如我们在这里看到的，等级是一个数字，表示文档与我们的样本查询 "最佳披萨配方 "的相关程度。有了这些分数，我们的 LTR 模型就能知道哪些文档应该在结果中显示得更高。</p><p>如何计算等级是我们训练数据集的核心。有<a href="https://www.elastic.co/search-labs/blog/judgment-lists"> 多种方法</a> 可以做到这一点，每种 方法 都有自己的优缺点。例如，我们可以分配一个二进制分数，1 表示相关 0 表示不相关，或者我们可以只计算每个查询结果文档中的点击次数。</p><p>在这篇博文中，我们将采用一种不同的方法，<em><strong>将用户行为作为我们的输入，并计算出一个等级数字作为输出</strong></em>。我们还将纠正可能出现的偏差，因为无论文件的相关性如何，结果越靠前，点击率越高。</p><h2>计算成绩 - COEC 算法</h2><p>COEC<a href="https://www.wsdm-conference.org/2010/proceedings/docs/p351.pdf">（点击量大于预期点击量</a>）算法是一种根据用户点击量计算判断等级的方法。如前所述，用户倾向于点击位置较高的结果，即使该文档并非与查询最相关；这就是所谓的<a href="https://eugeneyan.com/writing/position-bias/">位置偏差</a>。使用 COEC 算法的核心理念是，并非所有点击都具有同等意义；点击位置 10 的文档表明，该文档与查询的相关性远高于点击位置 1 的文档。引用有关 COEC 算法的研究论文（如上链接）：</p><p><em>"众所周知，搜索结果或广告的点击率（CTR）会随着搜索结果的位置而显著降低"。</em></p><p>您可以<a href="https://www.researchgate.net/publication/200110550_An_experimental_comparison_of_click_position-bias_models">在这里</a>进一步了解位置偏差。</p><p>为了利用 COEC 算法解决这个问题，我们采取了以下步骤：</p><p><strong>1.建立位置基线：</strong>我们计算每个搜索位置（从 1 到 10）的点击率（CTR）。这意味着我们要确定点击位置 1、位置 2 等的用户比例。这一步骤可捕捉用户的自然位置偏差。我们使用以下方法计算点击率：</p><p>在哪里？</p><p>p = 位置。从 1 到 10
Cp = 在所有查询中，任何文档在位置 p 上的总点击次数
Ip = 总印象次数：在所有查询中，任何文档在位置 p 上出现的次数</p><p>在这里，我们希望位置越高，点击越多。</p><p><strong>2.</strong> <strong>计算预期点击量 (EC)：</strong></p><p>该指标根据文档出现的位置和这些位置的点击率，确定文档 "本应 "获得的点击量：</p><p>在哪里？</p><p>Qd = 文档 d 出现的所有查询
pos(d,q)= 文档 d 在查询结果 q 中的位置</p><p>3.<strong>统计实际点击量： </strong>我们统计文档在所有查询中实际获得的总点击量，以下称为<strong>A(d)。</strong></p><p>4.<strong>计算 COEC 分数：</strong>这是实际点击量（A(d)）与预期点击量（EC(d)）之比：</p><p>该指标对位置偏差进行了这样的归一化处理：</p><ul><li><p>得分 1.0 表示文档在出现的位置上表现完全符合预期。</p></li><li><p>得分高于 1.0 表示该文件的位置表现优于预期。因此，这份文件与查询更相关。</p></li><li><p>如果得分低于 1.0，则表示该文件的表现不如预期。因此，该文件与查询的相关性较低。</p></li></ul><p><em><strong>最终结果是一个等级数字，它能反映用户正在寻找什么，并考虑到从与我们搜索系统的实际互动中提取的基于位置的期望值。</strong></em></p><h2>技术实施</h2><p>我们将创建一个脚本来创建判断列表，以训练 LTR 模型。</p><p>该脚本的输入是 Elastic 中索引的 UBI 数据（查询和事件）。</p><p>输出结果是使用 COEC 算法从这些 UBI 文档生成的 CSV 文件中的判断列表。该判断列表可与<a href="https://www.elastic.co/search-labs/blog/elasticsearch-learning-to-rank-introduction">Eland</a>结合使用，以提取相关特征并训练 LTR 模型。</p><h3>快速启动</h3><p>要根据本博客中的样本数据生成判断列表，可以按照以下步骤操作：</p><p>1.克隆版本库：</p>git clone https://github.com/Alex1795/elastic-ltr-judgement_list-blog.git  
cd elastic-ltr-judgement_list-blog<p>2.安装所需程序库</p><p>对于这个脚本，我们需要以下库：</p><ul><li><p><em>熊猫</em>：保存判断列表</p></li><li><p><em>elasticsearch</em>：从我们的弹性部署中获取 UBI 数据</p></li></ul><p>我们还需要 Python 3.11</p>pip install -r requirements.txt<p>3.在<a href="https://github.com/Alex1795/elastic-ltr-judgement_list-blog/blob/main/.env-example">.env 文件</a>中更新弹性部署的环境变量</p><ul><li><p>ES_HOST</p></li><li><p>API_KEY</p></li></ul><p>要添加环境变量，请使用</p>source .env<p>4.创建 ubi_queries、ubi_events 索引，并上传样本数据。运行 setup.py 文件：</p>python setup.py<p>5.运行 Python 脚本：</p>python judgement_list-generator.py<p>如果按照这些步骤操作，你应该会看到一个名为 judgment_list.csv 的新文件，它看起来像这样：</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt94317eda8f7af194/6a170aa46f7f04542f914821/2531090131ac9fe3e4e1d79de9d156fc47a7825a-782x531.png" alt="" /><p>该脚本将使用下图所示的<strong>calculate_relevance_grade()</strong>函数，应用之前讨论过的 COEC 算法计算等级。</p><h2>数据架构</h2><h3>乌比查询</h3><p>我们的 UBI 查询索引包含在搜索系统中执行的查询信息。这是一份样本文件：</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>在这里，我们可以看到来自用户（client_id）、查询结果（query_response_object_ids）和查询本身（时间戳、user_query）的数据。</p><h3>Ubi 点击事件</h3><p>我们的 ubi_events 索引拥有用户每次点击结果中的文档时产生的数据。这是一份样本文件：</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>判断列表生成脚本</h2><h3>脚本概述</h3><p>该脚本使用存储在 Elasticsearch 中的来自查询和点击事件的 UBI 数据自动生成判断列表。它执行这些任务：</p><ul><li><p>获取并处理 Elasticsearch 中的 UBI 数据。</p></li><li><p>将 UBI 事件与其查询相关联。</p></li><li><p>计算每个位置的点击率。</p></li><li><p>计算每份文档的预期点击量 (EC)。</p></li><li><p>计算每份文档的实际点击次数。</p></li><li><p>计算每个查询-文档配对的 COEC 分数。</p></li><li><p>生成判断列表并将其写入 CSV 文件。</p></li></ul><p>让我们逐一了解这些功能：</p><h3>connect_too_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>该函数使用主机和 api 密钥返回 Elasticsearch 客户端对象。</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>该函数是数据提取层；它连接 Elasticsearch，使用 match_all 查询获取 UBI 查询，并过滤 UBI 事件，只获取 "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>该函数处理判断列表的生成。它通过关联 UBI 事件和查询，开始处理 UBI 数据。然后，它会调用每个文档-查询对的 calculate_relevance_grade() 函数来获取判断列表的条目。最后，它会以 pandas 数据帧的形式返回结果列表。</p><h3>计算相关性等级()</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>这是实现 COEC 算法的函数。它先计算每个位置的点击率，然后比较文档-查询配对的实际点击率，最后计算每个位置的实际 COEC 分数。</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>它能从判断列表中生成有用的统计数据，如查询总数、唯一文档总数或等级分布。这纯粹是为了提供信息，不会改变最终的判断列表。</p><h2>成果和影响</h2><p>如果您按照快速入门部分的说明进行操作，您应该会看到一个 CSV 文件，其中包含一个有 320 个条目的判断列表（您可以在软件仓库中看到<a href="https://github.com/Alex1795/elastic-ltr-judgement_list-blog/blob/main/judgment_list.csv">输出示例</a>）。有了这些字段：</p><ul><li><p>qid：查询的唯一 ID</p></li><li><p>docid：生成文件的唯一标识符</p></li><li><p>等级：查询文件对的计算等级</p></li><li><p>查询：用户查询</p></li></ul><p> 让我们看看 "意大利菜谱 "的查询结果：</p><p>qid</p><p>docid</p><p>职级</p><p>查询</p><p>Q1 意大利食谱</p><p>基本食谱</p><p>0.0</p><p>意大利食谱</p><p>Q1 意大利食谱</p><p>菜谱_比萨_玛格丽塔</p><p>3.333333</p><p>意大利食谱</p><p>Q1 意大利食谱</p><p>菜谱指南</p><p>10.0</p><p>意大利食谱</p><p>Q1 意大利食谱</p><p>法式羊角面包食谱</p><p>0.0</p><p>意大利食谱</p><p>Q1 意大利食谱</p><p>西班牙海鲜饭食谱</p><p>0.0</p><p>意大利食谱</p><p>Q1 意大利食谱</p><p>希腊穆萨卡菜谱</p><p>1.875</p><p>意大利食谱</p><p>我们可以从结果中看到，查询 "意大利菜谱"：</p><ul><li><p>烩饭食谱无疑是该查询的最佳结果，其点击率比预期高出 10 倍</p></li><li><p>玛格丽塔比萨也是不错的选择。</p></li><li><p>希腊 Mousaka（令人吃惊）也取得了不错的成绩，其表现要好于它在比赛中的位置。这意味着一些寻找意大利食谱的用户转而对这一食谱感兴趣。也许这些用户普遍对地中海菜肴感兴趣。最后，这告诉我们，在我们上面讨论的另外两场 "更好 "的比赛中，这可能是一个很好的结果。</p></li></ul><h2>结论</h2><p>利用 UBI 数据，我们可以自动训练 LTR 模型，从自己的用户中创建高质量的判断列表。UBI 数据提供了一个大数据集，反映了我们的搜索系统是如何被使用的。通过使用 COEC 算法来生成等级，我们可以考虑到固有的偏差，同时，它也反映了用户认为更好的结果。本文概述的方法可应用于实际使用案例，以提供更好的搜索体验，并随着实际使用趋势的变化而变化。</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 托管]]></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[使用 ES|QL 进行 Elasticsearch 地理空间搜索]]></title>
    <description><![CDATA[Elasticsearch 查询语言 (ES|QL) 中的地理空间搜索。Elasticsearch 具有强大的地理空间搜索功能，ES|QL 将大幅提高易用性和 OGC 熟悉度。]]></description>
    <content:encoded><![CDATA[<p>多年来，Elasticsearch 一直拥有强大的<a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/geospatial-analysis.html">地理空间搜索和分析功能</a>，但其应用程序接口与典型 GIS 用户习惯的应用程序接口大相径庭。去年，我们<a href="https://www.elastic.co/search-labs/blog/esql-piped-query-language-goes-ga"> 添加了 ES|QL 查询语言</a> ，这是一种管道式 查询语言 ，与 SQL 一样简单，甚至更容易。它特别适合 Elastic 擅长的搜索、安全和可观察性用例。我们还在 ES|QL 中添加了对地理空间搜索和分析的支持，使其更易于使用，特别是对于来自 SQL 或<a href="https://en.wikipedia.org/wiki/Geographic_information_system">GIS</a>社区的用户。</p><p>Elasticsearch 8.12 和 8.13 为 ES|QL 带来了对地理空间类型的基本支持。8.14 版增加了地理空间搜索功能后，这一功能大大增强。更重要的是，这种支持的设计与其他空间数据库（如<a href="https://en.wikipedia.org/wiki/Simple_Features"> </a><a href="https://en.wikipedia.org/wiki/Open_Geospatial_Consortium">PostGIS ）</a> 使用的 开放地理空间联盟（OGC） 的 简单地物访问 标准非常一致，使熟悉这些标准的 GIS 专家更容易使用。</p><p>在本博客中，我们将向您介绍如何使用 ES|QL 执行地理空间搜索，以及它与 SQL 和查询 DSL 的等效功能的比较。我们还将向您展示如何使用 ES|QL 执行空间连接，以及如何在 Kibana 地图中将结果可视化。请注意，此处描述的所有功能都在"技术预览版" 中，我们非常乐意听取您对我们如何改进这些功能的反馈意见。</p><h2>搜索地理空间数据</h2><p>让我们从一个查询示例开始：</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>这将搜索与三亚凤凰国际机场 (SYX) 周围矩形搜索多边形相交的任何城市边界多边形。</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt3897df6bed5d6061/6a17d7c2abe0f29eccdfe861/e48bac8f246c8842f2ea97ddd54910045262aeb1-1440x808.png" alt="ESQL 地理空间搜索" /><p>在一个包含机场、城市和城市边界的示例数据集中，该搜索可找到相交的多边形，并从匹配文档中返回所需字段：</p><p>缩写</p><p>机场</p><p>地区</p><p>城市</p><p>城市位置</p><p>SYX</p><p>三亚凤凰国际机场</p><p>天涯区</p><p>三亚</p><p>point(109.5036 18.2533)</p><p>这很容易！现在将其与经典的 Elasticsearch 查询 DSL 进行比较：</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>这两个查询的意图都相当明确，但 ES|QL 查询与 SQL 非常相似。同样的查询在 PostGIS 中是这样的：</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>请看 ES|QL 的例子。如此相似，不是吗？</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>我们发现，Elasticsearch API 的现有用户发现 ES|QL 更易于使用。现在，我们希望现有的 SQL 用户，尤其是空间 SQL 用户，会发现 ES|QL 与他们习惯看到的感觉非常相似。</p><h4>为什么不是 SQL？</h4><p>Elasticsearch SQL 如何？它已经存在了一段时间，并具有一些地理空间功能。不过，Elasticsearch SQL 是作为原始查询 API 的封装器编写的，这意味着只支持可转译到原始 API 的查询。ES|QL 没有这种限制。作为一个全新的堆栈，它可以进行许多 SQL 无法实现的优化。我们的基准测试表明，ES|<a href="https://elasticsearch-benchmarks.elastic.co/#tracks/esql/nightly/default/6M">QQL 通常比查询 API 更快</a>，尤其是在聚合方面！</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt18bda964c8b24e36/6a17d7c3e3179155d22d568a/b8b6c2b2e45850d832805ed1e71e522f4955f53c-1440x813.png" alt="多边形交叉点基准" /><h2>与 SQL 的区别</h2><p>显然，从前面的例子来看，ES|QL 与 SQL 有些相似，但也有一些重要的区别。例如，ES|QL 是一种管道式查询语言，它以 FROM 等源命令开始，然后用管道 | 字符将所有后续命令串联起来。这样就很容易理解每条命令是如何接收数据表并对该表执行某些操作的，例如使用<code>WHERE</code> 进行过滤，使用<code>EVAL</code> 添加列，或使用<code>STATS</code> 执行聚合。与从<code>SELECT</code> 开始定义最终输出列不同，可以有一个或多个<code>KEEP</code> 命令，最后一个命令指定最终输出结果。这种结构简化了查询的推理过程。</p><p>仔细观察上述示例中的<code>WHERE</code> 命令，我们会发现它与 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>除了字符串引号字符的不同，最大的区别在于我们如何将字符串类型转换为空间类型。在 PostGIS 中，我们使用<code>::geometry</code> 后缀，而在 ES|QL 中，我们使用<code>::geo_shape</code> 后缀。这是因为 ES|QL 在 Elasticsearch 中运行，而类型转换操作符<code>::</code> 可用于将字符串转换为<a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/esql-limitations.html#_supported_types">ES|QL 支持的</a>任何类型，在本例中就是<code>geo_shape</code> 。此外，Elasticsearch 中的<code>geo_shape</code> 和<code>geo_point</code> 类型意味着空间坐标系为 WGS84，通常使用 SRID 编号 4326。在 PostGIS 中，这一点需要明确，因此在 WKT 字符串中使用了<code>SRID=4326;</code> 前缀。如果删除该前缀，SRID 将被设置为 0，这更像 Elasticsearch 类型<code>cartesian_point</code> 和<code>cartesian_shape</code> ，它们与任何特定坐标系无关。</p><p>ES|QL 和 PostGIS 也提供了类型转换函数语法：</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>OGC 功能</h2><p>Elasticsearch 8.14 引入了以下四种 OGC 空间搜索功能：</p><p>ES|QL</p><p>PostGIS</p><p>描述</p><p>ST_INTERSECTS</p><p>ST_Intersects</p><p>如果两个几何图形相交，则返回 true，否则返回 false。</p><p>ST_DISJOINT</p><p>ST_Disjoint</p><p>如果两个几何图形不相交，则返回 true，否则返回 false。ST_INTERSECTS 的倒数。</p><p>ST_CONTAINS</p><p>ST_Contains</p><p>如果一个几何体包含另一个几何体，则返回 true，否则返回 false。</p><p>ST_WITHIN</p><p>ST_Within</p><p>如果一个几何体位于另一个几何体内，则返回 true，否则返回 false。ST_CONTAINS 的倒数。</p><p>这些函数的行为与 PostGIS 的对应函数类似，使用方法也相同。例如，如果两个几何图形相交，<code>ST_INTERSECTS</code> 返回 true，否则返回 false。如果您跟踪上表中的文档链接，可能会发现所有 ES|QL 示例都在<code>FROM</code> 子句之后的<code>WHERE</code> 子句中，而所有 PostGIS 示例都使用了字面几何图形。事实上，这两个平台都支持在查询的任何部分使用这些函数。</p><p><code>ST_INTERSECTS</code> 的 PostGIS 文档中的第一个示例是</p>SELECT ST_Intersects(
    'POINT(0 0)'::geometry,
    'LINESTRING ( 2 0, 0 2 )'::geometry
);
<p>ES|QL 相当于此：</p>ROW ST_INTERSECTS(
    "POINT(0 0)"::geo_point,
    "LINESTRING ( 2 0, 0 2 )"::geo_shape
)
<p>请注意我们在 PostGIS 示例中没有指定 SRID。这是因为在 PostGIS 中使用<code>geometry</code> 类型时，所有计算都是在平面坐标系上进行的，因此如果两个几何图形具有相同的 SRID，SRID 是什么并不重要。在 Elasticsearch 中，大多数函数也是如此，但也有例外，<code>geo_shape</code> 和<code>geo_point</code> 使用球形计算，我们将在下一篇关于空间距离搜索的博客中看到这一点。</p><h2>ES|QL 多功能性</h2><p>因此，我们已经看到了在<code>WHERE</code> 子句和<code>ROW</code> 命令中使用空间函数的示例。它们还能在哪里发挥作用？<code>EVAL</code> 命令就是一个非常有用的地方。该命令允许您评估表达式并返回结果。例如，让我们确定按国名分组的所有机场的中心点是否都在勾勒出国家的边界内：</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>结果在意料之中，英国机场的中心点在英国边界内，而不在冰岛边界内，反之亦然：</p><p>中心点</p><p>计数</p><p>英国</p><p>冰岛</p><p>within_uk</p><p>冰岛境内</p><p>点（-21.94663446396589364.13187285885215)</p><p>1</p><p>错误</p><p>true</p><p>错误</p><p>true</p><p>点 (-2.597342072712148 54.33551226578214)</p><p>17</p><p>true</p><p>错误</p><p>true</p><p>错误</p><p>点 (0.04453958108176276 23.74658354606057)</p><p>873</p><p>错误</p><p>错误</p><p>错误</p><p>错误</p><p>事实上，这些函数可用于查询的任何部分，只要其签名是合理的。它们都接收两个参数，参数可以是一个字面空间对象，也可以是一个空间类型的字段，并且都返回一个布尔值。一个重要的考虑因素是，几何图形的坐标参考系（CRS）必须匹配，否则将返回错误信息。这意味着不能在同一个函数调用中混合使用<code>geo_shape</code> 和<code>cartesian_shape</code> 类型。不过，您可以混合使用<code>geo_point</code> 和<code>geo_shape</code> 类型，因为<code>geo_point</code> 类型是<code>geo_shape</code> 类型的特例，两者共享相同的坐标参考系。上文定义的每个函数的文档都列出了支持的类型组合。</p><p>此外，任一参数都可以是空间字面量或字段，顺序不限。您甚至可以指定两个字段、两个字面量、一个字段和一个字面量或一个字面量和一个字段。唯一的要求是类型必须兼容。例如，此查询比较同一索引中的两个字段：</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>该查询基本上是询问城市位置是否在城市边界内，一般来说应该是正确的，但总有例外：</p><p>基数</p><p>计数</p><p>城市</p><p>很少</p><p>29</p><p>错误</p><p>很多</p><p>740</p><p>true</p><p>一个更有趣的问题是，机场所在地是否在机场服务城市的边界之内。不过，机场位置所在的索引与包含城市边界的索引不同。这就需要一种方法来有效查询和关联这两个独立索引中的数据。</p><h2>空间连接</h2><p>ES|QL 不支持<code>JOIN</code> 命令，但可以使用<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>实现连接的特殊情况，其行为类似于 SQL 中的 "左连接"。该命令的操作类似于 SQL 中的 "左连接"，允许您根据两个数据集之间的空间关系，用另一个索引中的数据来丰富一个索引的结果。</p><p>例如，我们可以通过查找包含机场位置的城市边界，用机场服务城市的附加信息来丰富机场表的结果，然后对结果进行一些统计：</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>这将返回拥有最多机场的前 5 个区域，以及所有匹配区域机场的中心点和这些区域内城市边界的 WKT 表示长度范围：</p><p>中心点</p><p>计数</p><p>min_wkt</p><p>max_wkt</p><p>地区</p><p>点（-32.5609347096071932.598117914802714)</p><p>90</p><p>207</p><p>207</p><p>无效</p><p>点（-73.9451533276587740.70366442203522)</p><p>9</p><p>438</p><p>438</p><p>纽约市</p><p>点（-83.1039831787347842.300230911932886)</p><p>9</p><p>473</p><p>473</p><p>底特律</p><p>点（-156.302024586126220.176383580081165)</p><p>5</p><p>307</p><p>803</p><p>夏威夷</p><p>点（-73.8890273217111845.57078813901171)</p><p>4</p><p>837</p><p>837</p><p>蒙特利尔</p><p>那么，这里到底发生了什么？所谓的<code>JOIN</code> 发生在哪里？问题的关键在于<code>ENRICH</code> 命令：</p>FROM airports
| ENRICH city_boundaries ON city_location WITH airport, region, city_boundary
<p>该命令指示 Elasticsearch 丰富从<code>airports</code> 索引中获取的结果，并在原始索引的<code>city_location</code> 字段和<code>airport_city_boundaries</code> 索引的<code>city_boundary</code> 字段之间执行<code>intersects</code> 连接，我们在前面的几个示例中使用了该连接。但其中一些信息在该查询中并不清晰。我们看到的是丰富策略的名称<code>city_boundaries</code> ，缺失的信息被封装在该策略定义中。</p>{
  "geo_match": {
    "indices": "airport_city_boundaries",
    "match_field": "city_boundary",
    "enrich_fields": ["city", "airport", "region", "city_boundary"]
  }
}
<p>在这里我们可以看到，它将执行<code>geo_match</code> 查询（<code>intersects</code> 是默认值），要匹配的字段是<code>city_boundary</code> ，而<code>enrich_fields</code> 是我们要添加到原始文档中的字段。其中一个字段<code>region</code> 实际上被用作<code>STATS</code> 命令的分组键，如果没有这种 "左连接 "功能，我们是做不到这一点的。有关 enrich 策略的更多信息，请参阅<a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/enrich-setup.html">enrich 文档</a>。在阅读这些文档时，你会注意到它们描述了如何通过配置摄取管道，在索引时使用丰富索引来丰富数据。ES|QL 不需要这样做，因为<code>ENRICH</code> 命令在查询时起作用。只需用必要的数据和丰富策略准备丰富索引，然后在 ES|QL 查询中使用<code>ENRICH</code> 命令即可。</p><p>您可能还会注意到，最常见的地区是<code>null</code> 。这意味着什么？回想一下，我曾将此命令比作 SQL 中的 "左连接"，也就是说，如果没有为某个机场找到匹配的城市边界，则仍会返回该机场，但<code>airport_city_boundaries</code> 索引中的字段值为<code>null</code> 。结果发现，有 89 个机场没有找到匹配的<code>city_boundary</code> ，有一个机场找到了匹配的<code>region</code> 字段，但该字段是<code>null</code> 。这样就统计出了 90 个机场，结果中没有<code>region</code> 。另一个有趣的细节是<code>MV_EXPAND</code> 命令的必要性。这样做是必要的，因为<code>ENRICH</code> 命令可能会为每个输入行返回多个结果，而<code>MV_EXPAND</code> 则有助于将这些结果分成多行，每个结果一行。这也解释了为什么"Hawaii" 会显示不同的<code>min_wkt</code> 和<code>max_wkt</code> 结果：存在多个名称相同但边界不同的区域。</p><h2>Kibana 地图</h2><p>Kibana 在地图应用程序中添加了对空间 ES|QL 的支持。这意味着您现在可以使用 ES|QL 在 Elasticsearch 中搜索地理空间数据，并在地图上将结果可视化。</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt91922eb4ef43d290/6a16f7161949f737bfe7a7b5/bd78470bd8a4bc60f0db7006bd804b8fe87e2fea-1440x683.png" alt="Kibana 层 ES|QL" /><p>在添加图层菜单中有一个新的图层选项，名为"ES|QL" 。与迄今为止介绍的所有地理空间功能一样，该功能在"技术预览版" 中。选择该选项可根据 ES|QL 查询结果在地图上添加图层。例如，您可以在地图上添加一个图层，显示世界上所有的机场。</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt5185ef7461d7e81d/6a16f718839dfa1559dcfcb1/1dd28d3d0509f92d26b0bb5320a2925f7a54c5d9-1440x736.png" alt="Kibana ES|QL - 机场" /><p>或者您也可以添加一个图层，显示<code>airport_city_boundaries</code> 索引中的多边形，或者更好的办法是使用上面那个复杂的<code>ENRICH</code> 查询，生成每个地区有多少个机场的统计数据？</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt51ee3543bb811d98/6a16f71a8b73cbbe63189df1/679a0a401faa613c7cedddd07c64f614ac2b7144-1440x727.png" alt="Kibana ES|QL - 区域统计" /><h2>后续工作计划</h2><p>你可能已经注意到，在上面的两个例子中，我们又加入了一个空间函数<code>ST_CENTROID_AGG</code> 。这是<code>STATS</code> 命令中使用的聚合函数，也是我们计划添加到 ES|QL 中的众多空间分析功能中的第一个。当我们有更多展示时，我们会在博客上介绍！</p><p>在此之前，我们想向您详细介绍我们正在开发的一项特别令人兴奋的功能：执行空间距离搜索的能力，这是 Elasticsearch 最常用的空间搜索功能之一。你能想象距离搜索的语法会是什么样子吗？或许类似于 OGC 功能？敬请关注本系列的下一篇博客，一探究竟！</p><p>剧透警告：Elasticsearch 8.15 刚刚发布，其中包含使用 ES|QL 的空间距离搜索！</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[评估搜索相关性第一部分 —— BEIR 基准测试]]></title>
    <description><![CDATA[学习在深入理解 BEIR 基准测试的背景下评估您的搜索系统，并掌握提升搜索评估流程的技巧与方法。]]></description>
    <content:encoded><![CDATA[<p>这是系列博客文章的第一篇，探讨如何在更好地理解 BEIR 基准测试的背景下，评估您自己的搜索系统。我们将介绍具体的技巧与方法，帮助您在深入理解 BEIR 的背景下改进搜索评估流程。我们还将指出常见的陷阱，这些陷阱会降低评估的可靠性。最后，我们注意到，LLM 为搜索工程师提供了一种强大的新工具，我们将通过示例展示如何利用它们来协助搜索评估。</p><h2>理解搜索相关性评估中的 BEIR 基准测试</h2><p>要改进任何系统，您需要能够衡量其表现。在搜索领域，<a href="https://arxiv.org/abs/2104.08663">BEIR</a>（或等效 <a href="https://huggingface.co/spaces/mteb/leaderboard">MTEB</a> 排行榜的检索部分）被认为是信息检索社区的“圣杯”，这并不令人意外。它是一个结构非常完善的基准测试，涵盖了不同任务的多样化数据集。更具体地说，它涵盖了以下领域：</p><ul><li><p>参数检索 (ArguAna, Touche2020)</p></li><li><p>开放领域问答 (HotpotQA, Natural Questions, FiQA)</p></li><li><p>段落检索（MSMARCO）</p></li><li><p>重复问题检索（Quora，CQADupstack）</p></li><li><p>事实核查（FEVER, Climate-FEVER, Scifact）</p></li><li><p>生物医学信息检索（TREC-COVID、NFCorpus、BioASQ）</p></li><li><p>实体检索 (DBPedia)</p></li><li><p>引文预测 (SCIDOCS)</p></li></ul><p>它提供了一个单一的统计信息 nDCG@10，用于衡量系统在其返回的顶部结果中，为每个任务样例匹配最相关文档的程度。对于人机交互的搜索系统来说，顶部结果的相关性至关重要。然而，评估搜索效果有很多细微差别，而单一的汇总统计数据无法涵盖这些差别。</p><h2>BEIR 数据集的结构</h2><p>每个基准测试包含三个组成部分：</p><ul><li><p>要检索的语料库或文档</p></li><li><p>查询</p></li><li><p>查询的相关性判断（也称为 <code>qrels</code>）。</p></li></ul><p>相关性判断以零或更高的分数表示。非零分数表示文档与查询有一定的相关性。</p><p>数据集</p><p>语料库大小</p><p>#测试集中的查询数量</p><p>#qrels 中正向标注的数量</p><p>#qrels 等于零</p><p>#语料库中的重复项数量</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>Natural Questions</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 (sum)</p><p>457,199</p><p>13,145</p><p>23,703</p><p>0</p><p>0</p><p><strong>表 1</strong>：数据集统计信息。这些数字是在数据集的测试部分计算得出的（<code>MSMARCO</code> 的 <code>dev</code>）。</p><p><strong>表 1</strong> 列出了构成 <code>BEIR</code> 基准的数据集的一些统计数据，如语料库中的文档数量、测试数据集中的查询数量以及 <code>qrels</code> 文件中的正/负（查询、文档）对数量。通过快速查看数据，我们可以立即推断出以下几点：</p><ul><li><p>大多数数据集在 <code>qrels</code> 文件中不包含任何负面关系，即零分，这会明确地将文档标记为与给定查询无关。</p></li><li><p>每个查询的平均文档关联数（<code>#qrels</code>/<code>#queries</code>）从 <code>ArguAna</code> 的 1.0 到 493.5（<code>TREC-COVID</code>）不等，但大多数情况下的值为 <code>&lt;</code> 5。</p></li><li><p>一些数据集的语料库中存在重复文档，在某些情况下可能导致评估不准确，例如，当一个文档被认为与查询相关但其重复文档却不相关时。例如，在 <code>ArguAna</code> 中，我们识别出 96 个重复文档对案例，每对中只有一个文档被标记为与查询相关。通过将初始 qrels 列表“扩展”以包含重复文档，我们观察到 <code>nDCG@10</code> 分数平均相对提高了约 1%。</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>ArguAna 中的重复对示例。在 qrels 文件中，只有第一个文档显示为与查询（“test-economy-epiasghbf-pro02a”）相关（作为反方论点）</strong></p><p>在 MTEB 排行榜上比较模型时，人们很容易将重点放在平均检索质量上。这虽然是模型整体质量的一个良好代理指标，但并不一定能说明它在您特定场景下的表现。由于结果按数据集报告，因此值得了解不同数据集与您的搜索任务的关联程度，并仅使用最相关的数据集重新评估模型。如果您想深入研究，还可以检查各数据语料库的主题重叠情况。按主题对质量指标进行分层，可以更精细地评估模型的具体优势与劣势。</p><p>这里需要特别注意，当文档未在 <code>qrels</code> 文件中标注时，默认情况下它被视为与查询不相关。我们对此领域进行了更深入的探究，并收集了一些证据，以更清晰地阐明以下问题：“评估者面对没有真实标注信息的（查询，文档）对的频率有多高？”。这一点之所以重要，是因为当只有浅层标注可用时（即并非每个相关文档都被标记），一个信息检索系统可能仅仅因为它“选择”呈现了不同的相关（但未标记）文档，而被判定为比另一个系统更差。这是创建高质量评估集时的常见陷阱，尤其是对于大型数据集。为了可行，人工标注通常会关注当前系统返回的顶部结果，因此可能会遗漏其盲区中的相关文档。因此，通常更可取的做法是将更多资源集中在少数查询的更完整标注上，而不是广泛的浅层标注。</p><h2>利用 BEIR 基准测试进行搜索相关性评估</h2><p>为了启动分析，我们实施了以下场景（见<a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/evaluating-search-relevance-part-1/retrieve-and-rerank.ipynb">笔记本</a>）：</p><ol><li><p>首先，我们将每个数据集的语料库加载到 Elasticsearch 索引中。</p></li><li><p>对于测试集中的每个查询，我们使用 BM25 检索前 100 个文档。</p></li><li><p>我们使用多种 SOTA 重排序模型对检索到的文档进行重新排序。</p></li><li><p>最后，我们报告第 2 步（检索后）和第 3 步（重新排序后）得出的前 10 名文档的 "评判率"。换句话说，我们计算的是 <code>qrels</code> 文件中得分排名前 10 的文档的平均百分比。</p></li></ol><p>我们使用的模型重新排序列表如下：</p><ul><li><p><a href="https://docs.cohere.com/reference/rerank">Cohere 的</a> <code>rerank-english-v2.0</code> 和 <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>检索</p><p>重排序</p><p></p><p></p><p></p><p></p><p>数据集</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>Natural Questions</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（平均）</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>表 2</strong>：在检索/重新排序的前 10 个文档上计算的（数据集、重排序器）对的评判率</p><p>从 <strong>表 2</strong> 可以看出，除了 <code>TREC-COVID</code>（覆盖率&gt;90%）、<code>DBPedia</code>（~65%）、<code>Touche2020</code> 和 <code>nfcorpus</code>（~35%）之外，大多数数据集在检索或重新排序后的标注率介于 5% 到略高于 10% 之间。这并不意味着所有这些未标注文档都是相关的，但其中可能有一个子集——特别是那些排在靠前位置的——可能是正例。</p><p>随着通用指令调优语言模型的出现，我们拥有了一种新的强大工具，它有可能自动化判断相关性。这些方法的计算成本通常太高，不适合在线搜索，但我们这里关注的是离线评估。在下文中，我们将使用它们来探究一些 BEIR 数据集可能存在浅层标注问题的证据。</p><p>为了进一步验证这一假设，我们决定专注于 MSMARCO 数据集，选择 100 个查询及其前 5 个经重排序（使用 Cohere v2）且当前未被标记为相关的文档。我们遵循了两种不同的评估路径：首先，我们使用精心调优的提示（后续文章中将详述）引导最近发布的 <a href="https://huggingface.co/microsoft/Phi-3-mini-4k-instruct">Phi-3-mini-4k</a> 模型来预测文档与查询的相关性（或不相关性）。与此同时，这些案例也进行了人工标注，以评估 LLM 输出与人工判断之间的一致性率。总体而言，我们可以得出以下两个结论：</p><ul><li><p>LLM 响应与人工标注之间的一致性率接近 80%，作为该方向的起点，这似乎足够好。</p></li><li><p>在 57.6% 的情况下（基于人工判断），返回的文档被判定为实际上与查询相关。换一种说法：对于 100 个查询，我们有 107 个文档被判定为相关，但至少还有 0.576 × 5 × 100 = 288 个额外文档实际上是相关的！</p></li></ul><p>以下是一些从 <code>MSMARCO</code>/<code>dev</code> 数据集中提取的示例，其中包含查询、注释的正例文档（来自 <code>qrels</code>）一个由于标注不全导致的假负例文档：</p><p>示例 1：</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：</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>人工评估此类特定查询，是理解搜索质量的一种普遍有用的技术，它是对 nDCG@10 等定量指标的补充。如果您有一组代表性的查询集，在每次修改搜索时都运行它们，这将为您提供关于性能如何变化的重要定性信息，而这些信息在统计数据中是看不见的。例如，它为您提供了更多关于搜索返回的错误结果的见解：它可以帮助您发现检索结果中的明显错误，相关错误类别，如误解特定领域术语等等。</p><p>我们的结果与围绕 <code>MSMARCO</code> 评估的相关研究一致。例如，<a href="https://arxiv.org/pdf/2109.00062">Arabzadeh 等人</a> 遵循类似的流程，他们雇用众包工人进行偏好判断：除其他发现外，他们表明在许多情况下，重新排序模块返回的文档比 MSMARCO <code>qrels</code> 文件中的文档更受青睐。另一项证据来自 <a href="https://arxiv.org/pdf/2010.08191">RocketQA</a> 重新排序器的作者，他们报告称，经过人工检查，超过 70% 的重排序文档被判定为相关。</p><p> 更新 - 2023 年 9 月 9 日：在对数据集进行仔细重新评估后，我们识别出另外 15 个相关文档案例，使其总数从 273 个增加到 288 个。</p><h2>主要要点和后续步骤</h2><ul><li><p>对更高质量真实标注的追求永无止境，因为它对于基准测试和模型比较至关重要。如果谨慎使用并配以适当的指令调优，LLM 可以在某些评估领域提供协助</p></li><li><p>更普遍地说，鉴于基准测试永远不会完美，从单纯的分数比较转向捕捉统计显著性差异的更稳健技术可能更可取。<a href="https://arxiv.org/pdf/2109.00062">Arabzadeh 等人</a>等人的工作提供了一个很好的例子，他们基于研究结果构建了 95% 置信区间，以表明不同运行之间是否存在显著（或不显著）差异。在随附的<a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/evaluating-search-relevance-part-1/retrieve-and-rerank.ipynb">笔记本</a>中，我们提供了使用<a href="https://en.wikipedia.org/wiki/Bootstrapping_(statistics)">自助法</a>实现置信区间的方法。</p></li><li><p>从最终用户的角度来看，在阅读基准测试结果时，考虑任务对齐性是有益的。例如，对于构建 RAG 管道的 AI 工程师来说，如果知道最典型的用例涉及从不同来源汇集多条信息，那么在 HotpotQA 等多跳问答数据集上评估其检索模型的性能，会比在整个 BEIR 基准测试的全局平均分更有意义</p></li></ul><p>在<a href="https://www.elastic.co/search-labs/blog/evaluating-search-relevance-part-2">下一篇博客文章</a>中，我们将更深入地探讨如何使用 Phi-3 作为 LLM 评判工具，以及调优它以预测相关性的过程。</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[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[人工智能剽窃利用 Elasticsearch 检测剽窃行为]]></title>
    <description><![CDATA[以下是如何使用 Elasticsearch 检查人工智能剽窃，重点是使用 NLP 模型和矢量搜索的用例。]]></description>
    <content:encoded><![CDATA[<p>剽窃可以是<strong>直接剽窃</strong>，即抄袭部分或全部内容；也可以是<strong>转述剽窃</strong>，即对作者的作品进行改写，改动一些词或短语。</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltb6e139760e56ec98/6a171147dc55de0ad2e00edf/5d7073187fda829438aeec8d3a1194a5bea2ba57-1440x347.png" alt="" /><p>灵感和意译是有区别的。阅读一篇内容，获得启发，然后用自己的语言来探讨这个想法，即使得出的结论相似，也是有可能的。</p><p>虽然剽窃一直是讨论的话题，但内容的加速生产和发布使其具有现实意义，并构成了持续的挑战。</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltc2af1678cb04506b/6a171149cf4f251c2ab2d257/a0b9a98d729db09dae0a79315c001e6763c12704-1400x1016.png" alt="" /><p>这种挑战并不局限于书籍、学术研究或司法文件，在这些领域经常会进行抄袭检查。它还可以延伸到报纸甚至社交媒体。</p><p>随着信息的丰富和出版的便捷，如何在可扩展的层面上有效检查抄袭行为？</p><p>大学、政府机构和公司使用的工具多种多样，虽然直接的<a href="https://www.elastic.co/search-labs/lexical-and-semantic-search-with-elasticsearch">词汇搜索</a>可以有效检测直接抄袭，但主要的挑战在于识别<strong>转述的内容。</strong></p><h2>利用生成式人工智能检测剽窃行为</h2><p>生成式人工智能带来了新的挑战。人工智能生成的内容在复制时是否被视为抄袭？</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt8a1ed1b6fa3f1a56/6a17114b4a531bc40736aa69/9345b28d6d27c37469bc38e823c41780b4eabfe5-1440x875.png" alt="" /><p>例如，<a href="https://openai.com/">OpenAI</a>的<a href="https://openai.com/policies/terms-of-use">使用条款</a>规定，OpenAI 不会对 API 为用户生成的内容主张版权。在这种情况下，使用生成式人工智能的个人可以随意使用生成的内容，而无需引用。</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltbc2e08ab5b808e38/6a17114dab7f086cbadb9f93/e1f415f69a247666f02ddc81468944920c874cd7-968x814.png" alt="" /><p>不过，是否接受使用生成式人工智能来提高效率仍是一个讨论话题。</p><p>为了对剽窃检测做出贡献，OpenAI 开发了一个<a href="https://huggingface.co/roberta-base-openai-detector">检测模型</a>，但后来承认其准确性不够高。</p><p><em>"我们认为，这对于独立检测来说精度还不够高，需要与基于元数据的方法、人工判断和公众教育相结合，才能更加有效。"</em></p><p>然而，随着更多工具的出现，现在有了更多检测抄袭的选择，即使是在转述和人工智能内容的情况下。</p><h2>使用 Elasticsearch 检测剽窃行为</h2><p>有鉴于此，在本博客中，我们将利用自然语言处理（NLP）模型和矢量搜索（plagiarism detection）来探索元数据搜索之外的另一个使用案例。</p><p>我们利用<a href="https://www.sbert.net/"> SentenceTransformers</a> 提供的<a href="https://sbert.net/datasets/emnlp2016-2018.json"> 数据集（</a> 其中包含 NLP 相关文章），通过<a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/plagiarism-detection-with-elasticsearch/plagiarism_detection_es.ipynb"> Python 示例 进行了演示。</a>我们通过 "语义文本相似性 "检查摘要是否抄袭，"语义文本相似性 "考虑的是用之前导入 Elasticsearch 的<a href="https://huggingface.co/sentence-transformers/all-mpnet-base-v2">文本嵌入模型</a>生成的 "摘要 "嵌入。此外，为了识别人工智能生成的内容--人工智能剽窃，还将 OpenAI 开发的<a href="https://huggingface.co/roberta-base-openai-detector">NLP 模型</a>导入 Elasticsearch。</p><p>下图说明了数据流：</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt0464f88d3ed12070/6a17114fab7f084905db9f97/1ad89c98a2f42a497548ca3947749bad54ec1172-1440x880.png" alt="" /><p>在使用<a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/inference-processor.html"> 推理处理器</a> <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/ingest.html">的摄取管道</a> 中，"抽象 "段落被映射为一个 768 维的向量，即 "abstract_vector.predicted_value"。</p><p>绘图：</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>矢量表示之间的相似性使用矢量相似性度量来衡量，该度量使用 "相似性 "<a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/dense-vector.html#dense-vector-params">参数</a>来定义。</p><p><a href="https://en.wikipedia.org/wiki/Cosine_similarity">余弦</a>是默认的相似度量，计算公式为"(1 + cosine(query, vector))/ 2'.除非您需要保留原始矢量，并且无法事先对它们进行归一化处理，否则执行余弦相似性的最有效方法就是将所有矢量归一化为单位长度。这有助于避免在搜索过程中执行额外的矢量长度计算，而应使用 "dot_product"。</p><p>在同一管道中，另一个包含<a href="https://huggingface.co/roberta-base-openai-detector">文本分类模型</a>的推理处理器会检测内容是可能由人类撰写的 "真实 "内容，还是可能由人工智能撰写的 "虚假 "内容，并将 "openai-detector.predicted_value "添加到每个文档中。</p><p>摄取管道：</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>在查询时，同样的文本嵌入模型也被用于在 "query_vector_builder "对象中生成查询 "model_text "的向量表示。</p><p>k-nearest neighbor (kNN) 搜索会根据相似度量找到与查询向量最近的 k 个向量。</p><p>每份文档的  分数都来自相似度，确保分数越大，排名越靠前。这意味着文件在语义上更加相似。因此，我们打印了三种可能性：如果得分&gt; 0.9，我们认为是 "高度相似"；如果&lt; 0.7，是 "低度相似"，否则是 "中度相似"。您可以根据自己的使用情况，灵活设置不同的阈值，以确定哪种程度的 _score 是否属于抄袭。</p><p>此外，还进行了文本分类，以检查文本查询中是否有人工智能生成的元素。</p><p>查询：</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>输出：</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>在本例中，利用数据集中的一个 "摘要 "值作为文本查询 "model_text "后，剽窃行为被识别出来。相似度分数为 1.0，表明相似度很高，即<strong>直接抄袭</strong>。矢量化查询和文档没有被识别为人工智能生成的内容，这在意料之中。</p><p>查询：</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>输出：</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>通过更新文本查询 "model_text"，使用人工智能生成的文本，在传达相同信息的同时尽量减少相似词语的重复，检测到的相似度仍然很高，但得分为 0.9302529，而不是 1.0，这就是<strong>转述剽窃</strong>。人们还预计，由人工智能生成的这一查询会被检测到。</p><p>最后，考虑到文本查询 "model_text "是关于 Elasticsearch 的文本，而不是这些文档的摘要，检测到的相似度为 0.68991005，表明根据所考虑的阈值，相似度较低。</p><p>查询：</p>#different text - not a plagiarism

model_text = 'Elasticsearch provides near real-time search and analytics for all types of data.'
<p>输出：</p>Low similarity detected. This might not be plagiarism.
<p>虽然在人工智能生成的文本查询中，以及在转述和直接复制内容的情况下，都能准确识别出抄袭行为，但在剽窃检测领域中，还需要认识到各个方面的问题。</p><p>在人工智能生成的内容检测方面，我们探索了一种模式，这种模式做出了宝贵的贡献。不过，必须认识到独立检测的固有局限性，因此需要结合其他方法来提高准确性。</p><p>选择文本嵌入模型带来的可变性是另一个考虑因素。使用不同数据集训练的不同模型会产生不同程度的相似性，这突出了所生成的文本嵌入的重要性。</p><p>最后，在这些例子中，我们使用了文件摘要。然而，剽窃检测通常涉及大量文件，因此必须解决文本长度的难题。文本通常会超过模型的标记限制，这就要求在建立嵌入之前将文本分割成块。处理这种情况<a href="https://www.elastic.co/guide/en/elasticsearch/reference/8.11/knn-search.html#nested-knn-search">的实用方法</a>是使用 dense_vector 嵌套结构。</p><h2>结论</h2><p>在这篇博客中，我们讨论了检测抄袭（尤其是在转述和人工智能生成的内容中）所面临的挑战，以及如何利用语义文本相似性和文本分类来实现这一目的。</p><p>结合这些方法，我们提供了一个剽窃检测实例，成功识别了人工智能生成的内容、直接剽窃和转述剽窃。</p><p>我们的主要目标是建立一个过滤系统，简化检测工作，但人工评估对于验证工作仍然至关重要。</p><p>如果您有兴趣了解更多有关语义文本相似性和 NLP 的信息，我们建议您同时查看以下链接：</p><ul><li><p><a href="https://www.elastic.co/what-is/semantic-search">什么是语义搜索？</a></p></li><li><p><a href="https://www.elastic.co/what-is/natural-language-processing">什么是自然语言处理（NLP）？</a></p></li><li><p><a href="https://www.elastic.co/search-labs/blog/lexical-and-semantic-search-with-elasticsearch">使用 Elasticsearch 进行词法和语义搜索</a></p></li><li><p><a href="https://www.elastic.co/search-labs/blog/chunking-via-ingest-pipelines">通过 Ingest 管道对大型文档进行分块，再加上嵌套向量，就能轻松进行通道搜索</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[向量数据库]]></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[使用 Elasticsearch 进行词法和语义搜索]]></title>
    <description><![CDATA[在本博客中，我们将探讨使用 Elasticsearch 检索信息的各种方法，重点是词法和语义搜索。]]></description>
    <content:encoded><![CDATA[<p>搜索是根据您的搜索查询或组合查询查找最相关信息的过程，相关搜索结果是与这些查询最匹配的文档。尽管与搜索相关的挑战和方法有多种，但最终目标始终不变，即<strong>尽可能找到问题的最佳答案</strong>。</p><p>考虑到这一目标，在本篇博文中，我们将探讨使用 Elasticsearch 检索信息的不同方法，重点关注文本搜索：<strong>词法搜索和语义搜索。</strong></p><h2>准备工作</h2><p>为此，我们将提供 Python 示例，演示在模拟电子商务产品信息生成的<a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/lexical-and-semantic-search-with-elasticsearch/products-ecommerce.json">数据集上</a>进行搜索的各种情况。</p><p>该数据集包含 2,500 多种产品，每种产品都有说明。这些产品分为 76 个不同的产品类别，每个类别包含的产品数量各不相同，如下所示：</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt34415151c00ced2b/6a17d8710b0bed6c9bdd342c/4104466050f3024b6bcaf382da2a702650f62227-1440x708.png" alt="" /><p><em>树状地图可视化 - category.keyword 的前 22 个值（产品类别）</em></p><p>安装时您需要</p><ul><li><p>Python 3.6 或更高版本</p></li><li><p><a href="https://www.elastic.co/guide/en/elasticsearch/client/python-api/current/index.html">弹性 Python 客户端</a></p></li><li><p>Elastic 8.8 或更高版本，配备 8GB 内存的机器学习节点</p></li><li><p><a href="https://www.elastic.co/guide/en/machine-learning/8.9/ml-nlp-elser.html">Elastic Learned Sparse EncodeR</a>模型已预装到 Elastic 中，并已在部署中启动</p></li></ul><p>我们将使用 Elastic Cloud，<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">可免费试用</a>。</p><p>除了本博文提供的搜索查询外，<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 笔记本</a>还能指导您完成以下过程：</p><ul><li><p>使用 Python 客户端与我们的弹性部署建立连接</p></li><li><p>将文本嵌入模型载入 Elasticsearch 集群</p></li><li><p>创建一个索引，其中包含用于索引特征向量和密集向量的映射。</p></li><li><p>创建带推理处理器的摄取管道，用于文本嵌入和文本扩展</p></li></ul><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt0e95406ece8f08d9/6a17d8731d1b83d32e93e2f4/54a9a490a3b0cf1b9c2228bee8eddd3f566bd435-1418x1102.png" alt="" /><h2>词汇检索--稀疏检索</h2><p>Elasticsearch 根据文本查询对文档进行相关性排序的经典方法是使用<a href="https://en.wikipedia.org/wiki/Okapi_BM25">BM25</a>模型的 Lucene 实现，这是一种<strong>用于词法搜索的稀疏模型</strong>。这种方法沿用了传统的文本搜索方法，即寻找完全匹配的术语。</p><p>为了实现这种搜索，Elasticsearch 通过执行文本分析将文本<strong>字段</strong>数据转换为可搜索格式。</p><p><strong>文本分析</strong> 由<a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/analyzer-anatomy.html"> 分析器</a> 执行， 分析器 是一套规则，用于管理提取相关词组以供搜索的过程。一个分析器必须有一个<a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-tokenizers.html"> 标记符</a>。标记化器接收字符流，并将其分解为单个标记（通常是单个单词），就像下面的例子一样：</p><h3>用于词法搜索的字符串标记化</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>输出</p>Analyzed Tokens: ['comfortable', 'furniture', 'for', 'a', 'large', 'balcony']
<p>在本例中，我们使用的是默认分析器--<a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-standard-analyzer.html">标准</a>分析器，它能提供基于英语语法的标记化，因此能很好地满足大多数使用要求。标记化可实现单个术语的匹配，但每个标记仍按字面进行匹配。</p><p>如果您想个性化搜索体验，可以选择不同的<a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-analyzers.html"> 内置分析器</a>。例如，通过更新代码以使用<a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-stop-analyzer.html">停顿分析器</a>，就能在任何非字母字符处将文本分解为词块，并支持删除停顿词。</p>...
# Define the analyze request
request_body = {
  "analyzer": "stop",
  "text": text
}
...
<p>输出</p>Analyzed Tokens: ['comfortable', 'furniture', 'large', 'balcony']
<p>当内置分析器无法满足您的需求时，您可以创建一个<a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-custom-analyzer.html"> 自定义分析器</a> ，该 分析器 使用零个或多个<a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-charfilters.html"> 字符过滤器</a> 、一个<a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-tokenizers.html"> 标记器</a> 和零个或多个<a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-tokenfilters.html"> 标记过滤器的</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>在上述结合了标记符号化器和标记符号过滤器的示例中，文本将先由<a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-lowercase-tokenfilter.html"> 小写过滤器</a> 进行 小写<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."> 处理，然后再由 同义词标记符号过滤器</a> 进行处理。</p><h2>词汇匹配</h2><p><a href="https://www.elastic.co/blog/practical-bm25-part-2-the-bm25-algorithm-and-its-variables">BM25</a>将根据术语的频率及其重要性来衡量文档与给定搜索查询的相关性。</p><p>下面的代码执行<a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-match-query.html"> 匹配</a> 查询，搜索最多两个文档，考虑到<em> "ecommerce-search</em><em> "</em> 索引中的 <strong>"描述"</strong><em><strong> 字段值和搜索查询 " 大阳台的舒适家具</strong></em><strong> ".</strong></p><p>改进文档的匹配标准可以提高查询的精确度。然而，更具体的结果是以较低的差异容忍度为代价的。</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>输出</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>通过分析输出结果，最相关的结果是"<em>芭比娃娃梦幻屋</em>" 产品，属于"<em>玩具</em>" 类别，其描述高度相关，因为其中包括术语"<em>家具</em>" 、"<em>大型"</em>和<em>"阳台</em>" ，这是唯一一个在描述中有 3 个术语符合搜索查询的产品，该产品也是唯一一个在描述中有术语<em>"阳台"</em>的产品。</p><p>排名第二的产品是"<em>舒适摇椅</em>" ，归类为"<em>室内家具</em>" ，其描述包括"<em>舒适</em>" 和"<em>家具</em>" 等术语。数据集中只有 3 种产品至少与该搜索查询的 2 个词匹配，该产品就是其中之一。</p><p><em>"Comfortable"</em>出现在 105 个产品的描述中，<em>"furniture"</em>出现在 4 个产品的描述中，有 4 个不同的类别：<em>玩具</em>、<em>室内家具、室外家具和 "猫狗用品&amp; 玩具"。</em></p><p>正如您所看到的，考虑到该查询，最相关的产品是玩具，第二相关的产品是室内家具。如果你想了解分数计算的详细信息，知道为什么这些文档是匹配的，可以将<a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/search-explain.html"><em>explain</em></a>__query 参数设置为 true。</p><p>尽管这两个结果都是最相关的结果，但考虑到该数据集中的文档数量和术语出现率，查询"<em>大型阳台的舒适家具</em>" 背后的意图是搜索实际大型阳台的家具，不包括玩具和室内家具。</p><p>词法搜索相对<strong>简单快捷</strong>，但也有局限性，因为在不了解用户意图和查询的情况下，不可能总是知道所有可能的术语和同义词。自然语言使用中的一个常见现象是<strong>词汇不匹配</strong>。<a href="https://dl.acm.org/doi/abs/10.1145/32206.32212">研究</a>表明，不同的人（同一领域的专家）对同一事物的命名平均有<strong>80% </strong>不同。</p><p>这些局限性促使我们寻找其他包含语义知识的评分模型。基于变换器的模型擅长处理像自然语言这样的连续输入标记，它通过考虑文档和查询的数学表示来捕捉搜索的基本含义。这样就能对文本进行密集的、上下文感知的矢量表示，为<strong>语义搜索</strong>提供动力，这是一种查找相关内容的精细方法。</p><h2>语义搜索 - 密集检索</h2><p>在这种情况下，将数据转换成有意义的向量值后，利用<a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/knn-search.html">k-nearest neighbor（kNN）</a>搜索算法在数据集中找到与查询向量最相似的向量表示。Elasticsearch 支持两种 kNN 搜索方法，即<a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/knn-search.html#exact-knn">精确暴力 kNN</a>和<a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/knn-search.html#approximate-knn">近似 kNN</a>（也称为 ANN）。</p><p>粗暴的 kNN 可以保证准确的结果，但在处理大型数据集时并不能很好地扩展。近似 kNN 通过牺牲一些精度来提高性能，从而有效地找到近似近邻。</p><p>借助 Lucene 对 kNN 搜索和密集矢量索引的支持，Elasticsearch 利用了分层导航小世界 (HNSW) 算法，该算法在各种<a href="http://ann-benchmarks.com/">ann-benchmark 数据集上</a>都表现出了强大的搜索性能。可以使用下面的示例代码在 Python 中执行近似 kNN 搜索。</p><h3>使用近似 kNN 进行语义搜索</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>考虑到产品数据集中 " 描述"字段的嵌入情况，该代码块使用 Elasticsearch 的 kNN 返回最多两个描述与"<em>Comfortable furniture for a large balcony</em> " 的矢量化查询 (query_vector_build) 相似的产品。</p><p>产品嵌入之前是在摄取管道中生成的，摄取管道中的推理处理器包含 <em>"</em><a href="https://huggingface.co/sentence-transformers/all-mpnet-base-v2"><em>all-mpnet-base-v2</em></a><em>"</em>文本嵌入模型的推理处理器，根据管道中正在摄取的数据进行推理。</p><p>选择该模型的依据是使用 <em>"</em><a href="https://github.com/UKPLab/sentence-transformers/blob/master/docs/package_reference/sentence_transformer/evaluation.md"><em>句子转换器评估</em></a><em>"</em>在训练过程中，使用不同的类别来评估模型。"all-mpnet-base-v2" 模型在<a href="https://www.sbert.net/docs/pretrained_models.html">Sentence-Transformers</a>排行榜上显示出最佳平均性能，并在<a href="https://huggingface.co/spaces/mteb/leaderboard">大规模文本嵌入基准 (MTEB)</a>排行榜上占据有利位置。该模型预先训练了<a href="https://huggingface.co/microsoft/mpnet-base"> 微软/mpnet-base</a>模型，并在 1B 句子对数据集上进行了微调，将句子映射到 768 维的密集向量空间。</p><p>此外，还有许多其他模型可以使用，特别是那些针对特定领域数据进行微调的模型。</p><p>输出</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>根据所选模型、</em> <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/knn-search.html#knn-search-filter-example"><em>过滤器</em></a> <em>和</em> <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/knn-search.html#tune-approximate-knn-for-speed-accuracy"><em>近似 kNN 调整</em></a>，输出结果可能会有所不同<em>。</em></p><p>kNN 搜索结果都属于"<em>Outdoor Furniture</em>" 类别，尽管"<em>outdoor</em>" 这个词并没有作为查询的一部分被明确提及，这凸显了语义理解在上下文中的重要性。</p><p>密集矢量搜索有几个优点：</p><ul><li><p>启用语义搜索</p></li><li><p>处理超大数据集的可扩展性</p></li><li><p>灵活处理各种数据类型</p></li></ul><p>不过，<strong>密集矢量搜索也有其自身的挑战</strong>：</p><ul><li><p>为您的使用案例选择合适的嵌入模式</p></li><li><p>选定模型后，可能需要对模型进行微调，以优化特定领域数据集的性能，这一过程需要领域专家的参与</p></li><li><p>此外，索引高维向量的计算成本很高</p></li></ul><h2>语义搜索--学习型稀疏检索</h2><p>让我们来探索另一种方法：学习稀疏检索，这是执行语义搜索的另一种方式。</p><p>作为一种稀疏模型，它利用 Elasticsearch 基于 Lucene 的倒排索引，该索引得益于数十年的优化。不过，这种方法不仅仅是用 BM25 等词汇评分功能添加同义词那么简单。相反，它利用更深层次的语言知识将学习到的联想融入其中，以优化相关性。</p><p>通过扩展搜索查询以包含原始查询中不存在的相关术语，<a href="https://www.elastic.co/guide/en/machine-learning/current/ml-nlp-elser.html">Elastic Learned Sparse Encoder</a>可以<strong>改进稀疏向量嵌入</strong>，如下例所示。</p><h3>利用弹性学习稀疏编码器进行稀疏向量搜索</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>输出</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>本例中的结果包括"<em>Garden Furniture</em>" 类别，该类别提供的产品与"<em>Outdoor Furniture</em>" 非常相似。</p><p>通过分析"ml.tokens" 、"rank_features" 字段，其中包含学习稀疏检索生成的标记，可以明显看出，在生成的各种标记中，有一些术语虽然不是搜索查询的一部分，但其含义仍然相关，如"<em>relax</em>" （舒适）、"<em>sofa</em>" （家具）和"<em>outdoor</em>" （阳台）。</p><p>下面的图片突出显示了查询中的一些术语，包括术语扩展和未扩展术语。</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt7e869985347b19a7/6a17d875e31791dc2c2d56a1/dd86607fce6137d843a3ec002390eaa988b432f9-1440x502.png" alt="" /><p>正如观察到的那样，该模型提供了一种上下文感知搜索，有助于缓解词汇不匹配问题，同时提供更多可解释的结果。在不进行特定领域再训练的情况下，它甚至可以超越密集向量模型。</p><h2>混合搜索：结合词法和语义搜索获得相关结果</h2><p>在搜索方面，没有放之四海而皆准的解决方案。这些检索方法各有所长，但也面临挑战。根据不同的使用情况，最佳选择可能会有所变化。通常情况下，不同检索方法的最佳结果是互补的。因此，为了提高相关性，我们要把每种方法的优势结合起来。</p><p>实现<strong>混合搜索</strong>的方法有多种，包括线性组合、给每个得分加权和互惠等级融合（RRF），其中互惠等级融合无需指定权重。</p><h3>Elasticsearch：词法和语义搜索的两全之策</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>在这段代码中，我们使用两个查询值为"<em>大阳台的餐桌和舒适的椅子</em>" 进行了混合搜索。我们没有使用"<em>家具</em>" 作为搜索词，而是指定了我们要查找的内容，并且两个搜索都考虑了相同的字段值，即"描述" 。排名由 BM25 和 ELSER 分数等权重的线性组合确定。</p><p>输出</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>在下面的代码中，我们将对查询使用相同的值，但会使用对等排名融合方法合并 BM25（查询参数）和 kNN（knn 参数）的得分，对文档进行合并和排名。</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>RRF 功能处于技术预览阶段。在全球大会之前，语法可能会有所改变。</em></p><p>输出</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>在这里，我们还可以使用不同的字段和值，<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 笔记本</a>中提供了其中一些示例。</p><p>正如您所看到的，有了 Elasticsearch，您就可以两全其美：传统的词法搜索和矢量搜索（无论是稀疏搜索还是密集搜索），以实现您的目标<strong>并找到问题的最佳答案。</strong></p><p>如果您想继续了解这里提到的方法，这些博客会很有用：</p><ul><li><p><a href="https://www.elastic.co/blog/improving-information-retrieval-elastic-stack-hybrid">改进弹性堆栈中的信息检索：混合检索</a></p></li><li><p><a href="https://www.elastic.co/blog/vector-search-elasticsearch-rationale">Elasticsearch 中的矢量搜索：设计背后的原理</a></p></li><li><p><a href="https://www.elastic.co/blog/lexical-ai-powered-search-elastic-vector-database">如何利用 Elastic 的向量数据库获得词法搜索和人工智能驱动搜索的最佳效果</a></p></li><li><p><a href="https://www.elastic.co/blog/may-2023-launch-sparse-encoder-ai-model">介绍 Elastic 学习稀疏编码器：Elastic 用于语义搜索的人工智能模型</a></p></li><li><p><a href="https://www.elastic.co/blog/may-2023-launch-information-retrieval-elasticsearch-ai-model">改进 Elastic Stack 中的信息检索：介绍我们的新检索模型 Elastic Learned Sparse Encoder</a></p></li></ul><p>Elasticsearch 提供了矢量数据库以及构建矢量搜索所需的所有工具：</p><ul><li><p>Elasticsearch<a href="https://www.elastic.co/elasticsearch/vector-database">矢量数据库</a></p></li><li><p>使用 Elastic 的<a href="https://www.elastic.co/enterprise-search/vector-search">矢量搜索</a>用例</p></li></ul><h2>结论</h2><p>在这篇博文中，我们探讨了使用 Elasticsearch 检索信息的各种方法，尤其侧重于文本、词法和语义搜索。为了证明这一点，我们提供了 Python 示例，使用包含电子商务产品信息的数据集展示了不同的搜索场景。</p><p>我们回顾了 BM25 的经典词汇搜索，并讨论了其优势和挑战，如词汇不匹配。我们强调了结合语义知识来克服这一问题的重要性。此外，我们还讨论了实现语义搜索的密集矢量搜索，并介绍了与这种检索方法相关的挑战，包括索引高维矢量时的计算成本。</p><p>另一方面，我们提到稀疏向量的压缩效果特别好。因此，我们讨论了 Elastic 的 "学习稀疏编码器"（Learned Sparse Encoder），该编码器可扩展搜索查询，以包含原始查询中不存在的相关术语。</p><p>在搜索方面，没有放之四海而皆准的解决方案。每种检索方法都有其优势和挑战。因此，我们还讨论了混合搜索的概念。</p><p>正如您所看到的，有了 Elasticsearch，您就可以同时拥有传统词法搜索和矢量搜索这两种搜索方式的优点！</p><p>准备好开始了吗？查看可用的<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 笔记本</a>并开始<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">免费试用 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[向量数据库]]></category>
    <category><![CDATA[Python]]></category>
    <category><![CDATA[查询语言]]></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>