<?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[Elasticsearch Labs]]></title>
        <description><![CDATA[Elastic 搜索团队的文章和教程]]></description>
        <link>https://www.elastic.co/search-labs/cn</link>
        <image>
            <url>https://www.elastic.co/search-labs/favicon.svg</url>
            <title>Elasticsearch Labs</title>
            <link>https://www.elastic.co/search-labs/cn</link>
        </image>
        <generator>Elasticsearch Labs</generator>
        <lastBuildDate>Thu, 23 Apr 2026 16:49:42 GMT</lastBuildDate>
        <atom:link href="https://www.elastic.co/search-labs/cn/rss/feed" rel="self" type="application/rss+xml"/>
        <copyright><![CDATA[© 2026. Elasticsearch B.V. 保留所有权利]]></copyright>
        <language><![CDATA[cn]]></language>
        <item>
            <title><![CDATA[Jina Embeddings v3 现已登陆 Gemini Enterprise Agent Platform Model Garden]]></title>
            <description><![CDATA[Jina 搜索基础模型 jina-embeddings-v3 现可在 Gemini Enterprise Agent Platform Model Garden 上自行部署，后续还将有更多模型加入。在您自己的 VPC 内，使用单个 L4 GPU 即可运行 jina-embeddings-v3。]]></description>
            <link>https://www.elastic.co/search-labs/cn/blog/jina-embeddings-v3-gemini-enterprise-model-garden</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/cn/blog/jina-embeddings-v3-gemini-enterprise-model-garden</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Sa Zhang]]></dc:creator>
            <pubDate>Wed, 22 Apr 2026 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/42f72633f1e5453dbfd47730b5f776429f9f633e-721x420.png?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>今天我们推出 <code>jina-embeddings-v3</code>，这是第一个以可自行部署的合作伙伴模型形式登陆 <a href="https://console.cloud.google.com/vertex-ai/publishers/jinaai/model-garden/jina-embeddings-v3">Gemini Enterprise Agent Platform Model</a> <a href="https://console.cloud.google.com/vertex-ai/publishers/jinaai/model-garden/jina-embeddings-v3">Garden</a> 的 Jina 搜索基础模型。<em>自行部署</em>意味着模型在您的 Google Cloud 项目和虚拟私有云 (VPC) 中的 GPU 实例上运行。没有外部 API 调用，没有按词计量，没有速率限制。</p><p>通过此次集成，Elasticsearch 用户获得了一个新的部署选项，能够将数据控制在安全边界内，实现可预测的基础设施成本，并原生运行于 Google Cloud 上。与此同时，更广泛的 Google Cloud 生态系统也能够使用 Jina 专为搜索和检索构建的最先进模型。</p><p>这是更广泛发布计划的第一阶段。与后续即将推出的模型一起，这一系列将构成一个完整的检索堆栈：嵌入您的数据、嵌入查询、检索和重排序候选结果、使用多模态嵌入将搜索扩展到图像——所有这些都在您控制的基础架构上完成。您今天就可以从 <code>jina-embeddings-v3</code> 开始，模型已经通过 Elastic Inference Service（EIS）在 Elasticsearch 生态系统中为生产搜索管道提供支持。</p>
            <table style="border-collapse: collapse; margin: 20px 0;">
              <tbody>
                
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">模型</td><td style="border: 1px solid #ccc; padding: 4px;">类型</td><td style="border: 1px solid #ccc; padding: 4px;">参数</td><td style="border: 1px solid #ccc; padding: 4px;">主要功能</td><td style="border: 1px solid #ccc; padding: 4px;">在 Model Garden 中的状态</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">`jina-embeddings-v3`</td><td style="border: 1px solid #ccc; padding: 4px;">文本嵌入</td><td style="border: 1px solid #ccc; padding: 4px;">572M</td><td style="border: 1px solid #ccc; padding: 4px;">久经考验的多语言主力，8K 上下文，1024 维输出，可截断至 32 维</td><td style="border: 1px solid #ccc; padding: 4px;">现已提供</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">`jina-embeddings-v5-text-small`</td><td style="border: 1px solid #ccc; padding: 4px;">文本嵌入</td><td style="border: 1px solid #ccc; padding: 4px;">677M</td><td style="border: 1px solid #ccc; padding: 4px;">先进的小于 1B 参数多语言模型，32K 上下文，1,024 维输出，可截断至 32 维</td><td style="border: 1px solid #ccc; padding: 4px;">即将推出</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">`jina-embeddings-v5-text-nano`</td><td style="border: 1px solid #ccc; padding: 4px;">文本嵌入</td><td style="border: 1px solid #ccc; padding: 4px;">239M</td><td style="border: 1px solid #ccc; padding: 4px;">5 亿参数以内的最佳模型，8K 上下文，768 维输出，可截断至 32 维</td><td style="border: 1px solid #ccc; padding: 4px;">即将推出</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">`jina-reranker-v3`</td><td style="border: 1px solid #ccc; padding: 4px;">重排序器</td><td style="border: 1px solid #ccc; padding: 4px;">600M</td><td style="border: 1px solid #ccc; padding: 4px;">Listwise 重排序器，131K 上下文，最多支持 64 个文档</td><td style="border: 1px solid #ccc; padding: 4px;">即将推出</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">`jina-clip-v2`</td><td style="border: 1px solid #ccc; padding: 4px;">多模态嵌入</td><td style="border: 1px solid #ccc; padding: 4px;">9亿参数</td><td style="border: 1px solid #ccc; padding: 4px;">文本+图像共享空间，89 种语言，8K 文本上下文，512×512 图像</td><td style="border: 1px solid #ccc; padding: 4px;">即将推出</td>
                    </tr>
              </tbody>
            </table>
          <p>每个模型都运行在单个 NVIDIA L4（24 GB）上，这是 Google Cloud 上最具成本效益的 GPU 层级。Google Cloud Model Garden 上的大多数其他嵌入模型都需要 A100 80 GB 或 H100，其每小时实例成本大约是 L4 的三倍，这还没有算上词元用量。</p><p>通过 Vertex AI 部署时，无需额外的商业许可证。</p><h2><strong>为什么选择模型花园？</strong></h2><p>为什么要通过 Model Garden 进行部署，而不是使用 API？归结为三个因素：控制、成本和上下文。</p><h3>您的数据永远不会离开本地环境</h3><p>对大多数开发者来说，最大的吸引力在于自行部署的架构。当您通过 Model Garden 部署 Jina 模型时，模型权重运行在您自己的 Google Cloud 项目以及您自己的 VPC 内的 GPU 实例上。这对于任何面临数据安全顾虑（如金融或医疗行业）的从业者来说，都是一个颠覆性的改变。由于没有外部 API 调用，您的敏感数据始终保留在安全边界内。</p><h3>按预测进行扩展</h3><p>您不需要为每次嵌入句子或重排序文档付费，而是支付固定的每小时实例成本。而且，因为每个 Jina 模型都可以在 Google Cloud 上最经济的 GPU 级别 NVIDIA L4 上运行，所以入门门槛很低。无论您处理数千个请求还是数十亿个请求，您的基础架构费用都是可预测的。这种模式实际上会奖励您的流量增长，而不是向您征税。</p><h3>一切尽在同一屋檐下</h3><p>如果您的数据已经存放在 Google Cloud 上的 Elasticsearch、BigQuery 或 Cloud Storage 中，那么将推理引擎保持在这些数据附近是非常合理的。通过 Model Garden 部署，Jina 搜索基础模型继承了您已经在使用的所有企业级功能：用于访问控制的身份与访问管理 (IAM)、在现有 Google Cloud 账单上的统一计费，以及接入 Vertex AI Pipelines 进行机器学习运维 (MLOps) 工作流的能力。</p><p>虽然 Jina AI Cloud API 和 Elastic Cloud 为突发流量或现有搜索工作流提供了最快的路径，但 Model Garden 对于需要严格数据安全和大规模可预测成本的企业应用来说是理想选择。Elastic 希望在任何您需要的地方满足您的需求。</p><h2><strong>Jina AI 模型</strong></h2><h3><strong>jina-embeddings-v3</strong></h3><p>我们成熟的多语言嵌入模型，具有 572M 个参数和 8K 个词元上下文。在 Massive Text Embending Benchmark (MTEB) 英语测试中获得 65.5 分。支持五种任务特定的低秩适配 (LoRA) 适配器（检索查询/段落、文本匹配、分类、集群）以及从 1024 到 64 维度的 Matryoshka 截断。该模型已通过 EIS 在 Elasticsearch 生态系统中得到广泛采用。</p><p>我们首先推出 v3，因为许多生产系统已经依赖于它。如果您正在将基于 v3 的管道迁移到 Google Cloud，您现在可以直接运行相同的模型，无需更改嵌入维度或重建索引。</p><h3><strong>jina-embeddings-v5-text（small 和 nano）</strong></h3><p>我们于 2026 年 2 月发布的第五代文本嵌入模型，达到了顶级性能，可与数倍于其规模的模型相媲美。</p><p><code>v5-text-small</code> （6.77 亿参数）：在多语言 MTEB（MMTEB）基准套件上得分为 67.0，该套件涵盖 9 种任务类型的 131 项任务；在 MTEB 英文基准上得分为 71.7。它是 MTEB 排行榜上最强大的 10 亿以下多语言嵌入模型。</p><p><code>v5-text-nano</code> （2.39 亿参数）：在 MMTEB 上得分为 65.5。没有其他 5 亿参数以下的模型能达到这一水平。其规模不到大多数同类模型的一半，是边缘计算和延迟敏感型部署的自然选择。</p><p>两种模型均支持：</p><ul><li><strong>四个特定任务的 LoRa 适配器：</strong>检索、文本匹配、分类、集群。在推理时通过 <code>task</code> 参数选择合适的适配器。</li><li><strong>Matryoshka 维度截断：</strong>将嵌入维度从 1024（nano 为 768）减少至 32。适度截断（例如 256 维）时质量损失极小。维度减半大致使存储减半。</li><li><strong>二值量化：</strong>使用二值化将 1,024 维嵌入从 2 KB 压缩到 128 字节。专门的训练使得这种压缩的损失极小。</li><li><strong>多语言：</strong>small 支持 119 种语言，nano 支持 93 种语言。</li></ul><h3><strong>jina-reranker-v3</strong></h3><p>一个 0.6B 参数的多语言 listwise 重排序器，采用<em>最后但不晚</em>的交互架构。查询和最多 64 个候选匹配项被输入到单个 131K 词元的上下文窗口中，模型在评分前执行跨文档比较。Jina Reranker v3 在 BEIR 上达到了 61.94 的 nDCG@10，优于规模大 6 倍的模型。这与 pointwise 重排序器（对每个文档单独评分）有本质区别，能产生更好的结果，尤其是对于从单个文档中进行段落检索。</p><h3><strong>jina-clip-v2</strong></h3><p>一个 0.9B 参数的多模态、多语言嵌入模型，将文本和图像映射到共享的 1024 维空间。它支持：</p><ul><li><strong>89 种语言</strong>的文本图像检索。</li><li><strong>图像分辨率为 512×512。</strong></li><li><strong>8K 词元文本输入。</strong></li><li>针对两种模态的 <strong>Matryoshka 截断</strong>（从 1024 维到 64 维）</li></ul><p>在图像到文本基准测试中表现出色，包括多语言任务。</p><h2><strong>开始使用</strong></h2><p>Jina Embeddings v3 即日起在 Model Garden 上线。以下是运行它的方法。</p><p>您需要一个启用了 Vertex AI API 的 Google Cloud 项目，以及足够的 GPU 配额用于至少一个 g2-standard-8 实例（NVIDIA L4）。如果您是 Google Cloud 的新用户，<a href="https://cloud.google.com/vertex-ai/docs/start/cloud-environment">请从设置指南开始。</a></p><p><a href="https://console.cloud.google.com/vertex-ai/publishers/jinaai/model-garden/jina-embeddings-v3">Jina Embeddings v3 的 Model Garden 页面</a> 将引导您完成整个流程：上传模型、创建终端节点、选择机器类型，然后部署。在自己的项目中打开它，并遵循指导步骤。在区域和配额允许的情况下，也可以使用 A100 和 H100 机器，但入门只需 L4 即可。</p><p>从点击到首次嵌入，整个过程只需几分钟。</p><h2><strong>后续计划</strong></h2><p>Jina Embeddings v3 是一个起点。在接下来的几周里，我们将把 Jina 检索堆栈的其余部分带到 Model Garden：v5 文本嵌入（small 和 nano）、jina-reranker-v3 以及用于多模态搜索的 jina-clip-v2。所有这些模型都将以相同的自行部署模式在单个 L4 GPU 上运行。</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Elastic Security、Observability 和 Search 现已在您的 AI 工具中支持交互式 UI。]]></title>
            <description><![CDATA[Elastic 是首家在 AI Conversations 中推出交互式 UI 的安全厂商，该功能基于 Anthropic 和 OpenAI 共同制定的 MCP 应用标准构建。与之同步发布的还有另外两个分别用于可观测性和搜索的 Elastic MCP 应用。]]></description>
            <link>https://www.elastic.co/search-labs/cn/blog/mcp-apps-elastic</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/cn/blog/mcp-apps-elastic</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[David Elgut, Graham Hudgins, Jesse Miller]]></dc:creator>
            <pubDate>Tue, 21 Apr 2026 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/55cea79fd5c554656e8605525a61d4ad3ae0ee1b-2048x1143.png?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>AI 智能体的强大与否，很大程度上取决于其返回的交互界面。如果仅以纯文本形式输出，仪表板、告警列表、调查图谱或分布式追踪等原本直观、可交互的内容将变得索然无味。对话不应仅仅局限于一问一答；否则，一旦任务复杂度增加，您就不得不切换到其他标签页、产品或身份验证环境。</p><p><a href="https://modelcontextprotocol.io/extensions/apps/overview">MCP 应用</a>改变了答案的呈现形态。工具现在可以在返回文本摘要的同时提供交互式 UI，而宿主端（如 Claude Desktop、Claude.ai、VS Code Copilot、Cursor）则会直接在对话中进行内联渲染。模型利用保留的精炼文本进行逻辑推理。用户在聊天窗口旁就能获得一个实时可交互的界面。</p><p>三个特性让这种集成方式区别于“返回 URL 的 webhook”：</p><ul><li><strong>上下文保留。</strong>UI 就在对话中。无需切换标签页，无需切换上下文。</li><li><strong>双向数据流。</strong>UI 可以调用 MCP 服务器上的工具以获取最新数据，宿主端也能将智能体的新结果推送回 UI，无需额外的 API 层或复杂的身份验证基础设施。</li><li><strong>沙盒化的信任边界。</strong>MCP 应用在由宿主管控的 iframe 中运行。它们无法访问父页面、读取 Cookie，也无法逸出其容器。</li></ul><p>安全运营依托于告警分类、调查图谱和 Attack Discovery，AI 智能体可将数百条告警关联为少数几条攻击链。可观测性意味着分布式链路追踪和时间序列下钻。在 Kibana 中构建意味着采用网格化的仪表板布局。如果将这些内容平铺成文字，就会失去其有用之处。我们为这三个平台都构建了 MCP 应用，并将它们一起开源，这样同一个对话就可以从分类队列转移到依赖关系图，再到实时仪表板，而无需离开聊天界面。</p><p>三个参考应用中的每一个都是一个为多个交互视图提供服务的 MCP 服务器，而不是一组独立的产品。仅安全应用就提供六个仪表板，它们共享同一个服务器框架、同一个工具可见性模型和同一个宿主桥接层。这种模式虽然轻量，但其提供的交互面正是价值倍增之处。</p><h2>Elastic Security MCP 应用</h2><h3>为什么这对 SOC 很重要</h3><p>当智能体告诉 SOC 分析师“<em>主机 host-314 上有 47 条告警，这是摘要</em>”时，它实际上并未开始实质性工作。它只是指出了工作的起点。真正的工作发生在告警列表、进程树、调查图谱和案例记录中。您无法仅凭一段文本就完成这些实际工作。</p><p>安全 MCP 应用程序返回了工作流本身。分析师提示代理，代理在聊天中返回了一个交互式仪表板，分析师可以在其中深入查看警报、运行威胁搜寻、关联攻击链并打开案例，所有这些操作都不会丢失对话的线索。由于所有发现、查询和案例都会回写到 Elasticsearch，同样的调查会在 Kibana 中等待，分析师可以在对话结束后接着处理。</p><h3>六个交互式仪表板</h3><p><a href="https://github.com/elastic/example-mcp-app-security">Elastic Security MCP 应用</a>提供了六个交互式组件，分别对应 SOC 的核心工作流。每个都是一个 React UI，当代理调用相应工具时，会在对话中以内联方式渲染：</p>
            <table style="border-collapse: collapse; margin: 20px 0;">
              <tbody>
                
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">工具</td><td style="border: 1px solid #ccc; padding: 4px;">功能说明</td><td style="border: 1px solid #ccc; padding: 4px;">交互式 UI</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">告警分类</td><td style="border: 1px solid #ccc; padding: 4px;">获取、筛选和分类安全警报</td><td style="border: 1px solid #ccc; padding: 4px;">严重程度分组、AI 判定卡、进程树、网络事件</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">Attack Discovery</td><td style="border: 1px solid #ccc; padding: 4px;">基于 AI 关联的攻击链分析，支持按需生成</td><td style="border: 1px solid #ccc; padding: 4px;">攻击叙事卡片，包含置信度评分、实体风险，MITRE 映射</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">案例管理</td><td style="border: 1px solid #ccc; padding: 4px;">创建、搜索和管理调查案例</td><td style="border: 1px solid #ccc; padding: 4px;">包含告警、可观测对象、评论选项卡和 AI 操作的案例列表</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">检测规则</td><td style="border: 1px solid #ccc; padding: 4px;">浏览、调优和管理检测规则</td><td style="border: 1px solid #ccc; padding: 4px;">带 KQL 搜索、查询验证和噪声规则分析的规则浏览器</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">威胁搜寻</td><td style="border: 1px solid #ccc; padding: 4px;">支持实体调查的 ES|QL 工作台</td><td style="border: 1px solid #ccc; padding: 4px;">查询编辑器、可点击实体和调查图表</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">样例数据</td><td style="border: 1px solid #ccc; padding: 4px;">针对常见攻击场景生成 ECS 安全事件。</td><td style="border: 1px solid #ccc; padding: 4px;">带有四条预设攻击链的场景选择器</td>
                    </tr>
              </tbody>
            </table>
          <p>每个工具都会返回一个紧凑的文本摘要，供模型进行推理，同时分析人员可以在交互式 UI 上操作。UI 还可以通过 MCP 主机桥在幕后获取最新数据。完整的工具模型和桥接 API 位于 <a href="https://github.com/elastic/example-mcp-app-security/blob/main/docs/architecture.md">代码库的架构文档</a>。</p><p>该应用还随附 <a href="https://github.com/elastic/example-mcp-app-security/tree/main/skills">Claude Desktop 技能</a>，以及 <code>SKILL.md</code> 文件，用于指导代理何时以及如何使用各个工具。从<a href="https://github.com/elastic/example-mcp-app-security/releases/latest">最新版本</a>下载预构建的技能 zip 压缩包。</p><h3>从告警到案例</h3><p>四项技能覆盖了 SOC 的核心工作流。每个技能都会接收一个提示，调用一个工具，并返回一个交互式仪表板，以及供模型推理的文本摘要。分析师的一天通常从告警队列开始。</p><p><strong>对告警进行分类。</strong>让代理按主机、规则、用户或时间窗口进行分类。让代理按主机、规则、用户或时间窗口进行分类。Alert Triage 技能会返回一个 AI 判定仪表板，位于原始警报列表上方。每个检测规则都有一个判定，将该规则的活动分类为良性、可疑或恶意，并附有置信度分数和推荐操作。点击任意警报即可打开包含进程树、网络事件、相关警报和 MITRE ATT&amp;CK 标签的详细视图。无需在 AI 会话与 Kibana 告警仪表板之间反复来回，一切操作都在对话中实时完成。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/b0239ea916a828ee0bfd0cc046af17330568fe09-2048x1313.png" style="width: 100%;" /><p><strong>开展威胁搜寻。</strong>让代理在您的索引中执行威胁搜寻。Threat Hunt 技能会返回一个 ES|QL 工作台，该工作台预先填充并自动执行了查询，结果中的每个实体均可点击进行深入查看。模型在表格下方写了一个简短的读数：哪些地方异常、哪些内容彼此关联、哪些值得进一步查看。然后，它会给出下一步切入点：要么更深入地研究威胁，要么在 MCP 应用中开始一项新技能，以补充迄今为止所做的工作。与之完美契合的是启动 Attack Discovery，从而为您已深入探究的告警和目前搜寻到的威胁收集更多上下文信息。与之完美契合的是启动 Attack Discovery，从而为您已深入探究的告警和目前搜寻到的威胁收集更多上下文信息。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/9f0bde828bcedb24e2b45995cfa306f03e57d300-2048x1313.png" style="width: 100%;" /><p><strong>运行 Attack Discovery。</strong>Attack Discovery 技能会触发 Attack Discovery API，并返回按优先级排序的发现列表。每个发现都是一组相互关联的警报，这些警报会被串联成一条攻击链，并会在前面优先展示 MITRE 策略、风险评分、置信度标签，以及受影响的主机和用户。智能体的摘要会按相同的排名顺序显示在发现下方，而对话现在已包含采取行动所需的一切：搜寻查询、分类决策、关联的链条，均已为下一步做好准备。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/e52af22dbaa070192170280d519dfb219ad807f5-2048x1313.png" style="width: 100%;" /><p><strong>无需离开聊天窗口即可创建案例。</strong>批量批准发现项，或让代理为特定告警创建案例。案例管理技能会根据批准的发现项创建一个案例（附带源告警，继承自攻击链的 MITRE 策略），并以内联方式渲染实时案例列表。点击案例以查看其详细视图，其中包含一行 AI 操作按钮：总结案例、建议下一步、提取 IOC 和生成时间线。每个按钮都会将一个结构化的提示放回聊天中，这样代理就能获取案例上下文，无需重新说明上下文。代理的摘要位于案例列表下方，涵盖了完整的事件响应队列，包括刚创建的案例和仍需处理的早期发现。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/2057310b450c2917c5434c713f2b34732067429a-2048x1283.png" style="width: 100%;" /><p>本演练中的每一步都运行相同的循环：收到提示后，技能接收提示，工具返回一个简洁的文本摘要供模型进行推理，同时返回一个交互式用户界面（UI）供分析师操作。形成端到端的 SOC 流程——搜寻、分类、关联、创建案例，并推动下一步。模型会在每一步中保留会话上下文。单独调用其中任何一个，它仍然是完整的仪表板，指向您指定的数据切片。无论哪种方式，工作都会在对话中积累；没有标签切换，没有复制粘贴，没有交接。</p><p>另外两项技能补全了这个应用：检测规则浏览器，用于调整噪声较高的规则；样本数据生成器，用于在新集群上生成逼真的 ECS 事件。后续文章将深入探讨全部六个方面：调查图谱、攻击流程画布和端到端演练。<br/></p><blockquote><em>“Elastic Security 的 MCP 应用弥合了自动检测和手动搜索之间的差距。”通过将安全数据直接导入 Claude Desktop 的单一接口，我们在不到一小时内发现了“无声”威胁，这些风险不会触发标准警报，但需要立即采取行动。对于我们的分析师来说，这是一个力量倍增器。”Mandy Andress：Elastic 首席信息安全官（CISO）。</em></blockquote><h3>运作方式</h3><p>每个 MCP 应用都是一个小型 Node.js 服务器，其工具会返回供模型推理的精炼文本摘要，以及由宿主内联渲染的 React UI。由于它是基于开放的 MCP 应用程序规范构建的，因此同一服务器可在任何兼容主机上运行——有关完整设计，请参阅<a href="https://github.com/elastic/example-mcp-app-security/blob/main/docs/architecture.md">软件仓库的架构文档</a>。</p><h3>试用</h3><p>需要启用 Security 的 Elasticsearch 9.x，以及用于案例、规则和攻击发现的 Kibana。最快的方式是使用<a href="https://github.com/elastic/example-mcp-app-security/releases/latest">最新版本</a>中的一键式 <code>.mcpb</code> 安装包——在 Claude Desktop 中双击它，系统会提示您输入 Elasticsearch URL 和 API 密钥。<a href="https://github.com/elastic/example-mcp-app-security/blob/main/docs/setup-cursor.md">Cursor</a>、<a href="https://github.com/elastic/example-mcp-app-security/blob/main/docs/setup-vscode.md">VS Code</a>、<a href="https://github.com/elastic/example-mcp-app-security/blob/main/docs/setup-claude-code.md">Claude Code</a>、<a href="https://github.com/elastic/example-mcp-app-security/blob/main/docs/setup-claude-ai.md">Claude.ai</a> 和从源代码构建的设置指南都在<a href="https://github.com/elastic/example-mcp-app-security">软件仓库</a>中。</p><a href="https://videos.elastic.co/watch/voDxV9RWWpY9HeBYTWTS93">Watch video on Vidyard</a><h2>Elastic Search MCP 应用程序：通过对话构建的仪表板</h2><p>Kibana 用户都深知创建仪表板的繁琐：必须停下工作，打开 Kibana，选择索引、字段和可视化效果，最后再调整保存。一个图表还没显示出来，就已经经历了五次上下文切换。</p><p>新的 <a href="https://github.com/elastic/example-mcp-dashbuilder">example-mcp-dashbuilder</a> 参考应用将其简化为一个提示。要求代理“<strong>为我构建一个包含收入指标、订单趋势和类别细分的仪表板</strong>”，而且仪表板会在对话中返回，无需切换标签。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/b096b493726488f53040b08b5c8bc53ca0e44f9d-2048x1182.png" style="width: 100%;" /><p>在该提示后，代理通过 ES|QL 探索您的 Elasticsearch 数据，并选择与数据相匹配的图表类型：用于比较的条形图、用于趋势的折线图、用于 KPI 的指标卡，以及用于二维模式的热力图。它在 Kibana 的 48 列网格上布置面板，使用 Elastic UI Borealis 主题，结果是完全交互式的：您可以直接在聊天中拖动、调整大小和将面板分组到可折叠的部分中。当仪表板看起来合适时，只需一次工具调用即可将其导出到 Kibana，同时保留 ES|QL 查询和自定义颜色。您还可以将现有的 Kibana 仪表板导入到聊天中，以便进行 AI 辅助编辑。</p><p>其原理与 Security 应用相同：当产物本身就是产品时，在对话中直接返回它，就能打通从描述需求到看到结果之间的闭环。</p><p>在底层，它遵循相同的 MCP 应用模式。Node.js 服务器注册一个面向模型的 <code>view_dashboard</code> 工具，以及一组仅供应用内部调用、由 UI 直接调用的工具（数据获取、布局持久性、时间字段检测、导出/导入）。仪表板视图本身是一个单一的独立 HTML 文件，与 <code>vite-plugin-singlefile</code> 捆绑在一起，并作为 MCP 应用资源提供。开发者复刻（fork）该代码库后，会获得与 Security 应用中相同的服务器 shell 和主机桥，但面向不同的任务场景。example-mcp-dashbuilder README 文件包含了完整的架构和图表类型参考。</p><a href="https://videos.elastic.co/watch/wep6PxPiEDyyvsYTKq6v5B">Watch video on Vidyard</a><h2>Elastic Observability MCP 应用</h2><p>第三个参考应用 <a href="https://github.com/elastic/example-mcp-app-observability">Elastic Observability MCP 应用</a>则是针对 SRE 场景解决这一“交互形态”问题的对应版本。当生产环境中出现故障时，值班工程师需要的答案不是一张图表，而是由 K8s 指标、APM 拓扑、ML 异常、风险评估等信息拼接而成的诊断。答案的形式是一个因果故事：什么失败了，为什么，什么取决于它，以及下一步该怎么做。</p><p>六种支持可观测调查工作流的工具</p>
            <table style="border-collapse: collapse; margin: 20px 0;">
              <tbody>
                
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">工具</td><td style="border: 1px solid #ccc; padding: 4px;">依赖关系</td><td style="border: 1px solid #ccc; padding: 4px;">它的功能是什么</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">观测</td><td style="border: 1px solid #ccc; padding: 4px;">通用</td><td style="border: 1px solid #ccc; padding: 4px;">瞬时 ES|QL + ML 异常访问原语——执行一次查询、实时采样指标，或一直等待直到阈值触发或异常出现。</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">管理警报</td><td style="border: 1px solid #ccc; padding: 4px;">通用</td><td style="border: 1px solid #ccc; padding: 4px;">创建、列出、获取和删除 Kibana 自定义阈值告警规则。省略 Kibana URL 以运行为只读模式。</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">ML 异常</td><td style="border: 1px solid #ccc; padding: 4px;">ML 作业</td><td style="border: 1px solid #ccc; padding: 4px;">查询 ML 异常记录，并打开内联的异常说明视图。</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">apm-health-summary</td><td style="border: 1px solid #ccc; padding: 4px;">Elastic APM</td><td style="border: 1px solid #ccc; padding: 4px;">基于 APM 遥测的集群级健康汇总；在可用时还会纳入 K8s 和 ML 上下文的各层信息。</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">apm-service-dependencies</td><td style="border: 1px solid #ccc; padding: 4px;">Elastic APM</td><td style="border: 1px solid #ccc; padding: 4px;">服务依赖图——上游/下游、协议、调用量。</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">k8s-blast-radius</td><td style="border: 1px solid #ccc; padding: 4px;">Kubernetes 指标</td><td style="border: 1px solid #ccc; padding: 4px;">节点故障影响——完全中断、性能降级、不受影响、重调度可行性。</td>
                    </tr>
              </tbody>
            </table>
          <h3><strong>集群健康状况汇总</strong></h3><p>询问“哪里出故障了？”或“给我一份状态报告”，即可一目了然地掌握全局：包括整体健康状态标识、性能下降的服务及其原因、内存占用最高的 pod、异常严重程度分解，以及服务吞吐量——全部在一个内联视图中。这是当您感觉有些不对劲，但又不知道该从哪里入手的时候的起点。视图会根据您的部署支持的内容进行调整。APM 为您提供服务运行状况。Kubernetes 指标添加了 pod 和 Node 上下文。ML 作业也会将异常纳入其中。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/f31be8bbcffd54623fcec4e51fd914dae666c026-1510x1810.png" style="width: 100%;" /><h3><strong>服务依赖关系图表</strong></h3><p>问“哪些服务会调用 checkout？”或者“显示拓扑结构”并获取分层依赖图表——上游调用者、下游依赖项、协议、每条边的调用量和延迟。让我们让 Claude “给我看看前端的服务依赖关系”：</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/2afec7dbd2153bc3a9f05068f323911af766a258-2048x1184.png" style="width: 100%;" /><p>缩放、平移和悬停以获取理解复杂服务关系所需的所有详细信息：</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/ff99d2fd0a54c22c9f7b975e9d8d7073c4311e7a-2048x970.png" style="width: 100%;" /><h3><strong>利用影响半径评估风险</strong></h3><p>询问“如果我的 k8s Node 宕机会发生什么？”并获取一个辐射状影响图：目标 Node 位于中心，完全中断的部署用红色表示，性能下降的用琥珀色表示，未受影响的用灰色表示。浮动摘要卡显示有风险的 pod 和重新调度的可行性。单副本部署被标记为单点故障。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/7736bef9609e45a3f0c7f9f3090a314b0834315d-1272x1838.png" style="width: 100%;" /><h3><strong>观测</strong></h3><p>代理访问 Elastic 的主要方式——一个工具，三种模式，满足三种不同需求。说“CPU 当前情况如何？”，它会运行一次 ES|QL 查询并返回一个表。说“显示我接下来 60 秒的前端延迟”，它会实时采样该指标，并原地更新图表。说“当内存降到 80MB 以下时告诉我”或“在接下来 10 分钟内留意任何异常”，它会一直等待，直到条件触发或时间窗口结束。视图可根据模式进行调整：用于单次查询的结果表、用于采样和阈值条件的带当前/峰值/基线统计数据的实时趋势图，以及用于异常模式的按严重程度评分的触发卡。</p><h3>运作方式</h3><p>与 Security 和 Search 应用相同的 MCP 应用模式：一个 Node.js 服务器，六个面向模型的工具，对应六个单文件视图资源。工具按部署后端（通用、APM 依赖、K8s 依赖、ML 依赖）进行分组，因此代理和用户都可以提前知道哪些工具适用于特定部署，而不是在调用时发现功能缺口。MCP 应用还包含一个示例的 Agent Builder 工作流：<code>k8s-crashloop-investigation-otel</code> 可以在 Kubernetes 告警触发时启动，并在您打开任何仪表板前返回结构化的根因摘要。</p><a href="https://videos.elastic.co/watch/xcF3nKDziVgphc5bPAkv9R">Watch video on Vidyard</a><h2>代理化技术栈，交互式</h2><p>关于此模式的三个属性值得直接说明。首先，工具结果不再是工作的终点，而是工作的起点：对话返回的是您可以采取行动的接口，而不是您必须采取行动的摘要。其次，同一个智能体、同一个模型上下文和同一个对话线程，现在可以在 Security、Search 和 Observability 界面之间顺畅切换，而无需离开对话。第三，这之所以可行，是因为 Elasticsearch 和 Kibana 已经公开了 API。MCP 应用程序是我们已提供的产品功能之上的一个轻量级的交互层。</p><p>Attack Discovery 已经为该应用内的关联发现视图提供支持。在堆栈内部，同样的代理模式进一步延伸：Elastic 工作流自动执行确定性步骤（丰富实体、创建案例、隔离主机），而 Agent Builder 则对数据进行推理并调用这些工作流作为工具。MCP 应用将同一套安全界面引入外部对话；工作流和 Agent Builder 则在技术栈内部对其进行扩展。不同的入口点，底层是相同的 Elastic API。</p><p><strong>试用：</strong></p><ul><li><strong>安全性：</strong><a href="https://github.com/elastic/example-mcp-app-security">example-mcp-app-security</a></li><li><strong>搜索与仪表板：</strong> <a href="https://github.com/elastic/example-mcp-dashbuilder">example-mcp-dashbuilder</a></li><li><strong>可观测性：</strong><a href="https://github.com/elastic/example-mcp-app-observability">example-mcp-observability</a></li></ul><p>还没有 Elasticsearch 集群吗？<a href="https://cloud.elastic.co/registration">开始免费试用 Elastic Cloud</a>。如需进一步了解安全应用背后的构建模块，请参阅相关 Security Labs 文章：<a href="https://www.elastic.co/security-labs/from-alert-fatigue-to-agentic-response">Elastic 工作流和 Agent Builder</a>、<a href="https://www.elastic.co/security-labs/agent-skills-elastic-security">Agent 技能</a>，以及<a href="https://www.elastic.co/security-labs/speeding-apt-attack-discovery-confirmation-with-attack-discovery-workflows-and-agent-builder">攻击发现</a>。</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[为 Elastic Cloud Serverless 和 Elasticsearch 引入统一的 API 密钥。]]></title>
            <description><![CDATA[了解 Elastic 如何通过全局分布式 IAM 架构，在 Serverless 中统一控制平面与数据平面的身份验证。使用同一 API 密钥访问 Cloud 和 Elasticsearch API。]]></description>
            <link>https://www.elastic.co/search-labs/cn/blog/elastic-cloud-api-keys-unified-serverless</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/cn/blog/elastic-cloud-api-keys-unified-serverless</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[ Alex Chalkias]]></dc:creator>
            <pubDate>Mon, 20 Apr 2026 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/864e229f00eb2018084f13dd7f0e390e18383ed4-1980x1188.png?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>假设您是一名站点可靠性工程师 (SRE)，负责管理不断增长的 Elastic Cloud Serverless 项目组合：用于生产基础架构的 Elastic Observability、用于安全运营中心 (SOC) 团队的 Elastic Security，以及用于面向客户应用程序的 Elasticsearch。每个项目都有自己专用的 Elasticsearch API 密钥。您的持续集成和持续交付（CI/CD）管道需要单独一个 Cloud API 密钥来配置和管理这些项目。每季度一次的密钥轮换日到来时：您需要逐个检查每个项目，生成新密钥，更新 Terraform 状态，重新部署管道，并希望一切不出纰漏。当凌晨 2 点发生故障，需要快速撤销访问权限时，您不得不对照一份电子表格来确认哪个密钥属于哪个项目、哪个服务。</p><p>如今，这一切变得简单得多。<strong>Elastic Cloud API 密钥</strong>现在可以直接在<strong> Elastic Cloud Serverless</strong> 上对<strong> Elasticsearch 和 Kibana</strong> API 进行身份验证。您现在可以使用单一凭证来管理组织的资源<em>并</em>执行数据操作，例如 Elasticsearch 查询语言 (ES|QL) 查询、数据摄取和告警。</p><p>下面我们来看看我们构建这一功能的原因、如何设计全局分布式身份层来实现这一目标，以及它如何为跨项目搜索奠定基础。</p><h2>秘密管理负担</h2><p>围绕数据平台构建可靠的 CI/CD 管道、GitOps 工作流或 Terraform 自动化，都伴随着一项隐性成本：秘密信息蔓延。</p><p>在旧模式下，开发人员面临割裂的身份验证体验：</p><ul><li><strong>控制平面 (Elastic Cloud API 密钥)：</strong>组织级密钥，组织级作用域的密钥，用于通过 <a href="https://www.elastic.co/docs/api/doc/cloud/">Elastic Cloud API</a> 创建项目、邀请用户和管理计费。</li><li><strong>数据平面（Elasticsearch API 密钥）：</strong> 项目范围密钥是在特定的 Serverless 项目中创建的，<em>用于</em>与 <a href="https://www.elastic.co/docs/api/doc/elasticsearch-serverless/">Elasticsearch</a> 和 <a href="https://www.elastic.co/docs/api/doc/serverless">Kibana</a> API 进行交互。</li></ul><p>这意味着您的部署脚本必须对 Elastic Cloud 进行身份验证，配置 Serverless 项目，从该特定项目中提取新生成的 Elasticsearch API 密钥，然后将<em>该密钥</em>注入下游应用程序或自动化工具，从而导致复杂的管道、分散的审计日志以及更高的凭证泄露风险。</p><h2>Elastic Cloud Serverless 中的统一身份验证</h2><p>通过此次发布，Serverless 项目的拆分问题将不复存在。您现在可以创建一个明确授权用于 <strong>云、Elasticsearch 和 Kibana API</strong> 的 Elastic Cloud API 密钥。</p><ul><li><strong>以前：</strong>Elastic Cloud API 密钥严格来说是控制平面令牌。它可以创建项目、管理计费和邀请用户，但存在一个硬边界：它不能用于调用这些项目内部的 Elasticsearch 或 Kibana API。您始终需要第二个特定于项目的密钥来执行数据操作。</li><li><strong>现在：</strong> 在创建 Elastic Cloud API 密钥时，选择 <strong>Cloud、Elasticsearch 和 Kibana API</strong> 访问权限，Serverless 的硬边界就被移除了。该 API 密钥成为一个真正统一的凭证。它保留了管理组织基础架构的能力，同时获得了跨任何已授权 Serverless 项目进行查询、摄取和分析数据的原生访问能力。</li></ul><p>通过将这一切统一到单个 Elastic Cloud API 密钥之下，您获得了一个统一的身份，可以作为一个整体进行范围限定、审计、轮换和撤销。每个 API 调用——无论是配置新项目还是运行 ES|QL 查询——都会在审计日志中显示为使用同一凭证，从而在事件调查或合规性审查期间为您提供单一的追踪线索。凭证轮换成为一步操作，而无需跨独立的控制平面和数据平面秘密信息进行协调更新。而且由于角色分配是按项目进行的，一个密钥可以跨多个项目使用——在您的可观测项目中管理数据摄取，在安全项目中运行查询——无需为每个项目分别管理不同的凭证。</p><p>重要的是，<em>统一</em>并不意味着<em>全部权限</em>。通过使用 <code>role_assignments</code> 有效负载，您可以将统一密钥严格限定到单个项目和特定角色（例如只读），从而确保即使凭证泄露，其影响范围也能得到完全控制。如果某位开发人员离职或某个应用程序被停用，您可以在 Elastic Cloud Console 中撤销单个密钥，立即终止对控制平面以及所有关联 Elasticsearch 项目的访问。</p><p><em>(注意：对于 Elastic Cloud Hosted / 托管部署，Cloud API 密钥仍仅用于控制平面管理。计划在未来的版本中支持将其扩展到托管堆栈 API。）</em></p><h2>自动化您的工作流</h2><p>入门很简单。您可以完全通过 Elastic Cloud 控制台进行配置，也可以使用 Elastic Cloud <a href="https://www.elastic.co/docs/deploy-manage/api-keys/elastic-cloud-api-keys">API</a> 对其进行自动配置。</p><p>UI 流程保持不变，但现在您可以在项目角色分配下选择 <strong>Cloud、Elasticsearch 和 Kibana API</strong> 访问权限。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/c4f802f130655290cd474b283001a954d14c3088-2801x1681.png" style="width: 100%;" /><p>下面说明如何使用 Elastic Cloud API 以编程方式创建统一密钥。请注意 <code>application_roles</code> 数组，正是它授予了密钥对 Elasticsearch 数据平面的原生访问权限：</p><pre><code>curl -X POST \
  -H "Content-Type: application/json" \
  -H "Authorization: ApiKey $EC_API_KEY" \
  "https://api.elastic-cloud.com/api/v1/users/auth/keys" \
  -d '{
    "description": "unified-automation-key",
    "expiration": "90d",
    "role_assignments": {
      "project": {
        "elasticsearch": [
          {
            "role_id": "elasticsearch-admin",
            "organization_id": "YOUR_ORG_ID",
            "all": false,
            "project_ids": ["YOUR_PROJECT_ID"],
            "application_roles": ["admin"]
          }
        ]
      }
    }
  }'</code></pre><p>一旦创建，您只需在 <code>Authorization: ApiKey</code> 标头中向 <code>api.elastic-cloud.com</code> 以及您的特定 Serverless Elasticsearch 终端传递完全相同的这个密钥即可。</p><h2>底层实现：构建分布式身份层</h2><p>使一个 Cloud API 密钥能够在控制平面和数据平面同时工作，并不像传递一个令牌那么简单。这需要解决一个根本性的分布式系统挑战。</p><p>过去，Cloud API 密钥存储在一个中心化的全局安全集群中。这对于可以接受较高延迟的控制平面操作来说没有问题。然而，Elasticsearch 数据请求要求超低延迟。我们不能为了验证每个搜索查询或摄取请求而在全局范围内往返访问中央控制平面。</p><p>为了解决这个问题，我们引入了一种由全局分布式数据存储提供支持的新身份验证架构。下面的序列图展示了一个客户端使用 Elastic Cloud API 密钥发送 Elasticsearch 查询的过程，说明了身份验证完全在本地区域内完成，无需往返全局控制平面。Elasticsearch 将身份验证委托给区域 IAM 服务，该服务针对全局分布式数据库的本地副本验证密钥并解析其角色分配。一旦授权通过，Elasticsearch 执行查询并将结果返回给客户端。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/3e38d7a862b9981523c5393c441b92eae13aeb90-2401x1351.webp" style="width: 100%;" /><h3>全局分布式持久化</h3><p>Elastic Cloud API 密钥及其关联的角色定义不再仅依赖集中式安全集群，而是持久化存储在全球分布的高可用数据库中。该数据库在全球控制平面与实际运行您的 Serverless 项目的区域数据平面之间同步身份与访问管理 (IAM) 数据。</p><h3>使用区域 IAM 进行本地验证</h3><p>当您的客户端使用 Elastic Cloud API 密钥向 Elasticsearch 发送请求时，该请求不会返回全局控制平面。相反，它会被路由到新的区域 IAM 服务。它会验证本地数据库副本中的密钥，确保身份验证几乎无延迟，并且完全不受全局控制平面故障的影响。</p><h3>动态角色映射</h3><p>身份验证只是成功的一半；系统还需要对请求进行授权。区域 IAM 服务可立即将您的云端角色分配 (例如 <code>application_roles</code>) 转换为原生 Elasticsearch 权限。Elasticsearch 随后可在本地授权并执行请求，完全无需本地 <code>.security</code> 索引。</p><h2>跨项目搜索的基础</h2><p>这种分布式身份架构是 Elastic 平台未来发展的基础构建块。</p><p>由于身份和访问权限现在统一且全局同步，我们拥有了在不同项目之间安全传递您身份所需的框架。这为 Serverless 即将推出的 <strong>跨项目搜索 (CPS)</strong> 功能提供了支持。</p><p>借助 CPS，您将能够查询跨越多个远程 Serverless 项目的数据，例如将安全负载和可观测负载组合起来，就像它们是单一数据集一样简单。通过依赖统一 API 密钥，系统可以自动评估您在所有项目上的权限，而无需您在每个目标项目上配置复杂的信任关系、证书或重复的凭证。</p><h2>了解详情</h2><p>准备好简化您的技术栈了吗？</p><ul><li>请参阅 <a href="https://www.elastic.co/docs/deploy-manage/api-keys/elastic-cloud-api-keys">Elastic Cloud API 密钥文档</a>，了解如何分配堆栈访问权限。</li><li>请参考 <a href="https://www.elastic.co/docs/api/doc/cloud/operation/operation-create-api-key">Create API key（Elastic Cloud API）</a> 文档，以实现密钥自动生成。</li><li>查看 <a href="https://www.elastic.co/docs/deploy-manage/api-keys">Elastic API 密钥</a>，了解 Elastic 平台中各类密钥的完整对比。</li></ul><p>立即开始在 <a href="https://cloud.elastic.co/registration">Elastic Cloud</a> 上构建或继续您的构建之旅。</p><h2>免责声明</h2><p>本文中描述的任何功能或功能性的发布和时间均由 Elastic 自行决定。当前尚未发布的任何功能或功能性可能无法按时提供或根本无法提供。</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[基于 Elasticsearch + Jina 嵌入的无监督文档集群]]></title>
            <description><![CDATA[一种使用 Elasticsearch 和 Jina 嵌入进行无监督文档集群的实用、可复现方法。]]></description>
            <link>https://www.elastic.co/search-labs/cn/blog/unsupervised-document-clustering-elasticsearch-jina-embeddings</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/cn/blog/unsupervised-document-clustering-elasticsearch-jina-embeddings</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Matthew Adams]]></dc:creator>
            <pubDate>Fri, 10 Apr 2026 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/662c00694c3e0c2fb2128098bdb6813df9e86a72-1280x720.png?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>向量搜索从查询开始，但如果您没有查询呢？</p><p>组织往往会积累大量文档，例如支持工单、法律文件、新闻资讯和研究论文；在提出正确的问题之前，首先需要了解这些文档里都包含哪些内容。没有标签或训练数据，手动审查数千份文档是不切实际的。当您不知道要搜索什么时，传统搜索无济于事。</p><p>本文将介绍一种 Elasticsearch 原生方法，用于无监督文档集群和时序故事追踪，帮助解决这一发现难题。读完本文后，您就可以像这样跨天追踪故事脉络：</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/100a60a7fb85da8ab3813fd071a82c93f2c3f318-1300x650.png" style="width: 100%;" /><p><strong>您将发现：</strong></p><ul><li>为什么当您希望在没有查询的情况下进行主题发现时，<strong>集群嵌入</strong>（而非检索嵌入）至关重要。</li><li>如何借助 Elasticsearch 的 k 近邻 (kNN) 和批量 <code>msearch</code>，通过密度探测质心分类按主题对文档进行分组。</li><li><a href="https://www.elastic.co/docs/reference/aggregations/search-aggregations-bucket-significanttext-aggregation"><code>significant_text</code></a> 如何自动为集群添加标签，让主题在无需训练模型的情况下也能清晰呈现。</li><li>时间故事链如何将每日集群联系起来，展示主题如何逐日演变。</li></ul><div style="display:none">Unknown block type "infoPanel", specify a component for it in the `components.types` option</div><p>该管道以来自 BBC News 和 The Guardian 的约 8,500 篇 2025 年 2 月文章作为测试语料库。新闻之所以适合作为示例，是因为它具有清晰的时间演化特征；而在任何文档发现至关重要的场景中，这种模式同样适用，例如法律审查、合规监控、研究整合和客户支持分流。</p><p><strong>技术栈：</strong></p><ul><li><a href="https://www.elastic.co/search-labs/blog/jina-embeddings-v5-text"><strong>Jina v5</strong></a> <strong>集群嵌入：</strong>用于主题分组的任务专用低秩自适应 (LoRA) 适配器。<a href="https://www.elastic.co/blog/elastic-jina-ai">Jina 已加入 Elastic</a>，其模型可通过 <a href="https://www.elastic.co/docs/explore-analyze/elastic-inference/eis">Elastic Inference Service (EIS)</a> 原生调用。</li><li><strong>Elasticsearch：</strong>可扩展的 <a href="https://www.elastic.co/docs/solutions/search/vector/knn">kNN</a>、<code>significant_text</code> 标签生成和向量存储。</li><li><a href="https://www.elastic.co/search-labs/blog/diskbbq-elasticsearch-introduction"><strong>DiskBBQ：</strong></a>一种基于磁盘的向量索引格式，结合了 <a href="https://www.elastic.co/docs/reference/elasticsearch/index-settings/bbq">Better Binary Quantization (BBQ)</a> 与分层 k-means 分区，以加速近似最近邻 (ANN)。这种索引分区是向量搜索的内部机制，与本文使用的密度探测集群算法相互独立。与 <code>bbq_hnsw</code> 相比，<code>bbq_disk</code> 将量化向量存储在磁盘上，并仅在堆内存中保留分区元数据，在保持高召回率的同时，大幅降低了资源需求。</li><li><strong>全局集群 + 每日时间链接：</strong>发现与故事演变。</li></ul><p><strong>您需要：</strong></p><ul><li>Elasticsearch 部署（Elastic Cloud、Elasticsearch Serverless 或 Elastic 自托管 8.18+/9.0+）：<code>bbq_disk</code> 需要 8.18 或更高版本。可选的 diversify retriever 部分需要 9.3+ 或 serverless。</li><li><a href="https://jina.ai/embeddings/">Jina API 密钥</a>：免费层级包含 1,000 万个 token，足以覆盖核心集群管道的需求（约 425 万个 token）。可选的 retrieval-versus-clustering 对比需要进行第二轮嵌入计算。</li><li><a href="https://bonobo.capi.gutools.co.uk/register/developer">Guardian API 密钥</a>（免费）。</li></ul><h2>设置</h2><p>安装所需软件包：</p><pre><code>pip install elasticsearch pandas numpy plotly umap-learn python-dotenv pydantic-settings datasets requests</code></pre><p>可选（仅当您从此仓库运行抓取帮助程序时）：</p><pre><code>pip install beautifulsoup4</code></pre><p>然后在项目根目录的 <code>.env</code> 文件中配置 API 密钥：</p><pre><code>ELASTIC_CLOUD_ID=your-cloud-id        # or ELASTIC_HOST=https://...
ELASTIC_API_KEY=your-api-key
JINA_API_KEY=your-jina-key
GUARDIAN_API_KEY=your-guardian-key</code></pre><p>此笔记本调用 <code>load_dotenv(override=True)</code>，因此本地 <code>.env</code> 值优先。</p><pre><code>Connected to Elasticsearch</code></pre><h2>第 1 部分：发现式集群 —— 为什么要使用集群嵌入？</h2><p>大多数向量搜索都会使用经过训练的<strong>检索嵌入</strong>来将<em>查询</em>与相关<em>文档</em>进行匹配。这对于搜索非常合适，但并不适合用于发现。当您希望在没有任何查询的情况下找到语料库中的主题时，您需要使用能将相似文档组合在一起的嵌入。</p><p>Jina v5 通过<strong>面向特定任务的低秩适配 (LoRA) 适配器</strong>解决了这个问题。LoRA 在保持大部分基础模型权重冻结的同时，对目标内部层添加小幅低秩更新，使模型行为转向特定任务，而无需完全重新训练。同一基模型根据 <code>task</code> 参数产生不同的嵌入：</p>
            <table style="border-collapse: collapse; margin: 20px 0;">
              <tbody>
                
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">任务</td><td style="border: 1px solid #ccc; padding: 4px;">训练用于</td><td style="border: 1px solid #ccc; padding: 4px;">用例</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">检索.段落</td><td style="border: 1px solid #ccc; padding: 4px;">查询-文档匹配</td><td style="border: 1px solid #ccc; padding: 4px;">搜索，检索增强生成 (RAG)</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">聚类</td><td style="border: 1px solid #ccc; padding: 4px;">主题分组（针对紧密集群进行优化）</td><td style="border: 1px solid #ccc; padding: 4px;">发现与分类</td>
                    </tr>
              </tbody>
            </table>
          <p>集群适配器经过训练，使相同主题的文档在嵌入空间中<em>更接近</em>，而不同主题的文档则<em>更远离</em>。下面的可视化对比会更直观地呈现这种差异。</p><h3>检索与集群：可视化对比</h3><p>为了展示这种差异，我们分别使用两种任务类型对文档样本进行嵌入。集群在原始 1024 维嵌入空间中执行；Uniform Manifold Approximation and Projection (UMAP) 仅用于将这些嵌入投影到 2D 进行可视化。UMAP 保留局部邻域结构，因此可用于比较集群的分离程度。</p><p>下图展示了同一组 480 篇文档样本分别采用两种任务类型进行嵌入后，再通过 UMAP 投影到 2D 的结果。请观察集群面板中那些更紧密、彼此分离更明显的颜色分组。</p><pre><code>    Full dataset: 8,495 articles
    Sources: guardian: 5749, bbc: 2746
    Date range: 2025-02-01 to 2025-02-28


    Sample: 480 docs across 8 sections
    section
    Film              60
    World news        60
    Australia news    60
    Opinion           60
    Football          60
    US news           60
    Sport             60
    Business          60


    Clustering embeddings: 480
    Retrieval embeddings:  480


    UMAP projection complete</code></pre><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/9bcf7a744900560c1c6c63a2dc3af2f9bfd33e11-1100x500.png" style="width: 100%;" /><p><em>检索嵌入（左）会更分散地铺开各个主题；集群嵌入（右）则会基于相同文档形成更紧密、彼此分离更明显的分组。</em></p><p>集群嵌入能够形成更紧密、视觉上也更清晰的分组。检索嵌入会更均匀地分布各个主题，因此非常适合搜索（细粒度相似度）；但对发现来说，更关键的是紧密的主题集群。</p><p>这就是为什么在本演练的其余部分中使用 <code>task=&quot;clustering&quot;</code> 的原因。</p><h3>加载数据集</h3><p>该语料库结合了 2025 年 2 月的两个新闻来源：</p><ul><li><strong>BBC News</strong>通过<a href="https://huggingface.co/datasets/RealTimeData/bbc_news_alltime">RealTimeData/bbc_news_alltime</a>HuggingFace 数据集。</li><li><strong>The Guardian</strong> 通过 <a href="https://open-platform.theguardian.com/">Guardian Open Platform API</a>。</li></ul><p>纳入多个来源，有助于验证集群识别出的是真正的<em>主题</em>，而不是<em>某个来源特有的写作风格</em>。</p><pre><code>    Total articles:  8,495
    
    Source breakdown:
    source
    guardian    5749
    bbc         2746
    
    Date range: 2025-02-01 → 2025-02-28
    Days covered: 28
    
    Sample article:
      Source:  guardian
      Title:   Carbon monoxide poisoning ruled out in death of Gene Hackman and wife, police sa
      Section: Film
      Text:    Authorities have ruled out that Gene Hackman and his wife, Betsy Arakawa, died from carbon monoxide poisoning earlier this week in their home in Santa Fe, New Mexico. The Santa Fe county sheriff, Adan...</code></pre><h3>使用集群任务进行嵌入</h3><p>在调用 Jina v5 API 处理所有文档时均会传入 <code>task=&quot;clustering&quot;</code>。嵌入会缓存到磁盘，因此后续运行会完全跳过 API。</p><p>API 调用很简单。<code>task</code> 参数是与典型嵌入使用的关键区别：</p><pre><code>payload = {
    "model": "jina-embeddings-v5-text-small",
    "input": texts,
    "task": "clustering",  # ← This selects the clustering LoRA adapter
}</code></pre><p>以下时间反映的是缓存命中情况。第一次对 API 运行时间更长，具体取决于语料库大小。</p><pre><code>    Embeddings ready: 8,495 vectors of dimension 1024
    Time: 0.6s</code></pre><h3>索引到单个 Elasticsearch 索引</h3><p>对于发现式集群，整个月的数据都会写入同一个索引 (<code>docs-clustering-all</code>)。每日分区会在后续阶段进行，用于实现时间故事链接。</p><p>索引映射对向量字段使用 <a href="https://www.elastic.co/docs/reference/elasticsearch/index-settings/bbq"><code>bbq_disk</code></a>：</p><pre><code>{
  "embedding": {
    "type": "dense_vector",
    "dims": 1024,
    "index": true,
    "similarity": "cosine",
    "index_options": {
      "type": "bbq_disk"        // hierarchical k-means partitioning for ANN index lookup; separate from this post's clustering algorithm
    }
  }
}</code></pre><p>1024 维 float32 向量大小为 4 KB。 <a href="https://www.elastic.co/search-labs/blog/diskbbq-elasticsearch-introduction"><code>bbq_disk</code></a> 使用分层 k-means 将向量划分为小集群，对其进行二进制量化，并将全精度向量存储在磁盘上以便进行二次评分。只有分区元数据保留在堆内存中，因此即使面对大型语料库，内存需求仍然较低。对于能够承受更多堆内存的工作负载，<a href="https://www.elastic.co/docs/reference/elasticsearch/index-settings/bbq"><code>bbq_hnsw</code></a> 构建分层可导航小世界 (HNSW) 图，以实现更快的查找，但资源消耗更高。</p><p><a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/dense-vector"><code>dense_vector</code></a> 字段类型支持多种量化策略：<code>bbq_disk</code> 和 <code>bbq_hnsw</code> 最适合高维嵌入，例如此处使用的 1,024 维向量。</p><pre><code>    Indexed 8,495 documents into docs-clustering-all
    Time: 57.5s</code></pre><h3>集群：基于密度探测的质心分类</h3><p>传统的集群算法（如 HDBSCAN）假设您可以将完整的 N×d 向量矩阵保存在内存中，并运行重复的完整遍历更新。对于 8,495 篇 1024 维文档而言，这一规模尚可管理（约 35 MB）；但如果没有额外基础设施，这种方法就无法扩展到数百万篇文档。</p><p>从概念上看，该算法类似于采用 Voronoi 分配和噪声底限的 KMeans++ 初始化；但它将 Elasticsearch <a href="https://www.elastic.co/docs/solutions/search/vector/knn">kNN 搜索</a>作为计算原语，因此几乎所有工作都在服务器端完成。</p><ol><li><strong>抽取 5% 的文件</strong> 作为密度探针（随机抽样，至少 50 个）。</li><li><strong>通过批量</strong> <a href="https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-msearch"><strong><code>msearch</code></strong></a> <strong>kNN</strong> 查询探测密度。每个探针发出 kNN 查询，并记录其邻居的平均相似度。高平均相似度 = 嵌入空间中的稠密区域。<a href="https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-msearch"><code>msearch</code></a> 在单个 HTTP 调用中发送多个搜索请求，这一点至关重要：密度探测生成数百个 kNN 查询，批量处理可避免每个请求的开销。</li><li><strong>通过多样化策略选择高密度种子</strong>：将密度高于中位数的候选种子按密度从高到低排序，只有当它与每个现有种子的余弦相似度都低于分离阈值时，才按贪婪策略予以接受。这是唯一的客户端计算（8k 文档约 0.01 秒）。</li><li><strong>通过</strong> <strong><code>msearch</code></strong> <strong>kNN</strong> 按质心对所有文档进行分类：每个种子都作为一个质心，kNN 搜索会检索相似度高于阈值的邻近文档。每个文档都会被分配给返回该文档且得分最高的质心。小集群被归为噪声。</li></ol><p>Elasticsearch 负责处理核心计算：使用 <code>msearch</code> 进行密度探测和分类，并使用 <code>significant_text</code> 生成标签。对于该语料库（8,495 个文档），5% 的密度探针样本会发起 425 个 kNN 探针查询，<code>msearch</code> 会将其批量处理为 9 次 HTTP 调用（批大小为 50），从而避免每个探针单独发起一次请求的开销。再结合 <code>bbq_disk</code> ANN 查找，整个集群阶段便能兼顾速度与可扩展性。在集群过程中，kNN 查询会使用尽可能小的 <a href="https://www.elastic.co/docs/deploy-manage/production-guidance/optimize-performance/approximate-knn-search"><code>num_candidates</code></a> 值来提升速度；而在生产环境的搜索查询中，则应使用更高的 <code>num_candidates</code> 值，以牺牲一定延迟为代价换取更高的召回率。</p><p>集群的自然大小由每个质心周围的嵌入空间密度决定，而非硬性的 <code>k</code> 上限。主题越密集的区域，形成的集群就越大；而越小众的主题，则会形成更小的集群。</p><h4>为什么不选择 KMeans 或 HDBSCAN？</h4><p>KMeans 假设集群为球形，并需要将完整的 N×d 矩阵加载到内存中。对于适合内存的语料库，<a href="https://scikit-learn.org/stable/modules/generated/sklearn.cluster.HDBSCAN.html">HDBSCAN</a> 是一个强有力的替代方案。它既可以处理任意形状的集群，也具备更易理解的密度语义。</p><p>密度探测质心方法面向的是另一类场景：您希望在同一系统中完成存储、检索和集群，或者数据规模已经大到使客户端矩阵运算变得不切实际。它使用 Elasticsearch kNN 作为计算原语，处理任意大小的集群，并将几乎所有计算保留在服务器端。</p><pre><code>    Clustered global index in 31.6s
      Total clusters: 82
      Total noise:    2420 (28.5%)
      Density probes: 425 kNN queries via 9 _msearch HTTP calls</code></pre><h4>理解噪声率</h4><p>约 28% 的噪声率是有意为之，并不意味着系统出现了故障。在配置的 <code>similarity_threshold</code> 下，不属于任何密集集群的文档将保持未分配状态，而不是被强制匹配到不合适的集群中。这相当于一道质量门槛：评论专栏、短文和一次性报道往往难以形成集群，因为它们缺乏构成连贯分组所需的主题密度。</p><p>阈值可调：降低 <code>similarity_threshold</code> 会产生更激进的集群（分配更多文档，但集群更松散），提高则会收紧集群并增加噪声比例。对于这种包含混合新闻内容的语料库，约 30% 的噪声比例是一个合理的平衡点。生产部署应根据特定领域的质量标准调整阈值。</p><h3>使用 significant_text 自动添加标签</h3><p>现在，每个集群都需要一个便于人工理解的标签。Elasticsearch 的 <code>significant_text</code> 聚合会找出在前景集（集群）中出现异常频繁、而在背景集（完整语料库）中不常见的词项。</p><p>其底层采用统计启发式方法（默认为 JLH 分数），平衡了绝对频率与相对频率的变化，无需机器学习，也无需调用大语言模型 (LLM)。例如，一个关于英国政治的集群，可能会浮现出 <code>starmer</code>、<code>labour</code>、<code>downing</code> 等词项，因为与整体新闻语料库相比，这些词项在该集群中出现得异常频繁。</p><p>在这一全局处理阶段，标签直接基于 <code>docs-clustering-all</code> 计算，因此前景集和背景集都取自整个月的数据。在第 2 部分中，标签会使用每日索引模式 (<code>docs-clustering-*</code>)。这是一个通配符，可让查询同时覆盖所有匹配的索引，从而为 significant_text 提供更广泛的背景，以获得更好的对比效果。</p><p>一个最小查询形状如下所示：</p><pre><code>{
  "size": 0,
  "query": { "term": { "cluster_id": "72" } },
  "aggs": {
    "label_terms": {
      "significant_text": {
        "field": "text",
        "size": 5,
        "filter_duplicate_text": true
      }
    }
  }
}</code></pre><p><code>significant_text</code> significant_text 也可作为一道质量门槛：未产生任何显著词项的集群，说明其缺乏可区分的词汇特征。这类分组本身并不连贯，因此应归为噪声，而不应赋予带有误导性的标签。</p><p>一个轻量级的确定性清理步骤会移除噪声较大的标签词项（如数字 token 和通用词），并在必要时回退到代表性标题。这样既保留了 Elasticsearch 原生标签的特点，也提升了可读性。</p><pre><code>    Sample cluster labels:
      cluster   3  (200 docs)  arsenal | mikel | villa
      cluster   1  (198 docs)  volodymyr | ukrainian | kyiv
      cluster   0  (196 docs)  hostages | hamas | israeli
      cluster   4  (187 docs)  scrum | rugby | borthwick
      cluster  52  (185 docs)  fossil | renewable | renewables
      cluster  10  (156 docs)  labour | gwynne | mps
      cluster  40  (151 docs)  novel | novels | literary
      cluster  11  (149 docs)  mewis | sarina | wiegman
      cluster  44  (143 docs)  flooding | rainfall | rain
      cluster  13  (131 docs)  doge | musk | elon
      cluster  12  (128 docs)  murder | insp | knockholt
      cluster   5  (124 docs)  putin | backstop | starmer


    Reassigned 35 docs from incoherent clusters to noise
    Total docs: 8,495
    Clustered:  6,040 (71.1%)
    Noise:      2,455 (28.9%)</code></pre><h3>集群可视化</h3><p>下方的可视化结果展示了全局集群阶段的发现，包括按日期划分的集群文档与噪声文档分布、整个月的 UMAP 投影，以及用于验证集群反映的是主题而非来源的来源构成图。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/99099f5adaa945ae4097c50b0d7151c7dd28872e-1000x400.png" style="width: 100%;" /><p>2025 年 2 月期间，集群文档与噪声文档的每日分布情况。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/477c6c7177942955a942f85f5c881da50e517915-1100x700.png" style="width: 100%;" /><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/4f4302556c8974c416842452cf33bca06e90b966-1100x700.png" style="width: 100%;" /><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/327f96a79e382ef30614cb0570aa7fccd822b8f8-1100x700.png" style="width: 100%;" /><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/acfb1524a10e24d6ff2412e7c3ec0f2b3ac75193-900x600.png" style="width: 100%;" /><p>UMAP 中的每个彩色岛屿都代表一个集群：一组关于同一主题的文章，纯粹是通过嵌入相似性而发现的。灰色噪声点则是未能明确归入任何集群的文章（通常是短篇文章、观点文章或一次性报道）。</p><p>来源细分图表确认，集群中的文章<strong>同时</strong>来自 BBC News 和 The Guardian。集群找到的是<em>主题</em>，而非<em>来源</em>，这正是无监督发现应该产生的结果。</p><h3>使用 diversify retriever 探索集群的广度</h3><p>普通 kNN 返回与集群质心（密集核心）最相似的文档。但真实的集群往往还会涵盖多个子主题。<a href="https://www.elastic.co/docs/reference/elasticsearch/rest-apis/retrievers/diversify-retriever"><strong>diversify retriever</strong></a> 使用最大边际相关性 (MMR)，呈现既与质心相关、<em>彼此之间又有所差异</em>的文档。</p><p>关键参数是<strong>λ（lambda）</strong>：</p><ul><li>λ = 1.0 → 纯相关性（与普通 kNN 相同）。</li><li>λ = 0.0 → 纯多样性（结果最大程度分散）。</li><li>λ = 0.5 → 均衡：既与主题保持相关，又能覆盖不同角度。</li></ul><div style="display:none">Unknown block type "infoPanel", specify a component for it in the `components.types` option</div><p>最简 retriever 请求结构如下：</p><pre><code>{
  "size": 8,
  "retriever": {
    "diversify": {
      "type": "mmr",
      "field": "embedding",
      "lambda": 0.5,
      "query_vector": "<cluster-centroid-vector>",
      "retriever": {
        "knn": {
          "field": "embedding",
          "query_vector": "<cluster-centroid-vector>",
          "k": 50,
          "num_candidates": 100
        }
      }
    }
  }
}</code></pre><p>在 diversify 层级，<code>type</code>、<code>field</code> 和 <code>query_vector</code> 参数均为必需：<code>field</code> 用于告知 MMR 应使用哪个 dense_vector 字段来计算结果之间的相似度，而 <code>query_vector</code> 则提供相关性评分的参考向量。</p><p>这可以让您回答：“这个集群到底涵盖了什么？”而不仅仅是“它的中心是什么？”</p><pre><code>    Exploring cluster 52 (185 docs)
    Label: fossil | renewable | renewables
    Centroid computed (dim=1024)


    ========================================================================
    Plain kNN (closest to centroid)
    ========================================================================
      1. [0.9738] Green campaigners fear ministers are poised to award billions of pounds in fresh subsidies to Drax power station, despite strong concerns...
      2. [0.9710] Thirteen more oil and gas licences could be cancelled as ministers decide new guidance for fossil fuel extraction after a landmark court...
      3. [0.9699] Experts have accused the fossil fuel industry of seeking special treatment after lobbyists argued greenhouse gas emissions from oilfields...
      4. [0.9681] Burning wood is a terrible way of producing electricity . Chopping down trees destroys habitats for wildlife, and growing new trees cannot...
      5. [0.9649] Keir Starmer will do huge damage to the global fight against climate change if he gives in to political pressure and allows the development...
      6. [0.9641] Labour will next week be confronted with stark policy choices that threaten to expose the fault lines between the Treasury and the...
      7. [0.9638] The Drax power station near Selby in north Yorkshire burns imported wood pellets  The government has agreed a new funding arrangement with...
      8. [0.9581] If you care about the world we are handing on to future generations, the news on Thursday morning was dramatic. This January was the...
    
    ========================================================================
    Diversify retriever (MMR, lambda=0.5)
    ========================================================================
      1. [0.9738] Green campaigners fear ministers are poised to award billions of pounds in fresh subsidies to Drax power station, despite strong concerns...
      2. [0.9434] Oil and gas interests have waged a coordinated campaign to kill pro-electrification policies that ban gas connections in new buildings ,...
      3. [0.9303] It was interesting to read that new licences for oil and gas production in the North Sea are being delayed by legal action ( Thirteen more...
      4. [0.9139] The US energy secretary, Chris Wright, has said he “would love to see Australia get in the game of supplying uranium and maybe going down...
      5. [0.9077] Rachel Reeves was facing criticism on Saturday night as it was confirmed that a report she cited as evidence that a third ­runway at...
      6. [0.8996] When Margaret Thatcher opened the Hadley Centre for Climate Change in 1990 journalists suggested she was attempting to appear to be doing...
      7. [0.8993] The vast majority of governments are likely to miss a looming deadline to file vital plans that will determine whether or not the world has...
      8. [0.8987] European imports of seaborne gas shipments fell by a fifth last year to their lowest level since the pandemic, according to a new report,...
    
    Overlap: 1/8 documents appear in both result sets
    
    Avg pairwise similarity (lower = more diverse):
      Plain kNN:          0.9057
      Diversify retriever: 0.6965</code></pre><p>普通 kNN 的结果往往集中在主题的某一个侧面，也就是那些与质心最相似、彼此之间也最相似的文档。diversify retriever 则会展示同一集群的不同侧面，包括子主题、不同来源和多样化视角。</p><p>多样性指标定量证实了这一点：diversify retriever 结果的平均两两相似度较低，意味着返回的文档覆盖范围更广。</p><p>这适用于：</p><ul><li><strong>理解一个集群实际涵盖的范围</strong>，不仅要关注其中心，还要关注其边缘。</li><li><strong>生成摘要</strong>。多样化且有代表性的文档为 LLM 提供了更好的素材。</li><li><strong>寻找代表性示例</strong>，用于人工审核或下游标签生成。</li><li><strong>质量检查</strong>。如果多样化结果看起来不够连贯，就说明这个集群可能需要进一步拆分。</li></ul><h2>第 2 部分：时间故事链</h2><h3>跨天追踪故事</h3><p>第 1 部分对整个月的数据进行了全局集群，以发现其中的主题。为了呈现时间演化，同样的密度探测质心分类会按天在<strong>每日索引</strong>上独立运行，再将相邻日期的集群连接起来。请注意，每日集群与第 1 部分中的全局集群相互独立；每天都会生成自己的集群分配和标签，并根据当天的内容进行调整。</p><h4><strong>链接方法：采样与查询</strong></h4><p>对于第 A 天的每个集群：</p><ol><li>采样几个代表性文档。</li><li>对 B 天的索引运行 kNN。</li><li>统计落入 B 天每个集群的命中数量。</li><li>如果命中比例超过阈值（kNN 比例 ≥ 0.4），则记录一条链接。</li></ol><p>这速度很快（每个集群只查询少量文档，不是全部），并且使用 Elasticsearch 的原生 kNN，无需外部工具。</p><pre><code>Preparing daily indices for temporal linkage...


Indexed 8,495 docs into 28 daily indices


Temporal links found: 808 in 145.4s

Strongest links:
  2025.02.01 'league | arsenal | premier' -> 2025.02.02 'league | season | striker'  (100%)
  2025.02.03 'league | striker | loan' -> 2025.02.04 'league | striker | season'  (100%)
  2025.02.03 'score | operator | gedling' -> 2025.02.04 'league | striker | season'  (100%)
  2025.02.12 'playoff | leg | bayern' -> 2025.02.13 'league | players | injury'  (100%)
  2025.02.14 'league | injury | football' -> 2025.02.15 'league | premier | football'  (100%)
  2025.02.18 'russia | ukraine | talks' -> 2025.02.19 'saudi | russia | arabia'  (100%)
  2025.02.18 'football | league | bayern' -> 2025.02.19 'league | manchester | players'  (100%)
  2025.02.21 'league | premier | manchester' -> 2025.02.22 'game | players | defeat'  (100%)
  2025.02.21 'rugby | calcutta | brilliant' -> 2025.02.22 'game | players | defeat'  (100%)
  2025.02.26 'metals | kyiv | ukrainian' -> 2025.02.27 'ukraine | russia | talks'  (100%)</code></pre><p>kNN 比例达到 100% 表示源集群中的所有采样文档都落入同一个目标集群，也就是强度最高的跨日关 流水以上大多数关联都与足球相关，这很合理：英超联赛的报道每天都有，且主题一致性很高。</p><p><code>score | operator | gedling</code> → <code>league | striker | season</code> 链接是一个小众本地足球集群（Gedling 是一家非联赛俱乐部）在第二天被吸收到更广泛的英超联赛集群中的一个例子，这是每日以不同粒度重新集群的自然效果。</p><h3>构建故事链</h3><p>故事链是由连续多天的关联集群组成的序列。</p><p>单个配对链接可以显示周一与周二“英国政治”集群之间的关联。故事链则能揭示完整的发展脉络：一个故事从周一开始，在一周内持续发展，并在周五逐渐淡出。</p><p>链通过贪婪策略构建，所依据的是 kNN 比例 ≥ 0.4 的关联；这意味着源集群中至少有 40% 的采样文档会落入同一个目标集群。算法从最早出现的集群开始，并始终沿着最强的出向关联继续延伸。<br/></p><pre><code>    Strong links (kNN fraction >= 0.4): 244
    Story chains spanning 3+ days: 18
      Chain 1: 'ukrainian | kyiv | eastern' (19 days: Feb 3 → Feb 21)
      Chain 2: 'playing | opposition' (19 days: Feb 10 → Feb 28)
      Chain 3: 'tadhg | maro | cadan' (10 days: Feb 1 → Feb 10)
      Chain 4: 'invade | china | putin' (8 days: Feb 21 → Feb 28)
      Chain 5: 'elected | labour | leader' (7 days: Feb 12 → Feb 18)
      Chain 6: 'film | swift | awards' (6 days: Feb 2 → Feb 7)
      Chain 7: 'amendment | termination | reporting' (6 days: Feb 12 → Feb 17)
      Chain 8: 'officers | scene | police' (5 days: Feb 1 → Feb 5)</code></pre><p>最长的链条连续 19 天追踪乌克兰–俄罗斯相关报道。考虑到 2025 年 2 月持续紧张的地缘政治局势，这并不令人意外。其次是贯穿当月 19 天的英超足球报道。更短的链条则对应于颁奖季（电影/颁奖，6 天）、六国橄榄球赛（10 天）以及英国政治领导层相关报道（7 天）。每条链都代表一条故事轨迹，这些轨迹完全是基于每日索引之间的嵌入相似性自动发现的。</p><h3>Sankey：可视化故事流</h3><p>Sankey 图是一种流向可视化图表，其中连线宽度表示连接强度。在这里，每个垂直条带代表一天，每个节点代表一个每日集群（大小由文档数量决定），每条彩色路径则描绘出一条跨时间延展的故事链。链接宽度表示 kNN 重叠强度：更粗的链接意味着更多采样文档落入目标集群。每条链都使用统一颜色，因此从左到右的一条同色路径就代表一个故事的发展过程。</p><p>例如，乌克兰－俄罗斯链（作为较长路径之一清晰可见）从 2 月初一直延续到第三周；其链接始终较粗，表明该主题在不同日期之间具有很强的连续性。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/100a60a7fb85da8ab3813fd071a82c93f2c3f318-1300x650.png" style="width: 100%;" /><p><em>时间故事链贯穿 2025 年 2 月。每条彩色路径都代表一个跨天延续的故事；连线宽度表示 kNN 重叠强度。</em></p><h2>这种方法的成果</h2><p>本文完整介绍了基于 Elasticsearch 构建的无监督文档集群管道：</p><ol><li><strong>集群嵌入</strong>：Jina v5 的任务专用适配器可生成针对主题分组优化的嵌入，而不仅仅是用于查询-文档匹配。</li><li><strong>全局发现式集群</strong>：在一个索引中对整个月的数据进行集群，可最大限度地发掘跨日主题。</li><li><strong>密度探测质心分类</strong>：取样 5%，通过 <code>msearch</code> kNN 探测密度，选择不同的高密度种子，再根据这些质心对所有文档进行分类。Elasticsearch 负责处理大部分计算任务；客户端仅负责耗时极短（约 0.01 秒）的种子选择工作。</li><li><strong><a href="https://www.elastic.co/docs/reference/aggregations/search-aggregations-bucket-significanttext-aggregation"><code>significant_text</code></a>标签生成</strong>：无需借助 ML 模型或人工标注，显著性检验就能生成有意义的集群标签。无法产生任何显著词项的集群，说明其内部缺乏连贯性，因此会被降为噪声——这也是一种内置的质量控制机制。</li><li><strong>时间故事链接</strong>：借助每日索引以及跨索引的采样与查询 kNN，追踪故事如何随时间演变。</li></ol><p><strong>关键要点：</strong></p><ul><li>嵌入任务类型至关重要：集群嵌入能够形成明显更紧密的主题分组。</li><li>借助 <a href="https://www.elastic.co/docs/solutions/search/vector/knn">kNN 搜索</a>，Elasticsearch 既可以充当存储层，<em></em>也可以充当集群引擎。</li><li>密度探测质心分类几乎将所有计算保留在服务器端，并生成由嵌入空间密度决定的自然大小的集群。</li><li><code>significant_text</code> 该方法速度快、可解释性强，在自动标注和质量门控方面同样十分有效。</li></ul><p><strong>这种方法适用的场景：</strong></p><ul><li>您拥有带有时间戳的文本，且希望在无标注训练数据的情况下进行主题发现。</li><li>您希望使用同一套技术栈完成存储、向量搜索、标注和时间关联。</li></ul><p><strong>还可以进一步探索的扩展方向：</strong></p><ul><li>多周期集群（如按周、按月汇总）</li><li>通过增量集群分配进行实时摄取。</li><li>以 significant_text 词项为种子生成 LLM 集群摘要。</li><li>在更大规模下，采样得到的 KMeans 质心可以作为基于密度的集群算法的热启动种子，从而降低探测阶段的成本。</li></ul><h2>亲自试用</h2><p>您可以将其替换为自己的带时间戳文档语料库；任何包含日期信息的文本集合都适用于这一管道。完整的笔记本和支持代码可在 <a href="https://github.com/elastic/elasticsearch-labs/tree/main/supporting-blog-content/unsupervised-document-clustering-elasticsearch-jina-embeddings">配套仓库</a>中找到。</p><ul><li><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"><strong>开始免费试用 Elastic Cloud</strong></a>：几分钟内即可启动一个支持 <code>bbq_disk</code> 的托管集群。</li><li><a href="https://www.elastic.co/elasticsearch/serverless"><strong>试用 Elasticsearch Serverless</strong></a>：无需管理集群，可自动扩展，并支持本演练涵盖的全部内容。</li></ul>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[为什么电子商务搜索需要治理]]></title>
            <description><![CDATA[了解为什么没有治理的电子商务搜索会失效，以及控制层如何确保可预测和基于意图的结果，从而改善检索。]]></description>
            <link>https://www.elastic.co/search-labs/cn/blog/ecommerce-search-governance-improve-retrieval</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/cn/blog/ecommerce-search-governance-improve-retrieval</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Alexander Marquardt, Honza Král, Taylor Roy]]></dc:creator>
            <pubDate>Thu, 09 Apr 2026 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/3793259b01a5653a7520393a2f006610de0d21e7-1280x720.png?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>电子商务零售商需要在同一系统中处理各种有本质区别的查询类型。搜索“橙子”的购物者期望看到的是这种水果，而不是包含“橙色”一词的产品，例如橙汁或橙子果酱，也不是在语义上相关的柑橘类产品。搜索“送给爱吃甜食的爷爷的礼物”的购物者需要的是语义发现，而不是字面上的关键字匹配。</p><p><em>词汇检索</em>（文本匹配）、<em>语义检索</em>（概念匹配）和<em>混合检索</em>（结合词汇和语义信号）本身并不能解决这些问题。词汇检索可能返回所有包含“橙子”的结果，而针对“橙子”这类高意图查询的纯语义检索，则可能扩展至相关商品（如柠檬或葡萄柚）。混合检索虽能融合词汇与语义信号，但仍无法判定该查询应被视为导航型搜索、需应用哪些约束条件，或应遵循何种业务规则。问题根源不在于检索技术本身，而在于缺乏治理层。该层级需在检索启动前，识别查询类型并确定需执行的约束规则。</p><p>在这篇博文中，我们将探讨电子商务搜索管理、其重要性以及控制层如何确保可预测的准确检索。</p><h2>电子商务搜索中的治理含义</h2><p>在此语境下<em>，治理</em>意味着在用户查询与检索引擎之间引入决策层。该层执行以下功能：</p><ul><li>对查询意图进行分类：这是导航（“橙子”）还是发现（“送给爷爷的礼物”）？</li><li>适用业务限制：适用哪些类别界限、资格规则、供应限制或商品推广政策？</li><li>通向适当策略的路径：这应该使用词汇检索、语义检索，还是混合检索？</li></ul><p>治理层决定每次查询应使用哪种检索方法，必须执行哪些限制条件，以及在检索开始前应适用哪些业务策略。重要的是不要将治理层与混合检索混为一谈：混合检索是一种结合了词汇和语义信号的检索策略，而治理层是决定应使用词汇检索、语义检索还是混合检索的上游决策层。</p><h2>现状：应用层“spaghetti”的实现</h2><p>当前，许多零售商试图通过直接在应用层添加逻辑来解决这一问题，但这往往导致“<em>意大利面代码</em>”，即由数千行硬编码的条件语句、正则表达式和复杂搜索模板堆砌而成的代码结构。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/f532b099ee103458e15563a711dae92952f8df02-1024x765.png" style="width: 100%;" /><p>这种方法可以提供如上所示的期望搜索结果；然而，它会产生很大的操作障碍：</p><ul><li><strong>工程依赖问题：</strong>业务人员与商品运营团队若需修改搜索行为，必须通过提交工程工单并经历长达数周的部署周期，导致操作效率低下且灵活性受限。</li><li><strong>碎片化：</strong>搜索逻辑分散于应用代码与搜索模板之间，难以解释或审计，导致后续迭代风险陡增。</li></ul><p>即使团队认识到路由规则的必要性，争论也常常集中在错误的问题上：选择哪种检索方法。</p><h2>错误的选择：词汇、语义与混合</h2><p>搜索团队经常将挑战描述为检索策略的选择：词法/BM25、语义/向量和混合。这种框架是可以理解的（检索方法很重要），但它忽略了实际部署中最常见的失败模式，即对所有查询使用单一检索方法会导致次优结果。</p><p>商业搜索融合了几种截然不同的意图：</p><ul><li><strong>确定性、高意图导航</strong>（“橙子”、“牛奶”、“不含花生的巧克力”、“廉价橄榄油”）。</li><li><strong>探索发现</strong>（“山区徒步旅行夹克”，“送给喜欢机器人的 12 岁孩子的礼物”）。</li><li><strong>运营限制</strong>（供应、尺寸、价格、颜色）。</li><li><strong>商品推广与活动</strong>（包括流量提升、降权、季节性活动）</li></ul><p>当系统通过相同的检索策略来处理所有这些问题时，由于运行模式缺乏管理，结果往往会以可预见的方式出现系统性错误。当团队没有意识到这是一个治理缺口时，他们会用他们唯一掌握的手段来应对，那就是进行更多的调整。</p><h2>为何“相关性调整”会周而复始</h2><p>如果没有路由层，“相关性”通常会变成永无止境的待办事项：</p><ul><li>为何此查询结果将配件显示在核心产品之上？</li><li>为什么这个主查询突然开始出现相关内容？</li><li>为什么在我们添加同义词、调整分析器或启用混合模式后，结果发生了变化？</li><li>为什么业务团队需要一个工程版本来修复一个查询？</li></ul><p>团队的回应是更多的调整：更多的同义词，更多的提升，更多的重新排名的实验，更多的应用程序代码中的异常。这种方法可以维持一段时间，但常常导致脆弱行为，因为系统仍缺乏明确的决策层来确定查询类型并在检索前强制执行正确的约束。</p><h2>剖析电子商务意图：“头部”与“尾部”</h2><p>在本节中，我们采用“头部”与“尾部”作为电商领域中常见导航与探索性查询模式的实用简称。实际上，许多查询都同时包含这两方面的特征：</p><h3>头部查询（确定性意图）</h3><p>这些是直接的导航查询，用户清楚地知道自己想要什么：</p><ul><li>单项意图（“橙子”、“牛奶”、“面包”）。</li><li>具体品牌或产品系列（例如“iPhone 15 Pro”、“健怡可乐”）。</li><li>SKU、型号、尺寸（“ABC123”、“air max 270”）。</li></ul><p>对于这些查询而言，词汇检索能够处理词元对应关系（即匹配单词），但业务层面还期望能够遵循相关限制条件、返回可预测的排序结果，并确保结果可控。商品运营人员需要确保查询在正确的类别范围内得到解析，遵循适用性规则，并凸显特定的业务优先级。</p><p>需要建立治理机制以确保查询按预期分类解析。例如，“橙子”应归类至生鲜蔬果类别，而非橙汁、橙酱或橙味汽水等细分品类。</p><h3>尾部查询（探索性发现）</h3><p>这些是描述性强、意图明确的查询，购物者通过此类查询进行探索性搜索：</p><ul><li>“送给爱吃甜食的爷爷的礼物”</li><li>“山区徒步旅行夹克”</li><li>“适合全天站立的鞋子”</li></ul><p>在这方面，词汇检索往往会遇到问题。语义检索之所以出色，是因为它能将查询概念与产品联系起来，即使在措辞不匹配的情况下也是如此。但仅靠语义检索也很少能达到要求。无论使用哪种检索方法，实际查询通常都需要执行限制条件。</p><h2>约束条件与检索方法正交</h2><p>对语义检索进行约束并不意味着混合<em>搜索</em>。这些都是正交的概念。诸如 Elasticsearch 中的过滤器和增强等约束条件可以应用于任何词汇、语义或混合检索。所面临的挑战是决定如何解释查询、必须执行哪些约束条件以及使用哪种检索策略。</p><p>以下是一些结合检索与硬约束的查询示例：</p><ul><li><strong>橙子：</strong>对“橙子”进行词汇检索，并加上类别限制，如“水果”或“农产品”，排除橙子果酱、橙汁和橙汽水。</li><li><strong>价格低于 4 美元且富含维生素 C 的水果：</strong>营养意图语义检索加上限制条件，结果仅限于水果类别和 4 美元以下的产品。</li><li><strong>舒适的工作鞋：</strong>针对上下文意图的语义检索加上限制结果为鞋的类别约束。</li></ul><p>这些查询无法通过单一方法来处理：</p><ul><li><strong>纯词汇检索</strong>在此场景下往往不足，因为“富含维生素 C”或“舒适”等短语可能并非以清晰的结构化属性形式存在。这类信息通常需从产品描述、用户评价或规格参数中推断得出。</li><li><strong>纯语义检索</strong>往往也不足，因为如果没有明确限制，像“富含维生素C的水果”这样的查询可能会扩展到维生素补充剂、水果味饮料或高维生素蔬菜，超出预期类别和价格范围。</li></ul><p>治理层决定查询是否需要词汇检索、语义理解、约束执行或这些方面的组合。如果没有这一层，电子商务团队可能会最终：</p><ul><li><strong>过度限制：</strong>将词汇检索用于语义请求（例如“送给爷爷的礼物”）。</li><li><strong>限制不足：</strong>对高意图的头部查询使用语义查询（例如“橙子”）。</li></ul><p>治理挑战在于构建一个能够针对每类查询做出正确判断的系统。</p><h2>在没有治理的情况下会发生什么</h2><p>最常见的故障模式很简单：团队直接获取原始用户查询并将其传递给单一检索策略（词汇、语义或混合），而没有中间治理层。</p><h3>词汇检索未能达到预期的解析效果</h3><p>当用户搜索“橙子”时，词汇检索策略可能会返回任何包含该词项的内容：橙汁、橙子果酱或橙子汽水。系统正确匹配了该术语，但如果没有治理，它可能无法解析预期的购物上下文（水果）。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/99abc7a46f9c56a26a68d0a089d7ab830b9b5568-1560x814.png" style="width: 100%;" /><h3>语义检索的范围已超出预期限制</h3><p>当用户搜索“橙子”时，语义系统可能会检索邻近产品概念中与概念相关的项目。系统可能会正确理解更宽泛的领域（水果或农产品），但如果没有明确的治理，它仍然会超出用户的预期限制（具体来说就是橘子）。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/c9de86363ecbed499fe48259f47b3c5b2c26bc43-1568x796.png" style="width: 100%;" /><h3>差距在于治理</h3><p>所需的是一个上游决策层，该层在检索开始之前确定查询意图并强制执行正确的约束条件。这解决了以下问题：</p><ul><li>类似或相关的项目会出现在用户实际想要的项目旁边。</li><li>模糊的类别界限（“饮料”与“农产品”）。</li><li>无法进行季节性促销或活动。</li><li>不可预测且无法解释的结果。</li></ul><h2>意图理解与路由规则：必要的控制平面</h2><p>治理型搜索系统在检索前（在 Elasticsearch 中执行查询之前）引入了一个轻量级控制平面。控制机制将在本博客系列的第 3 部分和第 4 部分中详细讨论。目前，我们只讨论它能做什么，而不谈具体工作原理：</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/88c3d0f9731a128d73a765dcdffed897308110a6-2680x766.png" style="width: 100%;" /><p>控制平面可以理解意图、应用业务策略，并确保采用适当的检索策略，具体如下：</p><p><strong>1. 检测意图信号</strong></p><ul><li>此查询是导航型还是发现型？</li><li>这是已知的头部查询（牛奶、面包、香蕉）吗？</li><li>是否有已知的产品、品牌或类别解释（例如，“橙子”应解析为农产品）。</li><li>查询是否为类似 SKU 的模式？</li><li>查询是否属于活动或季节性政策（例如圣诞节期间，提升与火鸡相关的结果）？</li><li>查询是否包含约束条件（类别、属性、排除项、价格/尺寸/颜色）？</li></ul><p><strong>2. 应用治理与业务政策</strong></p><ul><li>首先强制执行确定性约束（类别/属性/否定/可用性）。</li><li>应用当前有效的商品推广策略（提升/下调/置顶/覆盖）。</li><li>通过优先规则解决冲突（例如活动覆盖与全局策略）。</li></ul><p><strong>3. 选择合适的检索策略</strong></p><ul><li>用于导航/高意图头部查询的词汇（快速、确定性）。</li><li>为真正的发现查询提供语义检索。</li><li>在明确业务约束下，结合词汇和语义信号可增加价值的混合搜索。</li></ul><p>实际上，控制平面的输出并不只是“使用混合检索”或“使用语义检索”。这是一个受治理的检索方案：对购物者意图的解读、应适用的约束和政策，以及应执行的检索策略。以下几个简单示例可以具体说明这一点：</p>
            <table style="border-collapse: collapse; margin: 20px 0;">
              <tbody>
                
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">购物者查询</td><td style="border: 1px solid #ccc; padding: 4px;">受治理的解释</td><td style="border: 1px solid #ccc; padding: 4px;">检索方案示例</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">“不含花生的巧克力”</td><td style="border: 1px solid #ccc; padding: 4px;">具有硬性排除约束的产品导向查询</td><td style="border: 1px solid #ccc; padding: 4px;">巧克力的词义检索以及含有花生的产品的排除过滤器</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">“廉价橄榄油”</td><td style="border: 1px solid #ccc; padding: 4px;">有价格限制的产品/类别查询</td><td style="border: 1px solid #ccc; padding: 4px;">针对橄榄油且价格筛选上限设为零售商“低价”阈值的词汇检索</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">“价格低于 4 美元且富含维生素 C 的水果”</td><td style="border: 1px solid #ccc; padding: 4px;">需要语义理解和硬约束的发现查询</td><td style="border: 1px solid #ccc; padding: 4px;">营养意图语义检索，限于水果类别，筛选价格低于 4 美元的产品</td>
                    </tr>
              </tbody>
            </table>
          <p>控制平面会为每个查询选择合适的策略和检索策略，且能够一致、可预测且可扩展。这使得高级检索方法在生产中的可预测性更高，因为首先执行的是意图一致性约束，路由决策为显式而非隐式。</p><h2>与其他方法的关系</h2><p>有些团队使用改进的嵌入模型来更好地捕捉产品语义，这可以大大提高语义检索的质量。其他方法则使用重新排序方法（如<a href="https://www.elastic.co/docs/solutions/search/ranking/learning-to-rank-ltr">学习排序 (LTR)</a>），在检索结果生成后基于用户交互或业务指标优化结果排序。这两种方法均有价值且常互为补充。更优质的嵌入向量能提升相似度匹配精度，而重排序可优化候选结果间的排序质量。</p><p>治理解决了问题的另一层：它位于检索的上游。它决定应使用哪种检索策略（例如，词汇检索、语义检索或混合检索）、需要哪些确定性约束，以及哪些查询应结合多项业务策略。</p><h2>受治理控制平面可实现哪些功能</h2><p>一旦治理层就位，运营模式将发生根本性变革。与收入紧密相关的查询将具备可预测性。业务团队无需等待工程团队的发布周期，即可自主更新搜索行为，而语义检索、混合检索等高级方法，则可通过路由规则和管控机制逐步部署，而非直接全局启用或禁用。</p><p>本系列的下一篇文章将探讨该操作模型在实践中的具体表现，以及为什么它可能与其背后的检索技术同等重要。</p><p>如果商户必须打开一个 Jira 工单并等待部署来修复一个关键的收入查询，瓶颈不在于引擎；而在于运营模式。现代电子商务搜索需要一种方式，能够快速安全地将业务意图转化为受控、可审计的搜索行为，同时在可测量增值的地方使用高级检索。</p><h2>本系列内容预告</h2><p>本系列探讨的模式在检索的上游运行：在查询生成开始之前，将业务意图转化为正确的查询策略。在下一篇文章中，我们将从技术问题转向运营问题：当业务团队无需工程部署即可更改搜索行为时会发生什么，以及为什么治理可以确保安全。</p><h2>将受治理的电子商务搜索付诸实践</h2><p>在企业级电商服务场景中，工程瓶颈、应用层逻辑脆弱性以及搜索结果不可预测性等问题，均可通过 Elastic Services 的专业服务得以解决。本系列所述的受治理控制平面架构，正是由 Elastic Services 工程团队精心打造。</p><p>若您的团队仍在耗费大量工程资源将商品运营需求转化为代码修改，或搜索相关性优化任务积压始终难以缩减，我们可协助评估现有技术架构，并规划一条实现搜索配置业务化、可治理的转型路径。请联系 <a href="https://www.elastic.co/consulting">Elastic Services</a>。</p><h2>加入讨论</h2><p>对搜索治理、检索策略或电子商务搜索架构有疑问？加入更广泛的 <a href="https://discuss.elastic.co/">Elastic 社区讨论</a>。</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[如何使用 Mastra 和 Elasticsearch 构建代理式 AI 应用程序]]></title>
            <description><![CDATA[通过一个实际示例，了解如何使用 Mastra 和 Elasticsearch 构建智能体 AI 应用。]]></description>
            <link>https://www.elastic.co/search-labs/cn/blog/build-agentic-ai-applications-mastra-elasticsearch</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/cn/blog/build-agentic-ai-applications-mastra-elasticsearch</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Enrico Zimuel]]></dc:creator>
            <pubDate>Wed, 08 Apr 2026 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/ab30baf2f908534840c5d71a46705773807baf54-1280x720.png?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>在本文中，我们将介绍如何使用 <a href="https://mastra.ai/">Mastra</a> TypeScript 框架来构建与 <a href="https://www.elastic.co/elasticsearch">Elasticsearch</a> 交互的智能体应用。</p><p>我们最近通过添加对 Elasticsearch 作为向量数据库的支持，参与了 <a href="https://github.com/mastra-ai/mastra">mastra-ai/mastra</a> 开源项目。借助这项新功能，您可以在 Mastra 中原生使用 Elasticsearch 来存储嵌入内容。除了向量之外，Elasticsearch 还提供了一系列高级功能，以满足您所有的上下文工程需求。(例如<a href="https://www.elastic.co/search-labs/blog/context-engineering-hybrid-search-evolution-agentic-ai">混合搜索和重排序</a>).</p><p>本文详细介绍了使用 Elasticsearch 实现检索增强生成 (RAG) 架构的智能体的创建过程。我们将展示一个演示项目，其中采用智能体方法来与存储在 Elasticsearch 中的科幻电影数据语料库进行交互。该项目可在 <a href="https://github.com/elastic/mastra-elasticsearch-example">elastic/mastra-elasticsearch-example</a> 获取。</p><h2>Mastra</h2><p>Mastra 是一个用于创建智能体 AI 应用的 TypeScript 框架。</p><p>Mastra的项目结构如下：</p><pre><code>src/
├── mastra/
│   ├── agents/
│   │   └── weather-agent.ts
│   ├── tools/
│   │   └── weather-tool.ts
│   ├── workflows/
│   │   └── weather-workflow.ts
│   ├── scorers/
│   │   └── weather-scorer.ts
│   └── index.ts
├── .env.example
├── package.json
└── tsconfig.json</code></pre><p>在 Mastra 中，您可以构建<a href="https://mastra.ai/docs/agents/overview">智能体</a>、<a href="https://mastra.ai/docs/agents/using-tools">工具</a>、<a href="https://mastra.ai/docs/workflows/overview">工作流</a>和<a href="https://mastra.ai/docs/evals/overview">评分</a>。</p><p><strong>智能体</strong>是一个接收消息作为输入并产生响应作为输出的类。智能体可以使用工具、大型语言模型 (LLM) 和内存（图 1）。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/f6aca2dcc7fcc45d25e06681649be1b2b7eb6781-706x721.png" style="width: 100%;" /><p>智能体的<strong>工具</strong>允许其与“外部世界”交互，例如与 Web API 通信或执行内部操作，如查询 Elasticsearch。<strong>内存</strong>组件对于存储对话历史（包括过去的输入和输出）至关重要。这些存储的上下文使智能体能够利用过去的交互，为未来的问题提供更知情且更相关的响应。</p><p><strong>工作流</strong>允许您使用清晰、结构化的步骤来定义复杂的任务序列，而不是依赖单个智能体的推理（图 2）。它们让您可以完全控制任务的分解方式、数据在任务之间的移动方式以及何时执行哪些任务。工作流默认使用内置执行引擎运行，也可以部署到<a href="https://mastra.ai/docs/deployment/workflow-runners">工作流运行器</a>。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/0dce161cf7891207015dc87532b5b90df1822432-880x252.png" style="width: 100%;" /><p>在 Mastra 中，您还可以定义分数，这些分数是通过模型评分、基于规则和统计方法来评估智能体输出的自动化测试结果。评分器返回<em>分数</em>：量化输出满足评估标准程度的数值（通常在 0 到 1 之间）。这些分数使您能够客观地跟踪性能、比较不同方法并识别 AI 系统中的改进领域。您可以使用自己的提示和评分函数自定义评分器。</p><h2>Elasticsearch</h2><p>要运行演示项目，我们需要一个正在运行的 Elasticsearch 实例。您可以在 <a href="https://www.elastic.co/cloud">Elastic Cloud</a> 上激活免费试用版，或使用 <a href="https://github.com/elastic/start-local"><code>start-local</code></a> 脚本在本地安装：</p><pre><code>curl -fsSL https://elastic.co/start-local | sh</code></pre><p>这将在您的计算机上安装 Elasticsearch 和 Kibana，并生成一个用于配置 Mastra 集成的 API 密钥。</p><p>API 密钥将显示为上一条命令的输出，并存储在 elastic-start-local 文件夹中的 <strong>.env</strong> 文件内。</p><h2>安装与配置演示</h2><p>我们创建了一个 <a href="https://github.com/elastic/mastra-elasticsearch-example">elastic/mastra-elasticsearch-example</a> 存储库，其中包含演示项目的源代码。存储库中报告的示例演示了如何在 Mastra 中创建一个实现 RAG 架构、用于从 Elasticsearch 检索文档的智能体。</p><p>我们为演示提供了一个关于科幻电影的数据集。我们从 <a href="https://www.kaggle.com/datasets/rajugc/imdb-movies-dataset-based-on-genre/versions/2?select=scifi.csv">Kaggle</a> 上的 IMDb 数据集中提取了 500 部电影。</p><p>第一步是使用 npm 安装项目依赖，执行以下命令：</p><pre><code>npm install</code></pre><p>然后我们需要配置包含各项设置的 <strong>.env</strong> 文件。我们可以使用以下命令，复制 <strong>.env.example</strong> 文件的结构来生成该文件：</p><pre><code>cp .env.example .env</code></pre><p>现在我们可以编辑 .env 文件，补充缺失的信息：</p><pre><code>OPENAI_API_KEY=
ELASTICSEARCH_URL=
ELASTICSEARCH_API_KEY=
ELASTICSEARCH_INDEX_NAME=scifi-movies</code></pre><p>Elasticsearch 索引的名称为 <strong><code>scifi-movies</code></strong>。如果您想更改它，可以使用环境变量 <code>ELASTICSEARCH_INDEX_NAME</code>。</p><p>我们使用 OpenAI 作为嵌入服务，这意味着您需要在 <code>OPENAI_API_KEY</code> 环境变量中提供 OpenAI 的 API 密钥。</p><p>示例中使用的嵌入模型是 <a href="https://developers.openai.com/api/docs/models/text-embedding-3-small">openai/text-embedding-3-small</a>，嵌入维度为 1536。</p><p>为了生成最终答案，我们使用了 <a href="https://developers.openai.com/api/docs/models/gpt-5-nano">openai/gpt-5-nano</a> 模型来降低成本。</p><p>RAG 架构允许您使用性能较低（且通常成本较低）的 LLM 模型，因为答案落地的主要工作是由检索组件（此处为 Elasticsearch）承担。</p><p>较小的 LLM 仅负责两个主要任务：</p><ul><li><strong>重写/嵌入查询：</strong>将用户的自然语言问题转换为用于语义搜索的向量嵌入。</li><li><strong>综合答案：</strong>获取高度相关的检索上下文块（文档/电影），并将它们合成为一个连贯的、最终的、人类可读的答案，并遵循给出的提示指示。</li></ul><p>由于 RAG 流程可<strong>提供答案所需的精确事实上下文</strong>，最终的 LLM 不需要非常庞大或高度复杂，也不需要在其自身参数中拥有所有必需的知识（这正是大型、昂贵模型的优势所在）。它本质上是一个针对 Elasticsearch 提供的上下文的高级文本摘要器和格式化器，而不是一个功能齐全的知识库本身。这使得可以使用像 <code>gpt-5-nano</code> 等模型来优化成本和延迟。</p><p>配置完 .env 文件后，可以使用以下命令将电影数据导入 Elasticsearch：</p><pre><code>npx tsx src/utility/store.ts</code></pre><p>您应该看到如下输出：</p><pre><code>🚀 Starting ingestion of 500 movies from 500_scifi_movies.jsonl...
Ingesting ░░░░░░░░░░░░░░░░░░░░░░░░ 1/500 (0%) | ok:1 | fail:0 | chunks:1 | eta:19m 33s | current:Capricorn One
Ingesting ░░░░░░░░░░░░░░░░░░░░░░░░ 2/500 (0%) | ok:2 | fail:0 | chunks:2 | eta:10m 32s | current:Doghouse
Ingesting ░░░░░░░░░░░░░░░░░░░░░░░░ 3/500 (1%) | ok:3 | fail:0 | chunks:3 | eta:7m 33s | current:Dinocroc
Ingesting ░░░░░░░░░░░░░░░░░░░░░░░░ 4/500 (1%) | ok:4 | fail:0 | chunks:7 | eta:6m 10s | current:Back to the Future           
Ingesting ░░░░░░░░░░░░░░░░░░░░░░░░ 5/500 (1%) | ok:5 | fail:0 | chunks:9 | eta:5m 14s | current:The Projected Man            
Ingesting ░░░░░░░░░░░░░░░░░░░░░░░░ 6/500 (1%) | ok:6 | fail:0 | chunks:11 | eta:4m 41s | current:I, Robot
...
✅ Ingestion complete in 1m 46s. Success: 500, Failed: 0, Chunks: 693.</code></pre><p>scifi-movies 索引的映射包含以下字段：</p><ul><li><strong>embedding</strong>：dense_vector，1536 维，cosine 相似度。</li><li><strong>description</strong>，包含电影描述的文本。</li><li><strong>director</strong>，包含导演姓名的文本。</li><li><strong>title</strong>，包含电影标题的文本。</li></ul><p>我们使用 title + description 生成嵌入向量。由于 title 和 description 是两个独立的字段，将两者拼接可以确保生成的嵌入向量同时捕获电影的具体唯一标识 (title) 和丰富的描述性上下文 (description)，从而实现更准确、更全面的语义搜索结果。这种组合输入为嵌入模型提供了更好的文档内容单一表示，便于相似性匹配。</p><h2>运行演示</h2><p>您可以使用以下命令运行演示：</p><pre><code>npm run dev</code></pre><p>该命令将在 <strong>localhost:4111</strong> 启动一个 Web 应用，以访问 Mastra Studio（图3）。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/1567e309df21a12bcf1dfef4429f82342549956c-1705x1079.png" style="width: 100%;" /><p><a href="https://mastra.ai/docs/getting-started/studio">Mastra Studio</a>提供了一个交互式 UI 用于构建和测试您的智能体，以及一个将 Mastra 应用程序作为本地服务公开的 REST API。这让您可以立即开始构建，无需担心集成问题。</p><p>我们提供了一个 <strong>Elasticsearch Agent</strong>，它使用 Mastra 的 <a href="https://mastra.ai/reference/tools/vector-query-tool">createVectorQueryTool</a> 作为工具，利用 Elasticsearch 执行语义搜索。该智能体采用 RAG 方法搜索相关文档（即电影）来回答用户的问题。</p><p>该智能体使用以下提示：</p><pre><code>You are a helpful assistant that answers questions based on the provided context.
Follow these steps for each response:

1. First, carefully analyze the retrieved context chunks and identify key information.
2. Break down your thinking process about how the retrieved information relates to the query.
3. Draw conclusions based only on the evidence in the retrieved context.
4. If the retrieved chunks don't contain enough information, explicitly state what's missing.

Format your response as:
THOUGHT PROCESS:
- Step 1: [Initial analysis of retrieved chunks]
- Step 2: [Reasoning based on chunks]

FINAL ANSWER:
[Your concise answer based on the retrieved context]

Important: When asked to answer a question, please base your answer only on the context provided in the tool. 
If the context doesn't contain enough information to fully answer the question, please state that explicitly and stop it.
Do not add more information than what is present in the retrieved chunks.
Remember: Explain how you're using the retrieved information to reach your conclusions.</code></pre><p>如果您点击 <code>Mastra Studio &gt; Agents</code>菜单并选择 <strong>Elasticsearch Agent</strong>，则可以使用聊天系统测试该智能体。例如，您可以提出如下关于科幻电影的问题：</p><p><em>查找五部关于 UFO 的电影或电视剧</em>。</p><p>您会注意到智能体将执行 vectorQueryTool。您可以点击调用的工具来查看输入和输出。执行结束时，LLM 将根据来自 Elasticsearch 的 scifi-movies 索引的上下文回答您的问题（图 4）。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/d9998d4f687984de98845dae52d1288166abf448-1344x1071.png" style="width: 100%;" /><p>Mastra 在内部执行以下步骤：</p><ol><li><strong>向量转换：</strong>用户的问题 “<em>查找五部关于 UFO 的电影或电视剧</em>” 使用 OpenAI 的 <code>openai/text-embedding-3-small</code> 模型转换为向量嵌入。</li><li><strong>向量搜索：</strong>然后将此嵌入向量用于通过向量搜索查询 Elasticsearch。</li><li><strong>结果检索：</strong>Elasticsearch 返回一组与查询高度相关的 10 部电影（即那些向量与用户查询向量最接近的电影）。</li><li><strong>答案生成：</strong>检索到的电影和原始用户问题被发送给 LLM，具体为 <code>openai/gpt-5-nano</code>。LLM 处理这些信息并生成最终答案，确保满足用户请求的五个结果。</li></ol><h2>Elasticsearch 智能体</h2><p>下面我们展示了 Elasticsearch 智能体的源代码。</p><pre><code>import { Agent } from "@mastra/core/agent";
import { ElasticSearchVector } from '@mastra/elasticsearch';
import { createVectorQueryTool } from '@mastra/rag';
import { ModelRouterEmbeddingModel } from "@mastra/core/llm";
import { Memory } from "@mastra/memory";

const es_url = process.env.ELASTICSEARCH_URL;
const es_apikey = process.env.ELASTICSEARCH_API_KEY;
const es_index_name = process.env.ELASTICSEARCH_INDEX_NAME;
const prompt = 'insert here the previous prompt';

const esVector = new ElasticSearchVector({
  id: 'elasticsearch-vector',
  url: es_url,
  auth: {
    apiKey : es_apikey
  }
});

const vectorQueryTool = createVectorQueryTool({
  vectorStore: esVector,
  indexName: es_index_name,
  model: new ModelRouterEmbeddingModel("openai/text-embedding-3-small")
});

export const elasticsearchAgent = new Agent({
  id: "elasticsearch-agent",
  name: "Elasticsearch Agent",
  instructions: prompt,
  model: 'openai/gpt-5-nano',
  tools: { vectorQueryTool },
  memory: new Memory(),
});</code></pre><p><strong>vectorQueryTool</strong> 是被调用来实现 RAG 示例中检索部分的工具。它使用了 <a href="https://mastra.ai/reference/vectors/elasticsearch">Elastic 为 Mastra 贡献的 ElasticSearchVector</a> 实现。</p><p>该智能体是 agent 类的一个对象，它使用了 vectorQueryTool、提示和内存组件。可以看出，将 Elasticsearch 连接到智能体所需的代码量非常少。</p><h2>结论</h2><p>本文展示了将 Elasticsearch 与 Mastra 框架集成以构建复杂的智能体 AI 应用程序的简便性和强大功能。具体来说，我们逐步实现了一个 RAG 智能体，能够对 Elasticsearch 中索引的科幻电影数据语料库执行语义搜索。</p><p>一个关键收获是 Elastic 对 Mastra 开源项目的直接贡献，提供了 Elasticsearch 作为向量存储的原生支持。这种集成显著降低了入门门槛，正如 <strong>Elasticsearch Agent</strong> 源代码所证明的那样。使用 <code>ElasticSearchVector</code> 和 <code>createVectorQueryTool</code>，将 Elasticsearch 连接到智能体的完整设置仅需最少数量的配置代码行。</p><p>Elasticsearch 提供了多项高级功能来增强结果相关性。例如，<a href="https://www.elastic.co/elasticsearch/hybrid-search">混合搜索</a>通过将词法搜索与向量搜索相结合，显著提高了准确性。另一个有趣的功能是在混合搜索结束时使用最新的<a href="https://www.elastic.co/search-labs/tutorials/jina-tutorial/jina-reranker-v3">Jina 模型</a>重排序。要了解有关这些技术的更多信息，请参阅 Elasticsearch Labs 的以下文章：</p><ul><li><a href="https://www.elastic.co/search-labs/blog/hybrid-search-elasticsearch">Elasticsearch 混合搜索</a> - Valentin Crettaz</li><li><a href="https://www.elastic.co/search-labs/blog/jina-models-elasticsearch-guide">Jina 模型介绍、功能及其在 Elasticsearch 中的应用</a> 作者：Scott Martens</li></ul><p>我们还鼓励您探索所提供的示例，并开始使用 Mastra 和 Elasticsearch 构建自己的数据驱动的智能体应用。如需了解更多关于 Mastra 的信息，您可在<a href="https://mastra.ai/docs">此处</a>查看官方文档。</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[使用 Elastic 工作流监测 Kibana 仪表板的浏览情况]]></title>
            <description><![CDATA[了解如何使用 Elastic 工作流每 30 分钟收集一次 Kibana 仪表板视图指标，并将其索引到 Elasticsearch 中，以便您可以基于自己的数据构建自定义分析和可视化。]]></description>
            <link>https://www.elastic.co/search-labs/cn/blog/monitor-kibana-dashboard-views-elastic-workflows</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/cn/blog/monitor-kibana-dashboard-views-elastic-workflows</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Gustavo Llermaly]]></dc:creator>
            <pubDate>Fri, 03 Apr 2026 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/0ed94ce00962287b5507f45c92ecb60fdcbf2718-1280x720.png?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p><a href="https://www.elastic.co/kibana">Kibana</a> 会跟踪每个仪表板的查看次数，但这些数据不会在任何内置仪表板中直接显示。在本文中，我们将使用 <strong>Elastic 工作流</strong>每 30 分钟自动收集这些数据，并将其索引到 Elasticsearch 中，这样我们就可以在此基础上构建自己的分析。</p><p><a href="https://www.elastic.co/docs/explore-analyze/workflows">Elastic 工作流</a>是 Kibana 内置的自动化引擎，允许您通过简单的 YAML 配置定义多步流程。每个工作流都可以按计划或事件触发，也可以作为 <a href="https://www.elastic.co/docs/explore-analyze/ai-features/elastic-agent-builder">Elastic Agent Builder</a> 中的工具触发，并且每个步骤都可以调用 Kibana API、查询 Elasticsearch 或转换数据。</p><p>我们将使用仪表板查看计数作为具体示例，但同样的模式也适用于通过 Kibana 已保存对象 API 公开的任何指标。</p><h2>准备工作</h2><ul><li>运行 9.3 的 <a href="https://www.elastic.co/cloud">Elastic Cloud</a> 或<a href="https://www.elastic.co/docs/deploy-manage/deploy/self-managed">自管型</a>集群</li><li><a href="https://www.elastic.co/docs/explore-analyze/workflows/get-started#workflows-prerequisites">已启用工作流</a>（高级设置）</li></ul><div style="display:none">Unknown block type "block", specify a component for it in the `components.types` option</div><p>在开始构建之前，我们先了解目前有哪些数据。Kibana 将其大部分配置和元数据作为<a href="https://www.elastic.co/docs/api/doc/kibana/group/endpoint-saved-objects">已保存对象</a>存储在专用的内部索引中。Kibana 通过这种方式跟踪的事项之一是仪表板查看次数，它使用一种名为“使用计数器”的特殊保存对象类型来实现。您可以在开发工具中直接查询它们：</p><pre><code>GET kbn:/api/saved_objects/_find?type=usage-counter&filter=usage-counter.attributes.domainId:"dashboard"%20and%20usage-counter.attributes.counterType:"viewed"&per_page=10000</code></pre><p>响应类似如下：</p><pre><code>{
  "page": 1,
  "per_page": 10000,
  "total": 1,
  "saved_objects": [
    {
      "type": "usage-counter",
      "id": "dashboard:346f3c64-ebca-484d-9d57-ec600067d596:viewed:server:20260310",
      "attributes": {
        "domainId": "dashboard",
        "counterName": "346f3c64-ebca-484d-9d57-ec600067d596",
        "counterType": "viewed",
        "source": "server",
        "count": 1
      },
      ...
    }
  ]</code></pre><p><code>counterName</code> 字段是仪表板 ID，而 <code>count</code> 是该仪表板在特定日期的累计查看次数。Kibana 每天会为每个仪表板创建一个计数器对象；您可以在对象 ID 中看到日期后缀 (...viewed:server:20260310)。随着用户打开仪表板，计数在一天中不断增长。</p><p>我们不会在索引中复制这种每日文档模型，而是为每个工作流执行创建一个文档。每份文档都记录了该仪表板在捕获时当天的累计浏览量。</p><h2>步骤 2：创建目标索引</h2><p>我们需要一个索引来存储仪表板视图快照。以下命令创建了明确的映射，以便我们稍后进行聚合和可视化。在开发工具中运行此命令：</p><pre><code>PUT dashboard-views
{
  "mappings": {
    "properties": {
      "captured_at": {
        "type": "date"
      },
      "dashboard_id": {
        "type": "keyword"
      },
      "dashboard_name": {
        "type": "keyword"
      },
      "view_count": {
        "type": "integer"
      }
    }
  }
}</code></pre><p>对 ID 和名称使用 <a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/keyword"><code>keyword</code></a> 映射可以进行<a href="https://www.elastic.co/docs/explore-analyze/query-filter/aggregations">聚合</a>。使用 <a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/number"><code>integer</code></a> 来表示 <code>view_count</code> 是一个安全的默认设置，因为 Kibana 每天都会重置计数器，所以达到 32 位限制（一天内超过 20 亿次查看）并非实际需要担心的问题。它仍然支持数值运算，例如 <a href="https://www.elastic.co/docs/reference/aggregations/search-aggregations-metrics-max-aggregation"><code>max</code></a>、<a href="https://www.elastic.co/docs/reference/aggregations/search-aggregations-metrics-avg-aggregation"><code>avg</code></a> 和 <a href="https://www.elastic.co/docs/reference/aggregations/search-aggregations-metrics-min-aggregation"><code>min</code></a> 等。</p><h2>步骤 3：创建工作流</h2><p>前往 <strong>Stack Management &gt; 工作流 &gt; 新建工作流</strong>，然后粘贴以下工作流 YAML 配置：</p><pre><code>name: dashboard-views-ingestion
triggers:
  - type: scheduled
    with:
      every: 30m

steps:
  - name: fetch_dashboard_views
    type: kibana.request
    with:
      method: GET
      path: >-
        /api/saved_objects/_find?type=usage-counter&per_page=10000&filter=usage-counter.attributes.domainId:"dashboard"%20and%20usage-counter.attributes.counterType:"viewed"

  - name: index_each_dashboard
    type: foreach
    foreach: "{{ steps.fetch_dashboard_views.output.saved_objects }}"
    steps:
      - name: fetch_dashboard_name
        type: kibana.request
        with:
          method: GET
          path: /api/saved_objects/dashboard/{{ foreach.item.attributes.counterName }}
        on-failure:
          continue: true

      - name: index_doc
        type: elasticsearch.request
        with:
          method: POST
          path: /dashboard-views/_doc
          body:
            dashboard_id: "{{ foreach.item.attributes.counterName }}"
            dashboard_name: "{{ steps.fetch_dashboard_name.output.attributes.title }}"
            view_count: "${{ foreach.item.attributes.count | plus: 0 }}"
            captured_at: "{{ execution.startedAt | date: '%Y-%m-%dT%H:%M:%SZ' }}"</code></pre><p>在下一节中，我们将逐步分解工作流。</p><h3>工作流如何运作</h3><h4>触发</h4><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/5670991d65c64ee833924225c2d375a1be868b13-325x162.png" style="width: 100%;" /><p>工作流每 30 分钟按计划触发运行一次。这样我们就能获得时序数据，而不会对 API 造成过多压力。</p><h4>fetch_dashboard_views</h4><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/66eaec147c3d01c524c67cf1c7f663ac56a3259d-812x215.png" style="width: 100%;" /><p>使用 <code>kibana.request</code> 调用 Kibana 已保存对象 API。无需进行身份验证设置：工作流引擎会根据执行上下文自动附加正确的标头。</p><h4>index_each_dashboard（循环）</h4><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/aad45e8aed8dc81ded6260cd6199ff78dcffe3b4-1892x290.png" style="width: 100%;" /><p>遍历由上一步返回的 <a href="https://www.elastic.co/docs/api/doc/kibana/group/endpoint-saved-objects"><code>saved_objects</code></a> 数组。每次迭代中的当前项目均可作为 <code>foreach.item</code>。在循环内部，我们为每个仪表板运行两个嵌套步骤。</p><p><strong>1. <code>fetch_dashboard_name</code>：</strong></p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/db21ec5094b743018b9cd66c5052681f14c7d7e3-1999x431.png" style="width: 100%;" /><p>通过调用 <code>GET /api/saved_objects/dashboard/{id}</code> 来解决人类可读的仪表板标题。我们添加了 <code>on-failure: continue: true</code>，以便如果仪表板被删除但仍有浏览计数器，循环就会继续，而不是导致整个执行失败。</p><p><strong>2. <code>index_doc</code>：</strong></p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/f49dd0c9f0817bb1e1e5d9f4a2b05d13ef331054-1999x626.png" style="width: 100%;" /><p>使用 <code>POST /dashboard-views/_doc</code>（无显式 ID）为每个文档建立索引，这样 Elasticsearch 就能自动生成 ID。这样，每次运行时都会创建一个新文档，从而随着时间推移构建浏览次数的历史记录，而不是覆盖之前的快照。</p><p>有两点值得注意：</p><ul><li><code>captured_at</code> 字段使用日期筛选器将时间戳格式化为 <a href="https://www.iso.org/iso-8601-date-and-time-format.html">ISO 8601</a>。如果没有它，值就会显示为 JavaScript 日期字符串，例如 <code>Tue Mar 10 2026 05:03:47 GMT+0000</code>，Elasticsearch 不会将其映射为日期。</li><li><code>view_count</code> 使用 <code>${{ }}</code> 语法和 <code>| plus: 0</code> 来保留数值类型。使用<code>{{ }}</code> 会将其显示为字符串，这将阻止在仪表板中进行数学运算。</li></ul><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/6d48c8784d5df6192e8b5175e69dbab5098194bc-919x774.png" style="width: 100%;" /><p><em>UI 允许您可以很好地对每个工作流步骤进行故障排查。</em></p><h2>第 4 步：构建统计仪表板</h2><p>一旦工作流运行了几次并收集了数据，使用 dashboard-views Data view在 Kibana 中创建一个新的仪表板。</p><p>一些入门面板：</p><ul><li><strong>按浏览量排列的顶级仪表板：</strong>使用<a href="https://www.elastic.co/docs/explore-analyze/visualize/charts/bar-charts"><strong>柱形图</strong></a>，X 轴为 <code>dashboard_name</code>，Y 轴为 <code>last_value(view_count)</code>。这将显示每个仪表板当前的每日浏览量。</li><li><strong>随时间变化的浏览量：</strong>使用<a href="https://www.elastic.co/docs/explore-analyze/visualize/charts/line-charts"><strong>折线图</strong></a>，X 轴为<code>captured_at</code>，Y 轴为<code>last_value(view_count)</code>，按 <code>dashboard_name</code> 细分。由于每次运行都会添加一个新文档，因此使用最后一个值来获取每个时间分桶的峰值计数，而不是重复计数的总和。</li><li><strong>当前快照：</strong>使用<a href="https://www.elastic.co/docs/explore-analyze/visualize/charts/tables"><strong>数据表</strong></a>和最新的 <code>captured_at</code> 来显示所有仪表板上最新的浏览量。</li></ul><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/e245b95f67daf76a2aaf4cb9df2c75ef4cfef582-1462x747.png" style="width: 100%;" /><p>由于每个工作流都会创建一个新文档，因此您可以按时间范围进行筛选，以分析特定时段的活动、比较周与周之间的差异，或在仪表板低于浏览量阈值时发出警报。</p><h2><strong>结论</strong></h2><p>Elastic 工作流非常适合这种定期数据收集，因为源 (Kibana API) 和目标 (Elasticsearch) 都是原生的，这意味着无需管理任何凭据。工作流引擎会自动处理 <code>kibana.request</code> 和 <code>elasticsearch.request</code> 步骤的身份验证，因此您只需编写逻辑即可。</p><h2><strong>资源</strong></h2><ul><li><a href="https://www.elastic.co/docs/explore-analyze/workflows">Elastic 工作流</a></li><li><a href="https://www.elastic.co/docs/api/doc/kibana/">Kibana API</a></li></ul>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[当 TSDS 遇到 ILM：设计不会拒绝延迟数据的时序数据流]]></title>
            <description><![CDATA[TSDS 时间边界如何与 ILM 阶段交互；以及如何设计能够容忍延迟到达指标的策略。]]></description>
            <link>https://www.elastic.co/search-labs/cn/blog/tsds-ilm-elasticsearch</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/cn/blog/tsds-ilm-elasticsearch</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Bret Wortman]]></dc:creator>
            <pubDate>Thu, 02 Apr 2026 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/e4c46e4a6f746d9c845857e80de036f5d51cd4e7-1280x720.png?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>最近，我将客户的指标集群从“所有内容都在热层”迁移到了热层/冷层/冻结层架构。这是我以前已执行过几十次的更改。几分钟内，Logstash 就完全停止了数据传输。</p><p>Elasticsearch 拒绝了延迟到达的指标。这些拒绝导致管道滞后，导致更多的延迟数据，从而引发了更多的拒绝。最终，该管道彻底停滞了。</p><p>我们不得不从快照中恢复数据，重新索引数据，并重新设计摄取管道以恢复数据。</p><p>根本原因并非索引生命周期管理 (ILM) 本身。而是时序数据流 (TSDS) 以及它们如何执行有时间限制的后备索引。</p><p>TSDS 可以将指标的存储需求减少 40——70%，但使 TSDS 高效的架构更改也改变了索引随时间推移的行为方式。这些变化在设计 ILM 策略或数据摄取管道可能会产生延迟到达的数据时非常重要。</p><h2>简要说明</h2><p>使用 TSDS 时：</p><ul><li>后备索引仅接受特定时间窗口内的文档。</li><li>如果在索引移动到冷冻或冻结状态后有延迟的数据到达，Elasticsearch 将拒绝接受这些文档，或将其路由到故障存储（如果已配置）。</li></ul><p>设计规则：</p><pre><code>warm_min_age > rollover_max_age + maximum_expected_lateness</code></pre><h2>什么是时序数据流？</h2><p><em>时序数据流</em> (TSDS) 是针对指标数据进行了优化的专用数据流。对数据进行路由，使相关文档位于同一分片内，从而优化它们以进行查询和检索。下面介绍 Elasticsearch 如何实现这一操作：</p><p>每个文档包含：</p><ul><li>时间戳。</li><li>用于识别时间序列的维度字段。</li><li>表示测量值的度量字段。</li></ul><p>示例包括：</p><ul><li>每台主机的 CPU 使用率。</li><li>每项服务的请求延迟。</li><li>每个传感器的温度读数。</li></ul><p><em>维度 </em>确定了我们要测量的内容，而<em>度量 </em>则代表了随时间变化的值。</p><h3>尺寸</h3><p>维度描述被测量的实体。</p><p>示例:</p><pre><code>host.name
service.name
container.id</code></pre><p>我们在映射中按以下方式定义它们：</p><pre><code>time_series_dimension: true</code></pre><h3>指标</h3><p>指标代表数值，并使用以下方式定义：</p><pre><code>time_series_metric</code></pre><p>常用指标类型：</p><ul><li>计量：数值会上升和下降。</li><li>计数器：数值不断增加，直至重置。</li></ul><p>Elastic Agent 主要收集指标和日志数据，因此，即使您没有手动启用任何 TSDS 索引，集群中仍可能包含这些索引。</p><h3>_tsid 字段</h3><p>Elasticsearch 内部会根据维度字段生成 <code>_tsid</code> 值。这样，具有相同尺寸的文档就可以路由到相同的分区，从而改进：</p><ul><li>压缩。</li><li>查询位置。</li><li>聚合性能。</li></ul><h2>关键区别：有时间限制的后备索引</h2><p>传统数据流始终写入最新的支持索引，称为 <em>写索引</em>，但 TSDS 的行为有所不同。</p><p>每个 TSDS 后备索引都有一个定义的时间窗口，并且仅接受 <code>@timestamp</code> 值在该窗口内的文档：</p><pre><code>GET _data_stream/my-metrics-data-stream


     "index_mode": "time_series",
     "time_series": {
       "temporal_ranges": [
         {
           "start": "2026-01-15T14:35:50.000Z",
           "end": "2026-03-16T11:34:40.000Z"
         }
       ]
     }</code></pre><p>为文档编制索引时，Elasticsearch 会将其路由到负责该时间戳的后备索引，这意味着与传统索引不同，TSDS 可以同时写入多个后备索引。</p><p>例如：</p><ul><li>实时数据 → 最新索引。</li><li>较晚的数据 → 覆盖该时间范围的较早索引。</li></ul><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/31af2bb3b3dc24db8342e791e1db77a44659ba7a-1589x502.png" style="width: 100%;" /><h2>为延迟到达的数据进行设计</h2><p>真正的摄取管道很少能完美地按时提供指标。指标可能会由于网络中断、传输过程中的积压、批量摄取以及边缘设备的丢失而延迟，这些设备重新连接后会开始追赶进度。</p><p>传统索引会悄然吸收这些延迟。TSDS 不会。</p><p>如果文档的时间戳超出了可写后备索引的范围，Elasticsearch 将拒绝该文档，这意味着您的 ILM 策略必须考虑延迟数据。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/32a103b95b20e31615c214271e27811a7ee315ae-1999x691.png" style="width: 100%;" /><h2>关键制约因素</h2><p>后备索引必须保持足够长的可写时间，以接受延迟数据。</p><p>实际上：</p><pre><code>time_until_readonly > maximum_expected_lateness</code></pre><p>由于 ILM 衡量的是从滚动更新开始算起的年限，因此操作规则变为：</p><pre><code>warm_or_cold_min_age > rollover_max_age + maximum_expected_lateness</code></pre><p></p><p>例如，如果指标最多可能延迟六小时到达，则索引在滚动更新后必须保持至少六小时的可写状态。</p><p></p><p>正是由于没有考虑到这一限制，才导致了前面所述的摄取失败。延迟到达的数据被定向到一个早期索引，该索引已经处于冷层并因此被写入阻塞。</p><p></p><h2>处理被拒绝的文档</h2><p>当 TSDS 拒绝文档时，Elasticsearch 返回一个错误，表明时间戳不在可写索引的范围内。您的摄取管道如何处理该错误，决定了是丢失数据还是停止摄取。</p><p>处理被拒绝文档的主要机制是故障存储。</p><h3>故障存储（在 Elasticsearch 9.1+ 中推荐）</h3><p>Elasticsearch 9.1 引入了失败存储，它能自动捕获被拒绝的文档。Elasticsearch 不会将错误返回给客户端，而是将失败的文档写入数据流中的专用失败索引。</p><p>您可以使用以下方法检查故障：</p><pre><code>GET metrics-myapp::failures/_search</code></pre><p>使用故障存储可防止摄取管道因拒绝错误而阻塞，同时保留失败的数据以供分析或<a href="https://www.elastic.co/docs/manage-data/data-store/data-streams/reindex-tsds">重新索引</a>。</p><h2>监测拒绝问题</h2><p>延迟到达问题通常首先表现为摄取异常。您可能会先注意到它们：</p><ul><li>索引速率突然下降。</li><li>拒绝的文档激增。</li><li>越来越多的故障存储条目。</li><li>管道输入和输出计数不匹配。</li></ul><p>通过对这些信号发出警报，操作人员可以在管道停滞之前检测问题。工作流、机器学习作业和其他机制可用于自动检测和通知。</p><h2>TSDS + ILM 迁移检查清单</h2><p>如果要将指标集群迁移到 TSDS、引入 ILM 分层，或升级到指标默认为 TSDS 的 Elasticsearch 版本，请先查看这些项目。</p><h3><strong>1. 测量摄取延迟</strong></h3><p>在更改 ILM 策略之前，请确定：</p><ul><li>正常摄取延迟。</li><li>事件期间的最坏延迟情况。</li><li>批量管道造成的延迟。</li></ul><p>您的 ILM 设计必须适应最大实际延迟。</p><h3><strong>2. 验证索引时间窗口</strong></h3><p>检查您的 TSDS 支持索引：</p><pre><code>GET _data_stream/<your-stream></code></pre><p>寻找：</p><ul><li><code>time_series.start_time</code></li><li><code>time_series.end_time</code></li></ul><p>这些界限决定了哪些索引可以接受文档。了解这些时间窗口有助于您确定数据最多可以延迟多久才不会被拒绝。</p><h3><strong>3. 为延迟到达的数据调整热层的大小</strong></h3><p>确保后备索引保持可写状态的时间足够长，以便写入延迟到达的数据。</p><p>操作规则：</p><ul><li><code>warm_min_age &gt; rollover_max_age + maximum_expected_lateness</code></li></ul><p>请记住，如果指标可能晚到六个小时，那么索引必须至少在六个小时内保持可写状态。</p><h3><strong>4. 决定如何处理被拒绝的文档</strong></h3><p>在启用 TSDS 之前选择策略：</p><ul><li>故障存储（在 Elasticsearch 9.1+ 中推荐）。</li><li>Logstash 死信队列。</li><li>为延迟到达的数据提供后备索引。</li><li>接受有限的数据丢失。</li></ul><h3><strong>5. 监测摄取健康状况</strong></h3><p>为以下内容添加警报：</p><ul><li>索引速率下降。</li><li>已拒绝的文档。</li><li>故障存储增长。</li><li>管道输入/输出不匹配。</li></ul><p>数据延迟问题通常首先表现为摄取异常。</p><h2>总结</h2><p>时序数据流为指标工作负载提供重大的存储和性能改进，但它们引入了重要的架构变更：后备索引是时间绑定的，这影响了 ILM 的行为。</p><p>使用 TSDS 时：</p><ul><li>索引必须保持足够长的可写时间，以接受延迟数据。</li><li>摄取管道应安全处理被拒绝的文档。</li></ul><p>要记住的关键规则是：</p><pre><code>warm_min_age > rollover_max_age + maximum_expected_lateness</code></pre><p>如果围绕这一约束条件设计 ILM 策略，TSDS 就能很好地处理指标工作负载。</p><p>但若忽视这一限制，您的摄取管道可能会很难发现这些时间界限。</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[LINQ to ES|QL：用 C# 编写代码，查询 Elasticsearch]]></title>
            <description><![CDATA[探索 Elasticsearch .NET 客户端中全新的 LINQ to ES|QL 提供程序。借助该程序，您可编写 C# 代码，这些代码会自动转换为 ES|QL 查询语句。]]></description>
            <link>https://www.elastic.co/search-labs/cn/blog/linq-esql-c-elasticsearch-net-client</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/cn/blog/linq-esql-c-elasticsearch-net-client</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Florian Bernd, Martijn Laarman]]></dc:creator>
            <pubDate>Wed, 01 Apr 2026 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/e54132e915217063e9ed0ec45059c6cfc38e31dd-1280x720.png?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>从 <strong>v9.3.4</strong> 和 <strong>v8.19.18</strong> 开始，Elasticsearch .NET 客户端包含一个<a href="https://learn.microsoft.com/en-us/dotnet/csharp/linq/">语言集成查询 (LINQ) </a>提供程序，可在运行时将 C# LINQ 表达式转换为 <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/esql.html">Elasticsearch 查询语言 (ES|QL)</a> 查询。您可以使用<code>Where</code>、<code>Select</code>、<code>OrderBy</code>、<code>GroupBy</code> 和其他标准操作符来编写查询，而无需手工编写 ES|QL 字符串。提供程序负责转换、参数化和结果反序列化，包括按行流式传输，无论结果集大小如何，都能保持稳定的内存使用量。</p><h2>您的第一个查询</h2><p>首先定义一个映射到 Elasticsearch 索引的普通旧 CLR 对象 (POCO)。属性名称通过标准<code>System.Text.Json</code> 属性（如<code>[JsonPropertyName]</code>）或配置的<code>JsonNamingPolicy</code> 解析为 ES|QL 列名。适用于客户端其他部分的<a href="https://www.elastic.co/docs/reference/elasticsearch/clients/dotnet/source-serialization">源序列化</a>规则在这里也同样适用。</p><pre><code>using System.Text.Json.Serialization;

public class Product
{
    [JsonPropertyName("product_id")]
    public string Id { get; set; }

    public string Name { get; set; }

    public string Brand { get; set; }

    [JsonPropertyName("price_usd")]
    public double Price { get; set; }

    [JsonPropertyName("in_stock")]
    public bool InStock { get; set; }
}</code></pre><p>类型设置完成后，查询语句如下所示：</p><pre><code>var minPrice = 100.0;
var brand = "TechCorp";

await foreach (var product in client.Esql.QueryAsync<Product>(q => q
    .From("products")
    .Where(p => p.InStock && p.Price >= minPrice && p.Brand == brand)
    .OrderByDescending(p => p.Price)
    .Take(10)))
{
    Console.WriteLine($"{product.Name}: ${product.Price}");
}</code></pre><p>该提供程序将此转换为以下 ES|QL：</p><pre><code>FROM products
| WHERE (in_stock == true AND price_usd >= ?minPrice AND brand == ?brand)
| SORT price_usd DESC
| LIMIT 10</code></pre><p>需要注意的一些细节：</p><ul><li><strong>属性名称解析：</strong>由于 <code>[JsonPropertyName]</code> 属性，<code>p.Price</code> 变成了 <code>price_usd</code>，根据默认 camelCase 命名策略，<code>p.Brand</code> 变成 <code>brand</code>。</li><li><strong>参数捕获：</strong>C# 变量 <code>minPrice</code> 和 <code>brand</code> 被捕获为命名参数 (<code>?minPrice</code>，<code>?brand</code>)。它们与 JSON 有效负载中的查询字符串分开发送，这样可以防止注入，并实现服务器端查询计划缓存。</li><li><strong>流式传输：</strong><code>QueryAsync&lt;T&gt;</code> 返回 <code>IAsyncEnumerable&lt;T&gt;</code>。从 Elasticsearch 返回数据时，数据会逐行具体化。</li></ul><p>您还可以在不执行的情况下检查生成的查询及其参数：</p><pre><code>var query = client.Esql.CreateQuery<Product>()
    .Where(p => p.InStock && p.Price >= minPrice && p.Brand == brand)
    .OrderByDescending(p => p.Price)
    .Take(10);

Console.WriteLine(query.ToEsqlString());
// FROM products | WHERE (in_stock == true AND price_usd >= 100) | SORT price_usd DESC | LIMIT 10

Console.WriteLine(query.ToEsqlString(inlineParameters: false));
// FROM products | WHERE (in_stock == true AND price_usd >= ?minPrice AND brand == ?brand) | SORT price_usd DESC | LIMIT 10

var parameters = query.GetParameters();
// { "minPrice": 100.0, "brand": "TechCorp" }</code></pre><h2>这如何运作？快速回顾一下 LINQ</h2><p>使 LINQ 提供程序成为可能的机制是 <code>IEnumerable&lt;T&gt;</code> 和 <code>IQueryable&lt;T&gt;</code> 之间的区别。</p><p>在 <code>IEnumerable&lt;T&gt;</code> 上调用 <code>.Where(p =&gt; p.Price &gt; 100)</code> 时，lambda 会编译为 <code>Func&lt;Product, bool&gt;</code>，即一个由运行时在进程内执行的常规委托。这就是 LINQ-to-Objects。</p><p>当您在<code>IQueryable&lt;T&gt;</code> 上调用相同的方法时，C# 编译器会将 lambda 封装在<code>Expression&lt;Func&lt;Product, bool&gt;&gt;</code> 中。这是一种数据结构，表示代码的<em>结构</em>，而不是代码的可执行形式。在运行时，该表达式树可被检查、分析，并转换为另一种语言。</p><pre><code>// IEnumerable: the lambda is a compiled delegate
IEnumerable<Product> local = products.Where(p => p.Price > 100);

// IQueryable: the lambda is an expression tree, a data structure
IQueryable<Product> remote = queryable.Where(p => p.Price > 100);</code></pre><p><code>IQueryProvider</code> 接口是扩展点。任何提供程序均可通过实现 <code>CreateQuery&lt;T&gt;</code> 和 <code>Execute&lt;T&gt;</code>，将这些表达式树转换为目标语言。实体框架就是利用此机制生成 SQL 语句。LINQ to ES|QL 提供程序使用它来生成 ES|QL 查询。</p><p>上述查询的表达式树如下所示：</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/f864cd18a390831f8d28503a29b5835efb1842f7-1000x720.png" style="width: 100%;" /><p><em>示例查询的表达式树。</em></p><p>此表达式树由内而外嵌套：<code>Take</code> 包裹着 <code>OrderByDescending</code>，它又包裹着 <code>Where</code>，而后者再包裹着 <code>From</code>，而最内层是根节点 <code>EsqlQueryable&lt;Product&gt;</code> 常量。对于 <code>&amp;&amp;</code>、<code>&gt;=</code> 和 <code>==</code> 这几种操作符而言，<code>Where</code> 谓词本身是一个由 <code>BinaryExpression</code> 个节点构成的子树，其中包含 <code>MemberExpression</code> 个叶子节点，这些叶子节点用于属性访问，以及对 <code>minPrice</code> 和 <code>brand</code> 变量的闭包捕获。提供程序会遍历这一数据结构，从而生成最终的 ES|QL 查询。</p><h2>深入了解：转换管道</h2><p>从 LINQ 表达式到查询结果的路径遵循六阶段管道：</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/2a2c772b63d720f61fc9a28b2f85668fa2db8d38-1999x1036.png" style="width: 100%;" /><p><em>转换管道概述。</em></p><h3>1. 表达式树捕获</h3><p>当在一个 <code>IQueryable&lt;T&gt;</code> 对象上串联使用 <code>.Where()</code>、<code>.OrderBy()</code>、<code>.Take()</code> 及其他操作符时，标准的 LINQ 基础架构会构建一个表达式树。<code>EsqlQueryable&lt;T&gt;</code> 实现了 <code>IQueryable&lt;T&gt;</code> 接口，并将处理委托给 <code>EsqlQueryProvider</code>。</p><h3>2. 翻译</h3><p>当查询被执行 (通过枚举、调用 <code>ToList()</code>，或使用 <code>await foreach)</code> 时)，<code>EsqlExpressionVisitor</code>自内而外遍历表达式树。它会将每个 LINQ 方法调用分派给一个专门的访问器进行处理：</p>
            <table style="border-collapse: collapse; margin: 20px 0;">
              <tbody>
                
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">访客</td><td style="border: 1px solid #ccc; padding: 4px;">翻译</td><td style="border: 1px solid #ccc; padding: 4px;">进入</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">WhereClauseVisitor</td><td style="border: 1px solid #ccc; padding: 4px;">.Where(predicate)</td><td style="border: 1px solid #ccc; padding: 4px;">WHERE 条件</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">SelectProjectionVisitor</td><td style="border: 1px solid #ccc; padding: 4px;">.Select(selector)</td><td style="border: 1px solid #ccc; padding: 4px;">评估 + 保留 + 重命名</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">GroupByVisitor</td><td style="border: 1px solid #ccc; padding: 4px;">.GroupBy().Select()</td><td style="border: 1px solid #ccc; padding: 4px;">统计信息 ... 依据</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">OrderByVisitor</td><td style="border: 1px solid #ccc; padding: 4px;">.OrderBy() / .ThenBy()</td><td style="border: 1px solid #ccc; padding: 4px;">SORT 字段 [ASC\|DESC]</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">EsqlFunctionTranslator</td><td style="border: 1px solid #ccc; padding: 4px;">EsqlFunctions.*、Math.*、字符串方法</td><td style="border: 1px solid #ccc; padding: 4px;">80+ ES|QL 函数</td>
                    </tr>
              </tbody>
            </table>
          <p>在翻译过程中，表达式中引用的 C# 变量被捕获为命名参数。</p><h3>3. 查询模型</h3><p>访问器不会直接生成字符串。相反，它们会产生 <code>QueryCommand</code> 对象，一个不可变的中间表征。一个 <code>FromCommand</code>、一个 <code>WhereCommand</code>、一个 <code>SortCommand</code> 和一个 <code>LimitCommand</code>，各代表一条 ES|QL 处理命令。这些数据被收集到<code>EsqlQuery</code> 模型中。</p><p></p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/2adc349b6cf655b96b7b3e826a134e8a17fe42fd-1999x1036.png" style="width: 100%;" /><p><em>查询模型和命令模式。</em></p><p>该中间模型与表达式树和输出格式均解耦。它可以被检查、拦截（通过 <code>IEsqlQueryInterceptor</code>）或在格式化前进行修改。</p><h3>4. 格式化</h3><p><code>EsqlFormatter</code> 依次访问每个<code>QueryCommand</code> ，并生成最终的 ES|QL 字符串。每条命令占一行，通过 ES|QL 中用于串联处理命令的管道 (|) 运算符分隔。若标识符包含特殊字符，系统会自动用反引号进行转义处理。</p><h3>5. 执行</h3><p>格式化后的 ES|QL 查询字符串及捕获的参数会以 JSON 数据载荷的形式发送至 Elasticsearch 的 <code>/_query</code> 终端。而 <code>IEsqlQueryExecutor</code> 接口则对传输层进行了抽象封装，这正是分层包架构发挥作用的关键环节。</p><h3>6. 实现</h3><p><code>EsqlResponseReader</code> 流式传输JSON响应，但不会将整个结果集缓冲到内存中。以流式方式传输 JSON 响应数据，无需将整个结果集缓存至内存。针对每次查询预先计算生成的 <code>ColumnLayout</code> 树结构，会将扁平化的 ES|QL 列名（如 <code>address.street</code>、<code>address.city</code>）映射到嵌套的 POCO 属性。每行数据会被组装为 <code>T</code> 实例，并通过 <code>IEnumerable&lt;T&gt;</code> 或 <code>IAsyncEnumerable&lt;T&gt;</code> 逐个返回。</p><h2>分层架构</h2><p>LINQ to ES|QL 功能分为三个软件包：</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/41b8aae860ecdc2480edcb1c1d4cc9b03cfb78c9-1999x1036.png" style="width: 100%;" /><p><em>软件包架构。</em><a href="https://www.nuget.org/packages/Elastic.Esql"><strong><code>Elastic.Esql</code></strong></a> 是纯转换引擎。该组件完全不依赖 HTTP 协议栈，集成了表达式访问器、查询模型、格式化器及响应解析器等核心模块。您可独立使用它来构建和检查 ES|QL 查询（无需连接 Elasticsearch），这在测试验证、查询日志记录或自定义执行层开发等场景中极具实用价值。翻译要点解析：</p><pre><code>// Translation-only: no Elasticsearch connection needed
var provider = new EsqlQueryProvider();
var query = new EsqlQueryable<Product>(provider)
    .From("products")
    .Where(p => p.InStock)
    .OrderByDescending(p => p.Price);

Console.WriteLine(query.ToEsqlString());
// FROM products | WHERE in_stock == true | SORT price_usd DESC</code></pre><p><a href="https://www.nuget.org/packages/Elastic.Clients.Esql"><strong><code>Elastic.Clients.Esql</code></strong></a> 是一款轻量级的独立 ES|QL 客户端。该组件通过 <code>Elastic.Transport</code> 在 <code>Elastic.Esql</code> 之上扩展了 HTTP 协议执行能力。如果您的应用程序仅需使用 ES|QL 而无需其他 Elasticsearch API，此方案可实现最小化依赖集成。</p><p><a href="https://www.nuget.org/packages/Elastic.Clients.Elasticsearch"><strong><code>Elastic.Clients.Elasticsearch</code></strong></a> 是完整的 Elasticsearch.NET 客户端。它还建立在<code>Elastic.Esql</code> 的基础上，并通过<code>client.Esql</code> 命名空间公开 LINQ 提供程序接口。这是大多数应用程序的推荐入口点。</p><p>两个执行层组件包均提供了针对 <code>IEsqlQueryExecutor</code> 接口的独立实现。该策略接口作为转换与传输层的桥梁。</p><p>当与源码生成的 <code>JsonSerializerContext</code> 配合使用时，这三个组件包均支持原生 AOT 编译。如需完整客户端集成方案，请参阅<a href="https://www.elastic.co/docs/reference/elasticsearch/clients/dotnet/source-serialization#native-aot">原生 AOT 文档</a>。</p><h2>不只使用基础功能</h2><p>上面的例子涵盖了筛选、排序和分页。该提供程序支持更广泛的操作范围。</p><h3>聚合</h3><p><code>GroupBy</code>结合 <code>Select</code> 中的聚合函数，转换为 ES|QL <a href="https://www.elastic.co/docs/reference/query-languages/esql/commands/stats-by"><code>STATS ... BY</code></a>：</p><pre><code>var stats = client.Esql.Query<Product, object>(q => q
    .GroupBy(p => p.Brand)
    .Select(g => new
    {
        Brand = g.Key,
        Count = g.Count(),
        AvgPrice = g.Average(p => p.Price),
        MaxPrice = g.Max(p => p.Price)
    }));

// -> FROM products | STATS COUNT(*), AVG(price_usd), MAX(price_usd) BY brand</code></pre><h3>投影</h3><p><code>Select</code>，使用匿名类型生成 <a href="https://www.elastic.co/docs/reference/query-languages/esql/commands/eval"><code>EVAL</code></a>、<a href="https://www.elastic.co/docs/reference/query-languages/esql/commands/keep"><code>KEEP</code></a> 和 <a href="https://www.elastic.co/docs/reference/query-languages/esql/commands/rename"><code>RENAME</code></a> 命令：</p><pre><code>var query = client.Esql.CreateQuery<Product>()
    .Select(p => new { ProductName = p.Name, p.Price, p.InStock });

// -> FROM products | KEEP name, price_usd, in_stock | RENAME name AS ProductName</code></pre><h3>丰富的函数库</h3><p>通过 <code>EsqlFunctions</code> 类，可以使用超过 80 个 ES|QL 函数，涵盖日期/时间、字符串、数学、IP、模式匹配和评分。标准 <code>Math.*</code> 和 <code>string.*</code> 方法也已转换：</p><pre><code>.Where(p => p.Name.Contains("Pro"))       // -> WHERE name LIKE "*Pro*"
.Where(p => EsqlFunctions.CidrMatch(      // -> WHERE CIDR_MATCH(ip, "10.0.0.0/8")
    p.IpAddress, "10.0.0.0/8"))</code></pre><h3>查找连接</h3><p>跨索引查找转换为 ES|QL <a href="https://www.elastic.co/docs/reference/query-languages/esql/commands/lookup-join"><code>LOOKUP JOIN</code></a>：</p><pre><code>var enriched = client.Esql.Query<Product, object>(q => q
    .LookupJoin<Product, CategoryLookup, string, object>(
        "category-lookup-index",
        product => product.Id,
        category => category.CategoryId,
        (product, category) => new { product.Name, category!.CategoryLabel }));</code></pre><h3>原生 ES|QL 直通接口</h3><p>对于 LINQ 提供程序尚未涵盖的 ES|QL 功能，您可以添加原始片段：</p><pre><code>var results = client.Esql.Query<Product>(q => q
    .Where(p => p.InStock)
    .RawEsql("| EVAL discounted = price_usd * 0.9"));</code></pre><h3>服务器端异步查询</h3><p>对于长时间运行的查询，可将其提交给服务器进行后台处理：</p><pre><code>await using var asyncQuery = await client.Esql.SubmitAsyncQueryAsync<Product>(
    q => q.Where(p => p.InStock),
    asyncQueryOptions: new EsqlAsyncQueryOptions
    {
        WaitForCompletionTimeout = TimeSpan.FromSeconds(5),
        KeepAlive = TimeSpan.FromMinutes(10)
    });

await asyncQuery.WaitForCompletionAsync();
await foreach (var product in asyncQuery.AsAsyncEnumerable())
    Console.WriteLine(product.Name);</code></pre><p>服务器端异步查询对于长时间运行的分析型查询/大规模数据集处理尤其有用，这类操作可能会超出常规的超时阈值；在存在负载均衡器、API 网关或代理（这些组件会强制执行严格的 HTTP 超时设置）的超时敏感环境中，异步查询同样优势显著。异步查询通过将查询提交与结果获取解耦，避免了连接中断的情况。</p><h2>开始使用</h2><p>LINQ to ES|QL 自以下版本起可用：</p><ul><li><strong>Elastic.Clients.Elasticsearch v9.3.4</strong>（9.x 分支）</li><li><strong>Elastic.Clients.Elasticsearch v8.19.18</strong>（8.x 分支）</li></ul><p>从 NuGet 安装：</p><p><code>dotnet add package Elastic.Clients.Elasticsearch</code></p><p>入口点位于<code>client.Esql</code>上：</p>
            <table style="border-collapse: collapse; margin: 20px 0;">
              <tbody>
                
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">方法</td><td style="border: 1px solid #ccc; padding: 4px;">返回值</td><td style="border: 1px solid #ccc; padding: 4px;">用例</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">查询<T>(...)</td><td style="border: 1px solid #ccc; padding: 4px;">IEnumerable<T></td><td style="border: 1px solid #ccc; padding: 4px;">同步执行</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">QueryAsync<T>(...)</td><td style="border: 1px solid #ccc; padding: 4px;">IAsyncEnumerable<T></td><td style="border: 1px solid #ccc; padding: 4px;">异步流式处理</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">CreateQuery<T>()</td><td style="border: 1px solid #ccc; padding: 4px;">IEsqlQueryable<T></td><td style="border: 1px solid #ccc; padding: 4px;">高级结构分析和检测</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">SubmitAsyncQueryAsync<T>(...)</td><td style="border: 1px solid #ccc; padding: 4px;">EsqlAsyncQuery<T></td><td style="border: 1px solid #ccc; padding: 4px;">长时间运行的服务器端查询</td>
                    </tr>
              </tbody>
            </table>
          <p>有关完整的功能参考，包括查询选项、多字段访问、嵌套对象和多值字段处理，请参阅<a href="https://www.elastic.co/docs/reference/elasticsearch/clients/dotnet/linq-to-esql">LINQ to ES|QL 文档</a>。</p><h2>结论</h2><p>LINQ 转 ES|QL 将 C# LINQ 的强大表达能力引入到 Elasticsearch 的 ES|QL 查询语言中，让您无需手工编写查询字符串，就能生成强类型、可组合的查询。它具备自动参数捕获、流式物化功能，还拥有分层式的软件包架构，既能满足独立转换需求，也能适配完整的 Elasticsearch 客户端，可自然融入任意规模的 .NET 应用程序。安装最新客户端，将 LINQ 表达式指向索引，剩下的就交给该提供程序来处理。</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[使用 TypeScript 构建 Elasticsearch MCP 服务器]]></title>
            <description><![CDATA[学习如何使用 TypeScript 和 Claude Desktop 创建 Elasticsearch MCP 服务器。]]></description>
            <link>https://www.elastic.co/search-labs/cn/blog/elastic-mcp-server-typescript-claude</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/cn/blog/elastic-mcp-server-typescript-claude</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Jeffrey Rengifo]]></dc:creator>
            <pubDate>Fri, 27 Mar 2026 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/0bb24c05fff391f42070c2883182ea6fe9cb9680-1280x720.png?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>在 Elasticsearch 中处理大型知识库时，找到信息只是成功的一半。工程师通常还需要综合多个文档的结果，生成摘要，并追溯答案的来源。模型上下文协议 (MCP) 提供了一种标准化的方式，可将 Elasticsearch 与大语言模型 (LLM) 驱动的应用程序连接起来，以实现上述目标。虽然 Elastic 提供官方解决方案，例如 Elastic Agent Builder（其功能包括 <a href="https://www.elastic.co/docs/solutions/search/agent-builder/mcp-server">MCP 终端</a>），但构建自定义 MCP 服务器可让您完全掌控搜索逻辑、结果格式，以及如何将检索到的内容传递给 LLM，以用于综合分析、生成摘要和提供引用。</p><p>本文将探讨构建自定义 Elasticsearch MCP 服务器的优势，并展示如何使用 TypeScript 创建该服务器，以将 Elasticsearch 连接到 LLM 驱动的应用程序。</p><h2>为什么要构建自定义 Elasticsearch MCP 服务器？</h2><p>Elastic 为 <a href="https://www.elastic.co/docs/solutions/search/mcp">MCP 服务器</a>提供了一些替代方案：</p><ul><li><a href="https://www.elastic.co/docs/solutions/search/agent-builder/mcp-server">Elastic Agent Builder MCP 服务器，适用于 Elasticsearch 9.2 及以上版本</a></li><li><a href="https://github.com/elastic/mcp-server-elasticsearch?tab=readme-ov-file#elasticsearch-mcp-server">适用于旧版本的 Elasticsearch MCP 服务器（Python）</a></li></ul><p>如果您需要更好地控制 MCP 服务器与 Elasticsearch 的交互，构建自己的自定义服务器可以让您灵活地根据自身需求进行定制。例如，Agent Builder 的 MCP 终端仅限于 Elasticsearch 查询语言 (ES|QL) 查询，而自定义服务器允许您使用完整的查询 DSL。在将结果传递给 LLM 之前，您还可以控制结果的格式，并可以集成其他处理步骤，例如我们将在本教程中实现的由 OpenAI 驱动的摘要功能。</p><p>通过阅读本文，您将学会使用 TypeScript 创建 MCP 服务器，该服务器可搜索存储在 Elasticsearch 索引中的信息，对其进行总结并提供引用。我们将使用 Elasticsearch 进行检索，使用 OpenAI 的 <code>gpt-4o-mini</code> 模型提炼摘要并生成引用，并使用 Claude Desktop 作为 MCP 客户端和 UI 来接收用户查询并提供回复。最终我们将得到一个内部知识助手，帮助工程师在整个组织的技术文档中发现并综合最佳实践。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/ec5771a874cf9740d4cac6888622cbe8cd6aede7-1999x1133.png" style="width: 100%;" /><h2>准备工作：</h2><ul><li>Node.js 20 +</li><li>Elasticsearch</li><li>OpenAI API 密钥</li><li>Claude Desktop</li></ul><h3>什么是 MCP？</h3><p><a href="https://www.elastic.co/what-is/mcp">MCP</a> 是由 <a href="https://www.anthropic.com/news/model-context-protocol">Anthropic</a> 创建的开放标准，提供大型语言模型与外部系统（如 Elasticsearch）之间的安全双向连接。您可以在<a href="https://www.elastic.co/search-labs/blog/mcp-current-state">这篇文章</a>中了解更多关于 MCP 现状的信息。</p><p>MCP 的发展<a href="https://www.elastic.co/search-labs/blog/mcp-current-state#mcp-project-updates:-transport,-elicitation,-and-structured-tooling">每天都在变化</a>，服务器的使用范围越来越广。此外，构建自定义 MCP 服务器也非常简单，我们将在本文中进行演示。</p><h3>MCP 客户端</h3><p><a href="https://modelcontextprotocol.io/clients">可用的 MCP 客户端</a>由很多，每个客户端都有自己的特点和局限性。为了简化和普及，我们将使用 <a href="https://claude.ai/download">Claude Desktop</a> 作为演示中的 MCP 客户端。它将作为聊天界面，用户可以用自然语言提问，它还将自动调用我们的 MCP 服务器提供的工具来搜索文档和生成摘要。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/66eb0b11473347b6cf2d85718251eeac38d6249d-1999x1491.png" style="width: 100%;" /><h2>创建 Elasticsearch MCP 服务器</h2><p>通过使用 <a href="https://github.com/modelcontextprotocol/typescript-sdk">TypeScript 软件开发工具包</a>，我们可以轻松创建一个能够根据用户查询输入来查询 Elasticsearch 数据的服务器。</p><p>本文将介绍将 Elasticsearch MCP 服务器与 Claude Desktop 客户端集成的步骤：</p><ol><li><a href="https://www.elastic.co/search-labs/blog/elastic-mcp-server-typescript-claude#configure-mcp-server-for-elasticsearch">为 Elasticsearch 配置 MCP 服务器。</a></li><li><a href="https://www.elastic.co/search-labs/blog/elastic-mcp-server-typescript-claude#load-the-mcp-server-into-claude-desktop">将 MCP 服务器加载到 Claude Desktop。</a></li><li><a href="https://www.elastic.co/search-labs/blog/elastic-mcp-server-typescript-claude#test-it-out">测试一下。</a></li></ol><h3>为 Elasticsearch 配置 MCP 服务器。</h3><p>首先，我们来初始化一个 Node 应用程序：</p><pre><code>npm init -y</code></pre><p>这将会创建一个 <code>package.json</code> 文件，有了它，我们就可以开始安装该应用程序所需的依赖项。</p><pre><code>npm install @elastic/elasticsearch @modelcontextprotocol/sdk openai zod && npm install --save-dev ts-node @types/node typescript</code></pre><ul><li><strong>@elastic/elasticsearch</strong> 将使我们能够访问 Elasticsearch Node.js 库。</li><li><strong>@modelcontextprotocol/sdk</strong> 提供核心工具来创建和管理 MCP 服务器、注册工具以及处理与 MCP 客户端的通信。</li><li><strong>openai</strong> 允许与 OpenAI 模型进行交互以生成摘要或自然语言响应。</li><li><strong><a href="https://zod.dev/">zod</a> </strong>帮助定义和验证每个工具中输入和输出数据的结构化模式。</li></ul><p><code>ts-node</code>，<code>@types/node</code> 和 <code>typescript</code> 将在开发过程中用于键入代码和编译脚本。</p><h4>配置数据集</h4><p>为了提供 Claude Desktop 可以使用我们的 MCP 服务器进行查询的数据，我们将使用模拟的<a href="https://github.com/Delacrobix/typescript-elasticsearch-mcp/blob/main/dataset.json">内部知识库数据集</a>。来自该数据集的文档是这样子的：</p><pre><code>{
    "id": 5,
    "title": "Logging Standards for Microservices",
    "content": "Consistent logging across microservices helps with debugging and tracing. Use structured JSON logs and include request IDs and timestamps. Avoid logging sensitive information. Centralize logs in Elasticsearch or a similar system. Configure log rotation to prevent storage issues and ensure logs are searchable for at least 30 days.",
    "tags": ["logging", "microservices", "standards"]
}</code></pre><p>为了摄取数据，我们准备了一个脚本，该脚本在 Elasticsearch 中创建一个索引并将数据集加载到其中。您可以<a href="https://github.com/Delacrobix/typescript-elasticsearch-mcp/blob/main/setup.ts">在这里</a>找到它。</p><h4>MCP 服务器</h4><p>创建一个名为 <a href="https://github.com/Delacrobix/typescript-elasticsearch-mcp/blob/main/index.ts"><code>index.ts</code></a> 的文件，并添加以下代码来导入依赖项并处理环境变量：</p><pre><code>// index.ts
import { z } from "zod";
import { Client } from "@elastic/elasticsearch";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import OpenAI from "openai";

const ELASTICSEARCH_ENDPOINT =
  process.env.ELASTICSEARCH_ENDPOINT ?? "http://localhost:9200";
const ELASTICSEARCH_API_KEY = process.env.ELASTICSEARCH_API_KEY ?? "";
const OPENAI_API_KEY = process.env.OPENAI_API_KEY ?? "";
const INDEX = "documents";</code></pre><p>此外，让我们初始化客户端以处理 Elasticsearch 和 OpenAI 的调用：</p><pre><code>const openai = new OpenAI({
  apiKey: OPENAI_API_KEY,
});

const _client = new Client({
  node: ELASTICSEARCH_ENDPOINT,
  auth: {
    apiKey: ELASTICSEARCH_API_KEY,
  },
});</code></pre><p>为了使我们的实现更加稳健，并确保输入和输出结构化，我们将使用 <a href="https://zod.dev/"><code>zod</code></a> 定义模式。这使我们能够在运行时验证数据，及早发现错误，并使工具响应更容易以编程方式进行处理：</p><pre><code>const DocumentSchema = z.object({
  id: z.number(),
  title: z.string(),
  content: z.string(),
  tags: z.array(z.string()),
});

const SearchResultSchema = z.object({
  id: z.number(),
  title: z.string(),
  content: z.string(),
  tags: z.array(z.string()),
  score: z.number(),
});

type Document = z.infer<typeof DocumentSchema>;
type SearchResult = z.infer<typeof SearchResultSchema>;</code></pre><p>请在<a href="https://www.elastic.co/search-labs/blog/structured-outputs-elasticsearch-guide">此处</a>了解更多关于结构化输出的信息。</p><p>现在让我们初始化 MCP 服务器：</p><pre><code>const server = new McpServer({
  name: "Elasticsearch RAG MCP",
  description:
    "A RAG server using Elasticsearch. Provides tools for document search, result summarization, and source citation.",
  version: "1.0.0",
});</code></pre><h4>定义 MCP 工具</h4><p>完成所有配置后，我们就可以开始编写将由 MCP 服务器公开的工具了。此服务器公开两种工具：</p><ul><li><strong><code>search_docs</code>：</strong>使用全文本搜索在 Elasticsearch 中搜索文档。</li><li><strong><code>summarize_and_cite</code>：</strong>汇总和综合先前检索到的文档中的信息，以回答用户的问题。该工具还可添加引用源文档的引文。</li></ul><p>这两个工具共同构成了一个简单的“检索后总结”工作流，其中一个工具获取相关文档，另一个工具使用这些文档生成汇总的引用回复。</p><h4>工具响应格式</h4><p>每个工具都可以接受任意输入参数，但必须以以下结构作出响应：</p><ul><li><strong>内容：</strong>这是工具以非结构化格式做出的响应。该字段通常用于返回文本、图像、音频、链接或嵌入内容。在本应用程序中，它将用于返回包含工具生成的信息的格式化文本。</li><li><strong>结构化内容： </strong>这是一个可选返回，用于以结构化格式提供每个工具的结果。这对程序化用途非常有用。虽然本 MCP 服务器没有使用它，但如果您想开发其他工具或以编程方式处理结果，它可能会很有用。</li></ul><p>基于这个结构，让我们详细探讨每个工具。</p><h4>Search_docs 工具</h4><p>此工具在 Elasticsearch 索引中执行 <a href="https://www.elastic.co/docs/solutions/search/full-text">全文本搜索</a>，以根据用户查询检索最相关的文档。它突出显示关键匹配项，并快速提供相关性评分概述。</p><pre><code>server.registerTool(
  "search_docs",
  {
    title: "Search Documents",
    description:
      "Search for documents in Elasticsearch using full-text search. Returns the most relevant documents with their content, title, tags, and relevance score.",
    inputSchema: {
      query: z
        .string()
        .describe("The search query terms to find relevant documents"),
      max_results: z
        .number()
        .optional()
        .default(5)
        .describe("Maximum number of results to return"),
    },
    outputSchema: {
      results: z.array(SearchResultSchema),
      total: z.number(),
    },
  },
  async ({ query, max_results }) => {
    if (!query) {
      return {
        content: [
          {
            type: "text",
            text: "Query parameter is required",
          },
        ],
        isError: true,
      };
    }

    try {
      const response = await _client.search({
        index: INDEX,
        size: max_results,
        query: {
          bool: {
            must: [
              {
                multi_match: {
                  query: query,
                  fields: ["title^2", "content", "tags"],
                  fuzziness: "AUTO",
                },
              },
            ],
            should: [
              {
                match_phrase: {
                  title: {
                    query: query,
                    boost: 2,
                  },
                },
              },
            ],
          },
        },
        highlight: {
          fields: {
            title: {},
            content: {},
          },
        },
      });

      const results: SearchResult[] = response.hits.hits.map((hit: any) => {
        const source = hit._source as Document;

        return {
          id: source.id,
          title: source.title,
          content: source.content,
          tags: source.tags,
          score: hit._score ?? 0,
        };
      });

      const contentText = results
        .map(
          (r, i) =>
            `[${i + 1}] ${r.title} (score: ${r.score.toFixed(
              2,
            )})\n${r.content.substring(0, 200)}...`,
        )
        .join("\n\n");

      const totalHits =
        typeof response.hits.total === "number"
          ? response.hits.total
          : (response.hits.total?.value ?? 0);

      return {
        content: [
          {
            type: "text",
            text: `Found ${results.length} relevant documents:\n\n${contentText}`,
          },
        ],
        structuredContent: {
          results: results,
          total: totalHits,
        },
      };
    } catch (error: any) {
      console.log("Error during search:", error);

      return {
        content: [
          {
            type: "text",
            text: `Error searching documents: ${error.message}`,
          },
        ],
        isError: true,
      };
    }
  }
);</code></pre><p><em>我们将 fuzziness : “AUTO” 配置为根据被分析的词元的长度具有可变的拼写错误容忍度。我们还设置了</em> <em><code>title^2</code></em> <em>来提高标题字段匹配的文档的分数。</em></p><h4>摘要和引用工具</h4><p>该工具根据上一次搜索中检索到的文档生成摘要。它使用 OpenAI 的 <code>gpt-4o-mini</code> 模型来综合最相关的信息，提供直接来自搜索结果的响应，以回答用户的问题。除了摘要之外，它还返回所使用源文档的引用元数据。</p><pre><code>server.registerTool(
  "summarize_and_cite",
  {
    title: "Summarize and Cite",
    description:
      "Summarize the provided search results to answer a question and return citation metadata for the sources used.",
    inputSchema: {
      results: z
        .array(SearchResultSchema)
        .describe("Array of search results from search_docs"),
      question: z.string().describe("The question to answer"),
      max_length: z
        .number()
        .optional()
        .default(500)
        .describe("Maximum length of the summary in characters"),
      max_docs: z
        .number()
        .optional()
        .default(5)
        .describe("Maximum number of documents to include in the context"),
    },
    outputSchema: {
      summary: z.string(),
      sources_used: z.number(),
      citations: z.array(
        z.object({
          id: z.number(),
          title: z.string(),
          tags: z.array(z.string()),
          relevance_score: z.number(),
        })
      ),
    },
  },
  async ({ results, question, max_length, max_docs }) => {
    if (!results || results.length === 0 || !question) {
      return {
        content: [
          {
            type: "text",
            text: "Both results and question parameters are required, and results must not be empty",
          },
        ],
        isError: true,
      };
    }

    try {
      const used = results.slice(0, max_docs);

      const context = used
        .map(
          (r: SearchResult, i: number) =>
            `[Document ${i + 1}: ${r.title}]\\n${r.content}`
        )
        .join("\n\n---\n\n");

      // Generate summary with OpenAI
      const completion = await openai.chat.completions.create({
        model: "gpt-4o-mini",
        messages: [
          {
            role: "system",
            content:
              "You are a helpful assistant that answers questions based on provided documents. Synthesize information from the documents to answer the user's question accurately and concisely. If the documents don't contain relevant information, say so.",
          },
          {
            role: "user",
            content: `Question: ${question}\\n\\nRelevant Documents:\\n${context}`,
          },
        ],
        max_tokens: Math.min(Math.ceil(max_length / 4), 1000),
        temperature: 0.3,
      });

      const summaryText =
        completion.choices[0]?.message?.content ?? "No summary generated.";

      const citations = used.map((r: SearchResult) => ({
        id: r.id,
        title: r.title,
        tags: r.tags,
        relevance_score: r.score,
      }));

      const citationText = citations
        .map(
          (c: any, i: number) =>
            `[${i + 1}] ID: ${c.id}, Title: "${c.title}", Tags: ${c.tags.join(
              ", ",
            )}, Score: ${c.relevance_score.toFixed(2)}`,
        )
        .join("\n");

      const combinedText = `Summary:\\n\\n${summaryText}\\n\\nSources used (${citations.length}):\\n\\n${citationText}`;

      return {
        content: [
          {
            type: "text",
            text: combinedText,
          },
        ],
        structuredContent: {
          summary: summaryText,
          sources_used: citations.length,
          citations: citations,
        },
      };
    } catch (error: any) {
      return {
        content: [
          {
            type: "text",
            text: `Error generating summary and citations: ${error.message}`,
          },
        ],
        isError: true,
      };
    }
  }
);</code></pre><p>最后，我们需要用 <a href="https://github.com/modelcontextprotocol/typescript-sdk?tab=readme-ov-file#stdio">stdio</a> 启动服务器。这意味着 MCP 客户端将通过读取和写入其标准输入和输出流与我们的服务器进行通信。stdio 是最简单的传输选项，适用于客户端作为子进程启动的本地 MCP 服务器。在文件末尾添加以下代码：</p><pre><code>const transport = new StdioServerTransport();
server.connect(transport);</code></pre><p>现在请您使用以下命令编译该项目：</p><pre><code>npx tsc index.ts --target ES2022 --module node16 --moduleResolution node16 --outDir ./dist --strict --esModuleInterop</code></pre><p>这将创建一个 <code>dist</code> 文件夹，并在其中创建一个 <code>index.js</code> 文件。</p><h3>将 MCP 服务器加载到 Claude Desktop。</h3><p>请按照<a href="https://modelcontextprotocol.io/docs/develop/connect-local-servers">本指南</a>配置 MCP 服务器和 Claude Desktop。在 Claude 配置文件中，我们需要设置以下值：</p><pre><code>{
  "mcpServers": {
    "elasticsearch-rag-mcp": {
      "command": "node",
      "args": [   "/Users/user-name/app-dir/dist/index.js"
      ],
      "env": {
        "ELASTICSEARCH_ENDPOINT": "your-endpoint-here",
        "ELASTICSEARCH_API_KEY": "your-api-key-here",
        "OPENAI_API_KEY": "your-openai-key-here"
      }
    }
  }
}</code></pre><p><code>args</code> 的值应指向 <code>dist</code> 文件夹中的编译后文件。您还需要在配置文件中设置环境变量，使其名称与代码中定义的名称完全一致。</p><h3>测试一下</h3><p>在执行每个工具前，点击<strong>搜索和工具</strong>，确保这些工具已启用。您还可以在这里启用或禁用每个工具：</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/172981c2a54adabc70d5819013c3007670935605-1999x1002.png" style="width: 100%;" /><p>最后，让我们从 Claude Desktop 聊天中测试 MCP 服务器，并开始提问：</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/03654c0f8c53c714f801fba8b25747071179209b-1999x1353.png" style="width: 100%;" /><p>针对问题“<strong>搜索有关身份验证方法和基于角色的访问控制的文档</strong>”，已执行 <code>search_docs</code> 工具并返回以下结果：</p><pre><code>Most Relevant Documents:
Access Control and Role Management (highest relevance) - This document covers role-based access control (RBAC) principles, including ensuring users only have necessary permissions, regular auditing of user roles, revoking inactive accounts, and implementing just-in-time access for sensitive operations.
User Authentication with OAuth 2.0 - This document explains OAuth 2.0 authentication, which enables secure delegated access without credential sharing. It covers configuring identity providers, token management with limited scope and lifetime, and secure storage of refresh tokens.
Container Security Guidelines - While primarily about container security, this document touches on access control aspects like running containers as non-root users and avoiding embedded credentials.
Incident Response Playbook - This mentions role assignment during incidents (incident commander, communications lead, etc.), which relates to access control in emergency scenarios.
Logging Standards for Microservices - This document includes guidance on avoiding logging sensitive information, which is relevant to authentication security.</code></pre><p>回复是：“太好了！我找到了 5 份关于身份验证方法和基于角色的访问控制的相关文档。以下是找到的内容：”</p><p>该工具调用会将源文档作为其响应有效负载的一部分返回，这些文档随后用于生成引用。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/25ff4c7e9563d99752f95540dafdc7fd211a66e3-800x530.gif" style="width: 100%;" /><p>您也可以在一次交互中串联使用多个工具。在这种情况下，Claude Desktop 会分析用户的问题，并确定首先需要调用 <code>search_docs</code> 来检索相关文档，然后将这些结果传递给 <code>summarize_and_cite</code> 以生成最终回答，所有这些都无需用户单独提示：</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/e6a8a46acb4236e77058f18bcd2f0737b5882c05-1999x1101.png" style="width: 100%;" /><p>在这种情况下，对于“<strong>在我们的系统中改进身份验证和访问控制的主要建议是什么？附上参考文献</strong>”，我们得到了以下结果：</p><pre><code>Based on the documentation, here are the main recommendations to improve authentication and access control across your systems:

Key Recommendations

1. Implement Role-Based Access Control (RBAC)
Ensure users have only the permissions necessary for their job functions. This principle of least privilege minimizes security risks by limiting access to what's truly needed. [1]

2. Regular Access Audits
Conduct regular audits of user roles and promptly revoke access for inactive accounts. This helps prevent unauthorized access from dormant or compromised accounts. [1]

3. Just-in-Time (JIT) Access
Implement just-in-time access for sensitive operations, where permissions are granted only when needed and for a limited duration. This reduces the attack surface by minimizing standing privileges. [1]

4. OAuth 2.0 for Secure Authentication
Use OAuth 2.0 to enable secure delegated access without sharing user credentials. This protocol allows third-party applications to access resources on behalf of users without exposing passwords. [2]

5. Token Security and Management
Configure your identity provider to issue tokens with limited scope and lifetime

Store refresh tokens securely
Validate access tokens consistently to maintain security [2]
References

Access Control and Role Management (Tags: security, access-control)
User Authentication with OAuth 2.0 (Tags: authentication, oauth)
These recommendations work together to create a defense-in-depth approach, where multiple security layers protect your systems from unauthorized access.</code></pre><p>与上一步一样，我们可以看到每个工具对该问题的响应：</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/cb606d356b2f7d5e4878a5eff71bc881869ac0ee-800x585.gif" style="width: 100%;" /><p><em>注意：如果出现子菜单询问是否批准使用每个工具，请选择<strong>“始终允许”</strong>或<strong>“允许一次”</strong>。</em></p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/aea942ba9b0037526ea215bec65690f1a5c3099c-1522x250.png" style="width: 100%;" /><h2>结论</h2><p>MCP 服务器代表了本地和远程应用中 LLM 工具标准化的重要一步。虽然完全兼容仍在开发中，但我们正朝这个方向快速推进。</p><p>在本文中，我们学习了如何用 TypeScript 构建一个自定义 MCP 服务器，将 Elasticsearch 连接到基于 LLM 的应用。我们的服务器公开了两个工具：<code>search_docs</code> 用于使用查询 DSL 检索相关文档；<code>summarize_and_cite</code> 用于通过 OpenAI 模型和 Claude Desktop 作为客户端 UI 生成带引用的摘要。</p><p>不同客户端和服务器提供商之间的兼容性前景看起来一片光明。下一步包括为您的智能体添加更多功能和灵活性。这里有一篇实用的<a href="https://www.elastic.co/search-labs/blog/llm-functions-elasticsearch-intelligent-query">文章</a>介绍了如何使用搜索模板参数化查询，以获得精确性和灵活性。</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[宣布 Kibana 仪表板的只读权限]]></title>
            <description><![CDATA[在 Kibana 中引入只读仪表板，为仪表板创建者提供细粒度的共享控制，以保持结果的准确性并防止不必要的更改。]]></description>
            <link>https://www.elastic.co/search-labs/cn/blog/kibana-dashboards-read-only-permissions</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/cn/blog/kibana-dashboards-read-only-permissions</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Fabio Busatto, Teresa Alvarez Soler]]></dc:creator>
            <pubDate>Thu, 26 Mar 2026 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/11e578bc317aea30d2e10ccc0334a532f6af2ef9-720x420.jpg?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>您去过那里。您花了一个小时构建完美的仪表板来监测日志：每个图表、每个过滤器和每个标签。您将其分享给了团队。几天后，您打开它，发现有些不对劲。一位同事对查询进行了微调。或者有人更改了日期范围。也许他们以为自己是在帮忙。现在您正在翻查修改记录，对每个数字都心存疑虑。听起来很熟悉？</p><p>正因如此，我们才构建了<strong>只读仪表板</strong>。这是您一直在要求的控制权。放心地共享仪表板，无需担心下一个拥有编辑权限的人会更改或破坏仪表板。</p><p>注意：只读权限在 Elastic Cloud Serverless 中可用，并且从 9.3 版本开始在 Elastic Cloud Hosted 和 Elastic Self-Managed 中可用。</p><h2>当“人人皆可编辑”成为障碍时</h2><p>在 Kibana 中，<em>共享</em>通常意味着空间层级的权限。如果有人可以在某个空间创建仪表板，他们也可以编辑或删除其他人的仪表板。这对协作来说原本是件好事，直到情况变得不妙。一次意外的编辑可能会导致错误的决策、失去信任和大量的清理工作。</p><p>我们听说过一些替代方案：<strong>“我们在仪表板名称中加上‘只读’，希望用户能注意到”。</strong>或者：<strong>“我们给它们贴上标签，然后祈祷好运。”</strong>希望并不是一种权限模型。您需要一种真正的方法来锁定仪表板，同时又不将所有人拒之门外。</p><h2>到底出了什么问题</h2><p>Deb 和 Kevin 都拥有对运营空间中的日志监控仪表板的编辑权限。Kevin 对图表进行了一些更改。当 Deb 回来后，发现数字与她之前提交的不符。她必须追溯哪些地方发生了变化（通常凭记忆），然后进行修正，还要弄清楚有多少份报告发出了错误数据。</p><h2>只读仪表板：合理的所有权和控制权</h2><p>只读仪表板可解决此问题，让您能够控制其他用户是否可以编辑该仪表板。共享仪表板时，您可以选择：<strong>编辑</strong>（默认，与当前相同）或<strong>查看</strong>。在<strong>查看</strong>模式下，只有你（和 Kibana 管理员）可以对其进行更改或删除。其他人可以打开它、使用它、信任它，但他们无法对其进行修改。</p><h3>您将获得的内容</h3><ul><li><strong>仪表板完整性：</strong>在<strong>查看</strong>模式下，该空间内具有编辑权限的其他用户无法修改或删除仪表板。如果他们尝试，会被告知仪表板已锁定。您的图表和逻辑将保持原样。</li><li><strong>您掌控一切：</strong>你是所有者。您随时都可以编辑、完善和更新。以“仅查看”的方式共享并不会将您锁定，而是会锁定其他人看到的版本。</li><li><strong>灵活的生命周期：</strong>您可以随时将仪表板切换回“可编辑”状态。Kibana 管理员仍然可以管理所有仪表板（例如，在仪表板所有者离开的情况下）。因此，不会出现无人管理的情况。</li></ul><p>您可以广泛共享最终确定的关键任务仪表板，并确信这些仪表板将保持一致。<strong>所有 Elastic 层级和产品</strong>（包括 Serverless）均提供此功能。</p><h3>谁能做什么？</h3><p>按角色快速参考：</p><ul><li><strong>仪表板所有者：</strong>您创建了它；您拥有完全的编辑权限。</li><li><strong>Kibana 管理员：</strong>可以管理所有仪表板。</li><li><strong>具有空间编辑权限的用户：</strong>可以创建和编辑自己的仪表板；但不能编辑或删除仅查看的仪表板。</li><li><strong>具有空间视图的用户：</strong>只能查看（和列出）仪表板。</li></ul>
            <table style="border-collapse: collapse; margin: 20px 0;">
              <tbody>
                
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">操作</td><td style="border: 1px solid #ccc; padding: 4px;">仪表板所有者</td><td style="border: 1px solid #ccc; padding: 4px;">Kibana 管理员</td><td style="border: 1px solid #ccc; padding: 4px;">具有空间编辑权限的用户</td><td style="border: 1px solid #ccc; padding: 4px;">具有空间视图的用户</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">列出并查看仪表板</td><td style="border: 1px solid #ccc; padding: 4px;">✔</td><td style="border: 1px solid #ccc; padding: 4px;">✔</td><td style="border: 1px solid #ccc; padding: 4px;">✔</td><td style="border: 1px solid #ccc; padding: 4px;">✔</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">创建新的仪表板</td><td style="border: 1px solid #ccc; padding: 4px;">✔</td><td style="border: 1px solid #ccc; padding: 4px;">✔</td><td style="border: 1px solid #ccc; padding: 4px;">✔</td><td style="border: 1px solid #ccc; padding: 4px;">✘</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">修改/删除可编辑的仪表板</td><td style="border: 1px solid #ccc; padding: 4px;">✔</td><td style="border: 1px solid #ccc; padding: 4px;">✔</td><td style="border: 1px solid #ccc; padding: 4px;">✔</td><td style="border: 1px solid #ccc; padding: 4px;">✘</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">修改/删除只读仪表板</td><td style="border: 1px solid #ccc; padding: 4px;">✔</td><td style="border: 1px solid #ccc; padding: 4px;">✔</td><td style="border: 1px solid #ccc; padding: 4px;">✘</td><td style="border: 1px solid #ccc; padding: 4px;">✘</td>
                    </tr>
              </tbody>
            </table>
          <h2>如何启用只读</h2><p>您可以在保存新仪表板时设置仅查看模式，也可以稍后从共享菜单中设置。</p><h3>保存新仪表板时</h3><ul><li>创建您的仪表板，然后单击“<strong>保存</strong>”。</li><li>在“另存为新仪表板”模态框中，找到“<strong>权限</strong>”。</li><li>从“<strong>可编辑</strong>”更改为“<strong>可查看</strong>”。</li><li>单击“<strong>保存</strong>”。完成。它对其他所有人都是只读的。</li></ul><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/42a71d1bb55f9d50bd079f53bf45a0e1999b27f7-1214x1306.png" style="width: 100%;" /><h2>对于您已拥有的仪表板</h2><ul><li>打开仪表板。</li><li>打开“<strong>共享仪表板</strong>”菜单。</li></ul><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/e8405938557c879b1d4c262b98cf5a7f66408c04-1246x264.png" style="width: 100%;" /><ul><li>在共享模式中，找到“<strong>权限</strong>”并切换到“<strong>可查看</strong>”。将立即应用更改；空间中的其他用户将无法再编辑或删除它。</li></ul><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/849405bc32701f3ebe0def012d8ae3cf3813ea0a-996x750.png" style="width: 100%;" /><ul><li>您可以将鼠标悬停在“<strong>共享</strong>”操作上，查看给定仪表板拥有的权限类型。</li></ul><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/80541ddb1b1bc567b0aeff693944ea8b6871d6a7-1270x320.png" style="width: 100%;" /><h3>查看哪些仪表板被锁定</h3><p>在主仪表板列表中，无法编辑或删除的仪表板有一个禁用选择复选框。这为找出“仅查看”的内容提供了一种简便的方法。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/24b2eba08df86174db949c662e7886c5aea1b460-1999x876.png" style="width: 100%;" /><p>在仪表板中，您还会发现“编辑”操作已禁用，并且会出现一个工具提示，说明仪表板已设置为“仅查看”。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/e0a2f9da6dc854e876fc6dc2a7c3ef8b313b52ef-1358x330.png" style="width: 100%;" /><h2>试用</h2><p>只读仪表板现已推出。创建仪表板，将其切换到“<strong>可查看</strong>”，然后共享。您的团队将获得单一可信来源，而您则高枕无忧。标题中不再包含“请勿编辑”字样。</p><p>我们很想听听您是如何使用只读仪表板的。在我们的<a href="https://discuss.elastic.co">社区论坛</a>中分享您的反馈。</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[shell 工具并非上下文工程的灵丹妙药]]></title>
            <description><![CDATA[了解当前用于上下文工程的上下文检索工具有哪些、它们的工作原理以及各自的取舍。]]></description>
            <link>https://www.elastic.co/search-labs/cn/blog/search-tools-context-engineering</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/cn/blog/search-tools-context-engineering</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Leonie Monigatti]]></dc:creator>
            <pubDate>Wed, 25 Mar 2026 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/f91e4d07915ba7bf3b7abf15fac8fab3350f7df2-1280x720.png?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>智能体最重要的工具是那些它可以用来构建自身上下文的搜索工具。最近 <a href="https://www.llamaindex.ai/blog/files-are-all-you-need">LlamaIndex</a> 和 <a href="https://x.com/hwchase17/status/2011814697889316930">LangChain</a> 的帖子引发了一场讨论：<em>shell 工具和文件系统是否就是智能体进行上下文工程所需的一切？</em>不幸的是，讨论很快偏离了焦点，转向了文件系统与数据库之争。</p><p>本文重新聚焦于这个问题：<em>智能体构建自身上下文需要哪些正确的搜索接口？</em>它首先讨论了 shell 工具与专用数据库工具之间的取舍。在此基础上，它提供了一个实用的框架，用于为您的智能体需求找到正确的接口。</p><h2>对智能体而言，“构建上下文”到底意味着什么？</h2><p>在早期的 <a href="https://www.elastic.co/what-is/retrieval-augmented-generation">Retrieval-Augmented Generation (RAG) 管道</a>中，开发人员设计了一个固定的检索管道，而大型语言模型（LLM）只是上下文的被动接收者。这是一个根本性的限制：无论是否需要，每次查询都要检索上下文，而且不检查上下文是否真的有帮助。</p><p>随着向智能体式 RAG 的转变，智能体现在可以访问一组搜索工具来构建自己的上下文。例如，Claude Code [1] 和 Cursor [2] 都允许智能体根据任务的实际需求，在不同的搜索工具之间进行选择，甚至将它们组合起来用于链式查询。</p><h2>有哪些用于上下文工程的搜索接口？</h2><p>上下文可以存在于不同的位置，例如网络上、本地文件系统中或数据库中。智能体可以通过不同的工具与这些脱离上下文的每个数据源进行交互：</p><ul><li><strong>shell 工具</strong> 可以执行 shell 命令并访问本地文件系统。一些内置 shell 工具的例子包括 <a href="https://platform.claude.com/docs/en/agents-and-tools/tool-use/bash-tool">Claude API 的 bash 工具</a>、<a href="https://docs.openclaw.ai/tools/exec">OpenClaw 的 exec 工具</a>以及 <a href="https://docs.langchain.com/oss/python/integrations/tools/bash">LangChain 的 shell 工具</a>。</li><li><strong>专用数据库工具，</strong>例如来自模型上下文协议（MCP）服务器（例如，<a href="https://www.elastic.co/docs/explore-analyze/ai-features/agent-builder/mcp-server">Elastic Agent Builder MCP 服务器</a>）的工具或自定义工具（例如，<code>run_esql(query)</code> 或 <code>db_list_index()</code>），可以查询数据库。</li><li><strong>专用文件搜索工具</strong>可以搜索和读取本地（或上传）文件（无需完整的 shell 访问权限）。一些内置文件搜索工具的例子是 <a href="https://ai.google.dev/gemini-api/docs/file-search">Gemini API 的文件搜索工具</a> 或 <a href="https://developers.openai.com/api/docs/guides/tools-file-search">OpenAI 的文件搜索工具</a>。</li><li><strong>网络搜索工具</strong>可以从网络上检索信息。</li><li><strong>记忆工具</strong>会存储和回忆长期记忆中的内容（无论存储方式如何）。</li></ul><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/115f20c8ded259e508f51524b2c06bdc702d70ab-1999x1050.png" style="width: 100%;" /><p>如图所示，shell 工具功能强大，可用于从不同数据源检索上下文，包括：</p><ul><li><strong>文件系统：</strong>智能体会探索目录结构（ls、find），搜索相关内容（grep、cat），并不断重复，直到构建足够的上下文。</li><li><strong>数据库：</strong>智能体可以使用数据库命令行接口（CLI）工具（例如，<a href="https://www.elastic.co/docs/reference/query-languages/sql/sql-cli"><code>elasticsearch-sql-cli</code></a>），通过 curl 调用 HTTP API 或运行脚本，这在与 Agent Skills 结合使用时特别有用。Agent Skills 是注入到智能体上下文中用于指导正确工具使用的可复用、带示例的文档（例如 <a href="https://github.com/elastic/agent-skills">Elastic Agent Skills for Elasticsearch</a>）。</li><li><strong>网络：</strong>智能体可以通过搜索提供商的 API，使用 curl 命令执行网络搜索。</li></ul><p>然而，shell 工具提供直接的系统访问权限，因此需要采取安全措施，例如在隔离的沙盒环境中运行，并记录所有执行的命令。</p><h2>何时使用哪种搜索接口</h2><p>正确的搜索接口取决于您的数据、查询模式以及用例场景。本节将作为实用的入门起点。</p><h3>文件系统并不会让数据库过时</h3><p>文件系统与数据库的讨论并非关于存储层。例如，LangChain 解释说，<a href="https://x.com/hwchase17/status/2011814697889316930">其记忆系统</a>实际上并不将记忆存储在真实的文件系统中。相反，它将记忆存储在数据库中，并以<em>文件集合的形式</em>呈现给智能体 [3]。</p><p>文件系统天然适用于以文件为中心的用例，例如编码智能体。它们也可以很好地用作临时暂存区或工作记忆，以及适用于无需考虑并发问题的单用户或单智能体场景。在这些情况下，在投入构建专用接口之前，使用物理文件系统或将数据表示为文件系统可以为您提供灵活性。</p><p>但是，文件系统存储确实存在缺点，例如并发性弱、需要手动执行模式约束、原子事务支持差。当您的应用程序需要扩展或迁移到多智能体场景时，这些缺点会更加明显。任何忽视这些缺点的人都注定要<a href="https://dx.tips/oops-database">痛苦地重新发明更糟糕的数据库</a>，却缺乏生产数据库已经提供的、经过数十年工程实践的事务安全或访问控制机制。此外，在大多数企业环境中，您并非选择是否使用数据库——因为数据库已经存在，并存储着业务关键数据。</p><h3>Shell 工具 + 文件系统</h3><p>对于文件系统搜索，shell 工具是自然的起点。当前，编码智能体正在推动该领域取得巨大进展。由于它们处理本地文件中的代码，因此自然是以文件为主的用例。因此，LLM 在后训练阶段会针对编码任务进行微调。这就是为什么许多 LLM 不仅擅长编写代码，还擅长使用 shell 命令和操作文件系统的原因。</p><p>使用带有内置 CLI（如 <code>ls</code> 和 <code>grep</code>）的 shell 工具来查找文件是有效的。使用 grep，像“Find all files that import <code>matplotlib</code>”这样的查询既快速、精确又廉价。但是，当智能体需要处理概念性查询（例如“How does our app handle failed authentication?”）时，使用 grep 进行模式匹配很快就会触及天花板。为了填补这一空白，出现了一些将语义搜索能力带到命令行的替代方案，包括 <a href="https://github.com/jina-ai/jina-grep-cli"><code>jina-grep</code></a>。</p><p>然而，grep 及其许多语义搜索替代方案在语料库上的运行速度为 O(n)。对于代码库相关的使用场景，这可能没问题。但是，如果数据量增加，延迟就会变得很明显。在这种情况下，为了保持性能，需要使用索引数据存储。</p><h3>shell 工具 + 数据库</h3><p>另一种为数据添加更多搜索能力（例如语义搜索或混合搜索）的方法是将数据存储在数据库中，就像 Cursor 所做的那样。此外，当数据需要复杂的关系连接或聚合时，数据库接口是不可或缺的。</p><p>数据存储在数据库中而非文件系统上时，shell 工具可以在某些用例中充当轻量级的数据库接口。如果您的查询足够简单，只需 CLI 或 curl 调用即可完成，那么专用的数据库工具可能会带来不必要的复杂性。</p><p>这种方法也适用于早期的探索阶段，此时您还不知道智能体最终会发展出什么样的查询模式。在这种情况下，Agent Skills 可以为智能体提供足够的结构来正确执行查询，而无需投入构建专用工具。但是，当智能体需要大量迭代才能找出针对重复任务查询数据库的正确方式时，使用 shell 工具作为接口所带来的词元开销，就不再能抵消避免使用额外工具的简单性优势了。</p><h3>专用数据库工具</h3><p>特别是当重复的查询模式是结构化的或分析性的时，专用的数据库工具就变得必要了。<a href="https://vercel.com/blog/testing-if-bash-is-all-you-need">Vercel 和 Braintrust 的一篇博客文章</a>比较了拥有不同搜索工具集的智能体，在半结构化数据（如客户支持工单和销售通话记录）上执行真实世界的检索任务（例如，“How many open issues mention &#x27;security&#x27;?”或“Find issues where someone reported a bug and later someone submitted a PR claiming to fix it?”）[4]。</p><p>拥有专用数据库工具的智能体，与仅拥有 shell 工具和文件系统的智能体相比，使用的词元更少，速度更快，犯的错误也更少。经验表明，当查询需要对半结构化数据进行分析推理时，直接使用数据库工具才是正确的选择。</p><h3>组合使用搜索接口</h3><p>没有哪一个搜索接口能完美处理所有查询。例如，Cursor 将 shell 工具（用于通过 grep 搜索）和语义搜索工具组合起来，让智能体根据用户提示选择正确的工具。智能体会选择 grep 来匹配特定的符号或字符串，选择语义搜索来处理概念性或行为性问题，并在探索性任务中同时使用两者。</p><p>Vercel 的实验报告了相同的结果：其混合型智能体可同时访问 shell 工具和专用数据库工具，通过首先使用专用数据库工具，然后通过文档系统 grepping 验证结果，在所有测试的智能体中取得了最佳性能。这种方法在工具选择和验证的推理上消耗了更多的词元和时间。</p><p>这两个示例中的模式是相同的：组合优于任何单一接口，但组合的代价是增加成本和延迟。</p><h2>寻找合适工具的实用建议</h2><p>合适的搜索接口应该简洁、目标明确，并且能够满足您的智能体的实际查询模式。当前的最佳实践是让智能体拥有尽可能少的工具，而不是让它拥有数百个 MCP 工具。这是因为，预先公开所有可能的工具会带来弊端：它会使上下文窗口臃肿，并使智能体困惑于到底该使用哪个工具。例如，据报道 Claude Code 只有大约 20 种工具。</p><p>相反，渐进式公开的理念是从一套最基本的工具开始，让智能体在需要时才发现额外的功能。Anthropic [5] 和 Cursor [6] 的研究表明，这种方法可以节省 47%–85% 的词元。例如，Claude Code 直接实现了这一点，允许智能体逐步发现如何查询 API 或数据库，而无需在每次 LLM 调用时都将这些知识消耗在上下文中。</p><p>一旦您熟悉了智能体的查询模式，就可以重新审视智能体默认可以访问的搜索工具集。一种思考这种取舍的有用方式是使用<a href="https://www.elastic.co/search-labs/blog/database-retrieval-tools-context-engineering#building-the-right-database-retrieval-tools-%5C(%E2%80%9Clow-floor,-high-ceiling%E2%80%9D%5C">”低门槛，高上限“原则</a>，用于决定哪些工具值得被纳入。高上限工具不会限制智能体的潜力。例如，一个通用的 shell 工具允许智能体编写完整的数据库查询（包括模糊查询），但代价是推理开销、更高的延迟和更低的可靠性。</p><p>“低门槛”工具则相反。它们是封装了特定查询的专用工具，智能体可以以最少的推理开销直接使用，从而产生更低的成本和更高的可靠性。但它们需要前期工程投入，无法覆盖所有可能的查询，并且可能使智能体更难选择正确的工具。</p><p>可将每个工具视为处在一条连续谱上：低门槛工具更容易被代理正确使用，但覆盖范围较窄。高上限工具用途广泛，但要用得好需要更多推理。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/e6d1b973be4b0a0a25c99c74f02a47e98395a3f7-1200x630.png" style="width: 100%;" /><p>多数智能体需要混合使用不同的搜索工具。但每个工具都需要“凭实力”加入。我们建议从一个通用的搜索工具（例如 <code>search_database()</code> 工具或 shell 工具）开始。然后，复用您出于安全目的已经保留的命令日志，来跟踪智能体的实际行为，包括工具调用、重试次数以及每个用户查询的调用次数。并且，当您看到某个查询模式重复出现或执行失败时，这就是为该模式构建专用工具的信号。</p><h2>总结</h2><p>文件系统与数据库的争论分散了工程师们真正需要关注的问题：<em>智能体构建自身上下文需要哪些正确的搜索接口？</em>答案很可能是：<em>不是单一一个接口</em>。</p><p>shell 工具是一种用于与不同上下文外数据源交互的通用工具，因此是一个很好的起点。但在结构化分析查询的用例中，它的效率和准确度不如专用数据库工具。</p><p>目标是找到能够良好处理智能体实际查询模式的最小搜索工具集。从 shell 工具开始，记录智能体的实际行为。当您发现某个查询模式重复出现且执行失败时，就该为该模式设计专用工具了。</p><h2>参考资料</h2><p>1. Thariq (Anthropic). <a href="https://x.com/trq212/status/2027463795355095314">Lessons from Building Claude Code: Seeing like an Agent</a> (2026).</p><p>2. Cursor: Documentation. <a href="https://cursor.com/docs/agent/tools/search">Semantic &amp; agentic search</a> (2026).</p><p>3. Harrison Chase (LangChain). <a href="https://x.com/hwchase17/status/2011814697889316930">How we built Agent Builder’s memory system</a> (2026).</p><p>4. Ankur Goyal (Braintrust) and Andrew Qu (Vercel). <a href="https://vercel.com/blog/testing-if-bash-is-all-you-need">Testing if &quot;bash is all you need&quot;</a> (2026).</p><p>5. Anthropic. <a href="https://www.anthropic.com/engineering/advanced-tool-use">Introducing advanced tool use on the Claude Developer Platform</a> (2025).</p><p>6. Cursor. <a href="https://cursor.com/blog/dynamic-context-discovery">Dynamic context discovery</a> (2026).</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[在无服务器环境中实现负载均衡的 Elasticsearch 副本]]></title>
            <description><![CDATA[了解 Elastic Cloud Serverless 如何根据搜索负载自动调整索引副本，无需手动配置即可确保最佳查询性能。]]></description>
            <link>https://www.elastic.co/search-labs/cn/blog/elasticsearch-replicas-load-balancing-serverless</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/cn/blog/elasticsearch-replicas-load-balancing-serverless</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Andrei Dan]]></dc:creator>
            <pubDate>Tue, 24 Mar 2026 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/3c4c1e99b892f026b7aba098973593f8298e2ea6-1280x717.png?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>在 Elastic Cloud Serverless 中，我们会根据搜索负载自动调整索引的副本数量，确保查询性能最佳，无需手动配置。在这篇博客中，我们将解释副本如何扩展，系统何时添加或移除它们，以及这对您的索引意味着什么。</p><h2>派对越来越拥挤了</h2><p>您要举办一个披萨派对。您有几位朋友协助您招待宾客，他们分别在房间的不同位置。您把披萨分给每位朋友，他们会在饥饿的宾客陆续到来时开始分发披萨。</p><p>起初，一切运行顺利。有几位宾客陆续进来，朋友们端上披萨片，大家都很开心。但随后关于您的披萨的消息传开了。门铃一直在响。宾客们蜂拥而至。很快，人群聚集在您的一位朋友周围，就是那个拿着意式辣味香肠披萨的朋友，似乎大家都想要那块披萨。</p><p>您那位拿着意式辣味香肠披萨的朋友感到不知所措。宾客们正在等待，变得不耐烦了，还排起了长队。与此同时，您的朋友手持玛格丽特披萨站在那里，几乎没有人要一片。</p><p>您需要怎么做？</p><p>您又点了几份意式辣味香肠披萨，并把它们分给其他朋友。现在有三位朋友在拿着意式辣味香肠披萨分发，而不再只是一个人。人群散开了，突然间您就能一次性接待三倍数量的宾客。</p><p>随着您举办的派对越来越多，有几件事会变得越来越清晰：</p><ul><li><strong>并非所有披萨都同样受欢迎。</strong>有些供不应求，有些则鲜有人问津。您不需要为那些不受欢迎的披萨准备多余的“份数”。您需要为那些排队的披萨准备更多“份数”。</li><li><strong>在排队人数变多之前多点几份披萨。</strong>如果您等到朋友已经忙得不可开交、客人都气得离场时才行动，那就太晚了。最好是在看到人群聚集时，就提前加点一份披萨。</li><li><strong>别太快把披萨撤走。</strong>即使意式辣味香肠披萨周围的人群散开了五分钟，也不代表高峰期已经过去。也许他们只是在加饮料，甚至只是在聊天（现在还是这样吗？）。把多余的披萨准备好。如果冷场确实持续了一段时间，那时再撤走也不迟。</li><li><strong>您能分发的披萨数量取决于有多少朋友来帮忙。</strong>如果您只有四个朋友在帮忙，十张披萨也改变不了结果。一次只能供应四份。将您的披萨数量与可用人手相匹配。</li><li><strong>当一个朋友离开时，记得接管他的披萨。</strong>如果您的朋友需要离开，请立即拿走他们的披萨。披萨不能无人看管地放置。将它交给其他人，或者妥善收好。</li></ul><h2>我们已经聊完了披萨和副本</h2><p>现在让我们把这些生动的小故事映射回 Elasticsearch。</p><p>在我们的类比中，披萨是副本（索引分片的副本），帮助提供服务的朋友是搜索节点，饥肠辘辘的宾客是搜索查询，而人头攒动的热门披萨则是搜索负载较高的热门索引。</p><p>当特定索引的搜索流量增加时，我们会创建额外的副本，并将它们分发到搜索节点上。任何副本都可以为该索引的任何查询提供服务，就像任何拿着意式辣味香肠披萨的朋友都可以分发意式辣味香肠披萨片一样。更多副本意味着更高的吞吐量：三个副本每秒处理的查询量是单个副本的三倍。</p><h2>衡量饥饿程度</h2><p>在决定订购多少披萨之前，我们需要了解人群的饥饿程度。</p><p>Elasticsearch 会跟踪每个分片的<strong>搜索负载</strong>。这是一个度量指标，用于衡量分片正在处理的搜索活动的数量。我们将此汇总到索引的所有分片中，以了解总的搜索需求。</p><p>最重要的是<strong>相对搜索负载</strong>：您的项目总搜索流量中，每个索引所占的比例是多少？如果一个索引的搜索量为 60%，而另一个索引的搜索量为 5%，我们就知道应该在哪里增加容量。</p><h2>披萨背后的数学原理</h2><p>我们按照以下公式计算最佳副本数量：</p><pre><code>desired_replicas = min(ceil(L × N / (S × X)), N)</code></pre><p>其中：</p><ul><li><strong>L</strong> = 索引的相对搜索负载（介于 0 和 1 之间）。</li><li><strong>N</strong> = 项目中所需搜索节点的数量。</li><li><strong>S</strong> = 索引中的分片数量。</li><li><strong>X</strong> = 用于避免热点的阈值（默认值：0.5）。</li></ul><p>示例：四个搜索节点，具有两个主分片的一个索引，接收 80% 的搜索流量：</p><pre><code>desired_replicas = min(ceil(0.8 × 4 / (2 × 0.5)), 4)
                 = min(4, 4)
                 = 4</code></pre><p>这个热索引有四个副本，分布在各个搜索节点上。</p><p>阈值 X（默认为 0.5）非常重要。我们不会等到副本完全不堪重负才采取行动；当副本的负载达到一半容量时，我们就会进行扩展。当看到人群聚集时再分发额外的披萨，而不是等到宾客已经开始离开的时候。</p><h2>快速扩展，缓慢收缩</h2><p>当搜索负载增加时，我们立即添加副本。没有理由让用户等待。</p><p>当搜索负载下降时，我们会等待一段时间再采取行动。在减少副本之前，我们需要看到持续约 30 分钟的低需求。（这是为了应对流量高峰，因为短暂的平静并不意味着派对结束。）</p><p>这很重要，因为添加副本是有成本的。在高效提供查询之前，新的副本会复制数据并预热其缓存。过于急切地移除副本意味着在流量自然波动时持续支付这种启动成本。</p><h2>尊重拓扑边界</h2><p>副本永远不能超过搜索节点的数量。拥有比节点更多的副本并不会带来任何好处（您能送出的披萨数量取决于帮忙的朋友数量）。</p><p>从您的项目中移除节点时，我们会立即减少副本数量以进行匹配。无需等待冷却时间，因为您无法拥有未分配的副本。朋友离开的那一刻，我们就会移除他们的披萨。</p><h2>无服务器的全貌</h2><p>用于搜索负载均衡的副本与其他自动缩放系统协同工作：</p><ul><li><strong>搜索自动缩放</strong>可调整搜索节点的数量（有多少朋友在帮忙）。</li><li><strong>用于搜索负载均衡的副本</strong>通过调整每个索引的副本数量来分发流量（我们需要每种披萨的数量）。</li><li><strong>数据流自动分片</strong>优化了写入的分片数量（如何将每个披萨切片，详情见<a href="https://www.elastic.co/search-labs/blog/datastream-autosharding-serverless">上一篇文章</a>）。</li></ul><p>一个重要的设计原则：用于负载均衡的副本不会直接触发搜索自动扩展。相反，通过将搜索请求分发给更多副本，可以提高搜索节点的资源利用率。这种更高的利用率会触发我们现有的自动扩展逻辑，以便在需要时增加容量。用于负载均衡的副本可让自动扩展发挥作用，确保搜索节点真正得到使用，而不是将所有流量都集中在单个副本上造成瓶颈，而其他节点却处于闲置状态。</p><h2>这对您意味着什么</h2><p>您无需预测哪些索引会更受欢迎。当流量模式发生变化时，您无需手动调整副本。您无需因为流量激增导致最繁忙的索引不堪重负，而不得不凌晨 3 点起床进行处理。</p><p>系统会观察排队的地点，并为这些地点订购更多披萨。冷索引不会在不必要的副本上浪费资源。热门索引会获得所需的容量。您的预算用在了最重要的地方。</p><h2>结论</h2><p>在<a href="https://www.elastic.co/search-labs/blog/datastream-autosharding-serverless">自动分片文章</a>中，我们确保您的披萨切得恰到好处。现在，有了用于搜索负载均衡的副本，我们可以确保在饥饿的人群到来时，有足够的披萨送到他们手中。</p><p>试用 <a href="https://www.elastic.co/cloud/serverless">Elastic Cloud Serverless</a>，让我们来处理披萨分发事宜。</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[使用 Elasticsearch 推理 API 以及 Hugging Face 模型]]></title>
            <description><![CDATA[了解如何使用推理终端将 Elasticsearch 连接到 Hugging Face 模型，并利用语义搜索和聊天补全功能构建多语言博客推荐系统。]]></description>
            <link>https://www.elastic.co/search-labs/cn/blog/hugging-face-elasticsearch-inference-api</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/cn/blog/hugging-face-elasticsearch-inference-api</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Jeffrey Rengifo]]></dc:creator>
            <pubDate>Mon, 23 Mar 2026 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/1417d6ff033712206c9bd4bcc22074ee3437ce96-1999x1125.png?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>在最近的更新中，Elasticsearch 引入了原生集成，用于连接到托管在 <a href="https://endpoints.huggingface.co/">Hugging Face Inference Service</a> 上的模型。在本文中，我们将探讨如何配置此集成，并使用大型语言模型 (LLM) 通过简单的 API 调用执行推理。我们将使用 <a href="https://huggingface.co/HuggingFaceTB/SmolLM3-3B">SmolLM3-3B</a>，这是一款轻量级通用模型，在资源使用和答案质量之间取得了良好的平衡。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/7ddadf1976421a860a7d62087239adb9150d808b-1999x1388.png" style="width: 100%;" /><h2>准备工作</h2><ul><li><strong>Elasticsearch 9.3 或 Elastic Cloud Serverless：</strong>您可以按照<a href="https://www.elastic.co/search-labs/tutorials/install-elasticsearch/elastic-cloud">这些说明</a>创建云部署，或者改用 <a href="https://www.elastic.co/docs/deploy-manage/deploy/self-managed/local-development-installation-quickstart#local-dev-quick-start"><code>start-local</code></a> 快速入门。</li><li><strong>Python 3.12：</strong><a href="https://www.python.org/">在此处</a>下载 Python。</li><li><strong>Hugging Face </strong><a href="https://huggingface.co/docs/hub/en/security-tokens">访问令牌</a>。</li></ul><h2>使用 Hugging Face 推理终端完成聊天</h2><p>首先，我们将构建一个实用示例，将 Elasticsearch 连接到 Hugging Face <a href="https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-inference-put">推理终端</a>，以从博客文章集合中生成 AI 驱动的推荐。对于应用知识库，我们将使用公司博客文章数据集，其中包含有价值但通常难以查找的信息。</p><p>通过这个终端，<a href="https://www.elastic.co/docs/solutions/search/semantic-search">语义搜索</a>可以检索与给定查询最相关的文章，而 Hugging Face LLM 则会根据这些结果生成简短的上下文推荐。</p><p>让我们来看看我们将要构建的信息流的高级概述：</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/1dfbc2323438feaaa42e13ab242dd1f7166f74aa-1200x676.png" style="width: 100%;" /><p>在本文中，我们将测试 <strong>SmolLM3-3B</strong> 是否能<strong></strong>将其紧凑的大小与强大的多语言推理和工具调用能力相结合。根据搜索查询，我们将把所有匹配的内容（英语和西班牙语）发送到 LLM，以生成一份推荐文章列表，并根据搜索查询和结果提供自定义描述。</p><p>以下是具备 AI 推荐生成系统的文章网站用户界面可能的外观。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/8d3b86b212f28ff279f2da67a33e6134039f0e4e-1999x949.png" style="width: 100%;" /><p>您可以在已链接的<a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/elasticsearch-inference-api-and-hugging-face/notebook.ipynb">笔记本</a>中找到此应用程序的完整实现。</p><h3>配置 Elasticsearch 推理终端</h3><p>要使用 Elasticsearch <a href="https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-inference-put-hugging-face">Hugging Face 推理终端</a>，我们需要两个重要元素：Hugging Face API 密钥和正在运行的 Hugging Face 终端 URL。它应该如下所示：</p><pre><code>PUT _inference/chat_completions/hugging-face-smollm3-3b
{
    "service": "hugging_face",
    "service_settings": {
        "api_key": "hugging-face-access-token", 
        "url": "url-endpoint" 
    }
}</code></pre><p>Elasticsearch 中的 Hugging Face 推理终端支持不同的任务类型：<code>text_embedding</code>、<code>completion</code>、<code>chat_completion</code> 和 <code>rerank</code>。在这篇博客文章中，我们使用 <code>chat_completion</code> 是因为我们需要模型根据搜索结果和系统提示生成对话式推荐。此终端允许我们使用 Elasticsearch API 以简单的方式直接从 Elasticsearch 执行聊天完成：</p><pre><code>POST _inference/chat_completion/hugging-face-smollm3-3b/_stream
{
  "messages": [
      { "role": "user", "content": "<user prompt>" }
  ]
}</code></pre><p>这将作为应用程序的核心，接收通过模型传递的提示和搜索结果。有了理论基础，我们就开始实施应用程序。</p><h4>在 Hugging Face 上设置推理终端</h4><p>要部署 Hugging Face 模型，我们将使用 <a href="https://huggingface.co/inference-endpoints/dedicated">Hugging Face 一键式部署</a>，这是一种用于部署模型终端的简单快速的服务。请记住，这是一项付费服务，使用它可能会产生额外费用。此步骤将创建用于生成文章推荐的模型实例。</p><p>您可以从一键目录中选择一个模型：</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/b816e9fba1fe172687bf58f5143fb1f838c1077f-549x331.png" style="width: 100%;" /><p>让我们选择 <strong>SmolLM3-3B</strong> 模型：</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/610d3aba0429f3666c2df3616d513eb6a4397c0c-502x478.png" style="width: 100%;" /><p>从此处获取 Hugging Face 终端 URL：</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/025094ddb2cfbd1f0f216a5ec4e119b0f4fa2c42-646x328.png" style="width: 100%;" /><p>正如在 Elasticsearch <a href="https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-inference-put-hugging-face">Hugging Face 推理终端文档</a>中提到的，文本生成需要一个与 OpenAI API 兼容的模型。因此，我们需要将 <code>/v1/chat/completions</code> 子路径附加到 Hugging Face 终端 URL。最终结果将如下所示：</p><pre><code>https://j2g31h0futopfkli.us-east-1.aws.endpoints.huggingface.cloud/v1/chat/completions</code></pre><p>有了这个，我们就可以在 Python 笔记本中开始编码了。</p><h4>生成 Hugging Face API 密钥</h4><p>创建 <a href="https://huggingface.co/join">Hugging Face 账户</a>，并按照<a href="https://huggingface.co/docs/hub/en/security-tokens#user-access-tokens">以下说明</a>获取 API 令牌。您可以选择三种令牌类型：<em>细粒度</em>（推荐用于生产，因为它仅提供对特定资源的访问）、<em>读取</em>（适用于只读访问）或<em>写入</em>（适用于读取和写入访问）。在本教程中，读取令牌就足够了，因为我们只需要调用推理终端。请保存此密钥以备下一步使用。</p><h4>设置 Elasticsearch 推理终端</h4><p>首先，让我们声明一个 Elasticsearch Python 客户端：</p><pre><code>os.environ["ELASTICSEARCH_API_KEY"] = "your-elasticsearch-api-key"
os.environ["ELASTICSEARCH_URL"] = "https://xxxx.us-central1.gcp.cloud.es.io:443"

es_client = Elasticsearch(
    os.environ["ELASTICSEARCH_URL"], api_key=os.environ["ELASTICSEARCH_API_KEY"]
)</code></pre><p>接下来，我们创建一个使用 Hugging Face 模型的 Elasticsearch 推理终端。此终端将允许我们基于博客文章和传递给模型的提示来生成响应。</p><pre><code>INFERENCE_ENDPOINT_ID = "smollm3-3b-pnz"

os.environ["HUGGING_FACE_INFERENCE_ENDPOINT_URL"] = (
 "https://j2g31h0futopfkli.us-east-1.aws.endpoints.huggingface.cloud/v1/chat/completions"
)
os.environ["HUGGING_FACE_API_KEY"] = "hf_xxxxx"

resp = es_client.inference.put(
        task_type="chat_completion",
        inference_id=INFERENCE_ENDPOINT_ID,
        body={
            "service": "hugging_face",
            "service_settings": {
                "api_key": os.environ["HUGGING_FACE_API_KEY"],
                "url": os.environ["HUGGING_FACE_INFERENCE_ENDPOINT_URL"],
            },
        },
    )</code></pre><h3>数据集</h3><p>该数据集包含将要查询的<a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/elasticsearch-inference-api-and-hugging-face/dataset.json">博客文章</a>，代表整个工作流中使用的多语言内容集：</p><pre><code>// Articles dataset document example: 
{
    "id": "6",
    "title": "Complete guide to the new API: Endpoints and examples",
    "author": "Tomas Hernandez",
    "date": "2025-11-06",
    "category": "tutorial",
    "content": "This guide describes in detail all endpoints of the new API v2. It includes code examples in Python, JavaScript, and cURL for each endpoint. We cover authentication, resource creation, queries, updates, and deletion. We also explain error handling, rate limiting, and best practices. Complete documentation is available on our developer portal."
  }</code></pre><h4>Elasticsearch 映射</h4><p>定义数据集后，我们需要创建一个适合博客文章结构的数据模式。以下<a href="https://www.elastic.co/docs/manage-data/data-store/mapping">索引映射</a>将用于在 Elasticsearch 中存储数据：</p><pre><code>INDEX_NAME = "blog-posts"

mapping = {
    "mappings": {
        "properties": {
            "id": {"type": "keyword"},
            "title": {
                "type": "object",
                "properties": {
                    "original": {
                        "type": "text",
                        "copy_to": "semantic_field",
                        "fields": {"keyword": {"type": "keyword"}},
                    },
                    "translated_title": {
                        "type": "text",
                        "fields": {"keyword": {"type": "keyword"}},
                    },
                },
            },
            "author": {"type": "keyword", "copy_to": "semantic_field"},
            "category": {"type": "keyword", "copy_to": "semantic_field"},
            "content": {"type": "text", "copy_to": "semantic_field"},
            "date": {"type": "date"},
            "semantic_field": {"type": "semantic_text"},
        }
    }
}


es_client.indices.create(index=INDEX_NAME, body=mapping)</code></pre><p>在这里，我们可以更清楚地看到数据的结构。我们将使用语义搜索来检索基于自然语言的结果，同时使用 <a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/copy-to"><code>copy_to</code></a> 属性将字段内容复制到 <a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/semantic-text"><code>semantic_text</code></a> 字段中。此外，<code>title</code> 字段包含两个子字段：<code>original</code> 子字段根据文章的原始语言存储英语或西班牙语标题；而 <code>translated_title</code> 子字段仅存在于西班牙语文章中，并包含原始标题的英语翻译。</p><h3>采集数据</h3><p>以下代码片段使用<a href="https://www.elastic.co/docs/reference/elasticsearch/clients/javascript/bulk_examples">批量 API</a> 将博客文章数据集摄取到 Elasticsearch 中：</p><pre><code>def build_data(json_file, index_name):
    with open(json_file, "r") as f:
        data = json.load(f)

    for doc in data:
        action = {"_index": index_name, "_source": doc}
        yield action


try:
    success, failed = helpers.bulk(
        es_client,
        build_data("dataset.json", INDEX_NAME),
    )
    print(f"{success} documents indexed successfully")

    if failed:
        print(f"Errors: {failed}")
except Exception as e:
    print(f"Error: {str(e)}")</code></pre><p>现在，我们已将文章摄取到 Elasticsearch 中，我们需要创建一个能够针对 <code>semantic_text</code> 字段进行搜索的函数：</p><pre><code>def perform_semantic_search(query_text, index_name=INDEX_NAME, size=5):
    try:
        query = {
            "query": {
                "match": {
                    "semantic_field": {
                        "query": query_text,
                    }
                }
            },
            "size": size,
        }

        response = es_client.search(index=index_name, body=query)
        hits = response["hits"]["hits"]

        return hits
    except Exception as e:
        print(f"Semantic search error: {str(e)}")
        return []</code></pre><p>我们还需要一个调用推理终端的函数。在这种情况下，我们将使用 <strong><code>chat_completion</code> </strong>任务类型调用终端，以获取流式响应：</p><pre><code>def stream_chat_completion(messages: list, inference_id: str = INFERENCE_ENDPOINT_ID):
    url = f"{ELASTICSEARCH_URL}/_inference/chat_completion/{inference_id}/_stream"
    payload = {"messages": messages}
    headers = {
        "Authorization": f"ApiKey {ELASTICSEARCH_API_KEY}",
        "Content-Type": "application/json",
    }

    try:
        response = requests.post(url, json=payload, headers=headers, stream=True)
        response.raise_for_status()

        for line in response.iter_lines(decode_unicode=True):
            if line:
                line = line.strip()

                if line.startswith("event:"):
                    continue

                if line.startswith("data: "):
                    data_content = line[6:]

                    if not data_content.strip() or data_content.strip() == "[DONE]":
                        continue

                    try:
                        chunk_data = json.loads(data_content)

                        if "choices" in chunk_data and len(chunk_data["choices"]) > 0:
                            choice = chunk_data["choices"][0]
                            if "delta" in choice and "content" in choice["delta"]:
                                content = choice["delta"]["content"]
                                if content:
                                    yield content

                    except json.JSONDecodeError as json_err:
                        print(f"\nJSON decode error: {json_err}")
                        print(f"Problematic data: {data_content}")
                        continue

    except requests.exceptions.RequestException as e:
        yield f"Error: {str(e)}"</code></pre><p>现在，我们可以编写一个函数，调用语义搜索函数以及 <code>chat_completions</code> 推理终端和建议终端，以生成将分配到卡片中的数据：</p><pre><code>def recommend_articles(search_query, index_name=INDEX_NAME, max_articles=5):
    print(f"\n{'='*80}")
    print(f"🔍 Search Query: {search_query}")
    print(f"{'='*80}\n")

    articles = perform_semantic_search(search_query, index_name, size=max_articles)

    if not articles:
        print("❌ No relevant articles found.")
        return None, None

    print(f"✅ Found {len(articles)} relevant articles\n")

    # Build context with found articles
    context = "Available blog articles:\n\n"
    for i, article in enumerate(articles, 1):
        source = article.get("_source", article)
        context += f"Article {i}:\n"
        context += f"- Title: {source.get('title', 'N/A')}\n"
        context += f"- Author: {source.get('author', 'N/A')}\n"
        context += f"- Category: {source.get('category', 'N/A')}\n"
        context += f"- Date: {source.get('date', 'N/A')}\n"
        context += f"- Content: {source.get('content', 'N/A')}\n\n"

    system_prompt = """You are an expert content curator that recommends blog articles.

    Write recommendations in a conversational style starting with phrases like:
    - "If you're interested in [topic], this article..."
    - "This post complements your search with..."
    - "For those looking into [topic], this article provides..."


    FORMAT REQUIREMENTS:
    - Return ONLY a JSON array
    - Each element must have EXACTLY these three fields: "article_number", "title", "recommendation"
    - If the original title is in spanish, use the "translated_title" subfield in the "title" field

    Keep each recommendation concise (2-3 sentences max) and focused on VALUE to the reader.

    EXAMPLE OF CORRECT FORMAT:
    [
        {"article_number": 1, "title": "Article title in english", "recommendation": "If you are interested in [topic], this article provides..."},
        {"article_number": 2, "title": "Article title in english", "recommendation": " for those looking into [topic], this article provides..."}
    ]

    Return ONLY the JSON array following this exact structure."""

    user_prompt = f"""Search query: "{search_query}"

    Generate recommendations for the following articles: {context}
    """

    messages = [
        {"role": "system", "content": "/no_think"},
        {"role": "system", "content": system_prompt},
        {"role": "user", "content": user_prompt},
    ]

    # LLM generation
    print(f"{'='*80}")
    print("🤖 Generating personalized recommendations...\n")

    full_response = ""

    for chunk in stream_chat_completion(messages):
        print(chunk, end="", flush=True)
        full_response += chunk

    return context, articles, full_response</code></pre><p>最后，我们需要提取信息并将其格式化以便打印：</p><pre><code>def display_recommendation_cards(articles, recommendations_text):
    print("\n" + "=" * 100)
    print("📇 RECOMMENDED ARTICLES".center(100))
    print("=" * 100 + "\n")

    # Parse JSON recommendations - clean tags and extract JSON
    recommendations_list = []
    try:

        # Clean up <think> tags
        cleaned_text = re.sub(
            r"<think>.*?</think>", "", recommendations_text, flags=re.DOTALL
        )
        # Remove markdown code blocks ( ... ``` or ``` ... ```)
        cleaned_text = re.sub(r"```(?:json)?", "", cleaned_text)
        cleaned_text = cleaned_text.strip()

        parsed = json.loads(cleaned_text)

        # Extract recommendations from list format
        for item in parsed:
            article_number = item.get("article_number")
            title = item.get("title", "")
            rec_text = item.get("recommendation", "")

            if article_number and rec_text:
                recommendations_list.append(
                    {
                        "article_number": article_number,
                        "title": title,
                        "recommendation": rec_text,
                    }
                )
    except json.JSONDecodeError as e:
        print(f"⚠️  Could not parse recommendations as JSON: {e}")
        return

    for i, article in enumerate(articles, 1):
        source = article.get("_source", article)

        # Card border
        print("┌" + "─" * 98 + "┐")

        # Find recommendation and title for this article number
        recommendation = None
        title = None
        for rec in recommendations_list:
            if rec.get("article_number") == i:
                recommendation = rec.get("recommendation")
                title = rec.get("title")
                break

        # Print title
        title_lines = textwrap.wrap(f"📌 {title}", width=94)
        for line in title_lines:
            print(f"│  {line}".ljust(99) + "│")

        # Card border
        print("├" + "─" * 98 + "┤")

        # Print recommendation
        if recommendation:
            recommendation_lines = textwrap.wrap(recommendation, width=94)
            for line in recommendation_lines:
                print(f"│  {line}".ljust(99) + "│")

        # Card bottom
        print("└" + "─" * 98 + "┘")</code></pre><p>让我们通过询问一个有关安全博客文章的问题来测试一下：</p><pre><code>search_query = "Security and vulnerabilities"

context, articles, recommendations = recommend_articles(search_query)

print("\nElasticsearch context:\n", context)

# Display visual cards
display_recommendation_cards(articles, recommendations)</code></pre><p>如下所示，我们可以看到工作流在控制台中生成的卡片：</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/730d35212594bb3db30447c3ea7e2a92857287b7-1999x1515.png" style="width: 100%;" /><p>您可以在<a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/elasticsearch-inference-api-and-hugging-face/results.md">此文件</a>中查看全部结果，包括所有点击和 LLM 响应。</p><p>我们正在征集与“安全与漏洞”相关的文章。此问题将用作针对 Elasticsearch 中存储的文档的搜索查询。然后将检索到的结果传递给模型，该模型根据这些结果的内容生成推荐。我们可以看到，该模型出色地生成了引人入胜的短文本，能够激发读者点击的欲望。</p><h2>结论</h2><p>本示例展示了如何将 Elasticsearch 和 Hugging Face 结合起来，为 AI 应用程序创建一个快速高效的集中式系统。由于 Hugging Face 拥有丰富的模型目录，这种方法不仅减少了人工操作，还具有灵活性。通过使用 SmolLM3-3B，我们特别看到了紧凑的多语言模型在与语义搜索搭配使用时，仍能提供有意义的推理和内容生成。这些工具共同为构建智能内容分析和多语言应用程序提供了可扩展且高效的基础。</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[快速与准确：测量量化向量搜索的召回率]]></title>
            <description><![CDATA[说明如何在 Elasticsearch 中以最少的设置来测量向量搜索的召回率。]]></description>
            <link>https://www.elastic.co/search-labs/cn/blog/recall-vector-search-quantization</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/cn/blog/recall-vector-search-quantization</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Jeff Vestal]]></dc:creator>
            <pubDate>Fri, 20 Mar 2026 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/09f03b9239d66c36763cdab3fafcdac207ff6d83-1280x720.png?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>所有人都希望向量搜索能即时响应，但是高维向量占用的资源很大。一个 1024 维的 float-32 向量会占用大量内存，将其与数百万个其他向量进行比较的计算成本非常高昂。</p><p>为了解决这个问题，像 Elasticsearch 这样的搜索引擎使用了两种主要的优化策略：</p><ol><li><strong>近似搜索（分层可导航小世界 [HNSW]）：</strong>我们不需要扫描每一份文档，而是建立一个导航图，以便快速跳转到答案的可能邻域。</li><li><strong>量化：</strong>我们对向量进行压缩（例如，从 32 位浮点数压缩为 8 位整数，甚至 1 位二进制值），以减少内存使用量并加快计算速度。</li></ol><p>但优化往往会使<strong>准确性</strong>下降。</p><p>这种担忧是有道理的：“如果我在搜索过程中压缩数据并使用快捷方式，我会错过最佳结果吗？”“这种优化是否会降低搜索引擎的相关性？”</p><p>为了证明 Elastic 的量化不会降低结果，我们使用<a href="https://huggingface.co/datasets/fancyzhx/dbpedia_14"><strong>DBPedia-14</strong> 数据集</a>构建了一个可重复的测试工具，以精确计算在使用 Elasticsearch 的默认优化时，准确率下降了多少（特别是<strong>召回率）</strong>才能提高速度。</p><p>总结：可能比您想象的要少得多。点击<a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/fast_vs_accurate_measuring_the_recall_of_quantized_vector_search/vector_recall_notebook.ipynb">此处查看笔记本</a>，亲自试试</p><h2><strong>定义（面向非专业人士）</strong></h2><p>在了解代码之前，让我们先明确一些术语。</p><ul><li><strong>相关性与召回率：相关性</strong>具有主观性（我找到的是优质内容吗？），而<strong>召回率</strong>则是基于数学计算。如果数据库中有 10 份文档与查询在数学层面<em>完美</em>匹配，而搜索引擎找到了其中 9 份，那么召回率就是 90%（或 0.9）。</li><li><strong>精确搜索（扁平式）：</strong>有时也被称为“暴力搜索”法。搜索引擎会扫描索引中的每一份文档并计算距离。<ul><li><em>优点：</em>召回率达到 100%。</li><li><em>缺点：</em>计算量大且大规模扩展缓慢。</li></ul></li><li><strong>近似搜索 (HNSW)：</strong>“捷径”方法。搜索引擎生成 <a href="https://www.elastic.co/search-labs/blog/hnsw-graph">HNSW</a> 图表。它遍历图表以找到最近邻。<ul><li><em>优点：</em>速度极快且可扩展。</li><li><em>缺点：</em>如果图表遍历过早停止，可能会错过近邻。</li></ul></li></ul><h2><strong>实验：精准与近似</strong></h2><p>为了测试召回率，我们使用了 <strong>DBPedia-14</strong> 数据集，这是一个包含 14 个本体类别的大型标题和摘要数据集，通常用于训练和评估文本分类模型。具体而言，我们将重点关注“电影”类别。我们希望将优化的生产设置与数学上完美的基准真值进行比较。</p><p>在此次实验中，我们采用 <a href="https://www.elastic.co/search-labs/blog/jina-embeddings-v5-text">jina-embeddings-v5-text-small</a> 模型。这是一款处于行业领先水平的多语言模型，在文本表征方面树立了行业基准。我们选择该模型，是因为它确立了当下高性能嵌入的标杆标准。通过将 Jina v5 卓越的精准度与Elasticsearch原生量化技术相结合，我们能够展示一种既具备高效计算能力，又在检索质量上毫不妥协的搜索架构。</p><p>我们设置了一个具有双重映射的索引。我们同时将相同的文本导入两个不同的字段：</p><ol><li><strong><code>content.raw</code> </strong>类型：<code>flat</code>。这会使 Elasticsearch 对全部 Float32 向量执行暴力扫描。通过这种扫描，系统会返回完全匹配的结果，并将用于我们的基线。</li><li><strong><code>content</code> </strong>类型为<code>semantic_text</code>。默认情况下使用 HNSW + 更好的二进制量化 (BBQ)。这是用于近似匹配的标准、优化生产设置。</li></ol><h3><strong>Recall@10 测试</strong></h3><p>在我们的评估指标中，我们使用了 Recall@10。</p><p>我们随机挑选了 50 部电影，并对这两个字段运行了相同的查询。</p><ul><li>如果<strong>精确（扁平式）</strong>搜索显示前 10 个近邻是 ID [1, 2, 3……10]，</li><li>而<strong>近似 (HNSW)</strong> 搜索结果显示的是 ID [1，2，3... 9，99]。</li><li>我们正确地找到了前 10 个中的 9 个。得分为 <strong>0.9</strong>。</li></ul><p>这是我们使用的映射：</p><pre><code># The "Control Group": Forces exact brute-force scan
"raw": {
    "type": "semantic_text",
    "inference_id": ".jina-embeddings-v5-text-small",
    "index_options": {
        "dense_vector": {
            "type": "flat"
        }
    }
}</code></pre><p><strong>结果：成功的“平直线”</strong></p><p>我们进行了一次规模测试，重新加载了整个数据集，并对 1,000 到 40,000 个文档的索引规模进行了测试。</p><p>召回率得分情况如下：</p>
            <table style="border-collapse: collapse; margin: 20px 0;">
              <tbody>
                
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">文档</td><td style="border: 1px solid #ccc; padding: 4px;">Recall@10得分</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">1,000</td><td style="border: 1px solid #ccc; padding: 4px;">1.000 (100%)</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">5,000</td><td style="border: 1px solid #ccc; padding: 4px;">0.998 (100%)</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">10,000</td><td style="border: 1px solid #ccc; padding: 4px;">0.992 (99.4%)</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">20,000</td><td style="border: 1px solid #ccc; padding: 4px;">0.999 (99.0%)</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">40,000</td><td style="border: 1px solid #ccc; padding: 4px;">0.992 (98.8%)</td>
                    </tr>
              </tbody>
            </table>
          <p>结果非常稳定。<strong>即使我们扩大了搜索范围，近似搜索也能</strong>在 99% 的情况下与暴力精确搜索相匹配。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/a4bfacb1d0cce6fdf6df0e1a9d4fc5d4007a66da-1999x1209.png" style="width: 100%;" /><h2><strong>为什么它如此有效？</strong></h2><p>您可能会认为将向量压缩成二进制值会对准确性的影响更大。不这样做的原因在于 Elasticsearch 处理检索的方式。</p><p>目前大多数嵌入模型输出的是 Float32 向量，这些向量很大。为了提高搜索效率，Elasticsearch 对高维向量使用量化技术。具体来说，自 9.2 版起，它默认使用<a href="https://www.elastic.co/search-labs/blog/elasticsearch-9-1-bbq-acorn-vector-search">BBQ</a>。</p><p>BBQ 采用<strong>重新打分</strong>机制：</p><ol><li><strong>遍历：</strong>搜索引擎使用压缩（量化）向量来快速遍历 HNSW 图表。由于向量较小，它可以高效地进行过度采样，收集更大的候选文档列表（例如，前 100 个大致相似的文档），而不会影响性能。</li><li><strong>重新评分：</strong>一旦有了这些候选文件，它就会只检索这几份文件的全精度值，以计算出最终的精确排名。</li></ol><p>这样就能两全其美，既能以量化的速度完成繁重的工作，又能以浮点运算的精度完成最终排序。</p><h2><strong>我们能做得更好吗？</strong></h2><p>值得注意的是，我们在这里看到的结果是使用默认设置和随机抽样数据得出的。可以将其视为高性能的起点。尽管 Jina v5 性能卓越，但这些召回率分数并非适用于所有数据集的“万能保障”。每个数据集都有其独特之处，虽然您肯定可以进一步调整优化以挖掘出更多性能潜力，但您始终应基于自身特定数据进行基准测试，以明确性能上限所在。</p><h2><strong>结论</strong></h2><p>这是一次规模非常小的测试。不过，本次测试的重点并非专门评估嵌入模型或 BBQ 的性能，而是要展示如何通过极简的设置轻松衡量数据集的召回率。</p><p>如果您想用自己的数据运行此测试，可以点击<a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/fast_vs_accurate_measuring_the_recall_of_quantized_vector_search/vector_recall_notebook.ipynb">此处查看笔记本</a>，亲自试试。</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[适用 Elasticsearch 的 Gemini CLI 扩展及工具和技能]]></title>
            <description><![CDATA[Elastic 推出了适用于谷歌 Gemini CLI 的扩展，用于在开发人员和智能体工作流中搜索、检索和分析 Elasticsearch 数据。
]]></description>
            <link>https://www.elastic.co/search-labs/cn/blog/gemini-cli-extension-elasticsearch</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/cn/blog/gemini-cli-extension-elasticsearch</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Walter Rafelsberger, Enrico Zimuel]]></dc:creator>
            <pubDate>Tue, 17 Mar 2026 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/6dfcec4a77b3dc83bf0d974417bf2e211abb1f4f-876x468.png?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>我们很高兴地宣布， Elastic 发布了适用于 Google 的 Gemini CLI 扩展，将 <a href="https://www.elastic.co/elasticsearch">Elasticsearch</a> 和 <a href="https://www.elastic.co/elasticsearch/agent-builder">Elastic Agent Builder</a> 的全部功能直接引入您的 AI 开发工作流。此扩展还提供几种最近开发的智能体技能，用于与 Elasticsearch 交互。</p><p>该扩展以开源项目的形式在<a href="https://github.com/elastic/gemini-cli-elasticsearch">此处</a>提供。</p><h2>Gemini CLI 是什么？如何安装？</h2><p><a href="https://geminicli.com/">Gemini CLI</a> 是一个开源的 AI 智能体，它可将 Google 的 Gemini 模型直接引入命令行。它允许开发人员从终端与 AI 进行交互，以执行诸如生成代码、编辑文件、运行 shell 命令和从网上检索信息等任务。</p><p>与典型的聊天界面不同，Gemini CLI 可与您的本地开发环境集成，这意味着它可以直接在终端内理解项目上下文、修改文件、运行构建或测试，以及自动化工作流。这对于想要在不离开命令行工作流的情况下进行 AI 辅助编码和自动化的开发人员、网站可靠性工程师 (SREs) 和工程师来说非常有用。</p><p>Gemini CLI 可通过多个软件包管理器安装。最常用的方法是通过 npm 安装：</p><pre><code>npm install -g @google/gemini-cli</code></pre><p>如要了解其他安装选项，请参阅<a href="https://geminicli.com/docs/get-started/installation/">官方安装页面</a>。</p><p>安装完成后，运行以下命令启动 CLI：</p><pre><code>gemini</code></pre><p>您会看到一个屏幕，如图 1 所示：</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/6dfcec4a77b3dc83bf0d974417bf2e211abb1f4f-876x468.png" style="width: 100%;" /><h2>配置 Elasticsearch</h2><p>我们需要运行一个 Elasticsearch 实例。如要使用模型上下文协议 (MCP) 服务器，您还需要安装 Kibana 9.3+。如要使用下面描述的 Elasticsearch 查询语言 (ES|QL) 技能 (<code>esql</code>)，则不需要 Kibana。</p><p>您可以在 <a href="https://www.elastic.co/cloud">Elastic Cloud</a> 上激活免费试用版，或使用 <a href="https://github.com/elastic/start-local"><code>start-local</code></a> 脚本在本地安装：</p><pre><code>curl -fsSL https://elastic.co/start-local | sh</code></pre><p>这将在您的计算机上安装 Elasticsearch 和 Kibana，并生成一个用于配置 Gemini CLI 的 API 密钥。</p><p>API 密钥将显示为上一条命令的输出，并存储在 <strong>.env</strong> 文件中，该文件位于 <strong><code>elastic-start-local</code></strong> 文件夹。</p><p>如果您使用的是本地部署的 Elasticsearch（例如使用 <code>start-local</code>），并且您想将 Elastic Agent Builder 与 MCP 一起使用，那么您还需要连接一个大型语言模型 (LLM)。您可以阅读<a href="https://www.elastic.co/docs/explore-analyze/ai-features/llm-guides/llm-connectors">此文档页面</a>以了解不同的选项。</p><p>如果您使用的是 Elastic Cloud（或无服务器架构），那么您已经预先建立了 LLM 连接。</p><h2>安装 Elasticsearch 扩展</h2><p>您可以使用以下命令为 Gemini CLI 安装 Elasticsearch 扩展：</p><pre><code>gemini extensions install https://github.com/elastic/gemini-cli-elasticsearch</code></pre><p>您可以通过打开 Gemini 并执行以下命令来检查扩展程序是否已成功安装：</p><pre><code>/extensions list</code></pre><p>您应该看到 Elasticsearch 扩展可用。</p><p>如要使用 MCP 集成，您需要安装 Elasticsearch 9.3 或更高版本。您需要从 <a href="https://www.elastic.co/kibana">Kibana</a> 获取您的 MCP 服务器 URL：</p><ul><li>从智能体处获取 MCP 服务器 URL &gt; 查看所有工具 &gt; 管理 MCP &gt; 复制 MCP 服务器 URL。</li><li>URL 将如下所示：https://your-kibana-instance/api/agent_builder/mcp</li></ul><p>您需要 Elasticsearch 终端 URL。这通常显示在 Kibana Elasticsearch 页面的顶部。如果您使用 <code>start-local</code> 运行 Elasticsearch，那么您已经在 <code>start-local</code>.env 文件的<code>ES_LOCAL_URL</code> 密钥中拥有了终端。</p><p>您还需要一个 API 密钥。如果您使用 <code>start-local</code> 运行 Elasticsearch，那么您已经在 <code>start-local</code> .env 文件中拥有了 <code>ES_LOCAL_API_KEY</code>。否则，您可以使用 Kibana 界面创建 API 密钥，详见<a href="https://www.elastic.co/docs/deploy-manage/api-keys/elasticsearch-api-keys">此处</a>：</p><ul><li>在 Kibana 中：Stack Management &gt; Security &gt; API 密钥 &gt; 创建 API 密钥。</li><li>我们建议仅设置 API 密钥的读取权限，并启用 <code>feature_agentBuilder.read</code> 权限，详见<a href="https://www.elastic.co/docs/explore-analyze/ai-features/agent-builder/permissions#grant-access-with-roles">此处</a>。</li><li>复制已编码的 API 密钥值。</li></ul><p>在您的 shell 中设置所需的环境变量：</p><pre><code>export ELASTIC_URL="your-elasticsearch-url"
export ELASTIC_MCP_URL="your-elasticsearch-mcp-url"
export ELASTIC_API_KEY="your-encoded-api-key"</code></pre><h2>安装示例数据集</h2><p>您可以安装 Kibana 提供的<strong>电子商务订单</strong>数据集。它包含一个名为 <strong><code>kibana_sample_data_ecommerce</code></strong> 的单个索引，其中包含来自一家电子商务网站的 4675 个订单的信息。对于每笔订单，我们都有以下信息：</p><ul><li>客户信息（姓名、ID 号码、出生日期、电子邮件等）。</li><li>订单日期。</li><li>订单编号。</li><li>产品（包含价格、数量、ID、类别、折扣和其他详情的所有产品列表）</li><li>SKU。</li><li>总价（不含税，含税）。</li><li>总数量。</li><li>地理信息（城市、国家、洲、位置、地区）。</li></ul><p>如要安装示例数据，请在 Kibana 中打开<strong>集成</strong>页面（在顶部搜索栏中搜索“集成”），然后安装<strong>示例数据</strong>。更多详情请参阅<a href="https://www.elastic.co/docs/explore-analyze/#gs-get-data-into-kibana">此处</a>的文档。</p><p>本文旨在展示如何轻松配置 Gemini CLI 以连接到 Elasticsearch 并与 <strong><code>kibana_sample_data_ecommerce</code></strong> 索引交互。</p><h2>如何使用 Elasticsearch MCP（模型上下文协议）</h2><p>您可以在 Gemini 中使用以下命令检查连接：</p><pre><code>/mcp list</code></pre><p>您应该会看到 <strong><code>elastic-agent-builder</code></strong> 已启用，如图 2 所示：</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/1508423bc1d1bc3c04a1cb01e2d59495a3516ed1-1465x844.png" style="width: 100%;" /><p>Elasticsearch 提供了一组默认工具。请参阅<a href="https://www.elastic.co/docs/explore-analyze/ai-features/agent-builder/tools/builtin-tools-reference">此处</a>的描述。</p><p>使用这些工具，您可以与 Elasticsearch 进行交互，提出类似以下的问题：</p><ul><li><code>Give me the list of all the indexes available in Elasticsearch.</code></li><li><code>How many customers are based in the USA in the kibana_sample_data_ecommerce index of Elasticsearch?</code></li></ul><p>根据问题的不同，Gemini 会使用一个或多个可用工具来尝试回答问题。</p><h2>/elastic 命令</h2><p>在 Gemini CLI 的 Elasticsearch 扩展中，我们还添加了<strong> <code>/elastic</code></strong> 命令。</p><p>如果执行 <strong><code>/help</code></strong> 命令，您将看到所有可用的 <code>/elastic</code> 选项（图 3）：</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/5b2a0727ce7a04354878dd048253d3f4d062324b-1983x230.png" style="width: 100%;" /><p>这些命令在您想直接执行 <code>elastic-agent-builder</code> MCP 服务器的特定工具时会很有用。例如，使用以下命令可以获取 <code>kibana_sample_data_ecommerce</code> 的映射：</p><pre><code>/elastic:get-mapping kibana_sample_data_ecommerce</code></pre><p>这些命令本质上是执行特定工具的快捷方式，而不是依赖 Gemini 模型来确定应该调用哪个工具。</p><h2>如何使用 Elasticsearch 的技能？</h2><p>该扩展还附带了 ES|QL 的<a href="https://github.com/elastic/gemini-cli-elasticsearch/tree/main/skills/esql">代理技能，ES|QL</a> 是 Elasticsearch 中提供的 <a href="https://www.elastic.co/docs/explore-analyze/discover/try-esql">Elasticsearch 查询语言</a>。<a href="https://agentskills.io/home">Agent Skills</a> 是一种开放格式，为 AI 编码智能体（如 Gemini CLI）提供特定任务的自定义指令。它们使用一种称为<em>渐进式披露</em>的概念，即在系统初始提示中只添加对技能的简要说明。当您要求智能体执行任务时，比如查询 Elasticsearch，它会将请求与相关技能匹配，并动态加载详细说明。这是一种高效管理词元预算的方法，同时为 AI 提供所需的准确上下文。</p><p><strong><code>esql</code>技能</strong>旨在让 Gemini CLI 直接针对集群编写和执行 ES|QL 查询。ES|QL 是一种功能强大的管道化查询语言，能非常直观地进行数据探索、日志分析和聚合。启用该技能后，您无需查找 ES|QL 语法；只需用自然语言向 Gemini CLI 提出有关数据的问题，智能体会处理剩下的问题。</p><p>执行操作是通过在终端中运行简单的 <a href="https://curl.se/">curl</a> 命令来完成的。之所以能做到这一点，是因为 Elasticsearch 提供了一套丰富的 REST API，可轻松用于将系统集成到任何架构中。</p><p><strong> esql  技能的功能：</strong></p><ul><li><strong>发现索引和模式：</strong>智能体可以使用该技能的内置工具列出可用索引并获取字段映射。例如，在为电子商务数据集编写查询之前，智能体可以在 <strong><code>kibana_sample_data_ecommerce</code></strong> 上运行模式检查，以了解可用的字段，如 <strong><code>taxful_total_price</code></strong> 或 <strong><code>category</code></strong>。</li><li><strong>无缝自然语言翻译：</strong>该技能不仅仅为智能体提供了一个简单的参考手册；它还提供了一个专门的指南，用于解读用户意图。当您用自然语言输入请求（如“按服务分组显示平均响应时间”）时，智能体会使用技能捆绑的模式匹配功能，将您的文字立即转换为正确的 ES|QL 聚合、筛选条件和命令。</li><li><strong>自我纠正：</strong>如果查询失败（例如由于类型不匹配或语法错误），该技能会返回生成的查询以及确切的 Elasticsearch 错误信息，使智能体能够立即修复查询并再次尝试，而无需您进行干预。</li></ul><p>由于 <code>esql</code> 技能也作为工具在 <code>elastic-agent-builder</code> MCP 服务器中可用，我们需要暂时禁用该服务器。您可以使用以下命令禁用它：</p><pre><code>/mcp disable elastic-agent-builder</code></pre><p>然后，您只需在 Gemini CLI 中输入类似这样的提示：</p><pre><code>Find the top 5 product categories by total sales revenue in the kibana_sample_data_ecommerce index</code></pre><p>代理将：</p><ul><li>认识到 <code>esql</code> 技能的必要性。</li><li>检查 <strong><code>kibana_sample_data_ecommerce</code></strong> 的模式。</li><li>构建一个 ES|QL 查询，比如：<code>FROM kibana_sample_data_ecommerce | STATS total_revenue = SUM(taxful_total_price) BY category.keyword | SORT total_revenue DESC | LIMIT 5</code>。</li><li>针对 Elasticsearch API 执行查询。</li><li>直接在终端中向您展示最终答案。</li></ul><p>在这里，我们分享了一个 Gemini 对先前提示的响应示例：</p><pre><code>───────────────────────────────────────────────────────────
> Find the top 5 product categories by total sales revenue in the kibana_sample_data_ecommerce index   ───────────────────────────────────────────────────────────
✓  Activate Skill "esql": Interact with Elasticsearch using ES|QL and curl. Use when querying, managing indices,  …
Skill esql activated. Resources loaded from /home/user/.gemini/extensions/elasticsearch/skills/esql: 
Showing up to 200 items (files + folders).                                                                     /home/user/.gemini/extensions/elasticsearch/skills/esql/                                                            ├───SKILL.md 
└───references/
├───esql-reference.md 
├───generation-tips.md 
└───query-patterns.md

✦ I will start by exploring the mapping of the kibana_sample_data_ecommerce index to identify the fields for product category and sales revenue. Then, I will construct and execute an ES|QL query to determine the top 5 product categories by total sales revenue.

✓  Shell curl -s "${ELASTIC_URL%/}/kibana_sample_data_ecommerce/_mapping" -H "Authorization: ApiKey $(printenv ELAS…                                                                                                                       {  
   "kibana_sample_data_ecommerce": { 
     "mappings": { 
       "properties": {
         "category": { 
           "type": "text", 
           "fields": {
             "keyword": {
               "type": "keyword"
             }
           }
         }, 
         "currency": {
           "type": "keyword"
         }, 

✦ Okay, I have the mapping. The category is products.category.keyword and sales revenue is taxful_total_price. I'll now execute the ES|QL query to get the top 5 categories by revenue.

✓  Shell curl -s -X POST "${ELASTIC_URL%/}/_query" \  -H "Authorization: ApiKey $(printenv ELASTIC_API_KEY)" \  -H …
{ 
   "took": 62,
   "is_partial": false, 
   "completion_time_in_millis": 1772805644164,
   "documents_found": 4675,
   "values_loaded": 12084, 
   "start_time_in_millis": 1772805644102, 
   "expiration_time_in_millis": 1773237644087,  
   "columns": [ 
     { 
       "name": "total_revenue",    
       "type": "double"  
     }, 
     {   
       "name": "products.category.keyword",

✦ The query for top 5 product categories by revenue was successful. I'll format the returned JSON values into a   user-friendly list.

 ✓  Shell 
                             
 Top 5 product categories by total sales revenue: 
 1. Men's Clothing: 149393.91
 2. Women's Clothing: 135099.91
 3. Women's Shoes: 105479.17
 4. Men's Shoes: 91797.92
 5. Women's Accessories: 60830.31 

✦ I've found the answer; now I'll format it for clarity.

 ✓  Shell 

 Here are the top 5 product categories by total sales revenue:                                                        
                                                                      
 | Rank | Category              | Total Revenue | 
 |------|-----------------------|---------------| 
 | 1    | Men's Clothing        | 149,393.91    |
 | 2    | Women's Clothing      | 135,099.91    |
 | 3    | Women's Shoes         | 105,479.17    |
 | 4    | Men's Shoes           | 91,797.92     | 
 | 5    | Women's Accessories   | 60,830.31     | 

✦ I've formatted and presented the answer. I'm finished.
 ───────────────────────────────────────────────────────────</code></pre><p>值得注意的是，Gemini 模型如何通过显示其遵循的所有步骤来生成最终响应。在这里，您可以清楚地看到技能对模型推理过程的影响。当模型第一次意识到需要使用某项技能或执行 shell 命令时，它会采用人机协同的方式请求权限。</p><p>通过处理模式发现、查询生成和执行的繁重工作，<code>esql</code> 技能可以让您完全专注于答案，而不是获得答案的机制。您将获得所需的数据，格式正确且直接存储在终端中，无需写一行语法或切换到其他应用。</p><h2>结论</h2><p>在本文中，我们介绍了我们最近发布的适用于 Gemini CLI 的 Elasticsearch 扩展。此扩展让您可以使用 Gemini 和 Elastic Agent Builder 提供的 Elasticsearch MCP 服务器（从 9.3.0 版本开始提供）以及 <code>/elastic</code> 命令与您的 Elasticsearch 实例进行交互。</p><p>此外，该扩展还包含一项 <code>esql</code> 技能，可以将用户的自然语言请求转换为 ES|QL 查询。这种技能在无法使用 MCP 服务器时特别有用，因为底层通信是由在终端中执行的简单 curl 命令驱动的。Elasticsearch 提供了一套丰富的 REST API，可以轻松集成到任何项目中。这在开发智能体 AI 应用时尤为有用。</p><p>有关 Gemini CLI 扩展的更多信息，请访问<a href="https://github.com/elastic/gemini-cli-elasticsearch">此处</a>的项目库。</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Elastic Agent Skills：将您的 AI 智能体变成 Elastic 专家。]]></title>
            <description><![CDATA[让您的 AI 编码智能体通过 Elastic Agent Skills 获得知识，以实现查询、可视化、安全和自动化。]]></description>
            <link>https://www.elastic.co/search-labs/cn/blog/agent-skills-elastic</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/cn/blog/agent-skills-elastic</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Graham Hudgins, Matt Ryan]]></dc:creator>
            <pubDate>Mon, 16 Mar 2026 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/09e64953819083168a9ecef0888c7f8bde1a43bd-720x420.jpg?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>每一位尝试在专业平台上使用 AI 编码智能体的开发人员、站点可靠性工程师 (SRE) 或分析师都会遇到同样的问题。您要求智能体编写查询、配置警报或调查某件事，结果接近但不完全正确。Elastic 在这方面具有优势：十多年来积累的文档、博客文章和社区解答意味着 AI 智能体比大多数数据平台更了解 Elastic。但这种深度也伴随着噪音。已弃用的 API 与当前的 API 并存。过时的模式与最佳实践的排名一样高。该智能体自信地复现了三个版本前行之有效的方法，因为在其训练数据中，这种方法确实奏效了。结果是产生了纠错税：用户手动将文档输入上下文，修复虚构的语法，并绕过智能体，而不是与智能体合作。更糟糕的是，高级功能完全未被使用，不是因为用户不需要它们，而是因为智能体不知道它们的存在。</p><p>这就是为什么我们要开源 <a href="https://github.com/elastic/agent-skills">Elastic Agent Skills</a>，即 Elasticsearch、Kibana、Elastic Observability 和 Elastic Security 方面的原生平台专业知识。您可以把它们添加到您已经使用的智能体运行时，把您的智能体从那种只会猜大量语法的“通才”提升成为一个具有专业知识的智能体，比如能像 Elastic 自己的工程团队一样使用许多架构标准。最初的技术预览版本侧重于与 <a href="https://www.elastic.co/cloud/serverless">Elastic Cloud Serverless</a> 具有最大兼容性的技能，但后续版本将迅速发展，以包含对旧堆栈版本的更好支持。</p><p>此外，Elastic 正在从两头解决这个问题。对于 Elastic 平台上的智能体，<a href="https://www.elastic.co/search-labs/blog/agent-builder-elastic-ga">Elastic Agent Builder</a>（现已正式发布）允许您创建和交互那些继承了您的数据访问控制的 AI 智能体，使用内置的搜索和分析工具，并结合上下文协同仪表板、警报和调查开展工作。我们正在努力确保在 Elastic 平台上提供卓越的智能体体验。但并非每个智能体都与 Elastic 兼容。您的团队可能已经在使用 Cursor、Claude Code 或其他运行时，这些智能体也需要正确理解 Elastic。这时 Agent Skills 就派上用场了。</p><h2>智能体在专业平台上为何面临重重困难</h2><p>大语言模型 (LLM) 是非常强大的通才。由于其训练数据包含丰富的示例，它们可以编写 Python 代码、解释 Kubernetes 清单，并重构 React 组件。但是，当涉及到平台特定的工作时，例如涉及专有查询语言、深度 API 接口和特定领域的最佳实践，它们的不足之处是可以预见的。</p><p>对于 Elasticsearch 来说，差距具体体现出来：</p><ul><li><strong>Elasticsearch 查询语言 (ES|QL) 是一个新领域。</strong>LLM 主要使用 SQL 进行训练，但 ES|QL 是一种管道化查询语言，具有不同的语法、不同的函数和不同的语义。智能体经常编写看似合理但无法解析的查询。它们会混淆 <code>WHERE</code> 和 <code>| WHERE</code>，编造不存在的函数，并完全忽略了基于管道的组合模型。</li><li><strong>API 接口表面范围广且具有专业深度。</strong>Elasticsearch、Kibana 和 Elastic Security 在搜索、摄取、告警、检测规则、案例管理、仪表板等多个领域有数百个 API。一个只配备一般训练数据的智能体必须猜测要调用哪个终端、请求正文是什么样子，以及如何处理响应。它经常会猜错，削弱了人们对它的信任。</li><li><strong>最佳实践不在训练数据中。</strong>何时应该使用 <code>semantic_text</code> 而不是自定义嵌入管道？如何构建 10GB CSV 的摄取管道？<a href="https://www.elastic.co/docs/solutions/security/detect-and-alert/mitre-attandckr-coverage">MITRE ATT&amp;CK</a> 技术的正确检测规则语法是什么？通用智能体在默认情况下不会加载经过整理、结构可靠的 Elastic 特定知识。它们需要查找这些知识，即使找到了，原始文档也并不总是包含熟练从业人员所具备的判断和最佳实践。</li></ul><p>结果就是，开发人员花在修复智能体输出上的时间比他们自己编写代码所需的时间还多。这不是任何人愿意接受的体验。</p><h2>代理技能：Platform 知识，专为代理人员量身定制</h2><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/8d16ec00d16e70a916c5eef0aaa23fcc735b7186-1067x1280.png" style="width: 100%;" /><p>Agent Skills 是包含指令、脚本和参考资料的独立目录，智能体运行时可以动态加载这些目录。当技能处于活动状态时，智能体可在正确的时间获得正确的上下文：查询语法、API 模式、验证逻辑、实例，因此它可以在第一次尝试时正确完成任务。</p><p>每个技能都遵循开放的 <a href="https://agentskills.io">agentskills.io</a> 规范：一个文件夹中包含一个 <code>SKILL.md</code> 文件，其中包含元数据和结构化说明。无专有格式，无锁定。技能可在智能体运行时中使用，包括 Cursor、Claude Code、GitHub Copilot、Windsurf、Gemini CLI、Cline 和 Codex <a href="https://agentskills.io">等</a>。</p><h3>v0.1.0 初始版本包含什么内容</h3><p>Elastic Stack 的第一组技能跨越五个领域：</p><ul><li>与 Elasticsearch API 交互（搜索、索引、集群管理）</li><li>构建和管理 Kibana 内容，例如仪表板、警报、连接器等</li><li>Elastic Observability 的领域专业知识</li><li>Elastic Security 的领域专业知识</li><li>在 Agent Builder 中创建高效的智能体</li></ul><h3>技能可组合</h3><p>技能不是单一的。它们采用的是模块化设计。您的智能体仅加载与当前任务相关的技能。正在编写 ES|QL 查询？ES|QL 技能将激活。需要从这些结果构建仪表板？仪表板技能将激活。要评估应用程序的健康状况？服务健康技能将发挥作用。要调查安全警报？随着调查的深入，分流技能将逐步衔接到案件管理和响应技能。</p><p>这种可组合性意味着您不需要一个庞大的、试图涵盖一切的单一提示。每种技能都完全符合其领域所需的语境，不多也不少。</p><h2>适用于构建搜索和 AI 应用程序的开发人员</h2><p>如果您正在将数据加载到 Elasticsearch、编写查询或迁移索引，技能可以缩短生成代码、遇到错误和搜索文档以查明问题所在的周期。</p><p>让您的智能体加载一个 CSV 文件，它会使用流式摄取工具来处理背压并从数据中推断映射。它不是那种手动编写的 _bulk 循环，不会在处理第一个大文件时就耗尽内存。让它使用 ES|QL 进行查询，它会发现您的实际索引名称和字段模式，然后编写具有正确语法、适当聚合和版本感知功能选择的有效管道查询，而不是需要三轮调试的 SQL 风格猜测。让它跨集群重新索引，它会遵循完整的操作工作流：用显式映射创建目的地，调整吞吐量设置，异步运行作业，完成后恢复生产设置，而不是简单地调用 _reindex，跳过有经验的操作员会遵循的一半步骤。</p><p>您得到的不是一个给您一个似是而非的起点，让您不得不去解决的智能体，而是一个编码了操作规范，让输出真正有效的智能体。</p><p><strong>使用 Elastic Agent Skills 的影响示例</strong></p>
            <table style="border-collapse: collapse; margin: 20px 0;">
              <tbody>
                
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">Eval</td><td style="border: 1px solid #ccc; padding: 4px;">技能引发了哪些改变</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">es-audit-query-failed-logins</td><td style="border: 1px solid #ccc; padding: 4px;">使用技能中的审计日志查询模式，而不是通用搜索</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">es-authz-role-mapping-ldap</td><td style="border: 1px solid #ccc; padding: 4px;">输出正确的角色映射 API 调用结构</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">esql-basic-query</td><td style="border: 1px solid #ccc; padding: 4px;">编写了 ES|QL 管道语法以替代查询 DSL</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">esql-error-handling</td><td style="border: 1px solid #ccc; padding: 4px;">先确定模式，而不是猜测字段名称</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">esql-schema-discovery</td><td style="border: 1px solid #ccc; padding: 4px;">从未猜测过索引名称</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">es-ingest-csv-with-infer</td><td style="border: 1px solid #ccc; padding: 4px;">单独使用 --infer-mappings，避免与 --source-format csv 组合使用，因为后者会导致索引为空</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">es-ingest-json-file</td><td style="border: 1px solid #ccc; padding: 4px;">采用了稳健的摄取方法，能够处理大文件</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">es-reindex-local-async</td><td style="border: 1px solid #ccc; padding: 4px;">首先创建目标索引，副本数为 0，刷新间隔为 “-1”，然后异步重建索引。基线跳过了任何准备工作</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">es-security-403-privileges</td><td style="border: 1px solid #ccc; padding: 4px;">按照技能诊断工作流程，而不是通用建议，处理特权错误</td>
                    </tr>
              </tbody>
            </table>
          <h2>面向安全团队</h2><p>安全团队每天都重复相同的操作工作流：对警报进行分流、调整检测规则、管理案例。Agent Skills 可对程序知识进行编码，让您的 AI 智能体能够正确执行这些工作流，以正确的顺序调用正确的 API，并使用正确的字段名称。如需通过实践操作指南，在不离开 IDE 的情况下从零开始构建一个完整的 Elastic Security 环境，请参阅 <a href="https://www.elastic.co/security-labs/agent-skills-elastic-security">从您的 AI 智能体开始使用 Elastic Security</a>。</p><h2>面向可观测与运维团队</h2><p>针对 Elastic Observability 的全新 Agent Skills 可以减轻对复杂系统进行检测、管理 SLO、筛选复杂数据以及评估服务健康状况的操作难度。将 Elastic 原生专业知识直接嵌入 AI 智能体中，可以让团队通过简单的自然语言执行复杂的可观测工作流。这使 SRE 和运营团队能够更快地解决事件，并更轻松地维护可靠的系统。阅读<a href="https://www.elastic.co/observability-labs/blog/elastic-agent-skills-observability-workflows">这篇博文</a>了解详情。</p><h2>开源、开放规范、社区驱动</h2><p>我们根据 Apache 2.0 许可协议发布 Agent Skills，因为我们认为智能体知识应该是开放的。技能所遵循的 <a href="https://agentskills.io">agentskills.io</a> 规范是一项开放标准，不是 Elastic 的专有格式。我们希望这些技能成为社区共同努力的成果，而不是封闭的生态系统。</p><h2>大局的一部分</h2><p>Agent Skills 是旨在使 Elasticsearch 成为最适合智能体使用的数据平台的更广泛计划的一部分。对于在 Elasticsearch 平台上运行的智能体，<a href="https://www.elastic.co/search-labs/blog/agent-builder-elastic-ga">Agent Builder</a> 还能更进一步，继承数据的访问控制和权限，提供用于搜索和分析的内置和自定义工具，并让用户在仪表板、警报和调查的上下文中与智能体进行交互。最后，Agent Builder 即将推出对技能的支持，允许开发者灵活地利用 Elastic Agent Skills 以及来自任何其他来源的技能，在 Elasticsearch 平台上实现安全、上下文增强的聊天和自动化。</p><p>对于部署在其他地方的智能体，我们正投入努力，建设开放的生态系统：</p><ul><li><strong>模型上下文协议 (MCP) 服务器扩展：</strong>扩展 Agent Builder 中的 <a href="https://www.elastic.co/docs/explore-analyze/ai-features/agent-builder/mcp-server">MCP 终端</a>，在当前搜索、ES|QL 和索引操作之外提供更多工具。</li><li><strong>身份验证改进：</strong>使代理能够更轻松地安全连接，目标是消除手动复制粘贴 API 密钥的操作。</li><li><strong>LLM 可读文档：</strong>发布 <code>llms.txt</code> 和 <code>AGENTS.md</code> 文件，让智能体能够自行发现和理解 Elastic API。</li><li><strong>用于智能体工作流的命令行接口 (CLI)：</strong>命令行工具，使连接管理和常见操作更适合智能体使用。</li></ul><p>技能是您今天能使用的层面。其余的功能即将到来。</p><h2>开始使用</h2><p><strong>开始之前：</strong>AI 编码智能体使用真实凭证、真实 shell 访问权限进行操作，通常还拥有运行它们的用户的全部权限。当这些智能体用于安全工作流时，风险会更高：您相当于是将检测逻辑、响应操作和敏感遥测数据的访问权限交给了一个自动化系统。每个组织的风险状况都是不同的。在启用 AI 驱动的安全工作流之前，请<strong>评估智能体可以访问哪些数据、可以采取哪些操作以及如果出现意外行为会如何</strong>。</p><p>将 Elastic Agent Skills 安装到您的智能体运行时：</p><p><code>npx skills add elastic/agent-skills</code></p><p>这会自动检测您已安装的智能体运行时，并将技能放置在正确的配置目录中。之后，您的智能体会自动获取这些技能。</p><p>您还可以直接浏览<a href="https://github.com/elastic/agent-skills">技能目录</a>，然后将技能文件夹复制到智能体的配置目录中，从而手动安装各个技能。</p><p>还没有 Elasticsearch 集群吗？开始<a href="https://cloud.elastic.co/registration">免费试用 Elastic Cloud</a>。您只需一分钟就能获得一个完全配置的环境。</p><p><strong>探索项目：</strong></p><ul><li><a href="https://github.com/elastic/agent-skills">Agent Skills 存储库</a></li><li><a href="https://agentskills.io">agentskills.io 规格</a></li><li><a href="https://www.elastic.co/docs">Elasticsearch 文档</a></li><li><a href="https://cloud.elastic.co/registration">Elastic Cloud 免费试用</a></li></ul>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[使用 Elasticsearch 解决实体问题，第 4 部分：终极挑战]]></title>
            <description><![CDATA[在专为防止走捷径而设计的高度多样化的“终极挑战”数据集上，解决并评估实体解析挑战。]]></description>
            <link>https://www.elastic.co/search-labs/cn/blog/entity-resolution-elasticsearch-llm-challenges</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/cn/blog/entity-resolution-elasticsearch-llm-challenges</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Jessica Moszkowicz]]></dc:creator>
            <pubDate>Fri, 13 Mar 2026 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/70fb0ff949f6db9ac9b8a28ecb4329ab915ebf46-720x420.png?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>我们现在已经看到智能实体解析通过两种方式实现。两种方法都以相同的方式开始：实体准备和提取，然后使用 Elasticsearch 检索候选对象。然后，我们通过基于提示的 JSON 生成或函数调用，使用大语言模型 (LLM) 对这些候选对象进行评估，并要求该模型对其判断做出透明的解释。</p><p>正如我们在<a href="https://www.elastic.co/search-labs/blog/elasticsearch-entity-resolution-llm-function-calling">之前的帖子</a>中看到的，函数调用提供的一致性不仅仅是一种不错的优化手段，它还是至关重要的。一旦我们从评估循环中消除了结构性错误，标准场景（例如第 4 层数据集中的场景）的结果便有了显著提升。</p><p>然而，有一个显而易见的问题需要回答：</p><p><em>当情况变得真正复杂时，这种方法还管用吗？</em></p><p>现实中的实体解析很少因简单情况而失败。当名称跨越语言、文化、书写系统、时间段和组织边界时，它就会失效。当人们用头衔而非名字称呼，公司更改名称，音译不一致，且尝试仅凭上下文（而非拼写）提及现实实体时，这种方法就失败了。</p><p>因此，在本系列的最后一篇文章中，我们对该系统进行了所谓的<strong>终极挑战</strong>。</p><h2>是什么让这成为终极挑战？</h2><p>在之前的评估中，我们使用越来越复杂的数据集对该系统进行了测试。当我们达到上一篇帖子中所讨论的第 4 层时，我们已经要应对昵称、头衔、多语言名称以及语义引用的混合情况了。这些测试表明，架构本身是可靠的，但可靠性问题，特别是不规范的 JSON 格式抑制了召回率。</p><p>有了函数调用，我们终于有了稳定的基础。这让我们有机会提出一个更有趣的问题：</p><p><em>一个统一的管道能否同时处理<strong>多种不同类型</strong>的实体解析问题？</em></p><p>终极挑战数据集的设计正是为了推动这一层面的发展。</p><p>该数据集并未专注于单一难点（如昵称或音译），而是结合了 <strong>50 多种不同的挑战类型</strong>，包括：</p><ul><li>文化命名惯例。</li><li>基于标题的引用。</li><li>业务关系和历史名称变更。</li><li>多语言和跨脚本提及。</li><li>复合挑战融合了上述多种元素。</li></ul><p>关键是，这并不是针对某个狭窄的用例进行优化。这是要测试当规则从一个实体变更到另一个实体时，<em>设计模式</em>是否成立。</p><h2>数据集概览</h2><p>终极挑战数据集包含：</p><ul><li><strong>50个实体</strong>，涵盖个人、组织和机构。</li><li><strong>约 60 篇文章</strong>，结构和语言复杂程度各不相同。</li><li><strong>51 个不同的挑战类别</strong>，大致分为以下几类：<ul><li>文化命名惯例。</li><li>标题和专业背景。</li><li>商业关系与组织关系。</li><li>多语言和音译挑战。</li><li>综合场景和边缘情况场景。</li></ul></li></ul><p>在本系列的前几篇文章中，我们看到使用生成式 AI (GenAI) 创建数据集可谓喜忧参半。如果没有它，要收集足够多、足够多样化的测试数据将极其困难。但如果不加以控制，这种模式往往会让事情变得过于简单。</p><p>例如，在早期的一次生成过程中，我们发现模型将“俄罗斯总统”等短语作为弗拉基米尔·普京 (Vladimir Putin) 的明确别名。这在今天看来可能是合理的，但却违背了测试上下文解析的目的。如果文章讨论的是 20 世纪 90 年代的俄罗斯，会发生什么情况？系统应根据上下文推断出正确的实体，而不是依赖于硬编码的别名。</p><p>因此，我们特意设计了这个数据集，以<strong>避免使用捷径</strong>。当系统能够推断出含义时，则不明确列出别名。描述性短语没有预先链接到实体。正确的匹配通常取决于文章层面的上下文，而不仅仅是局部文本。</p><p><strong>重要说明：</strong>尽管我们展示了系统在各种场景下的能力，但这仍是一个具有教育意义的原型。处理真实世界受制裁实体监控的生产系统需要额外的验证、合规性检查、审计跟踪，以及针对敏感用例的专门处理。</p><h2>为什么这些场景很难应对</h2><p>在本系列的第一篇文章中，我们介绍了一个简单但含义模糊的示例：“新的 Swift 更新来了！”挑战在于，“Swift”可以根据上下文解析为现实世界中的多个实体。这个示例反映了一个更广泛的事实：自然语言本质上是模棱两可的。</p><p>因此，实体解析不仅仅是字符串匹配的问题。人们经常依赖共享知识、文化规范和情境背景来解析引用，我们甚至很少注意到我们正在这样做。</p><p>考虑以下几个常见案例：</p><ul><li>没有地缘政治和时间背景，“总统”这样的头衔就毫无意义。</li><li>公司名称可能指母公司、子公司或之前的品牌，具体取决于文章的撰写时间。</li><li>一个人的名字可能会以不同的顺序、书写系统或音译方式出现，这取决于语言和文化。</li><li>同一个短语在不同的语境中可以合法地指代不同的实体，系统必须能够像接受匹配短语一样自信地<em>拒绝</em>匹配短语。</li></ul><p>没有单一的规则集可以高效地处理所有这些情况。这就是为什么这款原型如此激进地将关注点分开：</p><ul><li>Elasticsearch 高效而透明地缩小了候选空间。</li><li>LLM 仅在需要判断且必须自行解释的情况下使用。</li><li>检索和推理仍然是两个不同步骤。</li></ul><p>随着挑战类型多样性的增加，这种区分变得更加重要。</p><h2>系统如何在无特殊处理的情况下应对多样性</h2><p>这次评估最有趣的结果之一是<em>没有</em>改变的内容：</p><ul><li>我们<strong>没有</strong>针对日语名字添加特殊逻辑。</li><li>我们<strong>没有</strong>为阿拉伯语父名添加自定义规则。</li><li>我们<strong>没有</strong>添加历史公司名称的硬编码映射。</li></ul><p>相反，该系统依赖于系列早期引入的相同核心要素：</p><ul><li>为语义搜索编制索引的上下文丰富实体。</li><li>Elasticsearch 中的混合检索（精确检索、别名检索和语义检索）。</li><li>一组数量少且定义明确的候选匹配项。</li><li>受函数调用和最小模式约束的 LLM 判断。</li></ul><p>这表明系统的灵活性来自<strong>表征和架构</strong>，而非不断增长的规则集合。</p><p>当系统成功时，是因为检索到了正确的候选对象，且 LLM 有足够的上下文来解释为什么某个引用会（或不会）映射到某个特定实体。</p><h2>结果：它的表现如何？</h2><p>在最终挑战数据集上，系统得出了以下总体结果：</p><ul><li><strong>精度：</strong>约 91%</li><li><strong>召回：</strong> ~86%</li><li><strong>F1 分数：</strong>约 89%</li><li><strong>LLM 接受率：</strong>约 72%</li></ul><h3>在各类挑战中的表现</h3><p>按挑战类型细分结果可以揭示优势和局限性：</p><p>在以下领域的<strong>表现最为突出（100% F1 分数）</strong>：</p><ul><li>跨脚本匹配（西里尔字母、韩文、中文企业实体）。</li><li>希伯来语场景（父名、职业头衔、宗教头衔、音译）。</li><li>企业层级（航空航天、多元化制造、多部门公司）。</li><li>专业头衔（学术、军事、政治、宗教）。</li><li>涉及多种书写系统的综合日语场景。</li></ul><p><strong>表现优异（80–99% F1 分数）</strong>包括：</p><ul><li>国际政治人物（98%）。</li><li>历史名称变更 (90%)。</li><li>复杂的业务层次结构（89%）。</li><li>日本公司名称（93%）。</li><li>跨脚本音译（86%）。</li><li>阿拉伯语父名（86%）。</li></ul><p><strong>更具挑战性的领域</strong>包括：</p><ul><li>高级音译（中文、韩文）：0% F1。</li><li>某些日本场景（敬语、姓名顺序、书写系统变化）：~67% F1。</li><li>一些阿拉伯语场景（公司名称、机构引用）：约 40% F1。</li></ul><p>这里重要的是<em>为什么</em>系统在这些情况下会遇到困难。这些失败并非由于整体方法的崩溃，而是由于特定组件的局限性，尤其是在某些多语言场景中用于语义搜索的密集向量模型。</p><p>由于检索和判断是完全分离的，因此提高性能无需重写系统。更换功能更强大的多语言嵌入模型、丰富实体上下文或改进检索策略，都能在不改变核心架构的情况下改善这些类别的结果。</p><p>从架构的角度来看，这才是真正的成功指标。</p><h2>这告诉我们关于设计的启示</h2><p>回顾整个系列，有几个模式尤为突出：</p><ul><li><strong>准备工作比巧妙搭配更重要。</strong>预先为实体添加上下文信息可以显著减少以后可能出现的歧义。</li><li><strong>LLM 作为评判者最有价值，而非检索者。</strong>让他们解释<em>为什么</em>某个匹配是有意义的，比要求他们进行搜索要有效得多。</li><li><strong>可靠性确保准确性。</strong>函数调用不仅清理了 JSON，还释放了检索步骤中已存在的召回能力。</li><li><strong>通用性胜过专业化。</strong>少量经过精心挑选的抽象概念无需自定义逻辑即可处理数十种挑战类型。</li></ul><p>这就是为什么原型有意采用 Elasticsearch 原生架构，并在 LLM 的使用上有意采取保守策略的原因。目标不是取代搜索；而是在意义至关重要的情况下，使搜索变得可解释。</p><h2>总结</h2><p>最终的挑战并非追求完美的指标，而是回答一个更根本的问题：</p><p><em>一个透明、搜索优先、LLM 辅助的架构能否处理现实世界中的实体歧义，而不陷入规则或黑箱的情况？</em></p><p>对于这个具有教育意义的原型，答案是肯定的，但需要明确注意生产环境的强化、合规性、监控以及数据质量等方面的问题。如果您正在构建的系统需要说明<em>为什么</em>要进行实体匹配，那么这种模式值得认真考虑。我希望这个系列能告诉人们，实体解析其实并不神秘。只要合理地进行关注点分离，就可以对问题进行推理、评估和优化。</p><p>这项工作还提出了一种更广泛的架构模式。由此出现了经典检索增强生成 (RAG) 的一次细微但重要的演变。我们没有让检索直接为生成提供信息，而是引入了一个明确的评估步骤。首先使用 LLM 对检索到的候选结果进行判断和合理性检查，只有通过审核的结果才允许用于增强生成。您可以将其视为“生成增强型检索增强生成与评估”，或者简称为“GARAGE”，毕竟谁不喜欢一个好听的缩写词呢。</p><p>还有哪些其他用例可以从这种模式中受益？需要信任、透明和可辩护推理的系统是当然的候选者。未来在这一领域的工作应该会像我们在这里看到的结果一样引人注目，我很期待看到社区接下来会有什么新的发展。</p><h2>下一步：亲自试用</h2><p>想看看终极挑战的实际操作吗？请查看<a href="https://github.com/jesslm/entity-resolution-lab-public/tree/main/notebooks#:~:text=5%20minutes%20ago-,05_ultimate_challenge_v3.ipynb,-Initial%20public%20lab"><strong>终极挑战笔记本</strong></a>，它通过实际实现、详细解释和动手示例，提供了完整的实践指南。</p><p>完整的实体解析管道展示了生产使用所需的核心概念和架构。您可以将其用作构建系统的基础，这些系统可以监测新闻文章、跟踪实体提及情况，并回答有关哪些实体出现在哪些文章中的问题，同时还能保持透明度和可解释性。<br/></p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Elasticsearch 中 HNSW 的自适应提前终止]]></title>
            <description><![CDATA[为 Elasticsearch 中的 HNSW 引入一种新的自适应提前终止策略。]]></description>
            <link>https://www.elastic.co/search-labs/cn/blog/hnsw-elasticsearch-adaptive-early-termination</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/cn/blog/hnsw-elasticsearch-adaptive-early-termination</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Tommaso Teofili]]></dc:creator>
            <pubDate>Mon, 02 Mar 2026 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/e6d3186f609dd56dc5ffe33d70fa9e5cfa05b51f-1280x720.png?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>Elasticsearch 使用<a href="https://www.elastic.co/search-labs/blog/hnsw-graph">分层可导航小世界</a> (HNSW) 算法对邻近图进行矢量搜索。众所周知，HNSW 算法在 k 近邻 (KNN) 搜索结果的质量与相关计算成本之间实现了良好的平衡。</p><p>在 HNSW 中，搜索过程是通过在图中迭代扩展候选节点来推进的，同时维护一个迄今为止已发现的、规模受限的最近邻节点集合。每次扩展都会带来一定的影响（包括向量运算、磁盘随机寻址等操作），并且随着搜索的推进，这种影响所带来的边际效益往往会逐渐降低。</p><p>优化 HNSW 图遍历的一种方法是，当发现新真实邻近节点的边际概率不再提升时，立即停止搜索。因此，在 <a href="https://www.elastic.co/docs/reference/elasticsearch/index-settings/index-modules#index-dense-vector-hnsw-early-termination">Elasticsearch 9.2</a> 中，我们引入了新<a href="https://www.elastic.co/search-labs/blog/hnsw-knn-search-early-termination">提前终止机制</a>。当连续访问图节点的次数达到固定数量，但仍无法提供足够的新最近邻节点时，搜索过程就会停止。</p><p>本文将指导您了解我们如何改进 HNSW 中提到的提前终止机制，使其更适合不同的数据集和数据分布。</p><h2><strong>HNSW 中的提前终止</strong></h2><p>在 HNSW 中，搜索过程是通过在近邻图中迭代扩展候选节点来推进的，同时持续维护一个迄今已发现、规模受限的最近邻节点集合，直至搜索遍历完整个图，或者满足某些提前终止条件为止。</p><p>因此，提前终止不一定总是性能优化，它本身就是<strong>搜索算法不可或缺的组成部分</strong>。决定终止搜索的时机，直接决定了效率与召回率之间的权衡关系。在 Elasticsearch 中，针对 HNSW 图的查询已内置多种提前终止机制：</p><ul><li>访问节点的最大数量固定不变。</li><li>已达到固定的超时时间。</li></ul><p>这些规则虽然简单且可预测，但在很大程度上<strong>与搜索的实际操作无关</strong>。此外，它们主要用于确保最终用户在合理的时间内完成查询。</p><p>在<a href="https://www.elastic.co/search-labs/blog/hnsw-knn-search-early-termination">上一篇博文</a>中，我们介绍了 HNSW 冗余的概念。简而言之，当 HNSW 持续评估那些无法带来更多最近邻节点的新候选节点时，就会产生冗余计算。</p><h2><strong>耐心：衡量进展而非过程</strong></h2><p><em>“耐心”</em>这一概念将提前终止的判定标准重新定义为<strong>“衡量进展而非过程”</strong>。</p><p>而不是问：</p><p>“我们走了多少步？”</p><p>新的问题变成了：</p><p>&quot;在我们彻底丧失希望之前，我们能够接受浪费多少计算量？&quot;</p><p>在 HNSW 搜索过程中，早期探索阶段通常能显著提升前 k 个候选结果集的质量。在 HNSW 图探索的初始阶段，随着算法不断发现与查询向量距离更近的邻近节点，邻近节点集会持续更新。随着搜索逐步收敛，这类质量提升会逐渐减少。<a href="https://cs.uwaterloo.ca/~jimmylin/publications/Teofili_Lin_ECIR2025.pdf">基于“耐心”机制的终止</a>策略会监测这一变化模式，当持续一段时间内未再出现显著改进时，即终止搜索过程。</p><p>在实际操作中，我们在遍历 HNSW 图的过程中，每跳转到一个候选节点时，都会计算队列饱和度。该指标用于衡量在访问最近一个图节点期间，未发生变化的最近邻节点所占的百分比（或者说，是上一轮迭代中引入的新邻节点数量的倒数）。若连续多次迭代中，这一比率持续过高，我们便会停止对图的遍历。</p><p>从概念层面来看，“耐心”机制将HNSW搜索视为一个<strong>收益递减的过程</strong>。当搜索收益趋于平稳时，继续遍历图结构所带来的增益将微乎其微。</p><p>这种框架之所以强大，是因为它将终止与<em>可观察到的结果</em>直接联系起来，而不是与任意的固定限制联系起来。</p><p>采用这种智能提前终止技术的优势在于，HNSW 图探索过程在保持近乎完美的相对召回率的同时，往往会访问更少数量的图节点。</p><p>为了直观地说明这一点，我们可以在几个数据集（FinancialQA 和 Quora）和模型（JinaV3 和 E5-small）上绘制基于耐心的提前终止（标注为 <em><code>et=static</code></em>）与默认 HNSW 行为（标注为 <em><code>et=no</code></em>）的对比图。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/a9d07c5153ea64a2426c82487c36846030692bb9-1600x945.png" style="width: 100%;" /><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/dac56125c4b16d1b596c9876b6ca9ac7b2dc87fa-1600x944.png" style="width: 100%;" /><h2><strong>静态阈值和 HNSW 动态</strong></h2><p>实际上，Elasticsearch 使用<strong>静态阈值</strong>来实现这一点。其中一个阈值指的是<strong>饱和阈值</strong>，即我们认为次优的饱和度比率。另一个阈值指的是，在队列达到次优饱和度的情况下，我们允许访问的连续图节点数，即<strong>耐心阈值</strong>。</p><p>当我们在 Elasticsearch 9.2 中引入这种提前终止策略时，我们决定选择保守的默认设置，以便在延迟和内存消耗方面仍能达到效果的同时，尽可能多地让系统召回。因此，我们将饱和阈值设为 100%，耐心阈值设为 KNN 查询中 <a href="https://www.elastic.co/docs/reference/query-languages/query-dsl/query-dsl-knn-query#knn-query-top-level-parameters:~:text=search%20request%20size.-,num_candidates,-(Optional%2C%20integer)%20The"><em><code>num_candidates</code></em></a> 的（有界）30%。</p><p>在很多情况中，这些设置能取得不错的效果；然而，对于请求相同数量邻节点的两个查询而言，它们的收敛行为可能存在极大差异。有些查询会遇到密集的局部邻域，能迅速达到饱和状态；而有些查询则必须遍历漫长且稀疏的路径，才能找到具有竞争力的候选节点。事实证明，后一种情况最难以有效处理。</p><p>因此，我们有时会发现：</p><ul><li>简单查询的过度探索。</li><li>复杂查询的过早终止。</li></ul><p>因此，我们认为固定阈值编码了关于收敛的全局假设，而我们可以使 HNSW 更好地适应不同的动态。</p><h2><strong>实现 HNSW 的提前终止自适应</strong></h2><p>自适应提前终止从另一个角度解决了这个问题。该算法不是强制执行预定义的停止阈值，而是<strong>从搜索动态本身推断何时停止</strong>。</p><p>因此，我们不再比较连续两个候选节点间的队列饱和度比率，而是决定引入即时平滑发现率 $d_{q,i}$（即最近一次访问 <em>i</em> 中，针对查询 <em>q</em> 新发现的邻近节点数量），同时结合图遍历过程中该发现率的滑动均值 $\mu_{q,i} $和标准差$\sigma_{q,i}$（采用<a href="https://en.wikipedia.org/wiki/Algorithms_for_calculating_variance#Welford&#x27;s_online_algorithm">韦尔福德算法</a>计算）。这些关于发现率的统计量按每个查询独立计算，从而可根据不同查询的特性动态调整其“耐心”阈值。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/1958be7ca4425ade66eaf621ada3533173183598-694x118.png" style="width: 100%;" /><p>先前静态设定的阈值将根据发现率统计数据实现自适应调整：饱和阈值调整为滚动均值加上标准差；同时，我们将耐心值设为与标准差呈反比变化的动态参数。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/f7ee4a55c24853b657df26052b275e8bd76cf0f9-654x156.png" style="width: 100%;" /><p>提前退出的规则保持不变；当瞬时发现率低于自适应饱和阈值时，即判定达到饱和状态。如果在连续访问的候选节点数量超过自适应耐心值所设定的次数后，饱和状态仍持续存在，则停止对图的遍历。</p><p>如此一来，我们实现了搜索行为不再依赖于 KNN 查询中的 <em><code>num_candidates</code></em> 参数（该参数可能始终被设定为固定值或保留默认值，而与提前终止机制无关），同时能够根据每个查询和向量分布进行动态适配。</p><p>在 FinancialQA 和 Quora 数据集上，采用自适应策略（标记为 <em><code>et=adaptive</code></em>）时，每个访问节点的召回率相较于静态策略（ <em><code>et=static</code></em>）和默认 HNSW 行为（<em><code>et=no</code></em>）均有显著提升。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/2a906997d9a25d74c7038bd9661bc97581e7258e-1600x938.png" style="width: 100%;" /><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/3a114911e232c351dbb814cea20e8b0f1415a717-1600x925.png" style="width: 100%;" /><p>在 Elasticsearch 9.3 中，HNSW 密集向量字段的自适应提前终止默认处于启用状态（最终可以通过<a href="https://www.elastic.co/docs/reference/elasticsearch/index-settings/index-modules#index-dense-vector-hnsw-early-termination">相同的索引级别设置</a>将其关闭）。</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[通用表达式语言（CEL）：CEL 输入如何改进 Elastic Agent 集成中的数据收集]]></title>
            <description><![CDATA[了解通用表达式语言 (CEL) 与其他编程语言的区别、我们针对 Filebeat 的 CEL 输入所进行的扩展，及其如何赋能在 Elastic Agent 集成中更灵活地表达数据采集逻辑。]]></description>
            <link>https://www.elastic.co/search-labs/cn/blog/common-expression-language-elasticsearch</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/cn/blog/common-expression-language-elasticsearch</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Chris Berkhout]]></dc:creator>
            <pubDate>Fri, 27 Feb 2026 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/985c50bfabee3348494eb4307f0b3375a97a0644-1280x720.png?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>Elastic Agent <a href="https://www.elastic.co/integrations">集成</a>支持用户从多种来源将数据摄取至 Elasticsearch。这些集成会将采集逻辑、摄取管道、仪表板以及其他构件打包在一起，形成一个可通过 Kibana Web 界面安装和管理的包。</p><p>集成通过配置 <a href="https://www.elastic.co/docs/reference/beats/filebeat/configuration-filebeat-options">Filebeat 输入</a>来执行数据采集。为了从 HTTP API 采集数据，我们通常使用 <a href="https://www.elastic.co/docs/reference/beats/filebeat/filebeat-input-httpjson">HTTP JSON 输入</a>。然而，即便是最基础的列表 API，在具体细节上也可能千差万别，而 HTTP JSON 输入采用 YAML 配置进行数据转换的模式，往往使所需的采集逻辑难以自然表达，有时甚至无法实现。</p><p>为此，我们引入了<a href="https://www.elastic.co/docs/reference/beats/filebeat/filebeat-input-cel">通用表达式语言 (CEL) 输入</a>，让与 HTTP API 的交互更加灵活。<a href="https://cel.dev/">CEL</a> 是一种专为嵌入到应用中而设计的语言，适用于需要以快速、安全且可扩展的方式来表达条件和数据转换的场景。通过 CEL 输入，集成构建者只需编写一个表达式，即可读取设置、跟踪自身状态、发起请求、处理响应，并最终返回可直接摄取的事件。</p><p>本文将探讨 CEL 与其他编程语言的区别、我们针对 Filebeat 的 CEL 输入所进行的扩展，以及由此带来的数据采集逻辑表达灵活性与能力提升。</p><h2>CEL 及其在输入中的工作方式</h2><p>CEL 是一种表达式语言，它没有“语句”这一概念。在编写 CEL 时，您不是通过编写语句来指示它执行操作，而是通过编写表达式来定义要生成的值。每个 CEL 表达式都会计算出一个值，较小的表达式可以组合成更大的表达式，按照更复杂的规则生成结果。稍后，我们将了解如何通过表达式来实现其他语言中需要用语句来完成的逻辑。</p><p>CEL 有意设计为一种非图灵完备的语言，因此不支持无界循环。稍后，我们将介绍如何使用宏来处理列表和映射；正是通过禁止无界循环，CEL 能够保证每个表达式的执行时间可预测且有限。</p><p>CEL 输入需要配置一个 CEL 程序（即一个表达式）以及一些初始状态。这个初始状态会作为输入传递给程序。程序执行后会产生一个新的输出状态。如果输出状态中包含事件列表，这些事件会被提取出来并发布。输出状态的其余部分则作为下一次执行的输入。如果输出状态包含一个或多个事件，并且带有 <code>want_more: true</code> 标志，程序会立即再次执行；否则，它会在剩余的配置时间间隔内等待，然后才继续执行。下面是输入控制流的简化示意图：</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/42671541f97e2dba808fd53969fe12f517917f9a-1600x529.png" style="width: 100%;" /><p>只要输入还在运行，每次执行的输出都会作为下一次执行的输入。存储在 “<code>cursor</code>” 键下的数据会被保存到磁盘，并在输入重启后重新加载，但其余状态在重启后不会保留。</p><p>CEL 语言本身功能有限，且不会产生副作用，但它支持扩展。<a href="https://github.com/google/cel-go">cel-go</a> 实现为其添加了一些功能，例如可选语法和类型。<a href="https://github.com/elastic/mito">Mito</a> 库在 cel-go 的基础上进一步扩展，增加了发起 HTTP 请求等功能。CEL 输入使用的正是 Mito 提供的 CEL 版本。</p><h2>与 Mito 合作</h2><p>要使用 CEL 输入构建或调试集成，最关键的是理解：针对给定的输入状态，您的 CEL 程序会输出什么。在开发过程中，如果每次都要在完整的 Elastic Stack 环境中通过 CEL 输入来运行程序，会非常繁琐。为了更快地获得反馈，可以使用 Mito 的命令行工具，它允许您直接运行 CEL 程序，并查看它对给定输入产生的输出。</p><p>Mito 是用 Go 语言编写的，您可以通过以下命令安装：</p><pre><code>go install github.com/elastic/mito/cmd/mito@latest</code></pre><p>使用 Mito 运行 CEL 程序时，通常需要提供两个文件：一个是 JSON 文件，包含初始输入状态；另一个是 CEL 源文件，包含程序代码。</p><pre><code>mito -data state.json src.cel</code></pre><p>为了方便复制粘贴，本文示例采用单条命令的形式，利用 <code>&lt;(echo &#x27;...content...&#x27;)</code> 语法将每个文件的内容动态生成临时文件。在实际开发中，直接使用真实的文件会更方便。</p><h2>从 GitHub 获取 issue 数据</h2><p>以下示例是一个完整的 CEL 程序，用于从 <a href="https://docs.github.com/en/rest/issues/issues?apiVersion=2022-11-28#list-repository-issues">GitHub API</a> 获取 issue 数据。它的初始输入状态包含 API 端点的 URL 以及分页处理的相关信息。CEL 程序利用这些输入数据生成请求，然后解码响应，从中生成事件，并将这些事件作为输出状态的一部分返回。</p><pre><code>mito -data <(echo '
  {
    "url": "https://api.github.com/repos/elastic/integrations/issues",
    "per_page": 3,
    "max_pages": 3
  }
') <(echo '
  int(state.?cursor.page.orValue(1)).as(page,
    (
      state.url + "?" + {
        "state": ["all"],
        "sort": ["created"],
        "direction": ["asc"],
        "per_page": [string(state.per_page)],
        "page": [string(page)],
      }.format_query()
    ).as(full_url,
      request("GET", full_url).with({
        "Header": {
          "Accept": ["application/vnd.github+json"],
          "X-GitHub-Api-Version": ["2022-11-28"],
        }
      }).do_request().as(resp,
        resp.Body.decode_json().as(data,
          state.with({
            "events": data.map(i, {
              "html_url": i.html_url,
              "title": i.title,
              "created_at": i.created_at,
            }),
            "cursor": { "page": page + 1 },
            "want_more": size(data) == state.per_page && page < state.max_pages,
          })
        )
      )
    )
  )
')</code></pre><p>程序第一次执行会产生以下输出：</p><pre><code>{
  "cursor": {
    "page": 2
  },
  "events": [
    {
      "created_at": "2018-09-14T09:47:35Z",
      "html_url": "https://github.com/elastic/integrations/issues/3250",
      "title": "Increase support of log formats in haproxy filebeat module"
    },
    {
      "created_at": "2019-02-06T12:37:37Z",
      "html_url": "https://github.com/elastic/integrations/issues/487",
      "title": "ETCD Metricbeat module needs polishing and grooming"
    },
    {
      "created_at": "2019-08-13T11:33:11Z",
      "html_url": "https://github.com/elastic/integrations/pull/1",
      "title": "Initial structure"
    }
  ],
  "max_pages": 3,
  "per_page": 3,
  "url": "https://api.github.com/repos/elastic/integrations/issues",
  "want_more": true
}</code></pre><p>这些事件会被提取出来，并在 CEL 输入中发布，以便摄取。剩余的输出数据将作为输入状态传递给下一次 CEL 程序执行。</p><p></p><p>为了帮助理解这个 CEL 程序的工作原理，我们先看一些更简单的 CEL 示例，并深入讨论 CEL 输入的运行细节。</p><h2>CEL 基础知识</h2><p>CEL 语言中只有表达式，没有语句。每个 CEL 表达式成功执行后都会得到一个最终值。以下是一个最简单的 CEL 表达式示例及其输出：</p><pre><code>mito <(echo '
  "hello" + " " + "world"
')</code></pre><pre><code>"hello world"</code></pre><p>许多简单表达式都很直观。数学运算要求操作数类型相同（例如 <code>int</code> 与 <code>int</code>），因此请根据需要进行类型转换（此处是从 <code>int</code> 转换为 <code>double</code>）：</p><pre><code>mito <(echo '
  double((1 + 2) * (3 + 4)) / 2.0
')</code></pre><pre><code>10.5</code></pre><p>CEL 语言中没有变量，但您可以为表达式的结果命名，并借助 Mito 的 <a href="https://pkg.go.dev/github.com/elastic/mito/lib#hdr-As__Macro_-Collections"><code>as</code></a> 宏在更大的表达式中使用它。在此示例中，表达式 <code>(1 + 1)</code> 的值为 <code>2</code>，而 <code>.as(n, ...)</code> 会将该值命名为 <code>n</code>，以便在表达式 <code>&quot;one plus one is &quot;+string(n)</code> 中使用：</p><pre><code>mito <(echo '
  (1 + 1).as(n, "one plus one is "+string(n))
')</code></pre><pre><code>"one plus one is 2"</code></pre><p>还可以在映射中累积信息，并在表达式中稍后使用；下面的示例用 <a href="https://pkg.go.dev/github.com/elastic/mito/lib#hdr-With-Collections"><code>with</code></a> 演示了这一点：</p><pre><code>mito <(echo '
  { "key": "value" }.with({ "key2": "value2" }).as(data,
    {
      "data": data,
      "size": size(data),
    }
  )
')</code></pre><pre><code>{
  "data": {
    "key": "value",
    "key2": "value2"
  },
  "size": 2
}</code></pre><p>我们再来看这个例子。注意嵌套部分 <code>({ &quot;data&quot;: data, &quot;size&quot;: size(data), })</code>，它定义了最终值的结构。这是一个映射，包含 <code>&quot;data&quot;</code> 和 <code>&quot;size&quot;</code> 两个键。这些键的值依赖于 <code>data</code>，而它是由外层表达式定义的。从内向外阅读 CEL 表达式，有助于快速理解它们会返回什么。</p><p>CEL 没有 <code>if</code> 这样的控制流语句，但可以用三元运算符实现条件分支：</p><pre><code>mito <(echo '
  1 + 1 < 12 ? "few" : "many"
')</code></pre><pre><code>"few"</code></pre><p>由于 CEL 不是图灵完备语言，因此不支持无限循环和递归。这保证了执行时间可预测，并且与输入数据的大小和表达式的复杂度成正比。</p><p>虽然单个 CEL 表达式不能使用无限循环，但您可以用 <a href="https://github.com/google/cel-spec/blob/master/doc/langdef.md#macros"><code>map</code></a> 这样的宏来处理列表和映射：</p><pre><code>mito <(echo '
  [1, 2, 3].map(x, x * 2)
')</code></pre><pre><code>[2, 4, 6]</code></pre><p>本节介绍了以下内容：</p><ul><li>字符串、数字、列表和映射。</li><li>字符串连接。</li><li>数学运算。</li><li>类型转换。</li><li>条件语句。</li><li>命名子表达式。</li><li>处理集合。</li></ul><p>接下来，我们将学习如何发出 HTTP 请求。</p><h2>请求</h2><p>Mito 为 CEL 扩展了发起 <a href="https://pkg.go.dev/github.com/elastic/mito/lib#HTTP">HTTP</a> 请求的能力：</p><pre><code>mito <(echo '
  get("https://example.com").as(resp, string(resp.Body))
')</code></pre><pre><code>"<!doctype html><html lang=\"en\"><head><title>Example Domain</title>..."</code></pre><p>请求可以在执行前明确构造，这样就可以使用不同的 HTTP 方法，并添加请求头和请求体。</p><p>在这个示例中，我们借助 <a href="https://pkg.go.dev/github.com/elastic/mito/lib#hdr-Format_Query-HTTP"><code>format_query</code></a> 构建 URL，向请求添加一个请求头，并使用 <a href="https://pkg.go.dev/github.com/elastic/mito/lib#hdr-Decode_JSON-JSON"><code>decode_json</code></a> 解析响应体。当传入 <code>-log_requests</code> 选项时，Mito 会以 JSON 格式记录每个请求和响应的详细信息。</p><pre><code>mito -log_requests <(echo '
  request("GET",
    "https://postman-echo.com/get?" + {
        "q": ["query value"]
     }.format_query()
  ).with({
    "Header": { "Accept": ["application/json"] }
  }).do_request().as(resp, {
    "status": resp.StatusCode,
    "data": resp.Body.decode_json(),
  })
')</code></pre><pre><code>{"time":"...","level":"INFO","msg":"HTTP request",...}
{"time":"...","level":"INFO","msg":"HTTP response",...}
{
  "data": {
    "args": {
      "q": "query value"
    },
    "headers": {
      "accept": "application/json",
      "accept-encoding": "gzip, br",
      "host": "postman-echo.com",
      "user-agent": "Go-http-client/2.0",
      "x-forwarded-proto": "https"
    },
    "url": "https://postman-echo.com/get?q=query+value"
  },
  "status": 200
}</code></pre><h2>管理状态和评估</h2><p>我们已经介绍了如何发出请求，以及生成期望输出状态所需的 CEL 基础知识，接下来就来仔细看看输出状态应该包含哪些内容，以及这些内容如何帮助我们引导后续的处理流程。</p><p>集成的 CEL 程序需要确保其输出状态能够作为下一次执行的输入。配置设定了初始状态，输出中应保留这些状态值，并根据需要更新。一个简单的做法是使用 <code>state.with({ ... })</code>，在原有状态映射的基础上合并覆盖。小型程序常见的一种模式是将整个程序包裹在 <code>state.with()</code> 中，这样在成功、错误等输出数据的分支中，就无需重复处理状态传递逻辑。</p><p>如果某些状态值不是在初始输入状态中硬编码，而是在执行过程中动态初始化，那么程序在设置初始值之前需要先检查是否已存在值。这正是<a href="https://pkg.go.dev/github.com/google/cel-go/cel#OptionalTypes">可选语法和类型</a>支持能够发挥作用的场景。在映射键的字段名前面加上问号，访问就变为可选：可能得到值，也可能得不到，但后续仍可以进行可选访问，并且在无值时可以方便地提供默认值：<br/></p><pre><code>mito -data <(echo '{}') <(echo '
  int(state.?counter.orValue(0)).as(counter,
    state.with({
      "counter": counter + 1,
      "want_more": counter + 1 < 3,
    })
  )
')</code></pre><pre><code>{ "counter": 1, "want_more": true }
{ "counter": 2, "want_more": true }
{ "counter": 3, "want_more": false }</code></pre><p>在这个例子中，从 state 读取的计数器值需要转换为 <code>int</code>，因为状态中的所有数字都按照 JSON 和 JavaScript <code>Number</code> 类型的惯例序列化为浮点数。另外需要注意的是，Mito 会响应 <code>&quot;want_more&quot;: true</code>，但在 CEL 输入中运行时，只有当输出中同时包含事件时，才会重复执行。</p><p>由 CEL 输入运行的 CEL 程序必须在输出映射中包含一个 <code>&quot;events&quot;</code> 键。它的值可以是事件映射的列表、空列表，或单个事件映射。单个事件映射通常用于表示错误。该事件会被输入发布，其值也会被记录到日志；如果设置了 <code>error.message</code>，该值还会用于更新集成在 Fleet 中的健康状态。如果程序生成的是单个非错误事件，最好将其包装在列表中。</p><p>回顾一下我们之前的 GitHub issues 程序的输出：</p><pre><code>{
  "url": "https://api.github.com/repos/elastic/integrations/issues",
  "per_page": 3,
  "max_pages": 3,
  "cursor": {
    "page": 2
  },
  "events": [
    { ... },
    { ... },
    { ... }
  ],
  "want_more": true
}</code></pre><p>该程序通过以下方式有效地管理了其状态：</p><ul><li>在 <code>url</code>、<code>per_page</code> 和 <code>max_pages</code> 字段中沿用了初始状态的值。</li><li>在 <code>cursor.page</code> 中添加了需要在重启后持久化的状态。</li><li>返回准备发布在 <code>events</code> 列表中的事件。</li><li>请求立即与 <code>want_more: true</code> 进行重新评估。</li></ul><p>现在您已经理解了可选访问和状态管理，以及 CEL 基础知识和 HTTP 请求，完整的 GitHub issues 示例程序应该已经比较容易阅读了。不妨用 Mito 运行一下，并尝试做些修改。</p><h2>回顾与资源</h2><p>本文介绍了 CEL 语言，以及它如何在 Mito 库中得到扩展以用于 CEL 输入。通过一个从 GitHub API 获取 issue 数据的示例程序，我们展示了 CEL 的灵活性，并逐一解析了理解该程序所需的全部细节：访问初始状态中的配置、与 HTTP API 交互、返回待导入的事件，以及为后续执行管理状态。</p><p>要深入学习并使用 CEL 输入构建集成，以下资源值得探索：</p><ul><li><a href="https://www.elastic.co/docs/reference/beats/filebeat/filebeat-input-cel">CEL 输入 － Filebeat 文档</a></li><li><a href="https://pkg.go.dev/github.com/elastic/mito">Mito 文档</a></li><li><a href="https://cel.dev/">通用表达式语言 － cel.dev 网站</a></li><li><a href="https://www.elastic.co/docs/extend/integrations">创建集成 － Elastic 文档</a></li></ul><p>对于使用 CEL 输入构建集成，最有价值的资源或许是现有 Elastic 集成中的 CEL 代码，这些代码可以在 GitHub 上找到：</p><p><a href="https://github.com/search?q=repo%3Aelastic%2Fintegrations+path%3A**%2Fcel.yml.hbs&amp;type=code"><code>cel.yml.hbs</code> Elastic 集成存储库中的 cel.yml.hbs 文件 － GitHub。</a></p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[使用 Elasticsearch 与 LLM 进行实体解析，第 2 部分：通过 LLM 判断和语义搜索匹配实体]]></title>
            <description><![CDATA[在 Elasticsearch 中使用语义搜索和透明 LLM 判断进行实体解析。]]></description>
            <link>https://www.elastic.co/search-labs/cn/blog/elasticsearch-entity-resolution-llm-semantic-search</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/cn/blog/elasticsearch-entity-resolution-llm-semantic-search</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Jessica Moszkowicz]]></dc:creator>
            <pubDate>Thu, 26 Feb 2026 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/473ca4357c7d60f690edbd2a844acda169aca9c3-720x420.jpg?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>在<a href="https://www.elastic.co/search-labs/blog/entity-resolution-llm-elasticsearch"> 第 1 部分</a>中，我们准备了观察清单并提取了实体提及。现在我们准备回答那个难题：一个提及实际指的是哪个实体？让我们回到本系列第一篇博客中的例子，该例子说明了我们为何需要实体解析：“Swift 更新来了！”想象一下，这个标题伴随着一些上下文：</p><ol><li>新的 Swift 更新来了！开发人员迫不及待地想要尝试新功能。</li><li>新的 Swift 更新来了！新专辑将于下个月发布。</li></ol><p>有了这些增加的上下文，我们应该能够将名称“Swift”解析到正确的实体。</p><p>在<a href="https://www.elastic.co/search-labs/blog/entity-resolution-llm-elasticsearch">上一篇文章</a>中，我们设置了观察清单，并用额外的上下文丰富了实体信息。看上面的例子，我们需要在清单中至少包含以下两个实体：Taylor Swift 和 Swift 编程语言。我们还介绍了如何从文本中提取实体提及。这两个例子都能提取“Swift”。有了这些要素——丰富的观察清单和提取出的实体——我们终于可以介绍本次的主角了：实体匹配。</p><p><strong>请记住：</strong>这是一个教育原型，旨在教授实体匹配概念。生产系统可能会使用不同的大型语言模型 (LLM)、自定义匹配规则、专门的判断管道或结合多种匹配策略的集成方法。</p><h2>问题：为何匹配如此困难</h2><p>人类语言是一种非凡的事物。它最有趣的特性之一是其无限的创造力。我们可以生成并理解无数的新句子。那么，在实体解析中完全精确的匹配极为罕见，这还奇怪吗？作者们在可能的情况下都力求创新。如果每次提到某个实体时，我们都必须书写和阅读完整名称，那将会变得非常乏味。因此，尽管精确匹配很简单，但现实情况是我们需要一种更复杂的方法来进行实体解析：这种方法必须足够强大，以处理人类作者无限创造力中的至少一部分挑战。这就是我们将问题分解为两个步骤的原因：使用 Elasticsearch 大规模检索可能的候选实体，然后使用 LLM 来判断这些候选实体是否真正指向同一个现实世界中的实体。</p><h2>解决方案：三步匹配与透明的 LLM 判断</h2><p>我们正处于使用计算机方式的范式转变之中。正如互联网的兴起将我们从本地计算带入全球互联网络一样，生成式 AI (GenAI) 正在从根本上改变内容、代码和信息的创建方式。事实上，伴随本系列的教育型原型几乎完全是作者通过精心设计提示词，使用 LLM “vibe coded” 出来的。这并不是说 LLM 已经或将要达到人类语言所固有的那种生产力，但这确实意味着我们现在拥有一个强大的资源来帮助进行实体解析。</p><p>我们在使用生成式 AI 时的一个常见模式是检索增强生成 (RAG)。在这里，<em>检索</em>检索意味着检索实体候选（而不是生成答案），LLM 严格用于匹配评估和解释。虽然我们<em>可以</em>要求 LLM 帮助我们进行端到端的实体解析，但这在时间和金钱上都是一种成本高昂的方法。RAG 通过使用更高效的方式为 LLM 提供上下文，从而帮助 LLM 完成工作，进而使 LLM 能够有效地协助实体解析。</p><p>对于 RAG 中的检索部分，我们再次求助于 Elasticsearch。我们首先使用精确匹配、别名匹配以及结合了关键词和语义搜索的混合搜索来寻找潜在的匹配项。一旦找到这些潜在匹配项，我们就将它们发送给 LLM 进行判断。LLM 充当最终的匹配评估器。我们还让 LLM 解释其推理过程，这是与其他实体解析系统的一个重要区别。没有这些解释，实体解析就是一个黑匣子；有了它们，我们可以亲眼看到为什么某个匹配是合理的。</p><h2>关键概念：三步匹配、混合搜索和透明 LLM 判断</h2><p><strong>什么是三步匹配？</strong>在项目开始时，我们假设语义搜索将是系统的一个关键部分，但并非每个匹配都需要如此复杂的搜索。为了有效地找到匹配项，我们采用了渐进式的方法。首先，我们使用关键词搜索检查完全精确的匹配。如果找到这样的匹配，我们的工作就完成了，可以继续下一个。如果精确匹配失败，我们转向别名匹配。为简化起见，在原型中，别名匹配也是使用关键词进行精确匹配完成的。在生产环境中，您可能会通过标准化、音译规则、模糊匹配或精心管理的别名表来扩展这一步。如果在前两步之后仍未找到潜在的匹配项，那么是时候通过 Elasticsearch 的混合搜索（结合了倒数排序融合）来引入语义搜索了。</p><p><strong>什么是混合搜索？</strong>在 Elasticsearch 中，我们可以使用语义搜索来找到将上下文考虑在内的有意义的匹配。Elasticsearch 广泛用于向量搜索和混合检索。语义相似性对于理解含义非常强大，但它不能替代结构化过滤（例如，按时间范围、位置或标识符过滤），并且在存在精确匹配时通常是不必要的。Elasticsearch 以其词汇搜索而闻名，这在不适合语义搜索的任务中表现出色。为了充分利用这两种方法，我们在单个混合查询中将词汇搜索与语义搜索结合使用。然后，我们使用 RRF 合并结果，以找到最可能的匹配项。在原型中，排名前两位的结果成为可以发送给 LLM 进行判断的潜在匹配项。</p><p><strong>为什么需要 LLM 判断？</strong>LLM 的判断和解释使得我们的系统能够透明地处理歧义和上下文。这对于像“the president”这样可能根据上下文指代多个实体的情况至关重要，但它也使昵称和文化差异等情况在系统中能够很好地处理。最后，当我们考虑关键任务，例如识别制裁名单中的实体时，我们需要知道匹配被接受的原因，才能信任该系统。至关重要的是，LLM 并不搜索整个语料库；它只评估 Elasticsearch 返回的那一小部分候选集。</p><h2>实际结果：通过 LLM 推理进行匹配</h2><p>任何自然语言处理任务的一个主要挑战是创建一份黄金文档，一份告诉我们预期结果是什么的“答案”。没有它，几乎不可能判断一个系统在某个任务上表现如何，但创建这样一份文档可能是一个费力且费时的过程。对于实体解析原型，我们再次求助于生成式 AI 来帮助建立我们可以用来测试的数据。</p><p>我们首先定义了几种挑战类型，例如昵称和音译，然后要求 LLM 创建一个分层的数据集集合，这些数据集将逐渐变大，对系统来说也更具挑战性。数据集的创建并不像人们希望的那样简单。LLM 有一种强烈的“作弊”倾向，使得获取正确答案变得过于容易。例如，其中一种挑战类型侧重于语义上下文。这种类型包括将“Russian author”解读为“Leo Tolstoy”。LLM 错误地将“Russian author”作为“Leo Tolstoy”的一个别名，这就没有必要通过混合搜索来寻找匹配项了。</p><p>在进行了几次重构以修复此类问题后，我们有了五个可供使用的数据集层级。第 1-4 层规模逐渐增大，包含的挑战类型也更多。第 5 层是“终极挑战”数据集，由所有挑战类型中最棘手的例子组成。所有测试数据都可以在<a href="https://github.com/jesslm/entity-resolution-lab-public/tree/main/comprehensive_evaluation">全面评估目录</a>中找到。</p><p>为了评估我们基于提示的实体解析方法，我们将注意力集中在第 4 层数据集上。一个重要的说明是，评估是作为受控实验进行的，这样我们可以专注于实体匹配的质量。观察清单数据预先丰富了上下文，并且实体是提前从文章中提取出来的。这确保了评估的重点是匹配而非提取的准确性。这将匹配质量孤立出来；端到端的性能还将额外取决于提取的召回率和丰富数据的质量。</p><h3>评估数据集</h3><p>第 4 层评估数据集对系统的能力提供了一个全面的测试：[1]</p><ul><li><strong>观察清单实体：</strong>跨不同类型（人物、组织、地点）的 66 个实体。</li><li><strong>测试文章：</strong>69 篇涵盖现实世界实体解析场景的文章。</li><li><strong>预期匹配：</strong>所有文章中预期的 206 个实体匹配。</li><li><strong>挑战类型：</strong>测试实体解析各个方面的 15 种不同挑战类型。</li></ul><p>数据集中包含的挑战类型有：</p><ul><li><strong>昵称：</strong>“Bob Smith” → “Robert Smith”（七篇文章）。</li><li><strong>头衔和尊称：</strong> “Dr. Sarah Williams” → “Sarah Williams”（五篇文章）。</li><li><strong>语义上下文：</strong>“Russian author” → “Leo Tolstoy”（八篇文章）。</li><li><strong>多语言名字：</strong>处理不同书写系统中的名称（六篇文章）。</li><li><strong>商业实体：</strong> 公司名称变体（七篇文章）。</li><li><strong>高管引用：</strong>“Microsoft CEO”→“Satya Nadella”（五篇文章）。</li><li><strong>政治领导人：</strong>基于头衔的引用（五篇文章）。</li><li><strong>名称首字母：</strong>“J.Smith” → “John Smith”（三篇文章）。</li><li><strong>名称顺序变体：</strong>不同的名称顺序惯例（三篇文章）。</li><li><strong>名称截断：</strong>部分名称匹配（三篇文章）。</li><li><strong>名称拆分：</strong>名称在文本中拆分（三篇文章）。</li><li><strong>缺少空格/连字符：</strong>格式变体（两篇文章）。</li><li><strong>音译：</strong>跨书写系统的名称匹配（两篇文章）。</li><li><strong>组合挑战：</strong>一篇文章中包含多个挑战（共六篇文章）。</li><li><strong>复杂商业关系：</strong>分层商业关系（五篇文章）。</li></ul><p>让我们看看基于提示的实体解析表现如何。</p><h3>整体性能</h3><p>结果显示，由 LLM 驱动的匹配评估前景广阔，但也揭示了一个显著的可靠性问题。因为每个候选对都必须由 LLM 进行评估，结构化输出的失败可能会抑制接受率和召回率，即使检索环节工作正常。</p>
            <table style="border-collapse: collapse; margin: 20px 0;">
              <tbody>
                
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">指标</td><td style="border: 1px solid #ccc; padding: 4px;">值</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">精确率</td><td style="border: 1px solid #ccc; padding: 4px;">83.8%</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">召回</td><td style="border: 1px solid #ccc; padding: 4px;">62.6%</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">F1 分数</td><td style="border: 1px solid #ccc; padding: 4px;">71.7%</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">找到的总匹配数</td><td style="border: 1px solid #ccc; padding: 4px;">344</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">LLM 接受率</td><td style="border: 1px solid #ccc; padding: 4px;">44.8%</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">错误率</td><td style="border: 1px solid #ccc; padding: 4px;">30.2%</td>
                    </tr>
              </tbody>
            </table>
          <h3>错误率问题</h3><p>回顾一下，我们在原型中采取的第一步是使用 Elasticsearch 创建潜在的匹配对。每个这样的潜在匹配都需要由 LLM 进行评估。为了高效地处理所有这些匹配项，我们将 LLM 调用批量组合在一起。这降低了 API 成本和延迟，但也增加了在输出中得到格式错误 JSON 的风险。随着批量大小的增加，JSON 变得更长、更复杂，使得 LLM 更有可能生成无效的 JSON。这就是 30% 错误率的来源。在评估中，我们每个请求使用 5 个匹配项的批量大小。即使采用这个保守的批量大小，我们仍然遇到 JSON 解析失败的情况，这显著地影响了评估结果。</p><h2>下一步：优化 LLM 集成</h2><p>现在，我们已经使用语义搜索和 LLM 判断匹配了实体，我们拥有了一个完整的实体解析管道。然而，这种方法引入了一种新的故障模式：当模型的判断正确，但其输出却不可用时。我们可以优化 LLM 集成以获得更好的可靠性和成本效益。在下一篇文章中，我们将探讨如何使用函数调用来实现结构化输出，这可以在减少错误和成本的同时，提供有保障的结构和类型安全。</p><h2>亲自试用</h2><p>想亲眼看看实体匹配是如何运作的吗？请查看<a href="https://github.com/jesslm/entity-resolution-lab-public/tree/main/notebooks#:~:text=5%20minutes%20ago-,03_entity_matching_v3.ipynb,-Initial%20public%20lab">实体匹配笔记本</a>，它通过实际实现、详细解释和动手示例，提供了完整的实践指南。该笔记本精确地向您展示了如何使用三步搜索、带有 RRF 的混合搜索以及由 LLM 驱动的带推理的判断来匹配实体。</p><p><strong>请记住：</strong>这是一个教育原型，旨在教授这些概念。在构建生产系统时，需要考虑额外的因素，如模型选择、成本优化、延迟要求、质量验证、错误处理和监控等，而这些在本学习重点的原型中并未涵盖。</p><h2>备注</h2><ol><li>这些数据集是合成的，专为教育目的设计；它们模拟了真实的挑战，但不代表任何单一的生产环境领域。</li></ol>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Elasticsearch 向量搜索速度比 OpenSearch 快 8 倍]]></title>
            <description><![CDATA[探索 OpenSearch 与 Elasticsearch 的过滤向量搜索基准测试，以及为何向量搜索性能对上下文工程系统至关重要。]]></description>
            <link>https://www.elastic.co/search-labs/cn/blog/opensearch-vs-elasticsearch-filtered-vector-search</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/cn/blog/opensearch-vs-elasticsearch-filtered-vector-search</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Sachin Frayne]]></dc:creator>
            <pubDate>Wed, 25 Feb 2026 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/fb7ee623925ca6696d643e437ce8efe5fe749079-1280x720.png?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<h2>为什么搜索速度对 AI 智能体和上下文工程很重要</h2><p>我们在 2000 万文档语料库上进行的基准测试显示，Elasticsearch 在过滤向量搜索方面的吞吐量比 OpenSearch 高达 8 倍，同时在我们测试的配置中也实现了更高的 Recall@100。上下文工程不仅仅依赖快速的向量检索。随着工作流的迭代，团队还需要强大的相关性控制（如混合搜索和过滤）、操作简便性和可预测的性能。但是，由于智能体通常会在每个请求中多次运行检索、推理、检索循环，因此检索延迟会成倍增加，所以这方面的改进会直接转化为更好的端到端响应能力和更低的成本。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/d5a52a07773f0942c2baa732dacfe782aac0f415-1600x683.png" style="width: 100%;" /><p>对于上下文工程来说，检索不是一次性的步骤。智能体和应用程序会反复运行循环，例如检索→推理→检索，以完善查询、验证事实、组合基础上下文并完成任务。这种模式在智能体工作流和迭代检索增强生成 (RAG) 中很常见。由于每个用户请求可能会多次调用检索，这会增加响应延迟和/或增加基础设施成本。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/54ac476ff20a3cf93484298c9ae47612c12fc110-800x417.png" style="width: 100%;" /><h2>为什么向量搜索性能至关重要？</h2><p></p><p>想象一个购物助手回答以下问题：“我需要一个价格在 60 美元以下的随身背包，它可以容纳一台 15 英寸的笔记本电脑、防水，并且可以在周五之前送达。”</p><p>在生产环境中，助手很少只发出一个向量查询然后停止。它会运行一个检索循环以构建正确的上下文，并且每一步通常会受到过滤条件的限制，如可用性、地区、发货承诺、品牌规则和政策资格。</p><p><strong>第 1 步：解读意图，并转化为约束条件。</strong></p><p>智能体可将请求转化为结构化的过滤条件和语义查询，例如：</p><ul><li>过滤条件：有现货，可配送至用户邮编，可在周五前送达，价格低于 60 美元，有效上架</li><li>向量查询：“随身背包15英寸笔记本电脑防水”</li></ul><p><strong>步骤 2：检索候选对象，然后进行细化。</strong></p><p>它通常会重复检索，但会有所变化，以避免遗漏好的匹配结果：</p><ul><li>“旅行背包随身便携笔记本电脑保护套”</li><li>“15 英寸防水通勤背包”</li><li>“轻型机舱背包”</li></ul><p>每个查询都使用相同的资格过滤条件，因为检索无关或不可用的项目会造成上下文的浪费。</p><p><strong>步骤 3：展开以确认详细信息并降低风险。</strong></p><p>代理随后再次检索以验证影响最终答案的关键属性：</p><ul><li>材料与防水性表述</li><li>尺寸和笔记本隔层都适合</li><li>退货政策或保修限制</li><li>库存不足时的替代方案</li></ul><p>这就是多步上下文工程：检索、推理、检索、组合。</p><h2>延迟与召回为何对上下文工程至关重要</h2><p>这些交互可能涉及每个用户会话中数十次过滤的检索调用。这使得每次调用的延迟成为端到端响应时间的直接倍增因素，而低召回率则迫使进行额外的重试或导致智能体错过符合条件的项目，导致答案质量下降。</p><p>要点：在上下文工程系统中，过滤近似最近邻 (ANN) 并非一次单一查找。由于这是在约束条件下的重复操作，因此即使大型语言模型 (LLM) 是最明显的组件，向量搜索性能也会立即体现在延迟、吞吐量和成本上。</p><h2>基准测试</h2><h3>成果度</h3><p>在图表 2 中，每个点代表一个测试配置。最佳结果出现在左上方，这意味着更高的召回率和更低的延迟。Elasticsearch 的结果始终比 OpenSearch 更接近左上角，表明在相同的工作负载配置下，Elasticsearch 具有更好的速度和准确率。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/c50d1df00968cac18149a2799e6242fbe49b66a0-1600x990.png" style="width: 100%;" /><h4>一些关键见解</h4><ul><li><code>s_n_r_value</code>: <code>size_numCandidates_rescoreOversample</code> 的简写（在这些测试中，k 和 numCandidates 设置为等于 numCandidates），例如，<code>100_500_1</code> 表示 size=100、numCandidates=500 和 k=500，重新评分过采样=1。</li><li>召回率：该配置的测量召回率@100</li><li>平均延迟（毫秒）：每次查询的平均端到端延迟</li><li>吞吐量：每秒查询次数</li><li>召回率 (%)：Elasticsearch 相较于 OpenSearch 的相对召回率提升 (Elasticsearch - OpenSearch) / OpenSearch</li><li>延迟 Xs：OpenSearch 平均延迟除以 Elasticsearch 平均延迟</li><li>吞吐量 Xs：Elasticsearch 吞吐量除以 OpenSearch 吞吐量</li></ul>
            <table style="border-collapse: collapse; margin: 20px 0;">
              <tbody>
                
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">引擎</td><td style="border: 1px solid #ccc; padding: 4px;">`s_n_r_value`</td><td style="border: 1px solid #ccc; padding: 4px;">召回</td><td style="border: 1px solid #ccc; padding: 4px;">平均延迟（ms）</td><td style="border: 1px solid #ccc; padding: 4px;">吞吐量</td><td style="border: 1px solid #ccc; padding: 4px;">召回率%</td><td style="border: 1px solid #ccc; padding: 4px;">延迟 Xs</td><td style="border: 1px solid #ccc; padding: 4px;">吞吐量 Xs</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">Elasticsearch</td><td style="border: 1px solid #ccc; padding: 4px;">100_250_1</td><td style="border: 1px solid #ccc; padding: 4px;">0.7704</td><td style="border: 1px solid #ccc; padding: 4px;">25</td><td style="border: 1px solid #ccc; padding: 4px;">534.75</td><td style="border: 1px solid #ccc; padding: 4px;">9.70%</td><td style="border: 1px solid #ccc; padding: 4px;">2.28</td><td style="border: 1px solid #ccc; padding: 4px;">1.91</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">OpenSearch</td><td style="border: 1px solid #ccc; padding: 4px;">100_250_1</td><td style="border: 1px solid #ccc; padding: 4px;">0.7023</td><td style="border: 1px solid #ccc; padding: 4px;">57.08</td><td style="border: 1px solid #ccc; padding: 4px;">279.58</td><td style="border: 1px solid #ccc; padding: 4px;"></td><td style="border: 1px solid #ccc; padding: 4px;"></td><td style="border: 1px solid #ccc; padding: 4px;"></td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">Elasticsearch</td><td style="border: 1px solid #ccc; padding: 4px;">100_500_1</td><td style="border: 1px solid #ccc; padding: 4px;">0.8577</td><td style="border: 1px solid #ccc; padding: 4px;">25.42</td><td style="border: 1px solid #ccc; padding: 4px;">524.14</td><td style="border: 1px solid #ccc; padding: 4px;">7.20%</td><td style="border: 1px solid #ccc; padding: 4px;">2.4</td><td style="border: 1px solid #ccc; padding: 4px;">2</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">OpenSearch</td><td style="border: 1px solid #ccc; padding: 4px;">100_500_1</td><td style="border: 1px solid #ccc; padding: 4px;">0.8001</td><td style="border: 1px solid #ccc; padding: 4px;">60.9</td><td style="border: 1px solid #ccc; padding: 4px;">262.12</td><td style="border: 1px solid #ccc; padding: 4px;"></td><td style="border: 1px solid #ccc; padding: 4px;"></td><td style="border: 1px solid #ccc; padding: 4px;"></td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">Elasticsearch</td><td style="border: 1px solid #ccc; padding: 4px;">100_750_1</td><td style="border: 1px solid #ccc; padding: 4px;">0.8947</td><td style="border: 1px solid #ccc; padding: 4px;">29.67</td><td style="border: 1px solid #ccc; padding: 4px;">528.09</td><td style="border: 1px solid #ccc; padding: 4px;">5.72%</td><td style="border: 1px solid #ccc; padding: 4px;">2.25</td><td style="border: 1px solid #ccc; padding: 4px;">2.21</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">OpenSearch</td><td style="border: 1px solid #ccc; padding: 4px;">100_750_1</td><td style="border: 1px solid #ccc; padding: 4px;">0.8463</td><td style="border: 1px solid #ccc; padding: 4px;">66.76</td><td style="border: 1px solid #ccc; padding: 4px;">239.11</td><td style="border: 1px solid #ccc; padding: 4px;"></td><td style="border: 1px solid #ccc; padding: 4px;"></td><td style="border: 1px solid #ccc; padding: 4px;"></td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">Elasticsearch</td><td style="border: 1px solid #ccc; padding: 4px;">100_1000_1</td><td style="border: 1px solid #ccc; padding: 4px;">0.9156</td><td style="border: 1px solid #ccc; padding: 4px;">29.65</td><td style="border: 1px solid #ccc; padding: 4px;">534.5</td><td style="border: 1px solid #ccc; padding: 4px;">4.66%</td><td style="border: 1px solid #ccc; padding: 4px;">2.46</td><td style="border: 1px solid #ccc; padding: 4px;">2.44</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">OpenSearch</td><td style="border: 1px solid #ccc; padding: 4px;">100_1000_1</td><td style="border: 1px solid #ccc; padding: 4px;">0.8748</td><td style="border: 1px solid #ccc; padding: 4px;">72.88</td><td style="border: 1px solid #ccc; padding: 4px;">219.01</td><td style="border: 1px solid #ccc; padding: 4px;"></td><td style="border: 1px solid #ccc; padding: 4px;"></td><td style="border: 1px solid #ccc; padding: 4px;"></td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">Elasticsearch</td><td style="border: 1px solid #ccc; padding: 4px;">100_1500_1</td><td style="border: 1px solid #ccc; padding: 4px;">0.9386</td><td style="border: 1px solid #ccc; padding: 4px;">31.84</td><td style="border: 1px solid #ccc; padding: 4px;">497.3</td><td style="border: 1px solid #ccc; padding: 4px;">3.38%</td><td style="border: 1px solid #ccc; padding: 4px;">2.71</td><td style="border: 1px solid #ccc; padding: 4px;">2.68</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">OpenSearch</td><td style="border: 1px solid #ccc; padding: 4px;">100_1500_1</td><td style="border: 1px solid #ccc; padding: 4px;">0.9079</td><td style="border: 1px solid #ccc; padding: 4px;">86.16</td><td style="border: 1px solid #ccc; padding: 4px;">185.4</td><td style="border: 1px solid #ccc; padding: 4px;"></td><td style="border: 1px solid #ccc; padding: 4px;"></td><td style="border: 1px solid #ccc; padding: 4px;"></td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">Elasticsearch</td><td style="border: 1px solid #ccc; padding: 4px;">100_2000_1</td><td style="border: 1px solid #ccc; padding: 4px;">0.9507</td><td style="border: 1px solid #ccc; padding: 4px;">34.69</td><td style="border: 1px solid #ccc; padding: 4px;">457.2</td><td style="border: 1px solid #ccc; padding: 4px;">2.57%</td><td style="border: 1px solid #ccc; padding: 4px;">2.98</td><td style="border: 1px solid #ccc; padding: 4px;">2.96</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">OpenSearch</td><td style="border: 1px solid #ccc; padding: 4px;">100_2000_1</td><td style="border: 1px solid #ccc; padding: 4px;">0.9269</td><td style="border: 1px solid #ccc; padding: 4px;">103.36</td><td style="border: 1px solid #ccc; padding: 4px;">154.55</td><td style="border: 1px solid #ccc; padding: 4px;"></td><td style="border: 1px solid #ccc; padding: 4px;"></td><td style="border: 1px solid #ccc; padding: 4px;"></td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">Elasticsearch</td><td style="border: 1px solid #ccc; padding: 4px;">100_2500_1</td><td style="border: 1px solid #ccc; padding: 4px;">0.9582</td><td style="border: 1px solid #ccc; padding: 4px;">37.9</td><td style="border: 1px solid #ccc; padding: 4px;">418.43</td><td style="border: 1px solid #ccc; padding: 4px;">1.99%</td><td style="border: 1px solid #ccc; padding: 4px;">3.28</td><td style="border: 1px solid #ccc; padding: 4px;">3.26</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">OpenSearch</td><td style="border: 1px solid #ccc; padding: 4px;">100_2500_1</td><td style="border: 1px solid #ccc; padding: 4px;">0.9395</td><td style="border: 1px solid #ccc; padding: 4px;">124.29</td><td style="border: 1px solid #ccc; padding: 4px;">128.53</td><td style="border: 1px solid #ccc; padding: 4px;"></td><td style="border: 1px solid #ccc; padding: 4px;"></td><td style="border: 1px solid #ccc; padding: 4px;"></td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">Elasticsearch</td><td style="border: 1px solid #ccc; padding: 4px;">100_3000_1</td><td style="border: 1px solid #ccc; padding: 4px;">0.9636</td><td style="border: 1px solid #ccc; padding: 4px;">41.86</td><td style="border: 1px solid #ccc; padding: 4px;">379.4</td><td style="border: 1px solid #ccc; padding: 4px;">1.62%</td><td style="border: 1px solid #ccc; padding: 4px;">3.46</td><td style="border: 1px solid #ccc; padding: 4px;">3.44</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">OpenSearch</td><td style="border: 1px solid #ccc; padding: 4px;">100_3000_1</td><td style="border: 1px solid #ccc; padding: 4px;">0.9482</td><td style="border: 1px solid #ccc; padding: 4px;">144.67</td><td style="border: 1px solid #ccc; padding: 4px;">110.34</td><td style="border: 1px solid #ccc; padding: 4px;"></td><td style="border: 1px solid #ccc; padding: 4px;"></td><td style="border: 1px solid #ccc; padding: 4px;"></td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">Elasticsearch</td><td style="border: 1px solid #ccc; padding: 4px;">100_4000_1</td><td style="border: 1px solid #ccc; padding: 4px;">0.9705</td><td style="border: 1px solid #ccc; padding: 4px;">50.28</td><td style="border: 1px solid #ccc; padding: 4px;">316.21</td><td style="border: 1px solid #ccc; padding: 4px;">1.06%</td><td style="border: 1px solid #ccc; padding: 4px;">3.87</td><td style="border: 1px solid #ccc; padding: 4px;">3.85</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">OpenSearch</td><td style="border: 1px solid #ccc; padding: 4px;">100_4000_1</td><td style="border: 1px solid #ccc; padding: 4px;">0.9603</td><td style="border: 1px solid #ccc; padding: 4px;">194.36</td><td style="border: 1px solid #ccc; padding: 4px;">82.22</td><td style="border: 1px solid #ccc; padding: 4px;"></td><td style="border: 1px solid #ccc; padding: 4px;"></td><td style="border: 1px solid #ccc; padding: 4px;"></td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">Elasticsearch</td><td style="border: 1px solid #ccc; padding: 4px;">100_5000_1</td><td style="border: 1px solid #ccc; padding: 4px;">0.9749</td><td style="border: 1px solid #ccc; padding: 4px;">58.77</td><td style="border: 1px solid #ccc; padding: 4px;">270.91</td><td style="border: 1px solid #ccc; padding: 4px;">0.73%</td><td style="border: 1px solid #ccc; padding: 4px;">4.43</td><td style="border: 1px solid #ccc; padding: 4px;">4.41</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">OpenSearch</td><td style="border: 1px solid #ccc; padding: 4px;">100_5000_1</td><td style="border: 1px solid #ccc; padding: 4px;">0.9678</td><td style="border: 1px solid #ccc; padding: 4px;">260.33</td><td style="border: 1px solid #ccc; padding: 4px;">61.38</td><td style="border: 1px solid #ccc; padding: 4px;"></td><td style="border: 1px solid #ccc; padding: 4px;"></td><td style="border: 1px solid #ccc; padding: 4px;"></td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">Elasticsearch</td><td style="border: 1px solid #ccc; padding: 4px;">100_6000_1</td><td style="border: 1px solid #ccc; padding: 4px;">0.9781</td><td style="border: 1px solid #ccc; padding: 4px;">66.75</td><td style="border: 1px solid #ccc; padding: 4px;">238.59</td><td style="border: 1px solid #ccc; padding: 4px;">0.52%</td><td style="border: 1px solid #ccc; padding: 4px;">4.91</td><td style="border: 1px solid #ccc; padding: 4px;">4.89</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">OpenSearch</td><td style="border: 1px solid #ccc; padding: 4px;">100_6000_1</td><td style="border: 1px solid #ccc; padding: 4px;">0.973</td><td style="border: 1px solid #ccc; padding: 4px;">327.44</td><td style="border: 1px solid #ccc; padding: 4px;">48.81</td><td style="border: 1px solid #ccc; padding: 4px;"></td><td style="border: 1px solid #ccc; padding: 4px;"></td><td style="border: 1px solid #ccc; padding: 4px;"></td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">Elasticsearch</td><td style="border: 1px solid #ccc; padding: 4px;">100_7000_1</td><td style="border: 1px solid #ccc; padding: 4px;">0.9804</td><td style="border: 1px solid #ccc; padding: 4px;">74.64</td><td style="border: 1px solid #ccc; padding: 4px;">213.49</td><td style="border: 1px solid #ccc; padding: 4px;">0.38%</td><td style="border: 1px solid #ccc; padding: 4px;">5.28</td><td style="border: 1px solid #ccc; padding: 4px;">5.27</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">OpenSearch</td><td style="border: 1px solid #ccc; padding: 4px;">100_7000_1</td><td style="border: 1px solid #ccc; padding: 4px;">0.9767</td><td style="border: 1px solid #ccc; padding: 4px;">394.24</td><td style="border: 1px solid #ccc; padding: 4px;">40.53</td><td style="border: 1px solid #ccc; padding: 4px;"></td><td style="border: 1px solid #ccc; padding: 4px;"></td><td style="border: 1px solid #ccc; padding: 4px;"></td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">Elasticsearch</td><td style="border: 1px solid #ccc; padding: 4px;">100_8000_1</td><td style="border: 1px solid #ccc; padding: 4px;">0.9823</td><td style="border: 1px solid #ccc; padding: 4px;">82.28</td><td style="border: 1px solid #ccc; padding: 4px;">193.59</td><td style="border: 1px solid #ccc; padding: 4px;">0.27%</td><td style="border: 1px solid #ccc; padding: 4px;">6.86</td><td style="border: 1px solid #ccc; padding: 4px;">6.83</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">OpenSearch</td><td style="border: 1px solid #ccc; padding: 4px;">100_8000_1</td><td style="border: 1px solid #ccc; padding: 4px;">0.9797</td><td style="border: 1px solid #ccc; padding: 4px;">564.14</td><td style="border: 1px solid #ccc; padding: 4px;">28.33</td><td style="border: 1px solid #ccc; padding: 4px;"></td><td style="border: 1px solid #ccc; padding: 4px;"></td><td style="border: 1px solid #ccc; padding: 4px;"></td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">Elasticsearch</td><td style="border: 1px solid #ccc; padding: 4px;">100_9000_1</td><td style="border: 1px solid #ccc; padding: 4px;">0.9837</td><td style="border: 1px solid #ccc; padding: 4px;">90.08</td><td style="border: 1px solid #ccc; padding: 4px;">176.96</td><td style="border: 1px solid #ccc; padding: 4px;">0.16%</td><td style="border: 1px solid #ccc; padding: 4px;">7.63</td><td style="border: 1px solid #ccc; padding: 4px;">7.61</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">OpenSearch</td><td style="border: 1px solid #ccc; padding: 4px;">100_9000_1</td><td style="border: 1px solid #ccc; padding: 4px;">0.9821</td><td style="border: 1px solid #ccc; padding: 4px;">687.25</td><td style="border: 1px solid #ccc; padding: 4px;">23.25</td><td style="border: 1px solid #ccc; padding: 4px;"></td><td style="border: 1px solid #ccc; padding: 4px;"></td><td style="border: 1px solid #ccc; padding: 4px;"></td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">Elasticsearch</td><td style="border: 1px solid #ccc; padding: 4px;">100_10000_1</td><td style="border: 1px solid #ccc; padding: 4px;">0.9848</td><td style="border: 1px solid #ccc; padding: 4px;">97.64</td><td style="border: 1px solid #ccc; padding: 4px;">163.31</td><td style="border: 1px solid #ccc; padding: 4px;">0.08%</td><td style="border: 1px solid #ccc; padding: 4px;">8.38</td><td style="border: 1px solid #ccc; padding: 4px;">8.36</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">OpenSearch</td><td style="border: 1px solid #ccc; padding: 4px;">100_10000_1</td><td style="border: 1px solid #ccc; padding: 4px;">0.984</td><td style="border: 1px solid #ccc; padding: 4px;">818.64</td><td style="border: 1px solid #ccc; padding: 4px;">19.53</td><td style="border: 1px solid #ccc; padding: 4px;"></td><td style="border: 1px solid #ccc; padding: 4px;"></td><td style="border: 1px solid #ccc; padding: 4px;"></td>
                    </tr>
              </tbody>
            </table>
          <p>例如，在 <code>100_9000_1</code> 处，OpenSearch 每次检索平均为 687 毫秒， Elasticsearch 为 90 毫秒，而在 10 步检索循环中，等待时间约为 10 x (687 - 90) = 6 秒。 </p><p>查看<a href="https://github.com/elastic/competitive-benchmarking-studies/tree/main/es-9.3-vs-os-3.5-vector-search/jingra/results/20260220">完整结果</a>。</p><h3>方法</h3><p>我们使用 Python 发送查询并跟踪响应时间及其他统计数据，向引擎发送了以下查询。请记住，任何向量搜索引擎的性能取决于您如何调整其核心参数：考虑多少个候选项，重新评分的程度，以及返回多少上下文。这些设置直接影响召回率（找到正确答案的可能性）和延迟（获得结果的速度）。</p><p>在我们的基准测试中，我们使用了通常在智能体检索循环中调整的候选对象、重新评分和结果大小设置，并测量了 Elasticsearch 在该工作负载下的表现。然后我们以相同的设置运行了 OpenSearch 作为参考。</p><p>OpenSearch</p><pre><code>GET <INDEX_NAME>/_search
{
  "query": {
    "knn": {
      "<DENSE_VECTOR_FIELD_NAME>": {
        "vector": [...],
        "k": <NUMBER_OF_CANDIDATES>,
        "method_parameters": {
          "ef_search": <NUMBER_OF_CANDIDATES>
        },
        "rescore": {
          "oversample_factor": <OVERSAMPLE>
        },
        "filter": {
          <SOME_FILTER>
        }
      }
    }
  },
  "size": <RESULT_SIZE>,
  "_source": {
    "excludes": [
      "<DENSE_VECTOR_FIELD_NAME>"
    ]
  }
}</code></pre><ul><li><code>&quot;size&quot;: &lt;RESULT_SIZE&gt;</code>：返回给客户端的命中次数。在这个基准测试中，计算 Recall@100 的结果大小为 100。</li><li><code>&quot;k&quot;: &lt;NUMBER_OF_CANDIDATES&gt;</code>：最近邻候选对象的数量。</li><li><code>&quot;ef_search&quot;: &lt;NUMBER_OF_CANDIDATES&gt;</code>：要检查的向量数量。</li><li><code>&quot;oversample_factor&quot;: &lt;OVERSAMPLE&gt;</code>：在重新评分之前检索了多少个候选向量。</li></ul><p>Elasticsearch</p><pre><code>GET <INDEX_NAME>/_search
{
  "query": {
    "knn": {
      "field": "<DENSE_VECTOR_FIELD_NAME>",
      "query_vector": [...],
      "k": <NUMBER_OF_CANDIDATES>,
      "num_candidates": <NUMBER_OF_CANDIDATES>,
      "rescore_vector": {
        "oversample": <OVERSAMPLE>
      },
      "filter": {
        <SOME_FILTER>
      }
    }
  },
  "size": <RESULT_SIZE>,
  "_source": {
    "excludes": [
      "<DENSE_VECTOR_FIELD_NAME>"
    ]
  }
}</code></pre><ul><li><code>&quot;size&quot;: &lt;RESULT_SIZE&gt;</code>：返回给客户端的命中次数。在这个基准测试中，计算 Recall@100 的结果大小为 100。</li><li><code>&quot;k&quot;: &lt;NUMBER_OF_CANDIDATES&gt;</code>：从每个分片返回的最近邻数量。</li><li><code>&quot;num_candidates&quot;: &lt;NUMBER_OF_CANDIDATES&gt;</code>：进行 <code>knn</code> 搜索时每个分片要考虑的最近邻候选数目。</li><li><code>&quot;oversample&quot;: &lt;OVERSAMPLE&gt;</code>：在重新评分之前检索了多少个候选向量。</li></ul><p>示例</p><p><code>Knn</code> 查询, (<code>100_500_1</code>)，将如下所示：</p><p>OpenSearch</p><pre><code>GET search_catalog_128/_search
{
  "query": {
    "knn": {
      "search_catalog_embedding": {
        "vector": [...],
        "k": 500,
        "method_parameters": {
          "ef_search": 500
        },
        "rescore": {
          "oversample_factor": 1
        },
        "filter": {
          "term": {
            "valid": true
          }
        }
      }
    }
  },
  "size": 100,
  "_source": {
    "excludes": [
      "search_catalog_embedding"
    ]
  }
}</code></pre><p>Elasticsearch</p><pre><code>GET search_catalog_128/_search
{
  "query": {
    "knn": {
      "field": "search_catalog_embedding",
      "query_vector": [...],
      "k": 500,
      "num_candidates": 500,
      "rescore_vector": {
        "oversample": 1
      },
      "filter": {
        "term": {
          "valid": true
        }
      }
    }
  },
  "size": 100,
  "_source": {
    "excludes": [
      "search_catalog_embedding"
    ]
  }
}</code></pre><p>完整配置、Terraform 脚本、Kubernetes 清单和基准测试代码均可在此<a href="https://github.com/elastic/competitive-benchmarking-studies">存储库</a>的 <a href="https://github.com/elastic/competitive-benchmarking-studies/tree/main/es-9.3-vs-os-3.5-vector-search">es-9.3-vs-os-3.5-vector-search</a> 文件夹中找到。</p><h3>集群设置</h3><p>我们在六台 e2-standard-16 云服务器上运行了测试，每台服务器配备 16 个 vCPU 和 64 GB 内存。在每台服务器上，我们为每个运行搜索引擎节点的 Kubernetes pod 分配了 15 个 vCPU 和 56 GB RAM，其中 28 GB 保留给 JVM 堆。</p><p>这些集群运行 Elasticsearch 9.3.0 和 OpenSearch 3.5.0 (Lucene 10.3.2)。由于在此基准测试中两个系统使用相同的 Lucene 版本，我们观察到的吞吐量和延迟差异不能单独归因于 Lucene，而是反映了每个引擎如何集成和执行过滤后的 k 最近邻 (kNN) 检索和重新评分的差异。我们使用了一个单一索引，包含三个主分片和一个副本（因此总共 6 个分片，每个节点 1 个分片）。</p><p>我们还在同一区域使用了一台独立服务器运行基准客户端，并收集时序统计数据。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/f81002c9186e4c2d3e92f49d72418fee9860fc5e-761x401.png" style="width: 100%;" /><h3>该数据集</h3><p></p><p>对于这个基准测试，我们使用了一个大规模电子商务风格的目录嵌入数据集，包含 2000 万份文档，旨在反映实际的大规模筛选后向量检索的扩展能力。</p><p></p><p>每份文件代表一个目录项目，包括：</p><p></p><ul><li>一种用于近似 kNN 检索的 128 维稠密向量嵌入。</li><li>结构化元数据字段用于筛选（例如，项目有效性和可用性，以及其他目录限制条件），从而支持常见的生产环境模式，即仅在符合条件的子集内检索最近邻。</li></ul><p></p><p>我们之所以选择这个数据集，是因为它捕捉到了我们在生产中看到的智能体和 RAG 型系统所面临的核心性能挑战：仅有矢量相似性是不够的，检索经常受到筛选条件的限制，系统必须在这些限制条件下保持较高的召回率和较低的延迟。与较小的 QA 风格数据集相比，2000 万文档的语料库更能反映筛选后 ANN 系统在实践中面临的扩展和候选压力。</p><h2>结论</h2><p>在现代 AI 架构中，尤其是在那些围绕上下文工程构建的架构中，向量搜索速度并非一个微不足道的实现细节。它是一个倍增因素。当智能体和工作流迭代检索→推理→检索时，检索性能直接影响端到端延迟、吞吐量以及输入到模型中的上下文质量。</p><p>在我们的基准测试中，与 OpenSearch 相比，当 Elasticsearch 在正确性取决于检索正确文档而不仅仅是相似向量的情况下，始终能以更低的延迟提供更高的召回率。在受控数据集上，差异是显而易见的，而在生产中，这些收益会在大量检索调用中累积，从而提高响应速度、增加容量裕度并降低基础设施成本。</p><h3>延展阅读</h3><ol><li><a href="https://www.elastic.co/search-labs/blog/context-engineering-overview">什么是上下文工程？</a></li><li><a href="https://www.elastic.co/search-labs/blog/series/context-engineering-hybrid-search-evolution">混合搜索和上下文工程的演进</a></li><li><a href="https://www.elastic.co/search-labs/blog/context-engineering-relevance-ai-agents-elasticsearch">相关性在 AI 智能体的上下文工程中的影响</a></li></ol>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[jina-embeddings-v5-text：用于搜索和智能应用的紧凑且最先进的文本嵌入]]></title>
            <description><![CDATA[介绍 jina-embeddings-v5-text 模型，包括 jina-embeddings-v5-text-small 和 jina-embeddings-v5-text-nano，并解释如何通过 Elastic 推理服务 (EIS) 使用这些多语言嵌入模型。]]></description>
            <link>https://www.elastic.co/search-labs/cn/blog/jina-embeddings-v5-text</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/cn/blog/jina-embeddings-v5-text</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Scott Martens, Sofia Vasileva]]></dc:creator>
            <pubDate>Mon, 23 Feb 2026 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/e98dd30ab925b4bb32830228d71a1a51d02a0917-1600x840.png?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>Jina AI 和 Elastic 正在发布 <code>jina-embeddings-v5-text</code>，这是一系列新的、高性能、紧凑的文本嵌入模型，在所有主要任务类型中，其性能在同等规模的模型中处于最先进水平。</p><p>该系列包括两个型号：</p><ul><li><code>jina-embeddings-v5-text-small</code></li><li><code>jina-embeddings-v5-text-nano</code></li></ul><p>这些模型是创新的嵌入模型新训练方法的成功结果。它们的性能优于规模是其数倍的模型，节省了内存和计算资源，并更快地响应请求。</p><p><code>jina-embeddings-v5-text-small</code> 模型拥有 6.77 亿个参数，支持 32,768 个令牌的输入上下文窗口，并默认生成 1,024 维的嵌入。</p><p><code>jina-embeddings-v5-text-nano</code> 其大小约为同类产品的三分之一，拥有 2.39 亿个参数和 8,192 个令牌的输入上下文窗口，并生成 768 维的嵌入。</p>
            <table style="border-collapse: collapse; margin: 20px 0;">
              <tbody>
                
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">型号名称</td><td style="border: 1px solid #ccc; padding: 4px;">总大小</td><td style="border: 1px solid #ccc; padding: 4px;">输入上下文窗口大小</td><td style="border: 1px solid #ccc; padding: 4px;">嵌入大小</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">jina-v5-text-small</td><td style="border: 1px solid #ccc; padding: 4px;">677M 参数</td><td style="border: 1px solid #ccc; padding: 4px;">32,768 个令牌</td><td style="border: 1px solid #ccc; padding: 4px;">1024 维度</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">jina-v5-text-nano</td><td style="border: 1px solid #ccc; padding: 4px;">239M 参数</td><td style="border: 1px solid #ccc; padding: 4px;">8,192 个令牌</td><td style="border: 1px solid #ccc; padding: 4px;">768 维度</td>
                    </tr>
              </tbody>
            </table>
          <p>这两个模型在整体 MMTEB（<a href="https://huggingface.co/spaces/mteb/leaderboard">多语言 MTEB</a>）基准性能方面是同类中最好的。在参数量少于 5 亿的模型中，<code>jina-embeddings-v5-text-nano</code> 表现最佳，尽管其参数量不足 2.5 亿，而 <code>jina-embeddings-v5-text-small</code> 模型是参数量少于 7.5 亿的多语言嵌入模型中的领先者。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/d97126285fa196c3045b1913b7754e4b186c1e4c-1300x1100.png" style="width: 100%;" /><p>这些模型可通过 Elastic 推断服务 (EIS)、在线 API 获得，并支持本地托管。有关如何访问 <code>jina-embeddings-v5-text</code> 模型的说明，请参阅以下“<strong>入门</strong>”部分。</p><p>嵌入模型和语义索引显著提高了搜索算法的准确性，但在涉及语义相似性和意义提取的任务中也有各种其他用途，例如：</p><ul><li>查找重复文本。</li><li>识别释义和翻译。</li><li>发现主题。</li><li>推荐引擎。</li><li>情感和意图分析。</li><li>垃圾邮件过滤。</li><li>还有更多。</li></ul><h2><strong>功能</strong></h2><p>这个新型号系列具有多项旨在提高相关性和降低成本的功能。</p><h3>任务优化</h3><p>我们针对四种广泛的任务类型优化了 <code>jina-embeddings-v5-text</code> 模型：</p>
            <table style="border-collapse: collapse; margin: 20px 0;">
              <tbody>
                
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">任务</td><td style="border: 1px solid #ccc; padding: 4px;">示例用例</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">检索</td><td style="border: 1px solid #ccc; padding: 4px;">使用自然语言查询进行搜索，并从文档集合中检索出最相关的匹配项。</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">文本匹配</td><td style="border: 1px solid #ccc; padding: 4px;">语义相似性、去重、释义与翻译对齐等。</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">聚类</td><td style="border: 1px solid #ccc; padding: 4px;">发现主题，自动组织文件集合。</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">分类</td><td style="border: 1px solid #ccc; padding: 4px;">文档分类、情感和意图检测、类似任务。</td>
                    </tr>
              </tbody>
            </table>
          <p>针对一项任务进行优化通常意味着必须在另一项任务上做出妥协，因此大多数嵌入模型仅在某一种任务上具有竞争力。但 <code>jina-embeddings-v5-text</code> 模型能够通过训练特定任务的<a href="https://arxiv.org/abs/2106.09685">低秩适配 (LoRA) 适配器</a>在所有四个领域实现专业化，而不会相互影响。</p><p>LoRA 适配器是一种用于 AI 模型的插件，它可以显著改变模型的行为，同时仅略微增加模型的总体大小。与为每个任务都配备一个拥有数亿参数的完整模型不同，<code>jina-embeddings-v5-text</code> 模型系列允许您仅使用一个带有紧凑型 LoRa 适配器的模型来完成每项任务。这节省了内存、存储空间和推理成本。</p><h3>截断嵌入</h3><p>我们使用<a href="https://arxiv.org/abs/2205.13147">套娃式表征学习</a>对 <code>jina-embeddings-v5-text</code> 模型进行了训练，这种方法能让您在几乎不影响嵌入质量的情况下将其压缩到更小的尺寸。</p><p>默认情况下，<code>jina-embeddings-v5-text-small</code> 会生成 1024 维嵌入向量，每个向量由 16 位数字表示，使每个嵌入大小为 2KB。对于大量文档集合而言，这可能需要存储大量数据，而在充满嵌入向量的向量数据库中进行搜索，其复杂度与数据库的大小以及每个存储向量的维度数都成正比。</p><p>但您可以将嵌入的大小减半（丢弃 1,024 维度中的 512 维），这样占用的空间减半，而搜索速度将翻倍。这会对性能产生影响。丢弃信息会降低精确度。但如下图所示，即使去掉一半的嵌入，性能也只是略有下降：</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/f11969682c81bcf22b5b29a6204ba6ec40471b3d-900x700.png" style="width: 100%;" /><p>只要您的嵌入至少有 256 维，精确度损失就会相当小。然而，如果低于这一水平，相关性和准确性就会迅速下降。</p><p>通过这种截断嵌入方法，用户可以在准确性和计算成本之间自行权衡。它为您提供了从搜索 AI 中大幅提高效率和节约成本的工具。</p><h3>稳健量化</h3><p><em>量化</em>是另一种缩小嵌入尺寸的方法。量化不是丢弃嵌入中的部分数据，而是降低嵌入中数字的精度。<code>jina-embeddings-v5-text</code> 模型以 16 位数字生成嵌入，但我们可以对这些数字进行四舍五入，从而降低其精度和存储所需的位数。在最极端的情况下，我们可以将每个数字缩减为一位（0 或 1），将 <code>jina-embeddings-v5-text</code> 的默认 1024 维嵌入从 2 千字节压缩到 128 字节，仅通过二进制量化就实现了 94% 的压缩率。这与截断一样，能大幅节省内存和计算成本。然而，和截断一样，量化也会使嵌入的准确性降低。</p><p>我们已训练了 <code>jina-embeddings-v5-text</code> 模型，通过将精度损失降至最低，使这些模型与 <a href="https://www.elastic.co/cn/elasticsearch">Elasticsearch</a> 的<a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/bbq">更优的二进制量化</a>一起工作。对这些模型的二值化嵌入进行的基准测试表明，其性能几乎与未二值化的等效模型相当。请参阅<a href="https://arxiv.org/abs/2602.15547">技术报告</a>，以获取有关二值化性能的详细消融研究。</p><h3>多语言性能</h3><p>许多嵌入模型是多语言的，因为它们已在包含大量语言的材料上进行了训练。但这并不意味着它们在所有支持的语言中都有同样出色的表现。</p><p>我们在 MMTEB 多语言基准测试中确定了 211 种语言，并将它们区分开来，以便能够逐个语言地将我们的模型与类似模型进行比较。下图以热力图的形式总结了我们的结果。每个补丁都是一种语言（通过 ISO-639 编码识别），颜色越绿，表明该模型的表现与同类模型的平均表现相比越好：</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/852ac5d0f1977bb0c1124d87f8863a9bb94eb7da-1600x765.png" style="width: 100%;" /><p>虽然不同语言的准确性各不相同，但 <code>jina-embeddings-v5-text</code> 模型在世界大多数语言中都处于领先或接近领先地位。</p><p>有关多语言性能的详细信息，请参阅 <a href="https://arxiv.org/abs/2602.15547"><code>jina-embeddings-v5-text</code></a> <a href="https://arxiv.org/abs/2602.15547">技术报告</a>。</p><h2><strong>Elastic 中的 Jina：用于搜索的最先进的原生 AI</strong></h2><p>借助 EIS 上的 <code>jina-embeddings-v5-text</code> 模型，您可以在 <a href="https://www.elastic.co/cn/elasticsearch">Elasticsearch</a> 中原生运行高性能多语言嵌入模型，并享受完全托管、GPU 加速的推理服务，而无需配置或扩展基础设施。<code>jina-embeddings-v5-text</code> 模型通过采用最新的 AI 技术，以紧凑的多语言模型扩展了不断增长的 EIS 模型目录。这些模型在信息检索和标准数据分析基准方面拥有最先进的性能，并提供了无与伦比的全球多语言支持。</p><p>两种模型的大小相差很大，用户可以根据自己的应用需求和预算来选择最合适的模型。此外，<code>jina-embeddings-v5-text </code> 模型拥有强大的嵌入功能，即使截断为较小尺寸或量化为较低精度时仍能保持性能，从而为进一步节省存储和计算成本以及处理延迟提供了机会。</p><p>借助 <code>jina-embeddings-v5-text</code> 系列、Jina Reranker 以及 Elastic 的快速向量和 BM25 搜索，用户现在可以使用 Elastic 提供的端到端、最先进的<a href="https://www.elastic.co/docs/solutions/search/hybrid-search">混合搜索</a>功能。当您需要最相关的结果时，无论是用于检索增强生成 (RAG) 管道、搜索应用程序还是数据分析，Elastic 与 Jina 搜索 AI 模型都能提供可靠且经济高效的优质服务。</p><h2><strong>开始使用</strong></h2><p><code>jina-embeddings-v5-text</code> 模型已完全集成到 <a href="https://www.elastic.co/docs/explore-analyze/elastic-inference/eis">EIS</a> 中，您可以在创建索引时将 <strong><code>type</code> </strong>字段设置为<strong> <code>semantic_text</code></strong>，并在 <code>inference_id</code><strong> </strong> 字段中指定模型（<code>jina-embeddings-v5-text-small</code> 或<code>jina-embeddings-v5-text-nano</code>），如本示例所示：</p><pre><code>PUT multilingual-semantic-index
{
  "mappings": {
    "properties": {
      "content": {
        "type": "semantic_text",
        "inference_id": ".jina-embeddings-v5-text-small"
      }
    }
  }
}

# Ingest data about France
POST multilingual-semantic-index/_doc
{
  "content": "The capital of France is Paris"}

GET multilingual-semantic-index/_search
{
  "query": {
    "semantic": {
      "field": "content",
      "query": "What is the French capital?"
    }
  }
}</code></pre><p><a href="https://www.elastic.co/cn/elasticsearch">Elasticsearch</a> 在索引和检索过程中会自动选择合适的 LoRA 适配器。嵌入维度（请参阅上文“<strong>截断嵌入</strong>”部分）可以在<a href="https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-inference-put">创建自定义推理端点</a>时设置。</p><p>有关使用 <strong><code>jina-embeddings-v5-text</code> </strong>模型的更多信息，请参阅 <a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/dense-vector">Elasticsearch 文档</a>。</p><h2><strong>更多信息</strong></h2><p>要了解有关 <code>jina-embeddings-v5-text</code> 模型的更多信息，请阅读 <a href="https://jina.ai/news/jina-embeddings-v5-text-distilling-4b-quality-into-sub-1b-multilingual-embeddings/">Jina AI 博客上的发行说明</a>和<a href="https://arxiv.org/abs/2602.15547">技术报告</a>，其中包含有关性能和 Jina AI 创新的新训练程序的更详细技术信息。有关在本地下载和运行这些模型的信息，请访问 Hugging Face 上的<a href="https://huggingface.co/collections/jinaai/jina-embeddings-v5-text"><code>jina-embeddings-v5-text</code></a> <a href="https://huggingface.co/collections/jinaai/jina-embeddings-v5-text">集合页面</a>。</p><p>Jina AI 模型在 <a href="https://spdx.org/licenses/CC-BY-NC-4.0">CC-BY-NC-4.0 许可证</a>下可用，因此您可以免费下载并试用它们，但如需用于商业用途，请联系 <a href="https://www.elastic.co/contact?pg=global&amp;plcmt=nav&amp;cta=205352">Elastic 销售人员</a>。</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[以最低得分阈值确保语义精确性]]></title>
            <description><![CDATA[通过设定最低得分阈值提升语义精确性。本文提供语义搜索与混合搜索的实操案例。 ]]></description>
            <link>https://www.elastic.co/search-labs/cn/blog/semantic-precision-minimum-score</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/cn/blog/semantic-precision-minimum-score</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Mattias Brunnert]]></dc:creator>
            <pubDate>Fri, 20 Feb 2026 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/8b3b5910abfe16d48d309341a0027008b16c4340-720x420.jpg?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>语义搜索为检索相关性开辟了全新可能。以 ELSER、E5、Jina Embedding v4 为代表的高质量稀疏-稠密混合模型，通过解析词义而非简单关键词匹配返回相关结果。然而，这类模型在处理长尾查询或索引缺乏相关内容时，可能返回无关结果，这种特性既可能导致用户困惑，也会造成大型语言模型 (LLM) 的算力资源浪费。</p><p>本文将介绍如何使用最低得分参数来提高语义搜索结果的精确度。如想测试本博客文章中提供的示例，请访问 <a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/ensuring-semantic-precision-with-minimum-score/ensuring_semantic_precision_with_minimum_score.ipynb">相关 Jupyter 笔记本</a>。</p><h2>背景：精确率和召回率</h2><p>在搜索相关性中，<em>精确率</em>和<em>召回率</em>是关键概念。强烈建议尚不熟悉这些内容的读者查阅相关资料。以下是摘要。</p><ul><li><strong>精确率：</strong>返回的搜索结果中与用户相关的比例。</li><li><strong>召回率：</strong>搜索结果集中包含的语料库中所有相关文档的百分比。</li></ul><p>或者换句话说，精确率<strong>只</strong>返回相关结果，而召回率则返回<strong>所有</strong>相关结果。可以想象，这些需求经常相互冲突。语义搜索往往具有很高的召回率，但在精确率方面可能会不理想。继续阅读，了解如何避免这种情况。</p><h2>推出最低得分参数</h2><p>min_score参数通过设定最低得分阈值提升检索精度，系统将自动过滤得分低于该阈值的匹配结果，从而精简结果集。以下是一个简单的示例：</p><pre><code>GET search-movies/_search
{
  "retriever": {
    "linear": {
      "min_score": 4,
      "retrievers": [
        ...
      ]
    }
  }
}</code></pre><h2>得分归一化</h2><p>设置最低得分阈值固然可行，但并非所有语义模型都能返回适用于静态阈值的分值。以 ELSER 为例，它所返回的分值是无界得分。<a href="https://huggingface.co/intfloat/e5-small#faq">有些</a>密集模型得分是密集聚类的，只有在特定查询的背景下才有意义。</p><p>对于大多数语义搜索情况，我们建议在应用“min_score”之前使用归一化方法。归一化确保文档得分在规定区间内。Elasticsearch 检索器提供了两种此类<a href="https://www.elastic.co/docs/reference/elasticsearch/rest-apis/retrievers/linear-retriever#linear-retriever-normalizers">归一化器</a>，即“l2_norm”和“minmax”。最常用的是“minmax”，因为它简单易懂，在很多情况下都很有效。“minmax”的主要属性包括：</p><ul><li>文档分数分布在 0 到 1 之间。</li><li>得分最高的文件总是记为 1 分。</li><li>得分最低的文件总是记为 0 分。<ul><li>这可能会使其不太适合关键字搜索。更多讨论请参见“混合搜索”部分。</li></ul></li></ul><p>以下是一个包含<code>min_score</code>规范化语义查询的示例。排名窗口参数已增加到 500，使系统能够返回从第 100 条开始的更长结果列表。</p><pre><code>GET search-movies/_search
{
  "size": 100,
  "_source": [
    "title", "overview"
  ],
  "retriever": {
    "linear": {
      "rank_window_size": 500,
      "min_score": 0.25,
      "retrievers": [
        {
          "normalizer": "minmax",
          "retriever": {
            "standard": {
              "query": {
                "semantic": {
                  "field": "overview_vector",
                  "query": "superhero movie"
                }
              }
            }
          }
        }
      ]
    }
  }
}</code></pre><p>当前参数已设置为高于生产环境的常规值，以便我们全面检验搜索结果质量并针对性优化输出。</p><h2>使用线性检索器的混合搜索</h2><p>对于混合搜索，最简单的方法是归一化所有分数，分配权重，并应用最低得分。请注意，通过选择总和为 1 的权重，可以将总分控制在 0-1 的范围内。这样使得最终得分易于解读，且便于调整 <code>min_score</code>。以下是一个示例：</p><pre><code>GET search-movies/_search
{
  "size": 100,
  "_source": ["title", "overview","keywords"],
  "retriever": {
    "linear": {
      "rank_window_size": 500,
      "min_score": 0.25,
      "retrievers": [
        {
          "weight": 0.6,
          "normalizer": "minmax",
          "retriever": {
            "standard": {
              "query": {
                "semantic": {
                  "field": "overview_vector",
                  "query": "superhero movie"
                }
              }
            }
          }
        },
        {
          "weight": 0.4,
          "normalizer": "minmax",
          "retriever": {
            "standard": {
              "query": {
                "multi_match": {
                  "query": "superhero movie",
                  "fields": ["overview","keywords", "title"],
                  "type": "cross_fields",
                  "minimum_should_match": "2"
                }
              }
            }
          }
        }
      ]
    }
  }
}</code></pre><h2>使用 RRF 的混合搜索</h2><p>使用 BM25 时，我们通常通过其他方式控制精度，例如使用<code>AND</code> 操作符或<code>minimum_should_match</code> 。此外，由单个、精确和罕见术语组成的查询自然会导致搜索结果较少，而且往往都是高度相关的结果。这就可能导致：</p><ul><li>在 BM25 检索器中，排名靠后的结果即使绝对 BM25 分值接近头部结果，仍会被赋予较低的归一化分数。</li><li>将极低的 BM25 分值与语义分值相加，总分即可近似为语义分值。</li><li>缺少 BM25 分值参考可能导致 <code>min_score threshold</code> 丢弃该文档。</li></ul><p>作为解决方案，我们可以改用倒数排序融合 (RRF) 来结合 BM25 和语义结果。该方法通过关注各结果集中的文档位置而非原始分值，巧妙规避了不同检索算法评分体系难以直接比较的技术难题。在这种情况下，<code>min_score</code> 仅应用于语义检索器。</p><pre><code>GET search-movies/_search
{
  "_source": ["title", "overview","keywords"],
  "retriever": {
    "rrf": {
      "rank_window_size": 500,
      "retrievers": [
        {
          "linear": {
            "rank_window_size": 500,
            "min_score": 0.25,
            "retrievers": [
              {
                "normalizer": "minmax",
                "retriever": {
                  "standard": {
                    "query": {
                      "semantic": {
                        "field": "overview_vector",
                        "query": "superhero movie"
                      }
                    }
                  }
                }
              }
            ]
          }
        },
        {
          "standard": {
            "query": {
              "multi_match": {
                "query": "superhero movie",
                "fields": ["overview", "keywords","title"],
                "type": "cross_fields",
                "minimum_should_match": "2"
              }
            }
          }
        }
      ]
    }
  }
}</code></pre><h2>结论</h2><p>通过采用 <code>min_score</code>，我们已验证可有效降低语义检索算法因高召回率导致的结果集中误报数量。要了解有关检索器的更多信息，请参阅本<a href="https://www.elastic.co/search-labs/blog/elasticsearch-retrievers">博文</a>和<a href="https://www.elastic.co/docs/solutions/search/retrievers-overview">Elasticsearch 文档</a>。</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Kubernetes 上的依赖管理]]></title>
            <description><![CDATA[如何使用 Renovate CLI 和 Argo 工作流简化 Kubernetes 上的依赖管理。]]></description>
            <link>https://www.elastic.co/search-labs/cn/blog/dependency-management-kubernetes</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/cn/blog/dependency-management-kubernetes</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Nikos Fotiou]]></dc:creator>
            <pubDate>Thu, 19 Feb 2026 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/00519840e6eec7101c1fb096afcae976ee0c454e-1280x720.png?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>这就是我们如何使用 Kubernetes、Argo 工作流、Argo Events 和 Renovate CLI 构建自托管依赖管理平台，以实现自动化更新、快速解决常见漏洞和暴露 (CVE)，并高效地在数千个存储库中传播新包版本的方法。</p><h2><strong>Elastic 的依赖管理</strong></h2><p>在 Elastic，我们必须管理数百甚至数千个存储库，包括私有和公共存储库。当发现关键 CVE 时，我们需要立即找到答案并采取行动：哪些存储库存在漏洞？我们能多快把它们修补好？除了安全性，生产力问题也随之而来：我们如何才能在不花费太多时间进行手动操作的情况下，迅速将新软件包版本的发布信息传播到所有依赖它的存储库？</p><p>寻找依赖管理方法的最初原因是需要建立一个具有自动更新功能的安全基础，以<a href="https://www.elastic.co/blog/reducing-cves-in-elastic-container-images">减少 CVE</a>。仔细考虑有关依赖管理的解决方案后，我们首先着手构建一个自托管的基础设施。我们使用自己的 Kubernetes 集群来运行 Mend Renovate 社区自托管服务。我们的想法是能够提供一个依赖管理平台，让我们的用户能够以自助服务的方式访问该平台。</p><p>最初的实验取得了成功，因此越来越多的团队开始使用我们的平台，并将其应用于日常存储库的生命周期管理中，用于更新和 CVE 补丁修复。这种情况发生得太快，以至于我们很快就达到了自托管安装的上限。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/e14d9f98d4eccaa08a335d5bd23d88e5debbb344-1600x1103.png" style="width: 100%;" /><h3><strong>挑战：我们如何在拥有大量存储库的大型组织中扩展依赖管理平台？</strong></h3><p>我们的依赖管理平台一次只能处理一个存储库，由于我们拥有大量存储库，这种顺序处理模型已无法跟上需求。我们已经确定，问题在于我们的依赖管理工具的<strong>单个实例</strong>无法处理我们庞大且不断增长的存储库列表这一概念。存储库在队列中等待，有时长达数小时。我们超过 50% 的存储库甚至没有每天进行处理。这意味着超过 50% 的存储库扫描间隔时间超过 24 小时。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/45ade5bda08f82bed0b3d0d3736cbd6f056e7a4e-1312x816.jpg" style="width: 100%;" /><p>大型存储库由于代码库规模庞大且有多个开放 PR，因此会造成更大的瓶颈。GitHub webhook 事件打乱了顺序。由于扫描时间无法预测，自动合并变得不可靠。我们曾向用户承诺扫描频率，但未能兑现。</p><h3><strong>决定内部构建：满足 Elastic 独特的扩展和安全需求</strong></h3><p>虽然我们考虑了商业选项，包括 <strong>Mend 的 Renovate 自托管企业版</strong>，但在 Elastic 内部，我们有几个关键计划正在加速推进。</p><p>我们决定构建一个内部平台，这一决定源于我们认识到，只有深度定制的解决方案才能满足 Elastic 不可妥协的特殊要求：</p><ol><li><strong>投资我们的内部开发者平台：</strong>当时，我们已经开始大力投资内部开发者平台。我们正在讨论和设计每项服务都能融入其中的方法。这意味着我们希望为我们的依赖管理平台测试我们自己的规则和做法。除此之外，新的指南即将出台，我们希望在此之前设计好平台。</li><li><strong>本地集成和工作流程定制：</strong>我们需要与内部工具和内部流程直接集成。例如，我们希望通过服务目录（后台）将配置集中为代码。我们对后台的使用有特殊需求，希望我们的平台能与之兼容。因此，尽管可以将 Renovate 自托管 API 与我们的后台自动化结合使用，但这并不能完全覆盖我们的内部流程。</li><li><strong>针对 Elastic 的深度防御安全：</strong>我们严格的安全合规要求为我们的生态系统量身定制安全机制。我们正在努力<a href="https://entro.security/blog/how-elastic-scaled-secrets-nhi-security-elastics-playbook-from-visibility-to-automation/">强化对“非人类身份”的使用。</a>这种访问权限的强化方式意味着，如果工具不支持 GitHub 内部的这种实现方式，那么非标准的身份验证方法将无法使用现成的工具。我们的工作流包括实施父子工作流密钥加密模式，并使用临时的一次性 GitHub 令牌。在我们复杂的多云环境中，内部构建是嵌入这些独特的安全层并最大限度减少攻击面的唯一实用方法。</li></ol><h2><strong>解决方案：用于依赖管理的工作流编排</strong></h2><p>我们的解决方案源于这样一个事实，即我们希望在已使用的依赖管理工具的基础上进行构建，而不是将其替换掉并寻找其他方案。它已显示出其潜力，其灵活性对于满足我们整个组织的不同需求非常重要。我们考虑了不同的解决方案，而帮助我们做出决定的是我们必须承担的重大且有时特殊的需求。我们决定构建一个可靠且具有可扩展性的依赖管理平台，在这个Platform上，每个存储库都将单独处理，消除瓶颈，为未来发展奠定基础。</p><p>我们在设计该平台时遵循了三个核心原则：</p><h3><strong>1. 并行处理</strong></h3><p>每个存储库都有其专属的依赖管理处理环境。不再有排队的情况。我们的并发性仅受我们消耗的资源数量限制。我们还应用了智能分布式调度，以避免受到 GitHub 的速率限制。</p><h3><strong>2. 可自助服务</strong></h3><p>我们使用服务目录（后台）自动载入和管理任何新的存储库。我们使用自己的资源定义，让最终用户可以选择存储库的处理频率、计划分配多少资源，以及出于任何原因选择关闭或重新开启处理。随着用户需求的变化以及他们对新安装方式日益熟练，我们计划通过这种方式增加更多选项。</p><h3><strong>3. 缩小了机密范围和命名空间隔离</strong></h3><p>为了提高安全性，我们在每次工作流开始时为依赖管理 Pod 提供临时生成的 GitHub 令牌。此外，我们还将工作负载隔离在特定的命名空间中，以便仅向它们提供必要的机密。我们使用 Kubernetes RBAC 控制每个依赖管理工作流可以访问哪些机密。我们还使用加密技术将 GitHub 令牌从父工作流传播到子工作流。</p><p>我们使用 Kubernetes 重建了平台，并借助 Kubernetes 的强大功能，Argo 工作流为我们的流程逻辑提供支持，同时 Renovate CLI 已设置好，用于一次扫描和处理一个存储库。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/5560ed20e2bd9ecdd574a9c835126d12b24c332f-1600x1157.png" style="width: 100%;" /><p><strong>亮点：</strong>我们正以一种创新的方式使用经过实战验证的开源项目，为所有这些项目提供新的工作示例，同时为我们的团队提高开发速度并减少 CVE。</p><h2><strong>依赖管理架构：四个微服务</strong></h2><p>该平台由四个定制组件构成：</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/2b3d4046c05bb261e45d40c59f864eb51fb9eaa9-1217x1600.png" style="width: 100%;" /><h3><strong>工作流 Operator (Go/Kubebuilder)</strong></h3><p>Kubernetes Operator 通过三个自定义资源定义 (CRD) 管理工作流生命周期：</p><ul><li><strong>RepoConfig CRD：</strong>存储库配置的单一事实来源。</li></ul><p>这就是在 Operator 中定义 RepoConfig 的方式：</p><pre><code>// RepoConfig is the Schema for the repoconfigs API
type RepoConfig struct {
	metav1.TypeMeta `json:",inline"`

	// metadata is a standard object metadata
	// +optional
	metav1.ObjectMeta `json:"metadata,omitempty,omitzero"`

	// spec defines the desired state of RepoConfig
	// +required
	Spec RepoConfigSpec `json:"spec"`

	// status defines the observed state of RepoConfig
	// +optional
	Status RepoConfigStatus `json:"status,omitempty,omitzero"`
}</code></pre><p>这就是 RepoConfig 实例的样子：</p><pre><code>apiVersion: workflows.elastic.co/v1
kind: RepoConfig
metadata:
  generation: 3
  name: elastic-test-repo
  namespace: dependency-management-operator
spec:
  owner: group:my-team
  renovate:
    config:
      resourceGroup: SMALL
      runFrequency: 4h
    enabled: true
  repository: elastic/test-repo</code></pre><ul><li><strong>父级 CRD：</strong>管理用于计划扫描的 CronWorkflow。</li></ul><p>在父控制器的协调循环内部，我们确保创建并保持工作流设置的最新状态，甚至在必要时将其删除。</p><p>首先，它会获取一些全局配置的工作流设置：</p><pre><code>func (r *ParentReconciler) reconcileSubResources(ctx context.Context, req ctrl.Request, parent *workflowsv1.Parent) error {
	logger := logf.FromContext(ctx)
	logger.Info("Reconcile SubResources for Parent", "name", req.NamespacedName)
	wfSet := workflowsettings.WorkflowSettings{
		RunFrequency:   parent.Spec.RunFrequency,
		ResourceGroups: "parent",
	}</code></pre><p>它确保互斥 configmap 是最新的，以防止类似的工作流同时运行：</p><pre><code>	cfMngr := resources.NewConfigMapManager(r.Client, r.Scheme, r.OperatorConfig.ParentNamespace)
	err := cfMngr.CreateOrUpdateSyncMutexConfigmap(ctx, fmt.Sprintf("%s%s", r.OperatorConfig.ResourcesPrefix, r.OperatorConfig.SyncMutexCfgMapName), strings.TrimPrefix(parent.Spec.Repository, "elastic/"), r.OperatorConfig.SemaphoreConcurrencyLimit)</code></pre><p>然后创建工作流管理器，该结构将创建或更新 CronWorkflows 和工作流模板：</p><pre><code>	wfMngr := resources.NewArgoWorkflowManager(r.Client,
		r.Scheme,
		curateResourceName(
			strings.ReplaceAll(parent.Spec.Repository, "/", "-"),
		),
		parent.Namespace,
		"parent-workflow",
		false).
		WithOrganization(r.OperatorConfig.GitHubOrg).
		WithRepoName(parent.Spec.Repository).
		Init(true, true).
		WithPrefix(r.OperatorConfig.ResourcesPrefix).
		WithWfTemplateName(r.OperatorConfig.ParentWorkflowTemplate).
		WithResources(wfSet.GetResourceCategory()).
		WithSchedule(wfSet.GetCronSchedule()).
		WithImagePullSecrets([]corev1.LocalObjectReference{{
			Name: r.OperatorConfig.WorkflowImagePullSecrets,
		}}).
		AddArgument(true, true, "extra_cli_args").
		SetArgument(true, false, "extra_cli_args", "none").
		AddTemplate(resources.NewParentDAGTemplateInstance()).
		AddTemplate(resources.NewWorkflowsTemplateInstance("check-child-workflows", r.OperatorConfig.WorkflowImagePullPolicy, r.OperatorConfig.WorkflowNodeSelector)).
		AddTemplate(resources.NewWorkflowsTemplateInstance("security", r.OperatorConfig.WorkflowImagePullPolicy, r.OperatorConfig.WorkflowNodeSelector)).
		AddTemplate(resources.NewWorkflowsTemplateInstance("submit-child-workflow", r.OperatorConfig.WorkflowImagePullPolicy, r.OperatorConfig.WorkflowNodeSelector))
	wfMngr.OverWriteCommand("submit-child-workflow", r.OperatorConfig.ChildNamespace)
	wfMngr.OverwriteWfTemplateName("parent-wftmpl")
	wfMngr.AddSynchronization(fmt.Sprintf("%s%s", r.OperatorConfig.ResourcesPrefix, r.OperatorConfig.SyncMutexCfgMapName), "{{workflow.parameters.repo_name}}")
	err = wfMngr.CreateOrUpdateCronWorkflow(ctx)
	if err != nil {
		return fmt.Errorf("failed to create or update cron workflow: %w", err)
	}
	err = wfMngr.CreateOrUpdateWorkflowTemplate(ctx)
	if err != nil {
		return fmt.Errorf("failed to create or update workflow template: %w", err)
	}
	return nil</code></pre><ul><li><strong>子 CRD：</strong>使用每个存储库的资源管理 WorkflowTemplate。</li></ul><p>子控制器与父控制器有类似的协调职责，但这次它负责子命名空间中将由父工作流触发的工作流模板。</p><pre><code>func (r *ChildReconciler) reconcileSubResources(ctx context.Context, req ctrl.Request, child *workflowsv1.Child) error {
	logger := logf.FromContext(ctx)
	logger.Info("Reconcile SubResources for Child", "name", req.NamespacedName)
	wfSet := workflowsettings.WorkflowSettings{
		ResourceGroups: child.Spec.ResourceCategory,
	}
	wfMngr := resources.NewArgoWorkflowManager(r.Client,
		r.Scheme,
		curateResourceName(
			strings.ReplaceAll(child.Spec.Repository, "/", "-"),
		),
		child.Namespace,
		"runner",
		true).
		Init(false, true). // only manage workflow template
		WithPrefix(r.OperatorConfig.ResourcesPrefix).
		WithSuffix("-child-wftmpl").
		WithRepoName(child.Spec.Repository).
		WithOrganization(r.OperatorConfig.GitHubOrg).
		WithResources(wfSet.GetResourceCategory()). // will override resources of presets if set
		WithImagePullSecrets([]corev1.LocalObjectReference{{
			Name: r.OperatorConfig.WorkflowImagePullSecrets,
		}}).
		AddTemplate(resources.NewWorkflowsTemplateInstance("runner", r.OperatorConfig.WorkflowImagePullPolicy, r.OperatorConfig.WorkflowNodeSelector)).
		AddArgument(false, true, "repo_full_name").
		AddArgument(false, true, "repo_name").
		AddArgument(false, true, "encrypted_token").
		AddArgument(false, true, "extra_cli_args")
	wfMngr.OverWriteCommand("runner", r.OperatorConfig.ChildNamespace)
	err := wfMngr.CreateOrUpdateWorkflowTemplate(ctx)
	if err != nil {
		return fmt.Errorf("failed to create or update workflow template: %w", err)
	}
	return nil
}</code></pre><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/7ac70492a1266ba02cb8afbafc5a486cb38a0edc-1600x1290.png" style="width: 100%;" /><p>多控制器模式提供了明确的分隔：RepoConfig 控制器处理加入/退出，父控制器管理调度，子控制器处理执行模板。</p><h3><strong>GitHub 事件网关 (Go)</strong></h3><p>一个安全的 Webhook 代理，用于接收 GitHub 的 Webhook，验证签名，按组织/存储库进行筛选，并将其路由到 Argo Events。我们构建了 10 个不同的传感器，分别对依赖仪表板交互、PR 事件和软件包更新做出响应。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/4828625456cbd6efa8020a20f10d23f294f98a02-1306x1600.png" style="width: 100%;" /><p>此网关可通过以下方式与 GitHub 应用集成：</p><ul><li>验证传入的 GitHub Webhook 签名以确保安全。</li><li>将有效事件转发给 Argo Events EventSource，并附上所有相关标头和身份验证。</li><li>我们还在 EventSource 上配置了一个 authSecret，并在转发的请求中将其作为 Bearer 标头提供。</li><li>提供日志记录、指标和重试逻辑。</li></ul><p>它对每个 GitHub 事件请求执行各种验证。</p><p>它确保某些 HTTP 属性存在：</p><pre><code>// ValidateRequestMethod checks if the request method is POST.
func ValidateRequestMethod(r *http.Request) error {
	if r.Method != http.MethodPost {
		return fmt.Errorf("method not allowed, only POST is accepted")
	}
	return nil
}

// ValidateRequiredHeaders checks for required GitHub headers.
func ValidateRequiredHeaders(r *http.Request) error {
	eventType := r.Header.Get("X-GitHub-Event")
	deliveryID := r.Header.Get("X-GitHub-Delivery")
	signature := r.Header.Get("X-Hub-Signature-256")
	if eventType == "" || deliveryID == "" || signature == "" {
		return fmt.Errorf("missing required GitHub headers")
	}
	return nil
}

// ValidateUserAgent checks that the User-Agent header starts with GitHub-Hookshot/
func ValidateUserAgent(r *http.Request) error {
	userAgent := r.Header.Get("User-Agent")
	if !strings.HasPrefix(userAgent, "GitHub-Hookshot/") {
		return fmt.Errorf("invalid User-Agent")
	}
	return nil
}</code></pre><p>同时，它还会验证每个请求的签名及其组织。</p><pre><code>// ValidateSignature verifies the GitHub webhook signature.
func ValidateSignature(r *http.Request, secret string) ([]byte, error) {
	payload, err := GitHub.ValidatePayload(r, []byte(secret))
	if err != nil {
		return nil, fmt.Errorf("invalid GitHub signature: %w", err)
	}
	return payload, nil
}

// ValidateAllowedOwner checks if the organization login is in the allowed organizations list.
func ValidateAllowedOwner(payload []byte, allowedGitHubOrganizations []string) (string, error) {
	var orgLogin string
	var payloadMap map[string]any
	if err := json.Unmarshal(payload, &payloadMap); err == nil {
		if orgObj, ok := payloadMap["organization"].(map[string]any); ok {
			if login, ok := orgObj["login"].(string); ok {
				orgLogin = login
			} else if name, ok := orgObj["name"].(string); ok {
				orgLogin = name
			}
		}
	}
	if !slices.Contains(allowedGitHubOrganizations, orgLogin) {
		return orgLogin, fmt.Errorf("organization login not allowed")
	}
	return orgLogin, nil
}</code></pre><p>最后，它会根据事件类型路由到 Argo Events：</p><pre><code>	// Map eventType to Argo `EventSource` path
	var endpoint string
	switch eventType {
	case "push":
		endpoint = "/push"
	case "issues":
		endpoint = "/issues"
	case "pull_request":
		endpoint = "/pull-requests"
	default:
		slog.Info("Ignoring unhandled event type", "event_type", eventType, "delivery_id", deliveryID)
		w.WriteHeader(http.StatusOK)
		_, _ = w.Write([]byte("ok"))
		return
	}
	forwardURL := h.config.ArgoEventSourceForwardURL + endpoint</code></pre><p>在 Argo Events 方面，有 10 个传感器在监视 Argo Events EventBus 上的新事件。</p><pre><code>apiVersion: argoproj.io/v1alpha1
kind: Sensor
metadata:
  name: {{ .Values.sensors.packageUpdateOnDefaultBranch.name }}
  namespace: {{ .Release.Namespace }}
spec:
  eventBusName: {{ .Values.eventBus.name }}</code></pre><p>然后，脚本会应用每个传感器的逻辑：</p><pre><code>script: |
          local e = event
          if not e or not e.body or not e.body.repository then
            return false
          end

          -- e.g., "refs/heads/main"
          local ref = e.body.ref
          local default_branch = e.body.repository.default_branch
          if not ref or not default_branch then
            return false
          end

          local expected = "refs/heads/" .. default_branch
          if ref ~= expected then
            return false
          end

        {{- if .Values.sensors.packageUpdateOnDefaultBranch.packageFiles }}
          patterns = { {{- range $i, $f := .Values.sensors.packageUpdateOnDefaultBranch.packageFiles }}{{ if $i }}, {{ end }}"{{ $f }}"{{- end }} }
        {{- end }}

          local function anyMatch(path)
            if type(path) ~= "string" then return false end
            for _, pat in ipairs(patterns) do
              -- match filename at repo root, or anywhere under subdirs
              if path:match(pat) or path:match(".+/" .. pat) then
                return true
              end
            end
            return false
          end

          local function filesContainPackage(paths)
            if type(paths) ~= "table" then return false end
            for _, p in ipairs(paths) do
              if anyMatch(p) then return true end
            end
            return false
          end

          -- Inspect all commits (GitHub includes added/modified/removed lists)
          local commits = e.body.commits
          if type(commits) ~= "table" then
            -- Fallback: some payloads include only head_commit
            commits = {}
            if type(e.body.head_commit) == "table" then
              table.insert(commits, e.body.head_commit)
            end
          end

          for _, c in ipairs(commits) do
            if filesContainPackage(c.added) or filesContainPackage(c.modified) or filesContainPackage(c.removed) then
              return true
            end
          end

          return false</code></pre><h3><strong>后台同步器 (Go)</strong></h3><p>此过程将轮询我们的服务目录（后台）以获取存储库真实资源实体，将其转换为 RepoConfig CRD，并使平台与配置更改保持同步。更改将在三分钟内生效。</p><pre><code>repoMap := make(map[string]map[string]interface{})
			for i := range entities {
				entity := &entities[i]
				if entity.Spec.Type != "GitHub-repository" {
					continue
				}

				implRaw, err := json.Marshal(entity.Spec.Implementation)
				if err != nil {
					logger.Error("Failed to marshal implementation", "error", err)
					continue
				}

				var implMap map[string]interface{}
				err = json.Unmarshal(implRaw, &implMap)
				if err != nil {
					logger.Error("Failed to unmarshal implementation map", "error", err)
					continue
				}
				var repoName string
				if specMap, ok := implMap["spec"].(map[string]interface{}); ok {
					if repo, ok := specMap["repository"].(string); ok {
						repoName = repo
					}
				}
				if repoName == "" {
					continue
				}

				var workflowsRaw []byte
				if v, ok := implMap["spec"].(map[string]interface{}); ok {
					if r, ok := v["renovate"]; ok {
						workflowsRaw, _ = json.Marshal(r)
					} else {
						workflowsRaw = []byte(`{}`)
					}
				} else {
					workflowsRaw = []byte(`{}`)
				}

				var workflowsWithDefaults schema.WorkflowsMetadata
				err = json.Unmarshal(workflowsRaw, &rworkflowsWithDefaults)
				if err != nil {
					logger.Error("Failed to unmarshal workflows config", "error", err)
					continue
				}

				workflowsMap := map[string]interface{}{
					"enabled":        workflowsWithDefaults.Enabled,
					"require_pr":     workflowsWithDefaults.RequirePr,
					"resource_group": string(workflowsWithDefaults.ResourceGroup),
					"run_frequency":  string(workflowsWithDefaults.RunFrequency),
				}
				repoMap[repoName] = map[string]interface{}{
					"renovate": workflowsMap,
					"owner":    entity.Spec.Owner,
				}
			}
			logger.Info("Fetched GitHub Repository data from Backstage", "repository_count", len(repoMap), "status_code", resp.StatusCode)</code></pre><p>最后，它将数据写入 RepoConfig 实例。</p><h3><strong>工作流基础（混合：JavaScript、Go、Helm）</strong></h3><p>基础层包含 Helm 图表、JavaScript 配置、带有加密支持的适用于 Renovate CLI 的 Go 封装器，以及适用于 Alpine 软件包的自定义 APK 索引器。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/908d19278face3ce1119dbee9146c1264b6e2f30-1600x873.png" style="width: 100%;" /><h2><strong>自助服务配置</strong></h2><p>团队通过后台声明式配置其存储库：</p><pre><code>spec:
  renovate:
    enabled: true
    config:
      resourceGroup: LARGE      # SMALL | MEDIUM | LARGE  
      runFrequency: "0 */4 * * *"  # Every 4 hours</code></pre><p>资源组根据存储库大小分配 CPU 和内存：</p><ul><li><strong>小型：</strong>500m CPU，1Gi 内存。</li><li><strong>中型：</strong>1000m CPU，2Gi 内存。</li><li><strong>大型：</strong>2000m CPU，4Gi 内存。</li></ul><p>配置受版本控制、可审计并自动应用。</p><h2><strong>父子模式</strong></h2><p>执行模型采用父子工作流模式：</p><ul><li><strong>父工作流：</strong>按计划运行的轻量级 CronWorkflow。加密机密，确定是否应运行扫描，将配置传递给子项。</li><li><strong>子工作流：</strong>运行 Renovate CLI 的临时 Pod。动态分配资源，在隔离环境中解密机密，完成后终止。</li></ul><p>这种分离提供了安全性（在父级加密机密）、资源优化（父级使用最少资源）以及可扩展性（子级并行运行）。</p><h2><strong>结果</strong></h2><h3><strong>性能转换</strong></h3><ul><li><strong>之前：</strong>每次处理一个存储库，有些存储库可能一天甚至更长时间都无法得到处理，每天扫描量不足 1000 次。</li><li><strong>之后：</strong>超过 100 次并发扫描，通常每天 8,000 次扫描，最多可达 10000 次记录的扫描，仅受我们愿意投入的资源数量以及处理 GitHub 速率限制的方式的限制。</li></ul><h3><strong>成本效率</strong></h3><p>尽管听起来有点奇怪，但每天运行 8000 个 Pod 可以比让一个长期运行的 Pod 试图达到同样的结果花费少得多，而且效果相同。</p><p>在之前的设置中，我们运行的是单个实例，在状态良好的情况下，每天能执行 500 到 600 次扫描。同时，由于不同类型的存储库将在同一个 Pod 上执行，我们需要根据最大的存储库来调整 Pod 的大小。这种尺寸比我们目前提供的超大型产品要大得多，我们的 Pod 使用 8 个 CPU 和 16G 内存。</p><p>为满足当前的每日输出，单个 Pod 需要运行 12 天。因此，将单个 Pod 运行 12 天的成本与每天运行 8,000 个“中等”大小 Pod 的成本进行比较，我们的新设计在相同的扫描输出下要高效得多：</p>
            <table style="border-collapse: collapse; margin: 20px 0;">
              <tbody>
                
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">指标</td><td style="border: 1px solid #ccc; padding: 4px;">场景 A（工作流）</td><td style="border: 1px solid #ccc; padding: 4px;">场景 B（长时间运行的单个 pod）</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">设置</td><td style="border: 1px solid #ccc; padding: 4px;">8,000 个 pod（1 个 vCPU / 2GB）</td><td style="border: 1px solid #ccc; padding: 4px;">1 个 pod（8 个 vCPU / 16 GB）*</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">持续时间</td><td style="border: 1px solid #ccc; padding: 4px;">每次 10 分钟</td><td style="border: 1px solid #ccc; padding: 4px;">连续 12 天</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">总工作时间</td><td style="border: 1px solid #ccc; padding: 4px;">1,333 计算小时</td><td style="border: 1px solid #ccc; padding: 4px;">288 个计算小时</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">总成本</td><td style="border: 1px solid #ccc; padding: 4px;">$65.83</td><td style="border: 1px solid #ccc; padding: 4px;">$113.75</td>
                    </tr>
              </tbody>
            </table>
          <p>不过，我们应考虑到，我们的工作负载默认设置为“小型”，绝大多数工作负载在 0.5 CPU 和 1G RAM 的情况下成功运行，只有少数需要更改为中型或大型。让我们看看，如果 60% 的工作负载运行在“小型”级别，30% 运行在“中型”级别，10% 运行在“大型”级别会发生什么情况，这更接近实际情况。</p>
            <table style="border-collapse: collapse; margin: 20px 0;">
              <tbody>
                
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">指标</td><td style="border: 1px solid #ccc; padding: 4px;">场景 A（混合群）</td><td style="border: 1px solid #ccc; padding: 4px;">场景 B（长时间运行）</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">战略</td><td style="border: 1px solid #ccc; padding: 4px;">8,000 个 Pod（混合尺寸）</td><td style="border: 1px solid #ccc; padding: 4px;">1 个 pod（8 个 vCPU / 16 GB）*</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">持续时间</td><td style="border: 1px solid #ccc; padding: 4px;">每次 10 分钟</td><td style="border: 1px solid #ccc; padding: 4px;">连续 12 天</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">总成本</td><td style="border: 1px solid #ccc; padding: 4px;">$52.66</td><td style="border: 1px solid #ccc; padding: 4px;">$113.75</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">保存</td><td style="border: 1px solid #ccc; padding: 4px;">61.09 美元（便宜 54%）</td><td style="border: 1px solid #ccc; padding: 4px;">—</td>
                    </tr>
              </tbody>
            </table>
          <p>我们可以看到，在相同的输出下，我们目前的配置成本效益要高得多。</p><h3><strong>增强安全</strong></h3><ul><li>临时 GitHub 令牌（暴露时间为几分钟而不是几天）。</li><li>通过基于角色的访问控制 (RBAC) 边界实现命名空间隔离。</li><li>父工作流中的机密数据静态加密。</li><li>移除了直接访问金库的权限。</li></ul><h3><strong>可预测的性能</strong></h3><p>有了有保障的扫描频率，我们终于可以设定服务水平目标 (SLO)。自动合并功能运行可靠。团队信任平台能够兑现承诺。</p><h2><strong>关键架构决策</strong></h2><p>以下是一些塑造平台外观的里程碑式设计决策。</p><ul><li><strong>为何采用父子工作流？</strong></li></ul><p>我们采用这种模式来实施<strong>深度防御</strong>策略。通过将高价值证书（例如 GitHub 应用密钥）限制在专用且锁定的命名空间，我们使用<strong>基于角色的访问控制</strong> (RBAC) 来确保临时执行 Pod 无法随意访问敏感数据。最近的供应链漏洞（例如<strong>“Shai Hulud”</strong>持续集成/持续交付 [CI/CD] 攻击）表明，将执行动态脚本的运行时环境与凭据存储空间隔离开至关重要。</p><p>同时，这种解耦还实现了<strong>细粒度的资源优化</strong>。“父”工作流充当轻量级编排器，占用资源极少，而“子”工作流则处理计算密集型依赖扫描。这种分离简化了<strong>生命周期管理</strong>，使我们能够对每一层应用不同的协调逻辑，让用户能够控制执行参数（子级），同时保留对调度和安全基础设施的管理控制（父级）。</p><ul><li><strong>为什么采用可自助服务？</strong></li></ul><p>消除团队在存储库配置方面的瓶颈是一项关键要求。我们的使命是构建一个可扩展的<strong>自助服务平台</strong>，能够支持各种用例。我们认识到，鉴于存储库的庞大数量，为每项配置更改充当“<strong>守门员</strong>”的做法是不可持续的。相反，我们采取了一种赋能的理念：提供“轨道”（基础设施和<strong>保障措施</strong>），同时赋予用户驾驶“列车”（执行和自定义）的权力。我们相信，这种向<strong>团队自主权</strong>的转变，能让用户根据自己的具体运营需求来定制系统，从而显著提高了生产率。</p><ul><li><strong>为什么使用 Kubernetes Operator 模式？</strong></li></ul><p>如上所述，一个基本的设计原则是确保平台可以完全<strong>自助服务</strong>。我们需要一种自动机制来捕捉用户意图（例如切换扫描、调整调度频率或调整运行时资源限制），并立即将这些更改传播到底层工作流中。考虑到未来的需求，该系统还需要易于<strong>扩展</strong>。</p><p>为了实现这一目标，我们开发了自定义<strong>依赖管理 Kubernetes Operator</strong>。通过使用 <strong>CRD</strong> 作为配置接口，我们建立了一个<strong>原生 Kubernetes 协调循环</strong>。此 Operator 会持续监控用户定义的期望状态，并自动编排对工作流基础设施进行必要的更新。这确保了<strong>事件驱动</strong>的无缝操作，平台逻辑可以在后台处理所有复杂性。</p><ul><li><strong>为什么要设计 GitHub 事件网关？</strong></li></ul><p>采用<strong>事件驱动型架构 (EDA)</strong> 对平台的响应速度至关重要。尽管 CronWorkflows 提供了可靠的基线计划，但我们还需要具备灵活性来处理<strong>临时执行，</strong>例如用户通过仪表板手动触发扫描。为了实现这一目标，我们需要一个专用的<strong>摄取网关</strong>来验证有效负载的完整性并智能地路由请求。</p><p>我们评估了现有解决方案，包括 Argo 的原生 GitHub EventSource，但发现在<strong>运营开销</strong>和严格的 <strong>GitHub API 配额</strong>（例如，每个存储库的 Webhook 限制）方面存在重大风险。因此，我们构建了一个自定义网关，使我们的基础设施不受这些限制的影响。</p><p>至关重要的是，此网关在我们的迁移过程中充当了战略<strong>流量控制点</strong>。它充当了一个开关，使我们能够从传统系统向新的<strong>基础设施</strong>执行渐进式、细粒度的部署（流量切换）。这确保了数千个存储库的导入过程是受控且无风险的，而非“大爆炸”式切换。</p><p></p><h2><strong>经验教训</strong></h2><p>我们学到的一些经验教训与 <a href="https://www.elastic.co/about/our-source-code">Elastic 源代码</a>密切相关：</p><ol><li><strong>客户至上：</strong>平台为用户而构建。因此，将用户需求放在首位非常重要。这将平台塑造成高效设计的基础设施和应用程序，从而减少与用户的摩擦，简化平台的扩展，并使其易于采用。</li><li><strong>空间与时间：</strong>有时，最顺畅的道路也会通向<strong>变幻莫测的沙漠</strong>。我们最初尝试优化现有的顺序处理模型，但这并未解决我们的问题；事实上，它只是引入了更多复杂性和未解决的问题。<strong>重新构建</strong>并行处理平台的大胆决定需要大量的前期工作。然而，它最终为可持续的平台增长铺平了道路，并几乎消除了繁琐的日常管理工作。</li><li><strong>视情况而定：</strong>平台无法孤立运作；其成功取决于它与更广泛的生态系统的整合程度。在我们的案例中，与<strong>后台</strong>的集成至关重要，因为它是无缝服务导入的真实来源。同样，连接到 <strong>Artifactory</strong> 使我们能够高效地管理私有包更新，而且这些重要的集成远不止于此。</li><li><strong>进步，简单即完美：</strong>在整个实施过程中，我们不断对最初的假设进行压力测试，并在新障碍出现时进行调整。我们没有被完美主义所束缚，而是采取<strong>迭代的方法</strong>，逐一解决挑战，并根据实际情况调整迁移策略。</li></ol><h2><strong>未来发展</strong></h2><p>该平台的交付使我们能够开展更有意义的工作，这将有助于我们改善平台的用户体验和效率。一些示例包括：<br/></p><ul><li><strong>增加并规范自动合并的采用</strong></li></ul><p>自动合并功能通过消除繁琐的手动任务，显著加快了团队的工作进度。然而，我们需要确保设立严格的<strong>防护措施</strong>，确保这种速度提升不以牺牲安全性为代价。<br/></p><ul><li><strong>改善围绕最终用户体验的可观测性</strong></li></ul><p>我们路线图的一个重要优先事项是增强可观测性，不仅是在平台层面，而且特别是从<strong>最终用户的角度</strong>。虽然捕获基础设施指标很简单，但要理解实际的用户体验需要更深入的见解。我们正在努力定义以用户为中心的核心关键性能指标 (KPI)，以便我们的遥测技术能够在问题升级为用户投诉<strong>之前</strong>检测到摩擦点和性能问题。</p><ul><li><strong>消除障碍以促进更广泛的应用</strong></li></ul><p>展望未来，我们的首要任务是找出并消除任何阻碍平台采用的障碍。无论这需要开发新的集成还是部署特定的功能集，我们都致力于数据驱动的规划。我们已成功构建了一个专为扩展而设计的平台；现在我们的重点转向<strong>最大限度地发挥其潜力</strong>。<br/></p><h2><strong>了解全貌</strong></h2><p>依赖管理工作流项目展示了一个更广泛的原则：<strong>当您需要将开源工具扩展到其默认部署模型之外时，原生的 Kubernetes 模式提供了前进的道路</strong>。</p><p>通过拥抱：</p><ul><li>用于配置的 CRD。</li><li>适用于生命周期管理的 Operator。</li><li>用于响应的事件驱动架构</li><li>用于部署的 GitOps。</li></ul><p>我们构建了可独立于所管理的存储库数量进行扩展的编排。无论管理的是 100 个还是 1,000 个存储库，扫描单个存储库的性能都相同。</p><p>公布关键的 CVE 时，我们现在能在几分钟内给出答案，而不是几小时。这就是瓶颈和竞争优势的区别。</p><h2><strong>致谢</strong></h2><p>该平台建立在优秀的开源工具之上：</p><ul><li><strong>Kubebuilder：</strong>用于启动 Kubernetes Operator 的开源框架，这些 Operator 可引导和编排工作流。[<a href="https://github.com/kubernetes-sigs/kubebuilder">1</a>][<a href="https://book.kubebuilder.io/">2</a>]</li><li><strong>后台：</strong>构建服务目录所基于的开源框架，也是我们获取事实依据的来源。[<a href="https://github.com/backstage/backstage">1</a>][<a href="https://backstage.io/">2</a>]</li><li><strong>Argo 工作流和 Argo 事件：</strong>用于编排复杂流程并基于事件添加动态处理的开源套件。[<span class="unknown__pt__mark__1e5711660421">1</span>][<a href="https://argo-workflows.readthedocs.io/en/stable/">2</a>][<a href="https://argoproj.github.io/argo-events/">3</a>][<a href="https://github.com/argoproj/argo-events">4</a>]</li><li><strong>Renovate CLI：</strong>处理存储库的开源依赖管理工具。[<a href="https://github.com/renovatebot/renovate">1</a>][<a href="https://docs.renovatebot.com/getting-started/running/">2</a>]</li></ul><p>*尽管我们的工作负载不一定在 AWS 上运行，而是在完整的 Kubernetes 集群上运行，但我们还是以 AWS Fargate 的定价模型作为单个 Pod 成本的参考。</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[通过 best_compression 提升搜索性能]]></title>
            <description><![CDATA[虽然 best_compression 通常被视为 Elastic Observability 和 Elastic Security 用例的存储节省功能，但本篇博客将展示其作为搜索性能调优工具的有效性。]]></description>
            <link>https://www.elastic.co/search-labs/cn/blog/improve-elasticsearch-performance-best-compression</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/cn/blog/improve-elasticsearch-performance-best-compression</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Sherry Ger, Ryan Eno]]></dc:creator>
            <pubDate>Fri, 23 Jan 2026 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/5141a8c2618337207d848ce16b258a86885955b2-1600x1034.jpg?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p></p><p>在为高并发工作负载调优 Elasticsearch 时，标准方法是最大限度地增加 RAM，将工作文档集保存在内存中，以实现低搜索延迟。因此，<a href="https://www.elastic.co/docs/reference/elasticsearch/index-settings/index-modules"><code>best_compression</code></a> 很少被考虑用于搜索工作负载，因为它主要被视为 Elastic Observability 和 Elastic Security 用例中优先考虑存储效率的节省存储措施。</p><p>在本博客中，我们证明当数据集大小显著超出操作系统页面缓存时，<code>best_compression</code>通过减少 I/O 瓶颈来提升搜索性能和资源效率。</p><h2><strong>设置</strong></h2><p>我们的用例是一个运行在 <a href="https://www.elastic.co/docs/deploy-manage/deploy/elastic-cloud/ec-change-hardware-profile#ec-profiles-compute-optimized-arm">Elastic Cloud CPU 优化实例</a>上的高并发搜索应用程序。</p><ul><li>数据量：约 5 亿份文档</li><li>基础架构：6 个 Elastic Cloud（Elasticsearch 服务）实例（每个实例：1.76 TB 存储 | 60 GB 内存 | 31.9 个 vCPU）</li><li>内存与存储比率：约 5% 的总数据集可存储在 RAM 中</li></ul><h2><strong>症状：高延迟</strong></h2><p>我们观察到，当当前请求数在 19:00 左右激增时，搜索延迟显著恶化。如图 1 和图 2 所示，尽管每个 Elasticsearch 实例的流量峰值约为每分钟 400 个请求，但平均查询服务时间仍恶化至超过 60 毫秒。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/f9c6cc1882e7db24336c65c54bbc1d38dcdb7fa3-697x311.png" style="width: 100%;" /><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/bbb705ae2fcd14c81d335bf322346caf3bf33765-996x618.png" style="width: 100%;" /><p>在完成初始连接处理后，CPU 使用率保持相对较低，表明计算并非瓶颈。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/cec15a28d2d22e9adedd2951bb2334b3717890a1-1494x730.png" style="width: 100%;" /><p>查询量与页面错误之间出现了强相关性。随着请求增加，我们观察到页面错误比例上升，峰值约为每分钟 40 万次。这表明活跃数据集无法完全放入页面缓存。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/511f2f10300a9d10ba3d7a82b9a8c8d567ac5636-1492x678.png" style="width: 100%;" /><p>同时，JVM 堆使用率也显示正常且平稳。这排除了垃圾回收问题，并确认瓶颈在于 I/O。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/336bbad638f866304358dba1d06ee987de0f23cf-1490x568.png" style="width: 100%;" /><h2><strong>诊断：I/O 瓶颈</strong></h2><p>系统存在 I/O 瓶颈。<a href="https://www.elastic.co/blog/elasticsearch-caching-deep-dive-boosting-query-speed-one-cache-at-a-time">Elasticsearch 依赖操作系统页面缓存从内存提供索引数据</a>。当索引过大而无法放入缓存时，查询会触发开销很大的磁盘读取。虽然典型的解决方案是水平扩展（添加节点/RAM），但我们希望先充分利用现有资源的效率改进。</p><h2><strong>解决方案</strong></h2><p>默认情况下，Elasticsearch 对其索引段使用 <a href="https://en.wikipedia.org/wiki/LZ4_(compression_algorithm)">LZ4</a> 压缩，在速度和大小之间取得平衡。我们假设，改用 <code>best_compression</code> （使用 <a href="https://en.wikipedia.org/wiki/Zstd">zstd</a>）会减少索引的大小。更小的占用空间使得更大比例的索引能够放入页面缓存，以微不足道的 CPU 增加（用于解压缩）换取磁盘 I/O 的减少。</p><p>为了启用 <code>best_compression</code>，我们使用索引设置 <code>index.codec: best_compression</code> 重新索引了数据。或者，也可以通过关闭索引、将索引编解码器重置为 <code>best_compression</code>，然后进行段合并，也可实现相同的结果。</p><pre><code>POST my-index/_close
PUT my-index/_settings
{
    "codec": "best_compression"
}
  
POST my-index/_open  
POST my-index/_forcemerge?max_num_segments=1</code></pre><h2><strong>结果</strong></h2><p>结果证实了我们的假设：存储效率的提高直接转化为搜索性能的大幅提升，而 CPU 利用率并未相应增加。</p><p>应用 <code>best_compression</code> 后，索引大小减少了约 25%。虽然低于在重复日志数据中观察到的减少幅度，但这 25% 的减少实际上将我们的页面缓存容量提升了相同的比例。</p><p>在下一次负载测试期间（从 17:00 开始），流量甚至更高，每个 Elasticsearch 节点的请求峰值达到每分钟 500 次。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/fc1902f396cb2115c0013155ad07f6eb87389c60-660x309.png" style="width: 100%;" /><p>尽管负载更高，但 CPU 利用率仍低于上一次运行。先前测试中较高的使用率可能是由于过多的页面错误处理和磁盘 I/O 管理开销所致。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/15699ef4c65b3f0a9f8a3e1bae8bb18f7b647025-819x352.png" style="width: 100%;" /><p>至关重要的是，页面错误显著下降。即使在更高的吞吐量下，错误次数也稳定维持在每分钟低于 20 万次，而基准测试中的错误次数则超过 30 万次。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/f76ca967976d740af88a9359b66041701abb46fc-764x340.png" style="width: 100%;" /><p>尽管页面错误结果仍然不太理想，但查询服务时间却减少了约 50%，即使在负载更重的情况下也保持在 30 毫秒以下。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/750ec1c59b8eb5069aed4c066d856ecea82d5bca-620x311.png" style="width: 100%;" /><p></p><h2><strong>结论：为搜索启用 best_compression</strong></h2><p>对于搜索用例中数据量超过可用物理内存的情况，<code>best_compression</code> 是一个强大的性能调优工具。</p><p>应对缓存未命中的常规解决方案是通过扩展来增加 RAM。然而，通过减少索引占用空间，我们实现了相同的目标：最大化页面缓存中的文档数量。我们的下一步是探索<a href="https://www.elastic.co/blog/space-savings-a-lesser-known-benefit-of-index-sorting-in-elasticsearch"><strong>索引排序</strong></a>，以进一步优化存储并从现有资源中获得更多性能。</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Agent Builder 现已正式发布：几分钟内即可部署上下文驱动型代理]]></title>
            <description><![CDATA[Agent Builder 现已正式发布。了解它如何帮助您快速开发上下文驱动的 AI 代理。]]></description>
            <link>https://www.elastic.co/search-labs/cn/blog/agent-builder-elastic-ga</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/cn/blog/agent-builder-elastic-ga</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Anish Mathur, Evan Castle]]></dc:creator>
            <pubDate>Thu, 22 Jan 2026 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/6840eb7dbb884055ab0e965dcfd614fec54936af-2210x1440.png?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>我们非常高兴地宣布，Agent Builder 在 Elastic Cloud Serverless 和即将发布的 9.3 版本中正式推出。Agent Builder 带来了 Elasticsearch 作为上下文工程平台的强大功能，能够快速开发以数据为中心的上下文 AI 代理。</p><p>代理正凭借其提升效率和改善客户体验方面的潜力而日益受到重视。但在实践中，为代理提供正确的上下文是困难的，尤其是在处理杂乱无章的非结构化企业数据时。开发人员必须管理工具、提示、状态、推理逻辑、模型，最重要的是从业务来源检索相关上下文，以提供准确的结果和操作。Elastic Agent Builder 提供这些核心组件，用于开发安全、可靠、上下文驱动的代理。</p><h2>Agent Builder 核心功能</h2><p>Agent Builder 利用 Elastic 在搜索相关性和检索增强生成方面的长期投入，并致力于将 Elasticsearch 打造成最佳的向量数据库，从而简化以数据为中心的上下文 AI 代理的开发。</p><p>Agent Builder 允许您：</p><ul><li>立即开始使用内置的会话代理，它可以回答问题、执行分析并驱动对 Elasticsearch 中任何数据的调查。</li><li>快速从复杂的非结构化数据转变为具有基于配置的开发体验的自定义代理。</li><li>利用内置的 ES|QL 或自定义工具，利用最佳的混合搜索相关性来提高上下文质量和代理可靠性。</li><li>将复杂的工作流（预览）作为可重复使用的工具来执行，以丰富数据、更新记录、发送消息等，实现基于规则的自动化。</li><li>使用工作流和 MCP 连接 Elasticsearch 外部的数据源，以关联和整合代理的上下文。</li><li>使用通过 MCP 提供的内置和自定义工具与任何代理或应用程序框架集成，并能够连接到外部 MCP（预览版）、支持 A2A 和提供完整的 API 支持。</li><li>通过与第三方解决方案（如用于复杂文档处理的 LlamaIndex 或用于安全、结构化工具访问的 Arcade.dev）集成，扩展 Agent Builder 的功能。</li></ul><p>为了进一步扩展 Agent Builder 的功能，我们推出了 Elastic Workflows，这是我们新的基于规则的自动化功能，目前处于技术预览阶段。对于组织任务，代理有时需要基于规则的操作的确定性和可靠性，这通常是实现特定业务逻辑所必需的。Elastic Workflows 为代理提供了一种简单、声明式的方式，用于编排内部和外部系统，以执行操作、收集和转换数据和上下文。工作流是完全可组合、事件驱动且灵活的，并且可以通过 MCP 作为工具提供给代理。</p><h2>从数据到代理仅需几分钟</h2><p>开发代理可能需要花费数周的前期工作来整合独立的数据存储、构建手动管道、调整查询和管理复杂的编排。Agent Builder 不需要单独的数据存储、向量数据库、RAG 管道、搜索层、查询转换器和工具编排器，从而减少了开发代理的时间，使您能够专注于代理逻辑和应用程序交付。</p><p>Agent Builder 原生集成了 Elasticsearch 平台的基元，从而加快了代理开发的速度。</p><ul><li>首先，内置的对话代理可以立即与您的索引数据进行聊天和推理。</li><li>通过 Kibana、API 或 MCP 和 A2A 进行交互式访问，将代理集成到应用程序、仪表板或 CI/CD 系统中。</li><li>使用默认工具进行构建，以了解您的数据结构，选择适当的索引，生成优化的混合、语义和结构化查询，并根据自然语言提示使用 ES|QL 创建可配置的可视化。</li></ul><p>要深入了解，请尝试完整的<a href="https://www.elastic.co/search-labs/blog/ai-agent-builder-elasticsearch">实践演练</a>。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/b55b63eae40f72952967cc8f3ea4df4cd62d7d70-1080x608.gif" style="width: 100%;" /><h2>基于 Elasticsearch 构建，这是一个用于上下文工程的完整数据平台</h2><p>对于 AI 代理，上下文质量对于提供有效的推理和降低幻觉的风险至关重要。对于许多企业 AI 代理而言，执行任务所需的业务数据是最关键的上下文信息。作为大规模可扩展数据存储、向量数据库和相关性领域的领导者，Elasticsearch 已经提供了许多强大的上下文工程原语。上下文工程超越了简单的检索增强生成，允许您定制和扩展数据获取、排序、筛选和呈现给代理的方式，有助于减少噪音和歧义。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/419b9b6f13739e0a8983249d8ac31478e73dac89-1600x901.png" style="width: 100%;" /><p>Elasticsearch 提供的上下文引擎结合了词汇搜索、向量搜索和结构化筛选，通过确保模型在相关且精确的上下文中运行，显著<a href="https://www.elastic.co/search-labs/blog/context-engineering-relevance-ai-agents-elasticsearch">提高了 LLM 性能</a>。这种功能由代理检索提供支持，同时还具备内置工具和搜索逻辑，能够自动选择正确的索引，并将自然语言转化为针对上下文优化的查询。</p><p>利用 Agent Builder，您可以确保代理首先获得最有用的上下文，并设有相关性和排名控制，从而允许您微调评分、排名和筛选逻辑。Elasticsearch 可让您控制重要内容、重要原因以及优先级，而非依赖不透明的检索行为。这一切都由 Elasticsearch 作为可扩展性平台提供支持，可以在单个平台上存储和扩展来自文本、向量、元数据、日志等的所有数据，从而更轻松地管理代理的上下文。</p><h2>将复杂的工作流作为可重复使用的工具来执行</h2><p>虽然 AI 代理可以对复杂的任务进行推理，但许多自动化工作都依赖于可靠地执行基于规则的操作，以强制实施特定的业务逻辑。Elastic Workflows 提供了一种简单、声明式的方式来编排内部和外部系统，以执行操作、收集上下文或数据，并将其整合为代理的一部分。在 YAML 中定义的工作流是完全可组合的，这使得它们能够根据任务需求变得简单或复杂。这为代理提供了在 Elasticsearch 平台和解决方案以及第三方应用程序中执行操作的有效方式。</p><p>使用 Agent Builder 集成工作流可通过三个步骤完成（先决条件：启用工作流，详情请参阅<a href="https://github.com/elastic/workflows">此处</a>）</p><p>1. 使用内置自动完成和测试功能的基于 YAML 的简单编辑器创建并保存新的工作流。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/308888bf3d2fa013f9391a55be6a6fbd458b6dac-1600x998.png" style="width: 100%;" /><p>2. 在 Agent Builder 中创建一个类型为“工作流”的新工具，并提供说明，帮助代理确定何时使用工作流工具。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/c04810d30d226112c3610bd58e208607b213fc3d-1600x945.png" style="width: 100%;" /><p>3. 将工作流工具添加到您的自定义代理。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/724cd4ac93c46efb0d339fd140e5caf138f8150f-1600x948.png" style="width: 100%;" /><p>4. 就是这样！现在，代理可以在对话中调用工作流。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/8dfdd726ab89e31c48b79372650ce33946713dca-1600x929.png" style="width: 100%;" /><h2>您的代理，您的规则</h2><p>Agent Builder 不会将您限制在单一的开发范式中。相反，它旨在为代理提供开放、灵活的开发方法，使代理能够完全掌控数据、相关性、模型、互操作性、安全性和代理设计。</p><p>自定义代理定义可让您准确选择代理可访问的工具、嵌入自定义系统提示、定制代理的指令并定义安全边界。代理仍然与模式无关，让您能够灵活配置首选的本地和跨更广泛生态系统的 LLM，而无需受限于单一提供商。</p><p>构建可扩展的工具，将特定领域的逻辑（例如特定的索引筛选器、ES|QL 连接、分析管道）封装起来，并对其加以约束，以确保其在生产环境中安全使用。完整的 API 支持实现了与其他代理框架的互操作性，并原生支持模型上下文协议 (MCP)。A2A 集成意味着您可以将 Elastic 代理提供给其他框架、服务和客户端应用，从而在各种集成中重复使用相同的数据和上下文工程逻辑。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/5e903ba24ffb3f40231e901f63bd494c89cb7757-1600x1004.png" style="width: 100%;" /><p>Agent Builder 支持灵活、开放的开发，可与流行的代理框架和平台轻松集成。这些集成对于提供高效的代理至关重要。正如 <strong>Arcade.dev 的联合创始人 Sam Partee</strong> 所描述的那样，</p><p><em>&quot;如今，代理系统之所以失败，是因为将 AI 与工具和数据连接起来非常复杂。Elastic Agent Builder 与 Arcade.dev 结合，为开发者提供了一种结构化且安全的方式来处理代理如何检索上下文、进行推理和采取行动，从而将代理从演示级别提升至生产级别。”</em></p><p>Agent Builder 还利用了 Elasticsearch 的可扩展性来处理复杂数据。正如 <strong>LlamaIndex 首席执行官 Jerry Liu</strong> 所描述的那样，</p><p><em>“从非结构化数据源中解锁企业上下文是建立有效代理的关键。Elastic Agent Builder 与 LlamaIndex 复杂文档处理相结合，强化了关键的上下文层，帮助团队检索、处理和准备数据，从而使代理能够更准确地推理并提供更好的结果。”</em></p><h2>您可以构建什么？</h2><p>Agent Builder 已用于各种用例。以下是一些示例和参考架构，可帮助您开始使用代理：</p><ul><li><strong>基础设施自动化：</strong>在支持场景中，代理已被用于读取、思考和聊天，但迄今为止，它们还无法触及可能需要管理的基础设施。Elastic 的工程团队在黑客马拉松中构建了一个用于<a href="https://www.elastic.co/search-labs/blog/agent-builder-augmented-infrastructure">自动化基础设施管理</a>的代理。代理会主动调查应用程序基础设施的问题，并采取自动化操作。它使用工作流来优化配置、响应问题并扩展资源，所有这些都基于对基础设施日志的智能理解。</li><li><strong>安全威胁分析：</strong>已使用 Elastic Agent Builder、MCP 和 Elasticsearch 开发了一个安全漏洞代理。它通过将内部安全数据与外部威胁情报关联起来，自动进行威胁分析。该代理对历史事件和配置进行语义搜索，使用实时互联网数据增加结果，并应用 LLM 推理来评估环境相关性、确定风险优先级并生成可操作的补救措施。请参阅<a href="https://www.elastic.co/search-labs/blog/agent-builder-mcp-reference-architecture-elasticsearch">参考架构</a><strong>。</strong></li><li><strong>技术客户支持：</strong>代理可以执行多项支持任务，包括案例汇总、问题重复检测和创建，以及深入的技术调查。Agent Builder 可通过多步骤混合搜索实现这一功能，仅查找最相关的相关问题、解决方案和程序，并制定根本原因假设和补救计划。Agent Builder 可以简化复杂<a href="https://www.elastic.co/blog/generative-ai-customer-support-elastic-support-assistant">支持系统</a>的架构，并加快交付时间。</li><li><strong>产品和内容发现：</strong>Agent Builder 简化了<a href="https://www.elastic.co/search-labs/blog/build-voice-agents-elastic-agent-builder">将复杂的产品目录用于对话式体验</a>的过程，同时允许组织保持灵活性，以纳入其自身的业务逻辑和要求。</li><li><strong>构建您自己的系统：</strong>参加 <a href="https://elasticsearch.devpost.com/">Agent Builder 黑客松活动</a>，该活动将于 2026 年 1 月 22 日至 2 月 27 日举行。与社区合作，构建基于上下文的多步骤 AI 代理，将搜索、工作流、工具和推理相结合，以自动执行现实世界中的任务*</li></ul><h2>现在开始构建自定义代理</h2><p>开始 <a href="https://cloud.elastic.co/registration?onboarding_token=search&amp;pg=en-enterprise-search-page">Elastic Cloud 试用</a>，并在<a href="https://www.elastic.co/docs/solutions/search/elastic-agent-builder">此处</a>查看文档。对于现有客户，Agent Builder 可在 Cloud Serverless 中使用，也可在 Elastic Cloud Hosted 和自主管理的企业层中使用。</p><p>*<a href="https://elasticsearch.devpost.com/rules">点击此处</a>查看黑客松的完整条款、条件和资格要求</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[使用 Elastic Agent Builder 构建语音代理]]></title>
            <description><![CDATA[探索语音代理的工作原理，并了解如何使用 Elastic Agent Builder 和 LiveKit 构建语音代理。]]></description>
            <link>https://www.elastic.co/search-labs/cn/blog/build-voice-agents-elastic-agent-builder</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/cn/blog/build-voice-agents-elastic-agent-builder</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Kenneth Kreindler]]></dc:creator>
            <pubDate>Thu, 22 Jan 2026 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/43ceabb9e2c0966261c188bd40e03178d5a91e5c-1280x720.png?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>一直以来，AI 仿佛被关在玻璃盒子里：您输入命令，它用文字回应，交互就此结束。虽然能解决问题，但总显得疏离，好比隔着屏幕看别人行动。到 2026 年，企业会打破这层“玻璃”，将 AI 代理真正嵌入业务产品之中，让它们真正创造价值。</p><p>打破这层“玻璃”的一种方式，是引入<em>语音代理</em> — 这种 AI 代理能够识别人类语音，并合成计算机生成的音频。随着低延迟转写、快速大型语言模型（LLM）以及听起来与人声相近的文本转语音模型的兴起，这一切已经成为可能。</p><p>语音代理还需要能够访问业务数据，才能真正发挥价值。在这篇博客中，我们将先介绍语音代理的工作原理，再通过 <a href="https://livekit.io/">LiveKit</a> 和 <a href="https://www.elastic.co/elasticsearch/agent-builder">Elastic Agent Builder</a> 为虚构的户外运动装备商店 ElasticSport 构建一个语音代理。我们的语音代理能够感知上下文，并与我们的数据协同工作。</p><h2>运作方式</h2><p>语音代理领域主要有两种范式：第一种使用语音到语音模型，第二种使用由语音转文本、LLM 和文本转语音组成的语音处理流水线。语音到语音模型有其自身优势，但语音处理流水线在所用技术、上下文管理方式以及代理行为的控制方面提供了更多自定义空间。下文将重点介绍语音处理流水线模型。</p><h3>关键组件</h3><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/b237501903f9c3a71fe1b7755c3990e40c5495c8-1600x653.png" style="width: 100%;" /><h4>转写（语音转文本）</h4><p>转写模块是语音处理流水线的入口。转写组件以原始音频帧为输入，将语音转写为文本并输出。转写得到的文本会被缓存在系统中，直到系统检测到用户已停止说话，此时才会启动 LLM 生成。目前有多家第三方提供商提供低延迟转写服务。在选择提供商时，需要考虑延迟和转写准确性，并确保其支持流式转写。</p><p></p><p>第三方 API 示例：<a href="https://www.assemblyai.com/">AssemblyAI</a>、<a href="https://deepgram.com/product/speech-to-text">Deepgram</a>、<a href="https://platform.openai.com/docs/guides/realtime-transcription">OpenAI</a> 和 <a href="https://elevenlabs.io/speech-to-text">ElevenLabs</a></p><h4>轮次检测</h4><p>轮次检测是流水线中的一个组件，用于检测说话者何时讲完，从而确定何时开始生成回复。一种常见的方法是使用语音活动检测（VAD）模型，例如 <a href="https://github.com/snakers4/silero-vad">Silero VAD</a>。VAD 利用音频能量水平来检测音频中何时包含语音以及语音何时结束。但是，单独使用 VAD 无法区分暂时停顿与真正结束发言。因此，通常会将它与句末检测模型结合使用，该模型基于临时转写结果或原始音频来判断说话者是否已经说完。</p><p>示例（Hugging Face）：<a href="https://huggingface.co/livekit/turn-detector">livekit/turn-detector</a>、<a href="https://huggingface.co/pipecat-ai/smart-turn-v3">pipecat-ai/smart-turn-v3</a></p><h4>代理</h4><p>代理是语音处理流水线的核心。它负责理解用户意图、收集合适的上下文，并以文本形式生成回复。<a href="https://www.elastic.co/elasticsearch/agent-builder">Elastic Agent Builder</a> 凭借其内置的推理能力、工具库和工作流集成，使代理可以在您的数据之上工作，并与外部服务进行交互。</p><h4>LLM（文本到文本）</h4><p>在为 Elastic Agent Builder 选择 LLM 时，主要需要考虑两个指标：推理能力基准和首个 Token 时间（TTFT）。</p><p>推理基准反映 LLM 生成正确响应的能力水平。可以重点关注衡量多轮对话一致性和整体智能水平的基准，比如 MT-Bench 和 Humanity&#x27;s Last Exam 等数据集。</p><p>TTFT 基准用于评估模型产出第一个输出 Token 的速度。还有其他类型的延迟基准，但 TTFT 对语音代理尤为重要，因为在收到第一个 Token 后就可以开始音频合成，从而降低轮次之间的延迟，让对话更自然。</p><p>通常需要在这两个指标之间做权衡，因为速度更快的模型在推理基准测试中的表现往往较差。</p><p><a href="https://huggingface.co/openai/gpt-oss-20b">示例（Hugging Face）：openai/gpt-oss-20b</a>、<a href="https://huggingface.co/openai/gpt-oss-120b">openai/gpt-oss-120b</a></p><h4>合成（文本转语音）</h4><p>流水线的最后一环是文本转语音模型。该组件负责将 LLM 输出的文本转换为可听的语音。与 LLM 类似，在选择文本转语音提供商时也需要重点关注延迟这一指标。文本转语音的延迟通过首字节时间（TTFB）来衡量。即接收到第一个音频字节所需的时间。TTFB 越低，对话轮次之间的延迟也越低。</p><p>示例： <a href="https://elevenlabs.io/text-to-speech-api">ElevenLabs</a>、 <a href="https://cartesia.ai/sonic">Cartesia</a>、 <a href="https://www.rime.ai/">Rime</a></p><h4>构建语音处理流水线</h4><p>Elastic Agent Builder 可以在多个不同层级集成到语音处理流水线中：</p><ol><li>仅限 Agent Builder 工具：语音转文本 → LLM（使用 Agent Builder 工具） → 文本转语音</li><li>Agent Builder 作为 MCP：语音转文本 → LLM（通过 MCP 访问 Agent Builder）→ 文本转语音</li><li>Agent Builder 作为核心：语音转文本 → Agent Builder → 文本转语音</li></ol><p>在本项目中，我选择采用“Agent Builder 作为核心”的方案。采用这种方案，可以充分利用 Agent Builder 及其工作流的全部功能。该项目使用 LiveKit 来编排语音转文本、轮次检测和文本转语音环节，并实现了一个自定义的大型语言模型节点，直接与 Agent Builder 集成。</p><h2>Elastic 客服语音代理</h2><p>我们将为一家名为 ElasticSport 的虚构体育用品商店构建一个自定义客服语音代理。顾客可以拨打服务热线，咨询产品推荐、查看产品详情、查询订单状态，并通过短信接收订单信息。为此，我们首先需要配置一个自定义代理，并创建用于执行 Elasticsearch 查询语言（ES|QL）查询和工作流的工具。</p><a href="https://videos.elastic.co/watch/jpX6D21URZuh3PtXVV7Ks6">Watch video on Vidyard</a><h3>配置代理</h3><h4>提示词</h4><p>提示词用于告知代理应采用怎样的人设以及如何作答。更重要的是，其中还包含一些专门针对语音场景的提示词，用于确保响应能正确合成为音频，并在出现误解时实现自然的纠正。</p><pre><code>You are a Sales Assistant at ElasticSport, an outdoor sport shop specialized in hiking and winter equipment. 

[Profile]
- name: Iva
- company: ElasticSport
- role: Sales Assistant
- language: en-GB
- description: ElasticSport virtual sales assistant

[Context]
- Ask clarifying questions to understand the context.
- Use available tools to answer the user's question.
- Use the knowledge base to retrieve general information

[Style]
- Be informative and comprehensive.
- Maintain a professional, friendly and polite tone.
- Mimic human behavior and speech patterns.
- Be concise. Do not over explain initially

[Response Guideline]
- Present dates in spelled-out month date format (e.g., January fifteenth, two thousand and twenty-four).
- Avoid the use of unpronounceable punctuation such as bullet points, tables, emojis.
- Respond in plain text, avoid any formatting.
- Spell out numbers as words for more natural-sounding speech.
- Respond in short and concise sentences. Responses should be 1 or 2 sentences long.

[ERROR RECOVERY]
### Misunderstanding Protocol
1. Acknowledge potential misunderstanding
2. Request specific clarification</code></pre><h4>工作流</h4><p>我们将添加一个小型工作流，通过 Twilio 的消息传递 API 发送短信。该工作流会作为工具提供给自定义代理，使其在通话过程中即可向来电者发送短信，从而带来顺畅的使用体验。例如，这样一来，来电者就可以询问：“你能通过短信发送更多关于 <em>X</em> 的详细信息吗？”</p><pre><code>name: send sms
enabled: true
triggers:
  - type: manual
inputs:
  - name: message
    type: string
    description: The message to send to the phone number.

  - name: phone_number
    type: string
    description: The phone number to send the message to.

consts:
  TWILIO_ACCOUNT: "****"
  BASIC_AUTH: "****"
  FROM_PHONE_NNUMBER: "****"
steps:
  - name: http_step
    type: http
    with:
      url: https://api.twilio.com/2010-04-01/Accounts/{{consts.TWILIO_ACCOUNT}}/Messages.json
      method: POST
      headers:
        Content-Type: application/x-www-form-urlencoded
        Authorization: Basic {{consts.BASIC_AUTH | base64_encode}}
      body: From={{consts.FROM_PHONE_NNUMBER}}&To={{inputs.phone_number}}&Body={{inputs.message}}
      timeout: 30s</code></pre><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/b057e71b0a7c50eb3da47cd4f95e77ec7b4c6126-1600x1245.png" style="width: 100%;" /><h4>ES|QL 工具</h4><p>借助以下工具，代理可以基于真实数据提供相关的回复。示例代码库包含一个设置脚本，用于将产品、订单和知识库数据集导入并初始化 Kibana。</p><ul><li><strong>Product.search</strong></li></ul><p>产品数据集中包含 65 个虚构产品。以下是一个示例文档：</p><pre><code>{
      "sku": "ort3M7k",
      "name": "Ortovox Free Rider 26 Backpack",
      "price": 189,
      "currency": "USD",
      "image": "https://via.placeholder.com/150",
      "description": "The Ortovox Free Rider 26 is a technical freeride backpack with a dedicated safety compartment and diagonal ski carry system. Perfect for backcountry missions.\n\nKey Features:\n- 26L capacity\n- Diagonal ski carry system\n- Safety equipment compartment\n- Helmet holder\n- Hydration system compatible",
      "category": "Accessories",
      "subCategory": "Backpacks",
      "brand": "Ortovox",
      "sizes": ["One Size"],
      "colors": ["Black", "Blue", "Orange"],
      "materials": ["Nylon", "Polyester"]
    }</code></pre><p>通过将名称和描述字段映射为 <code>semantic_text</code>，LLM 就能借助 ES|QL 执行语义搜索并检索到匹配的产品。混合搜索查询会在这两个字段上执行语义匹配，并通过 boost 略微提高名称字段匹配结果的权重。</p><p>该查询首先检索按初始相关度得分排序的前 20 个结果。随后，这些结果会借助 <code>.rerank-v1-elasticsearch</code> 推理模型，基于描述字段重新排序，并最终缩减为最相关的前五个产品。</p><pre><code>type: ES|QL
toolId: products.search
description: Use this tool to search through the product catalogue by keywords.
query: |
    FROM products
        METADATA _score
      | WHERE
          MATCH(name, ?query, {"boost": 0.6}) OR
            MATCH(description, ?query, {"boost": 0.4})
      | SORT _score DESC
      | LIMIT 20
      | RERANK ?query
            ON description
            WITH {"inference_id": ".rerank-v1-elasticsearch"}
      | LIMIT 5

parameters:
    query: space separated keywords to search for in catalogue</code></pre><ul><li><strong>Knowledgebase.search</strong></li></ul><p>知识库数据集包含以下格式的文档，其中标题和内容字段以语义文本形式存储：</p><pre><code>{
        id: "8273645",
        createdAt: "2025-11-14",
        title: "International Orders",
        content: `International orders are processed through our international shipping partner. Below are the countries we ship to and average delivery times.
        Germany: 3-5 working days
        France: 3-5 working days
        Italy: 3-5 working days
        Spain: 3-5 working days
        United Kingdom: 3-5 working days
        United States: 3-5 working days
        Canada: 3-5 working days
        Australia: 3-5 working days
        New Zealand: 3-5 working days
        `
}</code></pre><p>该工具使用的查询与 <code>product.search</code> 工具类似：</p><pre><code>type: "ES|QL"
toolId: knowledgebase.search
description: Use this tool to search the knowledgebase.
query: |
  FROM knowledge_base
    METADATA _score
  | WHERE
      MATCH(title, ?query, {"boost": 0.6}) OR
      MATCH(content, ?query, {"boost": 0.4})
  | SORT _score DESC
  | LIMIT 20
  | RERANK ?query
      ON content
      WITH {"inference_id": ".rerank-v1-elasticsearch"}
  | LIMIT 5

parameters:
  query: space separated keywords or natural language phrase to semantically search for in the knowledge base</code></pre><ul><li><strong>Orders.search</strong></li></ul><p>我们要添加的最后一个工具用于通过 <code>order_id</code> 检索订单：</p><pre><code>type: "ES|QL"
toolId: order.search
description: Use this tool to retrieve an order by its ID.
query: |
  FROM orders
    METADATA _score
  | WHERE order_id == ?order_id
  | SORT _score DESC
  | LIMIT 1

parameters:
  order_id: "the ID of the order"</code></pre><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/d22bdd540a95b5a9c2bd5f308620835e8e6f7ecb-1600x1361.png" style="width: 100%;" /><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/d91709a50fb5391876b714885242d998b2b21027-1600x1443.png" style="width: 100%;" /><p>在完成代理配置并将这些工作流和 ES|QL 工具关联到代理之后，即可在 Kibana 中对其进行测试。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/8691f41624247a6b1352d158c970031e1426ce5e-1600x1056.png" style="width: 100%;" /><p>除了为 ElasticSport 构建客服代理外，还可以将该代理、工作流和工具拓展到其他场景，例如甄别潜在客户的销售代理、家庭维修服务代理、餐厅预订代理或预约安排代理。</p><p></p><p>最后一部分是将刚创建的代理与 LiveKit、文本转语音模型和语音转文本模型连接起来。本博客末尾链接的代码仓库中包含一个可与 LiveKit 搭配使用的自定义 Elastic Agent Builder LLM 节点。只需将 <code>AGENT_ID</code> 替换为您自己的值，并将其与 Kibana 实例关联即可。</p><h2>开始使用</h2><p>点击<a href="https://github.com/KDKHD/elastic_agent_builder_livekit">此处</a>查看代码并动手体验。 </p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Agent Builder，超越聊天框：介绍增强型基础架构]]></title>
            <description><![CDATA[了解具有增强型基础架构的 Elastic Agent Builder，这是一个 AI 智能体，能够实现增强型运维、增强型开发和增强型合成。]]></description>
            <link>https://www.elastic.co/search-labs/cn/blog/agent-builder-augmented-infrastructure</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/cn/blog/agent-builder-augmented-infrastructure</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Alexander Wert, Bill Easton, Gil Raphaelli, Graham Hudgins, Norrie Taylor]]></dc:creator>
            <pubDate>Thu, 22 Jan 2026 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/e08daf78f328e826f39d06329f6a5487f75d178d-1272x700.png?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p><strong>这不是空谈。我们正在付诸实践。</strong></p><p>我们都见证了 AI 智能体的兴起。它们在总结文本、编写代码片段以及基于文档回答问题方面表现出色。但对于我们从事 DevOps 和网站可靠性工程 (SRE) 的人来说，一直存在一个令人沮丧的限制。大多数智能体都困于呼叫中心模式，这意味着它们可以阅读、思考和聊天，但无法触及它们本该管理的基础架构。</p><p>在最新的黑客马拉松项目中，我们决定打破这一限制。</p><p>我们构建了<strong>增强型基础架构</strong>：这是一个基础架构协同助手，它不仅能为您提供建议，还能创建、部署、监测和修复您的实时环境。</p><h2><strong>问题：复制、重新格式化、粘贴</strong></h2><p>标准智能体在孤立状态下运行。如果您的应用宕机，给公司造成 500万美元的损失，标准智能体可以为您朗读如何修复的应急预案手册。但<em>您</em>仍然需要亲自动手。您只能复制代码，根据环境重新格式化，然后粘贴到终端中。</p><p>我们需要一个能理解<em>谈论</em> Kubernetes 和<em>配置</em> Kubernetes 之间区别的智能体。</p><h2><strong>引擎：什么是 Elastic Agent Builder？</strong></h2><p>我们并不是从零开始构建的。我们是基于 <a href="https://www.elastic.co/elasticsearch/agent-builder"><strong>Elastic Agent Builder</strong></a> 构建的。对于不熟悉的人来说，Elastic Agent Builder 是一个旨在快速开发智能体的框架，它充当大型语言模型 (LLM)（在我们的演示中，我们使用了 Google Gemini）与存储在 Elasticsearch 中的私有数据之间的桥梁。</p><p>Agent Builder 可以通过将 AI 与内部数据（如文档或日志）相结合，用于对话式 AI。但它最强大的功能是能够分配<strong>工具</strong>。这些工具允许 LLM 跳出聊天接口，执行特定任务。我们意识到，如果将此功能发挥到极致，我们可以将 Agent Builder 转变为一个自动化引擎。</p><h2><strong>使其运行：构建第一个版本</strong></h2><p>在项目启动之初，我们就知道要让智能体能够改变外部世界。我们当时有个想法：如果我们开发一些“运行器”软件（在主机上运行智能体能想到的任何命令）会怎样？然后：如果运行器、Elastic Agent Builder 和用户进行三方通话会怎样？</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/8dc8317c1301b8eb7b89438529e8d8d17411c95a-1024x559.png" style="width: 100%;" /><p>我们首先构建了一个 Python 项目“增强型基础架构运行器”，其本质是一个 <code>while(true)</code> 循环，每秒查询 Elastic Agent Builder 对话 API，并检查我们创建的特殊语法：</p><pre><code>{
	"tool_name": "my_tool",
       "tool_arguments": "\{stringified json arguments\}"
}</code></pre><p>然后我们更新了提示，以教会它我们新的工具调用语法。Bill 是 FastMCP 的维护者，<a href="https://gofastmcp.com/getting-started/welcome">FastMCP</a> 是在 Python 中构建模型上下文协议 (MCP) 服务器的最常用框架。他开始尝试使用 FastMCP 客户端配合这个新的运行器软件，来挂载 MCP 服务器并使其工具对运行器可用。当智能体看到这个时，它会执行工具调用，并将 <code>POST</code> 结果返回到对话中，就像用户发送了结果一样。这会触发 LLM 对结果作出回应，然后我们开始了！</p><p>这很好，但存在两个主要问题：</p><ol><li>代理会将所有这些 JSON 直接注入到与用户的对话中。</li><li>通过对话 API 能看到消息的最早时间点是一个对话轮次完成时（即 LLM 回复时）。</li></ol><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/7b3755aeae17722ff1bb9677712293e9195f96a0-1058x1034.png" style="width: 100%;" /><p>因此，我们着手研究如何将其移至后台。</p><p>然后我们切换到为智能体提供一个名为 <code>call_external_tool</code> 的工具，该工具有两个参数：<code>tool_name</code> 和字符串化的 JSON 工具参数。这个外部工具调用不会返回任何内容，但重要的是，它会在对对话 API 的 <code>GET</code> 请求中可见。然后，我们授予运行器直接将文档写入 Elasticsearch 的权限，Elastic Agent Builder 智能体可以根据需要检索这些文档。智能体总是在响应用户消息的情况下运行，所以我们需要用一个用户消息来启动智能体，这样它才会去查找结果并继续处理。因此，我们让智能体在聊天记录中插入一条简短的消息，以继续对话：</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/61ff59a57c68ed5fad492d19c0580644113a507d-1600x1321.png" style="width: 100%;" /><p>所以现在我们有了外部工具调用。然而，由于上面提到的第二个问题，我们不得不去掉最后的启动部分。否则，每个外部工具调用都需要一个完整的对话轮次来检索结果！</p><h2><strong>让它变得更好：介绍工作流</strong></h2><p>除了 Elasticsearch 查询语言 (ES|QL) 和索引搜索工具调用之外，Agent Builder 智能体还可以调用 Elastic 基于工作流的工具。Elastic 工作流提供了一种灵活且易于管理的方式来执行任意顺序和逻辑的操作。就我们的目的而言，我们只需要工作流做两件事：将外部工具请求存储到 Elasticsearch，并返回一个用于轮询结果的 ID。这产生了以下简单的工作流定义：</p><pre><code>name: ai-tool-call
enabled: true
triggers:
  - type: manual
inputs:
  - name: runner_id
    type: string
  - name: tool_calls
    type: string

steps:
  - name: store_request
    type: elasticsearch.create
    with:
      index: distributed-tool-requests
      id: "{{inputs.runner_id}}_{{ execution.id }}"
      document:
        request_id: "{{ execution.id }}"
        runner_id: "{{inputs.runner_id}}"
        tool_call: "{{inputs.tool_calls}}"
        status: "unhandled"

  - name: output_result
    type: console
    with:
      message: "Called tool, with execution id: {{ execution.id }}. Use this ID to poll the results."</code></pre><p>这样，运行器不再依赖将工具调用请求写入对话，而只需轮询 Elasticsearch <code>distributed-tool-requests</code> 索引中的新外部工具请求，并使用提供的 <code>execution.id</code> 将结果报告回另一个 Elasticsearch 索引。</p><p>这消除了上述两个主要问题：</p><ol><li>对话历史记录不再被外部工具调用的负载所充斥。</li><li>由于运行器轮询的是 Elasticsearch 索引而非对话历史记录，它们不会因需要等待对话轮次完成以使外部工具请求可见而被阻塞。</li></ol><p>第二点有一个巨大优势：外部工具调用的处理在智能体的思考阶段就开始了（而不是在对话轮次完成之后）。这允许我们在系统提示中指示 LLM 轮询外部工具结果，直到结果可用，从而消除了启动消息的需要。总的来说，这样做的好处是对话感觉更加自然：LLM 可以在单个对话轮次中处理多个外部工具请求（而不是每个工具请求需要一个对话轮次），因此可以一次性完成更复杂的用户请求。</p><h2><strong>将所有内容整合到一起</strong></h2><p>为了弥合 LLM 与服务器机架之间的鸿沟，我们利用 Agent Builder 的工具功能开发了一种特定的架构：</p><ol><li><strong>增强型基础架构运行器：</strong>我们在目标环境（服务器、Kubernetes 集群、云账户）中部署了轻量级运行器。这些运行器直接连接到 Elastic，使用受保护的终端和仅每个运行器可用的密钥。</li><li><strong>ES|QL 检索：</strong>该协同助手使用 Elastic 的 <strong>ES|QL</strong> 执行混合搜索。它不仅搜索知识，还会搜索<em>功能</em>。它查询已连接的运行器，查看哪些工具可用（例如<code>list_ec2_instances</code>、<code>install_helm_chart</code>)。</li><li><strong>工作流执行：</strong>一旦智能体决定行动方案，就会创建一个结构化的工作流。</li><li><strong>反馈循环：</strong>运行器在本地执行命令并将结果报告到 Elasticsearch。协同助手从索引中读取结果，并决定下一步。</li></ol><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/76be256da722c1965971fc506502768bd890f0c4-1290x1076.png" style="width: 100%;" /><h2><strong>演示：从故障到可观测性</strong></h2><a href="https://videos.elastic.co/watch/bRgPJfJLeABjPMMhvy1sbB">Watch video on Vidyard</a><p>在视频中，我们展示了两个不同的场景，彰显了该架构的支持。</p><h3><strong>场景 1：DevOps 开发运维救援</strong></h3><p>我们从一位用户因 Kubernetes 集群中的盲点导致 500 万美元宕机而惊慌失措的场景开始。</p><ul><li><strong>请求：</strong>“如何确保这种情况不再发生？”</li><li><strong>行动：</strong>该智能体不只是提供了教程。它识别了集群，创建了必要的命名空间，生成了 Kubernetes 密钥，安装了 OpenTelemetry Operator，并立即提供了一个指向实时 APM 仪表板的链接。</li><li><strong>结果：</strong>用户无需编写一行 YAML，即可获得完整的 Kubernetes 可观测和应用见解。</li></ul><h3><strong>场景 2：安全交接</strong></h3><p>基础架构安全的一条基本规则是，您无法保护看不到的东西。在执行我们的 DevOps 开发运维救援时，智能体看到了改善环境安全的机会。</p><p>借助之前一次与 Elastic Observability 相关调查触发的警报，我们展示了安全从业者如何直接与其基础架构聊天：首先，列举云环境中的资产和资源；其次，部署确保环境安全的必要工具。</p><ul><li><strong>发现：</strong>协同助手为安全从业者列举了 AWS 资源，并识别出一个关键缺口：一个 Amazon Elastic Compute Cloud (EC2) 实例和一个 Amazon Elastic Kubernetes Service (EKS) 集群的公共终端缺少终端保护。</li><li><strong>修复：</strong>只需简单批准，协同助手就将 <strong>Elastic Security</strong> <strong>扩展检测与响应 (XDR) 和云检测与响应 (CDR)</strong> 部署到了易受攻击的资产上，实时保护了环境。</li><li><strong>结果：</strong>已部署的 AWS 资产和资源得到了保护，实现了完整的运行时安全。</li></ul><h2><strong>未来：增强一切</strong></h2><p>这个项目证明 Elastic Agent Builder 可以成为分布式运维的中心大脑。我们不仅限于基础架构。我们的运行器技术可以驱动：</p><ul><li><strong>增强型合成：</strong>诊断全球运行器中的 TLS 错误。</li><li><strong>增强型开发：</strong>创建拉取请求并在前端服务上实现验证码。</li><li><strong>增强型运维：</strong>在宕机期间自动重新配置 DNS 解析器。</li></ul><h2><strong>亲自试用</strong></h2><p>我们认为，AI 的未来不仅仅是聊天支持，而是<strong>增强型基础架构</strong>。它关乎拥有一个可以与您并肩部署、修复、观测和保护的合作伙伴。</p><p>立即查看代码，并通过分布式运行器 (<a href="https://github.com/strawgate/augmented-infrastructure">GitHub</a>) 加上 <a href="https://cloud.elastic.co/">Elastic Cloud Serverless</a> 上的 Elastic Agent Builder 亲自尝试吧！</p><ul><li>在 Elastic Cloud 上创建一个无服务器项目。</li><li>请将代码部署到运行器。</li><li>设置运行器。</li><li>配置您的 mcp.json。</li><li>启动运行器，它会自动创建您的智能体及其工具。</li><li>与一位能够推理、规划并在您的分布式运行器上执行操作的代理聊天！</li></ul><p><strong>团队</strong>：<em>Alex、Bill、Gil、Graham 和 Norrie</em></p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[基于 Swiss 式哈希表实现更快的 ES|QL 统计数据]]></title>
            <description><![CDATA[受 Swiss 启发的哈希和 SIMD 友好型设计如何为 Elasticsearch 查询语言 (ES|QL) 中提供稳定、可衡量的速度提升。]]></description>
            <link>https://www.elastic.co/search-labs/cn/blog/esql-swiss-hash-stats</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/cn/blog/esql-swiss-hash-stats</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Chris Hegarty, Matthew Alp, Nik Everet]]></dc:creator>
            <pubDate>Mon, 19 Jan 2026 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/21036e031070f14faccb2b53b22723de2750c391-1280x720.png?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>我们近期将 Elasticsearch 哈希表实现的核心组件替换为 Swiss 式设计，在均匀高基数工作负载下观察到构建与迭代速度提升 2-3 倍。最终使 Elasticsearch 查询语言 (ES|QL) 的统计数据与分析操作实现更低延迟、更高吞吐量，且性能表现更可预测。</p><h2>为什么这很重要</h2><p>绝大多数典型分析流程最终都归结为数据分组操作。无论是计算每台主机的平均字节数、统计每个用户的事件数量，还是跨维度聚合指标，其核心操作始终如一，那就是将键映射到分组并更新累计聚合值。</p><p>在小规模场景下，几乎任何合理的哈希表都能良好运行。但在大规模场景（数亿文档、数百万独立分组）中，细节决定成败。负载因素、探测策略、内存布局和缓存行为，这些因素可能让性能呈现线性增长，也可能导致严重的缓存未命中问题。</p><p>Elasticsearch 多年来一直支持这些工作负载，但我们一直在寻找机会来更新核心算法。因此，我们评估了一种受 Swiss 表启发的新方法，并将其应用于 ES|QL 如何计算统计数据。</p><h2>到底什么是 Swiss 表？</h2><p>Swiss 表是一类由 Google SwissTable 推广的现代哈希表系列，后被 Abseil 等资料库采纳。</p><p>传统哈希表在探测过程中需频繁追踪指针或加载键值，却发现大量不匹配情况。Swiss 哈希表的核心创新在于通过独立于键值存储的微型缓存驻留数组结构（称为<em>控制字节</em>），可拒绝大多数探测，从而显著降低内存流量。</p><p>每个控制字节对应一个哈希槽，在我们的应用中编码两类信息：槽是否为空，以及从哈希值派生的短指纹。这些控制字节在内存中连续存储（通常以 16 字节为一组），使其非常适合<a href="https://en.wikipedia.org/wiki/Single_instruction,_multiple_data">单指令多数据</a> (SIMD) 并行处理。</p><p>Swiss 表摒弃逐槽探测的传统方式，转而通过向量指令一次性扫描整个控制字节块。CPU 在单次操作中，将待插入键的指纹与 16 个槽位的指纹进行批量比对，并过滤掉空条目。仅当少数候选键通过这一快速通道后，才需要加载并比对实际键值。</p><p>该设计通过引入少量额外元数据，换取了更高的缓存命中率和大幅减少的随机内存访问。随着哈希表规模扩大及探测链长度增加，这些特性将愈发凸显其价值。</p><h2>以SIMD为中心</h2><p>真正的主角是 SIMD。</p><p>控制字节不仅结构紧凑，更专门针对向量指令处理进行优化设计。单条 SIMD 比对指令可同时校验 16 个指纹，将传统循环操作转化为数条高效宽指令处理。例如：</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/bd418778f0c6144f8f5f18419f6220ac0c935c7a-903x407.png" style="width: 100%;" /><p>实际上，这意味着：</p><ul><li>更少的分支。</li><li>更短的探针链。</li><li>减少键值存储的内存加载次数。</li><li>更好地利用了CPU的执行单元。</li></ul><p>绝大多数查询在控制字节扫描阶段即可完成过滤。当需要进一步处理时，剩余操作高度集中且可预测，而这正是现代 CPU 所擅长的负载类型。</p><h2>深入了解 SIMD</h2><p>对于喜欢探究底层实现细节的读者，以下是向表中插入新键时的具体流程：我们使用 Panama Vector API 配合 128 位向量，因此可并行处理 16 个控制字节。</p><p>以下代码片段展示了在配备 AVX-512 的 Intel Rocket Lake 处理器上生成的代码。虽然这些指令反映了当前硬件环境，但该设计并不依赖 AVX-512。在其他平台上会生成等效指令（如 AVX2、SSE 或 NEON）来实现相同的高层向量操作。</p><pre><code>; Load 16 control bytes from the control block
vmovdqu xmm0, XMMWORD PTR [r9+r10*1+0x10]

; Broadcast the 7-bit fingerprint of the new key across the vector
vpbroadcastb xmm1, r11d

; Compare all 16 control bytes to the new fingerprint
vpcmpeqb k7, xmm0, xmm1
kmovq rbx, k7

; Check if any matches were found
test rbx, rbx
jne <handle_match></code></pre><p>每条指令在插入过程中都起着明确的作用：</p><ul><li><code>vmovdqu</code>：将 16 个连续的控制字节加载到 128 位 <code>xmm0</code> 寄存器中。</li><li><code>vpbroadcastb</code>：将新键的 7 位指纹复制到<code>xmm1</code>寄存器的所有向量通道中。</li><li><code>vpcmpeqb</code>：将每个控制字节与广播后的指纹进行并行比较，生成潜在匹配的掩码。</li><li><code>kmovq</code> + <code>test</code>：将掩码移动到通用寄存器，并快速检查是否存在匹配。</li></ul><p>最终，我们决定一次探测 16 个控制字节组，因为基准测试表明，扩展到 32 或 64 个字节并使用更宽的寄存器并没有带来明显的性能提升。</p><h2>ES|QL 中的集成</h2><p>在 Elasticsearch 中采用 Swiss 式哈希算法并非简单的替换操作。ES|QL 对内存核算、安全性以及与计算引擎其他部分的集成有着严苛要求。</p><p>我们将新型哈希表与 Elasticsearch 的内存管理机制深度集成，包括分页回收器和熔断器核算模块，确保内存分配始终透明且受控。Elasticsearch 的聚合数据采用密集存储方式并通过组 ID 索引，在保持内存布局紧凑、迭代高效的同时，通过支持随机访问实现了特定性能优化。</p><p>对于可变长度字节键，我们在存储组 ID 的同时缓存完整哈希值。此设计避免了探测过程中重复计算高开销的哈希码，并通过将关联元数据集中存储提升了缓存命中率。在重新哈希时，系统可直接利用缓存的哈希值和控制字节，无需检查键值本身，从而将容量调整成本降至最低。</p><p>我们实施中的一个重要简化策略是永不删除条目。这一设计消除了对<em>“墓碑”标记</em>（用于标识已释放槽位的占位符）的需求，使空槽保持真正空闲状态。这种优化进一步改善了探测行为，并确保控制字节扫描始终保持高效。</p><p>这样的设计在完美契合 Elasticsearch 执行模型的同时，保留了使 Swiss 表具吸引力的高性能特性。</p><h2>它的表现如何？</h2><p>在小规模数据量下，Swiss 表的性能与现有实现基本持平。这符合预期，当哈希表较小时，缓存效应的影响减弱，且待优化的探测操作本就较少。</p><p>随着数据规模扩大，性能特征迅速发生质变。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/24900afc47ab07b0e9933f6117b99d0f4613f794-962x599.png" style="width: 100%;" /><p>上方热图展示了不同键大小（8、32、64 和 128 字节）在数据规模从 1,000 至 10,000,000 组变化时的时间优化倍数。随着数据规模扩大，优化倍数呈稳定上升趋势，在均匀分布场景下最高可达 2-3 倍。</p><p>这一趋势完全符合设计预期。传统哈希表在数据规模扩大时会导致探测链长度增加，而 Swiss 式探测仍能在支持 SIMD 指令的控制字节块内完成绝大多数查询操作。</p><h2>缓存行为说明了一切</h2><p>为深入分析加速效果，我们在 Linux <code>perf</code>环境下运行相同的 JMH <a href="https://github.com/elastic/elasticsearch/pull/139343/files#diff-d0e0cc91a7495bf36b2d44eacce95f5185d01879e5f6c38089ac7a89aad17da7"><code>benchmarks</code></a>基准测试，并采集缓存与 TLB 统计数据。</p><p>与原始实施相比，Swiss 版实现的总缓存引用量减少约 60%，末级缓存（LLC）加载次数下降超 4 倍，LLC 加载未命中次数更是降低超 6 倍。由于 LLC 未命中通常直接导致主存访问，仅此一项优化就解释了端到端性能提升的绝大部分原因。</p><p>在更靠近 CPU 的层级，我们观察到 L1 数据缓存未命中次数显著减少，数据 TLB 未命中次数更降低近 6 倍，这表明数据空间局部性增强且内存访问模式更具可预测性。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/6e49b7609fba83e33692cb9834552b6ca7e42a83-998x499.png" style="width: 100%;" /><p>这正是 SIMD 友好型控制字节带来的实际效益。无需反复从分散的内存位置加载键和值，大多数探测操作仅需扫描紧凑、驻留缓存的结构体即可完成。内存访问量减少意味着缓存未命中率降低，而未命中率降低则直接提升查询速度。</p><h2>总结</h2><p>通过采用 Swiss 式哈希表设计并深度融合 SIMD 友好型探测机制，我们在高基数 ES|QL 统计工作负载中实现了 2-3 倍的速度提升，同时获得了更稳定且可预测的系统表现。</p><p>本研究揭示了现代 CPU 感知型数据结构如何为哈希表等老问题带来显著性能提升。该领域仍有广阔探索空间，例如扩展至更多基础数据类型的特化实现，以及在连接等高基数操作路径中的应用。这些工作均属于 Elasticsearch 内核持续现代化这一长期工程的重要组成部分。</p><p>如需了解详细信息或跟进项目进展，可查看 GitHub 上的该<a href="https://github.com/elastic/elasticsearch/pull/139343">拉取请求</a>及追踪进度的<a href="https://github.com/elastic/elasticsearch/issues/138799">元议题</a>。</p><p>祝您哈希愉快！</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[使用 Elasticsearch 管理智能体记忆]]></title>
            <description><![CDATA[使用 Elasticsearch 管理记忆，创建更具上下文感知能力和效率的智能体。]]></description>
            <link>https://www.elastic.co/search-labs/cn/blog/agentic-memory-management-elasticsearch</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/cn/blog/agentic-memory-management-elasticsearch</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Someshwaran Mohankumar]]></dc:creator>
            <pubDate>Fri, 16 Jan 2026 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/80907fd072e72d6ec902470b449c9f337957a0d7-1280x720.png?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>在新兴的<strong>上下文工程</strong>学科中，在正确的时间为 AI 智能体提供正确的信息至关重要。上下文工程最重要的一个方面就是管理 AI 的<strong>记忆</strong>。AI 系统就像人类一样，依赖于短期记忆和长期记忆来回忆信息。如果我们希望大型语言模型 (LLM) 智能体能够进行逻辑对话、记住用户偏好，或基于先前的结果或响应进行构建，我们需要为它们配备有效的记忆机制。</p><p>毕竟上下文中的所有内容都会影响 AI 的响应。“<em>垃圾进，垃圾出</em>”说的就是这个道理。</p><p>本文将介绍短期记忆和长期记忆对 AI 智能体的意义，具体包括：</p><ul><li>短期记忆和长期记忆的区别。</li><li>它们与使用向量数据库（如 Elasticsearch）的检索增强生成 (RAG) 技术有何关联，以及为什么需要细致的记忆管理。</li><li>忽略记忆（包括上下文溢出和上下文污染）有何风险。</li><li>最佳实践，如上下文修剪、总结和仅检索相关内容，以保持代理的记忆既有用又安全。</li><li>最后，我们将探讨如何使用 Elasticsearch 在多智能体系统中共享和传播记忆，使智能体能够协作而不会产生混乱。</li></ul><h2>AI 智能体中的短期记忆与长期记忆</h2><p><strong><em>AI 智能体的短期记忆</em></strong>通常指的是即时的对话上下文或状态——其本质上是活跃会话中的当前聊天历史记录或最近消息。这包括用户的最新查询和最近的来回交流。这与一个人在进行对话时脑海中记住的信息非常相似。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/4fbcc6f68055b2bccefc4176297a4ca50056dc0d-764x498.png" style="width: 100%;" /><p>AI 框架通常会将这种瞬时记忆作为智能体状态的一部分来维护（例如使用检查点进程来存储对话状态，<a href="https://docs.langchain.com/oss/python/langgraph/persistence#checkpoints">LangGraph 的此示例</a>就介绍了这一点）。短期记忆存在于<strong><em>会话范围</em></strong>内；也就是说，它存在于单个会话或任务中，会话结束后即重置或清除，除非明确保存在其他地方。ChatGPT 中提供的<strong><a href="https://help.openai.com/en/articles/8914046-temporary-chat-faq">临时聊天</a> </strong>就是会话范围内短期记忆的一个例子。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/150bdf209cda5ed20b59cddf34e624ad1a8016aa-1100x577.png" style="width: 100%;" /><p>而<strong><em>长期记忆</em></strong>指的是跨越<strong>对话或会话</strong>持续存在的信息。这是智能体日积月累保留的知识，包括早前学习的事实，或我们告知其永久记住的用户偏好或任何数据。</p><p>长期记忆通常通过从外部源（如文件或向量数据库）存储和获取来配置，这些外部源位于即时上下文窗口之外。与短期聊天历史记录不同，长期记忆并非自动包含在每个提示中。相反，基于特定场景，智能体必须在调用相关工具时<strong>回忆</strong>或检索该信息。在实践中，长期记忆可能包括用户的个人资料信息、智能体先前生成的答案或分析，或者智能体可以查询的知识库。</p><p>例如，如果您有一个旅行规划智能体，<em>短期记忆</em>将包含当前行程查询的详细信息（日期、目的地、预算）以及该聊天中的任何后续问题；而<em>长期记忆</em>可以存储用户的一般旅行偏好、过去的行程和在之前会话中分享的其他事实。当用户在后面再次访问时，智能体可以从这个长期存储中提取资源（例如，用户喜欢海滩和山脉，平均预算为 10 万卢比，有愿望清单，更喜欢体验历史和文化而非适合儿童的景点），这样就不会每次都把用户当作一张白纸。</p><p>短期记忆（聊天历史记录）提供即时的上下文和连续性，而长期记忆则提供更广泛的背景，供智能体在需要时调用。大多数先进的 AI 智能体框架都能做到这两点：它们会跟踪最近的对话以保持上下文，<em>并</em>提供在长期信息库中查找或存储信息的机制。管理短期记忆可确保其保持在上下文窗口内，而管理长期记忆则能帮助智能体基于以往的互动和角色来构建答案。</p><h2>上下文工程中的内存和 RAG</h2><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/46635aa11ceff89b8d6a26ac3e22da52407d82f3-1600x900.png" style="width: 100%;" /><p><strong><em>在实践中，我们如何让 AI 智能体拥有有用的长期记忆？</em></strong></p><p><strong><em>语义记忆</em></strong>是长期记忆的一个重要方法，通常通过<strong>检索增强生成 (RAG)</strong> 来配置。这需要将 LLM 与外部知识存储或支持向量的数据存储（如 Elasticsearch）耦合。当 LLM 需要提示或其内置训练之外的信息时，它会对 Elasticsearch 执行语义检索，并将最相关的结果作为上下文注入到提示中。通过这种方式，模型的有效上下文不仅包括最近的对话（短期记忆），还包括即时获取的相关长期事实。LLM 随后根据自身推理和检索到的信息来给出答案，它有效地将短期记忆和长期记忆结合起来，从而做出更准确和更感知上下文的响应。</p><p><strong>Elasticsearch</strong> 可用于为 AI 智能体配置长期记忆。下面是一个高级示例，演示如何从 Elasticsearch 中检索上下文以配置长期记忆。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/41ccbc7b5171e8170ac300139a963c0708816ba6-1600x900.png" style="width: 100%;" /><p>按照这种方式，智能体通过搜索相关数据来“记忆”，而不是将所有内容存储在有限的提示中，<strong>从而导致不同的风险。</strong></p><p><strong>将 RAG 与 Elasticsearch 或任何向量存储结合使用可带来诸多好处：</strong></p><p>首先，它将模型的<strong>知识扩展</strong>到了训练截止点之外。智能体可以检索 LLM 可能不知晓的最新信息或特定领域的数据。这对于询问近期事件或专业话题至关重要。</p><p>其次，按需获取上下文有助于减少幻觉，尤其是当 LLM 未针对您的细分用例进行专有或高度专业化的数据训练时，这很有可能会导致出现幻觉。正如 OpenAI 最近的一篇论文（<a href="https://arxiv.org/pdf/2509.04664">《Why Language Models Hallucinate》</a>）强调的那样， LLM 不是像以往所激励的那样通过评估来猜测或编造新信息，而是以 Elasticsearch 中的事实参考为基础。当然， LLM 依赖于向量存储中数据的可靠性来真正防止错误信息，并根据核心相关性措施检索相关数据。</p><p>第三，RAG 支持智能体处理的知识库远远大于提示所能容纳的任何内容。RAG 不是将整个文档（例如长篇研究论文或政策文件）推送到上下文窗口，从而导致信息过载或无关信息<a href="https://www.elastic.co/search-labs/blog/agentic-memory-management-elasticsearch#context-poisoning">上下文污染</a>，而是依赖于<a href="https://www.elastic.co/search-labs/blog/chunking-strategies-elasticsearch">分块</a>。大型文档会被分解成语义上有意义的小块，系统只检索与查询最相关的几个数据块。这样一来，模型要显得知识渊博不需要长达数百万个词元的上下文来支撑；它只需要访问更大语料库中的正确数据块。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/e6897356c9f0940e35a63d005e9cd20bc33e5dd7-1600x931.png" style="width: 100%;" /><p>值得注意的是，随着 LLM 上下文窗口的扩大（<a href="https://www.anthropic.com/news/1m-context">一些模型现在支持数十万甚至数百万个词元</a><em>）</em>，关于 RAG 是否“已死”的争论也随之出现。为什么不将所有数据推送到提示中？如果您有同样的疑惑，请参阅我的同事 Jeffrey Rengifo 和 Eduard Martin 撰写的精彩文章<a href="https://www.elastic.co/search-labs/blog/rag-vs-long-context-model-llm">《Longer context ≠ better: Why RAG still matters》</a>。这避免了“垃圾进，垃圾出”的问题：LLM 始终专注于少数重要内容，而不是应付噪音。</p><p>也就是说，将 Elasticsearch 或任何向量存储集成到 AI 智能体架构中可以提供<strong>长期记忆</strong>。智能体将知识存储在外部，并在需要时将其作为记忆上下文提取出来。这可以作为一种<em>架构</em>来实现，在每次用户查询后，智能体都会在 Elasticsearch 上搜索相关信息，然后在调用 LLM 之前将排序靠前的结果附加到提示中。如果响应包含有用的新信息，它也会保存回长期存储中（从而形成学习的反馈循环）。通过使用这种基于检索的记忆，智能体可以随时了解最新信息，无需将所有知识塞入每个提示，尽管上下文窗口支持<em>一百万个词元</em>。这种技术是上下文工程的基石，结合了信息检索和生成式 AI 的优势。 </p><p>下面是一个在会话期间使用 LangGraph 的检查点系统管理记忆中对话状态的示例。（请参阅我们的<a href="https://github.com/someshwaranM/elastic-context-engineering-short-term-long-term-memory">支持上下文工程应用程序</a>。）</p><pre><code># Initialize chat memory (Note: This is in-memory only, not persistent)
memory = MemorySaver()

# Create a LangGraph agent
langgraph_agent = create_react_agent(model=llm, tools=tools, checkpointer=memory)

...
...
# Only process and display checkpoints if verbose mode is enabled
if args.verbose:
    # List all checkpoints that match a given configuration
    checkpoints = memory.list({"configurable": {"thread_id": "1"}})
    # Process the checkpoints
    process_checkpoints(checkpoints)</code></pre><p>以下是它存储<strong>检查点</strong>的方式：</p><pre><code>Checkpoint:
Timestamp: 2025-12-30T09:19:41.691087+00:00
Checkpoint ID: 1f0e560a-c2fa-69ec-8001-14ee5373f9cf
User: Hi I'm Som, how are you? (Message ID: ad0a8415-5392-4a58-85ad-84154875bbf2)
Agent: Hi Som! I'm doing well, thank you! How about you? (Message ID: 
56d31efb-14e3-4148-806e-24a839799ece)
Agent:  (Message ID: lc_run--019b6e8e-553f-7b52-8796-a8b1fbb206a4-0)

Checkpoint:
Timestamp: 2025-12-30T09:19:40.350507+00:00
Checkpoint ID: 1f0e560a-b631-6a08-8000-7796d108109a
User: Hi I'm Som, how are you? (Message ID: ad0a8415-5392-4a58-85ad-84154875bbf2)
Agent: Hi Som! I'm doing well, thank you! How about you? (Message ID: 
56d31efb-14e3-4148-806e-24a839799ece)

Checkpoint:
Timestamp: 2025-12-30T09:19:40.349027+00:00
Checkpoint ID: 1f0e560a-b62e-6010-bfff-cbebe1d865f6</code></pre><p>对于长期记忆，以下是在 Elasticsearch 上执行语义搜索的方法，以便在将检查点汇总并索引到 Elasticsearch 后使用向量嵌入检索以前的相关对话。</p><pre><code>Functions: 
retrieve_from_elasticsearch() 

# Enhanced Elasticsearch retrieval with rank_window and verbose display
def retrieve_from_elasticsearch(query: str, k: int = 5, rank_window: int = None) -> tuple[List[Dict[str, Any]], str]:
    """
    Retrieve context from Elasticsearch with score-based ranking
    
    Args:
        query: Search query
        k: Number of results to return
        rank_window: Number of candidates to retrieve before ranking (default: args.rank_window)
        
    Returns:
        Tuple of (retrieved_documents, formatted_context_string)
    """
    if not es_client or not es_index_name:
        return [], "Elasticsearch is not available. Cannot search long-term memory."
    
    if rank_window is None:
        rank_window = args.rank_window
    
    try:
        # Check if index exists and has documents
        if not es_client.indices.exists(index=es_index_name):
            return [], "No previous conversations stored in long-term memory yet."
        
        # Get document count
        try:
            doc_count = es_client.count(index=es_index_name)["count"]
            if doc_count == 0:
                return [], "Long-term memory is empty. No previous conversations to search."
        except Exception as e:
            return [], f"Error checking memory: {str(e)}"
        
        # Generate embedding for the query
        try:
            query_embedding = embeddings.embed_query(query)
        except Exception as e:
            return [], f"Error generating embedding: {str(e)}"
        
        # Perform semantic search using kNN with rank_window
        try:
            search_body = {
                "knn": {
                    "field": "vector",
                    "query_vector": query_embedding,
                    "k": k,
                    "num_candidates": rank_window  # Retrieve more candidates, then rank top k
                },
                "_source": ["text", "content", "message_type", "timestamp", "thread_id"],
                "size": k
            }
            
            response = es_client.search(index=es_index_name, body=search_body)
            
            if not response.get("hits") or len(response["hits"]["hits"]) == 0:
                return [], "No relevant previous conversations found in long-term memory."
            
            # Extract documents with scores
            retrieved_docs = []
            for hit in response["hits"]["hits"]:
                source = hit["_source"]
                score = hit["_score"]
                retrieved_docs.append({
                    "content": source.get("content", source.get("text", "")),
                    "message_type": source.get("message_type", "unknown"),
                    "timestamp": source.get("timestamp", "unknown"),
                    "thread_id": source.get("thread_id", "unknown"),
                    "score": score
                })
            
            # Format context string
            context_parts = []
            for i, doc in enumerate(retrieved_docs, 1):
                context_parts.append(doc["content"])
            
            context_string = "\n\n".join(context_parts)
            
            # Verbose display
            if args.verbose:
                rich.print(f"\n[bold yellow]🔍 RETRIEVAL ANALYSIS[/bold yellow]")
                rich.print("="*80)
                rich.print(f"[blue]Query:[/blue] {query}")
                rich.print(f"[blue]Retrieved:[/blue] {len(retrieved_docs)} documents (from {rank_window} candidates)")
                rich.print(f"[blue]Total context length:[/blue] {len(context_string)} characters\n")
                
                for i, doc in enumerate(retrieved_docs, 1):
                    rich.print(f"[cyan]📄 Document {i} | Score: {doc['score']:.4f} | Type: {doc['message_type']}[/cyan]")
                    rich.print(f"[cyan]   Timestamp: {doc['timestamp']} | Thread: {doc['thread_id']}[/cyan]")
                    content_preview = doc['content'][:200] + "..." if len(doc['content']) > 200 else doc['content']
                    rich.print(f"[cyan]   Content: {content_preview}[/cyan]")
                    rich.print("-" * 80)
            
            return retrieved_docs, context_string
            
        except Exception as e:
            return [], f"Error searching memory: {str(e)}"
            
    except Exception as e:
        return [], f"Error accessing long-term memory: {str(e)}"</code></pre><p>既然我们已经探讨了如何利用 LangGraph 的检查点在 Elasticsearch 中索引和提取短期记忆和长期记忆，接下来让我们花点时间了解为什么索引和转储完整对话可能存在风险。</p><h2>不管理上下文内存的风险</h2><p>我们花了大量篇幅讨论了上下文工程以及短期和长期记忆，接下来让我们了解如果不好好管理智能体的记忆和上下文会发生什么。</p><p>遗憾的是，当 AI 的上下文变得极其庞大或包含不良信息时，很多事情都可能会出错。随着上下文窗口变大，<strong>新的失效模式</strong>也会随之出现，例如：</p><ul><li><strong>上下文污染</strong></li><li><strong>上下文干扰</strong></li><li><strong>上下文混淆</strong></li><li><strong>上下文冲突</strong></li><li><strong>上下文泄露和知识冲突</strong></li><li><strong>幻觉和错误信息</strong></li></ul><p>让我们来分析一下这些问题以及因上下文管理不善而产生的其他风险：</p><h3>上下文污染</h3><p><em>上下文污染</em>指的是错误或有害信息混入上下文中，并“污染”模型的后续输出。一个常见的例子是模型产生的幻觉被当作事实并插入到对话历史记录中。然后，该模型可能会在以后的响应中以该错误为基础，从而使错误更加严重。在迭代智能体循环中，一旦虚假信息进入共享上下文（例如智能体工作笔记的摘要），它可能会被反复强化。 </p><p><a href="https://storage.googleapis.com/deepmind-media/gemini/gemini_v2_5_report.pdf">DeepMind 的研究人员在 Gemini 2.5 报告</a>（TL;DR，请点击<a href="https://www.dbreunig.com/2025/06/17/an-agentic-case-study-playing-pok%C3%A9mon-with-gemini.html">此处</a>查看）中观察到，一个长期运行的 <em>Pokémon</em> 游戏智能体出现了这种情况：如果智能体产生一个错误的游戏状态幻觉，并且该状态被记录到它的<em>上下文</em>（它对目标的记忆）中，那么智能体就会围绕一个不可能完成的目标形成<strong>毫无意义的策略</strong>，从而陷入困境。换句话说，受污染的记忆会让智能体无限期地走上错误的道路。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/3f2facf5aad67613ad557422e09ec23a66adc0ed-1600x1388.png" style="width: 100%;" /><p>上下文污染可能是无意的（误操作），也可能是恶意的，例如通过提示注入攻击，用户或第三方偷偷输入隐藏指令或错误事实，智能体随后记住并遵循这些指令或事实。</p><p><strong>建议的应对措施：</strong></p><p>根据来自 <a href="https://www.wiz.io/academy/data-poisoning">Wiz</a>、<a href="https://zerlo.net/en/blog/what-is-llm-data-poisoning">Zerlo</a> 和 <a href="https://www.anthropic.com/research/small-samples-poison">Anthropic</a> 的见解，针对上下文污染的对策主要是防止不良或误导性信息进入 LLM 的提示、上下文窗口或检索管道。关键步骤包括：</p><ul><li>不断检查上下文：监测对话或检索到的文本中是否有任何可疑或有害内容，而不仅仅是监测起始提示。</li><li>使用可信来源：根据可信度对文档进行评分或标记，以便系统优先选择可靠的信息，并忽略低分数据。</li><li>发现异常数据：使用工具检测异常、不合适或被篡改的内容，并在模型使用前将其删除。</li><li>过滤输入和输出：添加护栏，防止有害或误导性文本进入系统或被模型重复。</li><li>用干净的数据不断更新模型：定期用经过验证的信息刷新系统，以纠正任何漏网的不良数据。</li><li>人机协同：安排人员审查重要的输出或将其与已知的可信来源进行比较。</li></ul><p>简单的用户习惯也很有帮助，比如重置冗长的聊天记录、只分享相关信息、将复杂的任务分解成更小的步骤，以及在模型外保留干净的备注。</p><p>这些措施可共同构建多层防御，保护 LLM 免受上下文污染，并保持输出的准确性和可信度。</p><p>如果不采取这里提到的对策，智能体可能会记住一些指令，比如忽略以前的指南<em></em>或攻击者插入的琐碎事实，从而导致得到有害的输出。</p><h3>上下文干扰</h3><p><em>上下文干扰</em>是指当上下文变得过长时，模型过度关注上下文而忽视在训练过程中学到的内容。在极端情况下，这类似于<a href="https://en.wikipedia.org/wiki/Catastrophic_interference"><em>灾难性遗忘</em></a>；也就是说，模型会有效地“遗忘”它的底层知识，变得过分依赖摆在它面前的信息。先前的研究表明，当提示过长时，LLM 往往会失去注意力。</p><p>以 Gemini 2.5 智能体为例，它支持百万词元级别的窗口，但当其上下文增长超过一定程度时（在实验中约为 100000 个词元），它会开始<strong>专注于重复其过去的操作</strong>，而不是提出新的解决方案。从某种意义上说，该智能体成了其广泛历史的囚徒。它不停地查看以前的动作记录（上下文）并模仿它们，而不是利用其底层的训练知识来制定新颖的策略。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/e08e5b6d2e8ec7e3511d455985eed3d7fa6241e0-1352x636.png" style="width: 100%;" /><p>这只会适得其反。我们希望模型能够利用相关的上下文来帮助推理，而不是让上下文凌驾于其思考能力之上。值得注意的是，即使是那些拥有巨大窗口的模型也会表现出这种<a href="https://research.trychroma.com/context-rot"><em>上下文腐烂</em></a>：随着词元的增加，它们的性能会不均匀地下降。它们仿佛有<em>注意力预算</em>。就像人类工作记忆有限一样， LLM 对词元的关注能力也是有限的，随着预算捉襟见肘，其精准度和专注度会下降。</p><p>作为缓解措施，您可以通过分块、工程化正确信息、定期总结上下文以及利用评分来评估和监控响应的准确性来防止上下文干扰。</p><p>这些方法可使模型始终基于相关的上下文和其底层训练，从而降低干扰的风险，并提高整体推理质量。</p><h3>上下文混淆</h3><p><em>上下文混淆</em>是指模型使用上下文中的多余内容生成低质量响应的情况。一个典型的例子是为智能体提供它可能会用到的大量工具或 API 定义。如果这些工具有很多与当前任务无关，模型可能仍然会试图不恰当地使用它们，仅仅因为它们出现在上下文中。实验发现，提供<em>过多</em>非必需的工具或文档可能会<em>降低</em>性能。智能体会开始出错，例如调用错误的函数或引用不相关的文本。 </p><p>在一个案例中，一个小型的 <strong>Llama 3.1 8B</strong> 模型在有 46 个工具可供考虑时未能完成任务，在只有 19 个工具可供考虑时却成功完成了任务。额外的工具造成了混乱，尽管上下文的长度没有超出限制。根本问题在于，提示中的任何信息都会被模型<em>关注</em>。如果它不知道忽略某些内容，那么这些内容可能会以不希望的方式影响其输出。不相关的内容可能会“窃取”模型的一些注意力并将其引入歧途（例如，不相关的文档可能会导致智能体答非所问）。上下文混淆通常表现为模型做出低质量的反应，将不相关的上下文整合在一起。参考研究论文：<a href="https://arxiv.org/pdf/2411.15399">《Less is More: Optimizing Function Calling for LLM Execution on Edge Devices》</a>。</p><p>这提醒我们，上下文不一定越多越好，尤其是在没有进行相关性<strong>管护</strong>的情况下。</p><h3>上下文冲突</h3><p><em>上下文冲突</em>是指<strong>上下文的某些部分相互矛盾</strong>，导致内部不一致，从而破坏模型的推理。如果智能体积累了多条相互冲突的信息，上下文冲突就会发生。 </p><p>例如，想象一个智能体从两个来源获取数据：一个说 <em>A 航班下午 5 点起飞</em>，另一个说 <em>A 航班下午 6 点起飞</em>。如果两个事实都出现在上下文中，可怜的模型无法知道哪个是正确的；它可能会混淆或生成不正确或不相似的答案。</p><p>上下文冲突也经常发生在多轮对话中，因为模型<strong>前期的回答尝试会与后来完善的信息一起留在上下文中。</strong></p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/500d7a80dc8db1923f9b5ca84728eed64fa296f7-1316x580.png" style="width: 100%;" /><p>Microsoft 和 Salesforce 的一项<a href="https://arxiv.org/pdf/2505.06120">研究</a>显示，如果将复杂查询分解为多个聊天机器人回合（逐步添加细节），与在单个提示中提供所有细节相比，前者的最终准确性会显著下降。为什么？因为前期的回合包含来自模型的部分或不正确的中间答案，而这些答案会保留在上下文中。当模型后来尝试用所有信息来回答问题时，它的<em>记忆</em>中仍然包含那些错误的尝试，这些尝试与更正后的信息相冲突，并导致模型偏离正轨。从根本上说，对话的上下文与对话本身发生了冲突。模型可能会无意中使用已经过时的上下文（来自前期的对话轮），这些上下文在添加新信息后不再适用。</p><p>在智能体系统中，上下文冲突尤其危险，因为智能体可能会结合来自不同工具或子智能体的输出。如果这些输出不一致，则汇总的上下文就不一致。这样一来，智能体在试图调和矛盾时就会陷入困境或产生无意义的结果。防止上下文冲突需要确保上下文的<strong>新鲜度和一致性</strong>，<strong></strong>例如清除或更新任何过时的信息，不混用未经一致性审查的来源。</p><h3>上下文泄露和知识冲突</h3><p>在多个智能体或用户共享记忆存储的系统中，存在信息在上下文间流失的风险。</p><p>例如，如果两个独立用户的数据嵌入存在于同一向量数据库中且没有适当的访问控制，响应用户 A 查询的智能体可能会意外检索用户 B 的部分记忆。这种<strong><em>跨上下文泄露</em></strong>可能会暴露私人信息，或在响应中造成混乱。</p><p>根据“<a href="https://wtit.com/blog/2025/04/17/owasp-top-10-for-llm-applications-2025/">OWASP 定义的LLM 应用十大风险</a>”，多租户向量数据库必须防范此类泄露：</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/8f0ccf0b2f7bd6715c14aceee2deffb213d50bd9-1600x936.png" style="width: 100%;" /><p>根据《<a href="https://wtit.com/blog/2025/04/17/owasp-top-10-for-llm-applications-2025/">LLM 08:2025 向量和嵌入弱点</a><em>，</em>常见的风险之一是上下文泄露：</p><blockquote><em>在多租户环境中，多类用户或应用共享同一个向量数据库，用户或查询之间存在上下文泄露的风险。当来自多个来源的数据相互矛盾时，可能会出现数据联合知识冲突错误。当 LLM 无法使用来自检索增强的新数据取代其在训练中学到的旧知识时，这种情况也可能会发生。</em></blockquote><p>另一方面，LLM 可能难以用记忆中的新信息覆盖其<strong>内置的知识</strong>。如果模型是根据某个事实进行训练的，而检索到的上下文却表明了相反的情况，那么模型可能会对应该相信哪个事实感到困惑。如果没有适当的设计，智能体可能会混淆上下文或未能用新证据更新旧知识，从而导致得出过时或不正确的答案。</p><h3><strong>幻觉和错误信息</strong></h3><p>即使没有长上下文的干扰，<em>幻觉</em>（LLM 编造听起来合理但实际上是虚假的信息）也已是一个老生常谈的问题，而糟糕的记忆管理会加剧这个问题。 </p><p>如果智能体的记忆缺少一个关键事实，模型可能会<strong>用猜测来填补空白</strong>，如果这个猜测随后进入上下文（使其受污染），错误就会持续存在。 </p><p>OWASP LLM 安全报告<a href="https://wtit.com/blog/2025/04/17/owasp-top-10-for-llm-applications-2025/"><strong>（LLM09:2025 错误信息）</strong></a>强调错误信息是一个核心漏洞：LLM 可以产生自信但虚构的答案，而用户可能会过度信任它们。一个拥有不良或过时长期记忆的智能体可能会自信地引用去年真实但现在错误的内容，除非其记忆保持更新。 </p><p>过度依赖 AI 输出（无论是用户还是智能体本身在循环中过度依赖）会使情况变得更糟。如果没有人定期检查记忆中的信息，智能体可能会积累虚假信息。这就是 RAG 经常被用来减少幻觉的原因：检索权威来源，模型就不必编造事实。但如果您的检索拉取了错误的文档（比如包含错误信息的文档），或者早期幻觉没有被去除，系统可能会在所有操作中传播这些错误信息。 </p><p>总之，记忆管理不善可能会导致<strong>错误和误导性的输出</strong>，这可能会造成损害，尤其是在高风险情况下（例如在金融或医疗领域提供错误建议）。智能体需要机制来验证或纠正其记忆内容，而不是无条件地信任上下文中的任何内容。</p><p>总的来说，给 AI 智能体无限长的记忆或将所有可能的东西都转储到其上下文中<em>并非</em>成功的秘诀。</p><h2>LLM 应用程序中内存管理的最佳实践</h2><p>为了避免上述陷阱，开发人员和研究人员为 AI 系统的上下文和记忆管理设计了许多<strong>最佳实践</strong>。这些做法旨在使 AI 的工作上下文保持精简、相关且更新的状态。以下是一些关键策略，以及它们如何发挥作用的示例。</p><h3>RAG：使用针对性上下文</h3><p>RAG 的大部分内容在前面的章节中已有介绍，所以这里仅作简要的实用提醒：</p><ul><li>使用有针对性的检索，而不是批量加载：只检索最相关的片段，而不是将整个文档或完整的对话历史记录推送到提示中。</li><li>将 RAG 视为即时记忆调用：仅在需要时才获取上下文，而不是将所有内容跨轮次传递。</li><li>优先使用感知相关性的检索策略：top-k 语义搜索、倒数排序融合或工具装载过滤等方法有助于减少噪音并提高基础。</li><li>扩大上下文窗口并不会消除对 RAG 的需求：两个高度相关的段落几乎总是比 20 页松散相关的内容更有效。</li></ul><p>也就是说，RAG 并不是要增加更多的上下文，而是要增加合适的上下文。</p><h3>工具装载</h3><p><em>工具装载</em>是指只给模型提供它在执行任务时实际需要的工具。这个词源于游戏：您要选择一种适合当下情况的装备。工具太多会减慢您的速度；错误的工具会导致失败。根据研究论文<a href="https://arxiv.org/abs/2411.15399">《Less is more》</a>，LLM 也是如此。当工具数量超过 30 个左右时，描述会开始重叠，模型会变得混乱。当工具数量超过约 100 个时，失败几乎是必然的。这不是一个上下文窗口问题，而是上下文混淆的问题。</p><p><a href="https://arxiv.org/abs/2505.03275"><strong>RAG-MCP</strong></a> 是一个简单有效的解决办法。它不是将每个工具都放入提示中，而是将工具描述存储在向量数据库中，每个请求只检索最相关的工具。在实际操作中，这样可以使装载保持小型化和专注化，大幅缩短提示，并且可以将工具选择的准确性至多提高 3 倍。</p><p>小模型甚至会更快出现这样的问题。研究表明，8B 模型在装载数十种工具时会失败，但在精简装载后会变得成功。动态选择工具（有时先由 LLM 推理它认为需要的工具）可将性能提高 44% ，同时还能降低功耗和缩短延迟。关键点是，大多数智能体只需要少量工具，但随着系统的发展，设计决策需要首先考虑工具装载和 RAG-MCP。</p><h3>上下文修剪：限制聊天历史记录的长度</h3><p>如果对话持续了很多轮，累积的聊天历史记录可能会变得太大而无法容纳，导致上下文溢出或对模型造成过多干扰。 </p><p><em>修剪</em>是指随着对话的增加，以编程方式删除或缩短对话中不太重要的部分。一种简单的形式是，当您达到一定限制时，删除对话中最早的轮次，只保留最新的 <em>N</em> 条消息。更复杂的修剪可能涉及删除无关的题外话或以前不再需要的指令。修剪的目标是<strong>保持上下文窗口不受旧新闻干扰</strong>。 </p><p>例如，如果智能体在 10 轮对话前解决了一个子问题，并且我们已经翻篇，我们可以从上下文中删除该部分历史记录（假设我们已不需要该部分）。许多基于聊天功能的实现方式都是如此：它们会维护一个滚动显示最新消息的窗口。 </p><p>修剪可以很简单，比如在对话的最早部分被总结或被认为无关紧要后“忘记”这些部分。这样一来，我们就能降低上下文溢出错误的风险，也能减少<a href="https://www.elastic.co/search-labs/blog/agentic-memory-management-elasticsearch#context-distraction"><strong>上下文干扰</strong></a>，使模型不会被旧的或偏离主题的内容干扰。这种方法非常类似于人类可能记不住一个小时谈话中的每一个字，但会记住重点。 </p><p>如果您对上下文修剪感到困惑，正如作者 Drew Breunig <a href="https://www.dbreunig.com/2025/06/26/how-to-fix-your-context.html#tool-loadout:~:text=Provence%20is%20fast%2C%20accurate%2C%20simple%20to%20use%2C%20and%20relatively%20small%20%E2%80%93%20only%201.75%20GB.%20You%20can%20call%20it%20in%20a%20few%20lines%2C%20like%20so%3A">在此</a>强调的那样，使用 Provence 模型 (`<a href="https://huggingface.co/naver/provence-reranker-debertav3-v1">naver/provence-reranker-debertav3-v1</a>`) 可能会有所帮助。Provence 模型是一个轻量级 (1.75 GB)、高效且准确的问答上下文修剪器。它可以将大型文档裁剪为仅与给定查询最相关的文本。您可以按特定时间间隔调用它。</p><p>以下是我们在代码中调用 `provence-reranker` 模型来修剪上下文的做法：</p><pre><code># Context pruning with Provence
def prune_with_provence(query: str, context: str, threshold: Optional[float] = None) -> str:
    """
    Prune context using Provence reranker model
    
    Args:
        query: User's query/question
        context: Original context to prune
        threshold: Relevance threshold (0-1) for Provence reranker.
                   If None, uses args.pruning_threshold.
                   0.1 = conservative (recommended, no performance drop)
                   0.3-0.5 = moderate to aggressive pruning
    
    Returns:
        Pruned context with only relevant sentences
    """
    if provence_model is None:
        return context
    
    if threshold is None:
        threshold = args.pruning_threshold
    
    try:
        # Use Provence's process method
        provence_output = provence_model.process(
            question=query,
            context=context,
            threshold=threshold,
            always_select_title=False,
            enable_warnings=False
        )
        
        # Extract pruned context from output
        pruned_context = provence_output.get('pruned_context', context)
        reranking_score = provence_output.get('reranking_score', 0.0)
        
        # Log statistics
        original_length = len(context)
        pruned_length = len(pruned_context)
        reduction_pct = ((original_length - pruned_length) / original_length * 100) if original_length > 0 else 0
        
        if args.verbose:
            rich.print(f"[cyan]📊 Pruning stats: {pruned_length}/{original_length} chars ({reduction_pct:.1f}% reduction, threshold={threshold:.2f}, rerank_score={reranking_score:.3f})[/cyan]")
        
        return pruned_context if pruned_context else context
        
    except Exception as e:
        rich.print(f"[yellow]⚠️ Error in Provence pruning: {str(e)}[/yellow]")
        rich.print(f"[yellow]⚠️ Falling back to original context[/yellow]")
        return context</code></pre><p>我们使用 Provence 重排序模型 (`naver/provence-reranker-debertav3-v1`) 来对句子的相关性进行评分。基于阈值的过滤功能可将句子保留在相关性阈值之上。此外，我们引入了一种回退机制，如果修剪失败，我们将返回原始上下文。最后，统计日志在详细模式下跟踪减少百分比。</p><h3>上下文总结：将旧信息浓缩而非完全放弃</h3><p><em>总结</em>是对修剪的补充。当历史记录或知识库变得过于庞大时，您可以使用 LLM 生成重要内容的简短总结，并在未来使用该总结代替完整内容，就像我们在上面的代码中所做的那样。</p><p>例如，如果 AI 助手进行了 50 轮对话，系统不是在第 51 轮将全部 50 轮对话发送给模型（很可能容纳不下），而是选择第 1 至 40 轮，让模型用一段话对其总结，然后在下一个提示中只提供该总结和最后的 10 轮对话。这样一来，模型无需每个细节也能知道讨论的内容。早期的聊天机器人用户是手动总结的，他们问聊天机器人：“你能总结一下我们到目前为止谈过的内容吗？”然后带着总结继续进行新的会话。现在总结可以自动进行。总结不仅可以节省上下文窗口的空间，还可以通过去除多余的细节并只保留重要的事实来减少<strong>上下文混淆/干扰</strong>。</p><p>以下展示了我们如何使用 OpenAI 模型（您可以使用任何大型语言模型）来压缩上下文，同时保留所有相关信息，并消除冗余和重复。<br/></p><pre><code># Context summarization
def summarize_context(query: str, context: str) -> str:
    """
    Summarize context using LLM to reduce duplication and focus on relevant information
    
    Args:
        query: User's query/question
        context: Context to summarize
        
    Returns:
        Summarized context
    """
    try:
        summary_prompt = f"""You are an expert at summarizing conversation context.

Your task: Analyze the provided conversation context and produce a condensed summary that fully answers or supports the user's specific question.

The summary must:
1. Preserve every fact, detail, and information that directly relates to the question
2. Eliminate redundancy and duplicate information
3. Maintain chronological flow when relevant
4. Focus on information that helps answer: "{query}"

Context to summarize:
{context}

Provide a concise summary that preserves all relevant information:"""

        summary = llm.invoke(summary_prompt).content
        
        if args.verbose:
            original_length = len(context)
            summary_length = len(summary)
            reduction_pct = ((original_length - summary_length) / original_length * 100) if original_length > 0 else 0
            rich.print(f"[cyan]📝 Summarization stats: {summary_length}/{original_length} chars ({reduction_pct:.1f}% reduction)[/cyan]")
        
        return summary
        
    except Exception as e:
        rich.print(f"[yellow]⚠️ Error in context summarization: {str(e)}[/yellow]")
        rich.print(f"[yellow]⚠️ Falling back to original context[/yellow]")
        return context</code></pre><p>重要的是，当上下文得到总结后，模型被琐碎细节或过往错误牵制的可能性会降低（假设总结是准确的）。 </p><p>不过，总结必须谨慎进行。糟糕的总结可能会遗漏关键细节，甚至引入错误。它本质上是对模型的另一个提示（“总结这个”），因此它可能会产生幻觉或失去细微差别。最佳做法是逐步总结，或许可以保留一些典型事实，不对它们进行总结。</p><p>尽管如此，它已经被证明非常有用。<a href="https://storage.googleapis.com/deepmind-media/gemini/gemini_v2_5_report.pdf">在 Gemini 智能体场景中，</a>每隔约 10 万个词元对上下文进行总结是抵消模型重复倾向的一种方法。总结就像对话或数据的压缩记忆。作为开发人员，我们可以通过让智能体针对对话历史记录或长文档定期调用总结函数（可能是一个较小的 LLM 或一个专用例程）来实现这一点。得到的总结会在提示中替代原始内容。这种策略已被广泛使用，以便将上下文限制在一定范围内，并提炼信息。</p><h3>上下文隔离：尽可能隔离上下文</h3><p>这在复杂的智能体系统或多步骤工作流程中更为重要。上下文隔离的理念是将一个大任务拆分成较小的孤立任务，每个任务都有自己的上下文，这样就不会积累一个包含所有内容的庞大上下文。每个子智能体或子任务使用特定的上下文处理问题的某一部分，然后由更高级的智能体、主管或协调员整合结果。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/f2de68c3339883d7658e633af3948f29f427e6cf-1600x900.png" style="width: 100%;" /><p><a href="https://www.anthropic.com/engineering/multi-agent-research-system">Anthropic 的研究策略使用多个子智能体</a>，每个子智能体研究问题的不同方面，各自拥有自己的上下文窗口，而主智能体负责阅读这些子智能体提炼的结果。这种并行的模块化方法意味着没有一个上下文窗口会变得过于臃肿。这也减少了无关信息混淆的可能性，每个线程都紧扣主题（避免上下文混淆），而且在回答具体子问题时不会携带不必要的包袱。从某种意义上说，这就像是在运行独立的思维线程，它们只分享各自的结果，而不是整个思维过程。</p><p>在多智能体系统中，这种方法至关重要。如果智能体 A 正在处理任务 A 而智能体 B 正在处理任务 B，那么除非确实需要，否则任何一个智能体都没有理由使用另一个智能体的完整上下文。智能体可以只交换必要的信息。例如，智能体 A 可以通过一个主管智能体将其发现的综合总结传递给智能体 B，同时每个子智能体都维护自己的专用上下文线程。这种设置不需要人机协同干预；它依赖于一个具有启用工具的主管智能体，并进行最小化和受控的上下文共享。</p><p>在设计系统时，尽量减少智能体或工具在运行时的必要上下文重叠，可以大大提高系统的清晰度和性能。您可以把它想象成 <strong>AI 微服务</strong>，每个组件处理自己的上下文，您以受控的方式在它们之间传递消息，而不是在一个单一的上下文中传递消息。这些最佳实践通常会结合使用。此外，这还能让您灵活地修剪琐碎的历史记录，总结重要的旧消息或对话，将详细日志卸载到 Elasticsearch 以获得长期上下文，并在需要时使用检索功能调回任何相关内容。</p><p>正如<a href="https://www.anthropic.com/engineering/effective-context-engineering-for-ai-agents#:~:text=While%20some%20models,to%20the%20LLM">此处</a>提到的那样，我们的指导原则是上下文是一种有限且宝贵的资源。我们希望提示中的每个词元都能发挥作用，换句话说，它应该对输出的质量有所帮助。如果记忆中的某些东西没有发挥其应有的作用（或者更糟糕的是，它们会主动造成混乱），那么我们就应该对其进行修剪、总结或将其去除。</p><p>作为开发者，我们现在可以像编写代码一样编程上下文，决定包含哪些信息，如何格式化它，以及何时删除或更新它。通过遵循这些实践，我们可以为 LLM 智能体提供所需的上下文，使其能够执行任务，而不会陷入前面描述的失败模式。其结果是，智能体能够记住应该记住的内容，忘记不需要的内容，并及时检索所需的内容。</p><h2>结论</h2><p>记忆不是您添加到智能体中的东西；它是您设计的一部分。短期记忆是智能体的工作记忆板，而长期记忆是其持久的知识存储。RAG 是两者之间的桥梁，将被动的数据存储（如 Elasticsearch）转变为一个主动的回忆机制，能够为输出提供依据并保持智能体的最新状态。</p><p>但记忆是把双刃剑。当您让上下文不受控制地增长时，您会导致上下文污染、干扰、混乱和冲突，在共享系统中甚至会导致数据泄露。这就是为什么最重要的记忆工作不是“多存储”，而是“更好地管护”：有选择地检索，积极修剪，仔细总结，避免混合无关的上下文（除非任务真的需要）。</p><p>在实践中，好的上下文工程看起来就像良好的系统设计：上下文更小和更充分、组件之间的交互受控、原始状态和您实际希望模型看到的提炼状态清晰分离。如果方法得当，您最终得到的不是一个什么都记得的智能体，而是一个在正确的时间，出于正确的原因，记住正确事情的智能体。</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[更高的吞吐量和更低的延迟：AWS 上的 Elastic Cloud Serverless 性能显著提升]]></title>
            <description><![CDATA[我们已将适用于 Elasticsearch Serverless 的 AWS 基础设施升级为更新、更快的硬件。了解这一巨大的性能提升如何提供更快的查询、更好的扩展和更低的成本。]]></description>
            <link>https://www.elastic.co/search-labs/cn/blog/elasticsearch-serverless-aws-performance-boost</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/cn/blog/elasticsearch-serverless-aws-performance-boost</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Pete Galeotti, Yuvraj Gupta, Rachel Forshee]]></dc:creator>
            <pubDate>Wed, 14 Jan 2026 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/3aa0b481994d2445ba979d3c79fff64c5ee6676a-720x420.jpg?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>Elastic Cloud Serverless 已成为那些希望构建高效搜索和 AI 应用程序而无需管理基础设施的开发人员的首选解决方案。现在，我们将无服务器项目的性能提升到了一个全新的水平。</p><p>我们已为在 AWS 上运行的所有 <a href="https://www.elastic.co/cloud/serverless">Elastic Cloud Serverless</a> 项目完成了一次重大基础设施升级，迁移到了更新、更快的硬件上。此更改已自动推广至所有无服务器项目。它为 AWS 上的 Elasticsearch、Elastic Observability 和 Elastic Security 无服务器项目提供了<strong>更高的吞吐量和更低的延迟</strong>。</p><h2><strong>为开发人员带来的关键性能优势</strong></h2><p>新的 AWS 硬件基础设施支撑着您使用 Elastic Cloud Serverless 所做的一切，为您的应用程序的速度和响应能力带来切实的好处。</p><h3><strong>降低查询延迟……提高吞吐量</strong></h3><p>改进后的硬件显著提升了计算资源的速度，这意味着您的搜索查询处理速度比以往任何时候都要快。</p><ul><li><strong>搜索和向量搜索：</strong>无论您是运行传统的全文本查询，还是在<a href="https://www.elastic.co/generative-ai">生成式 AI 和 Retrieval-Augmented Generation (RAG) 应用</a>中使用尖端的向量搜索，您都会发现延迟明显减少。内部基准测试显示，搜索延迟平均减少了 35%。</li><li><strong>索引速度更快：</strong>数据摄取速率得到优化，使您能够以更高的吞吐量为海量数据和复杂文档建立索引。这对于需要近实时数据可见性的应用程序至关重要。内部基准测试显示，索引吞吐量平均提高了 26%。</li></ul><h3><strong>在负载下保持稳定性能</strong></h3><p>无论您的工作负载如何，Elastic Cloud Serverless 都能实时动态地自动扩展，以满足需求，最大限度地减少延迟。通过这次硬件升级，这种扩展性现在变得性能更强且响应更迅速。</p><ul><li><strong>轻松应对流量高峰：</strong>无论您面临的是用户流量的突然激增，还是大规模批量数据的摄取，新的基础设施都能确保您的搜索和索引资源更高效地扩展，从而保持一致的低延迟。</li><li><strong>优化的计算存储解耦：</strong>无服务器架构将计算与存储分离，使工作负载能够独立扩展，从而实现最佳性能和成本效益。速度更快的硬件增强了计算层，最大限度地提高了这种解耦设计的效率。</li></ul><h2><strong>幕后揭秘：内部基准测试结果</strong></h2><p>为了量化 AWS 基础设施升级的影响，Elastic 工程团队针对一系列无服务器工作负载进行了全面的内部基准测试。这些工作负载为性能改进提供了经验证据，无论您的用例如何，您都可以期望在应用程序中获得性能提升。</p><h3><strong>基准测试方法</strong></h3><p>我们将测试重点放在直接影响开发者体验和应用程序响应速度的关键指标上：响应时间（即延迟）和搜索与索引操作的吞吐量。</p><ul><li><strong>测试的工作负载：</strong>测试包括面向用户的应用程序中典型的高并发搜索操作、复杂的向量搜索查询以及用于可观测性和安全用例的大量数据摄取/索引。特别是，我们的测试方法使用了 Elastic 基准测试工具 Rally 的<a href="https://github.com/elastic/rally-tracks/tree/master">公开可用的数据集</a>。<ul><li><a href="https://github.com/elastic/rally-tracks/tree/3bedd51/wikipedia"><code>wikipedia</code></a>：一个从维基百科文本内容的快照中提取的数据集，用于衡量通用文本搜索性能。</li><li><a href="https://github.com/elastic/rally-tracks/tree/3bedd51/msmarco-passage-ranking"><code>MSMARCO-Passage-Ranking</code></a>：一个源自 Microsoft 机器阅读理解 (MS MARCO) 的数据集，用于衡量稀疏向量字段上的搜索性能。</li><li><a href="https://github.com/elastic/rally-tracks/tree/3bedd51/openai_vector"><code>OpenAI_Vector</code></a>：一个源自 BEIR 的 NQ 并通过 OpenAI 的 <code>text-embedding-ada-002</code> 模型生成的嵌入进行丰富的数据集，用于衡量密集向量字段上的搜索性能。</li></ul></li><li><strong>测量：</strong>我们比较了新旧基础设施的性能，测量了第 99 百分位数 (P99) 的延迟，以捕捉最坏情况下的尾部延迟性能和每秒操作数。为确保结果的一致性，每个硬件配置文件的每条测试路径都运行了五次。</li><li><strong>目标：</strong>我们的目标是验证基础设施即使在快速自动扩展期间也能持续提供<strong>更快、更可预测的全面性能</strong>的能力。</li></ul><h3><strong>性能数据摘要</strong></h3><p>结果证实，效率和速度都有了明显提高。由于能够使用更少的计算资源完成相同的工作量，这些收益直接转化为更短的用户响应时间和更低的运营成本。</p><p>下表详细列出了数量上的改进。数值越大，吞吐量越高；数值越小，延迟越低。</p><p><strong>搜索基准测试结果：</strong></p>
            <table style="border-collapse: collapse; margin: 20px 0;">
              <tbody>
                
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">基准</td><td style="border: 1px solid #ccc; padding: 4px;">对比</td><td style="border: 1px solid #ccc; padding: 4px;">旧版基础设施</td><td style="border: 1px solid #ccc; padding: 4px;">新的基础设施</td><td style="border: 1px solid #ccc; padding: 4px;">差别</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">`wikipedia` (纯文本)</td><td style="border: 1px solid #ccc; padding: 4px;">搜索操作吞吐量 (ops/s)</td><td style="border: 1px solid #ccc; padding: 4px;">729</td><td style="border: 1px solid #ccc; padding: 4px;">1107</td><td style="border: 1px solid #ccc; padding: 4px;">+52%</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">`wikipedia` (纯文本)</td><td style="border: 1px solid #ccc; padding: 4px;">搜索操作延迟（p99，毫秒）</td><td style="border: 1px solid #ccc; padding: 4px;">56</td><td style="border: 1px solid #ccc; padding: 4px;">35</td><td style="border: 1px solid #ccc; padding: 4px;">-37%</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">`MSMARCO-Passage-Ranking`（稀疏向量）</td><td style="border: 1px solid #ccc; padding: 4px;">搜索操作吞吐量 (ops/s)</td><td style="border: 1px solid #ccc; padding: 4px;">220</td><td style="border: 1px solid #ccc; padding: 4px;">31</td><td style="border: 1px solid #ccc; padding: 4px;">+40%</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">`MSMARCO-Passage-Ranking`（稀疏向量）</td><td style="border: 1px solid #ccc; padding: 4px;">搜索操作延迟（p99，毫秒）</td><td style="border: 1px solid #ccc; padding: 4px;">108</td><td style="border: 1px solid #ccc; padding: 4px;">67</td><td style="border: 1px solid #ccc; padding: 4px;">-38%</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">`OpenAI_Vector`（密集向量）</td><td style="border: 1px solid #ccc; padding: 4px;">搜索操作吞吐量 (ops/s)</td><td style="border: 1px solid #ccc; padding: 4px;">475</td><td style="border: 1px solid #ccc; padding: 4px;">624</td><td style="border: 1px solid #ccc; padding: 4px;">+31%</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">`OpenAI_Vector`（密集向量）</td><td style="border: 1px solid #ccc; padding: 4px;">搜索操作延迟（p99，毫秒）</td><td style="border: 1px solid #ccc; padding: 4px;">35</td><td style="border: 1px solid #ccc; padding: 4px;">220</td><td style="border: 1px solid #ccc; padding: 4px;">-37%</td>
                    </tr>
              </tbody>
            </table>
          <p><strong>索引基准测试结果：</strong></p>
            <table style="border-collapse: collapse; margin: 20px 0;">
              <tbody>
                
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">基准</td><td style="border: 1px solid #ccc; padding: 4px;">对比</td><td style="border: 1px solid #ccc; padding: 4px;">旧版基础设施</td><td style="border: 1px solid #ccc; padding: 4px;">新的基础设施</td><td style="border: 1px solid #ccc; padding: 4px;">差别</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">`wikipedia` (纯文本)</td><td style="border: 1px solid #ccc; padding: 4px;">搜索操作吞吐量 (ops/s)</td><td style="border: 1px solid #ccc; padding: 4px;">2,845</td><td style="border: 1px solid #ccc; padding: 4px;">3220</td><td style="border: 1px solid #ccc; padding: 4px;">+13%</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">`wikipedia` (纯文本)</td><td style="border: 1px solid #ccc; padding: 4px;">搜索操作延迟（p99，毫秒）</td><td style="border: 1px solid #ccc; padding: 4px;">1,769</td><td style="border: 1px solid #ccc; padding: 4px;">1,120</td><td style="border: 1px solid #ccc; padding: 4px;">-37%</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">`MSMARCO-Passage-Ranking`（稀疏向量）</td><td style="border: 1px solid #ccc; padding: 4px;">搜索操作吞吐量 (ops/s)</td><td style="border: 1px solid #ccc; padding: 4px;">7,087</td><td style="border: 1px solid #ccc; padding: 4px;">8900</td><td style="border: 1px solid #ccc; padding: 4px;">+26%</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">`MSMARCO-Passage-Ranking`（稀疏向量）</td><td style="border: 1px solid #ccc; padding: 4px;">搜索操作延迟（p99，毫秒）</td><td style="border: 1px solid #ccc; padding: 4px;">824</td><td style="border: 1px solid #ccc; padding: 4px;">677</td><td style="border: 1px solid #ccc; padding: 4px;">-18%</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">`OpenAI_Vector`（密集向量）</td><td style="border: 1px solid #ccc; padding: 4px;">搜索操作吞吐量 (ops/s)</td><td style="border: 1px solid #ccc; padding: 4px;">2,972</td><td style="border: 1px solid #ccc; padding: 4px;">3,187</td><td style="border: 1px solid #ccc; padding: 4px;">+7%</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">`OpenAI_Vector`（密集向量）</td><td style="border: 1px solid #ccc; padding: 4px;">搜索操作延迟（p99，毫秒）</td><td style="border: 1px solid #ccc; padding: 4px;">2,946</td><td style="border: 1px solid #ccc; padding: 4px;">2,944</td><td style="border: 1px solid #ccc; padding: 4px;">0%</td>
                    </tr>
              </tbody>
            </table>
          <h2><strong>额外收获：成本降低</strong></h2><p>虽然我们的重点是提供低延迟性能，但新硬件的效率也对 Elasticsearch 项目的成本产生直接的积极影响。</p><p><a href="https://www.elastic.co/pricing/serverless-search">Elasticsearch Serverless 的定价</a>是基于使用量的，这意味着您只需为您所使用的摄取和搜索资源付费。由于更新、更快的硬件效率更高，您的工作负载通常能用更少的资源完成任务，从而降低大多数项目的固有成本。您无需支付高昂的价格，就能获得卓越的性能提升——这就是优化效率的定义。</p><h2><strong>这对您，开发者来说意味着什么？</strong></h2><p>此次基础设施升级完全由 Elastic 管理，因此您无需亲自操作——无需迁移，也无需更改配置。改进效果立竿见影，并自动应用于您所有基于 AWS 的无服务器项目。</p><p>此升级赋予您以下能力：</p><ul><li><strong>构建更快的应用程序：</strong>专注于功能开发速度，确保您的底层搜索平台能够提供用户所需的速度。</li><li><strong>自信创新：</strong>部署新的搜索、可观测性和安全功能（包括向量搜索和相关性排序等复杂的 AI 功能），并确保平台能够以最高性能处理负载。</li><li><strong>简化堆栈：</strong>使用完全托管的服务来处理基础设施管理、容量规划和扩展，这样您就可以专注于代码和数据。<br/></li></ul>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[借助 LangGraph 与 Elasticsearch 打造具人机协作功能的智能体]]></title>
            <description><![CDATA[了解如何借助 LangGraph 与 Elasticsearch 打造具人机协作功能的智能体，让人类参与决策流程，从而填补情境信息缺口，并在工具调用执行前进行审核。]]></description>
            <link>https://www.elastic.co/search-labs/cn/blog/human-in-the-loop-agents-langgraph-elasticsearch</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/cn/blog/human-in-the-loop-agents-langgraph-elasticsearch</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Jeffrey Rengifo]]></dc:creator>
            <pubDate>Mon, 05 Jan 2026 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/69634710b14adff494b51b2d57d119196ea6d644-838x532.png?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>在本文中，我们将探讨如何结合 <a href="https://langchain-ai.github.io/langgraph/">LangGraph</a> 与 Elasticsearch 来构建一个人机协同 (HITL) 应用。这种方法能够让 AI 系统直接将用户纳入决策流程，使交互更加可靠且具备情境感知能力。我们将通过一个基于情境的实例来展示，LangGraph 工作流如何与 Elasticsearch 集成，以实现数据检索、处理用户输入并生成精准结果。</p><h2>要求</h2><ul><li>NodeJS 18 或更高版本</li><li>OpenAI API密钥</li><li>Elasticsearch 8.x+ 部署</li></ul><h2>为何在生产级人机协同 (HITL) 系统里采用 LangGraph？</h2><p>在<a href="https://www.elastic.co/search-labs/blog/ai-agent-workflow-finance-langgraph-elasticsearch">先前的一篇文章</a>中，我们介绍了 LangGraph 以及它借助 LLM 和<a href="https://docs.langchain.com/oss/javascript/langgraph/graph-api#conditional-edges">条件边</a>来构建 RAG 系统的优势，该系统能够自动做出决策并展示结果。然而，有时我们并不希望系统实现端到端的自主运行，而是期望用户在执行循环中能够选择选项并做出决策。这一概念被命名为“<strong><em>人机协同</em></strong>”。</p><h2>人机协同或人工介入</h2><p>这是一种 AI 理念，让真人与 AI 系统进行交互，从而提供更多背景信息、评估回应内容、编辑回应结果、请求更多信息等。在合规、决策制定或内容生成等容错率低的场景中，这种理念非常实用，有助于提升 LLM 输出结果的可靠性。</p><p>一个常见的例子是，当你的编程助手请求你授权在终端执行某个特定命令时，或者在开始编程前，向你展示分步思考过程以供你审批。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/4df9fe7383f0dc665a1b0866651f4f6a40a14aa8-499x215.png" style="width: 100%;" /><h2>Elasticsearch + LangGraph：它们如何交互</h2><p><a href="https://www.langchain.com/">LangChain</a> 允许我们将 Elasticsearch 用作<a href="https://js.langchain.com/docs/integrations/vectorstores/elasticsearch/">向量存储库</a>，并在 LangGraph 应用中执行查询操作。这对于执行全文检索或语义搜索十分有用，而 LangGraph 则用于定义特定的工作流、工具以及交互方式。此外，它还引入 HITL 作为与用户的额外交互层。</p><h2>实际应用：人机协同</h2><p>让我们设想这样一种情形：一位律师就其近期承接的案件存在疑问。如果没有合适的工具，他需要手动查阅法律文章和判例，完整阅读后，再解读它们如何适用于自己所接的案件情况。然而，借助 LangGraph 和 Elasticsearch，我们能够构建一个系统，该系统可搜索法律判例数据库，并生成一份融入律师所提供具体细节和背景信息的案件分析报告。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/e95bf4ce115cbe9954ee5033776408bed485b8d2-1600x906.png" style="width: 100%;" /><p>工作流始于律师提交法律问题。系统在 Elasticsearch 中执行向量搜索，检索出最相关的判例，并以自然语言的形式呈现给律师供其选择。律师选择后，LLM 生成分析草案，并检查信息是否完整。此时，工作流有两条路径：如果一切清晰明确，则直接生成最终分析报告；如果存在疑问，则暂停并请求律师作出澄清。待缺失的背景信息提供完毕后，系统会综合考虑这些澄清内容，完成分析并返回结果。</p><p>以下是 LangGraph 绘制的图表，显示了应用程序在开发结束时的效果。每个节点代表一种工具或功能：</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/fcdc0dd53c436afc8e575d1f8fd3705b4dfe9a32-262x753.png" style="width: 100%;" /><h3>数据集</h3><p>以下是为本次示例所使用的<a href="https://github.com/Delacrobix/LangGraph-JS-Elasticsearch-human-in-the-loop/blob/main/dataset.json">数据集</a>。该数据集包含一系列法律判例，每个判例均描述了一起涉及服务延误的案件，涵盖法院的判决依据以及最终判决结果。</p><pre><code>[
  {
    "pageContent": "Legal precedent: Case B - Service delay not considered breach. A consulting contract used term 'timely delivery' without specific dates. A three-week delay occurred but contract lacked explicit schedule. Court ruled no breach as parties had not defined concrete timeline and delay did not cause demonstrable harm.",
    "metadata": {
      "caseId": "CASE-B-2022",
      "contractType": "consulting agreement",
      "delayPeriod": "three weeks",
      "outcome": "no breach found",
      "reasoning": "no explicit deadline defined, no demonstrable harm",
      "keyTerms": "timely delivery, open terms, schedule definition",
      "title": "Case B: Delay Without Explicit Schedule"
    }
  },
  ...
]</code></pre><h3>摄取和索引设置</h3><p>索引设置和数据摄取逻辑在 <a href="https://github.com/Delacrobix/LangGraph-JS-Elasticsearch-human-in-the-loop/blob/main/dataIngestion.ts">dataIngestion.ts</a> 文件中定义，在该文件中我们列出了用于处理索引创建的函数。此设置与 LangChain 针对 <a href="https://python.langchain.com/docs/integrations/vectorstores/elasticsearch/">Elasticsearch 的向量存储接口</a>兼容。</p><p><em>注意：映射设置也包含在 <a href="https://github.com/Delacrobix/LangGraph-JS-Elasticsearch-human-in-the-loop/blob/main/dataIngestion.ts">dataIngestion.ts</a> 文件中。</em></p><h3>安装软件包并设置环境变量</h3><p>让我们使用默认设置初始化一个 <a href="https://nodejs.org/en">Node.js</a> 项目，并引入</p><ul><li> <strong>@elastic/elasticsearch：</strong>这是适用于 Node.js 的 Elasticsearch 客户端，用于建立连接、创建索引以及执行查询操作。</li><li><strong>@langchain/community：</strong>提供对社区支持工具的集成功能，其中包含 ElasticVectorSearch 存储库。</li><li><strong>@langchain/core：</strong>LangChain 的核心构建模块，涵盖链、提示词以及工具函数等。</li><li><strong>@langchain/langgraph：</strong>增添基于图形的编排功能，支持具备节点、边以及状态管理的工作流。</li><li><strong>@langchain/openai：</strong>通过 LangChain 提供对 OpenAI 模型（LLM 及嵌入向量模型）的访问接口。</li><li><strong>dotenv：</strong>将环境变量从 .env 文件加载至 process.env 中。</li><li><strong>tsx：</strong>是一个用于执行 TypeScript 代码的实用工具。</li></ul><p>在控制台中运行以下命令以安装所有相关组件：</p><pre><code>npm install @elastic/elasticsearch @langchain/community @langchain/core @langchain/langgraph @langchain/openai dotenv --legacy-peer-deps && npm install --save-dev tsx</code></pre><p>创建 <code>.env</code> 文件来设置环境变量：</p><pre><code>ELASTICSEARCH_ENDPOINT=
ELASTICSEARCH_API_KEY=
OPENAI_API_KEY=</code></pre><p>我们将采用 <a href="https://www.typescriptlang.org/">TypeScript</a> 进行代码编写，原因在于它能提供一层类型安全保障，同时带来更优的开发体验。创建一个名为 <a href="https://github.com/Delacrobix/LangGraph-JS-Elasticsearch-human-in-the-loop/blob/main/main.ts">main.ts</a> 的 TypeScript 文件，并将下一节的代码插入其中。</p><h3>软件包导入</h3><p>在 <a href="https://github.com/Delacrobix/LangGraph-JS-Elasticsearch-human-in-the-loop/blob/main/main.ts">main.ts</a> 文件中，我们首先导入所需的模块，并初始化环境变量配置。这涵盖核心的 LangGraph 组件、OpenAI 模型集成以及 Elasticsearch 客户端。</p><p>我们还从 <a href="https://github.com/Delacrobix/LangGraph-JS-Elasticsearch-human-in-the-loop/blob/main/dataIngestion.ts">dataIngestion.ts</a> 文件导入以下各项：</p><ul><li><strong>ingestData：创建索引并摄取数据的函数。</strong></li><li><strong>Document 与 DocumentMetadata：</strong>用于定义数据集文档结构的接口。</li></ul><h3>Elasticsearch 向量存储客户端、嵌入客户端以及 OpenAI 客户端</h3><p>此代码将初始化向量存储、嵌入客户端和一个 OpenAI 客户端。</p><pre><code>const VECTOR_INDEX = "legal-precedents";

const llm = new ChatOpenAI({ model: "gpt-4o-mini" });
const embeddings = new OpenAIEmbeddings({
  model: "text-embedding-3-small",
});

const esClient = new Client({
  node: process.env.ELASTICSEARCH_ENDPOINT,
  auth: {
    apiKey: process.env.ELASTICSEARCH_API_KEY ?? "",
  },
});

const vectorStore = new ElasticVectorSearch(embeddings, {
  client: esClient,
  indexName: VECTOR_INDEX,
});</code></pre><p>应用程序工作流状态架构将有助于节点之间的通信：</p><pre><code>const LegalResearchState = Annotation.Root({
  query: Annotation<string>(),
  analyzedConcepts: Annotation<string[]>(),
  precedents: Annotation<Document[]>(),
  selectedPrecedent: Annotation<Document | null>(),
  draftAnalysis: Annotation<string>(),
  ambiguityDetected: Annotation<boolean>(),
  userClarification: Annotation<string>(),
  finalAnalysis: Annotation<string>(),
});</code></pre><p>在状态对象中，我们将传递用户查询内容、从查询中提取的概念、检索到的法律判例以及检测到的任何歧义信息至各节点。该状态对象还会跟踪用户选定的判例、过程中生成的草案分析，以及在所有澄清工作完成后生成的最终分析。</p><h3>节点</h3><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/9bda47908dc0a73fdda222c8ba1382a1ef9dc77a-304x300.png" style="width: 100%;" /><p><strong>searchPrecedents：</strong>此节点基于用户输入，在 Elasticsearch 向量存储库中执行相似性搜索。它最多可检索 5 份匹配文件，并打印出来供用户查看。</p><pre><code>async function searchPrecedents(state: typeof LegalResearchState.State) {
  console.log(
    "📚 Searching for relevant legal precedents with query:\n",
    state.query
  );

  const results = await vectorStore.similaritySearch(state.query, 5);
  const precedents = results.map((d) => d as Document);

  console.log(`Found ${precedents.length} relevant precedents:\n`);

  for (let i = 0; i < precedents.length; i++) {
    const p = precedents[i];
    const m = p.metadata;
    console.log(
      `${i + 1}. ${m.title} (${m.caseId})\n` +
        `   Type: ${m.contractType}\n` +
        `   Outcome: ${m.outcome}\n` +
        `   Key reasoning: ${m.reasoning}\n` +
        `   Delay period: ${m.delayPeriod}\n`
    );
  }

  return { precedents };
}</code></pre><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/82afb46c4373b01daf7640a528c2ea54db0837b5-304x300.png" style="width: 100%;" /><p><strong>precedentSelection：</strong>此node允许用户使用自然语言选择由最接近搜索检索到的、最匹配问题的用例。此时，应用程序<strong>中断工作流</strong>并等待用户输入。</p><pre><code>function precedentSelection(state: typeof LegalResearchState.State) {
  console.log("\n⚖️  HITL #1: Human input needed\n");
  const question = "👨‍⚖️  Which precedent is most similar to your case? ";
  const userChoice = interrupt({ question });

  return { userChoice };
}</code></pre><p><strong>selectPrecedent：</strong>此节点会将用户输入内容以及检索到的文档一并发送，以供解析，进而从中选出一个文档。LLM 通过返回一个数字来完成该任务，此数字代表模型依据用户自然语言输入所推断出的文档。</p><pre><code>async function selectPrecedent(state: typeof LegalResearchState.State) {
  const precedents = state.precedents || [];
  const userInput = (state as any).userChoice || "";

  const precedentsList = precedents
    .map((p, i) => {
      const m = p.metadata;
      return `${i + 1}. ${m.caseId}: ${m.title} - ${m.outcome}`;
    })
    .join("\n");

  const structuredLlm = llm.withStructuredOutput({
    name: "precedent_selection",
    schema: {
      type: "object",
      properties: {
        selected_number: {
          type: "number",
          description:
            "The precedent number selected by the lawyer (1-based index)",
          minimum: 1,
          maximum: precedents.length,
        },
      },
      required: ["selected_number"],
    },
  });

  const prompt = `
    The lawyer said: "${userInput}"

    Available precedents:
    ${precedentsList}

    Which precedent number (1-${precedents.length}) matches their selection?
  `;

  const response = await structuredLlm.invoke([
    {
      role: "system",
      content:
        "You are an assistant that interprets lawyer's selection and returns the corresponding precedent number.",
    },
    { role: "user", content: prompt },
  ]);

  const selectedIndex = response.selected_number - 1;
  const selectedPrecedent = precedents[selectedIndex] || precedents[0];

  console.log(`✅ Selected: ${selectedPrecedent.metadata.title}\n`);
  return { selectedPrecedent };
}</code></pre><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/714e840cc8dab48c436cd066a6fdb4b7dceccf13-428x260.png" style="width: 100%;" /><p><strong>createDraft：</strong>此节点根据用户选择的判例生成初步法律分析。它借助 LLM 评估所选判例如何适用于律师提出的问题，并判断系统是否具备足够信息以继续推进后续流程。</p><p>如果判例可直接适用，该节点将生成一份草案分析，并沿着正确路径直接跳转至最终节点。若 LLM 检测到模棱两可之处，例如未明确的合同条款、缺失的时间线细节或模糊的条件，它会返回一个提示需要澄清的标识，同时附上必须提供的具体信息列表。在此情况下，歧义将触发图中的左侧路径。</p><pre><code>async function createDraft(state: typeof LegalResearchState.State) {
  console.log("📝 Drafting initial legal analysis...\n");

  const precedent = state.selectedPrecedent;
  if (!precedent) return { draftAnalysis: "" };

  const m = precedent.metadata;

  const structuredLlm = llm.withStructuredOutput({
    name: "draft_analysis",
    schema: {
      type: "object",
      properties: {
        needs_clarification: {
          type: "boolean",
          description:
            "Whether the analysis requires clarification about contract terms or context",
        },
        analysis_text: {
          type: "string",
          description: "The draft legal analysis or the ambiguity explanation",
        },
        missing_information: {
          type: "array",
          items: { type: "string" },
          description:
            "List of specific information needed if clarification is required (empty if no clarification needed)",
        },
      },
      required: ["needs_clarification", "analysis_text", "missing_information"],
    },
  });

  const prompt = `
    Based on this precedent:
    Case: ${m.title}
    Outcome: ${m.outcome}
    Reasoning: ${m.reasoning}
    Key terms: ${m.keyTerms}

    And the lawyer's question: "${state.query}"

    Draft a legal analysis applying this precedent to the question.
    
    If you need more context about the specific contract terms, timeline details, 
    or other critical information to provide accurate analysis, set needs_clarification 
    to true and list what information is missing.
    
    Otherwise, provide the legal analysis directly.
  `;

  const response = await structuredLlm.invoke([
    {
      role: "system",
      content:
        "You are a legal research assistant that analyzes cases and identifies when additional context is needed.",
    },
    { role: "user", content: prompt },
  ]);

  let displayText: string;
  if (response.needs_clarification) {
    const missingInfoList = response.missing_information
      .map((info: string, i: number) => `${i + 1}. ${info}`)
      .join("\n");
    displayText = `AMBIGUITY DETECTED:\n${response.analysis_text}\n\nMissing information:\n${missingInfoList}`;
  } else {
    displayText = `ANALYSIS:\n${response.analysis_text}`;
  }

  console.log(displayText + "\n");

  return {
    draftAnalysis: displayText,
    ambiguityDetected: response.needs_clarification,
  };
}</code></pre><p>图中的两条路径如下所示：</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/72dda52e1d84818c9f98437f2fa6ecb8e5b2f4ff-438x638.png" style="width: 100%;" /><p>左侧路径包含一个额外的节点，用于处理澄清。</p><p><strong>requestClarification:</strong>当系统判定草稿分析缺少关键背景信息时，此节点会触发第二轮人机协同步骤。工作流会被中断，系统会要求用户对前一节点检测到的缺失合同细节进行说明。</p><pre><code>function requestClarification(state: typeof LegalResearchState.State) {
  console.log("\n⚖️  HITL #2: Additional context needed\n");
  const userClarification = interrupt({
    question: "👨‍⚖️  Please provide clarification about your contract terms:",
  });
  return { userClarification };
}</code></pre><p><strong>generateFinalAnalysis：</strong>此节点在必要时会将用户所选判例与用户提供的额外背景信息相结合，从而生成最终法律分析。借助上一轮 HITL 步骤中收集到的澄清信息，LLM 会综合判例的判决依据、用户提供的合同细节以及判定违约是否可能发生的条件，形成最终分析结果。</p><p>该节点可提供完整的分析，将法律解释和实际建议融为一体。</p><pre><code>async function generateFinalAnalysis(state: typeof LegalResearchState.State) {
  console.log("📋 Generating final legal analysis...\n");

  const precedent = state.selectedPrecedent;
  if (!precedent) return { finalAnalysis: "" };

  const m = precedent.metadata;

  const prompt = `
    Original question: "${state.query}"
    
    Selected precedent: ${m.title}
    Outcome: ${m.outcome}
    Reasoning: ${m.reasoning}
    
    Lawyer's clarification: "${state.userClarification}"
    
    Provide a comprehensive legal analysis integrating:
    1. The selected precedent's reasoning
    2. The lawyer's specific contract context
    3. Conditions for breach vs. no breach
    4. Practical recommendations
  `;

  const response = await llm.invoke([
    {
      role: "system",
      content:
        "You are a legal research assistant providing comprehensive analysis.",
    },
    { role: "user", content: prompt },
  ]);

  const finalAnalysis = response.content as string;

  console.log(
    "\n" +
      "=".repeat(80) +
      "\n" +
      "⚖️  FINAL LEGAL ANALYSIS\n" +
      "=".repeat(80) +
      "\n\n" +
      finalAnalysis +
      "\n\n" +
      "=".repeat(80) +
      "\n"
  );

  return { finalAnalysis };
}</code></pre><p>构建图表：</p><pre><code>const workflow = new StateGraph(LegalResearchState)
  .addNode("analyzeQuery", analyzeQuery)
  .addNode("searchPrecedents", searchPrecedents)
  .addNode("precedentSelection", precedentSelection)
  .addNode("selectPrecedent", selectPrecedent)
  .addNode("createDraft", createDraft)
  .addNode("requestClarification", requestClarification)
  .addNode("generateFinalAnalysis", generateFinalAnalysis)
  .addEdge("__start__", "analyzeQuery")
  .addEdge("analyzeQuery", "searchPrecedents")
  .addEdge("searchPrecedents", "precedentSelection") // HITL #1
  .addEdge("precedentSelection", "selectPrecedent")
  .addEdge("selectPrecedent", "createDraft")
  .addConditionalEdges(
    "createDraft",
    (state: typeof LegalResearchState.State) => {
      // If ambiguity detected, request clarification (HITL #2)
      if (state.ambiguityDetected) return "needsClarification";
      // Otherwise, generate final analysis
      return "final";
    },
    {
      needsClarification: "requestClarification",
      final: "generateFinalAnalysis",
    }
  )
  .addEdge("requestClarification", "generateFinalAnalysis") // HITL #2
  .addEdge("generateFinalAnalysis", "__end__");</code></pre><p>从图中，我们可以看出条件边定义了选择“最终”路径的条件。如图所示，现在的决策取决于草案分析是否检测到存在需要额外澄清的歧义。</p><p>将所有内容汇总起来执行：</p><pre><code>await ingestData();

// Compile workflow
const app = workflow.compile({ checkpointer: new MemorySaver() });
const config = { configurable: { thread_id: "hitl-circular-thread" } };

await saveGraphImage(app);

// Execute workflow
const legalQuestion =
    "Does a pattern of repeated delays constitute breach even if each individual delay is minor?"; 

console.log(`⚖️  LEGAL QUESTION: "${legalQuestion}"\n`);

let currentState = await app.invoke({ query: legalQuestion }, config);

// Handle all interruptions in a loop
while ((currentState as any).__interrupt__?.length > 0) {
  console.log("\n💭 APPLICATION PAUSED WAITING FOR USER INPUT...");

  const interruptQuestion = (currentState as any).__interrupt__[0]?.value
    ?.question;
  const userChoice = await getUserInput(
    interruptQuestion || "👤 YOUR CHOICE: "
  );

  currentState = await app.invoke(
    new Command({ resume: userChoice }),
    config
  );
}</code></pre><h3>执行脚本：</h3><p>分配好所有代码后，让我们在终端上执行 <code>main.ts</code> 文件，编写以下命令：</p><pre><code>tsx main.ts</code></pre><p>脚本执行后，问题<strong>“即便每次单独的延误都较为轻微，一连串重复出现的延误是否构成违约？”</strong>将被发送至 Elasticsearch 以执行邻近搜索，从索引中检索到的结果将予以展示。应用程序检测到多个相关判例与查询匹配，因此暂停执行，并请求用户协助明确哪个法律判例最为适用：</p><pre><code>📚 Searching for relevant legal precedents with query:
 Does a pattern of repeated delays constitute breach even if each individual delay is minor?
Found 5 relevant precedents:

1. Case H: Pattern of Repeated Delays (CASE-H-2021)
   Type: ongoing service agreement
   Outcome: breach found
   Key reasoning: pattern demonstrated failure to perform, cumulative effect
   Delay period: multiple instances

2. Case E: Minor Delay Quality Maintained (CASE-E-2022)
   Type: service agreement
   Outcome: minor breach only
   Key reasoning: delay minimal, quality maintained, termination unjustified
   Delay period: five days

3. Case A: Delay Breach with Operational Impact (CASE-A-2023)
   Type: service agreement
   Outcome: breach found
   Key reasoning: delay affected operations and caused financial harm
   Delay period: two weeks

4. Case B: Delay Without Explicit Schedule (CASE-B-2022)
   Type: consulting agreement
   Outcome: no breach found
   Key reasoning: no explicit deadline defined, no demonstrable harm
   Delay period: three weeks

5. Case C: Justified Delay External Factors (CASE-C-2023)
   Type: construction service
   Outcome: no breach found
   Key reasoning: external factors beyond control, force majeure applied
   Delay period: one month

⚖️  HITL #1: Human input needed

💭 APPLICATION PAUSED WAITING FOR USER INPUT...
👨‍⚖️  Which precedent is most similar to your case? </code></pre><p>这款应用程序的有趣之处在于，我们能够使用自然语言来选择一个选项，让 LLM 解析用户输入内容，从而确定正确选择。让我们看看，若输入文本“<strong>Case H</strong>”会发生什么。</p><pre><code>💭 APPLICATION PAUSED WAITING FOR USER INPUT...
👨‍⚖️  Which precedent is most similar to your case? Case H

✅ Selected: Case H: Pattern of Repeated Delays

📝 Drafting initial legal analysis...

AMBIGUITY DETECTED:
Based on Case H, a pattern of repeated delays can indeed constitute a breach of contract, even if each individual delay is minor. The outcome in Case H indicates that the cumulative effect of these minor delays led to a significant failure to perform the contractual obligations adequately. The reasoning emphasizes that consistent performance is critical in fulfilling the terms of a contract. Therefore, if the repeated delays create a situation where the overall performance is hindered, this pattern could be interpreted as a breach. However, the interpretation may depend on the specific terms of the contract at issue, as well as the expectations of performance set forth in that contract.

Missing information:
1. Specific contract terms regarding performance timelines
2. Details on the individual delays (duration, frequency)
3. Context on consequences of delays stated in the contract
4. Other parties' expectations or agreements related to performance


⚖️  HITL #2: Additional context needed


💭 APPLICATION PAUSED WAITING FOR USER INPUT...
👨‍⚖️  Please provide clarification about your contract terms:</code></pre><p>模型会采纳用户的澄清信息，并将其整合进工作流，在提供足够背景信息后开展最终分析。在此步骤中，系统还会利用先前检测到的歧义：草案分析指出合同中缺失的细节，这些细节可能对法律解释产生实质性影响。这些“缺失信息”项为模型提供指引，助其确定在给出可靠最终意见前，为消除不确定性而必须获取的关键澄清内容。</p><p>用户必须在下一次输入中包含此前被要求澄清的内容。我们以如下内容为例进行尝试：<strong>“合同要求‘及时交付’，但未规定时间期限。在 6 个月内出现 8 次 2-4 天的延误。因错过 3 次客户截止日期，造成 5 万美元损失。已通知供应商，但此类情况仍持续发生。”</strong></p><pre><code>💭 APPLICATION PAUSED WAITING FOR USER INPUT...
👨‍⚖️  Please provide clarification about your contract terms: Contract requires "prompt delivery" without timelines. 8 delays of 2-4 days over 6 months. $50K in losses from 3 missed client deadlines. Vendor notified but pattern continued.

📋 Generating final legal analysis...

================================================================================
⚖️  FINAL LEGAL ANALYSIS
================================================================================

To analyze the question of whether a pattern of repeated minor delays constitutes a breach of contract, we need to combine insights from the selected precedent, the specifics of the lawyer's contract situation, conditions that typically govern breach versus non-breach, and practical recommendations for the lawyer moving forward.

### 1. Selected Precedent's Reasoning

The precedent case, referred to as Case H, found that a pattern of repeated delays amounted to a breach of contract. The court reasoned that even minor individual delays, when considered cumulatively, demonstrated a failure to perform as stipulated in the contract. The underlying rationale was that the cumulative effect of these minor delays could significantly undermine the purpose of the contract, which typically aims for timely performance and reliable delivery.

### 2. Lawyer's Specific Contract Context

In the lawyer's situation, the contract specified "prompt delivery" but did not provide a strict timeline. The vendor experienced 8 delays ranging from 2 to 4 days over a period of 6 months. These delays culminated in $50,000 in losses due to three missed client deadlines. The vendor was notified regarding these delays; however, the pattern of delays persisted.

Key considerations include:
- **Nature of the Obligations**: While “prompt delivery” does not define a strict timeline, it does imply an expectation for timely performance.
- **Material Impact**: The missed client deadlines indicate that these delays had a material adverse effect on the lawyer's ability to fulfill contractual obligations to third parties, likely triggering damages.

### 3. Conditions for Breach vs. No Breach

**Conditions for Breach**:
- **Pattern and Cumulative Effect**: Similar to the reasoning in Case H, evidence of a habitual pattern of delays can amount to a breach. Even if individual delays are minor, when combined, they may show a lack of diligence or reliability by the vendor.
- **Materiality**: The impact of these delays is crucial. If the cumulative delays adversely affect the contract's purpose or cause significant losses, this reinforces the case for a breach.
- **Notification and Opportunity to Cure**: The fact that the vendor was notified of the delays and failed to rectify the behavior can often be interpreted as a further indication of breach.

**Conditions for No Breach**:
- **Non-Material Delays**: If the delays did not affect the overall contractual performance or client obligations, this may lessen the likelihood of establishing a breach. However, given the risks and losses involved, this seems less relevant in this scenario.
- **Force Majeure or Justifiable Delays**: If the vendor could show that these delays were due to justify circumstances not within their control, it may potentially provide a defense against breach claims.

### 4. Practical Recommendations

1. **Assess Damages**: Document the exact nature of the financial losses incurred due to the missed deadlines to substantiate claims of damages.
  
2. **Gather Evidence**: Collect all communication regarding the delays, including any notifications sent to the vendor about the issues.

3. **Consider Breach of Contract Action**: Based on the precedent and accumulated delays, consider formalized communication to the vendor regarding a breach of contract claim, highlighting both the pattern and the impact of these repeated delays.

4. **Evaluate Remedies**: Depending upon the contract specifics, the lawyer may wish to pursue several remedies, including:
   - **Compensatory Damages**: For the financial losses due to missed deadlines.
   - **Specific Performance**: If timely delivery is critical and can still be enforced.
   - **Contract Termination**: Depending on the severity, terminating the contract and seeking replacements may be warranted.

5. **Negotiate Terms**: If continuing to work with the current vendor is strategic, the lawyer should consider renegotiating terms for performance guarantees or penalties for further delays.

6. **Future Contracts**: In future contracts, consider including explicit timelines and conditions for prompt delivery, as well as specified damages for delays to better safeguard against this issue.

By integrating the legal principles from the precedent with the specific context and conditions outlined, the lawyer can formulate a solid plan to address the repeated delays by the vendor effectively.</code></pre><p>此输出展示了工作流的最终阶段，在该阶段，模型将所选判例 (Case H) 与律师的澄清信息相结合，以生成一份完整的法律分析报告。系统解释了延误交付很可能构成违约的原因，列举了支持这一解释的各项因素，并给出了切实可行的建议。总体而言，该输出展示了 HITL 澄清如何消除歧义，并使模型能够生成有充分依据、贴合具体情境的法律意见。</p><h3>其他真实场景</h3><p>这种借助 Elasticsearch、LangGraph 以及人机协同技术的应用，在其他各类应用程序中也可能颇具价值，例如：</p><ul><li><strong>在工具调用执行前对其进行审查</strong>，例如在金融交易领域，人类会在下买入/卖出订单前予以批准。</li><li><strong>在需要时提供额外参数</strong>，例如在客户支持分流场景中，当 AI 对客户问题存在多种可能的解读时，人类客服人员会选择正确的问题类别。</li></ul><p>还有大量有待发掘的用例，在这些用例中，人机协同将成为具有变革性的关键因素。</p><h2>结论</h2><p>借助 LangGraph 和 Elasticsearch，我们能够构建具备自主决策能力且可作为线性工作流运行的智能体，或者构建具有条件判断功能、可依据不同条件选择不同路径的智能体。引入人机协同机制后，这些智能体能在决策过程中让实际用户参与进来，以填补背景信息空白，并在容错性至关重要的系统中请求用户确认。</p><p>此方法的优势之一在于，能够借助 Elasticsearch 的功能对大规模数据集进行筛选，随后利用 LLM 获取用户所选的单一文档。若仅使用 Elasticsearch 完成最后这一步骤，难度会大得多，因为人类在运用自然语言指代某个结果时，方式多种多样。</p><p>这种方法能够确保系统保持高速运行且令牌使用高效，因为我们仅向 LLM 发送做出最终决策所需的信息，而非整个数据集。与此同时，该方法还能使系统在检测用户意图方面保持高度精准，并不断迭代，直至选定用户期望的选项。</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[在 Streams 中利用机器学习自动化日志解析]]></title>
            <description><![CDATA[了解一种混合 ML 方法如何在 Streams 中结合日志格式指纹开展自动化实验，实现 94% 的日志解析准确率和 91% 的日志分区准确率。]]></description>
            <link>https://www.elastic.co/search-labs/cn/blog/log-parsing-partitioning-automation-experiments-streams</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/cn/blog/log-parsing-partitioning-automation-experiments-streams</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Nastia Havriushenko]]></dc:creator>
            <pubDate>Fri, 02 Jan 2026 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/965c58f19742361160593c38fcaa8b2f4b0d6cc5-3838x2159.png?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>在现代可观测性技术栈中，将来自不同数据源的非结构化日志摄入 Elasticsearch 等平台仍是一项挑战。依赖人工编写的解析规则会让数据管道变得脆弱 — 即使上游代码只有少量更新，也可能导致解析失败、数据无法建立索引。这种脆弱性还会因可扩展性问题而进一步恶化，在动态的微服务环境中，新服务不断加入，手动维护规则很快就会变成运维噩梦。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/9108ec303339dd091faa3c363c7cf5c228155f49-3840x2160.png" style="width: 100%;" /><p>我们的目标是转向一种自动化、自适应的方法，能够同时处理日志解析（字段提取）和日志分区（来源识别）。我们假设，大语言模型（LLM）凭借对代码语法与语义模式的理解，能够在最少人工干预的情况下自动处理这些任务。</p><p>我们很高兴地宣布，此功能已在 <a href="http://elastic.co/elasticsearch/streams"><span style="text-decoration:underline">Streams</span></a> 中正式推出！</p><h2>数据集描述</h2><p>我们选择了<strong><a href="https://github.com/logpai/loghub">Loghub</a> </strong>日志集合用于概念验证。我们的调查从以下关键领域选取了代表性样本：</p><ul><li>分布式系统：我们使用了 Hadoop 分布式文件系统 (HDFS) 和 Spark 数据集。这些日志混合了大数据平台典型的信息、调试和错误消息。</li><li>服务器与 Web 应用：Apache Web 服务器和 OpenSSH 的日志提供了访问、错误以及与安全相关事件的重要信息来源，这对于监控 Web 流量和检测潜在威胁至关重要。</li><li>操作系统：我们纳入了 Linux 和 Windows 日志。这些数据集代表了运维团队日常处理的常见、半结构化系统级事件。</li><li>移动系统：为确保模型能处理移动环境日志，我们加入了 Android 数据集。这些日志通常较为冗长，涵盖了移动设备上广泛的应用程序和系统级活动。</li><li>超级计算机：为测试在高性能计算环境下的表现，我们引入了 BGL 数据集，其特点是包含使用特定领域术语的高度结构化日志。</li></ul><p>Loghub 集合的一个关键优势在于，其日志基本未经清洗处理和标注，真实模拟了具有微服务架构的、嘈杂的线上生产环境。</p><p>日志示例：</p><pre><code>[Sun Dec 04 20:34:21 2005] [notice] jk2_init() Found child 2008 in scoreboard slot 6
[Sun Dec 04 20:34:25 2005] [notice] workerEnv.init() ok /etc/httpd/conf/workers2.properties
[Mon Dec 05 11:06:51 2005] [notice] workerEnv.init() ok /etc/httpd/conf/workers2.properties
17/06/09 20:10:58 INFO output.FileOutputCommitter: Saved output of task 'attempt_201706092018_0024_m_000083_1138' to hdfs://10.10.34.11:9000/pjhe/test/1/_temporary/0/task_201706092018_0024_m_000083
17/06/09 20:10:58 INFO mapred.SparkHadoopMapRedUtil: attempt_201706092018_0024_m_000083_1138: Committed</code></pre><p>此外，我们还搭建了一个包含典型 Web 应用与数据库的 Kubernetes 集群，用于在最常见的场景中采集更多日志。</p><p>常见日志字段示例：时间戳、日志级别（INFO、WARN、ERROR）、来源、消息内容。</p><h2>使用 LLM 进行少样本日志解析</h2><p>我们的首轮实验聚焦于一个根本问题：<strong>LLM 能否可靠地识别关键字段，并生成一致的解析规则来提取它们？</strong></p><p>我们要求模型分析原始日志样本，并以正则表达式和 <a href="https://www.elastic.co/docs/explore-analyze/scripting/grok">Grok</a> 格式生成解析规则。结果显示，此方法潜力巨大，但也面临显著的实现挑战。</p><h3>高置信度与上下文感知</h3><p>初步结果令人鼓舞。LLM 展现出强大的能力，能高置信度地生成与提供的少数样本相匹配的解析规则。除了简单的模式匹配，模型还展现出对日志的理解能力 — 它能正确识别并命名产生日志的来源服务（例如健康追踪应用、Nginx Web 应用、Mongo 数据库）。</p><h3>输入样本的“恰到好处”困境</h3><p>我们的实验很快暴露出一个明显的鲁棒性问题，即<strong>对输入样本极其敏感</strong>。模型的性能会根据提示中包含的具体日志样本而剧烈波动。我们观察到一个日志相似性难题：样本里的日志需要达到<em>适中的多样性水平</em>，从而避免：</p><ul><li>过于同质（过拟合）<strong>：</strong>如果输入日志过于相似，LLM 倾向于<strong>过度具体化</strong>。它会把可变数据（例如堆栈跟踪里的具体 Java 类名）当成模板的固定部分。这导致生成的规则非常脆弱，只能覆盖极少部分日志，并提取出无用的字段。</li><li>过于异质（困惑）：反之，如果样本包含显著的格式差异（或更糟，包含了“垃圾日志”），模型就难以找到共同模式。它往往会生成复杂但有缺陷的正则表达式，或直接将整行内容过度泛化为一个单一的消息块字段。</li></ul><h3>上下文窗口限制</h3><p>我们还遇到了上下文窗口瓶颈。当输入日志较长、异构或包含大量可提取字段时，模型的输出质量常常会下降，变得“混乱”或过长而超出输出上下文窗口。在这种情况下，分块会有所帮助。通过使用基于字符和基于实体的分隔符来分割日志，我们可以帮助模型专注于提取主要字段，而不被噪声淹没。</p><h3>一致性与标准化差距</h3><p>即使模型成功生成规则，我们也注意到一些细微的不一致：</p><ul><li>服务命名差异：模型在不同运行中会对同一实体使用不同名称（例如将来源标记为“Spark”“Apache Spark”“Spark Log Analytics”）。</li><li>字段命名差异：字段名称缺乏标准化（例如，<code>id</code> vs. <code>service.id</code> vs. <code>device.id</code>）。我们使用标准化的 <a href="https://www.elastic.co/docs/reference/ecs/ecs-field-reference">Elastic 字段命名规范</a>对名称进行了统一。</li><li>解析粒度差异：字段提取的粒度因输入日志之间的相似程度而异。</li></ul><h2>日志格式指纹</h2><p>为了解决日志相似性问题，我们引入了一种高性能的启发式方法：<strong>日志格式指纹（LFF）</strong>。</p><p>我们不再将原始、嘈杂的日志直接输入 LLM，而是首先应用一种确定性转换来揭示每条消息的底层结构。这个预处理步骤抽象掉变量数据，生成一个简化的“指纹”，使我们能够对相关日志进行分组。</p><p>映射逻辑很简单，以确保速度和一致性：</p><ol><li>数字抽象：任何数字序列（0–9）都会替换为单个“0”。</li><li>文本抽象：任何由字母字符及其间空白组成的序列都会替换为单个“a”。</li><li>空白字符规范化：所有空白字符序列被压缩为单个空格。</li><li>符号保留：标点符号和特殊字符被保留，因为它们通常是日志结构最有力的指示符。</li></ol><p>我们引入了日志映射方法。基本映射模式包括以下几种：</p><ul><li>任意长度的数字（0–9）→ 替换为单个“0”。</li><li>任意长度的文本（字母字符及空白）→ 替换为单个“a”。</li><li>空格、制表符和换行符 → 合并为一个空格。</li></ul><p>让我们看一个这种映射如何转换日志的例子。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/78fa2887486eb9417804354ee3bf2a4fdb0f6383-846x252.png" style="width: 100%;" /><p>因此我们得到如下日志“掩码”（指纹）：</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/ec439a3d3a25002498b97defcff733ea5ebc6b55-826x94.png" style="width: 100%;" /><p>请注意前两个日志的指纹。尽管时间戳、来源类名和消息内容不同，但它们的前缀（<code>0/0/0 0:0:0 a a.a:</code>）完全一致。这种结构上的一致性使我们能够自动将这些日志归入同一个聚类。这种结构上的一致性使我们能自动把这些日志分桶到同一个聚类中。</p><p>第三个日志会生成完全不同的指纹（<code>0-0-0...</code>），这使我们能够在调用 LLM 之前就用算法将其与第一组区分开来。这使我们在调用LLM <em>之前</em> ，通过算法将其与第一组分离。</p><h2>奖励部分：使用 ES|QL 进行即时实施</h2><p>在 Discover 中运行这条查询就能做到这一点，非常简单。</p><pre><code>FROM loghub |
EVAL pattern = REPLACE(REPLACE(REPLACE(REPLACE(raw_message, "[ \t\n]+", " "), "[A-Za-z]+", "a"), "[0-9]+", "0"), "a( a)+", "a") |
STATS total_count = COUNT(), ratio = COUNT() / 2000.0, datasources=VALUES(filename), example=TOP(raw_message, 3, "desc") BY SUBSTRING(pattern, 0, 15) |
SORT total_count DESC |
LIMIT 100</code></pre><p><strong>查询解析：</strong></p><p><strong>FROM</strong> loghub：指向包含原始日志数据的索引。</p><p><strong>EVAL</strong> pattern =…：核心映射逻辑。我们通过链式 REPLACE 函数执行抽象化处理（例如将数字替换为“0”、文本替换为“a”等），并将结果保存至“pattern”字段。</p><p><strong>STATS </strong>[column1 =] expression1, …<strong> BY </strong>SUBSTRING(pattern, 0, 15):</p><p>这是一个集群步骤。我们将具有前 15 个字符相同的日志进行分组，并创建聚合字段，例如每组的日志总数、日志数据源列表、模式前缀以及 3 条日志示例。</p><p><strong>SORT</strong> total_count DESC | <strong>LIMIT</strong> 100：显示出现频率最高的前 100 个日志模式</p><p>查询结果的可视化如下所示：</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/b119498f124376c41d242a099bf9081fd6536be8-1600x394.png" style="width: 100%;" /><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/4dcfc0a5b7fa753497cc5def5ea3cd54449c0481-1600x719.png" style="width: 100%;" /><p>如可视化所示，这种“无需 LLM”的方法能够以很高的准确率对日志进行分区/归因分组。它（基于 LogHub 标签）在 16 个数据源中有 10 个实现了几乎完全的聚类（&gt;90%），并在 16 个数据源中的 13 个实现了多数聚类（&gt;60%），且无需额外清洗、预处理或微调。</p><p>日志格式指纹为<a href="https://www.elastic.co/docs/reference/aggregations/search-aggregations-bucket-categorize-text-aggregation">日志模式分析</a>等复杂的 ML 解决方案提供了一种务实、高效的替代与补充方案。它能立即洞察日志间的关系，并有效管理大型日志集群。</p><ul><li>作为基础组件的多功能性 </li></ul><p>借助 <a href="https://www.elastic.co/blog/getting-started-elasticsearch-query-language">ES|QL</a> 实现，LFF 既可作为独立工具用于快速数据诊断/可视化，也可作为日志分析流水线中的基础构件，支撑高吞吐量场景。 </p><ul><li>灵活性</li></ul><p>LFF 易于定制和扩展以捕获特定模式，例如十六进制数和 IP 地址。</p><ul><li>确定性稳定性</li></ul><p>与基于 ML 的聚类算法不同，LFF 逻辑简单且确定。新传入的日志不会追溯性地影响现有的日志聚类。</p><ul><li>性能与内存</li></ul><p>它需要最少的内存，无需训练或 GPU，非常适合实时高吞吐量环境。</p><h2>结合日志格式指纹与 LLM</h2><p>为了验证所提出的混合架构，每个实验都包含来自每个数据源的日志的随机 20% 子集。此约束模拟了现实世界的生产环境，在该环境中，日志是批量处理的，而不是作为一个整体的历史转储进行处理。</p><p>目标是证明 LFF 能作为有效的压缩层。我们希望证明，即使只用少量经过筛选的样本，也能生成高覆盖率的解析规则，并成功泛化到整个数据集。</p><h2>执行管道</h2><p>我们实现了一个多阶段流程，在数据到达 LLM 之前对其进行过滤、聚类和应用分层抽样。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/b3f46ea471760b406a32fc7d4bc74cc03faaced2-3840x1660.png" style="width: 100%;" /><p>1. 两阶段分层聚类</p><ul><li>子类（精确匹配）：通过完全相同的指纹对日志进行聚合。同一子类的每个日志共享完全相同的格式结构。</li><li>异常值清理：丢弃占总日志量少于 5% 的任何子类，这确保 LLM 聚焦于主要信号，不会被噪声或格式异常的日志带偏。</li><li>元类（前缀匹配）：剩余的子类通过格式指纹的前 N 个字符匹配分组到元类中。这种分组策略可有效将词汇相似的格式归并到同一个大类下。当数据源未知时，我们选择 N=5 用于日志解析，N=15 用于数据源未知时的日志分区。</li></ul><p>2. 分层抽样。一旦分层树构建完成，我们为 LLM 构建日志样本。战略目标是最大化方差覆盖，同时最小化 Token 使用。</p><ul><li>我们从更广泛的元类中，为<em>每个</em>有效子类选取具有代表性的日志。</li><li>为处理子类过多的边缘情况，应用随机下采样以适应目标窗口大小。</li></ul><p>3. 规则生成：最后，我们提示 LLM 为每个元类生成一个适用于所提供样本中所有日志的正则表达式解析规则。在概念验证中，我们使用了 GPT-4o mini 模型。</p><h2>实验结果与观察</h2><p>我们在 Loghub 数据集上实现了 94% 的解析准确率和 91% 的分区准确率。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/49b2b6a1401dd1f33951da68e5a3fac37d0b5aaa-1600x1506.png" style="width: 100%;" /><p>混淆矩阵展示了日志分区结果。垂直轴代表实际数据源，水平轴代表预测的数据源。热图颜色深浅对应日志量，颜色越浅表示数量越多。对角线排列显示了模型在来源归因上的高保真度，且分散极少。</p><h2>我们的性能基准测试洞察：</h2><ul><li><strong>最佳基线：</strong>每个类别 <strong>30–40 条日志样本</strong>的上下文窗口被证明是“最佳区间”，能稳定生成稳健的 Regex 与 Grok 解析模式。</li><li><strong>输入最小化：</strong>我们将每个类别的输入大小推至 10 个日志（用于正则表达式模式），仅观察到解析性能下降 2%，这证实了基于多样性的抽样比原始数量更为关键。</li></ul>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[在 Google MCP Toolbox for Databases 中引入 Elasticsearch 支持]]></title>
            <description><![CDATA[了解 Google MCP Toolbox for Databases 现在如何提供 Elasticsearch 支持，并利用 ES|QL 工具将您的索引安全地集成到任何 MCP 客户端中。]]></description>
            <link>https://www.elastic.co/search-labs/cn/blog/google-mcp-toolbox-elasticsearch-support</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/cn/blog/google-mcp-toolbox-elasticsearch-support</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Enrico Zimuel, Laurent Saint-Félix]]></dc:creator>
            <pubDate>Fri, 12 Dec 2025 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/425a48691f436ed47c9bdfaf5d561ac122b2c472-1062x668.png?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>在本文中，我们将介绍如何使用带有 <a href="https://github.com/elastic/elasticsearch">Elasticsearch</a> 的 Google MCP Toolbox 来构建一个用于从 Elasticsearch 索引中提取信息的简单工具。</p><p>我们最近为 <a href="https://github.com/googleapis/genai-toolbox">Google MCP Toolbox for Databases</a> 开源项目做出了贡献，为其添加了对 Elasticsearch 数据库的支持。</p><p>有了这项新功能，您现在可以使用 Google MCP Toolbox 连接到 Elasticsearch，并直接与数据“对话”。</p><h2>Elasticsearch</h2><p>我们需要运行一个 Elasticsearch 实例。您可以在 <a href="https://www.elastic.co/cloud">Elastic Cloud</a> 上激活免费试用版，或使用 <a href="https://github.com/elastic/start-local">start-local</a> 脚本在本地安装：</p><pre><code>curl -fsSL https://elastic.co/start-local | sh</code></pre><p>这将在计算机上安装 Elasticsearch 和 Kibana，并生成用于配置 Google MCP Toolbox 的 API 密钥。</p><p>API 密钥将显示为上一条命令的输出，并存储在 elastic-start-local 文件夹的 .env 文件中。</p><h2>安装示例数据集</h2><p>安装完成后，您可以使用启动本地脚本（存储在 .env 文件中）生成的用户名 <em>elastic</em> 和密码登录 Kibana。</p><p>您可以安装 Kibana 提供的<strong>电子商务订单</strong>数据集。它包含一个名为 <strong>kibana_sample_data_ecommerce</strong> 的单个索引，其中包含来自一家电子商务网站的 4,675 个订单的信息。对于每笔订单，我们都有以下信息：</p><ul><li>客户信息（姓名、ID 号码、出生日期、电子邮件等）</li><li>订单日期</li><li>订单编号</li><li>产品（包含价格、数量、ID、类别、折扣等信息的所有产品列表）</li><li>SKU</li><li>总价（不含税，含税）</li><li>总数量</li><li>地理信息（城市、国家、洲、位置、地区）</li></ul><p>要安装示例数据，请在 Kibana 中打开“<strong>集成</strong>”页面（在顶部搜索栏中搜索“集成”），然后安装“示例数据”。有关详细信息，请参阅此处的文档：<a href="https://www.elastic.co/docs/explore-analyze/#gs-get-data-into-kibana">https://www.elastic.co/docs/explore-analyze/#gs-get-data-into-kibana</a>。</p><p>本文旨在展示如何轻松配置 Google MCP Toolbox 以连接到 Elasticsearch，并使用自然语言与 <strong>kibana_sample_data_ecommerce</strong> 索引进行交互。</p><h2>Google MCP 工具箱</h2><p>Google MCP Toolbox 是一款开源 MCP 服务器，旨在使应用程序和 AI 代理能够轻松、安全、高效地与数据库进行交互。该项目以前称为“GenAI Toolbox for Databases”，在与<a href="https://www.anthropic.com/news/model-context-protocol">模型上下文协议</a> (MCP) 完全兼容后重新命名。其目的是通过在幕后处理连接池、身份验证、可观察性和其他操作问题，消除传统上需要将代理连接到数据库的繁重工作。</p><p>Toolbox 的核心功能是允许开发人员定义可重用的高级工具，封装数据库交互操作。然后，任何兼容 MCP 的客户端（如 AI 代理）都可以调用这些工具，而无需客户端执行低级 SQL 查询或管理数据库连接。这种方法大大减少了构建数据库感知代理所需的模板代码量，只需几行应用程序逻辑就能集成高级数据操作。一旦定义工具，就可以在多个代理、框架或语言之间共享（图 1）。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/137cea290bb70ad5da21853f9a6358cef4cf7451-1248x1056.png" style="width: 100%;" /><p>使用 Toolbox 的一大优势是内置的安全模型。原生支持 OAuth2 和 OIDC 等身份验证流程，使开发者避免在代理中处理或存储敏感的数据库凭据。该平台还通过 OpenTelemetry 提供可观测性功能（包括指标和跟踪），这对于调试、监控和生产部署至关重要。总而言之，MCP Toolbox 是一个统一、安全和可扩展的接口，可从任何支持 MCP 的系统与您的数据进行交互。</p><h2>如何安装 MCP Toolbox</h2><p>您可以使用以下命令在 Linux 上安装 MCP Toolbox 服务器：</p><pre><code>export VERSION=0.21.0
curl -L -o toolbox https://storage.googleapis.com/genai-toolbox/v$VERSION/linux/amd64/toolbox
chmod +x toolbox</code></pre><p>如果您想将其安装在 macOS 或 Windows 上，您可以按照<a href="https://googleapis.github.io/genai-toolbox/getting-started/introduction/#installing-the-server">此处</a>的详细说明进行操作。</p><h2>配置适用于 Elasticsearch 的 Toolbox</h2><p>要为 Elasticsearch 配置 MCP Toolbox，我们需要创建一个 <strong>tools.yaml</strong> 文件，如下所示：</p><pre><code>sources:
  my-cluster:
    kind: elasticsearch
    addresses:
      - http://localhost:9200
    apikey: <insert-here-api-key>

tools:
  customer-orders:
    kind: elasticsearch-esql
    source: my-cluster
    description: Get the orders made by a customer identified by name.
    query: |
    	FROM kibana_sample_data_ecommerce | WHERE MATCH(customer_full_name, ?name, {"operator": "AND"})
    parameters:
      - name: name
        type: string
        description: The customer name.

toolsets:
  elasticsearch-tools:
    - customer-orders</code></pre><p>您需要使用有效的 Elasticsearch API 密钥替换 <strong>&lt;insert-here-api-key&gt;</strong> 值。如果您使用 start-local 在本地运行 Elasticsearch，则可以在.env 文件中找到由 start-local 生成的 API 密钥，位于 <strong>ES_LOCAL_API_KEY</strong> 变量下。如果您正在使用 Elastic Cloud，则可以按照<a href="https://www.elastic.co/docs/deploy-manage/api-keys/elastic-cloud-api-keys">此处</a>所描述的步骤生成 API 密钥。</p><p>之前的工具包含以下适用于 Elasticsearch 的 ES|QL 查询：</p><pre><code>FROM kibana_sample_data_ecommerce | WHERE MATCH(customer_full_name, ?name)</code></pre><p>如果您不熟悉 ES|QL，它是由 Elastic 开发的一种类似于 SQL 的查询语言，可用于在一个或多个索引中进行搜索。您可以在<a href="https://www.elastic.co/docs/reference/query-languages/esql">此处</a>的正式文档中阅读有关 ES|QL 的更多信息。</p><p>上述查询使用 <strong>?name</strong> 参数（问号表示参数）搜索存储在 <strong>kibana_sample_data_ecommerce</strong> 索引中所有包含指定客户姓名的订单。</p><p>在之前的 YAML 配置中，客户名称使用字符串类型并附带描述“客户名称”来定义。</p><p>此工具可用于回答有关客户订单的问题——例如：<em>客户 Foo 在 2025 年 10 月下了多少订单？</em></p><p>对工具及其参数的描述对于从用户的自然语言请求中提取相关信息至关重要。这种提取是通过大型语言模型 (LLM) 的<strong>函数调用</strong>功能实现的。在实践中，LLM 可以确定需要执行哪个函数（工具）以获取必要的信息，并为该函数指定适当的参数。</p><p>有关函数调用的更多信息，我们建议阅读 Ashish Tiwari 撰写的《<a href="https://www.elastic.co/search-labs/blog/function-calling-with-elastic">使用 Elasticsearch 进行 OpenAI 函数调用</a>》。</p><h2>运行 Toolbox 服务器</h2><p>您可以使用之前的 tools.yaml 文件，通过以下命令运行 MCP 工具箱：</p><pre><code>./toolbox --tools-file tools.yaml --ui</code></pre><p><strong>—ui</strong> 参数在 <a href="http://127.0.0.1:5000/ui">http://127.0.0.1:5000/ui</a> 上运行 Web 应用程序（图 2）。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/3caf2339b632bafd5847af1ed8b33b518a25b8a2-1600x314.png" style="width: 100%;" /><p>您可以选择<strong>工具</strong> &gt; <strong>客户订单</strong>，并在参数<strong>名称</strong>（例如，Gwen Sanders）中插入客户名称。然后点击<strong>“运行工具”</strong>按钮。您应该会看到如图 3 所示的 JSON 响应。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/b167e0142afb8919d9cedf6d0fa431d33d0e55f8-1600x933.png" style="width: 100%;" /><p>设置已完成，MCP Toolbox 可以执行<strong>客户订单</strong>工具与 Elasticsearch 进行通信，运行 ES|QL 查询。</p><h2>将 MCP Toolbox 与 Gemini CLI 结合使用</h2><p>我们可以使用任何 MCP 客户端与 MCP Toolbox for Databases 进行通信。例如，我们可以使用命令行工具 <a href="https://github.com/google-gemini/gemini-cli">Gemini CLI</a> 来使用 Gemini。您可以按照<a href="https://geminicli.com/docs/get-started/installation/">此处</a>提供的说明安装 Gemini CLI。</p><p>Gemini CLI 为 MCP Toolbox 提供了一个预配置扩展程序，可在 <a href="https://github.com/gemini-cli-extensions/mcp-toolbox">gemini-cli-extensions/mcp-toolbox</a> 上获取。您可以通过运行以下命令来安装此扩展程序：</p><pre><code>gemini extensions install https://github.com/gemini-cli-extensions/mcp-toolbox</code></pre><p>安装完成后，您需要进入为 MCP Toolbox 存储 tools.yaml 配置文件的目录，并按如下步骤执行 Gemini CLI（此步骤是 Gemini CLI 与 MCP Toolbox 自动配置所必需的）：</p><pre><code>gemini</code></pre><p>您应该会看到图 4 中所示的输出广告。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/0f22df6d3da13c1dc50dcb560414fa7c630eb9a7-1434x341.png" style="width: 100%;" /><p>您可以使用以下命令检查 MCP Toolbox 是否已连接：</p><pre><code>/mcp list</code></pre><p>您应该能看到已列出<strong>客户订单</strong>工具的 <strong>mcp_toolbox</strong>（图 5）。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/97edbc40de9e44f469f6f3a09427532be167de0e-493x155.png" style="width: 100%;" /><p>如果 MCP Toolbox 已连接到 Gemini CLI，我们现在可以尝试问一些问题，例如：“<em>给我客户 Gwen Sanders 的订单</em>。”然后，Gemini CLI 将向 mcp_toolbox 服务器请求执行客户订单工具的权限（参见图 6）。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/c052f3b5e49436903b804280c0065f67ee02444b-1432x284.png" style="width: 100%;" /><p>确认后，Gemini CLI 将向 MCP Toolbox 执行请求，得到 JSON 响应结果，并使用它来格式化响应（图 7）。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/7ea5128f1705951c2757af6da4b456d394d4a080-1432x734.png" style="width: 100%;" /><p>Gemini CLI 的响应将报告 Gwen Sanders 只下了一个订单，包含 2 件产品，总价为 132 欧元。</p><h2>MCP 工具箱 SDK</h2><p>Google MCP Toolbox 还提供一个 SDK，可用于访问用 Go、Python 和 Javascript 编写的程序中的所有功能。</p><p>例如，Python SDK 可在 Github 上获取，页面如下：<a href="https://github.com/googleapis/mcp-toolbox-sdk-python">https://github.com/googleapis/mcp-toolbox-sdk-python</a>。</p><p>我们需要创建一个简单的代理来连接 MCP 工具箱。我们需要安装以下软件包：</p><pre><code>pip install toolbox-core
pip install google-adk</code></pre><p>然后使用以下命令创建一个新的代理项目：</p><pre><code>adk create my_agent</code></pre><p>这会创建一个名为 <strong>my_agent</strong> 的新目录，其中包含文件 <strong>agent.py</strong>。</p><p>使用以下内容更新 <strong>my_agent/agent.py</strong>，以连接到 Toolbox：</p><pre><code>from google.adk import Agent
from google.adk.apps import App
from toolbox_core import ToolboxSyncClient

client = ToolboxSyncClient("http://127.0.0.1:5000")

root_agent = Agent(
    name='root_agent',
    model='gemini-2.5-flash',
    instruction="You are a helpful AI assistant designed to search information about a dataset of ecommerce orders.",
    tools=client.load_toolset(),
)

app = App(root_agent=root_agent, name="my_agent")</code></pre><p>创建一个 <strong>.env</strong>文件，其中包含您的 Google API 密钥：</p><pre><code>echo 'GOOGLE_API_KEY="YOUR_API_KEY"' > my_agent/.env</code></pre><p>最后，我们可以运行代理并观察结果。要执行代理，您可以运行以下命令：</p><pre><code>adk run my_agent</code></pre><p>或者，您也可以通过 Web 接口提供服务：</p><pre><code>adk web --port 8000</code></pre><p>在这两种情况下，您都可以使用问答接口与 MCP Toolbox 进行交互。例如，您可以提出前一个问题：<em>给我客户 Gwen Sanders 的订单</em>。</p><p>有关不同 SDK 的更多信息，可以参考<a href="https://googleapis.github.io/genai-toolbox/sdks/">此文档页面</a>。</p><h2>结论</h2><p>在本文中，我们演示了 Elasticsearch 与 Google MCP Toolbox for Databases 的集成。使用简单的 YAML 配置文件，我们可以定义一组工具，这些工具使用 ES|QL 语言将自然语言问题转换为 Elasticsearch 查询。</p><p>我们展示了如何与 kibana_sample_data_ecommerce 数据集进行交互，该数据集包含来自电子商务网站的订单。通过这个配置文件，我们可以简单地运行 MCP Toolbox 服务器并从任何 MCP 客户端连接到它。</p><p>最后，我们演示了如何使用 Gemini CLI 作为客户端连接到 MCP Toolbox for Databases 并查询存储在 Elasticsearch 中的电子商务数据。我们执行了自然语言查询，以检索有关特定客户（以姓名标识）的订单信息。</p><p>随着 MCP 生态系统的不断发展，这种模式——轻量级工具定义，由安全、生产就绪的基础架构支持——为构建越来越强大、数据感知的代理提供了新的机会，且所需努力最小。无论您是在本地尝试 Elastic 的示例数据集，还是将搜索功能集成到更大的应用程序中，MCP 工具箱都为使用自然语言与 Elasticsearch 数据进行交互提供了可靠、可扩展的基础。</p><p>有关代理 AI 应用程序开发的更多信息，您可以阅读 Anish Mathur 和 Dana Juratoni 撰写的<a href="https://search-labs-redesign.vercel.app/search-labs/blog/ai-agentic-workflows-elastic-ai-agent-builder">《使用 Elasticsearch 构建 AI 代理工作流》</a>。</p><p>有关 Google MCP Toolbox 的更多信息，请访问 <a href="https://googleapis.github.io/genai-toolbox/getting-started/introduction/">https://googleapis.github.io/genai-toolbox/getting-started/introduction/</a>。</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[使用判断列表评估搜索查询的相关性]]></title>
            <description><![CDATA[针对在 Elasticsearch 中开展可扩展的搜索测试，探究如何构建判定列表以客观评估搜索查询相关性，并提升召回率等性能指标。]]></description>
            <link>https://www.elastic.co/search-labs/cn/blog/judgment-lists-search-query-relevance-elasticsearch</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/cn/blog/judgment-lists-search-query-relevance-elasticsearch</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Jhon Guzmán]]></dc:creator>
            <pubDate>Thu, 11 Dec 2025 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/25478d0ffb228afd5d65d82312998ec1c299c565-700x490.png?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>从事搜索引擎开发的工程师们常常会遇到同一个问题：业务团队对某次特定搜索结果并不满意，因为他们期望排在搜索结果首位的文档，实际却出现在结果列表的第三或第四位。</p><p>然而，当你解决这一问题时，却可能因无法手动测试所有情况而不经意间破坏其他查询的功能。但你或你的 QA 团队该如何测试，以确认某一项查询的改动是否会对其他查询产生连锁反应呢？或者更关键的是，你们要如何确保所作的改动确实优化了某项查询呢？</p><h2>转向系统性评估</h2><p>这个时候，判断列表就可以派上用场。与其在每次更改时依赖手动和主观测试，不如定义一组与业务案例相关的固定查询及其相关结果。</p><p>这一组（测试用例或数据）将成为基准参照。每次实施改动时，你都用它来评估搜索效果是否确实得到了提升。</p><p>这种方法的价值在于：</p><ul><li><strong>消除不确定性</strong>：无需再费心猜测所做的更改是否会影响其他查询；数据会直接告诉你答案。</li><li><strong>停止人工测试</strong>：一旦判定集被记录下来，测试便会自动执行。</li><li><strong>佐证更改</strong>：你可以展示出明确的指标，以佐证某项更改所带来的益处。</li></ul><h2>如何开始建立判断列表</h2><p>最简单的开始方式之一是获取具有代表性的查询，并手动选择相关文件。有两种方法可以列出此列表：</p><ul><li><strong>二元判断：</strong>与查询关联的每一份文档都会被赋予一个<strong>简单标签</strong>：<em>相关</em>（通常标注分数为“1”）和不相关（标注分数为“0”）。</li><li><strong>分级判断：</strong>在此情境下，每份文档会依据不同等级获得相应分数。例如：采用 0 至 4 分的评分量表，类似于<a href="https://en.wikipedia.org/wiki/Likert_scale">李克特量表</a>，其中 0 分表示“完全不相关”，4 分表示“完全相关”，中间还设有“相关”“有点相关”等不同程度表述。</li></ul><p>当搜索意图具有明确界限时，二元判断（是/否）十分奏效，即判断该文档是否应出现在搜索结果中？</p><p>当存在模糊地带时，分级判断更为实用：某些结果相较于其他结果更优，因此你可以将结果划分为“优秀”“良好”和“毫无价值”等不同等级，并运用能体现结果排序权重及用户反馈的评估指标。然而，分级量表也存在弊端：不同评审者对评分等级的使用方式可能存在差异，这会导致判断结果的一致性降低。并且，由于分级指标对高分赋予了更大的权重，即便是一个微小的改动（比如将某项评分从 4 分改为 3 分），也可能在指标上引发远超评审者预期的巨大波动。这种额外引入的主观性使得分级判断结果更具干扰性，且随时间推移愈发难以把控。</p><h2>我需要自己对文件分类吗？</h2><p>不一定，因为有多种不同方法创建判定列表，且每种方法各有其优缺点：</p><ul><li><strong>明确判断：</strong>在这种情况下，领域专家会逐一审阅每个查询/文档，并手动判定其相关性（或相关程度）。尽管此方法能确保质量并实现把控，但其可扩展性较差。</li><li><strong>隐式判断：</strong>采用这种方法时，你会依据真实用户行为（如点击量、跳出率、购买行为等）来推断相关文档。此方法可实现数据的自动收集，但可能存在偏差。例如，用户往往更倾向于点击排名靠前的结果，即便这些结果并不相关。</li><li><strong>AI 生成的判断：</strong>最后这种方法是借助模型（如 LLM）自动评估查询和文档，人们通常称之为<a href="https://en.wikipedia.org/wiki/LLM-as-a-Judge">LLM 陪审团</a>。其优势在于速度快且易于扩展，不过数据质量取决于所用模型的性能，以及大语言模型训练数据与您业务<a href="http://interests.as/">需求</a>的契合程度。与人工评分一样，LLM 评审团也可能引入自身偏见或出现前后不一致的情况，因此，必须对照一小部分可信判断结果来验证其输出结果。LLM 模型本质上具有概率性，所以即便将<a href="https://www.ibm.com/think/topics/llm-temperature">温度</a>参数设置为 0，也常见同一结果被 LLM 模型给出不同评分的情况。</li></ul><p>以下是一些选择最佳方法来构建判断集的建议：</p><ul><li>明确界定哪些仅用户能恰当判断的要素对你而言至关重要（例如价格、品牌、语言、风格以及产品细节等）。如果这些要素至关重要，则至少需针对<em>判断列表</em>中的部分内容获取<strong>明确的判断结果</strong>。</li><li>当你的搜索引擎已有足够流量时，可运用<strong>隐式判断</strong>，即借助点击量、转化率以及停留时长等指标来洞察使用趋势。不过，你仍需谨慎解读这些数据，将其与显式判断结果进行对比，以规避潜在偏差（例如用户往往更倾向于点击排名靠前的结果，即便排名靠后的结果更具相关性）。</li></ul><p>为解决这一问题，位置偏差消除技术会对点击数据进行调整或重新加权，以更准确地反映用户的真实兴趣。以下是一些方法：</p><ul><li><strong>结果随机排序：</strong>针对部分用户调整搜索结果的排序，以此估算结果位置对点击量的影响。</li><li><strong>点击模型</strong>包括<strong></strong><a href="https://wiki.math.uwaterloo.ca/statwiki/index.php?title=a_Dynamic_Bayesian_Network_Click_Model_for_web_search_ranking">动态贝叶斯网络 <strong>DBN</strong></a> 和<a href="https://rsrikant.com/papers/kdd10.pdf">用户浏览模型 <strong>UBM</strong></a>。这些统计模型会借助滚动行为、停留时长、点击顺序以及返回结果页等模式，来估算用户点击行为反映真实兴趣（而非仅受结果位置影响）的概率。</li></ul><h2>示例：电影评分应用</h2><h3>准备工作</h3><p>要运行此示例，需要一个正在运行的<a href="https://www.elastic.co/downloads/elasticsearch">本地</a>或部署在 <a href="https://www.elastic.co/cloud/cloud-trial-overview">Elastic Cloud</a> 上（托管或无服务器）的 Elasticsearch 8.x 集群，以及访问 <a href="https://www.elastic.co/docs/reference/elasticsearch/rest-apis">REST API</a> 或 Kibana 的权限。</p><p>想象有一款应用程序，用户可以在其中上传自己对电影的看法，还可以搜索要观看的电影。由于文本由用户自己撰写，因此可能存在拼写错误和表达方式上的多种差异。因此，搜索引擎必须能够解读这种多样性，并为用户提供有用的结果。</p><p>为能在不影响整体搜索行为的前提下对查询进行迭代优化，贵公司业务团队基于最常执行的搜索查询，创建了以下二元判断集：</p>
            <table style="border-collapse: collapse; margin: 20px 0;">
              <tbody>
                
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">查询</td><td style="border: 1px solid #ccc; padding: 4px;">DocID</td><td style="border: 1px solid #ccc; padding: 4px;">文本</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">迪卡普里奥的表演</td><td style="border: 1px solid #ccc; padding: 4px;">doc1</td><td style="border: 1px solid #ccc; padding: 4px;">迪卡普里奥在《荒野猎人》中的表演令人惊叹。</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">迪卡普里奥的表演</td><td style="border: 1px solid #ccc; padding: 4px;">doc2</td><td style="border: 1px solid #ccc; padding: 4px;">《盗梦空间》中，莱昂纳多·迪卡普里奥饰演了他最具标志性的角色之一。</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">迪卡普里奥的表演</td><td style="border: 1px solid #ccc; padding: 4px;">doc3</td><td style="border: 1px solid #ccc; padding: 4px;">布拉德·皮特在这部犯罪惊悚片中表现出色。</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">迪卡普里奥的表演</td><td style="border: 1px solid #ccc; padding: 4px;">doc4</td><td style="border: 1px solid #ccc; padding: 4px;">一部充满惊险动作、视觉效果惊艳的冒险大片。</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">让人热泪盈眶的悲伤电影</td><td style="border: 1px solid #ccc; padding: 4px;">doc5</td><td style="border: 1px solid #ccc; padding: 4px;">这是一个令人心碎的关于爱与失去的故事，让我哭了好几个小时。</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">让人热泪盈眶的悲伤电影</td><td style="border: 1px solid #ccc; padding: 4px;">doc6</td><td style="border: 1px solid #ccc; padding: 4px;">有史以来最催泪的电影之一，记得带上纸巾！</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">让人热泪盈眶的悲伤电影</td><td style="border: 1px solid #ccc; padding: 4px;">doc7</td><td style="border: 1px solid #ccc; padding: 4px;">让你捧腹大笑的轻松喜剧</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">让人热泪盈眶的悲伤电影</td><td style="border: 1px solid #ccc; padding: 4px;">doc8</td><td style="border: 1px solid #ccc; padding: 4px;">一部充满动作与激情的科幻史诗巨作。</td>
                    </tr>
              </tbody>
            </table>
          <p>正在创建索引：</p><pre><code>PUT movies
{
  "mappings": {
    "properties": {
      "text": {
        "type": "text"
      }
    }
  }
}</code></pre><p>批量请求：</p><pre><code>POST /movies/_bulk
{ "index": { "_id": "doc1" } }
{ "text": "DiCaprio performance in The Revenant was breathtaking." }
{ "index": { "_id": "doc2" } }
{ "text": "Inception shows Leonardo DiCaprio in one of his most iconic roles." }
{ "index": { "_id": "doc3" } }
{ "text": "Brad Pitt delivers a solid performance in this crime thriller." }
{ "index": { "_id": "doc4" } }
{ "text": "An action-packed adventure with stunning visual effects." }
{ "index": { "_id": "doc5" } }
{ "text": "A heartbreaking story of love and loss that made me cry for hours." }
{ "index": { "_id": "doc6" } }
{ "text": "One of the saddest movies ever made -- bring tissues!" }
{ "index": { "_id": "doc7" } }
{ "text": "A lighthearted comedy that will make you laugh." }
{ "index": { "_id": "doc8" } }
{ "text": "A science-fiction epic full of action and excitement." }</code></pre><p>以下是该应用程序正在使用的 Elasticsearch 查询：</p><pre><code>GET movies/_search
{
 "query": {
   "match": {
     "text": {
       "query": "DiCaprio performance",
       "minimum_should_match": "100%"
     }
   }
 }
}</code></pre><h3>从判断到指标</h3><p>就其本身而言，判断列表并不提供太多信息；它们只是我们查询结果的期望。它们真正的优势在于，当我们使用它们来计算客观指标以衡量我们的搜索性能时。</p><p>如今，大多数常用指标包含</p><ul><li><strong><a href="https://www.elastic.co/docs/reference/elasticsearch/rest-apis/search-rank-eval#k-precision">精度</a>：</strong>衡量所有搜索结果中真正相关的结果比例。</li><li><strong><a href="https://www.elastic.co/docs/reference/elasticsearch/rest-apis/search-rank-eval#k-recall">召回率</a>：</strong>衡量搜索引擎在检索出的 x 个结果中，找到的相关结果所占的比例。</li><li><strong><a href="https://www.elastic.co/docs/reference/elasticsearch/rest-apis/search-rank-eval#_discounted_cumulative_gain_dcg">折损累积增益（DCG）</a>：</strong>用于衡量结果排序的质量，该指标基于最相关的结果应排在前列这一原则进行评估。</li><li><a href="https://www.elastic.co/docs/reference/elasticsearch/rest-apis/search-rank-eval#_mean_reciprocal_rank"><strong>平均倒数排名（MRR）</strong></a>：用于衡量首个相关结果所处的排名位置情况 。在列表中越靠前，其分数就越高。</li></ul><p>以同样的电影评分应用程序为例，我们将计算召回率指标，看看我们的查询是否遗漏了任何信息。</p><p>在 Elasticsearch 中，我们可以通过<a href="https://www.elastic.co/docs/reference/elasticsearch/rest-apis/search-rank-eval">排名评估 API</a>，使用<em>判断列表</em>来计算指标。该 API 将判断列表、查询以及想要评估的指标作为输入，并返回一个数值，该数值是对查询结果与判断列表进行对比后得出的结果。</p><p>让我们针对已提出的这两个查询运行判定列表：</p><pre><code>POST /movies/_rank_eval
{
 "requests": [
   {
     "id": "dicaprio-performance",
     "request": {
       "query": {
         "match": {
           "text": {
             "query": "DiCaprio performance",
             "minimum_should_match": "100%"
           }
         }
       }
     },
     "ratings": [
       {
         "_index": "movies",
         "_id": "doc1",
         "rating": 1
       },
       {
         "_index": "movies",
         "_id": "doc2",
         "rating": 1
       },
       {
         "_index": "movies",
         "_id": "doc3",
         "rating": 0
       },
       {
         "_index": "movies",
         "_id": "doc4",
         "rating": 0
       }
     ]
   },
   {
     "id": "sad-movies",
     "request": {
       "query": {
         "match": {
           "text": {
             "query": "sad movies that make you cry",
             "minimum_should_match": "100%"
           }
         }
       }
     },
     "ratings": [
       {
         "_index": "movies",
         "_id": "doc5",
         "rating": 1
       },
       {
         "_index": "movies",
         "_id": "doc6",
         "rating": 1
       },
       {
         "_index": "movies",
         "_id": "doc7",
         "rating": 0
       },
       {
         "_index": "movies",
         "_id": "doc8",
         "rating": 0
       }
     ]
   }
 ],
 "metric": {
   "recall": {
     "k": 10,
     "relevant_rating_threshold": 1
     }
 }
}</code></pre><p>我们将向 rank_eval 发送两个请求：一个针对莱昂纳多·迪卡普里奥查询，另一个针对悲伤电影查询每个请求均包含一个查询及其对应的判定列表（评分）。我们无需对所有文档进行评分，因为未纳入评分范围的文档将被视为未作判定。在进行计算时，召回率仅考虑“相关文档集”，即那些在评分中被认定为相关的文档。</p><p>在此情形下，针对莱昂纳多·迪卡普里奥的查询召回率为 1，而悲伤电影查询的召回率为 0。这意味着对于第一个查询，我们能够获取到所有相关结果，而第二个查询则未获取到任何相关结果。因此，平均召回率为 0.5。</p><pre><code>{
 "metric_score": 0.5,
 "details": {
   "dicaprio-performance": {
     "metric_score": 1,
     "unrated_docs": [],
     "hits": [
       {
         "hit": {
           "_index": "movies",
           "_id": "doc1",
           "_score": 2.4826927
         },
         "rating": 1
       },
       {
         "hit": {
           "_index": "movies",
           "_id": "doc2",
           "_score": 2.0780432
         },
         "rating": 1
       }
     ],
     "metric_details": {
       "recall": {
         "relevant_docs_retrieved": 2,
         "relevant_docs": 2
       }
     }
   },
   "sad-movies": {
     "metric_score": 0,
     "unrated_docs": [],
     "hits": [],
     "metric_details": {
       "recall": {
         "relevant_docs_retrieved": 0,
         "relevant_docs": 2
       }
     }
   }
 },
 "failures": {}
}</code></pre><p>或许我们对 <strong>minimum_should_match</strong> 参数设置得过于严苛了，因为要求查询中的所有词汇都必须在文档中出现，这很可能会导致我们遗漏掉一些相关结果。不妨去掉 <strong>minimum_should_match</strong> 参数，这样只要文档中包含查询语句里的任意一个词汇，该文档就会被视为相关结果。</p><pre><code>POST /movies/_rank_eval
{
 "requests": [
   {
     "id": "dicaprio-performance",
     "request": {
       "query": {
         "match": {
           "text": {
             "query": "DiCaprio performance"
           }
         }
       }
     },
     "ratings": [
       {
         "_index": "movies",
         "_id": "doc1",
         "rating": 1
       },
       {
         "_index": "movies",
         "_id": "doc2",
         "rating": 1
       },
       {
         "_index": "movies",
         "_id": "doc3",
         "rating": 0
       },
       {
         "_index": "movies",
         "_id": "doc4",
         "rating": 0
       }
     ]
   },
   {
     "id": "sad-movies",
     "request": {
       "query": {
         "match": {
           "text": {
             "query": "sad movies that make you cry"
           }
         }
       }
     },
     "ratings": [
       {
         "_index": "movies",
         "_id": "doc5",
         "rating": 1
       },
       {
         "_index": "movies",
         "_id": "doc6",
         "rating": 1
       },
       {
         "_index": "movies",
         "_id": "doc7",
         "rating": 0
       },
       {
         "_index": "movies",
         "_id": "doc8",
         "rating": 0
       }
     ]
   }
 ],
 "metric": {
   "recall": {
     "k": 10,
     "relevant_rating_threshold": 1
     }
 }
}</code></pre><p>如你所见，通过在两个查询中的其中一个里移除 <strong>minimum_should_match</strong> 参数，现在两个查询的平均召回率都达到了 1。</p><pre><code>{
  "metric_score": 1,
  "details": {
    "dicaprio-performance": {
      "metric_score": 1,
      "unrated_docs": [],
      "hits": [
        {
          "hit": {
            "_index": "movies",
            "_id": "doc1",
            "_score": 2.0661702
          },
          "rating": 1
        },
        {
          "hit": {
            "_index": "movies",
            "_id": "doc3",
            "_score": 0.732218
          },
          "rating": 0
        },
        {
          "hit": {
            "_index": "movies",
            "_id": "doc2",
            "_score": 0.6271719
          },
          "rating": 1
        }
      ],
      "metric_details": {
        "recall": {
          "relevant_docs_retrieved": 2,
          "relevant_docs": 2
        }
      }
    },
    "sad-movies": {
      "metric_score": 1,
      "unrated_docs": [],
      "hits": [
        {
          "hit": {
            "_index": "movies",
            "_id": "doc7",
            "_score": 2.1307156
          },
          "rating": 0
        },
        {
          "hit": {
            "_index": "movies",
            "_id": "doc5",
            "_score": 1.3160692
          },
          "rating": 1
        },
        {
          "hit": {
            "_index": "movies",
            "_id": "doc6",
            "_score": 1.190063
          },
          "rating": 1
        }
      ],
      "metric_details": {
        "recall": {
          "relevant_docs_retrieved": 2,
          "relevant_docs": 2
        }
      }
    }
  },
  "failures": {}
}</code></pre><p>总而言之，移除 minimum_should_match: 100% 这一条件后，我们得以使两个查询均实现完美召回率。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/24d055da4348c63827ba7046fe8cafb6f47cadd8-546x628.png" style="width: 100%;" /><p>我们做到了！对不对？</p><p>没那么快！</p><p>通过提升召回率，我们能够获取到更广泛的结果范围。然而，每一次调整都意味着需要权衡取舍。这正是为何要定义完整的测试用例，并运用不同指标来评估各项更改的原因。</p><p>使用判断列表和指标可以防止您在进行更改时盲目行事，因为现在您有数据可以支持这些更改。验证不再是手动和重复的，您可以在多个用例中测试您的更改。此外，A/B 测试允许您实时测试哪种配置最适合您的用户和业务案例，从而实现从技术指标到实际指标的完整循环。</p><h2>使用判断列表的最终建议</h2><p>运用判定列表开展工作，不仅关乎评估测量，更在于构建一个能让你自信迭代优化的框架。为实现这一目标，可遵循以下建议：</p><ol><li><strong>从小处着手，但一定要开始行动。</strong>你无需准备 10000 个查询，且每个查询都配有 50 个判断列表。你只需找出 5 到 10 个对业务场景最为关键的查询，并明确你期望在结果顶部看到的文档即可。这已经能为你提供一个基础。通常，你应优先从热门查询以及无结果的查询入手开展工作。你也可以先使用像精确率这样易于配置的指标进行测试，然后再逐步尝试更复杂的指标。</li><li><strong>与用户核实。</strong>在生产环境中通过 A/B 测试对数据指标进行补充验证。如此一来，你便能知晓那些在指标上表现良好的更改是否也切实产生了实际影响。</li><li><strong>保持列表有效性。</strong>你的商业案例会不断变化，关键问题也会随之变化。定期更新判断以反映新的需求。</li><li><strong>使其成为流程的一部分。</strong>将判断列表整合到开发管道之中。确保每次配置更改、同义词添加或文本分析操作，都能自动对照基础列表进行验证。</li><li><strong>将技术知识与战略相结合。</strong>不要仅仅满足于衡量精确率或召回率等技术指标。要利用评估结果为业务成果提供决策依据。</li></ol>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[使用 LangGraph.js 和 Elasticsearch 构建金融 AI 搜索工作流。]]></title>
            <description><![CDATA[学习如何将 LangGraph.js 与 Elasticsearch 结合使用，构建一个 AI 驱动的金融搜索工作流，将自然语言查询转换为动态的条件过滤器，用于投资和市场分析。]]></description>
            <link>https://www.elastic.co/search-labs/cn/blog/ai-agent-workflow-finance-langgraph-elasticsearch</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/cn/blog/ai-agent-workflow-finance-langgraph-elasticsearch</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Jeffrey Rengifo]]></dc:creator>
            <pubDate>Fri, 05 Dec 2025 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/12b6057d84c6356267cd178a3c6c1a5c61123ece-2000x1256.png?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>构建 AI 搜索应用并不轻松：多重任务、数据拉取与抽取都需要紧密配合，才能形成流畅连贯的工作流。LangGraph 通过节点式结构让开发者轻松编排 AI 代理，从而大幅简化了整个流程。在本文中，我们将运用 <a href="https://langchain-ai.github.io/langgraphjs/">LangGraph.js</a> 构建一个面向金融场景的 AI 搜索解决方案。</p><h2>什么是 LangGraph</h2><p><a href="https://langchain-ai.github.io/langgraphjs/">LangGraph</a> 是一个用于构建 AI 代理，并将其编排进工作流，从而打造 AI 辅助应用的框架。LangGraph 采用节点式架构，我们可以声明代表不同任务的函数，并将这些函数指定为工作流中的节点。多个节点相互作用后形成的便是一个图结构。LangGraph 是更广泛的 <a href="https://js.langchain.com/docs/introduction/">LangChain</a> 生态系统的一部分，该生态为构建模块化、可组合的 AI 系统提供了丰富的工具。</p><p>为了更直观地理解 LangGraph 有何用处，我们不妨用它来解决一个真实的业务难题。</p><h2>解决方案概述</h2><p>在一家风险投资公司中，投资人可以访问一个带有大量筛选条件的大型数据库，但一旦需要组合多重条件，查询就会变得既繁琐又缓慢。这可能会导致一些本应纳入投资视野的优质初创公司被漏掉。结果就是，团队要耗费大量时间去筛选最佳标的，甚至因此错失投资机会。</p><p>借助 LangGraph 和 Elasticsearch，我们能够使用自然语言进行过滤搜索，从而无需用户手动构建包含数十个筛选器的复杂请求。为了提高灵活性，工作流会根据用户输入在两种查询类型之间自动选择：</p><ul><li><strong>聚焦投资维度的查询</strong>：这类查询专注于初创公司的财务与融资维度，例如<a href="https://www.investopedia.com/articles/personal-finance/102015/series-b-c-funding-what-it-all-means-and-how-it-works.asp">融资轮次</a>、估值或<a href="https://www.investopedia.com/terms/r/revenue.asp">营收</a>等指标。<em>示例：</em>“查找已完成 A 轮或 B 轮融资、融资额在 800 万至 2,500 万美元之间且月收入超过 50 万美元的初创公司。”</li><li><strong>聚焦市场维度的查询</strong>：这类查询侧重于<a href="https://en.wikipedia.org/wiki/Vertical_market">行业垂直领域</a>、<a href="https://en.wikipedia.org/wiki/Target_market">目标市场</a>或<a href="https://www.investopedia.com/terms/b/businessmodel.asp">商业模式</a>，帮助识别特定领域或地区中的投资机会。<em>示例：</em>“查找位于旧金山、纽约或波士顿的金融科技和医疗健康领域初创公司。”</li></ul><p>为了让查询更稳健，我们会让 LLM 生成<a href="https://www.elastic.co/docs/solutions/search/search-templates">搜索模板</a>，而不是直接构造完整的 <a href="https://www.elastic.co/docs/explore-analyze/query-filter/languages/querydsl">DSL 查询</a>。通过这种方式，你获得的始终是预期的查询结果，LLM 只需填入参数，而不必每次从头构建整条查询。</p><h2>开始前的准备工作</h2><ul><li>Elasticsearch API密钥</li><li>OpenAPI API密钥</li><li>Node 18 或更高版本</li></ul><h2>分步操作指南</h2><p>在本节中，我们先来看一下这个应用的外观。为此，我们将使用 <a href="https://www.typescriptlang.org/">TypeScript</a>，这是 JavaScript 的一个超集，添加了静态类型，使代码更可靠且更易维护，并能更早发现错误，同时又与现有 JavaScript 完全兼容。</p><p>节点的流程将如下所示：</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/b47d7f238c4964a6febc0de7fe5e68b186f539c3-363x555.png" style="width: 100%;" /><p>上图由 LangGraph 生成，直观地呈现了工作流结构，包括各节点的执行顺序和它们之间的条件分支关系：</p><ul><li><strong>decideStrategy：</strong>使用 LLM 分析用户查询，在“聚焦投资维度”与“聚焦市场维度”这两种专门搜索策略之间做出选择。</li><li><strong>PrepareInvestmentSearch：</strong>从查询中提取筛选值并构建一个强调财务和资金相关参数的预定义模板。</li><li><strong>prepareMarketSearch</strong>：同样会提取筛选条件，但重点是围绕市场、行业和地域背景，动态生成相应的搜索参数。</li><li><strong>ExecuteSearch：</strong>通过搜索模板将构建好的查询发送到 Elasticsearch，检索并返回所有匹配的初创公司文档。</li><li><strong>visualizeResults：</strong>将最终结果整理成清晰易读的摘要，呈现融资、行业、营收等关键创业公司属性。</li></ul><p>该流程包括一个<a href="https://langchain-ai.github.io/langgraphjs/how-tos/branching/?h=conditional#how-to-create-branches-for-parallel-node-execution">条件分支</a>，相当于一条“if”语句，可根据用户输入决定使用投资还是市场搜索路径。这种由 LLM 驱动的决策机制让工作流具备自适应和上下文感知能力，后续章节将对这一机制进行更详细的说明。</p><h3>LangGraph 状态</h3><p>在查看各个节点之前，我们需要先理解节点之间的通信和数据共享方式。为此，LangGraph 可支持定义工作流状态。这个状态就是在各个节点之间传递的共享状态。</p><p>该状态相当于一个共享容器，在整个工作流中保存中间数据：从最开始的用户自然语言查询，到选定的搜索策略、为 Elasticsearch 准备好的参数、检索到的搜索结果，一直到最后的格式化输出，都会依次写入其中。</p><p>这种结构让每个节点都能读取和更新状态，确保从用户输入到可视化实现顺畅一致的信息流动。</p><pre><code>const VCState = Annotation.Root({
  input: Annotation<string>(), // User's natural language query
  searchStrategy: Annotation<string>(), // Search strategy chosen by LLM
  searchParams: Annotation<any>(), // Prepared search parameters
  results: Annotation<any[]>(), // Search results
  final: Annotation<string>(), // Final formatted response
});</code></pre><h3>设置应用程序</h3><p>本节所有代码均可在 <a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/langgraph-js-elasticsearch">elasticsearch-labs 仓库</a> 中找到。</p><p>在应用所在的文件夹中打开终端，并通过以下命令初始化一个 Node.js 应用：</p><pre><code>npm init -y</code></pre><p>现在我们可以为这个项目安装必要的依赖项：</p><pre><code>npm install @elastic/elasticsearch @langchain/langgraph @langchain/openai @langchain/core dotenv zod && npm install --save-dev @types/node tsx typescript</code></pre><ul><li><strong><code>@elastic/elasticsearch</code></strong>：帮助我们处理 Elasticsearch 请求，例如数据摄取和检索。</li><li><strong><code>@langchain/langgraph</code></strong>：用于提供所有 LangGraph 工具的 JS 依赖项。</li><li><strong><code>@langchain/openai</code></strong>：适用于 LangChain 的 OpenAI LLM 客户端。</li><li>@langchain/core：为 LangChain 应用提供基础构建模块，包括提示模板。</li><li><strong><code>dotenv</code></strong>：在 JavaScript 中使用环境变量所需的依赖项。</li><li><strong><code>zod</code></strong>: 对类型数据的依赖。</li></ul><p><code>@types/node</code> <code>tsx</code> <code>typescript</code> 允许我们编写和运行 TypeScript 代码。</p><p>现在创建以下文件：</p><ul><li><code>elasticsearchSetup<a href="http://ingest.ts/">.ts</a></code>：将创建索引映射，从 JSON 文件加载数据集，并将数据摄取到 Elasticsearch。</li><li><a href="http://main.ts/"><code>main.ts</code></a>：将包含 LangGraph 应用。</li><li><code>.env</code>：用于存储环境变量的文件</li></ul><p>在 <code>.env</code> 文件中，我们添加以下环境变量：</p><pre><code>ELASTICSEARCH_ENDPOINT="your-endpoint-here"
ELASTICSEARCH_API_KEY="your-key-here"
OPENAI_API_KEY="your-key-here"</code></pre><p>OpenAPI APIKey 不会直接在代码中使用，而是由 <code>@langchain/openai</code> 库在内部调用。</p><p>所有关于映射创建、搜索模板创建和数据集摄取的逻辑都可以在 <a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/langgraph-js-elasticsearch/elasticsearchSetup.ts"><code>elasticsearchSetup.ts</code></a> 文件中找到。在接下来的步骤中，我们将重点关注 <a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/langgraph-js-elasticsearch/main.ts"><code>main.ts</code></a> 文件。此外，您可以查看该数据集，以便更好地理解 <a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/langgraph-js-elasticsearch/dataset.json"><code>dataset.json</code></a> 中数据结构。</p><h3>LangGraph 应用程序</h3><p>在 <code>main.ts</code> 文件中，我们导入一些必要的依赖项来构建整个 LangGraph 应用。在此文件中，您还必须定义各个节点函数以及工作流状态的声明。在后续步骤中，我们会在 <code>main</code> 方法中完成这个图结构的声明。<code>elasticsearchSetup.ts</code> 文件中包含一组 Elasticsearch 辅助函数，我们会在后续步骤的各个节点中使用这些函数。</p><pre><code>import { writeFileSync } from "node:fs";
import { StateGraph, Annotation, START, END } from "@langchain/langgraph";
import { ChatOpenAI } from "@langchain/openai";
import { z } from "zod";
import {
  esClient,
  ingestDocuments,
  createSearchTemplates,
  INDEX_NAME,
  INVESTMENT_FOCUSED_TEMPLATE,
  MARKET_FOCUSED_TEMPLATE,
  createIndex,
} from "./elasticsearchSetup.js";

const llm = new ChatOpenAI({ model: "gpt-4o-mini" });</code></pre><p>如前所述，LLM 客户端将根据用户的问题生成 Elasticsearch 搜索模板参数。</p><pre><code>async function saveGraphImage(app: any): Promise<void> {
  try {
    const drawableGraph = app.getGraph();
    const image = await drawableGraph.drawMermaidPng();
    const arrayBuffer = await image.arrayBuffer();

    const filePath = "./workflow_graph.png";
    writeFileSync(filePath, new Uint8Array(arrayBuffer));
    console.log(`📊 Workflow graph saved as: ${filePath}`);
  } catch (error: any) {
    console.log("⚠️  Could not save graph image:", error.message);
  }
}</code></pre><p>上面的方法会生成一张 png 格式的图结构图像，并在后台调用 <a href="https://mermaid.ink/">Mermaid.ink API</a>。当你希望通过一张带有样式的可视化图来直观了解应用中各个节点之间的交互时，这个功能就会非常有用。</p><h3>LangGraph 节点</h3><p>现在让我们看看每个节点的详细信息：</p><h3>decideSearchStrategy 节点</h3><p><code>decideSearchStrategy</code> 节点分析用户输入，并确定是执行投资聚焦搜索还是市场聚焦搜索。它使用具有结构化输出模式（用 Zod 定义）的 LLM 对查询类型进行分类。在做出决策之前，它会通过聚合从索引中检索可用的筛选条件，确保模型掌握最新的行业、地域和融资等上下文信息。</p><p>为了提取过滤器可能的值并将其发送到 LLM，让我们使用<a href="https://www.elastic.co/docs/explore-analyze/query-filter/aggregations">聚合</a>查询直接从 Elasticsearch 索引中检索它们。这个逻辑被分配到一个名为 <code>getAvailableFilters</code> 的方法中：</p><pre><code>async function getAvailableFilters() {
  try {
    const response = await esClient.search({
      index: INDEX_NAME,
      size: 0,
      aggs: {
        industries: {
          terms: { field: "industry", size: 100 },
        },
        locations: {
          terms: { field: "location", size: 100 },
        },
        funding_stages: {
          terms: { field: "funding_stage", size: 20 },
        },
        business_models: {
          terms: { field: "business_model", size: 10 },
        },
        lead_investors: {
          terms: { field: "lead_investor", size: 100 },
        },
        funding_amount_stats: {
          stats: { field: "funding_amount" },
        },
      },
    });

    return response.aggregations;
  } catch (error) {
    console.error("❌ Error getting available filters:", error);
    return {};
  }
}</code></pre><p>通过上述聚合查询，我们得到以下结果：</p><pre><code>{
  "industries": {
    "doc_count_error_upper_bound": 0,
    "sum_other_doc_count": 0,
    "buckets": [
      {
        "key": "logistics",
        "doc_count": 5
      },
      ...
    ]
  },
  "locations": {
    "doc_count_error_upper_bound": 0,
    "sum_other_doc_count": 0,
    "buckets": [
      {
        "key": "San Francisco, CA",
        "doc_count": 4
      },
      {
        "key": "New York, NY",
        "doc_count": 3
      },
      ...
    ]
  },
  "funding_stages": {
    "doc_count_error_upper_bound": 0,
    "sum_other_doc_count": 0,
    "buckets": [
      {
        "key": "Series A",
        "doc_count": 8
      },
      ...
    ]
  },
  "business_models": {
    "doc_count_error_upper_bound": 0,
    "sum_other_doc_count": 0,
    "buckets": [
      {
        "key": "B2B",
        "doc_count": 13
      },
      ...
    ]
  },
  "lead_investors": {
    "doc_count_error_upper_bound": 0,
    "sum_other_doc_count": 0,
    "buckets": [
      {
        "key": "Battery Ventures",
        "doc_count": 1
      },
      {
        "key": "Benchmark Capital",
        "doc_count": 1
      },
      ...
    ]
  },
  "funding_amount_stats": {
    "count": 20,
    "min": 4500000,
    "max": 35000000,
    "avg": 14075000,
    "sum": 281500000
  }
}</code></pre><p><a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/langgraph-js-elasticsearch/responses/aggregationsResponse.json">点击此处</a>查看所有结果。</p><p>对于这两种策略，我们将使用混合搜索来检测问题的结构化部分（过滤器）和主观部分（语义）。以下是使用<a href="https://www.elastic.co/docs/solutions/search/search-templates">搜索模板</a>的两个查询示例：</p><pre><code>await esClient.putScript({
      id: INVESTMENT_FOCUSED_TEMPLATE,
      script: {
        lang: "mustache",
        source: `{
          "size": 5,
          "retriever": {
            "rrf": {
              "retrievers": [
                {
                  "standard": {
                    "query": {
                      "semantic": {
                        "field": "semantic_field",
                        "query": "{{query_text}}"
                      }
                    }
                  }
                },
                {
                  "standard": {
                    "query": {
                      "bool": {
                        "filter": [
                          {"terms": {"funding_stage": {{#join}}{{#toJson}}funding_stage{{/toJson}}{{/join}}}},
                          {"range": {"funding_amount": {"gte": {{funding_amount_gte}}{{#funding_amount_lte}},"lte": {{funding_amount_lte}}{{/funding_amount_lte}}}}},
                          {"terms": {"lead_investor": {{#join}}{{#toJson}}lead_investor{{/toJson}}{{/join}}}},
                          {"range": {"monthly_revenue": {"gte": {{monthly_revenue_gte}}{{#monthly_revenue_lte}},"lte": {{monthly_revenue_lte}}{{/monthly_revenue_lte}}}}}
                        ]
                      }
                    }
                  }
                }
              ],
              "rank_window_size": 100,
              "rank_constant": 20
            }
          }
        }`,
      },
    });</code></pre><p>查看 <a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/langgraph-js-elasticsearch/elasticsearchSetup.ts#L119"><code>elasticsearchSetup.ts</code></a> 文件中详细的查询。在接下来的节点中，将决定使用这两个查询中的哪一个：</p><pre><code>// Node 1: Decide search strategy using LLM
async function decideSearchStrategy(state: typeof VCState.State) {
  // Zod schema for specialized search strategy decision
  const SearchDecisionSchema = z.object({
    search_type: z
      .enum(["investment_focused", "market_focused"])
      .describe("Type of specialized search strategy to use"),
    reasoning: z
      .string()
      .describe("Brief explanation of why this search strategy was chosen"),
  });

  const decisionLLM = llm.withStructuredOutput(SearchDecisionSchema);

  // Get dynamic filters from Elasticsearch
  const availableFilters = await getAvailableFilters();

  const prompt = `Query: "${state.input}"
    Available filters: ${JSON.stringify(availableFilters, null, 2)}

    Choose between two specialized search strategies:
    
    - investment_focused: For queries about funding stages, funding amounts, monthly revenue, lead investors, financial performance
    
    - market_focused: For queries about industries, locations, business models, market segments, geographic markets
    
    Analyze the query intent and choose the most appropriate strategy.
  `;

  try {
    const result = await decisionLLM.invoke(prompt);
    console.log(
      `🤔 Search strategy: ${result.search_type} - ${result.reasoning}`
    );

    return {
      searchStrategy: result.search_type,
    };
  } catch (error: any) {
    console.error("❌ Error in decideSearchStrategy:", error.message);
    return {
      searchStrategy: "investment_focused",
    };
  }
}</code></pre><h3>prepareInvestmentSearch 和 prepareMarketSearch 节点</h3><p>两个节点都使用共享的辅助函数 <code>extractFilterValues</code>，该函数利用 LLM 来识别用户输入中提到的相关过滤器，例如行业、地点、资金阶段、商业模式等。我们正在使用这个架构来构建我们的 <a href="https://www.elastic.co/docs/solutions/search/search-templates">搜索模板</a>。</p><pre><code>// Extract all possible filter values from user input
async function extractFilterValues(input: string) {
  const FilterValuesSchema = z.object({
    // Investment-focused filters
    funding_stage: z
      .array(z.string())
      .default([])
      .describe("Funding stage values mentioned in query"),
    funding_amount_gte: z
      .number()
      .default(0)
      .describe("Minimum funding amount in USD"),
    funding_amount_lte: z
      .number()
      .default(100000000)
      .describe("Maximum funding amount in USD"),
    lead_investor: z
      .array(z.string())
      .default([])
      .describe("Lead investor values mentioned in query"),
    monthly_revenue_gte: z
      .number()
      .default(0)
      .describe("Minimum monthly revenue in USD"),
    monthly_revenue_lte: z
      .number()
      .default(10000000)
      .describe("Maximum monthly revenue in USD"),
    industry: z
      .array(z.string())
      .default([])
      .describe("Industry values mentioned in query"),
    location: z
      .array(z.string())
      .default([])
      .describe("Location values mentioned in query"),
    business_model: z
      .array(z.string())
      .default([])
      .describe("Business model values mentioned in query"),
  });

  const extractorLLM = llm.withStructuredOutput(FilterValuesSchema);
  const availableFilters = await getAvailableFilters();

  const extractPrompt = `Extract ALL relevant filter values from: "${input}"
    Available options: ${JSON.stringify(availableFilters, null, 2)}
    Extract only values explicitly mentioned in the query. Leave fields empty if not mentioned.`;

  return await extractorLLM.invoke(extractPrompt);
}</code></pre><p>根据检测到的意图，工作流会选择以下两种路径之一：</p><p><strong>PrepareInvestmentSearch：</strong>构建以财务为导向的搜索参数，包括融资阶段、融资金额、投资者以及营收相关信息。您可以在 <a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/langgraph-js-elasticsearch/elasticsearchSetup.ts"><code>elasticsearchSetup.ts</code></a> 文件中找到整个查询模板：</p><pre><code>// Node 2A: Prepare Investment-Focused Search Parameters 
async function prepareInvestmentSearch(state: typeof VCState.State) {
  console.log(
    "💰 Preparing INVESTMENT-FOCUSED search parameters with financial emphasis..."
  );

  try {
    // Extract all filter values from input
    const values = await extractFilterValues(state.input);

    let searchParams: any = {
      template_id: INVESTMENT_FOCUSED_TEMPLATE,
      query_text: state.input,
      ...values,
    };

    return { searchParams };
  } catch (error) {
    console.error("❌ Error preparing investment-focused params:", error);
    return {
      searchParams: {},
    };
  }
}</code></pre><p><strong>prepareMarketSearch：</strong>创建以行业、地域和商业模式为重点的市场驱动参数。在 <a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/langgraph-js-elasticsearch/elasticsearchSetup.ts"><code>elasticsearchSetup.ts</code></a> 文件中查看完整查询：</p><pre><code>// Node 2B: Prepare Market-Focused Search Parameters
async function prepareMarketSearch(state: typeof VCState.State) {
  console.log(
    "🔍 Preparing MARKET-FOCUSED search parameters with market emphasis..."
  );

  try {
    // Extract all filter values from input
    const values = await extractFilterValues(state.input);

    let searchParams: any = {
      template_id: MARKET_FOCUSED_TEMPLATE,
      query_text: state.input,
      ...values,
    };

    return { searchParams };
  } catch (error) {
    console.error("❌ Error preparing market-focused params:", error);
    return {};
  }
}</code></pre><h3>executeSearch 节点</h3><p>该节点从状态中获取生成的搜索参数，首先将其发送到 Elasticsearch，使用<a href="https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-render-search-template">_render API</a>来可视化查询以便调试，然后发送请求以检索结果。</p><pre><code>// Node 3: Execute Search
async function executeSearch(state: typeof VCState.State) {
  const { searchParams } = state;

  try {
    // getting formed query from template for debugging
    const renderedTemplate = await esClient.renderSearchTemplate({
      id: searchParams.template_id,
      params: searchParams,
    });

    console.log(
      "📋 Complete query:",
      JSON.stringify(renderedTemplate.template_output, null, 2)
    );

    const results = await esClient.searchTemplate({
      index: INDEX_NAME,
      id: searchParams.template_id,
      params: searchParams,
    });

    return {
      results: results.hits.hits.map((hit: any) => hit._source),
    };
  } catch (error: any) {
    console.error(`❌ ${state.searchParams.search_type} search error:`, error);
    return { results: [] };
  }
}</code></pre><h3>visualizeResults 节点</h3><p>最后，此节点显示 Elasticsearch 结果。</p><pre><code>// Node 4: Visualize results
async function visualizeResults(state: typeof VCState.State) {
  const results = state.results || [];

  let formattedResults = `🎯 Found ${results.length} startups matching your criteria:\n\n`;

  results.forEach((startup: any, index: number) => {
    formattedResults += `${index + 1}. **${startup.company_name}**\n`;
    formattedResults += `   📍 ${startup.location} | 🏢 ${startup.industry} | 💼 ${startup.business_model}\n`;
    formattedResults += `   💰 ${startup.funding_stage} - $${(
      startup.funding_amount / 1000000
    ).toFixed(1)}M\n`;
    formattedResults += `   👥 ${startup.employee_count} employees | 📈 $${(
      startup.monthly_revenue / 1000
    ).toFixed(0)}K MRR\n`;
    formattedResults += `   🏦 Lead: ${startup.lead_investor}\n`;
    formattedResults += `   📝 ${startup.description}\n\n`;
  });

  return {
    final: formattedResults,
  };
}</code></pre><p>从程序角度来看，整个图结构如下所示：</p><pre><code>  const workflow = new StateGraph(VCState)
    // Register nodes - these are the processing functions
    .addNode("decideStrategy", decideSearchStrategy)
    .addNode("prepareInvestment", prepareInvestmentSearch)
    .addNode("prepareMarket", prepareMarketSearch)
    .addNode("executeSearch", executeSearch)
    .addNode("visualizeResults", visualizeResults)
    // Define execution flow with conditional branching
    .addEdge(START, "decideStrategy") // Start with strategy decision
    .addConditionalEdges(
      "decideStrategy",
      (state: typeof VCState.State) => state.searchStrategy, // Conditional function
      {
        investment_focused: "prepareInvestment", // If investment focused -> RRF template preparation
        market_focused: "prepareMarket", // If market focused -> dynamic query preparation
      }
    )
    .addEdge("prepareInvestment", "executeSearch") // Investment prep -> execute
    .addEdge("prepareMarket", "executeSearch") // Market prep -> execute
    .addEdge("executeSearch", "visualizeResults") // Execute -> visualize
    .addEdge("visualizeResults", END); // End workflow</code></pre><p>正如你所见，我们有一个条件边，应用在此决定接下来运行哪个“路径”或节点。当工作流需要分支逻辑时，例如在多个工具之间进行选择或包含人机交互步骤，此功能非常有用。</p><p>了解了 LangGraph 的核心功能后，我们可以设置代码运行的应用程序：</p><p>将所有内容在 <code>main</code> 方法中整合起来，在名为 workflow 的变量中声明这个包含所有元素的图结构：</p><pre><code>async function main() {
  await createIndex();
  await createSearchTemplates();
  await ingestDocuments();

  // Create the workflow graph with shared state
  const workflow = new StateGraph(VCState)
    // Register nodes - these are the processing functions
    .addNode("decideStrategy", decideSearchStrategy)
    .addNode("prepareInvestment", prepareInvestmentSearch)
    .addNode("prepareMarket", prepareMarketSearch)
    .addNode("executeSearch", executeSearch)
    .addNode("visualizeResults", visualizeResults)
    // Define execution flow with conditional branching
    .addEdge(START, "decideStrategy") // Start with strategy decision
    .addConditionalEdges(
      "decideStrategy",
      (state: typeof VCState.State) => state.searchStrategy, // Conditional function
      {
        investment_focused: "prepareInvestment", // If investment focused -> RRF template preparation
        market_focused: "prepareMarket", // If market focused -> dynamic query preparation
      }
    )
    .addEdge("prepareInvestment", "executeSearch") // Investment prep -> execute
    .addEdge("prepareMarket", "executeSearch") // Market prep -> execute
    .addEdge("executeSearch", "visualizeResults") // Execute -> visualize
    .addEdge("visualizeResults", END); // End workflow


  const app = workflow.compile();

  await saveGraphImage(app);

  const query =
    "Find startups with Series A or Series B funding between $8M-$25M and monthly revenue above $500K";

  const marketResult = await app.invoke({ input: query });
  console.log(marketResult.final);
}</code></pre><p>查询变量用来模拟用户在一个虚拟搜索框中输入的内容：</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/e8d76909eb2bc1bb62f3ca9a8b3e4b85fcec2893-1600x164.png" style="width: 100%;" /><p>系统会从这句自然语言“查找已完成 A 轮或 B 轮、融资额在 800 万至 2,500 万美元之间且月收入高于 50 万美元的初创公司”中，自动抽取出所有筛选条件。</p><p>最后，调用主方法：</p><pre><code>main().catch(console.error);</code></pre><h3>实施结果</h3><pre><code>🔍 Checking if index exists...
🏗️ Creating index...
✅ Index created successfully!
Ingesting documents...
✅ Documents ingested successfully!
✅ Investment-focused template created successfully!
✅ Market-focused template created successfully!

📊 Workflow graph saved as: ./workflow_graph.png

🔍 Query: "Find startups with Series A or Series B funding between $8M-$25M and monthly revenue above $500K"

🤔 Search strategy: investment_focused - The query specifically seeks profitable fintech startups with defined funding amounts and high monthly revenue, which aligns closely with financial performance metrics and investment-related criteria.

💰 Preparing INVESTMENT-FOCUSED search parameters with financial emphasis...

📋 Complete query: {
  "size": 5,
  "retriever": {
    "rrf": {
      "retrievers": [
        {
          "standard": {
            "query": {
              "semantic": {
                "field": "semantic_field",
                "query": "Find startups with Series A or Series B funding between $8M-$25M and monthly revenue above $500K"
              }
            }
          }
        },
        {
          "standard": {
            "query": {
              "bool": {
                "filter": [
                  {
                    "terms": {
                      "funding_stage": [
                        "Series A",
                        "Series B"
                      ]
                    }
                  },
                  {
                    "range": {
                      "funding_amount": {
                        "gte": 8000000,
                        "lte": 25000000
                      }
                    }
                  },
                  {
                    "terms": {
                      "lead_investor": []
                    }
                  },
                  {
                    "range": {
                      "monthly_revenue": {
                        "gte": 500000,
                        "lte": 0
                      }
                    }
                  }
                ]
              }
            }
          }
        }
      ],
      "rank_window_size": 100,
      "rank_constant": 20
    }
  }
}
🎯 Found 5 startups matching your criteria:

1. **TechFlow**
   📍 San Francisco, CA | 🏢 logistics | 💼 B2B
   💰 Series A - $8.0M
   👥 45 employees | 📈 $500K MRR
   🏦 Lead: Sequoia Capital
   📝 TechFlow optimizes supply chain operations using AI-powered route optimization and real-time tracking. Founded in 2023, shows remarkable growth with $500K monthly revenue.

2. **DataViz**
   📍 New York, NY | 🏢 enterprise software | 💼 B2B
   💰 Series A - $10.0M
   👥 42 employees | 📈 $450K MRR
   🏦 Lead: Battery Ventures
   📝 DataViz creates intuitive data visualization tools for enterprise customers. No-code platform allows business users to create dashboards without technical expertise.

3. **FinanceAI**
   📍 San Francisco, CA | 🏢 fintech | 💼 B2C
   💰 Series C - $25.0M
   👥 120 employees | 📈 $1200K MRR
   🏦 Lead: Tiger Global Management
   📝 FinanceAI provides AI-powered investment advisory services to retail investors. Uses machine learning to analyze market trends with over 100,000 active users.

4. **UrbanMobility**
   📍 New York, NY | 🏢 logistics | 💼 B2B2C
   💰 Series B - $15.0M
   👥 78 employees | 📈 $750K MRR
   🏦 Lead: Kleiner Perkins
   📝 UrbanMobility revolutionizes urban transportation through autonomous delivery drones and smart logistics hubs. Partners with major retailers for same-day delivery across Manhattan and Brooklyn.

5. **HealthTech Solutions**
   📍 Boston, MA | 🏢 healthcare | 💼 B2B
   💰 Series B - $18.0M
   👥 95 employees | 📈 $900K MRR
   🏦 Lead: General Catalyst
   📝 HealthTech Solutions develops medical devices and software for remote patient monitoring. Comprehensive telehealth platform reducing hospital readmissions by 30%.

✨  Done in 18.80s.</code></pre><p>对于这条输入，应用会选择<strong>聚焦投资维度</strong>的路径，由此我们可以看到 LangGraph 工作流生成的 Elasticsearch 查询，它会从用户输入中抽取出各类数值与区间。此外，我们还能看到应用了这些提取参数后实际发送到 Elasticsearch 的查询，以及最后由 <code>visualizeResults</code> 节点格式化输出的结果。</p><p>现在，我们再用这条查询来测试<strong>聚焦市场维度</strong>的节点：“查找位于旧金山、纽约或波士顿的金融科技和医疗健康初创公司”：</p><pre><code>...

🔍 Query: Find fintech and healthcare startups in San Francisco, New York, or Boston

🤔 Search strategy: market_focused - The query is focused on finding fintech startups in San Francisco that are disrupting traditional banking and payment systems, which pertains to specific industries (fintech) and locations (San Francisco). Thus, a market-focused strategy is more appropriate.

🔍 Preparing MARKET-FOCUSED search parameters with market emphasis...

📋 Complete query: {
  "size": 5,
  "retriever": {
    "rrf": {
      "retrievers": [
        {
          "standard": {
            "query": {
              "semantic": {
                "field": "semantic_field",
                "query": "Find fintech and healthcare startups in San Francisco, New York, or Boston"
              }
            }
          }
        },
        {
          "standard": {
            "query": {
              "bool": {
                "filter": [
                  {
                    "terms": {
                      "industry": [
                        "fintech",
                        "healthcare"
                      ]
                    }
                  },
                  {
                    "terms": {
                      "location": [
                        "San Francisco, CA",
                        "New York, NY",
                        "Boston, MA"
                      ]
                    }
                  },
                  {
                    "terms": {
                      "business_model": []
                    }
                  }
                ]
              }
            }
          }
        }
      ],
      "rank_window_size": 50,
      "rank_constant": 10
    }
  }
}
🎯 Found 5 startups matching your criteria:

1. **FinanceAI**
   📍 San Francisco, CA | 🏢 fintech | 💼 B2C
   💰 Series C - $25.0M
   👥 120 employees | 📈 $1200K MRR
   🏦 Lead: Tiger Global Management
   📝 FinanceAI provides AI-powered investment advisory services to retail investors. Uses machine learning to analyze market trends with over 100,000 active users.

2. **CryptoWallet**
   📍 Miami, FL | 🏢 fintech | 💼 B2C
   💰 Series B - $16.0M
   👥 73 employees | 📈 $820K MRR
   🏦 Lead: Coinbase Ventures
   📝 CryptoWallet provides secure digital wallet solutions for cryptocurrency trading and storage. Multi-chain support with enterprise-grade security features.

...

✨  Done in 7.41s.</code></pre><h2>学习经验</h2><p>在写作过程中我学到了：</p><ul><li>我们必须向 LLM 提供筛选器的精确取值，否则就要完全依赖用户输入这些值。对于低基数，这种方法很好，但当基数很高时，我们需要通过一些机制来过滤结果</li><li>使用搜索模板比让大语言模型编写 Elasticsearch 查询能使结果更加一致，而且速度也更快</li><li>条件边是一种强大的机制，用于构建具有多个变体和分支路径的应用程序。</li><li>结构化输出在使用大型语言模型生成信息时非常有用，因为它能强制执行可预测且类型安全的响应。这不仅提高了整体可靠性，还减少了对提示词的误解。</li></ul><p>通过混合检索结合语义和结构化搜索，可以产生更好、更相关的结果，在精确性和上下文理解之间取得平衡。</p><h2>结论</h2><p>在这个例子中，我们将 LangGraph.js 与 Elasticsearch 结合，创建一个动态工作流，能够解释自然语言查询并决定使用金融或市场聚焦的搜索策略。这种方法减少了手工查询的复杂性，同时提升了风险投资分析师的灵活性和准确性。</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[使用变量控件来提高 Kibana 仪表板的交互性]]></title>
            <description><![CDATA[了解如何在 Kibana 8.18+ 中使用变量控件来筛选单个可视化内容、调整时间间隔，并在 Kibana 仪表板中按不同字段分组。]]></description>
            <link>https://www.elastic.co/search-labs/cn/blog/kibana-dashboard-interactivity-variable-controls-overview</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/cn/blog/kibana-dashboard-interactivity-variable-controls-overview</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Teresa Alvarez Soler]]></dc:creator>
            <pubDate>Thu, 04 Dec 2025 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/59aa3adffc8c759e42b961ef7d63719ce232893a-1348x830.png?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>我们很高兴地宣布，<strong>从 8.18 版本和所有 9.x 系列开始，Kibana 仪表板现在可以使用变量控件</strong>。这一功能是仪表盘用户一直以来最为频繁要求新增的内容之一。如今，它终于上线啦 🎉 在过去的几个月里，我们持续拓展并优化<a href="https://www.elastic.co/docs/explore-analyze/dashboards/add-controls#add-variable-control">变量控件</a>，此刻正是为它们单独撰写一篇专题博客文章的绝佳时机。</p><h2>变量控件是什么？</h2><p>如果您以前用过 Kibana 仪表板，您可能知道我们经典的仪表板控件。那些方便的下拉菜单可以显示数据中的数值，让您只需点击几下就能筛选。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/b82c1b25a0b38661e5ce4552f763be487d5074aa-1600x701.png" style="width: 100%;" /><p>可变控件表面上看起来很相似，但却有巧妙的变化：它们并非自动筛选仪表板上的每个面板，而是可以直接插入<a href="https://www.elastic.co/docs/explore-analyze/visualize/esorql">单个可视化内部的 ES|QL 查询中</a>。</p><p>这意味着<em>您</em>可以决定每个控件的适用范围。更妙的是，您可以将它们用于各种创意技巧，例如实时调整时间间隔、切换细分字段，或更改可视化参数。简而言之，它们为您的仪表板提供了真正的交互式体验，使您能够更快、更轻松地获得见解。</p><h2>变量控件用例</h2><p>好吧，变量控件听起来很有用，但您实际上能用它们做些什么？下面举例说明它们如何提升仪表盘的功能性：</p><h3>筛选已选择的可视化内容</h3><p>想要筛选<em>部分</em>可视化内容，但保留其他内容不变？变量控件功能可以让您做到这一点。选择要响应的面板，并在可视化的 ES|QL 查询中将它们连接起来。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/efa367363830b03bc67028aceafe78c4b44e578f-1440x562.gif" style="width: 100%;" /><h3>选择不同的时间间隔</h3><p>让用户可以在“5 分钟”、“1 小时”、“1 天”或任何合理的时间间隔之间切换。构建有预定义时间间隔的变量控件，并将其连接到时间序列查询。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/62aa9f4e728036f8c70213b76b1cf131f36f5b4d-1440x606.gif" style="width: 100%;" /><h3>更改函数</h3><p>与其为每个操作创建多个图表，不如让仪表板用户选择自己想要查看最大值、平均值、不同百分位数或任何其他聚合器。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/f6a2b4c73dc35efe462c2924a153d7b3fa3a7922-1436x606.gif" style="width: 100%;" /><h3>按不同字段分组</h3><p>有时，您需要在调查过程中按不同维度对数据进行细分。通过变量控件，您可以定义多个“分组依据”字段，让仪表板用户选择有助于他们发现见解的字段。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/fe8745a6fddccadba0666686b8ebc67fdaf64158-1438x606.gif" style="width: 100%;" /><h2>如何创建？</h2><p>创建变量控件的最简单（可能也是最有趣）方法是直接从可视化中的 <strong>ES|QL 查询编辑器</strong>中创建。只需开始输入查询，使用自动完成菜单，Kibana 就会帮助您创建。</p><p>但是如果您更喜欢以变量本身为基础开始创建，也可以前往：<strong>添加面板 → 控件 → 变量控件</strong>，然后在创建控件后将变量添加到可视化中。</p><h3>示例 1：具有多值选择的筛选控件</h3><p>1. 选择由 ES|QL 查询驱动的可视化，并在 WHERE 条件中单击“创建控件”。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/46cb6f2a6775aee152d42eb5ee85170f1bdf26cb-1600x668.png" style="width: 100%;" /><p>2. 您将自动被重定向至变量创建弹出面板，此时“来自查询的值”这一类型已自动选定，并且变量的名称已经预填。请记住，控件的名称必须以“?...”开头，以便在可视化查询中使用。</p><p>您通常需要这样的查询来获取字段中的值，并根据仪表板中选择的时间范围进行更新：</p><pre><code>FROM <datasource_name>
| WHERE @timestamp <=?_tend and @timestamp >?_tstart
| STATS BY <field_name></code></pre><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/a2a72d4e3159923c6207908da9b4172e27cd5f81-1600x716.png" style="width: 100%;" /><p>3. 保存控件时，您会看到它出现在仪表盘的顶部，可视化查询也会用变量控件名称进行更新。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/5fc434c8951889e9769652b675191711d126a685-1600x653.png" style="width: 100%;" /><p>4. 如果要在控件中添加<a href="https://www.elastic.co/docs/explore-analyze/dashboards/add-controls#esql-multi-values-controls">多值选择</a>，则需要在查询中使用<code>MV_CONTAINS</code>函数，并在步骤 2 创建控件时选择“允许多选”（9.3 及更高版本可用）。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/1f237cea0a37cb25a7917a2a683707a269adae8e-1600x670.png" style="width: 100%;" /><h3>示例 2：时间间隔控制</h3><p>如果您正在构建时间序列，可轻松地为日期直方图间隔添加一个变量控制：</p><p>1. 为时间序列编写 ES|QL 查询时，单击“创建控件”。为时间间隔创建变量时，最好使用 <code>TBUCKET</code> 而非 <code>BUCKET</code>，这样它就可以接受更具可读性的间隔，例如“1 小时”、“1 天”等。我们也会很快推出 <code>TBUCKET</code> 自动选项，以便自动适应时间范围。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/b0ad53d790ff9bdd42db5e77477318319f423534-1600x664.png" style="width: 100%;" /><p>2. 确定用于填充下拉菜单选项的时间间隔。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/f3bd83f530cfa4698c1a3b1ae60d08d0414043b5-1600x757.png" style="width: 100%;" /><p>3. 在下拉菜单中选择不同的时间间隔，查看可视化如何变化。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/0f928d9c70929f64926e065059188d140cd48943-1600x671.png" style="width: 100%;" /><h3>示例 3：函数变量</h3><ol><li>用“静态值”类型控件构建一个变量，并在下拉菜单的值中添加函数名。为了替换函数，请使用以“??...”开头的变量名。</li></ol><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/531444237b7e152d3c8a6f3ca7e464f954f9e856-1600x663.png" style="width: 100%;" /><p>2. 在 ES|QL 查询中包含变量名称。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/9858442abb26d8d266d464852871b139fde63b89-1600x665.png" style="width: 100%;" /><h3>示例 4：字段变量</h3><ol><li>您可以使用“静态值”类型的控件，并填入所需字段的名称。为了让变量名在字段中生效，使用以“??...”开头的变量名非常重要。</li></ol><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/33534c3df2fae024b25c28b4aed5d742e54202a2-1600x710.png" style="width: 100%;" /><p>2. 在可视化查询中引用想要的变量。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/71cdf3e9df72c59d957628a3aa6e4aa9bd60d6d5-1600x676.png" style="width: 100%;" /><h2>Discover 中的变量控件</h2><p>变量控件不仅仅是仪表盘的功能，还可以直接在 Discover 的 ES|QL 编辑器中使用。您可以在 Discover 中构建控件，以获得更快的数据探索体验，并将其引入仪表盘，反之亦然。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/fdddeec902d0bc746caed9276d01d7d48793dd85-1600x709.png" style="width: 100%;" /><h2>技术细节</h2><p>到现在为止，您可能已经注意到变量控件有一些规则，比如它们可以引用查询的哪些部分，以及您需要使用的命名前缀（“?...”用于值，“??...”用于字段或函数）。这是因为变量不仅仅是在客户端进行简单的字符串替换。它们实际上是查询语言本身最重要的因素（<a href="https://www.elastic.co/docs/solutions/search/agent-builder/tools/esql-tools#parameter-types">在 ES|QL 中称为</a>参数）。</p><p></p><p>这种设计具有一些很大的优势。首先，Kibana 可以理解每个变量的上下文，这使我们能够自动为您生成并预先填充其配置。它也更加安全：由于该语言严格验证变量输入，因此可以防止恶意注入，并在出现异常时轻松处理错误。此外，它将复杂的验证和错误处理转移到服务器而不是客户端，从而提高了性能和稳定性。关于性能，最佳做法是创建包含快速查询的变量，因为它们在仪表盘之前加载，所以慢速查询会影响整个仪表盘的性能。</p><p>当然，这种架构暂时也有一些<a href="https://www.elastic.co/docs/solutions/search/agent-builder/limitations-known-issues#esql-limitations">限制</a>。变量尚不支持用于筛选的“Any”选项，目前也不能与<code>LIKE</code><strong> </strong>或 <code>FROM</code>（用于切换数据源）等操作符结合使用。好消息是什么？我们正在着手添加这些功能。</p><h2>控件的未来发展趋势</h2><p>我们不会就此止步！我们所关注的一些改进包括：</p><p>✨ 在仪表板上随处放置控件的能力</p><p>✨ 控件链式连接：意味着一个控件的输出成为下一个控件的输入。</p><p>✨ 选择选项更理想，比如变量的“任意”选择</p><p>✨ 新控件类型（搜索类型控件和数据源变量）</p><p>✨ 以及更多您一直要求的生活质量改进，比如预筛选常规控件</p><p>如果您有任何想法或反馈，欢迎向我们提出。</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[使用 NVIDIA cuVS 将 Elasticsearch 中的向量索引速度提升高达 12 倍：GPU 加速（第二章）]]></title>
            <description><![CDATA[了解 Elasticsearch 如何借助 GPU 加速的向量索引和 NVIDIA cuVS，实现近 12 倍的索引吞吐量提升。]]></description>
            <link>https://www.elastic.co/search-labs/cn/blog/elasticsearch-gpu-accelerated-vector-indexing-nvidia</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/cn/blog/elasticsearch-gpu-accelerated-vector-indexing-nvidia</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Chris Hegarty, Hemant Malik, Corey Nolet, Manas Singh, Mithun Radhakrishnan, Mayya Sharipova, Lorenzo Dematte, Ben Frederickson]]></dc:creator>
            <pubDate>Wed, 03 Dec 2025 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/08f7469a4daaf67b7c5999585aae179b6680c78d-896x746.png?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>今年早些时候，Elastic 宣布与 NVIDIA <a href="https://ir.elastic.co/news/news-details/2025/Elastic-Brings-Enterprise-Data-to-NVIDIA-AI-Factories/default.aspx">合作</a>，为 Elasticsearch 引入 GPU 加速功能，并与 <a href="https://developer.nvidia.com/cuvs">NVIDIA cuVS</a> 集成 — 相关详情可参阅 <a href="https://www.nvidia.com/en-us/on-demand/session/gtc25-S71286/">NVIDIA GTC 大会</a>的相关会议以及多篇<a href="https://www.elastic.co/search-labs/blog/gpu-accelerated-vector-search-elasticsearch-nvidia">博文</a>。本文主要介绍我们与 NVIDIA 向量搜索团队在联合工程方面的最新进展。</p><h2>回顾</h2><p>先简单回顾一下最新动态。Elasticsearch 现已确立其作为强大向量数据库的地位，在大规模相似性搜索方面提供了丰富的功能和强劲的性能。凭借标量量化、Better Binary Quantization (<a href="https://www.elastic.co/search-labs/blog/better-binary-quantization-lucene-elasticsearch">BBQ</a>)、<a href="https://www.elastic.co/blog/accelerating-vector-search-simd-instructions">SIMD</a> 向量运算以及 <a href="https://www.elastic.co/search-labs/blog/diskbbq-elasticsearch-introduction">DiskBBQ</a> 等在磁盘利用方面更高效的算法，Elasticsearch 已经为管理向量工作负载提供了高效而灵活的多种选项。</p><p>通过将 NVIDIA cuVS 集成为可调用的向量搜索模块，我们希望大幅提升向量索引的性能和效率，从而更好地支撑大规模向量工作负载。</p><h2>挑战</h2><p>构建高性能向量数据库的最大挑战之一，就是构建向量索引，即 <a href="https://arxiv.org/abs/1603.09320">HNSW</a> 图。随着每个向量都要与大量其他向量进行比对，索引构建很快就会被数以百万乃至数十亿次的算术运算所主导。此外，压缩、合并等索引生命周期操作还会进一步增加索引的整体计算开销。随着数据量和相关向量嵌入呈指数级增长，专为大规模并行和高吞吐量数值运算而设计的加速计算 GPU 非常适合处理这些工作负载。</p><h2>进入 Elasticsearch-GPU 插件</h2><p><a href="https://developer.nvidia.com/cuvs">NVIDIA cuVS</a>是一个开源的 CUDA-X 库，用于 GPU 加速的向量搜索和数据集群，能够为 AI 和推荐工作负载快速构建索引和嵌入检索。</p><p>Elasticsearch 通过 <a href="https://mvnrepository.com/artifact/com.nvidia.cuvs/cuvs-java">cuvs-java</a> 使用 cuVS，这是一个由社区开发并由 NVIDIA 维护的开源库。cuvs-java 库十分轻量，基于 <a href="https://docs.rapids.ai/api/cuvs/nightly/c_api/">cuVS C API</a> 构建，并借助 <a href="https://openjdk.org/projects/panama/">Panama</a> 外部函数接口，以符合 Java 习惯用法的方式暴露 cuVS 功能，同时兼具现代性和高性能。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/5f6daa1eb07f704a6707d9e6b7ccb81d0abaa8c9-566x419.png" style="width: 100%;" /><p>cuvs-java 库被集成到<a href="https://github.com/elastic/elasticsearch/pull/135545">一个新的 Elasticsearch 插件</a>中；因此，GPU 上的向量索引可在同一 Elasticsearch 节点和同一进程内完成，无需部署任何外部组件或额外硬件。在索引构建过程中，如果已安装 cuVS 库且存在已正确配置的 GPU，Elasticsearch 会利用 GPU 加速向量索引过程。向量会被传递给 GPU，由 GPU 构建 <a href="https://arxiv.org/abs/2308.15136">CAGRA</a> 图。随后将该图转换为 HNSW 格式，使其能够立即在 CPU 上用于向量搜索。构建完成的图，其最终格式与在 CPU 上构建的图完全一致；这使得在底层硬件支持的情况下，Elasticsearch 可以利用 GPU 实现高吞吐量的向量索引，同时释放 CPU 算力，用于并发搜索、数据处理等其他任务。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/3ea255bd9bfd7983f78143c5eba999d2149d72be-671x356.png" style="width: 100%;" /><h2>索引构建加速</h2><p>作为将 GPU 加速集成到 Elasticsearch 的一部分，对 cuvs-java 进行了多项增强，重点是高效的数据输入/输出和函数调用。一项关键增强是使用 <a href="https://github.com/rapidsai/cuvs/blob/2cf5fa7666d703dccbe655f8214656b0952bb69b/java/cuvs-java/src/main/java/com/nvidia/cuvs/CuVSMatrix.java">cuVSMatrix</a> 对向量进行透明建模，无论它们位于 Java 堆中、堆外还是 GPU 内存中。这使数据可以在内存与 GPU 之间高效传输，避免对可能多达数十亿个向量进行不必要的复制。</p><p>由于这种底层的零拷贝抽象，数据传输到 GPU 内存以及从中检索图时都可以直接完成。在索引过程中，向量首先缓存在 Java 堆内存中，然后发送到 GPU，以构建 CAGRA 图。随后，从 GPU 中取回该图，将其转换为 HNSW 格式，并持久化到磁盘。</p><p>在合并时，向量已经存储在磁盘上，完全绕过了 Java 堆。索引文件采用内存映射，数据直接传输到 GPU 内存中。该设计还能轻松适应不同的位宽，如 float32 或 int8，并自然扩展到其他量化方案。</p><h2>话不多说，那它的实际表现如何呢？</h2><p>在我们探讨数字之前，了解一些背景信息会有所帮助。在索引期间，Elasticsearch 中的分段合并通常在后台自动运行，这会导致在隔离环境中进行基准测试变得十分困难。为了获得可重复的结果，我们使用了强制合并来在受控实验中明确触发分段合并。由于强制合并与后台合并执行相同的底层合并操作，因此其性能可作为预期改进的有用指标，尽管在实际索引工作负载中，具体收获可能会有所不同。</p><p>现在让我们来探讨数字。</p><p>我们的初步基准测试结果非常令人鼓舞。我们在 AWS <code>g6.4xlarge</code> 实例上运行了基准测试，该实例具有本地连接的 NVMe 存储。我们将单个 Elasticsearch 节点配置为使用默认的最佳索引线程数（8 个，每个物理核心一个），并关闭<a href="https://www.elastic.co/docs/reference/elasticsearch/index-settings/merge">合并限速功能</a>（在使用快速 NVMe 磁盘时，这一功能的适用性较低）。</p><p>对于数据集，我们使用了 <a href="https://github.com/elastic/rally-tracks/blob/master/openai_vector/README.md">OpenAI Rally 向量赛道</a>中的 260 万个、每个具有 1,536 维的向量，将其编码为 <a href="https://github.com/elastic/elasticsearch/pull/137072">base64 字符串</a>，并以 float32 <em>hnsw</em> 结构进行索引。在所有场景中，构建的图都能达到最高约 95% 的召回率。以下是我们的发现：</p><ul><li><strong>索引吞吐量：</strong>通过在内存缓冲区刷新期间将图构建移交给 GPU 处理，我们将吞吐量提高了约 12 倍。</li><li><strong>强制合并：</strong>索引完成后，GPU 继续加速分段合并，将强制合并阶段加快约 7 倍。</li></ul><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/f60ea9ee5996e456f393ffd195ee7eada6e5a7c2-948x387.png" style="width: 100%;" /><ul><li><strong>CPU 使用率：</strong>将图构建任务分流到 GPU，可显著降低 CPU 的平均和峰值利用率。以下图表展示了索引和合并期间的 CPU 使用情况，凸显了在 GPU 上运行这些操作时 CPU 使用率的显著降低。GPU 索引期间降低 CPU 使用率，可释放 CPU 周期并重新分配，从而提升搜索性能。</li></ul><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/5e680a5fc41700a877f3d8b2e5ce18ebd3f37a0b-1600x562.png" style="width: 100%;" /><ul><li><strong>召回率：</strong>在 CPU 与 GPU 的运行结果中，准确性几乎一致，而 GPU 所构建的图在召回率方面略胜一筹。</li></ul><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/48a2b7758606bd321712b7d8378cd2640e652a4e-1384x544.png" style="width: 100%;" /><h2>再从价格这个维度来进行比较</h2><p>前面的对比特意选用了相同的硬件配置，唯一的区别只是索引时是否启用 GPU。这种设置有助于单独考察计算性能的影响，不过也可以从成本角度来进行对比。</p><p>在与 GPU 加速配置大致相同的按小时费用下，可以搭建一套仅使用 CPU 的环境，其 CPU 和内存资源大约是前者的两倍：32 个 vCPU（AMD EPYC）和 64 GB RAM，因而可将索引线程数量增加到 16</p><p>为了保持比较的公平和一致性，我们在 AWS g6.8xlarge 实例上运行了这个仅 CPU 的实验，并且明确禁用了 GPU。这使我们能够在评估 GPU 加速与仅 CPU 索引的成本-性能权衡时，保持所有其他硬件特性不变。</p><p>正如您所预期的那样，更强大的 CPU 实例与上述部分的基准测试相比，性能确实有所提高。然而，将这一性能更强的 CPU 实例与最初的 GPU 加速结果进行对比后可以看到，GPU 依然带来显著性能提升：索引吞吐量提高<strong>约 5 倍</strong>，强制合并阶段加速<strong>约 6 倍</strong>，同时构建的图其召回率最高可达 <strong>95%</strong>。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/8ffa58cae3ad175ef2932a351aeef4c34a1407b9-948x394.png" style="width: 100%;" /><h2>结论</h2><p>在端到端场景中，使用 NVIDIA cuVS 进行的 GPU 加速使索引吞吐量提高了近 12 倍，将强制合并延迟降低到原来的 1/7，同时显著降低了 CPU 利用率。这表明向量索引和合并工作负载从 GPU 加速中受益显著。在成本调整后的对比中，GPU 加速依然带来显著的性能提升：索引吞吐量约提升 5 倍，强制合并操作的速度提升约 6 倍。</p><p>GPU 加速的向量索引目前计划在 Elasticsearch 9.3 的技术预览版中推出，该版本计划于 2026 年初发布。</p><p>敬请关注更多内容。</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[9.2 中的 ES|QL：Smart Lookup Joins 和时间序列支持]]></title>
            <description><![CDATA[探索 Elasticsearch 9.2 中 ES|QL 的三个独立更新：增强的 LOOKUP JOIN 用于更具表现力的数据关联、新的 TS 命令用于时间序列分析，以及灵活的 INLINE STATS 命令用于聚合。]]></description>
            <link>https://www.elastic.co/search-labs/cn/blog/esql-elasticsearch-9-2-multi-field-joins-ts-command</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/cn/blog/esql-elasticsearch-9-2-multi-field-joins-ts-command</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Tyler Perkins, Kostas Krikellas, Julian Kiryakov]]></dc:creator>
            <pubDate>Tue, 02 Dec 2025 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/415894e21e7758c907d6e60d4efc94230349beef-2012x1164.png?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>10 月发布的 Elasticsearch 9.2 版本包含诸多重大改进，使数据分析比以往任何时候都更快、更灵活、更易访问。该版本的核心是对我们的管道式查询语言 ES|QL 的重要增强，旨在直接为终端用户带来更多价值。</p><p>下面将介绍 Elasticsearch 9.2 中的功能，这些功能将利用 ES|QL 改变您的数据分析工作流。</p><h2>数据关联的革命：更智能、更快速、更灵活的 Lookup Join</h2><p>在 Elasticsearch 9.2 中，ES|QL 中的 <a href="https://www.elastic.co/docs/reference/query-languages/esql/commands/lookup-join">LOOKUP JOIN</a> 命令发生了重大变化，变得更加高效和多功能。LOOKUP JOIN 将 ES|QL 查询结果表中的数据与指定查找模式索引中的匹配记录结合起来。它会根据连接字段中的匹配值，将查找索引中的字段作为新列添加到结果表中。以前，连接数据仅限于单一字段和简单相等。不再是这样了！这些增强功能使您能够轻松应对复杂的数据关联方案。</p><p><strong>Lookup Join 的主要增强功能包括：</strong></p><ul><li><strong>多字段连接：</strong>轻松连接多个字段。例如，要将 <code>application_logs</code> 与 <code>service_registry</code> 连接到 <code>service_name</code>，<code>environment</code> 和 <code>version:</code></li></ul><pre><code>FROM application_logs
| LOOKUP JOIN service_registry ON service_name, environment, version</code></pre><ul><li><strong>使用表达式释放复杂的连接谓词（技术预览）：</strong></li></ul><p>您不再局限于简单相等。LOOKUP JOIN 现在允许您指定<strong>多个相关性标准</strong>，并纳入一系列<strong>二进制运算符</strong>，包括 ==、 !=、&lt;、&gt;、&lt;= 和 &gt;=。这意味着您可以创建高度细致的连接条件，从而能够对数据提出更复杂的问题。</p><p>示例 1：使用按服务 SLA 阈值查找应用程序指标</p><pre><code>FROM application_metrics
| LOOKUP JOIN sla_thresholds
      ON service_name == sla_service AND response_time > sla_response_time</code></pre><p>示例 2：此查询根据随时间变化的地区定价政策计算应付金额。它根据复杂的日期范围和相等条件连接三个数据集，计算出最终的 <code>due_amount</code>。第二个查找连接使用 <code>meter_readings</code> 索引中的<code>measurement_date</code> 字段和<code>customers</code> 索引中的<code>region_id</code> 字段连接到<code>pricing_policies</code> 索引，并为特定 <code>region</code> 和 <code>measurement_date</code> 查找正确的定价策略。</p><pre><code>FROM meter_readings
| LOOKUP JOIN customers
      ON meter_id
| LOOKUP JOIN pricing_policies
      ON
        region_id == region AND
          measurement_date >= policy_begin_date AND
          measurement_date < policy_end_date
| EVAL due_amount = (kwh_consumed * rate_per_kwh + base_charge) * (1 + tax_rate)
| EVAL period = policy_name
| KEEP customer_name, period, due_amount, measurement_date, kwh_consumed,
    rate_per_kwh, base_charge, tax_rate
| SORT measurement_date</code></pre><ul><li><strong>过滤连接带来的巨大性能提升： </strong></li></ul><p>我们提高了使用查找表条件筛选的“扩展连接”的性能。扩展连接会使每条输入行产生多个匹配项，从而产生较大的中间结果集。当后续筛选器丢弃其中许多行时，情况会变得更糟。在 9.2 中，我们通过在对查找数据应用筛选器时筛选掉不必要的行来优化这些连接，而避免处理将被丢弃的行。在某些情况下，这些连接的速度最多可以<strong>快 1000 倍</strong>！</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/002c014ee29b1aaf9ddeb8c554bb76efe3ed180c-1572x954.png" style="width: 100%;" /><p>这种优化在处理“扩展连接”时至关重要，因为在这种情况下，查找最初可能会产生许多潜在的匹配项。通过智能推送筛选器，仅处理相关数据，从而大幅缩短查询执行时间，实现对海量数据集的实时分析。这意味着，即使是大型或复杂的连接操作，您也能更快地获得见解。</p><p><strong>Lookup Join 跨集群搜索 (CCS) 兼容性：</strong></p><p>当 Lookup Join 在 8.19 和 9.1 版本中正式发布时，它缺少跨集群搜索 (CCS) 支持。对于在多个集群中运行的组织，LOOKUP JOIN 现在可与 9.2 中的 CCS 无缝集成。只需在要执行连接的所有远程集群上放置查找索引，ES|QL 就会自动利用这些远程查找索引来连接远程数据。这简化了分布式数据分析，并确保在整个 Elasticsearch 部署中实现一致的丰富性。</p><p>这些改进意味着您可以以前所未有的精度、速度和便捷性关联各种数据集，从而发现更深入、更具可操作性的见解，而无需复杂的替代方案或预处理步骤。</p><h2>轻松丰富您的数据：适用于 Lookup 索引的 Kibana Discover 用户体验</h2><p>数据丰富应该简单，而不是障碍。我们在 Kibana 的 Discover 中为创建和管理查找索引引入了绝佳的全新用户体验。</p><p><strong>直观的工作流：</strong>Discover 全面的自动完成功能将引导您完成整个流程，并在 ES|QL 编辑器中建议查找索引和连接字段，使您可以非常轻松地将上传的数据与现有索引连接起来。键入一个不存在的查找索引名称，然后直接访问 Lookup 编辑器，只需单击一下即可创建索引。键入现有查找索引的名称，我们将为您提供编辑该索引的选项：</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/d36fd6ffd6bc0bf8d31067f6266445c68d15c71c-1400x184.png" style="width: 100%;" /><p><strong>在线管理 (CRUD)：</strong>直接在 Discover 中使用行内编辑功能（创建、读取、更新、删除），使参考数据集保持最新状态。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/a0e4aa66540b1f725c24ccb0519d978415073bb6-1453x842.png" style="width: 100%;" /><p><strong>轻松上传文件：</strong>现在，您可以在 Discover 中直接上传 CSV 等文件，并立即在 <code>LOOKUP JOIN</code> 中使用这些文件。再也不需要在 Kibana 的不同区域之间来回切换上下文了！</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/0f5dc2c712af4c4cada50292a7c8b836eb02aa67-1600x748.png" style="width: 100%;" /><p>无论您是将用户 ID 映射到名称、添加业务元数据，还是连接静态参考文件，此功能都能让数据丰富化变得触手可及，将连接的强大功能直接交到每个用户的手中——快速、简单且集中于一处。</p><h2>保留上下文：INLINE STATS 简介（技术预览）</h2><p>数据聚合至关重要，但有时您需要在查看原始数据的<em>同时</em>查看聚合数据。我们很高兴将 <a href="https://www.elastic.co/docs/reference/query-languages/esql/commands/inlinestats-by">INLINE STATS</a> 作为<strong>技术预览</strong>功能推出。</p><p><code>STATS</code> 命令会将输入字段替换为聚合输出，而<code>INLINE STATS</code> 命令则不同，它保留了所有原始输入字段，只是添加了新的聚合字段。这样，您就可以在聚合<em>后</em>对原始输入字段执行进一步操作，从而提供更连续、更灵活的分析工作流。</p><p>例如，要在计算平均飞行距离的同时保留单个飞行记录行：</p><pre><code>FROM kibana_sample_data_flights
 | KEEP Carrier, Dest, DistanceMiles
 | INLINE STATS avgDist = ROUND(AVG(DistanceMiles))
       BY Dest
 | WHERE DistanceMiles > avgDist</code></pre><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/6eea943035e0ab371270084c504a06bb89f8b82b-1496x290.png" style="width: 100%;" /><p>在此查询中，将 <code>avgDist</code> 与我们分组的相应 <code>Dest</code> (ination) 添加到每一行中，然后，由于我们仍有航班信息列，我们可以将结果筛选为飞行距离大于平均值的航班。</p><h2>ES|QL 中的时间序列支持（技术预览版）</h2><p>Elasticsearch 使用<a href="https://www.elastic.co/docs/manage-data/data-store/data-streams/time-series-data-stream-tsds">时序数据流</a>来存储指标。我们将通过 <a href="https://www.elastic.co/docs/reference/query-languages/esql/commands/ts"><code>TS</code></a> 源代码命令在 ES|QL 中添加对时间序列聚合的支持。此功能在 Elastic Cloud serverless 和 9.2 基础版中以技术预览版的形式提供。</p><p>时间序列分析主要基于聚合查询，这些查询按照一个或多个筛选维度对时间分桶的指标值进行汇总。大多数聚合查询依靠两步处理，包括：(a) 内部聚合函数对每个时间序列的值进行汇总，以及 (b) 外部聚合函数将 (a) 的结果在时间序列之间进行组合。</p><p><a href="https://www.elastic.co/docs/reference/query-languages/esql/commands/ts"><code>TS</code></a> 源命令与 <a href="https://www.elastic.co/docs/reference/query-languages/esql/commands/stats-by"><code>STATS</code></a> 结合使用，为表达对时间序列的查询提供了一种简洁而有效的方法。具体来说，请考虑以下计算每个主机和每小时的总请求率的示例：</p><pre><code>TS my_metrics
| WHERE @timestamp > NOW() - 1 day
| STATS SUM(RATE(requests))
      BY host, TBUCKET(1h)</code></pre><p>在这种情况下，首先对每个时间序列和每个小时的时间序列聚合函数 <code>RATE</code> 进行评估。生成的部分聚合值随后使用 <code>SUM</code> 进行组合，以计算每个主机和每小时的最终聚合值。</p><p>您可以在<a href="https://www.elastic.co/docs/reference/query-languages/esql/functions-operators/time-series-aggregation-functions">此处</a>查看可用的时间序列聚合函数列表。现在支持<a href="https://www.elastic.co/docs/manage-data/data-store/data-streams/time-series-data-stream-tsds#time-series-metric">计数</a>率，这可以说是处理计数器最重要的聚合功能。</p><p><code>TS</code> 源命令旨在与<code>STATS</code> 结合使用，其执行经过调整，可有效支持时间序列聚合。例如，数据在进入<code>STATS</code> 之前要进行排序。目前不允许在 <code>TS</code> 和 <code>STATS</code> 之间执行可能丰富或改变时序数据或其顺序的处理命令，例如 <a href="https://www.elastic.co/docs/reference/query-languages/esql/commands/fork"><code>FORK</code></a> 或 <a href="https://www.elastic.co/docs/reference/query-languages/esql/commands/inlinestats-by"><code>INLINE STATS</code></a>。将来可能会取消这一限制。</p><p><code>STATS</code> 表格输出可使用任何适用命令进行进一步处理。例如，以下查询计算每台主机和每小时的平均 <code>cpu_usage</code> 与最大值的比率：</p><pre><code>TS my_metrics
| STATS avg_usage = AVG(AVG_OVER_TIME(cpu_usage))
      BY host, time_bucket = TBUCKET(1h)
| INLINE STATS max_avg_usage = MAX(avg_usage)
      BY host
| EVAL ratio = avg_usage / max_avg_usage
| KEEP host, time_bucket, ratio
| SORT host, time_bucket DESC</code></pre><p>时序数据存储在我们基于 Lucene 文档值的底层列式存储引擎中。TS 命令通过 ES|QL 计算引擎增加了向量化查询执行功能。与等效的<a href="https://www.elastic.co/docs/reference/query-languages/querydsl">DSL</a>查询相比，查询性能往往提高一个数量级以上，并可与已建立的特定度量系统相媲美。我们将在未来提供详细的架构和性能分析，敬请期待。</p><h2>扩展您的工具包：ES|QL 新功能</h2><div style="display:none">Unknown block type "block", specify a component for it in the `components.types` option</div><p><strong>字符串操作：</strong><a href="https://www.elastic.co/docs/reference/query-languages/esql/functions-operators/string-functions#esql-contains">CONTAINS</a>、<a href="https://www.elastic.co/docs/reference/query-languages/esql/functions-operators/mv-functions#esql-mv_contains">MV_CONTAINS</a>、<a href="https://www.elastic.co/docs/reference/query-languages/esql/functions-operators/string-functions#esql-url_encode">URL_ENCODE</a>、<a href="https://www.elastic.co/docs/reference/query-languages/esql/functions-operators/string-functions#esql-url_encode_component">URL_ENCODE_COMPONENT</a>、<a href="https://www.elastic.co/docs/reference/query-languages/esql/functions-operators/string-functions#esql-url_decode">URL_DECODE</a>，用于更强大的文本和 URL 处理。</p><p><strong>时间序列和地理空间：</strong> <a href="https://www.elastic.co/docs/reference/query-languages/esql/functions-operators/grouping-functions#esql-tbucket">TBUCKET</a>用于灵活的时间分桶，TO_DENSE_VECTOR 用于矢量运算，以及一套全面的<a href="https://www.elastic.co/docs/reference/query-languages/esql/functions-operators/spatial-functions">地理空间函数，</a>如<code>ST_GEOHASH</code> 、 <code>ST_GEOTILE</code> 、 <code>ST_GEOHEX</code> 、 <code>TO_GEOHASH</code> 、 <code>TO_GEOTILE</code> 、 <code>TO_GEOHEX</code>用于高级基于位置的分析。</p><p><strong>日期格式：</strong>使用 <a href="https://www.elastic.co/docs/reference/query-languages/esql/functions-operators/date-time-functions#esql-day_name">DAY_NAME</a>、<a href="https://www.elastic.co/docs/reference/query-languages/esql/functions-operators/date-time-functions#esql-month_name">MONTH_NAME</a>，以获得更易读的日期表示形式。</p><p>这些功能为您提供了一套更丰富的工具，可直接在 ES|QL 中操作和分析您的数据。</p><h2>内在优势：更高的性能和效率</h2><p>除了上述突出的功能外，Elasticsearch 9.2 还对 ES|QL 进行了大量性能优化。在函数能够替代多个类似 RLIKE 查询的情况下，我们使用 pushdown 加快了 <a href="https://www.elastic.co/docs/reference/query-languages/esql/commands/where#like-and-rlike">RLIKE(LIST)</a> 的执行速度。通过 <code>RLIKE</code> (LIST)，我们可以将这些查询合并为一个自动机，并应用一个自动机而不是多个自动机。我们还通过索引排序加快了关键字字段的加载速度，并对一般查询进行了优化——这些改进可确保您的 ES|QL 查询比以往更高效地运行。</p><h2>立即开始！</h2><p>Elasticsearch 9.2 标志着 ES|QL 的重大飞跃，为您的数据分析工作流带来了前所未有的强大功能和灵活性。我们鼓励您探索这些新功能，并体验它们带来的不同。</p><p>有关 Elasticsearch 9.2 中所有更改和增强功能的完整列表，请参阅<a href="https://www.elastic.co/guide/en/elasticsearch/reference/9.2/release-notes-9.2.0.html">正式发布说明</a>。祝您查询愉快！</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[使用 Elasticsearch 构建 ChatGPT 连接器以查询 GitHub 问题]]></title>
            <description><![CDATA[学习如何构建自定义 ChatGPT 连接器并部署使用混合搜索查询内部 GitHub 问题的 Elasticsearch MCP 服务器。]]></description>
            <link>https://www.elastic.co/search-labs/cn/blog/chatgpt-connector-mcp-server-github-elasticsearch</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/cn/blog/chatgpt-connector-mcp-server-github-elasticsearch</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Tomás Murúa]]></dc:creator>
            <pubDate>Mon, 01 Dec 2025 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/77a6fc1eb263a0eb16aac64f2ecaca5f4ac12ec2-966x568.gif?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>最近，OpenAI 宣布为专业版/商务版/企业版和教育版 ChatGPT 提供<a href="https://help.openai.com/en/articles/11487775-connectors-in-chatgpt">自定义连接器</a>功能。除了提供开箱即用的连接器来获取 Gmail、GitHub、Dropbox 等平台上的数据。还可以使用 MCP 服务器创建自定义连接器。</p><p>定制连接器使您能够将现有的 ChatGPT 连接器与其他数据源（如 Elasticsearch）结合，以获得全面的答案。</p><p>在本文中，我们将构建一个 <a href="https://modelcontextprotocol.io/docs/getting-started/intro">MCP</a> 服务器，将 ChatGPT 连接到包含内部 GitHub 问题和拉取请求信息的 Elasticsearch 索引。这样就可以使用 Elasticsearch 数据回答自然语言查询。</p><p>我们将在 Google Colab 上使用 <a href="https://gofastmcp.com/getting-started/welcome">FastMCP</a> 和 ngrok 部署 MCP 服务器，以获取 ChatGPT 可以连接的公共 URL，从而省去复杂的基础架构设置。</p><p>有关 MCP 及其生态系统的全面概述，请参阅《<a href="https://www.elastic.co/search-labs/blog/mcp-current-state">MCP 的现状</a>》。</p><h2>准备工作</h2><p>在开始之前，您需要：</p><ul><li>Elasticsearch 集群（8.X 或更高版本）</li><li>Elasticsearch API密钥，具有对您的索引的读取访问权限</li><li>Google 账户（用于 Google Colab）</li><li>Ngrok账户 （免费套餐可用）</li><li>拥有专业版/企业版/商务版或教育版套餐的 ChatGPT 账户</li></ul><h2>了解 ChatGPT MCP 连接器的要求</h2><p>ChatGPT MCP 连接器需要实现两个工具：<code>search</code> 和 <code>fetch</code>。有关更多详情，请参阅 <a href="https://platform.openai.com/docs/mcp#create-an-mcp-server">OpenAI 文档</a>。</p><h3><a href="https://platform.openai.com/docs/mcp#search-tool">搜索工具</a></h3><p>根据用户查询，从 Elasticsearch 索引中返回相关结果列表。</p><h4>接收的内容：</h4><ul><li>一个单一的字符串，包含用户的自然语言查询。</li><li>示例：“查找与 Elasticsearch 迁移相关的问题。”</li></ul><h4>返回的内容：</h4><ul><li>一个对象，其<code>result</code> 关键字包含一个结果对象数组。每个结果包括：<ul><li><code>id</code> - 唯一文档标识符</li><li><code>title</code> - 问题或拉取请求标题</li><li><code>url</code> - 链接到问题或 PR</li></ul></li></ul><h4>在我们的实现中：</h4><pre><code>return {
    "results": [
        {
            "id": "PR-612",
            "title": "Fix memory leak in WebSocket notification service",
            "url": "https://internal-git.techcorp.com/pulls/612"
        },
        # ... more results
    ]
}</code></pre><h3><a href="https://platform.openai.com/docs/mcp#fetch-tool">获取工具</a></h3><p>获取指定文档的完整内容。</p><h4>接收的内容：</h4><ul><li>搜索结果中包含 Elasticsearch 文档 ID 的单个字符串</li><li>示例：“获取 PR-578 的详细信息。”</li></ul><h4>它返回的内容：</h4><ul><li>一个完整的文档对象，包含：<ul><li><code>id</code> - 唯一文档标识符</li><li><code>title</code> - 问题或拉取请求标题</li><li><code>text</code> - 完整的问题/PR描述和详细信息</li><li><code>url</code> - 链接到问题或 PR</li><li><code>type</code> - 文档类型（问题、pull_request）</li><li><code>status</code> - 当前状态（打开、进行中、已解决）</li><li><code>priority</code> - 优先级别（低、中、高、关键）</li><li><code>assignee</code> - 负责此问题/PR 的人员</li><li><code>created_date</code> - 何时创建</li><li><code>resolved_date</code> - 何时解决（如适用）</li><li><code>labels</code> - 与文件相关的标签</li><li><code>related_pr</code> － 相关拉取请求 ID</li></ul></li></ul><pre><code>return {
    "id": "PR-578",
    "title": "Security hotfix: Patch SQL injection vulnerabilities",
    "text": "Description: CRITICAL SECURITY FIX for ISSUE-1889. Patches SQL...",
    "url": "https://internal-git.techcorp.com/pulls/578",
    "type": "pull_request",
    "status": "closed",
    "priority": "critical",
    "assignee": "sarah_dev",
    "created_date": "2025-09-19",
    "resolved_date": "2025-09-19",
    "labels": "security, hotfix, sql",
    "related_pr": null
}</code></pre><p><strong>注意</strong>：本示例使用扁平结构，其中所有字段都位于根级别。OpenAI 的要求非常灵活，还支持嵌套的元数据对象。</p><h2>GitHub 问题和 PR 数据集</h2><p>在本教程中，我们将使用包含问题和拉取请求的内部 GitHub 数据集。这代表了一个您希望通过 ChatGPT 查询私有、内部数据的场景。</p><p>数据集可以在<a href="https://gist.github.com/TomasMurua/4e7bbdf7a7ebbdffaa663c43578d934a">此处</a>找到。我们将使用<a href="https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-bulk">批量 API</a> 更新数据索引。</p><p>这个数据集包含：</p><ul><li>有关描述、状态、优先级和分配人员的问题</li><li>包含代码更改、审查和部署信息的拉取请求</li><li>问题与 PR 之间的关系（例如，PR-578 修复了 ISSUE-1889）</li><li>标签、日期和其他元数据</li></ul><h3>索引映射</h3><p>该索引使用以下<a href="https://www.elastic.co/docs/manage-data/data-store/mapping">映射</a>来支持使用 <a href="https://www.elastic.co/docs/explore-analyze/machine-learning/nlp/ml-nlp-elser">ELSER</a> 的混合搜索。<a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/semantic-text">text_semantic</a> 用于语义搜索，而其他字段用于关键字搜索。</p><pre><code>{
  "mappings": {
    "properties": {
      "id": {
        "type": "keyword"
      },
      "title": {
        "type": "text"
      },
      "text": {
        "type": "text"
      },
      "text_semantic": {
        "type": "semantic_text",
        "inference_id": ".elser-2-elasticsearch"
      },
      "url": {
        "type": "keyword"
      },
      "type": {
        "type": "keyword"
      },
      "status": {
        "type": "keyword"
      },
      "priority": {
        "type": "keyword"
      },
      "assignee": {
        "type": "keyword"
      },
      "created_date": {
        "type": "date",
        "format": "iso8601"
      },
      "resolved_date": {
        "type": "date",
        "format": "iso8601"
      },
      "labels": {
        "type": "keyword"
      },
      "related_pr": {
        "type": "keyword"
      }
    }
  }
}</code></pre><h2>构建MCP服务器</h2><p>我们的 MCP 服务器按照 OpenAI 规范实现了两个工具，使用混合搜索将语义和文本匹配相结合，以获得更好的结果。</p><h3>搜索工具</h3><p>利用 <a href="https://www.elastic.co/docs/reference/elasticsearch/rest-apis/reciprocal-rank-fusion">RRF</a>（倒数排序融合）进行混合搜索，将语义搜索与文本匹配相结合：</p><pre><code>@mcp.tool()
    async def search(query: str) -> Dict[str, List[Dict[str, Any]]]:
        """
        Search for internal issues and PRs using hybrid search (semantic + text with RRF).
        Returns list with id, title, and url per OpenAI spec.
        """
        if not query or not query.strip():
            return {"results": []}

        logger.info(f"Searching for: '{query}'")

        try:
            # Hybrid search with RRF (Reciprocal Rank Fusion)
            response = es_client.search(
                index=ELASTICSEARCH_INDEX,
                size=10,
                source=["id", "title", "url", "type", "priority"],
                retriever={
                    "rrf": {
                        "retrievers": [
                            {
                                # Semantic search with ELSER
                                "standard": {
                                    "query": {
                                        "semantic": {
                                            "field": "text_semantic",
                                            "query": query
                                        }
                                    }
                                }
                            },
                            {
                                # Text search (BM25) for keyword matching
                                "standard": {
                                    "query": {
                                        "multi_match": {
                                            "query": query,
                                            "fields": [
                                                "title^3",
                                                "text^2",
                                                "assignee^2",
                                                "type",
                                                "labels",
                                                "priority"
                                            ],
                                            "type": "best_fields",
                                            "fuzziness": "AUTO"
                                        }
                                    }
                                }
                            }
                        ],
                        "rank_window_size": 50,
                        "rank_constant": 60
                    }
                }
            )

            results = []
            if response and 'hits' in response:
                for hit in response['hits']['hits']:
                    source = hit['_source']
                    results.append({
                        "id": source.get('id', hit['_id']),
                        "title": source.get('title', 'Unknown'),
                        "url": source.get('url', '')
                    })

            logger.info(f"Found {len(results)} results")
            return {"results": results}

        except Exception as e:
            logger.error(f"Search error: {e}")
            raise ValueError(f"Search failed: {str(e)}")</code></pre><h3>要点：</h3><ul><li><strong>使用 RRF 的混合搜索：</strong>结合语义搜索 (ELSER) 和文本搜索 (BM25)，以获得更好的结果。</li><li><strong>多匹配查询：</strong><a href="https://www.elastic.co/docs/reference/query-languages/query-dsl/query-dsl-multi-match-query">在多个字段中进行搜索</a>，并使用增强功能（标题^3、文本^2、分配人员^2）。插入符号 (^) 会乘以相关性分数，优先考虑标题中的匹配项而非内容中的匹配项。</li><li><strong>模糊匹配：</strong><a href="https://www.elastic.co/docs/reference/elasticsearch/rest-apis/common-options#fuzziness"><code>fuzziness: AUTO</code></a> 通过允许近似匹配来处理错别字和拼写错误。</li><li><strong>RRF 参数调整：</strong><ul><li><code>rank_window_size: 50</code> - 指定在合并之前从每个检索器（语义和文本）中考虑最靠前结果的数量。</li><li><code>rank_constant: 60</code> - 该值决定了单个结果集中的文档对最终排序结果的影响程度。</li></ul></li><li><strong>仅返回必填字段：</strong>根据 OpenAI 规范返回 <code>id</code>、<code>title</code>、<code>url</code>，避免不必要地暴露其他字段。</li></ul><h3>获取工具</h3><p>按文档 ID（如果存在）检索文档详细信息：</p><pre><code>@mcp.tool()
    async def fetch(id: str) -> Dict[str, Any]:
        """
        Retrieve complete issue/PR details by ID.
        Returns id, title, text, url.
        """
        if not id:
            raise ValueError("ID is required")

        logger.info(f"Fetching: {id}")

        try:
            # Search by the 'id' field (not _id) since IDs are stored as a field
            response = es_client.search(
                index=ELASTICSEARCH_INDEX,
                body={
                    "query": {
                        "term": {
                            "id": id  # Search by your custom 'id' field
                        }
                    },
                    "size": 1
                }
            )

            if not response or not response['hits']['hits']:
                raise ValueError(f"Document with id '{id}' not found")

            hit = response['hits']['hits'][0]
            source = hit['_source']

            result = {
                "id": source.get('id', id),
                "title": source.get('title', 'Unknown'),
                "text": source.get('text', ''),
                "url": source.get('url', ''),
                "type": source.get('type', ''),
                "status": source.get('status', ''),
                "priority": source.get('priority', ''),
                "assignee": source.get('assignee', ''),
                "created_date": source.get('created_date', ''),
                "resolved_date": source.get('resolved_date', ''),
                "labels": source.get('labels', ''),
                "related_pr": source.get('related_pr', '')
            }

            logger.info(f"Fetched: {result['title']}")
            return result

        except Exception as e:
            logger.error(f"Fetch error: {e}")
            raise ValueError(f"Failed to fetch '{id}': {str(e)}")</code></pre><h3>要点：</h3><ul><li><strong>按文档 ID 字段进行搜索：</strong>使用自定义 <code>id</code> 字段上的术语查询</li><li><strong>返回完整文档：</strong>包含完整的 <code>text</code> 字段及其所有内容</li><li><strong>扁平结构：</strong>所有字段均位于根级别，与 Elasticsearch 的文档结构相匹配。</li></ul><h2>在 Google Colab 上部署</h2><p>我们将使用 Google Colab 来运行 MCP 服务器，并使用 ngrok 将其公开，以便 ChatGPT 可以连接到它。</p><h3>步骤 1：打开 Google Colab 笔记本</h3><p>访问我们预配置的笔记本<a href="https://github.com/elastic/elasticsearch-labs/tree/main/supporting-blog-content/elasticsearch-chatgpt-connector">适用于 ChatGPT 的 Elasticsearch MCP</a>。</p><h3>步骤 2：配置您的凭据</h3><p>您需要三项信息：</p><ul><li><strong>Elasticsearch URL：</strong>您的 <a href="https://www.elastic.co/docs/deploy-manage/deploy/cloud-enterprise/connect-elasticsearch">Elasticsearch 集群 URL</a>。</li><li><strong>Elasticsearch API 密钥：</strong>具有索引读取权限的 <a href="https://www.elastic.co/docs/deploy-manage/api-keys/elasticsearch-api-keys">API 密钥</a>。</li><li><strong>Ngrok 身份验证令牌：来自 </strong><a href="https://ngrok.com/">ngrok</a> 的免费令牌。我们将使用 ngrok 将 MCP URL 公开到互联网，以便 ChatGPT 可以连接到它。</li></ul><h4>获取 ngrok 令牌</h4><ol><li>在 <a href="https://ngrok.com/">ngrok</a> 注册免费账户</li><li>前往您的 <a href="https://dashboard.ngrok.com/">ngrok 仪表板</a></li><li>复制您的身份验证令牌</li></ol><h4>为 Google Colab 添加机密</h4><p>在 Google Colab 笔记本中：</p><ol><li>点击左侧边栏中的“<strong>密钥图标</strong>”以打开“<strong>机密</strong>”。</li><li>添加这三个秘密：</li></ol><pre><code>ELASTICSEARCH_URL=https://your-cluster.elastic.com:443
ELASTICSEARCH_API_KEY=your-api-key
NGROK_TOKEN=your-ngrok-token</code></pre><p>3. 为每个机密启用笔记本访问权限</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/d5dd6ac19fe816a562c6351fdb0f11369da0e877-609x321.jpg" style="width: 100%;" /><h3>步骤 3：运行 Notebook</h3><ol><li>点击“<strong>运行时</strong>”，然后点击“<strong>全部运行</strong>”，以执行所有单元格</li><li>等待服务器启动（约30秒）</li><li>查找显示您的公开 ngrok URL 的输出</li></ol><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/f185100e8869624bc9e1c7b2b4eb32785e2d89e7-1189x283.png" style="width: 100%;" /><p>4. 该输出将显示如下内容：</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/e02e625e91ed9136454e4401b184575fb03a336e-1052x465.jpg" style="width: 100%;" /><h2>连接 ChatGPT</h2><p>现在我们将 MCP 服务器连接到您的 ChatGPT 账户。</p><ol><li>打开 ChatGPT，前往“<strong>设置</strong>”。</li><li>导航到<strong>“连接器”。</strong>如果您使用的是专业版账户，则需要在连接器中打开“<a href="https://platform.openai.com/docs/guides/developer-mode">开发者模式</a>”。</li></ol><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/32c02192912fc0e7e5a52e9399077ba7ae3b4901-739x715.png" style="width: 100%;" /><p><em>如果您使用的是 ChatGPT 企业版或商业版，您需要将连接器发布到您的工作场所。</em></p><p>3. 点击“<strong>创建</strong>”。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/15c53e5ccc381108a9dc0052cca05bf0fc97679a-755x683.png" style="width: 100%;" /><p><em><strong>注意</strong>：在商业版、企业版和教育版工作区中，只有工作区所有者、管理员和已启用相应设置（针对企业版/教育版）的用户才能添加自定义连接器。具有普通成员角色的用户无法自行添加自定义连接器。</em></p><p><em>一旦连接器被所有者或管理员用户添加并启用，工作区中的所有成员即可使用该连接器。</em></p><p>4. 输入所需信息和以 <code>/sse/</code> 结尾的 ngrok URL。请注意“sse”后面的“/”。没有它就无法正常工作：</p><ul><li><strong>名字：</strong> Elasticsearch MCP</li><li><strong>描述：</strong>用于搜索和获取 GitHub 内部信息的自定义 MCP。</li></ul><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/162a85705cc8ac48a3f2f665551d513e0719f93d-479x684.png" style="width: 100%;" /><p>5. 按下“<strong>创建</strong>”保存自定义 MCP。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/97eb5fb0a32b86bfadfb35561f698616f217c049-913x629.png" style="width: 100%;" /><p>如果您的服务器正在运行，则连接是即时的。无需额外的身份验证，因为 Elasticsearch API 密钥已在服务器中配置。</p><h2>测试 MCP 服务器</h2><p>在提问之前，您需要先选择 ChatGPT 应该使用的连接器。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/77a6fc1eb263a0eb16aac64f2ecaca5f4ac12ec2-966x568.gif" style="width: 100%;" /><h3>提示 1: 搜索问题</h3><p>提问：“<strong>查找与 Elasticsearch 迁移相关的问题”</strong>并确认操作工具调用。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/cfd781acbff8cd7c8095bbe29224f8b26d581f77-650x375.png" style="width: 100%;" /><p>ChatGPT 将调用<code>search</code> 工具处理您的查询。你可以看到它正在查找可用工具，并准备调用 Elasticsearch 工具，在对该工具执行任何操作之前与用户确认。</p><h4>工具调用请求：</h4><pre><code>{
  "query": "Elasticsearch migration issues"
}</code></pre><h4>工具响应：</h4><pre><code>{
  "results": [
    {
      "id": "PR-598",
      "title": "Elasticsearch 8.x migration - Application code changes",
      "url": "https://internal-git.techcorp.com/pulls/598"
    },
    {
      "id": "ISSUE-1712",
      "title": "Migrate from Elasticsearch 7.x to 8.x",
      "url": "https://internal-git.techcorp.com/issues/1712"
    },
    {
      "id": "RFC-045",
      "title": "Design Proposal: Microservices Migration Architecture",
      "url": "https://internal-git.techcorp.com/rfcs/045"
    }
    // ... 7 more results
  ]
}</code></pre><p>ChatGPT 会处理这些结果，并以自然对话的形式呈现。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/9d5b6cff85c7e54ccc2584b8ae96d45495fae8c1-923x1352.png" style="width: 100%;" /><h3>幕后</h3><h4>提示：“查找与 Elasticsearch 迁移相关的问题”</h4><p>1. ChatGPT 调用 <code>search(“Elasticsearch migration”)</code></p><p>2. Elasticsearch 执行混合搜索。</p><ul><li><strong>语义搜索</strong>能理解“升级”和“<em>版本兼容性</em>”等概念。</li><li><strong>文本搜索</strong>可查找与“<em>Elasticsearch</em>”和“迁移”完全匹配的内容。</li><li><strong>RRF</strong> 将两种方法的结果进行合并和排序</li></ul><p>3. 返回与 <code>id</code>、<code>title</code> 匹配度最高的 10 个事件。 <code>url</code></p><p>4. ChatGPT 将“<em>ISSUE-1712：从 Elasticsearch 7.x 迁移到 8.x</em>”作为最相关的结果</p><h3>提示 2：获取完整的详细信息</h3><p>问：<strong><em>“请提供有关 ISSUE-1889 的详细信息”</em></strong></p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/5c0db5245535ce67a36056e61e135bddc97ce496-934x629.png" style="width: 100%;" /><p>ChatGPT 识别到您需要有关特定问题的详细信息，并调用 <code>fetch</code> 工具，在对该工具采取任何行动前与用户确认。</p><h4>工具调用请求：</h4><pre><code>{
  "id": "ISSUE-1889"
}</code></pre><h4>工具响应：</h4><pre><code>{
  "id": "ISSUE-1889",
  "title": "SQL injection vulnerability in search endpoint",
  "text": "Description: Security audit identified SQL injection vulnerability in /api/v1/search endpoint. User input from query parameter is not properly sanitized before being used in raw SQL query. Severity: HIGH - Immediate action required Affected Code: - File: services/search/query_builder.py - Line: 145-152 - Issue: String concatenation used instead of parameterized queries Investigation: - @security_team_alice: Confirmed exploitable with UNION-based injection - @sarah_dev: Checking all other endpoints for similar patterns - @john_backend: Found 3 more instances in legacy codebase Remediation: - Rewrite using SQLAlchemy ORM or parameterized queries - Add input validation and sanitization - Implement WAF rules as additional layer - Security regression tests Comments: - @tech_lead_mike: Stop all other work, this is P0 - @sarah_dev: PR-578 ready with fixes for all 4 vulnerable endpoints - @alex_devops: Deployed hotfix to production 2025-09-19 at 14:30 UTC - @security_team_alice: Verified fix, conducting full pentest next week Resolution: All vulnerable endpoints patched. Added pre-commit hooks to catch raw SQL queries. Security training scheduled for team.",
  "url": "https://internal-git.techcorp.com/issues/1889",
  "type": "issue",
  "status": "closed",
  "priority": "critical",
  "assignee": "sarah_dev",
  "created_date": "2025-09-18",
  "resolved_date": "2025-09-19",
  "labels": "security, vulnerability, bug, sql",
  "related_pr": "PR-578"
}</code></pre><p>ChatGPT 会整合信息并清晰呈现。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/410f19f213e94fc4e3c47eeef6e04b69e0c86159-602x462.png" style="width: 100%;" /><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/54d8ffa117628a1e3afc317c3ab75d4f7731d7ab-767x1600.png" style="width: 100%;" /><h3>幕后</h3><h4>提示：“获取有关 ISSUE-1889 的详细信息”</h4><ol><li>ChatGPT 调用 <code>fetch(“ISSUE-1889”)</code></li><li>Elasticsearch 会检索完整文档</li><li>返回一个包含所有字段在根级别的完整文档</li><li>ChatGPT会综合信息并提供正确的引用。</li></ol><h2>结论</h2><p>在本文中，我们构建了一个自定义 MCP 服务器，使用专用的<strong>搜索</strong>和<strong>获取</strong> MCP 工具将 ChatGPT 连接到 Elasticsearch，从而实现对私有数据的自然语言查询。</p><p>这种 MCP 模式适用于任何您想通过自然语言查询的 Elasticsearch 索引、文档、产品、日志或其他数据。</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[使用 LangChain 和 Elasticsearch 开发代理 RAG 助手]]></title>
            <description><![CDATA[了解如何使用 LangChain 和 Elasticsearch 构建一个代理式抹布新闻助手，通过自适应路由回答有关文章的查询。]]></description>
            <link>https://www.elastic.co/search-labs/cn/blog/agentic-rag-news-assistant-langchain-elasticsearch</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/cn/blog/agentic-rag-news-assistant-langchain-elasticsearch</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Kirti Sodhi]]></dc:creator>
            <pubDate>Fri, 28 Nov 2025 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/d7213956309c6b5863627aca2ba95569b02fc4c7-720x420.jpg?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>本博文将深入探讨代理 RAG 工作流，解释其主要特点和常见设计模式。它通过一个使用 Elasticsearch 作为向量存储和 LangChain 构建代理 RAG 框架的实践示例，进一步演示了如何实施这些工作流程。最后，文章简要讨论了与设计和实施此类架构相关的最佳实践和挑战。您可以使用此<a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/agentic-rag/agent_rag_news_assistant.ipynb">Jupyter 笔记本</a>创建一个简单的代理 RAG 管道。</p><h2>代理 RAG 简介</h2><p>检索增强生成<a href="https://www.elastic.co/docs/solutions/search/rag">（RAG</a>）已成为基于 LLM 的应用的基石，它使模型能够根据用户查询检索相关上下文，从而提供最佳答案。RAG 系统通过从应用程序接口或数据存储中获取外部信息，而不是局限于预先训练的 LLM 知识，从而提高了 LLM 响应的准确性和上下文。另一方面，人工智能代理可自主运行，为实现指定目标做出决策并采取行动。</p><p>代理 RAG 是一个将检索增强生成和代理推理的优势结合在一起的框架。它将 RAG 集成到代理的决策过程中，使系统能够动态地选择数据源，改进查询以获得更好的上下文检索，生成更准确的响应，并应用反馈循环来不断提高输出质量。</p><h2>代理 RAG 的主要特点</h2><p>代理 RAG 框架标志着传统 RAG 系统的重大进步。它不再遵循固定的检索流程，而是利用能够实时规划、执行和优化结果的动态代理。</p><p>让我们来看看代理 RAG 管道的一些主要特点：</p><ul><li><strong>动态决策</strong>：Agentic RAG 使用推理机制来理解用户的意图，并将每个查询路由到最相关的数据源，从而生成准确且能感知上下文的响应。</li><li><strong>全面的查询分析：</strong>Agentic RAG 深入分析用户查询，包括子问题及其总体意图。它能评估查询的复杂性，并动态选择最相关的数据源来检索信息，确保准确和完整的响应。</li><li><strong>多阶段协作</strong>：该框架通过专业代理网络实现多阶段协作。每个代理处理更大目标中的特定部分，依次或同时工作，以实现协调一致的结果。</li><li><strong>自我评估机制</strong>：代理式 RAG 管道利用自我反思来评估检索到的文档和生成的回复。它可以检查检索到的信息是否完全符合查询要求，然后审查输出信息的准确性、完整性和事实一致性。</li><li><strong>与外部工具集成</strong>：该工作流程可与外部应用程序接口、数据库和实时信息源交互，纳入最新信息并动态适应不断变化的数据。</li></ul><h2>代理 RAG 的工作流程模式</h2><p>工作流模式定义了代理人工智能如何以可靠、高效的方式构建、管理和协调基于 LLM 的应用程序。一些框架和平台，如<a href="https://www.langchain.com/"> LangChain</a> 、<a href="https://www.langchain.com/langgraph"> LangGraph</a> 、<a href="https://www.crewai.com/"> CrewAI</a> 和<a href="https://www.llamaindex.ai/"> LlamaIndex</a> ，可用于实现这些代理工作流。</p><ol><li><strong>顺序检索链</strong>：顺序工作流将复杂的任务划分为简单、有序的步骤。每一步都会改进下一步的输入，从而取得更好的结果。例如，在创建客户档案时，一名代理可能会从客户关系管理中获取基本信息，另一名代理可能会从交易数据库中检索购买历史记录，最后一名代理可能会将这些信息结合起来，生成一份完整的客户档案，用于推荐或报告。</li><li><strong>路由检索链</strong>：在这种工作流程模式中，路由器代理分析输入，并将其导向最合适的流程或数据源。当存在多个不同的数据源且重叠程度极低时，这种方法尤为有效。例如，在客户服务系统中，路由器代理会对收到的请求（如技术问题、退款或投诉）进行分类，并将其路由到相应的部门进行有效处理。</li><li><strong>并行检索链</strong>：在这种工作流程模式中，多个独立的子任务同时执行，然后将其输出汇总，生成最终响应。这种方法大大缩短了处理时间，提高了工作流程效率。例如，在客户服务并行工作流程中，一名代理检索过去的类似请求，另一名则查阅相关的知识库文章。然后，聚合器将这些输出合并起来，生成一份综合决议。</li><li><strong>Orchestrator 工作链</strong>：这种工作流程与并行化有相似之处，因为它利用了独立的子任务。然而，一个关键的区别在于集成了一个协调代理。该代理负责分析用户查询，在运行期间将查询动态地划分为子任务，并确定制定准确回复所需的适当流程或工具。</li></ol><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/ece6fc2403f211556c93e99d5227bfb7053b0c31-1600x1047.png" style="width: 100%;" /><h2>从零开始建立代理 RAG 管道</h2><p>为了说明代理 RAG 的原理，让我们使用 LangChain 和 Elasticsearch 设计一个工作流程。该工作流程采用基于路由的架构，多个代理协作分析查询、检索相关信息、评估结果并生成一致的回复。您可以参考这个<a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/agentic-rag/agent_rag_news_assistant.ipynb">Jupyter 笔记本</a>来学习这个示例。</p><p>工作流程从路由器代理开始，路由器代理分析用户的查询，选择最佳检索方法，即<code>vectorstore</code> 、<code>websearch</code> 或<code>composite</code> 方法。矢量存储处理传统的基于 RAG 的文档检索，网络搜索获取未存储在矢量存储中的最新信息，而复合方法则在需要来自多个来源的信息时将两者结合起来。</p><p>如果文件被认为合适，摘要代理就会生成清晰且与上下文相符的回复。但是，如果文档不足或不相关，查询重写代理就会重新制定查询，以改进搜索。修改后的查询会重新启动路由过程，使系统能够改进搜索并提高最终输出结果。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/ed8701a7f15558fbf2e967a884b3e770eccb826b-1256x1092.png" style="width: 100%;" /><h3>准备工作</h3><p>该工作流程依靠以下核心组件来有效执行示例：</p><ul><li>Python 3.10</li><li><a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/agentic-rag/agent_rag_news_assistant.ipynb">Jupyter 笔记本</a></li><li>Azure OpenAI</li><li>Elasticsearch</li><li>LangChain</li></ul><p>在继续之前，系统会提示您配置本例所需的以下环境变量。</p><pre><code>AZURE_OPENAI_ENDPOINT="Add your azure openai endpoint"
AZURE_OPENAI_KEY="Add your azure openai key"
AZURE_OPENAI_DEPLOYMENT="gpt-4.1"
AZURE_OPENAI_API_VERSION="Add your azure openai api version"

ES_ENDPOINT = "Add your Elasticsearch ENDPOINT"
ES_API_KEY = "Add your Elasticsearch API KEY"</code></pre><h3>数据来源</h3><p>本工作流程使用 AG 新闻数据集的一个子集进行说明。数据集包含不同类别的新闻文章，如国际、体育、商业和科学/技术。</p><pre><code>dataset = load_dataset("ag_news", split="train[:1000]")
docs = [
    Document(
        page_content=sample["text"],
        metadata={"category": sample["label"]}
    )
    for sample in dataset
]</code></pre><p>从<code>langchain_elasticsearch</code> 开始使用<a href="https://python.langchain.com/docs/integrations/vectorstores/elasticsearch/">ElasticsearchStore 模块</a>作为我们的向量存储。在检索方面，我们采用 Elastic 专有的嵌入模型<a href="https://www.elastic.co/docs/explore-analyze/machine-learning/nlp/ml-nlp-elser">ELSER</a>，实施 SparseVectorStrategy。在启动向量存储之前，必须确认 ELSER 模型已正确安装并部署到 Elasticsearch 环境中。</p><pre><code>elastic_vectorstore = ElasticsearchStore.from_documents(
    docs,
    es_url=ES_ENDPOINT,
    es_api_key=ES_API_KEY,
    index_name=index_name,
    strategy=SparseVectorStrategy(model_id=".elser_model_2"),
)

elastic_vectorstore.client.indices.refresh(index=index_name)</code></pre><p>网络搜索功能是利用 LangChain 社区工具中的<a href="https://python.langchain.com/api_reference/community/tools/langchain_community.tools.ddg_search.tool.DuckDuckGoSearchRun.html">DuckDuckGoSearchRun</a>实现的，它能让系统高效地从网上检索实时信息。您还可以考虑使用其他搜索 API，它们可能会提供更相关的结果。之所以选择该工具，是因为它无需 API 密钥即可进行搜索。</p><pre><code>duckduckgo = DuckDuckGoSearchRun(description= "A custom DuckDuckGo search tool for finding latest news stories.", verbose=True)
def websearch_retriever(query):
    results = duckduckgo.run(f"{query}")
    return results</code></pre><p>复合检索器专为需要结合多种来源的查询而设计。它通过同时检索网络上的实时数据和查询矢量存储中的历史新闻，提供全面、准确的响应。</p><pre><code>def composite_retriever(query):
    related_docs = vectorstore_retriever(query)
    related_docs += websearch_retriever(query)
    return related_docs</code></pre><h3>设置代理</h3><p>下一步，将定义 LLM 代理，以便在该工作流程中提供推理和决策能力。我们将创建的 LLM 链包括<code>router_chain</code>,<code>grade_docs_chain</code>,<code>rewrite_query_chain</code>, 和<code>summary_chain</code> 。</p><p>路由器代理使用 LLM 助手，在运行时为给定查询确定最合适的数据源。分级代理对检索到的文档进行相关性评估。如果文件被认为是相关的，它们就会被传递给摘要代理，以生成摘要。否则，重写查询代理会重新制定查询，并将其发送回路由过程，进行另一次检索尝试。您可以在<a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/agentic-rag/agent_rag_news_assistant.ipynb">笔记本</a>的 LLM chains 部分找到所有代理的说明。</p><pre><code>class RouteQuery(BaseModel):
    datasource: Literal["vectorstore", "websearch", "composite"] = Field(
        ...,
        description="Choose to route the query to web search, vectorstore or composite."
    )

router_prompt = ChatPromptTemplate.from_template("""You are an assistant that decides the best data source for questions based on news articles.
Choose one of the following options:
- 'vectorstore': for general, background, or historical news articles.
- 'websearch': for recent discoveries, 'latest', 'current', or '2025' type queries.
- 'composite': when the question needs both historical and current knowledge on news articles.

Question: {query}

Return one word: 'vectorstore', 'websearch', or 'composite'.
""")
router_structured = llm.with_structured_output(RouteQuery)
router_chain: RunnableSequence = router_prompt | router_structured</code></pre><p><code>llm.with_structured_output</code> 约束模型的输出，使其遵循<code>RouteQuery</code> 类下 BaseModel 定义的预定义模式，确保结果的一致性。第二行通过连接<code>router_prompt</code> 和<code>router_structured</code> 来组成<code>RunnableSequence</code> ，形成一个流水线，在这个流水线中，语言模型对输入提示进行处理，产生结构化的、符合模式的结果。</p><h3>定义图形节点</h3><p>这部分包括定义图形的状态，这些状态代表系统不同组件之间流动的数据。对这些状态的明确说明可确保工作流程中的每个节点都知道自己可以访问和更新哪些信息。</p><pre><code>class RAGState(TypedDict):
    query: str
    docs: List[Document]
    router: str
    summary: str
    self_reflection: bool
    retry_count: int = 0</code></pre><p>一旦定义了状态，下一步就是定义图的节点。节点就像图中的功能单元，可对数据执行特定操作。我们的管道中有 7 个不同的节点。</p><pre><code>def router(state: RAGState):
   router = router_chain.invoke({'query': state["query"]})
   logger.info(f"Router selected the datasource: {router.datasource}")
   logger.info(f"User query: {state['query']}")
   return {"router": router.datasource}

def vectorstore(state: RAGState):
   return {"docs": vectorstore_retriever(state["query"])}

def websearch(state: RAGState):
   return {"docs": websearch_retriever(state["query"])}

def composite(state: RAGState):
   return {"docs": composite_retriever(state["query"])}

def self_reflection(state: RAGState):
   evaluation = grade_docs_chain.invoke(
       {"query": state["query"], "docs": state["docs"]}
   )
   if evaluation.binary_score:
       logger.info(f"Self-reflection passed -- binary_score={evaluation.binary_score}")
   else:
       logger.info(f"Self-reflection failed -- binary_score={evaluation.binary_score}")

   return {
       "self_reflection": evaluation.binary_score,
   }

def query_rewriter(state: RAGState):
   retry_count = state.get("retry_count", 0) + 1
   new_query = rewrite_query_chain.invoke({"query": state["query"]})
   logger.info(f"Query rewritten: {new_query}, retry_count: {retry_count}")
   return {
       "query": new_query,
       "retry_count": retry_count,
   }

def summarize(state: RAGState):
   summary = summarize_chain.run(
       query=state["query"],
       docs=state["docs"],
   )
   return {"summary": summary}</code></pre><p><code>query_rewriter</code> 节点在工作流程中有两个作用。首先，当自我反思代理评估的文档被认为不充分或不相关时，它会使用<code>rewrite_query_chain</code> 重写用户查询，以改进检索。其次，它还可以作为一个计数器，跟踪查询被重写的次数。</p><p>每次调用节点时，都会递增存储在工作流状态中的<code>retry_count</code> 。这种机制可防止工作流程进入无限循环。如果<code>retry_count</code> 超过预定义的阈值，系统就会退回到错误状态、默认响应或您选择的任何其他预定义条件。</p><h3>编制图表</h3><p>最后一步是定义图的边，并在编译前添加必要的条件。每个图都必须从指定的起始节点开始，作为工作流程的入口点。图中的边代表节点之间的数据流，有两种类型：</p><ul><li>直边：它们定义了从一个节点到另一个节点的直接、无条件的流动。每当第一个节点完成任务后，工作流程就会自动沿直线进入下一个节点。</li><li>条件边：这些边允许工作流根据节点的当前状态或计算结果进行分支。下一个节点根据评估结果、路由决定或重试次数等条件动态选择。</li></ul><pre><code>graph.add_edge(START, "router")

def after_router(state: RAGState):
   route = state.get("router", None)
   if route == "vectorstore":
       return "vectorstore"
   elif route == "websearch":
       return "websearch"
   else:
       return "composite"

def after_self_reflection(state: RAGState):
   if state["self_reflection"]:
           return "summarize"
   return "query_rewriter"

def after_query_rewriter(state: RAGState):
   while state['retry_count'] <= 3:
           return "router"
   raise RuntimeError("Maximum retries (3) reached -- evaluation failed.")

graph.add_conditional_edges(
   "router",
   after_router,
   {
       "vectorstore": "vectorstore",
       "websearch": "websearch",
       "composite": "composite"
   }
)

graph.add_edge("vectorstore", "self_reflection")
graph.add_edge("websearch", "self_reflection")
graph.add_edge("composite", "self_reflection")
graph.add_conditional_edges(
   "self_reflection",
   after_self_reflection,
   {
       "summarize": "summarize",
       "query_rewriter": "query_rewriter"
   }
)
graph.add_conditional_edges("query_rewriter", after_query_rewriter, {"router": "router"})
graph.add_edge("summarize", END)
agent=graph.compile()</code></pre><p>这样，第一个代理 RAG 管道就准备就绪，可以使用编译后的代理进行测试了。</p><pre><code>result = agent.invoke({"query": query1})
logger.info(f"\nFinal Summary:\n: {result['summary']}")</code></pre><h3>测试代理 RAG 管道</h3><p>现在，我们将使用以下三种不同类型的查询对该管道进行测试。请注意，结果可能各不相同，下面的例子只是说明了一种可能的结果。</p><pre><code>query1="What are the latest AI models released this month?"
query2="What technological innovations are discussed in Sci/Tech news?"
query3="Compare a Sci/Tech article from the dataset with a current web article about AI trends."</code></pre><p>对于第一次查询，路由器选择<code>websearch</code> 作为数据源。如输出所示，该查询未通过自我反省评估，随后被重定向到查询重写阶段。</p><pre><code>INFO     | __main__:router:11 - Router selected the datasource: websearch
INFO     | __main__:router:12 - User query: What are the latest AI models released this month?
Latest Singapore news, including the city state's relationships with Malaysia and Mahathir, China and Xi Jinping, and the rest of Southeast Asia. 3 days ago · The latest military news, insights and analysis from China. All the latest news, opinions and analysis on Hong Kong, China, Asia and around the world Latest news, in-depth features and opinion on Malaysia, covering politics, economy, society and the Asean member-nation's relationships with China, Singapore, and other Southeast Asian ... Oct 12, 2025 · Brics (an acronym for Brazil, Russia, India, China and South Africa) refers to an association of 10 leading emerging markets. The other member states are Egypt, Ethiopia, ...
INFO     | __main__:self_reflection:31 - Self-reflection failed -- binary_score=False
INFO     | __main__:query_rewriter:40 - Query rewritten: query='Which AI models have been officially released in June 2024?', retry_count: 1
INFO     | __main__:router:11 - Router selected the datasource: websearch
INFO     | __main__:router:12 - User query: query='Which AI models have been officially released in June 2024?'
Dream Machine is a text-to-video model created by Luma Labs and launched in June 2024 . It generates video output based on user prompts or still images. Dream Machine has been noted for its ability to realistically capture motion... Released in June 2023. In June 2024 , Baidu announced Ernie 4.0 Turbo. In April 2025, Ernie 4.5 Turbo and X1 Turbo were released . These models are optimized for faster response times and lower operational costs.[28][29]. The meaning of QUERY is question, inquiry. How to use query in a sentence. Synonym Discussion of Query. QUERY definition: 1. a question, often expressing doubt about something or looking for an answer from an authority.... Learn more. Query definition: a question; an inquiry.. See examples of QUERY used in a sentence.
INFO     | __main__:self_reflection:29 - Self-reflection passed -- binary_score=True
INFO     | __main__:<module>:2 - 
Final Summary:
: In June 2024, two AI models were officially released: Dream Machine, a text-to-video model launched by Luma Labs, and Ernie 4.0 Turbo, announced by Baidu, which is optimized for faster response times and lower operational costs.</code></pre><p>接下来，我们以第二个查询为例，对使用<code>vectorstore</code> 检索的示例进行研究。</p><pre><code>INFO     | __main__:router:11 - Router selected the datasource: vectorstore
INFO     | __main__:router:12 - User query: What technological innovations are discussed in Sci/Tech news?
INFO     | __main__:self_reflection:29 - Self-reflection passed -- binary_score=True
INFO     | __main__:<module>:2 - 
Final Summary:
: Recent Sci/Tech news highlights several technological innovations: NASA is collaborating with Silicon Valley firms to build a powerful Linux-based supercomputer to support theoretical research and shuttle engineering; new chromatin transfer techniques have enabled the cloning of cats; cybersecurity advancements are being discussed in relation to protecting personal technology; Princeton University scientists assert that existing technologies can be used immediately to stabilize global warming; and a set of GameBoy micro-games has been recognized for innovation in game design.</code></pre><p>最后的查询被导向复合检索，它同时利用了矢量存储和网络搜索。</p><pre><code>INFO     | __main__:router:11 - Router selected the datasource: composite
INFO     | __main__:router:12 - User query: Compare a Sci/Tech article from the dataset with a current web article about AI trends.
Atlas currently only available on macOS, built on Chromium with planned features like ad-blocking still in development. OpenAI's Atlas browser launched with bold promises of AI -powered web browsing, but early real-world testing reveals a different story. Career-long data are updated to end-of-2024 and single recent year data pertain to citations received during calendar year 2024. The selection is based on the top 100,000 scientists by c-score (with and without self-citations) or a percentile rank of 2% or above in the sub-field. In this article I list 45 AI tools across 21 different categories. After exploring all the available options in each category, I've carefully selected the best tools based on my personal experience. Reading a complex technical article ? Simply highlight confusing terminology and ask "what's this?" to receive instant explanations. compare browsers. Comparison showing traditional browser navigation versus OpenAI Atlas AI -powered workflows. After putting Gemini, ChatGPT, Grok, and DeepSeek through rigorous testing in October 2025, it's clear that there isn't one AI that reigns supreme across all categories.
INFO     | __main__:self_reflection:29 - Self-reflection passed -- binary_score=True
INFO     | __main__:<module>:2 - 
Final Summary:
: A Sci/Tech article from the dataset highlights NASA's development of robust artificial intelligence software for planetary rovers, aiming to make them more self-reliant and capable of decision-making during missions. In contrast, a current web article about AI trends focuses on the proliferation of AI-powered tools across various categories, including browsers like OpenAI Atlas, and compares leading models such as Gemini, ChatGPT, Grok, and DeepSeek, noting that no single AI currently excels in all areas. While the NASA article emphasizes specialized AI applications for autonomous robotics in space exploration, the current trends article showcases the broadening impact of AI across consumer and professional technologies, with ongoing competition and rapid innovation among major AI platforms.</code></pre><p>在上述工作流程中，代理 RAG 可以在检索用户查询的信息时智能地确定使用哪个数据源，从而提高响应的准确性和相关性。您可以创建更多示例来测试代理，并查看输出结果是否产生了任何有趣的结果。</p><h2>构建代理 RAG 工作流程的最佳实践</h2><p>既然我们已经了解了代理 RAG 的工作原理，那么让我们来看看构建这些工作流程的一些最佳实践。遵循这些准则将有助于保持系统的效率和易于维护。</p><ul><li><strong>做好后备准备</strong>：提前规划后备策略，以应对工作流程中任何步骤出现故障的情况。这可能包括返回默认答案、触发错误状态或使用替代工具。这可确保系统从容应对故障，而不会破坏整体工作流程。</li><li><strong>实施全面的日志记录</strong>：尝试在工作流程的每个阶段实施日志记录，如重试、生成输出、路由选择和查询重写。这些日志有助于提高透明度，方便调试，并有助于随着时间的推移完善提示、代理行为和检索策略。</li><li><strong>选择合适的工作流程模式</strong>：检查您的使用案例，选择最适合您需求的工作流程模式。使用顺序工作流进行逐步推理，使用并行工作流处理独立数据源，使用协调器-工作器模式处理多工具或复杂查询。</li><li><strong>纳入评估战略</strong>：在工作流程的不同阶段纳入评估机制。这可以包括自我反思代理、对检索到的文件进行分级或自动质量检查。评估有助于验证检索到的文件是否相关、响应是否准确，以及复杂查询的所有部分是否都得到了处理。</li></ul><h2>挑战</h2><p>虽然代理 RAG 系统在适应性、精确性和动态推理方面具有显著优势，但它们在设计和实施阶段也面临着一些必须解决的挑战。一些主要挑战包括</p><ul><li><strong>复杂的工作流程</strong>：随着代理和决策点的增加，整个工作流程会变得越来越复杂。这可能导致运行时出现错误或故障的几率增加。在可能的情况下，消除多余的代理和不必要的决策点，优先简化工作流程。</li><li><strong>可扩展性</strong>：要扩展代理 RAG 系统以处理大型数据集和高查询量，可能具有挑战性。采用高效的索引、缓存和分布式处理策略，以保持大规模性能。</li><li><strong>协调和计算开销</strong>：使用多个代理执行工作流需要高级协调。这包括谨慎的调度、依赖管理和代理协调，以防止出现瓶颈和冲突，所有这些都会增加整个系统的复杂性。</li><li><strong>评估的复杂性</strong>：对这些工作流程进行评估本身就存在挑战，因为每个阶段都需要不同的评估策略。例如，RAG 阶段必须对检索文件的相关性和完整性进行评估，而生成的摘要则需要检查其质量和准确性。同样，查询重写的有效性也需要一个单独的评估逻辑，以确定重写后的查询是否改善了检索结果。</li></ul><h2>结论</h2><p>在这篇博文中，我们介绍了代理 RAG 的概念，并强调了它如何通过结合代理人工智能的自主能力来增强传统的 RAG 框架。我们探索了代理 RAG 的核心功能，并通过一个实践案例演示了这些功能，即使用 Elasticsearch 作为向量存储和 LangChain 创建代理框架来构建一个新闻助手。</p><p>此外，我们还讨论了在设计和实施代理 RAG 管道时需要考虑的最佳实践和主要挑战。这些见解旨在指导开发人员创建稳健、可扩展和高效的代理系统，将检索、推理和决策有效地结合起来。</p><h2>未来发展</h2><p>我们建立的工作流程非常简单，为改进和实验留下了足够的空间。我们可以通过尝试各种嵌入模型和改进检索策略来加强这一点。此外，集成一个重新排序代理来确定检索文件的优先次序也是有益的。另一个探索领域涉及为代理框架制定评估战略，特别是确定适用于不同类型框架的通用和可重复使用的方法。最后，在大型和更复杂的数据集上试验这些框架。</p><p>与此同时，如果您也有类似的实验，欢迎与我们分享！欢迎提供反馈意见，或通过我们的<a href="https://ela.st/slack">社区 Slack 频道</a>或<a href="https://discuss.elastic.co/c/security">论坛</a>与我们联系。</p><h2>资源</h2><ul><li><a href="https://arxiv.org/abs/2310.11511">Self-RAG：通过自我反思学会检索、生成和批判</a></li><li><a href="https://arxiv.org/abs/2501.09136">代理检索-增强生成：关于代理 RAG 的调查</a></li></ul>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[混合搜索不头疼：用检索器简化混合搜索]]></title>
            <description><![CDATA[探索如何利用线性和 RRF 检索器的多字段查询格式简化 Elasticsearch 中的混合搜索，并在不了解 Elasticsearch 索引的情况下创建查询。]]></description>
            <link>https://www.elastic.co/search-labs/cn/blog/hybrid-search-multi-field-query-retrievers-elasticsearch</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/cn/blog/hybrid-search-multi-field-query-retrievers-elasticsearch</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Mike Pellegrini]]></dc:creator>
            <pubDate>Thu, 27 Nov 2025 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/4494f98ae3958bbdbc6171df9677fc4d65ec5640-1536x1024.png?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p><a href="https://www.elastic.co/what-is/hybrid-search">混合搜索</a>被公认为是一种强大的搜索方法，它将<a href="https://www.elastic.co/search-labs/blog/lexical-and-semantic-search-with-elasticsearch#lexical-search---sparse-retrieval">词法搜索</a>的精确性和速度与<a href="https://www.elastic.co/what-is/semantic-search">语义搜索</a>的自然语言能力结合在一起。不过，在实际应用中可能会很棘手，往往需要对索引有深入的了解，并通过非繁琐的配置来构建冗长的查询。在本博客中，我们将探讨<a href="https://www.elastic.co/docs/reference/elasticsearch/rest-apis/retrievers#multi-field-query-format">线性和 RRF 检索器的多字段查询格式</a>如何使混合搜索变得更简单、更易用，从而消除常见的头痛问题，让您更轻松地充分利用其强大功能。我们还将回顾多字段查询格式如何使您在不了解索引的情况下执行混合搜索查询。</p><h2>分数范围问题</h2><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/c8b49153c47d2cdc233c0d2e440db04711d48ca5-1600x1600.jpg" style="width: 100%;" /><p>首先，让我们回顾一下混合搜索困难的主要原因之一：不同的分数范围。我们的老朋友<a href="https://www.elastic.co/elasticon/conf/2016/sf/improved-text-scoring-with-bm25">BM25</a>会产生无限制的分数。换句话说，BM25 可以生成从接近 0 到（理论上）无穷大的分数。与此相反，针对<code>dense_vector</code> 字段的查询会产生介于 0 和 1 之间的分数。由于<code>semantic_text</code> 混淆了用于索引嵌入的字段类型，因此除非您对索引和推理端点配置有详细了解，否则很难说清查询的分数范围。这在试图交错使用词汇和语义搜索结果时会带来问题，因为即使语义结果更相关，词汇结果也可能优先于语义结果。对于这个问题，普遍接受的解决方案是在交织结果之前对分数进行归一化处理。Elasticsearch 为此提供了两种工具：<a href="https://www.elastic.co/docs/reference/elasticsearch/rest-apis/retrievers/linear-retriever">线性</a>检索器和<a href="https://www.elastic.co/docs/reference/elasticsearch/rest-apis/retrievers/rrf-retriever">RRF</a>检索器。<br/></p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/264481268c8b6ac259e3c257b85431b513f16672-1077x586.png" style="width: 100%;" /><p><strong>RRF</strong>检索器采用<a href="https://www.elastic.co/docs/reference/elasticsearch/rest-apis/reciprocal-rank-fusion">RRF 算法</a>，将文档排名作为衡量相关性的标准，并舍弃分数。由于不考虑分数，因此分数范围不匹配不是问题。</p><p><strong>线性</strong>检索器使用线性组合来确定文档的最终得分。这包括获取文档中每个组件查询的得分，对其进行归一化处理，然后求和生成总分。在数学上，这一操作可以表示为</p><pre><code>Total Score = 𝚺(N(Sx))</code></pre><p>其中<code>N</code> 是归一化函数，SX 是查询 X 的得分。归一化功能在这里非常关键，因为它将每个查询的得分转换为使用相同的范围。您可以<a href="https://www.elastic.co/search-labs/blog/linear-retriever-hybrid-search">在这里</a>了解有关线性寻回猎犬的更多信息。</p><h2>分解</h2><p>用户可以利用这些工具实现有效的混合搜索，但需要对索引有一定的了解。让我们看一个使用线性检索器的示例，在这个示例中，我们将查询一个包含两个字段的索引：</p><pre><code>PUT linear_retriever_example
{
  "mappings": {
    "properties": {
      "semantic_text_field": { <1>
        "type": "semantic_text",
        "inference_id": ".multilingual-e5-small-elasticsearch"
      },
      "text_field": { <2>
        "type": "text"
      }
    }
  }
}</code></pre><p>1.<code>semantic_text_field</code> 是一个<code>semantic_text</code> 字段，使用文本嵌入模型<a href="https://www.elastic.co/docs/explore-analyze/machine-learning/nlp/ml-nlp-e5">E5</a></p><p><code>text_field</code> 是一个标准的<code>text</code> 字段</p><pre><code>GET linear_retriever_example/_search
{
  "retriever": {
    "linear": {
      "retrievers": [
        {
          "retriever": {
            "standard": {
              "query": {
                "match": { <1>
                  "semantic_text_field": "foo"
                }
              }
            }
          },
          "normalizer": "minmax"
        },
        {
          "retriever": {
            "standard": {
              "query": {
                "match": {
                  "text_field": "foo"
                }
              }
            }
          },
          "normalizer": "minmax"
        }
      ]
    }
  }
}</code></pre><p>1.我们在 字段上使用 查询，我们<a href="https://www.elastic.co/search-labs/blog/semantic-search-match-knn-sparse-vector#we-made-match-happen-in-semantic-search!"> 在 Elasticsearch 8.18/9.0</a> 中添加了对<code>match</code> 该查询的 支持<code>semantic_text</code></p><p><br/>在构建查询时，我们需要牢记<code>semantic_text_field</code> 使用的是文本嵌入模型，因此对它的任何查询都会产生 0 到 1 之间的分数。我们还需要知道<code>text_field</code> 是一个标准的<code>text</code> 字段，因此对它的查询将产生一个无限制的分数。为了创建具有适当相关性的结果集，我们需要使用一种检索器，在合并查询得分之前将其归一化。在本例中，我们使用了<code>minmax</code> 归一化的线性检索器，它将每个查询的得分归一化为介于 0 和 1 之间的值。</p><p>本例中的查询结构相当简单，因为只涉及两个字段。然而，随着字段的增加和类型的变化，它很快就会变得复杂。这表明，要编写有效的混合搜索查询，往往需要对所查询的索引有更深入的了解，这样才能在组合之前对组件查询得分进行适当的归一化处理。这对混合搜索的广泛应用构成了障碍。</p><h3>查询分组</h3><p>让我们扩展一下示例：如果我们想查询一个<code>text</code> 字段和两个<code>semantic_text</code> 字段，该怎么办？我们可以构建这样一个查询：</p><pre><code>GET linear_retriever_example/_search
{
  "retriever": {
    "linear": {
      "retrievers": [
        {
          "retriever": {
            "standard": {
              "query": {
                "semantic": {
                  "field": "semantic_text_field_1",
                  "query": "foo"
                }
              }
            }
          },
          "normalizer": "minmax"
        },
        {
          "retriever": {
            "standard": {
              "query": {
                "semantic": {
                  "field": "semantic_text_field_2",
                  "query": "foo"
                }
              }
            }
          },
          "normalizer": "minmax"
        },
        {
          "retriever": {
            "standard": {
              "query": {
                "match": {
                  "text_field": "foo"
                }
              }
            }
          },
          "normalizer": "minmax"
        }
      ]
    }
  }
}</code></pre><p>这表面上看起来不错，但也有潜在的问题。现在，<code>semantic_text</code> 场比赛占总分的⅔：</p><pre><code>Total Score = N(semantic_text_field_1 score) + N(semantic_text_field_2 score) + N(text_field score)</code></pre><p>这可能不是你想要的结果，因为这会造成分数不平衡。在只有 3 个字段的示例中，这种影响可能并不明显，但如果查询的字段较多，就会出现问题。例如，大多数索引包含的词法字段远远多于语义字段（即<code>dense_vector</code>,<code>sparse_vector</code>, 或<code>semantic_text</code>) 。如果我们使用上述模式查询一个包含 9 个词法字段和 1 个语义字段的索引呢？词性匹配将占得分的 90% ，从而削弱语义搜索的有效性。</p><p>解决这一问题的常用方法是将查询分为词汇和语义两个类别，并对两者进行平均加权。这就避免了任一类别在总分中占主导地位。</p><p>让我们付诸实践。在使用线性检索器时，本例中的分组查询方法会是怎样的？</p><pre><code>GET linear_retriever_example/_search
{
  "retriever": {
    "linear": {
      "retrievers": [
        {
          "retriever": {
            "linear": {
              "retrievers": [
                {
                  "retriever": {
                    "standard": {
                      "query": {
                        "semantic": {
                          "field": "semantic_text_field_1",
                          "query": "foo"
                        }
                      }
                    }
                  },
                  "normalizer": "minmax"
                },
                {
                  "retriever": {
                    "standard": {
                      "query": {
                        "semantic": {
                          "field": "semantic_text_field_2",
                          "query": "foo"
                        }
                      }
                    }
                  },
                  "normalizer": "minmax"
                }
              ]
            }
          },
          "normalizer": "minmax"
        },
        {
          "retriever": {
            "standard": {
              "query": {
                "match": {
                  "text_field": "foo"
                }
              }
            }
          },
          "normalizer": "minmax"
        }
      ]
    }
  }
}</code></pre><p>哇，真是啰嗦！您甚至可能需要上下滚动多次才能查看整个查询！在这里，我们使用两级标准化来创建查询组。数学上可以表示为</p><pre><code>Total Score = N(N(semantic_text_field_1 score) + N(semantic_text_field_2 score)) + N(text_field score)</code></pre><p>这第二级规范化可确保<code>semantic_text</code> 字段和<code>text</code> 字段的查询权重均匀。请注意，在本例中，我们省略了<code>text_field</code> 的二级规范化，因为只有一个词法字段，这样可以避免<em>更多的</em>繁琐。</p><p>这种查询结构已经很笨重了，而且我们只查询三个字段。随着查询字段的增多，即使是经验丰富的搜索从业人员也越来越难以驾驭。</p><h2>多字段查询格式</h2><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/dd04e1686076a574b717c1460acfe4eb79299208-1600x1600.jpg" style="width: 100%;" /><p>我们在 Elasticsearch 8.19、9.1 和 无服务器 中为线性和 RRF 检索器添加了<a href="https://www.elastic.co/cloud/serverless"> </a><a href="https://www.elastic.co/docs/reference/elasticsearch/rest-apis/retrievers#multi-field-query-format">多字段查询格式</a> ，以简化所有这些操作。现在，您只需使用&quot;...... &quot;即可执行与上述相同的查询：</p><pre><code>GET linear_retriever_example/_search
{
  "retriever": {
    "linear": {
      "fields": [ "semantic_text_field_1", "semantic_text_field_2", "text_field" ],
      "query": "foo",
      "normalizer": "minmax"
    }
  }
}</code></pre><p>这将查询从 55 行缩减到 9 行！Elasticsearch 自动将索引映射用于</p><ul><li>确定每个查询字段的类型</li><li>将每个字段归入一个词汇或语义类别</li><li>在最终得分中平均分配每个类别的权重</li></ul><p>这样，任何人都可以执行有效的混合搜索查询，而无需了解有关索引或所用推理端点的详细信息。</p><p>使用 RRF 时，可以省略<code>normalizer</code> ，因为排名是相关性的代表：</p><pre><code>GET rrf_retriever_example/_search
{
  "retriever": {
    "rrf": {
      "fields": [ "semantic_text_field_1", "semantic_text_field_2", "text_field" ],
      "query": "foo"
    }
  }
}</code></pre><h2>每场增强</h2><p>在使用线性检索器时，您可以应用每个字段增强功能来调整某些字段中匹配的重要性。例如，假设您要查询四个字段：两个<code>semantic_text</code> 字段和两个<code>text</code> 字段：</p><pre><code>GET linear_retriever_example/_search
{
  "retriever": {
    "linear": {
      "fields": [ "semantic_text_field_1", "semantic_text_field_2", "text_field_1", "text_field_2" ],
      "query": "foo",
      "normalizer": "minmax"
    }
  }
}</code></pre><p>默认情况下，每个字段在其组（词法或语义）中的权重相同。比分细目如下</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/ba31c76189e3a1e5b1638437ccf0528aafec2598-1600x549.png" style="width: 100%;" /><p>换句话说，每个字段占总分的 25% 。</p><p>我们可以使用<code>field^boost</code> 语法为任何字段添加每个字段的提升。让我们将<code>semantic_text_field_1</code> 和<code>text_field_1</code> 提升 2：</p><pre><code>GET linear_retriever_example/_search
{
  "retriever": {
    "linear": {
      "fields": [ "semantic_text_field_1^2", "semantic_text_field_2", "text_field_1^2", "text_field_2" ]
      "query": "foo",
      "normalizer": "minmax"
    }
  }
}</code></pre><p>现在的比分是这样的</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/de20e555d52f914bf483a048d056f54f4fece757-1600x549.png" style="width: 100%;" /><p>每个查询组的权重仍然相同，但组内字段的权重发生了变化：</p><ul><li><code>semantic_text_field_1</code> 是语义查询组得分的 66% ，是总分的 33% </li><li><code>text_field_1</code> 是词法查询组得分的 66% ，是总分的 33% </li></ul>
            <table style="border-collapse: collapse; margin: 20px 0;">
              <tbody>
                
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">ℹ️ 请注意，在按字段提升时，总分范围不会改变。这是分数标准化的预期副作用，可确保词法和语义查询分数保持直接可比性。</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">ℹ️ 在 Elasticsearch 9.2+ 中，每个字段的提升也可与 RRF 检索器一起使用</td>
                    </tr>
              </tbody>
            </table>
          <h3>通配符分辨率</h3><p>您可以在<code>fields</code> 参数中使用<code>*</code> 通配符来匹配多个字段。继续上面的例子，这个查询在功能上等同于明确查询<code>emantic_text_field_1</code>,<code>semantic_text_field_2</code>, 和<code>text_field_1</code> ：</p><pre><code>GET linear_retriever_example/_search
{
  "retriever": {
    "linear": {
      "fields": [ "semantic_text_field_*", "*_field_1" ],
      "query": "foo",
      "normalizer": "minmax"
    }
  }
}</code></pre><p>值得注意的是，<code>*_field_1</code> 模式同时匹配<code>text_field_1</code> 和<code>semantic_text_field_1</code> 。查询将自动执行，就像明确查询每个字段一样。<code>semantic_text_field_1</code> 同时匹配两种模式也没有问题；在执行查询之前，所有匹配的字段名称都会被去除重复。</p><p>您可以通过多种方式使用通配符：</p><ul><li>前缀匹配（例如：<code>*_text_field</code>)</li><li>内联匹配 (ex:<code>semantic_*_field</code>)</li><li>后缀匹配（例如：<code>semantic_text_field_*</code>)</li></ul><p>您还可以使用多个通配符来应用上述组合，例如<code>*_text_field_*</code> 。</p><h3>默认查询字段</h3><p>多字段查询格式还允许您查询您一无所知的索引。如果省略<code>fields</code> 参数，它将查询由<a href="https://www.elastic.co/docs/reference/elasticsearch/index-settings/index-modules">index.query.default_field 索引设置</a>指定的所有字段：</p><pre><code>GET linear_retriever_example/_search
{
  "retriever": {
    "linear": {
      "query": "foo",
      "normalizer": "minmax"
    }
  }
}</code></pre><p>默认情况下，<code>index.query.default_field</code> 设置为<code>*</code> 。该通配符将解析索引中支持术语查询的所有字段类型，其中大多数字段类型都支持术语查询。例外情况是</p><ul><li><code>dense_vector</code> 领域</li><li><code>rank_vector</code> 领域</li><li>几何领域：<code>geo_point</code>, <code>shape</code></li></ul><p>当你想在第三方提供的索引上执行混合搜索查询时，该功能尤其有用。多字段查询格式可让您以简单的方式执行适当的查询。只需排除<code>fields</code> 参数，就能查询所有适用字段。</p><h2>结论</h2><p>分数范围问题会让有效的混合搜索实施起来很头疼，尤其是在对所查询的索引或所使用的推理端点了解有限的情况下。线性和 RRF 检索器的多字段查询格式将基于查询分组的自动混合搜索方法打包到简单易用的应用程序接口中，从而减轻了这种痛苦。附加功能（如按字段增强、通配符解析和默认查询字段）扩展了功能，涵盖了多种使用情况。</p><h2>立即试用多字段查询格式</h2><p>您可以通过<a href="https://www.elastic.co/docs/deploy-manage/deploy/elastic-cloud/create-serverless-project"> 免费试用</a> ，在完全托管的 Elasticsearch<a href="https://www.elastic.co/cloud/serverless"> Serverless 项目中使用多字段查询格式检查线性检索器和</a> RRF 检索器。它还提供从 8.19&amp; 9.1 开始的堆栈版本。</p><p>只需一条命令，几分钟即可在本地环境中开始使用：</p><pre><code>curl -fsSL https://elastic.co/start-local | sh</code></pre><p></p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[利用弹性代理生成器和 GPT-OSS 构建人力资源人工智能代理]]></title>
            <description><![CDATA[了解如何使用 Elastic Agent Builder 和 GPT-OSS 构建一个人工智能代理，回答有关员工人力资源数据的自然语言查询。]]></description>
            <link>https://www.elastic.co/search-labs/cn/blog/build-an-ai-agent-hr-elastic-agent-builder-gpt-oss</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/cn/blog/build-an-ai-agent-hr-elastic-agent-builder-gpt-oss</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Tomás Murúa]]></dc:creator>
            <pubDate>Wed, 26 Nov 2025 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/05d2d0513fff67d975f9223d75108aa9f50646bc-1600x914.png?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<h2>引言</h2><p>本文将向您展示如何使用<a href="https://openai.com/index/introducing-gpt-oss/">GPT-OSS</a>和 Elastic Agent Builder 为人力资源部门构建人工智能代理。代理可以回答你的问题，而无需向 OpenAI、Anthropic 或任何外部服务发送数据。</p><p>我们将使用 LM Studio 在本地为 GPT-OSS 提供服务，并将其连接到 Elastic Agent Builder。</p><p>本文结束时，您将拥有一个定制的人工智能代理，可以回答有关员工数据的自然语言问题，同时保持对信息和模型的完全控制。</p><h2>准备工作</h2><p>这篇文章需要</p><ul><li><a href="https://www.elastic.co/cloud">弹性云</a>托管 9.2，无服务器或<a href="https://www.elastic.co/docs/deploy-manage/deploy/self-managed/local-development-installation-quickstart">本地</a>部署</li><li>建议使用 32GB 内存的机器（GPT-OSS 20B 最低 16GB 内存）</li><li>已安装<a href="https://lmstudio.ai/">LM 工作室</a></li><li>已安装<a href="https://www.docker.com/products/docker-desktop/">Docker 桌面</a></li></ul><h2>为什么使用 GPT-OSS？</h2><p>有了本地 LLM，您就可以将其部署到自己的基础设施中，并根据自己的需求进行微调。当然，您也不必向外部供应商支付许可费。</p><p>作为对开放模型生态系统承诺的一部分，OpenAI 于 2025 年 8 月 5 日<a href="https://openai.com/index/introducing-gpt-oss/">发布了 GPT-OSS</a>。</p><p>20B 参数模型提供</p><ul><li><strong>工具使用能力</strong></li><li><strong>高效推理</strong></li><li><strong>兼容 OpenAI SDK</strong></li><li><strong>与代理工作流程兼容</strong></li></ul><p>基准比较：</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/29160e3345352088e8213297630882f252b00c47-1600x680.png" style="width: 100%;" /><h2>解决方案架构</h2><p>该架构完全在本地计算机上运行。Elastic（在 Docker 中运行）通过 LM Studio 与本地 LLM 直接通信，Elastic Agent Builder 利用这种连接创建可查询员工数据的自定义人工智能代理。</p><p>有关详细信息，请参阅本<a href="https://www.elastic.co/docs/solutions/observability/connect-to-own-local-llm">文档</a>。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/a4a886750ff25fa8bb7aefc7448161e52cf73ed3-1600x896.png" style="width: 100%;" /><h2>为人力资源部门建立人工智能代理：步骤</h2><p>我们将把实施分为 5 个步骤：</p><ol><li>使用本地模型配置 LM 工作室</li><li>使用 Docker 部署本地弹性</li><li>在 Elastic 中创建 OpenAI 连接器</li><li>将员工数据上传到 Elasticsearch</li><li>构建并测试人工智能代理</li></ol><h2>步骤 1：使用 GPT-OSS 20B 配置 LM Studio</h2><p>LM Studio 是一款用户友好型应用程序，可让您在本地计算机上运行大型语言模型。它提供了与 OpenAI 兼容的 API 服务器，无需复杂的设置过程即可轻松与 Elastic 等工具集成。有关详细信息，请参阅<a href="https://lmstudio.ai/docs/app">LM Studio 文档</a>。</p><p>首先，从官方网站下载并安装<span class="unknown__pt__mark__51566e628851">LM Studio</span>。安装完成后，打开应用程序。</p><h3>在 LM Studio 界面：</h3><ol><li>转到搜索选项卡，搜索 &quot;GPT-OSS</li><li>从 OpenAI 选择<code>openai/gpt-oss-20b</code> </li><li>点击下载</li></ol><p>该模型的大小约为<strong>12.10GB</strong>。下载可能需要几分钟时间，具体取决于您的网络连接。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/5d01bc4dcb377b5259fc6b521fe2425a31b90ca4-1312x872.png" style="width: 100%;" /><h4>下载模型后</h4><ol><li>转到本地服务器选项卡</li><li>选择 openai/gpt-oss-20b</li><li>使用默认端口 1234</li><li>在右侧面板上，转到 &quot;<strong>加载 </strong>&quot;，将上下文长度设置为<strong>40K</strong>或更高</li></ol><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/e546033f916381647b876815b2c1f1ae2a08365f-326x337.png" style="width: 100%;" /><p>5.单击启动服务器</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/28ee78a3caa84d14e04db3d42f30acbe4d4d005a-1312x872.png" style="width: 100%;" /><p>如果服务器正在运行，您应该会看到这个提示。</p><pre><code>[LM STUDIO SERVER] Success! HTTP server listening on port 1234
[LM STUDIO SERVER] Supported endpoints:
[LM STUDIO SERVER] ->	GET  http://localhost:1234/v1/models
[LM STUDIO SERVER] ->	POST http://localhost:1234/v1/responses
[LM STUDIO SERVER] ->	POST http://localhost:1234/v1/chat/completions
[LM STUDIO SERVER] ->	POST http://localhost:1234/v1/completions
[LM STUDIO SERVER] ->	POST http://localhost:1234/v1/embeddings
Server started.</code></pre><h2>第 2 步：使用 Docker 部署本地弹性</h2><p>现在，我们将使用 Docker 在本地设置 Elasticsearch 和 Kibana。Elastic 提供了一个方便的脚本来处理整个设置过程。更多详情，请参阅<a href="https://www.elastic.co/docs/deploy-manage/deploy/self-managed/local-development-installation-quickstart">官方文档</a>。</p><h3>运行启动本地脚本</h3><p>在终端中执行以下命令</p><pre><code>curl -fsSL https://elastic.co/start-local | sh</code></pre><p>该脚本将</p><ul><li>下载并配置 Elasticsearch 和 Kibana</li><li>使用 Docker Compose 启动两个服务</li><li>自动激活 30 天白金试用版许可证</li></ul><h3>预期产出</h3><p>只需等待以下信息并保存显示的密码和 API 密钥；访问 Kibana 时需要它们：</p><pre><code>🎉 Congrats, Elasticsearch and Kibana are installed and running in Docker!
🌐 Open your browser at http://localhost:5601
   Username: elastic
   Password: KSUlOMNr
🔌 Elasticsearch API endpoint: http://localhost:9200
🔑 API key: cnJGX0pwb0JhOG00cmNJVklUNXg6cnNJdXZWMnM4bncwMllpQlFlUTlWdw==
Learn more at https://github.com/elastic/start-local</code></pre><h3>访问 Kibana</h3><p>打开浏览器并导航至</p><pre><code>http://localhost:5601</code></pre><p>使用终端输出中获得的证书登录。</p><h3>启用代理生成器</h3><p>登录 Kibana 后，导航至<strong>管理 </strong>&gt;<strong> AI </strong>&gt;<strong> Agent Builder </strong>并激活 Agent Builder。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/92e104cb846c20d875865ded8a3d37f5c7daae9b-1491x1528.png" style="width: 100%;" /><h2>第 3 步：在 Elastic 中创建 OpenAI 连接器</h2><p>现在，我们将配置 Elastic 以使用本地 LLM。</p><h3>接入连接器</h3><ol><li>在 Kibana 中</li><li>转到<strong>项目设置</strong> &gt; <strong>管理</strong></li><li>在<strong>&quot;警报和洞察 &quot;</strong>下，选择 &quot;<strong>连接器</strong></li><li>单击创建连接器</li></ol><h3>配置连接器</h3><p>从连接器列表中选择<strong>OpenAI</strong>。LM Studio 使用 OpenAI SDK，因此与 OpenAI 兼容。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/5ac87042e086c7a2bd47a8039e646ec831f0dcc6-923x974.png" style="width: 100%;" /><p>用这些值填写字段：</p><ul><li><strong>连接器名称： </strong>LM Studio - GPT-OSS 20B</li><li><strong>选择 OpenAI 提供商： </strong>其他（OpenAI 兼容服务）</li><li><strong>URL： </strong><code>http://host.docker.internal:1234/v1/chat/completions</code></li><li><strong>默认型号： </strong>openai/gpt-oss-20b</li><li><strong>API 密钥：</strong>testkey-123（任何文本都可以，因为 LM Studio 服务器不要求验证。）</li></ul><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/2084ac32fcf1fb810c8b54ecab1c85a1e3e8905b-672x1302.png" style="width: 100%;" /><p>要完成配置，请单击<strong>保存&amp; 测试</strong>。</p><p><strong>重要：</strong>打开 &quot;<strong>启用本地函数调用</strong>&quot;；这是使代理生成器正常工作的必要条件。如果不启用，就会出现<strong><code>No tool calls found in the response</code></strong> 错误。</p><h3>测试连接</h3><p>Elastic 会自动测试连接。如果一切配置正确，您将看到如下成功信息：</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/f567d767f1969c4730c1daa92f651789dc3742ac-1042x812.png" style="width: 100%;" /><p>响应：</p><pre><code>{
  "status": "ok",
  "data": {
    "id": "chatcmpl-flj9h0hy4wcx4bfson00an",
    "object": "chat.completion",
    "created": 1761189456,
    "model": "openai/gpt-oss-20b",
    "choices": [
      {
        "index": 0,
        "message": {
          "role": "assistant",
          "content": "Hello! 👋 How can I assist you today?",
          "reasoning": "Just greet.",
          "tool_calls": []
        },
        "logprobs": null,
        "finish_reason": "stop"
      }
    ],
    "usage": {
      "prompt_tokens": 69,
      "completion_tokens": 23,
      "total_tokens": 92
    },
    "stats": {},
    "system_fingerprint": "openai/gpt-oss-20b"
  },
  "actionId": "ee1c3aaf-bad0-4ada-8149-118f52dad757"
}</code></pre><h2>第 4 步：将员工数据上传到 Elasticsearch</h2><p>现在，我们将上传<a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/gpt-oss-with-elasticsearch/hr-employees-bulk.json">人力资源员工数据集</a>，以演示代理如何处理敏感数据。我用这种结构生成了一个虚构的数据集。</p><h3>数据集结构</h3><pre><code>{
  "employee_id": "0f4dce68-2a09-4cb1-b2af-6bcb4821539b",
  "full_name": "Daffi Stiebler",
  "email": "lscutchings0@huffingtonpost.com",
  "date_of_birth": "1975-06-20T15:39:36Z",
  "hire_date": "2025-07-28T00:10:45Z",
  "job_title": "Physical Therapy Assistant",
  "department": "HR",
  "salary": "108455",
  "performance_rating": "Needs Improvement",
  "years_of_experience": 2,
  "skills": "Java",
  "education_level": "Master's Degree",
  "manager": "Carl MacGibbon",
  "emergency_contact": "Leigha Scutchings",
  "home_address": "5571 6th Park"
}</code></pre><h3>使用映射创建索引</h3><p>首先，创建具有适当映射的索引。请注意，我们对一些关键字段使用了<a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/semantic-text">semantic_text 字段</a>；这样就能为我们的索引提供语义搜索功能。</p><pre><code>​​PUT hr-employees
{
  "mappings": {
    "properties": {
      "@timestamp": {
        "type": "date"
      },
      "employee_id": {
        "type": "keyword"
      },
      "full_name": {
        "type": "text",
        "copy_to": "employee_semantic"
      },
      "email": {
        "type": "keyword"
      },
      "date_of_birth": {
        "type": "date",
        "format": "iso8601"
      },
      "hire_date": {
        "type": "date",
        "format": "iso8601"
      },
      "job_title": {
        "type": "text",
        "copy_to": "employee_semantic"
      },
      "department": {
        "type": "text",
        "copy_to": "employee_semantic"
      },
      "salary": {
        "type": "double"
      },
      "performance_rating": {
        "type": "text",
        "copy_to": "employee_semantic"
      },
      "years_of_experience": {
        "type": "long"
      },
      "skills": {
        "type": "text",
        "copy_to": "employee_semantic"
      },
      "education_level": {
        "type": "text",
        "copy_to": "employee_semantic"
      },
      "manager": {
        "type": "text",
        "copy_to": "employee_semantic"
      },
      "emergency_contact": {
        "type": "keyword"
      },
      "home_address": {
        "type": "keyword"
      },
      "employee_semantic": {
        "type": "semantic_text"
      }
    }
  }
}</code></pre><h3>使用批量 API 索引</h3><p>将<a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/gpt-oss-with-elasticsearch/hr-employees-bulk.json">数据集</a>复制并粘贴到 Kibana 的 Dev Tools 中并执行：</p><pre><code>POST hr-employees/_bulk
{"index": {}}
{"employee_id": "57728b91-e5d7-4fa8-954a-2384040d3886", "full_name": "Filide Gane", "email": "vhallahan1@booking.com", "job_title": "Business Systems Development Analyst", "department": "Marketing", "salary": "$52330.27", "performance_rating": "Meets Expectations", "years_of_experience": 12, "skills": "Java", "education_level": "Bachelor's Degree", "date_of_birth": "2000-02-07T16:49:32Z", "hire_date": "2023-11-07T13:03:16Z", "manager": "Freedman Kings", "emergency_contact": "Vilhelmina Hallahan", "home_address": "75 Dennis Junction"}
{"index": {}}
{"employee_id": "...", ...}</code></pre><h3>验证数据</h3><p>运行查询进行验证：</p><pre><code>GET hr-employees/_search</code></pre><h2>第 5 步：构建并测试人工智能代理</h2><p>一切配置完成后，就可以使用 Elastic Agent Builder 创建自定义人工智能代理了。有关详细信息，请参阅<a href="https://www.elastic.co/docs/solutions/search/agent-builder/get-started">Elastic 文档</a>。</p><h3>添加连接器</h3><p>在创建新代理之前，我们必须将代理生成器设置为使用名为<code>LM Studio - GPT-OSS 20B</code> 的自定义连接器，因为默认连接器是<a href="https://www.elastic.co/docs/reference/kibana/connectors-kibana/elastic-managed-llm">Elastic Managed LLM</a>。为此，我们需要进入 &quot;<strong>项目设置</strong>&quot;&gt; <strong>&quot;管理</strong>&quot;&gt; <strong>&quot;GenAI 设置&quot;</strong>；现在选择我们创建的设置，然后单击 &quot;<strong>保存&quot;</strong>。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/11e830c3e2fb4c298b020c928fa5422f3397ba08-1600x1152.png" style="width: 100%;" /><h3>访问代理生成器</h3><ol><li>前往<strong>代理商</strong></li><li>点击<strong>创建新代理</strong></li></ol><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/c1e60541563650163f972ac9088dc1ed1de759a7-1600x1054.png" style="width: 100%;" /><h3>配置代理</h3><p>要创建新代理，必须填写<strong>代理 ID</strong>、<strong>显示名称</strong>和<strong>显示说明</strong>。</p><p>但还有更多的自定义选项，比如 &quot;自定义指令&quot;，它可以指导代理如何与工具进行交互，类似于系统提示，但适用于我们的自定义代理。标签可帮助您组织代理人、头像颜色和头像符号。</p><p>我根据数据集为我们的代理选择的<strong>代理编号</strong>是：<br/><br/>Agent ID： <code>hr_assistant</code></p><p><strong>自定义说明：</strong></p><pre><code>You are an HR Analytics Assistant that helps answer questions about employee data.
When responding to queries:
- Provide clear, concise answers
- Include relevant employee details (name, department, salary, skills)
- Format monetary values with currency symbols
- Be professional and maintain data confidentiality</code></pre><p><br/>标签：<code>Human Resources</code> 和 <code>GPT-OSS</code></p><p>显示名称： <code>HR Analytics Assistant</code></p><p>显示说明：</p><pre><code>A specialized AI assistant for Human Resources that helps analyze employee data, compensation, performance metrics, and talent management. Ask questions about employees, departments, salaries, or performance analytics.</code></pre><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/f94bb2bf08497e5e756ca76b30a3a51f42927756-1424x1217.png" style="width: 100%;" /><p>有了所有数据，我们就可以点击 &quot;<strong>保存</strong>新代理&quot;。</p><h3>测试代理</h3><p>现在，您可以就员工数据提出自然语言问题，GPT-OSS 20B 将理解您的意图并生成适当的回复。</p><h4>提示：</h4><pre><code>Which employee is the one with the highest salary in the hr-employees index?</code></pre><h4>请回答：</h4><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/94ad19f80b96304028a59f60beca51dfc9aecc8a-899x631.png" style="width: 100%;" /><p>代理过程是</p><p>1.使用 GPT-OSS 连接器了解您的问题</p><p>2.生成适当的 Elasticsearch 查询（使用内置工具或自定义<a href="https://www.elastic.co/docs/reference/query-languages/esql">ES|QL）</a></p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/6f2961d0d1b97475f6dda300acee84da540938e6-844x466.png" style="width: 100%;" /><p>3.检索匹配的员工记录</p><p>4.以自然语言和适当的格式呈现结果</p><p>与传统的词法搜索不同，由 GPT-OSS 支持的代理可以理解意图和上下文，从而在不知道确切字段名称或查询语法的情况下更容易找到信息。有关代理人思维过程的更多详情，请参阅<a href="https://www.elastic.co/search-labs/blog/ai-agent-builder-experiments-performance">本文</a>。</p><h2>结论</h2><p>在本文中，我们使用 Elastic 的代理生成器（Agent Builder）构建了一个自定义人工智能代理，以连接到本地运行的 OpenAI GPT-OSS 模型。通过在本地机器上部署 Elastic 和 LLM，这种架构可以让您利用生成式人工智能功能，同时保持对数据的完全控制，而无需向外部服务发送信息。</p><p>我们使用 GPT-OSS 20B 作为实验，但<a href="https://www.elastic.co/docs/solutions/search/agent-builder/models#recommended-models">此处</a>参考了官方推荐的 Elastic Agent Builder 模型。如果您需要更高级的推理能力，还可以选择<a href="https://huggingface.co/openai/gpt-oss-120b">120B 参数变体</a>，它在复杂情况下的表现更好，不过需要更高级的机器才能在本地运行。更多详情，请参阅<a href="https://openai.com/open-models/">OpenAI 官方文档</a>。</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[来自 Cal Hacks 12.0 的顶级弹性代理生成器项目和学习成果]]></title>
            <description><![CDATA[探索 Cal Hacks 12.0 中的顶级 Elastic Agent Builder 项目，深入了解我们在无服务器、ES|QL 和代理架构方面的技术要点。]]></description>
            <link>https://www.elastic.co/search-labs/cn/blog/agent-builder-projects-learnings-cal-hacks-12-0</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/cn/blog/agent-builder-projects-learnings-cal-hacks-12-0</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[JD Armada]]></dc:creator>
            <pubDate>Tue, 25 Nov 2025 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/8ba034a6f19b50521f541b8131756a8acdb52975-1280x960.jpg?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<a href="https://www.youtube.com/watch/GUI0WXNZduo">Watch video on YouTube</a><p>几周前，我们有幸赞助了<a href="https://cal-hacks-12-0.devpost.com/">Cal Hacks 12.0</a>，这是规模最大的个人黑客马拉松之一，有来自世界各地的 2000 多名参赛者。我们为在 Serverless 上最佳使用 Elastic Agent Builder 设立了专门的奖项，反响非常好。在短短 36 小时内，我们就收到了 29 份以创造性方式使用 Agent Builder 的提交，其中包括构建野火情报工具和 StackOverflow 验证器。</p><p>除了令人印象深刻的项目之外，Cal Hacks 12.0 还为我们带来了同样宝贵的经验：首次接触我们 Stack 的开发人员提供了快速、未经过滤的反馈。黑客马拉松是一种独特的压力测试，时间紧迫，事先完全不熟悉，还有不可预知的障碍（比如臭名昭著的 WiFi 中断）。它们准确地揭示了开发人员体验的闪光点和仍需改进的地方。随着开发人员越来越多地通过 LLM 驱动的工作流，以新的方式与 Elastic Stack 进行交互，这一点现在变得更加重要。在这篇博文中，我们将深入探讨参与者使用 Agent Builder 构建的内容，以及我们在此过程中学到的东西。</p><h2>获奖项目</h2><h3>第一名AgentOverflow</h3><p>为 LLM 和代理时代重建的 Stack Overflow。</p><p><a href="https://devpost.com/software/agentoverflow">点击此处</a>了解有关 AgentOverflow 的更多信息。</p><p>AgentOverflow 解决了大多数人工智能开发人员遇到的问题：LLM 会产生幻觉，聊天记录会消失，开发人员会浪费时间重新解决同样的问题。</p><p>AgentOverflow 可以捕捉、验证和重新浮现真实的问题-解决方案对，因此开发人员可以打破幻觉漩涡，更快地完成开发。</p><h4>如何使用</h4><p><strong>1.共享 JSON--&quot;解决方案模式&quot;。</strong></p><p>从克劳德共享中点击一下，就能刮取、提取并组装一个共享解决方案 JSON，这是一种结构化格式，其中包含：</p><ul><li>问题</li><li>上下文</li><li>代码</li><li>标记</li><li>验证解决方案步骤。</li></ul><p>验证器（LAVA）检查并强制执行结构，用户添加一行额外的上下文，然后在 Elasticsearch 中进行存储和索引。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/45a3e96f4474050a855419628c2a7338bb12c706-1600x877.png" style="width: 100%;" /><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/2d65cb154d8ee32fc96ff17dfa5b0bf2636e3777-1600x1002.png" style="width: 100%;" /><p><strong>2.查找解决方案</strong></p><p>当您遇到困难时，点击<code>Find Solution</code> ，AgentOverflow 就会抓取您当前的对话，利用它建立一个查询，然后运行混合 Elasticsearch 搜索，使其浮出水面：</p><ul><li>排名靠前、经过社区验证的修复方案</li><li>最初解决问题的确切提示</li></ul><p>这样，开发人员就可以快速复制、粘贴和解除对当前会话的封锁。</p><p><strong>3.MCP - LLM 的上下文注入</strong></p><p>通过 MCP（模型上下文协议）连接到 Elasticsearch 中存储的结构化解决方案，LLM 可在运行时获得高信号上下文（代码、日志、配置、先前的修复），而不会产生额外的噪音。</p><p>AgentOverflow 使用 Agent Builder 和 Elasticsearch 作为结构化内存层，将相关上下文注入 LLM。这就使它们从被动的聊天机器人转变为能感知上下文的问题解决者。</p><h3>亚军MarketMind</h3><p>由六个弹性代理提供支持的可实时解释的市场能量视图。</p><p><a href="https://devpost.com/software/marketmind-b6cy2q">点击此处</a>了解有关 MarketMind 的更多信息。</p><p>MarketMind 通过为新手交易者提供一个平台，将零散的市场数据转换成清晰的实时信号，赢得了自己的一席之地。MarketMind 将所有这些信息整合到一个平台中，帮助交易者获得可操作的洞察力，而不是在不同的工具中纠缠价格走势、基本面、情绪和波动性。该项目在构建代理时还使用了一些复杂的 ES|QL 查询。</p><h4>如何使用</h4><p><strong>1.收集实时市场数据</strong></p><p>MarketMind 从雅虎财经中提取价格-行动、基本面、情绪、波动性和风险指标。这些数据被摄取并组织到多个 Elasticsearch 索引中。</p><p><strong>2.六家专业代理商分析市场</strong></p><p>使用 Agent Builder 创建的每个代理都专注于不同的市场层。它们从 Elasticsearch 索引中读取数据，计算自己特定领域的指标，并生成包含分数和推理的标准化 JSON 输出。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/7d9716cca06a047a2b3584378b5c7e592a785ba1-1284x878.png" style="width: 100%;" /><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/5aac6a833347c0d2e596c02049ec4b4d3aae5cd7-794x764.png" style="width: 100%;" /><p><strong>3.将信号汇总为统一的 &quot;市场能量 &quot;模型</strong></p><p>综合输出显示为每只股票周围的发光脉冲，说明势头是否正在形成、风险是否正在上升、情绪是否正在转变。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/46b3da8e3d528c5dd4e2829416c5446098acb3aa-744x718.png" style="width: 100%;" /><p><strong>4.可视化洞察力</strong></p><p>前端采用 React 和<a href="https://github.com/vercel/next.js"> Next.js</a> ，使用 TypeScript、SVG 物理视觉效果和<a href="https://github.com/chartjs"> Chart. js</a> 制作实时蜡烛图。这将原始分析转化为实时可操作的反馈。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/3f000c043117b77ed4127202be5a49c12e3682ba-1600x930.png" style="width: 100%;" /><h2>其他有趣的项目</h2><p>以下是在其堆栈的不同部分使用 Elastic 的其他一些有力竞争者：</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/76c49a853426844f475cd6b2a74999e60af20e8c-926x1080.png" style="width: 100%;" /><p><a href="https://cal-hacks-12-0.devpost.com/submissions/search?utf8=%E2%9C%93&amp;prize_filter%5Bprizes%5D%5B%5D=91882">点击此处</a>查看提交给我们赛道的全部项目清单。</p><h2>我们从开发人员那里学到了什么</h2><ul><li><strong>代理生成器方便用户使用：</strong></li></ul><p>大多数团队以前从未使用过 Elastic，但仍能在几乎没有支持的情况下快速建立代理。我们为那些需要更多指导的人举办了一次研讨会，但大多数人都能获取他们的数据，并建立一个代理对这些数据执行操作。</p><ul><li><strong>法律硕士擅长 <code>kNN</code> 查询，但在生成 ES|QL 方面仍需要指导：</strong></li></ul><p>要求 ChatGPT-5 生成 ES|QL 查询会返回不正确的信息，通常会混淆 ES|QL 和 SQL。在标记文件中向 LLM 提供文档似乎是一个可行的解决方案。</p><ul><li><strong>仅快照 ES|QL 函数泄露到文档中：</strong></li></ul><p>即将推出的<code>FIRST</code> 和<code>LAST</code> 聚合函数无意中滑入了我们的 ES|QL 文档。因为我们将这些文档提供给了 ChatGPT，所以该模型会尽职尽责地使用这些函数，尽管它们在无服务器中还不可用。多亏了该小组的反馈意见，工程设计人员迅速打开并合并了一个修复程序，从发布的文档中删除了这些功能<a href="https://github.com/elastic/elasticsearch/pull/137341">（PR #137341</a>）。</p><ul><li><strong>缺少针对服务器的指导：</strong></li></ul><p>一个小组尝试在一个不是以查找模式创建的索引上启用<code>LOOKUP JOIN</code> 。错误信息让他们追逐 Serverless 上不存在的命令。我们将这一情况反映给了产品团队，他们立即启动了一个针对无服务器的可执行消息的修复程序。从长远来看，我们的目标是完全隐藏重新索引的复杂性<a href="https://github.com/elastic/elasticsearch-serverless/issues/4838">（问题编号 4838</a>）。</p><ul><li><strong>现场活动的价值：</strong></li></ul><p>在线黑客马拉松固然很棒，但没有什么能比得上与建设者并肩调试时获得的快速反馈回路。我们看到各团队在不同的使用案例中集成了代理生成器，发现了开发人员使用 ES|QL 的体验可以改进的地方，并比尝试通过异步渠道更快地修复了问题。</p><h2>结论</h2><p>Cal Hacks 12.0 为我们带来的不仅仅是一个周末的酷炫演示，它还让我们深入了解了新开发人员如何与 Elastic Stack 交互。在短短 36 个小时内，我们看到各个团队开始使用 Agent Builder，将数据导入 Elasticsearch，设计多代理系统，并以各种方式测试我们的功能。这次活动还提醒我们，为什么面对面的活动很重要。快速的反馈循环、真实的对话和亲自动手的调试帮助我们了解了当前开发人员的需求。我们很高兴能把学到的东西带回工程团队。我们下次黑客马拉松再见。</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[在 Elasticsearch 中使用 A2A 协议和 MCP 创建 LLM 代理新闻室：第二部分]]></title>
            <description><![CDATA[了解如何使用 A2A 协议（用于代理协作）和 MCP（用于 Elasticsearch 中的工具访问）建立专门的混合 LLM 代理新闻室。]]></description>
            <link>https://www.elastic.co/search-labs/cn/blog/a2a-protocol-mcp-llm-agent-workflow-elasticsearch</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/cn/blog/a2a-protocol-mcp-llm-agent-workflow-elasticsearch</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Justin Castilla]]></dc:creator>
            <pubDate>Mon, 24 Nov 2025 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/f87330e5d4ca961593b3cfb861ca850a4cc34186-1519x1173.png?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<h2>A2A 和 MCP：行动守则</h2><p>本文是 &quot;在 Elasticsearch 中使用 A2A 协议和 MCP 创建 LLM 代理新闻室！&quot;一文的配套文章，该文章介绍了在同一个代理中同时实施 A2A 和 MCP 架构的好处，以真正获得这两种框架的独特优势。如果您希望自行运行演示，我们还提供了一个<a href="https://github.com/justincastilla/elastic-newsroom">资源库</a>。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/7196f004089127f83547b2e5dc3f663205cfcdce-1162x1600.png" style="width: 100%;" /><p>让我们来看看我们的新闻室代理是如何使用 A2A 和 MCP 协作来制作一篇新闻文章的。可<a href="https://github.com/justincastilla/elastic-newsroom">在此处</a>查看随附的存储库，了解代理的运行情况。</p><h3>步骤 1：故事任务</h3><p><strong>新闻主管</strong>（作为客户）指定一篇报道：</p><pre><code>{
  "message_type": "task_request",
  "sender": "news_chief",
  "receiver": "reporter_agent",
  "payload": {
    "task_id": "story_renewable_energy_2024",
    "assignment": {
      "topic": "Renewable Energy Adoption in Europe",
      "angle": "Policy changes driving solar and wind expansion",
      "target_length": 1200,
      "deadline": "2025-09-30T18:00:00Z"
    }
  }
}</code></pre><h3>第 2 步：记者要求进行研究</h3><p><strong>报告代理</strong>认识到它需要背景信息，并通过 A2A 委托给<strong>研究代理</strong>：</p><pre><code>{
  "message_type": "task_request",
  "sender": "reporter_agent",
  "receiver": "researcher_agent",
  "payload": {
    "task_id": "research_eu_renewable_2024",
    "parent_task_id": "story_renewable_energy_2024",
    "capability": "fact_gathering",
    "parameters": {
      "queries": [
        "EU renewable energy capacity 2024",
        "Solar installations growth Europe",
        "Wind energy policy changes 2024"
      ],
      "depth": "comprehensive"
    }
  }
}</code></pre><h3>第 3 步：报告人请求存档代理提供历史背景信息</h3><p><strong>记者代理</strong>认识到，历史背景会加强故事性。它通过 A2A 委托<strong>档案代理</strong>（由<a href="https://www.elastic.co/docs/solutions/search/elastic-agent-builder">Elastic 的 A2A 代理</a>提供支持）搜索新闻编辑室由 Elasticsearch 支持的文章档案：</p><pre><code>{
  "message_type": "task_request",
  "sender": "reporter_agent",
  "receiver": "archive_agent",
  "payload": {
    "task_id": "archive_search_renewable_2024",
    "parent_task_id": "story_renewable_energy_2024",
    "capability": "search_archive",
    "parameters": {
      "query": "European renewable energy policy changes and adoption trends over past 5 years",
      "focus_areas": ["solar", "wind", "policy", "Germany", "France"],
      "time_range": "2019-2024",
      "result_count": 10
    }
  }
}</code></pre><h3>步骤 4：归档代理使用带有 MCP 的弹性 A2A 代理</h3><p><strong>存档代理</strong>使用 Elastic 的 A2A 代理，而 A2A 代理又使用 MCP 访问 Elasticsearch 工具。这展示了混合架构，其中 A2A 实现了代理协作，而 MCP 提供了工具访问：</p><pre><code># Archive Agent using Elastic A2A Agent
async def search_historical_articles(self, query_params):
    # The Archive Agent sends a request to Elastic's A2A Agent
    elastic_response = await self.a2a_client.send_request(
        agent="elastic_agent",
        capability="search_and_analyze",
        parameters={
            "natural_language_query": query_params["query"],
            "index_pattern": "newsroom-articles-*",
            "filters": {
                "topics": query_params["focus_areas"],
                "date_range": query_params["time_range"]
            },
            "analysis_type": "trend_analysis"
        }
    )
    
    # Elastic's A2A Agent internally uses MCP tools:
    # - platform.core.search (to find relevant articles)
    # - platform.core.generate_esql (to analyze trends)
    # - platform.core.index_explorer (to identify relevant indices)
    
    return elastic_response</code></pre><p><strong>存档代理</strong>从 Elastic 的 A2A 代理接收全面的历史数据，并将其返回给报告器：</p><pre><code>{
  "message_type": "task_response",
  "sender": "archive_agent",
  "receiver": "reporter_agent",
  "payload": {
    "task_id": "archive_search_renewable_2024",
    "status": "completed",
    "archive_data": {
      "historical_articles": [
        {
          "title": "Germany's Energiewende: Five Years of Solar Growth",
          "published": "2022-06-15",
          "key_points": [
            "Germany added 7 GW annually 2020-2022",
            "Policy subsidies drove 60% of growth"
          ],
          "relevance_score": 0.94
        },
        {
          "title": "France Balances Nuclear and Renewables",
          "published": "2023-03-20",
          "key_points": [
            "France increased renewable target to 40% by 2030",
            "Solar capacity doubled 2021-2023"
          ],
          "relevance_score": 0.89
        }
      ],
      "trend_analysis": {
        "coverage_frequency": "EU renewable stories increased 150% since 2019",
        "emerging_themes": ["policy incentives", "grid modernization", "battery storage"],
        "coverage_gaps": ["Small member states", "offshore wind permitting"]
      },
      "total_articles_found": 47,
      "search_confidence": 0.91
    }
  }
}</code></pre><p>这一步骤演示了 Elastic 的 A2A Agent 如何集成到新闻编辑室的工作流程中。Archive Agent（新闻编辑室专用代理）与 Elastic 的 A2A Agent（第三方专家）协调，以利用 Elasticsearch 强大的搜索和分析功能。Elastic 的代理在内部使用 MCP 访问 Elasticsearch 工具，显示了代理协调 (A2A) 和工具访问 (MCP) 之间的明确分离。</p><h3>步骤 5：研究人员使用 MCP 服务器</h3><p><strong>研究员代理</strong>访问多个 MCP 服务器以收集信息：</p><pre><code># Researcher Agent using MCP to access tools
async def gather_facts(self, queries):
    results = []
    
    # Use News API MCP Server
    news_data = await self.mcp_client.invoke_tool(
        server="news_api_mcp",
        tool="search_articles",
        parameters={
            "query": queries[0],
            "date_range": "2024-01-01 to 2024-09-30",
            "sources": ["reuters", "bloomberg", "euractiv"]
        }
    )
    
    # Use Statistics MCP Server
    stats_data = await self.mcp_client.invoke_tool(
        server="eurostat_mcp",
        tool="get_energy_statistics",
        parameters={
            "metrics": ["solar_capacity_mw", "wind_capacity_mw"],
            "countries": ["EU27"],
            "year": 2024
        }
    )
    
    # Use Fact-Checking MCP Server
    verified_facts = await self.mcp_client.invoke_tool(
        server="factcheck_mcp",
        tool="verify_claims",
        parameters={
            "claims": [
                "EU solar capacity increased 40% in 2024",
                "Germany leads in wind installations"
            ]
        }
    )
    
    return self._synthesize_research(news_data, stats_data, verified_facts)</code></pre><h3>步骤 6：研究人员将数据返回给报告人</h3><p><strong>研究员代理</strong>通过 A2A 发送综合研究结果：</p><pre><code>{
  "message_type": "task_response",
  "sender": "researcher_agent",
  "receiver": "reporter_agent",
  "payload": {
    "task_id": "research_eu_renewable_2024",
    "status": "completed",
    "research_data": {
      "key_facts": [
        {
          "claim": "EU solar capacity reached 260 GW in 2024",
          "source": "Eurostat Q3 2024 Report",
          "confidence": 0.95,
          "verified": true
        },
        {
          "claim": "Germany installed 12 GW of wind capacity in 2024",
          "source": "German Federal Network Agency",
          "confidence": 0.92,
          "verified": true
        }
      ],
      "statistics": {
        "solar_growth_rate": "35%",
        "wind_growth_rate": "28%"
      },
      "sources_count": 15
    }
  }
}</code></pre><h3>第 7 步：记者撰写文章</h3><p><strong>报告代理</strong>利用研究数据和自身的 LLM 能力撰写文章。在撰写过程中，记者使用 MCP 服务器获取样式和模板：</p><pre><code># Reporter Agent writing with MCP assistance
async def write_article(self, research_data, assignment):
    # Get style guidelines via MCP
    style_guide = await self.mcp_client.get_resource(
        server="newsroom_mcp",
        resource="style://editorial/ap_style_guide"
    )
    
    # Get article template via MCP
    template = await self.mcp_client.get_resource(
        server="newsroom_mcp",
        resource="template://articles/news_story"
    )
    
    # Generate article using LLM + research + style
    draft = await self.llm.generate(
        prompt=f"""
        Write a news article following these guidelines:
        {style_guide}
        
        Using this template:
        {template}
        
        Based on this research:
        {research_data}
        
        Assignment: {assignment}
        """
    )
    
    # Self-evaluate confidence in claims
    confidence_check = await self._evaluate_confidence(draft)
    
    return draft, confidence_check</code></pre><h3>第 8 步：信心不足引发重新研究</h3><p><strong>报告代理</strong>评估了其草稿，发现有一项索赔的可信度较低。它会向<strong>研究员代理</strong>发送另一个请求：</p><pre><code>{
  "message_type": "collaboration_request",
  "sender": "reporter_agent",
  "receiver": "researcher_agent",
  "payload": {
    "request_type": "fact_verification",
    "claims": [
      {
        "text": "France's nuclear phase-down contributed to 15% increase in renewable capacity",
        "context": "Discussing policy drivers for renewable growth",
        "current_confidence": 0.45,
        "required_confidence": 0.80
      }
    ],
    "urgency": "high"
  }
}</code></pre><p><strong>研究员</strong>使用事实核查 MCP 服务器核实索赔，并返回更新的信息：</p><pre><code>{
  "message_type": "collaboration_response",
  "sender": "researcher_agent",
  "receiver": "reporter_agent",
  "payload": {
    "verified_claims": [
      {
        "original_claim": "France's nuclear phase-down contributed to 15% increase...",
        "verified_claim": "France's renewable capacity increased 18% in 2024, partially offsetting reduced nuclear output",
        "confidence": 0.88,
        "corrections": "Percentage was 18%, not 15%; nuclear phase-down is gradual, not primary driver",
        "sources": ["RTE France", "French Energy Ministry Report 2024"]
      }
    ]
  }
}</code></pre><h3>第 9 步：记者修改并提交给编辑</h3><p><strong>记者</strong>将核实的事实纳入其中，并通过 A2A 将完成的草稿发送给<strong>编辑代理</strong>：</p><pre><code>{
  "message_type": "task_request",
  "sender": "reporter_agent",
  "receiver": "editor_agent",
  "payload": {
    "task_id": "edit_renewable_story",
    "parent_task_id": "story_renewable_energy_2024",
    "content": {
      "headline": "Europe's Renewable Revolution: Solar and Wind Surge 30% in 2024",
      "body": "[Full article text...]",
      "word_count": 1185,
      "sources": [/* array of sources */]
    },
    "editing_requirements": {
      "check_style": true,
      "check_facts": true,
      "check_seo": true
    }
  }
}</code></pre><h3>步骤 10：编辑使用 MCP 工具进行审查</h3><p><strong>编辑代理</strong>使用多个 MCP 服务器来审核文章：</p><pre><code># Editor Agent using MCP for quality checks
async def review_article(self, content):
    # Grammar and style check
    grammar_issues = await self.mcp_client.invoke_tool(
        server="grammarly_mcp",
        tool="check_document",
        parameters={"text": content["body"]}
    )
    
    # SEO optimization check
    seo_analysis = await self.mcp_client.invoke_tool(
        server="seo_mcp",
        tool="analyze_content",
        parameters={
            "headline": content["headline"],
            "body": content["body"],
            "target_keywords": ["renewable energy", "Europe", "solar", "wind"]
        }
    )
    
    # Plagiarism check
    originality = await self.mcp_client.invoke_tool(
        server="plagiarism_mcp",
        tool="check_originality",
        parameters={"text": content["body"]}
    )
    
    # Generate editorial feedback
    feedback = await self._generate_feedback(
        grammar_issues, 
        seo_analysis, 
        originality
    )
    
    return feedback</code></pre><p><strong>编辑</strong>批准文章并将其转发：</p><pre><code>{
  "message_type": "task_response",
  "sender": "editor_agent",
  "receiver": "reporter_agent",
  "payload": {
    "status": "approved",
    "quality_score": 9.2,
    "minor_edits": [
      "Changed 'surge' to 'increased' in paragraph 3 for AP style consistency",
      "Added Oxford comma in list of countries"
    ],
    "approved_content": "[Final edited article]"
  }
}</code></pre><h3>第 11 步：发布者通过 CI/CD 发布</h3><p>最后，<strong>打印机代理</strong>使用 CMS 和 CI/CD 管道的 MCP 服务器发布已批准的文章：</p><pre><code># Publisher Agent publishing via MCP
async def publish_article(self, content, metadata):
    # Upload to CMS via MCP
    cms_result = await self.mcp_client.invoke_tool(
        server="wordpress_mcp",
        tool="create_post",
        parameters={
            "title": content["headline"],
            "body": content["body"],
            "status": "draft",
            "categories": metadata["categories"],
            "tags": metadata["tags"],
            "featured_image_url": metadata["image_url"]
        }
    )
    
    post_id = cms_result["post_id"]
    
    # Trigger CI/CD deployment via MCP
    deploy_result = await self.mcp_client.invoke_tool(
        server="cicd_mcp",
        tool="trigger_deployment",
        parameters={
            "pipeline": "publish_article",
            "environment": "production",
            "post_id": post_id,
            "schedule": "immediate"
        }
    )
    
    # Track analytics
    await self.mcp_client.invoke_tool(
        server="analytics_mcp",
        tool="register_publication",
        parameters={
            "post_id": post_id,
            "publish_time": datetime.now().isoformat(),
            "story_id": metadata["story_id"]
        }
    )
    
    return {
        "status": "published",
        "post_id": post_id,
        "url": f"https://newsroom.example.com/articles/{post_id}",
        "deployment_id": deploy_result["deployment_id"]
    }</code></pre><p><strong>出版商</strong>确认通过 A2A 出版：</p><pre><code>{
  "message_type": "task_complete",
  "sender": "printer_agent",
  "receiver": "news_chief",
  "payload": {
    "task_id": "story_renewable_energy_2024",
    "status": "published",
    "publication": {
      "url": "https://newsroom.example.com/articles/renewable-europe-2024",
      "published_at": "2025-09-30T17:45:00Z",
      "post_id": "12345"
    },
    "workflow_metrics": {
      "total_time_minutes": 45,
      "agents_involved": ["reporter", "researcher", "archive", "editor", "printer"],
      "iterations": 2,
      "mcp_calls": 12
    }
  }
}</code></pre><p>下面是随附的资料库中使用上述相同代理的 A2A 工作流程的完整序列。</p>
            <table style="border-collapse: collapse; margin: 20px 0;">
              <tbody>
                
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">#</td><td style="border: 1px solid #ccc; padding: 4px;">来自</td><td style="border: 1px solid #ccc; padding: 4px;">至</td><td style="border: 1px solid #ccc; padding: 4px;">行动</td><td style="border: 1px solid #ccc; padding: 4px;">规程</td><td style="border: 1px solid #ccc; padding: 4px;">描述</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">1</td><td style="border: 1px solid #ccc; padding: 4px;">用户</td><td style="border: 1px solid #ccc; padding: 4px;">新闻主管</td><td style="border: 1px solid #ccc; padding: 4px;">指定故事</td><td style="border: 1px solid #ccc; padding: 4px;">HTTP POST</td><td style="border: 1px solid #ccc; padding: 4px;">用户提交故事主题和角度</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">2</td><td style="border: 1px solid #ccc; padding: 4px;">新闻主管</td><td style="border: 1px solid #ccc; padding: 4px;">内部</td><td style="border: 1px solid #ccc; padding: 4px;">创建故事</td><td style="border: 1px solid #ccc; padding: 4px;">-</td><td style="border: 1px solid #ccc; padding: 4px;">创建具有唯一 ID 的故事记录</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">3</td><td style="border: 1px solid #ccc; padding: 4px;">新闻主管</td><td style="border: 1px solid #ccc; padding: 4px;">记者</td><td style="border: 1px solid #ccc; padding: 4px;">代表任务</td><td style="border: 1px solid #ccc; padding: 4px;">A2A</td><td style="border: 1px solid #ccc; padding: 4px;">通过 A2A 协议发送故事任务</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">4</td><td style="border: 1px solid #ccc; padding: 4px;">记者</td><td style="border: 1px solid #ccc; padding: 4px;">内部</td><td style="border: 1px solid #ccc; padding: 4px;">接受任务</td><td style="border: 1px solid #ccc; padding: 4px;">-</td><td style="border: 1px solid #ccc; padding: 4px;">内部存储任务</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">5</td><td style="border: 1px solid #ccc; padding: 4px;">记者</td><td style="border: 1px solid #ccc; padding: 4px;">MCP 服务器</td><td style="border: 1px solid #ccc; padding: 4px;">生成大纲</td><td style="border: 1px solid #ccc; padding: 4px;">MCP/HTTP</td><td style="border: 1px solid #ccc; padding: 4px;">创建文章大纲和研究问题</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">6a</td><td style="border: 1px solid #ccc; padding: 4px;">记者</td><td style="border: 1px solid #ccc; padding: 4px;">研究员</td><td style="border: 1px solid #ccc; padding: 4px;">申请研究</td><td style="border: 1px solid #ccc; padding: 4px;">A2A</td><td style="border: 1px solid #ccc; padding: 4px;">发送问题（与 6b 并行）</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">6b</td><td style="border: 1px solid #ccc; padding: 4px;">记者</td><td style="border: 1px solid #ccc; padding: 4px;">档案员</td><td style="border: 1px solid #ccc; padding: 4px;">搜索档案</td><td style="border: 1px solid #ccc; padding: 4px;">A2A JSONRPC</td><td style="border: 1px solid #ccc; padding: 4px;">搜索历史文章（与 6a 并行）</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">7</td><td style="border: 1px solid #ccc; padding: 4px;">研究员</td><td style="border: 1px solid #ccc; padding: 4px;">MCP 服务器</td><td style="border: 1px solid #ccc; padding: 4px;">研究问题</td><td style="border: 1px solid #ccc; padding: 4px;">MCP/HTTP</td><td style="border: 1px solid #ccc; padding: 4px;">通过 MCP 使用人类学来回答问题</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">8</td><td style="border: 1px solid #ccc; padding: 4px;">研究员</td><td style="border: 1px solid #ccc; padding: 4px;">记者</td><td style="border: 1px solid #ccc; padding: 4px;">返回研究</td><td style="border: 1px solid #ccc; padding: 4px;">A2A</td><td style="border: 1px solid #ccc; padding: 4px;">返回研究答案</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">9</td><td style="border: 1px solid #ccc; padding: 4px;">档案员</td><td style="border: 1px solid #ccc; padding: 4px;">Elasticsearch</td><td style="border: 1px solid #ccc; padding: 4px;">搜索索引</td><td style="border: 1px solid #ccc; padding: 4px;">ES REST API</td><td style="border: 1px solid #ccc; padding: 4px;">查询 news_archive 索引</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">10</td><td style="border: 1px solid #ccc; padding: 4px;">档案员</td><td style="border: 1px solid #ccc; padding: 4px;">记者</td><td style="border: 1px solid #ccc; padding: 4px;">返回存档</td><td style="border: 1px solid #ccc; padding: 4px;">A2A JSONRPC</td><td style="border: 1px solid #ccc; padding: 4px;">返回历史搜索结果</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">11</td><td style="border: 1px solid #ccc; padding: 4px;">记者</td><td style="border: 1px solid #ccc; padding: 4px;">MCP 服务器</td><td style="border: 1px solid #ccc; padding: 4px;">生成文章</td><td style="border: 1px solid #ccc; padding: 4px;">MCP/HTTP</td><td style="border: 1px solid #ccc; padding: 4px;">创建具有研究/档案背景的文章</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">12</td><td style="border: 1px solid #ccc; padding: 4px;">记者</td><td style="border: 1px solid #ccc; padding: 4px;">内部</td><td style="border: 1px solid #ccc; padding: 4px;">商店草案</td><td style="border: 1px solid #ccc; padding: 4px;">-</td><td style="border: 1px solid #ccc; padding: 4px;">内部保存草稿</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">13</td><td style="border: 1px solid #ccc; padding: 4px;">记者</td><td style="border: 1px solid #ccc; padding: 4px;">新闻主管</td><td style="border: 1px solid #ccc; padding: 4px;">提交草案</td><td style="border: 1px solid #ccc; padding: 4px;">A2A</td><td style="border: 1px solid #ccc; padding: 4px;">提交完成的草稿</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">14</td><td style="border: 1px solid #ccc; padding: 4px;">新闻主管</td><td style="border: 1px solid #ccc; padding: 4px;">内部</td><td style="border: 1px solid #ccc; padding: 4px;">更新故事</td><td style="border: 1px solid #ccc; padding: 4px;">-</td><td style="border: 1px solid #ccc; padding: 4px;">存储草稿，将状态更新为"draft_submitted"</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">15</td><td style="border: 1px solid #ccc; padding: 4px;">新闻主管</td><td style="border: 1px solid #ccc; padding: 4px;">编辑</td><td style="border: 1px solid #ccc; padding: 4px;">审查草案</td><td style="border: 1px solid #ccc; padding: 4px;">A2A</td><td style="border: 1px solid #ccc; padding: 4px;">自动路由至编辑器以供审核</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">16</td><td style="border: 1px solid #ccc; padding: 4px;">编辑</td><td style="border: 1px solid #ccc; padding: 4px;">MCP 服务器</td><td style="border: 1px solid #ccc; padding: 4px;">评论文章</td><td style="border: 1px solid #ccc; padding: 4px;">MCP/HTTP</td><td style="border: 1px solid #ccc; padding: 4px;">通过 MCP 使用 Anthropic 分析内容</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">17</td><td style="border: 1px solid #ccc; padding: 4px;">编辑</td><td style="border: 1px solid #ccc; padding: 4px;">新闻主管</td><td style="border: 1px solid #ccc; padding: 4px;">返回评论</td><td style="border: 1px solid #ccc; padding: 4px;">A2A</td><td style="border: 1px solid #ccc; padding: 4px;">发送编辑反馈和建议</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">18</td><td style="border: 1px solid #ccc; padding: 4px;">新闻主管</td><td style="border: 1px solid #ccc; padding: 4px;">内部</td><td style="border: 1px solid #ccc; padding: 4px;">商店评论</td><td style="border: 1px solid #ccc; padding: 4px;">-</td><td style="border: 1px solid #ccc; padding: 4px;">存储编辑反馈</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">19</td><td style="border: 1px solid #ccc; padding: 4px;">新闻主管</td><td style="border: 1px solid #ccc; padding: 4px;">记者</td><td style="border: 1px solid #ccc; padding: 4px;">应用编辑</td><td style="border: 1px solid #ccc; padding: 4px;">A2A</td><td style="border: 1px solid #ccc; padding: 4px;">将审查反馈意见转达给报告人</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">20</td><td style="border: 1px solid #ccc; padding: 4px;">记者</td><td style="border: 1px solid #ccc; padding: 4px;">MCP 服务器</td><td style="border: 1px solid #ccc; padding: 4px;">应用编辑</td><td style="border: 1px solid #ccc; padding: 4px;">MCP/HTTP</td><td style="border: 1px solid #ccc; padding: 4px;">根据反馈意见修改文章</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">21</td><td style="border: 1px solid #ccc; padding: 4px;">记者</td><td style="border: 1px solid #ccc; padding: 4px;">内部</td><td style="border: 1px solid #ccc; padding: 4px;">更新草案</td><td style="border: 1px solid #ccc; padding: 4px;">-</td><td style="border: 1px solid #ccc; padding: 4px;">对草案进行修订更新</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">220</td><td style="border: 1px solid #ccc; padding: 4px;">记者</td><td style="border: 1px solid #ccc; padding: 4px;">新闻主管</td><td style="border: 1px solid #ccc; padding: 4px;">返回修订版</td><td style="border: 1px solid #ccc; padding: 4px;">A2A</td><td style="border: 1px solid #ccc; padding: 4px;">返回修订后的文章</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">23</td><td style="border: 1px solid #ccc; padding: 4px;">新闻主管</td><td style="border: 1px solid #ccc; padding: 4px;">内部</td><td style="border: 1px solid #ccc; padding: 4px;">更新故事</td><td style="border: 1px solid #ccc; padding: 4px;">-</td><td style="border: 1px solid #ccc; padding: 4px;">存储修订草案，状态为"修订版"</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">24</td><td style="border: 1px solid #ccc; padding: 4px;">新闻主管</td><td style="border: 1px solid #ccc; padding: 4px;">出版商</td><td style="border: 1px solid #ccc; padding: 4px;">发表文章</td><td style="border: 1px solid #ccc; padding: 4px;">A2A</td><td style="border: 1px solid #ccc; padding: 4px;">出版商自动路由</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">25</td><td style="border: 1px solid #ccc; padding: 4px;">出版商</td><td style="border: 1px solid #ccc; padding: 4px;">MCP 服务器</td><td style="border: 1px solid #ccc; padding: 4px;">生成标签</td><td style="border: 1px solid #ccc; padding: 4px;">MCP/HTTP</td><td style="border: 1px solid #ccc; padding: 4px;">创建标记和类别</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">26</td><td style="border: 1px solid #ccc; padding: 4px;">出版商</td><td style="border: 1px solid #ccc; padding: 4px;">Elasticsearch</td><td style="border: 1px solid #ccc; padding: 4px;">索引文章</td><td style="border: 1px solid #ccc; padding: 4px;">ES REST API</td><td style="border: 1px solid #ccc; padding: 4px;">将文章索引到 news_archive 索引</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">27</td><td style="border: 1px solid #ccc; padding: 4px;">出版商</td><td style="border: 1px solid #ccc; padding: 4px;">文件系统</td><td style="border: 1px solid #ccc; padding: 4px;">保存标记</td><td style="border: 1px solid #ccc; padding: 4px;">文件输入/输出</td><td style="border: 1px solid #ccc; padding: 4px;">将文章保存为 .md文件在 /articles</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">28</td><td style="border: 1px solid #ccc; padding: 4px;">出版商</td><td style="border: 1px solid #ccc; padding: 4px;">新闻主管</td><td style="border: 1px solid #ccc; padding: 4px;">确认出版</td><td style="border: 1px solid #ccc; padding: 4px;">A2A</td><td style="border: 1px solid #ccc; padding: 4px;">返回成功状态</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">29</td><td style="border: 1px solid #ccc; padding: 4px;">新闻主管</td><td style="border: 1px solid #ccc; padding: 4px;">内部</td><td style="border: 1px solid #ccc; padding: 4px;">更新故事</td><td style="border: 1px solid #ccc; padding: 4px;">-</td><td style="border: 1px solid #ccc; padding: 4px;">将故事状态更新为"已发布"</td>
                    </tr>
              </tbody>
            </table>
          <h2>结论</h2><p>A2A 和 MCP 在现代增强型 LLM 基础设施范例中都可以发挥重要作用。A2A 为复杂的多代理系统提供了灵活性，但潜在的可移植性较差，操作复杂性较高。MCP 提供了一种标准化的工具集成方法，更易于实施和维护，但它并不是为处理多代理协调而设计的。</p><p>选择不是二元对立的。正如我们的新闻编辑室示例所示，最复杂、最有效的 LLM 支持系统往往将这两种方法结合在一起：代理通过 A2A 协议进行协调和专业化，同时通过 MCP 服务器访问其工具和资源。这种混合架构在提供多代理系统的组织优势的同时，还提供了 MCP 的标准化和生态系统优势。这表明可能根本不需要做出选择：只需将两者都作为标准方法使用即可</p><p>作为开发人员或架构师，您需要测试并确定这两种解决方案的最佳组合，从而为您的特定用例创造正确的结果。了解每种方法的优势、局限性和适当应用，将使您能够构建更有效、可维护和可扩展的人工智能系统。</p><p>无论您是要建立数字新闻编辑室、客户服务平台、研究助手，还是其他任何由 LLM 驱动的应用程序，仔细考虑您的协调需求 (A2A) 和工具访问要求 (MCP) 都将使您走上成功之路。</p><h2>其他资源</h2><ul><li><strong>Elasticsearch 代理生成器 </strong><a href="https://www.elastic.co/docs/solutions/search/elastic-agent-builder">：https://www.elastic.co/docs/solutions/search/elastic-agent-builder</a></li><li><strong>A2A 规格</strong> <a href="https://a2a-protocol.org/latest/specification/">： https://a2a-protocol.org/latest/specification/</a></li><li><strong>A2A 和 MCP 集成</strong> <a href="https://a2a-protocol.org/latest/topics/a2a-and-mcp/">：https://a2a-protocol.org/latest/topics/a2a-and-mcp/</a></li><li><strong>模型上下文协议</strong> <a href="https://modelcontextprotocol.io/">： https://modelcontextprotocol.io</a></li></ul>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[你懂的，语境--第三部分：混合搜索在语境工程中的威力]]></title>
            <description><![CDATA[了解如何利用上下文工程和混合搜索，通过聚合、RBAC 和非内容信号来提高人工智能输出的准确性。]]></description>
            <link>https://www.elastic.co/search-labs/cn/blog/context-engineering-hybrid-search-agentic-ai-accuracy</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/cn/blog/context-engineering-hybrid-search-agentic-ai-accuracy</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Woody Walton]]></dc:creator>
            <pubDate>Thu, 20 Nov 2025 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/b82ff25242e4229cc20b218d9cc91c60cfd680bc-720x420.jpg?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>我们已经讨论了混合搜索<a href="https://www.elastic.co/search-labs/blog/context-engineering-hybrid-search-evolution-agentic-ai">（第一部分</a>）和上下文工程<a href="https://www.elastic.co/search-labs/blog/context-engineering-llm-evolution-agentic-ai">（第二部分</a>）；现在，让我们深入探讨它们如何协同工作，为 RAG 和代理人工智能操作提供有针对性的上下文，以达到最佳效果。</p><h2>搜索并未消亡，只是转移了位置</h2><p>因此，我们已经从主要通过文本框搜索上下文，然后使用返回的信息（上下文）自己构建答案，转变为现在使用自然语言告诉代理我们想要什么，然后让它自动研究并为我们编译答案。科技界的许多人都指出了这一转变，并宣称 &quot;搜索已死&quot;（搜索引擎优化和广告词的世界<a href="https://www.pewresearch.org/short-reads/2025/07/22/google-users-are-less-likely-to-click-on-links-when-an-ai-summary-appears-in-the-results/"> 肯定 在 变化</a> ：<a href="https://www.wired.com/story/goodbye-seo-hello-geo-brandlight-openai/"> GEO</a> 谁知道？</p><p>以前，人类是主观相关性的主要仲裁者：每个用户都有自己进行搜索的理由，他们的个人经验会影响搜索结果的相对准确性。如果我们要相信代理能得出与我们相同（或更好）的结论，我们就必须确保代理能获得的上下文信息尽可能接近我们的主观意图。为了实现这一目标，我们必须设计为法律硕士提供的环境！</p><h2>利用混合搜索检索生成上下文</h2><p>在此提醒大家，Elastic 的混合搜索结合了传统基于关键字搜索的优势（语法灵活性、关键字精确度和相关性评分）和向量相似性搜索的语义理解，并提供了多种重排技术。这种协同作用（这个词从未有过如此真实的用法）这样就能获得高度相关的结果，查询内容的针对性也会更加细致。这不仅仅是说你可以将主观相关性作为检索阶段<em>之一</em>，而是说第一阶段检索可以同时包括相关性评分和所有其他模式。</p><h3>卓越的精度&amp; 效率</h3><p>使用可提供分布式搜索、检索和重新排序的数据平台作为主要的上下文检索引擎非常有意义。您可以使用高级查询语法来添加主观意图的缺失部分，并过滤掉可能干扰或混淆所返回的上下文信息价值的内容。您可以从任何可用的单独语法选项中进行选择，也可以将各种模式组合成一个单一的搜索，以其最能理解的方式针对每种类型的数据进行搜索，然后通过重新排序对其进行组合/重新排序。您可以对响应进行过滤，使其只包含您想要的字段/值，从而避免无关数据。在为代理提供服务时，这种目标定位的灵活性可让您构建的工具在检索上下文时极为准确。</p><h3>语境细化（聚合和非内容信号）</h3><p>聚合在塑造工具向上下文窗口提供的内容方面特别有用。聚合自然会提供有关返回的上下文数据形状的基于数字的事实，这使得 LLM 的推理更容易、更准确。由于聚合可以分层嵌套，因此很容易为 LLM 增加多层次的细节，从而产生更细致入微的理解。聚合还有助于管理上下文窗口的大小--您可以轻松地将 10 万个文档的查询结果减少到几百个聚合洞察的标记。</p><p>非内容信号是数据中的固有指标，它们能告诉你所查看内容的全貌；它们是结果的附加特征，如受欢迎程度、新鲜度、地理位置、类别、主机多样性或价格带。这些信息可以为代理如何权衡所接收到的上下文的重要性提供有用信息。一些简单的例子也许最能说明这一点：</p><ul><li><strong>提升近期发布的热门内容</strong>--想象一下，您有一个文章知识库。您希望找到与用户查询相关的文章，但同时也希望推广那些最近发表的、对其他用户有帮助的文章（例如，具有较高&quot;likes&quot; 数量的文章）。在这种情况下，我们可以使用混合搜索来查找相关文章，然后根据文章的发表日期和受欢迎程度对其进行排序。</li><li><strong>带有销售和库存调整功能的电子商务搜索</strong>- 在电子商务环境中，您希望向客户展示与其搜索词相匹配的产品，但同时也希望推广销售良好且有库存的产品。您可能还想把库存少的产品降级，以避免客户失望。</li><li><strong>在错误跟踪器中确定高严重性问题的优先级</strong>--对于软件开发团队来说，在搜索问题时，首先浮现高严重性、高优先级和最近更新的问题至关重要。您可以使用 &quot;关键性 &quot;和 &quot;讨论最多 &quot;等非信号来独立权衡不同的因素，确保最关键和讨论最活跃的问题排在最前面</li></ul><p>这些示例查询和更多内容可在随附的 Elasticsearch Labs<a href="https://github.com/elastic/elasticsearch-labs/tree/main/supporting-blog-content/you-know-for-context/">内容页面</a>中找到。</p><h3>安全执法</h3><p>利用 Elastic 等搜索驱动的速度层进行上下文工程的一个重要优势是其内置的安全框架。Elastic 的平台通过细粒度的基于角色的访问控制（RBAC）和基于属性的访问控制（ABAC），确保向代理和生成式人工智能操作提供的上下文尊重并保护敏感的私人信息。这意味着不仅能高效处理查询，还能根据代理或发起请求的用户的特定权限对结果进行过滤。</p><p>代理以认证用户的身份运行，因此通过平台内置的安全功能隐式地应用了安全功能：</p><ul><li><strong>细粒度权限：</strong>在文档、字段甚至术语级别定义访问权限，确保人工智能代理只接收他们有权查看的数据。</li><li><strong>基于角色的访问控制（RBAC）：</strong>为代理或用户分配角色，根据其定义的职责授予对特定数据集或功能的访问权限。</li><li><strong>基于属性的访问控制（ABAC）：</strong>根据数据、用户或环境的属性实施动态访问策略，从而实现高度适应性和上下文感知的安全性。</li><li><strong>文档级安全（DLS）和字段级安全（FLS）：</strong>这些功能可确保即使在检索的文档中，也只能看到授权部分，从而防止敏感信息外泄。</li><li><strong>与企业安全集成：</strong>与现有身份管理系统（如 LDAP、SAML、OIDC）无缝集成，在整个组织内执行一致的安全策略。</li></ul><p>通过将这些安全措施直接集成到上下文检索机制中，Elastic 成为了一个安全的看门人，确保人工智能代理在定义的数据边界内运行，防止未经授权的数据暴露，并维护数据隐私法规的合规性。这对于在处理机密或专有信息的人工智能代理系统中建立信任至关重要。</p><p>此外，通过在企业数据源上使用统一的数据速度层，还可以减轻代理工具在这些资源库上产生的意外临时查询负载。您只需在一个地方就能近乎实时地搜索所有内容，并在一个地方应用安全和治理控制。</p><h2>基于搜索的混合工具</h2><p>Elastic 平台的一些核心功能（<a href="https://www.elastic.co/blog/whats-new-elastic-9-2-0">更多</a>功能将陆续推出）能极大地促进情境工程的发展。这里最主要的是，该平台提供了多种实现方法，随着人工智能生态系统的发展，可以灵活地调整、改变和扩展方法。</p><h3>代理生成器介绍</h3><p>Elastic<a href="https://www.elastic.co/elasticsearch/agent-builder">Agent Builder</a>是我们在代理式人工智能工具领域的首次尝试，该工具可与您已存储在 Elastic 中的数据聊天。Agent Builder 提供了一个聊天界面，使用户能够在 Kibana 中创建和管理自己的代理和工具。它内置 MCP 和 A2A 服务器、编程 API 和一套预置系统工具，用于查询和探索 Elasticsearch 索引，以及从自然语言生成 ES|QL 查询。代理生成器允许您创建自定义工具，通过富有表现力的<a href="https://www.elastic.co/docs/reference/query-languages/esql">ES|QL</a>查询语法，瞄准并雕琢返回给代理的上下文数据。</p><p>你会问，ES|QL 如何执行混合搜索？核心功能是通过结合<a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/semantic-text"> semantic_text</a> 字段 类型和<a href="https://www.elastic.co/docs/reference/query-languages/esql/commands/fork"> </a><a href="https://www.elastic.co/docs/reference/query-languages/esql/commands/fuse">FORK/FUSE</a> 命令来实现的（FUSE 默认使用<a href="https://www.elastic.co/docs/reference/elasticsearch/rest-apis/reciprocal-rank-fusion"> RRF</a> 来合并每个分叉的结果）。下面是一个虚构产品搜索的简单示例：</p><pre><code>FROM products
| FORK
  (MATCH description "high performance gaming laptop" | EVAL search_type = "bm25"),
  (MATCH description_semantic "high performance gaming laptop" | EVAL search_type = "semantic")
| FUSE 
| LIMIT 20
| KEEP product_name, description, _score, search_type</code></pre><p>在上面的示例中，每个 FORK 分支都包含了 EVAL 子句，但<a href="https://www.elastic.co/docs/reference/query-languages/esql/commands/eval"> EVAL 子句并不是绝对必要的；包含 EVAL 子句只是为了演示如何跟踪给定结果是从哪种搜索模式返回的。</a></p><h3>搜索模板</h3><p>假设您想将自己的外部代理工具指向 Elastic 部署。您希望使用多级检索器或重新使用已开发的现有 DSL 语法，而不是 ES|QL，还希望能够控制查询接受的输入、执行搜索时使用的语法以及输出中返回的字段。<a href="https://www.elastic.co/docs/solutions/search/search-templates">搜索模板</a>允许用户为常用搜索模式定义预定义结构，从而提高检索数据的效率和一致性。这对与搜索应用程序接口交互的代理工具尤其有利，因为它们有助于规范模板代码，加快搜索逻辑的迭代速度。如果您需要调整其中任何一个因素，只需更新搜索模板，就可以实现更改。如果您正在寻找搜索模板与代理工具配合使用的示例，可以看看 Elasticsearch 实验室的博客 &quot;<a href="https://www.elastic.co/search-labs/blog/mcp-intelligent-search">MCP for intelligent</a>search&quot;，它在来自外部 MCP 服务器的工具调用背后使用了搜索模板。</p><h3>集成工作流程（FTW!）</h3><p>在我们新的代理人工智能世界中，最难驾驭的事情之一就是半自主、自导自演的 &quot;推理 &quot;代理的非确定性。情境工程是代理人工智能的一门关键学科：这些技术有助于将我们的代理可能得出的结论缩小到我们所知道的基本事实。即使有了高度准确和相关的上下文窗口（当我们跳出数字事实的范畴时），我们仍然缺少一点保证，即代理的反应是完全可重复和可靠的。</p><p>当您多次向代理运行同一个请求时，得到的答案可能<em>基本相同</em>，<em>只是</em>在响应上有那么一点点差别。对于简单的查询来说，这通常没什么问题，也许几乎不会引起注意，我们可以尝试使用上下文工程技术来塑造输出。但是，随着我们要求代理完成的任务变得越来越复杂，一个或多个子任务就更有可能带来差异，从而稍微改变最终结果。随着我们开始更多地依赖代理与代理之间的通信，这种情况可能会变得更糟，而这些差异也会累积起来。这再次说明，与我们的代理互动的工具需要非常灵活，并可进行调整，以精确瞄准上下文数据，而且它们应该以预期的输出格式做出响应。这也表明，在许多使用案例中，我们需要指导代理和工具之间的交互--这就是工作流的作用所在！</p><p>Elastic 将很快在平台核心中内置完全可定制的工作流程。这些工作流程将能以双向方式与代理和工具一起运行，因此工作流程将能呼叫代理和工具，代理和工具也能呼叫工作流程。将这些功能完全集成到同一搜索人工智能平台中，您的所有数据都将在该平台中存活，这将是一场变革，工作流程的潜力令人无比振奋！很快，很快就会到来！</p><h3>作为统一记忆库的弹性</h3><p>Elastic 是一个分布式数据平台，专为近乎实时的搜索而设计，因此能自然而然地为代理型人工智能系统提供长期记忆功能。通过内置的 Agent Builder 聊天体验，我们还可以跟踪和管理短期记忆和聊天记录。由于整个平台以 API 为先，因此利用 Elastic 作为平台来持久化工具的上下文输出（并能在稍后参考）非常容易，而这些输出可能会淹没代理的上下文窗口；这种技术在上下文工程领域有时被称为 &quot;<a href="https://www.anthropic.com/engineering/effective-context-engineering-for-ai-agents#:~:text=Agents%20can%20assemble%20understanding%20layer%20by%20layer%2C%20maintaining%20only%20what%27s%20necessary%20in%20working%20memory%20and%20leveraging%20note%2Dtaking%20strategies%20for%20additional%20persistence">记笔记</a>&quot;。</p><p>在同一个搜索平台上同时拥有短期和长期记忆会带来很多内在的好处：试想一下，我们可以将聊天记录和持久化的上下文回复作为语义影响因素的一部分，用于未来的聊天互动，或用于执行威胁分析，或用于创建从频繁重复的工具调用中自动生成的持久化数据产品......这种可能性是无穷无尽的！</p><h2>结论</h2><p>大型语言模型的出现改变了我们匹配内容的方式，也改变了我们查询数据的方法。在我们的世界里，人类正在迅速地从研究、背景考虑和逻辑推理来回答自己的问题，转变为这些步骤在很大程度上通过代理人工智能实现自动化。为了让我们相信所收到的生成答案，我们需要确保代理在生成答案时考虑了<em>所有</em> <em>最相关的</em>信息（包括主观相关性因素）。我们使代理人工智能值得信赖的主要方法是，通过 RAG 和上下文工程技术将检索额外上下文的工具落地，但这些工具如何进行<em>初始检索</em>对响应的准确性至关重要。</p><p>Elastic Search 人工智能平台提供了混合搜索的灵活性和优势，同时还提供了多项内置功能，有助于代理式人工智能的准确性、性能和可扩展性；换句话说，Elastic 是语境工程多个方面的绝佳平台！通过搜索平台将上下文检索标准化，我们在多个方面简化了代理工具的操作--与 &quot;放慢速度才能更快 &quot;的矛盾论类似，上下文生成层的简化意味着代理人工智能更快、更可信。</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[你知道的，为了情境--第二部分：代理人工智能和情境工程的必要性]]></title>
            <description><![CDATA[了解 LLM 如何向代理人工智能发展，从而增加了对上下文工程的需求，以解决 RAG 上下文限制和内存管理问题。]]></description>
            <link>https://www.elastic.co/search-labs/cn/blog/context-engineering-llm-evolution-agentic-ai</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/cn/blog/context-engineering-llm-evolution-agentic-ai</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Woody Walton]]></dc:creator>
            <pubDate>Tue, 18 Nov 2025 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/6153ce399e5498841ef2ffa5363a3e2d2dea726d-720x420.jpg?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>有了关于 LLM 如何改变信息检索底层过程的（相当广泛的）<a href="https://www.elastic.co/search-labs/blog/context-engineering-hybrid-search-evolution-agentic-ai">背景</a>知识，让我们来看看它们是如何改变我们查询数据的方式的。</p><h2>与数据交互的新方式</h2><p>生成式人工智能（genAI）和代理式人工智能的工作方式与传统搜索不同。过去，我们开始研究信息的方式是搜索（&quot;让我谷歌一下......&quot;），而基因人工智能和代理的发起行动通常是通过在聊天界面输入自然语言。聊天界面是与 LLM 的讨论，LLM 利用其语义理解能力将我们的问题转化为经过提炼的答案，这种经过总结的回答似乎来自一个对各种信息都有广泛了解的神谕。真正的卖点在于，法学硕士能够产生连贯、深思熟虑的句子，将浮现的知识点串联起来--即使不准确或完全是幻觉，也有其<a href="https://en.wikipedia.org/wiki/Truthiness">真实性</a>。</p><p>我们习惯于使用的老式搜索栏，可以看作是我们<strong><em>自己</em></strong>作为推理代理时使用的 RAG 引擎。现在，即使是互联网搜索引擎也正在将我们习以为常的 &quot;猎取和啄食 &quot;词条搜索体验转变为人工智能驱动的概述，通过对结果的总结来回答查询，帮助用户避免自己点击和评估单个结果。</p><h2>生成式人工智能&amp; RAG</h2><p>生成式人工智能试图利用其对世界的语义理解来解析聊天请求中表达的主观意图，然后利用其推理能力即时创建专家答案。生成式人工智能交互由几个部分组成：首先是用户的输入/询问，聊天会话中之前的对话可用作额外的上下文，然后是指导性提示，告诉 LLM 如何推理以及在构建回复时应遵循哪些程序。提示已从简单的&quot;&quot;像五岁小孩一样解释给我听 &quot;类型的指导发展到如何处理请求的完整细分。这些细目通常包括不同的部分，详细描述人工智能的角色/作用、生成前的推理/内部思维过程、客观标准、限制条件、输出格式、受众，以及有助于展示预期结果的示例。</p><p>除了用户查询和系统提示外，检索增强生成（RAG）还在所谓的 &quot;上下文窗口 &quot;中提供额外的上下文信息。RAG 是该架构的重要补充；我们用它来告知 LLM 在其对世界的语义理解中缺失的部分。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/5b9671d5d07d4caefde372bb3188000754a91eed-1470x746.png" style="width: 100%;" /><p>背景窗口在提供内容、地点和数量方面可能有点<a href="https://www.dbreunig.com/2025/06/22/how-contexts-fail-and-how-to-fix-them.html">挑剔</a>。当然，选择哪种上下文非常重要，但所提供上下文的信噪比以及窗口的长度也很重要。</p><h3>信息太少</h3><p>在查询、提示或上下文窗口中提供过少的信息可能会导致幻觉，因为 LLM 无法准确判断正确的语义上下文，从而生成响应。文件块大小的矢量相似性也存在问题--一个简短、简单的问题可能与我们矢量化知识库中丰富、详细的文件在语义上不一致。目前已开发出<a href="https://medium.com/data-science/how-to-use-hyde-for-better-llm-rag-retrieval-a0aa5d0e23e8">假设文档嵌入（HyDE）</a>等查询扩展技术，利用 LLM 生成比简短查询更丰富、更具表现力的假设答案。当然，这里的危险在于，假定的文件本身就是一种幻觉，它使法律硕士更加偏离正确的语境。</p><h3>信息太多</h3><p>就像我们人类一样，上下文窗口中过多的信息会让法律硕士不知所措，不知道哪些是重要部分。上下文溢出（或 &quot;<a href="https://research.trychroma.com/context-rot">上下文腐烂</a>&quot;）会影响生成式人工智能操作的质量和性能；它会极大地影响 LLM 的 &quot;注意力预算&quot;（其工作记忆），并稀释许多竞争标记的相关性。语境轮换 &quot;的概念还包括这样一个观察结果，即语言学习者往往有一种<a href="https://alexandrabarr.beehiiv.com/p/context-windows">位置偏差</a>--他们更喜欢语境窗口开头或结尾的内容，而不是中间部分的内容。</p><h3>分散注意力或相互冲突的信息</h3><p>上下文窗口越大，就越有可能包含多余或相互冲突的信息，从而分散 LLM 的注意力，使其无法选择和处理正确的上下文。在某种程度上，这就成了一个 &quot;垃圾进/垃圾出 &quot;的问题：只需将一组文档结果倒入上下文窗口，就能为 LLM 提供大量信息供其咀嚼（可能太多），但根据上下文的选择方式，更有可能渗入相互冲突或无关的信息。</p><h2>智能体 AI</h2><p>我告诉过你有很多内容要讲，但我们做到了--我们终于开始讨论代理人工智能话题了！代理式人工智能（Agentic AI）是 LLM 聊天界面的一种非常令人兴奋的新用法，它扩展了生成式人工智能（我们可以称之为 &quot;传统 &quot;人工智能吗？）的能力，即根据自身知识和您提供的上下文信息合成回复。随着生成式人工智能变得越来越成熟，我们意识到可以让 LLM 执行一定程度的任务和自动化操作，这些操作最初被归类为乏味的低风险活动，可以很容易地由人工进行检查/验证。在很短的时间内，最初的范围就扩大了：一个 LLM 聊天窗口现在可以成为一个火花，让一个人工智能代理去自主规划、执行、迭代评估和调整其计划，以实现指定的目标。代理可以访问其 LLM 自身的推理、聊天历史和思维记忆（比如说），他们还可以利用特定的工具来实现这一目标。我们现在看到的架构还允许一个顶级代理作为多个<a href="https://www.philschmid.de/the-rise-of-subagents"> 子代理</a> 的协调者，每个 子代理 都有自己的逻辑链、指令集、上下文和工具。</p><p>代理是大部分自动化工作流程的切入点：它们是自主的，能够与用户聊天，然后使用 &quot;逻辑 &quot;来决定有哪些工具可以帮助回答用户的问题。与代理相比，工具通常被认为是被动的，是为完成一种任务而构建的。工具可以执行的任务<em>类型</em>是无限的（这确实令人兴奋！），但工具执行的一项主要任务是收集上下文信息，供代理在执行工作流程时考虑。</p><p>作为一项技术，代理人工智能仍处于起步阶段，很容易患上法学硕士的注意力缺陷症--很容易忘记要求它做的事情，经常跑去做其他根本不在任务范围内的事情。在表面神奇的背后，LLM 的 &quot;推理 &quot;能力仍然是基于预测序列中下一个最有可能的标记。要使推理（或有朝一日的人工通用智能（AGI））变得可靠和值得信赖，我们需要能够验证，在获得正确、最新的信息时，它们会按照我们所期望的方式进行推理（也许还会给我们提供我们自己可能没有想到的更多信息）。要做到这一点，代理架构需要具备清晰的通信能力（协议），遵守我们赋予它们的工作流程和约束条件（护栏），记住它们在任务中的位置（状态），管理可用的内存空间，以及验证它们的响应是否准确并符合任务标准。</p><h2>用我能听懂的语言跟我说话</h2><p>在新的开发领域（尤其是在 LLM 领域），代理与工具之间的通信最初有很多方法，但很快就趋同于<a href="https://modelcontextprotocol.io/docs/getting-started/intro">模型上下文协议（MCP）</a>，将其作为事实上的标准。模型上下文协议的定义其实就在名字里--它是<strong> 模型</strong> 用来请求和接收 <strong>上下文</strong> 信息的<strong> 协议 。</strong>MCP 是 LLM 代理连接外部工具和数据源的通用适配器；它简化了应用程序接口并使之标准化，这样不同的 LLM 框架和工具就能轻松互操作。这就使得 MCP 成为一种支点，它介于协调逻辑和系统提示与发送给工具的操作之间，前者要求代理为实现其目标而自主执行，而后者则要求代理以更孤立的方式执行（至少与启动代理隔离）。</p><p>这个生态系统是如此之新，以至于每个扩展方向都像是一个新领域。我们有类似的协议用于代理与代理之间的交互 （Agent2Agent<a href="https://developers.googleblog.com/en/a2a-a-new-era-of-agent-interoperability/"> (A2A)</a> natch!），也有其他项目用于改进代理的推理记忆<a href="https://venturebeat.com/ai/new-memory-framework-builds-ai-agents-that-can-handle-the-real-worlds"> （ReasoningBank</a> ），为手头的工作选择最佳的 MCP 服务器<a href="https://arxiv.org/abs/2505.03275"> （RAG-MCP</a> ），以及使用语义分析 （ 如输入和输出的零点分类和模式检测）作为控制代理操作内容的<a href="https://openai.github.io/openai-guardrails-python/"> Guardrails</a> 。</p><p>您可能已经注意到，这些项目的根本目的都是为了提高返回到代理/人工智能上下文窗口的信息的质量和控制？虽然人工智能代理生态系统将继续发展更好地处理上下文信息（对其进行控制、管理和操作）的能力，但始终需要检索<em>最相关的</em>上下文信息，作为代理的研磨材料。</p><h2>欢迎使用情境工程！</h2><p>如果你熟悉生成式人工智能术语，你可能听说过 &quot;提示工程&quot;--在这一点上，它几乎是一门伪科学。提示工程用于找到最佳和最有效的方法，主动描述您希望 LLM 在生成响应时使用的行为。<a href="https://www.elastic.co/search-labs/blog/context-engineering-overview">上下文工程</a>&quot;将 &quot;提示工程 &quot;技术从代理侧扩展到 MCP 协议工具侧的可用上下文源和系统，并包括上下文管理、处理和生成等广泛主题：</p><ul><li><strong>上下文管理 </strong>- 与在长期运行和/或更复杂的代理工作流程中保持状态和上下文效率有关。对任务和工具的调用进行迭代规划、跟踪和协调，以实现代理的目标。由于代理工作的 &quot;注意力预算 &quot;有限，上下文管理主要涉及帮助完善上下文窗口的技术，以捕捉最全面和最重要的上下文信息（精确度与召回率！）。这些技术包括压缩、归纳，以及持续保留先前步骤或工具调用的上下文，以便在工作记忆中为后续步骤中的额外上下文留出空间。</li><li><strong>上下文处理 </strong>--对从不同来源获取的上下文进行整合、规范化或细化的逻辑步骤，希望这些步骤主要是程序性的，以便代理能够以某种统一的方式对所有上下文进行推理。底层工作是让所有来源（提示、RAG、记忆等）的上下文都能被代理尽可能高效地消耗掉。 </li><li>上下文<strong>生成 </strong>--如果上下文处理的目的是让代理可以使用检索到的上下文，那么上下文生成就赋予了代理随意请求和接收附加上下文信息的能力，但同时也有限制条件。</li></ul><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/4a8240e1eb078b2294b8d981b9caa8593589cac4-1600x900.png" style="width: 100%;" /><p>LLM 聊天应用程序的各种历时直接（有时以重叠的方式）映射到上下文工程的这些高级功能：</p><ul><li><strong>指令/系统提示</strong>--提示是生成式（或代理式）人工智能活动如何引导其思维实现用户目标的支架。提示本身就是一种语境；它们不仅仅是音调指令，还经常包含任务执行逻辑和规则，如 &quot;逐步思考 &quot;或 &quot;深呼吸&quot;，然后再做出回应，以验证答案是否完全满足用户的要求。最近的测试表明，标记语言在框定提示的不同部分时非常有效，但也要注意在过于模糊和过于具体之间调整指示；我们希望提供足够的指示，让 LLM 找到正确的上下文，但又不能过于规范，以至于错过意想不到的见解。</li><li><strong>短期记忆</strong>（状态/历史）--短期记忆主要是用户与 LLM 之间的聊天会话互动。这些信息有助于在现场会议中完善上下文，并可保存起来供今后检索和继续使用。 </li><li><strong>长时记忆</strong>--长时记忆应包含在多个时段都有用的信息。通过 RAG 访问的不仅仅是特定领域的知识库，最近的研究还利用以前的代理/生成式人工智能请求的结果，在当前的代理互动中进行学习和参考。在长期记忆领域，一些最有趣的创新与调整状态的<a href="https://steve-yegge.medium.com/introducing-beads-a-coding-agent-memory-system-637d7d92514a">存储和链接</a>方式有关，这样，代理就能从他们离开的地方继续前进。 </li><li><strong>结构化输出</strong>--认知需要花费精力，因此，即使拥有推理能力，LLM（就像人类一样）也希望在思考时花费更少的精力，这一点不足为奇。在没有定义好的应用程序接口或协议的情况下，有一个如何读取工具调用返回数据的地图（模式）是非常有用的。将 &quot;<a href="https://platform.openai.com/docs/guides/structured-outputs?lang=javascript">结构化输出 &quot;</a>作为代理框架的一部分，有助于使这些机器与机器之间的交互更快、更可靠，同时减少思维驱动的解析。</li><li><strong>可用工具</strong>- 工具可以做各种各样的事情，从收集额外信息（如向企业数据存储库或通过在线 API 发出 RAG 查询）到代表代理执行自动操作（如根据代理请求的标准预订酒店房间）。工具也可以是子代理，有自己的代理处理链。 </li><li><strong>检索增强生成（RAG）</strong>--我非常喜欢将 RAG 描述为 &quot;动态知识集成&quot;。如前所述，RAG 是一种提供 LLM 在接受训练时无法获得的额外信息的技术，或者说是重申我们认为对获得正确答案最重要的想法--与我们的主观疑问最相关的想法。</li></ul><h2>惊人的宇宙力量，微不足道的生活空间！</h2><p>代理人工智能有许多迷人而令人兴奋的新领域有待探索！我们仍有许多传统的数据检索和处理问题需要解决，但同时也面临着全新的挑战，这些挑战现在才在新的 LLM 时代暴露出来。我们今天要解决的许多紧迫问题都与情境工程有关，即如何在不占用有限工作记忆空间的前提下，为 LLM 提供所需的额外情境信息。</p><p>半自主代理可以使用一系列工具（和其他代理），其灵活性为人工智能的实施带来了许多新思路，我们很难想象会有什么不同的方法可以将这些碎片组合在一起。目前的大部分研究都属于上下文工程学领域，主要集中在构建能够处理和跟踪大量上下文的内存管理结构上，这是因为我们真正希望 LLM 能够解决的深度思考问题具有更高的复杂性和更长的多阶段思考步骤，在这些问题中，记忆极为重要。</p><p>该领域正在进行的许多实验都是为了找到最佳的任务管理和工具配置，以满足代理的需求。代理推理链中的每次工具调用都会产生累积成本，既包括执行工具功能所需的计算量，也包括对有限上下文窗口的影响。为 LLM 代理管理上下文的一些最新技术造成了意想不到的连锁效应，如 &quot;<a href="https://venturebeat.com/ai/ace-prevents-context-collapse-with-evolving-playbooks-for-self-improving-ai">上下文崩溃</a>&quot;，在这种情况下，压缩/汇总长期运行任务的累积上下文会造成<em>过多</em>损失。理想的结果是工具能够返回简洁准确的上下文，而不会让无关信息渗入宝贵的上下文窗口内存空间。</p><h3>太多/太多种可能性</h3><p>我们希望职责分离，并能灵活地重复使用工具/组件，因此创建专用的代理工具来连接特定的数据源是完全合理的--每种工具都可以专门查询一种类型的存储库、一种类型的数据流，甚至一种使用案例。但要注意：为了节省时间/金钱/证明某些事情是可行的，我们会受到强烈的诱惑，把 LLM 用作联盟工具......尽量不要这样做，我们以前<a href="https://www.elastic.co/pdf/elastic-distributed-not-federated-search.pdf">走过这条路</a>！联合查询就像一个 &quot;通用翻译器&quot;，它将输入的查询转换成远程存储库能理解的语法，然后以某种方式将多个来源的结果合理化为一个连贯的响应。联盟作为一种技术，在小范围内<em>效果</em> <em>还可以</em>，但在大范围内，特别是当数据是多模态的时候，联盟试图弥合的差距就太大了。</p><p>在代理世界中，代理将是联合器，而工具（通过 MCP）将是人工定义的与不同资源的连接。使用专用工具跨未连接的数据源进行访问，看似是在每次查询的基础上动态联合不同数据流的强大新方法，但使用工具向多个数据源提出相同的问题，最终可能会造成更多问题，而不是解决问题。每个数据源下面都可能有不同类型的存储库，每个存储库都有自己的数据检索、排序和安全功能。当然，资源库之间的差异或 &quot;阻抗不匹配 &quot;会增加处理负荷。它们还可能引入相互冲突的信息或信号，看似无关紧要的评分失准可能会严重影响对返回上下文的重视程度，并最终影响生成回复的相关性。</p><h3>计算机也很难进行上下文切换</h3><p>当你派出一名特工执行任务时，他们的首要任务往往是找到其可以访问的所有相关数据。就像人类一样，如果代理连接的每个数据源都给出了不同的分类回复，那么从检索到的内容中提取显著的上下文信息就会产生认知负荷（尽管不是完全相同的类型）。这需要时间/计算，而在代理逻辑链中，每一点都是累加的。由此得出的结论是，就像正在讨论的<a href="https://blog.cloudflare.com/code-mode/">MCP</a> 一样，大多数代理工具的行为应该更像应用程序接口（API）--具有已知输入和输出的孤立函数，经过调整以支持不同类型代理的需求。哎呀，我们甚至意识到，<a href="https://arxiv.org/html/2501.12372v5">语言学硕士需要上下文语境</a>--他们在连接语义点方面做得更好，尤其是在将自然语言翻译成结构化语法这样的任务中，当他们有模式可参考时（确实是 RTFM！）。</p><h2>第 7 局</h2><p>现在，我们已经介绍了<a href="https://www.elastic.co/search-labs/blog/context-engineering-hybrid-search-evolution-agentic-ai">LLM 对数据检索和查询的影响</a>，以及聊天窗口如何逐渐成为人工智能代理体验。让我们把这两个主题放在一起，看看如何利用新式搜索和检索功能来改进上下文工程的结果。进入<a href="https://www.elastic.co/search-labs/blog/context-engineering-hybrid-search-agentic-ai-accuracy">第三部分：混合搜索在情境工程中的威力</a>！</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[如何在 Azure AKS 上自动部署 Elasticsearch]]></title>
            <description><![CDATA[了解如何使用 AKS Automatic 和 ECK 在 Azure 上部署带有 Kibana 的 Elasticsearch，以实现部分托管的 Elasticsearch 设置配置。]]></description>
            <link>https://www.elastic.co/search-labs/cn/blog/elasticsearch-azure-aks-automatic-deployment</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/cn/blog/elasticsearch-azure-aks-automatic-deployment</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Eduard Martin]]></dc:creator>
            <pubDate>Fri, 14 Nov 2025 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/4d821659a606e04390b09215e9a0d32eb01f0d1b-854x489.png?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>本文是系列文章的一部分，我们将学习如何使用不同的基础架构安装 Elasticsearch。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/0a65ca8b62fd8a42d7751b8f4bf057e33d877304-940x458.png" style="width: 100%;" /><p>与基于 Marketplace 的弹性云解决方案相比，ECK 需要付出更多努力，但它比自己部署虚拟机更加自动化，因为 Kubernetes 操作员将负责系统协调和节点扩展。</p><p>这一次，我们将使用自动功能与 Azure Kubernetes 服务 (AKS) 配合工作。在其他文章中，您将学习如何使用<a href="https://www.elastic.co/search-labs/blog/azure-elasticsearch-vm-deployment">Azure VM</a>和<a href="https://www.elastic.co/search-labs/blog/deploy-elasticsearch-azure-marketplace">Azure Marketplace</a>。</p><h2>什么是 AKS 自动系统？</h2><p><a href="https://learn.microsoft.com/en-us/azure/aks/intro-aks-automatic">Azure Kubernetes 服务（AKS）可自动 </a>管理集群设置、动态分配资源并集成安全最佳实践，同时保持 Kubernetes 的灵活性，使开发人员能够在几分钟内从容器镜像转为部署应用程序。</p><p>AKS Automatic 消除了大部分集群管理开销，在简单性和灵活性之间取得了良好的平衡。正确的选择取决于您的使用情况，但如果您计划这样做，决定就会容易得多：</p><ul><li><strong>部署测试环境： </strong>部署快速而简单，是快速实验或短期集群的理想选择。</li><li><strong>无需严格的虚拟机、存储或网络要求即可工作： </strong>AKS Automatic 提供预定义的默认设置，因此，如果这些设置符合您的需求，就可以省去额外的配置。</li><li><strong>首次使用 Kubernetes： </strong>通过处理集群的大部分设置工作，AKS Automatic 可降低学习曲线，让团队专注于自己的应用。</li></ul><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/2e4c09b8c5e0ce5e8ea9c369626a373b7030a5ba-854x489.png" style="width: 100%;" /><p>对于 Elasticsearch，我们将使用<a href="https://www.elastic.co/docs/deploy-manage/deploy/cloud-on-k8s">Elastic Cloud on Kubernetes </a>(ECK)，它是官方的 Elastic Kubernetes 操作员，可以简化 Elastic Stack 的 Kubernetes 部署协调。</p><h2>如何设置 AKS 自动系统</h2><p>1.登录<a href="https://azure.microsoft.com/">Microsoft Azure 门户</a>。</p><p>2.在<strong>右上角， </strong>单击<strong> </strong>上的<strong> Cloud Shell</strong>按钮访问控制台，并从那里部署 AKS 群集。或者，您也可以使用<a href="https://learn.microsoft.com/en-us/azure/cloud-shell/overview">Azure 云外壳</a>。</p><p><strong><em>请记住，在教程中将项目 ID 更新为您的项目 ID。</em></strong></p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/0aa60605777c0a6e3aef8faa4e54388c2cb582c8-624x495.png" style="width: 100%;" /><p><em>打开 AKS 时的样子应该如上截图所示。</em></p><p>3.安装 aks-preview Azure CLI 扩展。该预览版允许我们在创建群集时选择<code>--sku automatic</code> ，从而启用 AKS 自动功能。</p><pre><code>az extension add --name aks-preview</code></pre><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/2e05ab67fc554c5fb5208683c179fdeaeadd95db-624x56.png" style="width: 100%;" /><p><em>如果看到此信息，说明 AKS 扩展已正确安装。</em></p><p>4.使用<code>az feature register</code> 命令注册<a href="https://learn.microsoft.com/en-us/azure/azure-app-configuration/concept-feature-management"> 功能标志</a></p><pre><code>az feature register --namespace Microsoft.ContainerService --name AutomaticSKUPreview</code></pre><p><em>您将看到我们刚刚创建的功能订阅的详细信息：</em></p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/3aca710c1f312ba91de461638e518386919ec722-801x138.png" style="width: 100%;" /><p>确认注册状态，直到从 &quot;<strong><em>正在注册</em></strong>&quot;变为 &quot;<strong><em>已注册</em></strong>&quot;。完成注册可能需要几分钟时间。</p><pre><code>az feature show --namespace Microsoft.ContainerService --name AutomaticSKUPreview</code></pre><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/186486b08ab8e1c372efaff50f10cbddeaf4e0cd-844x177.png" style="width: 100%;" /><p>运行<code>az provider register</code> 以传播更改。</p><pre><code>az provider register --namespace Microsoft.ContainerService</code></pre><p>5.创建资源组</p><p>资源组是要管理和部署的 Azure 资源的逻辑组。</p><pre><code>az group create --name elastic-resource --location eastus</code></pre><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/d1952269e97d94f914020754bd02702f9eafd037-770x212.png" style="width: 100%;" /><p>6.创建自动驾驶仪群集。我们将把它命名为<strong><em>myAKSAutomaticCluster </em></strong>，并使用刚刚创建的资源组。确保以下任何一种虚拟机大小都有<strong><em>16 个</em></strong>可用vCPU：<a href="https://learn.microsoft.com/en-us/azure/virtual-machines/sizes/general-purpose/dpsv5-series">Standard_D4pds_v5</a>、<a href="https://learn.microsoft.com/en-us/azure/virtual-machines/sizes/general-purpose/dldsv5-series">Standard_D4lds_v5</a>、<a href="https://learn.microsoft.com/en-us/azure/virtual-machines/sizes/general-purpose/dadsv5-series">Standard_D4ads_v5</a>、<a href="https://learn.microsoft.com/en-us/azure/virtual-machines/sizes/general-purpose/ddsv5-series">Standard_D4ds_v5</a>、<a href="https://learn.microsoft.com/en-us/azure/virtual-machines/sizes/general-purpose/ddv5-series">Standard_D4d_v5</a>、<a href="https://learn.microsoft.com/en-us/azure/virtual-machines/sizes/general-purpose/ddv4-series">Standard_D4d_v4</a>、<a href="https://learn.microsoft.com/en-us/azure/virtual-machines/sizes/general-purpose/dsv3-series">Standard_DS3_v2</a>、<a href="https://learn.microsoft.com/en-us/azure/virtual-machines/sizes/memory-optimized/dv2-dsv2-series-memory">Standard_DS12_v2</a>，以便 AKS 分配资源。</p><pre><code>az aks create \
    --resource-group elastic-resource \
    --name myAKSAutomaticCluster \
    --sku automatic \
    --generate-ssh-keys</code></pre><p><em>* 如果出现 <code>MissingSubscriptionRegistration</code> 错误，请带着缺失的订阅返回第 4 步。例如， <code>The subscription is not registered to use namespace &#x27;</code> <strong><code>microsoft.insights</code></strong></em> <em><code>&#x27;</code></em> 需要运行 <em><code>az provider register --namespace Microsoft.Insights.</code></em></p><p>按照交互式登录：</p><p><em>此时会出现一条要求运行 &quot;az login &quot;的信息。您必须运行该命令，然后等待。</em></p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/14c0238f755fe6347519e69d3cb28c0fa52ec044-775x203.png" style="width: 100%;" /><p>7.等待准备就绪。制作大约需要 10 分钟。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/eb00c3ad18f884f47db6645b196808ebec07c1fc-797x177.png" style="width: 100%;" /><p>8.配置 kubectl 命令行访问权限。</p><pre><code>az aks get-credentials --resource-group elastic-resource --name myAKSAutomaticCluster</code></pre><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/5136d72a5d455345b0b6205bb232c4bdf7762998-793x52.png" style="width: 100%;" /><p><em>请注意，我们安装的扩展正在启用 AKS Automatic。</em></p><p>9.确认节点已部署。</p><pre><code>kubectl get nodes</code></pre><p>您将看到一条禁止的错误信息；请复制错误信息中的用户 ID。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/d6c021fa54f4db00d2d795f5ba9b5a93376d03cd-818x47.png" style="width: 100%;" /><p>10.将用户添加到 AKS 访问控制中。</p><p>获取 AKS ID。复制命令输出。</p><pre><code>az aks show --resource-group elastic-resource  --name myAKSAutomaticCluster --query id --output tsv</code></pre><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/22a1cdc538bd379812a752c6a368a0651000abb8-810x36.png" style="width: 100%;" /><p>使用 AKS ID 和用户的主要 ID 创建角色分配。</p><pre><code>az role assignment create --role "Azure Kubernetes Service RBAC Cluster Admin" --assignee <PRINCIPAL_ID> --scope <AKS_ID></code></pre><p>11.尝试再次确认节点已部署。</p><pre><code>kubectl get nodes</code></pre><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/c9b3a5c1cc540ef732c3e7f60b0a973bdbd0b6fd-617x99.png" style="width: 100%;" /><p>12.安装 Kubernetes 上的弹性云（ECK）操作员。</p><pre><code># Install ECK Custom Resource Definitions
kubectl create -f https://download.elastic.co/downloads/eck/2.16.1/crds.yaml

# Install the ECK operator
kubectl apply -f https://download.elastic.co/downloads/eck/2.16.1/operator.yaml</code></pre><p>13.让我们使用默认值创建一个单节点 Elasticsearch 实例。</p><pre><code>cat <<EOF | kubectl apply -f -
apiVersion: elasticsearch.k8s.elastic.co/v1
kind: Elasticsearch
metadata:
  name: quickstart
spec:
  version: 9.0.0
  nodeSets:
  - name: default
    count: 1
    config:
      node.store.allow_mmap: false
EOF</code></pre><p>我们禁用<code>nmap</code> 是因为默认 AKS 机器的<code>vm.max_map_count</code> 值过低。不建议在生产中禁用它，但可以增加<code>vm.max_map_count</code> 的值。您可以<a href="https://www.elastic.co/docs/deploy-manage/deploy/cloud-on-k8s/virtual-memory">在这里</a>阅读更多关于如何做到这一点的信息。</p><p>14.我们也来部署一个 Kibana 单节点集群。对于 Kibana，我们将添加一个负载平衡器，它将为我们提供一个外部 IP，我们可以用它从我们的设备访问 Kibana。</p><pre><code>cat <<EOF | kubectl apply -f -
apiVersion: kibana.k8s.elastic.co/v1
kind: Kibana
metadata:
  name: quickstart
spec:
  version: 9.0.0
  http:
    service:
      spec:
        type: LoadBalancer
  count: 1
  elasticsearchRef:
    name: quickstart
EOF</code></pre><p>默认情况下，AKS Automatic 会将负载平衡器配置为公共负载平衡器；您可以通过设置元数据注释来更改行为：</p><p><code>service.beta.kubernetes.io/azure-load-balancer-internal: &quot;true&quot;</code></p><p>15.检查 pod 是否正在运行。</p><pre><code>kubectl get pods</code></pre><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/213a897176c0af6cea19c7c777cfaf8734e3ee6e-616x84.png" style="width: 100%;" /><p>16.您还可以运行<code>kubectl get elasticsearch</code> 和<code>kubectl get kibana</code> 获取更具体的统计信息，如 Elasticsearch 版本、节点和健康状况。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/c84988e725ef892eddd8fb7e5a03d58c35a8f9d6-470x62.png" style="width: 100%;" /><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/452dd03d314cd00c8a3c19e19862b968592a0435-415x62.png" style="width: 100%;" /><p>17.获取您的服务。</p><pre><code>kubectl get svc</code></pre><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/b2d3e8f368be22b89aa2ed4d4d514f97dd6cbabd-624x115.png" style="width: 100%;" /><p>这将在 EXTERNAL-IP 下显示 Kibana 的<a href="https://www.elastic.co/docs/deploy-manage/deploy/cloud-on-k8s/accessing-services">外部 URL</a>。负载平衡器的调配可能需要几分钟时间。<strong><em>复制 EXTERNAL-IP 的值。</em></strong></p><p>18.获取 &quot;elastic &quot;用户的 Elasticsearch 密码：</p><pre><code>kubectl get secret quickstart-es-elastic-user -o=jsonpath='{.data.elastic}' | base64 --decode</code></pre><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/bd74537f8c35c4e027c518913fdb0a0524621d56-624x31.png" style="width: 100%;" /><p>19.通过浏览器<strong>访问 Kibana</strong>：</p><p>a.url: https://&lt;EXTERNAL_IP&gt;:5601</p><p>b.用户名：elastic</p><p>c.密码：c44A295CaEt44D6xIzN6Zs5m（来自上一步）</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/a601112527d80721b292328ed8da58386d2837eb-463x503.png" style="width: 100%;" /><p>20.从浏览器访问 Elastic Cloud 时，您将看到欢迎屏幕。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/f91e71fa961d215a8d886601d1a9fc5c452ce329-1999x1256.png" style="width: 100%;" /><p>如果要更改 Elasticsearch 集群规格，如更改或调整节点大小，可以使用新设置再次应用 YML 清单：</p><pre><code>cat <<EOF | kubectl apply -f -
apiVersion: elasticsearch.k8s.elastic.co/v1
kind: Elasticsearch
metadata:
  name: quickstart
spec:
  version: 9.0.0
  nodeSets:
    - name: default
      count: 2
      config:
        node.store.allow_mmap: false
      podTemplate:
        spec:
          containers:
            - name: elasticsearch
              resources:
                requests:
                  memory: 1.5Gi
                  cpu: 2
                limits:
                  memory: 1.5Gi
                  cpu: 2
EOF</code></pre><p>在本例中，我们将增加一个节点，并修改 RAM 和 CPU。如您所见，现在<code>kubectl get elasticsearch</code> 显示了 2 个节点：</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/4b139a476b50933d45d99e09479112817964f76a-624x60.png" style="width: 100%;" /><p>Kibana 也是如此：</p><pre><code>cat <<EOF | kubectl apply -f -
apiVersion: kibana.k8s.elastic.co/v1
kind: Kibana
metadata:
  name: quickstart
spec:
  version: 9.0.0
  http:
    service:
      spec:
        type: LoadBalancer
  count: 1
  elasticsearchRef:
    name: quickstart
  podTemplate:
    spec:
      containers:
        - name: kibana
          env:
            - name: NODE_OPTIONS
              value: "--max-old-space-size=1024"
          resources:
            requests:
              memory: 0.5Gi
              cpu: 0.5
            limits:
              memory: 1Gi
              cpu: 1
EOF</code></pre><p>我们可以调整容器的 CPU/RAM，也可以调整<a href="https://nodejs.org/">Node.js </a>的内存使用量<a href="https://nodejs.org/api/cli.html#--max-old-space-sizesize-in-mib">（max-old-space-size</a>）。</p><p>请记住，<a href="https://www.elastic.co/docs/deploy-manage/deploy/cloud-on-k8s/volume-claim-templates">现有的批量索赔不能缩减</a>。应用更新后，操作员将在最短的时间内完成更改。</p><p>测试完成后，请记住删除群集，以避免不必要的成本。</p><pre><code>az aks delete --name myAKSAutomaticCluster --resource-group elastic-resource</code></pre><h2>结论</h2><p>使用 Azure AKS Automatic 和 ECK 可为部署 Elasticsearch 和 Kibana 提供一个平衡的解决方案：它降低了操作复杂性，确保了自动扩展和更新，并充分利用了 Kubernetes 的灵活性。这种方法非常适合需要可靠、可重复和可维护的部署流程，而无需手动管理每个基础架构细节的团队，使其成为测试和生产环境的实用选择。</p><h2>后续步骤</h2><p>如果您想了解有关 Kubernetes 的更多信息，可点击此处查看官方文档：</p><ul><li><a href="https://www.elastic.co/docs/deploy-manage/deploy/cloud-on-k8s">Kubernetes 上的弹性云 | 弹性文档</a></li><li><a href="https://learn.microsoft.com/en-us/azure/aks/intro-aks-automatic">Azure Kubernetes 服务 (AKS) 自动化介绍（预览版）</a></li><li><a href="https://azure.github.io/AKS/2024/05/22/aks-automatic">AKS Automatic - AKS 工程博客</a></li></ul>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[在 Elasticsearch 中使用 A2A 协议和 MCP 创建 LLM Agent 新闻室：第一部分]]></title>
            <description><![CDATA[在一个实际的新闻编辑室案例中探索 A2A 协议和 MCP 的概念，在这个案例中，专门的 LLM 代理合作研究、撰写、编辑和发布新闻文章。]]></description>
            <link>https://www.elastic.co/search-labs/cn/blog/a2a-protocol-mcp-llm-agent-newsroom-elasticsearch</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/cn/blog/a2a-protocol-mcp-llm-agent-newsroom-elasticsearch</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Justin Castilla]]></dc:creator>
            <pubDate>Thu, 13 Nov 2025 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/9f938d8e2f0fdf7509edf028816c48bdbc8b3fc7-1600x900.png?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<h2>引言</h2><p>当前由 LLM 支持的系统正在迅速发展，超越了单一模型应用，成为复杂的网络，其中专门的代理共同完成现代计算前所未有的任务。随着这些系统的复杂性不断增加，使代理通信和工具访问成为可能的基础设施成为开发的重点。为满足这些需求，出现了两种互补的方法：用于多代理协调的<strong>代理2代理（A2A）</strong>协议，以及用于标准化工具和资源访问的<strong>模型上下文协议（MCP）</strong>。</p><p>了解在什么情况下可以同时使用和不使用这两种方法，会对应用程序的可扩展性、可维护性和有效性产生重大影响。本文以数字新闻编辑室为例，探讨了<strong>A2A</strong>的概念和实现方法，在数字新闻编辑室中，专门的 LLM 代理合作研究、撰写、编辑和发布新闻文章。</p><p>我们将<a href="https://github.com/justincastilla/elastic-newsroom/tree/main">在</a>文章最后的第 5 部分探讨 A2A 的具体应用实例。</p><h3>准备工作</h3><p><a href="https://github.com/justincastilla/elastic-newsroom/tree/main">资源库</a>由 A2A 代理的 Python 实现组成。Flask 提供了一个 API 服务器，以及一个名为 Event Hub 的自定义 Python 消息传递服务，用于路由日志和 UI 更新消息。最后，还提供了一个 React UI，用于独立使用新闻编辑室的功能。所有内容都包含在一个 Docker 镜像中，以便于实施。如果您想直接在机器上运行服务，则需要确保安装了这些技术：</p><p>语言和运行时</p><ul><li>Python 13.12 - 核心后端语言</li><li>Node.js 18+ - 可选 React UI</li></ul><p>核心框架和 SDKS：</p><ul><li>A2A SDK 0.3.8 - Agent 协调与通信</li><li>Anthropic SDK--克劳德集成人工智能生成器</li><li>Uvicorn - 用于运行代理的 ASGI 服务器</li><li>FastMCP 2.12.5+ - MCP 服务器实施</li><li>React 18.2 - 前端用户界面框架</li></ul><p>数据&amp; 搜索</p><ul><li>Elasticsearch 9.1.1+- 文章索引和搜索</li></ul><p>Docker 部署（可选，但建议使用）</p><ul><li>Docker 28.5.1+</li></ul><h2>第 1 部分：什么是 Agent2Agent（A2A）？</h2><h3>定义和核心概念</h3><div style="display:none">Unknown block type "block", specify a component for it in the `components.types` option</div><p><strong>官方规格</strong> <a href="https://a2a-protocol.org/latest/specification/">：https://a2a-protocol.org/latest/specification/</a></p><h3>起源与进化</h3><p>Agent2Agent 通信或多代理系统的概念源于<a href="https://en.wikipedia.org/wiki/Multi-agent_system">几十年</a>前的分布式系统、微服务和多代理研究。分布式人工智能的早期工作为能够进行协商、协调和协作的代理奠定了基础。这些早期系统专门用于大规模<a href="https://www.jasss.org/5/1/7.html">社会模拟</a>、<a href="https://arxiv.org/html/2410.09403v1">学术研究</a>和<a href="https://www.researchgate.net/publication/334765661_Generation_Expansion_Planning_Considering_Investment_Dynamic_of_Market_Participants_Using_Multi-agent_System">电网管理</a>。</p><p>在谷歌和更广泛的人工智能研究界的支持下，随着 LLM 的出现和运行成本的降低，多代理系统开始进入 &quot;专业消费者 &quot;市场。现在，A2A 协议被称为 Agent2Agent 系统，它已发展成为一个现代标准，专为多个大型语言模型协调工作和任务的时代而设计。</p><p>A2A 协议将一致的标准和原则应用于 LLM 连接和通信的交互点，从而确保代理之间的无缝通信和协调。这种标准化使来自不同开发商、使用不同底层模型的代理能够有效地协同工作。</p><p>通信协议并非新生事物，在互联网上进行的几乎所有数字交易中都有广泛的应用。如果您键入<a href="https://www.elastic.co/search-labs">https://www.elastic.co/search-labs</a>在浏览器中访问这篇文章时，很有可能 TCP/IP、HTTP 传输和 DNS 查询协议都已执行，从而确保我们获得一致的浏览体验。</p><h3>主要特点</h3><p>A2A 系统建立在几个基本原则之上，以确保通信顺畅。以这些原则为基础，可以确保基于不同 LLM、框架和编程语言的不同代理都能无缝互动。</p><p>以下是四项主要原则：</p><ul><li><strong>信息传递</strong>：代理通过具有明确属性和格式的结构化信息进行通信</li><li><strong>协调</strong>：代理通过相互委派任务和管理依赖关系来协调复杂的工作流程，而不会阻塞其他代理</li><li><strong>专业化</strong>：每个代理都专注于某一特定领域或能力，成为该领域的专家，并根据技能组合完成任务</li><li><strong>分布式状态</strong>：状态和知识分布在各个代理之间，而不是集中在一起，代理之间能够相互更新任务状态和部分回报（工件）的进展情况</li></ul><h3>新闻编辑室运行范例</h3><p>试想一个由人工智能代理驱动的数字新闻编辑室，每个代理都擅长新闻业的不同方面：</p><ul><li><strong>新闻主管</strong>（协调员/客户）：分配报道任务并监督工作流程</li><li><strong>记者代理</strong>：根据研究和采访撰写文章</li><li><strong>研究员代理</strong>：收集事实、统计数据和背景信息</li><li><strong>档案代理</strong>：使用 Elasticsearch 搜索历史文章并确定趋势</li><li><strong>编辑代理</strong>：对文章的质量、风格和搜索引擎优化进行审核</li><li><strong>发布者代理</strong>：通过 CI/CD 将批准的文章发布到博客平台上</li></ul><p>当新闻主管指派一篇关于<em>可再生能源应用的</em>报道时，记者需要研究员收集统计数据，编辑需要审阅草稿，出版商需要出版最终稿件。这种协调是通过 A2A 协议进行的。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/cc0760dbd74c49b92fa00dafbb8c2e8740eb70b6-963x693.png" style="width: 100%;" /><h2>第 2 节：了解 A2A 架构</h2><h3>客户代理和远程代理角色</h3><p>在 A2A 架构中，代理主要扮演两种角色。<strong>客户代理</strong>负责制定任务并将任务传达给系统中的其他代理。它能识别远程代理及其能力，并利用这些信息就任务授权做出明智的决策。客户代理负责协调整个工作流程，确保任务分配得当，系统朝着目标前进。</p><p>而<strong>远程代理</strong>则负责执行客户委托的任务。它根据请求提供信息或采取具体行动，但不会独立发起行动。远程代理还可以根据需要与其他远程代理进行通信，以履行其指定职责，从而创建一个具有专业能力的协作网络。</p><p>在我们的新闻编辑室，新闻主管充当客户代理，而记者、研究员、编辑和出版商则是远程代理，负责响应请求并相互协调。</p><h3>A2A 核心能力</h3><p>A2A 协议定义了几种实现多代理协作的功能：</p><h4>1.发现</h4><p>A2A 服务器必须公布其功能，以便客户知道何时以及如何利用它们完成特定任务。这可以通过描述代理能力、输入和输出的代理卡--JSON 文档来实现。代理卡在一致的知名端点（如推荐的<code>/.well-known/agent-card.json</code> 端点）上提供，允许客户在启动协作之前发现并查询代理的能力。</p><p>以下是 Elastic 定制存档代理&quot;Archie Archivist&quot; 的代理卡示例。请注意，Elastic 等软件提供商会托管其 A2A 代理，并提供一个 url 供访问：</p><pre><code>{
  "name": "Archie Archivist",
  "description": "Helps find historical news documents in the Elasticsearch Index of archived news articles and content.",
  "url": "https://xxxxxxxxxxxxx-abc123.kb.us-central1.gcp.elastic.cloud/api/agent_builder/a2a/archive-agent",
  "provider": {
    "organization": "Elastic",
    "url": "https://elastic.co"
  },
  "version": "0.1.0",
  "protocolVersion": "0.3.0",
  "preferred_transport": "JSONRPC",
  "documentationURL": "https://www.elastic.co/docs/solutions/search/agent-builder/a2a-server"
  "capabilities": {
    "streaming": false,
    "pushNotifications": false,
    "stateTransitionHistory": false
  },
  "skills": [
    {
      "id": "platform.core.search",
      "name": "platform.core.search",
      "description": "A powerful tool for searching and analyzing data within your Elasticsearch cluster.",
      "inputModes": ["text/plain", "application/json"],
      "outputModes": ["text/plain", "application/json"]
    },
    {
      "id": "platform.core.index_explorer",
      "name": "platform.core.index_explorer",
      "description": "List relevant indices, aliases and datastreams based on a natural language query.",
      "inputModes": ["text/plain", "application/json"],
      "outputModes": ["text/plain", "application/json"]
    }
  ],
  "defaultInputModes": ["text/plain"],
  "defaultOutputModes": ["text/plain"]
}</code></pre><p>该代理卡揭示了 Elastic 档案代理的几个重要方面。该代理将自己定位为&quot;Archie Archivist&quot; ，并明确说明了自己的目的：帮助在 Elasticsearch 索引中查找历史新闻文档。该卡指定了提供商（Elastic）和协议版本（0.3.0），以确保与其他 A2A 兼容代理的兼容性。最重要的是，<code>skills</code> 数组列举了该代理提供的具体功能，包括强大的搜索功能和智能索引探索。每种技能都定义了它所支持的输入和输出模式，使客户能够准确了解如何与该代理进行通信。该代理源于 Elastic 的代理生成器服务，它提供了一套本地 LLM 支持的工具和 API 端点，用于与数据存储对话，而不仅仅是从存储中检索。可<a href="https://www.elastic.co/docs/solutions/search/agent-builder/a2a-server">在此处</a>访问 Elasticsearch 中的 A2A 代理。</p><h4>2.谈判</h4><p>客户和代理需要就交流方式达成一致--无论互动是通过文本、表单、iframe 还是音频/视频进行，以确保适当的用户互动和数据交换。这种协商发生在代理合作的开始阶段，并确立了整个工作流程中的交互协议。例如，语音客户服务代理可能会协商通过音频流进行通信，而数据分析代理可能更喜欢结构化的 JSON。谈判过程可确保双方以适合自身能力和当前任务要求的形式有效交换信息。</p><p>上述 JSON 代码段中列出的功能都有输入和输出模式；这些模式设定了如何与其他代理交互。</p><h4>3.任务和状态管理</h4><p>在整个任务执行过程中，客户端和代理需要有机制来交流任务状态、变化和依赖关系。这包括管理任务从创建、分配到进度更新和状态更改的整个生命周期。典型的状态包括待处理、进行中、已完成或失败状态。系统还必须跟踪任务之间的依赖关系，以确保在依赖任务开始之前完成前提工作。错误处理和重试逻辑也是必不可少的组成部分，可让系统从容地从故障中恢复，并继续朝着主要目标前进。</p><p>任务信息示例：</p><pre><code>{
  "message_id": "msg_789xyz",
  "message_type": "task_request",
  "sender": "news_chief",
  "receiver": "researcher_agent",
  "timestamp": "2025-09-30T10:15:00Z",
  "payload": {
    "task_id": "task_456abc",
    "capability": "fact_gathering",
    "parameters": {
      "query": "renewable energy adoption rates in Europe 2024",
      "sources": ["eurostat", "iea", "ember"],
      "depth": "comprehensive"
    },
    "context": {
      "story_id": "story_123",
      "deadline": "2025-09-30T18:00:00Z",
      "priority": "high"
    }
  }
}</code></pre><p>这个任务信息示例展示了 A2A 通信的几个关键方面。</p><ul><li><strong>信息结构</strong>包括元数据，如唯一的信息标识符、发送的信息类型、发送方和接收方标识，以及用于跟踪和调试的时间戳。</li><li><strong>有效载荷</strong>包含实际的任务信息，指明远程代理正在调用的功能，并提供执行该功能所需的参数。</li><li><strong>上下文</strong>部分提供了更多信息，帮助接收代理了解更广泛的工作流程，包括截止日期和优先级，告知代理应如何分配资源和安排工作。</li></ul><h4>4.合作</h4><p>客户端和代理<strong>必须</strong>支持动态但有条理的交互，使代理能够要求客户端、其他代理或用户提供说明、信息或子操作。这就创造了一个协作环境，代理可以在初始指令不明确时提出后续问题，要求提供更多的背景信息以做出更好的决策，将子任务委托给其他具有更合适专业知识的代理，并在继续执行完整任务之前提供中间结果以获得反馈。这种多向沟通可确保代理商不是孤立地工作，而是参与到持续的对话中，从而取得更好的成果。</p><h3>分布式点对点通信</h3><p>A2A 实现了分布式通信，其中代理可能由不同的组织托管，一些代理由内部维护，另一些则由第三方服务提供。这些代理可以在不同的基础设施上运行，可能跨越多个云提供商或内部数据中心。它们可能使用不同的底层 LLM，一些代理采用 GPT 模型，另一些采用 Claude 模型，还有一些采用开源替代模型。代理甚至可以跨越不同的地理区域运行，以符合数据主权要求或减少延迟。尽管存在这种多样性，但所有代理都同意使用共同的通信协议来交换信息，从而确保了互操作性，而不管实施细节如何。这种分布式架构为系统的构建和部署提供了灵活性，使企业能够根据自身的具体需求，混合和匹配最佳的代理和基础设施。</p><p>这就是新闻编辑室应用程序的最终架构：</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/82e01a0d9746038eafd69d11177042b5390507ae-1600x838.png" style="width: 100%;" /><h2>第 3 节：模型上下文协议（MCP）</h2><h3>定义和目的</h3><p>模型上下文协议（MCP）是 Anthropic 开发的一种标准化协议，旨在通过用户定义的工具、资源和提示，以及其他补充代码库增添的内容，来增强单个 LLM 的功能和能力。MCP 在语言模型和它们有效完成任务所需的外部资源之间提供了一个通用接口。<a href="https://www.elastic.co/search-labs/blog/mcp-current-state">本文</a>通过用例、新兴趋势和 Elastic 自身的实施，概述了 MCP 的现状。</p><h3>MCP 核心概念</h3><p>MCP 采用客户服务器架构，由三个主要部分组成：</p><ul><li><strong>客户端：</strong>连接到 MCP 服务器以访问其功能的应用程序（如 Claude Desktop 或自定义 AI 应用程序）。</li><li><strong>服务器</strong>：向语言模型提供资源、工具和提示的应用程序。每个服务器都专门提供对特定功能或数据源的访问。<ul><li><strong>工具</strong>：用户定义的函数，模型可调用这些函数进行操作，如搜索数据库、调用外部应用程序接口或对数据执行转换等。</li><li><strong>资源：</strong>模型可以读取的数据源，提供动态或静态数据，并通过 URI 模式访问（类似于 REST 路由）。</li><li><strong>提示： </strong>可重复使用的提示模板，带有变量，可指导模型完成特定任务。</li></ul></li></ul><h3>请求-响应模式</h3><p>MCP 采用熟悉的请求-响应交互模式，类似于 REST API。客户端（LLM）请求资源或调用工具，然后 MCP 服务器处理请求并返回结果，LLM 利用该结果继续执行任务。与点对点代理通信相比，这种带有外围服务器的集中模式提供了一种更简单的集成模式。</p><h3>新闻编辑室中的 MCP</h3><p>在我们的新闻编辑室示例中，各个代理使用 MCP 服务器访问他们需要的工具和数据：</p><ul><li><strong>研究员代理</strong>使用：<ul><li>新闻 API MCP 服务器（访问新闻数据库）</li><li>事实核查 MCP 服务器（根据可信来源核查声明）</li><li>学术数据库 MCP 服务器（学术文章和研究）</li></ul></li><li><strong>记者代理</strong>用途：<ul><li>风格指南 MCP 服务器（新闻编辑室写作标准）</li><li>模板 MCP 服务器（文章模板和格式）</li><li>图片库 MCP 服务器（图片库照片和图形）</li></ul></li><li><strong>编辑器代理</strong>使用：<ul><li>语法检查程序 MCP 服务器（语言质量工具）</li><li>剽窃检测 MCP 服务器（原创性验证）</li><li>搜索引擎优化分析 MCP 服务器（标题和关键词优化）</li></ul></li><li><strong>出版商代理</strong>使用：<ul><li>内容管理系统 MCP 服务器（内容管理系统 API）</li><li>CI/CD MCP 服务器（部署管道）</li><li>分析 MCP 服务器（跟踪和监控）</li></ul></li></ul><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/b67311e3b58b27f9eb1b42a7b1dbad47ef3be4ad-808x535.png" style="width: 100%;" /><h2><br/>第 4 部分：架构比较</h2><h3>何时使用 A2A</h3><p>A2A 架构在<strong>需要真正多代理协作的场景中</strong>表现出色。需要协调的多步骤工作流从 A2A 中受益匪浅，尤其是当任务涉及多个连续或并行步骤、需要迭代和改进的工作流以及需要检查点和验证的流程时。在我们的新闻编辑室示例中，报道工作流程要求记者撰写，但如果对某些事实的信心不足，可能需要返回研究员，然后再返回编辑，最后返回出版商。</p><p><strong>跨越多个领域的特定领域专业化</strong>是 A2A 的另一个强大用例。当需要不同领域的多位专家来完成一项更大的任务时，每个代理都会带来深厚的领域知识和针对不同方面的专门推理能力，A2A 提供了建立这些联系所需的协调框架。新闻编辑室完美地体现了这一点：研究员擅长信息收集，记者擅长写作，编辑擅长质量控制--每个人都有自己独特的专长。</p><p>对自主代理行为的需求使得 A2A 尤其有价值。在 A2A 架构中，能够<strong> 根据不断变化的条件做出独立决策、表现出积极主动行为并能动态适应工作流程要求的</strong>代理可茁壮成长。专业化功能的横向扩展是另一个关键优势--多个专业化代理协同工作，而不是只有一个万能代理，同一代理的多个实例可以异步处理子任务。例如，在我们的新闻编辑室报道突发新闻时，多名记者代理可能会同时从不同角度报道同一新闻。</p><p>最后，需要真正多代理协作的任务是 A2A 的理想选择。这包括<a href="https://arxiv.org/abs/2404.18796">法律硕士即评审团的评估</a>机制、建立共识和投票系统，以及<strong>需要多角度</strong>达成最佳结果的协作式问题解决方法。</p><h3>何时使用 MCP</h3><p>模型上下文协议是扩展单一人工智能模型功能的理想选择。当单个人工智能模型需要访问多个工具和数据源时，MCP 提供了完美的解决方案，集中式推理与分布式工具和直接的工具集成相结合。在我们的新闻编辑室示例中，研究员代理（一种模式）需要访问多个数据源，包括新闻 API、事实核查服务和学术数据库--所有这些都通过标准化的 MCP 服务器访问。</p><p>当工具集成的广泛共享和可重用性变得非常重要时，标准化工具集成就成了优先事项。MCP 凭借其预构建的 MCP 服务器生态系统大放异彩，大大缩短了常见集成的开发时间。当需要简单性和可维护性时，MCP 的请求-响应模式是开发人员所熟悉的，比分布式系统更容易理解和调试，操作复杂性也更低。</p><p>最后，软件供应商通常会提供 MCP，以方便与其系统进行远程通信。这些由供应商提供的 MCP 服务器大大缩短了入网和开发时间，同时为专有系统提供了标准化接口，使集成比定制 API 开发更加简单。</p><h3>何时同时使用两种方法（A2A ❤️ 的 MCP）</h3><p><a href="https://a2a-protocol.org/latest/topics/a2a-and-mcp/">正如 A2A 有关 MCP 集成的文档</a> 所指出的，许多复杂的系统都能从 A2A 和 MCP 的 结合中受益。既需要协调又需要标准化的系统是混合方法的理想选择。A2A 处理代理协调和工作流程协调，而 MCP 则为单个代理提供工具访问。在我们的新闻编辑室示例中，代理通过 A2A 进行协调；工作流程从记者到研究员，再到编辑，最后到出版商。不过，每个代理都使用 MCP 服务器来管理其专用工具，从而实现了干净利落的架构分离。</p><p>多个专门的代理，每个都使用 MCP 进行工具访问，这代表了一种常见的模式，即代理协调层由 A2A 处理，工具访问层由 MCP 管理。这种明确的分工使系统更容易理解和维护。</p><p>将这两种方法结合起来的好处是巨大的。您可以获得多代理系统的组织优势，包括专业化、自主性和并行处理，同时还可以享受 MCP 的标准化和生态系统优势，如工具集成和资源访问。代理协调（A2A）和资源访问（MCP）之间有明确的分离，而且重要的是，A2A 不需要单独用于 API 访问等较小的任务，MCP 可以高效地处理这些任务，而不需要多代理协调的开销。</p><p><strong>常见问题：A2A 与 MCP--使用案例</strong></p>
            <table style="border-collapse: collapse; margin: 20px 0;">
              <tbody>
                
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">功能</td><td style="border: 1px solid #ccc; padding: 4px;">Agent2Agent (A2A)</td><td style="border: 1px solid #ccc; padding: 4px;">模型上下文协议（MCP）</td><td style="border: 1px solid #ccc; padding: 4px;">混合型（A2A + MCP）</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">首要目标</td><td style="border: 1px solid #ccc; padding: 4px;">多代理协调：使专业代理团队能够在复杂的多步骤工作流程中协同工作。</td><td style="border: 1px solid #ccc; padding: 4px;">单一代理增强：利用外部工具、资源和数据扩展单一 LLM/Agent 的能力。</td><td style="border: 1px solid #ccc; padding: 4px;">综合实力：A2A 负责团队的工作流程，而 MCP 则为每个团队成员提供工具。</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">新闻编辑室团队范例</td><td style="border: 1px solid #ccc; padding: 4px;">工作流程链：新闻主管 → 记者 → 研究员 → 编辑 → 出版商。这是协调层。</td><td style="border: 1px solid #ccc; padding: 4px;">单个代理的工具：记者代理访问样式指南服务器和模板服务器（通过 MCP）。这是工具访问层。</td><td style="border: 1px solid #ccc; padding: 4px;">完整的系统：记者与编辑（A2A）协调，记者使用图像库 MCP 服务器为报道寻找图片。</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">何时使用</td><td style="border: 1px solid #ccc; padding: 4px;">当您需要真正的协作、迭代和改进，或需要多个代理分担专业知识时。</td><td style="border: 1px solid #ccc; padding: 4px;">当单个代理需要访问多个工具和数据源或需要与专有系统进行标准化集成时。</td><td style="border: 1px solid #ccc; padding: 4px;">当您需要多代理系统的组织优势以及 MCP 的标准化和生态系统优势时。</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">核心效益</td><td style="border: 1px solid #ccc; padding: 4px;">自主性和扩展性：代理可以独立做出决定，系统允许专门功能的横向扩展。</td><td style="border: 1px solid #ccc; padding: 4px;">简单化和标准化：由于集中推理，调试和维护更容易，并为资源提供了通用接口。</td><td style="border: 1px solid #ccc; padding: 4px;">明确区分关注点：使系统更易于理解：A2A = 团队合作，MCP = 工具使用。</td>
                    </tr>
              </tbody>
            </table>
          <img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/ddf6a29b1107ac6a63e94ecef703abc561a29e1e-986x656.png" style="width: 100%;" /><h2>结论</h2><p>这是两篇文章的第一部分，内容涉及基于 A2A 的代理的实施，并通过 MCP 服务器提供支持和外部数据及工具访问。下一篇文章将探讨实际代码，以演示它们如何共同模拟在线新闻编辑室的活动。虽然这两种框架本身都具有极强的能力和灵活性，但当它们协同工作时，你就会发现它们之间的互补性有多大。</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[你懂的，语境--第一部分：混合搜索和语境工程的演变]]></title>
            <description><![CDATA[探索混合搜索和上下文工程如何从词汇基础发展到支持下一代代理人工智能工作流程。]]></description>
            <link>https://www.elastic.co/search-labs/cn/blog/context-engineering-hybrid-search-evolution-agentic-ai</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/cn/blog/context-engineering-hybrid-search-evolution-agentic-ai</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Woody Walton]]></dc:creator>
            <pubDate>Wed, 12 Nov 2025 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/7e59a0671aa9ea32d68188a693936a66ebf48625-1000x628.png?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<h2>我们全新的人工智能代理世界</h2><p>和我们许多人一样，我发现自己对人工智能能力的发展速度既目瞪口呆又惊叹不已。我们首先看到大型语言模型（LLMs）和矢量搜索将我们带入语义革命，在这场革命中，我们不再需要用关键字来寻找事物。随后，法学硕士们向我们展示了与数据交互的新方法，他们使用聊天界面将自然语言请求转化为回复，将庞大的知识库提炼为易于使用的摘要。我们现在（已经）以 &quot;代理人工智能&quot;（agentic AI）工作流的形式出现的自动 LLM 驱动逻辑已经初具雏形，它可以从语义上理解接收到的请求，推理出需要采取的步骤，然后从可用的工具中选择迭代执行的行动来实现这些目标。</p><p>人工智能代理的前景正迫使我们从主要使用 &quot;提示工程 &quot;来塑造我们的人工智能生成交互，发展到关注我们如何帮助代理工具获得最相关、最有效的额外信息，以便 LLM 在生成其响应时加以考虑--&quot;情境工程 &quot;是下一个前沿领域。混合搜索是迄今为止最强大、最灵活的浮现相关上下文的手段，Elastic 的搜索人工智能平台开辟了一种全新的方式来利用数据为上下文工程服务。在本文中，我们将从两个角度讨论法律硕士如何改变了信息检索的世界，然后再讨论如何将它们结合起来以取得更大的成果。有相当多的地方需要覆盖...</p><h2>第 I 部分：法律硕士如何改变搜索方式</h2><p>让我们从法律硕士如何改变了我们获取和检索信息的方式这个角度出发。</p><h3>我们的词汇遗产</h3><p>长期以来，我们一直生活在有限的词库搜索世界中（尽我们所能，相当不错）。搜索是我们在研究或开始一个新项目时最先使用的工具，直到最近，我们还需要以词法搜索引擎能够理解的方式来描述我们的查询。词法搜索依赖于将某种形式的查询术语与文档语料库中的关键字进行匹配，无论内容是非结构化的还是结构化的。词法搜索要返回命中的文档，必须与该关键词相匹配（或者有同义词列表或词典等受控词汇来为我们建立概念联系）。</p><pre><code>POST my-index/_search
{
  "size": 10,
  "query": {
    "semantic": {
      "query": "machine learning applications",
      "field": "semantic-content-field"
    }
  }
}</code></pre><p><em>词法 <a href="https://www.elastic.co/docs/reference/query-languages/query-dsl/query-dsl-multi-match-query">多匹配</a> 查询</em>示例 </p><p>至少搜索引擎有能力返回相关性得分的点击率。搜索引擎提供丰富的查询语法选项，可有效定位索引数据，并提供内置相关性算法，根据用户查询语法的意图对结果进行评分。几十年来，搜索引擎在相关性排序算法方面取得了长足的进步，这使搜索引擎成为一个高效的数据检索平台，可以根据查询结果的相关性对结果进行评分和排序。使用 SQL 作为主要数据检索方法的数据库和其他系统在这方面处于劣势：数据库查询中没有相关性的概念；它们能做的最好的事情就是按字母或数字对结果进行排序。好消息是，您将获得这些关键词的所有点击率（召回率），但相对于您询问这些关键词的<em>原因</em>（精确度）而言，它们的顺序不一定有帮助。这一点很重要，我们很快就会看到...</p><h3>进入（语义）龙</h3><p>信息矢量表示法作为关键字搜索的替代方法，其潜力已被研究了<a href="https://www.elastic.co/search-labs/blog/introduction-to-vector-search">很长时间</a>。矢量让我们摆脱了只用关键词匹配内容的模式，因此前景十分广阔--由于矢量是术语和权重的数字表示，因此可以根据语言模型对术语在训练领域中相互关系的理解，在数学上接近概念。通用矢量搜索之所以拖延了很长时间，是因为模型大多局限于特定领域，它们根本不足以充分理解一个术语在不同语境中可能代表的许多不同概念。</p><p>直到几年前出现了大型语言模型（LLM），它们能够在更大的数据量上进行训练（使用<a href="https://en.wikipedia.org/wiki/Transformer_(deep_learning_architecture)">转换器</a>和<a href="https://en.wikipedia.org/wiki/Attention_(machine_learning)">注意力</a>），矢量搜索才变得实用起来--LLM 的大小和深度最终使矢量能够存储足够的细微差别，从而真正捕捉语义。理解深度的骤然增加使得 LLM 现在可以实现大量以前无法实现的自然语言处理（NLP）功能，其中影响最大的可能是根据序列中迄今为止的上下文推断序列中最有可能出现的下一个术语。推理过程赋予了生成式人工智能近乎人类的文本生成能力。人工智能生成的文本参考了 LLM 对训练数据中术语相关性的理解，并利用请求的措辞来区分术语可能出现的不同语境。</p><p>尽管生成式人工智能非常神奇，但 LLM 也<em>有</em>其局限性，会导致质量和准确性方面的误差，也就是通常所说的幻觉。当 LLM 无法获得信息（或没有正确的上下文引导）来根据事实回答问题时，就会产生幻觉，因此，为了帮助 LLM，它会产生一个自信满满、听起来似是而非的回答。部分原因在于，虽然 LLM 可以在包含各种信息的大型领域中学习语言的用法，但它们必须在某个时间点停止训练，因此它们的理解存在时效性因素--也就是说，模型只能知道在停止训练之前的准确性。造成幻觉的另一个因素是，模型通常不知道私人持有的数据（不能在公共互联网上获取的数据），当这些数据包含特定术语和名词时，这一点尤为重要。</p><h3>矢量数据库</h3><p>LLM 使用一种称为文本嵌入的技术将内容矢量化到其模型空间中，这种技术是指根据所接受的训练，将内容的语义<a href="https://www.elastic.co/search-labs/blog/hybrid-search-multiple-embeddings">嵌入</a>或映射到模型的世界观中。准备和处理嵌入内容需要几个步骤，包括<a href="https://www.elastic.co/search-labs/blog/chunking-strategies-elasticsearch">分块</a>和标记化（以及<a href="https://www.kaggle.com/code/danishmahdi/subword-tokenization-bpe-wordpiece-and-unigram">子词标记化</a>）。其结果通常是一组密集的向量，代表了模型在其向量空间内对该内容块含义的理解。分块是一个不精确的过程，目的是使内容符合模型生成嵌入的处理限制，同时还尝试使用语义结构（如句子和段落指示符）将相关文本归入一个分块。</p><p>由于单个分块与同一文档中的其他分块并不完全关联，因此分块的需要可能会在嵌入文档中造成一些语义损失。神经网络固有的不透明性会使这种损失变得更加严重--LLM 是一个真正的 &quot;黑盒子&quot;，训练过程中术语和概念之间的联系是非确定的，人类无法解释。这就导致了可解释性、可重复性、无意识偏见等问题，并可能失去信任和准确性。不过，从语义上将想法联系起来的能力，以及在查询时不受特定关键词束缚的能力还是非常强大的：</p><pre><code>POST my-index/_search 
{
  "size": 10, 
  "query": {
    "semantic": {
      "query": "machine learning applications",
      "field": "semantic-content-field"
    }
  }
} </code></pre><p><em><a href="https://www.elastic.co/docs/reference/query-languages/query-dsl/query-dsl-semantic-query">语义</a> 查询 示例</em></p><p>矢量数据库还有一个问题需要考虑：它们不是搜索引擎，而是数据库！在进行<a href="https://www.elastic.co/search-labs/blog/introduction-to-vector-search">矢量相似性搜索</a>时，会对查询词进行编码，以便在模型的矢量空间中找到一组（嵌入）坐标。然后将这些坐标作为靶心，找出与靶心 &quot;近邻 &quot;的文档--这意味着文档的排名（或在结果中的位置）是由计算出的文档坐标与查询坐标的相似度<em>距离</em>决定的。在可能的上下文中，哪个最接近用户的意图？我将其比喻为电影《<a href="https://www.youtube.com/watch?v=x3h7xz558EY&amp;start=3&amp;end=86">星际之门</a>》中的一个场景，我们有六个相交的坐标点来告诉我们目的地（靶心），但如果不知道 &quot;第七个符号&quot;--代表用户主观意图的起点坐标，我们就无法到达目的地。因此，通过表达式语法和相关性评分来考虑查询的主观意图，我们就能得到类似于主观相关性分级的<em>圆柱体</em>，而不是根据不断扩大和无差别的相似性来对向量进行相对排序。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/1ddfaae0c1496d08d7d30419e6d2aeaeacfc0ea2-1600x544.png" style="width: 100%;" /><p>LLM 的推理能力可能有助于确定<em>它</em>对查询所掌握的最有可能的上下文，但问题是，<em>在没有帮助的</em>情况下，输入查询的坐标<em>只能</em>根据模型最初的训练方式来确定。</p><p>在某些方面，你可以说矢量相似性走向了与严格的关键词匹配相反的极端--它的优势在于能够克服术语不匹配的问题，但<a href="https://medium.com/data-science/vector-embeddings-are-lossy-heres-what-to-do-about-it-4f9a8ee58bb7">几乎可以</a>说是无懈可击：LLM 倾向于统一相关概念，而不是区分它们。矢量相似性提高了我们从语义上匹配内容的能力，但并不能保证精确度，因为它可能会忽略精确的关键字和特定的细节，而这些细节在模型中并没有得到足够的消歧。矢量相似性搜索本身就很强大，但我们需要将从矢量数据库中获取的结果与其他检索方法的结果关联起来。</p><h3>重新排名技术</h3><p>现在是提及一种名为 &quot;重排 &quot;的通用技术的好时机。&quot;重排 &quot;是对结果集进行重新评分或归一化，使其达到统一的排名顺序。需要重新排序的原因可能是来自多个来源或检索方法的结果具有不同的排序/评分机制（或者根本没有，SQL！），或者重新排序可能是为了使来自非语义来源的结果与用户的查询在语义上保持一致。重新排序是第二阶段的操作，是指通过某种<em>初始检索</em>方法收集到的一组结果（即SQL、词法搜索、向量搜索），然后用不同的评分方法重新排序。</p><p>有几种可用的方法，包括<a href="https://www.elastic.co/docs/solutions/search/ranking/learning-to-rank-ltr">学习排名（Learning-To-Rank，</a>LTR）和<a href="https://www.elastic.co/docs/reference/elasticsearch/rest-apis/reciprocal-rank-fusion">互惠排名融合（Reciprocal Rank Fusion，RRF</a>）--LTR 适用于捕捉搜索结果特征（喜欢、评分、点击等），并利用这些特征对搜索结果进行评分、提升或倾斜。RRF 非常适合合并不同查询模式返回的结果（如词法搜索和矢量数据库搜索）合并为一个结果列表。Elastic 还提供了使用<a href="https://www.elastic.co/search-labs/blog/linear-retriever-hybrid-search">线性重排</a>方法调整分数的灵活性。</p><p>不过，最有效的重排技术之一是<a href="https://www.elastic.co/docs/solutions/search/ranking/semantic-reranking">语义重排</a>，它利用 LLM 的语义理解能力来分析查询和结果的向量嵌入，然后应用相关性评分/重评分来确定最终顺序。当然，语义重排需要与重排模型建立连接，Elasticsearch提供了<a href="https://www.elastic.co/docs/api/doc/elasticsearch/group/endpoint-inference">推理API</a>，让您可以创建<strong>重排</strong>端点，利用内置模型<a href="https://www.elastic.co/docs/explore-analyze/machine-learning/nlp/ml-nlp-rerank">（Elastic Rerank</a>）、<a href="https://www.elastic.co/docs/reference/elasticsearch/clients/eland/machine-learning">导入的</a>第三方模型或外部托管服务（如<a href="https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-inference-put-cohere">Cohere</a>或<a href="https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-inference-put-googlevertexai">Google Vertex AI</a>）。然后就可以通过<a href="https://www.elastic.co/docs/solutions/search/retrievers-overview">检索器</a>查询抽象语法执行重新排序：</p><pre><code>POST my-index/_search 
{
  "size": 10,
  "retriever": {
    "text_similarity_reranker": {
      "retriever": {
        "rrf": {
          "retrievers": [
            {
              "standard": {
                "query": {
                  "multi_match": {
                    "query": "machine learning applications",
                    "fields": ["title", "content"]
                  }
                }
              }
            },
            {
              "knn": {
                "field": "semantic-content-field",
                "k": 10,
                "num_candidates": 100,
                "query_vector_builder": {
                  "text_embedding": {
                    "model_id": "my-text-embedding-model",
                    "model_text": "machine learning applications"
                  }
                }
              }
            }
          ],
          "rank_window_size": 50,
          "rank_constant": 20
        }
      }
    },
    "field": "content",
    "inference_id": "my-reranker",
    "inference_text": "machine learning applications",
    "rank_window_size": 20
  }
}</code></pre><p><em>多级检索器重排操作示例</em></p><p>听起来不错吧？我们可以对来自不同来源的结果进行重新排序，从而接近对所有类型内容的语义理解......语义重新排序的计算成本和处理时间都很高，正因为如此，语义重新排序只能在数量有限的结果上进行，这意味着<em>如何</em>检索这些初始结果非常重要。</p><h3>语境检索方法很重要</h3><p>主观意图是确定结果准确性和评分相关性的一个重要因素。由于无法考虑用户执行查询的意图（如通过灵活的语法或第二阶段重排所表达的意图），我们只能从模型空间中已编码的现有上下文中进行选择。我们通常通过<a href="https://en.wikipedia.org/wiki/Retrieval-augmented_generation">检索增强生成（RAG）</a>等技术来解决这种缺乏上下文的问题。RAG 的工作原理是，它可以有效地转移查询的坐标，包括通过预查询返回的其他相关术语，以获取与上下文相关的数据。这就使得提供额外上下文的引擎及其执行检索<em>的</em>初始方法对上下文的准确性更加重要！</p><p>让我们回顾一下不同的上下文检索方法，以及它们对 RAG 操作的帮助或伤害：</p><ul><li><strong>没有搜索引擎的混合搜索检索仍然缺乏主观相关性。</strong>如果提供 RAG 的平台主要基于下面的 SQL（包括大多数 &quot;数据湖 &quot;平台），那么在初始检索阶段就缺乏相关性评分。许多数据湖平台提供自己版本的混合检索（而非搜索），通常在基于 SQL 的检索和矢量数据库结果上结合语义重排和 RRF 等重排技术。简单的排序显然不足以进行主观排序，但即使作为第二阶段语义重排操作的基础，SQL 作为第一阶段检索，在只对 &quot;前 k 个 &quot;点击进行语义重排时也会出现问题--如果不在检索时对结果进行某种评分，我们又如何保证<em>最好的</em>结果确实在最前面的结果中呢？</li><li><strong>对于 RAG 来说，仅有矢量相似性是不够的</strong>。这实际上是由一系列复杂问题造成的--这是嵌入的损失性，还有天真的分块方法、相似性的计算方法，以及主观意图这一至关重要的缺失部分。RAG 的主要目标之一是将生成式人工智能交互建立在客观事实的基础上，既能防止产生幻觉，又能让 LLM 了解它在训练过程中不知道的私人信息。我们可以利用 RAG 提供的额外语境来约束和引导 LLM，使其考虑我们所知道的对回答当前问题最重要的关联和细节。为此，我们需要<em>同时</em>使用语义和词汇方法。</li><li><strong>基于文件的 grep/regex RAG。</strong>在人工智能代理领域，有一些<a href="https://www.nicolasbustamante.com/p/the-rag-obituary-killed-by-agents">人</a>指出，应使用大幅放大的上下文窗口，通过 grep 和 regex 访问本地文件，以实现 RAG，而不是使用外部检索平台。我们的想法是，有了更大的上下文窗口，法律硕士就能在自己的思维空间内建立概念联系，而不是依赖分块的碎片和多种检索方法/平台来收集相关信息。虽然从理论上讲，拥有整个文档比拥有文档片段能提供更全面的信息，但这只适用于小数据域（或者，例如，在提供用于<a href="https://en.wikipedia.org/wiki/Vibe_coding">振动编码的</a>文件时），即使在这种情况下，初始检索方法也是扫描所有仅有关键字匹配的文档。</li></ul><p><strong>搜索不仅仅是检索</strong></p><p>搜索引擎的设计目的是使查询尽可能快速和灵活。在内部，它们利用专门的数据结构来存储和检索不同类型的数据，以满足这些数据类型的需要。Elasticsearch 可优化所有类型数据的存储和查询，包括非结构化/全文词法搜索（匹配、短语、近似、多重匹配）、快速关键字（精确匹配）匹配和过滤、数字范围、日期、IP 地址，而且存储文档结构的方式也非常灵活（例如，可通过&quot;...嵌套或扁平化文档）。Elasticsearch 还是一个原生矢量数据库，可以存储和查询稀疏和密集矢量类型。我们将继续探索创新方法（例如，<a href="https://www.elastic.co/search-labs/blog/better-binary-quantization-lucene-elasticsearch">更好的二进制量化 (BBQ)</a> &amp; <a href="https://www.elastic.co/search-labs/blog/diskbbq-elasticsearch-introduction">DiskBBQ</a>），以保持搜索保真度，同时提高速度、可扩展性以及与矢量化内容相关的成本。Elasticsearch 平台还提供内置的数据弹性和高可用性，并包含数据生命周期管理功能，<a href="https://www.elastic.co/docs/deploy-manage/tools/snapshot-and-restore/searchable-snapshots">如可搜索快照</a>，让您可以在经济高效的对象存储上保留不常访问或长期保留的数据，但仍可完全搜索。</p><h3>混合搜索是最好的选择</h3><p><a href="https://www.elastic.co/what-is/hybrid-search">混合搜索</a>（不仅仅是混合检索）将传统词汇搜索的优势与 LLM 的语义理解和向量相似性搜索相结合。这种协同作用允许在<em>检索</em>阶段通过搜索引擎提供的任何灵活的查询语法选项：意图驱动语法选项和相关性评分、多模态数据检索、过滤、聚合和偏置来定位高度相关的结果。利用<a href="https://www.elastic.co/docs/reference/query-languages/esql">ES|QL</a>等搜索语法和多级<a href="https://www.elastic.co/docs/solutions/search/retrievers-overview">检索器</a>，我们可以在一个请求中灵活地将传统搜索与语义搜索、过滤器和多种重排技术结合起来。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/f25180cb430414b99ae553d3b8eb161dbccea4d4-1920x1080.png" style="width: 100%;" /><p>混合搜索的最大优势之一是，您的查询可以同时针对多种不同的数据类型使用专门的语法。这些不同的查询语法不仅可用于<em>查找</em>结果，还可用作结果<em>的</em>筛选器或聚合器。例如，最常见的查询类型之一是<a href="https://www.elastic.co/docs/explore-analyze/geospatial-analysis">地理空间分析</a>，它经常与其他语法相结合。您可以查询地理坐标在某一点指定距离内的结果，或要求按地区对结果进行汇总，或进行汇总以跟踪进入/离开某个区域的移动情况并发出警报。使用混合搜索，您可以灵活地组合语法，以最准确的方式定位搜索结果，检索最贴近您的上下文的内容。</p><h2>中场休息</h2><p>第一部分讲述了矢量搜索如何改变了我们检索数据的方式，并为 LLM 给我们用来与数据交互的查询机制带来的变化做了铺垫。我们将假装不得不把这部分内容分成多个部分，以便 LLM 能够在不丢失上下文的情况下理解......;-)让我们在<em> </em><a href="https://www.elastic.co/search-labs/blog/context-engineering-llm-evolution-agentic-ai">第二部分 &quot;代理人工智能和上下文工程的必要性&quot;中进一步了解 这一点的重要性 ，在第三部分中，我们将继续讨论混合搜索。</a></p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[在 Elasticsearch 中为结构化文档配置递归分块]]></title>
            <description><![CDATA[了解如何在 Elasticsearch 中使用分块大小、分隔符组和自定义分隔符列表配置递归分块，以优化结构文档索引。]]></description>
            <link>https://www.elastic.co/search-labs/cn/blog/recursive-chunking-structured-documents-elasticsearch</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/cn/blog/recursive-chunking-structured-documents-elasticsearch</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Daniel Rubinstein]]></dc:creator>
            <pubDate>Tue, 11 Nov 2025 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/591872e31880768ca927507654a621addc0d124d-1600x960.png?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>自 8.16 版起，用户可以配置将长文档导入语义文本字段时使用的分块策略。从 9.1 / 8.19 版开始，我们引入了一种新的可配置递归分块策略，使用正则表达式列表对文档进行分块。分块的目的是将长文档分割成囊括相关内容的部分。我们现有的策略会按单词/句子的粒度分割文本，但以结构化格式编写的文档（例如：&quot;......&quot;）则不会这样做。Markdown）通常会在由一些分隔字符串定义的部分内包含相关内容（例如：&quot;......&quot;）。标题）。对于这些类型的文档，我们正在引入递归分块策略，以利用结构化文档的格式来创建更好的分块！</p><h2>什么是递归分块？</h2><p>递归分块法会遍历所提供的分块模式列表，逐步将文档分成更小的分块，直到达到所需的最大分块大小。</p><h3>如何配置递归分块？</h3><p>以下是用户为递归分块提供的可配置值：</p><ul><li>(必填）<code>max_chunk_size</code> ：字块中的最大字数。</li><li>任选其一：<ul><li><code>separators</code>:用于将文档分割成块的 regex 字符串模式列表。</li><li><code>separator_group</code>:一个字符串，它将映射到 Elastic 定义的默认分隔符列表，用于特定类型的文档。目前，<code>markdown</code> 和<code>plaintext</code> 。</li></ul></li></ul><h3>递归分块是如何工作的？</h3><p>递归分块的过程如下：给定输入文档、<code>max_chunk_size</code> （以字数为单位）和分隔符字符串列表：</p><ol><li>如果输入文档已经在最大分块大小范围内，则返回一个涵盖整个输入文档的分块。</li><li>根据分隔符的出现次数，将文本分割成潜在的文本块。对于每个潜在的数据块<ol><li>如果潜在数据块在最大数据块大小范围内，则将其添加到要返回给用户的数据块列表中。</li><li>否则，从第 2 步开始重复，只使用潜在文本块中的文本，并使用列表中的下一个分隔符进行分割。如果没有其他分隔符可以尝试，就退回到基于句子的分块。</li></ol></li></ol><h2>配置递归分块的示例</h2><p>除了分块大小，递归分块的主要配置是选择应使用哪些分隔符来分割文档。如果您不确定从哪里开始，Elasticsearch 提供了一些默认的分离器组，可用于常见的使用情况。</p><h3>利用分离器组</h3><p>要使用分隔组，只需在配置分块设置时提供要使用的组名即可。例如</p><pre><code>"chunking_settings": {
    "strategy": "recursive",
    "max_chunk_size": 25,
    "separator_group": "plaintext"
}</code></pre><p>这样就可以利用分隔符列表<code>[&quot;(?&lt;!\\n)\\n\\n(?!\\n)&quot;, &quot;(?&lt;!\\n)\\n(?!\\n)&quot;)]</code> 来实现递归分块策略。对于一般的纯文本应用程序，这种方法效果很好，可以在 2 个换行符后再分隔出 1 个换行符。</p><p>我们还提供一个分隔符组<code>markdown</code> ，它将利用分隔符列表：</p><pre><code>[
"\n# ",
       "\n## ",
       "\n### ",
       "\n#### ",
       "\n##### ",
       "\n###### ",
       "\n^(?!\\s*$).*\\n-{1,}\\n",
       "\n^(?!\\s*$).*\\n={1,}\\n"
]</code></pre><p>这个分隔符列表可以很好地适用于一般的标记符使用情况，在 6 个标题层次和分节符上分别进行分隔。</p><p>创建资源（推理端点/语义文本字段）时，与当时分隔符组相对应的分隔符列表将存储在您的配置中。如果以后更新了分隔符组，也不会改变已创建资源的行为。</p><h3>使用自定义分隔符列表</h3><p>如果预定义的分隔符组不适合您的使用情况，您可以定义一个符合您需求的自定义分隔符列表。请注意，可以在分隔符列表中提供正则表达式。以下是使用自定义分隔符配置分块设置的示例：</p><pre><code>"chunking_settings": {
    "strategy": "recursive",
    "max_chunk_size": 25,
    "separators": ["\n\n", "\n", "<my-custom-separator>"]
}</code></pre><p>上述分块策略将在 2 个换行符、1 个换行符和一个字符串<code>“&lt;my-custom-separator&gt;”</code> 上进行分割。</p><h2>递归分块的实际应用示例</h2><p>让我们来看一个递归分块的实例。在本示例中，我们将使用以下分块设置和自定义分隔符列表，使用顶部两层标题分割标记符文档：</p><pre><code>"chunking_settings": {
    "strategy": "recursive",
    "max_chunk_size": 25,
    "separators": ["\n# ", "\n## "]
}</code></pre><p>让我们来看看一个简单的未分块 Markdown 文档：</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/3a5507f4a1288065097231548e5b18e240508785-1302x1446.png" style="width: 100%;" /><p>现在，让我们使用上面定义的分块设置对文档进行分块：</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/a3313c4c40ff39b8dbcdd7c4878c723f088e6c1a-1600x1187.png" style="width: 100%;" /><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/79a2921943191ea631df94c9d465818ec8d3e738-1600x1206.png" style="width: 100%;" /><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/459e695cce7540267422396b9a62ff4ad35f61db-1600x1260.png" style="width: 100%;" /><p>注意：每个分块（分块 3 除外）末尾的换行符不会突出显示，而是包含在实际分块边界内。</p><h3>今天就开始使用递归分块技术！</h3><p>有关使用该功能的更多信息，请查看有关<span class="unknown__pt__mark__88e241676926">配置分块设置的</span>文档。</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[介绍 Kibana 中的 Elasticsearch 查询规则用户界面]]></title>
            <description><![CDATA[了解如何使用 Elasticsearch 查询规则用户界面，在 Kibana 中使用可定制的规则集从搜索查询中添加或排除文档，而不影响有机排名。]]></description>
            <link>https://www.elastic.co/search-labs/cn/blog/elasticsearch-query-rules-ui-introduction</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/cn/blog/elasticsearch-query-rules-ui-introduction</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Jhon Guzmán]]></dc:creator>
            <pubDate>Fri, 07 Nov 2025 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/1fb10bd31c509cc9b9bb4f71f49970f140e6c36f-1600x945.png?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>搜索引擎的工作就是返回相关结果。然而，有些业务需求并不限于此，比如突出销售、优先考虑季节性产品或展示赞助项目，而开发人员不可能总是在搜索查询中做到这一点。</p><p>此外，这些用例通常具有时间敏感性，而经历典型的开发阶段（创建代码分支，然后等待新版本发布）是一个耗时的过程。</p><p>那么，如果我们只需调用 API，或者在 Kibana 中点击几下就能完成整个过程，那会怎样呢？</p><h2>查询规则用户界面</h2><p>Elasticsearch 8.10 引入了<a href="https://www.elastic.co/blog/introducing-query-rules-elasticsearch-8-10"><strong>查询规则</strong></a>和<a href="https://www.elastic.co/docs/reference/elasticsearch/rest-apis/retrievers/rule-retriever"><strong>规则检索器</strong></a>。这些工具旨在根据规则在不影响有机结果排名的情况下将<a href="https://www.elastic.co/docs/reference/query-languages/query-dsl/query-dsl-pinned-query"><em>钉入结果</em></a>注入查询。它们只是以声明式的简单方式在结果之上添加业务逻辑。</p><p>查询规则的一些常见用例包括</p><ul><li><strong>突出显示促销列表或销售</strong>：在顶部显示促销或赞助商品。</li><li><strong>根据上下文或地理位置排除</strong>：当当地法规不允许显示某些项目时，隐藏这些项目。</li><li><strong>优先处理关键结果</strong>：确保热门搜索或固定搜索始终排在前面，无论有机搜索排名如何。</li></ul><p>要访问界面并与这些工具互动，需要点击 Kibana 侧边菜单，然后转到相关性下的<strong>查询规则</strong> <strong>：</strong></p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/242e33e89d1a07ffa0e76009c46b3a9236722741-458x1010.png" style="width: 100%;" /><p>查询规则菜单弹出后，点击<strong>创建第一个规则集：</strong></p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/30b3a91bbbf243d314cf38298e01ca5cff784430-1600x945.png" style="width: 100%;" /><p>接下来，您需要为规则集命名。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/26c5462f88678867776f933b5655ca0df0d72a16-708x446.png" style="width: 100%;" /><p>定义每条规则的表格有三个关键部分：</p><ul><li><strong>标准</strong>：适用规则必须满足的条件。例如，&quot;当 query_string 字段包含<em>Christmas</em>值时 &quot;或 &quot;当 country 字段为<em>CO 时&quot;。</em></li><li><strong>行动</strong>：这是您希望在条件满足时发生的事情。它可以被固定（将文档固定到顶部结果）或排除（隐藏文档）。</li><li><strong>元数据</strong>：这些字段在查询运行时会随查询一起出现。它们可以包括用户信息（如位置或语言）以及搜索数据（query_string）。这些值是标准用于决定是否应用规则的值。</li></ul><h2>例如：热门项目</h2><p>假设我们有一个电子商务网站，上面有不同的商品。在查看这些指标时，我们注意到在游戏机类别中，&quot;DualShock 4 无线控制器 &quot;是销售量最大的商品之一，尤其是当用户搜索关键词 &quot;PS4 &quot;或 &quot;PlayStation 4 &quot;时。因此，我们决定在用户搜索这些关键词时，将该产品放在搜索结果的顶部。</p><p>首先，让我们使用批量 API 请求为每个项目的文档建立索引：</p><pre><code>POST _bulk
{ "index": { "_index": "products", "_id": "1" } }
{ "id": "1", "name": "PlayStation 4 Slim 1TB", "category": "console", "brand": "Sony", "price": 1200 }
{ "index": { "_index": "products", "_id": "2" } }
{ "id": "2", "name": "DualShock 4 Wireless Controller", "category": "accessory", "brand": "Sony", "price": 250 }
{ "index": { "_index": "products", "_id": "3" } }
{ "id": "3", "name": "PlayStation 4 Camera", "category": "accessory", "brand": "Sony", "price": 200 }
{ "index": { "_index": "products", "_id": "4" } }
{ "id": "4", "name": "PlayStation 4 VR Headset", "category": "accessory", "brand": "Sony", "price": 900 }
{ "index": { "_index": "products", "_id": "5" } }
{ "id": "5", "name": "Charging Station for DualShock 4", "category": "accessory", "brand": "Sony", "price": 80 }</code></pre><p>如果我们不干预查询，该项目通常会出现在第四位。问题是这样的</p><pre><code>GET products/_search
{
 "query": {
   "match": {
     "name": "PlayStation 4"
   }
 }
}</code></pre><p>结果如下</p><pre><code>{
 "took": 1,
 "timed_out": false,
 "_shards": {
   "total": 1,
   "successful": 1,
   "skipped": 0,
   "failed": 0
 },
 "hits": {
   "total": {
     "value": 5,
     "relation": "eq"
   },
   "max_score": 0.6973252,
   "hits": [
     {
       "_index": "products",
       "_id": "3",
       "_score": 0.6973252,
       "_source": {
         "id": "3",
         "name": "PlayStation 4 Camera",
         "category": "accessory",
         "brand": "Sony",
         "price": 200
       }
     },
     {
       "_index": "products",
       "_id": "1",
       "_score": 0.6260078,
       "_source": {
         "id": "1",
         "name": "PlayStation 4 Slim 1TB",
         "category": "console",
         "brand": "Sony",
         "price": 1200
       }
     },
     {
       "_index": "products",
       "_id": "4",
       "_score": 0.6260078,
       "_source": {
         "id": "4",
         "name": "PlayStation 4 VR Headset",
         "category": "accessory",
         "brand": "Sony",
         "price": 900
       }
     },
     {
       "_index": "products",
       "_id": "2",
       "_score": 0.08701137,
       "_source": {
         "id": "2",
         "name": "DualShock 4 Wireless Controller",
         "category": "accessory",
         "brand": "Sony",
         "price": 250
       }
     },
     {
       "_index": "products",
       "_id": "5",
       "_score": 0.07893815,
       "_source": {
         "id": "5",
         "name": "Charging Station for DualShock 4",
         "category": "accessory",
         "brand": "Sony",
         "price": 80
       }
     }
   ]
 }
}</code></pre><p>让我们创建一个查询规则来改变这种情况。首先，让我们像这样把它添加到规则集中：</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/fdc42646fb3e76a09bca7d19047a76efe343f7a2-1600x650.png" style="width: 100%;" /><p>或相应的<a href="https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-query-rules-put-ruleset">API 请求</a>：</p><pre><code>PUT _query_rules/my-rules
{
  "rules": [
    {
      "rule_id": "rule-1232",
      "type": "pinned",
      "criteria": [
        {
          "type": "exact",
          "metadata": "query_string",
          "values": [
            "PS4",
            "PlayStation 4"
          ]
        }
      ],
      "actions": {
        "docs": [
          {
            "_index": "products",
            "_id": "2"
          }
        ]
      }
    }
  ]
}</code></pre><p>要在查询中使用<strong>规则集 </strong>，我们必须使用查询规则类型。这种查询主要由两部分组成：</p><pre><code>GET /products/_search
{
 "retriever": {
   "rule": {
     "retriever": {
       "standard": {
         "query": {
           "match": { "name": "PlayStation 4" }
         }
       }
     },
     "match_criteria": {
       "query_string": "PlayStation 4"
     },
     "ruleset_ids": ["my-rules"]
   }
 }
}</code></pre><ul><li><strong>匹配标准</strong>：这些是用于与用户查询进行比较的元数据。在本例中，当 query_string 字段的值为 &quot;PlayStation 4 &quot;时，规则集被激活。</li><li><strong>query</strong>：实际查询，用于搜索和获取有机结果。</li></ul><p>这样，首先运行有机查询，然后 Elasticsearch 应用规则集中的规则：</p><pre><code>{
 "took": 17,
 "timed_out": false,
 "_shards": {
   "total": 1,
   "successful": 1,
   "skipped": 0,
   "failed": 0
 },
 "hits": {
   "total": {
     "value": 5,
     "relation": "eq"
   },
   "max_score": 1.7014122e+38,
   "hits": [
     {
       "_index": "products",
       "_id": "2",
       "_score": 1.7014122e+38,
       "_source": {
         "id": "2",
         "name": "DualShock 4 Wireless Controller",
         "category": "accessory",
         "brand": "Sony",
         "price": 250
       }
     },
     {
       "_index": "products",
       "_id": "3",
       "_score": 0.6973252,
       "_source": {
         "id": "3",
         "name": "PlayStation 4 Camera",
         "category": "accessory",
         "brand": "Sony",
         "price": 200
       }
     },
     {
       "_index": "products",
       "_id": "1",
       "_score": 0.6260078,
       "_source": {
         "id": "1",
         "name": "PlayStation 4 Slim 1TB",
         "category": "console",
         "brand": "Sony",
         "price": 1200
       }
     },
     {
       "_index": "products",
       "_id": "4",
       "_score": 0.6260078,
       "_source": {
         "id": "4",
         "name": "PlayStation 4 VR Headset",
         "category": "accessory",
         "brand": "Sony",
         "price": 900
       }
     },
     {
       "_index": "products",
       "_id": "5",
       "_score": 0.07893815,
       "_source": {
         "id": "5",
         "name": "Charging Station for DualShock 4",
         "category": "accessory",
         "brand": "Sony",
         "price": 80
       }
     }
   ]
 }
}</code></pre><h2>示例：基于用户的元数据</h2><p>查询规则的另一个有趣应用是使用元数据，根据用户或网页的上下文信息显示特定文档。</p><p>例如，假设我们想根据用户的忠诚度（用数值表示）来突出显示商品或定制销售。</p><p>我们可以直接将这些元数据导入查询，这样当所述值满足特定条件时，规则就会激活。</p><p>首先，我们将为一份只有忠诚度高的用户才能看到的文档建立索引：</p><pre><code>POST _bulk
{ "index": { "_index": "products", "_id": "6" } }
{ "id": "6", "name": "PlayStation Plus Deluxe Card - 12 months", "category": "membership", "brand": "Sony", "price": 300 }</code></pre><p>现在，让我们在同一规则集内创建一条新规则，这样当忠诚度_级别等于或高于 80 时，项目就会出现在结果的顶部。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/58de12dff93305440608f51465462fcc68653a08-1421x496.png" style="width: 100%;" /><p>保存规则和规则集。</p><p>以下是相应的 REST 请求：</p><pre><code>PUT _query_rules/my-rules
{
  "rules": [
    {
      "rule_id": "pin-premiun-user",
      "type": "pinned",
      "criteria": [
        {
          "type": "gte",
          "metadata": "loyalty_level",
          "values": [
            80
          ]
        }
      ],
      "actions": {
        "docs": [
          {
            "_index": "products",
            "_id": "6"
          }
        ]
      }
    }
  ]
}</code></pre><p>现在，在运行查询时，我们需要在元数据中包含新参数<strong>loyalty_level </strong>。如果满足规则中的条件，新文档将出现在结果的顶部。</p><p>例如，在发送忠诚度级别为 80 的查询时：</p><pre><code>POST /products/_search
{
  "retriever": {
    "rule": {
      "retriever": {
        "standard": {
          "query": {
            "match": {
              "name": "PlayStation"
            }
          }
        }
      },
      "match_criteria": {
        "query_string": "PlayStation",
        "loyalty_level": 80
      },
      "ruleset_ids": ["my-rules"]
    }
  }
}</code></pre><p>我们将在结果上方看到忠诚度文件：</p><pre><code>{
  "took": 31,
  "timed_out": false,
  "_shards": {
    "total": 1,
    "successful": 1,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": {
      "value": 4,
      "relation": "eq"
    },
    "max_score": 1.7014122e+38,
    "hits": [
      {
        "_index": "products",
        "_id": "6",
        "_score": 1.7014122e+38,
        "_source": {
          "id": "6",
          "name": "PlayStation Plus Deluxe Card - 12 months",
          "category": "membership",
          "brand": "Sony",
          "price": 300
        }
      },
      {
        "_index": "products",
        "_id": "3",
        "_score": 0.5054567,
        "_source": {
          "id": "3",
          "name": "PlayStation 4 Camera",
          "category": "accessory",
          "brand": "Sony",
          "price": 200
        }
      },
      {
        "_index": "products",
        "_id": "1",
        "_score": 0.45618832,
        "_source": {
          "id": "1",
          "name": "PlayStation 4 Slim 1TB",
          "category": "console",
          "brand": "Sony",
          "price": 1200
        }
      },
      {
        "_index": "products",
        "_id": "4",
        "_score": 0.45618832,
        "_source": {
          "id": "4",
          "name": "PlayStation 4 VR Headset",
          "category": "accessory",
          "brand": "Sony",
          "price": 900
        }
      }
    ]
  }
}</code></pre><p>在下面的例子中，由于忠诚度等级为 70，因此不符合规则，物品不应出现在顶部：</p><pre><code>POST /products/_search
{
  "retriever": {
    "rule": {
      "retriever": {
        "standard": {
          "query": {
            "match": {
              "name": "PlayStation"
            }
          }
        }
      },
      "match_criteria": {
        "query_string": "PlayStation",
        "loyalty_level": 70
      },
      "ruleset_ids": ["my-rules"]
    }
  }
}</code></pre><p>结果如下：</p><pre><code>{
  "took": 7,
  "timed_out": false,
  "_shards": {
    "total": 1,
    "successful": 1,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": {
      "value": 4,
      "relation": "eq"
    },
    "max_score": 0.5054567,
    "hits": [
      {
        "_index": "products",
        "_id": "3",
        "_score": 0.5054567,
        "_source": {
          "id": "3",
          "name": "PlayStation 4 Camera",
          "category": "accessory",
          "brand": "Sony",
          "price": 200
        }
      },
      {
        "_index": "products",
        "_id": "1",
        "_score": 0.45618832,
        "_source": {
          "id": "1",
          "name": "PlayStation 4 Slim 1TB",
          "category": "console",
          "brand": "Sony",
          "price": 1200
        }
      },
      {
        "_index": "products",
        "_id": "4",
        "_score": 0.45618832,
        "_source": {
          "id": "4",
          "name": "PlayStation 4 VR Headset",
          "category": "accessory",
          "brand": "Sony",
          "price": 900
        }
      },
      {
        "_index": "products",
        "_id": "6",
        "_score": 0.3817649,
        "_source": {
          "id": "6",
          "name": "PlayStation Plus Deluxe Card - 12 months",
          "category": "membership",
          "brand": "Sony",
          "price": 300
        }
      }
    ]
  }
}</code></pre><h2>例如：立即排除</h2><p>假设我们的<strong>DualShock 4 无线控制器（ID 2）</strong>暂时缺货，无法出售。因此，业务团队决定在此期间将其从搜索结果中删除，而不是手动删除文档或等待某些数据流程启动。</p><p>我们将使用与刚才应用于热门项目类似的过程，但这次我们不选择 &quot;<em>已固定&quot;</em>，而是选择 &quot;<em>排除</em>&quot;。这条规则就像一个黑名单。将条件改为 &quot;<strong>始终&quot;</strong>，这样每次运行查询时，排除都会起作用。</p><p>规则应该是这样的</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/f10971e4f1bc9520105111adfa3a476581a27130-1600x623.png" style="width: 100%;" /><p>保存规则和规则集以应用更改。以下是相应的 REST 请求：</p><pre><code>PUT _query_rules/my-rules
{
  "rules": [
    {
      "rule_id": "rule-6358",
      "type": "pinned",
      "criteria": [
        {
          "type": "always"
        }
      ],
      "actions": {
        "docs": [
          {
            "_index": "products",
            "_id": "2"
          }
        ]
      }
    }
  ]
}</code></pre><p>现在，当我们再次运行查询时，你会发现结果中不再有该项目，尽管之前的规则是将其固定。这是因为<strong>排除结果的优先级高于钉牢结果</strong>。</p><pre><code>{
 "took": 6,
 "timed_out": false,
 "_shards": {
   "total": 1,
   "successful": 1,
   "skipped": 0,
   "failed": 0
 },
 "hits": {
   "total": {
     "value": 4,
     "relation": "eq"
   },
   "max_score": 2.205655,
   "hits": [
     {
       "_index": "products",
       "_id": "3",
       "_score": 2.205655,
       "_source": {
         "id": "3",
         "name": "PlayStation 4 Camera",
         "category": "accessory",
         "brand": "Sony",
         "price": 200
       }
     },
     {
       "_index": "products",
       "_id": "1",
       "_score": 1.9738505,
       "_source": {
         "id": "1",
         "name": "PlayStation 4 Slim 1TB",
         "category": "console",
         "brand": "Sony",
         "price": 1200
       }
     },
     {
       "_index": "products",
       "_id": "4",
       "_score": 1.9738505,
       "_source": {
         "id": "4",
         "name": "PlayStation 4 VR Headset",
         "category": "accessory",
         "brand": "Sony",
         "price": 900
       }
     },
     {
       "_index": "products",
       "_id": "5",
       "_score": 0.69247496,
       "_source": {
         "id": "5",
         "name": "Charging Station for DualShock 4",
         "category": "accessory",
         "brand": "Sony",
         "price": 80
       }
     }
   ]
 }
}</code></pre><h2>结论</h2><p><strong>查询规则</strong>使调整相关性变得非常容易，无需修改任何代码。新的<strong>Kibana</strong> <strong>UI </strong>允许<strong> </strong>在几秒钟内做出这些更改，让您和您的业务团队对搜索结果拥有更多控制权。</p><p>除电子商务外，查询规则还能支持许多其他应用场景：在支持门户中突出显示故障排除指南，在知识库中显示关键的内部文档，在新闻网站中宣传突发事件，或过滤掉过期的职位或内容列表。它们甚至可以执行合规规则，如根据用户角色或地区隐藏受限资料。</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[使用 Mastra 和 Elasticsearch 构建具有语义召回功能的知识代理]]></title>
            <description><![CDATA[了解如何使用 Mastra 和 Elasticsearch 作为记忆和信息检索的向量存储，构建具有语义调用功能的知识代理。]]></description>
            <link>https://www.elastic.co/search-labs/cn/blog/knowledge-agent-semantic-recall-mastra-elasticsearch</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/cn/blog/knowledge-agent-semantic-recall-mastra-elasticsearch</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[JD Armada]]></dc:creator>
            <pubDate>Thu, 06 Nov 2025 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/b8d51c2726d5573385c9246a7821d12ade4f1b0e-720x420.jpg?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>在构建可靠的人工智能代理和架构方面，<a href="https://www.elastic.co/search-labs/blog/context-engineering-overview">情境工程</a>正变得越来越重要。随着模型越来越完善，其有效性和可靠性已不再依赖于训练有素的数据，而更多地取决于模型在正确环境中的立足程度。能够在正确的时间检索和应用最相关信息的代理更有可能产生准确和可信的输出结果。</p><p>在本博客中，我们将使用<a href="https://mastra.ai/">Mastra</a>构建一个知识代理，它能记住用户所说的话，并能在稍后调用相关信息，使用 Elasticsearch 作为记忆和检索后端。您可以轻松地将这一概念扩展到现实世界的使用案例中，例如，支持代理可以记住过去的对话和解决方案，使他们能够根据先前的上下文为特定用户定制响应或更快地提供解决方案。</p><p>在这里，您将看到如何一步一步地建造它。如果你迷失了方向，或者只是想运行一个已完成的示例，请<a href="https://github.com/jdarmada/getting-started-mastra-elastic/tree/main">点击此处</a>查看软件仓库。</p><h2>什么是 Mastra？</h2><p>Mastra 是一个开源的 TypeScript 框架，用于构建具有可交换推理、内存和工具部分的人工智能代理。它的<a href="https://mastra.ai/docs/memory/semantic-recall">语义调用</a>功能通过将信息作为嵌入信息存储在向量数据库中，使代理能够记住和检索过去的互动。这样，代理就能保留长期对话的上下文和连续性。Elasticsearch 支持高效的密集矢量搜索，是实现这一功能的绝佳矢量存储工具。当触发语义调用时，代理会将过去的相关信息拉入模型的上下文窗口，使模型能够将检索到的上下文作为其推理和响应的基础。</p><h2>入门必备</h2><ul><li>节点 v18+</li><li>Elasticsearch（8.15 或更新版本）</li><li>Elasticsearch API 密钥</li><li><a href="https://help.openai.com/en/articles/4936850-where-do-i-find-my-openai-api-key">OpenAI API 密钥</a></li></ul><p>注意：您需要这个是因为演示使用了 OpenAI 提供商，但 Mastra 支持其他人工智能 SDK 和社区模型提供商，因此您可以根据自己的设置轻松更换。</p><h2>构建 Mastra 项目</h2><p>我们将使用 Mastra 内置的 CLI 为我们的项目提供脚手架。运行该命令：</p><pre><code>npm create mastra@latest</code></pre><p>您将收到一组提示，首先是</p><p>1.为项目命名。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/2b9fe559e0276140dd539e24f916a73c60870405-620x84.png" style="width: 100%;" /><p>2.我们可以保留默认值，也可以不填。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/e04729eb03bce8499e973e18c28642402340d0e5-852x68.png" style="width: 100%;" /><p>3.在本项目中，我们将使用 OpenAI 提供的模型。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/a86596a469a71bdf8bd99cbaf528d0f0cf7272c0-436x222.png" style="width: 100%;" /><p>4.选择 &quot;暂时跳过 &quot;选项，因为我们将把所有环境变量存储在一个&quot;.env &quot;文件中，稍后再进行配置。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/02b19ccc34af0bdacf52fd94b519d036540ca2e6-426x114.png" style="width: 100%;" /><p>5.我们也可以跳过该选项。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/b3fe63d19d270bc2e0de1dd92033bf8b26750819-990x208.png" style="width: 100%;" /><p>初始化完成后，我们就可以进入下一步。</p><h3>安装依赖项</h3><p>接下来，我们需要安装一些依赖项：</p><pre><code>npm install ai @ai-sdk/openai @elastic/elasticsearch dotenv</code></pre><ul><li><code>ai</code> - 核心人工智能 SDK 软件包，提供用于在 JavaScript/TypeScript 中管理人工智能模型、提示和工作流程的工具。Mastra 是在 Vercel 的<a href="https://ai-sdk.dev/">人工智能 SDK</a>基础上构建的，因此我们需要依赖它来实现模型与代理的交互。</li><li><code>@ai-sdk/openai</code> - 将 AI SDK 连接到 OpenAI 模型（如 GPT-4、GPT-4o 等）的插件，可使用 OpenAI API 密钥进行 API 调用。</li><li><code>@elastic/elasticsearch</code> -<a href="https://www.elastic.co/docs/reference/elasticsearch/clients/javascript">Node.js 的官方 Elasticsearch 客户端</a>、用于连接到弹性云或本地集群，以进行索引、搜索和矢量操作。</li><li><code>dotenv</code> - 从 .env 文件中加载环境变量文件到 process.env 文件中、允许您安全地注入 API 密钥和 Elasticsearch 端点等凭证。</li></ul><h3>配置环境变量</h3><p>如果还没有<code>.env</code> 文件，请在项目根目录下创建该文件。或者，你也可以复制并重命名我在<a href="https://github.com/jdarmada/getting-started-mastra-elastic/blob/main/.env.example">软件仓库</a>中提供的<code>.env</code> 示例。在该文件中，我们可以添加以下变量：</p><pre><code>ELASTICSEARCH_ENDPOINT="your-endpoint-here"
ELASTICSEARCH_API_KEY="your-key-here"
OPENAI_API_KEY="your-key-here"</code></pre><p>基本设置到此结束。从这里，您就可以开始构建和协调代理。我们将更进一步，添加 Elasticsearch 作为存储和矢量搜索层。</p><h2>添加 Elasticsearch 作为向量存储</h2><p>新建一个名为<code>stores</code> 的文件夹，并在其中添加此<a href="https://github.com/jdarmada/getting-started-mastra-elastic/blob/main/src/mastra/stores/elastic-store.ts">文件</a>。在 Mastra 和 Elastic 正式推出 Elasticsearch 向量存储集成之前，<a href="https://github.com/abhiaiyer91">Abhi Aiyer</a>（Mastra 首席技术官）分享了名为<code>ElasticVector</code> 的早期原型类。简单地说，它将 Mastra 的内存抽象与 Elasticsearch 的密集向量功能连接起来，因此开发人员可以将 Elasticsearch 作为其代理的向量数据库。</p><p>让我们深入了解整合的重要部分：</p><h3>输入 Elasticsearch 客户端</h3><p>本节定义了<code>ElasticVector</code> 类，并设置了 Elasticsearch 客户端连接，同时支持标准部署和无服务器部署。</p><pre><code>export interface ElasticVectorConfig extends ClientOptions {
    /**
     * Explicitly specify if connecting to Elasticsearch Serverless.
     * If not provided, will be auto-detected on first use.
     */
    isServerless?: boolean;
    
    /**
     * Maximum documents to count accurately when describing indices.
     * Higher values provide accurate counts but may impact performance on large indices.
     * 
     * @default 10000
     */
    maxCountAccuracy?: number;
}

export class ElasticVector extends MastraVector {
    private client: Client;
    private isServerless: boolean | undefined;
    private deploymentChecked: boolean = false;
    private readonly maxCountAccuracy: number;

    constructor(config: ElasticVectorConfig) {
        super();
        this.client = new Client(config);
        this.isServerless = config.isServerless;
        this.maxCountAccuracy = config.maxCountAccuracy ?? 10000;
    }
}</code></pre><ul><li><code>ElasticVectorConfig extends ClientOptions</code>:这将创建一个新的配置接口，继承所有 Elasticsearch 客户端选项（如<code>node</code>,<code>auth</code>,<code>requestTimeout</code> ）并添加我们的自定义属性。这意味着用户可以通过任何有效的 Elasticsearch 配置和我们的无服务器特定选项。</li><li><code>extends MastraVector</code>:这样，<code>ElasticVector</code> 就可以继承 Mastra 的基础<code>MastraVector</code> 类，这是所有矢量存储集成都要遵守的通用接口。这可以确保从代理的角度来看，Elasticsearch 的行为与其他任何 Mastra 向量后端一样。</li><li><code>private client: Client</code>:这是一个私有属性，用于保存 Elasticsearch JavaScript 客户端的实例。这样，班级就可以直接与群集对话。</li><li><code>isServerless</code> 和<code>deploymentChecked</code> ：这些属性共同作用，以检测和缓存我们连接的是无服务器还是标准 Elasticsearch 部署。首次使用时会自动检测，也可以明确配置。</li><li><code>constructor(config: ClientOptions)</code>:该构造函数接收一个配置对象（包含 Elasticsearch 凭据和可选的无服务器设置），并使用它在<code>this.client = new Client(config)</code> 行中初始化客户端。</li><li><code>super()</code>:它调用 Mastra 的基本构造函数，因此继承了日志记录、验证助手和其他内部钩子。</li></ul><p>此时，Mastra 知道有一个名为 <code>ElasticVector</code></p><h3>检测部署类型</h3><p>在创建索引之前，适配器会自动检测您使用的是标准 Elasticsearch 还是 Elasticsearch Serverless。这一点很重要，因为无服务器部署不允许手动配置分片。</p><pre><code>private async detectServerless(): Promise<boolean> {
    // Return cached result if already detected
    if (this.deploymentChecked) {
        return this.isServerless ?? false;
    }

    // Use explicit configuration if provided
    if (this.isServerless !== undefined) {
        this.deploymentChecked = true;
        this.logger?.info(
            `Using explicit deployment type: ${this.isServerless ? 'Serverless' : 'Standard'}`
        );
        return this.isServerless;
    }

    try {
        const info = await this.client.info();
        
        // Primary detection: build flavor (most reliable)
        const isBuildFlavorServerless = info.version?.build_flavor === 'serverless';
        
        // Secondary detection: tagline (fallback)
        const isTaglineServerless = info.tagline?.toLowerCase().includes('serverless') ?? false;
        
        this.isServerless = isBuildFlavorServerless || isTaglineServerless;
        this.deploymentChecked = true;
        
        this.logger?.info(
            `Auto-detected ${this.isServerless ? 'Serverless' : 'Standard'} Elasticsearch deployment`,
            { 
                buildFlavor: info.version?.build_flavor, 
                version: info.version?.number,
                detectionMethod: isBuildFlavorServerless ? 'build_flavor' : 'tagline'
            }
        );
        
        return this.isServerless;
    } catch (error) {
        this.logger?.warn(
            'Could not auto-detect deployment type, assuming Standard Elasticsearch. ' +
            'Set isServerless: true explicitly in config if using Serverless.',
            { error: error instanceof Error ? error.message : String(error) }
        );
        this.isServerless = false;
        this.deploymentChecked = true;
        return false;
    }
}</code></pre><p>发生了什么？</p><ul><li>首先检查您是否在配置中明确设置了<code>isServerless</code> （跳过自动检测）。</li><li>调用 Elasticsearch 的<code>info()</code> API 获取群集信息</li><li>检查<code>build_flavor field</code> （无服务器部署返回<code>serverless</code>)</li><li>如果没有 &quot;构建味道&quot;，则退回到检查标语阶段</li><li>缓存结果，避免重复调用应用程序接口</li><li>如果检测失败，则默认为标准部署</li></ul><p> 使用示例</p><pre><code>// Option 1: Auto-detect (recommended)
const vector = new ElasticVector({
    node: 'https://your-cluster.es.cloud',
    auth: { apiKey: 'your-api-key' }
});
// Detection happens automatically on first index operation

// Option 2: Explicit configuration (faster startup)
const vector = new ElasticVector({
    node: 'https://your-serverless.es.cloud',
    auth: { apiKey: 'your-api-key' },
    isServerless: true  // Skips auto-detection
});</code></pre><h3>在 Elasticsearch 中创建 &quot;内存 &quot;存储</h3><p>下面的函数设置了一个 Elasticsearch 索引，用于存储嵌入式内容。它会检查索引是否已经存在。如果没有，它就会用下面的映射创建一个，其中包含一个<code>dense_vector</code> 字段，用于存储嵌入和自定义相似度度量。</p><p>有些事情需要注意：</p><ul><li><code>dimension</code> 参数是每个嵌入向量的长度，这取决于你使用的嵌入模型。在我们的例子中，我们将使用 OpenAI 的<code>text-embedding-3-small</code> 模型生成嵌入，该模型输出大小为<code>1536</code> 的向量。我们将以此作为默认值。</li><li>下面的映射中使用的<code>similarity</code> 变量是由辅助函数 c<code>onst similarity = this.mapMetricToSimilarity(metric)</code> 定义的，该函数接收<code>metric</code> 参数的值，并将其转换为与 Elasticsearch 兼容的关键字，用于所选的距离度量。<ul><li>例如Mastra 使用<code>cosine</code>,<code>euclidean</code>, 和<code>dotproduct</code> 等一般术语来表示向量相似性。如果我们直接将度量<code>euclidean</code> 传递到 Elasticsearch 映射中，就会出现错误，因为 Elasticsearch 希望关键字<code>l2_norm</code> 代表欧氏距离。</li></ul></li><li>无服务器兼容性：代码会自动省略无服务器部署的分片和副本设置，因为 Elasticsearch Serverless 会自动管理这些设置。</li></ul><pre><code>async createIndex(params: CreateIndexParams): Promise<void> {
    const { indexName, dimension = 1536, metric = 'cosine' } = params;

    try {
        const exists = await this.client.indices.exists({ index: indexName });

        if (exists) {
            try {
                await this.validateExistingIndex(indexName, dimension, metric);
                this.logger?.info(`Index "${indexName}" already exists and is valid`);
                return;
            } catch (validationError) {
                throw new Error(
                    `Index "${indexName}" exists but does not match the required configuration: ${
                        validationError instanceof Error ? validationError.message : String(validationError)
                    }`
                );
            }
        }

        const isServerless = await this.detectServerless();
        const similarity = this.mapMetricToSimilarity(metric);

        const indexConfig: any = {
            index: indexName,
            mappings: {
                properties: {
                    vector: {
                        type: 'dense_vector',
                        dims: dimension,
                        index: true,
                        similarity: similarity,
                    },
                    metadata: {
                        type: 'object',
                        enabled: true,
                        dynamic: true, // Allows flexible metadata structures
                    },
                },
            },
        };

        // Only configure shards/replicas for non-serverless deployments
        // Serverless manages infrastructure automatically
        if (!isServerless) {
            indexConfig.settings = {
                number_of_shards: 1,
                number_of_replicas: 0, // Increase for production HA deployments
            };
        }

        await this.client.indices.create(indexConfig);

        this.logger?.info(
            `Created ${isServerless ? 'Serverless' : 'Standard'} Elasticsearch index "${indexName}"`,
            { dimension, metric, similarity }
        );
    } catch (error) {
        const errorMessage = error instanceof Error ? error.message : String(error);
        this.logger?.error(`Failed to create index "${indexName}": ${errorMessage}`);
        throw new Error(`Failed to create index "${indexName}": ${errorMessage}`);
    }
}</code></pre><h3>互动后存储新的记忆或笔记</h3><p>该函数接收每次交互后生成的新嵌入以及元数据，然后使用 Elastic 的<code>bulk</code> API 将其插入或更新到索引中。<code>bulk</code> API 将多个写入操作合并为一个请求；索引性能的提升确保了在代理内存不断增长的情况下，更新仍能保持高效。</p><pre><code>async upsert(params: UpsertVectorParams): Promise<string[]> {
    const { indexName, vectors, metadata = [], ids } = params;

    try {
        // Generate unique IDs if not provided
        const vectorIds = ids || vectors.map((_, i) => 
            `vec_${Date.now()}_${i}_${Math.random().toString(36).substr(2, 9)}`
        );

        const operations = vectors.flatMap((vec, index) => [
            { index: { _index: indexName, _id: vectorIds[index] } },
            {
                vector: vec,
                metadata: metadata[index] || {},
            },
        ]);

        const response = await this.client.bulk({
            refresh: true,
            operations,
        });

        if (response.errors) {
            const erroredItems = response.items.filter((item: any) => item.index?.error);
            const erroredIds = erroredItems.map((item: any) => item.index?._id);
            const errorDetails = erroredItems.slice(0, 3).map((item: any) => ({
                id: item.index?._id,
                error: item.index?.error?.reason || item.index?.error,
                type: item.index?.error?.type
            }));
            
            const errorMessage = `Failed to upsert ${erroredIds.length}/${vectors.length} vectors`;
            console.error(`${errorMessage}. Sample errors:`, JSON.stringify(errorDetails, null, 2));
            this.logger?.error(errorMessage, { 
                failedCount: erroredIds.length, 
                totalCount: vectors.length,
                sampleErrors: errorDetails 
            });
            
            // Still return successfully inserted IDs
            const successfulIds = vectorIds.filter((id, idx) => 
                !erroredIds.includes(id)
            );
            
            if (successfulIds.length === 0) {
                throw new Error(`${errorMessage}. All operations failed. See logs for details.`);
            }
            
            return successfulIds;
        }

        this.logger?.info(`Successfully upserted ${vectors.length} vectors to "${indexName}"`);
        return vectorIds;
    } catch (error) {
        const errorMessage = error instanceof Error ? error.message : String(error);
        this.logger?.error(`Failed to upsert vectors to "${indexName}": ${errorMessage}`);
        throw new Error(`Failed to upsert vectors to "${indexName}": ${errorMessage}`);
    }
}</code></pre><h3>查询相似向量以实现语义召回</h3><p>该功能是语义召回功能的核心。代理使用向量搜索，在我们的索引中找到类似的存储嵌入。</p><pre><code>async query(params: QueryVectorParams<any>): Promise<QueryResult[]> {
    const { indexName, queryVector, topK = 10, filter, includeVector = false } = params;

    try {
        const knnQuery: any = {
            field: 'vector',
            query_vector: queryVector,
            k: topK,
            num_candidates: Math.max(topK * 10, 100), // Search more candidates for better recall
        };

        // Apply metadata filters if provided
        if (filter) {
            knnQuery.filter = this.buildElasticFilter(filter);
        }

        const sourceFields = ['metadata'];
        if (includeVector) {
            sourceFields.push('vector');
        }

        const response = await this.client.search({
            index: indexName,
            knn: knnQuery,
            size: topK,
            _source: sourceFields,
        });

        const results = response.hits.hits.map((hit: any) => ({
            id: hit._id,
            score: hit._score || 0,
            metadata: hit._source?.metadata || {},
            vector: includeVector ? hit._source?.vector : undefined,
        }));

        this.logger?.debug(`Query returned ${results.length} results from "${indexName}"`);
        return results;
    } catch (error) {
        const errorMessage = error instanceof Error ? error.message : String(error);
        this.logger?.error(`Failed to query vectors from "${indexName}": ${errorMessage}`);
        throw new Error(`Failed to query vectors from "${indexName}": ${errorMessage}`);
    }
}</code></pre><p>引擎盖下</p><ul><li>使用 Elasticsearch 中的<code>knn</code> API 运行<a href="https://www.elastic.co/docs/solutions/search/vector/knn">kNN</a>（k-近邻）查询。</li><li>检索与输入查询向量最相似的 K 个向量。</li><li>可选择应用元数据过滤器来缩小搜索结果范围（例如，仅在特定类别或时间范围内进行搜索）</li><li>返回结构化结果，包括文档 ID、相似性得分和存储的元数据。</li></ul><h2>创建知识代理</h2><p>现在，我们已经通过<code>ElasticVector</code> 集成看到了 Mastra 和 Elasticsearch 之间的连接，让我们来创建知识代理本身。</p><p>在<code>agents</code> 文件夹中，创建一个名为<code>knowledge-agent.ts</code> 的文件。我们可以从连接环境变量和初始化 Elasticsearch 客户端开始。</p><pre><code>import { Agent } from '@mastra/core/agent';
import { Memory } from '@mastra/memory';
import { openai } from '@ai-sdk/openai';
import { Client } from '@elastic/elasticsearch';
import { ElasticVector } from '../stores/elastic-store';
import dotenv from "dotenv";

dotenv.config();

const ELASTICSEARCH_ENDPOINT = process.env.ELASTICSEARCH_ENDPOINT;
const ELASTICSEARCH_API_KEY = process.env.ELASTICSEARCH_API_KEY;

//Error check for undefined credentials
if (!ELASTICSEARCH_ENDPOINT || !ELASTICSEARCH_API_KEY) {
  throw new Error('Missing Elasticsearch credentials');
}

//Check to see if a connection can be established
const testClient = new Client({
  node: ELASTICSEARCH_ENDPOINT,
  auth: { 
    apiKey: ELASTICSEARCH_API_KEY 
  },
});

try {
  await testClient.ping();
  console.log('Connected to Elasticsearch successfully');
} catch (error: unknown) {
  if (error instanceof Error) {
    console.error('Failed to connect to Elasticsearch:', error.message);
  } else {
    console.error('Failed to connect to Elasticsearch:', error);
  }
  process.exit(1);
}
//Initialize the Elasticsearch vector store
const vectorStore = new ElasticVector({
  node: ELASTICSEARCH_ENDPOINT,
  auth: {
    apiKey: ELASTICSEARCH_API_KEY,
  },
//Optional: Explicitly set to true if using Elasticsearch Serverless to skip auto-detection and improve startup time
//isServerless: true,
});</code></pre><p>在这里，我们</p><ul><li>使用<code>dotenv</code> 从<code>.env</code> 文件中加载变量。</li><li>检查 Elasticsearch 凭据是否被正确注入，我们是否能成功建立与客户端的连接。</li><li>在<code>ElasticVector</code> 构造函数中输入 Elasticsearch 端点和 API 密钥，以创建我们之前定义的向量存储实例。</li><li>如果使用 Elasticsearch Serverless，可选择指定<code>isServerless: true</code> 。这样可以跳过自动检测步骤，缩短启动时间。如果省略，适配器将在首次使用时自动检测您的部署类型。</li></ul><p>接下来，我们可以使用 Mastra 的<code>Agent</code> 类来定义代理。</p><pre><code>export const knowledgeAgent = new Agent({
    name: 'KnowledgeAgent',
    instructions: 'You are a helpful knowledge assistant.',
    model: openai('gpt-4o'),
    memory: new Memory({

        vector: vectorStore,

        //embedder used to create embeddings for each message
        embedder: 'openai/text-embedding-3-small',

        //set semantic recall options
        options: {
            semanticRecall: {
                topK: 3, // retrieve 3 similar messages
                messageRange: 2, // include 2 messages before/after each match
                scope: 'resource',
            },
        },
    }),
});</code></pre><p>我们可以定义的字段有</p><ul><li><code>name</code> 和<code>instructions</code> ：赋予其特性和主要功能。</li><li><code>model</code>:我们通过<code>@ai-sdk/openai</code> 软件包使用 OpenAI 的<code>gpt-4o</code> 。</li><li><code>memory</code>:<ul><li><code>vector</code>:指向我们的 Elasticsearch 存储库，因此嵌入式会从那里存储和检索。</li><li><code>embedder</code>:使用哪种模型生成嵌入模型</li><li><code>semanticRecall</code> 选项决定召回如何进行：<ul><li><code>topK</code>:检索多少条语义相似的信息。</li><li><code>messageRange</code>:每场比赛应包括多少对话内容。</li><li><code>scope</code>:定义内存边界。</li></ul></li></ul></li></ul><p>快好了我们只需将新创建的代理添加到 Mastra 配置中。在名为<a href="http://index.ts/"><code>index.ts</code></a> 的文件中，导入知识代理并将其插入<code>agents</code> 字段。</p><pre><code>export const mastra = new Mastra({
  agents: { knowledgeAgent },
  storage: new LibSQLStore({
    // stores observability, scores, ... into memory storage, if it needs to persist, change to file:../mastra.db
    url: ":memory:",
  }),
  logger: new PinoLogger({
    name: 'Mastra',
    level: 'info',
  }),
  telemetry: {
    // Telemetry is deprecated and will be removed in the Nov 4th release
    enabled: false, 
  },
  observability: {
    // Enables DefaultExporter and CloudExporter for AI tracing
    default: { enabled: true }, 
  },
});</code></pre><p>其他领域包括</p><ul><li><code>storage</code>:这是 Mastra 的内部数据存储，用于存储运行历史、可观察性指标、分数和缓存。有关 Mastra 存储的更多信息，请访问<a href="https://mastra.ai/docs/server-db/storage">此处</a>。</li><li><code>logger</code>:Mastra 使用<a href="https://github.com/pinojs/pino">Pino</a>，这是一个轻量级结构化 JSON 日志记录器。它可捕捉代理启动和停止、工具调用和结果、错误以及 LLM 响应时间等事件。</li><li><code>observability</code>:控制人工智能跟踪和代理执行的可见性。它可以跟踪<ul><li>每个推理步骤的开始/结束。</li><li>使用了哪种模式或工具。</li><li>输入和输出。</li><li>分数和评估</li></ul></li></ul><h3>使用 Mastra Studio 测试代理</h3><p>祝贺你如果您已经到达这里，那么您就可以运行这个代理，测试它的语义回忆能力了。幸运的是，Mastra 提供了一个内置的聊天用户界面，这样我们就不必自己创建了。</p><p>要启动 Mastra 开发服务器，请打开终端并运行以下命令：</p><pre><code>npm run dev</code></pre><p>在初始捆绑和启动服务器后，它应该会为你提供一个 Playground 的地址。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/8b045f70008d26aec4d2e6b59d61085555b9c5b2-686x116.png" style="width: 100%;" /><p>将此地址粘贴到浏览器中，您将看到 Mastra Studio。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/69bc80fe8486edd9e0cf91d87b39f465aeb23111-1600x438.png" style="width: 100%;" /><p>选择<code>knowledgeAgent</code> ，然后开始聊天。</p><p>为了快速测试一切接线是否正确，请给它提供一些信息，如 &quot;团队宣布 10 月份的销售业绩增长了 12% ，主要是由企业续订驱动的。下一步是扩大对中端市场客户的拓展&quot;。下一步，开始新的聊天，并提问：&quot;我们说过下一步需要关注哪个客户群？知识代理应该能够回忆起您在第一次聊天中提供给它的信息。您应该会看到类似的回复：</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/da8ebddad89874023ed440a8f1ad2cb04ed043f4-1070x288.png" style="width: 100%;" /><p>看到这样的响应，意味着代理成功地将我们之前的信息以嵌入的形式存储在 Elasticsearch 中，并在稍后使用向量搜索进行检索。</p><h3>检查代理的长期记忆存储</h3><p>在 Mastra Studio 的代理配置中，前往<code>memory</code> 选项卡。这可以让您了解您的代理随着时间的推移学到了什么。嵌入并存储在 Elasticsearch 中的每一条消息、响应和交互都会成为长期记忆的一部分。您可以对过去的交互进行语义搜索，以快速找到代理之前了解到的信息或上下文。这与代理在语义回想时使用的机制基本相同，但在这里你可以直接检查它。在下面的示例中，我们搜索 &quot;销售 &quot;一词，并返回所有包含销售内容的互动。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/3decaa0c332d288c5ae0b11c25f592c7d50c2f0f-1104x1320.png" style="width: 100%;" /><h2>结论</h2><p>通过连接 Mastra 和 Elasticsearch，我们可以为代理提供内存，这是上下文工程的关键层。有了语义记忆功能，代理可以随着时间的推移建立上下文，将他们的反应建立在所学知识的基础上。这意味着更准确、更可靠、更自然的互动。</p><p>早期的整合只是一个起点。同样的模式可以让支持代理记住过去的票单，让内部机器人检索相关文档，或者让人工智能助理在对话中回忆起客户的详细信息。我们还在努力实现与 Mastra 的正式集成，以便在不久的将来使这种搭配更加完美。</p><p>我们很期待看到您的下一个作品。试试吧，探索<a href="https://mastra.ai/">Mastra</a>及其内存功能，并随时与社区分享您的发现。</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[利用 Elasticsearch 和 SigLIP-2 对山峰进行多模式搜索 ]]></title>
            <description><![CDATA[了解如何使用 SigLIP-2 嵌入和 Elasticsearch kNN 向量搜索实现文本到图像和图像到图像的多模态搜索。项目重点：寻找珠峰徒步旅行中拍摄的阿玛达布拉姆峰照片。]]></description>
            <link>https://www.elastic.co/search-labs/cn/blog/multimodal-search-siglip-2-elasticsearch</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/cn/blog/multimodal-search-siglip-2-elasticsearch</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Navneet Kumar]]></dc:creator>
            <pubDate>Tue, 04 Nov 2025 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/ffcf93358a7c5dadcea82faf3de460bf060d003c-1600x1200.png?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>您是否曾想过按含义搜索相册？试着询问 &quot;给我看我穿着蓝色夹克坐在长椅上的照片&quot;、&quot;给我看珠穆朗玛峰的照片 &quot;或 &quot;清酒和寿司&quot;。喝杯咖啡（或您最喜欢的饮料），继续阅读。在本博客中，我们将向您展示如何构建多模态混合搜索应用程序。多模态是指应用程序可以理解和搜索不同类型的输入（文本、图像和音频），而不仅仅是文字。混合式意味着它结合了关键词匹配、kNN 向量搜索和地理围栏等技术，以提供更清晰的结果。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/0ec6bbb45013846b59ee00d2bf73ee2182ee7392-1920x1080.gif" style="width: 100%;" /><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>按名称查找山峰图片</li><li>从图片中猜测山峰名称，并在相册中找到类似的山峰</li><li>让概念查询发挥作用<em>（人</em>、<em>河流</em>、<em>祈祷旗</em> <em>等）</em></li></ul><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/e9d0d720a9b565d5b749bdc915068852d4f157ad-1200x1600.png" style="width: 100%;" /><h2>组建梦之队：SigLIP-2、Elasticsearch&amp; Streamlit</h2><p>很快我们就发现，要想实现这一目标，我们需要将文字（&quot;阿玛达布拉姆&quot;）和图像（我相册中的照片）都转化为可以进行有意义比较的矢量，即在同一个矢量空间中。一旦我们做到了这一点，搜索就只是 &quot;寻找最近的邻居&quot;。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/20e6f8b7d4fa48414f407ec200adbe00ee28d517-1536x1024.png" style="width: 100%;" /><div style="display:none">Unknown block type "block", specify a component for it in the `components.types` option</div><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>
            <table style="border-collapse: collapse; margin: 20px 0;">
              <tbody>
                
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">现场</td><td style="border: 1px solid #ccc; padding: 4px;">类型</td><td style="border: 1px solid #ccc; padding: 4px;">示例</td><td style="border: 1px solid #ccc; padding: 4px;">目的/说明</td><td style="border: 1px solid #ccc; padding: 4px;">矢量/索引</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">本我</td><td style="border: 1px solid #ccc; padding: 4px;">关键词</td><td style="border: 1px solid #ccc; padding: 4px;">阿玛-达布拉姆</td><td style="border: 1px solid #ccc; padding: 4px;">稳定的弹头/ID</td><td style="border: 1px solid #ccc; padding: 4px;">-</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">姓名</td><td style="border: 1px solid #ccc; padding: 4px;">文本 + 关键字子字段</td><td style="border: 1px solid #ccc; padding: 4px;">["Ama Dablam","Amadablam"]</td><td style="border: 1px solid #ccc; padding: 4px;">别名/多语言名称；names.raw 用于精确筛选</td><td style="border: 1px solid #ccc; padding: 4px;">-</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">纬纶</td><td style="border: 1px solid #ccc; padding: 4px;">地理点</td><td style="border: 1px solid #ccc; padding: 4px;">{"lat":27.8617,"lon":86.8614}</td><td style="border: 1px solid #ccc; padding: 4px;">以经纬度组合形式显示的山顶 GPS 坐标（可选）</td><td style="border: 1px solid #ccc; padding: 4px;">-</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">海拔_m</td><td style="border: 1px solid #ccc; padding: 4px;">整数</td><td style="border: 1px solid #ccc; padding: 4px;">6812</td><td style="border: 1px solid #ccc; padding: 4px;">海拔（可选）</td><td style="border: 1px solid #ccc; padding: 4px;">-</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">嵌入文本</td><td style="border: 1px solid #ccc; padding: 4px;">dense_vector</td><td style="border: 1px solid #ccc; padding: 4px;">768</td><td style="border: 1px solid #ccc; padding: 4px;">该山峰的混合原型（提示和可选的 1-3 幅参考图片</td><td style="border: 1px solid #ccc; padding: 4px;">index:true, similarity:"cosine", index_options：{type:"hnsw", m:16, ef_construction:128}</td>
                    </tr>
              </tbody>
            </table>
          <p>该索引主要用于图像到图像的搜索，例如从图像中识别山峰。我们还使用该索引来增强文本到图片的搜索结果。</p><p>总之，<code>peaks_catalog</code> 将问题&quot;&quot;这是什么山？&quot; &quot;转化为一个重点突出的 &quot;最近邻问题&quot;，有效地将概念理解与图像数据的复杂性分离开来。</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>创建文本原型：<ul><li>山峰名称</li><li><a href="https://github.com/navneet83/multimodal-mountain-peak-search/blob/main/scripts/embed_and_index_photos.py#L301">提示组合</a>（使用多个不同的提示来尝试回答同一个问题），例如<ul><li>&quot;尼泊尔喜马拉雅山脉山峰的自然照片{name} &quot;</li><li>&quot;{name} 昆布地区的地标性山峰，高山景观&quot;</li><li>&quot;{name} 山顶，积雪，岩石山脊线&quot;</li></ul></li><li>可选的<a href="https://github.com/navneet83/multimodal-mountain-peak-search/blob/main/scripts/embed_and_index_photos.py#L333">反概念</a>（告诉 SigLIP-2 什么不能匹配）：为 &quot;绘画、插图、海报、地图、徽标 &quot;减去一个小矢量，这样我们就偏向于真实照片。</li></ul></li><li>如果提供了峰值的参考图像，可选择<a href="https://github.com/navneet83/multimodal-mountain-peak-search/blob/main/scripts/embed_and_index_photos.py#L388C13-L388C29">创建图像原型</a>。</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><pre><code>def l2norm(v: np.ndarray) -> 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,
) -> 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 (<= 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 > 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]</code></pre><p><code>peaks_catalog</code> 索引中的文件样本：</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/bc05fbd0c4f8d721d5170c28a3884a9eda80bb7d-1210x1132.png" style="width: 100%;" /><h4>照片索引</h4><p>该主索引存储相册中所有照片的详细信息。每份文档代表一张照片，包含以下信息：</p><ul><li>相册中照片的相对路径。可用于查看匹配图像或在搜索用户界面中加载图像。</li><li>图片的 GPS 和时间信息。</li><li>SigLIP-2 生成的图像编码密集矢量。</li><li><code>predicted_peaks</code> 可让我们根据峰名进行筛选。<strong>索引映射</strong></li></ul>
            <table style="border-collapse: collapse; margin: 20px 0;">
              <tbody>
                
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">现场</td><td style="border: 1px solid #ccc; padding: 4px;">类型</td><td style="border: 1px solid #ccc; padding: 4px;">示例</td><td style="border: 1px solid #ccc; padding: 4px;">目的/说明</td><td style="border: 1px solid #ccc; padding: 4px;">矢量/索引</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">路径</td><td style="border: 1px solid #ccc; padding: 4px;">关键词</td><td style="border: 1px solid #ccc; padding: 4px;">data/images/IMG_1234.HEIC</td><td style="border: 1px solid #ccc; padding: 4px;">用户界面如何打开缩略图/全图</td><td style="border: 1px solid #ccc; padding: 4px;">-</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">剪贴图片</td><td style="border: 1px solid #ccc; padding: 4px;">dense_vector</td><td style="border: 1px solid #ccc; padding: 4px;">768</td><td style="border: 1px solid #ccc; padding: 4px;">SigLIP-2 图像嵌入</td><td style="border: 1px solid #ccc; padding: 4px;">index:true, similarity:"cosine", index_options：{type:"hnsw", m:16, ef_construction:128}</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">预测峰值</td><td style="border: 1px solid #ccc; padding: 4px;">关键词</td><td style="border: 1px solid #ccc; padding: 4px;">["ama-dablam","pumori"]</td><td style="border: 1px solid #ccc; padding: 4px;">索引时的 Top-K 猜想（廉价用户体验过滤器/面）</td><td style="border: 1px solid #ccc; padding: 4px;">-</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">全球定位系统</td><td style="border: 1px solid #ccc; padding: 4px;">地理点</td><td style="border: 1px solid #ccc; padding: 4px;">{"lat":27.96,"lon":86.83}</td><td style="border: 1px solid #ccc; padding: 4px;">启用地理筛选器</td><td style="border: 1px solid #ccc; padding: 4px;">-</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">拍摄时间</td><td style="border: 1px solid #ccc; padding: 4px;">date</td><td style="border: 1px solid #ccc; padding: 4px;">2023-10-18T09:41:00Z</td><td style="border: 1px solid #ccc; padding: 4px;">捕捉时间：排序/过滤</td><td style="border: 1px solid #ccc; padding: 4px;">-</td>
                    </tr>
              </tbody>
            </table>
          <p><strong>照片索引的索引策略： </strong>对于相册中的每张照片，我们会采取以下措施：<br/><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><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> 字段中。</li><li><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 个山峰，忽略其余的。</li><li>我们通过对图片名称和路径进行<a href="https://github.com/navneet83/multimodal-mountain-peak-search/blob/main/scripts/embed_and_index_photos.py#L509">散列</a>计算<code>_id</code> 字段。这可以确保我们在多次运行后不会出现重复。</li></ul><p>一旦我们确定了照片的所有字段，就会使用<a href="https://github.com/navneet83/multimodal-mountain-peak-search/blob/main/scripts/embed_and_index_photos.py#L530"> 批量 索引对照片文件进行</a> 批量 索引：</p><pre><code>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",
) -> 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) >= 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")</code></pre><p>照片索引中的样本文件：</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/1dc1406ac2a97440b6804838795b3c2205c4c6b2-1080x1234.png" style="width: 100%;" /><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://cdn.sanity.io/images/me0ej585/search-labs-import-testing/1c75d0ed0ce53d28a94bf2f47a354e25581d2baf-1600x1402.png" style="width: 100%;" /><h2>搜索用例</h2><p><strong>按名称搜索（文本到图像）：</strong>该功能可让用户使用文本查询查找山峰照片（甚至是 &quot;祈祷旗 &quot;等抽象概念）。为此，使用 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://cdn.sanity.io/images/me0ej585/search-labs-import-testing/b5fac28ffdbedb820505365ca07df125cd01b939-946x370.png" style="width: 100%;" /><p><strong>带有地理过滤器的 Elasticsearch 查询：</strong></p><pre><code>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"
       }
     }
}</code></pre><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://cdn.sanity.io/images/me0ej585/search-labs-import-testing/3a3d5635bee7a222b95529dd7f9fbee016381610-1226x550.png" style="width: 100%;" /><p><strong>Elasticsearch 查询：</strong></p><p>第 1 步：找到匹配的山峰名称</p><pre><code>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...
         ]
       }
     }
   ]
 }
}</code></pre><p>第 2 步：在<code>photos</code> 索引上进行搜索，找到匹配的图片（与文本到图片搜索用例中的查询相同）：</p><pre><code>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"
       }
     }
}</code></pre><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://cdn.sanity.io/images/me0ej585/search-labs-import-testing/dca540cbf5279e6d6102c5a0c0351ddd4ac91cda-1600x1112.png" style="width: 100%;" /><p><strong>文本搜索</strong>从文本中查找相册中匹配的峰值</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/b1e8618db746cd49ea4962d3dc73031387b975dd-1600x1166.png" style="width: 100%;" /><h2>结论</h2><p><em>我们能看看 <strong>阿玛-达布拉姆</strong> 的照片吗？</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><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>博客中讨论。</li><li><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>。</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><strong>GitHub 代码库</strong> <a href="https://github.com/navneet83/multimodal-mountain-peak-search"><em>： https://github.com/navneet83/multimodal-mountain-peak-search</em></a></li><li><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></li></ul><p>我们的旅程就此结束，是时候飞回去了。希望这对你有帮助，如果你改动（或改进）了它，我很乐意听听你的改动。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/d324d1e1472f1bfbd8f25747f57bdeeb9c7f16b2-1600x1200.png" style="width: 100%;" />]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[利用混合搜索重排提高多语言嵌入模型的相关性]]></title>
            <description><![CDATA[了解如何在 Elasticsearch 中使用 Cohere 的 reranker 和混合搜索提高 E5 多语言嵌入模型搜索结果的相关性。]]></description>
            <link>https://www.elastic.co/search-labs/cn/blog/multilingual-embedding-model-hybrid-search-reranking</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/cn/blog/multilingual-embedding-model-hybrid-search-reranking</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Quynh Nguyen]]></dc:creator>
            <pubDate>Mon, 03 Nov 2025 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/d341b04acecc8eeec321f5404e1643447ecc8526-720x420.png?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<h2>引言</h2><p>在<a href="https://www.elastic.co/search-labs/blog/multilingual-embedding-model-deployment-elasticsearch">本系列的最后一部分</a>，我们介绍了如何部署 Elastic 预训练的 E5 模型（以及 Hugging Face 提供的其他多语言文本嵌入模型），并深入探讨了如何使用 Elasticsearch 和 Kibana 从文本数据中生成密集向量嵌入。在本博客中，我们将研究这些嵌入的结果，并强调利用多语言模型的显著优势。</p><p>现在我们已经有了索引<code>coco_multilingual</code> ，执行搜索时将会得到多种语言的文档，其中的 &quot;en &quot;字段可供我们参考：</p><pre><code># GET coco_multilingual/_search
    {
       "_index": "coco_multilingual",
       "_id": "WAiXQJYBgf6odR9bLohZ",
       "_score": 1,
       "_source": {
         "description": "Ein Parkmeßgerät auf einer Straße mit Autos",
         "en": "A row of parked cars sitting next to parking meters.",
         "language": "de",
         "vector_description": {...}
       }
     },
     . . .</code></pre><h2>用英语进行搜索</h2><p>让我们尝试用英语进行搜索，看看效果如何：</p><pre><code>GET coco_multi/_search
{
"size": 10,
"_source": [
  "description", "language", "en"
],
"knn": {
  "field": "vector_description.predicted_value",
  "k": 10,
  "num_candidates": 100,
  "query_vector_builder": {
    "text_embedding": {
      "model_id": ".multilingual-e5-small_linux-x86_64_search",
      "model_text": "query: kitty"
    }
  }
}
}</code></pre><pre><code>{
       "_index": "coco_multi",
       "_id": "JQiXQJYBgf6odR9b6Yz0",
       "_score": 0.9334303,
       "_source": {
         "description": "Eine Katze, die in einem kleinen, gepackten Koffer sitzt.",
         "en": "A brown and white cat is in a suitcase.",
         "language": "de"
       }
     },
      {
       "_index": "coco_multi",
       "_id": "3AiXQJYBgf6odR9bFod6",
       "_score": 0.9281012,
       "_source": {
         "description": "Una bambina che tiene un gattino vicino a una recinzione blu.",
         "en": "A little girl holding a kitten next to a blue fence.",
         "language": "it"
       }
     },
     . . .</code></pre><p>在这里，尽管查询看起来非常简单，但我们要搜索的是 &quot;kitty &quot;一词在所有语言的所有文档中的数字嵌入。由于我们执行的是矢量搜索，因此可以从语义上搜索所有可能与 &quot;kitty &quot;相关的词：猫&quot;、&quot;小猫&quot;、&quot;猫科动物&quot;、&quot;gatto&quot;（意大利语）、&quot;mèo&quot;（越南语）、고양이（韩语）、猫（汉语）等。因此，即使我的查询是以英语进行的，我们也可以搜索到所有其他语言的内容。例如，搜索 kitty l<code>ying on something</code> ，也会出现意大利语、荷兰语或越南语的文档。谈效率</p><h2>搜索其他语言的内容</h2><pre><code>GET coco_multi/_search
{  
 "size": 100,
 "_source": [
   "description", "language", "en"
 ],
 "knn": {
   "field": "vector_description.predicted_value",
   "k": 50,
   "num_candidates": 1000,
   "query_vector_builder": {
     "text_embedding": {
       "model_id": ".multilingual-e5-small_linux-x86_64_search",
       "model_text": "query: kitty lying on something"
     }
   }
 }
}</code></pre><pre><code>{
 "description": "A black kitten lays on her side beside remote controls.",
 "en": "A black kitten lays on her side beside remote controls.",
 "language": "en"
},
{
 "description": "un gattino sdraiato su un letto accanto ad alcuni telefoni ",
 "en": "A black kitten lays on her side beside remote controls.",
 "language": "it"
},
{
 "description": "eine Katze legt sich auf ein ausgestopftes Tier",
 "en": "a cat lays down on a stuffed animal",
 "language": "de"
},
{
 "description": "Một chú mèo con màu đen nằm nghiêng bên cạnh điều khiển từ xa.",
 "en": "A black kitten lays on her side beside remote controls.",
 "language": "vi"
}
. . .</code></pre><p>同样，用韩语（&quot;고양이&quot;）搜索关键词 &quot;猫 &quot;也会得到有意义的结果。令人惊叹的是，在这份索引中，我们甚至没有任何韩文文件！</p><pre><code>GET coco_multi/_search
{
 "size": 100,
 "_source": [
   "description", "language", "en"
 ],
 "knn": {
   "field": "vector_description.predicted_value",
   "k": 50,
   "num_candidates": 1000,
   "query_vector_builder": {
     "text_embedding": {
       "model_id": ".multilingual-e5-small_linux-x86_64_search",
       "model_text": "query: 고양이"
     }
   }
 }
}</code></pre><pre><code> {
       {
         "description": "eine Katze legt sich auf ein ausgestopftes Tier",
         "en": "a cat lays down on a stuffed animal",
         "language": "de"
       }
     },
     {
       {
         "description": "Một con chó và con mèo đang ngủ với nhau trên một chiếc ghế dài màu cam.",
         "en": "A dog and cat lying  together on an orange couch. ",
         "language": "vi"
       }
     },</code></pre><p>这是因为嵌入模型在共享语义空间中表示意义，即使查询语言与索引标题不同，也能检索到相关图像。</p><h2>利用混合搜索和重新排序增加相关搜索结果</h2><p>我们很高兴相关结果如期出现。但是，在现实世界中，比如在电子商务或 RAG 应用程序中，需要缩小到前 5-10 个最适用的结果，我们可以使用 rerank 模型来优先处理最相关的结果。</p><p>在这里，用越南语查询 &quot;猫是什么颜色的？&quot;会得到很多结果，但前 1 或前 2 个结果可能不是最相关的。</p><pre><code>GET coco_multi/_search
{
 "size": 20,
 "_source": [
   "description",
   "language",
   "en"
 ],
 "knn": {
   "field": "vector_description.predicted_value",
   "k": 20,
   "num_candidates": 1000,
   "query_vector_builder": {
     "text_embedding": {
       "model_id": ".multilingual-e5-small_linux-x86_64_search",
       "model_text": "query: con mèo màu gì?"
     }
   }
 }
}</code></pre><p>结果都提到了猫或某种颜色：</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/33e1e887dbbdd1066cfedc7375f5e3b46538529e-859x847.png" style="width: 100%;" /><p>因此，让我们来改进这一点！让我们结合<a href="https://cohere.com/blog/rerank-3pt5">Cohere</a>的多语言 rerank 模型来改进与我们的问题相对应的推理。</p><pre><code>PUT _inference/rerank/cohere_rerank
{
 "service": "cohere",
 "service_settings": {
   "api_key": "your_api_key",
   "model_id": "rerank-v3.5"
 },
 "task_settings": {
   "top_n": 10,
   "return_documents": true
 }
}


GET coco_multi/_search
{
"size": 10,
"_source": [
  "description",
  "language",
  "en"
],
"retriever": {
  "text_similarity_reranker": {
    "retriever": {
      "rrf": {
        "retrievers": [
          {
            "knn": {
              "field": "vector_description.predicted_value",
              "k": 50,
              "num_candidates": 100,
              "query_vector_builder": {
                "text_embedding": {
                  "model_id": ".multilingual-e5-small_linux-x86_64_search",
                  "model_text": "query: con mèo màu gì?" // English: What color is the cat?
                }
              }
            }
          }
        ],
        "rank_window_size": 100,
        "rank_constant": 0
      }
    },
    "field": "description",
    "inference_id": "cohere_rerank",
    "inference_text": "con mèo màu gì?"
  }
}
}</code></pre><pre><code> {
       "_index": "coco_multi",
       "_id": "rQiYQJYBgf6odR9bBYyH",
       "_score": 1.5501487,
       "_source": {
         "description": "Hai cái điện thoại được đặt trên một cái chăn cạnh một con mèo con màu đen.",
         "en": "A black kitten lays on her side beside remote controls.",
         "language": "vi"
       }
     },
     {
       "_index": "coco_multi",
       "_id": "swiXQJYBgf6odR9b04uf",
       "_score": 1.5427427,
       "_source": {
         "description": "Một con mèo sọc nâu nhìn vào máy quay.", // Real translation: A brown striped cat looks at the camera 
         "en": "This cat is sitting on a porch near a tire.",
         "language": "vi"
       }
     },</code></pre><p>现在，有了最重要的结果，我们的应用程序就可以肯定地回答，这只小猫的颜色是黑色或带条纹的棕色。更有趣的是，我们的矢量搜索实际上发现了原始数据集中英文标题的遗漏。尽管参考英译本忽略了这一细节，但它还是能找到那只棕色条纹的猫。这就是矢量搜索的威力。</p><h2>结论</h2><p>在这篇博客中，我们介绍了多语言嵌入模型的实用性，以及如何利用 Elasticsearch 来集成模型以生成嵌入，并通过混合搜索和重新分词器来有效提高相关性和准确性。您可以<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> ，<a href="https://www.elastic.co/docs/explore-analyze/machine-learning/nlp/ml-nlp-e5"> 使用我们开箱即用的 E5 模型</a> 在您选择的语言和数据集上尝试 多语言语义搜索 。</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[在 Elasticsearch 中部署多语言嵌入模型]]></title>
            <description><![CDATA[了解如何在 Elasticsearch 中为向量搜索和跨语言检索部署 e5 多语言嵌入模型。]]></description>
            <link>https://www.elastic.co/search-labs/cn/blog/multilingual-embedding-model-deployment-elasticsearch</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/cn/blog/multilingual-embedding-model-deployment-elasticsearch</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Quynh Nguyen]]></dc:creator>
            <pubDate>Wed, 22 Oct 2025 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/8f2aa7bebb6b2f701e274ba7282273f9ab4abed6-720x432.png?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<h2>引言</h2><p>在用户遍布全球的世界里，跨语言信息检索（CLIR）至关重要。CLIR 可让您以<em>任何</em>语言查找信息，而不是将搜索局限于单一语言，从而增强用户体验并简化操作。想象一下，在全球市场上，电子商务客户可以用自己的语言搜索商品，正确的结果就会出现，而无需提前对数据进行本地化。或者，学术研究人员可以用自己的母语搜索论文，即使资料来源是另一种语言，也会有细微差别和复杂性。</p><p>多语言文本嵌入模型让我们能够做到这一点。嵌入是一种用数字向量表示文本含义的方法。设计这些向量的目的是让含义相似的文本在高维空间中彼此靠近。多语言文本嵌入模型专门用于将不同语言中具有相同含义的单词和短语映射到相似的向量空间中。</p><p>开源的多语言 E5 等模型是在海量文本数据的基础上进行训练的，通常使用对比学习等技术。在这种方法中，模型学会区分意义相似的文本对（正对）和意义不同的文本对（负对）。对模型进行训练，以调整其产生的向量，从而使正向配对之间的相似性最大化，反向配对之间的相似性最小化。对于多语言模型，这些训练数据包括不同语言的文本对，这些文本对互为翻译，从而使模型能够学习多种语言的共享表示空间。由此产生的嵌入结果可用于各种 NLP 任务，包括跨语言搜索，在跨语言搜索中，文本嵌入之间的相似性可用于查找相关文档，而不受查询语言的限制。</p><h2>多语言矢量搜索的优势</h2><ul><li><strong>细致入微</strong>：矢量搜索擅长捕捉语义，超越关键词匹配。这对于需要理解语境和语言微妙之处的任务至关重要。</li><li><strong>跨语言理解</strong>：即使查询和文档使用不同的词汇，也能跨语言进行有效的信息检索。</li><li><strong>相关性</strong>：通过关注查询和文档之间的概念相似性，提供更相关的结果。</li></ul><p>例如，一位学术研究人员正在研究&quot;社交媒体对不同国家政治话语的影响&quot; 。通过矢量搜索，他们可以输入&quot;l&#x27;impatto dei social media sul discorso politico&quot; (意大利文) 或&quot;ảnh hưởng của mạng xã hội đối với diễn ngôn chính trị&quot; (越南文) 等查询，并找到相关的英文论文、西班牙语或任何其他索引语言的相关论文。这是因为矢量搜索可以识别讨论社交媒体对政治影响这一<em>概念</em>的论文，而不仅仅是包含确切关键词的论文。这大大提高了他们研究的广度和深度。</p><h2>开始使用</h2><p>下面介绍如何使用 Elasticsearch（开箱即用的 E5 模型）设置 CLIR。我们将使用<a href="https://huggingface.co/datasets/romrawinjp/multilingual-coco">开源的多语言 COCO 数据集</a>（其中包含多种语言的图片说明）来帮助我们可视化两种类型的搜索：</p><ol><li>一个英语数据集上的其他语言查询和搜索词，以及</li><li>在包含多语言文档的数据集上进行多语言查询。</li></ol><p>然后，我们将利用混合搜索和重新排序的功能，进一步改进搜索结果。</p><h2>准备工作</h2><ul><li>Python 3.6+</li><li>Elasticsearch 8+</li><li>Elasticsearch Python 客户端： pip install elasticsearch</li></ul><h2>数据集</h2><p><a href="https://huggingface.co/datasets/romrawinjp/multilingual-coco">COCO 数据集是</a>一个大型字幕数据集。数据集中的每张图片都有多种不同语言的标题，每种语言都有几种翻译。为便于演示，我们将把每份译文作为单独的文件进行索引，并附上第一份可用的英文译文以供参考。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/d4f0632529c71a22fbdecf21c9f4f0bb64b8e69c-1600x567.png" style="width: 100%;" /><h3>步骤 1：下载多语种 COCO 数据集</h3><p>为了简化博客并方便阅读，我们在这里通过简单的 API 调用，将 restval 的前 100 行加载到本地 JSON 文件中。或者，您也可以使用 HuggingFace 的库数据集来加载整个数据集或数据集的子集。</p><pre><code>import requests
import json
import os
### Download multilingual coco dataset into a json file (for easy viewing)
### Here we are retrieving first 100 rows for this example
### Alternatively, you can use `datasets` library from Hugging Face
url = "https://datasets-server.huggingface.co/rows?dataset=romrawinjp%2Fmultilingual-coco&config=default&split=restval&offset=0&length=100"
response = requests.get(url)


if response.status_code == 200:
   data = response.json()
   output_file = "multilingual_coco_sample.json" 
   ### Loading the downloaded content into a json file locally
   with open(output_file, "w", encoding="utf-8") as f:
       json.dump(data, f, indent=4, ensure_ascii=False)
   print(f"Data successfully downloaded and saved to {output_file}")
else:
   print(f"Failed to download data: {response.status_code}")
   print(response.text)</code></pre><p>如果数据成功加载到 JSON 文件中，你应该会看到类似下面的内容：</p><p><code>Data successfully downloaded and saved to multilingual_coco_sample.json</code></p><h3>第 2 步：（启动 Elasticsearch）并在 Elasticsearch 中编制数据索引</h3><p>a) 启动本地 Elasticsearch 服务器。</p><p>b) 启动 Elasticsearch 客户端。</p><pre><code>from elasticsearch import Elasticsearch
from getpass import getpass


# Initialize Elasticsearch client
es = Elasticsearch(getpass("Host: "), api_key=getpass("API Key: "))


index_name = "coco"


# Create the index if it doesn't exist
if not es.indices.exists(index=index_name):
   es.indices.create(index=index_name, body=mapping)</code></pre><p>c) 指数数据</p><pre><code># Load the JSON data
with open('./multilingual_coco_sample.json', 'r') as f:
   data = json.load(f)


rows = data["rows"]
# List of languages to process
languages = ["en", "es", "de", "it", "vi", "th"]


# For each image, we will process each individual caption as its own document
bulk_data = []
for data in rows:
   row = data["row"]
   image = row.get("image")
   image_url = image["src"]


   # Process each language
   for lang in languages:
       # Skip if language not present in this row
       if lang not in row:
           continue


       # Get all descriptions for this language
 # along with first available English caption for reference
       descriptions = row[lang]
       first_eng_caption = row["en"][0]


       # Prepare bulk indexing data
       for description in descriptions:
           if description == "":
               continue
           # Add index operation
           bulk_data.append(
               {"index": {"_index": index_name}}
           )
           # Add document
           bulk_data.append({
               "language": lang,
               "description": description,
               "en": first_eng_caption,
               "image_url": image_url,
           })


# Perform bulk indexing
if bulk_data:
   try:
       response = es.bulk(operations=bulk_data)
       if response["errors"]:
           print("Some documents failed to index")
       else:
           print(f"Successfully bulk indexed {len(bulk_data)} documents")
   except Exception as e:
       print(f"Error during bulk indexing: {str(e)}")


print("Indexing complete!")</code></pre><p>数据编入索引后，您应该会看到类似下面的内容：</p><p><code>Successfully bulk indexed 4840 documents</code></p><p><code>Indexing complete!</code></p><h3>步骤 3：部署 E5 训练模型</h3><p>在 Kibana 中，导航到 &quot;堆栈管理&gt; <strong>训练模型</strong>&quot;页面，然后单击 &quot;<strong>部署</strong>.multilingual-e5-small_linux-x86_64选择。这款 E5 机型是为 linux-x86_64 优化的小型多语言机型，开箱即可使用。单击 &quot;部署 &quot;将显示一个屏幕，您可以在此调整部署设置或 vCPU 配置。为简单起见，我们将使用默认选项，并选择自适应资源，它将根据使用情况自动调整部署规模。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/95cd8f352425d1db2d04b00c3c88d1e71d1ef19a-1600x440.png" style="width: 100%;" /><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/1599d99949dda8267acc58f400a403a3af5373ef-1600x655.png" style="width: 100%;" /><p>如果您想使用其他文本嵌入模型，也可以选择使用。例如，要使用 BGE-M3，可以使用<a href="https://www.elastic.co/docs/reference/elasticsearch/clients/eland/machine-learning#ml-nlp-pytorch">Elastic 的 Eland Python 客户端</a>从 HuggingFace 导入模型。</p><pre><code>export MODEL_ID="bge-m3"
export HUB_MODEL_ID="BAAI/bge-m3"
export CLOUD_ID={{CLOUD_ID}}
export ES_API_KEY={{API_KEY}}
docker run -it --rm docker.elastic.co/eland/eland \
eland_import_hub_model --cloud-id $CLOUD_ID --es-api-key $ES_API_KEY --hub-model-id $HUB_MODEL_ID --es-model-id $MODEL_ID --task-type text_embedding --start</code></pre><p>然后，导航到 &quot;训练有素的模型 &quot;页面，使用所需的配置部署导入的模型。</p><h3>第 4 步：利用已部署的模型对原始数据进行矢量化或创建嵌入模型</h3><p>要创建嵌入模型，我们首先需要创建一个摄取管道，使我们能够获取文本并通过推理文本嵌入模型运行。您可以在 Kibana 的用户界面或通过 Elasticsearch 的应用程序接口进行此操作。</p><p><strong>要通过 Kibana 界面完成此操作</strong>，在部署训练模型后，单击<strong>测试 </strong>按钮。这将使您能够测试和预览生成的嵌入式内容。为<code>coco</code><strong> </strong> 索引创建一个新的数据视图，将数据视图设置为新创建的 coco 数据视图，并将字段设置为<code>description</code> ，因为这是我们要生成嵌入的字段。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/cfe189e13dc118d325e7fb90bdace0c912e29f51-1088x1600.png" style="width: 100%;" /><p>效果很好！现在，我们可以继续创建摄取管道，并重新为原始文档建立索引，将它们通过管道，然后用嵌入创建一个新索引。单击 &quot;<strong>创建管道</strong>&quot;即可实现这一功能，它将引导您完成管道创建过程，并自动填充所需的处理器，帮助您创建嵌入。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/1e043c1c3279b66fbdf19c06b41e76e613043998-1600x1126.png" style="width: 100%;" /><p>向导还可以自动填充所需的处理器，以便在摄取和处理数据时处理故障。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/8804d31b4f869078e3b2245040bbb0ab1720a94a-1600x1084.png" style="width: 100%;" /><p>现在让我们创建摄取管道。我将管道命名为<code>coco_e5</code> 。管道创建成功后，可以立即使用管道生成嵌入，方法是将原始索引数据重新索引到向导中的新索引。单击 &quot;<strong>重新索引 </strong>&quot;启动该过程。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/e34b1b18f5b24420d4581fe4d657c569926c2023-1600x1126.png" style="width: 100%;" /><h2>对于更复杂的配置，我们可以使用 Elasticsearch API。</h2><p>对于某些模型，由于训练模型的方式不同，我们可能需要在生成嵌入之前在实际输入中预置或附加某些文本；否则，我们将看到性能下降。</p><p>例如，在使用 e5 时，模型希望输入文本跟随 &quot;passage：{content of passage}&quot;.让我们利用摄取管道来实现这一目标：我们将创建一个新的摄取管道<strong>vectorize_descriptions</strong>。在此流程中，我们将创建一个新的临时<code>temp_desc</code> 字段，在 &quot;passage：&quot; 添加到<code>description</code> 文本中，通过模型运行<code>temp_desc</code> 以生成文本嵌入，然后删除<code>temp_desc</code> 。</p><pre><code>PUT _ingest/pipeline/vectorize_descriptions
{
"description": "Pipeline to run the descriptions text_field through our inference text embedding model",
"processors": [
 {
   "set": {
     "field": "temp_desc",
     "value": "passage: {{description}}"
   }
 },
 {
   "inference": {     
"field_map": {
       "temp_desc": "text_field"
     },
     "model_id": ".multilingual-e5-small_linux-x86_64_search",
     "target_field": "vector_description"
   }
 },
 {
   "remove": {
     "field": "temp_desc"
   }
 }
]
}</code></pre><p>此外，我们可能还想为生成的向量指定<a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/dense-vector#dense-vector-quantization">量化类型</a>。默认情况下，Elasticsearch 使用<code>int8_hnsw</code> ，但在这里，我需要<a href="https://www.elastic.co/search-labs/blog/better-binary-quantization-lucene-elasticsearch">更好的二进制量化</a>（或<code>bqq_hnsw</code> ），它将每个维度降低到一位精度。这样，内存占用减少了 96% （或 32 倍），但精度却提高了。我之所以选择这种量化类型，是因为我知道稍后会使用重新anker 来改善精度损失。</p><p>为此，我们将创建一个名为<strong>coco_multi 的</strong>新索引，并指定映射。这里的奥妙在于 vector_description 字段，我们在其中指定<strong>index_options</strong>的类型为<strong>bbq_hnsw</strong>。</p><pre><code>PUT coco_multi
{
 "mappings": {
   "properties": {
     "description": {
       "type": "text"
     },
     "en": {
       "type": "text"
     },
     "image_url": {
       "type": "keyword"
     },
     "language": {
       "type": "keyword"
     },
     "vector_description.predicted_value": {
       "type": "dense_vector",
       "dims": 384,
       "index": "true",
       "similarity": "cosine",
       "index_options": {
         "type": "bbq_hnsw" 
       }
     }
   }
 }
}</code></pre><p>现在，我们可以将原始文档重新索引到新的索引中，我们的摄取管道将对描述字段进行 &quot;矢量化 &quot;或创建嵌入。</p><pre><code>POST _reindex?wait_for_completion=false
{
 "source": {
   "index": "coco"
 },
 "dest": {
   "index": "coco_multilingual",
   "pipeline": "vectorize_descriptions"
 }
}</code></pre><p>就是这样！我们已经成功地利用 Elasticsearch 和 Kibana 部署了一个多语言模型，并逐步了解了如何通过 Kibana 用户界面或 Elasticsearch API 利用 Elastic 数据创建向量嵌入。在本系列的第二部分，我们将探讨使用多语言模型的结果和细微差别。同时，您可以<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> ，在您选择的语言和数据集上<a href="https://www.elastic.co/docs/explore-analyze/machine-learning/nlp/ml-nlp-e5"> 使用我们开箱即用的 E5 模型</a> 尝试 多语言语义搜索 。</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[弹性 MCP 服务器：将代理生成器工具暴露给任何人工智能代理]]></title>
            <description><![CDATA[了解如何使用代理生成器中的内置弹性 MCP 服务器安全地扩展任何人工智能代理，以访问您的私人数据和自定义工具。]]></description>
            <link>https://www.elastic.co/search-labs/cn/blog/elastic-mcp-server-agent-builder-tools</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/cn/blog/elastic-mcp-server-agent-builder-tools</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Jedr Blaszyk, Joe McElroy]]></dc:creator>
            <pubDate>Mon, 20 Oct 2025 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/ef5153551a1d14833c7f512fede554d1dfb31553-720x420.jpg?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>Elastic Agent Builder 是一个平台，用于创建与 Elasticsearch 中自己的数据深度集成的工具和代理。例如，您可以创建对内部文档进行语义搜索、分析可观察性日志或查询安全警报的工具。</p><p>但是，当你能将这些定制的、数据感知工具带入你花费时间最多的环境中时，真正的奇迹就发生了。如果您的代码编辑器代理可以安全地访问组织的私人知识库，那会怎样？</p><p>这就是<strong>模型上下文协议（MCP）</strong>的作用所在。Elastic Agent Builder 内置 MCP 服务器，可访问平台中的工具。</p><h2>为什么要使用 Elastic Agent Builder MCP 服务器？</h2><p>人工智能代理的功能非常强大，但它们的知识通常仅限于它们接受过训练的数据以及它们可以在公共互联网上主动搜索的信息。他们不了解贵公司的内部设计文档、团队的特定部署运行手册或应用程序日志的独特结构。</p><p>我们面临的挑战是如何为人工智能助手提供其所需的专业背景。这正是 MCP 所要解决的问题。<strong>MCP 是一种开放标准，允许人工智能模型或代理发现和使用外部工具。</strong></p><p>为了实现这一点，Elastic Agent Builder 通过内置的 MCP 服务器本机公开了您的自定义工具。这意味着您可以轻松地将任何与 MCP 兼容的客户端（如<strong>Cursor</strong>、<strong>VS Code</strong> 或<strong>Claude Desktop</strong>）与您使用 Elastic Agent Builder 创建的专门的数据感知工具连接起来。</p><h2>何时使用 MCP（何时不使用）</h2><p>Elastic Agent Builder 包含多种协议，可支持不同的集成模式。选择正确的人工智能工作流是建立有效人工智能工作流的关键。</p><ul><li><strong>使用 <a href="https://www.elastic.co/docs/solutions/search/agent-builder/mcp-server">MCP</a></strong>通过专业工具来增强人工智能代理（如在<strong>Cursor</strong>或<strong>VS Code</strong> 中）。这是&quot;自带工具&quot; 方法，通过安全访问您的私人数据来增强您已经使用的助手。只有工具是通过 MCP 服务器公开的，Elastic 的代理是独立于 MCP 服务器的。</li><li><strong>使用 <a href="https://www.elastic.co/docs/solutions/search/agent-builder/a2a-server">A2A 协议</a></strong>，让您的完整自定义弹性代理与其他自主代理协作（如<a href="https://www.elastic.co/search-labs/blog/a2a-protocol-elastic-agent-builder-gemini-enterprise"><strong>谷歌的双子座企业版</strong></a>）。这是针对代理对代理的委托，即每个代理都作为同行来解决问题。</li><li>在从头开始构建自定义应用程序时，<strong>使用 <a href="https://www.elastic.co/docs/solutions/search/agent-builder/kibana-api">代理生成器应用程序接口（API</a></strong>）实现完全的编程控制。</li></ul><p>对于希望在不离开集成开发环境的情况下从内部文档中获得答案的开发人员来说，MCP 是最合适的选择。</p><h2>示例：在 Cursor 中使用代理生成器 MCP 服务器的自定义工具</h2><p>让我们来看一个我每天都在使用的实际例子。首先，我将我们的内部工程文档抓取并编入一个名为<code>elastic-dev-docs</code> 的 Elasticsearch 索引。虽然我们可以使用 Agent Builder 中的通用内置工具，但我们将创建自己的自定义工具来查询这个特定的知识库。</p><p>定制工具的原因很简单：<strong>控制和精度</strong>。这种方法使我们能够直接针对<code>elastic-dev-docs</code> 索引运行快速语义查询。我们可以完全控制具体针对哪个索引以及如何检索数据。</p><p>现在，我们来看看如何在 Cursor 等人工智能驱动的代码编辑器中使用自定义知识库。</p><h3>第 1 步：在 Agent Builder 中创建自定义知识库工具</h3><p>首先，在 Agent Builder 中创建一个新工具。清晰而具体的工具描述非常重要，因为这是任何人工智能代理（无论是内部的弹性代理还是通过 MCP 连接的外部工具，如 Cursor）发现并为正确的任务选择工具的方式。</p><p>有力的描述应该是明确的。例如&quot;在 elastic-dev-docs 索引上执行语义搜索，以查找内部工程文档、运行手册和发布程序&quot;。</p><p>有了这些，就可以对工具进行配置，以便针对我们的特定索引执行语义搜索。一旦保存，就可以立即食用。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/1eea079908fdf7cc72dbe81abd07ff51601a43d4-1472x1600.png" style="width: 100%;" /><p>在连接到外部世界之前，您可以直接在用户界面中进行测试。只需单击 &quot;<strong>测试</strong>&quot;按钮，手动填写参数，模拟 LLM 的工作，然后检查结果，确认一切工作正常。</p><h3>第 2 步：将光标连接到弹性 MCP 服务器</h3><p>Elastic Agent Builder 可通过安全的 MCP 端点自动公开所有可用工具。您可以在 Kibana 的工具用户界面中找到唯一的服务器 URL。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/ba137be30f0eaa7f028b96bd8af4e2779c3f8a33-1600x589.png" style="width: 100%;" /><p>要连接到 Cursor，我们只需将此 URL 添加到其配置文件中，同时添加一个用于身份验证的 Elastic API 密钥<a href="https://www.elastic.co/docs/deploy-manage/api-keys/elasticsearch-api-keys">（了解如何创建 ES API 密钥</a>）。我们使用 API 密钥进行授权，因为它能确保工具只在您授予的权限内执行，并尊重您的所有访问控制规则。</p><p>Cursor&#x27;s<code>~/.cursor/mcp.json</code> 中的 MCP 配置如下所示：</p><pre><code>{
  "mcpServers": {
    "elastic-agent-builder": {
      "command": "npx",
      "args": [
        "mcp-remote",
        "https://your-kibana.kb.company.io/api/agent_builder/mcp",
        "--header",
        "Authorization:${AUTH_HEADER}"
      ],
      "env": {
        "AUTH_HEADER": "ApiKey <ELASTIC_API_KEY>"
      }
    }
  }
}</code></pre><p>保存配置后，你应该能在光标中看到 Elastic Agent Builder MCP 服务器工具。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/d302c6d3609fbf14fd40e21b9e69e567bf12553f-1600x1002.png" style="width: 100%;" /><h3>第三步：提问！</h3><p>建立连接后，Cursor 代理现在可以调用您的自定义工具来回答您的问题或指导代码生成过程。</p><p>让我们提出一个具体问题：</p><p><em>&quot;从弹性搜索组织的工程内部文档中查找释放爬虫服务的步骤&quot;</em></p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/14f572730203c23615bb9dd38234bcb3b0f81155-1600x1468.png" style="width: 100%;" /><p>在幕后，神奇的事情发生了：</p><ol><li>光标代理决定如何以最佳方式回答您的问题，并决定调用 <code>engineering_documentation_internal_search</code></li><li>它通过自然语言查询调用该工具</li><li>该工具根据<code>elastic-dev-docs</code> 索引执行语义搜索，并返回最相关的最新程序。</li></ol><p>我们无需离开代码编辑器，就能根据内部文档得到准确、可信的答案。这种体验天衣无缝、功能强大。</p><h2>轮到您建造</h2><p>您现在已经了解了如何使用 Elastic Agent Builder 中的内置 MCP 服务器来扩展人工智能助手，使其能够安全地访问您的私人数据。将模型建立在自己的信息基础上是使其真正有用的关键。</p><p>概括地说，我们介绍了核心步骤：</p><ul><li>根据需要选择合适的协议（MCP）。</li><li>构建自定义知识库工具</li><li>将该工具与 Cursor 等集成开发环境助手连接起来。</li></ul><p>您的代理和工具不再需要与最有价值的环境脱节。希望本指南能帮助您创建更有效的数据感知工作流程。快乐建筑</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[使用基于用户行为数据的判断列表在 Elasticsearch 中训练 LTR 模型]]></title>
            <description><![CDATA[了解如何使用 UBI 数据创建判断列表，以便在 Elasticsearch 中自动训练学习排名 (LTR) 模型。]]></description>
            <link>https://www.elastic.co/search-labs/cn/blog/training-learning-to-rank-models-elasticsearch-ubi-data</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/cn/blog/training-learning-to-rank-models-elasticsearch-ubi-data</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Alexander Dávila]]></dc:creator>
            <pubDate>Wed, 15 Oct 2025 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/762bf09c28829d626d42c2cfadc719e1dd618d1b-1536x1024.png?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>使用<a href="https://www.elastic.co/docs/solutions/search/ranking/learning-to-rank-ltr"><strong><em>&quot;学习-排名 &quot;</em></strong></a>模型的一大挑战是创建一个高质量的<a href="https://www.elastic.co/search-labs/blog/judgment-lists"><strong><em>判断列表</em></strong></a>来训练模型。传统上，这一过程需要对查询与文档的相关性进行<strong><em>人工</em></strong>评估，为每个文档分配一个等级。这是一个缓慢的过程，不能很好地扩展，也很难维护（想象一下要手工更新一个有数百个条目的列表）。</p><p>现在，如果我们可以使用真实用户与搜索应用程序的交互来创建这些训练数据，会怎样呢？使用<a href="https://www.elastic.co/search-labs/blog/elasticsearch-plugin-user-behavior-insights"><strong><em>UBI</em></strong></a>数据可以让我们做到这一点。创建一个自动系统，捕捉并利用我们的搜索、点击和其他互动来生成判断列表。与人工交互相比，这一过程更容易扩展和重复，而且往往会产生更好的结果。在本博客中，我们将探讨如何查询存储在 Elasticsearch 中的 UBI 数据，以计算有意义的信号，从而为<a href="https://www.elastic.co/search-labs/blog/elasticsearch-learning-to-rank-introduction"><strong><em>LTR</em></strong></a>模型生成训练数据集。</p><p><strong><em>您可以 <a href="https://github.com/Alex1795/elastic-ltr-judgement_list-blog.git">在这里</a></em></strong>找到完整的实验 <strong><em>。</em></strong></p><h2>为什么 UBI 数据有助于训练 LTR 模型？</h2><p>与人工标注相比，UBI 数据具有多项优势：</p><ul><li><strong>数量：</strong>鉴于 UBI 数据来自真实的互动，我们可以收集到比人工生成的数据多得多的数据。当然，前提是我们有足够的流量来生成这些数据。</li><li><strong>真实用户意图：</strong>传统上，人工判断列表来自专家对可用数据的评估。另一方面，UBI 数据反映了真实的用户行为。这意味着我们可以生成更好的训练数据，从而提高搜索系统的准确性，因为这些数据是基于用户如何与您的内容进行实际互动并从中发现价值的，而不是对相关内容的理论假设。</li><li><strong>不断更新：</strong>判断列表需要不断刷新。如果我们根据 UBI 数据创建它们，我们就可以获得最新的数据，从而更新判断列表。</li><li><strong>成本效益高：</strong>无需手动创建判断列表，可多次高效重复这一过程。</li><li><strong>自然查询分布</strong>：UBI 数据代表真实的用户查询，可推动更深层次的变化。例如，用户是否使用自然语言在我们的系统中进行搜索？如果是这样，我们可能需要采用语义搜索或混合搜索方法。</li></ul><p>不过也有一些警告：</p><ul><li><strong>偏见放大： </strong>热门内容更容易获得点击，因为它的曝光率更高。因此，这最终可能会放大热门项目，可能会淹没更好的选择。</li><li><strong>覆盖面不全： </strong>新内容缺乏互动，因此可能难以在结果中占据重要位置。罕见查询也可能缺乏足够的数据点来创建有意义的训练数据。</li><li><strong>季节性变化：</strong>如果您预期用户行为会随时间发生巨大变化，那么历史数据可能无法告诉您什么是好结果。</li><li><strong>任务模糊：</strong>点击并不总能保证用户找到想要的东西。</li></ul><h2>成绩计算</h2><h3>LTR 培训的成绩</h3><p>为了训练 LTR 模型，我们需要提供一些数字来表示文档与查询的相关程度。在我们的实施过程中，这个数字是一个连续的分数，从 0.0 到 5.0+，分数越高表示相关性越高。</p><p>为了说明这个分级系统是如何运行的，请看这个手动创建的示例：</p>
            <table style="border-collapse: collapse; margin: 20px 0;">
              <tbody>
                
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">查询</td><td style="border: 1px solid #ccc; padding: 4px;">文件内容</td><td style="border: 1px solid #ccc; padding: 4px;">等级</td><td style="border: 1px solid #ccc; padding: 4px;">说明</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">"最佳比萨饼配方"</td><td style="border: 1px solid #ccc; padding: 4px;">"正宗意大利比萨面团配方及步骤图片"</td><td style="border: 1px solid #ccc; padding: 4px;">4.0</td><td style="border: 1px solid #ccc; padding: 4px;">高度相关，正是用户正在寻找的内容</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">"最佳比萨饼配方"</td><td style="border: 1px solid #ccc; padding: 4px;">"意大利比萨的历史"</td><td style="border: 1px solid #ccc; padding: 4px;">1.0</td><td style="border: 1px solid #ccc; padding: 4px;">与主题有点吻合，是关于披萨的，但不是食谱</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">"最佳比萨饼配方"</td><td style="border: 1px solid #ccc; padding: 4px;">"适合初学者的 15 分钟快速比萨食谱"</td><td style="border: 1px solid #ccc; padding: 4px;">3.0</td><td style="border: 1px solid #ccc; padding: 4px;">这是个不错的结果，但也许还达不到 "最佳 "配方的标准。</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">"最佳比萨饼配方"</td><td style="border: 1px solid #ccc; padding: 4px;">"汽车保养指南"</td><td style="border: 1px solid #ccc; padding: 4px;">0.0</td><td style="border: 1px solid #ccc; padding: 4px;">完全不相关，与查询完全无关</td>
                    </tr>
              </tbody>
            </table>
          <p>正如我们在这里看到的，等级是一个数字，表示文档与我们的样本查询 &quot;最佳披萨配方 &quot;的相关程度。有了这些分数，我们的 LTR 模型就能知道哪些文档应该在结果中显示得更高。</p><p>如何计算等级是我们训练数据集的核心。有<a href="https://www.elastic.co/search-labs/blog/judgment-lists"> 多种方法</a> 可以做到这一点，每种 方法 都有自己的优缺点。例如，我们可以分配一个二进制分数，1 表示相关 0 表示不相关，或者我们可以只计算每个查询结果文档中的点击次数。</p><p>在这篇博文中，我们将采用一种不同的方法，<strong><em>将用户行为作为我们的输入，并计算出一个等级数字作为输出</em></strong>。我们还将纠正可能出现的偏差，因为无论文件的相关性如何，结果越靠前，点击率越高。</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>&quot;众所周知，搜索结果或广告的点击率（CTR）会随着搜索结果的位置而显著降低&quot;。</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><div class="katex">CTRp=CpIp</div><p>在哪里？</p><p>p = 位置。从 1 到 10<br/>Cp = 在所有查询中，任何文档在位置 p 上的总点击次数<br/>Ip = 总印象次数：在所有查询中，任何文档在位置 p 上出现的次数</p><p>在这里，我们希望位置越高，点击越多。</p><p><strong>2.</strong> <strong>计算预期点击量 (EC)：</strong></p><p>该指标根据文档出现的位置和这些位置的点击率，确定文档 &quot;本应 &quot;获得的点击量：</p><div class="katex">EC (for a document) = qQdCTRpos(d,q)</div><p>在哪里？</p><p>Qd = 文档 d 出现的所有查询<br/>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><div class="katex">COEC = A(d)EC(d)</div><p>该指标对位置偏差进行了这样的归一化处理：</p><ul><li>得分 1.0 表示文档在出现的位置上表现完全符合预期。</li><li>得分高于 1.0 表示该文件的位置表现优于预期。因此，这份文件与查询更相关。</li><li>如果得分低于 1.0，则表示该文件的表现不如预期。因此，该文件与查询的相关性较低。</li></ul><p><strong><em>最终结果是一个等级数字，它能反映用户正在寻找什么，并考虑到从与我们搜索系统的实际互动中提取的基于位置的期望值。</em></strong></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><pre><code>git clone https://github.com/Alex1795/elastic-ltr-judgement_list-blog.git  
cd elastic-ltr-judgement_list-blog</code></pre><p>2.安装所需程序库</p><p>对于这个脚本，我们需要以下库：</p><ul><li><em>熊猫</em>：保存判断列表</li><li><em>elasticsearch</em>：从我们的弹性部署中获取 UBI 数据</li></ul><p>我们还需要 Python 3.11</p><pre><code>pip install -r requirements.txt</code></pre><p>3.在<a href="https://github.com/Alex1795/elastic-ltr-judgement_list-blog/blob/main/.env-example">.env 文件</a>中更新弹性部署的环境变量</p><ul><li>ES_HOST</li><li>API_KEY</li></ul><p>要添加环境变量，请使用</p><pre><code>source .env</code></pre><p>4.创建 ubi_queries、ubi_events 索引，并上传样本数据。运行 setup.py 文件：</p><pre><code>python setup.py</code></pre><p>5.运行 Python 脚本：</p><pre><code>python judgement_list-generator.py</code></pre><p>如果按照这些步骤操作，你应该会看到一个名为 judgment_list.csv 的新文件，它看起来像这样：</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/2531090131ac9fe3e4e1d79de9d156fc47a7825a-782x531.png" style="width: 100%;" /><p>该脚本将使用下图所示的<strong>calculate_relevance_grade()</strong>函数，应用之前讨论过的 COEC 算法计算等级。</p><h2>数据架构</h2><h3>乌比查询</h3><p>我们的 UBI 查询索引包含在搜索系统中执行的查询信息。这是一份样本文件：</p><pre><code>{
          "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"
        }</code></pre><p>在这里，我们可以看到来自用户（client_id）、查询结果（query_response_object_ids）和查询本身（时间戳、user_query）的数据。</p><h3>Ubi 点击事件</h3><p>我们的 ubi_events 索引拥有用户每次点击结果中的文档时产生的数据。这是一份样本文件：</p><pre><code>{
          "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"
        }</code></pre><h2>判断列表生成脚本</h2><h3>脚本概述</h3><p>该脚本使用存储在 Elasticsearch 中的来自查询和点击事件的 UBI 数据自动生成判断列表。它执行这些任务：</p><ul><li>获取并处理 Elasticsearch 中的 UBI 数据。</li><li>将 UBI 事件与其查询相关联。</li><li>计算每个位置的点击率。</li><li>计算每份文档的预期点击量 (EC)。</li><li>计算每份文档的实际点击次数。</li><li>计算每个查询-文档配对的 COEC 分数。</li><li>生成判断列表并将其写入 CSV 文件。</li></ul><p>让我们逐一了解这些功能：</p><h3>connect_too_elasticsearch()</h3><pre><code>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</code></pre><p>该函数使用主机和 api 密钥返回 Elasticsearch 客户端对象。</p><h3>fetch_ubi_data()</h3><pre><code>def fetch_ubi_data(es_client: Elasticsearch, queries_index: str, events_index: str,
                   size: int = 10000) -> 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</code></pre><p>该函数是数据提取层；它连接 Elasticsearch，使用 match_all 查询获取 UBI 查询，并过滤 UBI 事件，只获取 &quot;CLICK_THROUGH &quot;事件。</p><h3>process_ubi_data()</h3><pre><code>def process_ubi_data(queries_data: List[Dict], events_data: List[Dict]) -> 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</code></pre><p>该函数处理判断列表的生成。它通过关联 UBI 事件和查询，开始处理 UBI 数据。然后，它会调用每个文档-查询对的 calculate_relevance_grade() 函数来获取判断列表的条目。最后，它会以 pandas 数据帧的形式返回结果列表。</p><h3>计算相关性等级()</h3><pre><code>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) -> 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 > 3:
                return 4.0
            elif position >= 1 and position <= 3:
                return 3.0
        if document_id in query_response_ids:
            position = query_response_ids.index(document_id) + 1
            if position <= 5:
                return 2.0
            elif position >= 6 and position <= 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 <= 10:
                position_click_counts[position] += 1

    # Calculate average CTR per position
    for pos in range(1, 11):
        if position_impression_counts[pos] > 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 <= 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 > 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</code></pre><p>这是实现 COEC 算法的函数。它先计算每个位置的点击率，然后比较文档-查询配对的实际点击率，最后计算每个位置的实际 COEC 分数。</p><h3>generate_judgment_statistics()</h3><pre><code>def generate_judgment_statistics(df: pd.DataFrame) -> 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() > 0 else 0,
        'queries_with_clicks': len(df[df['grade'] > 1]['qid'].unique()),
        'click_through_rate': len(df[df['grade'] > 1]) / len(df) if len(df) > 0 else 0
    }
    return stats</code></pre><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>qid：查询的唯一 ID</li><li>docid：生成文件的唯一标识符</li><li>等级：查询文件对的计算等级</li><li>查询：用户查询</li></ul><p> 让我们看看 &quot;意大利菜谱 &quot;的查询结果：</p>
            <table style="border-collapse: collapse; margin: 20px 0;">
              <tbody>
                
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">qid</td><td style="border: 1px solid #ccc; padding: 4px;">docid</td><td style="border: 1px solid #ccc; padding: 4px;">职级</td><td style="border: 1px solid #ccc; padding: 4px;">查询</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">Q1 意大利食谱</td><td style="border: 1px solid #ccc; padding: 4px;">基本食谱</td><td style="border: 1px solid #ccc; padding: 4px;">0.0</td><td style="border: 1px solid #ccc; padding: 4px;">意大利食谱</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">Q1 意大利食谱</td><td style="border: 1px solid #ccc; padding: 4px;">菜谱_比萨_玛格丽塔</td><td style="border: 1px solid #ccc; padding: 4px;">3.333333</td><td style="border: 1px solid #ccc; padding: 4px;">意大利食谱</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">Q1 意大利食谱</td><td style="border: 1px solid #ccc; padding: 4px;">菜谱指南</td><td style="border: 1px solid #ccc; padding: 4px;">10.0</td><td style="border: 1px solid #ccc; padding: 4px;">意大利食谱</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">Q1 意大利食谱</td><td style="border: 1px solid #ccc; padding: 4px;">法式羊角面包食谱</td><td style="border: 1px solid #ccc; padding: 4px;">0.0</td><td style="border: 1px solid #ccc; padding: 4px;">意大利食谱</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">Q1 意大利食谱</td><td style="border: 1px solid #ccc; padding: 4px;">西班牙海鲜饭食谱</td><td style="border: 1px solid #ccc; padding: 4px;">0.0</td><td style="border: 1px solid #ccc; padding: 4px;">意大利食谱</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">Q1 意大利食谱</td><td style="border: 1px solid #ccc; padding: 4px;">希腊穆萨卡菜谱</td><td style="border: 1px solid #ccc; padding: 4px;">1.875</td><td style="border: 1px solid #ccc; padding: 4px;">意大利食谱</td>
                    </tr>
              </tbody>
            </table>
          <p>我们可以从结果中看到，查询 &quot;意大利菜谱&quot;：</p><ul><li>烩饭食谱无疑是该查询的最佳结果，其点击率比预期高出 10 倍</li><li>玛格丽塔比萨也是不错的选择。</li><li>希腊 Mousaka（令人吃惊）也取得了不错的成绩，其表现要好于它在比赛中的位置。这意味着一些寻找意大利食谱的用户转而对这一食谱感兴趣。也许这些用户普遍对地中海菜肴感兴趣。最后，这告诉我们，在我们上面讨论的另外两场 &quot;更好 &quot;的比赛中，这可能是一个很好的结果。</li></ul><h2>结论</h2><p>利用 UBI 数据，我们可以自动训练 LTR 模型，从自己的用户中创建高质量的判断列表。UBI 数据提供了一个大数据集，反映了我们的搜索系统是如何被使用的。通过使用 COEC 算法来生成等级，我们可以考虑到固有的偏差，同时，它也反映了用户认为更好的结果。本文概述的方法可应用于实际使用案例，以提供更好的搜索体验，并随着实际使用趋势的变化而变化。</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[人工智能代理评估：Elastic 如何测试代理框架]]></title>
            <description><![CDATA[了解我们如何在向 Elastic 用户发布代理系统变更之前对其进行评估和测试，以确保结果的准确性和可验证性。]]></description>
            <link>https://www.elastic.co/search-labs/cn/blog/ai-agent-evaluation-elastic</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/cn/blog/ai-agent-evaluation-elastic</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Susan Chang, Abhimanyu Anand]]></dc:creator>
            <pubDate>Mon, 13 Oct 2025 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/e681aab6ee77dfef38dc95e36c8535dfe7bcc105-721x420.png?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<h2>引言</h2><p>在 Elastic Stack 中，有许多由 LLM 驱动的代理应用程序，例如<a href="https://www.elastic.co/search-labs/blog/ai-agentic-workflows-elastic-ai-agent-builder"> Agent Builder</a> 中即将推出的 Elastic AI Agent（目前处于技术预览阶段）和<a href="https://www.elastic.co/docs/solutions/security/ai/attack-discovery"> Attack Discovery</a> （ 8.18 和 9.0+ 中的 GA<a href="https://www.elastic.co/blog/whats-new-elastic-security-9-0-0"> ），还有更多正在开发中。</a>在开发过程中，甚至在部署之后，回答这些问题都非常重要：</p><ul><li>我们如何估算这些人工智能应用的响应质量？</li><li>如果我们做出改变，如何保证这种改变是真正的改进，而不会导致用户体验下降？</li><li>如何以可重复的方式轻松测试这些结果？</li></ul><p>与传统的软件测试不同，评估生成式人工智能应用涉及统计方法、细致的定性审查以及对用户目标的深刻理解。</p><p>本文详细介绍了 Elastic 开发人员团队进行评估、确保部署前变更的质量以及监控系统性能的流程。我们的目标是确保每一项变革都有据可依，从而取得可信和可验证的成果。这一过程的一部分直接集成到了 Kibana 中，体现了我们对透明度的承诺，这也是我们开源精神的一部分。通过公开分享我们的部分评估数据和指标，我们力求促进社区信任，并为开发人工智能代理或使用我们产品的任何人提供一个清晰的框架。</p><h2>产品示例</h2><p>本文档中使用的方法是我们迭代和改进 &quot;攻击发现 &quot;和 &quot;弹性人工智能代理 &quot;等解决方案的基础。分别对两者进行简要介绍：</p><h3>弹性安全的攻击发现</h3><p>攻击发现使用 LLM 来识别和总结 Elastic 中的攻击序列。在给定的时间范围（默认 24 小时）内收到 Elastic Security 警报后，Attack Discovery 的代理工作流程会自动查找是否发生了攻击，以及重要信息，如哪台主机或用户受到了攻击，哪些警报促成了这一结论。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/20fabb47642dad7b588daaaa8c3a98de860ad01d-1251x758.png" style="width: 100%;" /><p></p><p>我们的目标是，基于 LLM 的解决方案所产生的输出结果至少与人类的输出结果一样好。</p><h3>弹性人工智能代理</h3><p><strong>Elastic Agent Builder</strong>是我们的新平台，用于构建可利用我们所有搜索功能的上下文感知人工智能代理。它配备了<strong>Elastic AI Agent</strong>，这是一个预构建的通用代理，旨在通过对话式交互帮助用户理解数据并从中获得答案。</p><p>该代理通过自动识别 Elasticsearch 或连接的知识库中的相关信息，并利用一套预建工具与之交互，来实现这一目标。这使得 Elastic AI Agent 能够响应各种用户查询，从单个文档的简单 Q&amp;A 到需要在多个索引中进行聚合和单步或多步搜索的复杂请求。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/d29dee100bb8a17bb623acd745773a5164a1df4f-1600x1014.png" style="width: 100%;" /><h2>通过实验衡量改进</h2><p>就人工智能代理而言，实验是对系统进行的结构化、可测试的更改，旨在提高系统在明确定义的维度（如有用性、正确性、延迟）上的性能。我们的目标是明确回答&quot;如果我们合并这一改动，能否保证它是真正的改进，不会降低用户体验？</p><p>我们进行的大多数实验通常包括</p><ul><li><strong>假设：</strong>一个具体的、可证伪的主张。<em>例如</em>&quot;增加对攻击发现工具的访问权限，可提高安全相关查询的正确性&quot;。</li><li><strong>成功标准：</strong>明确界定 &quot;成功 &quot;含义的阈值。<em>例如</em>&quot;在安全数据集上，正确性得分提高了 +5% ，其他方面没有降低&quot;。</li><li><strong>评估计划：</strong>我们如何衡量成功（衡量标准、数据集、比较方法）</li></ul><p>成功的实验是一个系统的探究过程。从细微的提示调整到重大的架构转变，每一项改变都要遵循这七个步骤，以确保结果是有意义和可操作的：</p><ul><li>第 1 步：确定问题</li><li>第 2 步：确定衡量标准</li><li>步骤 3：提出明确的假设</li><li>步骤 4：准备评估数据集</li><li>步骤 5：运行实验</li><li>第 6 步：分析结果 + 反复试验</li><li>第 7 步：做出决定并记录在案</li></ul><p><em>图 1</em> 举例说明了这些步骤。下面的小节将对每个步骤进行说明，我们将在接下来的文件中详细介绍每个步骤的技术细节。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/3a9f5a3e92dd4922a795a19104c6e4ad8c98958d-2400x1352.png" style="width: 100%;" /><h2>使用真实的 Elastic 示例逐步讲解</h2><h3>第 1 步：确定问题</h3><p><em>这一变化究竟要解决什么问题？</em></p><p>攻击发现示例：摘要有时不完整，或者良性活动被错误地标记为攻击（误报）。</p><p>弹性人工智能代理示例：代理的工具选择，尤其是分析查询工具的选择，不够理想且不一致，经常导致选择错误的工具。这反过来又增加了令牌成本和延迟。</p><h3>第 2 步：确定衡量标准</h3><p><em>使问题可测量，以便我们能将变化与当前状态进行比较。</em></p><p>常用指标包括<a href="https://developers.google.com/machine-learning/crash-course/classification/accuracy-precision-recall">精确度和召回率</a>、<a href="https://en.wikipedia.org/wiki/Semantic_similarity">语义相似性</a>、事实性等。根据不同的使用情况，我们使用代码检查来计算指标，例如匹配警报 ID 或正确检索的 URL，或者使用 LLM-as-judge 等技术来计算更自由的答案。</p><p>以下是实验中使用的一些指标示例<em>（并非详尽无遗</em>）：</p><p><strong>Attack Discovery</strong></p>
            <table style="border-collapse: collapse; margin: 20px 0;">
              <tbody>
                
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">公制</td><td style="border: 1px solid #ccc; padding: 4px;">描述</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">精确度& 召回率</td><td style="border: 1px solid #ccc; padding: 4px;">在实际输出和预期输出之间匹配警报 ID，以衡量检测准确性。</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">相似性</td><td style="border: 1px solid #ccc; padding: 4px;">使用 BERTScore 比较回复文本的语义相似性。</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">事实性</td><td style="border: 1px solid #ccc; padding: 4px;">是否存在关键的 IOC（妥协指标）？是否正确反映了 MITRE 战术（行业攻击分类）？</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">攻击链一致性</td><td style="border: 1px solid #ccc; padding: 4px;">比较发现的次数，检查是否存在多报或少报攻击事件的情况。</td>
                    </tr>
              </tbody>
            </table>
          <p><strong>弹性人工智能代理</strong></p>
            <table style="border-collapse: collapse; margin: 20px 0;">
              <tbody>
                
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">公制</td><td style="border: 1px solid #ccc; padding: 4px;">描述</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">精确度& 召回率</td><td style="border: 1px solid #ccc; padding: 4px;">将代理为回答用户查询而检索的文档/信息与回答查询所需的实际信息或文档进行匹配，以衡量信息检索的准确性。</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">事实性</td><td style="border: 1px solid #ccc; padding: 4px;">是否存在回答用户查询所需的关键事实？程序性查询的事实顺序是否正确？</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">回应相关性</td><td style="border: 1px solid #ccc; padding: 4px;">回复是否包含与用户查询无关的信息？</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">答复完整性</td><td style="border: 1px solid #ccc; padding: 4px;">回复是否回答了用户查询的所有部分？回复是否包含地面实况中的所有信息？</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">ES|QL 验证</td><td style="border: 1px solid #ccc; padding: 4px;">生成的 ES|QL 语法正确吗？它在功能上是否与地面实况 ES|QL 相同？</td>
                    </tr>
              </tbody>
            </table>
          <h3>步骤 3：提出明确的假设</h3><p><em>利用问题和上文定义的衡量标准，制定明确的成功标准。</em></p><p>弹性人工智能代理示例：</p><ol><li><strong>对 relevance_search 和 nl_search 工具的说明进行修改，以明确定义其具体功能和用例</strong>。</li><li>我们预测，我们的<strong> 工具调用准确率</strong> 将<strong> 提高 25%</strong> 。</li><li>我们将通过确保不对其他指标产生负面影响来验证这是否是一个净积极因素，例如<strong>事实性和完整性</strong>。</li><li>我们相信这将行之有效，因为<strong>精确的工具描述将帮助代理针对不同查询类型更准确地选择和应用最合适的搜索工具，从而减少错误应用，提高整体搜索效率</strong>。</li></ol><h3>步骤 4：准备评估数据集</h3><p><em>为了衡量系统的性能，我们使用了能捕捉真实世界场景的数据集。</em></p><p>根据我们所进行的评估类型，我们可能需要不同类型的数据格式，例如反馈给 LLM 的原始数据（例如：&quot;......&quot;）。攻击发现的攻击场景）和预期产出。如果应用程序是聊天机器人，那么输入可能是用户查询，输出可能是聊天机器人的正确回复、本应检索到的正确链接等。</p><p>攻击发现示例</p>
            <table style="border-collapse: collapse; margin: 20px 0;">
              <tbody>
                
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">10 种新颖的攻击情景</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">8 集 Oh My Malware (ohmymalware.com)</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">4 种多重攻击情景（通过组合前两类攻击而创建）</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">3 种良性情景</td>
                    </tr>
              </tbody>
            </table>
          <p>弹性人工智能代理评估数据集示例<a href="https://github.com/elastic/kibana/blob/main/x-pack/platform/packages/shared/onechat/kbn-evals-suite-onechat/evals/kb/kb.spec.ts">（Kibana 数据集链接</a>）：</p>
            <table style="border-collapse: collapse; margin: 20px 0;">
              <tbody>
                
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">14 使用开放源码数据集模拟 KB 中多个来源的指数。</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">5 种查询类型（分析型、文本检索型、混合型...）</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">7 查询意图类型（程序、事实--分类、调查......）</td>
                    </tr>
              </tbody>
            </table>
          <h3>步骤 5：运行实验</h3><p>执行实验，根据评估数据集生成现有代理和修改版代理的响应。计算事实性等指标（见第 2 步）。</p><p>我们根据步骤 2 中要求的指标，将各种评估混合在一起：</p><ul><li>基于规则的评估（如使用 Python/TypeScript 检查 .json 是否有效）。</li><li>法学硕士即法官（询问另一位法学硕士某项答复是否与源文件的事实相符）</li><li>人在回路中审查，进行细微差别质量检查</li></ul><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/8648e75818d3291f0ac66f069438a500d42b8225-1600x1099.png" style="width: 100%;" /><h3>第 6 步：分析结果 + 反复试验</h3><p>现在我们有了衡量标准，可以对结果进行分析。<em><span style="text-decoration:underline">即使结果符合步骤 3 中定义的成功标准，在将变更合并到生产之前，我们仍要进行人工审核</span></em>；如果结果不符合标准，则要进行迭代并修复问题，然后在新变更上运行评估。</p><p>我们预计，在合并之前，需要反复几次才能找到最佳修改。与在推送提交之前运行本地软件测试类似，离线评估也可与本地变更或多个建议变更一起运行。自动保存实验结果、综合分数和可视化效果，简化分析过程，非常有用。</p><h3>第 7 步：做出决定并记录在案</h3><p>根据决策框架和验收标准，决定是否合并变更，并将实验记录在案。决策是多方面的，可以考虑评估数据集以外的因素，如检查其他数据集的回归情况，或权衡拟议变更的成本效益。</p><p>举例说明：在测试和比较几次迭代后，选择得分最高的变更，发送给产品经理和其他相关利益者审批。附上前几个步骤的结果，以帮助指导决策。有关攻击发现方面的更多示例，请参阅《<a href="https://www.elastic.co/blog/elastic-security-generative-ai-features">Elastic Security 的生成式人工智能功能幕后</a>》。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/74c80b8f34dce8ddd20873ecb2f553873587ed35-1600x618.png" style="width: 100%;" /><h2>结论</h2><p>在这篇博客中，我们介绍了实验工作流程的端到端过程，说明了我们如何在向 Elastic 用户发布代理系统变更之前对其进行评估和测试。我们还提供了一些在 Elastic 中改进基于代理的工作流的示例。在随后的博文中，我们将详细介绍不同步骤的细节，例如如何创建一个好的数据集、如何设计可靠的度量标准，以及在涉及多个度量标准时如何做出决策。</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[通过 A2A 协议将弹性代理连接到 Gemini Enterprise]]></title>
            <description><![CDATA[了解如何使用 Agent Builder 通过 A2A 协议将定制的 Elastic Agent 暴露给 Gemini Enterprise 等外部服务。]]></description>
            <link>https://www.elastic.co/search-labs/cn/blog/a2a-protocol-elastic-agent-builder-gemini-enterprise</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/cn/blog/a2a-protocol-elastic-agent-builder-gemini-enterprise</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Jedr Blaszyk, Valerio Arvizzigno, Joe McElroy]]></dc:creator>
            <pubDate>Thu, 09 Oct 2025 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/5be8a425fab55dca2f9717d2e50812b0450fa625-1440x840.png?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p><strong>Elastic Agent Builder</strong>是一套直接在 Elasticsearch 中创建数据驱动的人工智能代理的功能。在本<a href="https://www.elastic.co/search-labs/blog/series/context-aware-ai-agentic-workflows-with-elastic">系列</a>的前几篇文章中，我们演示了如何为自定义代理配备执行复杂任务的工具，并为其提供一系列自定义指令来指导其行为。</p><p>但是，如果您想将自定义代理与您已经依赖的应用程序和生产力工具一起使用，该怎么办？</p><p>这就是<strong>代理对代理（A2A）协议</strong>的作用所在。A2A 是互操作性的<a href="https://github.com/a2aproject/A2A">开放标准</a>，允许来自不同平台的代理进行通信和协作。我们已将其直接内置到弹性代理生成器中。</p><p>今天，我们将向您展示如何将您创建的自定义代理与其他服务（特别是<strong>Gemini Enterprise </strong>，前身为 Agentspace）进行交互。</p><h2>开放标准的力量：A2A 为何重要</h2><p>在博文 &quot;<a href="https://www.elastic.co/search-labs/blog/ai-agent-builder-elasticsearch">你的第一个弹性代理</a>&quot;中，我们展示了如何构建自定义代理，例如可安全访问市场数据的<em>财务助理</em>代理。但是，如果你不能在其他环境（如双子座企业版）中使用其洞察力，而又不重建你的工作，那么它的价值就会受到限制。</p><p>这种互操作性的挑战正是阻碍人工智能发展的原因。代理需要一种跨平台交流的通用语言，这正是 A2A 协议的作用所在。它提供了一个标准通信层，不仅可以让您与代理直接互动，还能开启未来，让整个组织的专业代理都能协作并分享见解。</p><p>为了实现这一点，Elastic Agent Builder 通过两个标准端点为所有代理提供 A2A 协议本机支持：</p><ol><li><strong>Agent Card 端点 (<code>GET {your-kibana-url}/api/agent_builder/a2a/{agentId}.json</code></strong> )<strong>- </strong>这是您的自定义代理名片。它向任何 A2A 兼容服务提供有关代理的元数据（名称、描述、功能等）。</li><li><strong>A2A 协议端点 (<code>POST {your-kibana-url}/api/agent_builder/a2a/{agentId}</code> )</strong> - 这是通信通道。其他代理在此发送请求，您的代理处理请求并返回响应，所有这些都遵循<a href="https://a2a-protocol.org/latest/specification/">A2A 协议规范</a>。</li></ol><h2>使用 A2A 检查员测试您的代理</h2><p>在将我们的代理连接到生产系统之前，最好检查一下它的通信是否正确。最简单的方法是使用<strong>A2A 检查器</strong>，这是一款专门用于测试和调试 A2A 集成的工具。</p><p>检查器的运行非常简单。您可以克隆<a href="https://github.com/a2aproject/a2a-inspector">a2a-inspector</a>软件源，然后按照 README 说明<a href="https://github.com/a2aproject/a2a-inspector?tab=readme-ov-file#3-run-the-application">运行应用程序</a>。启动后，用户界面默认在<code>http://localhost:5001/</code> 上可用。</p><p>要将 A2A 检查员与您的代理联系起来，您需要提供两条关键信息：</p><ul><li>代理卡 URL：这是描述代理的端点。对于<a href="https://www.elastic.co/search-labs/blog/ai-agent-builder-elasticsearch">上一个职位中的财务助理代理</a>而言，这个 URL 将是<code>{your-kibana-url}/api/agent_builder/a2a/financial_assistant.json</code> 。</li><li>验证头：我们将使用标准 API 密钥进行身份验证。</li></ul><p>在检查员用户界面输入这些详细信息后，您就可以立即连接并开始与您的代理聊天。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/7231c72bf30bed2a854f58658c1eca2843f43bfc-1600x1296.png" style="width: 100%;" /><p>这一简单的验证让我们确信，我们的代理已正确配置并准备好进行下一步操作。</p><h2>开始直播您在双子座企业中的定制代理</h2><p>现在是激动人心的部分：在 Gemini Enterprise（前身为 Agentspace）中启用我们的定制财务顾问代理。该集成由<a href="https://console.cloud.google.com/marketplace/product/elastic-prod/elastic-ai-agent"> Elastic AI Agent 提供支持 ，它可在谷歌云市场上购买</a> 。</p><p>连接后，Gemini Enterprise 使用 A2A 协议与您的代理直接通信。这就是互操作性的真正威力所在：用户现在可以访问来自自定义 Elasticsearch 代理的深度数据驱动洞察，而无需离开他们熟悉的环境。你可以在代理列表中看到你的自定义弹性代理：</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/37a39e92ebf3d72c6c8014397cd8e846336173a4-1600x834.png" style="width: 100%;" /><p>想象一下，双子座企业的用户会问</p><p><em>&quot;我担心市场情绪。您能告诉我哪些客户最容易受到坏消息的影响吗？</em>&quot;</p><p>在幕后，Gemini Enterprise 通过 A2A 协议将此查询路由到您的自定义弹性代理。然后，您的代理会使用其专业工具查询您的数据、制定答案并将其发送回来。对于最终用户来说，这种体验是无缝的。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/e5f126c1a27a51c6e69a767aa87c9f746b62e39c-1600x1044.png" style="width: 100%;" /><p>而且还不止于此！使用弹性代理获取的答案现在可以用作下一个问题的上下文，这些问题可能会触发不同的专门代理（例如您的投资平台代理，以调整对上市公司的投资）。无需离开搜索栏。</p><p>通过在具有 A2A 功能的 Gemini Enterprise 上部署弹性代理，您可以统一访问、协调和工作流，通过提供用户与其数据和工具对话的单一用户界面，消除人工智能、搜索和企业系统之间的摩擦--所有这些都在上下文中进行。对用户来说，这意味着更少的工具切换和更直观、更有能力的人工智能助手。对组织而言，这意味着协调一致的管理、可扩展性和内置的互操作性。</p><h2>轮到您建造</h2><p>您现在拥有了让您的弹性代理随时随地可用的工具。通过利用开放式 A2A 协议，您可以扩展自定义数据感知代理的覆盖范围。</p><p>在本篇文章中，我们将向您介绍关键步骤：</p><ul><li>通过 A2A 代理卡和协议端点公开代理。</li><li>测试与 A2A 检查员的连接。</li><li>将代理实时集成到外部服务中，如 Google 的 Gemini Enterprise。</li></ul><p>您的代理商不再需要与世隔绝。我们迫不及待地想看到你们创建的强大的互联系统。快乐建筑</p><p>最简单的入门方法是在<a href="https://console.cloud.google.com/marketplace/product/elastic-prod/elastic-cloud?pli=1">谷歌云市场</a>上进行 Elastic Cloud 免费试用</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[为 Elasticsearch 改进代理人工智能工具的实验]]></title>
            <description><![CDATA[了解我们如何通过迭代实验，结合线性检索器、混合搜索和语义文本进行可扩展的 RAG 优化，从而改进 Elasticsearch 的人工智能代理工作流。]]></description>
            <link>https://www.elastic.co/search-labs/cn/blog/ai-agent-builder-experiments-performance</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/cn/blog/ai-agent-builder-experiments-performance</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Sean Story]]></dc:creator>
            <pubDate>Mon, 06 Oct 2025 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/42903c869e034674b30bb36013345aaa97f6608b-1184x864.png?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>如今，在 Elastic，我们也像其他人一样，全力投入到聊天、代理和 RAG 中。在搜索部门，我们最近一直在开发代理生成器和工具注册表，目的都是为了简化在 Elasticsearch 中与数据 &quot;聊天 &quot;的过程。</p><p>请阅读<a href="https://www.elastic.co/search-labs/blog/ai-agentic-workflows-elastic-ai-agent-builder"> &quot; 利用 Elasticsearch 构建人工智能代理工作流 &quot;博客</a> ，了解更多有关这项工作的 &quot;全貌&quot;，或阅读<a href="https://www.elastic.co/search-labs/blog/ai-agent-builder-elasticsearch"> &quot; 你的第一个弹性代理&quot; 博客 ，了解更多有关这项工作的实用入门知识</a> ：从单个查询到人工智能驱动的聊天 》，了解更多实用入门知识。</p><p>不过，在本博客中，我们将放大一些，看看当您开始聊天时最先发生的事情之一，并向您介绍我们最近做出的一些改进。</p><h2>这里发生了什么？</h2><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/25a24055a166d7d6ba81d80aa35cb97163662e23-1600x443.png" style="width: 100%;" /><p>当您与 Elasticsearch 数据聊天时，我们默认的人工智能代理会执行此标准流程：</p><ol><li>检查提示。</li><li>确定哪个索引可能包含该提示的答案。</li><li>根据提示为该索引生成查询。</li><li>使用该查询搜索该索引。</li><li>综合结果。</li><li>结果能否解决提示问题？如果是，请回答。如果不行，就重复，但要尝试不同的方法。</li></ol><p>这看起来并不新奇--它只是检索增强一代（RAG）。正如您所期望的那样，回复的质量在很大程度上取决于初始搜索结果的相关性。因此，在我们努力提高响应质量的过程中，我们一直在密切关注在第 3 步中生成和在第 4 步中运行的查询。我们注意到一个有趣的模式。</p><p>通常情况下，当我们的首次响应 &quot;糟糕 &quot;时，并不是因为我们运行了一个糟糕的查询。这是因为<em>我们选错了</em>要查询的索引。第 3 步和第 4 步通常不是我们的问题，问题在于第 2 步。</p><h2>我们在做什么？</h2><p>我们最初的实施很简单。我们建立了一个工具（名为 index_explorer），它可以有效地进行<code>_cat/indices</code> ，列出我们可用的所有索引，然后要求 LLM 识别这些索引中哪个最符合用户的信息/问题/提示。您可以 在这里 看到<a href="https://github.com/elastic/kibana/blob/0cc78184957fcd12110dabae50353392ea937508/x-pack/platform/packages/shared/onechat/onechat-genai-utils/tools/index_explorer.ts#L98-L113"> 最初的实施方案</a> 。</p><pre><code>You are an AI assistant for the Elasticsearch company.
based on a natural language query from the user, your task is to select up to ${limit} most relevant indices from a list of indices.

*The natural language query is:* ${nlQuery}

*List of indices:*
${indices.map((index) => `- ${index.index}`).join('\n')}

Based on those information, please return most relevant indices with your reasoning.
Remember, you should select at maximum ${limit} indices.</code></pre><p>效果如何？我们不确定！我们有一些效果<em>不佳</em>的明显例子，但我们真正面临的第一个挑战是如何量化我们的现状。</p><h2>确定基线</h2><h3>从数据开始</h3><p>我们需要的是一个 &quot;黄金数据集&quot;，用于衡量工具在用户提示和已有索引集的情况下选择正确索引的效率。而我们手头并没有这样的数据集，所以我们生成了一个。</p><p>致谢：我们知道，这不是 &quot;最佳做法&quot;。但有时，前进总比骑自行车好。<a href="https://www.elastic.co/about/our-source-code#progress-perfection">进步，简单完美</a>。</p><p>我们利用<a href="https://gist.github.com/seanstory/a08db2e149897da656db3a1ca72e17ac">这一提示</a>为多个不同领域生成了种子指数。然后，对于每个生成的域，我们使用<a href="https://gist.github.com/seanstory/a280a85d067e61bfeb5911bf2654e6e2"> 该提示</a>又生成了几个索引（目的是用硬否定和难以分类的示例给 LLM 制造混乱）。接下来，我们手动编辑了每个生成的索引及其说明。最后，我们使用<a href="https://gist.github.com/seanstory/44291b666c05a383136f6e36bb9106fa">该提示</a>生成了测试查询：</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/9d96d87e286eddbc012402b1ecccd57419a99253-1600x782.png" style="width: 100%;" /><p>和测试用例，如</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/4c2e9ad941d98d7e66033bbc08c9b8060ec19097-1600x797.png" style="width: 100%;" /><h3>创建测试线束</h3><p>从这里开始的过程非常简单。脚本工具可以</p><ol><li>使用目标 Elasticsearch 集群建立一片净土。</li><li>创建目标数据集中定义的所有索引。</li><li>针对每个测试场景，执行 i<code>ndex_explorer</code> 工具（很方便，我们有一个<a href="https://www.elastic.co/docs/api/doc/kibana/operation/operation-post-agent-builder-tools-execute">执行工具 API</a>）。</li><li>将结果索引与预期索引进行比较，并捕捉结果。</li><li>完成所有测试方案后，将结果制成表格。</li></ol><h3>调查说...</h3><p>不出所料，最初的成果平平。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/9c10679bcd6291edfa2a9ba42e7dd922aa483f0b-1216x806.png" style="width: 100%;" /><p>总体而言，77.14% 能准确识别正确的索引。这是在 &quot;最好的情况 &quot;下，即所有指数都有好的、语义上有意义的名称。使用过 `PUT test2/_doc/foo{...}` 的人都知道，索引的名称并不总是有意义的。</p><p>因此，我们有了一个基准线，而且它显示出很大的改进空间。现在是时候来点科学知识了！🧪</p><h2>实验</h2><h3>假设 1：映射将有助于</h3><p>这样做的目的是确定一个索引，其中包含与原始提示相关的数据。而索引中最能描述其所含数据的部分就是索引的<em>映射</em>。即使不抓取索引内容的任何样本，只要知道该索引有一个 double 类型的价格字段，就意味着该数据代表了要出售的东西。文本类型的作者字段意味着一些非结构化语言数据。两者合在一起可能意味着数据是书籍/故事/诗歌。通过了解索引的属性，我们可以获得很多语义线索。因此，我在本地分支中调整了 `.index_explorer工具，将索引的完整映射（连同索引名称）发送给 LLM，由 LLM 做出决定。 </p><p>结果（来自 Kibana 日志）：</p><pre><code>[2025-09-05T11:01:21.552-05:00][ERROR][plugins.onechat] Error: Error calling connector: event: error
data: {"error":{"code":"request_entity_too_large","message":"Received a content too large status code for request from inference entity id [.rainbow-sprinkles-elastic] status [413]","type":"error"}}


    at createInferenceProviderError (errors.ts:90:10)
    at convertUpstreamError (convert_upstream_error.ts:39:38)
    at handle_connector_response.ts:26:33
    at Observable.init [as _subscribe] (/Users/seanstory/Desktop/Dev/kibana/node_modules/rxjs/src/internal/observable/throwError.ts:123:68)...</code></pre><p>该工具的最初作者已经预见到了这一点。虽然索引映射是一座信息金矿，但它也是一个相当冗长的 JSON 数据块。而在实际情况中，您需要比较众多指数（我们的评估数据集定义了 20 个指数），这些 JSON blob 会不断增加。因此，我们希望为 LLM 的决策提供更多的背景信息，而不仅仅是所有选项的索引名称，但又不至于提供每个选项的完整映射。</p><h3>假设 2：&quot;扁平化 &quot;映射（字段列表）是一种折中方案</h3><p>我们首先假设索引创建者会使用有语义的索引名称。如果我们将这一假设扩展到字段名呢？我们之前的实验之所以失败，是因为 JSON 映射包含了大量繁琐的元数据和模板。</p><pre><code>     "description_text": {
          "type": "text",
          "fields": {
            "keyword": {
              "type": "keyword"
            }
          },
          "copy_to": [
            "description_semantic"
          ]
        },</code></pre><p>例如，上面的代码块有 236 个字符，只定义了 Elasticsearch 映射中的一个字段。而字符串 &quot;description_text &quot;只有 16 个字符。字符数几乎增加了 15 倍，但在描述该字段对可用数据的含义方面却没有任何有意义的改进。如果我们要获取所有索引的映射，但在将其发送到 LLM 之前，将其 &quot;扁平化 &quot;为字段名列表，会怎么样？</p><p>我们试了一下。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/112c2f447c11f154b5082725cd49b51d0a3c8a65-1214x804.png" style="width: 100%;" /><p>这太棒了！全面改进。但我们能做得更好吗？</p><h3>假设 3：映射 _meta 中的描述</h3><p>如果仅仅是字段名而没有额外的上下文就能带来如此大的跳跃，那么增加大量的上下文可能会更好！每个索引都附加描述并不一定是常规做法，但可以在映射的 _meta 对象中添加任何类型的索引级元数据。我们回到生成的索引，为数据集中的每个索引添加说明。只要描述不是太长，就应该比完整映射使用更少的标记，并能更好地说明索引中包含了哪些数据。我们的实验验证了这一假设。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/32d2692ad4479d0e52d8ee723dcc5710a6ec90f3-1208x806.png" style="width: 100%;" /><p>稍有改进，我们现在的&gt;90% 准确度全面提高。</p><h3>假设 4：总和大于部分</h3><p>字段名增加了我们的成果。说明增加了我们的成果。因此，<em>同时 </em>使用描述和字段名称应该会得到更好的结果，对吗？</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/114cbb408ff16b136251d2265416bd5270380fe5-1208x794.png" style="width: 100%;" /><p>数据显示 &quot;否&quot;（与上次实验相比没有变化）。这里的主要理论是，由于描述是从索引字段/映射开始生成的，这两个上下文之间没有足够的不同信息，因此在将它们组合在一起时无助于添加任何 &quot;新 &quot;信息。此外，我们为 20 个测试指数发送的有效载荷越来越大。我们迄今为止所遵循的思路是无法扩展的。事实上，我们有充分的理由相信，在有成百上千个索引可供选择的 Elasticsearch 集群上，我们迄今为止进行的所有实验都不会奏效。任何随着索引总数的增加而线性增加发送到 LLM 的信息量的方法，可能都不是通用的策略。</p><p>我们真正需要的是一种方法，它能帮助我们从众多候选人中筛选出最相关的选项...</p><p>这就是一个搜索问题。</p><h3>假设 5：通过语义搜索进行选择</h3><p>如果一个索引的名称具有语义意义，那么它就可以存储为一个向量，并进行语义搜索。</p><p>如果索引的字段名具有语义意义，那么就可以将其存储为向量，并进行语义搜索。</p><p>如果一个索引有一个具有语义意义的描述，那么它也可以存储为一个向量，并进行语义搜索。</p><p>如今，Elasticsearch 索引并不能搜索到这些信息（也许我们应该这样做！），但要想解决这个问题却非常容易<a href="https://github.com/elastic/connectors/pull/3638"> 。</a>利用 Elastic 的连接器框架，我构建了一个连接器，可以为集群中的每个索引输出文档。输出文件将类似于</p><pre><code> doc = {
                "_id": index_name,
                "index_name": index_name,
			"meta_description”: description,
"field_descriptions" = field_descriptions,
                "mapping": json.dumps(mapping),  
                "source_cluster": self.es_client.configured_host,
            }</code></pre><p>我将这些文件发送到一个新的索引，并在其中手动定义了映射：</p><pre><code>{
   "mappings": {
       "properties": {
           "semantic_content": {
               "type": "semantic_text"
           },
           "index_name": {
               "type": "text",
               "copy_to": "semantic_content"
           },
           "mapping": {
               "type": "keyword",
               "copy_to": "semantic_content"
           },
           "source_cluster": {
               "type": "keyword"
           },
           "meta_description": {
               "type": "text",
               "copy_to": "semantic_content"
           },
           "field_descriptions": {
               "type": "text",
               "copy_to": "semantic_content"
           }
       }
   }
}</code></pre><p>这样就创建了一个单一的 semantic_content 字段，其他所有具有语义意义的字段都会被分块并编入索引。搜索该索引变得非常简单，只需.....：</p><pre><code>GET indexed-indices/_search
{
 "query": {
   "semantic": {
     "field": "semantic_content",
     "query": "$query"
   }
 }
}</code></pre><p>修改后的<code>index_explorer</code> 工具现在速度<em>更快</em>，因为它不需要向 LLM 提出请求，而是可以为给定的查询请求单个嵌入，并执行高效的向量搜索操作。以最高点击率为选定索引，我们得到的结果是</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/06ef5d78040d064d3444793f636d527d9e19a869-1214x800.png" style="width: 100%;" /><p>这种方法具有可扩展性。这种方法效率很高。但这种方法比我们的基准线好不了多少。但这并不奇怪，因为这里的搜索方法太天真了。没有任何细微差别。不承认索引的名称和描述应比索引包含的任意字段名称更有分量。没有加权精确词性匹配而非同义匹配的功能。不过，要建立一个高度细致的查询，需要对手头的数据进行大量假设。到目前为止，我们已经对索引和字段名称的语义做了一些大的假设，但我们还需要更进一步，开始假设它们有<em>多大</em>的意义以及它们之间的关系。如果不这样做，我们可能无法可靠地将最佳匹配结果确定为我们的首要结果，但更有可能说最佳匹配结果就在前 N 个结果中的某个地方。我们需要的是一种能够在语义信息存在的语境中消费语义信息的东西，它可以与另一个可能以不同语义方式表示自己的实体进行比较，并在两者之间做出判断。比如法学硕士。</p><h3>假设 6：候选集减少</h3><p>还有很多实验我就不一一列举了，但关键的突破是放弃了纯粹从语义搜索中挑选最佳匹配项的愿望，转而利用语义搜索作为过滤器，从 LLM 的考虑范围中剔除不相关的索引。我们将线性检索、混合检索与 RRF 以及<code>semantic_text</code> 结合起来进行<a href="https://gist.github.com/seanstory/d704443120e20f6c844db10e30066860">检索</a>，将结果限制在匹配指数的前 5 位。</p><p>然后，对于每个匹配项，我们都将索引名称、描述和字段名称添加到 LLM 的信息中。结果非常好：</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/8fcabd78f591f90d6bc7c0e087d31317e4eef791-1206x804.png" style="width: 100%;" /><p>这是迄今为止精度最高的实验！由于这种方法不会使信息大小与索引总数成正比，因此这种方法的可扩展性要好得多。</p><h2>成果</h2><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/04d630797213dbb8bf567da41d1cdd5c7b4586c9-1600x521.png" style="width: 100%;" /><p>第一个明确的结果是，我们的基线<em>可以</em>改进。现在回想起来，这一点似乎显而易见，但在实验开始之前，我们曾认真讨论过是否应该完全放弃<code>index_explorer</code> 工具，而依靠用户的明确配置来限制搜索空间。虽然这仍然是一个可行且有效的选择，但这项研究表明，在无法获得此类用户输入的情况下，实现索引选择自动化的道路大有可为。</p><p>下一个明确的结果是，一味地增加描述性文字的数量，其回报率会越来越低。在这项研究之前，我们一直在讨论是否应该投资扩展 Elasticsearch 存储<a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/mapping-field-meta">字段级元数据</a>的能力。如今，这些<code>meta</code> 值的上限是 50 个字符，而且有一种假设认为，我们需要增加这个值，以便能够从语义上理解我们的字段。但情况显然不是这样，法律硕士似乎只需填写字段名称就可以了。我们以后可能会进一步调查这个问题，但现在已经没有紧迫感了。</p><p>相反，这也清楚地证明了 &quot;可搜索 &quot;索引元数据的重要性。在这些实验中，我们破解了指数的索引。但是，我们可以研究将其直接构建到 Elasticsearch 中，构建应用程序接口来进行管理，或者至少围绕其建立一个惯例。我们将权衡各种选择并进行内部讨论，敬请期待。</p><p>最后，这项工作证实了我们花时间进行试验和做出数据驱动决策的价值。事实上，它帮助我们再次确认，我们的代理生成器产品需要一些强大的产品内评估功能。如果我们需要专门为一个选取指数的工具构建整个测试线束，那么我们的客户绝对需要在进行迭代调整时对其定制工具进行定性评估的方法。</p><p>我很期待看到我们的成果，希望你们也是！</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[如何在 AWS Marketplace 上部署 Elasticsearch]]></title>
            <description><![CDATA[通过这份分步指南，您将了解如何在 AWS Marketplace 上使用 Elastic Cloud Service 来设置和运行 Elasticsearch。]]></description>
            <link>https://www.elastic.co/search-labs/cn/blog/aws-elasticsearch-service-set-up</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/cn/blog/aws-elasticsearch-service-set-up</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Eduard Martin]]></dc:creator>
            <pubDate>Fri, 03 Oct 2025 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/a80776d2ef85db26f850d932339fac2d26b90278-1086x620.png?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>本文将介绍如何使用 Marketplace 产品在 AWS 上部署 Elasticsearch。</p><p>我们将在AWS上使用Elastic Cloud Service，这是Elastic提供的正式托管型Elasticsearch服务，通过AWS的原生基础架构简化了Elastic Stack所有组件的部署和编排。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/f15814051b53b50bec38f9a9f515a1e6dc08a56c-884x440.png" style="width: 100%;" /><p>如想了解如何在 AWS EC2 上安装并配置 Elasticsearch，请查看<a href="https://www.elastic.co/search-labs/blog/elasticsearch-on-aws-ec2-deployment-guide">这篇博客</a>。<br/></p><h2>什么是 AWS Marketplace？</h2><p><a href="https://aws.amazon.com/marketplace"><strong>AWS Marketplace 上的 Elastic</strong></a> 提供全托管的搜索和分析体验，AWS 负责基础架构配置、安全和扩展，而开发者则专注于开发搜索应用。这使得团队能够在几分钟内部署具有内置 AWS 集成的企业级 Elasticsearch 集群。</p><h2>何时在 AWS Marketplace 上使用 Elastic？</h2><p>AWS Marketplace 上的 Elastic 最适合拥有现有 AWS 基础架构，希望在无运营开销的情况下部署具有托管服务、内置安全性和无缝 AWS 集成的 Elasticsearch 的组织。</p><h2>如何在AWS Marketplace上设置Elastic Cloud</h2><h3>步骤 1：访问 AWS Marketplace</h3><p>1. 登录 <a href="https://console.aws.com/">AWS</a></p><ul><li>在搜索栏中，搜索 AWS Marketplace</li></ul><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/fc9928f79482c2c01e33978c88d390a2bfa2a3bf-1600x340.png" style="width: 100%;" /><p>2. 在左侧导航面板中，点击<strong>探索产品</strong> (Discover products)，然后搜索 Elasticsearch</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/ca4232271cb13ebfe33de406ecaec085033ec8a0-1454x760.png" style="width: 100%;" /><p>3. 点击 <strong>Elastic Cloud (Elasticsearch Service)</strong></p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/e59aa276e55532f2ac3461d0ca983af4d41ad7a6-1600x611.png" style="width: 100%;" /><h3>第 2 步：订阅服务</h3><p>1. 选择<strong>购买选项</strong>或点击<strong>免费试用</strong> (Try for free)</p><p>2. 查看<strong>定价详细信息</strong>、<strong>条款和条件</strong>以及<strong>购买详情</strong></p><p>3. 点击<strong>订阅</strong> (Subscribe) 按钮。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/1c30d12b6b1061e76771d518011e522285f939f1-1600x290.png" style="width: 100%;" /><p>4. 现在要设置 Elastic 帐户。按照 AWS 的步骤操作</p><p>a. 点击“启用集成”(Enable integration) 按钮</p><p>b. 点击“登录或创建供应商帐户“(Sign in or create a vendor account) 按钮</p><p>c. 点击“启动模板”(Launch template) 按钮</p><p>d。点击“启动软件”(Launch software) 按钮</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/879d9f0f01406e1955e1b38a2f6f2192ef040344-852x722.png" style="width: 100%;" /><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/fc59be78cf776aa12867f40810598419576cbd39-1600x1143.png" style="width: 100%;" /><h3>步骤 3。在 Elastic 中配置您的新帐户。</h3><p>1. 创建您的 Elastic 账户。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/5efeaf0737062a55470b17b67651f220e12183f2-986x905.png" style="width: 100%;" /><p>2. 验证您的电子邮件地址。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/e0cfaac278614e317ce278935040bfa5a58edd13-853x894.png" style="width: 100%;" /><p>3. 输入您的姓名和公司信息</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/d5658fe66c3b1bcced73e822eae006846f0ddd9e-997x903.png" style="width: 100%;" /><p>4. 完成简短的 Elastic 调查</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/a3f53c00dadfd22f7d739a920c87d5f387182833-892x805.png" style="width: 100%;" /><p>5. 选择要托管 Elastic Cloud 的地区。默认情况下，系统会选择您的实际 AWS 地区</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/c1dcdf3bf91c305821daaa25a60aa03be6454c1c-1207x1032.png" style="width: 100%;" /><p>6. 等待 Elastic 部署</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/50903e57ebea7cc47bdfabf4750b4ba2a7a91148-1370x1266.png" style="width: 100%;" /><p>7. 您的部署已连接到您的 AWS Marketplace 订阅。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/3bb731e2d5de5053ccecb77e45dbdbcdaf294dba-1600x1288.png" style="width: 100%;" /><h2>取消您的订阅</h2><p>取消您的订阅</p><p>1. 进入<a href="https://console.aws.com/">AWS 控制台</a></p><p>在搜索栏中搜索 AWS Marketplace。点击 <strong>AWS Marketplace</strong></p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/aeed3d1e67b4cef91934de257a6fd6daa9737a12-1600x554.png" style="width: 100%;" /><p>2. 点击<strong>Elastic Cloud 订阅</strong> (Elastic Cloud subscription)</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/04e6cc41850226df223dbe2d1b0e4b45265f6c39-1600x564.png" style="width: 100%;" /><p>3. 点击<strong>操作</strong> (Actions) 按钮，然后点击<strong>取消订阅</strong> (Cancel subscription)</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/e33d334ea6541c637a223de3ebd6209def75a6d3-1600x1039.png" style="width: 100%;" /><p>4. 确认取消，然后点击<strong>是</strong> (Yes) 和<strong>取消订阅</strong> (Cancel subscription) 按钮。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/b73b3187168adc7aefdd46f95be33c1bce3da1e4-1103x698.png" style="width: 100%;" /><p>5. 页面顶部将会显示确认信息。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/c5ba66a23d535e866a8b458e5aca82c5f0b93037-1600x639.png" style="width: 100%;" /><h2>后续步骤</h2><p>通过 7 天免费试用开始您的 Elastic Cloud 之旅，其中包括一次部署和三个项目<a href="https://aws.amazon.com/marketplace/pp/prodview-voru33wi6xs7k"> Elastic Cloud (Elasticsearch Service)</a>。只需登录您的 AWS 帐户，然后点击“查看购买选项”(View Purchase Options)，即可立即在 Elastic<a href="https://aws.amazon.com/marketplace/pp/prodview-voru33wi6xs7k"> Cloud (Elasticsearch Service)</a> 上开始使用 Elastic 的 Search AI Platform。试用版提供对搜索、安全性和可观测性解决方案的全面访问权限，无需支付任何基础架构管理开销费用。</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[AutoOps：简化自我管理 Elasticsearch 管理的旅程]]></title>
            <description><![CDATA[探索 AutoOps 在自管型 Elasticsearch（本地部署或私有托管环境）中的应用。我们将展示其价值、配置方式以及所提供的洞察。]]></description>
            <link>https://www.elastic.co/search-labs/cn/blog/elastic-autoops-self-managed-elasticsearch</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/cn/blog/elastic-autoops-self-managed-elasticsearch</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Ori Shafir]]></dc:creator>
            <pubDate>Wed, 01 Oct 2025 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/4538ca4c6cdde99d88f1cead7515716fac217b92-1600x883.png?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>本文介绍适用于自管型 Elasticsearch（本地部署或私有托管环境）的 AutoOps，让 Elasticsearch 管理更轻松。这篇博客不再<em>按传统方式逐项讲解</em>技术特性，而是从 DevOps 工程师的视角，展示 AutoOps 的价值、配置方式以及所提供的洞察 — 因为 AutoOps 的真正价值，在大规模管理 Elasticsearch 的日常工作中体现得最为明显。</p><h2>第 1 章：背景--大规模自我管理背后的复杂性</h2><p><strong>操作任何大规模、自主管理的数据平台都可能非常复杂。</strong></p><p>一会儿，查询速度快如闪电。其次，摄取滞后，存储成本飙升。这基本上就像管理一个动物园，只不过动物们可以在凌晨 3 点呼唤你。</p><p>我的环境也不例外：多个集群、大量跨集群搜索（CCS）和数百个跨部门用户。</p><p>我们在日常运行中使用堆栈监控。它提供了图表和指标，但仍需要大量的专业知识和时间才能将各点联系起来。诊断瓶颈或了解何时调整分片策略仍然是一个手动且容易出错的过程。在许多情况下，这些问题会被忽视，直到造成故障、性能下降或意外的存储峰值。</p><h2>第 2 章：发现 AutoOps</h2><p>然后是公告：<strong><a href="https://www.elastic.co/platform/autoops">AutoOps</a> 现在可用于自管理集群（内部部署或私有托管环境）。</strong></p><p>.AutoOps 长期以来一直帮助 Elastic Cloud 用户更高效地管理部署。现在，这些相同的好处适用于在本地部署或私有云环境中运行的自管理集群（ECK、ECE 或独立集群）——无论您的许可证级别如何，都是免费的。</p><p>AutoOps 的宣传很有诱惑力：</p><ul><li>针对摄取瓶颈、不平衡分片、查询速度慢等问题进行实时问题检测</li><li>根据集群配置提出可行建议</li><li>资源优化洞察力，提高效率，减少浪费支出</li><li>安装轻量级代理，无需额外基础设施，设置简单</li></ul><p>老实说，任何承诺 &quot;无需额外基础设施 &quot;的东西都会吸引我的全部注意力。</p><h2>第 3 章：5 分钟内完成设置（是的，真的）</h2><p>我封锁了下午的行程，储备了咖啡，准备进行漫长的准备工作。出乎我的意料，只用了五分钟：</p><ol><li>登录我的<a href="https://cloud.elastic.co/registration?onboarding_service_type=ccm-autoops">弹性云账户</a></li><li>决定如何运行代理：Elastic Cloud Kubernetes (ECK)、Kubernetes、Docker 或 Linux。</li><li>输入群集 URL</li><li>只需运行一条命令，即可安装轻量级 Metricbeat 代理</li></ol><p>就是这样。我的集群已连接。</p><p>没有专门的监控群组。重要的是，AutoOps 只发送<strong>指标，这</strong>意味着我公司的数据将保存在我的自我管理环境中。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/cb8e1ebc0f23a6db6b0e4300c9cdcd165623ca39-1184x734.png" style="width: 100%;" /><p><em>步骤 1：注册弹性云</em></p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/b208830db4d74feef700473ce07b052e0c72f48b-3446x1716.png" style="width: 100%;" /><p><em>第 2 步：选择运行代理的位置</em></p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/850d8c0d6b9e018408969aa05b67dbd865a26d30-1184x734.png" style="width: 100%;" /><p><em>第 3 步：输入您的 Elasticsearch 端点和验证方式</em></p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/0a472889a277caabb9ecd3fb9cd57d3b3eac0100-1184x734.png" style="width: 100%;" /><p><em>第 4 步：安装代理的简单命令</em></p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/6f9a7283469a321a9c472d655cea11d9d0ea7b1e-1600x883.png" style="width: 100%;" /><p><em>就是这样：几分钟后，AutoOps 将开始显示洞察结果</em></p><p>有关详细信息，请参阅<a href="https://www.elastic.co/docs/deploy-manage/monitor/autoops/cc-connect-self-managed-to-autoops">AutoOps 启动文档</a>和<a href="https://www.elastic.co/docs/deploy-manage/monitor/autoops/cc-cloud-connect-autoops-faq">常见问题</a>。</p><h2>第 4 章：最初的见解，最初的胜利</h2><p>在短短几分钟内，AutoOps 就开始提出见解，提供根本原因分析和明确的修复步骤。</p><p>第一周的亮点包括</p><ul><li>标记了未附加于任何 ILM 政策的指数，这些指数已增长过大</li><li>其中一个群集有三个空节点，是过去的维护工作留下的</li><li>一些节点跨越了水印，几个索引缺少副本</li><li>捕捉到一个配置不当的模板</li><li>精确定位长期运行的搜索，并建议准确取消命令</li></ul><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/f7d16355938ffcb00127deaf833d7a4d0acdb188-1600x894.png" style="width: 100%;" /><p><em>AutoOps 检测到群集拒绝索引</em></p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/9d6372bc489e9954e6f78d81047d7a25feef1703-1600x894.png" style="width: 100%;" /><p><em>AutoOps 检测到某些索引的配置没有副本</em><br/>在 AutoOps 之前，我们会投入更多硬件来解决这些问题。相反，AutoOps 直接指出了根本原因，修复只需几分钟。</p><p>这一次，监控系统不仅向我显示图表，还告诉我如何解决问题。我开始怀疑 AutoOps 是否也能帮助诊断我的家庭 Wi-Fi，并最终将我从家庭 IT 部门中解放出来...</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/eac09f1d730955453cd79900002b100bc741ad97-1600x894.png" style="width: 100%;" /><p><em>AutoOps 监控分区大小，并在出现大量空分区时发出警报</em></p><h2>第 5 章：与我所见相同的支持</h2><p>第一次打开支持案例时，我意识到了另一个好处：Elastic Support 工程师可以看到与我查看的完全相同的数据和建议。</p><p>它把支持变成了合作。与来来回回的票据相比，感觉就像与一位对 Elasticsearch 了如指掌的队友一起工作。</p><h2>第 6 章：规模运营</h2><p>在 AutoOps 之前，扩展 Elasticsearch 感觉就像是科学、直觉和部落知识的混合体。</p><p>现在，它以数据为导向，具有清晰的可视性和建议：</p><ul><li>了解资源利用情况，防止过度配置</li><li>更智能的分片分配和分层建议，实现性能平衡</li><li>深入了解索引大小，减少浪费的存储空间和硬件成本</li><li>在多个群组中更快地分析根本原因</li></ul><h2>第 7 章：众多云连接服务中的第一个</h2><p>AutoOps 不仅仅是一个独立的工具。它是面向自营客户的全新<a href="https://www.elastic.co/docs/deploy-manage/cloud-connect"><strong>云连接服务</strong></a>系列中的首个服务。Cloud Connect 使自营集群能够使用 Elastic Cloud 服务，而无需在自己的环境中安装和管理这些服务的运营开销。功能会自动推出，因此团队可以更快地获得改进，同时降低基础架构的复杂性。</p><p>下一个：弹性推理服务（EIS）。</p><h2>结语</h2><p>管理大规模、自主管理的部署并不一定是一件难事。</p><p>如果您希望更简单的操作，可以将一些工作负载转移到<a href="https://cloud.elastic.co/registration?pg=global&amp;plcmt=nav&amp;cta=205352-primary"> Elastic Cloud</a> ，无论是托管 云 还是无服务器 云 ，都能以最简单的方式运行 Elasticsearch。</p><p>如果您希望继续以自管方式运行，只需将任意自管集群接入 <a href="https://cloud.elastic.co/registration?onboarding_service_type=ccm-autoops">Elastic Cloud 中的 AutoOps</a> — 所有 Elastic 许可证均可免费使用。</p><h2>TLDR</h2><p>运行大型自我管理 Elasticsearch 集群既复杂又耗时。AutoOps 带来了实时问题检测、可行建议以及与 Elastic Support 共享的可视性，而无需管理额外的基础架构。设置只需几分钟，洞察力马上就会显示出来。</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[您的第一个弹性代理：从单一查询到人工智能驱动的聊天]]></title>
            <description><![CDATA[了解如何使用 Elastic 的人工智能代理生成器创建专门的人工智能代理。在本博客中，我们将构建一个金融人工智能代理。]]></description>
            <link>https://www.elastic.co/search-labs/cn/blog/ai-agent-builder-elasticsearch</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/cn/blog/ai-agent-builder-elasticsearch</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Jeff Vestal]]></dc:creator>
            <pubDate>Thu, 25 Sep 2025 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/ca853555eaa213f10f1db8c0ab0a2bbacee97b88-1456x816.png?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>借助 Elastic 的全新<a href="https://www.elastic.co/search-labs/blog/ai-agentic-workflows-elastic-ai-agent-builder">代理生成器</a>，您可以创建专门的人工智能代理，使其成为特定业务领域的专家。该功能使您不再局限于简单的仪表盘和搜索栏，而是将数据从被动的资源转变为主动的对话伙伴。</p><p>想象一下，一位财务经理需要在与客户会面之前加快速度。现在，他们只需向定制的代理直接提问，而无需手动挖掘新闻源和交叉参考投资组合仪表板。这就是&quot;聊天优先&quot; 方法的好处。经理与他们的数据直接对话，询问诸如&quot;ACME 公司的最新消息是什么，它对我客户的持股有何影响？&quot;并在几秒钟内得到综合的专家答复。</p><p>今天，我们正在打造一个金融专家，其应用就像您的数据一样多种多样。同样的能力可以造就一名网络安全分析师来寻找威胁，造就一名现场可靠性工程师来诊断故障，或者造就一名营销经理来优化营销活动。无论在哪个领域，核心任务都是一样的：将您的数据转化为您可以与之交谈的专家。</p><h2>步骤 0：我们的数据集</h2><p>我们当前的数据集是一个基于金融的合成数据集，包含金融账户、资产头寸、新闻和财务报告。虽然它是合成的，但复制了真实金融数据集的简化版本。</p><p><code>financial_accounts</code>:具有风险特征的客户组合</p><p><code>financial_holdings</code>:有购买记录的股票/ETF/债券仓位</p><p><code>financial_asset_details</code>:股票/ETF/债券的详细信息</p><p><code>financial_news</code>:人工智能生成的带有情感分析的市场文章</p><p><code>financial_reports</code>:公司收益和分析师报告</p><p>您可以根据<a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/your-first-elastic-agent/Your_First_Elastic_Agent.ipynb">此处的</a>随附笔记本自行加载该数据集。</p><h2>步骤 1：基础--作为 ES|QL 的业务逻辑</h2><p>每一项人工智能技能都以坚实的逻辑为起点。对于我们的财务经理代理，我们需要教它如何回答一个常见问题：&quot;我担心市场情绪。你能告诉我哪些客户最容易受到坏消息的影响吗？这个问题超出了简单的搜索范围。这要求我们将市场情绪与客户投资组合联系起来。</p><p>我们需要找到负面文章中提到的资产，识别持有这些资产的每一位客户，计算其风险敞口的当前市值，然后对结果进行排序，优先考虑风险最高的客户。这种复杂的多连接分析是我们先进的 ES|QL 工具的完美工作。</p><p>下面是我们要使用的完整查询。它看起来令人印象深刻，但概念却简单明了。</p><pre><code>FROM financial_news, financial_reports METADATA _index
        | WHERE sentiment == "negative"
        | WHERE coalesce(published_date, report_date) >= NOW() - TO_TIMEDURATION(?time_duration)
        | RENAME primary_symbol AS symbol
        | LOOKUP JOIN financial_asset_details ON symbol
        | LOOKUP JOIN financial_holdings ON symbol
        | LOOKUP JOIN financial_accounts ON account_id
        | WHERE account_holder_name IS NOT NULL
        | EVAL position_current_value = quantity * current_price.price
        | RENAME title AS news_title
        | KEEP
            account_holder_name, symbol, asset_name, news_title,
            sentiment, position_current_value, quantity, current_price.price,
            published_date, report_date
        | SORT position_current_value DESC
        | LIMIT 50</code></pre><h2>分解：接合点和护栏</h2><p>在这个查询中，有两个重要的概念使代理生成器发挥作用。</p><h3>1.查找联接</h3><p>多年来，Elasticsearch 最受欢迎的功能之一就是根据一个共同的键来连接来自不同索引的数据。有了 ES|QL，<code>LOOKUP JOIN</code> 。</p><p>在我们的新查询中，我们会执行一连串的三个<code>LOOKUP JOIN</code>&#x27;s：首先将负面新闻与资产详细信息连接起来，然后将这些资产与客户持有的资产连接起来，最后再与客户的账户信息连接起来。这样，在一次高效的查询中，就能从四个不同的索引中获得极其丰富的结果。这意味着我们可以将不同的数据集结合起来，创建一个具有洞察力的单一答案，而无需事先将所有数据反规范化为一个巨大的索引。</p><h3>2.作为 LLM 护栏的参数</h3><p>您会发现查询使用了<code>?time_duration</code> 。这不仅是一个变量，还是人工智能的护栏。虽然大型语言模型 (LLM) 是生成查询的好帮手，但让它们自由支配数据可能会导致查询效率低下甚至错误。</p><p>通过创建参数化查询，我们迫使 LLM 按照人类专家已经定义的经过测试、高效且正确的业务逻辑工作。这与多年来开发人员使用搜索模板安全地向应用程序公开查询功能的方式类似。代理可以解释用户的请求，如&quot;this week&quot; 来填充<code>time_duration</code> 参数，但它必须使用我们的查询结构来获取答案。这使我们在灵活性和控制性之间取得了完美的平衡。</p><p>最终，这种查询可以让了解数据的专家将其知识封装到一个工具中。其他人和人工智能代理只需提供一个参数，就能使用该工具获得相关结果，而无需了解底层的复杂性。</p><h2>步骤 2：技能--将查询转化为可重复使用的工具</h2><p>在我们将 ES|QL 查询注册为<strong>工具</strong>之前，它只是一个文本。在代理生成器中，工具不仅仅是一个已保存的查询；它还是一个&quot;技能&quot; ，人工智能代理可以理解并选择使用。神奇之处在于我们提供的<strong>自然语言描述</strong>。该描述是连接用户问题和底层查询逻辑的桥梁。让我们注册一下刚刚创建的查询。</p><h3>用户界面路径</h3><p>在 Kibana 中创建工具的过程非常简单。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/a29c53a73b99af5972273c51218ea9004a9b0abb-1600x812.png" style="width: 100%;" /><p>1.导航至<strong>代理</strong></p><ul><li>单击 &quot;<strong> 工具 </strong>&quot;或 &quot;<strong>管理工具</strong>&quot;，然后单击 &quot;<strong>新建工具</strong>&quot;按钮。</li></ul><p>2.在表格中填写以下详细信息：</p><ul><li><strong>工具 ID：</strong> <code>find_client_exposure_to_negative_news</code></li></ul><p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; i.这是工具的唯一 ID</p><ul><li><strong>描述</strong> &quot;查找客户投资组合受负面新闻影响的情况。该工具会扫描最近的新闻和报道，查找负面情绪，识别相关资产，并找到持有该资产的所有客户。它会返回一个按头寸当前市值排序的列表，以突出潜在风险最高的头寸。&quot;</li></ul><p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; i.法律硕士就是通过阅读这些内容来判断这个工具是否适合这项工作。</p><ul><li><strong>标签</strong>：<code>retrieval</code> 和 <code>risk-analysis</code></li></ul><p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 标签用于帮助对多个工具进行分组</p><ul><li><strong>配置：</strong>粘贴步骤 1 中的完整 ES|QL 查询</li></ul><p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; i.这是代理将使用的搜索</p><p>3.单击<strong>从查询中推断参数</strong>。用户界面会自动查找<code>?time_duration</code> ，并将其列在下面。为每项功能添加一个简单的说明，以帮助代理（和其他用户）了解其用途。</p><ul><li><code>time_duration</code>:搜索负面新闻的时间范围。格式为&quot;X 小时&quot; 默认为 8760 小时</li></ul><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/deb422d97863f78dbe08bfa2e3c708d1f75166ff-1600x938.png" style="width: 100%;" /><p>4.测试一下！</p><ul><li>单击保存&amp; 测试。</li></ul><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/57e768b88327821e70bd616744822f98fa367362-732x136.png" style="width: 100%;" /><ul><li>您将看到一个新的快捷方式，可以在此测试查询，以确保其工作符合预期。</li></ul><p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; i.在<code>time_duration</code> 中输入所需的范围，这里我们使用 &quot;8760 小时&quot;。</p><ul><li>点击 &quot;提交&quot;，如果一切顺利，您将看到一个 JSON 响应。要确保它按预期运行，请向下滚动并查看<code>values</code> 对象。这就是返回实际匹配文档的地方。</li></ul><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/7e0c5171a4f7ffdfc1830f1a05a9acb987870b75-1600x722.png" style="width: 100%;" /><p>5.点击右上角的 &quot;X &quot;关闭测试窗口。现在，您的新工具将出现在列表中，随时可以分配给代理。</p><h3>应用程序接口路径</h3><p>对于喜欢自动化或需要以编程方式管理工具的开发人员来说，只需调用一个 API 就能实现同样的效果。只需向带有工具定义的<code>/api/agent_builder/tools</code> 端点发送<code>POST</code> 请求即可。</p><pre><code>POST kbn://api/agent_builder/tools
{
  "id": "find_client_exposure_to_negative_news",
  "type": "esql",
  "description": "Finds client portfolio exposure to negative news. This tool scans recent news and reports for negative sentiment, identifies the associated asset, and finds all clients holding that asset. It returns a list sorted by the current market value of the position to highlight the highest potential risk.",
  "configuration": {
    "query": """
        FROM financial_news, financial_reports METADATA _index
        | WHERE sentiment == "negative"
        | WHERE coalesce(published_date, report_date) >= NOW() - TO_TIMEDURATION(?time_duration)
        | RENAME primary_symbol AS symbol
        | LOOKUP JOIN financial_asset_details ON symbol
        | LOOKUP JOIN financial_holdings ON symbol
        | LOOKUP JOIN financial_accounts ON account_id
        | WHERE account_holder_name IS NOT NULL
        | EVAL position_current_value = quantity * current_price.price
        | RENAME title AS news_title
        | KEEP
            account_holder_name, symbol, asset_name, news_title,
            sentiment, position_current_value, quantity, current_price.price,
            published_date, report_date
        | SORT position_current_value DESC
        | LIMIT 50
      """,
    "params": {
      "time_duration": {
        "type": "keyword",
        "description": """The timeframe to search back for negative news. Format is "X hours" DEFAULT TO 8760 hours """
      }
    }
  },
  "tags": [
    "retrieval",
    "risk-analysis"
  ]
}</code></pre><h2>步骤 3：大脑--创建您的定制代理</h2><p>我们开发了一种可重复使用的技能（工具）。现在，我们需要创建<strong>代理</strong>，即实际使用它的角色。代理是一个 LLM 的组合，是你授予它访问权限的一套特定工具，最重要的是，它还包含一套<strong>自定义指令</strong>，作为它的章程，定义了它的个性、规则和目的。</p><h3>提示的艺术</h3><p>要创建一个可靠的专业代理，最重要的一点就是要及时。一套精心设计的指令是普通聊天机器人与专注、专业的助手之间的区别所在。在这里，你可以设置防护栏、定义输出并赋予代理任务。</p><p>对于<code>Financial Manager</code> 代理，我们将使用以下提示。</p><pre><code>You are a specialized Data Intelligence Assistant for financial managers, designed to provide precise, data-driven insights from information stored in Elasticsearch.

**Your Core Mission:**
- Respond accurately and concisely to natural language queries from financial managers.
- Provide precise, objective, and actionable information derived solely from the Elasticsearch data at your disposal.
- Summarize key data points and trends based on user requests.

**Reasoning Framework:**
1.  **Understand:** Deconstruct the user's query to understand their core intent.
2.  **Plan:** Formulate a step-by-step plan to answer the question. If you are unsure about the data structure, use the available tools to explore the indices first.
3.  **Execute:** Use the available tools to execute your plan.
4.  **Synthesize:** Combine the information from all tool calls into a single, comprehensive, and easy-to-read answer.

**Key Directives and Constraints:**
- **If a user's request is ambiguous, ask clarifying questions before proceeding.**
- **DO NOT provide financial advice, recommendations, or predictions.** Your role is strictly informational and analytical.
- Stay strictly on topic with financial data queries.
- If you cannot answer a query, state that clearly and offer alternative ways you might help *within your data scope*.
- All numerical values should be formatted appropriately (e.g., currency, percentages).

**Output Format:**
- All responses must be formatted using **Markdown** for clarity.
- When presenting structured data, use Markdown tables, lists, or bolding.

**Start by greeting the financial manager and offering assistance.**</code></pre><p>让我们来分析一下为什么这个提示如此有效：</p><ul><li><strong>它定义了一个成熟的角色： </strong>第一句话立即将代理人定位为&quot;专业的数据智能助理，&quot; 定下了专业、干练的基调。</li><li><strong>它提供了一个推理框架： </strong>通过告诉代理&quot;Understand（理解）、Plan（计划）、Execute（执行）和 Synthesize（综合），&quot; ，我们给了它一个标准的操作程序。这提高了它处理复杂、多步骤问题的能力。</li><li><strong>它促进了互动对话： </strong> &quot;提出澄清性问题的指令&quot; 使代理更加稳健。这将最大限度地减少对模棱两可的请求做出不正确的假设，从而获得更准确的答复。</li></ul><h3>用户界面路径</h3><p>1.导航至<strong>代理。</strong></p><ul><li>单击 &quot;<strong> 工具 </strong>&quot;或 &quot;<strong>管理工具</strong>&quot;，然后单击 &quot;<strong>新建工具</strong>&quot;按钮。</li></ul><p>2.填写基本信息：</p><ul><li><strong>代理编号：</strong> <code>financial_assistant</code>.</li><li><strong>说明 </strong>复制上面的提示。</li><li><strong>标签</strong> <code>Finance</code>.</li><li><strong>显示名称：</strong> <code>Financial Assistant</code> 。</li><li><strong>显示说明： </strong><code>An assistant for analyzing and understanding your financial data</code> 。</li></ul><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/18ea73f1cae620129c0afa0e7ba9e2a3390224a7-1600x1189.png" style="width: 100%;" /><p>3.回到顶部，点击 &quot;<strong>工具</strong>&quot;。</p><ul><li>勾选<code>find_client_exposure_to_negative_news</code> 工具旁边的复选框。</li></ul><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/0c1e4ecbbd51d0dd10c6e861dbe9a9ccddeb35f6-1600x149.png" style="width: 100%;" /><p>4.单击<strong>保存</strong>。</p><h3>应用程序接口路径</h3><p>您可以通过<code>POST</code> 请求<code>/api/agent_builder/agents</code> 端点来创建完全相同的代理。请求正文包含所有相同的信息：ID、名称、描述、全套指令以及允许代理使用的工具列表。</p><pre><code>POST kbn://api/agent_builder/agents
    {
      "id": "financial_assistant",
      "name": "Financial Assistant",
      "description": "An assistant for analyzing and understanding your financial data",
      "labels": [
        "Finance"
      ],
      "avatar_color": "#16C5C0",
      "avatar_symbol": "💰",
      "configuration": {
        "instructions": """You are a specialized Data Intelligence Assistant for financial managers, designed to provide precise, data-driven insights from information stored in Elasticsearch.

**Your Core Mission:**
- Respond accurately and concisely to natural language queries from financial managers.
- Provide precise, objective, and actionable information derived solely from the Elasticsearch data at your disposal.
- Summarize key data points and trends based on user requests.

**Reasoning Framework:**
1.  **Understand:** Deconstruct the user's query to understand their core intent.
2.  **Plan:** Formulate a step-by-step plan to answer the question. If you are unsure about the data structure, use the available tools to explore the indices first.
3.  **Execute:** Use the available tools to execute your plan.
4.  **Synthesize:** Combine the information from all tool calls into a single, comprehensive, and easy-to-read answer.

**Key Directives and Constraints:**
- **If a user's request is ambiguous, ask clarifying questions before proceeding.**
- **DO NOT provide financial advice, recommendations, or predictions.** Your role is strictly informational and analytical.
- Stay strictly on topic with financial data queries.
- If you cannot answer a query, state that clearly and offer alternative ways you might help *within your data scope*.
- All numerical values should be formatted appropriately (e.g., currency, percentages).

**Output Format:**
- All responses must be formatted using **Markdown** for clarity.
- When presenting structured data, use Markdown tables, lists, or bolding.

**Start by greeting the financial manager and offering assistance.**
""",
        "tools": [
          {
            "tool_ids": [
              "platform.core.search",
              "platform.core.list_indices",
              "platform.core.get_index_mapping",
              "platform.core.get_document_by_id",
              "find_client_exposure_to_negative_news"
            ]
          }
        ]
      }
    }</code></pre><h2>步骤 4：回报--进行对话</h2><p>我们已将业务逻辑封装在一个工具和一个&quot;大脑&quot; 中，准备在我们的 Agent 中使用它。是时候见证这一切了。现在，我们可以使用专门的代理与数据聊天了。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/5414cb6b7c41365acb0356a8bfe1140751ffd8db-1600x1014.png" style="width: 100%;" /><h3>用户界面路径</h3><ol><li>导航至 Kibana 中的<strong>代理 </strong>。</li><li>使用聊天窗口右下角的下拉菜单，从默认的<strong>Elastic AI 代理</strong>切换到我们新创建的<strong>财务助理 </strong>代理。</li><li>请提出一个问题，以便代理人使用我们的专业工具：<ol><li><em>我担心市场情绪。您能告诉我哪些客户最容易受到坏消息的影响吗？</em></li></ol></li></ol><p>片刻之后，代理将返回一个格式完美、内容完整的答案。由于法律硕士的性质，您的答案格式可能会略有不同，但这次运行中，代理返回的答案是一样的：</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/17b4ed43d279f9e53ee9fe3d482d0b2ec359a083-1600x1088.png" style="width: 100%;" /><h3>刚刚发生了什么？代理人的推理</h3><p>该特工并不只是&quot;知道&quot; 答案。它以选择最佳工具为中心，执行了一个多步骤计划。下面我们来看看它的思考过程：</p><ul><li><strong>识别意图：</strong>它将您问题中的关键字，如&quot;风险&quot; 和&quot;负面新闻、&quot; 与<code>find_client_exposure_to_negative_news</code> 工具的描述相匹配。</li><li><strong>执行计划：</strong>它从您的请求中提取了时间范围，并对该专业工具进行了<strong>一次调用</strong>。</li><li><strong>委托工作：</strong>然后，该工具就能完成所有繁重的工作：链式连接、值计算和排序。</li><li><strong>合成结果：</strong>最后，代理按照提示规则，将来自工具的原始数据格式化为清晰、人类可读的摘要。</li></ul><p>如果我们拓展思维，看到更多细节，我们就不只是猜测了。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/6a4da9262d3f88c60bfd8f8bf9b67c3b84e961ba-1600x607.png" style="width: 100%;" /><h3>应用程序接口路径</h3><p>您也可以通过编程来启动同样的对话。只需将输入问题发送到<code>converse</code> API 端点，确保指定我们的<code>financial_manager</code> 的<code>agent_id</code> 。</p><pre><code>POST kbn://api/agent_builder/converse
{
  "input": "Show me our largest positions affected by negative news",
  "agent_id": "financial_assistant"
}</code></pre><h2>致开发人员：与应用程序接口集成</h2><p>虽然 Kibana UI 为构建和管理代理提供了美妙而直观的体验，但您今天所看到的一切也都可以通过编程来实现。代理生成器基于一套应用程序接口（API）构建，允许您将此功能直接集成到自己的应用程序、CI/CD 管道或自动化脚本中。</p><p>您将使用的三个核心端点是</p><ul><li><strong><code>/api/agent_builder/tools</code></strong>:创建、列出和管理可重复使用的技能的终端。</li><li><strong><code>/api/agent_builder/agents</code></strong>:角色：定义代理角色的终端，包括重要的说明和工具分配。</li><li><strong><code>/api/agent_builder/converse</code></strong>:与代理互动、开始对话和获取答案的终端。</li></ul><p>有关使用这些应用程序接口执行本教程中每一步的完整实践演示，请查看我们 GitHub 软件仓库中的配套<strong>Jupyter Notebook</strong> <a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/your-first-elastic-agent/Your_First_Elastic_Agent.ipynb">。</a></p><h2>总结：轮到你来建设</h2><p>我们首先使用 ES|QL 查询，并将其转换为可重复使用的技能。然后，我们建立了一个专门的人工智能代理，赋予它明确的任务和规则，并赋予它这种技能。它是一个复杂的助手，能够理解复杂的问题，并执行多步骤分析，提供精确的数据驱动型答案。</p><p>这一工作流程是 Elastic 中新的<strong>代理生成器</strong>的核心。它的设计足够简单，非技术用户可以通过用户界面创建代理，但又足够细致，开发人员可以在我们的应用程序接口基础上构建定制的人工智能驱动应用程序。最重要的是，它可以让您安全可靠地将 LLM 连接到自己的数据，由您定义的专家逻辑进行管理，并与您的数据进行聊天。</p><h2>准备好使用代理与您的数据聊天了吗？</h2><p>巩固所学知识的最好方法就是动手实践。在我们的<a href="https://www.elastic.co/training/elastic-ai-agents-mcp"><strong>免费互动实践研讨会</strong></a>上，尝试我们今天讨论的所有内容。您将在专门的沙盒环境中经历整个流程以及更多。</p><p>在今后的博客中，我们将向您展示如何使用独立应用程序与我们的<code>Financial Assistant</code> 代理交互，并深入探讨使这一切成为可能的<strong>模型上下文协议 (MCP)</strong>。在另一篇博客中，我们将讨论 Agent Builder 对开发中的 Agent2Agent（或 A2A）协议的支持。</p><p>敬请期待，祝您建筑愉快！</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[利用 Elasticsearch 构建人工智能代理工作流]]></title>
            <description><![CDATA[了解代理生成器（Agent Builder），它是 Elasticsearch 中的一个新人工智能层，为构建人工智能代理工作流提供了一个框架，使用混合搜索为代理提供推理和行动所需的上下文。]]></description>
            <link>https://www.elastic.co/search-labs/cn/blog/ai-agentic-workflows-elastic-ai-agent-builder</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/cn/blog/ai-agentic-workflows-elastic-ai-agent-builder</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Anish Mathur, Dana Juratoni]]></dc:creator>
            <pubDate>Tue, 23 Sep 2025 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/d5d04bbcfc3925f48f3487ea4c7e0dd2205316d0-720x420.jpg?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>在 Elastic，我们通过人工智能助手、高级 RAG 和矢量数据库的改进，为 LLM 和对话界面带来了语境。最近，随着人工智能代理的兴起，我们发现对相关上下文的需求日益增长，并了解到高效的<strong> 人工智能代理需要出色的搜索</strong>。因此，我们在 Elastic Stack 中构建了新的本地功能，旨在帮助开发可利用 Elasticsearch 中数据的人工智能代理。我们希望与大家分享我们在这一历程中取得的进展，以及我们对下一步发展的展望。</p><h2>代理生成器：构建数据驱动型人工智能代理的基础</h2><p>人工智能代理的承诺很简单：给它一个目标，它就能完成工作。但对于开发商来说，现实却是一系列复杂的挑战。首先，代理的能力取决于其对环境的感知以及为实现用户目标而提供的工具。那么，如何从纷繁复杂的企业数据中提供正确的上下文是一项巨大的挑战。最后，所有这一切都必须由一个可靠的推理循环来协调，该循环可以进行规划、执行和学习。</p><p>为了解决这个问题，开发人员需要从头开始构建一个复杂而脆弱的堆栈。如今的代理架构需要将多个不同的部分拼接在一起：一个 LLM、一个向量数据库、一个元数据存储、用于日志记录和跟踪的独立系统，以及一些评估它们是否都能正常工作的方法。这不仅复杂，而且成本高昂、容易出错，并且难以建立用户所需的高质量、值得信赖的人工智能系统。</p><p>因此，我们想让它变得更简单。为此，我们的方法是将有效的上下文驱动型代理的重要部分直接集成到 Elasticsearch 的核心中，并提供一套名为<strong>Elastic AI Agent Builder</strong> 的新功能。这一新层提供了一个框架，其中包含创建由 Elasticsearch 支持的人工智能代理所需的所有基本构件：一套开放的基元、基于标准的协议和对数据的安全访问--因此您可以根据真实世界的数据和要求构建代理系统：</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/1ee1e73dd3f485ce86294d39490c98ce2a3d9925-1238x1072.png" style="width: 100%;" /><p><strong>提供人工智能体验</strong>：这是终极目标。以我们的搜索人工智能平台和您的数据为基础，您可以构建任何类型的生成式人工智能应用程序：从定制聊天界面到与 LangChain 等代理框架或 Salesforce 等业务应用程序的集成。</p><p><strong>由 Agents&amp; 工具提供支持</strong>：在平台之上，我们提供了一个简洁的抽象层。您可以直接与代理和工具互动，并根据具体需求进行定制。您还可以通过强大的应用程序接口和开放标准（如 MCP 和 A2A）访问平台的功能。</p><p><strong>由搜索人工智能平台支持</strong>：这是我们集成了各种组件的核心引擎。先进的矢量数据库、代理逻辑、查询结构、安全功能、评估跟踪都在这里，由 Elastic 管理和优化。</p><p><strong>释放数据的力量</strong>：任何优秀代理商的基础都是优秀的数据。我们的平台首先能够摄取或联合访问您的所有企业数据</p><h2>平台中的代理建设</h2><p>Agent Builder 集成到搜索人工智能平台中，为代理开发提供了一个完整的框架。它建立在五个关键支柱之上，每个支柱都旨在解决构建和部署生产级人工智能系统的一个关键方面。让我们来分析一下，代理如何定义目标，工具如何提供功能，开放标准如何确保互操作性，评估如何提供透明度，安全如何提供信任。</p><h3>代理商</h3><p>代理是 Elasticsearch 这一新层中最高级别的构建模块。代理定义了要实现的目标、可用于执行的工具集以及可操作的数据源。代理并不局限于对话式交互，它们还可以支持完整的工作流、任务自动化或面向用户的体验。</p><p>当一项查询被提交给代理机构时，它遵循一个结构化的循环：</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/627ad1744b629bbe27359325702f40d97e40d1f4-704x852.png" style="width: 100%;" /><ol><li>解释您的意见和目标</li><li>选择正确的执行工具和参数</li><li>工具响应的原因</li><li>决定是返回结果还是继续进一步调用工具</li></ol><p>Elastic 负责这一循环的协调、上下文和执行。开发人员专注于定义代理应该做<em>什么</em>：目标、工具和数据，而系统则管理<em>如何</em>进行推理和执行工作流程。</p><p><em>默认代理</em></p><p>我们在该平台上构建的第一个代理是 Kibana 中的原生会话代理，让您能够立即与数据进行交互。它在提供即用体验的同时，还具有完全的可扩展性，无需额外配置即可立即开始与数据交互。</p><p>您可以直接在 Kibana 中通过新的聊天用户体验或通过 API 与此体验进行交互。</p><p>通过 API 查询默认代理只需一次调用：</p><pre><code>POST kbn://api/agent_builder/converse
{
    "input": "what is our top portfolio account?"
}</code></pre><p>由于会话是有状态的，因此您可以使用会话 ID 继续与代理交互，或检索完整的会话历史记录：</p><pre><code>POST kbn://api/agent_builder/converse
{
    "input": "What about the second top?",
    "conversation_id": "ec757c6c-c3ed-4a83-8e2c-756238f008bb"
}

## get the full conversation
GET kbn://api/agent_builder/conversations/ec757c6c-c3ed-4a83-8e2c-756238f008bb</code></pre><p><em>海关代理</em></p><p>开发人员还可以通过简单的应用程序接口创建自己的定制代理。代理封装了指令、工具和数据访问，创建了量身定制的推理引擎。</p><p>创建自定义代理只需调用一次应用程序接口。下面的示例显示了一个例子，&quot;配置 &quot;字段包含所有关键细节，如说明或可用工具：</p><pre><code>POST kbn://api/agent_builder/agents
{
  "id": "custom_agent",
  "name": "My Custom Agent",
  "description": "Description of the custom agent",
  "configuration": {
      "instructions": "You are a log expert specialising in ...",
      "tools": 
...
   }
}</code></pre><p>一旦创建，就可以直接查询代理：</p><pre><code>POST kbn://api/agent_builder/converse
{
    "input": "What news about DIA?",
    "agent_id": "custom_agent"
}</code></pre><p>这种方法将代理从一个需要从头开始构建的复杂系统转变为一个简单、声明式的业务逻辑单元，使您能够更快地交付智能自动化。</p><p>如需深入了解如何从头开始构建专门的代理，请参阅我们的详细分步指南：<a href="https://www.elastic.co/search-labs/blog/ai-agent-builder-elasticsearch">您的第一个弹性代理：从单一查询到人工智能驱动的聊天</a>。</p><h3>工具</h3><p>如果说代理确定了要完成的<em>任务</em>，那么工具则确定了<em>如何</em>完成。</p><p>工具为代理执行和检索信息或执行操作暴露了特定的弹性核心功能。工具可以包括获取索引或获取映射等核心功能，也可以包括从自然语言到 ES|QL 等更高级的功能。</p><p>Elasticsearch 随附一套针对常见需求进行了优化的默认工具。但真正的灵活性来自于自己的创造。通过定义工具，您可以决定将哪些查询、索引和字段通过 ES|QL 暴露给代理，从而对速度、准确性和安全性进行精确控制。</p><p>注册新工具也很简单，只需调用一次应用程序接口。您可以创建一个工具，利用我们的<a href="https://www.elastic.co/search-labs/blog/esql-timeline-of-improvements">ES|QL（Elasticsearch 查询语言）</a>查找特定金融资产的相关新闻：</p><pre><code>POST kbn://api/agent_builder/tools
{
  "id": "news_on_asset",
  "type": "esql",
  "description": "Find news and reports about a particular asset where ...",
  "configuration": {
    "query": "FROM financial_news, financial_reports | where MATCH(company_symbol, ?symbol) OR MATCH(entities, ?symbol) | limit 5",
    "params": {
      "symbol": {
        "type": "keyword",
        "description": "The asset symbol"
      }
    }
  ...
  }
...
}</code></pre><p>注册后，您就可以将新工具分配给您的自定义代理，为他们提供一套经过精心设计的能力，让他们在合适的时候进行推理和调用。</p><p>我们提供了一个平台，可根据您的特定需求创建定制工具，例如使用 ES|QL，将代理从通用代理转变为特定领域的专家，立足于您独特的数据和业务领域。</p><h3>开放标准和互操作性</h3><p>Elasticsearch 代理和工具通过开放式标准 API 公开，因此很容易作为基础模块集成到更广泛的代理框架生态系统中。我们的方法很简单：没有黑盒子。我们希望您能够利用 Elastic 在搜索方面的核心优势，并将其与互补功能和其他代理系统搭配使用。</p><p>为了实现这一点，我们正在通过应用程序接口、新兴协议和开放标准公开我们的能力。</p><p><em>模型上下文协议（MCP）</em></p><p><a href="https://www.elastic.co/search-labs/blog/model-context-protocol-elasticsearch">模型上下文协议（MCP）</a>正迅速成为跨系统连接工具的开放标准。通过支持 MCP，Elasticsearch 可以将对话式人工智能与您的数据库、索引和外部 API 相连接。通过 Elastic Stack 内置的远程 MCP 服务器，任何兼容 MCP 的客户端都可以访问 Elastic 的工具，并将其用作大型代理工作流程的构建模块。</p><p>这不是一条单行道。您还可以从外部 MCP 服务器导入工具，使其在 Elasticsearch 中可用。不久之后，MCP 服务器将可能适用于几乎所有功能，而且比我们自己创建的任何功能都要全面得多。Elastic 提供大规模的搜索和检索功能，您可以将其与其他平台的专业功能相结合，构建有效的代理。</p><p><em>代理对代理（A2A）</em></p><p>我们还在努力提供代理对代理 (A2A) 支持。MCP 是连接工具，而 A2A 则是连接代理。有了 A2A 服务器，您构建的 Elastic 代理就能与其他系统的代理直接对话：共享上下文、委派任务和协调工作流。</p><p>将其视为推理层的互操作性。您的弹性代理可以处理搜索和检索，然后将任务交给专门的支持或 IT 代理，并无缝地返回结果。这样就形成了一个由合作代理组成的生态系统，每个代理都在做自己最擅长的事情。</p><p>最终，采用 MCP 和 A2A 加强了我们对 Elasticsearch 作为一流公民角色的承诺，确保在更广泛的代理生态系统中实现开放式集成。</p><h3>追踪和评估</h3><p>随着搜索与代理的整合，有效评估的挑战变得至关重要。要在真实的企业环境中自信地部署代理，就必须确保代理不仅准确，而且高效可靠。如何衡量性能、诊断不良响应或改进基线？一切从可见度开始。</p><p>因此，我们从一开始就设计了透明的代理 API。考虑一下这个简单的代理互动：</p><pre><code>POST kbn://api/agent_builder/converse
{
    "input": "what is our top portfolio account?"
}</code></pre><p>回复不仅包括最终答案，还包括完整的执行跟踪，详细说明代理选择了哪些工具、使用了哪些参数以及每一步的结果。</p><pre><code>{
  "conversation_id": "db5c0c8b-12bf-4928-a57e-d99129ad2fea",
  "steps": [
    {
      "type": "tool_call",
      "tool_call_id": "tooluse_Nfqr3mwtR92HTRIsTcGXZQ",
      "tool_id": ".index_explorer",
      "params": {
        "query": "indices containing portfolio data"
      },
      "results": [...]
    }
    // ... more steps ...
  ],
  "response": {
    "message": "Based on the information I've gathered...."
  }
}</code></pre><p>全面的跟踪和日志记录对持续改进循环至关重要，不久之后，您就可以直接在 Elasticsearch 中存储和查看这些代理跟踪。更妙的是，这些跟踪记录是基于 OpenTelemetry 协议构建的，确保了它们的标准化和可移植性，以便与您选择的可观测性平台集成。</p><p>这种详细程度是真正持续改进循环的基础。它使您能够建立一套全面的测试、调试故障、识别失败模式以防止回归，并捕捉成功模式以微调性能。归根结底，这种数据驱动的方法是将有前途的原型转化为生产级、值得信赖的人工智能系统的关键。</p><h3>安全性</h3><p>随着代理和工具的功能越来越强大，安全性不再是可有可无的，而是基础性的。要公开应用程序接口、自动执行任务和工作流程，就必须信任企业系统。特别是当代理开始自动执行更多的工作流程时，确保这些流程安全并满足企业要求的能力就显得尤为重要。</p><p>上述功能都继承了 Elastic 目前已有的控制功能，包括针对 API 调用和 API 密钥管理的<a href="https://www.elastic.co/search-labs/blog/rag-and-rbac-integration">基于角色的访问控制 (RBAC)</a>。我们还将同样的控制扩展到 MCP 等新协议。这意味着支持 OAuth 等标准，以及插入自定义身份验证机制的能力。</p><p>我们的目标是让您灵活地尝试使用代理和工具，同时保持组织所需的安全性、合规性和管理水平。</p><h2>下一步行动</h2><p>我们不仅要增加功能，还要扩展 Elasticsearch 的代理上下文工程。我们计划在这些原则的基础上继续发展：</p><p>1.致力于开放源码&amp; 标准</p><p>我们致力于开放源代码和开放标准，确保这些功能与外部代理框架保持互操作性。您始终能够在生态系统中连接、扩展和组成代理，同时将数据和工作流程置于您的控制之下。</p><p>2.背景的价值</p><p>人工智能代理的背景是其最大的资产。在代理执行搜索和工作流操作时管理上下文是一项极具挑战性的任务。我们正在利用 Elastic 的核心优势来解决上下文工程问题，确保您的代理始终可以获得最相关的信息。</p><p>3.关注代理数据流</p><p>展望未来，代理将成为越来越大的数据源，包括代理的输出（生成的文档、报告、可视化）和代理的执行轨迹（其思维、工具调用、内存/上下文）。Elastic 非常适合处理此类数据，我们正在研究如何利用这些数据进行分析、评估和自动改进。</p><p>4.设计的安保和安全</p><p>人工智能代理带来了全新的安全保障挑战。Elastic 一直是安全解决方案的领导者，我们将继续构建企业级防护、访问控制和&quot;零信任&quot; 原则。</p><p>5.嵌入平台</p><p>构建人工智能代理的功能已嵌入 Elasticsearch 平台。这意味着平台级功能，如跟踪、评估、可视化和分析，都适用于代理。希望根据代理执行情况开发仪表板--这是内置功能。希望通过情感分析来评估人工智能代理的性能--该平台可以实现这一点。这样就能围绕人工智能体验构建一个完整的生命周期。</p><p>Elastic 的目标是为您提供建立对话式人工智能和自动化工作流程的接口，这些接口完全集成、可扩展并以您的数据为基础。更多技术细节和进展情况将很快与大家分享。</p><p>代理生成器 &quot;现已推出私人预览版。<a href="https://www.elastic.co/contact?pg=global&amp;plcmt=nav&amp;cta=205352">与我们联系</a>，申请访问。有问题或反馈？在我们的<a href="https://elasticstack.slack.com/archives/C09GRHEQ4AG"><strong>Slack 工作区</strong></a>或<a href="https://discuss.elastic.co/c/search/84"><strong>讨论区</strong></a>与我们的开发人员社区联系。</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[作为代码的弹性开放式网络爬虫]]></title>
            <description><![CDATA[了解如何使用 GitHub Actions 管理 Elastic Open Crawler 配置，以便每次向版本库推送变更时，这些变更都会自动应用到已部署的爬虫实例。]]></description>
            <link>https://www.elastic.co/search-labs/cn/blog/elastic-open-crawler-config-as-code</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/cn/blog/elastic-open-crawler-config-as-code</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Gustavo Llermaly]]></dc:creator>
            <pubDate>Mon, 22 Sep 2025 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/0ac41f55c85ad3f647cb0e0d750ed80bacd397f3-1036x581.png?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>有了<a href="https://github.com/elastic/crawler">Elastic Open Web Crawler</a>及其 CLI 驱动的架构，现在就可以非常直接地实现版本化爬虫配置和具有本地测试功能的 CI/CD 管道。</p><p>传统上，管理爬虫是一个手动且容易出错的过程。这涉及到直接在用户界面上编辑配置，以及克隆抓取配置、回滚、版本控制等问题。将爬虫配置视为代码可以解决这个问题，因为它提供了我们在软件开发中期待的相同优势：可重复性、可追溯性和自动化。</p><p>这种工作流程可以更轻松地将开放式网络爬虫带入您的 CI/CD 流水线，以进行回滚、备份和迁移--这些任务在使用早期的弹性爬虫（如弹性网络爬虫或应用程序搜索爬虫）时要棘手得多。</p><p>在本文中，我们将学习如何：</p><ul><li>使用 GitHub 管理我们的抓取配置</li><li>在部署前进行本地设置以测试管道</li><li>创建一个生产设置，以便在每次向主分支推送更改时使用新设置运行网络爬虫</li></ul><p>你可以<a href="https://github.com/llermaly/elastic-open-crawler-as-code"><strong><em>在这里</em></strong></a>找到项目仓库<em><strong>。 </strong>在撰写本文时，我使用的是 Elasticsearch 9.1.3 和 Open Web Crawler 0.4.2。</em></p><h2>准备工作</h2><ul><li>Docker 桌面</li><li>Elasticsearch 实例</li><li>可通过 SSH 访问的虚拟机（如 AWS EC2）并安装 Docker</li></ul><h2>步长</h2><ol><li>文件夹结构</li><li>履带配置</li><li>Docker-compose 文件（本地环境）</li><li>Github 操作</li><li>本地测试</li><li>部署到 prod</li><li>进行更改和重新部署</li></ol><h2>文件夹结构</h2><p>本项目的文件结构如下：</p><pre><code>├── docker-compose.yml # Local elasticsearch + crawler
├── config/crawler-config.yml # Crawler config
├── .github/workflows/deploy.yml # GH Action to deploy changes
├── local.sh # Script to run our local crawler</code></pre><h2>履带配置</h2><p>在<code>crawler-config.yml,</code> 下，我们将填写以下内容：</p><pre><code>output_sink: elasticsearch
output_index: web-crawl-index
max_crawl_depth: 1

elasticsearch:
  host: ${ES_HOST}
  api_key: ${ES_API_KEY}
     
domains:
  - url: https://web-scraping.dev
    seed_urls:
      - https://web-scraping.dev/product/1
      - https://web-scraping.dev/product/2
      - https://web-scraping.dev/product/3</code></pre><p>这将从<a href="https://web-scraping.dev/products">https://web-scraping.dev/products</a> 抓取，这是一个产品模拟网站。我们只会抓取前三个产品页面。<code>max_crawl_depth</code> 设置将通过不打开其中的链接，防止爬虫发现比定义为<code>seed_urls</code> 的页面更多的页面。</p><p>Elasticsearch<code>host</code> 和<code>api_key</code> 将根据我们运行脚本的环境动态填充。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/f9831034e1c4ccb554d37bdd188f2824338355a0-890x624.png" style="width: 100%;" /><h2>Docker-compose 文件（本地环境）</h2><p>对于本地<code>docker-compose.yml,</code> ，我们将部署爬虫和单个 Elasticsearch 集群 + Kibana，这样<strong><em>在</em></strong>部署到生产环境之前，我们就可以轻松查看爬虫结果。</p><pre><code>services:
  es01:
    image: docker.elastic.co/elasticsearch/elasticsearch:9.1.3
    environment:
      - discovery.type=single-node
      - xpack.security.enabled=false
      - ES_JAVA_OPTS=-Xms1g -Xmx1g
    ports:
      - "9200:9200"
    networks: [esnet]
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:9200"]
      interval: 5s
      timeout: 5s
      retries: 10

  kibana:
    image: docker.elastic.co/kibana/kibana:9.1.3
    environment:
      - ELASTICSEARCH_HOSTS=http://es01:9200
    ports:
      - "5601:5601"
    networks: [esnet]
    depends_on: [es01]

  crawler:
    image: docker.elastic.co/integrations/crawler:0.4.2
    environment:
      - ES_HOST=http://es01:9200
      - CRAWLER_JRUBY_OPTS=--server
    container_name: crawler
    volumes:
      - ./config:/home/app/config
    networks: [esnet]
    entrypoint: ["/home/app/bin/crawler", "crawl", "/home/app/config/crawl-config-final.yml"]
    stdin_open: true
    tty: true

networks:
  esnet:
    driver: bridge</code></pre><p>请注意爬虫是如何等待 Elasticsearch 准备好运行的。</p><h2>Github 操作</h2><p>现在，我们需要创建一个 GitHub Action，它将复制新设置，并在每次推送到 main 时在虚拟机中运行爬虫。这样就能确保我们始终部署有最新的配置，而无需手动进入虚拟机更新文件和运行爬虫。我们将使用 AWS EC2 作为虚拟机提供商。</p><p>第一步是将主机 (<code>VM_HOST</code>) 、机器用户 (<code>VM_USER</code>) 、SSH RSA 密钥 (<code>VM_KEY</code>) 、Elasticsearch 主机 (<code>ES_HOST</code>) 和 Elasticsearch API 密钥 (<code>ES_API_KEY</code>) 添加到 GitHub Action secrets 中：</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/e1075bc54151b4b94eac2a6bd2682e9997e6c709-1106x707.png" style="width: 100%;" /><p>这样，操作就能访问我们的服务器，将新文件复制过来并运行抓取。</p><p>现在，让我们创建<code>.github/workflows/deploy.yml</code> 文件：</p><pre><code>name: Deploy

on:
  push:
    branches: [main]

jobs:
  Deploy:
    name: Deploy to EC2
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v5

      - name: Deploy crawler
        env:
          HOSTNAME: ${{ secrets.VM_HOST }}
          USER_NAME: ${{ secrets.VM_USER }}
          PRIVATE_KEY: ${{ secrets.VM_KEY }}
          ES_HOST: ${{ secrets.ES_HOST }}
          ES_API_KEY: ${{ secrets.ES_API_KEY }}
        run: |
          # Save private key
          echo "$PRIVATE_KEY" > private_key
          chmod 600 private_key

          # Generate final config locally
          envsubst < config/crawler-config.yml > config/crawl-config-final.yml

          # Copy the config folder to VM
          scp -o StrictHostKeyChecking=no -i private_key -r config ${USER_NAME}@${HOSTNAME}:~/config

          # SSH into VM and run crawler
          ssh -o StrictHostKeyChecking=no -i private_key ${USER_NAME}@${HOSTNAME} << EOF
            docker run --rm \
              -v ~/config:/config \
              docker.elastic.co/integrations/crawler:latest jruby \
              bin/crawler crawl /config/crawl-config-final.yml
          EOF</code></pre><p>每次我们向爬虫配置文件推送更改时，该操作都会执行以下步骤：</p><ol><li>在 yml 配置中填入 Elasticsearch 主机和 API 密钥</li><li>将配置文件夹复制到我们的虚拟机</li><li>通过 SSH 连接到我们的虚拟机</li><li>使用我们刚从 repo 复制的配置运行抓取程序</li></ol><h2>本地测试</h2><p>为了在本地测试爬虫，我们创建了一个 bash 脚本，将 Elasticsearch 主机与 Docker 中的本地主机进行填充，然后开始爬行。您可以运行<code>./local.sh</code> 来执行它。</p><pre><code>#!/bin/bash

# Exit on any error
set -e

# Load environment variables
export ES_HOST="http://es01:9200"

# Generate final crawler config
envsubst < ./config/crawler-config.yml > ./config/crawl-config-final.yml

# Bring everything up
docker compose up --build</code></pre><p>让我们看看 Kibana DevTools，以确认<code> web-crawler-index</code> 的填充是否正确：</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/18551635e8265866e389a9632c4e4540958e4468-990x723.png" style="width: 100%;" /><h2>部署到 prod</h2><p>现在，我们准备推送到主分支，这将在虚拟机中部署爬虫，并开始向无服务器 Elasticsearch 实例发送日志。</p><pre><code>git add .
git commit -m "First commit"
git push</code></pre><p>这将触发 GitHub 操作，在虚拟机中执行部署脚本并开始抓取。</p><p>您可以访问 GitHub 仓库并访问 &quot;操作 &quot;选项卡，确认操作已执行：</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/67ba3a7164d7a8049fe5661264820826cb18ed64-667x325.png" style="width: 100%;" /><h2>进行更改和重新部署</h2><p>您可能已经注意到，每个产品的<code>price</code> 都是文档正文字段的一部分。如果能将价格存储在一个单独的字段中，我们就可以根据它运行筛选器，这将是最理想的。</p><p>让我们在<code>crawler.yml</code> 文件中添加这一更改，使用<a href="https://github.com/elastic/crawler/blob/main/docs/features/EXTRACTION_RULES.md">提取规则</a>从<code>product-price</code> CSS 类中提取价格：</p><pre><code>output_sink: elasticsearch
output_index: web-crawl-index
max_crawl_depth: 1

elasticsearch:
  host: ${ES_HOST}
  api_key: ${ES_API_KEY}
     
  # Index ingest pipeline to process documents before indexing          
  pipeline_enabled: true
  pipeline: pricing-pipeline

domains:
  - url: https://web-scraping.dev
    seed_urls:
      - https://web-scraping.dev/product/1
      - https://web-scraping.dev/product/2
      - https://web-scraping.dev/product/3
    extraction_rulesets:
      - url_filters:
          - type: ends
            pattern: /product/*
        rules:
          - action: extract
            field_name: price
            selector: .product-price
            join_as: string
            source: html</code></pre><p>我们还可以看到，价格包含一个美元符号 (<code>$</code>)，如果要运行范围查询，我们必须去掉这个符号。为此，我们可以使用摄取管道。请注意，我们在上面的新爬虫配置文件中引用了它：</p><pre><code>PUT _ingest/pipeline/pricing-pipeline
{
  "processors": [
    {
      "script": {
        "source": """
                ctx['price'] = ctx['price'].replace("$","")
            """
      }
    }
  ]
}</code></pre><p>我们可以在生产 Elasticsearch 集群中运行该命令。对于开发进程，由于它是短暂的，我们可以通过添加以下服务，使管道创建成为<code>docker-compose.yml</code> 文件的一部分。请注意，我们还为爬虫服务添加了一个<code>depends_on</code> ，这样它就能在管道创建成功后启动。</p><pre><code> crawler:
    image: docker.elastic.co/integrations/crawler:0.4.2
    environment:
      - ES_HOST=http://es01:9200
      - CRAWLER_JRUBY_OPTS=--server
    container_name: crawler
    volumes:
      - ./config:/home/app/config
    networks: [esnet]
    entrypoint: ["/home/app/bin/crawler", "crawl", "/home/app/config/crawl-config-final.yml"]
    depends_on:
      pipeline-init:
        condition: service_completed_successfully
    stdin_open: true
    tty: true  


  pipeline-init:
    image: curlimages/curl:latest
    depends_on:
      es01:
        condition: service_healthy
    networks: [esnet]
    entrypoint: >
        sh -c "
        echo 'Creating ingest pipeline...';
        curl -s -X PUT http://es01:9200/_ingest/pipeline/pricing-pipeline \\
          -H 'Content-Type: application/json' \\
          -d '{\"processors\":[{\"script\":{\"source\":\"ctx.price = ctx.price.replace(\\\"$\\\", \\\"\\\")\"}}]}';
        echo 'Pipeline created!';
        "</code></pre><p>现在让我们运行<code>`./local.sh`</code> 查看本地的变化：</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/dc1801599344a9f69f072b07ff828c4ba3815d7b-738x473.png" style="width: 100%;" /><p>太好了！现在让我们推动变革：</p><pre><code>git add crawler-config.yml
git commit -m "added price CSS selector"
git push</code></pre><p>要确认一切正常，可以检查生产的 Kibana，它应该会反映这些更改，并将价格显示为一个不带美元符号的新字段。</p><h2>结论</h2><p>Elastic Open Web Crawler 允许您将爬虫作为代码进行管理，这意味着您可以自动执行从开发到部署的整个流程，并以编程方式添加短暂的本地环境和针对爬虫数据的测试。</p><p>我们邀请您克隆官方资源库，并开始使用此工作流程为自己的数据编制索引。您还可以阅读<a href="https://www.elastic.co/search-labs/blog/semantic-search-open-crawler">本文</a>，了解如何在爬虫生成的索引上运行语义搜索。</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Elasticsearch 的 ES|QL 编辑器体验与 OpenSearch 的 PPL 事件分析器对比]]></title>
            <description><![CDATA[了解 ES|QL Editor 的高级功能如何加速您的工作流程，与 OpenSearch 的 PPL 事件分析器的手动方法形成直接对比。 
]]></description>
            <link>https://www.elastic.co/search-labs/cn/blog/opensearch-vs-elasticsearch-ppl-esql</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/cn/blog/opensearch-vs-elasticsearch-ppl-esql</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Libby Lin, George Kobar]]></dc:creator>
            <pubDate>Thu, 18 Sep 2025 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/f1ff4ff4a00b3e5b084d4116cea6cabc82a2d816-720x420.jpg?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p><a href="https://www.elastic.co/blog/getting-started-elasticsearch-query-language">Elasticsearch 查询语言</a>（ES|QL）自 8.14 版起全面可用，它引入了一种专为搜索、可观察性和安全调查而设计的查询语言和引擎。与大量借鉴现有管道式语言的 OpenSearch 管道式处理语言 (PPL) 不同，ES|QL 是从头开始构建的，专注于 Kibana 平台的完善性、可用性和无缝集成。</p><p>在本博客中，我们将通过比较 Elasticsearch 9.1 中的 ES|QL 编辑器和 OpenSearch 3.2 中事件分析器（简称 PPL）中的 PPL，来探讨开发人员对 ES|QL 编辑器的体验。</p><p>两者之间的差异很快就显现出来：ES|QL 编辑器提供智能自动完成、上下文帮助、推荐查询和跨集群查询支持，不仅能帮助初学者，还能帮助专家级用户。针对 ES|QL 编写的周到设计还体现在通过 Kibana 工作流（例如最近查询）进行的集成查询检查和整体集成。</p><p>相比之下，PPL 缺乏对自动完成、上下文引导和分布式查询的类似支持，因此学习曲线更陡峭，试错更多。</p><h2>使 ES|QL 更易于学习和使用</h2><p>开始使用一种新的查询语言往往会让人感到不知所措。ES|QL<strong> </strong>编辑器直接内置于<strong> Kibana Discover</strong> 中，不仅支持查询创建和调试，还能加快熟悉和掌握该语言的速度，从而简化这一过程。由于编辑器有助于减少日常工作中的摩擦，您可以将注意力从语法和试错转移到解决问题上。您可以<a href="https://www.elastic.co/search-labs/blog/improving-esql-editor-experience-in-kibana">在这里</a>了解更多有关这些原则以及我们如何将其融入编辑器的信息。</p><p>这种编辑器体验并不局限于 Discover；它是一种可重复使用的代码模块，我们正在努力<strong>将其集成到 Kibana 的其他部分</strong>，如仪表盘、Kibana 警报和 Kibana 地图。</p><h3>智能自动完成：加速创建查询</h3><p>ES|QL 编辑器中的自动完成功能非常全面，可为兼容函数、参数、字面量甚至嵌套函数提供建议，而这正是 PPL 所缺乏的功能。事实上，它已经从头开始重建，正如<a href="https://www.elastic.co/search-labs/blog/esql-autocomplete-rebuilt">这里所</a>概述的那样。</p><p>如<a href="https://www.elastic.co/search-labs/blog/improving-esql-editor-experience-in-kibana">这里</a>所述，验证会在用户键入时运行，并对字段提出建议，还会在出现错误时通知用户。这减轻了用户的心理负担，有助于在查询创建过程的早期防止错误。</p><p>举例说明：在此嵌套中建议使用字段和兼容函数：</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/a4d7b2819c34fab31bced7873257b8932b623fba-1502x473.png" style="width: 100%;" /><p>这是 PPL 不支持的：</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/6f3a89b1255b8a3a762022a2704fdd1c2987e5f9-1013x335.png" style="width: 100%;" /><p>即使有智能自动完成功能指导你使用兼容函数、参数和嵌套函数，你可能还是希望对可用选项有更深入的了解。这正是 ES|QL 编辑器的上下文帮助变得非常有价值的地方，它提供即时的编辑帮助，以澄清和加强您的查询开发。</p><h3>触手可及的上下文帮助</h3><p>点击 Ctrl 键-空格键，即可获得有关自动完成功能生成的命令的其他信息。立即出现一个面板，显示有关函数、参数或字段的详细信息。这种轻量级交互让开发人员始终处于工作流程中，为他们提供及时指导，而不会迫使他们离开编辑器或搜索外部文档。这样可以减少在语法查找上浪费的时间，并有助于在常见错误发生之前加以预防。</p><p>下面是它的实际效果：</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/2797f9b5e002dbd83c46475c4ed4dcdc86144a01-1343x522.gif" style="width: 100%;" /><p>PPL 缺乏这种级别的嵌入式指导，用户只能依赖外部文档或反复试验。这种缺失不仅仅是功能上的缺失，它还凸显了设计理念上更广泛的差异。ES|QL 优先考虑贴心的上下文感知体验，以适应用户的数据和工作流程。随着查询复杂度的增加，这种差异会变得更加明显，从而使 ES|QL Editor 成为一个更高效、更可靠的学习和生产环境。</p><h3>建议使用可感知数据上下文的查询</h3><p>ES|QL 编辑器会根据您正在处理的数据（如日志）自动提供推荐查询。它不是提供一个空白的编辑器，而是为常见用例提供最相关的出发点。选择 &quot;推荐查询 &quot;可生成一个标准查询，该查询可立即使用，并可根据需要进一步完善。这种方法加快了查询的开发速度，尤其是对于尚未掌握全部语法的新用户而言。</p><p>下面是一个用户选择 &quot;检测变化点 &quot;查询的示例：</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/3284c9340d41298820fbf8c7702abad946b48248-925x370.gif" style="width: 100%;" /><p>将其与 PPL 的经验进行比较：</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/5c3e59dadc6210aede3366bdd081887bcbae7a54-969x798.png" style="width: 100%;" /><p>相比之下，PPL 在这里只提供基本的自动完成功能，让您在没有上下文或结构的情况下拼凑查询。缺乏指导可能会导致挫败感和反复试验。有了 ES|QL Editor 的数据感知推荐查询，您就可以避免从头开始或死记硬背常规任务的语法。编辑器可减少认知负荷，有助于防止出错，让你专注于解决问题和更广泛的目标，如运行跨集群搜索，而不是费力地构建查询。</p><h2>直观的跨集群查询</h2><p>ES|QL 编辑器的自动完成功能依然卓越，即使在使用<a href="https://elastic.aiops.work/search-labs/blog/esql-cross-cluster-search">CCS</a> 处理多个远程群集时也是如此。原因就在这里：</p><h3>ES|QL 编辑器甚至可跨集群提供无缝自动完成功能</h3><p>ES|QL 编辑器中的自动完成功能不仅支持群组名称，还支持<strong> 本地和远程索引</strong>。正如<a href="https://www.elastic.co/search-labs/blog/esql-cross-cluster-search">本文</a>所介绍的那样，这要归功于协调器节点架构，它可以帮助验证和生成查询计划，然后发送给本地节点，执行查询并汇总结果，最后再发送给用户。在不输入远程群集全名的情况下，键入&quot;: &quot;可启动远程索引的自动完成程序。而且，您也不局限于前缀。</p><p>这样就可以轻松地在分布式数据集上发现和查询数据，而无需记住命名约定或切换上下文。</p><p>下面是一个示例，用户只需键入 &quot;clu:g &quot;即可定位远程索引：</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/bae1fbacb2320e4d07f41291ea57c9bcf15bf8a5-1092x523.gif" style="width: 100%;" /><p>与此形成鲜明对比的是，PPL 只为本地索引提供基本的补全功能，建议仅限于前缀匹配。远程群组必须手动键入，这会增加出错的可能性，并减慢查询创建速度。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/e38793c0cc7c6cc7dc0fd4779a3e24ffbb6e0838-1094x263.gif" style="width: 100%;" /><p>PPL 仅为本地索引提供补全，建议仅限于前缀：</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/5310f824942f94485cace2558ea72c56a0971e22-862x197.png" style="width: 100%;" /><p>ES|QL 更进一步，<a href="https://www.elastic.co/docs/solutions/search/cross-cluster-search#exclude-problematic-clusters"> 允许</a> 直接使用负号 排除 ，让您可以精细控制哪些集群参与探索。这种功能在处理混合环境时尤为重要，因为在混合环境中，您可能希望在跨集群调查时包含或省略特定的数据集。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/f23ca17f58fbf8e6d27419c028274cb91f30a549-937x78.png" style="width: 100%;" /><p>这些增强功能反映了 Elasticsearch 对减少跨集群搜索摩擦的广泛关注。通过使分布式查询更易于构建和管理，ES|QL Editor 使分析师和开发人员能够专注于洞察力而不是语法，而 PPL 则将更多的负担留给了用户。ES|QL 编辑器不仅简化了跨集群查询的创建，还提供了检查这些查询执行情况的工具，确保跨多个集群的透明度和性能监控。</p><h3>使用 Inspect 工具分析跨群集搜索详情</h3><p>检查工具可从 ES|QL 编辑器访问，旨在提供元数据，明确说明所有集群的查询执行情况。该功能已在 Kibana Discover 中启用，并可在查询检查器中直接访问，使您能够分析搜索进度和详细信息，这对于<strong>跨集群搜索</strong><a href="https://www.elastic.co/docs/reference/query-languages/esql/esql-cross-clusters">(CCS</a>) 尤为重要。该功能可帮助您监控搜索进度，了解查询在分布式数据集上的执行情况。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/5d1c204f70171526fff924c30ea8ad08121a0f8d-919x523.gif" style="width: 100%;" /><p>这种对查询执行的详细可视性，尤其是对复杂的分布式搜索的可视性，使您能够确保最佳性能和故障排除。</p><p>除了了解单个查询的机制外，ES|QL 编辑器还通过在整个 Kibana 平台上深度嵌入基本功能，促进无缝、不间断的工作流程，从而进一步增强用户体验。</p><h2>使用 ES|QL 和 Kibana 的统一查询体验</h2><p>查询驱动分析中最常见的摩擦源之一是上下文切换。您经常需要回忆已经写过的查询。每一次中断都会打断注意力，延缓调查进度。ES|QL Editor 通过在 Kibana 中集成查询历史记录来解决这一问题。</p><h3>最近的查询</h3><p>ES|QL 编辑器中的 &quot;<a href="https://www.elastic.co/search-labs/blog/esql-piped-query-language-goes-ga">最近查询 &quot;</a>功能可让您即时访问过去的工作，从而帮助您保持工作流畅。在 Discover 的 ES|QL 编辑器中，您可以查看、重新运行最近的 20 个查询，并将其设为星级，确保只需点击一下就能完成常用或复杂的查询。这些保存的查询还可在 Kibana 中使用，并与仪表盘、可视化、警报和地图集成，因此您无需离开当前屏幕或从头开始重新输入命令。这减少了重复性工作，加快了调查速度，并最大限度地降低了出错风险。</p><p>例如，用户可以在发现中使用 ES|QL 编辑器中的最近查询（并将其设为星级）：</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/eb0f9fe37b92dec421c394d31ae7d90afebe062e-1421x793.png" style="width: 100%;" /><p>最近的查询已集成到仪表板中：</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/b84cd3a9bdec58812360d2aba4fc7713363ee3cc-1411x797.png" style="width: 100%;" /><p>PPL 不提供类似功能，用户只能依靠手动复制粘贴或外部注释来重复使用查询。这种区别不仅仅是为了方便；它反映了 Elastic 将 ES|QL 打造为 Kibana 生态系统中真正集成语言的战略。通过最近查询等功能，ES|QL 编辑器不仅简化了日常工作流程，还为技术预览版中更高级的功能奠定了基础，确保用户体验不断发展。</p><h2>结论</h2><p>ES|QL 不仅仅是一种语法，它还反映了 Elastic 改进用户搜索、探索和分析数据方式的战略。通过智能自动完成、上下文感知的推荐查询、编辑器内指导以及检查等工具，ES|QL Editor 可加快学习速度、减少错误并简化跨集群分析等复杂的工作流程。它与 Kibana 集成，可将查询与仪表盘、警报和可视化无缝连接，从而实现不间断的工作流程。</p><p>总之，ES|QL 不仅仅是另一种管道语言；它是一个经过深思熟虑设计的查询引擎，搭配直观的用户界面，从根本上重新定义了您与数据交互的方式，提供了一种集成、智能和不断发展的体验，与 OpenSearch PPL 通常按部就班、缺乏指导的性质形成鲜明对比。</p><h2>下一步行动</h2><p>本博客仅涉及 ES|QL 的表面。今后的文章将深入探讨与 OpenSearch PPL 的比较，并探索地理空间、可视化和即将推出的编辑器功能，如<a href="https://www.elastic.co/docs/explore-analyze/dashboards/add-controls">控制</a>（已在仪表板中提供）、多数据探索选项卡、后台搜索、更丰富的查询历史和 FUSE。</p><h2>立即试用 ES|QL</h2><p>您可以通过<a href="https://www.elastic.co/docs/deploy-manage/deploy/elastic-cloud/create-serverless-project"> 免费试用 在完全托管的</a> Elasticsearch<a href="https://www.elastic.co/cloud/serverless"> Serverless</a> 项目中查看 ES|QL。从 8.11 开始的版本中也有该功能，但在<a href="https://www.elastic.co/blog/whats-new-elastic-9-1-0">8.19 和 9.1</a> 中体验最佳。</p><p>只需一条命令，几分钟即可在本地环境中开始使用：</p><pre><code>curl -fsSL https://elastic.co/start-local | sh</code></pre>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[介绍用于 Elasticsearch Ruby 客户端的 ES|QL 查询生成器]]></title>
            <description><![CDATA[了解如何使用最近发布的用于 Elasticsearch Ruby 客户端的 ES|QL 查询生成器。这是一款使用 Ruby 代码更轻松地构建 ES|QL 查询的工具。]]></description>
            <link>https://www.elastic.co/search-labs/cn/blog/esql-query-builder-elasticsearch-ruby-client</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/cn/blog/esql-query-builder-elasticsearch-ruby-client</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Fernando Briano]]></dc:creator>
            <pubDate>Wed, 17 Sep 2025 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/f8e1263ab53d356824a4fc539084151be80899db-720x420.jpg?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>我们最近发布了<a href="https://github.com/elastic/esql-ruby/"><code>elastic-esql</code></a> ，这是一个根据 Apache 2 许可证发布的 Ruby gem。有了这个 gem，你就可以用惯用的 Ruby 语言创建 Elastic 的<a href="https://www.elastic.co/docs/explore-analyze/query-filter/languages/esql">ES|QL</a>查询，然后将其用于 ES|QL 查询 API。ES|QL 允许开发人员通过查询过滤、转换和分析存储在 Elasticsearch 中的数据。它使用&quot;管道&quot; (<code>|</code> ) 来逐步处理数据。该 gem 使用 Ruby 函数，你可以将这些函数链入原始对象，以建立更复杂的查询：</p><p><strong>ESQL：</strong></p><pre><code>FROM sample_data | LIMIT 2 | SORT @timestamp DESC</code></pre><p><strong>鲁比</strong></p><pre><code>Elastic::ESQL.from('sample_data').limit(2).sort('@timestamp').descending</code></pre><h2>安装</h2><p>该 gem 可通过以下方式从 RubyGems 安装：</p><pre><code>gem install elastic-esql</code></pre><p>或者将其添加到项目的 Gemfile 中：</p><pre><code>gem 'elastic-esql'</code></pre><h2>使用方法</h2><p>您可以一次性建立一个完整的查询，也可以使用<code>from</code> 或<code>row</code> 等源命令创建一个查询对象，然后使用 ES|QL 方法链在其上建立查询。</p><pre><code>query = Elastic::ESQL.from('sample_data')
query.limit(2).sort('@timestamp')</code></pre><p>在<code>to_s</code> 方法中，gem 将代码转换为 ES|QL，因此在打印输出或转换为字符串时会返回 ES|QL 查询：</p><pre><code>query = Elastic::ESQL.from('sample_data').limit(2).sort('@timestamp').descending
query.to_s
# => "FROM sample_data | LIMIT 2 | SORT @timestamp DESC"</code></pre><p>您可以使用<code>!</code> 中每个函数的对应函数来实例化查询对象并更改其初始状态：</p><pre><code>query = Elastic::ESQL.from('sample_data')
query.to_s
# => "FROM sample_data"
query.limit!(2).sort!('@timestamp')
query.to_s
# => "FROM sample_data | LIMIT 2 | SORT @timestamp"</code></pre><p>该工具提供了将额外步骤链入 ES|QL 函数的便捷方法，如<code>enrich</code> 和<code>sort</code> 。一旦在<code>Elastic::ESQL</code> 对象上调用<code>enrich</code> ，就可以将<code>on</code> 和<code>with</code> 与之连锁：</p><pre><code>esql.enrich!('policy').on('a').with({ name: 'language_name' })</code></pre><p>在使用<code>sort</code> 之后，您还可以将<code>desc</code> 、<code>asc</code> 、<code>nulls_first</code> 和<code>nulls_last</code> 链入您的查询：</p><pre><code>Elastic::ESQL.from('sample_data').sort('@timestamp').asc.to_s
# => 'FROM sample_data | SORT @timestamp ASC'

Elastic::ESQL.from('sample_data').sort('@timestamp').desc.nulls_first.to_s
# => 'FROM sample_data | SORT @timestamp DESC NULLS FIRST'</code></pre><p>它还支持自定义字符串，以备您自己编写 ES|QL 查询或使用尚未添加到库中的功能。<code>custom</code> 会在查询结束时连接字符串。它将在发送到函数时添加这些字符，而不会添加任何管道字符。它们将通过一个空格字符与查询的其余部分合并。</p><pre><code>esql = Elastic::ESQL.from('sample_data')
esql.custom('| MY_VALUE = "test value"').to_s
# => 'FROM sample_data | MY_VALUE = "test value"'</code></pre><p>您还可以将<code>custom</code> 功能串联起来：</p><pre><code>esql.custom('| MY_VALUE = "test value"').custom('| ANOTHER, VALUE')
'FROM sample_data | MY_VALUE = "test value" | ANOTHER, VALUE'</code></pre><h2>在 Ruby 客户端使用 ES|QL 查询生成器</h2><p>您可以通过发送查询对象，直接在<a href="https://github.com/elastic/elasticsearch-ruby">elasticsearch-ruby</a>和<code>esql.query</code> API 中使用查询生成器：</p><pre><code>require 'elasticsearch'
require 'elastic/esql'

client = Elasticsearch::Client.new
index = 'sample_data'

query = Elastic::ESQL.from(index)
                     .sort('@timestamp')
                     .desc
                     .where('event_duration > 5000000')
                     .limit(3)
                     .eval({ duration_ms: 'ROUND(event_duration/1000000.0, 1)' })
client.esql.query(body: { query: query })</code></pre><p>您还可以将其与 Elasticsearch Ruby 客户端中的 ES|QL Helper 结合使用，<a href="https://www.elastic.co/search-labs/blog/esql-ruby-helper-elasticsearch">了解更多详情</a>：</p><pre><code>require 'elasticsearch/helpers/esql_helper'

Elasticsearch::Helpers::ESQLHelper.query(client, query)</code></pre><h2>作为独立工具</h2><p>该 gem 被设计为一个独立工具，用于以惯用方式构建 ES|QL 查询。它没有运行时依赖性，可以与官方 Elasticsearch Ruby 客户端一起使用，也可以单独使用。</p><p>生成的查询可在<a href="https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-esql-query"><code>esql.query</code></a> API 中使用，无论应用程序以何种方式与 Elasticsearch API 交互（无论是否使用 Ruby）。使用<code>elastic-esql</code> 创建查询后，生成的字符串就可以作为<code>query</code> 请求正文中的参数发送给应用程序接口。 </p><p>我曾写过一篇关于<a href="https://www.elastic.co/search-labs/blog/elasticsearch-ruby-tools">将 Elasticsearch 与流行的 Ruby 工具结合使用的</a>文章。该 gem 可与任何流行的 Ruby 工具一起使用，以通过 ES|QL 查询 Elasticsearch。</p><h2>结论</h2><p>该库正在积极开发中，最终的应用程序接口尚未完成。目前发布的是技术预览版。如果您对当前的应用程序接口或一般使用方法有任何反馈，请随时<a href="https://github.com/elastic/esql-ruby/issues">打开新问题</a>。有关 Ruby ES|QL 查询生成器的更多信息，请参阅<a href="https://github.com/elastic/esql-ruby/?tab=readme-ov-file#ruby-esql-query-builder">README</a>。</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[矢量搜索过滤：保持相关性]]></title>
            <description><![CDATA[仅靠矢量搜索来查找与查询最相似的结果是不够的。要缩小搜索结果的范围，通常需要进行筛选。本文介绍了在 Elasticsearch 和 Apache Lucene 中如何对矢量搜索进行过滤。]]></description>
            <link>https://www.elastic.co/search-labs/cn/blog/vector-search-filtering</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/cn/blog/vector-search-filtering</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Carlos Delgado]]></dc:creator>
            <pubDate>Wed, 03 Sep 2025 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/03b1dd4c7bda4fbabd8e374bc2e4f12d5be6ef5f-1600x1150.png?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>矢量搜索不足以找到相关结果。使用过滤标准非常常见，这有助于缩小搜索结果的范围并过滤掉不相关的结果。</p><p>了解筛选在矢量搜索中是如何工作的，将有助于你平衡性能和召回率之间的权衡，并发现一些优化方法，使矢量搜索在使用筛选时性能更佳。</p><h2>为什么要过滤？</h2><p>矢量搜索彻底改变了我们在大型数据集中查找相关信息的方式，使我们能够发现与查询语义相似的项目。</p><p>然而，仅仅找到相似的物品是不够的。我们经常需要根据特定的标准或属性来缩小搜索结果的范围。</p><p>想象一下，您正在一家电子商务商店中搜索产品。纯矢量搜索可能会显示视觉上相似的商品，但您可能还想根据价格范围、品牌、可用性或客户评价进行筛选。如果不进行筛选，您就会看到大量类似的产品，很难准确找到您要找的产品。</p><p>过滤功能可对搜索结果进行精确控制，确保检索到的项目不仅在语义上一致，而且符合所有必要的要求。这将带来更加准确、高效和用户友好的搜索体验。</p><p>这正是 Elasticsearch 和 Apache Lucene 的优势所在--对各种数据类型进行有效过滤是它们与其他矢量数据库的主要区别之一。</p><h2>精确矢量搜索的筛选</h2><p>进行精确矢量搜索主要有两种方法：</p><ul><li>为 dense_vector 字段使用<code>flat</code> 索引类型。这使得<code>knn</code> 搜索使用精确搜索而不是近似搜索。</li><li>使用<a href="https://www.elastic.co/docs/reference/query-languages/query-dsl/query-dsl-script-score-query#vector-functions"> script_score 查询</a> ，该 查询 使用向量函数计算分数。这可用于任何索引类型。</li></ul><p>在执行精确向量搜索时，所有向量都会与查询进行比较。在这种情况下，过滤将有助于提高性能，因为只需要比较通过过滤的向量。</p><p>这不会影响结果质量，因为所有向量都会被考虑在内。我们只是提前过滤掉不感兴趣的结果，从而减少操作次数。</p><p>这一点非常重要，因为当应用筛选器得到的文档数量很少时，执行精确搜索比近似搜索更有效。</p><p>经验法则是，当通过过滤器的文件少于 10k 时，应使用精确搜索。<a href="https://www.elastic.co/search-labs/blog/better-binary-quantization-lucene-elasticsearch">BBQ</a>索引的比较速度更快，因此当基于索引的数据少于 100k 时，使用精确搜索是合理的。详情请查看<a href="https://www.elastic.co/search-labs/blog/knn-exact-vs-approximate-search">本博文</a>。</p><p>如果您的筛选器总是限制性很强，您可以考虑使用<code>flat</code> 索引类型而不是基于 HNSW 的索引类型，将索引重点放在精确搜索而不是近似搜索上。更多详情，请参阅<a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/dense-vector#dense-vector-params">index_options 的属性</a>。</p><h2>近似矢量搜索的筛选</h2><p>在执行近似向量搜索时，我们需要用结果的准确性来换取性能。像 HNSW 这样的矢量搜索数据结构可在数百万个矢量上高效搜索近似近邻。它们的重点是通过进行最少的向量比较来检索最相似的向量，而向量比较的计算成本很高。</p><p>这意味着其他过滤属性不属于矢量数据的一部分。不同的数据类型有自己的索引结构，如术语字典、发布列表和 doc 值等，可以有效地查找和过滤这些数据。</p><p>既然这些数据结构与矢量搜索机制是分开的，那么我们如何将过滤功能应用于矢量搜索呢？有两种选择：在矢量搜索后应用过滤器（后过滤）或在矢量搜索前应用过滤器（预过滤）。</p><p>每种方案都各有利弊。让我们深入了解它们！</p><h3>后过滤</h3><p>后过滤在矢量搜索完成后应用过滤器。这意味着，在找到前 k 个最相似的向量结果后，才会应用筛选器。</p><p>显然，在对结果进行筛选后，我们可能会得到少于 k 个结果。当然，我们可以从矢量搜索中获取更多的结果（k 值更高），但我们无法确定在应用过滤器后是否会得到 k 或更多的结果。</p><p>后过滤的优势在于它不会改变矢量搜索的运行时行为--矢量搜索不知道过滤的存在。但是，它确实会改变检索结果的最终数量。</p><p>下面是使用<a href="https://www.elastic.co/docs/reference/query-languages/query-dsl/query-dsl-knn-query">knn 查询</a>进行后过滤的示例。检查过滤子句是否与 knn 查询分开：</p><pre><code>{
  "query": {
    "bool": {
      "must": {
        "knn": {
          "field": "image-vector",
          "query_vector": [54, 10, -2],
          "k": 5,
          "num_candidates": 50
        }
      },
      "filter": {
        "term": {
          "file-type": "png"
        }
      }
    }
  }
}</code></pre><p>使用后置<a href="https://www.elastic.co/docs/reference/elasticsearch/rest-apis/filter-search-results#post-filter">过滤器</a>还可对 knn 搜索进行后置过滤：</p><pre><code>{
  "knn": {
    "field": "image-vector",
    "query_vector": [54, 10, 2],
    "k": 5,
    "num_candidates": 50
  },
  "post_filter": {
    "term": {
      "file-type": "png"
    }
  }
}</code></pre><p>请记住，您需要在 knn 搜索中使用明确的后置过滤器部分。如果不使用后置过滤器，knn 搜索<a href="https://www.elastic.co/docs/solutions/search/vector/knn#_combine_approximate_knn_with_other_features"> 会将最近邻</a> 搜索 结果 与其他查询或过滤器 结合起来 ，而不是进行后置过滤器。</p><h3>预过滤</h3><p>在矢量搜索前应用筛选器将首先检索出满足筛选条件的文档，然后将这些信息传递给矢量搜索。</p><p>Lucene 使用<a href="https://github.com/apache/lucene/blob/7a60d7ce92392181e137361336e5196bd486cdd9/lucene/core/src/java/org/apache/lucene/util/BitSet.java">BitSets</a>高效地存储满足筛选条件的文档。然后，矢量搜索会遍历 HNSW 图，并将满足条件的文档考虑在内。在将候选文件添加到结果中之前，它会检查该候选文件是否包含在有效文件的 BitSet 中。</p><p>不过，即使候选文件不是有效文件，也必须对其进行探索并与查询进行比较。HNSW 的有效性取决于图中向量之间的联系--如果我们停止探索某个候选向量，就意味着我们可能也会跳过它的邻近向量。</p><p>就像开车去加油站一样。如果放弃任何一条没有加油站的道路，您就不可能到达目的地。其他道路可能不是你所需要的，但它们将你<em>连接</em>到目的地。HNSW 图形上的向量也是如此！</p><p>因此，应用预过滤比不应用过滤的性能要低。我们需要对搜索中访问的<em>所有</em>向量进行处理，并丢弃不符合筛选条件的向量。我们正在做更多的工作，花更多的时间来获得最高 K 值的结果。</p><p>下面是在 Elasticsearch 查询 DSL 中进行预过滤的示例。检查过滤子句是否已成为 knn 部分的一部分：</p><pre><code>{
  "knn": {
    "field": "image-vector",
    "query_vector": [54, 10, -2],
    "k": 5,
    "num_candidates": 50,
    "filter": {
      "term": {
        "file-type": "png"
      }
    }
  }
}</code></pre><p><a href="https://www.elastic.co/docs/solutions/search/vector/knn#knn-search-filter-example">knn 搜索</a>和<a href="https://www.elastic.co/docs/reference/query-languages/query-dsl/query-dsl-knn-query#knn-query-filtering">knn 查询</a>均可使用预过滤功能：</p><pre><code>{
  "query": {
    "knn": {
      "field": "image-vector",
      "query_vector": [-5, 9, -12],
      "k": 5,
      "filter": {
        "term": {
          "file-type": "png"
        }
      }
    }
  }
}</code></pre><h4>预过滤优化</h4><p>我们可以进行一些优化，以确保预过滤的性能。</p><p>如果筛选条件非常严格，我们可以切换到精确搜索。当需要比较的向量很少时，对满足筛选条件的少数文档进行精确搜索会更快。</p><p>这是<a href="https://github.com/apache/lucene/blob/eb876b618da5d04c1ad14b04a48321638318493a/lucene/core/src/java/org/apache/lucene/search/AbstractKnnVectorQuery.java#L218">Lucene</a>和 Elasticsearch 自动应用的优化。</p><p>另一种优化方法是忽略不符合筛选条件的向量。相反，该方法会检查滤波向量的邻近向量是否通过滤波。这种方法不考虑过滤后的向量，而是继续探索与当前路径相连的向量，从而有效减少了比较次数。</p><p>这种算法就是 ACORN-1，<a href="https://www.elastic.co/search-labs/blog/filtered-hnsw-knn-search">本篇博文</a>将详细介绍其过程。</p><h2>使用文档级安全过滤</h2><p><a href="https://www.elastic.co/docs/deploy-manage/users-roles/cluster-or-deployment-auth/controlling-access-at-document-field-level#document-level-security">文档级别安全（DLS）</a>是 Elasticsearch 的一项功能，可指定用户角色可检索的文档。</p><p>DLS 通过查询来执行。一个角色可以有一个与索引相关联的查询，这实际上限制了属于该角色的用户可以从索引中检索的文档。</p><p>角色查询用作过滤器，用于<a href="https://github.com/elastic/elasticsearch/blob/c3a1cb34294e902a9f46d7e840ea09965019f456/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/accesscontrol/SecurityIndexReaderWrapper.java#L92">检索与之匹配的文档</a>，并作为 BitSet 缓存。然后，这个 BitSet 会被用来封装底层的 Lucene 阅读器，因此只有从查询返回的文档才会被认为是<em>实时的</em>，也就是说，它们存在于索引中，并且没有被删除。</p><p>由于要<a href="https://github.com/apache/lucene/blob/a211d30097a8e3264d3ef073a054bd31eb847231/lucene/core/src/java/org/apache/lucene/search/AbstractKnnVectorQuery.java#L196">从阅读器获取</a>实时文档来执行 knn 查询，因此只考虑用户可用的文档。如果有预检器，DLS 文件将被<a href="https://github.com/apache/lucene/blob/a211d30097a8e3264d3ef073a054bd31eb847231/lucene/core/src/java/org/apache/lucene/search/AbstractKnnVectorQuery.java#L204"> 添加到 预检器 中</a> 。</p><p>这意味着，DLS 过滤可以作为近似矢量搜索的预过滤，具有相同的性能影响和优化效果。</p><p>使用精确搜索的 DLS 与应用任何过滤器的好处相同--从 DLS 检索的文档越少，精确搜索的性能就越高。还要考虑 DLS 返回的文档数量--如果 DLS 的作用非常有限，可以考虑使用精确搜索而不是近似搜索。</p><h2>基准</h2><p>在 Elasticsearch，我们希望确保矢量搜索过滤的效率。我们有<a href="https://elasticsearch-benchmarks.elastic.co/#tracks/so_vector/nightly/default/90d">一个专门的向量过滤基准</a>，通过不同的过滤执行近似向量搜索，以确保向量搜索尽可能快地检索到相关结果。</p><p>查看 ACORN-1 推出时的<a href="https://elasticsearch-benchmark-analytics.elastic.co/app/dashboards#/view/43b63e80-5ba2-11ed-aede-a742809feed4?_g=(refreshInterval:(pause:!t,value:60000),time:(from:&#x27;2025-05-28T01:27:58.456Z&#x27;,to:&#x27;2025-06-30T13:53:26.430Z&#x27;))&amp;_a=()">改进</a>情况。在只有 2% 个向量通过过滤器的测试中，查询延迟时间缩短到原来的 55% ：</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/3eac3748a33376fc97d957364a5c1f5108d5c58b-1023x896.png" style="width: 100%;" /><h2>结论</h2><p>过滤是搜索不可或缺的一部分。确保过滤在矢量搜索中的性能，并了解权衡和优化，是高效和准确搜索的关键所在。</p><p>过滤会影响向量搜索的性能：</p><ul><li>使用过滤功能时，精确搜索速度更快。如果过滤条件足够严格，应考虑使用精确搜索而不是近似搜索。这是 Elasticsearch 的自动优化功能。</li><li>使用预过滤时，近似搜索速度较慢。通过预过滤，我们可以得到与过滤器匹配的前 k 个结果，但搜索速度会减慢。</li><li>后过滤并不一定能检索到前 k 个结果，因为在应用过滤器时，这些结果可能已被过滤器过滤。</li></ul><p>快乐过滤</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Elasticsearch 分片和副本：实用指南]]></title>
            <description><![CDATA[掌握 Elasticsearch 分片和副本的概念，并学习如何优化它们。]]></description>
            <link>https://www.elastic.co/search-labs/cn/blog/elasticsearch-shards-and-replicas-guide</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/cn/blog/elasticsearch-shards-and-replicas-guide</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Piotr Przybyl]]></dc:creator>
            <pubDate>Thu, 14 Aug 2025 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/7775c44f01f2516c4ff4cce6d6bbe9e7b2c38908-720x420.jpg?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>Elasticsearch 在 Lucene 的基础上建立了一个分布式系统，解决了可扩展性和容错问题，从而增强了 Lucene 的功能。它还提供基于 JSON 的 REST 应用程序接口，使与其他系统的互操作性变得非常简单。</p><p>Elasticsearch 等分布式系统可能非常复杂，影响其性能和稳定性的因素很多。<strong>分片</strong>是 Elasticsearch 中最基本的概念之一，了解分片的工作原理将使您能够有效地管理 Elasticsearch 集群。</p><p>本文将解释什么是主分片和副本分片，它们对 Elasticsearch 集群的影响，以及有哪些工具可以调整它们以适应不同的需求。</p><h2>了解碎片</h2><p>Elasticsearch 索引中的数据可能会大量增长。为了便于管理，每条数据都保存在一个索引中，而索引是将一个索引分割成若干<strong>碎片</strong>。每个 Elasticsearch 分区都是一个 Apache Lucene 索引，每个单独的 Lucene 索引都包含 Elasticsearch 索引中文档的一个子集。以这种方式拆分索引可以控制资源使用量。Apache Lucene 索引的上限为 2,147,483,519 (2³¹ - 129) 个文档。</p><p>有时，出于重新平衡的目的，需要在节点间移动指数。由于这一过程需要大量时间和资源，因此索引不应过大，这有助于保持可控的恢复时间。此外，由于索引是由需要不断合并在一起的 Lucene 段组成的，因此段不能太大，这一点很重要。由于这些原因，Elasticsearch 将索引数据分割成更易于管理的小块（称为<strong>主分片</strong>），这些分片可以更方便地分布在多台计算机上。<strong>复制</strong>分区只是相应主分区的一个精确副本，我们将在本文稍后部分介绍它们的功能。</p><p>拥有适当数量的分片对性能非常重要。因此，提前制定计划是明智之举。当查询在不同分片上并行运行时，其执行速度要快于由单个分片组成的索引，但前提是每个分片位于不同的节点上，且集群中有足够多的节点。但与此同时，分片也会消耗内存和磁盘空间，包括索引数据和集群元数据。分片过多（也称为过度分片）会降低查询、索引请求和管理操作的速度，因此保持适当的平衡至关重要。</p><p>主分区的数量是在<strong>为特定索引实例</strong>创建索引时定义的。如果以后需要不同数量的主分片，可以使用<strong> 调整大小</strong><a href="https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-indices-split">API</a> --拆分（更多的主分片）、<a href="https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-indices-shrink">收缩</a>（更少的主分片）或<a href="https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-indices-clone">克隆</a>（相同数量的主分片，并对副本进行新的设置）。创建索引时，可以<strong>将主</strong>分片和副本分片的数量设置为索引的设置：</p><pre><code>PUT /sensor
{
   "settings" : {
       "index" : {
           "number_of_shards" : 6,
           "number_of_replicas" : 2
       }
   }
}</code></pre><p>(如果没有指定分片或副本的数量，从 Elasticsearch 7.0 开始，两者的默认值都是 1）。理想的分片数量应根据索引中的数据量来确定。一般来说，<a href="https://www.elastic.co/docs/deploy-manage/production-guidance/optimize-performance/size-shards">一个最佳分区应容纳 10-50GB 的数据</a>，每个分区的文件数少于 2 亿。例如，如果您预计一天内会积累约 300GB 的应用程序日志，那么在该索引中设置约 10 个分片是合理的，前提是您有足够多的节点来托管这些分片。</p><p>碎片在其生命周期中会经历多种状态，包括</p><ul><li><strong>初始化：</strong>使用分片前的初始状态。</li><li><strong>已启动：</strong>分片处于激活状态，可以接收请求。</li><li><strong>搬迁：</strong>当分片正在被移动到不同节点时出现的一种状态。这在某些情况下可能是必要的，例如，当它们所在的节点快用完磁盘空间时。</li><li><strong>未分配：</strong>未能分配的分区的状态。发生这种情况时会给出原因，例如，托管分片的节点已不在集群中<em>（NODE_LEFT）</em>或由于恢复到一个已关闭的索引<em>中（EXISTING_INDEX_RESTORED）。</em></li></ul><p>要查看所有分片、它们的状态和其他元数据，可以使用以下请求：</p><pre><code>GET _cat/shards</code></pre><p>要查看特定索引的分片，可以在 URL 中添加索引名称，例如传感器：</p><pre><code>GET _cat/shards/sensor</code></pre><p>该命令会产生输出结果，如下面的示例。默认情况下，显示的列包括索引名称、名称（即编号）、是主分片还是副本、状态、文件数量、磁盘大小以及分片所在节点的 IP 地址和节点 ID。</p><pre><code>sensor 5 p STARTED    0  283b 127.0.0.1 ziap
sensor 5 r UNASSIGNED                  
sensor 2 p STARTED    1 3.7kb 127.0.0.1 ziap
sensor 2 r UNASSIGNED                  
sensor 3 p STARTED    3 7.2kb 127.0.0.1 ziap
sensor 3 r UNASSIGNED                  
sensor 1 p STARTED    1 3.7kb 127.0.0.1 ziap
sensor 1 r UNASSIGNED                  
sensor 4 p STARTED    2 3.8kb 127.0.0.1 ziap
sensor 4 r UNASSIGNED                  
sensor 0 p STARTED    0  283b 127.0.0.1 ziap
sensor 0 r UNASSIGNED</code></pre><h2>了解副本</h2><p>每个分区只包含一份数据副本，而索引则可以包含多个分区副本。因此有两种分片，<strong>即主分片</strong>和副本或<strong>复制</strong> 分片。主分片的每个副本总是位于不同的节点上，这就确保了在节点发生故障时数据的高可用性。除了冗余及其在防止数据丢失和宕机方面的作用外，副本还可以帮助提高搜索性能，因为它允许查询与主分片并行处理，因此速度更快。</p><p>主分片和副本分片的行为方式存在一些重要差异。虽然两者都能处理查询、索引请求（即向索引添加数据）必须先经过主分片，然后才能复制到副本分片。如上所述，如果主分片不可用--例如，由于节点断开或硬件故障--副本就会被提升以接替其角色。</p><p>虽然复制可以在节点发生故障时提供帮助，但重要的是不要有太多的复制，因为它们会在编制索引时消耗内存、磁盘空间和计算能力。主分片和副本之间的另一个区别是，虽然主分片的数量在索引创建后无法更改，但副本的数量可以通过更新索引设置随时动态更改。</p><p>复制的另一个考虑因素是可用节点的数量。副本总是放在与主分片不同的节点上，因为如果节点发生故障，同一节点上的两个相同数据副本将无法提供保护。因此，一个系统要支持<em>n 个</em>副本，集群中至少需要有<em>n + 1 个</em>节点。例如，如果集群中有两个节点，而索引配置了六个副本，则只会分配一个副本。另一方面，拥有七个节点的系统完全可以处理一个主分片和六个副本。</p><h2>优化分片和副本</h2><p>即使在创建了主分片和副本分片平衡得当的索引后，也需要对这些分片进行监控，因为索引的动态会随着时间的推移而发生变化。例如，在处理时间序列数据时，最新数据的指数通常比旧数据的指数更活跃。如果不对这些指数进行调整，它们将消耗相同数量的资源，尽管它们的需求非常不同。</p><p>翻转索引 API 可用于区分新旧索引。可以对其进行设置，一旦达到某个阈值（磁盘上索引的大小、文档数量或年限），它就会自动创建新索引。该 API 对于控制分片大小也很有用。由于索引创建后无法轻易更改分片数量，因此如果不满足翻转条件，分片将继续积累数据。对于只需不经常访问的旧索引，缩小和强制合并索引是减少其内存和磁盘占用的两种不同方法。前者减少了索引中分片的数量，后者则减少了 Lucene 片段的数量，并释放了已删除文档的空间。</p><h2>作为 Elasticsearch 基础的主分片和副本分片</h2><p>Elasticsearch 作为适用于海量数据的分布式存储、搜索和分析平台，已经建立了良好的声誉。然而，在如此大规模的运作中，挑战将不可避免地出现。这就是为什么了解主分片和副本分片如何工作对 Elasticsearch 如此重要和基础的原因，因为这有助于优化平台的可靠性和性能。</p><p>了解它们如何工作以及如何优化它们，对于实现更强大、更高性能的 Elasticsearch 集群至关重要。如果您经常遇到查询响应迟缓或中断的情况，这些知识可能是克服这些障碍的关键。</p><p>请关注 Elasticsearch 的官方文档，了解有关<a href="https://www.elastic.co/docs/deploy-manage/distributed-architecture/clusters-nodes-shards">群集、节点和分片</a>、<a href="https://www.elastic.co/docs/deploy-manage/production-guidance/optimize-performance/size-shards">如何确定分片大小</a>、<a href="https://www.elastic.co/docs/deploy-manage/distributed-architecture/shard-allocation-relocation-recovery">分片分配和恢复的</a>更多信息。</p><p>本主题还可作为入门课程在<a href="https://youtu.be/sAySPSyL2qE">Elastic Community YouTube 频道</a>上观看。</p><p>最后但并非最不重要的一点：如果你不想担心节点、分片或副本，可以试试<a href="https://www.elastic.co/docs/deploy-manage/deploy/elastic-cloud/serverless">Elastic Cloud Serverless</a>。该 Elastic 云产品由 Elastic 全面管理，并可根据您的工作负载自动扩展。免费试用可以帮助您熟悉无服务器方法的其他优势。</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[如何显示 Elasticsearch 索引的字段]]></title>
            <description><![CDATA[了解如何使用 _mapping 和 _search API、子字段、合成 _source 和运行时字段显示 Elasticsearch 索引的字段。]]></description>
            <link>https://www.elastic.co/search-labs/cn/blog/elasticsearch-index-show-fields</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/cn/blog/elasticsearch-index-show-fields</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[JD Armada]]></dc:creator>
            <pubDate>Wed, 06 Aug 2025 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/23b96aaa1a38b1f4747b4a87695d816f24c0cf70-720x421.jpg?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>本文将讨论如何显示 Elasticsearch 索引的字段。这对了解数据结构、识别特定字段和排除故障非常有用。我们将讨论以下主题</p><ol><li>使用<code>_mapping</code> API 获取字段信息</li><li>使用<code>_search</code> API 显示字段值</li><li>显示子字段</li><li>合成 _source</li><li>运行时字段</li></ol><h2>1.使用 _mapping API 获取字段信息</h2><p><code>_mapping</code> API 允许您检索一个或多个索引的映射定义。这包括有关字段、其数据类型和其他属性的信息。要检索特定索引的映射，请使用以下请求：</p><pre><code>GET /<index_name>/_mapping</code></pre><p>例如，如果有一个名为<code>my_index</code> 的索引，可以通过以下请求检索其映射：</p><pre><code>GET /my_index/_mapping</code></pre><p>响应将包括索引的映射定义，其中包含有关字段及其属性的信息。</p><p>还可以检索某一特定字段的映射。如果您的映射范围很大，而您只想专注于某一特定领域，这将非常有用。要检索特定字段的映射，请使用以下请求：</p><pre><code>GET /my_index/_mapping/field/my_field</code></pre><p>您还可以检索多个字段的映射，方法是用逗号分隔字段名，如下面的请求：</p><pre><code>GET /my_index/_mapping/field/my_field_1,my_field_2,my_field_3</code></pre><h2>2.使用 _search API 显示字段值</h2><p>要显示 Elasticsearch 索引中字段的值，可以使用<code>_search</code> API。<code>_search</code> API 为您提供了多种控制返回字段的方法，主要有两种：</p><ol><li><strong><code>_source</code></strong>:<a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/mapping-source-field"><code>_source</code></a> 字段包含的原始 JSON 文档正文与索引中的一模一样，包括摄取管道或预处理步骤所做的任何更改。要显示源文件中的特定字段，请执行源过滤，如下所示。</li><li><strong><code>fields</code></strong>:<a href="https://www.elastic.co/docs/reference/elasticsearch/rest-apis/retrieve-selected-fields"><code>fields</code></a> 参数可让您在执行搜索时，根据索引映射从文档中检索特定字段。与<code>_source</code> 不同，<code>fields</code> 也可以从存储字段、文档值或运行时字段中返回值，而无需引用<code>_source</code> ，不过对于没有文档值或存储设置的标准字段，它会返回<code>_source</code> 。这将带来许多好处，如性能等，我们将在下文中看到。</li></ol><h3>使用 _source<code> </code>字段</h3><p>默认情况下，<code> _search</code> API 返回<code>_source</code> 字段，该字段包含被索引的原始 JSON 文档。要显示特定字段，可以在搜索请求的<code>_source </code>参数中添加筛选器；这称为源筛选。</p><p>下面是一个搜索请求示例，它返回<code>my_index</code> 索引中文档的<code>title </code>和<code>author</code> 字段的值：</p><pre><code>GET /my_index/_search
{
  "query": {
    "match_all": {}
  },
  "_source": ["title", "author"]
}</code></pre><p>在本例中，<code>_source</code> 参数指定了要返回的字段。</p><p>如果需要更多控制，可以使用<code>_source</code> 对象的<code>includes</code> 和<code>excludes </code>属性。例如，下面的查询返回顶层<code>title</code> 字段和<code>author</code> 的所有子字段，但<code>author.description</code> 除外。</p><pre><code>GET /my_index/_search
{
  "query": {
    "match_all": {}
  },
  "_source": {
     “includes”: [“title”, “author.*],
     “excludes”: [“author.description”]
  }
}</code></pre><p>在本例中，我们使用<code>author.* </code>模式检索<code>author </code>对象的每个直接子字段。然后，我们明确排除<code>author.description </code>，这样就只返回其他作者字段。请注意，这并不能提高性能，因为它仍然需要加载和解析源 JSON，但可以减少通过网络发送的响应大小。</p><h3>使用字段参数</h3><p>您可以使用<code>fields</code> 参数过滤搜索响应中返回的字段。使用<code>fields</code> 而不是<code>_source</code> 有几个好处，包括</p><ul><li><strong>提高性能： </strong><code>fields </code>可以直接从<a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/mapping-store">存储字段</a>或<a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/doc-values">doc 值</a>返回值，而无需加载完整的<code>_source</code> ，从而减小了响应有效载荷的大小。</li><li><strong>格式化输出：</strong>对于标准字段，<code> fields</code> 可能会回到<code>_source</code> 抓取值，但它会查看索引映射，以正确格式化输出，如格式化日期，使其与用于聚合和排序的内容保持一致。</li><li><strong>访问运行时字段：</strong> <code>fields</code> 可以返回运行时字段，这些字段在原始<code>_source</code> 上并不存在。</li><li>更多好处请<a href="https://www.elastic.co/docs/reference/elasticsearch/rest-apis/retrieve-selected-fields#search-fields-param">点击此处</a>。</li></ul><p>例如，要只返回<code>my_index</code> 索引中的<code>title</code> 和<code>author</code> 字段，可以使用以下搜索请求：</p><pre><code>GET /my_index/_search
{
  "query": {
    "match_all": {}
  },
  "fields": ["title", "author"],
  "_source": false
}</code></pre><p>在上述查询中，我们将<code>_source </code>字段设置为 false，这样就不会返回源文件。这可以大大减小响应的有效载荷大小，但请记住，这只是因为<code>title</code> 和<code>author</code> 字段属于<code>keyword </code>字段类型，默认情况下已启用<code>doc_values</code> 。如果字段未启用<code>doc_values</code> 且<code>_source</code> 设置为 false，Elasticsearch 将无法检索它们，并在响应中跳过它们。</p><p>值得注意的是，<code>fields</code> 响应总是为每个字段返回一个值数组，即使只有一个值。这是因为 Elasticsearch 没有专用的数组类型，任何字段都可能有多个值。有关 Elasticsearch 中数组的更多信息，请单击<a href="http://elastic.co/docs/reference/elasticsearch/mapping-reference/array">此处</a>。</p><h3>检索字段的其他方法</h3><p>虽然使用<code>_source</code> 或<code>fields</code> 检索字段是推荐的方法，但也有针对特定用例的不同方法，例如</p><p><strong>文档值字段：</strong>如果想完全避免使用<code>_source</code> ，可以使用<code><a href="https://www.elastic.co/docs/reference/elasticsearch/rest-apis/retrieve-selected-fields#docvalue-fields">docvalue_fields</a> </code> 参数进行搜索。Doc 值存储的字段值与<code>_source</code> 相同，但采用磁盘数据结构，优化了排序和聚合。</p><p>由于它与<code>_source</code> 中存储的值是分开的，因此可以请求特定字段，而无需加载整个<code>_source</code> 。如果您要查询大型文档，但只需要几个支持文档值的小字段，这将非常有用。使用<code>docvalue_fields </code>的另一个用例是在<code>date</code> 和<code>numeric</code> 字段上使用自定义格式，我们将在下面的示例中看到这一点。</p><p>请注意，这只适用于启用<code>doc_values</code> 的字段或默认已启用 的字段类型，如<code>keyword</code> 、<code>date</code> 、数字类型和<code>boolean</code> ，不适用于<a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/text"><code>text</code></a> 或<a href="https://www.elastic.co/docs/reference/elasticsearch/plugins/mapper-annotated-text-usage"><code>annotated_text</code></a> 。</p><p>在本例中，我们使用<code>docvalue_fields</code> 参数检索<code>title</code> 、<code>author</code> 和<code>published</code> 字段，而无需加载完整的<code>_source</code> 文档：</p><pre><code>GET /my_index/_search
{
  "query": {
    "match_all": {}
  },
  "docvalue_fields": [
    "title",
    "author",
    {
      "field": "published",
      "format": "epoch_millis"
    }
  ],
  "_source": false
}</code></pre><p>运行该查询时，Elasticsearch 会直接从磁盘上的列存储中抓取值，而不是为每个文档引用<code>_source </code>。由于查询中提供了<code>format</code> 参数，<code>published</code> 字段将以<code>epoch_millis</code> 格式返回，而不是默认格式。</p><p><strong>存储字段：</strong>如果在映射中明确标记了<a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/mapping-store">存储</a>的特定字段，则可以使用<code>stored_fields</code> 参数过滤这些字段。如果您只想使用这些特定字段或您特意存储以备日后检索的字段进行轻量级回复，这将非常有用。它与<code>_source</code> 分开存储，因此这种方法还可以避免加载<code>_source</code> 。</p><p>需要注意的是，该选项默认为关闭，一般不推荐使用。而使用源过滤则可返回原始源文件的某些子集。</p><p>在下面的示例查询中，我们使用<code>stored_fields</code> 参数来检索<code>summary</code> 字段，该字段的索引映射配置为 &quot;<code>store”: true</code> 。</p><pre><code>GET /my_index/_search
{
  "query": {
    "match_all": {}
  },
  "stored_fields": ["summary"]
}</code></pre><p>运行该查询时，Elasticsearch 会查看该字段是否被标记为<code>”store”: true</code> ，如果没有找到，则会完全跳过该字段。</p><h2>3.显示子字段</h2><p>如果索引包含子字段，可以使用点符号在<code>fields</code> 参数中指定字段路径。请注意，子字段不同于<a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/nested">嵌套字段类型</a>。例如，如果您有一个名为<code>address.city</code> 的子字段，您可以像这样将其包含在搜索响应中：</p><pre><code>GET /my_index/_search
{
  "query": {
    "match_all": {}
  },
  "fields": ["title", "author", "address.city"],
  "_source": false
}</code></pre><p>在此示例中，搜索响应将包括<code>title</code> 、<code>author</code> 和<code>address.city</code> 字段的值。</p><h2>4.合成_来源</h2><p>如果既想保留<code> _source</code> 的功能，又想节省磁盘空间，可以选择在索引映射中使用合成<code>_source</code> 。<a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/mapping-source-field#synthetic-source">合成 <code>_source</code></a> 是一项允许 Elasticsearch 从存储字段和 doc 值等现有数据重建<code>_source</code> 的功能，即使<code>_source</code> 已禁用。这样可以节省大量存储空间，但查询时的速度会稍低，因为重建是即时进行的。在索引设置中使用以下值来启用此功能：</p><pre><code>PUT idx
{
  "settings": {
    "index": {
      "mapping": {
        "source": {
          "mode": "synthetic"
        }
      }
    }
  }
}</code></pre><p>使用合成<code>_source </code>的一些好处包括：在使用<code>_search</code> API 时显示完整文档、源过滤以及与其他功能和工具（如希望<code>_source</code> 可用的 Kibana）的兼容性，所有这些都避免了存储完整<code>_source</code> 文档的需要。</p><h2>5.运行时字段</h2><p><a href="https://www.elastic.co/docs/manage-data/data-store/mapping/runtime-fields">运行时字段</a>可让你在查询时或在运行时块下的索引映射中定义脚本字段。这些字段从未编入索引，因此添加运行时字段不会增加索引大小，但也不会显示在<code>_source</code> 中。在映射中定义的运行时字段是持久的，可用于所有查询，而在查询时定义的运行时字段是临时的，仅在该搜索请求中可用。</p><p>使用运行时字段的主要好处是，可以在摄取文档后将字段添加到文档中，从而简化映射决策。运行时字段也非常适合用原始文档中不存在但使用脚本生成的值来丰富文档，例如格式化字符串或计算分数。</p><p>还值得注意的是，运行时字段会损害性能，因为需要为结果集中的每个文档运行脚本。要<a href="https://www.elastic.co/docs/manage-data/data-store/mapping/retrieve-runtime-field">检索运行时字段</a>，还可以使用<code>_search</code> API 上的<code>fields</code> 参数。</p><h2>结论</h2><p>显示 Elasticsearch 索引的字段可以是使用索引映射或<code>_source</code> 简单检索值，也可以是使用<code>fields</code> 、<code>docvalue_fields</code> 或运行时字段的更高级方法，以获得更强的控制力和更高的效率。了解不同方法之间的权衡是优化搜索体验的关键。无论您是要优化有效载荷、丰富文档内容，还是使用合成<code>_source</code> 来节省存储空间，Elasticsearch 都能为您提供多种工具和功能，让您以所需的方式找到所需的数据。这些技术可以帮助您了解数据结构、识别特定字段并排除故障。</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[人工智能驱动的仪表盘：从设想到 Kibana]]></title>
            <description><![CDATA[使用 LLM 生成仪表盘，处理图像并将其转化为 Kibana 仪表盘。
]]></description>
            <link>https://www.elastic.co/search-labs/cn/blog/ai-powered-dashboards</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/cn/blog/ai-powered-dashboards</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Jeffrey Rengifo, Tomás Murúa]]></dc:creator>
            <pubDate>Wed, 16 Jul 2025 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/eb60ceb2fbc3941745b21ae3357cbb6ea8fab18c-1443x811.png?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p><a href="https://www.elastic.co/kibana/kibana-lens">Kibana Lens</a>让仪表盘的拖放变得非常简单，但当你需要几十个面板时，点击次数就会增加。如果你能勾画出一个仪表盘，截图后让法律硕士为你完成整个过程，那会怎么样？</p><p>在本文中，我们将实现这一目标。我们将创建一个应用程序，它可以获取仪表盘的图像，分析映射，然后生成仪表盘，而无需接触 Kibana！</p><p><strong>步骤</strong>：</p><ol><li><a href="https://www.elastic.co/search-labs/blog/ai-powered-dashboards#background-&amp;-application-workflow">后台&amp; 应用程序工作流程</a></li><li><a href="https://www.elastic.co/search-labs/blog/ai-powered-dashboards#prepare-data">准备数据</a></li><li><a href="https://www.elastic.co/search-labs/blog/ai-powered-dashboards#llm-configuration">LLM 配置</a></li><li><a href="https://www.elastic.co/search-labs/blog/ai-powered-dashboards#application-functions">应用功能</a></li></ol><h2>后台&amp; 应用程序工作流程</h2><p>我首先想到的是让 LLM 生成整个 NDJSON 格式的 Kibana<a href="https://www.elastic.co/docs/explore-analyze/find-and-organize/saved-objects">保存对象</a>，然后将它们导入 Kibana。</p><p>我们尝试了几种型号：</p><ul><li>双子座 2.5 pro</li><li>GPT o3 / o4-mini-high / 4.1</li><li>克劳德 4 号十四行诗</li><li>Grok 3</li><li>Deepseek (Deepthink R1)</li></ul><p>至于提示语，我们从最简单的开始：</p><pre><code>You are an Elasticsearch Saved-Object generator (Kibana 9.0).
INPUTS
=====
1. PNG screenshot of a 4-panel dashboard (attached).
2. Index mapping (below) – trimmed down to only the fields present in the screenshot.
3. Example NDJSON of *one* metric visualization (below) for reference.

TASK
====
Return **only** a valid NDJSON array that recreates the dashboard exactly:
* 2 metric panels (Visits, Unique Visitors)
* 1 pie chart (Most used OS)
* 1 vertical bar chart (State Geo Dest)
* Use index pattern `kibana_sample_data_logs`.
* Preserve roughly the same layout (2×2 grid).
* Use `panelIndex` values 1-4 and random `id` strings.
* Kibana version: 9.0</code></pre><p>尽管我们看了<a href="https://www.elastic.co/search-labs/blog/function-calling-with-elastic#:~:text=Few%2Dshot%20prompting%20involves%20providing%20examples%20of%20the%20types%20of%20queries%20you%20want%20it%20to%20return%2C%20which%20helps%20in%20increasing%20consistency.">一些简单的示例</a>，并详细解释了如何建立每种可视化，但我们还是一无所获。如果您对这项实验感兴趣，请<a href="https://gist.github.com/TomasMurua/a78dc283e115624731beffc98984b70b">点击此处</a>了解详情。</p><p>采用这种方法的结果是，在尝试将 LLM 生成的文件上传到 Kibana 时看到了这些信息：</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/2b599443b5613c9f0fc3235581614add5b4b3900-891x98.png" style="width: 100%;" /><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/d87ccfc033bc00ee8188c5cae18043fbca22784c-741x233.png" style="width: 100%;" /><p>这意味着生成的 JSON 无效或格式不当。最常见的问题是 LLM 生成不完整的 NDJSON、产生参数幻觉，或者返回普通 JSON 而非 NDJSON，无论我们如何努力去执行其他操作。</p><p>受<a href="https://www.elastic.co/search-labs/blog/llm-functions-elasticsearch-intelligent-query">这篇文章</a>的启发--<a href="https://www.elastic.co/docs/solutions/search/search-templates">搜索模板</a>比 LLM 自由式更有效--我们决定给 LLM 提供模板，而不是要求它生成完整的 NDJSON 文件，然后我们在代码中使用 LLM 给出的参数来创建适当的可视化。</p><p>申请工作流程如下：</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/52c587cf0cf3517fdd4ee7ab95581dd4f2bce030-725x668.png" style="width: 100%;" /><p></p><p><em>为简单起见，我们将省略一些代码，但您可以在 <a href="https://github.com/elastic/elasticsearch-labs/tree/main/supporting-blog-content/from-image-idea-to-kibana-dashboard-using-ai/from-image-idea-to-kibana-dashboard-using-ai.ipynb"><strong>本</strong></a> 笔记本</em>上找到完整应用程序的工作代码&nbsp; 。</p><h2>准备工作</h2><p>在开始开发之前，您需要具备以下条件：</p><ol><li>Python 3.8 或更高版本</li><li><a href="https://docs.python.org/3/library/venv.html">Venv</a>Python 环境</li><li>运行的 Elasticsearch 实例及其端点和 API 密钥</li><li>存储在环境变量 OPENAI_API_KEY 下的 OpenAI API 密钥：</li></ol><pre><code>export OPENAI_API_KEY="your-openai-api-key"</code></pre><h2>准备数据</h2><p>在数据方面，我们将保持简单，使用 Elastic 样本网络日志。您可以<a href="https://www.elastic.co/docs/manage-data/ingest/sample-data#add-sample-data-sets">在此</a>了解如何将这些数据导入群集。</p><p>每份文档都包含向应用程序发出请求的主机的详细信息，以及请求本身及其响应状态的信息。下面是一个文件示例：</p><pre><code>{
    "agent": "Mozilla/5.0 (X11; Linux i686) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.50 Safari/534.24",
    "bytes": 8509,
    "clientip": "70.133.115.149",
    "extension": "css",
    "geo": {
        "srcdest": "US:IT",
        "src": "US",
        "dest": "IT",
        "coordinates": {
            "lat": 38.05134111,
            "lon": -103.5106908
        }
    },
    "host": "cdn.elastic-elastic-elastic.org",
    "index": "kibana_sample_data_logs",
    "ip": "70.133.115.149",
    "machine": {
        "ram": 5368709120,
        "os": "osx"
    },
    "memory": null,
    "message": "70.133.115.149 - - [2018-08-30T23:35:31.492Z] \"GET /styles/semantic-ui.css HTTP/1.1\" 200 8509 \"-\" \"Mozilla/5.0 (X11; Linux i686) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.50 Safari/534.24\"",
    "phpmemory": null,
    "referer": "http://twitter.com/error/john-phillips",
    "request": "/styles/semantic-ui.css",
    "response": 200,
    "tags": [
        "success",
        "info"
    ],
    "@timestamp": "2025-07-03T23:35:31.492Z",
    "url": "https://cdn.elastic-elastic-elastic.org/styles/semantic-ui.css",
    "utc_time": "2025-07-03T23:35:31.492Z",
    "event": {
        "dataset": "sample_web_logs"
    },
    "bytes_gauge": 8509,
    "bytes_counter": 51201128
}</code></pre><p>现在，让我们抓取刚刚加载的索引的映射，<code>kibana_sample_data_logs</code> ：</p><pre><code>INDEX_NAME = "kibana_sample_data_logs"

es_client = Elasticsearch(
    [os.getenv("ELASTICSEARCH_URL")],
    api_key=os.getenv("ELASTICSEARCH_API_KEY"),
)

result = es_client.indices.get_mapping(index=INDEX_NAME)
index_mappings = result[list(result.keys())[0]]["mappings"]["properties"]</code></pre><p>我们将把映射与稍后加载的图像一起传递。</p><h2>LLM 配置</h2><p>让我们对 LLM 进行配置，使其使用<a href="https://python.langchain.com/docs/concepts/structured_outputs/">结构化输出</a>来输入图像，并接收包含我们需要传递给函数的信息的 JSON，以生成 JSON 对象。</p><p>我们安装依赖项：</p><pre><code>pip install elasticsearch pydantic langchain langchain-openai -q</code></pre><p>Elasticsearch 将帮助我们检索<a href="https://www.elastic.co/docs/manage-data/data-store/mapping">索引映射</a>。Pydantic 允许我们在 Python 中定义模式，然后要求 LLM 遵循这些模式，而<a href="https://www.elastic.co/search-labs/integrations/langchain">LangChain</a>框架则有助于更轻松地调用 LLM 和人工智能工具。</p><p>我们将创建一个 Pydantic 模式，以定义我们希望从 LLM 得到的输出。我们需要从图片中了解图表类型、字段、可视化标题和仪表盘标题：</p><pre><code>class Visualization(BaseModel):
    title: str = Field(description="The dashboard title")
    type: List[Literal["pie", "bar", "metric"]]
    field: str = Field(
        description="The field that this visualization use based on the provided mappings"
    )


class Dashboard(BaseModel):
    title: str = Field(description="The dashboard title")
    visualizations: List[Visualization]</code></pre><p>对于图像输入，我们将发送一个我刚刚画好的仪表盘：</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/36441d7b5dc1f3ff2ac2a30710208d57ad41c716-1600x898.jpg" style="width: 100%;" /><p>现在我们声明 LLM 模型调用和图像加载。该函数将接收 Elasticsearch 索引的映射和我们要生成的仪表盘图像。</p><p>通过<code>with_structured_output</code> ，我们可以使用 Pydantic<code>Dashboard</code> 模式作为 LLM 生成的响应对象。通过<a href="https://docs.pydantic.dev/latest/">Pydantic</a>，我们可以定义带有验证功能的数据模型，从而确保 LLM 输出与预期结构相匹配。</p><p>要将图像转换为 base64 并作为输入发送，可以使用<a href="https://www.base64-image.de/">在线转换器</a> <a href="https://www.geeksforgeeks.org/python-convert-image-to-string-and-vice-versa/">或用代码</a>完成。</p><pre><code>prompt = f"""
    You are an expert in analyzing Kibana dashboards from images for the version 9.0.0 of Kibana.

    You will be given a dashboard image and an Elasticsearch index mapping.

    Below are the index mappings for the index that the dashboard is based on.
    Use this to help you understand the data and the fields that are available.

    Index Mappings:
    {index_mappings}

    Only include the fields that are relevant for each visualization, based on what is visible in the image.
    """

message = [
    {
        "role": "user",
        "content": [
            {"type": "text", "text": prompt},
            {
                "type": "image",
                "source_type": "base64",
                "data": image_base64,
                "mime_type": "image/png",
            },
        ],
    }
]


try:
    llm = init_chat_model("gpt-4.1-mini")
    llm = llm.with_structured_output(Dashboard)
    dashboard_values = llm.invoke(message)

    print("Dashboard values generated by the LLM successfully")
    print(dashboard_values)
except Exception as e:
    print(f"Failed to analyze image and match fields: {str(e)}")</code></pre><p>LLM 已经掌握了 Kibana 面板的上下文，因此我们不需要在提示中解释所有内容，只需提供一些细节，确保它不会忘记自己正在使用 Elasticsearch 和 Kibana。</p><p>让我们来分析一下提示：</p>
            <table style="border-collapse: collapse; margin: 20px 0;">
              <tbody>
                
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">部门</td><td style="border: 1px solid #ccc; padding: 4px;">原因</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">您是根据 Kibana 9.0.0 版本的图像分析 Kibana 仪表板的专家。</td><td style="border: 1px solid #ccc; padding: 4px;">通过强化 Elasticsearch 和 Elasticsearch 版本，我们降低了 LLM 产生旧参数/无效参数的可能性。</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">您将获得一个仪表盘图像和一个 Elasticsearch 索引映射。</td><td style="border: 1px solid #ccc; padding: 4px;">我们解释说，图片是关于仪表盘的，以避免法律硕士做出任何错误的解释。</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">下面是仪表盘所基于的索引的索引映射，使用它可以帮助你理解数据和可用字段。索引映射： {index_mappings}</td><td style="border: 1px solid #ccc; padding: 4px;">提供映射至关重要，这样 LLM 才能动态选择有效字段。否则，我们就可能在这里硬编码映射，这太死板了，或者依靠图像包含正确的字段名，这也不可靠。</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">根据图像中可见的内容，只包含与每个可视化相关的字段。</td><td style="border: 1px solid #ccc; padding: 4px;">我们必须添加这一增强功能，因为有时它会尝试添加与图像无关的字段。</td>
                    </tr>
              </tbody>
            </table>
          <p>这将返回一个包含要显示的可视化数组的对象：</p><pre><code>"Dashboard values generated by the LLM successfully
title=""Client, Extension, OS, and Response Keyword Analysis""visualizations="[
   "Visualization(title=""Count of Client IP",
   "type="[
      "metric"
   ],
   "field=""clientip"")",
   "Visualization(title=""Extension Keyword Distribution",
   "type="[
      "pie"
   ],
   "field=""extension.keyword"")",
   "Visualization(title=""Most Used OS",
   "type="[
      "bar"
   ],
   "field=""machine.os.keyword"")",
   "Visualization(title=""Response Keyword Distribution",
   "type="[
      "bar"
   ],
   "field=""response.keyword"")"
]</code></pre><h2>处理 LLM 答复</h2><p>我们在<strong> </strong>上创建了一个 2x2 面板仪表盘示例，然后使用 &quot;<a href="https://www.elastic.co/docs/api/doc/kibana/operation/operation-get-dashboards-dashboard">获取仪表盘 API &quot;</a>将其导出为 JSON 格式，然后将面板存储为可视化模板（饼状、条状、度量），在这些模板中，我们可以替换部分参数，根据问题创建带有不同字段的新可视化。</p><p>您可以<a href="https://github.com/Delacrobix/elasticsearch-labs/tree/supporting-blog-content/from-image-idea-to-kibana-dashboard-using-ai/supporting-blog-content/from-image-idea-to-kibana-dashboard-using-ai/templates"><strong>在此处</strong></a>查看模板 JSON 文件。请注意我们是如何用 {<code>variable_name</code>} 更改我们稍后要替换的对象值的。<br/></p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/ec7e1ac0cd8b470df13e60940162b56778acb386-315x234.png" style="width: 100%;" /><p>根据 LLM 提供的信息，我们可以决定使用哪个模板，替换哪些值。</p><p><code>fill_template_with_analysis</code> 将接收单个面板的参数，包括可视化的 JSON 模板、标题、字段和可视化在网格上的坐标。</p><p>然后，它会替换模板的值，并返回最终的 JSON 可视化。</p><pre><code>def fill_template_with_analysis(
    template: Dict[str, Any],
    visualization: Visualization,
    grid_data: Dict[str, Any],
):
    template_str = json.dumps(template)
    replacements = {
	 "{visualization_id}": str(uuid.uuid4()),
        "{title}": visualization.title,
        "{x}": grid_data["x"],
        "{y}": grid_data["y"],
    }

    if visualization.field:
        replacements["{field}"] = visualization.field

    for placeholder, value in replacements.items():
        template_str = template_str.replace(placeholder, str(value))

    return json.loads(template_str)</code></pre><p>为了简单起见，我们将为 LLM 决定创建的面板分配静态坐标，并生成如上图所示的 2x2 网格仪表盘。</p><pre><code># Filling templates fields
panels = []    
grid_data = [
    {"x": 0, "y": 0},
    {"x": 12, "y": 0},
    {"x": 0, "y": 12},
    {"x": 12, "y": 12},
]


i = 0

for vis in dashboard_values.visualizations:
    for vis_type in vis.type:
        template = templates.get(vis_type, templates.get("bar", {}))
        filled_panel = fill_template_with_analysis(template, vis, grid_data[i])
        panels.append(filled_panel)
        i += 1</code></pre><p>根据 LLM 决定的可视化类型，我们将选择一个 JSON 文件模板，并使用<code>fill_template_with_analysis</code> 替换相关信息，然后将新面板追加到稍后用于创建仪表盘的数组中。</p><p>仪表盘准备就绪后，我们将使用<a href="https://www.elastic.co/docs/api/doc/kibana/operation/operation-post-dashboards-dashboard-id"> 创建 仪表盘 API</a> 将新的 JSON 文件推送到 Kibana 以生成仪表盘：<br/></p><pre><code>try:
    dashboard_id = str(uuid.uuid4())

    # post request to create the dashboard endpoint
    url = f"{os.getenv('KIBANA_URL')}/api/dashboards/dashboard/{dashboard_id}"

    dashboard_config = {
        "attributes": {
            "title": dashboard_values.title,
            "description": "Generated by AI",
            "timeRestore": True,
            "panels": panels,  # Visualizations with the values generated by the LLM
            "timeFrom": "now-7d/d",
            "timeTo": "now",
        },
    }

    headers = {
        "Content-Type": "application/json",
        "kbn-xsrf": "true",
        "Authorization": f"ApiKey {os.getenv('ELASTICSEARCH_API_KEY')}",
    }

    requests.post(
        url,
        headers=headers,
        json=dashboard_config,
    )

    # Url to the generated dashboard
    dashboard_url = f"{os.getenv('KIBANA_URL')}/app/dashboards#/view/{dashboard_id}"

    print("Dashboard URL: ", dashboard_url)
    print("Dashboard ID: ", dashboard_id)

except Exception as e:
    print(f"Failed to create dashboard: {str(e)}")</code></pre><p>要执行脚本并生成仪表盘，请在控制台中运行以下命令：</p><pre><code>python <file_name>.py</code></pre><p>最终结果将是这样的</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/e909afbf0e47d9a6e0f7bd07dfb2efcfa5cf06ac-921x715.png" style="width: 100%;" /><h2>结论</h2><p>在将文本转化为代码或将图像转化为代码时，LLM 展示了其强大的视觉能力。仪表盘 API 还能将 JSON 文件转化为仪表盘，而通过 LLM 和一些代码，我们就能将图片转化为 Kibana 仪表盘。</p><p>下一步是通过使用不同的网格设置、仪表盘大小和位置来提高仪表盘视觉效果的灵活性。此外，为更复杂的可视化和可视化类型提供支持也是对该应用程序的有益补充。</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[揭开独特模式的面纱：Elasticsearch 中重要术语聚合指南]]></title>
            <description><![CDATA[了解如何使用重要术语聚合来发现数据中的洞察力。]]></description>
            <link>https://www.elastic.co/search-labs/cn/blog/significant-terms-aggregation-elasticsearch</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/cn/blog/significant-terms-aggregation-elasticsearch</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Alexander Dávila]]></dc:creator>
            <pubDate>Mon, 07 Jul 2025 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/13adbc901837835bb56abf15e377127b017cfac8-1536x1024.png?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>在 Elasticsearch 中，<a href="https://www.elastic.co/docs/reference/aggregations/search-aggregations-bucket-significantterms-aggregation">重要术语聚合</a>超出了<a href="https://www.elastic.co/docs/reference/aggregations/search-aggregations-bucket-terms-aggregation">最常见术语的</a>范围，可在数据集中找到统计上不寻常的值。这使我们能够发现有价值的见解和非显而易见的模式。一个重要的术语集合提供了两个有用参数的响应：</p><ul><li><strong>bg_count（背景计数）： </strong>在父数据集中找到的文件数</li><li><strong>doc_count：</strong>结果数据集中找到的文件数</li></ul><p>例如，在手机销售数据集中，我们可以像这样查找 iPhone 16 销售的重要术语：</p><pre><code>GET phone_sales_analysis/_search
{
 "size": 0,
 "query": {
   "term": {
     "phone_model": {
       "value": "iPhone 16"
     }
   }
 },
 "aggs": {
   "significant_cities": {
     "significant_terms": {
       "field": "city_region",
       "size": 1
     }
   }
 }
}</code></pre><p>然后，答复给了我们：</p><pre><code>{
 "aggregations": {
   "significant_cities": {
     "doc_count": 122,
     "bg_count": 424,
     "buckets": [
       {
         "key": "Houston",
         "doc_count": 12,
         "score": 0.1946481360617346,
         "bg_count": 14
       }

     ]
   }
 }
}</code></pre><p>在整个数据集中，休斯顿既不是排名前十的城市，也不是 iPhone 16 的热门城市。不过，重要术语汇总显示，与其他数据相比，<strong><em> 该城市购买 iPhone 16 的比例过高</em></strong>。让我们深入了解这些数字：</p><ul><li><strong>在最高层：</strong><ul><li><strong>doc_count：122 - </strong>查询总共匹配了 122 份文件</li><li><strong>bg_count：424 - </strong>背景集（所有销售文件）包含 424 份文件</li></ul></li><li><strong>在休斯顿的水桶里：</strong><ul><li><strong>doc_count：12 - </strong>休斯顿出现在 122 条查询结果中的 12 条中</li><li><strong>bg_count：14 - </strong>在背景数据集的 424 份文件中，休斯顿出现在 14 份文件中</li></ul></li></ul><p>这告诉我们，在 424 次总购物中，只有 14 次发生在休斯顿，占总购物次数的 3.3% 。然而，如果我们只看 iPhone 16 的销售情况，就会发现 122 件中有 12 件发生在休斯顿，比整个数据集多 3 倍，即 9.8% ；这是非常重要的！</p><p>以下是可视化效果图：每个城市/地区的销售总额。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/f31335b0b7793650025f941820f238dd35bfb09f-1486x1066.png" style="width: 100%;" /><p>我们可以看到，休斯顿有 14 笔销售，是数据集中销售额第 14 高的城市。</p><p>现在，如果我们只对 iPhone 16 的销售情况进行筛选，休斯顿就有 12 台，成为该机型销售量第二大的城市：</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/6516040db77e6c62af5541a74c723b18008ad3c6-1472x1038.png" style="width: 100%;" /><h2>了解重要术语汇总</h2><p>根据 Elastic 文档，<a href="https://www.elastic.co/docs/reference/aggregations/search-aggregations-bucket-significantterms-aggregation">重要的术语是聚合</a>：</p><p><em>&quot;（查找）在前景集和背景集之间流行度发生显著变化的术语&quot;。</em></p><p>这意味着它使用统计指标，将数据子集（前景集）中某个术语的频率与父数据集（背景集）中同一术语的频率进行比较。这样，评分反映的是统计意义，而不是术语在数据中出现的频率。</p><p>重要术语聚合与普通术语聚合的主要区别在于</p><ul><li>重要术语对数据的子集进行比较，而术语聚合只对查询产生的数据集起作用。</li><li>术语聚合的结果是数据集中最常见的术语，而重要术语的结果则忽略了常见术语，以找出数据集的独特之处。</li><li>重要术语对性能的影响更大，因为它需要从磁盘而不是内存中获取数据，就像术语聚合所做的那样。</li></ul><h2>实际应用（消费者行为分析）</h2><h3>为分析准备数据</h3><p>为了进行分析，我们生成了一个合成的手机销售数据集，其中包括价格、手机规格、购买者的人口统计数据和反馈信息。我们还根据用户的反馈生成了嵌入信息，以便日后进行语义查询。我们使用了 Elasticsearch 上开箱即用的<a href="https://huggingface.co/intfloat/multilingual-e5-small">多语言 e5 小型模型</a>。</p><p></p><p>要在 Elasticsearch 上使用此数据集：</p><ol><li>使用 Kibana<a href="https://www.elastic.co/docs/manage-data/ingest/upload-data-files"> 上传数据文件</a> 功能上传 CSV 文件（可从<a href="https://github.com/Alex1795/significant_terms_blog_dataset/blob/main/phone_sales_analysis_dataset.csv"> 此处 下载）。</a></li><li><a href="https://www.elastic.co/search-labs/blog/chat-with-pdf-elastic-playground#upload-pdfs-to-kibana">如本博客</a>所示，设置一个名为 &quot;嵌入 &quot;的语义字段，使用 <code>multilingual-e5-small model</code></li><li>使用字段类型默认值完成导入（除<code>purchase_date</code> 和<code>user_feedback)</code> 外，每个字段都使用关键字。请确保添加索引名称<code>phone_sales_analysis</code> ，以便能够按原样运行此处提供的查询。</li></ol><p>这项分析的主要重点是发现<strong><em>&quot;iPhone 16 购买者与其他人群的不同之处</em></strong>&quot;，并为营销目的对购买者进行细分。 </p><p>这是数据集中的一份样本文件：</p><pre><code>{
         "customer_type": "Returning",
         "user_feedback": "I have to say, quality is great for the price. The battery life is really good.",
         "upgrade_frequency": "2 years",
         "storage_capacity": "256GB",
         "occupation": "Technology & Data",
         "color": "Phantom Black",
         "gender": "Male",
         "price_paid": 899,
         "previous_brand_loyalty": "Mixed",
         "location_type": "Urban",
         "phone_model": "Samsung Galaxy S24",
         "city_region": "San Francisco Bay Area",
         "@timestamp": "2024-03-15T00:00:00.000-05:00",
         "income_bracket": "75000-100000",
         "purchase_channel": "Online",
         "feedback_sentiment": "positive",
         "education_level": "Bachelor",
         "embedding": "I have to say, quality is great for the price. The battery life is really good.",
         "customer_id": "C001",
         "purchase_date": "2024-03-15",
         "age": 34,
         "trade_in_model": "iPhone 13"
}</code></pre><h3>了解人口模式</h3><p>在此，我们将对一般人群进行分析，并将其与 iPhone 16 用户重要术语汇总的有趣发现进行比较。</p><h4>正常模式</h4><p>为了了解正常的购买模式，我们可以汇总不同领域所有文档的数据。为简单起见，我们将重点探讨购买手机的人的职业。我们可以通过向 Elasticsearch 提出请求来实现这一点。</p><pre><code>GET phone_sales_analysis/_search
{
 "aggs": {
   "occupation_distribution": {
     "terms": {
       "size": 5,
       "field": "occupation"
     }
   }
 },
 "size": 0
}</code></pre><p>这告诉我们，数据集中的主要职业（按记录数计）是</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/99136ddddd7abad5d74481158a04501b6915441b-1518x480.png" style="width: 100%;" /><h4>iPhone 16 用户的使用模式</h4><p>为了了解购买了 iPhone 16 的人有什么不同，让我们在同一字段上运行术语聚合，并在查询中使用过滤器找到这些人，就像这样：</p><pre><code>GET phone_sales_analysis/_search
{
  "query": {
    "term": {
      "phone_model": "iPhone 16"
    }
  },
  "aggs": {
    "occupation_distribution": {
      "terms": {
        "size": 5,
        "field": "occupation"
      }
    }
  },
  "size": 0
}</code></pre><p>因此，iPhone 16 用户的主要职业是</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/36ce86475beb03810c6ad81d7c776d1eec736654-1500x484.png" style="width: 100%;" /><p>我们可以看到，iPhone 16 用户的职业模式与其他型号手机的用户不同。让我们使用 Kibana 来轻松实现结果的可视化：</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/b70ab05bc962a274e1617b6caf20575c489a62d8-1448x1128.png" style="width: 100%;" /><p></p><p>在这张图表中，我们可以看到 iPhone 16 的趋势与整个人群的趋势不同。</p><p>我们可以跳过整个分析，通过一个重要项的汇总，来看看 iPhone 16 用户与普通用户的不同之处：</p><pre><code>GET phone_sales_analysis/_search
{
  "query": {
    "term": {
      "phone_model": "iPhone 16"
    }
  },
  "aggs": {
    "occupation_distribution": {
      "significant_terms": {
        "size": 5,
        "field": "occupation"
      }
    }
  },
  "size": 0
}</code></pre><p>简而言之，我们得到了这样的答复：</p>
            <table style="border-collapse: collapse; margin: 20px 0;">
              <tbody>
                
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">iPhone 16 的职业值</td><td style="border: 1px solid #ccc; padding: 4px;">文件数量</td><td style="border: 1px solid #ccc; padding: 4px;">bg_count</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">职业分布（最高级别）</td><td style="border: 1px solid #ccc; padding: 4px;">122</td><td style="border: 1px solid #ccc; padding: 4px;">424</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">医疗& 保健桶</td><td style="border: 1px solid #ccc; padding: 4px;">45</td><td style="border: 1px solid #ccc; padding: 4px;">57</td>
                    </tr>
              </tbody>
            </table>
          <p>这些回复清楚地表明，iPhone 16 用户有一个不常见的（读作 &quot;重要！&quot;）问题。与普通人相比，医疗&amp; 保健领域的人数更多。让我们看看回复中的数字意味着什么：</p><ul><li><strong>在最高层：</strong><ul><li><strong>doc_count：122 - </strong>查询总共匹配了 122 份文件</li><li><strong>bg_count：424 - </strong>背景集（所有销售文件）包含 424 份文件</li></ul></li><li><strong>在医疗&amp; 保健桶中：</strong><ul><li><strong>doc_count：45 - </strong>&quot;医疗&amp; 保健&quot; 在 122 条查询结果中出现了 45 条</li><li><strong>bg_count：57 - </strong>&quot;医疗&amp; 保健&quot; 在背景数据集中的全部 424 份文件中出现 57 份</li></ul></li></ul><p>在 424 位买家中，有 57 位在医疗&amp; 保健领域工作，即 13.44% 。但是，当我们查看 iPhone 16 的购买者时，122 位购买者中有 45 位从事医疗&amp; ，即 36.88% 。这意味着在 iPhone 16 用户中，从事医疗&amp; 保健工作的可能性要高出一倍！</p><p>我们可以将同样的分析应用于其他领域（年龄、地点、收入阶层等），从而发现更多有关 iPhone 16 用户独特之处的信息。 </p><h3>消费者细分</h3><p>我们可以利用重要术语聚合来提取产品、类别和客户群之间的关系洞察。为此，我们为感兴趣的类别建立一个父聚合。我们还使用了重要术语和普通术语子分类，以发现对该类别的有趣见解，并将其与该职业中大多数人使用的术语进行比较。</p><p>例如，让我们看看某些工作领域的人喜欢什么：</p><ol><li>为了更清楚地进行分析，我们将搜索范围限制在 3 个工作领域：[&quot;行政&amp; 支持&quot;,&quot;技术&amp; 数据&quot;,&quot;医疗&amp; 保健&quot;]</li><li>在汇总方面，我们首先按职业进行术语汇总</li><li>增加一个子分类：按手机型号分类--查找在各个领域工作的用户正在购买哪些手机型号</li><li>添加第二个子分类：按手机型号分类的重要术语，以找出每个工作领域中的特殊型号</li></ol><pre><code>GET phone_sales_analysis/_search
{
 "query": {
   "terms": {
     "occupation": [
       "Administrative & Support",
       "Technology & Data",
       "Medical & Healthcare"
     ]
   }
 },
 "aggs": {
   "occupations": {
     "terms": {
       "size": 15,
       "field": "occupation"
     },
     "aggs": {
       "general_models": {
         "terms": {
           "field": "phone_model"
         }
       },
       "significant_models": {
         "significant_terms": {
           "field": "phone_model"
         }
       }
     }
   }
 },
 "size": 0
}</code></pre><p>让我们来分析一下汇总结果：</p><p><strong>职业</strong>行政&amp; 支持</p><p><strong>术语汇总</strong></p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/a4ad519c9013867a3f4cee032160eadd8a47804a-1506x398.png" style="width: 100%;" /><p><strong>重要术语汇总</strong></p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/e5604fa8036667bdfe733576a5e7c6153760dd3a-306x220.png" style="width: 100%;" /><p>从该表中我们可以推断出，该职业的趋势与整个人口的趋势之间没有显著差异</p><p><strong>职业</strong>：技术&amp; 数据</p><p><strong>术语汇总</strong></p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/13b09039bb7d183276451007d2d69dc190b1d3c0-1508x836.png" style="width: 100%;" /><p></p><p><strong>重要术语汇总</strong></p><p>文件总数424</p><p>该职业的文件：71</p>
            <table style="border-collapse: collapse; margin: 20px 0;">
              <tbody>
                
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">手机型号</td><td style="border: 1px solid #ccc; padding: 4px;">doc_count （本职业中的本模型）</td><td style="border: 1px solid #ccc; padding: 4px;">bg_count （所有文件中都有此模型）</td><td style="border: 1px solid #ccc; padding: 4px;">% 在所有文件中</td><td style="border: 1px solid #ccc; padding: 4px;">% 从事这一职业</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">谷歌 Pixel 8</td><td style="border: 1px solid #ccc; padding: 4px;">12</td><td style="border: 1px solid #ccc; padding: 4px;">220</td><td style="border: 1px solid #ccc; padding: 4px;">5.19%</td><td style="border: 1px solid #ccc; padding: 4px;">16.90%</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">OnePlus 11</td><td style="border: 1px solid #ccc; padding: 4px;">9</td><td style="border: 1px solid #ccc; padding: 4px;">14</td><td style="border: 1px solid #ccc; padding: 4px;">3.30%</td><td style="border: 1px solid #ccc; padding: 4px;">12.68%</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">OnePlus 12 Pro</td><td style="border: 1px solid #ccc; padding: 4px;">3</td><td style="border: 1px solid #ccc; padding: 4px;">3</td><td style="border: 1px solid #ccc; padding: 4px;">0.71%</td><td style="border: 1px solid #ccc; padding: 4px;">4.23%</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">谷歌 Pixel 8 Pro</td><td style="border: 1px solid #ccc; padding: 4px;">9</td><td style="border: 1px solid #ccc; padding: 4px;">21</td><td style="border: 1px solid #ccc; padding: 4px;">4.95%</td><td style="border: 1px solid #ccc; padding: 4px;">12.68%</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">无手机 2</td><td style="border: 1px solid #ccc; padding: 4px;">5</td><td style="border: 1px solid #ccc; padding: 4px;">8</td><td style="border: 1px solid #ccc; padding: 4px;">1.89%</td><td style="border: 1px solid #ccc; padding: 4px;">7.04%</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">三星 Galaxy Z Fold5</td><td style="border: 1px solid #ccc; padding: 4px;">4</td><td style="border: 1px solid #ccc; padding: 4px;">6</td><td style="border: 1px solid #ccc; padding: 4px;">1.42%</td><td style="border: 1px solid #ccc; padding: 4px;">5.63%</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">OnePlus 12</td><td style="border: 1px solid #ccc; padding: 4px;">8</td><td style="border: 1px solid #ccc; padding: 4px;">20</td><td style="border: 1px solid #ccc; padding: 4px;">4.72%</td><td style="border: 1px solid #ccc; padding: 4px;">11.27%</td>
                    </tr>
              </tbody>
            </table>
          <p><strong>职业</strong>：医疗&amp; 保健</p><p><strong>术语汇总</strong></p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/b008e996742fc0bb48dc6bacff17cfbc56cf0d73-1492x398.png" style="width: 100%;" /><p><strong>重要术语汇总</strong></p><p>文件总数424</p><p>该职业的文件：57</p>
            <table style="border-collapse: collapse; margin: 20px 0;">
              <tbody>
                
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">手机型号</td><td style="border: 1px solid #ccc; padding: 4px;">doc_count （本职业中的本模型）</td><td style="border: 1px solid #ccc; padding: 4px;">bg_count （所有文件中都有此模型）</td><td style="border: 1px solid #ccc; padding: 4px;">% 在所有文件中</td><td style="border: 1px solid #ccc; padding: 4px;">% 从事这一职业</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">iPhone 16</td><td style="border: 1px solid #ccc; padding: 4px;">45</td><td style="border: 1px solid #ccc; padding: 4px;">122</td><td style="border: 1px solid #ccc; padding: 4px;">28.77%</td><td style="border: 1px solid #ccc; padding: 4px;">78.95%</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">iPhone 15 Pro Max</td><td style="border: 1px solid #ccc; padding: 4px;">3</td><td style="border: 1px solid #ccc; padding: 4px;">13</td><td style="border: 1px solid #ccc; padding: 4px;">3.07%</td><td style="border: 1px solid #ccc; padding: 4px;">5.26%</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">iPhone 15</td><td style="border: 1px solid #ccc; padding: 4px;">7</td><td style="border: 1px solid #ccc; padding: 4px;">40</td><td style="border: 1px solid #ccc; padding: 4px;">9.43%</td><td style="border: 1px solid #ccc; padding: 4px;">12.28%</td>
                    </tr>
              </tbody>
            </table>
          <p>让我们看看这些数据告诉了我们什么故事：</p><ul><li>医疗&amp; 医疗保健专业人士更喜欢 iPhone 16，而且普遍倾向于使用苹果手机。</li><li>技术&amp; 数据专业人士更喜欢高端安卓手机，但不一定使用三星品牌。在这一类别中，iPhone 也有相当大的发展趋势。</li><li>行政管理&amp; 支持专业人员更喜欢三星和谷歌手机，但没有形成强烈而独特的趋势。</li></ul><h3>重要术语汇总和混合搜索</h3><p>混合搜索结合了文本搜索和语义结果，可提供更好的搜索体验。在这种情况下，一个重要的术语聚合可以通过回答问题来深入了解上下文感知搜索的结果：<strong>与所有文档相比，这个数据集有什么特别之处？</strong>为了展示这一特点，让我们看看当用户谈论良好性能时，哪些模型的代表性过高： </p><ul><li>让我们建立一个语义查询，通过字段嵌入找到最接近输入 &quot;性能良好 &quot;的用户反馈</li><li>我们还将在文本字段 user_feedback 中使用相同的术语进行文本搜索</li><li>我们还将添加一个重要术语查询，以找到在这些结果中出现频率高于完整数据集的手机型号<br/></li></ul><pre><code>GET phone_sales_analysis/_search
{
 "retriever": {
   "rrf": {
     "retrievers": [
       {
         "standard": {
           "query": {
             "bool": {
               "must": [
                 {
                   "match": {
                     "user_feedback": {
                       "query": "good performance",
                       "operator": "and"
                     }
                   }
                 }
               ]
             }
           }
         }
       },
       {
         "standard": {
           "query": {
             "semantic": {
               "field": "embedding",
               "query": "good performance"
             }
           }
         }
       }
     ],
    "rank_window_size": 20
   }
 },
 "aggs": {
   "Models": {
     "significant_terms": {
       "field": "phone_model"
     }
   }
 }
}</code></pre><p>让我们来看一个匹配文件的例子：</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/4cb488097a382f0c28c21540db4f593d23633473-1600x162.png" style="width: 100%;" /><p>这就是我们得到的答复：</p><pre><code>{
  "took": 388,
  "timed_out": false,
  "_shards": {
    "total": 1,
    "successful": 1,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": {
      "value": 20,
      "relation": "eq"
    },
    "max_score": 0.016393442,
    "hits": [...]
  },
  "aggregations": {
    "Models": {
      "doc_count": 20,
      "bg_count": 424,
      "buckets": [
        {
          "key": "iPhone 15",
          "doc_count": 5,
          "score": 0.4125,
          "bg_count": 40
        }
      ]
    }
  }
}</code></pre><p></p><p>这告诉我们，虽然 iPhone 15 在总共 424 篇文档中出现了 40 次（占文档总数的 9.4% ），但在符合语义搜索 &quot;良好表现 &quot;的 20 篇文档（占文档总数的 25% ）中却能找到 5 次。因此，我们可以得出这样的结论：在谈论良好性能时，发现 iPhone 15 的可能性是偶然发现的 2.7 倍。</p><h2>结论</h2><p>重要术语聚合可以通过将数据集与全局文档进行比较，发现数据集的独特细节。这可以揭示数据中意想不到的关系，而不仅仅是出现次数的计算。例如，我们可以在各种使用案例中应用重要术语，从而实现非常有趣的功能：</p><ul><li>在<a href="https://www.elastic.co/blog/significant-terms-aggregation#credit"> 侦查 欺诈行为时找出模式 --识别被盗信用卡的常见交易。</a></li><li>从用户评论中洞察品牌质量--发现差评过多的品牌。</li><li><a href="https://www.elastic.co/docs/reference/aggregations/search-aggregations-bucket-significantterms-aggregation#_use_on_free_text_fields">发现 </a>分类错误的文档--发现属于某个类别（术语过滤器）但在描述中使用了该类别不常用词的文档（重要术语汇总）。</li></ul>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[使用 JavaScript、Mastra 和 Elasticsearch 构建代理 RAG 助手]]></title>
            <description><![CDATA[了解如何在 JavaScript 生态系统中构建人工智能代理]]></description>
            <link>https://www.elastic.co/search-labs/cn/blog/agentic-rag</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/cn/blog/agentic-rag</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[JD Armada]]></dc:creator>
            <pubDate>Tue, 01 Jul 2025 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/8132ed781c1ea5d46ca244182f421ed5c721f23b-1200x628.png?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>我是在激烈的高风险梦幻篮球联赛中萌生这个想法的。我想知道<em>我能否建立一个人工智能代理，帮助我在每周的对阵中占据优势？当然可以！</em></p><p>在本篇文章中，我们将探讨如何使用<a href="https://mastra.ai/en/docs">Mastra</a>和一个轻量级 JavaScript 网络应用程序来构建一个代理 RAG 助手，并与其进行交互。通过将该代理连接到 Elasticsearch，我们可以让它访问结构化的球员数据，并能够运行实时统计汇总，从而为您提供基于球员统计数据的推荐。请访问 GitHub<a href="https://github.com/jdarmada/nba-ai-assistant-js.git">软件源</a>，了解如何克隆和运行应用程序；<a href="https://github.com/jdarmada/nba-ai-assistant-js/blob/main/README.md">README</a>提供了相关说明。 </p><p>下面是全部组装好后的样子：</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/1c73bd1dc1b5fe54f025c7a2b7c322acc9122f3a-1999x1393.png" style="width: 100%;" /><p>注：本博文以 &quot;<a href="https://www.elastic.co/search-labs/blog/ai-agents-ai-sdk-elasticsearch">使用 AI SDK 和 Elastic 构建 AI 代理</a>&quot;为基础。如果您是第一次接触人工智能代理及其用途，请从这里开始。<br/></p><h2><strong>结构概述</strong></h2><p>该系统的核心是一个大型语言模型（LLM），它充当了代理的推理引擎（大脑）。它能解释用户输入，决定调用哪些工具，并协调生成相关响应所需的步骤。</p><p>代理本身由 JavaScript 生态系统中的代理框架 Mastra 搭建脚手架。Mastra 将 LLM 与后端基础设施封装在一起，将其作为 API 端点公开，并提供了一个用于定义工具、系统提示和代理行为的接口。</p><p>在前端，我们使用<a href="https://vite.dev/guide/">Vite</a>快速搭建了一个 React 网络应用程序，它提供了一个聊天界面，用于向代理发送查询并接收其回复。</p><p>最后，我们还有 Elasticsearch，它存储了代理可以查询和汇总的球员统计数据和对阵数据。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/443bdc00d84ed1dd49e9f9e431e86ca4b0892563-1999x977.png" style="width: 100%;" /><h2><strong>背景</strong></h2><p>让我们来回顾一下几个基本概念：</p><h3><strong>什么是代理 RAG？</strong></h3><p>人工智能代理可以与其他系统互动，独立运行，并根据其定义的参数执行操作。代理式 RAG 将人工智能代理的自主性与检索增强生成的原则相结合，使 LLM 能够选择调用哪些工具和使用哪些数据作为上下文来生成响应。<a href="https://www.elastic.co/search-labs/blog/retrieval-augmented-generation-rag">点击此处</a>了解有关 RAG 的更多信息。</p><h3><strong>选择框架，为什么要超越 AI-SDK？</strong></h3><p>目前有许多人工智能代理框架，你可能听说过<a href="https://www.elastic.co/search-labs/blog/using-crewai-with-elasticsearch">CrewAI</a>、<a href="https://www.elastic.co/search-labs/blog/using-autogen-with-elasticsearch">AutoGen</a>和<a href="https://www.elastic.co/search-labs/blog/build-rag-workflow-langgraph-elasticsearch">LangGraph</a> 等比较流行的框架。这些框架大多有一套共同的功能，包括支持不同的模型、工具使用和内存管理。</p><p>下面是哈里森-蔡斯（LangChain 首席执行官）的框架<a href="https://docs.google.com/spreadsheets/d/1B37VxTBuGLeTSPVWtz7UMsCdtXrqV5hCjWkbHN8tfAo/edit?gid=0#gid=0">比较表</a>。</p><p>让我对 Mastra 产生兴趣的是，它是一个 JavaScript 优先框架，专为全栈开发人员设计，可以轻松地将代理集成到他们的生态系统中。Vercel 的 AI-SDK 也能实现大部分功能，但 Mastra 的优势在于当项目包含更复杂的代理工作流程时。Mastra 增强了 AI-SDK 设置的基本模式，在本项目中，我们将同时使用它们。</p><h3><strong>框架和模型选择考虑因素</strong></h3><p>虽然这些框架可以帮助您快速构建人工智能代理，但也有一些缺点需要考虑。例如，在使用人工智能代理或任何抽象层之外的其他框架时，你会失去一些控制权。如果 LLM 没有正确使用工具，或者做了一些你不希望它做的事情，抽象化就会增加调试难度。不过，在我看来，这种折衷还是值得的，尤其是因为这些框架的发展势头越来越好，而且还在不断迭代。</p><p>同样，这些框架与模型无关，这意味着您可以即插即用不同的模型，但请记住，模型在不同的数据集上训练出来的结果是不同的，反过来，它们给出的响应也是不同的。有些型号甚至不支持工具调用。因此，可以切换和测试不同的型号，看看哪种型号能给您带来最好的响应，但请记住，您很可能需要为每种型号重写系统提示。例如，使用 Llama3.3与 GPT-4o 相比，它需要更多的提示和具体指令才能得到您想要的回应。</p><h3><strong>NBA 梦幻篮球</strong></h3><p>梦幻篮球就是和你的一群朋友组成一个联盟（警告，这可能会影响你们的友谊，这取决于你们的竞争有多激烈），通常会涉及到一些金钱问题。然后，你们每个人起草一支由 10 名球员组成的队伍，每周轮流与另一位朋友的 10 名球员比赛。您的总得分取决于您的每位球员在一周内与对手的对战情况。</p><p>如果您队中有球员受伤、停赛等，会有一份自由球员名单供您选择。这也是梦幻体育中最难思考的地方，因为你只有有限的选择权，而每个人都在不断地寻找最好的球员。</p><p>这正是我们的 NBA AI 助手大显身手的地方，尤其是在您必须迅速决定选择哪位球员的情况下。助手无需手动查找球员在与特定对手比赛时的表现，而是可以快速找到这些数据并比较平均值，从而为您提供明智的建议。</p><p>现在，您已经了解了代理 RAG 和 NBA 梦幻篮球的一些基本知识，让我们来看看它的实际应用。</p><h2><strong>建设项目</strong></h2><p>如果您遇到任何问题或不想从头开始构建，请参考<a href="https://github.com/jdarmada/nba-ai-assistant-js.git">软件仓库</a>。</p><h3><strong>我们的内容</strong></h3><ol><li><strong>为项目搭建脚手架：</strong><ol><li><strong>后端（Mastra）：</strong>使用 npx create mastra@latest 构建后端并定义代理逻辑。</li><li><strong>前端（Vite + React）：</strong>使用 npm create vite@latest 构建与代理交互的前端聊天界面。</li></ol></li><li><strong>设置环境变量</strong><ol><li>安装 dotenv 来管理环境变量。</li><li>创建 .env文件，并提供所需的变量。</li></ol></li><li><strong>设置 Elasticsearch</strong><ol><li>启动 Elasticsearch 集群（本地或云端）。</li><li>安装官方 Elasticsearch 客户端。</li><li>确保环境变量可访问。</li><li>建立与客户端的连接。</li></ol></li><li><strong>将 NBA 数据批量导入 Elasticsearch</strong><ol><li>创建具有适当映射的索引，以启用聚合。</li><li>将 CSV 文件中的玩家游戏统计数据批量导入 Elasticsearch 索引。</li></ol></li><li><strong>定义 Elasticsearch 聚合</strong><ol><li>查询计算与特定对手的历史平均值。</li><li>查询计算对特定对手的赛季平均分。</li></ol></li><li><strong>播放器比较实用程序文件</strong><ol><li>整合辅助函数和 Elasticsearch 聚合。</li></ol></li><li><strong>建立代理</strong><ol><li>添加代理定义和系统提示。</li><li>安装 zod 和定义工具。</li><li>添加中间件设置以处理 CORS。</li></ol></li><li><strong>整合前端</strong><ol><li>使用 AI-SDK 的 useChat 与代理互动。</li><li>创建用户界面，以保存格式正确的对话。</li></ol></li><li><strong>运行应用程序</strong><ol><li>同时启动后端（Mastra 服务器）和前端（React 应用程序）。</li><li>查询和使用示例。</li></ol></li><li><strong>下一步是什么？让代理更智能</strong><ol><li>增加语义搜索功能，提供更具洞察力的建议。</li><li>将搜索逻辑移至 Elasticsearch MCP（模型上下文协议）服务器，从而启用动态查询。</li></ol></li></ol><h3><strong>准备工作</strong></h3><ul><li><strong>Node.js 和 npm</strong>：后端和前端都在 Node 上运行。确保已安装 Node 18+ 和 npm v9+（与 Node 18+ 绑定）。</li><li><strong>Elasticsearch 集群：</strong>本地或云端的活动 Elasticsearch 集群。</li><li><strong>OpenAI API 密钥</strong>：在<a href="https://platform.openai.com/api-keys">OpenAI 开发人员门户网站</a>的 API 密钥页面上生成一个。</li></ul><p></p><h3><strong>项目结构</strong></h3><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/1c0bde11ad0eead523a95e03b9b905aa776e3fd1-1420x934.png" style="width: 100%;" /><h4><strong>步骤 1：为项目搭建脚手架</strong></h4><ol><li>首先，创建目录 nba-ai-assistant-js，并在其中导航： </li></ol><pre><code>mkdir nba-ai-assistant-js && cd nba-ai-assistant-js</code></pre><p><strong>后台</strong></p><ol><li>使用 Mastra 创建工具并执行命令： </li></ol><pre><code>npx create-mastra@latest</code></pre><p>2.你的终端应该会收到一些提示，第一个提示是命名项目后台：</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/de2725031ed6837db99a979efcdd0ece1e197dbb-608x84.png" style="width: 100%;" /><p>3.接下来，我们将保留存储 Mastra 文件的默认结构，因此输入<code>src/</code>.</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/88919d9ff1852126395e1fcd700ecb1b59aac63c-866x116.png" style="width: 100%;" /><p>4.然后，我们将选择 OpenAI 作为默认的 LLM 提供商。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/2328761e769f3ded134e5a21e8a0bf8f41e88f68-404x210.png" style="width: 100%;" /><p>5.最后，它会要求你提供 OpenAI API 密钥。现在，我们选择跳过选项，稍后在<code> .env</code> 文件中提供。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/0662de9bd28758e377e4c63df8d08b479068ce63-444x120.png" style="width: 100%;" /><p><strong>前台</strong></p><ol><li>返回根目录，使用此命令运行<a href="https://vite.dev/guide/">Vite 创建工具</a>： <code>npm create vite@latest frontend -- --template react</code></li></ol><p>这将创建一个名为<code>frontend</code> 的轻量级 React 应用程序，并为 React 提供特定模板。</p><p>如果一切顺利，在你的项目目录中，你应该会看到一个存放 Mastra 代码的后台目录和一个存放 React 应用程序的<code>frontend</code> 目录。</p><p></p><h4><strong>步骤 2：设置环境变量</strong></h4><ol><li>为了管理敏感键，我们将使用<code>dotenv</code> 软件包从 .env 中加载环境变量。锉刀导航至后台目录，安装<code>dotenv</code> ：</li></ol><pre><code>cd backend
npm install dotenv --save</code></pre><p>2.在后台目录中，会提供一个 example.env 文件，其中包含需要填写的相应变量。如果您自己创建，请确保包含以下变量：</p><pre><code># OpenAI Configuration
OPENAI_API_KEY=your_openai_api_key_here

# Elasticsearch Configuration
ELASTIC_ENDPOINT=your_elasticsearch_endpoint_here
ELASTIC_API_KEY=your_elasticsearch_api_key_here
</code></pre><p></p><p>注意：通过在<code>.gitignore</code> 中添加<code>.env</code> ，确保将此文件排除在版本控制之外。</p><h4><strong>第 3 步：设置 Elasticsearch</strong></h4><p>首先，您需要一个活动的 Elasticsearch 集群。有两种选择：</p><ul><li><strong>选项 A：使用 Elasticsearch 云</strong><ul><li>注册<a href="https://cloud.elastic.co/registration">弹性云</a></li><li>创建新的部署</li><li>获取端点 URL 和 API 密钥（已编码）</li></ul></li><li><strong>选项 B：在本地运行 Elasticsearch</strong><ul><li>在本地安装并运行 Elasticsearch</li><li>使用 http://localhost:9200 作为终端</li><li>生成 API 密钥</li></ul></li></ul><p></p><p><strong>在后台安装 Elasticsearch 客户端：</strong></p><ol><li>首先，在后台目录中安装 Elasticsearch 官方客户端：</li></ol><pre><code>npm install @elastic/elasticsearch</code></pre><p>2.然后创建一个 lib 目录来存放可重复使用的函数，并导航进入该目录：</p><pre><code>mkdir lib && cd lib</code></pre><p>3.在其中创建一个名为<a href="https://github.com/jdarmada/nba-ai-assistant-js/blob/main/backend/lib/elasticClient.js">elasticClient.js</a> 的新文件。该文件将初始化 Elasticsearch 客户端，并在整个项目中公开使用。</p><p>4.由于我们使用的是 ECMAScript 模块 (ESM)，因此无法使用__dirname and __文件名。为确保您的环境变量能从 .env文件，将此设置添加到文件顶部：</p><pre><code>import { config } from 'dotenv';
import { fileURLToPath } from 'url';
import { dirname, join } from 'path';
import { Client } from '@elastic/elasticsearch';

// Grab current directory and load .env from backend folder
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const envPath = join(__dirname, '../.env');

// Load environment variables from the correct path
config({ path: envPath });</code></pre><p>5.现在，使用环境变量初始化 Elasticsearch 客户端并检查连接：</p><pre><code>//Elastic client Initialization, make sure environment variables are being loaded in correctly
const config= {
    node: `${process.env.ELASTIC_ENDPOINT}`,
    auth: {
        apiKey: `${process.env.ELASTIC_API_KEY}`,
    },
};

export const elasticClient = new Client(config);

//Check if the client is connected
async function checkConnection() { 
    try {
        const info = await elasticClient.info();
        console.log('Elasticsearch is connected:', info);
    } catch (error) {
        console.error('Elasticsearch connection error:', error);
    }
}

checkConnection();
</code></pre><p>现在，我们可以将此客户端实例导入任何需要与 Elasticsearch 集群交互的文件。</p><p></p><h4><strong>第 4 步：将 NBA 数据批量导入 Elasticsearch</strong></h4><p><strong>数据集：</strong></p><p>在本项目中，我们将引用软件版本<a href="https://github.com/jdarmada/nba-ai-assistant-js/tree/main/backend">中后端/数据</a>目录下的数据集。我们的 NBA 助手将以这些数据为知识基础，进行统计比较并生成建议。</p><ul><li><a href="https://github.com/jdarmada/nba-ai-assistant-js/blob/main/backend/data/sample_nba_data.csv">sample_player_game_stats.csv</a>- NBA 球员职业生涯的球员比赛统计数据样本（如得分、篮板、抢断等）。我们将使用该数据集进行聚合。(注：这是模拟数据，为演示目的而预先生成，并非来自 NBA 官方来源）。</li><li><a href="https://github.com/jdarmada/nba-ai-assistant-js/blob/main/backend/data/playerAndTeamInfo.js">playerAndTeamInfo.js</a>- 替代通常由应用程序接口调用提供的球员和球队元数据，以便代理能将球员和球队名称与 ID 匹配。由于我们使用的是样本数据，我们不希望从外部应用程序接口获取数据造成开销，因此我们硬编码了一些代理可以引用的值。</li></ul><p></p><p><strong>实施：</strong></p><ol><li>在<code>backend/lib</code> 目录中，创建名为<a href="https://github.com/jdarmada/nba-ai-assistant-js/blob/main/backend/lib/playerDataIngestion.js">playerDataIngestion.js</a> 的文件。</li><li>设置导入、解析 CSV 文件路径并设置解析。同样，由于我们使用的是 ESM，因此需要重构<code>__dirname</code> 来解析 CSV 样本的路径。此外，我们还将导入<a href="http://node.js/">Node.js</a>的内置模块<code>fs</code> 和<code>readline</code> 逐行解析给定的 CSV 文件。</li></ol><pre><code>import fs from 'fs';
import readline from 'readline';
import path from 'path';
import { fileURLToPath } from 'url';
import { elasticClient } from './elasticClient.js';

const indexName = 'sample-nba-player-data'; //Replace with your preferred index name

//Since we are using ES modules __dirname and __filename don't exist, so this is a workaround that allows us to use the absolute file path for our sample data.
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const filePath = path.resolve(__dirname, '../data/sample_nba_data.csv');</code></pre><p>这样，当我们进入批量摄取步骤时，就能高效地读取和解析 CSV。</p><p>3.创建具有适当映射的索引。虽然 Elasticsearch 可以通过<a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/dynamic">动态映射</a>自动推断字段类型，但我们希望在此明确说明，以便每个统计信息都被视为数字字段。这一点很重要，因为稍后我们将使用这些字段进行聚合。我们还希望对得分、篮板等统计数据使用<code>float </code>类型，以确保包含小数值。最后，我们要添加映射属性<code>dynamic: &#x27;strict&#x27;</code> ，这样 Elasticsearch 就不会动态映射未识别的字段。 <br/></p><pre><code>// Function to create an index with mappings
async function createIndex() {
    try {
        // Check if the index already exists
        const exists = await elasticClient.indices.exists({ index: indexName });

        if (exists) {
            console.log(`Index "${indexName}" already exists, deleting it now.`);
            await elasticClient.indices.delete({ index: indexName });
            console.log(`Deleted index "${indexName}".`);
        }
        // Create the index with mappings
        const response = await elasticClient.indices.create({
            index: indexName,
            body: {
                mappings: {
                    dynamic: 'strict', // Prevent dynamic mapping
                    properties: {
                        game_id: { type: 'integer' },
                        game_date: { type: 'date' },
                        player_id: { type: 'integer' },
                        player_full_name: { type: 'text' },
                        player_team_id: { type: 'integer' },
                        player_team_name: { type: 'text' },
                        home_team: { type: 'boolean' },
                        opponent_team_id: { type: 'integer' },
                        opponent_team_name: { type: 'text' },
                        points: { type: 'float' },
                        rebounds: { type: 'float' },
                        assists: { type: 'float' },
                        steals: { type: 'float' },
                        blocks: { type: 'float' },
                        fg_percentage: { type: 'float' },
                        minutes_played: { type: 'float' },
                    },
                },
            },
        });

        console.log('Index created:', response);
        return true;
    } catch (error) {
        console.error('Error creating index:', error);
        return false;
    }
}
</code></pre><p>4.添加将 CSV 数据批量导入 Elasticsearch 索引的函数。在代码块内，我们跳过标题行。然后，用逗号分隔每个行项目，并将其推入文档对象。这一步骤还可以清洁它们，并确保它们是正确的类型。接下来，我们将文档连同索引信息一起推送到 bulkBody 数组中，作为批量摄取到 Elasticsearch 的有效载荷。</p><pre><code>async function bulkIngestCsv(filePath) {
    const readStream = fs.createReadStream(filePath);
    const rl = readline.createInterface({
        input: readStream,
        crlfDelay: Infinity,
    });

    const bulkBody = [];
    let lineNum = 0;

    //Skip the header line
    let headerLine = true;
    for await (const line of rl) {
        if (headerLine) {
            headerLine = false;
            continue;
        }
        lineNum++;

        // Split the line by comma and remove whitespace
        const [
            game_id,
            game_date,
            player_id,
            player_full_name,
            player_team_id,
            player_team_name,
            home_team,
            opponent_team_id,
            opponent_team_name,
            points,
            rebounds,
            assists,
            steals,
            blocks,
            fg_percentage,
            minutes_played,
        ] = line.split(',');

        // Create a document object
        const document = {
            game_id: parseInt(game_id),
            game_date: game_date.trim(),
            player_id: parseInt(player_id),
            player_full_name: player_full_name.trim(),
            player_team_id: parseInt(player_team_id),
            player_team_name: player_team_name.trim(),
            home_team: home_team.trim() === 'True', // Converts True/False into a boolean
            opponent_team_id: parseInt(opponent_team_id),
            opponent_team_name: opponent_team_name.trim(),
            points: parseFloat(points),
            rebounds: parseFloat(rebounds),
            assists: parseFloat(assists),
            steals: parseFloat(steals),
            blocks: parseFloat(blocks),
            fg_percentage: parseFloat(fg_percentage),
            minutes_played: parseFloat(minutes_played),
        };

        // Prepare the bulk operation format
        bulkBody.push({ index: { _index: indexName } });
        bulkBody.push(document);
    }

    console.log(`Parsed ${lineNum} lines from CSV`);
</code></pre><p>5.然后，我们可以通过<code>elasticClient.bulk()</code> 使用 Elasticsearch 的<a href="https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-bulk">批量 API</a>，在一次请求中摄取多个文档。下面的错误处理结构可以让你计算有多少文档未能被摄取，有多少文档被成功摄取。</p><pre><code>try {
        // Perform the bulk request
        const response = await elasticClient.bulk({ body: bulkBody });

        if (response.errors) {
            console.log('Bulk Ingestion had some hiccups:');

            // Count successful vs failed operations
            let successCount = 0;
            let errorCount = 0;
            const errorDetails = [];

            response.items.forEach((item, index) => {
                const operation = item.index || item.create || item.update || item.delete;
                if (operation.error) {
                    errorCount++;
                    errorDetails.push({
                        document: index + 1,
                        error: operation.error,
                    });
                } else {
                    successCount++;
                }
            });

            console.log(`Successfully indexed: ${successCount} documents`);
            console.log(`Failed to index: ${errorCount} documents, here are the details`, errorDetails);

        } else {
            console.log(`Bulk Ingestion fully successful!`);
        }

    } catch (error) {
        console.error('Error performing bulk ingestion:', error);
    }
}
</code></pre><p>6.运行下面的<code>main()</code> 函数，依次运行<code>createIndex()</code> 和<code>bulkIngestCsv()</code> 函数。</p><pre><code>// Run this function
async function main() {
    const result = await createIndex();
    if (!result) {
        console.error('Index setup failed. Aborting.');
        return;
    }

    await bulkIngestCsv(filePath);
    console.log('Bulk ingestion completed!');
}

main();
</code></pre><p>如果看到控制台日志显示批量摄取成功，请在 Elasticsearch 索引上执行快速检查，查看是否确实成功摄取了文档。</p><h4><strong>步骤 5：定义 Elasticsearch 聚合和合并</strong></h4><p>这些将是我们为人工智能代理定义工具时使用的主要功能，以便对球员的统计数据进行比较。</p><p>1.导航至<code>backend/lib</code> 目录，创建名为<a href="https://github.com/jdarmada/nba-ai-assistant-js/blob/main/backend/lib/elasticAggs.js">elasticAggs.js</a> 的文件。</p><p>2.添加下面的查询，计算球员对特定对手的历史平均分。该查询使用<code>bool</code> <a href="https://www.elastic.co/search-labs/tutorials/search-tutorial/full-text-search/filters">过滤器</a>，其中包含两个条件：一个匹配<code>player_id</code> ，另一个匹配<code>opponent_team_id</code> ，以便只检索相关游戏。我们不需要返回任何文档，我们只关心聚合，因此我们设置<code>size:0</code> 。在<code>aggs</code> 块下，我们在<code>points, rebounds, assists, steals, blocks</code> 和<code>fg_percentage</code> 等字段上并行运行多个度量<a href="https://www.elastic.co/docs/explore-analyze/query-filter/aggregations">聚合</a>，以计算它们的平均值。LLM 的计算可能会出现偏差，而这一功能可将计算过程卸载到 Elasticsearch，确保我们的 NBA AI 助手能够访问准确的数据。</p><pre><code>export async function getHistoricalAveragesAgainstOpponent(player_id, opponent_team_id) {
    try {
        //Query for Historical Averages
        const historicalQuery = await elasticClient.search({
            index: 'sample-nba-player-data', 
            size: 0,
            query: {
                bool: {
                    must: [
                        {
                            term: {
                                player_id: {
                                    value: player_id,
                                },
                            },
                        },
                        {
                            term: {
                                opponent_team_id: {
                                    value: opponent_team_id,
                                },
                            },
                        },
                    ],
                },
            },
            aggs: {
                avg_points: { avg: { field: 'points' } },
                avg_rebounds: { avg: { field: 'rebounds' } },
                avg_assists: { avg: { field: 'assists' } },
                avg_steals: { avg: { field: 'steals' } },
                avg_blocks: { avg: { field: 'blocks' } },
             avg_fg_percentage: { avg: { field: 'fg_percentage' } },
            },
        });

        return {
            points: historicalQuery.aggregations.avg_points.value || 0,
            rebounds: historicalQuery.aggregations.avg_rebounds.value || 0,
            assists: historicalQuery.aggregations.avg_assists.value || 0,
            steals: historicalQuery.aggregations.avg_steals.value || 0,
            blocks: historicalQuery.aggregations.avg_blocks.value || 0,
            fgPercentage: historicalQuery.aggregations.avg_fg_percentage.value || 0,
        };
    } catch (error) {
        console.error('Query error from getHistoricalAveragesAgainstOpponent function:', error);
        return { error: 'Queries failed in getting historical averages against opponent.' };
    }
}
</code></pre><p>3.要计算一名球员对阵特定对手的赛季平均值，我们将使用与历史查询几乎相同的查询方式。该查询的唯一区别是<code>bool</code> 过滤器对<code>game_date</code> 附加了一个条件。<code>game_date</code> 必须在当前 NBA 赛季的范围内。在这种情况下，范围介于<code>2024-10-01</code> 和<code>2025-06-30</code> 之间。下面这个额外的条件确保了后面的汇总将只分离出本赛季的比赛。<br/></p><pre><code>        {
                            range: {
                    //Range for this season, change to match current season
                                game_date: {
                                    gte: '2024-10-01',
                                    lte: '2025-06-30',
                                },
                            },
</code></pre><h4><strong>步骤 6：球员比较实用程序</strong></h4><p>为了保持代码的模块化和可维护性，我们将创建一个实用程序文件来整合元数据辅助函数和 Elasticsearch 聚合。这将为特工使用的主要工具提供动力。稍后再详述：</p><p>1.在<code>backend/lib</code> 目录中新建一个文件<a href="https://github.com/jdarmada/nba-ai-assistant-js/blob/main/backend/lib/comparePlayers.js">comparePlayers.js</a>。</p><p>2.添加下面的函数，将元数据助手和 Elasticsearch 聚合逻辑合并为一个函数，为代理使用的主要工具提供动力。<br/></p><pre><code>import { playersByName } from '../data/playerAndTeamInfo.js';
import { teamsByName } from '../data/playerAndTeamInfo.js';
import { upcomingMatchups } from '../data/playerAndTeamInfo.js';
import { getHistoricalAveragesAgainstOpponent } from './elasticAggs.js';
import { getSeasonAveragesAgainstOpponent } from './elasticAggs.js';

//Simple helper functions to simulate API calls for player and team metadata. These reference the hardcoded values from playerAndTeamInfo.js in the data directory
export function getPlayerInfo(playerFullName) {
    return playersByName[playerFullName];
}

export function getTeamID(teamFullName) {
    return teamsByName[teamFullName];
}

export function getUpcomingMatchups(teamId) {
    return upcomingMatchups[teamId];
}

//Main function used by the 'playerComparisonTool' agent tool
export async function comparePlayersForNextMatchup(player1Name, player2Name) {
    //Get Player Info
    const player1Info = getPlayerInfo(player1Name);
    const player2Info = getPlayerInfo(player2Name);

    //Get upcoming matchups
    const player1NextGame = getUpcomingMatchups(player1Info.team_id)[0];
    const player2NextGame = getUpcomingMatchups(player2Info.team_id)[0];

    //Get season and historical averages against next opponent for player 1
    const player1SeasonAverages = await getSeasonAveragesAgainstOpponent(
        player1Info.player_id,
        player1NextGame.opponent_team_id
    );
    const player1HistoricalAverages = await getHistoricalAveragesAgainstOpponent(
        player1Info.player_id,
        player1NextGame.opponent_team_id
    );

    //Get season and historical averages against next opponent for player 2
    const player2SeasonAverages = await getSeasonAveragesAgainstOpponent(
        player2Info.player_id,
        player2NextGame.opponent_team_id
    );
    const player2HistoricalAverages = await getHistoricalAveragesAgainstOpponent(
        player2Info.player_id,
        player2NextGame.opponent_team_id
    );

    const player1 = {
        name: player1Name,
        playerId: player1Info.player_id,
        teamId: player1Info.team_id,
        nextOpponent: {
            teamId: player1NextGame.opponent_team_id,
            teamName: player1NextGame.opponent_team_name,
            home: player1NextGame.home,
        },
        stats: {
            seasonAverages: player1SeasonAverages,
            historicalAverages: player1HistoricalAverages,
        },
    };

    const player2 = {
        name: player2Name,
        playerId: player2Info.player_id,
        teamId: player2Info.team_id,
        nextOpponent: {
            teamId: player2NextGame.opponent_team_id,
            teamName: player2NextGame.opponent_team_name,
            home: player2NextGame.home,
        },
        stats: {
            seasonAverages: player2SeasonAverages,
            historicalAverages: player2HistoricalAverages,
        },
    };

    return [player1, player2];
}
</code></pre><h4><strong>步骤 7：建立代理</strong></h4><p>现在，您已经创建了前端和后端脚手架，摄取了 NBA 游戏数据，并建立了与 Elasticsearch 的连接，我们可以开始将所有部件组装在一起以构建代理。</p><p><strong>定义代理</strong></p><p>1.导航至<code>backend/src/mastra/agents</code> 目录中的<a href="https://github.com/jdarmada/nba-ai-assistant-js/blob/main/backend/src/mastra/agents/index.ts">index.ts</a>文件并添加代理定义。您可以指定以下字段</p><ul><li><strong>名称：</strong>给代理起一个名字，在前台调用时用作参考。</li><li><strong>指令/系统提示： </strong>系统提示为 LLM 提供交互过程中需要遵循的初始环境和规则。它类似于用户通过聊天框发出的提示，但这个提示是在用户输入之前发出的。同样，这也会根据您选择的机型而变化。</li><li><strong>模型：</strong>使用哪种 LLM（Mastra 支持 OpenAI、Anthropic、本地模型等）。</li><li><strong>工具：</strong>代理可调用的工具功能列表。</li><li><strong>记忆：</strong>（可选）如果我们希望代理记住对话历史等。为了简单起见，我们可以不使用持久内存，尽管 Mastra 支持持久内存。</li></ul><p></p><pre><code>import { openai } from '@ai-sdk/openai';
import { Agent } from '@mastra/core/agent';
import { playerComparisonTool } from '../tools';

export const basketballAgent = new Agent({
    name: 'Basketball Agent',
    instructions: `
      You are a NBA Basketball expert.
      Your primary function is to compare two NBA players and recommend which one is the better fantasy pickup.

      Only compare players from the following list:
      - LeBron James
      - Stephen Curry
      - Jayson Tatum
      - Jaylen Brown
      - Nikola Jokic
      - Luka Doncic
      - Kyrie Irving
      - Anthony Davis
      - Kawhi Leonard
      - Russell Westbrook

      Input Handling Rules:
      - If the user asks about a player that is not on this list, respond with the list of available players for comparison.
      - If the user only inputs one player, ask the user to add another player from the list provided.
      - If the user inputs a player with the wrong spelling or capitalizations, infer from the list of available players provided.
      - IMPORTANT: If the user asks a question or asks you to generate a response about anything outside of basketball or the scope of this project, DO NOT answer and affirm you can only talk about basketball.

      Tool Usage:
      - Extract and standardize player names to match the list exactly.
      - Use the playerComparisonTool, passing both names as strings.
      - The tool will return an object with game information, stats, and analysis.

      Format your response using Markdown syntax. Use:

        Example output format:

       
        #### Next Game Info
        - ***LeBron James** vs Warriors, May 24 (Home)  
        - ***Stephen Curry** vs Lakers, May 24 (Away)


        #### Stats Comparison  
        \`\`\`  
        Stat                  LeBron James (vs Warriors)    Stephen Curry (vs Lakers)  
        --------------------  -----------------------------  ----------------------------  
        Historical Points     28.3                          30.3  
        Historical Assists    6.7                           8.7  
        Season Points         28.8                          23.3  
        Season Assists        6.2                           4.7  
        \`\`\`

        #### Fantasy Recommendation  
        Explain which player is the better fantasy pickup and why.
      
    `,
    model: openai('gpt-4o'),
    tools: { playerComparisonTool },
});
</code></pre><p><strong><br/>定义工具</strong></p><ol><li>导航至<code>backend/src/mastra/tools</code> 目录中的<a href="https://github.com/jdarmada/nba-ai-assistant-js/blob/main/backend/src/mastra/tools/index.ts">index.ts</a>文件。</li><li>使用命令安装 Zod：</li></ol><pre><code>npm install zod</code></pre><p>3.添加工具定义。请注意，我们将<code>comparePlayers.js</code> 文件中的函数导入为代理在调用该工具时将使用的主函数。使用 Mastra 的<code>createTool()</code> 功能，我们将注册<code>playerComparisonTool</code> 。这些领域包括</p><ul><li><code>id</code>:这是一种自然语言描述，用于帮助代理理解工具的功能。</li><li><code>input schema</code>:为了定义工具的输入形状，Mastra 使用了<a href="https://zod.dev/">Zod</a>模式，这是一个 TypeScript 模式验证库。Zod 可确保代理输入结构正确的输入，并在输入结构不匹配时阻止工具执行。</li><li><code>description</code>:这是一种自然语言描述，帮助代理了解何时呼叫和使用工具。</li><li><code>execute</code>:调用工具时运行的逻辑。在本例中，我们使用一个导入的辅助函数来返回性能统计信息。</li></ul><pre><code>import { comparePlayersForNextMatchup } from '../../../lib/comparePlayers.js'
import { createTool } from "@mastra/core/tools";
import { z } from "zod";

export const playerComparisonTool = createTool({
    id: "Compare two NBA players",
    inputSchema: z.object({
        player1:z.string(),
        player2:z.string()
    }),
    description: "Use this tool to compare two players given in the user prompt.",
    execute: async ({ context: { player1, player2 } }) => {
        return await comparePlayersForNextMatchup(player1, player2);
      },
})</code></pre><p><strong>添加中间件处理 CORS</strong></p><p>在 Mastra 服务器中添加中间件以处理<a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CORS">CORS</a>。俗话说，人生有三件事无法避免：死亡、税收，而对于网络开发人员来说，就是 CORS。简而言之，跨源资源共享是一种浏览器安全功能，可阻止前台向运行在不同域或端口的后台发出请求。尽管我们在 localhost 上运行后端和前端，但它们使用不同的端口，从而触发了 CORS 策略。我们需要添加<a href="https://mastra.ai/en/docs/server-db/middleware">Mastra 文档</a>中指定的中间件，以便我们的后端允许来自前端的请求。</p><p>1.导航至<code>backend/src/mastra</code> 目录中的<a href="https://github.com/jdarmada/nba-ai-assistant-js/blob/main/backend/src/mastra/index.ts">index.ts</a>文件，并添加 CORS 配置：</p><ul><li><code>origin: [&#x27;http://localhost:5173&#x27;]</code><ul><li>只允许来自该地址的请求（Vite 默认地址）</li></ul></li><li><code>allowMethods: [&quot;GET&quot;, &quot;POST&quot;]</code><ul><li>允许使用的 HTTP 方法。大多数情况下，它将使用 POST。</li></ul></li><li><code>allowHeaders: [&quot;Content-Type&quot;, &quot;Authorization&quot;, &quot;x-mastra-client-type, &quot;x-highlight-request&quot;, &quot;traceparent&quot;],</code><ul><li>它们决定了哪些自定义标头可以在请求中使用</li></ul></li></ul><p></p><pre><code>import { Mastra } from '@mastra/core/mastra';
import { basketballAgent } from './agents';

console.log('Starting Mastra server...');

export const mastra = new Mastra({
  agents: { basketballAgent },
  server:{
    timeout: 10 * 60 * 1000, // 10 minutes
    cors: {
      origin: ['http://localhost:5173'],
      allowMethods: ["GET", "POST"],
      allowHeaders: [
        "Content-Type",
        "Authorization",
        "x-mastra-client-type",
        "x-highlight-request",
        "traceparent",
      ],
      exposeHeaders: ["Content-Length", "X-Requested-With"],
      credentials: false,
    },
  },

});

console.log('Mastra server configured.'); // Log after server configuration
</code></pre><h4><strong>步骤 8：整合前端</strong></h4><p>这个 React 组件提供了一个简单的聊天界面，可使用<code>@ai-sdk/react</code> 中的<a href="https://mastra.ai/en/docs/frameworks/agentic-uis/ai-sdk#using-the-usechat-hook">useChat()</a>钩子连接到 Mastra AI 代理。我们还将使用此钩子来显示标记的使用情况、工具调用情况并渲染对话。在上面的系统提示中，我们还要求代理以 markdown 格式输出响应，因此我们将使用<code>react-markdown</code> 来正确格式化响应。</p><p></p><p>1.在前端目录中，安装 @ai-sdk/react 软件包以使用 useChat() 钩子。</p><pre><code>npm install @ai-sdk/react</code></pre><p>2.在同一目录下，安装 React Markdown，这样我们就能正确格式化代理生成的响应。</p><pre><code>npm install react-markdown</code></pre><p>3.实施<code>useChat()</code> 。此钩子将管理前台与人工智能代理后台之间的交互。它可以处理消息状态、用户输入和状态，并为您提供生命周期钩子，以实现可观察性。我们提供的选项包括</p><ul><li><code>api:</code> 这定义了 Mastra AI 代理的端点。默认端口为 4111，我们还要添加支持流式响应的路由。</li><li><code>onToolCall</code>:在代理调用工具时执行；我们用它来跟踪代理调用了哪些工具。</li><li><code>onFinish</code>:在代理完成完整响应后执行。尽管我们启用了流式传输，但<code>onFinish</code> 仍将在收到完整报文后运行，而不是在每个分块后运行。在这里，我们用它来跟踪令牌的使用情况。这对监控 LLM 成本和优化成本很有帮助。</li></ul><p>4.最后，前往<code>frontend/components</code> 目录中的<a href="https://github.com/jdarmada/nba-ai-assistant-js/blob/main/frontend/components/ChatUI.jsx">ChatUI.jsx</a>组件，创建用户界面来进行对话。接下来，用<code>ReactMarkdown</code> 组件封装响应，以便正确格式化来自代理的响应。</p><pre><code>import React, { useState } from 'react';
import { useChat } from '@ai-sdk/react';
import ReactMarkdown from 'react-markdown';

export default function ChatUI() {
    const [totalTokenUsage, setTotalTokenUsage] = useState(0);
    const [promptTokenUsage, setPromptTokenUsage] = useState(0);
    const [completionTokenUsage, setCompletionTokenUsage] = useState(0);
    const [toolsCalled, setToolsCalled] = useState([]);

    const { messages, input, handleInputChange, handleSubmit, status } = useChat({
        api: 'http://localhost:4111/api/agents/basketballAgent/stream', //Replace with your own endpoint for your agent
        id: 'my-chat-session',

        //Optional parameter to check agent tool calls
        onToolCall: ({ toolCall }) => {
            setToolsCalled((prev) => [...prev, toolCall.toolName]);
        },

        //Optional parameter to check token usages
        onFinish: (message, { usage }) => {
            setTotalTokenUsage((prev) => prev + usage.totalTokens);
            setPromptTokenUsage((prev) => prev + usage.promptTokens);
            setCompletionTokenUsage((prev) => prev + usage.completionTokens);
        },

        //Optional parameter for error handling
        onError: (error) => {
            console.error('Agent error:', error);
        },
    });

    return (
        <div>
            <div className="agent-info">
                <h4 className="stats-title">What's My Agent Doing?</h4>

                <div className="stats-box">
                    <strong className="stats-sub-title">Tools Called:</strong>
                    <ul className="tool-list">
                        {toolsCalled.map((tool, idx) => (
                            <li key={idx}>{tool}</li>
                        ))}
                        {toolsCalled.length === 0 && <li>No tools called yet.</li>}
                    </ul>

                    <div className="usage-stats">
                        <p>Prompt Token Usage: {promptTokenUsage}</p>
                        <p>Completion Token Usage: {completionTokenUsage}</p>
                        <p>Total Token Usage: {totalTokenUsage}</p>
                    </div>
                </div>
            </div>

            <strong>Conversation:</strong>
            <div className="convo-box">
                {messages.map((msg) => (
                    <div key={msg.id} className="message-item">
                        <strong className="message-role">{msg.role === 'assistant' ? 'Basketbot' : 'You'}:</strong>
                        <ReactMarkdown>{msg.content}</ReactMarkdown>
                    </div>
                ))}
            </div>

            <form onSubmit={handleSubmit}>
                <input
                    type="text"
                    value={input}
                    onChange={handleInputChange}
                    placeholder="Input two players you want to compare."
                    className="input-box"
                />
                <button type="submit" disabled={status === 'streaming'}>
                    {status === 'streaming' ? 'Thinking...' : 'Send'}
                </button>
            </form>
        </div>
    );
}</code></pre><h4><strong>步骤 9：运行应用程序</strong></h4><p>祝贺你现在就可以运行应用程序了。按照以下步骤启动后台和前台。</p><ol><li>在终端窗口中，从根目录开始，导航到后台目录并启动 Mastra 服务器：</li></ol><pre><code>cd backend

npm run dev</code></pre><p>2.在另一个终端窗口中，从根目录开始，导航到前端目录并启动 React 应用程序：</p><p></p><pre><code>cd frontend

npm run dev</code></pre><p></p><p>3.打开浏览器，导航到</p><p></p><p><a href="http://localhost:5173/">http://localhost:5173</a></p><p></p><p>您应该可以看到聊天界面。试试这些提示样本：</p><ul><li>&quot;对比勒布朗-詹姆斯和斯蒂芬-库里&quot;</li><li>&quot;我应该在杰森-塔图姆和卢卡-东契奇之间选谁？&quot;</li></ul><p></p><h3><strong>下一步是什么？让代理更智能</strong></h3><p>为了让助手更具代理能力，建议更具洞察力，我将在下一次迭代中添加一些关键升级。</p><p></p><p><strong>NBA 新闻的语义搜索</strong></p><p>有很多因素会影响球员的表现，其中很多并不会在原始数据中体现出来。像伤病报告、阵容变化，甚至赛后分析，你只能在新闻报道中找到。为了捕捉这些额外的上下文，我将添加语义搜索功能，这样代理就可以检索相关的 NBA 文章，并将这些叙述纳入其推荐中。</p><p></p><p><strong>使用 Elasticsearch MCP 服务器进行动态搜索</strong></p><p>MCP（模型上下文协议）正迅速成为代理连接数据源的标准。我将把搜索逻辑迁移到 Elasticsearch MCP 服务器中，这样代理就可以动态建立查询，而不是依赖我们提供的预定义搜索功能。这使我们能够使用更多的自然语言工作流，并减少了手动编写每个搜索查询的需要。<a href="https://www.elastic.co/search-labs/blog/mcp-current-state">点击此处</a>了解有关 Elasticsearch MCP 服务器和生态系统现状的更多信息。</p><p></p><p>这些更改正在进行中，敬请期待！</p><h3><strong>结论</strong></h3><p></p><p>在本博客中，我们使用 JavaScript、Mastra 和 Elasticsearch 构建了一个代理 RAG 助手，为您的梦幻篮球队提供量身定制的建议。我们报道了</p><ul><li><strong>代理 RAG 的基本原理</strong>，以及如何将人工智能代理的自主性与有效使用 RAG 的工具相结合，从而产生更细致入微、更具活力的代理。</li><li><strong>Elasticsearch </strong>及其数据存储能力和强大的本地聚合功能如何使其成为法律硕士知识库的最佳合作伙伴。</li><li><strong>Mastra </strong>框架及其如何为 javaScript 生态系统中的开发人员简化这些代理的构建。</li></ul><p>无论你是篮球迷，还是在探索如何构建人工智能代理，或者像我一样两者兼而有之，我都希望这篇博客能为你提供一些入门的基础知识。完整的软件源可在<a href="https://github.com/jdarmada/nba-ai-assistant-js">GitHub</a> 上获取，请随意克隆和修补。现在，去赢得梦幻联赛吧！</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[在 Logstash 中使用 Ruby 脚本]]></title>
            <description><![CDATA[了解 Logstash Ruby 过滤器插件，以便在 Logstash 管道中进行高级数据转换。]]></description>
            <link>https://www.elastic.co/search-labs/cn/blog/ruby-scripting-logstash</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/cn/blog/ruby-scripting-logstash</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Dai Sugimori]]></dc:creator>
            <pubDate>Tue, 24 Jun 2025 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/b1bcd63c033ccbde102c20ba3085f165f9289a71-1600x1000.png?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>Logstash 是一个数据处理管道，可从多个来源摄取数据，对其进行转换，然后发送到您选择的目的地。过滤器插件是这一过程的关键；当数据通过管道时，它们会对数据执行特定操作。</p><p>Logstash 包含多个内置过滤器，可用于解析、丰富和修改数据等常见任务。但有时，您会遇到需要自定义逻辑的情况，这超出了这些标准过滤器所能提供的范围。这就是<a href="https://www.elastic.co/docs/reference/logstash/plugins/plugins-filters-ruby">Ruby 过滤器插件的</a>用武之地。</p><p><strong>Ruby 过滤器插件允许您在 Logstash 管道中直接执行自定义 Ruby 代码。</strong>如果标准过滤器还不够用，Ruby 过滤器还能让您处理复杂的数据转换、实施自定义业务逻辑或与外部系统集成。</p><p>在本博客中，我们将探讨如何从基础到高级使用 Ruby 过滤器。</p><h2>何时使用 Ruby 过滤器？</h2><p>作为 Elastic 的咨询架构师，我经常看到客户在数据处理管道中使用 Logstash，尽管现在它还不是最先进的数据处理引擎。当涉及到复杂的数据操作或自定义逻辑时，他们往往会因为标准过滤器的局限性而苦恼。在这种情况下，Ruby 过滤器可以帮助克服这些挑战。</p><p>当标准 Logstash 过滤器无法满足您的特定要求时，Ruby 过滤器就会派上用场。下面是一些常见的使用案例：</p><ul><li><strong>深度嵌套数据操作</strong>：修改复杂的 JSON 结构、数组中的数组或根据内容动态重组数据</li><li><strong>高级字符串处理</strong>：从非结构化文本中解析和提取结构化数据</li><li><strong>实施复杂的业务逻辑</strong>：创建需要条件逻辑、循环或复杂计算的自定义转换</li></ul><h2>基本用法</h2><p>让我们从一个简单的例子开始，了解 Ruby 过滤器是如何工作的。</p><h3>配置 Ruby 过滤器</h3><p>创建 Logstash 管道时，应将配置文件放在<code>/etc/logstash/conf.d</code> 目录中。或者，你也可以使用<code>-f</code> 选项，在手动启动 Logstash 时指定配置文件的路径，这样就可以轻松试验你的管道了。</p><pre><code>$ ./bin/logstash -f /path/to/your_pipeline.conf</code></pre><p>配置文件的扩展名应为<code>.conf</code> 。</p><p>要使用 Ruby 过滤器，请在 Logstash 管道配置 (*.conf) 文件的过滤器部分定义<code>ruby</code> 过滤器。下面是一个基本例子：</p><pre><code>filter {
  ruby {
    code => "
      event.set('new_field', 'Hello from Ruby!')
    "
  }
}</code></pre><p>此内联 Ruby 过滤器在 Logstash 配置中定义了一个 Ruby 过滤器实例。<code>code</code> 参数提供了 Logstash 将为该过滤器处理的每个事件执行的内联 Ruby 脚本。在该脚本中，有一个<code>event</code> 变量可用于表示事件本身。事件对象包含发送到 Logstash 的原始数据以及在 Logstash 过滤阶段创建的任何附加字段。您可以通过 Logstash 事件 API 访问这些字段，如<code>event.get()</code> 和<code>event.set()</code> 。在此示例代码中，<code>event.set(&#x27;new_field&#x27;, &#x27;Hello from Ruby!&#x27;)</code> 将名为<code>new_field</code> 的新字段设置为字符串值<code>Hello from Ruby!</code> 。您可以根据需要在<code>code</code> 块中添加任何其他代码。</p><p>请注意，这个<code>event</code> 对象虽然是键值类型的数据容器，但它并不是通常的 Ruby 哈希对象。查看<a href="https://www.elastic.co/docs/reference/logstash/event-api">此官方文档</a>，了解有关事件 API 的更多信息。</p><h3>外部化 Ruby 脚本</h3><p>对于简单的转换，内联 Ruby 代码非常方便。但是，对于复杂的逻辑或可重复使用的功能，建议将代码移到外部 Ruby 脚本中。这样可以提高可维护性，并保持 Logstash 管道配置的整洁。</p><p>首先，创建 Ruby 脚本并将其保存为<code>my_ruby_script.rb</code> 。脚本必须定义一个处理事件的<code>filter</code> 方法。它将一个事件对象作为参数，该对象代表正在处理的当前事件。<code>filter</code> 方法需要返回一个要发射的事件数组。要删除事件，返回空数组。</p><p>例如，下面的脚本读取<code>message</code> 字段，计算其长度，并将结果存储到名为<code>message_length</code> 的新字段中。</p><pre><code>def register(params)
  # This method is called when the plugin is loaded.
  # You can use it to initialize any instance variables or perform setup tasks.
end

def filter(event)
  message = event.get('message')

  if message
    event.set('message_length', message.length)
  end

  return [event]
end</code></pre><p>接下来，使用<code>path</code> 选项设置 Ruby 过滤器配置，以引用脚本。这会告诉 Logstash 加载并执行外部脚本。使用外部脚本时，请确保文件存在并具有正确的权限。</p><pre><code>filter {
  ruby {
    path => "/path/to/my_ruby_script.rb"
  }
}</code></pre><p>现在，每个事件都会被传递到<code>my_ruby_script.rb</code> 中的<code>filter</code> 方法，并由它进行处理。</p><p>这种方法能帮助你更有效地管理复杂的逻辑，使测试、调试和重用 Ruby 代码变得更容易。</p><h2>高级用法</h2><p>在本节中，我们将探讨在 Logstash 中使用 Ruby 过滤器的一些高级示例。这些示例将演示如何使用 Ruby 执行数据转换、丰富事件和实现自定义逻辑。</p><h3>操作嵌套数据结构</h3><p>Logstash 事件是 Logstash 处理的核心数据结构。它可以包含各种字段，包括嵌套数据结构，如数组和哈希值。通过 Ruby 过滤器，您可以轻松处理这些嵌套结构。</p><p>Ruby 过滤器可以处理嵌套数据结构，如哈希值和数组，允许你修改或添加这些结构中的字段。这在处理 JSON 等复杂数据格式时非常有用。</p><pre><code>input {
  generator {
    lines => [
      '{"nested": {"key1": "value1", "key2": "value2"}}'
    ]
    count => 1
    codec => "json"
    ecs_compatibility => "disabled"
  }
}

filter {
  ruby {
    code => "
      nested_data = event.get('nested')

      if nested_data.is_a?(Hash)
        nested_data['key3'] = 'value3'
        event.set('nested', nested_data)
      end
    "
  }
}

output {
  stdout { codec => rubydebug }
}</code></pre><p>此示例在输入数据中包含一个嵌套 JSON 对象。Ruby 过滤器通过添加新的键值对来修改嵌套数据。标准 Logstash 过滤器无法对嵌套数据进行此类操作，因此 Ruby 过滤器成为复杂数据结构的便捷选择。</p><h3>将单个事件拆分为多个事件</h3><p>Ruby 过滤器还可用于将单个事件分割成多个事件。当您有一个包含项目数组的单个事件，并希望为每个项目创建单独的事件时，这个功能就非常有用。</p><p>请注意，Elasticsearch 的摄取管道和 Beats/Elastic Agent 的处理器都不支持拆分事件。这是 Logstash 最强大的用例之一。</p><h4>带分体式过滤器</h4><p>您可以使用<code>split</code> 过滤器，根据指定字段将一个事件拆分成多个事件。不过，如果需要在分割过程中执行额外的转换或逻辑，可以将 Ruby 过滤器与分割过滤器结合使用。</p><p>在下面的示例中，我们将 RSS 订阅作为一行 XML 文本。它包含多个<code>&lt;item&gt;</code> 元素。Ruby 过滤器用于从 XML 中提取<code>&lt;item&gt;</code> 元素，并将其存储在名为<code>items</code> 的新字段中。然后，根据<code>items</code> 字段，使用拆分过滤器将事件拆分成多个事件。</p><pre><code>input {
  generator {
    lines => [
      '<rss version="2.0"><channel><title>Sample RSS</title><item><title>Article 1</title><link>http://example.com/1</link><description>Desc 1</description></item><item><title>Article 2</title><link>http://example.com/2</link><description>Desc 2</description></item></channel></rss>'
    ]
    count => 1
    codec => "plain"
    ecs_compatibility => "disabled"
  }
}

filter {
  xml {
    source => "message"
    target => "rss"
    store_xml => true
    force_array => false
  }
  ruby {
    code => "event.set('items', event.get('[rss][channel][item]')) if event.get('[rss][channel][item]')"
  }
  split {
    field => "items"
  }
  ruby {
    code => "
      item = event.get('items')
      event.set('title', item['title']) if item['title']
      event.set('link', item['link']) if item['link']
      event.set('description', item['description']) if item['description']
    "
  }
  mutate {
    remove_field => ["@timestamp", "@version", "sequence", "host", "event", "message", "rss", "items"]
  }
}

output {
  stdout { codec => rubydebug }
}</code></pre><p>输出结果如下</p><pre><code>{
          "title" => "Article 1",
           "link" => "http://example.com/1",
    "description" => "Desc 1"
}
{
          "title" => "Article 2",
           "link" => "http://example.com/2",
    "description" => "Desc 2"
}</code></pre><p>您可能已经注意到，在这种情况下，<code>ruby</code> 过滤器并不是必不可少的。<code>split</code> 过滤器可用于根据<code>items</code> 字段将事件拆分成多个事件，而<code>mutate</code> 过滤器可用于删除不必要的字段。不过，如果需要在分割过程中执行额外的转换或逻辑，可以使用 Ruby 过滤器。</p><h4>使用内联 Ruby 脚本</h4><p>您还可以使用内联 Ruby 脚本，通过<code>event.clone</code> 方法和<code>new_event_block variable</code> ，将单个事件拆分成多个事件，如<code>new_event_block.call(new_event)</code> 。这样，您就可以根据原始事件创建新事件，同时保留其数据。</p><p>下面是一个如何使用 Ruby 过滤器将单个事件分割成多个事件的示例。输入和输出与上一个示例相同。</p><pre><code>filter {
  xml {
    source => "message"
    target => "rss"
    store_xml => true
    force_array => false
  }
  ruby {
    code => "
      items = event.get('[rss][channel][item]')
      if items.is_a?(Array)
        items.each do |item|
          new_event = event.clone
          new_event.set('title', item['title'])
          new_event.set('link', item['link'])
          new_event.set('description', item['description'])
          new_event_block.call new_event
        end
        event.cancel
      elsif items.is_a?(Hash)
        event.set('title', items['title'])
        event.set('link', items['link'])
        event.set('description', items['description'])
      end
    "
  }
  mutate {
    remove_field => ["@timestamp", "@version", "sequence", "host", "event", "message", "rss", "items"]
  }
}</code></pre><h4>使用外部 Ruby 脚本</h4><p>您还可以使用外部 Ruby 脚本将单个事件分割成多个事件。</p><p>配置文件：</p><pre><code>filter {
  xml {
    source => "message"
    target => "rss"
    store_xml => true
    force_array => false
  }
  ruby {
    path => "path/to/ruby/split_event.rb"
  }
  mutate {
    remove_field => ["@timestamp", "@version", "sequence", "host", "event", "message", "rss", "items"]
  }
}</code></pre><p>Ruby 脚本需要外部化为<code>split_event.rb</code> ：</p><pre><code>def filter(event)
  items = event.get('[rss][channel][item]')
  events = []
  if items.is_a?(Array)
    items.each do |item|
      new_event = event.clone
      new_event.set('title', item['title'])
      new_event.set('link', item['link'])
      new_event.set('description', item['description'])
      events << new_event
    end
    return events
  elsif items.is_a?(Hash)
    event.set('title', items['title'])
    event.set('link', items['link'])
    event.set('description', items['description'])
    return [event]
  else
    return []
  end
end</code></pre><p>请记住，<code>filter</code> 方法必须返回一个事件数组。您可以通过克隆传入的事件对象并将其添加到数组中来返回多个事件，也可以将单个事件作为包含一个元素的数组来返回。</p><pre><code>return events
# or
# return [event]</code></pre><p>这样就可以将单个事件拆分成多个事件。</p><h3>执行外部命令并解析其输出结果</h3><p>Logstash exec 输入插件允许您执行外部命令，其输出将成为 Logstash 的事件。命令的输出将存储在事件的<code>message</code> 字段中。</p><p>通常情况下，系统命令的输出是人类可读的，但并不是 Logstash 可以轻松解析的 JSON 或其他格式的结构。要处理这种情况，可以使用 Ruby 过滤器解析输出并从中提取信息。</p><p>下面是一个使用<code>exec</code> 输入插件执行<code>ps -ef</code> 命令的示例，该命令可列出类 Unix 系统上所有正在运行的进程。Ruby 过滤器将对输出进行解析，以提取每个进程的相关信息。</p><pre><code>input {
  exec {
    command => "ps -ef"
    interval => 60
  }
}

filter {
  ruby {
    code => '
      processes = []
      lines = event.get("message").split("\n")  
      lines.each_with_index do |line, index|
        # Skip header line and empty lines
        next if index == 0 || line.strip.empty?
        entry = nil
        
        # Use regex to match the ps -ef output format more flexibly
        # This pattern accounts for variable spacing and different time formats
        if line =~ /^\s*(\S+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\S+)\s+(\S+)\s+([\d:]+\.?\d*)\s+(.+)$/
          uid, pid, ppid, c, stime, tty, time, cmd = $1, $2, $3, $4, $5, $6, $7, $8
          
          entry = {
            "UID" => uid,
            "PID" => pid,
            "PPID" => ppid,
            "C" => c,
            "STIME" => stime,
            "TTY" => tty,
            "TIME" => time,
            "CMD" => cmd.strip
          }
        elsif line =~ /^\s*(\S+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(.+)$/
          # Fallback pattern for lines that might not match the exact format
          # Split the remaining part more carefully
          uid, pid, ppid, c, remainder = $1, $2, $3, $4, $5
          
          # Split remainder into STIME, TTY, TIME, CMD
          parts = remainder.strip.split(/\s+/, 4)
          if parts.length >= 4
            stime, tty, time, cmd = parts[0], parts[1], parts[2], parts[3]
            
            entry = {
              "UID" => uid,
              "PID" => pid,
              "PPID" => ppid,
              "C" => c,
              "STIME" => stime,
              "TTY" => tty,
              "TIME" => time,
              "CMD" => cmd
            }
          end
        end
        if entry && entry["UID"] == "0"
          original_line = line.strip
          entry["original_line"] = original_line if original_line.length > 0
          processes.push(entry)
        end
      end
      event.set("processes", processes)
      event.remove("message")
      event.remove("event")
    '
  }
}

output {
  stdout { codec => rubydebug }
}</code></pre><p>本例使用<code>exec</code> 输入插件，每 60 秒运行一次<code>ps -ef</code> 命令。Ruby 过滤器处理输出，提取相关字段，如 UID、PID、PPID、CPU 使用率 (C)、开始时间 (STIME)、TTY、CPU 总时间 (TIME) 和执行的命令 (CMD)。它在我的 macOS 环境中运行良好，但你可能需要调整 regex 模式，以匹配你系统中<code>ps -ef</code> 命令的输出格式。</p><h3>使用内置库</h3><p>Ruby 过滤器插件允许您使用内置的 Ruby 库，这对各种任务都非常有用。例如，您可以使用<code>json</code> 库解析 JSON 字符串，或使用<code>date</code> 库处理日期。</p><p>下面是一个使用<code>json</code> 库解析存储在字段中的 JSON 字符串的示例：</p><pre><code>require 'json'

def filter(event)
  json_string = event.get('message')
  parsed_json = JSON.parse(json_string)
  event.set('parsed_json', parsed_json)
  return [event]
end</code></pre><p>为了避免每次都需要使用库，您应该将 Ruby 代码外部化，以便在 Ruby 过滤器脚本的开头使用<code>require</code> 语句。这将加载一次库，使其可在脚本中使用。</p><p>要检查环境中哪些库可用，可以在 Ruby 过滤器中运行以下代码，列出内置库：</p><pre><code>Gem.loaded_specs.sort_by { |name, _| name }.each do |name, spec|
  puts "#{name}: #{spec.version}"
end</code></pre><p><strong>注意： </strong>Logstash 官方不支持内置库，它们的行为可能会发生变化，或者在未来版本中不可用。使用风险自负。</p><h2>结论</h2><p>Logstash Ruby 过滤器允许您自定义和扩展 Logstash 管道的功能。在本篇文章中，我们介绍了使用 Ruby 过滤器的基础知识，并提供了高级用法示例。</p><p>利用 Ruby 过滤器，您可以处理需要自定义逻辑或高级操作的复杂数据处理任务。无论您是处理嵌套数据结构、拆分事件，还是解析复杂/非结构化文本并将其转换为结构化 JSON，Ruby 过滤器都能灵活满足您的特定需求。</p><p>希望本指南能为您提供探索 Logstash Ruby 过滤器全部潜力的知识和灵感。祝您编写脚本愉快！</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[如何在 GCP GKE Autopilot 上部署 Elasticsearch。]]></title>
            <description><![CDATA[了解如何通过 GKE Autopilot 与 ECK 在 GCP 上部署 Elasticsearch 集群，实现部分托管的 Elasticsearch 设置配置。]]></description>
            <link>https://www.elastic.co/search-labs/cn/blog/eck-gke-autopilot</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/cn/blog/eck-gke-autopilot</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Eduard Martin]]></dc:creator>
            <pubDate>Thu, 19 Jun 2025 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/d9c462fe63011356671479ccfedd435eec1ede52-1200x628.png?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>在本文中，我们将学习如何使用 Autopilot 在 Google Cloud Kubernetes (GKE) 上部署 Elasticsearch。</p><p>对于 Elasticsearch，我们将使用 <a href="https://www.elastic.co/docs/deploy-manage/deploy/cloud-on-k8s">Elastic Cloud on Kubernetes</a> (ECK)，这是正式的 Elasticsearch Kubernetes 运维工具，简化了所有 Elastic Stack 组件的 Kubernetes 部署协调。</p><p>要了解更多关于如何在不同 GCP 基础架构上部署 Elasticsearch 集群的信息，您可以阅读我们关于 <a href="https://www.elastic.co/search-labs/blog/elasticsearch-gpc-google-compute-engine">Google Cloud Compute</a> 和 <a href="https://www.elastic.co/search-labs/blog/deploy-elastic-gcp-marketplace">Google Cloud Marketplace</a> 的入门文章。</p><h2>Elasticsearch 部署步骤</h2><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/56b54d718dcff9af9050873c41fdf738074851da-1428x582.png" style="width: 100%;" /><h3>什么是 GKE Autopilot？</h3><p><a href="https://cloud.google.com/kubernetes-engine/docs/concepts/autopilot-overview?hl=es-419"><strong>Google Kubernetes Engine (GKE) Autopilot</strong></a> 提供完全托管的 Kubernetes 体验，由 Google 负责集群配置、节点管理、安全与扩展，而开发人员只需专注于应用程序部署，使团队能够凭借内置的最佳实践在几分钟内完成从代码到生产环境的转化。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/bbf6de63fa0a199326352f521cb22654818799f6-1600x958.png" style="width: 100%;" /><h2>何时在 Google Cloud 中使用 ECK？</h2><p>Elastic Cloud on Kubernetes (ECK) 最适合那些拥有现有 Kubernetes 基础架构并希望部署具有高级功能（如专用节点角色、高可用性和自动化）的 Elasticsearch 的组织。</p><h2>如何在 Google Cloud 中设置 ECK？</h2><p>1. 登录 <a href="https://console.cloud.google.com">Google Cloud Console </a>。</p><p>2. 在<strong>右上角</strong>点击<strong></strong>“<strong>Cloud Shell</strong>”按钮进入控制台，并从那里部署 GKE 集群。或者，您也可以使用 <a href="https://cloud.google.com/cli">gcloud CLI</a>。</p><p><strong><em>操作过程中记得将项目 ID 替换为您自己的项目 ID。</em></strong></p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/999b03861d4fe44f360ab4c7e2616e1dc10cf182-1558x1248.png" style="width: 100%;" /><p>3. 启用 <a href="https://console.cloud.google.com/flows/enableapi?apiid=container.googleapis.com">Google Kubernetes Engine API</a>。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/03827d3dc0e987c019e7747d33e7c01920047beb-911x246.png" style="width: 100%;" /><p>点击<strong><em>下一步</em></strong>。</p><p>现在，搜索 Kubernetes Engine API 时，应该显示 Kubernetes Engine API 已启用。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/d5cd46f0333086bcb31b80cf9c08a469b449ec0f-640x250.png" style="width: 100%;" /><p>4. 在 Cloud Shell 中创建一个 Autopilot 集群。我们将其命名为 autopilot-cluster-1，并请将 autopilot-test 替换为您的项目 ID。</p><pre><code>gcloud beta container --project "autopilot-test-457216" clusters create-auto "autopilot-cluster-1" --region "us-central1" --release-channel "regular" --tier "standard" --enable-ip-access --no-enable-google-cloud-access --network "projects/autopilot-test-457216/global/networks/default" --subnetwork "projects/autopilot-test-457216/regions/us-central1/subnetworks/default" --cluster-ipv4-cidr "/17" --binauthz-evaluation-mode=DISABLED</code></pre><p>5. 等待集群就绪。创建过程大约需要 10 分钟。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/81fbc45ba56d0f16ba42724cb8ae45e60b327dbc-1581x258.png" style="width: 100%;" /><p>正确设置集群后，将显示一条确认消息。</p><p>6. 配置 kubectl 命令行访问。</p><pre><code>gcloud container clusters get-credentials autopilot-cluster-1 --region us-central1 --project autopilot-test-457216</code></pre><p>您应该看到：</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/b0beba1ee00ce9029f586ee32693fc2aa58c7f65-3442x142.png" style="width: 100%;" /><p><em>已为 autopilot-cluster-1 生成 kubeconfig 条目。</em></p><p>7. 安装 <a href="https://www.elastic.co/docs/deploy-manage/deploy/cloud-on-k8s">Elastic Cloud on Kubernetes</a>(ECK) 运维工具。</p><pre><code># Install ECK Custom Resource Definitions
kubectl create -f https://download.elastic.co/downloads/eck/2.16.1/crds.yaml

# Install the ECK operator
kubectl apply -f https://download.elastic.co/downloads/eck/2.16.1/operator.yaml</code></pre><p>8. 让我们创建一个具有默认值的单节点 Elasticsearch 实例。</p><p>如果您想查看不同设置的配方，可以访问<a href="https://www.elastic.co/docs/deploy-manage/deploy/cloud-on-k8s/recipes">此链接</a>。</p><p>请注意，如果未指定 <code>storageClass</code>，ECK 将使用默认设置，GKE 的默认设置为 <code>standard-rwo</code>，该配置使用 <a href="https://cloud.google.com/kubernetes-engine/docs/how-to/persistent-volumes/gce-pd-csi-driver?cloudshell=true">Compute Engine 持久化磁盘 CSI 驱动</a>）并创建 1GB 的卷。</p><pre><code>cat <<EOF | kubectl apply -f -
apiVersion: elasticsearch.k8s.elastic.co/v1
kind: Elasticsearch
metadata:
  name: quickstart
spec:
  version: 9.0.0
  nodeSets:
  - name: default
    count: 1
    config:
      node.store.allow_mmap: false
EOF</code></pre><p>我们禁用<code>nmap</code>，是因为默认 GKE 机器的 <code>vm.max_map_count</code> 值过低。不建议在生产环境中禁用它，但建议增加 <code>vm.max_map_count</code> 的值。您可以<a href="https://www.elastic.co/docs/deploy-manage/deploy/cloud-on-k8s/virtual-memory">在这里</a>阅读更多关于如何做到这一点的信息。</p><p>9. 我们还要部署一个 Kibana 单节点集群。对于 Kibana，我们将添加一个 LoadBalancer，它将为我们提供一个外部 IP，我们可以使用该 IP 从我们的设备访问 Kibana。</p><pre><code>cat <<EOF | kubectl apply -f -
apiVersion: kibana.k8s.elastic.co/v1
kind: Kibana
metadata:
  name: quickstart
spec:
  version: 9.0.0
  http:
    service:
      metadata:
        annotations:
          cloud.google.com/l4-rbs: "enabled"
      spec:
        type: LoadBalancer
  count: 1
  elasticsearchRef:
    name: quickstart
EOF</code></pre><p>请注意注释： </p><p><code>cloud.google.com/l4-rbs: &quot;enabled&quot;</code></p><p><strong><em>该注释非常重要，因为它指示 Autopilot 提供一个面向公众的 LoadBalancer。如果未设置，LoadBalancer 将为内部类型。</em></strong></p><p>10. 检查您的 pod 是否正在运行</p><pre><code>kubectl get pods</code></pre><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/99da1270581a137683770efdb9c6e1577ec9fc01-3150x442.png" style="width: 100%;" /><p>11. 您还可以使用 <code>run kubectl get elasticsearch</code> 和 <code>kubectl get kibana</code> 来获取更具体的统计信息，例如 Elasticsearch 版本、节点和健康状况。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/86234f307970fd5f78b8acd41496e8cc89ff82d3-3414x326.png" style="width: 100%;" /><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/29160286ccc88928734c8ea11b1923db8e85d49d-3142x318.png" style="width: 100%;" /><p>12. 获取您的服务。</p><pre><code>kubectl get svc</code></pre><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/ed1a07314b84a99b4aa1fec3db4b9badeb9587ee-3446x610.png" style="width: 100%;" /><p>这将显示 Kibana 在 EXTERNAL-IP 下的外部 URL。可能需要几分钟时间来配置 LoadBalancer。<strong><em>复制 EXTERNAL-IP 的值。</em></strong></p><p>13. 获取“elastic”用户的 Elasticsearch 密码：</p><pre><code>kubectl get secret quickstart-es-elastic-user -o=jsonpath='{.data.elastic}' | base64 --decode</code></pre><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/ab5054219216ebc15fc0d96e27605aaf13b720c6-3448x210.png" style="width: 100%;" /><p>14. 通过浏览器<strong>访问 Kibana</strong>：</p><ul><li>URL: https://&lt;EXTERNAL_IP&gt;:5601</li><li>用户名：elastic</li><li>密码：28Pao50lr2GpyguX470L2uj5（来自上一步）</li></ul><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/47cbe88dc14db64db3a256f3f7504cc86a843475-463x503.png" style="width: 100%;" /><p>15. 通过浏览器访问时，您将看到欢迎界面。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/a75c151c0144b7efe2b730698c0ed0156fa9b16a-1600x1005.png" style="width: 100%;" /><p>如果您想更改 Elasticsearch 集群规格，例如更改或调整节点大小，可重新应用包含新设置的 yml 配置文件：</p><pre><code>cat <<EOF | kubectl apply -f -
apiVersion: elasticsearch.k8s.elastic.co/v1
kind: Elasticsearch
metadata:
  name: quickstart
spec:
  version: 9.0.0
  nodeSets:
    - name: default
      count: 2
      config:
        node.store.allow_mmap: false
      podTemplate:
        spec:
          containers:
            - name: elasticsearch
              resources:
                requests:
                  memory: 1.5Gi
                  cpu: 2
                limits:
                  memory: 1.5Gi
                  cpu: 2
EOF</code></pre><p>在此示例中，我们将再添加一个节点，并修改 RAM 和 CPU。如您所见，现在 <code>kubectl get elasticsearch</code> 显示 2 个节点：</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/48b475622cc48890bff8105d151f2cbde28d7021-3418x298.png" style="width: 100%;" /><p>这同样适用于 Kibana：</p><pre><code>cat <<EOF | kubectl apply -f -
apiVersion: kibana.k8s.elastic.co/v1
kind: Kibana
metadata:
  name: quickstart
spec:
  version: 9.0.0
  http:
    service:
      metadata:
        annotations:
          cloud.google.com/l4-rbs: "enabled"
      spec:
        type: LoadBalancer
  count: 1
  elasticsearchRef:
    name: quickstart
  podTemplate:
    spec:
      containers:
        - name: kibana
          env:
            - name: NODE_OPTIONS
              value: "--max-old-space-size=1024"
          resources:
            requests:
              memory: 0.5Gi
              cpu: 0.5
            limits:
              memory: 1Gi
              cpu: 1
EOF</code></pre><p>我们可以调整容器 CPU/RAM 以及 <a href="https://nodejs.org/">Node.js</a> 的内存使用量（<a href="https://nodejs.org/api/cli.html#--max-old-space-sizesize-in-mib">max-old-space-size</a>）。</p><p>请注意，<a href="https://www.elastic.co/docs/deploy-manage/deploy/cloud-on-k8s/volume-claim-templates">现有的卷声明无法缩小容量</a>。应用更新后，运维工具将在最短中断时间内完成更改。</p><p>请记得在测试结束后删除集群，以避免产生不必要的成本。</p><pre><code>gcloud container clusters delete autopilot-cluster-1</code></pre><h2>后续步骤</h2><p>如果您想了解更多关于 Kubernetes 和 Google Kubernetes Engine 的信息，请查看以下文章：</p><ul><li><a href="https://www.elastic.co/docs/deploy-manage/deploy/cloud-on-k8s">Kubernetes 上的 Elastic Cloud | Elastic 文档</a></li><li><a href="https://cloud.google.com/blog/products/containers-kubernetes/introducing-gke-autopilot">推出 GKE Autopilot ｜ Google Cloud 博客</a></li><li><a href="https://cloud.google.com/kubernetes-engine/docs/concepts/autopilot-overview">Autopilot 概述 | Google Kubernetes Engine (GKE)</a></li></ul>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[将 Azure LLM 函数与 Elasticsearch 结合使用，获得更智能的查询体验]]></title>
            <description><![CDATA[探索一个房地产搜索应用程序示例，该应用程序使用 Azure Gen AI LLM 函数和 Elasticsearch 提供灵活的混合搜索结果。在 GitHub Codespaces 中逐步查看如何配置和运行示例应用程序。]]></description>
            <link>https://www.elastic.co/search-labs/cn/blog/azure-llm-functions-elasticsearch</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/cn/blog/azure-llm-functions-elasticsearch</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Jonathan Simon]]></dc:creator>
            <pubDate>Fri, 13 Jun 2025 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/1431b90c7e00de06574c1e33c44a2e89296c824e-1200x628.png?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>精确。重要的时候，它非常重要。当您搜索特定的内容时，精确度就显得格外重要。不过，有时过于精确的查询不会返回任何结果，因此，灵活地扩大查询范围以查找更多潜在的相关数据是有好处的。</p><p>这篇博文介绍了如何使用 Elasticsearch 和 Azure Open AI 创建一个示例应用程序，演示如何在搜索非常具体的房地产物业时找到准确的结果，同时在没有具体匹配结果时仍然提供相关结果。我们将介绍创建 Elasticsearch 索引和搜索模板所需的所有步骤。然后，我们将介绍如何创建一个应用程序，使用 Azure OpenAI 接收用户查询并将其转换为 Elasticsearch 搜索模板查询，从而生成令人惊叹的自定义结果的整个过程。</p><p>下面列出了我们在创建房地产搜索应用程序示例时要用到的所有资源：</p><ul><li>Elasticsearch 索引和搜索模板</li><li>Azure OpenAI</li><li>Azure 地图 API</li><li><a href="https://github.com/jwilliams-elastic/msbuild-intelligent-query-demo/blob/main/VectorDBSetup.ipynb">Codespaces Jupyter 笔记本</a></li><li>语义内核</li><li>使用 Blazor 前端的 C# 应用程序</li></ul><h2>智能查询工作流程</h2><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/0c4a7c835e06c514f158c00ab1055a7ba719a35f-1600x765.png" style="width: 100%;" /><p>该工作流程将 LLM、LLM 工具和搜索结合起来，将自然语言查询转化为结构化的相关搜索结果：</p><ul><li><strong>LLM（大型语言模型）</strong>--解释复杂的用户查询并协调工具以提取搜索意图和丰富上下文。</li><li><strong>LLM 工具</strong>- 每个 LLM 工具都是我们为本职位创建的 C# 程序。有三种工具：<ul><li><em>参数提取工具</em>：从查询中提取卧室、浴室、功能和价格等关键属性。</li><li><em>地理编码工具</em>：将地点名称转换为经纬度，以便进行空间筛选。</li><li><em>搜索工具</em>：使用查询参数填充 Elasticsearch 搜索模板并执行搜索。<strong>混合搜索</strong>- 利用内置 ML 推理执行混合搜索（全文 + 密集向量）。这种分层方法可确保终端用户获得更智能、更能感知上下文的查询体验。</li></ul></li></ul><h2>应用架构</h2><p>下面是示例应用程序的系统架构图。我们将使用 Codespaces Jupyter Notebook 与 Elastic Cloud 进行交互。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/2880afee184cd9270c0eb4310e51418e2339784d-936x452.png" style="width: 100%;" /><h2>准备工作</h2><div style="display:none">Unknown block type "block", specify a component for it in the `components.types` option</div><h2>在 Codespaces 中克隆示例应用程序 repo</h2><p>首先克隆示例应用程序的代码。<a href="https://github.com/codespaces/">GitHub Codespaces</a> 提供了克隆和运行应用程序的方法。单击<strong>新建代码空间。</strong></p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/b89cbec491659b6c8a0bb9551ed2629f7a37f9fd-1600x427.png" style="width: 100%;" /><p>然后，在<strong> Repository</strong> 下拉菜单中选择 repo<a href="https://github.com/jwilliams-elastic/msbuild-intelligent-query-demo"> jwilliams-elastic/msbuild-intelligent-query-demo</a><strong> ，然后单击 Create Codespace</strong> 。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/43ea377554527af9578400f16cd2342bf8fff3a2-1600x1049.png" style="width: 100%;" /><h2>创建 .env文件</h2><p>我们将使用 Python Jupyter Notebook 访问 Elastic Cloud 并与之交互，它使用存储在配置文件中的配置值进行访问和交互。笔记本配置文件的文件名应为<strong><em>.env</em></strong>您现在就可以创建它。</p><ol><li>在 GitHub 代码空间中，单击 &quot;<strong>新建文件 </strong>&quot;按钮，添加一个名为<strong><em>.env</em></strong>的文件</li><li>在新创建的<strong><em>.env</em></strong>中添加以下内容文件</li></ol><pre><code>ELASTIC_URL=
ELASTIC_API_KEY=</code></pre><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/84d4f327948858ba61db0001dd8cf780d42fe0a7-1600x875.gif" style="width: 100%;" /><p><em>正如您所看到的，我们需要在.env</em> 中添加几个缺失值，即 <strong>ELASTIC_URL</strong> 和<strong> ELASTIC_API_KEY。</strong>锉刀现在，让我们创建一个 Elasticsearch 无服务器项目，将其作为后端，为我们示例应用的搜索功能提供动力。</p><h2>创建弹性无服务器项目</h2><ol><li>访问<a href="http://cloud.elastic.co">cloud.elastic.co</a>，点击<strong>创建新的无服务器项目</strong></li><li>单击 &quot;<strong>下一步 </strong>&quot;查看<strong>Elasticsearch</strong>解决方案</li><li>选择<strong>优化的载体</strong></li><li>将<strong>云提供商</strong>设置为<strong>Azure</strong></li><li>点击<strong>创建无服务器项目</strong></li><li>单击主导航菜单中的 &quot;<strong>入门</strong>&quot;，然后向下滚动以复制 &quot;<strong>连接详情</strong>&quot;。</li><li>单击 &quot;<strong> </strong>复制&quot;按钮，从 &quot; 连接详细信息&quot;中复制<strong> Elasticsearch 端点 。</strong></li><li>更新<strong><em>.env</em></strong>文件，将<strong>ELASTIC_URL</strong>设置为复制的<strong>Elasticsearch 端点</strong></li></ol><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/77a35e55446d396066b68cfd132d1543a07b81cc-1600x875.gif" style="width: 100%;" /><h2>创建弹性 API 密钥</h2><ol><li>打开 Elasticsearch 入门 页面，单击<strong> 添加 API 密钥</strong> 部分中的 新建</li><li>输入密钥<strong>名称</strong></li><li>单击<strong> 创建 API 密钥</strong></li><li>单击 &quot;复制 &quot;按钮复制 API 密钥值</li><li>回到<strong>Codespaces，</strong>在这里我们有<strong><em>.env</em> </strong>打开编辑文件，粘贴复制的值以设置<strong> ELASTIC_API_KEY</strong></li></ol><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/20168cba493d8e2c0d9ae7704eb0ae707df58e4c-1600x875.gif" style="width: 100%;" /><h2>打开 Codespaces 笔记本并安装依赖库</h2><p>在文件资源管理器中，选择<strong><em>VectorDBSetup.ipynb</em></strong>文件，打开笔记本。加载笔记本后，找到笔记本中<a href="https://github.com/jwilliams-elastic/msbuild-intelligent-query-demo/blob/main/VectorDBSetup.ipynb?short_path=17c25d8#L40-L52"> 标题为 &quot;<strong> 安装库&quot; 的部分</strong></a><strong> 。</strong>点击章节播放按钮。</p><p>如果这是你第一次在 GitHub Codespaces 中运行笔记本，系统会提示你选择 Codespaces 内核并配置 Python 环境。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/2272f70615dfb9dcbeb91f39b6dd5076213e24a5-1600x875.gif" style="width: 100%;" /><h2>使用 Codespaces Notebook 定义导入和加载环境变量</h2><p>转到笔记本的下一节<a href="https://github.com/jwilliams-elastic/msbuild-intelligent-query-demo/blob/main/VectorDBSetup.ipynb?short_path=17c25d8#L58-L104">，标题为 &quot; <strong>定义导入和加载环境变量</strong></a>&quot;。点击章节播放按钮。</p><p>这段代码导入了 Notebook 使用的 Python 库，并从<em>.env </em>中加载了环境变量。 您之前创建的</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/aa9f9198ff452ac0c4ce33b00f253731dbee22c5-1600x875.gif" style="width: 100%;" /><h2>使用 Codespaces Notebook 创建 Elastic ML 推理端点</h2><p>转到下一个笔记本<a href="https://github.com/jwilliams-elastic/msbuild-intelligent-query-demo/blob/main/VectorDBSetup.ipynb?short_path=17c25d8#L112-L157">部分，标题为 <strong>创建 ML 推理端点</strong></a>。点击章节播放按钮。</p><p>这将在 Elasticsearch 项目中创建一个新的 ML 推理端点，我们将用它从数据中生成文本嵌入。文本嵌入是文本的向量表示，将存储在 Elasticsearch 中以支持语义搜索。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/97c1afab3e64027ee5ae77f377d56ba406ae1765-1600x875.gif" style="width: 100%;" /><h2>使用 Codespaces Notebook 创建 Elasticsearch 索引</h2><p>转到下一个笔记本<a href="https://github.com/jwilliams-elastic/msbuild-intelligent-query-demo/blob/main/VectorDBSetup.ipynb?short_path=17c25d8#L165-L224">部分，标题为 <strong>创建 Elasticsearch 索引</strong></a>。点击章节播放按钮。</p><p>这将创建 Elasticsearch 索引，用于存储示例数据和通过 ML 推理端点生成的任何相关向量数据。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/23a66283ee41239e24fb8455c3cd95641982ca6b-1600x875.gif" style="width: 100%;" /><h2>使用 Codespaces Notebook 创建 Elasticsearch 搜索模板</h2><p>继续下一个笔记本<a href="https://github.com/jwilliams-elastic/msbuild-intelligent-query-demo/blob/main/VectorDBSetup.ipynb?short_path=17c25d8#L232-L384">部分，标题为 &quot; <strong>搜索模板</strong></a>&quot;。点击章节播放按钮。</p><p>这样就创建了一个<a href="https://www.elastic.co/cn/docs/solutions/search/search-templates">搜索模板</a>，我们的示例应用程序将使用它作为模板，其中填充了从用户搜索查询中解析出的单词。这样，我们就可以配置和控制在 Elasticsearch 索引中查询数据时的特定程度。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/4c001a3e4d1cca4cfb5c043fea92c7ccaf9cb64a-1600x875.gif" style="width: 100%;" /><h2>使用 Codespaces Notebook 将数据输入 Elasticsearch 索引</h2><p>转到笔记本的下一部分<a href="https://github.com/jwilliams-elastic/msbuild-intelligent-query-demo/blob/main/VectorDBSetup.ipynb?short_path=17c25d8#L392-L450">，标题为 &quot; <strong>输入属性数据</strong></a>&quot;。点击章节运行按钮。</p><p>运行该代码段将批量加载<em>properties.jsonl </em>文件中包含的示例数据。几分钟后，您应该会看到程序成功完成的确认信息。您可以访问 Elastic Cloud 中的 &quot;<strong>索引管理</strong>&quot;部分，确认索引包含预期记录。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/e9698c798541ccfc08143a939846597028e3c566-1600x875.gif" style="width: 100%;" /><h2>创建 appsetting.json 以配置 C# 应用程序</h2><p>创建了 Elasticsearch 索引并填充了数据后，我们现在就可以配置示例应用程序，使其与 Elastic 和 Azure Cloud 协同工作。C# 示例应用程序使用名为<em>appsettings.json</em>的文件来存储和加载 API 密钥等访问信息。现在，您将使用 Codespaces 中的编辑器创建<em>appsettings.json</em>文件。</p><p>1.在<strong> HomeFinderApp</strong> 文件夹中创建<em> appsettings.json</em></p><p>2.将以下代码粘贴到<em>appsettings.json</em>文件中</p><pre><code>{
 "ElasticSettings": {
   "Url": "",
   "ApiKey": "",
   "IndexName": "properties",
   "TemplateId": "properties-search-template"
 },
 "AzureOpenAISettings": {
   "Endpoint": "",
   "ApiKey": "",
   "DeploymentName": "gpt-4o"
 },
 "AzureMapsSettings": {
   "Url": "https://atlas.microsoft.com/geocode",
   "ApiKey": ""
 },
 "Logging": {
   "LogLevel": {
 	"Default": "Information",
 	"Microsoft.AspNetCore": "Warning"
   }
 },
 "AllowedHosts": "*"
}
</code></pre><p>3.查找<strong> ElasticSettings</strong> 部分下的<strong> Url</strong> 和<strong> ApiKey</strong> 值。将它们设置为与在<em>.env</em>文件。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/00dad25bacdea2adcbd1e6eca7658867a49b0d8c-1600x875.gif" style="width: 100%;" /><h2>创建 Azure OpenAI 服务</h2><p>我们的示例应用程序将使用 Azure OpenAI 来解析应用程序用户的查询，然后通过填充搜索模板向 Elasticsearch 提交请求，以尝试灵活地传达用户正在搜索的内容。</p><ol><li>打开新的浏览器选项卡，转到<a href="https://portal.azure.com/#blade/Microsoft_Azure_ProjectOxford/CognitiveServicesHub/OpenAI"> Azure 门户中的 AI Foundry | Azure OpenAI</a> 。点击<strong>+</strong>创建</li><li>在创建表格中，选择一个<strong>资源组</strong>。</li><li>输入<strong>名称</strong></li><li>选择<strong>定价层级</strong></li><li>点击<strong>下一步</strong></li><li>在<strong>网络</strong>选项卡上，单击<strong> 下一步</strong></li><li>在 &quot;<strong>标签</strong>&quot;选项卡上，单击 &quot;<strong>下一步</strong></li><li>在 &quot;<strong>审核和提交</strong>&quot;选项卡上，单击 &quot;<strong>创建</strong></li><li>创建完成后，单击<strong>转到资源</strong></li><li>从左侧导航菜单中选择<strong>密钥和端点</strong></li><li>复制<strong>端点</strong>并将其粘贴到在浏览器标签页中创建的<em>appsettings.json</em>文件中，该标签页已打开 Codespaces 编辑器。</li><li>然后返回包含 Azure OpenAI<strong>密钥和端点页面</strong>的浏览器选项卡。单击<strong>关键字 1</strong> 的复制按钮，然后将复制的值粘贴到<em>appsettings.json</em>文件中，回到打开 Codespaces 编辑器的浏览器标签页。</li></ol><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/6d49197302d110410dca0a53b6ae90237cf2dfd6-1600x875.gif" style="width: 100%;" /><h2>为 Azure Open AI 服务添加 gpt-4o 模型部署</h2><p>很好，我们现在已经有了一个正在运行的 Azure OpenAI 服务，但它仍然需要一个模型部署，以提供我们的示例应用程序所需的 LLM 功能。有多种型号可供选择。既然已在创建的 <em>appsettings.json</em> 文件中指定了<em> gpt-4o ，那就部署它吧。</em></p><p></p><ol><li>转到<a href="https://ai.azure.com/resource/playground">Azure AI Foundry</a>并单击<strong>创建部署</strong></li><li>搜索<em>gpt-4o</em>，并从结果中选择它</li><li>单击<strong>确认</strong>选择</li><li>单击 &quot;部<strong>署</strong>&quot;来部署模型</li></ol><p>成功部署<em>gpt-4o</em>模型后，可以从左侧导航菜单中选择 &quot;<strong>部署</strong>&quot;，确认<strong><em>gpt-4o</em></strong>部署的<strong>状态</strong>为 &quot;<strong>已成功</strong>&quot;。<br/></p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/1b817ab67c05634e9c72777593b4d1a2c6c28191-1600x875.gif" style="width: 100%;" /><h2>创建 Azure 地图账户</h2><p>我们希望我们示例应用程序的用户能够搜索特定区域的房地产，但又不必太具体。如果有人想搜索当地农贸市场附近的房产，OpenAI LLM 可以使用 Azure 地图服务来获取市场的纬度和经度坐标。然后，在发送给 Elasticsearch 的基于搜索模板的请求中可以包含这些坐标，以便用户查询包含特定位置和地理距离的信息。</p><ol><li><strong></strong><a href="https://portal.azure.com/#browse/Microsoft.Maps%2Faccounts">单击 Azure 地图账户 中的 创建</a></li><li>选择<strong>资源组</strong></li><li>输入<strong>名称</strong></li><li>同意许可和隐私声明</li><li>单击<strong>审查并创建</strong></li><li>点击<strong>创建</strong></li><li>账户创建完成后，单击<strong>转到资源</strong></li><li>单击左侧导航菜单中的<strong>身份验证</strong></li><li>复制 主键值<strong> </strong>并粘贴到包含 Codespaces 编辑器的浏览器选项卡上，使其成为<em> appsettings.json</em> 文件中<strong> AzureMapsSettings</strong> 部分的 ApiKey<strong> 值</strong></li></ol><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/90fadd48e366682e2bad91e32988f93c6354e126-1600x875.gif" style="width: 100%;" /><h2>试用示例应用程序</h2><p>现在是有趣的部分。是时候运行示例应用程序了。我们已经掌握了所有配置细节，以及为应用程序提供动力所需的 Elastic Cloud 和 Azure Cloud 资源。</p><p>1.在 Codespaces 编辑器中打开终端窗口。</p><p>2.使用以下命令将活动目录更改为示例应用程序文件夹。<br/></p><pre><code>cd HomeFinderApp</code></pre><p>3.使用以下<em>dotnet</em>命令运行应用程序。</p><pre><code>dotnet run</code></pre><p>4.出现 &quot;<strong>在浏览器中打开 </strong>&quot;按钮时，单击该按钮。</p><p>5.测试默认搜索，然后尝试一些自己的自定义搜索。如果想查看后台生成搜索结果的更多详细信息，可以单击 工具调用 旁边的<strong> </strong>显示<strong> 链接。</strong></p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/821fe7b9446de5ed646d938cc9484a7ddad21030-1600x875.gif" style="width: 100%;" /><p><strong>奖励： </strong>如果你真的想测试 GPT-4o，请尝试以下搜索：<em>我正在寻找佛罗里达州迪斯尼世界附近的房产，有 30 多间卧室、20 多间浴室、游泳池和车库，靠近海滩，价格在 20 万以下。 </em>该查询将在多次调用搜索工具后返回结果。</p><h2>Elastic 是您的搜索人工智能解决方案</h2><p>运行中的应用程序是 Gen AI LLM 引导搜索的一个示例，它通过搜索模板使用 Elasticsearch 作为基础数据源。请随意尝试和定制示例应用程序，以创建精确而灵活的搜索体验，帮助用户找到他们想要的内容。</p><p>感谢您的阅读。试试<a href="https://cloud.elastic.co/registration">弹性云</a>。</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[模型上下文协议（MCP）的现状]]></title>
            <description><![CDATA[了解 MCP、项目更新、功能、安全挑战、新兴用例以及如何使用 Elastic 的 Elasticsearch MCP 服务器。]]></description>
            <link>https://www.elastic.co/search-labs/cn/blog/mcp-current-state</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/cn/blog/mcp-current-state</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[JD Armada]]></dc:creator>
            <pubDate>Thu, 12 Jun 2025 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/02b8c8672ffa129e0ed91a92d6cab612a01d27f2-1200x628.png?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>我最近参加了在旧金山举行的<a href="https://mcpdevsummit.ai/">MCP 开发者峰会</a>，很明显，模型上下文协议（MCP）正在迅速成为人工智能代理和上下文丰富的人工智能应用的基础构件。在 Elastic，我们正朝着这个方向努力，通过<a href="https://www.elastic.co/cn/elasticsearch/agent-builder">Agent Builder</a> 直接公开 MCP 服务器，使 Elasticsearch 成为任何兼容 MCP 的代理的一流上下文和工具提供商。在本篇文章中，我将介绍此次活动的主要更新、新兴用例、MCP 的发展前景，以及如何使用 Agent Builder 通过 MCP 向代理提供 Elasticsearch。</p><h2>什么是模型上下文协议（MCP）？</h2><p>对于不熟悉的人来说，<a href="https://modelcontextprotocol.io/introduction">模型上下文协议</a>是一种开放标准，它提供了一种结构化的双向方式，将人工智能模型连接到各种数据源和工具中，使其能够生成更相关、更明智的响应。它通常被称为 &quot;<a href="https://modelcontextprotocol.io/introduction">用于人工智能应用的 USB-C 端口</a>&quot;。</p><p>下面的结构图突出了它的双向性：</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/5eba1e59514eb58a5220bb92bb49e6328ee83cd7-674x466.png" style="width: 100%;" /><p>这对人工智能从业人员来说是一个重大转变，因为扩展人工智能应用的主要挑战之一就是必须为每个新数据源建立自定义集成。MCP 提供了一个可持续、可重复使用的架构，用于管理和提供模型的上下文。它与模型无关，与服务器无关，并且完全开源。</p><p>MCP 是一系列 API 规范的最新版本，旨在实现应用程序之间集成的标准化。过去，我们有用于 RESTful 服务的 OpenAPI、用于数据查询的 GraphQL 和用于微服务通信的 gRPC。MCP 不仅在结构上与这些旧规范一样严谨，而且还将其带入了生成式人工智能环境，使其更容易将代理插入不同的系统，而无需定制连接器。在许多方面，MCP 的目标都是为人工智能代理做 HTTP 为网络所做的事情。正如 HTTP 标准化了浏览器和网站之间的通信一样，MCP 试图标准化人工智能代理与周围数据世界的交互方式。</p><h2>MCP 与其他代理协议的比较</h2><p>代理协议的范围正在迅速扩大，有十多种新兴标准竞相定义代理的交互方式。来自 LlamaIndex 的<a href="https://x.com/seldo">Laurie Voss</a>介绍说，大多数协议可分为两类：侧重于代理相互对话的代理间协议和侧重于向 LLM 提供结构化上下文的面向上下文的协议（如 MCP）。</p><p>其他流行的协议，如谷歌的<a href="https://developers.googleblog.com/en/a2a-a-new-era-of-agent-interoperability/">A2A</a>（代理对代理）、思科和 IBM 的<a href="https://agentcommunicationprotocol.dev/introduction/welcome">ACP</a>（代理通信协议）以及<a href="https://agoraprotocol.org/">Agora</a>，都旨在实现代理对代理的协商、联盟建设甚至分散式身份系统。在这方面，MCP 采取了更加务实的方法，因为它关注的是代理如何访问工具和数据，而不一定是代理之间如何对话（尽管 MCP 在未来也可以通过不同的方式实现这一点）。</p><p>目前，MCP 的与众不同之处在于其牵引力和动力。与前端框架早期的 React 一样，MCP 也是从一个利基问题开始的，现在已成为实践中最常用、最具扩展性的代理协议之一。</p><h2>峰会回顾：MCP 不断变化的优先事项</h2><p>此次峰会的演讲者来自 Anthropic、Okta、OpenAI、AWS、GitHub 等公司。会谈内容从核心协议的改进到现实世界的实施，并概述了当前和长期的优先事项。这些会谈反映了从早期实验和简单的工具调用到以 MCP 为基础构建可信、可扩展和模块化的人工智能系统的转变。</p><p>几位发言人预言，在未来，MCP 不仅仅是协议管道，它还能成为人工智能原生网络的基础。就像 JavaScript 能让用户点击网页并与之互动一样，MCP 也能让代理代表我们执行同样的操作。例如，在电子商务中，用户无需手动导航到网站购物，只需告诉代理登录、找到特定产品、将其添加到购物车并结账即可。</p><p>这也不是纯粹的猜测和炒作；PayPal 在峰会上展示了他们新的代理工具包和 MCP 服务器，可以实现这种确切的代理商务体验。有了 MCP 提供的对工具和数据源的安全可靠访问，代理商将不仅仅是阅读网络，他们还将能够对其采取行动。如今，MCP 已经成为一个强大的标准，发展势头迅猛，未来可能成为全网人工智能增强型用户交互的标准。</p><h2>MCP 项目更新：运输、征询和结构化工具</h2><p>MCP 的核心贡献者<a href="https://x.com/JeromeSwannack">杰罗姆-斯旺纳克（Jerome Swannack</a>）分享了过去 6 个月中协议规范的一些更新。这些变化的主要目标是</p><ol><li>添加可流 HTTP 后启用远程 MCP</li><li>通过增加诱导和工具输出模式，建立更丰富的代理交互模型</li></ol><p>由于 MCP 是开放源代码，开发人员已经可以实现流式 HTTP 等更改。激发和工具输出示意图目前尚未发布；它们正处于草案阶段，可能会有所变化。</p><p><strong>可流 HTTP </strong><a href="https://modelcontextprotocol.io/specification/2025-03-26/basic/transports">（发布于 03-26-2025</a>）<strong>：</strong>一个有影响的技术更新是引入了可流 HTTP 作为新的传输机制。它以一种更具可扩展性的双向模式取代了服务器发送事件（SSE），该模式支持分块传输编码和通过单个 HTTP 连接渐进式信息传输。这使您能够在 AWS Lambda 等云基础设施上部署 MCP 服务器，并支持企业网络限制，而无需长期连接或轮询。</p><p><strong>诱导</strong> （ Elicitation ）<a href="https://modelcontextprotocol.io/specification/2025-06-18/client/elicitation"> （发布于 06-18-2025 ）</a><strong> ：</strong> 诱导允许服务器定义一个模式，说明他们希望如何从客户端获得上下文结构。从根本上说，服务器可以描述它需要什么以及它期望的输入类型。这将产生一些影响：对于服务器构建者来说，他们可以构建更复杂的代理交互。对于客户端构建者来说，他们可以实现适应这些模式的动态用户界面。不过，不应利用诱导来获取用户的敏感信息或个人身份信息。开发人员应遵循<a href="https://modelcontextprotocol.io/specification/draft/client/elicitation#security-considerations">最佳实践</a>，确保诱导提示安全、适当，尤其是随着 MCP 的成熟。这与我们稍后将讨论的更广泛的安全问题有关。</p><p><strong>工具输出示意图 </strong><a href="https://modelcontextprotocol.io/specification/draft/server/tools#output-schema">（发布于 06-18-2025</a>）<strong>： </strong>这一概念允许客户和 LLM 提前了解工具输出形状。工具输出模式可让开发人员描述工具预期返回的内容。这些模式解决了直接调用工具的一个主要局限性，即上下文窗口的使用效率低下。上下文窗口被认为是使用 LLM 时最重要的资源之一，当你直接调用一个工具时，它返回的原始内容会全部推送到 LLM 的上下文中。工具输出模式可让 MCP 服务器提供结构化数据，从而帮助您更好地利用标记和上下文窗口。以下是一些有关工具的一般<a href="https://modelcontextprotocol.io/specification/draft/server/tools#security-considerations">最佳实践</a>。</p><p>这些新的更新和未来的新增功能将共同帮助 MCP 成为一个更加模块化、类型化和生产就绪的代理协议。</p><h2>未充分利用的动力特性：采样和根</h2><p>虽然 MCP 规范并不新鲜，但在主题演讲中，采样和根都得到了强调。这两个基本原理目前被忽视，也未得到充分探索，但却能极大地促进代理之间更丰富、更安全的交互。</p><p><strong>取样 - 服务器可从客户端请求完成： </strong><a href="https://modelcontextprotocol.io/docs/concepts/sampling">取样</a>允许 MCP 服务器向客户端 LLM 请求完成。这就增加了协议的双向性，即服务器不只是响应请求，还可以提示并要求客户端的模型生成响应。这样，客户就可以完全控制成本、安全性以及 MCP 服务器使用的模型。因此，在使用带有预配置模型的外部 MCP 服务器的情况下，您无需提供自己的 API 密钥或配置自己对该模型的订阅，因为服务器可以直接提示已连接到客户端的模型。这使得代理行为更加复杂，互动性更强。</p><p><strong>根 - 资源的范围访问： </strong><a href="https://modelcontextprotocol.io/docs/concepts/roots">根</a>的设计目的是为客户端提供一种方法，让服务器了解相关的资源和工作区。这对于设定服务器的运行范围非常有效。值得注意的是，根是 &quot;<a href="https://modelcontextprotocol.io/docs/concepts/roots#how-roots-work">信息性的，而不是严格执行的</a>&quot;，这意味着它们并不定义 MCP 服务器或代理的权利或权限。换句话说，不能仅靠根来阻止服务器或代理执行某些工具或执行写入操作。有了根，权限仍应通过用户批准机制在客户端处理。此外，开发人员仍应注意使用在设计上尊重根设置的界限并采用<a href="https://modelcontextprotocol.io/docs/concepts/roots#best-practices">最佳实践的</a>服务器。</p><h2>代理身份验证：OAuth 2.1 和受保护的元数据</h2><p>本节重点介绍 OAuth 2.1，它是 OAuth 2.0 的最新迭代版本，删除了不安全流程并整合了最佳实践。</p><p>OAuth 支持是一个备受期待的话题，尤其是因为安全性和可扩展性被视为阻碍 MCP 成为将代理连接到工具的标准的主要障碍。<a href="https://x.com/aaronpk">Aaron Parecki</a>（Okta 的 OAuth 2.1 编辑和身份标准专家）讨论了 MCP 如何采用简洁、可扩展的 OAuth 流程，从服务器开发人员那里卸载大部分复杂性。官方 OAuth 2.1 授权规范最近于<a href="https://modelcontextprotocol.io/specification/2025-06-18/basic/authorization">2025 年 6 月 18</a> 日发布了最新的协议修订版。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/2ea159116fe5e03ff800f077adf16d6ca9f1c1d1-1594x1280.png" style="width: 100%;" /><p>在这种实现方式中，OAuth 的责任可由 MCP 客户端和服务器分担。大部分身份验证流程都由 MCP 客户端启动和处理，只有服务器在最后才参与接收和验证安全令牌。这种拆分有助于解决一个关键的扩展问题，即如何在不要求开发人员配置每个连接的情况下跨多个工具进行身份验证，并确保 MCP 服务器开发人员不必成为 OAuth 专家。</p><p>讲座的两大亮点</p><ol><li><a href="https://datatracker.ietf.org/doc/rfc9728/"><strong>受保护资源元数据</strong></a>：MCP 服务器可发布一个 JSON 文件，描述其目的、端点和验证方法。这样，客户端只需输入服务器 URL 就能启动 OAuth 流程，从而简化了连接过程。了解更多信息：<a href="https://aaronparecki.com/2025/04/03/15/oauth-for-model-context-protocol">让我们修复 MCP 中的 OAuth</a></li><li><a href="https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-13"><strong>支持 IDP 和 SSO</strong></a>：企业可以整合身份提供商，集中管理访问。这对用户体验和安全性来说都是双赢。用户无需点击 10 个不同的同意屏幕，安全团队也可以观察到每个连接。</li></ol><p>通过将 OAuth 逻辑推送到客户端并依赖服务器的元数据，MCP 生态系统避免了一个主要瓶颈。这使 MCP 与当今生产环境中现代应用程序接口的安全保护方式更加一致。</p><p>补充阅读：<a href="https://aaronparecki.com/oauth-2-simplified/">OAuth 2 简化版</a></p><h2>可组合生态系统中的安全挑战</h2><p>新的发展也带来了新的攻击面。思科公司的 Arjun Sambamoorthy 列出了 MCP 环境中的几个主要威胁，包括</p>
            <table style="border-collapse: collapse; margin: 20px 0;">
              <tbody>
                
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">威胁</td><td style="border: 1px solid #ccc; padding: 4px;">描述</td><td style="border: 1px solid #ccc; padding: 4px;">补救& 最佳做法</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">及时注射& 工具中毒</td><td style="border: 1px solid #ccc; padding: 4px;">在 LLM 系统上下文或工具描述中注入恶意提示的方法，导致 LLM 执行读取文件或泄漏数据等意外操作。</td><td style="border: 1px solid #ccc; padding: 4px;">使用 MCP 扫描等工具对工具元数据进行检查。在提示中包含说明和参数前对其进行验证。最后，考虑对高风险工具实施用户审批。有关详细信息，请参阅表格下方附加阅读列表中的 OWASP 提示注入指南。</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">采样攻击</td><td style="border: 1px solid #ccc; padding: 4px;">就 MCP 而言，采样为 MCP 服务器对 LLM 进行提示注入攻击打开了大门。</td><td style="border: 1px solid #ccc; padding: 4px;">对不信任的服务器禁用取样功能，并考虑对取样请求增加人工在环审批。</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">恶意 MCP 服务器</td><td style="border: 1px solid #ccc; padding: 4px;">在目前收集的 MCP 服务器中，很难逐一审查以确保安全。流氓服务器会悄悄收集你的数据，并将其暴露给恶意行为者。</td><td style="border: 1px solid #ccc; padding: 4px;">仅从受信任的注册表或内部列表中连接到 MCP 服务器。在具有沙箱功能的容器中运行第三方服务器。</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">恶意 MCP 安装工具</td><td style="border: 1px solid #ccc; padding: 4px;">命令行安装程序和脚本可方便地快速实施 MCP 服务器或工具，但最终可能会安装未经验证的加密代码。</td><td style="border: 1px solid #ccc; padding: 4px;">在沙箱环境中安装并验证软件包签名。切勿从未经验证的来源自动更新。</td>
                    </tr>
              </tbody>
            </table>
          <p>为了进一步解决这个问题，Arjun 建议使用可信的 MCP 注册表来处理所有验证（这是一个前沿和中心话题--更多详情，请参阅下面阅读清单中的前两项），以及使用这份<a href="https://github.com/slowmist/MCP-Security-Checklist">安全检查清单</a>。</p><p>补充阅读：</p><ul><li><a href="https://modelcontextprotocol.io/specification/2025-06-18/basic/security_best_practices">官方 MCP 安全最佳实践</a></li><li><a href="https://owasp.org/www-project-top-10-for-large-language-model-applications/">OWASP 法律硕士申请 10 强</a></li><li><a href="https://hiddenlayer.com/innovation-hub/">隐藏层威胁研究</a></li><li><a href="https://github.com/invariantlabs-ai/mcp-scan">MCP 扫描</a></li><li><a href="https://genai.owasp.org/llmrisk/llm01-prompt-injection/">OWASP 提示注入指南</a></li></ul><h2>下一步：注册、管理和生态系统</h2><p>一个集中的 MCP 注册中心正在开发中，这也是峰会上讨论最多的话题之一。当前的服务器生态系统存在分散、信任度低和可发现性差等问题。开发人员很难找到 MCP 服务器、验证它们的功能并安全安装，尤其是在元数据可能不完整或被欺骗的分散生态系统中。</p><p>集中式注册表可作为可信的真相来源，提高可发现性，确保服务器元数据的完整性，降低安装恶意工具的风险，从而直接解决这些痛点。</p><p>登记册的目标是</p><ul><li>为服务器元数据（服务器的功能、如何验证、安装和调用）提供单一真实来源</li><li>消除不完整的第三方注册表和碎片，这样当服务器想要注册时，就不必更新互联网上的所有其他注册表。</li><li>提供服务器注册流程，其中包括 CLI 工具和包含上述元数据的 server.json 文件。</li></ul><p>更广泛的希望是，可信的注册表将有助于安全地扩展生态系统，使开发人员能够放心地构建和共享新工具。</p><p>对 Anthropic 来说，治理是另一个首要问题。他们明确表示，MCP 应保持开放和社区驱动，但这种管理模式的推广仍在进行中。他们目前正在寻求这方面的帮助，并希望任何有开源协议管理经验的人伸出援手。这就引出了我想说的另一个话题。在整个活动中，发言人都强调，只有内部开发人员做出贡献，生态系统才能发展壮大。为了使 MCP 成为新的网络标准，并从其他流行的代理协议中脱颖而出，我们需要集中精力。</p><h2>现实世界中的 MCP：案例研究和演示</h2><p>一些组织分享了如何在实际应用中使用 MCP：</p><ul><li><strong>PayPal - 用于代理商务的 MCP 服务器： </strong>PayPal 展示了其新的<a href="https://github.com/paypal/agent-toolkit/">代理工具包</a>和 MCP 服务器，它们可以从根本上改变用户的购物体验。用户无需在社交媒体上搜索商品、比较价格和结账，而是可以与连接到 PayPal MCP 服务器的代理聊天，处理所有这些操作。<br/></li><li><strong>EpicAI.pro - 贾维斯：</strong>MCP 的发展让我们越来越接近拥有一个现实生活中的 Jarvis 型助手。对于那些不熟悉《钢铁侠》电影的人来说，Jarvis 是一个人工智能助手，它使用自然语言，能对多模式输入做出响应，响应零延迟，能主动预测用户需求，自动管理集成，并能在设备和位置之间进行上下文切换。如果我们把贾维斯想象成一个物理机器人助手，那么 MCP 就赋予了贾维斯 &quot;双手 &quot;或处理复杂任务的能力。<br/></li><li><strong>Postman - <a href="https://www.postman.com/explore/mcp-generator">MCP 服务器生成器</a>： </strong>为 API 请求提供购物车体验，您可以挑选不同的 API 请求，将其放入购物篮，然后将整个购物篮下载为 MCP 服务器。<br/></li><li><strong>彭博社 - 彭博社 </strong>解决了企业 GenAI 开发中的一个关键瓶颈。他们拥有近 10,000 名工程师，需要一种标准化的方法来跨团队集成工具和代理。通过 MCP，他们将内部工具转变为模块化、远程优先的组件，代理可以在统一界面上轻松调用。这使他们的工程师能够在整个组织内贡献工具，而人工智能团队则专注于构建代理，而不是定制集成。Bloomberg 现在支持可扩展的安全代理工作流，可实现与 MCP 生态系统的完全互操作性。彭博社没有链接任何公共资源，但这是他们在峰会上公开展示的内容。<br/></li><li><strong>Block- </strong>-Block使用MCP为内部人工智能代理<a href="https://github.com/block/goose?tab=readme-ov-file">Goose</a>提供支持，使员工能够自动执行工程、销售、营销等方面的任务。他们为 Git、Snowflake、Jira 和 Google Workspace 等工具构建了 60 多个 MCP 服务器，以实现与日常使用的系统进行自然语言交互。现在，Block 公司的员工可以使用 Goose 查询数据、检测欺诈、管理事件、浏览内部流程等，而无需编写代码。MCP 在短短 2 个月内就帮助 Block 在许多工作职能中推广了人工智能。<br/></li><li><strong>AWS - <a href="https://github.com/awslabs/mcp">AWS MCP 服务器</a>： </strong>AWS 推出了一款以《龙与地下城》为主题的有趣 MCP 服务器，该服务器可模拟掷骰子，跟踪过去的掷骰子情况，并使用可流式 HTTP 返回结果。这个轻量级示例强调了使用 Lambda 和 Fargate 等 AWS 工具和基础设施构建和部署 MCP 服务器是多么容易。他们还介绍了<a href="https://aws.amazon.com/blogs/opensource/introducing-strands-agents-an-open-source-ai-agents-sdk/">Strands SDK</a>，这是一个用于构建与 MCP 服务器交互的多模式代理的开源工具包。</li></ul><h2>弹性代理生成器支持 MCP</h2><p>您现在就可以使用<a href="https://www.elastic.co/cn/search-labs/blog/elastic-ai-agent-builder-context-engineering-introduction">Elastic Agent Builder</a>开始尝试使用 MCP，它是直接在您的数据之上构建代理的最简单方法。代理生成器可让您将由 Elasticsearch 支持的工具暴露给兼容 MCP 的代理，它已配备了一些功能强大的内置工具，包括</p><ul><li><code>platform.core.search</code> - 使用完整的 Elasticsearch 查询 DSL 执行搜索</li><li><code>platform.core.list_indices</code> - 列出 Elasticsearch 中所有可用的索引（帮助代理发现存在的数据）</li><li><code>platform.core.get_index_mapping</code> - 检索特定索引的字段映射（帮助代理了解数据的形状和类型）</li><li><code>platform.core.get_document_by_id</code> - 按 ID 抓取特定文件（用于精确检索）</li></ul><p>只需使用这些工具，您就能为您的代理提供企业级搜索和相关性，这是建立可靠的人工智能代理的核心。</p><p>使 Agent Builder 功能更加强大的是，它能够根据应用程序的需要，定义和公开您自己的定制工具。这对意见性或可重复的工作流特别有用，因为在这种情况下，你希望代理针对特定索引执行特定类型的搜索，而不必每次都重新发现该逻辑。您可以将这种意图直接编码到工具中，从而使您的代理更快、更可靠、更具成本效益，而不必为得出相同的结论而花费大量时间进行规划和推理。</p><p>在代理生成器用户界面中，下面是一个使用 ES|QL 的自定义工具定义示例：</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/c1f6405a374b707e8e6fa36b9e21db5f3c7cd127-1376x864.png" style="width: 100%;" /><p>定义好自定义工具后，点击<code>Manage MCP</code> 下拉菜单并复制 MCP 服务器 URL，就可以使用 MCP 公开这些工具（以及内置的本地工具）。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/9f39588525ca2643475de557ea54a6bcf5c150f6-1282x616.png" style="width: 100%;" /><p>现在，您可以将此 MCP 端点导入任何使用 MCP 的客户端，使其连接到 Agent Builder，并访问所有可用工具。更多信息，请阅读<a href="https://www.elastic.co/cn/search-labs/blog/elastic-ai-agent-builder-context-engineering-introduction">Agent Builder</a> 简介。</p><h2>结论</h2><p>MCP Dev 峰会清楚地表明，MCP 正在塑造这些人工智能代理彼此互动以及与周围数据世界互动的方式。无论您是将代理连接到企业数据，还是设计完全自主的代理，MCP 都能提供一种标准化、可组合的集成方式，并迅速在大规模应用中发挥作用。从传输协议和安全模式到注册和管理，MCP 生态系统正在迅速成熟。MCP 将继续保持开放性和社区驱动，因此现在的开发人员有机会塑造其发展。</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[混合搜索重温：在 Elasticsearch 中引入线性检索器！]]></title>
            <description><![CDATA[了解线性检索器如何利用加权分数和 MinMax 归一化来增强混合搜索，从而获得更精确、更一致的排名，并学习如何使用它。]]></description>
            <link>https://www.elastic.co/search-labs/cn/blog/linear-retriever-hybrid-search</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/cn/blog/linear-retriever-hybrid-search</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <pubDate>Wed, 28 May 2025 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/7a07e27e3095463ff93b4cb7f8a0cf3b8e44eab0-1777x1000.png?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>在<a href="https://www.elastic.co/cn/search-labs/blog/elasticsearch-retrievers-ga-8.16.0">上一篇博文</a>中，我们介绍了重新设计的 &quot;从零开始 &quot;检索器框架，它可以创建复杂的排名管道。我们还探讨了互易排名融合（RRF）检索器如何通过合并不同查询的结果来实现混合搜索。虽然 RRF 很容易实现，但它有一个明显的局限性：它只关注相对排名，而忽略了实际得分。这就给微调和优化带来了挑战。</p><h2>直线型寻回犬</h2><p>在本篇文章中，我们将介绍<a href="https://www.elastic.co/cn/docs/solutions/search/retrievers-overview#retrievers-overview-types"><code>linear</code></a> <a href="https://www.elastic.co/cn/docs/solutions/search/retrievers-overview#retrievers-overview-types">retriever</a>，它是我们支持混合搜索的最新成员！与<code>rrf</code> 不同，<code>linear</code> retriever 计算的是与文档匹配的所有查询的加权总和。这种方法既能保留结果集中每个文档的相对重要性，又能精确控制每个查询对最终得分的影响。因此，它为微调混合搜索提供了一种更直观、更灵活的方式。</p><p>定义一个线性检索器，其最终得分的计算公式为</p><div class="katex">score = 5 * knn + 1.5 * bm25</div><p>就这么简单：</p><pre><code>GET linear_retriever_blog/_search
{
   "retriever": {
       "linear": {
           "retrievers": [
               {
                   "retriever": {
                       "knn": {
                          ...
                        }
                    },
                   "weight": 5
               },
                  {
                   "retriever": {
                       "standard": {
                          ...
                        }
                    },
                   "weight": 1.5
               },


           ]
        }
     }
}</code></pre><p>注意到它有多简单直观了吗？(与<code>rrf</code> 非常相似！）这种配置允许您精确控制每种查询类型对最终排名的贡献程度，这与<code>rrf</code> 不同，后者仅依赖于相对排名。</p><p>需要注意的是：<code>knn</code> 分数可能有严格的界限，这取决于所使用的相似性指标。例如，使用余弦相似度或单位归一化向量的点积，得分总是在<code>[0, 1]</code> 范围内。相比之下，<code>bm25</code> 分数的可预测性较差，而且没有明确的界限。</p><h2>评分缩放：KNN vs BM25</h2><p>混合搜索面临的一个挑战是，不同的检索器会产生不同的分数。例如，请考虑以下情况：</p><p>查询 A 得分：</p>
            <table style="border-collapse: collapse; margin: 20px 0;">
              <tbody>
                
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;"></td><td style="border: 1px solid #ccc; padding: 4px;">doc1</td><td style="border: 1px solid #ccc; padding: 4px;">doc2</td><td style="border: 1px solid #ccc; padding: 4px;">doc3</td><td style="border: 1px solid #ccc; padding: 4px;">文档4</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">knn</td><td style="border: 1px solid #ccc; padding: 4px;">0.347</td><td style="border: 1px solid #ccc; padding: 4px;">0.35</td><td style="border: 1px solid #ccc; padding: 4px;">0.348</td><td style="border: 1px solid #ccc; padding: 4px;">0.346</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">bm25</td><td style="border: 1px solid #ccc; padding: 4px;">100</td><td style="border: 1px solid #ccc; padding: 4px;">1.5</td><td style="border: 1px solid #ccc; padding: 4px;">1</td><td style="border: 1px solid #ccc; padding: 4px;">0.5</td>
                    </tr>
              </tbody>
            </table>
          <p>查询 B 得分：</p>
            <table style="border-collapse: collapse; margin: 20px 0;">
              <tbody>
                
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;"></td><td style="border: 1px solid #ccc; padding: 4px;">doc1</td><td style="border: 1px solid #ccc; padding: 4px;">doc2</td><td style="border: 1px solid #ccc; padding: 4px;">doc3</td><td style="border: 1px solid #ccc; padding: 4px;">文档4</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">knn</td><td style="border: 1px solid #ccc; padding: 4px;">0.347</td><td style="border: 1px solid #ccc; padding: 4px;">0.35</td><td style="border: 1px solid #ccc; padding: 4px;">0.348</td><td style="border: 1px solid #ccc; padding: 4px;">0.346</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">bm25</td><td style="border: 1px solid #ccc; padding: 4px;">0.63</td><td style="border: 1px solid #ccc; padding: 4px;">0.01</td><td style="border: 1px solid #ccc; padding: 4px;">0.3</td><td style="border: 1px solid #ccc; padding: 4px;">0.4</td>
                    </tr>
              </tbody>
            </table>
          <p>您可以从上面看到这种差异：<code>kNN</code> 分数介于 0 和 1 之间，而<code>bm25</code> 分数可能相差悬殊。这种差异使得设置静态最佳权重以合并结果变得非常棘手。</p><h2>归一化拯救：MinMax 归一化器</h2><p>为了解决这个问题，我们引入了一个可选的<code>minmax</code> 归一化器，该归一化器使用以下公式将每个查询的分数独立缩放至<code>[0, 1]</code> 范围：</p><div class="katex">normalized_score = (score - min) / (max - min)</div><p>这就保留了每个文档在查询结果集中的相对重要性，从而更容易合并来自不同检索器的得分。正常化后，分数变为</p><p>查询 A 得分：</p>
            <table style="border-collapse: collapse; margin: 20px 0;">
              <tbody>
                
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;"></td><td style="border: 1px solid #ccc; padding: 4px;">doc1</td><td style="border: 1px solid #ccc; padding: 4px;">doc2</td><td style="border: 1px solid #ccc; padding: 4px;">doc3</td><td style="border: 1px solid #ccc; padding: 4px;">文档4</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">knn</td><td style="border: 1px solid #ccc; padding: 4px;">0.347</td><td style="border: 1px solid #ccc; padding: 4px;">0.35</td><td style="border: 1px solid #ccc; padding: 4px;">0.348</td><td style="border: 1px solid #ccc; padding: 4px;">0.346</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">bm25</td><td style="border: 1px solid #ccc; padding: 4px;">1.00</td><td style="border: 1px solid #ccc; padding: 4px;">0.01</td><td style="border: 1px solid #ccc; padding: 4px;">0.005</td><td style="border: 1px solid #ccc; padding: 4px;">0.000</td>
                    </tr>
              </tbody>
            </table>
          <p>查询 B 得分：</p>
            <table style="border-collapse: collapse; margin: 20px 0;">
              <tbody>
                
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;"></td><td style="border: 1px solid #ccc; padding: 4px;">doc1</td><td style="border: 1px solid #ccc; padding: 4px;">doc2</td><td style="border: 1px solid #ccc; padding: 4px;">doc3</td><td style="border: 1px solid #ccc; padding: 4px;">文档4</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">knn</td><td style="border: 1px solid #ccc; padding: 4px;">0.347</td><td style="border: 1px solid #ccc; padding: 4px;">0.35</td><td style="border: 1px solid #ccc; padding: 4px;">0.348</td><td style="border: 1px solid #ccc; padding: 4px;">0.346</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">bm25</td><td style="border: 1px solid #ccc; padding: 4px;">1.00</td><td style="border: 1px solid #ccc; padding: 4px;">0.000</td><td style="border: 1px solid #ccc; padding: 4px;">0.465</td><td style="border: 1px solid #ccc; padding: 4px;">0.645</td>
                    </tr>
              </tbody>
            </table>
          <p>现在，所有得分都在<code>[0, 1]</code> 范围内，加权总和的优化也更加简单明了，因为我们现在捕捉的是结果的重要性（相对于查询而言），而不是绝对得分，并能在不同查询中保持一致。</p><h2>线性寻回器示例 </h2><p>现在，让我们通过一个例子来展示上述内容，以及<code>linear</code> Retriever 如何解决<code>rrf</code> 的一些不足之处。RRF 仅依靠相对排名，不考虑实际分数差异。例如，给出这些分数：</p>
            <table style="border-collapse: collapse; margin: 20px 0;">
              <tbody>
                
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;"></td><td style="border: 1px solid #ccc; padding: 4px;">doc1</td><td style="border: 1px solid #ccc; padding: 4px;">doc2</td><td style="border: 1px solid #ccc; padding: 4px;">doc3</td><td style="border: 1px solid #ccc; padding: 4px;">文档4</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">knn</td><td style="border: 1px solid #ccc; padding: 4px;">0.347</td><td style="border: 1px solid #ccc; padding: 4px;">0.35</td><td style="border: 1px solid #ccc; padding: 4px;">0.348</td><td style="border: 1px solid #ccc; padding: 4px;">0.346</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">bm25</td><td style="border: 1px solid #ccc; padding: 4px;">100</td><td style="border: 1px solid #ccc; padding: 4px;">1.5</td><td style="border: 1px solid #ccc; padding: 4px;">1</td><td style="border: 1px solid #ccc; padding: 4px;">0.5</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">rrf 分数</td><td style="border: 1px solid #ccc; padding: 4px;">0.03226</td><td style="border: 1px solid #ccc; padding: 4px;">0.03252</td><td style="border: 1px solid #ccc; padding: 4px;">0.03200</td><td style="border: 1px solid #ccc; padding: 4px;">0.03125</td>
                    </tr>
              </tbody>
            </table>
          <p>rrf 会将文件排序为</p><div class="katex">doc2 > doc1 > doc3 > doc4</div><p>但是，doc1 的<code>bm25</code> 得分明显高于其他文件，而<code>rrf</code> 只查看相对排名，因此未能捕捉到这一点。<code>linear</code> Retriever 结合归一化处理，可以正确地考虑分数及其差异，从而得出更有意义的排名：</p>
            <table style="border-collapse: collapse; margin: 20px 0;">
              <tbody>
                
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;"></td><td style="border: 1px solid #ccc; padding: 4px;">doc1</td><td style="border: 1px solid #ccc; padding: 4px;">doc2</td><td style="border: 1px solid #ccc; padding: 4px;">doc3</td><td style="border: 1px solid #ccc; padding: 4px;">文档4</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">knn</td><td style="border: 1px solid #ccc; padding: 4px;">0.347</td><td style="border: 1px solid #ccc; padding: 4px;">0.35</td><td style="border: 1px solid #ccc; padding: 4px;">0.348</td><td style="border: 1px solid #ccc; padding: 4px;">0.346</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">bm25</td><td style="border: 1px solid #ccc; padding: 4px;">1</td><td style="border: 1px solid #ccc; padding: 4px;">0.01</td><td style="border: 1px solid #ccc; padding: 4px;">0.005</td><td style="border: 1px solid #ccc; padding: 4px;">0</td>
                    </tr>
              </tbody>
            </table>
          <p>如上图所示，doc1 的优秀排名和<code>score</code> 的<code>bm25</code> 都得到了适当的考虑，并反映在最终得分上。此外，所有分数现在都在<code>[0, 1]</code> 范围内，这样我们就能以更直观的方式对它们进行比较和组合（甚至建立离线优化流程）。</p><h2>将所有内容整合在一起</h2><p>要充分利用<code>linear</code> 检索器的正常化功能，搜索请求应如下所示：</p><pre><code>GET linear_retriever_blog/_search
{
   "retriever": {
       "linear": {
           "retrievers": [
               {
                   "retriever": {
                       "knn": {
                          ...
                        }
                    },
                   "weight": 5
               },
                  {
                   "retriever": {
                       "standard": {
                          ...
                        }
                    },
                   "weight": 1.5,
                   "normalizer": "minmax"
               },


           ]
       }
   }
}</code></pre><p>这种方法结合了两方面的优点：既保留了<code>linear</code> Retriever 的灵活性和直观评分，又通过 MinMax 归一化确保了一致的评分缩放。</p><p>与我们所有的检索工具一样，<code>linear</code> 检索工具可以集成到分层检索树的任何层级中，并支持可解释性、匹配高亮、字段折叠等功能。</p><h2>何时选择线性寻回犬，为什么会有区别</h2><p><code>linear</code> 猎犬：</p><ul><li>通过利用实际得分，而不仅仅是排名，来保留相对重要性。</li><li>允许利用不同查询的加权贡献进行微调。</li><li>利用规范化增强一致性，使混合搜索更稳健、更可预测。</li></ul><h2>结论</h2><p><code>linear</code> retriever 已经在 Elasticsearch Serverless 以及 8.18 和 9.0 版本中可用！更多示例和配置参数可参阅我们的文档。试用一下，看看它如何改善您的混合搜索体验--我们期待您的反馈。搜索愉快</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[使用 Quepid 创建判断列表]]></title>
            <description><![CDATA[学习如何通过协作式人工评分流程在 Quepid 中创建判断列表，并利用基准指标优化搜索相关性。]]></description>
            <link>https://www.elastic.co/search-labs/cn/blog/quepid-judgement-lists</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/cn/blog/quepid-judgement-lists</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Daniel Wrigley]]></dc:creator>
            <pubDate>Mon, 26 May 2025 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/307c1774bd31f92bb4aa7b69e1a6796240465100-1600x914.png?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>创建<a href="https://www.elastic.co/search-labs/blog/judgment-lists">判断列表</a>是优化搜索结果质量的关键步骤，但这可能是一项复杂而艰巨的任务。判断列表是一组经过整理的搜索查询，并对其相应结果进行相关性评级，也称为测试集合。使用该列表计算的指标可作为衡量搜索引擎性能的基准。为了帮助简化创建判断列表的过程，<a href="https://opensourceconnections.com/">OpenSource Connections</a>团队开发了<a href="https://quepidapp.com/">Quepid</a>。判断可以是明确的，也可以基于用户的隐性反馈。本博客将指导您在 Quepid 中建立一个协作环境，以便有效地让人类评分员进行明确的判断，这是每个判断列表的基础。</p><p>Quepid 在搜索质量评估过程中为搜索团队提供支持：</p><ul><li>建立查询集</li><li>创建判断列表</li><li>计算搜索质量指标</li><li>根据计算得出的搜索质量指标，比较不同的搜索算法/排名器</li></ul><p>在我们的博客中，假设我们经营一家电影租赁店，目标是提高搜索结果的质量。</p><h2>准备工作</h2><p>本博客使用<a href="https://github.com/o19s/es-tmdb">es-tmdb 资源库</a>中的数据和映射。数据来自<a href="https://www.themoviedb.org/">电影数据库</a>。接下来，使用映射建立名为 tmdb 的索引，并为数据建立索引。不管是建立本地实例还是使用弹性云部署，都可以正常工作。我们假设本博客使用的是弹性云部署。你可以在<a href="https://github.com/o19s/es-tmdb/blob/master/README.md">es-tmdb 软件仓库的 README</a> 中找到有关如何为数据建立索引的信息。</p><p>对<code>rocky</code> 的标题字段进行简单的匹配查询，以确认有数据可供搜索：</p><pre><code>GET tmdb/_search
{
 "query": {
   "match": {
     "title": "rocky"
   }
 }
}</code></pre><p>您将看到 8 项结果。</p><pre><code>{
 "took": 2,
 "timed_out": false,
 "_shards": {
   "total": 1,
   "successful": 1,
   "skipped": 0,
   "failed": 0
 },
 "hits": {
   "total": {
     "value": 8,
     "relation": "eq"
   }
…
}</code></pre><h2>登录 Quepid</h2><p><a href="https://github.com/o19s/quepid">Quepid</a>是一款能让用户衡量搜索结果质量并进行离线实验以提高质量的工具。</p><p>您可以通过两种方式使用 Quepid：一种是使用<a href="https://app.quepid.com">https://app.quepid.com</a> 上的免费公开托管版本、或在你可以访问的机器上设置 Quepid。本帖假设您使用的是免费托管版本。如果您想在自己的环境中建立一个 Quepid 实例，请遵循《<a href="https://github.com/o19s/quepid/wiki/Installation-Guide">安装指南》</a>。</p><p>无论您选择哪种设置，如果还没有账户，您都需要创建一个账户。</p><h2>如何设置 Quepid 案例</h2><p>Quepid 的组织结构围绕&quot;案例展开。&quot;案例可存储查询、相关性调整设置以及如何与搜索引擎建立连接。</p><ul><li>对于首次使用的用户，请选择<strong>创建第一个相关性案例</strong>。</li><li>老用户可以从顶层菜单中选择<strong>相关性案例</strong>，然后点击<strong>+ 创建案例</strong>。</li></ul><p>请描述性地命名您的案例，例如&quot;电影搜索基线，&quot; ，因为我们希望开始测量和改进我们的基线搜索。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/8f9e480d9aae0d706cfc5371e41f19c706dd452a-594x251.png" style="width: 100%;" /><p>选择<strong>继续</strong>，确认名称。</p><p>接下来，我们建立 Quepid 与搜索引擎的连接。Quepid 可以连接各种搜索引擎，包括 Elasticsearch。</p><p>配置会因 Elasticsearch 和 Quepid 设置的不同而有所差异。要将 Quepid 连接到 Elastic Cloud 部署，我们需要为 Elastic Cloud 部署启用和配置 CORS，并准备好 API 密钥。详细说明见<a href="https://quepid-docs.dev.o19s.com/2/quepid/49/how-to-connect-quepid-to-elastic-cloud"> Quepid 文档中的 相应 操作</a> 指南。</p><p>输入 Elasticsearch 端点信息 (<code>https://YOUR_ES_HOST:PORT/tmdb/_search</code>) 和连接所需的其他信息（如果在<strong>高级</strong>配置选项中部署了 Elastic Cloud，则输入 API 密钥），点击<strong>ping</strong>测试连接，然后选择<strong>继续</strong>进入下一步。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/267339dfaecae2740eb2ee2739bdc971608bdb5f-588x1169.png" style="width: 100%;" /><p>现在，我们定义要在案例中显示的字段。选择所有有助于我们的人工评判员稍后评估文档与给定查询相关性的内容。</p><p>将<code>title</code> 设置为<em>标题字段</em>，将<code>_id</code> 保留为<em>ID 字段</em>，将<code>overview, tagline, cast, vote_average, thumb:poster_path</code> 添加为<em>附加显示字段</em>。最后一个条目显示了结果中电影的小缩略图，为我们和人类评分员提供视觉指导。</p><p>选择<strong>继续</strong>按钮确认显示设置。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/02c58aae8c2ebb6d31f538b27462b4c65428fdc3-594x493.png" style="width: 100%;" /><p>最后一步是在案例中添加搜索查询。通过输入框逐一添加 &quot;<em>星球大战</em>&quot;、&quot;<em>哈里森-福特</em>&quot;和 &quot;<em>最佳动作片</em>&quot;三个查询，然后<strong>继续</strong>。</p><p>理想情况下，案例包含的查询能代表真实的用户查询，并能说明不同类型的查询。现在，我们可以把《<em>星球大战》</em>想象成一个查询，代表所有关于电影名称的查询；把<em>哈里森-福特</em>想象成一个查询，代表所有关于演员的查询；把<em>最佳动作片</em>想象成一个查询，代表所有搜索特定类型电影的查询。这通常称为查询集。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/baccfe96766319aa7255e9bff08913ac87d1517f-595x326.png" style="width: 100%;" /><p>在生产场景中，我们将通过应用<a href="https://opensourceconnections.com/blog/2022/10/13/how-to-succeed-with-explicit-relevance-evaluation-using-probability-proportional-to-size-sampling/">概率比例大小采样</a>等统计技术，从事件跟踪数据中抽取查询样本，并将这些采样查询导入 Quepid，以根据查询频率包含头部（频繁查询）和尾部（不频繁查询）的查询，这意味着我们会偏向于更频繁的查询，而不会排除罕见的查询。</p><p>最后，选择 &quot;<strong>完成&quot;</strong>，您将转到案例界面，看到三个已定义的查询。</p><h2>查询和信息需求</h2><p>为了实现我们的总体目标--评判列表，人类评判者需要对给定查询的搜索结果（通常是文档）进行评判。这就是所谓的查询/文档对。</p><p>有时，在查看查询时似乎很容易知道用户想要什么。查询<code>harrison ford</code> 的目的是查找演员哈里森-福特主演的电影。查询<code>action</code> 如何？我知道我很想说用户的意图是寻找动作类型的电影。但是是哪些呢？最新的、最受欢迎的、用户评价最好的？或者，用户是否想找到所有名为 &quot;动作 &quot;的电影？<a href="https://www.themoviedb.org/search/movie?query=Action">在电影数据库中，至少有 12 部（！）电影被称为 &quot;动作片&quot;</a>，它们的名称主要区别在于片名中感叹号的数量。</p><p>如果查询的意图不明确，两名人工评分员对查询的解释可能会有所不同。输入信息需求：<a href="https://en.wikipedia.org/wiki/Information_needs">信息需求</a>是一种有意识或无意识的信息渴望。定义信息需求有助于人类评判员判断查询的文档，因此他们在建立判断列表的过程中发挥着重要作用。专家用户或主题专家是明确信息需求的最佳人选。从用户的角度来定义信息需求是一种很好的做法，因为搜索结果应该满足用户的需求。</p><p>电影搜索基线 &quot;案例查询的信息需求：</p><ol><li><strong>星球大战</strong>用户希望查找《星球大战》系列电影或节目。有可能相关的是关于《星球大战》的纪录片。</li><li><strong>哈里森</strong>-福特用户希望查找演员 Harrison Ford 主演的电影。哈里森-福特扮演不同角色的电影也可能与此相关，比如旁白。</li><li><strong>最佳动作片</strong>：用户希望找到动作片，最好是用户平均票数高的动作片。</li></ol><h2>如何在 Quepid 中定义信息需求</h2><p>要在 Quepid 中定义信息需求，请访问案例界面：</p><p>1.打开一个查询（例如<em>星际大战</em>）并选择<em>切换备注。</em></p><p>2.在第一个字段中输入信息需求，并在第二个字段中输入任何附加说明：</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/e01d3d5242a350d8797faa665eb3170039f5dfa2-1483x559.png" style="width: 100%;" /><p>3.单击<strong>保存</strong>。</p><p>对于少数几个查询，这个过程没有问题。但是，当您将案例从 3 个查询扩展到 100 个查询时（Quepid 案例通常在 50 到 100 个查询之间），您可能希望在 Quepid 之外定义信息需求（例如，在电子表格中），然后通过<strong>导入</strong>并选择<strong>信息需求</strong>来上传。</p><h2>在 Quepid 中创建团队并共享案例</h2><p>合作判断可提高相关性评估的质量。组建团队：</p><p>1.在顶层菜单中导航至<strong>团队</strong>。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/797706e8d130b474a95d30b6fa22ecaf36f98c03-613x58.png" style="width: 100%;" /><p>2.单击<strong>+ 添加新成员</strong>，输入团队名称（例如&quot;Search Relevance Raters&quot; ），然后单击<strong>创建</strong>。</p><p>3.输入成员的电子邮件地址并单击 &quot;<strong>添加用户</strong>&quot;，添加成员。</p><p>4.在个案界面，选择<strong>共享个案</strong>。</p><p>5.选择合适的团队并确认。</p><h2>在 Quepid 中创建评估手册</h2><p>Quepid 中的一本书允许多个评分者对查询/文档对进行系统评估。创建一个</p><p>1.转到案件界面中的<strong> 判决书</strong> ，点击<strong> + 创建一本书</strong> 。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/6a077f26225961150b7414463d7db04f090b68d6-896x365.png" style="width: 100%;" /><p>2.为图书配置一个描述性的名称，将其分配给您的团队，选择一种评分方法（例如 DCG@10），并设置选择策略（单个或多个评分者）。图书使用以下设置：</p><ul><li><strong>名称</strong>：&quot;电影搜索 0-3 刻度&quot;</li><li><strong>要与之分享此书的团队</strong>：勾选您创建的团队</li><li><strong>得分者</strong>DCG@10</li></ul><p>3.单击<strong>创建图书。</strong></p><p>名称具有描述性，包含搜索内容（&quot;电影&quot;）和评判标准（&quot;0-3&quot;）的信息。所选的 Scorer DCG@10 定义了搜索指标的计算方式。DCG &quot;是 &quot;<a href="https://en.wikipedia.org/wiki/Discounted_cumulative_gain">贴现累积收益</a>&quot;的缩写，&quot;@10 &quot;是在计算该指标时，从顶部开始考虑的结果数量。</p><p>在这种情况下，我们使用一种衡量信息增益的指标，并将其与位置加权相结合。可能还有其他搜索指标更适合您的使用情况，<a href="https://opensourceconnections.com/blog/2020/02/28/choosing-your-search-relevance-metric"> 选择合适的 指标 本身就是一项挑战</a> 。</p><h2>用查询/文档对填充评估手册</h2><p>要添加查询/文档对进行相关性评估，请按照以下步骤操作：</p><p>1.在案件界面中，导航至&quot;判决。&quot;</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/d717c5b06ae6cb42ed2b9e771486a12f738a9890-1041x218.png" style="width: 100%;" /><p>2.选择您创建的图书。</p><p>3.单击&quot;Populate Book&quot; ，然后选择&quot;Refresh Query/Doc Pairs for Book 进行确认。&quot;</p><p>该操作根据每个查询的热门搜索结果生成配对，供团队评估。</p><h2>让您的人工评分团队进行评估 </h2><p>到目前为止，已完成的步骤都是相当技术性和行政性的。现在，这些必要的准备工作已经完成，我们可以让我们的评委团队开展工作了。从本质上讲，法官的工作就是评定特定文档与给定查询的相关性。这一过程的结果就是判断列表，其中包含了被判断的查询文档对的所有相关性标签。接下来，我们将进一步详细解释这一过程及其界面。</p><h3>人工评分界面概览</h3><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/be3016091b49655dab3354d84e6dc638f3468390-1283x664.png" style="width: 100%;" /><p>Quepid 的人工评分界面专为高效评估设计：</p><ul><li><strong>查询：</strong>显示搜索词。</li><li><strong>信息需求：</strong>显示用户的意图。</li><li><strong>评分指南：</strong>提供一致评价的说明。</li><li><strong>文件元数据：</strong>介绍文件的相关详细信息。</li><li><strong>评级按钮：</strong>允许评定者使用相应的键盘快捷键指定评定结果。</li></ul><h3>使用人工评分界面</h3><p>作为一名人工评审员，我通过图书概览进入界面：</p><p>1.导航至案件界面并单击<strong>判决</strong>。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/d717c5b06ae6cb42ed2b9e771486a12f738a9890-1041x218.png" style="width: 100%;" /><p>2.点击 &quot;<strong>需要更多判决！</strong>&quot;。</p><p>系统会显示一个尚未评级的查询/文件对，该查询/文件对需要额外的判断。这是由图书的选择策略决定的：</p><ul><li><em>单一评判者</em>：每个查询/文档对只有一个评判。</li><li><em>多个评分者</em>：每个查询/文档对最多可有三个评判。</li></ul><h3>评估查询/文档对</h3><p>让我们举几个例子。当您按照本指南进行操作时，很可能会看到不同的电影。不过，评级原则保持不变。</p><p>第一个例子是电影 &quot;英雄 &quot;中的查询 &quot;<em>哈里森-福特</em>&quot;：</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/c1e14b0d8b04dd579471932dbe4ff72ae5692a02-981x571.png" style="width: 100%;" /><p>我们首先查看查询，然后是信息需求，最后根据给出的元数据对电影进行判断。</p><p>这部电影与我们的查询结果相关，因为演员中有哈里森-福特（Harridson Ford）。我们可能会主观地认为近期的电影更具相关性，但这并不是我们信息需求的一部分。因此，我们给这份文件的评分是 &quot;完美&quot;，在我们的评分标准中是 3 分。</p><p>下一个例子是电影 &quot;福特诉法拉利&quot;，查询条件是<em>哈里森-福特</em>：</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/052af7894506d7a765af156ba8e26ceec3559973-981x789.png" style="width: 100%;" /><p>按照同样的做法，我们通过查看查询、信息需求以及文档元数据与信息需求的匹配程度来判断该查询/文档。</p><p>这是一个糟糕的结果。我们可能会看到这个结果，因为我们的查询词之一 &quot;福特 &quot;在标题中匹配。但哈里森-福特在这部电影中没有扮演任何角色，也没有扮演任何其他角色。因此，我们将这份文件评为 &quot;差&quot;，在我们的评分标准中是 0 分。</p><p>第三个例子是电影 &quot;动作杰克逊 &quot;的<em>最佳动作片</em>查询：</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/247ab862fbc7435537709f8c96619cb331133d09-985x606.png" style="width: 100%;" /><p>这看起来像是一部动作片，因此至少满足了部分信息需求。不过，投票的平均值为 5.4（满分 10 分）。因此，这部电影可能不是我们收藏的最好的动作片。因此，作为评委，我给这份文件的评分是 &quot;尚可&quot;，在我们的评分标准中是 1 分。</p><p>这些示例特别说明了使用 Quepid 对查询/文档进行评级的过程，既有高层次的，也有一般的。</p><h2>人工评分员最佳实践</h2><p>所展示的示例可能会让人觉得可以直接得出明确的判断。但是，建立一个可靠的人工评级程序并非易事。这是一个充满挑战的过程，很容易影响数据质量：</p><ul><li>人类评分员可能会因重复性工作而感到疲劳。</li><li>个人喜好可能会影响判断。</li><li>不同法官的领域专业知识水平各不相同。</li><li>评级员往往身兼数职。</li><li>文档的感知相关性可能与查询的真实相关性不一致。</li></ul><p>这些因素可能导致判决不一致、质量不高。不过不用担心，有一些经过验证的最佳实践可以帮助你最大限度地减少这些问题，并建立一个更强大、更可靠的评估流程：</p><ul><li><strong>一致的评估：</strong>依次审查查询、信息需求和文件元数据。</li><li><strong>参考指南：</strong>使用评分指南以保持一致性。评分指南可以举例说明何时采用哪种等级，从而说明评审过程。事实证明，在第一批评判结束后与人工评判员进行核对是一种很好的做法，可以了解具有挑战性的边缘案例以及在哪些方面需要额外的支持。</li><li><strong>利用选项：</strong>如果不确定，可使用&quot;I Will Judge Later&quot; 或&quot;I Can&#x27;t Tell,&quot; ，必要时提供解释。</li><li><strong>休息：</strong>定期休息有助于保持判断质量。每当人工评判员完成一批评判时，Quepid 都会弹出彩纸，帮助用户定期休息。</li></ul><p>按照这些步骤，您就可以在 Quepid 中建立一个结构化的协作方法来创建判断列表，从而提高搜索相关性优化工作的效率。</p><h2>后续步骤</h2><p>何去何从？判断列表只是提高搜索结果质量的一个基础步骤。下面是接下来的步骤：</p><h3>计算指标并开始实验</h3><p>一旦有了判断列表，利用判断和计算<a href="https://opensourceconnections.com/blog/2020/02/28/choosing-your-search-relevance-metric/">搜索质量指标</a>就水到渠成了。当有判决书时，Quepid 会自动计算当前案件的配置指标。指标以 &quot;计分器 &quot;的形式实现，如果支持的指标不包括您最喜欢的指标，您可以提供自己的指标！</p><p>进入案例界面，导航至 &quot;<strong>选择评分员</strong>&quot;，选择<em>DCG@10</em>，点击 &quot;<strong>选择评分员</strong>&quot;确认。现在，Quepid 将计算每次查询的 DCG@10，并计算总体查询的平均值，以量化搜索结果的质量。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/0ff3b9918342071744d681a43d542102e927abd3-1163x551.png" style="width: 100%;" /><p>既然已经量化了搜索结果的质量，那么就可以进行第一次实验了。实验从提出假设开始。在对截图中的三个查询进行评级后，可以明显看出这三个查询在搜索质量指标方面的表现截然不同：<em>《星球大战》</em>表现不错，《<em>哈里森-福特》</em>看起来还行，但《<em>最佳动作片》</em>的潜力最大。</p><p>扩大查询范围后，我们就能看到查询结果，并能深入研究细节，探索文档匹配的原因以及影响其得分的因素：</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/c45c6cae085a492198c0f8b7060a1a7204e3724e-1131x691.png" style="width: 100%;" /><p>点击 &quot;Explain Query（解释查询）&quot;并进入 &quot;Parsing（解析）&quot;选项卡，我们可以看到该查询是一个 DisjunctionMaxxQuery，搜索三个字段：<em>演员</em>、<em>概述</em>和<em>标题</em>：</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/4a1e1bb2a9cd28e9c48e0ba16357d17ed9d3a5cf-894x557.png" style="width: 100%;" /><p>通常情况下，作为搜索工程师，我们对搜索平台的一些特定领域了如指掌。在这种情况下，我们可能知道我们有一个<em>基因</em>字段。让我们将其添加到查询中，看看搜索质量是否有所提高。</p><p>我们使用在案例界面选择 &quot;<strong> 调整相关性&quot;时打开的</strong> &quot; 查询沙盒&quot;。请添加您搜索的<em>流派字段</em>，继续探索：</p><pre><code>{
  "query": {
    "multi_match": {
      "query": "#$query##",
      "type": "best_fields",
      "fields": [
        "title^10",
        "overview",
        "cast",
        "genres"
      ]
    }
  }
}</code></pre><p>单击重新运行我的搜索！并查看结果。他们变了吗？遗憾的是没有。我们现在有很多选项可以探索，基本上是 Elasticsearch 提供的所有查询选项：</p><ul><li>我们可以增加基因字段的字段权重。</li><li>我们可以添加一个功能，根据文件的平均得票率来提升文件。</li><li>我们可以创建一个更复杂的查询，只在有强基因匹配的情况下，才按投票平均值提升文档。</li><li>…</li></ul><p>在 Quepid 中拥有所有这些选项并对其进行探索的最大好处是，我们不仅可以量化我们试图改进的查询的效果，还可以量化我们的所有查询的效果。这就避免了我们通过牺牲其他搜索结果的质量来改善一个表现不佳的查询。我们可以快速、低成本地迭代，并在没有任何风险的情况下验证我们假设的价值，这使得离线实验成为所有搜索团队的基本能力。</p><h3>评估评分员间信度</h3><p>即使有任务说明、信息需求和类似 Quepid 提供的人工评定界面，人工评定者也会出现分歧。</p><p>意见分歧本身并不是坏事，恰恰相反：衡量意见分歧可以让你发现你可能想要解决的问题。相关性可能是主观的，查询可能是模糊的，数据可能是不完整或不正确的。<a href="https://en.wikipedia.org/wiki/Fleiss%27_kappa">弗莱斯卡帕（Fleiss&#x27; Kappa</a>）是衡量评分者之间一致性的一种统计方法，Quepid 中有一个示例笔记本可供使用。要找到它，请在顶层导航中选择<strong> 笔记本</strong> ，然后在<strong> 示例</strong> 文件夹中选择笔记本<strong> Fleiss Kappa.ipynb</strong> 。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/f0669ae96371368ef4d84bb28669560ef09d755c-624x61.png" style="width: 100%;" /><h2>结论</h2><p>Quepid 使您能够应对最复杂的搜索相关性挑战，并将继续发展：<a href="https://github.com/o19s/quepid/blob/main/CHANGELOG.md#800----2024-02-14">从第 8 版开始，Quepid 支持人工智能生成判断</a>，这对希望扩展判断生成流程的团队特别有用。</p><p>Quepid工作流程使您能够高效地创建可扩展的判断列表，最终产生真正满足用户需求的搜索结果。有了判断列表，您就有了衡量搜索相关性、迭代改进和改善用户体验的坚实基础。</p><p>在前进的过程中，请记住相关性调整是一个持续的过程。判断列表可以让你系统地评估自己的进步，但如果能与实验、指标分析和迭代改进相结合，判断列表的作用会更加强大。</p><h2>延展阅读</h2><ul><li>Quepid docs：<ul><li><a href="https://quepid-docs.dev.o19s.com/2/quepid/32/relevancy-is-a-team-sport">相关性是一项团队运动</a></li><li><a href="https://quepid-docs.dev.o19s.com/2/quepid/18/quepid-for-human-raters">人类评级员的 Quepid</a></li><li><a href="https://quepid-docs.dev.o19s.com/2/quepid/49/how-to-connect-quepid-to-elastic-cloud">如何将 Quepid 连接到弹性云</a></li></ul></li><li><a href="https://github.com/o19s/quepid">Quepid Github 存储库</a></li><li><a href="https://opensourceconnections.com/blog/2020/07/07/meet-pete-the-e-commerce-search-product-manager/">认识皮特，关于改进电子商务搜索的系列博客</a></li><li><a href="https://opensourceconnections.com/slack">相关性 Slack</a>：加入 #quepid 频道</li></ul><p><strong>与 <a href="https://opensourceconnections.com/">Open Source Connections</a></strong> 合作 ，改造您的搜索和人工智能能力，并使您的团队能够不断发展这些能力。我们的业绩记录遍布全球，客户在搜索质量、团队能力和业务绩效方面不断取得显著提高。<a href="https://opensourceconnections.com/contact/">现在就联系我们</a>，了解更多信息。</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[显示 Elasticsearch 索引中的字段]]></title>
            <description><![CDATA[探索在 Elasticsearch 索引中显示字段的技术。
]]></description>
            <link>https://www.elastic.co/search-labs/cn/blog/displaying-fields-in-an-elasticsearch-index</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/cn/blog/displaying-fields-in-an-elasticsearch-index</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Kofi Bartlett]]></dc:creator>
            <pubDate>Mon, 26 May 2025 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/fa386d7bbaeab6855e62897ace8d7dca91a060b4-720x420.jpg?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>本文将讨论如何在 Elasticsearch 索引中显示字段。这对了解数据结构、识别特定字段和排除故障非常有用。我们将讨论以下主题</p><ol><li><a href="https://www.elastic.co/search-labs/blog/displaying-fields-in-an-elasticsearch-index#1.-using-the--mapping-api-to-retrieve-field-information">使用 <code>_mapping</code> API 获取字段信息</a></li><li><a href="https://www.elastic.co/search-labs/blog/displaying-fields-in-an-elasticsearch-index#2.-using-the--search-api-to-display-field-values">使用 <code>_search</code> API 显示字段值</a></li><li><a href="https://www.elastic.co/search-labs/blog/displaying-fields-in-an-elasticsearch-index#3.-filtering-fields-using-the-fields-parameter">使用<code>fields</code> 参数 过滤字段</a></li><li><a href="https://www.elastic.co/search-labs/blog/displaying-fields-in-an-elasticsearch-index#4.-displaying-nested-fields">显示嵌套字段</a></li></ol><h2>1.使用 _mapping API 获取字段信息</h2><p><code>_mapping</code> API 允许您检索一个或多个<a href="https://opster.com/guides/elasticsearch/glossary/elasticsearch-index/">索引</a>的映射定义。这包括有关字段、其数据类型和其他属性的信息。要检索特定索引的映射，请使用以下请求：</p><pre><code>GET /<index_name>/_mapping</code></pre><p>例如，如果有一个名为<code>my_index</code> 的索引，可以通过以下请求检索其映射：</p><pre><code>GET /my_index/_mapping</code></pre><p>响应将包括索引的映射定义，其中包含有关字段及其属性的信息。</p><p>还可以检索某一特定字段的映射。如果您的映射范围很大，而您只想专注于某一特定领域，这将非常有用。要检索特定字段的映射，请使用以下请求：</p><pre><code>GET /my_index/_mapping/field/my_field</code></pre><p>您还可以检索多个字段的映射，方法是用逗号分隔字段名，如下所示：</p><pre><code>GET /my_index/_mapping/field/my_field_1,my_field_2,my_field_3</code></pre><h2>2.使用 _search API 显示字段值</h2><p>要显示 Elasticsearch 索引中字段的值，可以使用<code>_search</code> API。默认情况下，<code>_search</code> API 会返回<code>_source</code> 字段，其中包含被索引的原始 JSON 文档。要只显示特定字段，可以在搜索请求中使用<code>_source</code> 参数。</p><p>下面是一个搜索请求示例，它返回<code>my_index</code> 索引中文档的<code>title</code> 和<code>author</code> 字段的值：</p><pre><code>GET /my_index/_search
{
  "query": {
    "match_all": {}
  },
  "_source": ["title", "author"]
}</code></pre><p>在本例中，<code>_source</code> 参数指定了要返回的字段。</p><h2>3.使用字段参数过滤字段</h2><p>您还可以使用<code>fields</code> 参数过滤搜索响应中返回的字段。如果您只需要特定字段，并希望减小回复的大小，这可能会很有用。<code>fields</code> 参数接受一个字段名或通配符模式数组。</p><p>例如，要只返回<code>my_index</code> 索引中文档的<code>title</code> 和<code>author</code> 字段，可以使用以下搜索请求：</p><pre><code>GET /my_index/_search
{
  "query": {
    "match_all": {}
  },
  "fields": ["title", "author"],
  "_source": false
}</code></pre><p>请注意，为了不返回源文件，<code>_source</code> 参数设置为 false。</p><p>要返回<code>text</code> 数据类型的所有字段，可以使用通配符模式，如下所示：</p><pre><code>GET /my_index/_search
{
  "query": {
    "match_all": {}
  },
  "fields": ["*.text"],
  "_source": false
}</code></pre><h2>4.显示嵌套字段</h2><p>如果索引包含嵌套字段，可以使用点符号在<code>fields</code> 参数中指定嵌套字段路径。例如，如果嵌套字段的名称是<code>address.city</code> ，可以像这样将其包含在搜索响应中：</p><pre><code>GET /my_index/_search
{
  "query": {
    "match_all": {}
  },
  "fields": ["title", "author", "address.city"],
  "_source": false
}</code></pre><p>在此示例中，搜索响应将包括<code>title</code> 、<code>author</code> 和<code>address.city</code> 字段的值。</p><h2>结论</h2><p>总之，在 Elasticsearch 索引中显示字段可以使用<code>_mapping</code> API 来检索字段信息，使用<code>_search</code> API 来显示字段值。您可以使用<code>_source</code> 或<code>fields</code> 参数过滤搜索响应中返回的字段，并使用点符号显示嵌套字段。这些技术可以帮助您了解数据结构、识别特定字段并排除故障。</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[将 Spring AI 和 Elasticsearch 作为矢量数据库]]></title>
            <description><![CDATA[了解如何使用 Spring AI 和 Elasticsearch 构建生产可用的 RAG 应用，并借助向量数据库将 LLM 与您的自有数据集成。
]]></description>
            <link>https://www.elastic.co/search-labs/cn/blog/spring-ai-elasticsearch-application</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/cn/blog/spring-ai-elasticsearch-application</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Philipp Krenn, Laura Trotta]]></dc:creator>
            <pubDate>Tue, 20 May 2025 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/0771fb5b3d9234697cb868cd7d9d1b840000bf29-1280x720.png?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p><strong>Spring AI</strong>现已全面上市，其首个<a href="https://spring.io/blog/2025/05/20/spring-ai-1-0-GA-released">稳定版本 1.0</a>已在<a href="https://mvnrepository.com/artifact/org.springframework.ai/spring-ai-core">Maven Central</a> 上供您下载。让我们立即使用它来构建一个完整的人工智能应用程序，使用你最喜欢的<a href="https://www.elastic.co/what-is/large-language-models">LLM</a>和我们最喜欢的<a href="https://www.elastic.co/elasticsearch/vector-database">向量数据库</a>。或者直接进入<a href="https://github.com/xeraa/rag-with-java-springai-elasticsearch">资源库</a>，进行最终应用。</p><a href="https://www.youtube.com/watch/ms2xRO8B1YU">Watch video on YouTube</a><h2>什么是春季人工智能？</h2><p><strong>Spring AI 1.0</strong> 是 Java 中人工智能工程的全面解决方案，在经历了人工智能领域快速发展的重要开发阶段后，现已面世。该版本为人工智能工程师提供了许多重要的新功能。</p><p>Java 和 Spring 在人工智能浪潮中处于有利位置。很多公司都在 Spring Boot 上运行他们的产品，这使得将人工智能插入到他们已经在做的事情中变得非常容易。您基本上可以将您的业务逻辑和数据直接与这些人工智能模型连接起来，而不会有太多麻烦。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/328d7c51e1c145e94ea1e73ee9ff91836d3b180e-1600x773.png" style="width: 100%;" /><p>Spring AI 为<a href="https://docs.spring.io/spring-ai/reference/api/index.html">各种人工智能模型和技术</a>提供支持，例如</p><ul><li><strong>图像模型</strong>：根据文字提示生成图像。</li><li><strong>转录模型</strong>：获取音频源并将其转换为文本。</li><li><strong>嵌入模型： </strong>将任意数据转换为<a href="https://www.elastic.co/what-is/vector-embedding">矢量，矢量</a>是为语义相似性搜索而优化的数据类型。</li><li><strong>聊天模式： </strong>这些<strong> </strong>应该很熟悉！毫无疑问，你甚至在某个地方与他们进行过简短的交谈。</li></ul><p>在人工智能领域，聊天模型似乎最受关注，这也是理所当然的，因为它们非常棒！你可以让他们帮你修改文件或写诗。(只是先别让他们讲笑话......）。它们很棒，但也存在一些问题。</p><h2>应对人工智能挑战的春季人工智能解决方案</h2><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/2ebd68a90ebc73847df6ef7325936d4d06b35c8c-1600x900.jpg" style="width: 100%;" /><p>让我们来看看 Spring AI 中的一些问题及其解决方案。</p>
            <table style="border-collapse: collapse; margin: 20px 0;">
              <tbody>
                
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;"></td><td style="border: 1px solid #ccc; padding: 4px;">问题</td><td style="border: 1px solid #ccc; padding: 4px;">解决方案</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">一致性</td><td style="border: 1px solid #ccc; padding: 4px;">聊天模式思想开放，容易分心</td><td style="border: 1px solid #ccc; padding: 4px;">您可以给它们一个系统提示，以规范它们的整体形状和结构</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">内存</td><td style="border: 1px solid #ccc; padding: 4px;">人工智能模型没有内存，因此无法将某个用户的一条信息与另一条信息关联起来</td><td style="border: 1px solid #ccc; padding: 4px;">您可以为他们提供一个记忆系统，存储对话的相关部分</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">隔离</td><td style="border: 1px solid #ccc; padding: 4px;">人工智能模型生活在与世隔绝的小沙箱中，但如果你能让它们使用一些工具--在它们认为必要时可以调用的功能，它们就能做出非常了不起的事情</td><td style="border: 1px solid #ccc; padding: 4px;">Spring AI 支持工具调用，让你可以告诉 AI 模型其环境中的工具，然后它可以要求你调用这些工具。这种多轮互动都是透明处理的</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">私人数据</td><td style="border: 1px solid #ccc; padding: 4px;">人工智能模型很聪明，但并非无所不知！他们不知道你专有数据库中的内容，我们认为你也不希望他们知道！</td><td style="border: 1px solid #ccc; padding: 4px;">您需要通过填充提示来告知他们的回复--基本上就是在模型查看所提问题之前，使用强大的字符串连接运算符将文本放入请求中。背景资料，如果你愿意的话。如何决定哪些应该发送，哪些不应该发送？使用矢量存储，只选择相关数据并将其继续发送。这被称为检索增强生成，或 RAG</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">幻觉</td><td style="border: 1px solid #ccc; padding: 4px;">人工智能聊天模型喜欢，嗯，聊天！有时，他们自信到可以胡编乱造</td><td style="border: 1px solid #ccc; padding: 4px;">您需要使用评估--使用一个模型来验证另一个模型的输出--来确认合理的结果</td>
                    </tr>
              </tbody>
            </table>
          <p></p><p>当然，任何人工智能应用都不是一座孤岛。如今，现代人工智能系统和服务在与其他系统和服务集成时效果最佳。通过<strong><a href="https://modelcontextprotocol.io/introduction">模型上下文协议</a> </strong>(MCP)，可以将人工智能应用程序与其他基于 MCP 的服务连接起来，无论这些服务是用什么语言编写的。您可以将所有这些集合到<strong>代理 </strong>工作流程中，以实现更大的目标。</p><p>最棒的是什么？您可以在熟悉的成语和抽象基础上完成所有这些工作，Spring Boot 开发人员都会期待这些成语和抽象：<a href="https://start.spring.io"><strong>Spring Initializr</strong></a>上提供了方便的启动依赖项，基本上所有东西都可以使用<strong>。</strong></p><p>Spring AI 提供方便的 Spring Boot 自动配置，为您提供您所熟知和期待的 &quot;惯例重于配置 &quot;设置。Spring AI 通过 Spring Boot 的 Actuator 和 Micrometer 项目支持可观察性。它还能很好地与 GraalVM 和虚拟线程配合使用，让您可以构建超快、高效的人工智能应用程序，并进行扩展。</p><h2>为什么选择 Elasticsearch</h2><p>Elasticsearch 是一个全文搜索引擎，这一点你可能知道。那么，我们为什么要在这个项目中使用它呢？嗯，这<em>也</em>是一家矢量商店！数据与全文并存，相当不错。其他显著优势</p><ul><li>设置超级简单</li><li>开源</li><li>可横向扩展</li><li>您企业的大部分自由格式数据可能已经存在于 Elasticsearch 集群中</li><li>具有完整的搜索引擎功能</li><li>完全<a href="https://docs.spring.io/spring-ai/reference/api/vectordbs/elasticsearch.html">集成到 Spring AI 中</a>！</li></ul><p>综上所述，Elasticsearch 符合优秀矢量存储的所有条件，因此让我们对其进行设置，然后开始构建我们的应用程序！</p><h2>Elasticsearch 入门</h2><p>我们需要 Elasticsearch 和 Kibana，这是用户界面控制台，用于与数据库中的数据交互。</p><p>借助 Docker 镜像和<a href="http://elastic.co">Elastic.co 主页</a>，您可以在本地机器上尝试一切。访问该网站，向下滚动找到<code>curl</code> 命令，运行该命令并将其直接导入 shell：</p><pre><code> curl -fsSL https://elastic.co/start-local | sh 
  ______ _           _   _      
 |  ____| |         | | (_)     
 | |__  | | __ _ ___| |_ _  ___ 
 |  __| | |/ _` / __| __| |/ __|
 | |____| | (_| \__ \ |_| | (__ 
 |______|_|\__,_|___/\__|_|\___|
-------------------------------------------------
🚀 Run Elasticsearch and Kibana for local testing
-------------------------------------------------
ℹ️  Do not use this script in a production environment
⌛️ Setting up Elasticsearch and Kibana v9.0.0...
- Generated random passwords
- Created the elastic-start-local folder containing the files:
  - .env, with settings
  - docker-compose.yml, for Docker services
  - start/stop/uninstall commands
- Running docker compose up --wait
[+] Running 25/26
 ✔ kibana_settings Pulled                                                 16.7s 
 ✔ kibana Pulled                                                          26.8s 
 ✔ elasticsearch Pulled                                                   17.4s                                                                     
[+] Running 6/6
 ✔ Network elastic-start-local_default             Created                 0.0s 
 ✔ Volume "elastic-start-local_dev-elasticsearch"  Created                 0.0s 
 ✔ Volume "elastic-start-local_dev-kibana"         Created                 0.0s 
 ✔ Container es-local-dev                          Healthy                12.9s 
 ✔ Container kibana_settings                       Exited                 11.9s 
 ✔ Container kibana-local-dev                      Healthy                21.8s 
🎉 Congrats, Elasticsearch and Kibana are installed and running in Docker!
🌐 Open your browser at http://localhost:5601
   Username: elastic
   Password: w1GB15uQ
🔌 Elasticsearch API endpoint: http://localhost:9200
🔑 API key: SERqaGlKWUJLNVJDODc1UGxjLWE6WFdxSTNvMU5SbVc5NDlKMEhpMzJmZw==
Learn more at https://github.com/elastic/start-local
➜  ~ </code></pre><p>这只需为 Elasticsearch 和 Kibana 提取和配置 Docker 映像，几分钟后，它们就会在本地计算机上运行，并提供完整的连接凭据。</p><p>您还可以使用两个不同的 URL 与 Elasticsearch 实例交互。按提示操作，将浏览器指向<a href="http://localhost:5601">http://localhost:5601。</a></p><p>请注意打印在控制台上的用户名<code>elastic</code> 和密码：登录时需要使用这两个用户名和密码（在上面的输出示例中，它们分别是<code>elastic</code> 和<code>w1GB15uQ</code> ）。</p><p></p><h2>整合应用程序</h2><p>转到<a href="https://start.spring.io">Spring Initializr</a>页面并生成一个新的 Spring AI 项目，其中包含以下依赖项：</p><ul><li><code>Elasticsearch Vector Store</code></li><li><code>Spring Boot Actuator</code></li><li><code>GraalVM</code></li><li><code>OpenAI</code></li><li><code>Web</code></li></ul><p>确保选择最新、最好的 Java 版本（最好是 Java 24（截至本文撰写之时）或更高版本）和您所选择的构建工具。本例中我们使用 Apache Maven。</p><p>单击<code>Generate</code> ，然后解压缩项目并将其导入您选择的集成开发环境。(我们使用的是 IntelliJ IDEA）。</p><p>首先：让我们为 Spring Boot 应用程序指定连接细节。在<code>application.properties,</code> 中写下以下内容：</p><pre><code>spring.elasticsearch.uris=http://localhost:9200
spring.elasticsearch.username=elastic
spring.elasticsearch.password=w1GB15uQ</code></pre><p>我们还将利用 Spring AI 的向量存储功能来初始化 Elasticsearch 端所需的数据结构，请具体说明：</p><pre><code>spring.ai.vectorstore.elasticsearch.initialize-schema=true</code></pre><p>在本演示中，我们将使用<strong>OpenAI</strong>，特别是<strong>嵌入模型</strong>和<strong>聊天模型 </strong>（只要<a href="https://docs.spring.io/spring-ai/reference/api/embeddings.html#available-implementations">Spring AI 支持</a>，您可以随意使用自己喜欢的服务）。</p><p>在将数据存储到 Elasticsearch 之前，我们需要嵌入模型来创建数据的嵌入。为了让 OpenAI 正常工作，我们需要指定<code>API key</code> ：</p><pre><code>spring.ai.openai.api-key=...</code></pre><p>您可以将其定义为一个环境变量，如<code>SPRING_AI_OPENAI_API_KEY</code> ，以避免在源代码中存储凭据。</p><p>我们将上传文件，因此请务必自定义可上传至 servlet 容器的数据量：</p><pre><code>spring.servlet.multipart.max-file-size=20MB
spring.servlet.multipart.max-request-size=20MB</code></pre><p>我们快到了在开始编写代码之前，让我们先预览一下它是如何工作的。</p><p>在我们的机器上，我们下载了<a href="https://images-cdn.fantasyflightgames.com/filer_public/9f/aa/9faa23a3-9f71-4c77-865f-bba4aac8a258/runewars-revised-_rulebook.pdf">以下文件</a>（一个棋盘游戏的规则列表），将其重命名为<code>test.pdf</code> 并放入<code>~/Downloads/test.pdf</code>.</p><p>文件将被发送到<code>/rag/ingest</code> 端点（根据本地设置替换相应路径）：</p><pre><code>http --form POST http://localhost:8080/rag/ingest path@/Users/jlong/Downloads/test.pdf</code></pre><p>这可能需要几秒钟...</p><p>在幕后，数据被发送到 OpenAI，OpenAI 正在创建数据嵌入；然后，数据（包括向量和原始文本）被写入 Elasticsearch。</p><p>这些数据以及其中的所有嵌入式数据，就是神奇之处。然后，我们就可以使用<code>VectorStore</code> 界面查询 Elasticsearch。</p><p>整个流程是这样的</p><ul><li>HTTP 客户端会将您选择的 PDF 上传到 Spring 应用程序。</li><li>Spring AI 负责从我们的 PDF 中提取文本，并将每页分成 800 个字符的小块。</li><li>OpenAI 会为每个块生成向量表示。</li><li>然后，分块文本和嵌入都会存储在 Elasticsearch 中。</li></ul><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/9734adb2d7128e61c515d5855dfad6d3a326a4a1-1454x706.png" style="width: 100%;" /><p>最后，我们将发出一个查询：</p><pre><code>http :8080/rag/query question=="where do you place the reward card after obtaining it?" </code></pre><p>我们会得到相关的答案：</p><pre><code>After obtaining a Reward card, you place it facedown under the Hero card of the hero who received it.
Found at page: 28 of the manual</code></pre><p>不错！这一切是如何运作的？</p><ul><li>HTTP 客户端将问题提交给 Spring 应用程序。</li><li>Spring AI 从 OpenAI 获取问题的向量表示。</li><li>有了这种嵌入，它就能在存储的 Elasticsearch 块中搜索相似文档，并检索出最相似的文档。</li><li>然后，Spring AI 将问题和检索到的上下文发送给 OpenAI，以生成 LLM 答案。</li><li>最后，它会返回生成的答案和检索到的上下文的引用。</li></ul><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/3799de6e8cb13ce49b9e136cfe593263030231a8-1464x1050.png" style="width: 100%;" /><p>让我们深入 Java 代码，看看它到底是如何工作的。</p><p>首先是<strong>Main</strong>类：它是 Spring Boot 应用程序的标准主类。</p><pre><code>@SpringBootApplication
public class DemoApplication {
 	public static void main(String[] args) { 
     		SpringApplication.run(DemoApplication.class, args);
 	}
}</code></pre><p>没什么好看的。继续...</p><p>接下来是基本 HTTP 控制器：</p><pre><code>@RestController
class RagController {

   private final RagService rag;

   RagController(RagService rag) {
       this.rag = rag;
   }

   @PostMapping("/rag/ingest")
   ResponseEntity<?> ingestPDF(@RequestBody MultipartFile path) {
       rag.ingest(path.getResource());
       return ResponseEntity.ok().body("Done!");
   }

   @GetMapping("/rag/query")
   ResponseEntity<?> query(@RequestParam String question) {
       String response = rag.directRag(question);
       return ResponseEntity.ok().body(response);
   }
}</code></pre><p>控制器只是调用我们构建的一项服务，以处理文件摄取并将其写入 Elasticsearch 向量存储，然后方便对同一向量存储进行查询。</p><p>让我们来看看服务：</p><pre><code>@Service
class RagService {

   private final ElasticsearchVectorStore vectorStore;

   private final ChatClient ai;

   RagService(ElasticsearchVectorStore vectorStore, ChatClient.Builder clientBuilder) {
       this.vectorStore = vectorStore;
       this.ai = clientBuilder.build();
   }

   void ingest(Resource path) {
       PagePdfDocumentReader pdfReader = new PagePdfDocumentReader(path);
       List<Document> batch = new TokenTextSplitter().apply(pdfReader.read());
       vectorStore.add(batch);
   }

  // TBD
}</code></pre><p>这段代码将处理所有的输入：给定一个 Spring Framework<code>Resource</code> ，它是一个围绕字节的容器，我们使用 Spring AI 的<code>PagePdfDocumentReader</code> 读取 PDF 数据（假定是一个<code>.PDF</code> 文件--在接受任意输入之前，请确保您验证了这一点！），然后使用 Spring AI 的<code>TokenTextSplitter</code> 对其进行标记化，最后将生成的<code>List&lt;Document&gt;</code>s 添加到<code>VectorStore</code> 的实现<code>ElasticsearchVectorStore</code> 中。</p><p>您可以使用 Kibana 进行确认：向<code>/rag/ingest</code> 端点发送文件后，打开浏览器<code>localhost:5601</code> ，然后在左侧菜单中导航至<code>Dev Tools</code> 。在这里，你可以发出查询，与 Elasticsearch 实例中的数据交互。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/c85e522f02f8b2da7462cd428dc7e952c9692542-1600x1040.png" style="width: 100%;" /><p>发出这样的查询</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/00974a176cbce11e70fcab24fb4b3f9c6e205982-1600x1040.png" style="width: 100%;" /><p>现在，有趣的事情来了：我们怎样才能在响应用户查询时将数据返回？</p><p>下面是查询的初步实现，方法名为<code>directRag</code> 。</p><pre><code>String directRag(String question) {
   // Query the vector store for documents related to the question
   List<Document> vectorStoreResult =
           vectorStore.doSimilaritySearch(SearchRequest.builder().query(question).topK(5)
                   .similarityThreshold(0.7).build());

   // Merging the documents into a single string
   String documents = vectorStoreResult.stream()
           .map(Document::getText)
           .collect(Collectors.joining(System.lineSeparator()));

   // Exit if the vector search didn't find any results
   if (documents.isEmpty()) {
       return "No relevant context found. Please change your question.";
   }

   // Setting the prompt with the context
   String prompt = """
           You're assisting with providing the rules of the tabletop game Runewars.
           Use the information from the DOCUMENTS section to provide accurate answers to the
           question in the QUESTION section.
           If unsure, simply state that you don't know.
          
           DOCUMENTS:
           """ + documents
           + """
           QUESTION:
           """ + question;


   // Calling the chat model with the question
   String response = ai
           .prompt()
           .user(prompt)
           .call()
           .content();

   return response +
           System.lineSeparator() +
           "Found at page: " +
           // Retrieving the first ranked page number from the document metadata
           vectorStoreResult.getFirst().getMetadata().get(PagePdfDocumentReader.METADATA_START_PAGE_NUMBER) +
           " of the manual";

}</code></pre><p>代码非常简单，但让我们把它分成多个步骤：</p><ol><li>使用<code>VectorStore</code> 进行相似性搜索。</li><li>给定所有结果后，获取底层 Spring AI<code>Document</code>s 并提取其文本，将所有结果串联成一个结果。</li><li>将<code>VectorStore</code> 的结果发送给模型，同时发送一个提示，指示模型如何处理这些结果以及用户提出的问题。等待回复并返回。</li></ol><p></p><p>这就是<strong>RAG</strong>--检索增强生成。这就是我们使用矢量存储的数据为模型的处理和分析提供信息的理念。既然你已经知道怎么做了，那就希望你永远都不必这么做！反正不是这样的：Spring AI<a href="https://docs.spring.io/spring-ai/reference/api/advisors.html">顾问</a>将进一步简化这一过程。</p><p>除了在应用程序和矢量存储之间提供一个抽象层外，Advisors 还允许你对给定模型的请求进行预处理和后处理。在构建过程中添加以下依赖项<br/></p><pre><code><dependency>
   <groupId>org.springframework.ai</groupId>
   <artifactId>spring-ai-advisors-vector-store</artifactId>
</dependency></code></pre><p>在类中添加另一个名为<code>advisedRag(String question)</code> 的方法：</p><pre><code>String advisedRag(String question) {
   return this.ai
           .prompt()
           .user(question)
           .advisors(new QuestionAnswerAdvisor(vectorStore))
           .call()
           .content();
}</code></pre><p>所有 RAG 模式逻辑都封装在<code>QuestionAnswerAdvisor</code> 中。其他一切都与向<code>ChatModel</code> 提出的任何请求一样！不错！</p><p>你可以<a href="https://github.com/xeraa/rag-with-java-springai-elasticsearch">从 GitHub 获取完整的代码</a>。</p><h2>结论</h2><p>在这个演示中，我们使用了 Docker 镜像，并在本地机器上完成了所有操作，但我们的目标是构建可投入生产的人工智能系统和服务。为了实现这一目标，您可以做几件事。</p><p>首先，你可以添加<a href="https://docs.spring.io/spring-boot/reference/actuator/index.html#actuator">Spring Boot Actuator</a>来监控令牌的消耗。代币代表了特定请求对模型造成的复杂性成本（有时是美元和美分）。</p><p>您已经在类路径上安装了 Spring Boot Actuator，因此只需指定以下属性即可显示所有指标（由宏伟的<a href="http://micrometer.io">Micrometer.io</a>项目捕获）：</p><pre><code>management.endpoints.web.exposure.include=*</code></pre><p>重新启动应用程序。进行查询，然后转到<a href="http://localhost:8080/actuator/metrics">： http://localhost:8080/actuator/metrics。</a>搜索 &quot;<code>token</code>&quot;，就能看到应用程序使用的令牌信息。请务必留意这一点。当然，您也可以使用 Micrometer<a href="https://docs.micrometer.io/micrometer/reference/implementations/elastic.html">与 Elasticsearch 的集成</a>来推送这些指标，并让 Elasticsearch 作为您选择的时间序列数据库！</p><p>因此，您应该考虑到，每次我们向 Elasticsearch 等数据存储、OpenAI 或其他网络服务发出请求时，我们都在进行 IO，而且 IO 经常会阻塞执行它的线程。Java 21 及更高版本配备了非阻塞<strong>虚拟线程</strong>，大大提高了可扩展性。使用<br/></p><pre><code>spring.threads.virtual.enabled=true</code></pre><p>最后，您还需要将应用程序和数据托管在一个能够使其蓬勃发展和扩展的地方。我们相信，您可能已经考虑好在哪里运行应用程序了，但在哪里托管您的数据呢？我们可以推荐<a href="https://cloud.elastic.co/">弹性云吗</a>？它安全、私密、可扩展，功能齐全。我们最喜欢的部分？如果你愿意，还可以购买无服务器版，在无服务器版中，Elastic 会为你佩戴传呼机，而不是你！</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[正确使用 JavaScript 的 Elasticsearch，第二部分]]></title>
            <description><![CDATA[了解生产环境最佳实践，并学习如何在 Serverless 环境中运行 Elasticsearch Node.js 客户端，以减少代码错误。 ]]></description>
            <link>https://www.elastic.co/search-labs/cn/blog/how-to-use-elasticsearch-in-javascript-part-ii</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/cn/blog/how-to-use-elasticsearch-in-javascript-part-ii</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Jeffrey Rengifo]]></dc:creator>
            <pubDate>Mon, 19 May 2025 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/70fb0ff949f6db9ac9b8a28ecb4329ab915ebf46-720x420.png?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>这是 Elasticsearch in JavaScript 系列的第二部分。在<a href="https://www.elastic.co/search-labs/blog/how-to-use-elasticsearch-in-javascript-part-i"> 第一部分 中 ，</a> 我们学习了如何正确设置环境、配置 Node.js 客户端、索引数据和搜索。在第二部分中，我们将学习如何实施生产最佳实践，并在无服务器环境中运行 Elasticsearch<a href="http://node.js">Node.js</a>客户端。</p><p>我们将审查</p><ul><li><a href="https://www.elastic.co/search-labs/blog/how-to-use-elasticsearch-in-javascript-part-ii#production-best-practices">生产最佳实践</a><ul><li><a href="https://www.elastic.co/search-labs/blog/how-to-use-elasticsearch-in-javascript-part-ii#error-handling">错误处理能力</a></li><li><a href="https://www.elastic.co/search-labs/blog/how-to-use-elasticsearch-in-javascript-part-ii#testing">测试</a></li></ul></li><li><a href="https://www.elastic.co/search-labs/blog/how-to-use-elasticsearch-in-javascript-part-ii#serverless-environments">无服务器环境</a><ul><li><a href="https://www.elastic.co/search-labs/blog/how-to-use-elasticsearch-in-javascript-part-ii#running-the-client-on-elastic-serverless">在 Elastic Serverless 上运行客户端</a></li><li><a href="https://www.elastic.co/search-labs/blog/how-to-use-elasticsearch-in-javascript-part-ii#running-the-client-on-function-as-a-service-environment">在功能即服务环境中运行客户端</a></li></ul></li></ul><p><em>您可以 <a href="https://github.com/Delacrobix/JS-client-best-practices_article"><strong>在这里</strong></a></em>查看示例的源代码 <strong><em>。</em></strong></p><h2>生产最佳实践</h2><h3>Elasticsearch 中的错误处理</h3><p>Node.js 中 Elasticsearch 客户端的一个有用功能是，它为 Elasticsearch 中可能出现的错误提供了对象，因此您可以用不同的方式验证和处理这些错误。</p><p>要<a href="https://www.elastic.co/docs/reference/elasticsearch/clients/javascript/connecting#client-error-handling">查看全部内容</a>，请执行此操作： </p><pre><code>const { errors } = require('@elastic/elasticsearch')
console.log(errors)</code></pre><p>让我们回到搜索示例，处理一些可能出现的错误：</p><pre><code>app.get("/search/lexic", async (req, res) => {
 ....
  } catch (error) {
    if (error instanceof errors.ResponseError) {
      let errorMessage =
        "Response error!, query malformed or server down, contact the administrator!";

      if (error.body.error.type === "parsing_exception") {
        errorMessage = "Query malformed, make sure mappings are set correctly";
      }

      res.status(error.meta.statusCode).json({
        erroStatus: error.meta.statusCode,
        success: false,
        results: null,
        error: errorMessage,
      });
    }

    res.status(500).json({
      success: false,
      results: null,
      error: error.message,
    });
  }
});</code></pre><p><code>ResponseError</code> 尤其是当答案为<code>4xx</code> 或<code>5xx</code> 时，即表示请求不正确或服务器不可用。</p><p>我们可以通过生成错误查询来测试这类错误，比如尝试<strong>在文本类型字段上进行术语查询：</strong></p><p>默认错误：</p><pre><code> {
    "success": false,
    "results": null,
    "error": "parsing_exception\n\tRoot causes:\n\t\tparsing_exception: [terms] query does not support [visit_details]"
}</code></pre><p>定制错误： </p><pre><code>{
    "erroStatus": 400,
    "success": false,
    "results": null,
    "error": "Response error!, query malformed or server down; contact the administrator!"
}</code></pre><p>我们还可以以某种方式捕捉和处理每种类型的错误。例如，我们可以在<code>TimeoutError</code> 中添加重试逻辑。</p><pre><code>app.get("/search/semantic", async (req, res) => {
    try {
  ...
  } catch (error) {
    if (error instanceof errors.TimeoutError) {


     // Retry logic...

      res.status(error.meta.statusCode).json({
        erroStatus: error.meta.statusCode,
        success: false,
        results: null,
        error:
          "The request took more than 10s after 3 retries. Try again later.",
      });
    }
  }
});</code></pre><h3>测试</h3><p>测试是保证应用程序稳定性的关键。为了以一种与 Elasticsearch 隔离的方式测试代码，我们可以在创建集群时使用<a href="https://github.com/elastic/elasticsearch-js-mock">elasticsearch-js-mock</a>库。</p><p>通过该库，我们可以实例化一个与真实客户端非常相似的客户端，但只需将客户端的 HTTP 层替换为模拟层，其他部分与原始客户端保持一致，就能满足我们的配置要求。</p><p>我们将安装 mocks 库和用于自动测试的<a href="https://github.com/avajs/ava">AVA</a>。</p><p><code>npm install @elastic/elasticsearch-mock</code></p><p><code>npm install --save-dev ava</code></p><p>我们将配置<code>package.json</code> 文件以运行测试。确保它看起来是这样的：</p><pre><code>"type": "module",
	"scripts": {
		"test": "ava"
	},
	"devDependencies": {
		"ava": "^5.0.0"
	}</code></pre><p>现在，让我们创建<code>test.js</code> 文件并安装我们的模拟客户端：</p><pre><code>const { Client } = require('@elastic/elasticsearch')
const Mock = require('@elastic/elasticsearch-mock')

const mock = new Mock()
const client = new Client({
  node: 'http://localhost:9200',
  Connection: mock.getConnection()
})</code></pre><p>现在，为语义搜索添加一个模拟：</p><pre><code>function createSemanticSearchMock(query, indexName) {
  mock.add(
    {
      method: "POST",
      path: `/${indexName}/_search`,
      body: {
        query: {
          semantic: {
            field: "semantic_field",
            query: query,
          },
        },
      },
    },
    () => {
      return {
        hits: {
          total: { value: 2, relation: "eq" },
          hits: [
            {
              _id: "1",
              _score: 0.9,
              _source: {
                owner_name: "Alice Johnson",
                pet_name: "Buddy",
                species: "Dog",
                breed: "Golden Retriever",
                vaccination_history: ["Rabies", "Parvovirus", "Distemper"],
                visit_details:
                  "Annual check-up and nail trimming. Healthy and active.",
              },
            },
            {
              _id: "2",
              _score: 0.7,
              _source: {
                owner_name: "Daniel Kim",
                pet_name: "Mochi",
                species: "Rabbit",
                breed: "Mixed",
                vaccination_history: [],
                visit_details:
                  "Nail trimming and general health check. No issues.",
              },
            },
          ],
        },
      };
    }
  );
}</code></pre><p>现在我们可以为代码创建一个测试，确保 Elasticsearch 部分始终返回相同的结果：</p><pre><code>import test from 'ava';

test("performSemanticSearch must return formatted results correctly", async (t) => {
  const indexName = "vet-visits";
  const query = "Which pets had nail trimming?";

  createSemanticSearchMock(query, indexName);

  async function performSemanticSearch(esClient, q, indexName = "vet-visits") {
    try {
      const result = await esClient.search({
        index: indexName,
        body: {
          query: {
            semantic: {
              field: "semantic_field",
              query: q,
            },
          },
        },
      });

      return {
        success: true,
        results: result.hits.hits,
      };
    } catch (error) {
      if (error instanceof errors.TimeoutError) {
        return {
          success: false,
          results: null,
          error: error.body.error.reason,
        };
      }

      return {
        success: false,
        results: null,
        error: error.message,
      };
    }
  }

  const result = await performSemanticSearch(esClient, query, indexName);

  t.true(result.success, "The search must be successful");
  t.true(Array.isArray(result.results), "The results must be an array");

  if (result.results.length > 0) {
    t.true(
      "_source" in result.results[0],
      "Each result must have a _source property"
    );
    t.true(
      "pet_name" in result.results[0]._source,
      "Results must include the pet_name field"
    );
    t.true(
      "visit_details" in result.results[0]._source,
      "Results must include the visit_details field"
    );
  }
});</code></pre><p>让我们进行测试。</p><p><code>npm run test</code></p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/42feae845ae8eae03c37ad7ad114e8db35984812-1186x302.png" style="width: 100%;" /><p>完成！从现在起，我们就可以测试我们的应用程序，100% 专注于代码而不是外部因素。</p><h2>无服务器环境</h2><h3>如何在 Elastic Serverless 上运行客户端</h3><p>我们介绍了在云端或内部运行 Elasticsearch 的情况；不过，Node.js 客户端也支持与<a href="https://www.elastic.co/guide/en/serverless/current/intro.html">Elastic Cloud Serverless</a> 的连接。</p><p>Elastic Cloud Serverless 允许您创建一个项目，在这个项目中，您无需担心基础设施问题，因为 Elastic 会在内部处理这些问题，您只需担心您想索引的数据以及您想在多长时间内访问这些数据。</p><p>从使用角度来看，Serverless 将计算与存储分离，为<a href="https://www.elastic.co/search-labs/blog/elasticsearch-serverless-tier-autoscaling">搜索</a>和<a href="https://www.elastic.co/search-labs/blog/elasticsearch-ingest-autoscaling">索引</a>提供了自动扩展功能。这样，您就可以只增长实际需要的资源。</p><p>客户端会进行以下调整，以连接到无服务器：</p><ul><li>关闭嗅探，忽略任何与嗅探相关的选项</li><li>忽略配置中传递的除第一个节点外的所有节点，并忽略任何节点过滤和选择选项</li><li>启用压缩和 &quot;TLSv1_2_method&quot;（与为弹性云配置时相同）</li><li>为所有请求添加 &quot;elastic-api-version &quot;HTTP 头信息</li><li>默认使用 &quot;云连接池&quot;，而不是 &quot;加权连接池</li><li>关闭卖方 &quot;内容类型 &quot;和 &quot;接受 &quot;标头，转而使用标准 MIME 类型</li></ul><p>要连接无服务器项目，需要使用参数 serverMode：serverless。</p><pre><code>const { Client } = require('@elastic/elasticsearch')
const client = new Client({
  node: 'ELASTICSEARCH_ENDPOINT',
  auth: { apiKey: 'ELASTICSEARCH_API_KEY' },
  serverMode: "serverless",
});</code></pre><h3>如何在函数即服务环境中运行客户端</h3><p>在示例中，我们使用了 Node.js 服务器，但您也可以使用功能即服务环境连接 AWS lambda、GCP Run 等功能。</p><pre><code>'use strict'

const { Client } = require('@elastic/elasticsearch')

const client = new Client({
  // client initialisation
})

exports.handler = async function (event, context) {
  // use the client
}</code></pre><p>另一个例子是连接像 Vercel 这样的服务，它也是无服务器的。您可以查看这个<a href="https://github.com/elastic/elasticsearch-js/blob/main/docs/examples/proxy/README.md">完整的示例</a>，了解如何做到这一点，但<a href="https://github.com/elastic/elasticsearch-js/blob/main/docs/examples/proxy/api/search.js">搜索端点</a>最相关的部分如下所示：</p><pre><code>const response = await client.search(
  {
    index: INDEX,
    // You could directly send from the browser
    // the Elasticsearch's query DSL, but it will
    // expose you to the risk that a malicious user
    // could overload your cluster by crafting
    // expensive queries.
    query: {
      match: { field: req.body.text },
    },
  },
  {
    headers: {
      Authorization: `ApiKey ${token}`,
    },
  }
);</code></pre><p>该端点位于 /api 文件夹中，从服务器端运行，因此客户端只能控制与搜索词相对应的 &quot;文本 &quot;参数。</p><p>使用 &quot;功能即服务 &quot;的意义在于，与全天候运行的服务器不同，功能只启动运行该功能的机器，一旦完成，机器就会进入休息模式，以减少资源消耗。</p><p>如果应用程序没有收到太多请求，这种配置会很方便；否则，成本会很高。您还需要考虑<a href="https://docs.aws.amazon.com/lambda/latest/dg/lambda-runtime-environment.html">函数的生命周期</a>和运行时间（在某些情况下可能只有几秒钟）。</p><h2>结论</h2><p>在本文中，我们学习了如何处理错误，这在生产环境中至关重要。我们还介绍了在模拟 Elasticsearch 服务的过程中测试应用程序的方法，无论集群的状态如何，这种方法都能提供可靠的测试，让我们专注于我们的代码。</p><p>最后，我们演示了如何通过配置 Elastic Cloud Serverless 和 Vercel 应用程序来启动完全无服务器堆栈。</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[如何优化 Elasticsearch 磁盘空间和使用率]]></title>
            <description><![CDATA[了解如何预防并处理 Elasticsearch 磁盘使用率过高（超载）以及容量利用率不足的情况，从而优化集群成本。]]></description>
            <link>https://www.elastic.co/search-labs/cn/blog/optimize-elasticsearch-disk-space-and-usage</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/cn/blog/optimize-elasticsearch-disk-space-and-usage</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <pubDate>Fri, 16 May 2025 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/5d41a078dd03e4480a0ff4e9591c8618b9bab4d0-720x420.png?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>磁盘管理对任何数据库都很重要，Elasticsearch 也不例外。如果没有足够的可用磁盘空间，Elasticsearch 将停止向节点分配分片。这将最终导致您无法向群集写入数据，并有可能导致应用程序中的数据丢失。另一方面，如果磁盘空间过大，则需要为超出需要的资源付费。</p><h2>水印背景</h2><p>Elasticsearch 集群上有各种 &quot;水印 &quot;阈值，可帮助您跟踪可用磁盘空间。当节点上的磁盘填满时，第一个越过的阈值就是 &quot;低磁盘水印&quot;。 第二个阈值就是 &quot;高磁盘水印阈值&quot;。 最后，将达到 &quot;磁盘淹没阶段&quot;。一旦过了这个阈值，群集就会阻止写入已通过水印的节点上有一个分片（主分片或副本）的所有索引。 仍可进行读取（搜索）。</p><h2>如何预防和处理磁盘过满（利用率过高）的情况</h2><p>有多种方法可以处理 Elasticsearch 磁盘过满的情况：</p><ol><li><strong>删除</strong> <strong>旧数据：</strong>通常情况下，数据不应无限期保存。防止和解决磁盘过满的方法之一是确保当数据达到一定年限时，对其进行可靠的归档和删除。一种方法是使用<a href="https://www.elastic.co/docs/manage-data/lifecycle/index-lifecycle-management">ILM</a>。</li><li><strong>增加存储容量：</strong>如果无法删除数据，可能需要添加更多数据节点或增加磁盘大小，以便在不影响性能的情况下保留所有数据。如果需要为群集增加存储容量，则应考虑是否只需增加存储容量，还是同时按比例增加存储容量以及 RAM 和 CPU 资源（请参阅下文有关<a href="https://www.elastic.co/search-labs/blog/optimize-elasticsearch-disk-space-and-usage#the-relationship-between-disk-size,-ram-and-cpu">磁盘大小、RAM 和 CPU 比例的</a>部分）。</li></ol><h2>如何为 Elasticsearch 集群增加存储容量</h2><ol><li><strong>增加数据节点的数量： </strong>请记住，新节点的大小应与现有节点相同，并使用相同的 Elasticsearch 版本。</li><li><strong>扩大现有节点的规模： </strong>在基于云的环境中，增加现有节点的磁盘大小和内存/CPU 通常很容易。</li><li><strong>只增加磁盘大小： </strong>在基于云的环境中，增加磁盘大小通常相对容易。</li><li><a href="https://www.elastic.co/docs/deploy-manage/tools/snapshot-and-restore"><strong>快照</strong> <strong>和</strong> <strong>恢复</strong></a><strong>：</strong>如果您愿意让旧数据根据要求通过自动流程从备份中检索出来，您可以对旧索引进行快照、删除，并根据要求从快照中临时恢复数据。 </li><li><strong>减少每个分片的副本数量：</strong>减少数据的另一个方法是减少每个分片的副本数量。为了实现高可用性，您希望每个分片有一个副本，但当数据变旧时，您可能不需要副本也能工作。如果数据是持久性的，或者您有备份可以在需要时恢复，那么这种方法通常是可行的。</li><li><strong>创建警报：</strong>为了防止磁盘将来被填满并采取主动行动，应根据磁盘使用情况创建警报，以便在磁盘开始填满时发出通知。 </li></ol><h2>如何预防和处理磁盘容量利用不足的情况</h2><p>如果磁盘容量未得到充分利用，有多种选择可以减少群集的存储容量。</p><h3>如何减少 Elasticsearch 集群的存储容量</h3><p>减少群集存储容量的方法有很多种。</p><p><strong>1.减少数据节点数量</strong></p><p>如果你想减少数据存储，同时按相同比例减少 RAM 和 CPU 资源，那么这是最简单的策略。停用不必要的节点可能会节省最大的成本。</p><p>在停止节点运行之前，您应该</p><ul><li>确保要停用的节点不需要作为 MASTER 节点。应始终至少有三个节点具有 MASTER 节点角色。</li><li>将数据碎片从要退役的节点上移走。</li></ul><p><strong>2.用较小的节点取代现有节点</strong></p><p>如果无法进一步减少节点数量（通常最低配置为 3 个），则可能需要缩小现有节点的规模。请记住，最好确保所有数据节点的 RAM 内存和磁盘大小相同，因为分片是根据每个节点的分片数量进行平衡的。</p><p>过程如下</p><ul><li>向群集添加新的、较小的节点</li><li>将碎片迁移到远离将要退役的节点的地方</li><li>关闭旧节点</li></ul><p><strong>3.缩小节点上的磁盘大小</strong></p><p>如果只想减少节点上的磁盘大小，而不改变群集的整体 RAM 或 CPU，那么可以减少每个节点的磁盘大小。减少 Elasticsearch 节点上的磁盘大小并非易事。</p><p>最简单的方法通常是</p><ul><li>从节点迁移碎片</li><li>停止节点</li><li>在节点上挂载新数据卷，并设置适当大小</li><li>将旧磁盘卷中的所有数据复制到新卷中</li><li>分离旧卷 A</li><li>启动节点并将碎片迁移回节点</li></ul><p>这就要求其他节点上有足够的容量，以便在此过程中临时存储节点上的额外碎片。在许多情况下，管理这一流程的成本可能会超过潜在的磁盘使用节余。因此，用具有所需磁盘大小的新节点完全替换该节点可能更简单（请参阅上文 &quot;用较小节点替换现有节点&quot;）。</p><p>在为不必要的资源付费时，显然可以通过优化资源利用率来降低成本。</p><h2>磁盘大小、内存和 CPU 之间的关系</h2><p>集群中磁盘容量与内存的理想比例取决于您的具体使用情况。因此，在考虑更改存储容量时，还应考虑当前的磁盘/内存/CPU 比例是否适当平衡，以及是否需要按相同比例增加/减少内存/CPU。</p><p>内存和 CPU 需求取决于<a href="https://opster.com/guides/elasticsearch/glossary/elasticsearch-indexing/">索引</a>活动量、查询次数和类型，以及搜索和汇总的数据量。这通常与群集上存储的数据量成正比，因此也应与磁盘大小相关。</p><p>磁盘容量和内存之间的比例可根据使用情况进行调整。请看这里的几个例子：</p>
            <table style="border-collapse: collapse; margin: 20px 0;">
              <tbody>
                
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;"></td><td style="border: 1px solid #ccc; padding: 4px;">指数活动</td><td style="border: 1px solid #ccc; padding: 4px;">保留</td><td style="border: 1px solid #ccc; padding: 4px;">搜索活动</td><td style="border: 1px solid #ccc; padding: 4px;">磁盘容量</td><td style="border: 1px solid #ccc; padding: 4px;">内存</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">企业搜索应用程序</td><td style="border: 1px solid #ccc; padding: 4px;">适度摄入原木</td><td style="border: 1px solid #ccc; padding: 4px;">长</td><td style="border: 1px solid #ccc; padding: 4px;">灯光</td><td style="border: 1px solid #ccc; padding: 4px;">2TB</td><td style="border: 1px solid #ccc; padding: 4px;">32GB</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">应用程序监控</td><td style="border: 1px solid #ccc; padding: 4px;">大量摄入原木</td><td style="border: 1px solid #ccc; padding: 4px;">短</td><td style="border: 1px solid #ccc; padding: 4px;">灯光</td><td style="border: 1px solid #ccc; padding: 4px;">1TB</td><td style="border: 1px solid #ccc; padding: 4px;">32GB</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">电子商务</td><td style="border: 1px solid #ccc; padding: 4px;">轻型数据索引</td><td style="border: 1px solid #ccc; padding: 4px;">无限期</td><td style="border: 1px solid #ccc; padding: 4px;">重型</td><td style="border: 1px solid #ccc; padding: 4px;">500GB</td><td style="border: 1px solid #ccc; padding: 4px;">32GB</td>
                    </tr>
              </tbody>
            </table>
          <p><em>请记住，修改节点机器配置时必须小心谨慎，因为这可能会导致节点宕机，而且需要确保分片不会开始迁移到其他已经过度紧张的节点上。</em></p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[正确使用 JavaScript 的 Elasticsearch，第一部分]]></title>
            <description><![CDATA[讲解如何用 JavaScript 创建可投入生产的 Elasticsearch 后端。  

探索如何使用 JavaScript 与 Elasticsearch，遵循客户端/服务器最佳实践，搭建包含多个搜索端点的服务器，用于查询 Elasticsearch 文档。]]></description>
            <link>https://www.elastic.co/search-labs/cn/blog/how-to-use-elasticsearch-in-javascript-part-i</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/cn/blog/how-to-use-elasticsearch-in-javascript-part-i</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Jeffrey Rengifo]]></dc:creator>
            <pubDate>Thu, 15 May 2025 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/72200540ed258779d87e53a72ea189f8a138540c-1600x901.png?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>本文是系列文章的第一篇，介绍如何使用 JavaScript 使用 Elasticsearch。在本系列中，您将学习如何在 JavaScript 环境中使用 Elasticsearch 的基础知识，并回顾创建搜索应用程序的最相关功能和最佳实践。最后，您将了解使用 JavaScript 运行 Elasticsearch 所需的一切。</p><p>在第一部分中，我们将回顾</p><ul><li><a href="https://www.elastic.co/search-labs/blog/how-to-use-elasticsearch-in-javascript-part-i#environment">环境</a><ul><li><a href="https://www.elastic.co/search-labs/blog/how-to-use-elasticsearch-in-javascript-part-i#frontend,-backend,-or-serverless?">前端、后端还是无服务器？</a></li><li><a href="https://www.elastic.co/search-labs/blog/how-to-use-elasticsearch-in-javascript-part-i#connecting-the-client">连接客户端</a></li></ul></li><li><a href="https://www.elastic.co/search-labs/blog/how-to-use-elasticsearch-in-javascript-part-i#indexing-documents">编制文件索引</a><ul><li><a href="https://www.elastic.co/search-labs/blog/how-to-use-elasticsearch-in-javascript-part-i#elasticsearch-client">Elasticsearch 客户端</a></li><li><a href="https://www.elastic.co/search-labs/blog/how-to-use-elasticsearch-in-javascript-part-i#semantic-mappings">语义映射</a></li><li><a href="https://www.elastic.co/search-labs/blog/how-to-use-elasticsearch-in-javascript-part-i#bulk-helper">批量助手</a></li></ul></li><li><a href="https://www.elastic.co/search-labs/blog/how-to-use-elasticsearch-in-javascript-part-i#searching-data">搜索数据</a><ul><li><a href="https://www.elastic.co/search-labs/blog/how-to-use-elasticsearch-in-javascript-part-i#lexical-query-(/search/lexic?q=%3Cquery-term%3E)">词法查询</a></li><li><a href="https://www.elastic.co/search-labs/blog/how-to-use-elasticsearch-in-javascript-part-i#semantic-query-(/search/semantic?q=%3Cquery-term%3E)">语义查询</a></li><li><a href="https://www.elastic.co/search-labs/blog/how-to-use-elasticsearch-in-javascript-part-i#hybrid-query-(/search/hybrid?q=%3Cquery-term%3E)">混合查询</a></li></ul></li></ul><p><em>您可以 <a href="https://github.com/Delacrobix/JS-client-best-practices_article"><strong>在这里</strong></a></em>查看示例的源代码 <strong><em>。</em></strong></p><h3>什么是 Elasticsearch Node.js 客户端？</h3><p><a href="https://www.elastic.co/guide/en/elasticsearch/client/javascript-api/current/index.html">Elasticsearch Node.js 客户端</a>是一个 JavaScript 库，它将 Elasticsearch API 的 HTTP REST 调用放到了 JavaScript 中。这样就能更轻松地处理和使用帮助程序，简化批量编制文档索引等任务。</p><h2>环境</h2><h3>前端、后端还是无服务器？</h3><p>要使用 JavaScript 客户端创建搜索应用程序，我们至少需要两个组件：Elasticsearch 集群和运行客户端的 JavaScript 运行时。</p><p>JavaScript 客户端支持所有 Elasticsearch 解决方案（云、on-prem 和 Serverless），它们之间没有重大区别，因为客户端内部会处理所有变化，所以你不必担心使用哪一种。</p><p>不过，JavaScript 运行时必须从<strong>服务器</strong>运行，而<strong>不能直接从浏览器</strong>运行。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/92ce6cfd923c8008fa44f617a58193642d9d5879-661x410.png" style="width: 100%;" /><p>这是因为从浏览器调用 Elasticsearch 时，用户可能会获得敏感信息，如集群 API 密钥、主机或查询本身。Elasticsearch 建议<strong>永远不要将集群直接暴露在互联网上 </strong>，而是使用一个中间层来抽象所有这些信息，这样用户只能看到参数。您可以<a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/es-security-principles.html#security-protect-cluster-traffic">在这里</a>了解更多相关信息。</p><p>我们建议使用这样的模式：</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/a08769f08ec73fe57bf2e961cfdfbb1cdd57919d-972x429.png" style="width: 100%;" /><p>在这种情况下，客户端只向服务器发送搜索条件和验证密钥，而服务器则完全控制查询和与 Elasticsearch 的通信。</p><h3>连接客户端</h3><p>首先，按照<a href="https://www.elastic.co/search-labs/tutorials/install-elasticsearch/elastic-cloud">以下步骤</a>创建一个 API 密钥。</p><p>按照前面的示例，我们将创建一个简单的 Express 服务器，并使用 Node.JS 服务器的客户端连接到该服务器。</p><p>我们将使用 NPM 初始化项目，并安装 Elasticsearch 客户端和<a href="https://expressjs.com/">Express。</a>后者是一个在 Node.js 中调用服务器的库。使用 Express，我们可以通过 HTTP 与后端交互。</p><p>让我们初始化项目：</p><p><code>npm init -y</code></p><p>安装依赖项：</p><p><code>npm install @elastic/elasticsearch express split2 dotenv</code></p><p>让我来为你分析一下：</p><ul><li><a href="https://www.npmjs.com/package/@elastic/elasticsearch"><strong><em>@elastic/elasticsearch</em></strong></a>：它是 Node.js 的官方客户端</li><li><a href="https://www.npmjs.com/package/express"><strong><em>快递</em></strong></a>：它将使我们能够运行一个轻量级的 nodejs 服务器，以暴露 Elasticsearch</li><li><a href="https://www.npmjs.com/package/split2"><strong><em>split2</em></strong></a>： 将文本行分割成数据流。每次处理一行 ndjson 文件时非常有用</li><li><a href="https://www.npmjs.com/package/dotenv"><strong><em>dotenv</em></strong></a>：允许我们使用 .env 管理环境变量文件</li></ul><p>创建 .env文件，并添加以下几行：</p><pre><code>ELASTICSEARCH_ENDPOINT="Your Elasticsearch endpoint"
ELASTICSEARCH_API_KEY="Your Elasticssearch API"</code></pre><p>这样，我们就可以使用<code>dotenv</code> 软件包导入这些变量。</p><p>创建<code>server.js</code> 文件：</p><pre><code>const express = require("express");
const bodyParser = require("body-parser");
const { Client } = require("@elastic/elasticsearch");
 
require("dotenv").config(); //environment variables setup

const ELASTICSEARCH_ENDPOINT = process.env.ELASTICSEARCH_ENDPOINT;
const ELASTICSEARCH_API_KEY = process.env.ELASTICSEARCH_API_KEY;
const PORT = 3000;


const app = express();

app.listen(PORT, () => {
  console.log("Server running on port", PORT);
});
app.use(bodyParser.json());


let esClient = new Client({
  node: ELASTICSEARCH_ENDPOINT,
  auth: { apiKey: ELASTICSEARCH_API_KEY },  
});

app.get("/ping", async (req, res) => {
  try {
    const result = await esClient.info();

    res.status(200).json({
      success: true,
      clusterInfo: result,
    });
  } catch (error) {
    console.error("Error getting Elasticsearch info:", error);

    res.status(500).json({
      success: false,
      clusterInfo: null,
      error: error.message,
    });
  }
});</code></pre><p>这段代码设置了一个基本的 Express.js 服务器，该服务器监听端口 3000，并使用 API 密钥进行身份验证，连接到 Elasticsearch 集群。它包括一个 /ping 端点，通过 GET 请求访问时，可使用 Elasticsearch 客户端的<code>.info()</code> 方法查询 Elasticsearch 集群的基本信息。 </p><p>如果查询成功，会以 JSON 格式返回群集信息；否则会返回错误信息。服务器还使用 body-parser 中间件来处理 JSON 请求体。</p><p>运行文件，启动服务器：</p><p><code>node server.js</code></p><p>答案应该是这样的</p><pre><code>Server running on port 3000</code></pre><p>现在，让我们查阅端点<code>/ping</code> ，检查 Elasticsearch 集群的状态。</p><pre><code>curl http://localhost:3000/ping
{
    "success": true,
    "clusterInfo": {
        "name": "instance-0000000000",
        "cluster_name": "61b7e19eec204d59855f5e019acd2689",
        "cluster_uuid": "BIfvfLM0RJWRK_bDCY5ldg",
        "version": {
            "number": "9.0.0",
            "build_flavor": "default",
            "build_type": "docker",
            "build_hash": "112859b85d50de2a7e63f73c8fc70b99eea24291",
            "build_date": "2025-04-08T15:13:46.049795831Z",
            "build_snapshot": false,
            "lucene_version": "10.1.0",
            "minimum_wire_compatibility_version": "8.18.0",
            "minimum_index_compatibility_version": "8.0.0"
        },
        "tagline": "You Know, for Search"
    }
}</code></pre><h2>编制文件索引</h2><p>一旦连接起来，我们就可以使用语义<a href="https://www.elastic.co/search-labs/blog/semantic-search-simplified-semantic-text">_文本（</a>用于语义搜索）和文本（用于全文查询）等映射对文档进行索引。有了这两种字段类型，我们还可以进行<a href="https://www.elastic.co/what-is/hybrid-search">混合搜索</a>。</p><p>我们将创建一个新的<code>load.js</code> 文件来生成映射并上传文件。</p><h3>Elasticsearch 客户端</h3><p>我们首先需要对客户端进行实例化和身份验证：</p><pre><code>const { Client } = require("@elastic/elasticsearch");

const ELASTICSEARCH_ENDPOINT = "cluster/project_endpoint";
const ELASTICSEARCH_API_KEY = "apiKey";

const esClient = new Client({
  node: ELASTICSEARCH_ENDPOINT,
  auth: { apiKey: ELASTICSEARCH_API_KEY },
});</code></pre><h3>语义映射</h3><p>我们将创建一个包含兽医院数据的索引。我们将保存主人、宠物和访问详情的信息。</p><p>我们要进行全文搜索的数据，如名称和描述，将以文本形式存储。类别中的数据，如动物的种类或品种，将以关键字的形式存储。</p><p>此外，我们还将把所有字段的值复制到一个 semantic_text 字段中，以便也能针对这些信息运行语义搜索。</p><pre><code>const INDEX_NAME = "vet-visits";

const createMappings = async (indexName, mapping) => {
  try {
    const body = await esClient.indices.create({
      index: indexName,
      body: {
        mappings: mapping,
      },
    });

    console.log("Index created successfully:", body);
  } catch (error) {
    console.error("Error creating mapping:", error);
  }
};

await createMappings(INDEX_NAME, {
  properties: {
    owner_name: {
      type: "text",
      copy_to: "semantic_field",
    },
    pet_name: {
      type: "text",
      copy_to: "semantic_field",
    },
    species: {
      type: "keyword",
      copy_to: "semantic_field",
    },
    breed: {
      type: "keyword",
      copy_to: "semantic_field",
    },
    vaccination_history: {
      type: "keyword",
      copy_to: "semantic_field",
    },
    visit_details: {
      type: "text",
      copy_to: "semantic_field",
    },
    semantic_field: {
      type: "semantic_text",
    },
  },
});</code></pre><h3>批量助手</h3><p>客户端的另一个优势是，我们可以使用<a href="https://www.elastic.co/guide/en/elasticsearch/client/javascript-api/current/client-helpers.html#bulk-helper">批量助手</a>来分批建立索引。通过批量辅助器，我们可以轻松处理并发、重试等问题，以及如何处理通过函数成功或失败的每个文档。</p><p>该助手的一个吸引人的特点是可以使用数据流。该功能允许您逐行发送文件，而不是将整个文件存储在内存中并一次性发送到 Elasticsearch。</p><p>要将数据上传到 Elasticsearch，请在项目根目录下创建名为 data.ndjson 的文件，并添加以下信息（也可以从<a href="https://github.com/Delacrobix/JS-client-best-practices_article/blob/main/data.ndjson">此处</a>下载包含数据集的文件）：</p><pre><code>{"owner_name":"Alice Johnson","pet_name":"Buddy","species":"Dog","breed":"Golden Retriever","vaccination_history":["Rabies","Parvovirus","Distemper"],"visit_details":"Annual check-up and nail trimming. Healthy and active."}
{"owner_name":"Marco Rivera","pet_name":"Milo","species":"Cat","breed":"Siamese","vaccination_history":["Rabies","Feline Leukemia"],"visit_details":"Slight eye irritation, prescribed eye drops."}
{"owner_name":"Sandra Lee","pet_name":"Pickles","species":"Guinea Pig","breed":"Mixed","vaccination_history":[],"visit_details":"Loss of appetite, recommended dietary changes."}
{"owner_name":"Jake Thompson","pet_name":"Luna","species":"Dog","breed":"Labrador Mix","vaccination_history":["Rabies","Bordetella"],"visit_details":"Mild ear infection, cleaning and antibiotics given."}
{"owner_name":"Emily Chen","pet_name":"Ziggy","species":"Cat","breed":"Mixed","vaccination_history":["Rabies","Feline Calicivirus"],"visit_details":"Vaccination update and routine physical."}
{"owner_name":"Tomás Herrera","pet_name":"Rex","species":"Dog","breed":"German Shepherd","vaccination_history":["Rabies","Parvovirus","Leptospirosis"],"visit_details":"Follow-up for previous leg strain, improving well."}
{"owner_name":"Nina Park","pet_name":"Coco","species":"Ferret","breed":"Mixed","vaccination_history":["Rabies"],"visit_details":"Slight weight loss; advised new diet."}
{"owner_name":"Leo Martínez","pet_name":"Simba","species":"Cat","breed":"Maine Coon","vaccination_history":["Rabies","Feline Panleukopenia"],"visit_details":"Dental cleaning. Minor tartar buildup removed."}
{"owner_name":"Rachel Green","pet_name":"Rocky","species":"Dog","breed":"Bulldog Mix","vaccination_history":["Rabies","Parvovirus"],"visit_details":"Skin rash, antihistamines prescribed."}
{"owner_name":"Daniel Kim","pet_name":"Mochi","species":"Rabbit","breed":"Mixed","vaccination_history":[],"visit_details":"Nail trimming and general health check. No issues."}</code></pre><p>我们使用 split2 对文件行进行流式处理，而批量助手则将它们发送到 Elasticsearch。</p><pre><code>const { createReadStream } = require("fs");
const split = require("split2");
 
const indexData = async (filePath, indexName) => {
  try {
    console.log(`Indexing data from ${filePath} into ${indexName}...`);

    const result = await esClient.helpers.bulk({
      datasource: createReadStream(filePath).pipe(split()),

      onDocument: () => {
        return {
          index: { _index: indexName },
        };
      },
      onDrop(doc) {
        console.error("Error processing document:", doc);
      },
    });

    console.log("Bulk indexing successful elements:", result.items.length);
  } catch (error) {
    console.error("Error indexing data:", error);
    throw error;
  }
};

await indexData("./data.ndjson", INDEX_NAME);</code></pre><p>上面的代码读取 .ndjson文件，并使用<code>helpers.bulk</code> 方法将每个 JSON 对象批量索引到指定的 Elasticsearch 索引中。它使用<code>createReadStream</code> 和<code>split2</code> 对文件进行流式处理，为每个文件设置索引元数据，并记录处理失败的文件。完成后，它会记录成功索引的项目数。</p><p>除<code>indexData</code> 功能外，您还可以使用 Kibana 直接通过用户界面上传文件，并使用<a href="https://www.elastic.co/docs/manage-data/ingest/upload-data-files">上传数据文件用户界面。</a></p><p>我们运行文件，将文件上传到 Elasticsearch 集群。</p><p><code>node load.js</code></p><pre><code>Creating mappings for index vet-visits...
Index created successfully: { acknowledged: true, shards_acknowledged: true, index: 'vet-visits' }
Indexing data from ./data.ndjson into vet-visits...
Bulk indexing completed. Total documents: 10, Failed: 0</code></pre><h2>在 Elasticsearch 中搜索数据</h2><p>回到<code>server.js</code> 文件，我们将创建不同的端点来执行词法、语义或混合搜索。</p><p>简而言之，这些类型的搜索并不相互排斥，而是取决于您需要回答的问题类型。</p>
            <table style="border-collapse: collapse; margin: 20px 0;">
              <tbody>
                
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">查询类型</td><td style="border: 1px solid #ccc; padding: 4px;">用例</td><td style="border: 1px solid #ccc; padding: 4px;">问题示例</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">词法查询</td><td style="border: 1px solid #ccc; padding: 4px;">问题中的单词或词根很可能出现在索引文件中。问题与文件之间的标记相似性。</td><td style="border: 1px solid #ccc; padding: 4px;">我在找一件蓝色运动 T 恤。</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">语义查询</td><td style="border: 1px solid #ccc; padding: 4px;">问题中的词语不可能出现在文件中。问题与文件之间的概念相似性。</td><td style="border: 1px solid #ccc; padding: 4px;">我在寻找适合寒冷天气穿的衣服。</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">混合搜索</td><td style="border: 1px solid #ccc; padding: 4px;">问题包含词汇和/或语义成分。问题与文档之间的标记和语义相似性。</td><td style="border: 1px solid #ccc; padding: 4px;">我想为海滩婚礼找一件 S 码的礼服。</td>
                    </tr>
              </tbody>
            </table>
          <p>问题的<strong><em>词汇 </em></strong>部分很可能是标题和说明的一部分，或者是类别名称，而<strong><em>语义 </em></strong>部分则是与这些领域相关的概念。<strong><em>蓝色</em></strong>可能是一个类别名称或描述的一部分，<strong><em>海滩婚礼</em></strong>不太可能是，但可以与亚麻服装在语义上相关。</p><h3>词法查询 (/search/lexic?q=&lt;query_term&gt;)</h3><p>词法搜索也称全文搜索，是指基于标记的相似性进行搜索；也就是说，经过分析后，将返回包含搜索标记的文档。</p><p>您可以<a href="https://www.elastic.co/demo-gallery/lexical-search">点击此处</a>查看我们的词法搜索实践教程。</p><pre><code>app.get("/search/lexic", async (req, res) => {
  const { q } = req.query;

  const INDEX_NAME = "vet-visits";

  try {
    const result = await esClient.search({
      index: INDEX_NAME,
      size: 5,
      body: {
        query: {
          multi_match: {
            query: q,
            fields: ["owner_name", "pet_name", "visit_details"],
          },
        },
      },
    });

    res.status(200).json({
      success: true,
      results: result.hits.hits
    });
  } catch (error) {
    console.error("Error performing search:", error);

    res.status(500).json({
      success: false,
      results: null,
      error: error.message,
    });
  }
});</code></pre><p>我们测试：<strong><em>修剪指甲</em></strong></p><pre><code>curl http://localhost:3000/search/lexic?q=nail%20trimming</code></pre><p>请回答：</p><pre><code>{
    "success": true,
    "results": [
        {
            "_index": "vet-visits",
            "_id": "-RY6RJYBLe2GoFQ6-9n9",
            "_score": 2.7075968,
            "_source": {
                "pet_name": "Mochi",
                "owner_name": "Daniel Kim",
                "species": "Rabbit",
                "visit_details": "Nail trimming and general health check. No issues.",
                "breed": "Mixed",
                "vaccination_history": []
            }
        },
        {
            "_index": "vet-visits",
            "_id": "8BY6RJYBLe2GoFQ6-9n9",
            "_score": 2.560356,
            "_source": {
                "pet_name": "Buddy",
                "owner_name": "Alice Johnson",
                "species": "Dog",
                "visit_details": "Annual check-up and nail trimming. Healthy and active.",
                "breed": "Golden Retriever",
                "vaccination_history": [
                    "Rabies",
                    "Parvovirus",
                    "Distemper"
                ]
            }
        }
    ]
}</code></pre><h3>语义查询 (/search/semantic?q=&lt;query_term&gt;)</h3><p>语义搜索与词汇搜索不同，它通过矢量搜索找到与搜索词含义相似的结果。</p><p>您可以<a href="https://www.elastic.co/demo-gallery/semantic-search">点击这里</a>查看我们的语义搜索实践教程。</p><pre><code>app.get("/search/semantic", async (req, res) => {
  const { q } = req.query;

  const INDEX_NAME = "vet-visits";

  try {
    const result = await esClient.search({
      index: INDEX_NAME,
      size: 5,
      body: {
        query: {
          semantic: {
            field: "semantic_field",
            query: q
          },
        },
      },
    });

    res.status(200).json({
      success: true,
      results: result.hits.hits,
    });
  } catch (error) {
    console.error("Error performing search:", error);

    res.status(500).json({
      success: false,
      results: null,
      error: error.message,
    });
  }
});</code></pre><p>我们进行测试：<strong><em>谁做了修脚？</em></strong></p><pre><code>curl http://localhost:3000/search/semantic?q=Who%20got%20a%20pedicure?</code></pre><p>请回答：</p><pre><code>{
    "success": true,
    "results": [
        {
            "_index": "vet-visits",
            "_id": "-RY6RJYBLe2GoFQ6-9n9",
            "_score": 4.861466,
            "_source": {
                "owner_name": "Daniel Kim",
                "pet_name": "Mochi",
                "species": "Rabbit",
                "breed": "Mixed",
                "vaccination_history": [],
                "visit_details": "Nail trimming and general health check. No issues."
            }
        },
        {
            "_index": "vet-visits",
            "_id": "8BY6RJYBLe2GoFQ6-9n9",
            "_score": 4.7152824,
            "_source": {
                "pet_name": "Buddy",
                "owner_name": "Alice Johnson",
                "species": "Dog",
                "visit_details": "Annual check-up and nail trimming. Healthy and active.",
                "breed": "Golden Retriever",
                "vaccination_history": [
                    "Rabies",
                    "Parvovirus",
                    "Distemper"
                ]
            }
        },
        {
            "_index": "vet-visits",
            "_id": "9RY6RJYBLe2GoFQ6-9n9",
            "_score": 1.6717153,
            "_source": {
                "pet_name": "Rex",
                "owner_name": "Tomás Herrera",
                "species": "Dog",
                "visit_details": "Follow-up for previous leg strain, improving well.",
                "breed": "German Shepherd",
                "vaccination_history": [
                    "Rabies",
                    "Parvovirus",
                    "Leptospirosis"
                ]
            }
        },
        {
            "_index": "vet-visits",
            "_id": "9xY6RJYBLe2GoFQ6-9n9",
            "_score": 1.5600781,
            "_source": {
                "pet_name": "Simba",
                "owner_name": "Leo Martínez",
                "species": "Cat",
                "visit_details": "Dental cleaning. Minor tartar buildup removed.",
                "breed": "Maine Coon",
                "vaccination_history": [
                    "Rabies",
                    "Feline Panleukopenia"
                ]
            }
        },
        {
            "_index": "vet-visits",
            "_id": "-BY6RJYBLe2GoFQ6-9n9",
            "_score": 1.2696637,
            "_source": {
                "pet_name": "Rocky",
                "owner_name": "Rachel Green",
                "species": "Dog",
                "visit_details": "Skin rash, antihistamines prescribed.",
                "breed": "Bulldog Mix",
                "vaccination_history": [
                    "Rabies",
                    "Parvovirus"
                ]
            }
        }
    ]
}</code></pre><h3>混合查询 (/search/hybrid?q=&lt;query_term&gt;)</h3><p>混合搜索允许我们将语义搜索和词法搜索结合起来，从而获得两全其美的效果：既能获得标记搜索的精确性，又能获得语义搜索的意义接近性。</p><pre><code>app.get("/search/hybrid", async (req, res) => {
  const { q } = req.query;

  const INDEX_NAME = "vet-visits";

  try {
    const result = await esClient.search({
      index: INDEX_NAME,
      body: {
        retriever: {
          rrf: {
            retrievers: [
              {
                standard: {
                  query: {
                    bool: {
                      must: {
                         multi_match: {
             query: q,
            fields: ["owner_name", "pet_name", "visit_details"],
          },
                      },
                    },
                  },
                },
              },
              {
                standard: {
                  query: {
                    bool: {
                      must: {
                        semantic: {
                          field: "semantic_field",
                          query: q,
                        },
                      },
                    },
                  },
                },
              },
            ],
          },
        },
        size: 5,
      },
    });

    res.status(200).json({
      success: true,
      results: result.hits.hits,
    });
  } catch (error) {
    console.error("Error performing search:", error);

    res.status(500).json({
      success: false,
      results: null,
      error: error.message,
    });
  }
});</code></pre><p>我们以 &quot;<strong><em>谁做了修脚或牙科治疗？&quot;</em></strong></p><pre><code>curl http://localhost:3000/search/hybrid?q=who%20got%20a%20pedicure%20or%20dental%20treatment</code></pre><p>响应：</p><pre><code>{
    "success": true,
    "results": [
        {
            "_index": "vet-visits",
            "_id": "9xY6RJYBLe2GoFQ6-9n9",
            "_score": 0.032522473,
            "_source": {
                "pet_name": "Simba",
                "owner_name": "Leo Martínez",
                "species": "Cat",
                "visit_details": "Dental cleaning. Minor tartar buildup removed.",
                "breed": "Maine Coon",
                "vaccination_history": [
                    "Rabies",
                    "Feline Panleukopenia"
                ]
            }
        },
        {
            "_index": "vet-visits",
            "_id": "-RY6RJYBLe2GoFQ6-9n9",
            "_score": 0.016393442,
            "_source": {
                "pet_name": "Mochi",
                "owner_name": "Daniel Kim",
                "species": "Rabbit",
                "visit_details": "Nail trimming and general health check. No issues.",
                "breed": "Mixed",
                "vaccination_history": []
            }
        },
        {
            "_index": "vet-visits",
            "_id": "8BY6RJYBLe2GoFQ6-9n9",
            "_score": 0.015873017,
            "_source": {
                "pet_name": "Buddy",
                "owner_name": "Alice Johnson",
                "species": "Dog",
                "visit_details": "Annual check-up and nail trimming. Healthy and active.",
                "breed": "Golden Retriever",
                "vaccination_history": [
                    "Rabies",
                    "Parvovirus",
                    "Distemper"
                ]
            }
        },
        {
            "_index": "vet-visits",
            "_id": "9RY6RJYBLe2GoFQ6-9n9",
            "_score": 0.015625,
            "_source": {
                "pet_name": "Rex",
                "owner_name": "Tomás Herrera",
                "species": "Dog",
                "visit_details": "Follow-up for previous leg strain, improving well.",
                "breed": "German Shepherd",
                "vaccination_history": [
                    "Rabies",
                    "Parvovirus",
                    "Leptospirosis"
                ]
            }
        },
        {
            "_index": "vet-visits",
            "_id": "8xY6RJYBLe2GoFQ6-9n9",
            "_score": 0.015384615,
            "_source": {
                "pet_name": "Luna",
                "owner_name": "Jake Thompson",
                "species": "Dog",
                "visit_details": "Mild ear infection, cleaning and antibiotics given.",
                "breed": "Labrador Mix",
                "vaccination_history": [
                    "Rabies",
                    "Bordetella"
                ]
            }
        }
    ]
}</code></pre><h2>结论</h2><p>在本系列的第一部分中，我们介绍了如何按照客户端/服务器最佳实践设置环境并创建带有不同搜索端点的服务器，以查询 Elasticsearch 文档。查看我们系列的<a href="https://www.elastic.co/search-labs/blog/how-to-use-elasticsearch-in-javascript-part-i">第二部分</a>，您将了解生产最佳实践以及如何在无服务器环境中运行 Elasticsearch Node.js 客户端。</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[如何配置 Elasticsearch 索引中的副本数量]]></title>
            <description><![CDATA[了解如何在 Elasticsearch 索引中配置 number_of_replicas 以提升搜索性能并提供节点故障恢复能力。 
]]></description>
            <link>https://www.elastic.co/search-labs/cn/blog/elasticsearch-index-number-of_replicas</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/cn/blog/elasticsearch-index-number-of_replicas</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <pubDate>Wed, 14 May 2025 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/23b96aaa1a38b1f4747b4a87695d816f24c0cf70-720x421.jpg?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>Elasticsearch 设计为分布式系统，可处理大量数据并提供高可用性。实现这一点的关键功能之一是索引复制概念，该概念由<code>number_of_replicas</code> 设置控制。本文将深入探讨这一设置的细节、影响以及如何正确配置。</p><h2>副本在 Elasticsearch 中的作用</h2><p>在 Elasticsearch 中，索引是在多个主分片上分区的文档集合。每个主分片都是一个独立的 Apache Lucene 索引，索引中的文档分布在所有主分片中。为确保高可用性和数据冗余，Elasticsearch 允许每个分片拥有一个或多个副本（称为副本）。<code>number_of_replicas</code> 设置可控制 Elasticsearch 为索引中每个主分区创建的副本分区（拷贝）数量。默认情况下，Elasticsearch 会为每个主分片创建一个副本，但可以根据系统要求进行更改。</p><h2>配置复制数</h2><p><code>number_of_replicas</code> 设置可在创建索引时配置，也可稍后更新。下面是创建索引时的设置方法：</p><pre><code>PUT /my_index
{
  "settings": {
    "number_of_replicas": 2
  }
}</code></pre><p>在此示例中，Elasticsearch 将为<code>my_index</code> 索引中的每个主分区创建两个副本。</p><p>要更新现有索引的<code>number_of_replicas</code> 设置，可以使用<code>_settings</code> API：</p><pre><code>PUT /my_index/_settings
{
  "number_of_replicas": 3
}</code></pre><p>该命令将更新<code>my_index</code> 索引，使每个主分区都有三个副本。</p><h2>复制数设置的影响</h2><p><code>number_of_replicas</code> 设置对 Elasticsearch<a href="https://opster.com/guides/elasticsearch/glossary/elasticsearch-cluster/">集群的</a>性能和弹性有重大影响。以下是一些需要考虑的要点：</p><ol><li><strong>数据冗余和可用性：</strong>通过为每个分片创建更多副本，增加<code>number_of_replicas</code> 可提高数据的可用性。如果某个节点发生故障，Elasticsearch 仍可从其余<a href="https://opster.com/guides/elasticsearch/glossary/elasticsearch-node/">节点</a>上的副本分片提供数据。</li><li><strong>搜索性能：</strong>副本分片可以为读取请求提供服务，因此拥有更多的副本可以通过在更多分片上分配负载来提高搜索性能。</li></ol><div style="display:none">Unknown block type "block", specify a component for it in the `components.types` option</div><ol><li><strong>存储要求：</strong>更多的副本意味着更多的存储空间。应确保群集有足够的容量来存储额外的副本。</li><li><strong>节点故障恢复能力：</strong> <code>number_of_replicas</code> 的设置应考虑群集中的节点数量。如果<code>number_of_replicas</code> 等于或大于节点数，则群集可以承受多个节点的故障而不会丢失数据。</li></ol><h2>设置复制数的最佳做法</h2><p><code>number_of_replicas</code> 的最佳设置取决于系统的具体要求。不过，这里有一些通用的最佳做法：</p><ul><li>对于单节点集群，<code>number_of_replicas</code> 应设置为 0，因为没有其他节点可以容纳副本。</li><li>对于多节点集群，<code>number_of_replicas</code> 至少应设置为 1，以确保数据冗余和高可用性。</li><li>如果搜索性能是一个优先事项，请考虑增加<code>number_of_replicas</code> 。不过，请注意写入性能和存储要求之间的权衡。</li><li>始终确保集群有足够的容量来存储额外的副本。</li></ul>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[将嵌入映射到 Elasticsearch 字段类型：semantic_text、dense_vector、sparse_vector]]></title>
            <description><![CDATA[讨论如何以及何时使用 semantic_text、dense_vector 或 sparse_vector，以及它们与嵌入生成的关系。]]></description>
            <link>https://www.elastic.co/search-labs/cn/blog/mapping-embeddings-to-elasticsearch-field-types</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/cn/blog/mapping-embeddings-to-elasticsearch-field-types</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <pubDate>Tue, 13 May 2025 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/f98fdff837db55b466780c0bae672aa6f6c3a966-1200x628.png?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>多年来，利用嵌入式技术提高信息检索相关性和准确性的做法有了长足的发展。Elasticsearch 等工具已经通过密集向量、稀疏向量和语义文本等专门字段类型支持这类数据。不过，要取得良好的效果，必须了解如何将嵌入正确映射到可用的 Elasticsearch 字段类型：<code>semantic_text</code>、<code>dense_vector</code> 和<code>sparse_vector</code> 。</p><p>在本文中，我们将讨论这些字段类型、每种类型的使用时间，以及它们与嵌入生成和使用策略的关系，包括在索引和查询过程中的关系。</p><h2>密集矢量类型</h2><p>Elasticsearch 中的<code>dense_vector</code> 字段类型用于存储密集向量，密集向量是文本、图像和音频等数据的数字表示，其中几乎所有维度都是相关的。这些向量是使用 OpenAI、Cohere 或 Hugging Face 等平台提供的嵌入模型生成的，旨在捕捉数据的整体语义，即使数据与其他文档不共享确切术语。</p><p>在 Elasticsearch 中，稠密向量的维度最多可达 4096，具体取决于所使用的模型。例如，all-MiniLM-L6-v2 模型生成的向量有 384 个维度，而 OpenAI 的 text-embedding-ada-002 模型生成的向量有 1536 个维度。</p><p><code>dense_vector</code> 字段通常被用作存储这类嵌入的默认类型，当需要更多控制时，例如使用预生成向量、应用自定义相似性函数或与外部模型集成。</p><h3>何时以及为何使用 dense_vector 类型？</h3><p>密集向量非常适合捕捉句子、段落或整个文档之间的语义相似性。当目标是比较文本的整体含义时，即使它们不共享相同的术语，它们也能很好地发挥作用。</p><p>密集矢量字段非常适合已经拥有外部嵌入生成管道，使用 OpenAI、Cohere 或 Hugging Face 等平台提供的模型，并且只想手动存储和查询这些矢量的情况。这种类型的字段与嵌入模型具有很高的兼容性，在生成和查询方面具有充分的灵活性，允许您控制矢量的生成、索引和搜索使用方式。</p><p>此外，它还支持不同形式的语义搜索，在需要调整排名逻辑的情况下，可使用 k-NN 或 script_score 等查询。这些可能性使密集矢量成为 RAG（检索增强生成）、推荐系统和基于相似性的个性化搜索等应用的理想选择。</p><p>最后，该字段允许您自定义相关性逻辑，使用<code>cosineSimilarity</code> 、<code>dotProduct</code> 或<code>l2norm</code> 等函数，根据使用情况的需要调整排名。 </p><p>对于需要灵活性、定制化和与上述高级用例兼容的用户来说，密集矢量仍然是最佳选择。</p><h3>如何使用密集矢量类型查询？</h3><p>对定义为<strong><code>dense_vector</code></strong> 的字段的搜索使用 k 近邻查询。该查询负责查找密集向量与查询向量最接近的文档。下面举例说明如何将 k-NN 查询应用于密集向量场：</p><pre><code>{
  "knn": {
    "field": "my_dense_vector",
    "k": 10,
    "num_candidates": 50,
    "query_vector": [/* vector generated by model */]
  }
}</code></pre><p>除 k-NN 查询外，如果需要自定义文档评分，也可以使用 script_score 查询，将其与<strong>余弦相似度、点积或 l2norm 等</strong>向量比较函数相结合，以更可控的方式计算相关性。请看示例：</p><pre><code>{
"script_score": {
    "query": { "match_all": {} },
    "script": {
      "source": "cosineSimilarity(params.query_vector,
'my_dense_vector') + 1.0",
      "params": {
        "query_vector": [/* vector */]
      }
    }
  }
}</code></pre><p>如果您想深入了解，我建议您阅读《<a href="https://www.elastic.co/search-labs/blog/vector-search-set-up-elasticsearch">如何在 Elasticsearch 中设置向量搜索</a>》一文。</p><p></p><h2>稀疏矢量类型</h2><p><strong><code>sparse_vector</code></strong> 字段类型用于存储稀疏矢量，稀疏矢量是一种数值表示，其中大部分值为零，只有少数项具有重要权重。这种类型的向量在基于术语的模型中很常见，如 SPLADE 或 ELSER（弹性学习稀疏 EncodeR）。</p><h3>何时以及为何使用稀疏向量类型？</h3><p>当你需要更精确的词汇搜索而又不牺牲语义智能时，稀疏向量是理想的选择。它们将文本表示为标记/值对，只突出显示最相关的术语和相关权重，从而提供清晰度、控制和效率。</p><p>这类字段在根据术语生成向量时特别有用，例如在 ELSER 或 SPLADE 模型中，这些模型会根据每个标记在文本中的相对重要性为其分配不同的权重。</p><p>如果您想控制查询中特定词语的影响，稀疏向量类型允许您手动调整词语的权重，以优化结果的排名。</p><p>它的主要优点包括：搜索透明，因为可以清楚地了解为什么某个文件被认为是相关的；存储高效，因为只保存非零值的标记，而不像密集向量那样存储所有维度。</p><p>此外，稀疏向量是混合搜索策略的理想补充，甚至可以与密集向量相结合，将词汇精确性与语义理解相结合。</p><h3>如何使用稀疏向量类型查询？</h3><p><strong><code>sparse_vector</code></strong> 查询可让您根据标记/值格式的查询向量搜索文档。请看下面的查询示例：</p><pre><code>{
  "query": {
    "sparse_vector": {
      "field": "field_sparse",
      "query_vector": {
        "token1": 0.6,
        "token2": 0.2,
        "token3": 0.9
      }
    }
  }
}</code></pre><p>如果希望使用训练有素的模型，可以使用推理端点自动将查询文本转换为稀疏向量：</p><pre><code>{
  "query": {
    "sparse_vector": {
      "field": "field_sparse",
      "inference_id": "the inference ID to produce the token/weights",
      "query": "search text"
    }
  }
}</code></pre><p>要进一步探讨这一主题，我建议阅读《<a href="https://www.elastic.co/search-labs/blog/sparse-vector-embedding">用训练有素的 ML 模型理解稀疏向量嵌入</a>》。</p><h2>语义文本类型</h2><p><strong><code>semantic_text</code></strong> 字段类型是在 Elasticsearch 中使用语义搜索的最简单、最直接的方法。它通过一个推理端点，在索引和查询时自动处理嵌入生成。这意味着你不必担心手动生成或存储矢量的问题。</p><h3>何时以及为何使用语义文本？</h3><p><code>semantic_text</code> 字段非常适合那些希望以最少的技术投入、无需手动处理矢量即可开始工作的用户。该字段可自动执行嵌入生成和矢量搜索映射等步骤，使设置更快更方便。</p><p>如果您重视<strong>简单性和抽象性</strong>，就应该考虑使用<code>semantic_text</code> ，因为它<strong>消除了手动配置映射、嵌入生成和摄取管道的复杂性</strong>。只需选择推理模型，其余的就交给 Elasticsearch 处理。</p><p>其主要优势包括在索引和查询过程中<strong>自动生成嵌入</strong>，以及<strong>可随时使用的映射</strong>，该映射经过预先配置，可支持选定的推理模型。</p><p>此外，该领域还提供<strong>对自动分割长文本（文本分块）的本地支持</strong>，可将大文本分割成较小的段落，每个段落都有自己的嵌入，从而提高搜索精度。这极大地提高了工作效率，尤其是对于那些希望在不处理语义搜索底层工程的情况下快速实现价值的团队而言。</p><p>不过，虽然<code>semantic_text</code> 提供了速度和简便性，但这种方法也有一些局限性。它允许使用市场标准模型，只要这些模型可以作为 Elasticsearch 中的推理端点。但<strong>它不支持外部生成的嵌入</strong>，而<code>dense_vector</code> 字段则可以做到这一点。</p><p>如果您需要对向量的生成方式进行更多控制，希望使用自己的嵌入，或需要将多个字段结合起来以实现高级策略，<code>dense_vector</code> 和<code>sparse_vector</code> 字段可提供更多自定义或特定领域方案所需的灵活性。</p><h3>如何使用语义文本类型查询</h3><p>在<strong><code>semantic_text</code></strong> 之前，必须根据嵌入类型（密集或稀疏）使用不同的查询。<code>sparse_vector</code> 查询用于稀疏字段，而<code>dense_vector</code> 字段则需要 KNN 查询。</p><p>使用语义文本类型时，搜索是通过<a href="https://www.elastic.co/docs/reference/query-languages/query-dsl/query-dsl-semantic-query">语义查询</a>进行的，查询会自动生成查询向量，并与索引文档的嵌入进行比较。<strong><code>semantic_text</code></strong> 类型允许您定义用于嵌入查询的推理端点，但如果未指定任何推理端点，则将对查询应用索引过程中使用的相同端点。</p><pre><code>{
  "query": {
    "semantic": {
      "field": "semantic_text_field",
      "query": "search text"
    }
  }
}</code></pre><p>要了解更多信息，我建议您阅读<a href="https://www.elastic.co/search-labs/blog/semantic-search-simplified-semantic-text"> Elasticsearch 新语义_文本映射这 篇文章 ：简化语义搜索</a> 。</p><h2>结论</h2><p>在选择如何在 Elasticsearch 中映射嵌入时，必须了解要如何生成向量以及需要对向量进行何种程度的控制。如果您追求简单，语义文本字段可实现自动和可扩展的语义搜索，使其成为许多初始用例的理想选择。当需要更多控制、微调性能或与自定义模型集成时，密集矢量和稀疏矢量场可提供必要的灵活性。</p><p>理想的字段类型取决于您的使用案例、可用基础设施以及机器学习堆栈的成熟度。最重要的是，Elastic 提供了用于构建现代化和高度适应性搜索系统的工具。</p><h2>参考资料</h2><ul><li><a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/semantic-text.html">语义文本字段类型</a></li><li><a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/sparse-vector.html">稀疏矢量场类型</a></li><li><a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/dense-vector.html">密集矢量场类型</a></li><li><a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-semantic-query.html">语义查询</a></li><li><a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-sparse-vector-query.html">稀疏向量查询</a></li><li><a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/knn-search.html">kNN 搜索</a></li><li><a href="https://www.elastic.co/search-labs/blog/semantic-search-simplified-semantic-text">Elasticsearch 新语义文本映射：简化语义搜索</a></li><li><a href="https://www.elastic.co/search-labs/blog/sparse-vector-embedding">用训练有素的 ML 模型理解稀疏向量嵌入</a></li></ul>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[从索引中排除 Elasticsearch 字段]]></title>
            <description><![CDATA[了解如何配置 Elasticsearch 以排除字段、从索引中排除字段的主要原因以及应遵循的最佳实践。]]></description>
            <link>https://www.elastic.co/search-labs/cn/blog/excluding-elasticsearch-fields-from-indexing</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/cn/blog/excluding-elasticsearch-fields-from-indexing</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <pubDate>Mon, 12 May 2025 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/3aa0b481994d2445ba979d3c79fff64c5ee6676a-720x420.jpg?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>在 Elasticsearch 中，索引是指以易于搜索的方式存储和组织数据的过程。虽然索引文档中的所有字段在某些情况下都很有用，但在某些情况下，您可能希望将某些字段排除在索引之外。这有助于提高性能，降低存储成本，并最大限度地减少 Elasticsearch 索引的总体大小。</p><p>在本文中，我们将讨论从索引中排除字段的原因、如何配置 Elasticsearch 以排除特定字段，以及在这样做时应遵循的一些最佳实践。</p><h2>将字段排除在索引之外的原因</h2><ol><li><strong>性能： </strong>索引文档中的所有字段会导致索引时间增加，搜索性能降低。通过排除搜索或聚合不需要的字段，可以提高 Elasticsearch 集群的整体性能。</li><li><strong>存储空间 </strong>索引字段会消耗存储空间。排除搜索或聚合不需要的字段有助于降低 Elasticsearch 集群的存储需求。</li><li><strong>索引大小： </strong>Elasticsearch 索引的大小与索引字段的数量直接相关。通过排除不必要的字段，可以最大限度地减少索引的大小，从而提高搜索和索引性能。</li></ol><h2>配置 Elasticsearch 以排除字段</h2><p>要在 Elasticsearch 中排除对某个字段的索引，可以在字段映射中使用 &quot;index &quot;属性。如果将 &quot;index &quot;属性设置为 &quot;false&quot;，Elasticsearch 将不会为该字段建立索引，也就无法对其进行搜索或聚合。</p><p>下面是一个如何使用 Elasticsearch 映射将字段排除在索引之外的示例：</p><pre><code>PUT /my_index
{
  "mappings": {
    "properties": {
      "field_to_exclude": {
        "type": "text",
        "index": false
      }
    }
  }
}</code></pre><p>在本例中，我们将创建一个名为 &quot;my_index &quot;的新索引，其中包含一个名为 &quot;field_to_exclude &quot;的字段。通过将 &quot;index &quot;属性设置为 &quot;false&quot;，我们告诉 Elasticsearch 不要为这个字段建立索引。但该字段仍可在源文件中使用。</p><h2>从索引中排除字段的最佳做法</h2><ol><li><strong>分析数据： </strong>在将字段排除在索引之外之前，有必要对数据进行分析，了解哪些字段对于搜索和聚合是必要的。这将有助于您做出明智的决定，排除哪些字段。</li><li><strong>测试更改： </strong>在将字段排除在索引之外时，必须对更改进行测试，以确保搜索和聚合功能仍能按预期运行。这可以帮助您避免任何意外问题或性能问题。</li><li><strong>监控性能：</strong>从索引中排除字段后，请监控 Elasticsearch 集群的性能，以确保您的更改达到了预期效果。这可以帮助您确定可能需要进行的其他优化。</li><li><strong>使用源过滤：</strong>如果需要在 Elasticsearch 中存储一个字段，但又不想让它被搜索或用于聚合，那么可以考虑使用源过滤。这样就可以将字段存储在 _source 字段中，但将其排除在索引之外。</li></ol><h2>结论</h2><p>将字段排除在 Elasticsearch 索引之外有助于提高性能、降低存储成本，并最大限度地减少索引的总体大小。通过仔细分析数据并了解哪些字段对于搜索和汇总是必要的，您就可以做出明智的决定，排除哪些字段。请始终测试您的更改并监控 Elasticsearch 集群的性能，以确保您的优化达到预期效果。</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[从 Elasticsearch 文档中删除字段]]></title>
            <description><![CDATA[了解如何使用 Update API、脚本或重新索引，从 Elasticsearch 文档中删除字段，支持单次及批量操作。]]></description>
            <link>https://www.elastic.co/search-labs/cn/blog/elasticsearch-delete-field-from-document</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/cn/blog/elasticsearch-delete-field-from-document</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <pubDate>Fri, 09 May 2025 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/89278eb7309b7f3018c61be2b514d1fd25b9564d-720x420.jpg?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>在 Elasticsearch 中，从文档中删除字段是一项常见需求。当您想从索引中删除不必要或过时的信息时，这将非常有用。在本文中，我们将讨论从 Elasticsearch 文档中删除字段的不同方法以及示例和分步说明。 </p><h2>方法 1：使用更新 API</h2><p><a href="https://www.elastic.co/docs/reference/elasticsearch/rest-apis/update-document">Update API</a> 允许您通过提供一段修改文档源数据的脚本来更新文档。您可以通过将字段值设为 null 来使用此 API 删除文档中的字段。以下是详细的操作步骤：</p><p>1.确定要更新的文档的索引、文档类型（如果使用 Elasticsearch 6.x 或更早版本）和文档 ID。</p><p>2.使用更新 API 并编写脚本，将字段设置为空，或者更好的做法是从源文档中删除该字段。下面的示例演示了如何从 &quot;my_index &quot;索引中 ID 为 &quot;1 &quot;的文档中删除 &quot;field_to_delete &quot;字段：</p><pre><code>POST /my_index/_update/1
{
  "script": "ctx._source.remove('field_to_delete')"
}</code></pre><p>3.执行请求。如果成功，Elasticsearch 将返回一个响应，表明文档已被更新。</p><p>注意：此方法只能从指定文档中删除字段。该字段仍将存在于映射和索引中的其他文档中。</p><h2>方法二：使用修改后的源数据进行重新索引</h2><p>若要从某个索引的所有文档中删除一个字段，您可以使用 <a href="https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-reindex">Reindex API</a> 来创建一个包含修改后源数据的新索引。具体操作如下：</p><p>1.创建一个新索引，其设置和映射与原始索引相同。您可以使用获取索引 API 来检索原始索引的设置和映射。</p><p>2.使用重新索引 API 将原始索引中的文档复制到新索引中，同时从源中删除字段。下面的示例演示了如何从 &quot;my_index &quot;索引中的所有文档中删除 &quot;field_to_delete &quot;字段：</p><pre><code>POST /_reindex
{
  "source": {
    "index": "my_index"
  },
  "dest": {
    "index": "new_index"
  },
  "script": {
    "source": "ctx._source.remove('field_to_delete')"
  }
}</code></pre><p><br/>3.验证新索引是否包含已删除字段的正确文件。</p><p>4.如果一切正常，就可以删除原始索引，如有必要，还可以为新索引添加一个别名，其名称与原始索引名称相同。</p><h2>方法三：更新映射并重新索引</h2><p>如果要从映射和索引中的所有文档中删除某个字段，可以更新映射，然后重新索引文档。具体方法如下</p><p>1.创建一个新索引，设置与原始索引相同。</p><p>2.使用获取映射 API 检索原始索引的映射。</p><p>3.修改映射，删除要删除的字段。</p><p>4.使用 Put Mapping API 将修改后的映射应用到新索引。</p><p>5.如方法 2 所述，使用重新索引 API 将原始索引中的文档复制到新索引中。</p><p>6.验证新索引是否包含已删除字段的正确文件，以及映射中是否不存在该字段。</p><p>7. 如果一切正常，您可以删除原始索引，并在必要时为新的索引添加一个与原始索引同名的别名。</p><h2>结论</h2><p>在本文中，我们讨论了从 Elasticsearch 文档中删除字段的三种方法：使用 Update API、使用修改后的源重新索引，以及更新映射并重新索引。每种方法都有自己的用例和权衡，因此请选择最适合您要求的方法。在将更改应用到生产环境之前，请务必记住测试更改并验证结果。</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[如何在 Elasticsearch 中连接两个索引]]></title>
            <description><![CDATA[解释如何使用术语查询、Logstash elasticsearch 过滤器、浓缩处理器和 ES|QL 来连接 Elasticsearch 中的两个索引。]]></description>
            <link>https://www.elastic.co/search-labs/cn/blog/elasticsearch-join-two-indexes</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/cn/blog/elasticsearch-join-two-indexes</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Carly Richmond]]></dc:creator>
            <pubDate>Wed, 07 May 2025 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/0d4736d10fa3e12e6233cd59993299c7bd48911b-680x450.png?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>在 Elasticsearch 中，连接两个索引不像在传统 SQL 关系数据库中那么简单。不过，使用 Elasticsearch 提供的某些技术和功能也可以实现类似的结果。</p><p>历史上，许多人使用<a href="https://www.elastic.co/cn/docs/reference/elasticsearch/mapping-reference/nested"><code>nested</code> 字段类型</a>作为将不同索引连接在一起的机制。然而，由于 Kibana 的查询成本高昂且支持不完整，特别是镜头可视化功能，该功能受到了限制。</p><p>本文将深入探讨在 Elasticsearch 中连接两个索引的过程，重点介绍以下方法： </p><ol><li>使用<code>terms</code> 查询</li><li>在摄取流水线中使用<code>enrich</code> 处理器</li><li>Logstash<code>elasticsearch</code> 过滤器插件</li><li>ES|QL <code>ENRICH</code></li><li>ES|QL <code>LOOKUP JOIN</code></li></ol><h2>使用术语查询</h2><p><a href="https://www.elastic.co/cn/docs/reference/query-languages/query-dsl/query-dsl-terms-query">术语查询</a>是 Elasticsearch 中连接两个索引的最有效方法之一。该查询用于检索在特定字段中包含一个或多个精确术语的文档。下面我们讨论如何使用它来连接两个索引。</p><p>首先，您需要从第一个索引中获取所需的数据。这可以通过简单的 GET 请求和从<code>_source</code> 属性中提取值来实现。</p><pre><code># Simple GET request
GET first_index/_search</code></pre><p>获得第一个索引的数据后，就可以用它来查询第二个索引。这是通过<code>terms</code> 查询完成的，您可以在查询中指定要匹配的字段和值。</p><p>下面就是一个例子：</p><pre><code>GET second_index/_search
{
  "query": {
    "terms": {
      "field_in_second_index": ["value1_from_first_index", "value2_from_first_index"]
    }
  }
}</code></pre><p><br/>在本例中，<code>field_in_second_index</code> 是第二个索引中要与第一个索引中的值匹配的字段。<code>value1_from_first_index</code> 和<code>value2_from_first_index</code> 是第一个索引中要在第二个索引中匹配的值。</p><p>术语查询还支持使用<a href="https://www.elastic.co/cn/docs/reference/query-languages/query-dsl/query-dsl-terms-query#query-dsl-terms-lookup">术语查找</a>技术一次性完成上述两个步骤。Elasticsearch 会以透明方式从另一个索引中检索要匹配的值。例如，如果您有一个包含球员列表的球队索引：</p><pre><code>PUT teams/_doc/team1
{
  "players":   ["john", "bill", "michael"]
}
PUT teams/_doc/team2
{
  "players":   ["aaron", "joe", "donald"]
}</code></pre><p>如下图所示，可以通过人员索引查询在 team1 队中比赛的所有人员：</p><pre><code>GET people/_search?pretty
{
  "query": {
    "terms": {
        "name" : {
            "index" : "teams",
            "id" : "team1",
            "path" : "players"
        }
    }
  }
}</code></pre><p>在上面的示例中，Elasticsearch 会以透明方式从球队索引中 id 为 team1 的文档中检索球员姓名（即&quot;john&quot;、&quot;bill &quot;和 &quot;michael&quot;），并查找人物索引中所有在姓名字段中包含这些值的文档。</p><p>对于那些好奇的人来说，等价的 SQL 查询应该是这样的：</p><pre><code>SELECT p.* FROM people p
INNER JOIN teams t ON p.name = t.players</code></pre><h2>使用浓缩处理器</h2><p><a href="https://www.elastic.co/cn/docs/reference/enrich-processor/enrich-processor"><code>enrich</code> 处理器</a>是另一个可用于连接 Elasticsearch 中两个索引的强大工具。该处理器通过添加来自预定义丰富索引的数据来丰富输入文件的数据。</p><p>下面介绍如何使用浓缩处理器连接两个索引：</p><p>1.首先，您需要创建一个浓缩策略。该策略定义了使用哪个索引来丰富输入文档、匹配哪个字段以及使用哪个字段来丰富输入文档。</p><p>下面就是一个例子：</p><pre><code>PUT _enrich/policy/my_enrich_policy
{
  "match": {
    "indices": "first_index",
    "match_field": "field_in_first_index",
    "enrich_fields": ["field_to_enrich"]
  }
}</code></pre><p>2.创建策略后，需要执行该策略，以便根据新创建的策略创建 enrich 索引：</p><pre><code>PUT _enrich/policy/my_enrich_policy/_execute</code></pre><p>这将建立一个新的隐藏浓缩索引，在浓缩过程中使用。根据源索引的大小，这一操作可能需要一些时间。在进行下一步之前，请确保充实政策已完全制定。</p><p>3.建立丰富策略后，就可以在摄取管道中使用丰富处理器来丰富传入文档的数据：</p><pre><code>PUT _ingest/pipeline/my_pipeline
{
  "processors": [
    {
      "enrich": {
        "policy_name": "my_enrich_policy",
        "field": "field_in_second_index",
        "target_field": "enriched_field"
      }
    }
  ]
}</code></pre><p>在本例中，<code>field_in_second_index</code> 是第二个索引中需要与第一个索引中的<code>match_field</code> 匹配的字段。<code>enriched_field</code> 是第二个索引中的新字段，将包含第一个索引<code>enrich_fields</code> 中的丰富数据。</p><p>这种方法的一个缺点是，如果<code>first_index</code> 中的数据发生变化，则需要重新执行浓缩策略。丰富索引不会自动更新或同步源索引。但是，如果<code>first_index</code> 相对稳定，那么这种方法就很有效。</p><h2>Logstash elasticsearch 过滤器插件</h2><p>如果使用 Logstash，另一个与上述<code>enrich</code> 处理器类似的选项是使用<code>elasticsearch</code> 过滤器插件，根据指定的查询将相关字段添加到事件中。Logstash 管道的配置位于<code>.conf</code> 文件中，如<code>my-pipeline.conf</code> 。</p><p>假设我们的管道使用<a href="https://www.elastic.co/cn/docs/reference/logstash/plugins/plugins-inputs-elasticsearch"><code>elasticsearch</code> 输入插件</a>从 Elasticsearch 中提取日志，并通过查询缩小选择范围：</p><pre><code>input {
  # Read all documents from Elasticsearch matching the given query
  elasticsearch {
    hosts => "localhost"
    query => '{ "query": { "match": { "statuscode": 200 } }, "sort": [ "_doc" ] }'
  }
}</code></pre><p>如果我们想用给定索引的信息来丰富这些信息，可以使用<code>filter</code> 部分的<a href="https://www.elastic.co/cn/docs/reference/logstash/plugins/plugins-filters-elasticsearch"><code>elasticsearch</code> 过滤器插件</a>来丰富我们的日志：</p><pre><code>filter {
   elasticsearch {
      hosts => ["localhost"]
      index => "index_name"
      query => "type:start AND operation:%{[opid]}"
      fields => { "@timestamp" => "started" }
   }
}</code></pre><p>上述代码将从索引<code>index_name</code> 中查找文件，其中<code>type</code> 为起始值，操作字段与指定的<code>opid</code> 匹配，然后将<code>@timestamp</code> 字段的值复制到名为<code>started</code> 的新字段中。</p><p>然后，丰富的文档将被发送到适当的输出源，在本例中是使用<a href="https://www.elastic.co/cn/docs/reference/logstash/plugins/plugins-outputs-elasticsearch"><code>elasticsearch</code> 输出插件</a>发送到 Elasticsearch：</p><pre><code>output {
    elasticsearch {
        hosts => "localhost"
        data_stream => "true"
    }
}</code></pre><p>如果您已经在使用 Logstash，该选项可能有助于将丰富逻辑整合到一个地方，并在新事件发生时进行处理。但是，如果您不这样做，就会增加解决方案的复杂性，而且您还需要运行和维护另一个组件。</p><h2>ES|QL ENRICH</h2><p>在 8.14 版本中引入的<a href="https://www.elastic.co/cn/docs/explore-analyze/query-filter/languages/esql">ES|QL</a> 是 Elasticsearch 支持的管道式查询语言，可用于过滤、转换和分析数据。使用 ENRICH 处理命令，我们就可以使用丰富策略从现有索引中添加数据。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/03aadddc08afffff3f6526c9c052999c97fa09dd-1600x989.png" style="width: 100%;" /><p>以原浓缩处理器示例中的相同策略<code>my_enrich_policy</code> 为例，ES|QL 示例如下：</p><pre><code>FROM first_index
| WHERE field_in_first_index IS NOT NULL
| ENRICH my_enrich_policy</code></pre><p>也可以覆盖匹配字段和丰富字段，在我们的例子中分别是<code>field_in_first_index</code> 和<code>field_to_enrich</code> ：</p><pre><code>FROM first_index
| WHERE field_in_first_index IS NOT NULL
| ENRICH my_enrich_policy ON another_field_in_first_index WITH different_field_to_enrich</code></pre><p>虽然 ES|QL 的明显限制是需要先指定丰富策略，但 ES|QL 确实提供了根据需要调整字段的灵活性。</p><h2>es|ql 查找连接</h2><p>Elasticsearch 8.18 引入了一种在 Elasticsearch 中连接索引的新方法，即<code>LOOKUP JOIN</code> 命令。该命令在连接的右侧使用新的<a href="https://www.elastic.co/cn/docs/reference/elasticsearch/index-settings/index-modules#index-mode-setting">查找索引模式</a>，以 SQL 风格的 LEFT OUTER JOIN 方式运行。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/1d73495979c4d6bb675c4c966ea86d9a72dc1c48-510x605.png" style="width: 100%;" /><p>再看我们之前的例子，新的查询如下，其中<code>match_field</code> 需要同时出现在<code>first_index</code> 和<code>second_index</code> 中：</p><pre><code>FROM first_index
| WHERE field_in_first_index IS NOT NULL
| LOOKUP JOIN second_index ON match_field</code></pre><p>与其他方法相比，LOOKUP JOIN 的优势在于它不需要任何<code>enrich</code> 策略，因此也不需要与设置策略相关的额外处理。与本文讨论的其他方法不同，它在处理经常变化的丰富数据时非常有用。</p><h2>结论</h2><p>总之，虽然 Elasticsearch 不支持传统的连接操作，但它提供了各种功能，可用于实现类似的结果。具体来说，我们介绍了如何使用连接操作：</p><ol><li><code>terms</code> 查询</li><li><code>enrich</code> 摄录流水线中的处理器</li><li>Logstash<code>elasticsearch</code> 过滤器插件</li><li>ES|QL <code>ENRICH</code></li><li>ES|QL <code>LOOKUP JOIN</code></li></ol><p>需要注意的是，这些方法都有其局限性，应根据具体要求和数据性质谨慎使用。</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[了解 Elasticsearch 评分和解释 API]]></title>
            <description><![CDATA[了解 Elasticsearch 的评分机制与实用评分函数，借助 Explain API 检查搜索相关性并提升文档排名效果。]]></description>
            <link>https://www.elastic.co/search-labs/cn/blog/elasticsearch-scoring-and-explain-api</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/cn/blog/elasticsearch-scoring-and-explain-api</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <pubDate>Mon, 05 May 2025 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/a70c5403064d5bbceff66a17373332362227f13c-720x420.jpg?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>Elasticsearch 是一个功能强大的搜索引擎，通过计算索引中每个文档的得分，提供快速、相关的搜索结果。这个分数是决定搜索结果排序的关键因素。在本文中，我们将深入探讨 Elasticsearch 的评分机制，并探索 Explain API，这有助于理解评分过程。</p><h2>Elasticsearch 中的评分机制</h2><p>Elasticsearch 默认使用一种名为实用评分函数 (BM25) 的评分模型。该模型以概率信息检索理论为基础，考虑了术语频率、反向文档频率和字段长度规范化等因素。让我们简要讨论一下这些因素：</p><ol><li><strong>术语频率 (TF)：</strong>它表示术语在文档中出现的次数。术语频率越高，说明术语与文档之间的关系越密切。</li><li><strong>反向文档频率 (IDF)：</strong>该因子用于衡量术语在整个文档集中的重要性。出现在许多文件中的术语被认为不太重要，而出现在较少文件中的术语则被认为更重要。</li><li><strong>字段长度归一化</strong>：该因子考虑了术语所在字段的长度。较短字段的权重更大，因为在较短字段中，术语被认为更重要。</li></ol><h2>使用解释 API</h2><p>Elasticsearch 中的解释 API 是了解评分过程的重要工具。它详细解释了如何计算特定文件的得分。要使用解释 API，您需要向以下端点发送 GET 请求：</p><pre><code>GET /<index>/_explain/<document_id></code></pre><p>在请求正文中，您需要提供想要了解评分的查询。这里有一个例子：</p><pre><code>{
  "query": {
    "match": {
      "title": "elasticsearch"
    }
  }
}</code></pre><p>解释 API 的回复将包括评分过程的详细分类，包括各个因素（TF、IDF 和字段长度正常化）及其对最终得分的贡献。下面是一个答复样本：</p><pre><code>{
  "_index": "example_index",
  "_type": "_doc",
  "_id": "1",
  "matched": true,
  "explanation": {
    "value": 1.2,
    "description": "weight(title:elasticsearch in 0) [PerFieldSimilarity], result of:",
    "details": [
      {
        "value": 1.2,
        "description": "score(doc=0,freq=1.0 = termFreq=1.0\n), product of:",
        "details": [
          {
            "value": 2.2,
            "description": "idf, computed as log(1 + (docCount - docFreq + 0.5) / (docFreq + 0.5)) from:",
            "details": [
              {
                "value": 1,
                "description": "docFreq",
                "details": []
              },
              {
                "value": 1,
                "description": "docCount",
                "details": []
              }
            ]
          },
          {
            "value": 0.5,
            "description": "tfNorm, computed as (freq * (k1 + 1)) / (freq + k1 * (1 - b + b * fieldLength / avgFieldLength)) from:",
            "details": [
              {
                "value": 1,
                "description": "termFreq=1.0",
                "details": []
              },
              {
                "value": 1.2,
                "description": "parameter k1",
                "details": []
              },
              {
                "value": 0.75,
                "description": "parameter b",
                "details": []
              },
              {
                "value": 1,
                "description": "avgFieldLength",
                "details": []
              },
              {
                "value": 1,
                "description": "fieldLength",
                "details": []
              }
            ]
          }
        ]
      }
    ]
  }
}</code></pre><p>在本例中，回复显示 1.2 分是 IDF 值（2.2）和 tfNorm 值（0.5）的乘积。详细的解释有助于了解评分因素，并有助于微调搜索相关性。</p><h2>结论</h2><p>Elasticsearch 评分是提供相关搜索结果的一个重要方面。通过了解评分机制和使用解释 API，您可以深入了解影响搜索结果的因素，并优化搜索查询以提高相关性和性能。</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Elasticsearch 中的索引模板：如何使用可组合模板]]></title>
            <description><![CDATA[探索如何在 Elasticsearch 中创建可组合索引模板与组件索引模板，以确保映射一致性并实现索引配置的自动化。]]></description>
            <link>https://www.elastic.co/search-labs/cn/blog/index-composable-templates</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/cn/blog/index-composable-templates</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <pubDate>Fri, 02 May 2025 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/510750708df50bf79463586a1bbf35bf94acfa30-1200x628.png?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>Elasticsearch 索引可通过映射、设置和别名进行配置： </p><ul><li>映射定义指定了数据模式。</li><li>设置将设定分区大小和刷新率。 </li><li>别名用于为索引提供备用名称。</li></ul><p>当我们首次为文档创建索引或使用创建索引 API 创建空索引时，将使用默认设置创建索引，不包含数据模式和别名。这些默认设置在开发和测试环境中非常有效，但我们可能需要为生产环境定制索引。</p><p>在生产中使用默认映射和设置可能会导致索引和搜索性能低下。 手动实例化索引是一个繁琐耗时的过程。如果我们有一个复杂的映射模式以及自定义设置和别名，在每个环境中重新创建这样的索引尤其不切实际。</p><p>幸运的是，Elasticsearch 提供了一种工具，可以在创建索引时以<em>索引</em> <em>模板</em>的形式自动应用预定义配置。</p><h2>索引模板</h2><p>通过索引模板，我们可以创建用户自定义配置的索引。索引在实例化过程中可以从这些模板中提取配置，例如设定分片和副本的数量或字段映射。模板中将定义一个名称模式和一些配置。如果索引名称与模板的命名模式相匹配，新索引将按照模板中定义的配置创建。</p><p>Elasticsearch 在 7.8 版本中升级了模板功能，增加了可组合模板。如本文所示，新版本提供了更多可重复使用的索引模板。</p><h3>索引模板的类型</h3><p>索引模板可分为两类：</p><ul><li><strong>索引模板（或可组合索引模板）</strong>：可组合索引模板既可以单独存在，也可以由一个或多个组件模板组成（见第二类）。</li><li><strong>组件模板：</strong>组件模板本身就是一个<em>可重复使用的</em>模板，它定义了所需的配置。通常，组件模板应与索引模板相关联。每个组件模板都可以附加一个或多个索引模板。 </li></ul><p>如下图所示，索引模板 A 和 B 之间共享组件模板（本例中只有一个--模板 3）。索引模板可由一个或多个组件模板组成，每个组件模板可与一个或多个索引模板相关联。这两种模板都可以单独存在，但组件模板除非附加到索引模板上，否则没有任何用处。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/96c0aac29d3992e54a79be34e14cf909e0ca2ea9-1202x556.png" style="width: 100%;" /><p>总体思路是开发一个组件模板目录，供企业用于满足各种需求（例如，为个别环境指定各种组件模板），并通过可组合索引模板将其附加到各种索引。</p><h2>如何创建可组合（索引）模板</h2><p>Elasticsearch 提供了一个 _index_template 端点，用于管理索引模板。用户在此模板中提供所有必需的映射、设置和别名以及索引名称模式。让我们以创建微服务应用程序<em>customer-order-service</em>的模板为例，该应用程序负责订单生成逻辑。 </p><p>比方说，我们的需求是为客户订单创建一个模板，该模板使用带有通配符的模式表示：*orders.该模板应具有某些映射和设置，如订单日期字段以及分片和副本编号。</p><p>任何在创建过程中与此模板匹配的索引都会继承此模板中定义的配置。例如，black_friday_orders 索引将包含 order_date 字段，分片将设置为 5，副本设置为 2。 除此之外，从该模板创建的<em>所有</em>索引也将继承一个<a href="https://opster.com/guides/elasticsearch/glossary/elasticsearch-alias/">别名</a>！让我们创建一个订单模板（orders_template），其索引模式定义为 *orders，映射模式由一个日期格式为 dd-MM-yyyy 的 oder_date 字段组成。下面的代码显示了如何创建该索引模板。</p><pre><code>PUT _index_template/orders_template
{
  "index_patterns": ["*orders"],
  "priority": 300,
  "template": {
    "mappings": {
      "properties": {
        "order_date": {
          "type": "date",
          "format":"dd-MM-yyyy"
        }
      }
    },
    "settings":{
      "number_of_shards":5,
      "number_of_replicas":2
    },
    "aliases":{
      "all_orders":{}
    }
  }
}</code></pre><p>在 Kibana 的 DevTools 中执行此查询时，会创建包含索引模式 *orders 以及预定义映射、设置和别名的模板。index_patterns 是一个匹配模式数组；任何与该模式匹配的索引都将导出模板配置。您可以执行以下操作来获取持久化模板，该模板应重申我们的操作：</p><pre><code>GET _index_template/orders_template </code></pre><p>在创建模板属性时还会定义一个优先级（正数）：每个模板都有一个优先级，这样不同模板的任何冲突更改都会通过使用该值来解决，优先级较高的值优先。下面我们将更深入地探讨模板的优先级。</p><h2>使用模板创建索引</h2><p>现在我们有了一个模板--创建索引的蓝图--下一步就是创建索引。当索引名称与给定模式匹配时，将自动应用模板化配置。为了证明这一点，正如下面的代码所示，让我们创建一个名为：blackfriday_orders 的全新索引：</p><pre><code>PUT blackfriday_orders</code></pre><p>由于索引名称（blackfriday_orders）与模板中定义的命名模式（即*orders），该索引应获取从模板派生的所有配置。让我们通过执行以下代码来检索这个新创建的索引，并检查其是否属实：</p><pre><code>GET blackfriday_orders</code></pre><p>应该返回</p><pre><code>{
  "blackfriday_orders" : {
    "aliases" : {
      "all_orders" : { }
    },
    "mappings" : {
      "properties" : {
        "order_date" : {
          "type" : "date",
          "format" : "dd-MM-yyyy"
        }
      }
    },
    "settings" : {
      "index" : {
         ...
        "number_of_shards" : "5",
        "number_of_replicas" : "2"
      }
    }
  }
}</code></pre><p>如回复所示，blackfriday_orders 的配置已从模板继承。我们可以尝试各种指数组合，以成功继承模板配置：</p><pre><code>PUT blackfriday_orders
PUT americaorders
PUT cancelled--orders
PUT undefined101orders</code></pre><p>但是，下列索引将不会继承配置，因为名称与模式不匹配：</p><pre><code>PUT blackfriday_orders2
PUT open_orders_
PUT allorders_total</code></pre><p>需要记住的重要一点是，在本例中，从模板派生的所有索引都有相同的别名--all_orders。使用这种别名有一个好处--我们可以简单地查询这个单一的别名，而不是多个索引。</p><pre><code>GET blackfriday_orders,americaorders,undefined101orders/_search
GET all_orders/_search 
{
  "query": {
    "range": {
      "order_date": {
        "gte": "01-12-2021",
        "lte": "31-12-2021"
      }
    }
  }
}</code></pre><p>当我们为 *orders 创建模板时，任何匹配的索引都会采用模板配置。通常，出于各种原因，团队会有意无意地多创建几个模板。这意味着有时索引名称可能会匹配两个不同的模板模式！Elasticsearch 必须决定需要应用这些模板中的哪些配置。幸运的是，使用模板优先权可以解决这一难题。</p><h2>如何创建组件模板</h2><p>我们在本文前半部分了解了索引模板。使用内置配置创建模板有几个缺点，其中一个缺点是不能将配置导出到其他模板中。如果我们希望有类似的配置，比如与客户相关的模板（*客户），我们可能需要重新创建整个模板。这意味着，在一个典型的组织中，我们可能会创建几十个这样的系统（此外，根据环境的不同，您可能还会创建更多的系统）。</p><p>我们一直期待着可重用性，因此 Elasticsearch 在重新设计模板时考虑到了可重用性。组件模板正好符合这一要求。如果你是 DevOps 背景，很可能需要为每个环境创建预设配置的索引。与其繁琐地手动应用这些配置，不如为每个环境创建一个组件模板。</p><p>组件模板只不过是一个可重复使用的配置块，我们可以用它来组成更多的索引模板。请注意，组件模板除非与索引模板结合在一起，否则没有任何价值。它们通过 _component_template 端点公开。让我们看看这一切是如何组合在一起的。</p><h3>索引模板中的设置</h3><p>让我们提取之前在索引模板中定义的设置，并从中创建一个组件模板。settings_component_template 预计有五个主分区，每个主分区有两个副本。正如下面的代码列表所示，第一步是用此配置声明并执行一个组件模板。</p><pre><code>PUT _component_template/settings_component_template
{
  "template":{
    "settings":{
      "number_of_shards":5,
      "number_of_replicas":2
    }
  }
}</code></pre><p>如上面的代码所示，我们使用 _component_template 端点创建组件模板。请求正文在模板对象中保存模板信息。settings_component_template 现在可在索引模板的其他地方使用。一个显著的区别是，这个模板没有定义任何索引模式；它只是一个为我们配置一些属性的代码块。</p><h3>映射模板</h3><p>同样，让我们创建另一个模板。这次，让我们提取之前在独立索引模板中定义的映射模式。下面的代码显示了脚本：</p><pre><code>PUT _component_template/mappings_component_template
{
  "template": {
    "mappings": {
      "properties": {
        "order_date": {
          "type": "date",
          "format":"dd-MM-yyyy"
        }
      }
    }
  }
}</code></pre><h3>别名模板</h3><p>按照同样的流程，我们还可以在组件模板中加入别名--两个别名（all_orders 和 sales_orders）：</p><pre><code>PUT _component_template/aliases_component_template
{
  "template": {
    "aliases": {
      "all_orders": {},
      "sales_orders":{}
    }
  }
}</code></pre><h3>可组合索引模板</h3><p>现在我们有了这三个组件模板，下一步就是将它们投入使用。我们可以让索引模板使用它，比如 christmas_orders：</p><pre><code>PUT _index_template/composed_orders_template
{
  "index_patterns": [
    "*orders"
  ],
  "priority": 500,
  "composed_of": [
    "settings_component_template",
    "mappings_component_template",
    "aliases_component_template"
  ]
}</code></pre><p>composed_of 标签是组成该模板的所有组件模板的集合。在这种情况下，我们要选择设置、映射和别名组件模板。我们还将提高优先级，使该模板优先于其他模板。模板就绪后，任何与 *orders 模式匹配的索引都将继承这三个组件模板的配置。</p><p>尽管如此，如果我们希望创建一个新模板，例如客户模板，只使用一个现有模板（settings_component_template）和一个新创建的别名模板（aliases_component_template，见下文），我们可以通过以下方法实现：</p><pre><code>PUT _component_template/aliases_component_template2
{
  "template": {
    "aliases": {
      "all_customers": {}
    }
  }
}</code></pre><p>索引模板是这样的</p><pre><code>PUT _index_template/composed_customers_template
{
  "index_patterns": [
    "*customers*"
  ],
  "priority": 200,
  "composed_of": [
    "settings_component_template",
    "aliases_component_template2"
  ]
}</code></pre><p>您是否发现 settings_component_template 被（重新）用于两个不同的模板？这就是组件模板的力量。</p><h2>索引模板的优先级</h2><p>开发人员有可能在没有查看现有库存的情况下创建多个索引模板。重要的是要为每个模板设置优先级，以便使用优先级较高的模板。例如，在下面的代码片段中，my_orders_template_1 会覆盖 my_orders_template_2：</p><pre><code>PUT _index_template/my_orders_template_1
{
  "index_patterns": ["*orders"],
  "priority": 1000,
  "template": { ... }
}
PUT _index_template/my_orders_template2
{
  "index_patterns": ["*orders"],
  "priority": 300,
  "template": { ... }
}</code></pre><p>如果有多个模板与正在创建的索引相匹配，Elasticsearch 会应用所有匹配模板中的所有配置，但会覆盖优先级较高的配置。</p><h2>模板优先</h2><p>最后，您可能想知道模板的优先级--组件模板中定义的配置是否优先于主索引模板本身定义的配置？还是相反？嗯，是有一些规则的：</p><ul><li>使用显式配置创建的索引优先于所有配置--这意味着如果使用显式配置创建索引，不要指望它们会被模板覆盖。</li><li>传统模板（7.8 版之前创建的模板）的优先级低于可组合模板。</li></ul><h2>总结</h2><ul><li>索引包含映射、设置和别名：映射定义字段模式，设置设置索引参数（如分片数和副本数），别名为索引提供备用名称。</li><li>通过模板，我们可以创建具有预定义配置的索引。使用与特定模板中定义的索引模式相匹配的名称命名索引，将自动根据模板配置该索引。</li><li>Elasticsearch 在 7.8 版中引入了可组合索引模板。可组合的索引模板可实现模板的模块化和版本化。</li><li>可组合模板由一个或多个组件模板组成。</li><li>索引模板也可以定义自己的配置。</li><li>组件模板是一种可重复使用的模板，具有预定义的配置，就像可组合索引模板一样。</li><li>但是，组件模板应该是索引模板的一部分；如果没有 &quot;组成 &quot;索引模板，组件模板就毫无用处。</li><li>组件模板中没有定义索引模式，这也是它们 &quot;有望 &quot;成为索引模板一部分的另一个原因。</li><li>每个模板都有一个优先级--一个正数。数字越大，应用该模板的优先级就越高。</li></ul>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[通过两个字段进行 Elasticsearch 搜索]]></title>
            <description><![CDATA[探索在两个字段中搜索的技巧，包括 multi_match 查询、bool 查询，以及查询时字段加权。]]></description>
            <link>https://www.elastic.co/search-labs/cn/blog/elasticsearch-search-by-two-fields</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/cn/blog/elasticsearch-search-by-two-fields</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <pubDate>Wed, 30 Apr 2025 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/d5d04bbcfc3925f48f3487ea4c7e0dd2205316d0-720x420.jpg?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>在 Elasticsearch 中跨多个字段搜索是许多应用程序的共同要求。本文将探讨通过两个字段执行搜索的高级技术，包括多重匹配查询、布尔查询和查询时字段提升。这些技术将帮助您为用户创建更准确、更相关的搜索结果。</p><h2>通过两个字段执行搜索的高级技术</h2><h3>1.多匹配查询</h3><p>多匹配查询允许您在多个字段中搜索单个查询字符串。当您要查找两个字段中任何一个包含给定查询字符串的文档时，这种方法非常有用。下面是一个多重匹配查询的示例，在 &quot;标题 &quot;或 &quot;描述 &quot;字段中搜索术语 &quot;示例&quot;：</p><pre><code>{
  "query": {
    "multi_match": {
      "query": "example",
      "fields": ["title", "description"]
    }
  }
}</code></pre><h3>2.布尔查询</h3><p>bool 查询允许您使用布尔逻辑组合多个查询。您可以使用 &quot;should &quot;子句搜索两个字段中任何一个与查询匹配的文档。下面是一个在 &quot;标题 &quot;和 &quot;描述 &quot;字段中搜索术语 &quot;示例 &quot;的 bool 查询示例：</p><pre><code>{
  "query": {
    "bool": {
      "should": [
        {"match": {"title": "example"}},
        {"match": {"description": "example"}}
      ]
    }
  }
}</code></pre><h3>3.查询时间字段增强</h3><p>有时，您可能希望在搜索过程中更重视一个字段而不是另一个字段。您可以在查询时对字段应用提升因子来实现这一点。提升值越高，该字段的权重就越大，从而更有可能影响最终搜索得分。下面是一个多匹配查询的示例，在 &quot;标题 &quot;字段中应用了提升因子：</p><pre><code>{
  "query": {
    "multi_match": {
      "query": "example",
      "fields": ["title^3", "description"]
    }
  }
}</code></pre><p>在这个例子中，&quot;标题 &quot;字段的提升系数为 3，因此在决定搜索得分时，它比 &quot;描述 &quot;字段重要三倍。</p><h3>4.组合使用不同提升因子的查询</h3><p>您还可以使用 bool 查询将具有不同提升因子的多个查询组合起来。这样，您就可以微调搜索结果中每个字段的重要性。下面是一个 bool 查询的示例，&quot;标题 &quot;和 &quot;描述 &quot;字段应用了不同的提升因子：</p><pre><code>{
  "query": {
    "bool": {
      "should": [
        {"match": {"title": {"query": "example", "boost": 3}}},
        {"match": {"description": {"query": "example", "boost": 1}}}
      ]
    }
  }
}</code></pre><p>在这个例子中，&quot;标题 &quot;字段的提升因子为 3，而 &quot;描述 &quot;字段的提升因子为 1。</p><h2>结论</h2><p>在 Elasticsearch 中，可以使用多匹配查询、布尔查询和查询时字段增强等高级技术实现两个字段的搜索。通过结合这些技术，您可以为用户创建更准确、更相关的搜索结果。尝试使用不同的查询组合和提升因素，为您的特定使用情况找到最佳搜索配置。</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[使用 LangGraph 和 Elasticsearch 构建 RAG 工作流程]]></title>
            <description><![CDATA[了解如何使用 Elasticsearch 配置和定制 LangGraph 检索代理模板，以构建 RAG 工作流，从而实现高效的数据检索和人工智能驱动的响应。]]></description>
            <link>https://www.elastic.co/search-labs/cn/blog/build-rag-workflow-langgraph-elasticsearch</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/cn/blog/build-rag-workflow-langgraph-elasticsearch</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <pubDate>Fri, 25 Apr 2025 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/17b7e7b336f73e232375d1add582ae5f6c52a279-1440x840.png?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p><a href="https://github.com/langchain-ai/retrieval-agent-template">LangGraph 检索代理模板</a>是 LangChain 开发的一个启动项目，目的是方便在 LangGraph Studio 中使用 LangGraph 创建基于检索的问题解答系统。该模板经过预配置，可与 Elasticsearch 无缝集成，使开发人员能够快速构建可高效索引和检索文档的代理。</p><p>本博客主要介绍如何使用 LangGraph Studio 和 LangGraph CLI 运行和定制 LangChain 检索代理模板。该模板为利用 Elasticsearch 等各种检索后端构建检索增强生成 (RAG) 应用程序提供了一个框架。</p><p>我们将指导您设置、配置环境，并使用 Elastic 高效执行模板，同时定制代理流程。</p><h2>准备工作</h2><p>在继续之前，请确保已安装以下设备：</p><ul><li>Elasticsearch 云部署或内部部署（或在 Elastic Cloud 上创建 14 天<a href="https://www.elastic.co/cn/cloud/cloud-trial-overview">免费试用版 </a>）- 版本 8.0.0 或更高</li><li>Python 3.9+</li><li>访问 LLM 提供商，如<a href="https://cohere.com/">Cohere</a>（本指南中使用）、<a href="https://openai.com/">OpenAI</a> 或<a href="https://www.anthropic.com/claude">Anthropic/Claude</a></li></ul><h2>创建 LangGraph 应用程序</h2><h3>1.安装 LangGraph CLI</h3><pre><code>pip install --upgrade "langgraph-cli[inmem]"</code></pre><h3>2.根据检索代理模板创建 LangGraph 应用程序</h3><pre><code>mkdir lg-agent-demo
cd lg-agent-demo
langgraph new lg-agent-demo </code></pre><p><em>您将看到一个交互式菜单，可以从可用模板列表中进行选择。 </em>为检索代理选择 4，为 Python 选择 1，如下图所示：</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/6a41a41f95c2477c67810adc7be46d91faf06878-1600x407.png" style="width: 100%;" /><ul><li><strong>故障排除</strong>：如果遇到以下错误：&quot;urllib.error.URLError:&lt;urlopen error [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:1000)&gt;&quot;</li></ul><p>请运行 Python 的安装证书命令来解决问题，如下所示。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/83ec238136c41738457299fd42c83aff32eb5b97-1407x75.png" style="width: 100%;" /><h3>3.安装依赖项</h3><p>在新 LangGraph 应用程序的根目录下创建虚拟环境，并以<code>edit</code> 模式安装依赖项，这样服务器就会使用本地更改：</p><pre><code>#For Mac
python3 -m venv lg-demo
source lg-demo/bin/activate 
pip install -e .

#For Windows
python3 -m venv lg-demo
lg-demo\Scripts\activate 
pip install -e .</code></pre><h2>设置环境</h2><h3>1.创建 .环境文件</h3><p><code>.env</code> 文件包含 API 密钥和配置，因此应用程序可以连接到您选择的 LLM 和检索提供商。复制示例配置，生成新的<code>.env</code> 文件：</p><pre><code>cp .env.example .env</code></pre><h3>2.配置 .env文件</h3><p><code>.env</code> 文件带有一组默认配置。您可以根据设置添加必要的 API 密钥和值来更新它。任何与使用案例无关的键都可以保持不变或删除。</p><pre><code># To separate your traces from other applications
LANGSMITH_PROJECT=retrieval-agent

# LLM choice (set the API key for your selected provider):
ANTHROPIC_API_KEY=your_anthropic_api_key
FIREWORKS_API_KEY=your_fireworks_api_key
OPENAI_API_KEY=your_openai_api_key

# Retrieval provider (configure based on your chosen service):

## Elastic Cloud:
ELASTICSEARCH_URL=https://your_elastic_cloud_url
ELASTICSEARCH_API_KEY=your_elastic_api_key

## Elastic Local:
ELASTICSEARCH_URL=http://host.docker.internal:9200
ELASTICSEARCH_USER=elastic
ELASTICSEARCH_PASSWORD=changeme

## Pinecone:
PINECONE_API_KEY=your_pinecone_api_key
PINECONE_INDEX_NAME=your_pinecone_index_name

## MongoDB Atlas:
MONGODB_URI=your_mongodb_connection_string

# Cohere API key:
COHERE_API_KEY=your_cohere_api_key</code></pre><ul><li><code>.env</code> 文件示例（使用弹性云和 Cohere）</li></ul><p>下面是<code>.env</code> 配置示例，用于将<strong>Elastic Cloud</strong>用作检索提供商，将<strong>Cohere</strong>用作 LLM，本博客对此进行了演示：</p><pre><code># To separate your traces from other applications
LANGSMITH_PROJECT=retrieval-agent
#Retrieval Provider
# Elasticsearch configuration
ELASTICSEARCH_URL=elastic-url:443
ELASTICSEARCH_API_KEY=elastic_api_key
# Cohere API key
COHERE_API_KEY=cohere_api_key</code></pre><p><em>注：本指南使用 Cohere 进行响应生成和嵌入，您也可以 </em>根据自己的使用情况使用 其他 LLM 提供商，如 <em><strong>OpenAI</strong>、 <strong>Claude</strong>，甚至本地 LLM 模型。请确保您打算使用的每个密钥都已存在，并在 <code>.env</code> 文件</em>中正确设置&nbsp; 。</p><h3>3.更新配置文件 -configuration.py </h3><p>使用适当的 API 密钥设置<code>.env</code> 文件后，下一步就是更新应用程序的默认模型配置。更新配置可确保系统使用您在<code>.env</code> 文件中指定的服务和模型。</p><p>导航至配置文件：</p><pre><code> cd src/retrieval_graph</code></pre><p><code>configuration.py</code> 文件包含检索代理用于三项主要任务的默认模型设置：</p><ul><li><strong>嵌入模型</strong>--将文件转换为矢量表示</li><li><strong>查询模型</strong>- 将用户的查询转化为矢量</li><li><strong>响应模型</strong>- 生成最终响应</li></ul><p>默认情况下，代码使用<strong>OpenAI</strong>（如<code>openai/text-embedding-3-small</code> ）和<strong>Anthropic</strong>（如<code>anthropic/claude-3-5-sonnet-20240620 and anthropic/claude-3-haiku-20240307</code> ）的模型。在本博客中，我们将改用 Cohere 模型。如果您已经在使用 OpenAI 或 Anthropic，则无需更改。</p><h4>更改示例（使用 Cohere）：</h4><p>打开<code>configuration.py</code> 并修改模型默认值，如下图所示：</p><pre><code>…
 embedding_model: Annotated[
       str,
       {"__template_metadata__": {"kind": "embeddings"}},
   ] = field(
       default="cohere/embed-english-v3.0",
…
response_model: Annotated[str, {"__template_metadata__": {"kind": "llm"}}] = field(
       default="cohere/command-r-08-2024",
…
query_model: Annotated[str, {"__template_metadata__": {"kind": "llm"}}] = field(
       default="cohere/command-r-08-2024",
       metadata={</code></pre><h2>使用 LangGraph CLI 运行检索代理</h2><h3>1.启动 LangGraph 服务器</h3><pre><code>cd lg-agent-demo
langgraph dev</code></pre><p>这将在本地启动 LangGraph API 服务器。如果运行成功，你应该会看到类似的内容：</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/e3c3344b24651067e2d0892d870feca505b3be35-1494x542.png" style="width: 100%;" /><p>开放工作室用户界面 URL。</p><p>有两种图表可供选择：</p><ul><li><strong>检索图</strong>：从 Elasticsearch 中检索数据，并使用 LLM 响应查询。</li><li><strong>索引图</strong>：将文档索引到 Elasticsearch，并使用 LLM 生成嵌入。</li></ul><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/99b579ca44b2d13f61da7e1de5886ea5b9a5e16f-1600x1009.png" style="max-width:100%; margin-bottom:1em;" /><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/f54d182d2789b6a4d113b0c88063531ea0b6de5d-1600x1009.png" style="max-width:100%; margin-bottom:1em;" /><h3>2.配置索引图</h3><ul><li>打开索引图。</li><li>单击管理助手。<ul><li>点击<strong>&quot;添加新助手</strong>&quot;，输入指定的用户详细信息，然后关闭窗口。</li></ul></li></ul><pre><code>{"user_id": "101"}</code></pre><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/6f87958e40760ac9671639f4995962bcea090991-1600x1139.png" style="max-width:100%; margin-bottom:1em;" /><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/8f802f6b65b3f06d7d1a521c3f40385c6006413b-1422x1212.png" style="max-width:100%; margin-bottom:1em;" /><h3>3.为样本文件编制索引</h3><ul><li>为以下样本文件编制索引，这些文件是消费与工业专用技术公司的假设季度报告：</li></ul><pre><code>[
  {    "page_content": "NoveTech Solutions Q1 2025 Report - Revenue: $120.5M, Net Profit: $18.2M, EPS: $2.15. Strong AI software launch and $50M government contract secured."
  },
  {
    "page_content": "NoveTech Solutions Business Highlights - AI-driven analytics software gained 15% market share. Expansion into Southeast Asia with two new offices. Cloud security contract secured."
  },
  {
    "page_content": "NoveTech Solutions Financial Overview - Operating expenses at $85.3M, Gross Margin 29.3%. Stock price rose from $72.5 to $78.3. Market Cap reached $5.2B."
  },
  {
    "page_content": "NoveTech Solutions Challenges - Rising supply chain costs impacting hardware production. Regulatory delays slowing European expansion. Competitive pressure in cybersecurity sector."
  },
  {
    "page_content": "NoveTech Solutions Future Outlook - Expected revenue for Q2 2025: $135M. New AI chatbot and blockchain security platform launch planned. Expansion into Latin America."
  },
  {
    "page_content": "NoveTech Solutions Market Performance - Year-over-Year growth at 12.7%. Stock price increase reflects investor confidence. Cybersecurity and AI sectors remain competitive."
  },
  {
    "page_content": "NoveTech Solutions Strategic Moves - Investing in R&D to enhance AI-driven automation. Strengthening partnerships with enterprise cloud providers. Focusing on data privacy solutions."
  },
  {
    "page_content": "NoveTech Solutions CEO Statement - 'NoveTech Solutions continues to innovate in AI and cybersecurity. Our growth strategy remains strong, and we foresee steady expansion in the coming quarters.'"
  }
]</code></pre><p>文件索引完成后，你会在线程中看到一条删除信息，如下图所示。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/fd3a1efd64cb54d54ea56ef5055249dd066d5708-1600x854.png" style="width: 100%;" /><h3>4.运行检索图</h3><ul><li>切换到检索图。</li><li>输入以下搜索查询：</li></ul><pre><code>What was NovaTech Solutions total revenue in Q1 2025?</code></pre><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/eb4d8ddfe0effd7e1868fba921b8ef13f7baf27a-1600x755.png" style="width: 100%;" /><p>系统将返回相关文件，并根据索引数据提供准确答案。</p><h2>自定义检索代理</h2><p>为了提升用户体验，我们在检索图中引入了一个定制步骤，以预测用户可能提出的下三个问题。这一预测的依据是</p><ul><li>从检索到的文件中获取上下文</li><li>以前的用户互动</li><li>最后一次用户查询</li></ul><p>实施查询预测功能需要更改以下代码：</p><h3>1.更新 graph.py</h3><ul><li>添加<code>predict_query</code> 功能：</li></ul><pre><code>async def predict_query(
   state: State, *, config: RunnableConfig
) -> dict[str, list[BaseMessage]]:
   logger.info(f"predict_query predict_querypredict_query predict_query predict_query predict_query")  # Log the query

   configuration = Configuration.from_runnable_config(config)
   prompt = ChatPromptTemplate.from_messages(
       [
           ("system", configuration.predict_next_question_prompt),
           ("placeholder", "{messages}"),
       ]
   )
   model = load_chat_model(configuration.response_model)
   user_query = state.queries[-1] if state.queries else "No prior query available"
   logger.info(f"user_query: {user_query}")
   logger.info(f"statemessage: {state.messages}")
   #human_messages = [msg for msg in state.message if isinstance(msg, HumanMessage)]

   message_value = await prompt.ainvoke(
       {
           "messages": state.messages,
           "user_query": user_query,  # Use the most recent query as primary input
           "system_time": datetime.now(tz=timezone.utc).isoformat(),
       },
       config,
   )

   next_question = await model.ainvoke(message_value, config)
   return {"next_question": [next_question]}</code></pre><ul><li>修改<code>respond</code> 函数，以返回<strong><code>response</code></strong> 对象，而不是消息：</li></ul><pre><code>async def respond(
   state: State, *, config: RunnableConfig
) -> dict[str, list[BaseMessage]]:
   """Call the LLM powering our "agent"."""
   configuration = Configuration.from_runnable_config(config)
   # Feel free to customize the prompt, model, and other logic!
   prompt = ChatPromptTemplate.from_messages(
       [
           ("system", configuration.response_system_prompt),
           ("placeholder", "{messages}"),
       ]
   )
   model = load_chat_model(configuration.response_model)

   retrieved_docs = format_docs(state.retrieved_docs)
   message_value = await prompt.ainvoke(
       {
           "messages": state.messages,
           "retrieved_docs": retrieved_docs,
           "system_time": datetime.now(tz=timezone.utc).isoformat(),
       },
       config,
   )
   response = await model.ainvoke(message_value, config)
   # We return a list, because this will get added to the existing list
   return {"response": [response]}</code></pre><ul><li>更新图结构，为 predict_query 添加新节点和边：</li></ul><pre><code>builder.add_node(generate_query)
builder.add_node(retrieve)
builder.add_node(respond)
builder.add_node(predict_query)
builder.add_edge("__start__", "generate_query")
builder.add_edge("generate_query", "retrieve")
builder.add_edge("retrieve", "respond")
builder.add_edge("respond", "predict_query")</code></pre><h3>2.更新 prompts.py</h3><ul><li>在<code>prompts.py</code> 中进行guery预测的工艺提示：</li></ul><pre><code>PREDICT_NEXT_QUESTION_PROMPT = """Given the user query and the retrieved documents, suggest the most likely next question the user might ask.

**Context:**
- Previous Queries:
{previous_queries}

- Latest User Query: {user_query}

- Retrieved Documents:
{retrieved_docs}

**Guidelines:**
1. Do not suggest a question that has already been asked in previous queries.
2. Consider the retrieved documents when predicting the next logical question.
3. If the user's query is already fully answered, suggest a relevant follow-up question.
4. Keep the suggested question natural and conversational.
5. Suggest at least 3 question

System time: {system_time}"""</code></pre><h3>3.更新 configuration.py</h3><ul><li>添加<code>predict_next_question_prompt</code> ：</li></ul><pre><code>predict_next_question_prompt: str = field(
       default=prompts.PREDICT_NEXT_QUESTION_PROMPT,
       metadata={"description": "The system prompt used for generating responses."},
   )</code></pre><h3>4.更新 state.py</h3><ul><li>添加以下属性</li></ul><pre><code>response: Annotated[Sequence[AnyMessage], add_messages]
next_question : Annotated[Sequence[AnyMessage], add_messages]</code></pre><h3>5.重新运行检索图</h3><ul><li>再次输入以下搜索查询：</li></ul><pre><code>What was NovaTech Solutions total revenue in Q1 2025?</code></pre><p>系统将处理输入信息，并预测用户可能提出的三个相关问题，如下图所示。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/70e855a2e4edc0ba5a147588df0de30eb081d053-1600x777.png" style="width: 100%;" /><h2>结论</h2><p>在 LangGraph Studio 和 CLI 中集成检索代理模板有几个主要好处：</p><ul><li><strong>加速开发</strong>：模板和可视化工具简化了检索工作流的创建和调试，缩短了开发时间。</li><li><strong>无缝部署</strong>：对 API 和自动扩展的内置支持可确保跨环境的顺利部署。</li><li><strong>易于更新：</strong>修改工作流程、添加新功能和集成其他节点都很简单，从而更容易扩展和增强检索流程。</li><li><strong>持久记忆</strong>：系统保留代理状态和知识，提高一致性和可靠性。</li><li><strong>灵活的工作流程建模</strong>：开发人员可针对特定用例定制检索逻辑和通信规则。</li><li><strong>实时交互和调试</strong>：通过与运行中的代理互动，可以高效地进行测试和解决问题。</li></ul><p>利用这些功能，企业可以建立强大、高效和可扩展的检索系统，从而提高数据的可访问性和用户体验。</p><p>该项目的完整源代码可在<a href="https://github.com/elastic/elasticsearch-labs/tree/main/supporting-blog-content/langraph-retrieval-agent-template-demo">GitHub</a> 上获取。</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[如何在使用案例中实施更好的二进制量化 (BBQ)]]></title>
            <description><![CDATA[探讨为什么要在用例中实施更好的二进制量化 (BBQ) 以及如何实施。]]></description>
            <link>https://www.elastic.co/search-labs/cn/blog/bbq-implementation-into-use-case</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/cn/blog/bbq-implementation-into-use-case</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Sachin Frayne]]></dc:creator>
            <pubDate>Wed, 23 Apr 2025 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/66842055367cdd795532b01c167f2a4b03dc65e3-1200x628.png?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>矢量搜索为实现文本的语义搜索或图像、视频或音频的相似性搜索提供了基础。在矢量搜索中，矢量是数据的数学表示，可能非常庞大，有时也会比较迟钝。更好的二进制量化（以下简称 BBQ）是一种矢量压缩方法。它可以让你找到正确的匹配，同时缩小矢量，使搜索和处理速度更快。本文将介绍 BBQ 和 rescore_vector，这是一个仅适用于量化索引的字段，可自动对向量重新评分。</p><p>本文中提到的所有完整查询和输出都可以在我们的<a href="https://github.com/elastic/elasticsearch-labs/tree/main/supporting-blog-content/how-and-why-bbq">Elasticsearch Labs 代码库中</a>找到。</p><h2>为什么要在使用案例中实施更好的二进制量化 (BBQ)？</h2><blockquote>注：要深入了解 BBQ 背后的数学原理，请查看下面的<a href="https://www.elastic.co/cn/search-labs/blog/bbq-implementation-into-use-case#further-learning">&quot;进一步学习 &quot;部分</a>。就本博客而言，重点是实施。</blockquote><p>数学知识固然耐人寻味，但要想完全掌握矢量搜索保持精确的原因，这一点至关重要。归根结底，这一切都与压缩有关，因为事实证明，目前的矢量搜索算法受到数据读取速度的限制。因此，如果能将所有数据都存储到内存中，那么与从存储设备中读取数据相比，速度将得到显著提升 （内存的 读取<a href="https://sre.google/static/pdf/rule-of-thumb-latency-numbers-letter.pdf"> 速度约为固态硬盘的 200 倍</a> ）。</p><p>有几点需要注意：</p><ul><li>基于图形的索引，如<a href="https://arxiv.org/pdf/1603.09320">HNSW</a>（层次导航小世界），对于向量检索来说是最快的。<ul><li>HNSW：一种近似近邻搜索算法，可构建多层图结构，从而实现高效的高维相似性搜索。</li></ul></li></ul><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/590f3b3c72a76023a38a0436cd9ff90a9f80e936-1964x1262.png" style="width: 100%;" /><ul><li>从根本上说，HNSW 的速度受限于从内存读取数据的速度，或者在最糟糕的情况下，受限于从存储器读取数据的速度。<ul><li>理想情况下，您希望能够将所有存储的向量加载到内存中。</li></ul></li><li>嵌入模型通常以 float32 的精度生成向量，每个浮点数 4 个字节。</li><li>最后，根据向量和/或维数的多少，内存很快就会不够存放所有向量。</li></ul><p>如果把这看作是理所当然的，那么一旦你开始摄入数百万甚至数十亿的向量，每个向量都可能有数百甚至数千个维度，你就会发现问题很快就出现了。题为 &quot;<a href="https://www.elastic.co/cn/search-labs/blog/bbq-implementation-into-use-case#approximate-numbers-on-the-compression-ratios">压缩比近似值</a>&quot;的部分提供了一些粗略的数字。</p><h2>开始需要什么？</h2><p>要开始使用，您需要具备以下条件：</p><ul><li>如果使用 Elastic Cloud 或内部部署，则需要高于 8.18 的 Elasticsearch 版本。虽然 BBQ 是在 8.16 中引入的，但在本文中，您将使用<code>vector_rescore</code> ，它是在 8.18 中引入的。</li><li>此外，您还需要确保集群中有一个<a href="https://www.elastic.co/cn/guide/en/elasticsearch/reference/8.18/ml-settings.html">机器学习（ML）节点</a>。(注意：加载模型需要至少 4GB 的 ML 节点，但如果要完成生产工作负载，可能需要更大的节点）。</li><li>如果使用的是无服务器，则需要选择针对向量进行了优化的实例。</li><li>您还需要具备矢量数据库方面的基础知识。如果您还不熟悉 Elastic 中的矢量搜索概念，可能需要先查看以下资源：<ul><li><a href="https://www.elastic.co/cn/search-labs/blog/elastic-vector-database-practical-example">导航弹性矢量数据库</a></li><li><a href="https://www.elastic.co/cn/blog/retrieval-augmented-generation-explained">检索增强生成背后的重大理念</a></li></ul></li></ul><h2>更好的二进制量化 (BBQ) 实现</h2><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/4d388078495566f0527e931e0c2e38facdce83c6-1503x748.png" style="width: 100%;" /><p>为了使本博客简单明了，您将在可用时使用内置函数。在这种情况下，<a href="https://www.elastic.co/cn/guide/en/machine-learning/8.17/ml-nlp-e5.html"><code>.multilingual-e5-small</code></a> 向量嵌入模型将直接在 Elasticsearch 内部的机器学习节点上运行。请注意，您可以用自己选择的嵌入器<a href="https://www.elastic.co/cn/guide/en/elasticsearch/reference/8.18/infer-service-openai.html">（OpenAI</a>、<a href="https://www.elastic.co/cn/guide/en/elasticsearch/reference/8.18/infer-service-google-ai-studio.html">Google AI Studio</a>、<a href="https://www.elastic.co/cn/guide/en/elasticsearch/reference/8.18/infer-service-cohere.html">Cohere</a>等）替换<code>text_embedding</code> 模型。如果您喜欢的模型尚未集成，您也可以<a href="https://www.elastic.co/cn/guide/en/elasticsearch/reference/8.18/bring-your-own-vectors.html">自带密集向量嵌入</a>模型）。</p><p>首先，您需要创建一个推理端点，为给定文本生成向量。您将从 Kibana<a href="https://www.elastic.co/cn/guide/en/kibana/8.18/console-kibana.html">Dev Tools 控制台</a>运行所有这些命令。该命令将下载<code>.multilingual-e5-small</code>.如果端点还不存在，它将为您设置端点；这可能需要一分钟的时间。你可以在 Outputs 文件夹中的<a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/how-and-why-bbq/Outputs/01-create-an-inference-endpoint-output.json">01-create-an-inference-endpoint-output.json</a>文件中看到预期输出。 </p><pre><code>PUT _inference/text_embedding/my_e5_model
{
  "service": "elasticsearch",
  "service_settings": {
    "num_threads": 1,
    "model_id": ".multilingual-e5-small",
    "adaptive_allocations": {
      "enabled": true,
      "min_number_of_allocations": 1
    }
  }
}</code></pre><p>返回后，模型就设置好了，您可以使用以下命令测试模型是否按预期运行。你可以在 Outputs 文件夹中的<a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/how-and-why-bbq/Outputs/02-embed-text-output.json">02-embed-text-output.json</a>文件中看到预期输出。</p><pre><code>POST _inference/text_embedding/my_e5_model
{
  "input": "my awesome piece of text"
}</code></pre><p>如果遇到训练好的模型没有分配到任何节点的问题，可能需要手动启动模型。</p><pre><code>POST _ml/trained_models/.multilingual-e5-small/deployment/_start</code></pre><p>现在，让我们创建一个带有 2 个属性的新映射，一个标准文本字段 (<code>my_field</code>) 和一个 384 维的密集矢量字段 (<code>my_vector</code>) ，以匹配嵌入模型的输出。您还可以覆盖<code>index_options.type to bbq_hnsw</code> 。你可以在 Outputs 文件夹中的文件<a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/how-and-why-bbq/Outputs/03-create-byte-qauntized-index-output.json">03-create-byte-qauntized-index-output.json</a>中看到预期输出。</p><pre><code>PUT bbq-my-byte-quantized-index
{
  "mappings": {
    "properties": {
      "my_field": {
        "type": "text"
      },
      "my_vector": {
        "type": "dense_vector",
        "dims": 384,
        "index_options": {
          "type": "bbq_hnsw"
        }
      }
    }
  }
}</code></pre><p>要确保 Elasticsearch 生成向量，可以使用<a href="https://www.elastic.co/cn/guide/en/elasticsearch/reference/8.18/ingest.html">Ingest Pipeline</a>。该管道需要三样东西：端点 (<code>model_id</code>)、要为其创建向量的<code>input_field</code> 以及用于存储这些向量的<code>output_field</code> 。下面的第一条命令将创建推理摄取管道，该管道使用<a href="https://www.elastic.co/cn/guide/en/elasticsearch/reference/current/inference-apis.html">推理服务 </a>，第二条命令将测试管道是否正常工作。你可以在 Outputs 文件夹中的文件<a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/how-and-why-bbq/Outputs/04-create-and-simulate-ingest-pipeline-output.json">04-create and-simulate-ingest-pipeline-output.json</a>中看到预期输出。 </p><pre><code>PUT _ingest/pipeline/my_inference_pipeline
{
  "processors": [
    {
      "inference": {
        "model_id": "my_e5_model",
        "input_output": [
          {
            "input_field": "my_field",
            "output_field": "my_vector"
          }
        ]
      }
    }
  ]
}

POST _ingest/pipeline/my_inference_pipeline/_simulate
{
  "docs": [
    {
      "_source": {
        "my_field": "my awesome text field"
      }
    }
  ]
}</code></pre><p>现在，您可以使用下面的前 2 个命令添加一些文档，并使用第 3 个命令测试搜索是否有效。你可以在 Outputs 文件夹中的<a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/how-and-why-bbq/Outputs/05-bbq-index-output.json">05-bbq-index-output.json</a>文件中查看预期输出。 </p><pre><code>PUT bbq-my-byte-quantized-index/_doc/1?pipeline=my_inference_pipeline
{
    "my_field": "my awesome text field"
}

PUT bbq-my-byte-quantized-index/_doc/2?pipeline=my_inference_pipeline
{
    "my_field": "some other sentence"
}

GET bbq-my-byte-quantized-index/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "knn": {
            "field": "my_vector",
            "query_vector_builder": {
              "text_embedding": {
                "model_id": "my_e5_model",
                "model_text": "my awesome search field"
              }
            },
            "k": 10,
            "num_candidates": 100
          }
        }
      ]
    }
  },
  "_source": [
    "my_field"
  ]
}</code></pre><p>正如<a href="https://www.elastic.co/cn/search-labs/blog/better-binary-quantization-lucene-elasticsearch#lucene-benchmarking">本文章</a>所建议的，当您扩展到非数量级的数据时，建议使用重采样和超采样，因为它们有助于在受益于压缩优势的同时保持较高的召回准确率。从 Elasticsearch 8.18 版开始，您可以使用<a href="https://www.elastic.co/cn/guide/en/elasticsearch/reference/8.18/knn-search.html#dense-vector-knn-search-rescoring">rescore_vector</a> 这样做。预期输出在 Outputs 文件夹中的<a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/how-and-why-bbq/Outputs/06-bbq-search-8-18-output.json">06-bbq-search-8-18-output.json</a>文件中。</p><pre><code>GET bbq-my-byte-quantized-index/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "knn": {
            "field": "my_vector",
            "query_vector_builder": {
              "text_embedding": {
                "model_id": "my_e5_model",
                "model_text": "my awesome search field"
              }
            },
            "rescore_vector": {
              "oversample": 3
            },
            "k": 10,
            "num_candidates": 100
          }
        }
      ]
    }
  },
  "_source": [
    "my_field"
  ]
}</code></pre><p>这些分数与原始数据的分数相比如何？如果您再次进行上述操作，但使用<code>index_options.type: hnsw</code> ，您会发现得分非常接近。你可以在 Outputs 文件夹中的<a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/how-and-why-bbq/Outputs/07-raw-vector-output.json">07-raw-vector-output.json</a>文件中看到预期输出。</p><pre><code>PUT my-raw-vector-index
{
  "mappings": {
    "properties": {
      "my_field": {
        "type": "text"
      },
      "my_vector": {
        "type": "dense_vector",
        "dims": 384,
        "index_options": {
          "type": "hnsw"
        }
      }
    }
  }
}

PUT my-raw-vector-index/_doc/1?pipeline=my_inference_pipeline
{
    "my_field": "my awesome text field"
}

PUT my-raw-vector-index/_doc/2?pipeline=my_inference_pipeline
{
    "my_field": "some other sentence"
}

GET my-raw-vector-index/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "knn": {
            "field": "my_vector",
            "query_vector_builder": {
              "text_embedding": {
                "model_id": "my_e5_model",
                "model_text": "my awesome search field"
              }
            },
            "k": 10,
            "num_candidates": 100
          }
        }
      ]
    }
  },
  "_source": [
    "my_field"
  ]
}</code></pre><h2>压缩比的近似值</h2><p>在使用矢量搜索时，存储和内存需求很快就会成为一项重大挑战。下面的细目说明了不同的量化技术如何显著减少矢量数据的内存占用。</p>
            <table style="border-collapse: collapse; margin: 20px 0;">
              <tbody>
                
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">向量 (V)</td><td style="border: 1px solid #ccc; padding: 4px;">尺寸（D）</td><td style="border: 1px solid #ccc; padding: 4px;">未加工（V x D x 4）</td><td style="border: 1px solid #ccc; padding: 4px;">int8 (V x (D x 1 + 4))</td><td style="border: 1px solid #ccc; padding: 4px;">int4 (V x (D x 0.5 + 4))</td><td style="border: 1px solid #ccc; padding: 4px;">bbq (V x (D x 0.125 + 4))</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">10,000,000</td><td style="border: 1px solid #ccc; padding: 4px;">384</td><td style="border: 1px solid #ccc; padding: 4px;">14.31GB</td><td style="border: 1px solid #ccc; padding: 4px;">3.61GB</td><td style="border: 1px solid #ccc; padding: 4px;">1.83GB</td><td style="border: 1px solid #ccc; padding: 4px;">0.58GB</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">50,000,000</td><td style="border: 1px solid #ccc; padding: 4px;">384</td><td style="border: 1px solid #ccc; padding: 4px;">71.53GB</td><td style="border: 1px solid #ccc; padding: 4px;">18.07GB</td><td style="border: 1px solid #ccc; padding: 4px;">9.13GB</td><td style="border: 1px solid #ccc; padding: 4px;">2.89GB</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">100,000,000</td><td style="border: 1px solid #ccc; padding: 4px;">384</td><td style="border: 1px solid #ccc; padding: 4px;">143.05GB</td><td style="border: 1px solid #ccc; padding: 4px;">36.14GB</td><td style="border: 1px solid #ccc; padding: 4px;">18.25GB</td><td style="border: 1px solid #ccc; padding: 4px;">5.77GB</td>
                    </tr>
              </tbody>
            </table>
          <h2>结论</h2><p>BBQ 是一种优化方法，可用于压缩矢量数据而不影响精度。它的工作原理是将向量转换为比特，让您能够有效地搜索数据，并使您能够扩展人工智能工作流程，加快搜索速度并优化数据存储。</p><h2>进一步学习</h2><p>如果您想了解有关烧烤的更多信息，请务必查看以下资源：</p><ul><li><a href="https://www.elastic.co/cn/search-labs/blog/better-binary-quantization-lucene-elasticsearch">Lucene 和 Elasticsearch 中的二进制量化 (BBQ)</a></li><li><a href="https://www.elastic.co/cn/search-labs/blog/bit-vectors-elasticsearch-bbq-vs-pq">更好的二进制量化（BBQ）与乘积量化比较</a></li><li><a href="https://www.elastic.co/cn/search-labs/blog/optimized-scalar-quantization-elasticsearch">优化的标量量化更好的二进制量化</a></li><li><a href="https://www.youtube.com/watch?v=04NzMt2Nigc">更好的二进制量化 (BBQ)：从字节到烧烤，更好的矢量搜索的秘密》，本-特伦特著</a></li></ul>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Elasticsearch 堆大小使用情况和 JVM 垃圾收集]]></title>
            <description><![CDATA[探索 Elasticsearch 堆大小使用和 JVM 垃圾收集，包括最佳实践以及如何在堆内存使用率过高或 JVM 性能不佳时解决问题。]]></description>
            <link>https://www.elastic.co/search-labs/cn/blog/elasticsearch-heap-size-jvm-garbage-collection</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/cn/blog/elasticsearch-heap-size-jvm-garbage-collection</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <pubDate>Tue, 22 Apr 2025 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/b162c28623b9070fd1980bcd891b9dd1e868f2f0-720x421.jpg?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>堆大小是指分配给 Elasticsearch 节点 Java 虚拟机的 RAM 容量。</p><p>从 7.11 版开始，Elasticsearch 默认会根据节点的角色和总内存自动设置 JVM 堆大小。建议大多数生产环境使用默认大小。不过，如果要手动设置 JVM 堆大小，一般来说，应将 -Xms 和 -Xmx 设置为相同的值，即总可用内存的 50% ，最大（约）为 31GB。</p><p>堆大小越大，节点用于索引和搜索操作的内存就越大。不过，节点也需要内存进行缓存，因此使用 50% 可以在两者之间保持健康的平衡。出于同样的原因，在生产中应避免在 Elasticsearch 的同一节点上使用其他内存密集型进程。</p><p>通常情况下，堆使用量会呈现锯齿状，在最大堆使用量的 30 到 70% 之间摇摆。这是因为 JVM 会稳步增加堆使用百分比，直到垃圾回收过程再次释放内存。当垃圾回收进程跟不上时，就会出现堆使用率高的情况。堆使用率高的一个指标是垃圾回收无法将堆使用率降低到 30% 左右。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/0a17a67cc589a3c1fbf9e918eadc119df7bd7619-858x278.png" style="width: 100%;" /><p>在上图中，您可以看到 JVM 堆的正常锯齿形。</p><p>你还会看到有两种类型的垃圾回收，即年轻的 GC 和年老的 GC。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/8df5c24c4894404de4617be7a13683c9027d607d-875x281.png" style="width: 100%;" /><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/e01eb2537310b052580411153b8eddc187d97687-890x264.png" style="width: 100%;" /><p>在健康的 JVM 中，垃圾回收最好能满足以下条件：</p><ul><li>年轻 GC 的处理速度很快（50 毫秒内）。</li><li>年轻的 GC 执行频率不高（约 10 秒）。</li><li>旧 GC 处理速度很快（1 秒内）。</li><li>旧 GC 执行频率不高（每 10 分钟或更长时间执行一次）。</li></ul><h3><strong>如何解决堆内存使用率过高或 JVM 性能不佳的问题</strong></h3><p>堆内存使用量增加有多种原因：</p><h4><strong>过度包庇</strong></h4><p>请<a href="https://www.elastic.co/docs/deploy-manage/production-guidance/optimize-performance/size-shards#sizing-shard-guidelines">点击此处</a>查看有关过度储藏的文件。</p><h4><strong>聚合规模大</strong></h4><p>为了避免聚合大小过大，请尽量减少查询中聚合桶的数量（大小）。</p><pre><code>GET /_search
{
   "aggs" : {
       "products" : {
           "terms" : {
               "field" : "product",
               "size" : 5
                          }
       }
   }
}</code></pre><p>您可以使用慢查询日志（慢日志），并通过以下方式在特定索引上实施。</p><pre><code>PUT /my_index/_settings
{
   "index.search.slowlog.threshold.query.warn": "10s",
   "index.search.slowlog.threshold.query.info": "5s",
   "index.search.slowlog.threshold.query.debug": "2s",
   "index.search.slowlog.threshold.query.trace": "500ms",
   "index.search.slowlog.threshold.fetch.warn": "1s",
   "index.search.slowlog.threshold.fetch.info": "800ms",
   "index.search.slowlog.threshold.fetch.debug": "500ms",
   "index.search.slowlog.threshold.fetch.trace": "200ms",
   "index.search.slowlog.level": "info"
}</code></pre><p>需要很长时间才能返回结果的查询可能是资源密集型查询。</p><h4><strong>批量索引大小过大</strong></h4><p>如果您发送的请求很大，那么这可能是堆消耗大的原因。尝试减少批量索引请求的大小。</p><h4><strong>制图问题</strong></h4><p>特别是，如果您使用 &quot;fielddata: true&quot;，那么这将成为 JVM 堆的主要用户。</p><h4><strong>堆大小设置错误</strong></h4><p>堆大小可以通过以下方式手动定义：</p><p>设置环境变量</p><pre><code>ES_JAVA_OPTS="-Xms2g -Xmx2g"</code></pre><p>编辑 Elasticsearch 配置目录中的 jvm.options 文件：</p><pre><code>-Xms2g
-Xmx2g</code></pre><p>环境变量设置优先于文件设置。</p><p>必须重新启动节点才能将设置考虑在内。</p><h4><strong>JVM 新比率设置错误</strong></h4><p>通常无需设置，因为 Elasticsearch 默认设置此值。该参数定义 JVM 中 &quot;新一代 &quot;和 &quot;老一代 &quot;对象的可用空间比例。</p><p>如果发现旧 GC 变得非常频繁，可以尝试在 Elasticsearch 配置目录下的 jvm.options 文件中专门设置该值。</p><pre><code>-XX:NewRatio=3</code></pre><h3><strong>在大型 Elasticsearch 集群中，管理堆大小使用和 JVM 垃圾收集的最佳实践是什么？</strong></h3><p>在大型 Elasticsearch 集群中，管理堆大小使用和 JVM 垃圾收集的最佳做法是确保堆大小最多设置为可用 RAM 的 50% ，并根据具体使用情况优化 JVM 垃圾收集设置。必须监控堆大小和垃圾回收指标，以确保群集以最佳状态运行。具体来说，监控 JVM 堆大小、垃圾收集时间和垃圾收集暂停非常重要。此外，监控垃圾回收周期的次数和垃圾回收所花费的时间也很重要。通过监控这些指标，可以发现堆大小或垃圾回收设置方面的任何潜在问题，并在必要时采取纠正措施。</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[如何增加 Elasticsearch 中的主分区数量]]></title>
            <description><![CDATA[了解如何使用 split 和 reindex API 在 Elasticsearch 中增加主分片数量，以实现最佳分片扩展。]]></description>
            <link>https://www.elastic.co/search-labs/cn/blog/elasticsearch-increase-primary-shard-count</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/cn/blog/elasticsearch-increase-primary-shard-count</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <pubDate>Thu, 17 Apr 2025 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/7034b76019a0cba52c25eda29fceb18afc96ed0b-720x420.png?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>无法增加现有索引的主分区数，这意味着如果要增加主分区数，必须重新创建索引。在这种情况下，通常使用两种方法：_reindex API 和 _split API。</p><p>与 _reindex API 相比，_split API 通常是更快的方法。在进行这两项操作前，<strong> 必须停止 索引 ，否则源索引和目标索引的文档计数将不同。</strong></p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/aa0ae010c2f5691ca00440fb453ed6b47bacd24f-1200x628.png" style="width: 100%;" /><h2>方法 1 - 使用拆分 API</h2><p>拆分 API 用于通过复制设置和映射现有索引来创建一个新索引，其中包含所需的主分片数量。可在创建过程中设置所需的主分区数量。在实施拆分 API 之前，应检查以下设置：</p><ol><li>源索引必须是只读的。这意味着需要停止索引进程。</li><li>目标索引中主分区的数量必须是源索引中主分区数量的倍数。例如，如果源索引有 5 个主分区，那么目标索引的主分区可以设置为 10、15、20，以此类推。</li></ol><p>注意：如果只需要更改主分区编号，则首选拆分 API，因为它比 Reindex API 快得多。</p><h3>实施拆分应用程序接口</h3><p>创建测试索引：</p><pre><code>POST test_split_source/_doc
{
  "test": "test"
}</code></pre><p>源索引必须是只读的，才能进行拆分：</p><pre><code>PUT test_split_source/_settings
{
  "index.blocks.write": true
}</code></pre><p>设置和映射将自动从源索引中复制：</p><pre><code>POST /test_split_source/_split/test_split_target
{
  "settings": {
    "index.number_of_shards": 3
  }
}</code></pre><p>您可以通过以下方式检查进度：</p><pre><code>GET _cat/recovery/test_split_target?v&h=index,shard,time,stage,files_percent,files_total</code></pre><p>由于设置和映射是从源索引中复制的，因此目标索引是只读的。让我们启用目标索引的写操作：</p><pre><code>PUT test_split_target/_settings
{
    "index.blocks.write": null
}</code></pre><p>删除原始索引前，检查源索引和目标索引的 docs.count：</p><pre><code>GET _cat/indices/test_split*?v&h=index,pri,rep,docs.count</code></pre><p>索引名称和别名不能相同。您需要删除源索引，并将源索引名称作为别名添加到目标索引中：</p><pre><code>DELETE test_split_source
PUT /test_split_target/_alias/test_split_source</code></pre><p>将<strong>test_split_source</strong>别名添加到<strong>test_split_target</strong>索引后，应使用</p><pre><code>GET test_split_source
POST test_split_source/_doc
{
  "test": "test"
}</code></pre><h2>方法 2 - 使用重新索引 API</h2><p>通过使用 Reindex API 创建新索引，可以给出任意数量的主分区计数。在创建具有预定主分片数量的新索引后，源索引中的所有数据都可以重新索引到这个新索引。</p><p>除了拆分 API 功能外，还可以使用 reindex AP 中的 ingest_pipeline 对数据进行操作。通过摄取管道，只有符合过滤器的指定字段才会被索引到使用查询的目标索引中。数据内容可通过简单的脚本进行更改，多个索引可合并为一个索引。</p><h3>实施重新索引 API</h3><p>创建测试重新索引：</p><pre><code>POST test_reindex_source/_doc
{
    "test": "test"
}</code></pre><p>从源索引中复制设置和映射：</p><pre><code>GET test_reindex_source</code></pre><p>创建包含设置、映射和所需分片数的目标索引：</p><pre><code>PUT test_reindex_target
{
  "mappings" : {},
  "settings": {
    "number_of_shards": 10,
    "number_of_replicas": 0,
    "refresh_interval": -1
  }
}</code></pre><p>*注意：设置 number_of_replicas：0 和 refresh_interval: -1 会提高重新索引的速度。</p><p>启动重新索引程序。设置 requests_per_second=-1 和 slices=auto 可以调整重新索引的速度。</p><pre><code>POST _reindex?requests_per_second=-1&slices=auto&wait_for_completion=false
{
  "source": {
    "index": "test_reindex_source"
  },
  "dest": {
    "index": "test_reindex_target"
  }
}</code></pre><p>运行 reindex API 时，您将看到 task_id。复制并使用 _tasks API 进行检查：</p><pre><code>GET _tasks/<task_id></code></pre><p>重新索引完成后更新设置：</p><pre><code>PUT test_reindex_target/_settings
{
  "number_of_replicas": 1,
  "refresh_interval": "1s"
}</code></pre><p>删除原始索引前，请检查源索引和目标索引的 docs.count，它们应该是一样的：</p><pre><code>GET _cat/indices/test_reindex_*?v&h=index,pri,rep,docs.count</code></pre><p>索引名称和别名不能相同。删除源索引，并将源索引名称作为别名添加到目标索引中：</p><pre><code>DELETE test_reindex_source
PUT /test_reindex_target/_alias/test_reindex_source</code></pre><p>将 test_split_source 别名添加到 test_split_target 索引后，使用</p><pre><code>GET test_reindex_source</code></pre><h2>总结</h2><p>如果要增加现有索引的主分区数，则需要重新创建新索引的设置和映射。主要有两种方法：重新索引 API 和拆分 API。在使用这两种方法之前，必须先停止主动索引。</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Elasticsearch BBQ 与 OpenSearch FAISS：矢量搜索性能比较]]></title>
            <description><![CDATA[Elasticsearch BBQ 和 OpenSearch FAISS 的性能比较。]]></description>
            <link>https://www.elastic.co/search-labs/cn/blog/elasticsearch-bbq-vs-opensearch-faiss</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/cn/blog/elasticsearch-bbq-vs-opensearch-faiss</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <dc:creator><![CDATA[Ugo Sangiorgi]]></dc:creator>
            <pubDate>Tue, 15 Apr 2025 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/796de4829e29566f1f3efa2482f5c3e54b31b1d6-1536x1024.png?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p><strong>二进制量化矢量搜索使用 BBQ 的 Elasticsearch 比使用 FAISS 的 OpenSearch 快 5 倍</strong>。Elastic 收到了来自社区的请求，希望澄清 Elasticsearch 和 OpenSearch 之间的性能差异，尤其是在语义搜索/矢量搜索领域，因此我们进行了这些性能测试，以提供清晰的、数据驱动的比较。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/7f4f8d08f7bee188423e4e65f0caefc7e34f0355-1600x681.png" style="width: 100%;" /><h2>二进制量化对决</h2><p>以原始形式存储高维向量会耗费大量内存。量化技术可将这些矢量压缩为紧凑的表示形式，从而大大减少内存占用。然后，搜索在压缩空间内进行，从而降低了计算复杂度，加快了搜索速度，尤其是在大型数据集中。</p><p>Elastic 致力于使 Lucene 成为性能一流的矢量引擎。在 Elasticsearch 8.16 中，我们在 Lucene 的基础上引入了<a href="https://www.elastic.co/cn/search-labs/blog/better-binary-quantization-lucene-elasticsearch">更好的二进制量化</a>(BBQ)，并在 8.18 和 9.0 中进一步发展。BBQ 基于一种新的<a href="https://www.elastic.co/cn/search-labs/blog/optimized-scalar-quantization-elasticsearch">标量量化</a>方法，将 float32 维度减少到比特，在保持高排名质量的同时，减少了 ~95% 内存。</p><p>另一方面，OpenSearch 使用多种矢量引擎：nmslib（现已废弃）、Lucene 和 FAISS。在<a href="https://www.elastic.co/cn/search-labs/blog/elasticsearch-opensearch-vector-search-performance-comparison"> 上</a> 一篇 博客 中，我们比较了 Elasticsearch 和 OpenSearch 的向量搜索。我们使用了三个不同的数据集，并在两种产品上测试了不同的引擎和配置组合。</p><p>本博客重点介绍这两种产品目前提供的二进制量化算法。我们使用 BBQ 对 Elasticsearch 进行了测试，并使用<a href="https://opensearch.org/docs/latest/search-plugins/knn/knn-vector-quantization/#binary-quantization">FAISS 的二进制量化</a>（<a href="https://github.com/elastic/rally-tracks/edit/master/openai_vector">openai_vector</a>Rally track）对 OpenSearch 进行了测试。</p><p>主要目的是评估两种解决方案在相同召回率下的性能。<em>召回</em>是什么意思？召回率是衡量搜索系统成功检索到多少相关结果的指标。</p><p>在这项评估中，<em>recall</em>@k 尤为重要，其中k代表所考虑的顶级结果的数量。因此，<strong>Recall@10</strong>、<strong>Recall@50 和 Recall@100</strong>分别用来衡量有多少真正相关的结果出现在检索结果的前 10、50 和 100 项中。召回率以 0 到 1 的范围表示（或 0% 到 100% 精确度）。这一点很重要，因为我们讨论的是近似 KNN (ANN)，而不是精确 KNN，后者的召回率总是 1 (100%).</p><p>对于每个<em>k</em>值，我们还指定了<em>n， </em>即在应用最终排名之前考虑的候选者数量。这意味着，对于 Recall@10、Recall@50 和 Recall@100，系统首先使用二进制量化算法检索<em>n 个</em>候选项，然后对它们进行排序，以确定前<em>k 个</em>结果是否包含预期的相关项。</p><p>通过控制<em>n</em>，我们可以分析效率和精度之间的权衡。较高的<em>n</em>通常会<strong>增加</strong>召回率，因为有更多候选者可供排序，但同时也会<strong>增加</strong>延迟和<strong> 降低 </strong>吞吐量。相反，<em>n</em>越小，检索速度越快，但如果初始集合中包含的相关候选信息太少，则可能会降低召回率。</p><p>在这次比较中，Elasticsearch 在相同的设置下比 OpenSearch 的延迟更低，吞吐量更高。</p><h2>方法</h2><p>完整的配置，以及 Terraform 脚本、Kubernetes 清单和特定的 Rally 轨道，都可在此<a href="https://github.com/elastic/elasticsearch-opensearch-vector-performance/tree/bbq">仓库</a> <a href="https://github.com/elastic/elasticsearch-opensearch-vector-performance/tree/bbq/rally-custom/custom_tracks/elasticsearch/openai_vector_bq"><em>openai_vector_bq</em></a> 下找到。</p><p>与之前的基准测试一样，我们使用的 Kubernetes 集群由以下部分组成：</p><ul><li>1 个用于 Elasticsearch 9.0 的节点池，包含 3 台<code>e2-standard-32</code> 机器（128GB 内存和 32 个 CPU）</li><li>1 个用于 OpenSearch 2.19 的节点池，包含 3 台<code>e2-standard-32</code> 机器（128GB 内存和 32 个 CPU）</li><li>1 个用于 Rally 的节点池，包含 2 台<code>e2-standard-4</code> 机器（16GB 内存和 4 个 CPU）</li></ul><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/169bb6033f6eebfd1b177b3446bf916fde4ee5c5-1600x856.png" style="width: 100%;" /><p>我们建立了一个版本为 9.0 的 Elasticsearch 集群和一个版本为 2.19 的 OpenSearch 集群。</p><p>Elasticsearch 和 OpenSearch 都使用了完全相同的设置进行测试：我们使用了经过<a href="https://github.com/elastic/elasticsearch-opensearch-vector-performance/commit/b97d5d95c22c8cf862f2030964524bdd156a5da3"> 一些修改的</a><a href="https://github.com/elastic/rally-tracks/edit/master/openai_vector"> openai_vector</a> Rally track，它使用了来自<a href="https://huggingface.co/datasets/BeIR/nq"> NQ 数据集</a> 的 250 万份文档，并使用 OpenAI 的<a href="https://openai.com/blog/new-and-improved-embedding-model"> text-embedding-ada-002 模型</a> 生成了丰富的嵌入。</p><pre><code>{
  "source-file": "open_ai_corpus-initial-indexing.json.bz2",
  "document-count": 2580961,
  "compressed-bytes": 32076749416,
  "uncompressed-bytes": 90263571686
}</code></pre><p>结果报告了在不同召回级别（召回率@10、召回率@50 和召回率@100）下，使用 8 个客户端同时执行搜索操作所测出的延迟和吞吐量。我们只使用一个分片，没有副本。</p><p>我们运行了以下 k-n-rescore 组合，例如10-2000-2000，或<em>k:10</em>、<em>n:2000</em>和<em>rescore:2000</em>将检索 n 个候选者（2000）中的前 k（10），并对 2000 个结果进行重新评分（相当于 &quot;超抽样因子 &quot;1）。每次搜索运行 10.000 次，预热 1000 次：</p><p></p><p><strong><span style="text-decoration:underline">召回@10</span></strong></p><ul><li>10-40-40</li><li>10-50-50</li><li>10-100-100</li><li>10-200-200</li><li>10-500-500</li><li>10-750-750</li><li>10-1000-1000</li><li>10-1500-1500</li><li>10-2000-2000</li></ul><p><strong><span style="text-decoration:underline">回忆@50</span></strong></p><ul><li>50-150-150</li><li>50-200-200</li><li>50-250-250</li><li>50-500-500</li><li>50-750-750</li><li>50-1000-1000</li><li>50-1200-1200</li><li>50-1500-1500</li><li>50-2000-2000</li></ul><p><strong><span style="text-decoration:underline">Recall@100</span></strong></p><ul><li>100-200-200</li><li>100-250-250</li><li>100-300-300</li><li>100-500-500</li><li>100-750-750</li><li>100-1000-1000</li><li>100-1200-1200</li><li>100-1500-1500</li><li>100-2000-2000</li></ul><p>为了复制基准，Rally-elasticsearch 和 rally-opensearch 的 Kubernetes 清单都将所有相关变量外部化到了 ConfigMap 中，可<a href="https://github.com/elastic/elasticsearch-opensearch-vector-performance/blob/bbq/k8s/rally-openai_vector-es-bq.yml">在此处</a>（ES）和<a href="https://github.com/elastic/elasticsearch-opensearch-vector-performance/blob/bbq/k8s/rally-openai_vector-os-bq.yml">此处</a>（OS）查看。可以自定义<em>search_ops</em>参数，以测试 k、n 和 rescore 的任意组合。</p><h3>OpenSearch Rally 配置</h3><p><code>/k8s/rally-openai_vector-os-bq.yml</code></p><pre><code>apiVersion: v1
kind: ConfigMap
metadata:
  name: rally-params-os
  labels:
    app: rally-opensearch
data:
  user-tags.json: |
    {
      "product": "OpenSearch",
      "product-version": "OpenSearch-2.19.0",
      "product-label": "OpenSearch-2.19-faiss",
      "benchmark-run": "19-feb-recall@100"
    }
  track-params.json: |
    {
      "mapping_type": "vectors-only-mapping-with-docid",
      "standalone_search_clients": 8,
      "standalone_search_iterations": 5000,
      "ann_threshold": 0,
      "vector_mode": "on_disk",
      "compression_level": "32x",
      "vector_method_name": "hnsw",
      "vector_method_engine": "faiss",
      "search_ops": [
        [100, 200, 200],
        [100, 250, 250],
        [100, 300, 300],
        [100, 500, 500],
        [100, 750, 750],
        [100, 1000, 1000],
        [100, 1200, 1200],
        [100, 1500, 1500],
        [100, 2000, 2000]
      ]
    }</code></pre><h3>Opensearch 索引配置</h3><p>然后，ConfigMap 中的变量将用于索引配置，某些参数则保持不变。OpenSearch 中的 1 位量化是通过<a href="https://opensearch.org/docs/latest/search-plugins/knn/knn-vector-quantization/#binary-quantization"> 将压缩级别设置为 &quot;32x &quot; 来配置的</a> 。</p><p><code>index-vectors-only-mapping-with-docid-mapping.json</code></p><pre><code>{
  "settings": {
    {% if preload_pagecache %}
    "index.store.preload": [
      "vec", "vex", "vem", "veq", "veqm", "veb", "vebm"
    ],
    {% endif %}
    "index.number_of_shards": {{ number_of_shards | default(1) }},
    "index.number_of_replicas": {{ number_of_replicas | default(0) }},
    "index.knn": true,
    "index.knn.advanced.approximate_threshold": {{ ann_threshold | default(15000) }}
  },
  "mappings": {
    "dynamic": false,
    "properties": {
      "docid": {
        "type": "keyword"
      },
      "emb": {
        "type": "knn_vector",
        "dimension": 1536,
        "space_type": "innerproduct",
        "data_type": "float",
        "mode": {{ vector_mode | default("in_memory") | tojson }},
        "compression_level": {{ compression_level | default("32x") | tojson }},
        "method": {
          "name": {{ vector_method_name | default("hnsw") | tojson }},
          "engine": {{ vector_method_engine | default("faiss") | tojson }},
          "parameters": {
            "ef_construction": 100,
            "m": 16
          }
        }
      }
    }
  }
}</code></pre><h3>Elasticsearch Rally 配置</h3><p><code>/k8s/rally-openai_vector-es-bq.yml</code></p><pre><code>apiVersion: v1
kind: ConfigMap
metadata:
  name: rally-params-es
  labels:
    app: rally-elasticsearch
data:
  user-tags.json: |
    {
      "product": "Elasticsearch",
      "product-version": "Elasticsearch-9.0.0-ade01164",
      "product-label": "Elasticsearch-9.0-BBQ",
      "benchmark-run": "19-feb-recall@100"
    }
  track-params.json: |
    {
      "mapping_type": "vectors-only-mapping-with-docid",
      "standalone_search_clients": 8,
      "standalone_search_iterations": 5000,
      "vector_index_type": "bbq_hnsw",
      "search_ops": [
        [100, 200, 200],
        [100, 250, 250],
        [100, 300, 300],
        [100, 500, 500],
        [100, 750, 750],
        [100, 1000, 1000],
        [100, 1200, 1200],
        [100, 1500, 1500],
        [100, 2000, 2000]
      ]
    }</code></pre><h3>Elasticsearch 索引配置</h3><p><code>index-vectors-only-mapping-with-docid-mapping.json</code></p><pre><code>{
  "settings": {
    {# non-serverless-index-settings-marker-start #}
    {%- if build_flavor != "serverless" or serverless_operator == true -%}
    {% if preload_pagecache %}
    "index.store.preload": [ "vec", "vex", "vem", "veq", "veqm", "veb", "vebm" ],
    {% endif %}
    "index.number_of_shards": {{ number_of_shards | default(1) }},
    "index.number_of_replicas": {{ number_of_replicas | default(0) }}
    {%- endif -%}
    {# non-serverless-index-settings-marker-end #}
  },
  "mappings": {
    "dynamic": false,
    "properties": {
      "docid": {
        "type": "keyword"
      },
      "emb": {
        "type": "dense_vector",
        "element_type": "float",
        "dims": 1536,
        "index": true,
        "similarity": "dot_product",
        "index_options": {
          "type": {{ vector_index_type | default("bbq_hnsw") | tojson }},
          "ef_construction": 100,
          "m": 16
        }
      }
    }
  }
}</code></pre><h2>实施结果</h2><p>对结果有多种解释方法。对于延迟和吞吐量，我们绘制了每个召回级别的简化图和详细图。如果我们考虑到每项指标 &quot;越高越好&quot;，就很容易发现其中的差异。不过，延迟是负值（实际上越低越好），而吞吐量是正值。在简化图表中，我们使用<strong>（召回率/延迟）*10000 </strong>（简称 &quot;速度&quot;）和<strong> 召回率*吞吐量</strong>，因此这两个指标都意味着速度越快和吞吐量越大越好。让我们开始吧。</p><h3>召回 @ 10 - 简化</h3><p>在这种召回水平下，Elasticsearch BBQ 比 OpenSearch FAISS<strong>快 5 倍 </strong>（平均快 3.9 倍），<strong>吞吐量</strong>平均高出 3.2 倍。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/d580ad53e8974bd3aa75957c413a0136c4e465c5-1600x681.png" style="width: 100%;" /><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/18edce667fe36ab95033264ef8df6f352dda2425-2044x866.png" style="width: 100%;" /><h4>召回 @ 10 - 详细</h4><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/a7459b87e679f4ad963d0e2f1685499b40f6f050-1600x799.png" style="width: 100%;" /><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/c6036f55a13377654d296eb3148c7199e1965475-1600x799.png" style="width: 100%;" />
            <table style="border-collapse: collapse; margin: 20px 0;">
              <tbody>
                
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;"></td><td style="border: 1px solid #ccc; padding: 4px;">工作</td><td style="border: 1px solid #ccc; padding: 4px;">latency.mean</td><td style="border: 1px solid #ccc; padding: 4px;">吞吐量平均值</td><td style="border: 1px solid #ccc; padding: 4px;">平均调用次数</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">Elasticsearch-9.0-BBQ</td><td style="border: 1px solid #ccc; padding: 4px;">10-100-100</td><td style="border: 1px solid #ccc; padding: 4px;">11.70</td><td style="border: 1px solid #ccc; padding: 4px;">513.58</td><td style="border: 1px solid #ccc; padding: 4px;">0.89</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">Elasticsearch-9.0-BBQ</td><td style="border: 1px solid #ccc; padding: 4px;">10-1000-100</td><td style="border: 1px solid #ccc; padding: 4px;">27.33</td><td style="border: 1px solid #ccc; padding: 4px;">250.55</td><td style="border: 1px solid #ccc; padding: 4px;">0.95</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">Elasticsearch-9.0-BBQ</td><td style="border: 1px solid #ccc; padding: 4px;">10-1500-1500</td><td style="border: 1px solid #ccc; padding: 4px;">35.93</td><td style="border: 1px solid #ccc; padding: 4px;">197.26</td><td style="border: 1px solid #ccc; padding: 4px;">0.95</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">Elasticsearch-9.0-BBQ</td><td style="border: 1px solid #ccc; padding: 4px;">10-200-200</td><td style="border: 1px solid #ccc; padding: 4px;">13.33</td><td style="border: 1px solid #ccc; padding: 4px;">456.16</td><td style="border: 1px solid #ccc; padding: 4px;">0.92</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">Elasticsearch-9.0-BBQ</td><td style="border: 1px solid #ccc; padding: 4px;">10-2000-2000</td><td style="border: 1px solid #ccc; padding: 4px;">44.27</td><td style="border: 1px solid #ccc; padding: 4px;">161.40</td><td style="border: 1px solid #ccc; padding: 4px;">0.95</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">Elasticsearch-9.0-BBQ</td><td style="border: 1px solid #ccc; padding: 4px;">10-40-40</td><td style="border: 1px solid #ccc; padding: 4px;">10.97</td><td style="border: 1px solid #ccc; padding: 4px;">539.94</td><td style="border: 1px solid #ccc; padding: 4px;">0.84</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">Elasticsearch-9.0-BBQ</td><td style="border: 1px solid #ccc; padding: 4px;">10-50-50</td><td style="border: 1px solid #ccc; padding: 4px;">11.00</td><td style="border: 1px solid #ccc; padding: 4px;">535.73</td><td style="border: 1px solid #ccc; padding: 4px;">0.85</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">Elasticsearch-9.0-BBQ</td><td style="border: 1px solid #ccc; padding: 4px;">10-500-500</td><td style="border: 1px solid #ccc; padding: 4px;">19.52</td><td style="border: 1px solid #ccc; padding: 4px;">341.45</td><td style="border: 1px solid #ccc; padding: 4px;">0.93</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">Elasticsearch-9.0-BBQ</td><td style="border: 1px solid #ccc; padding: 4px;">10-750-750</td><td style="border: 1px solid #ccc; padding: 4px;">22.94</td><td style="border: 1px solid #ccc; padding: 4px;">295.19</td><td style="border: 1px solid #ccc; padding: 4px;">0.94</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">OpenSearch-2.19-faiss</td><td style="border: 1px solid #ccc; padding: 4px;">10-100-100</td><td style="border: 1px solid #ccc; padding: 4px;">35.59</td><td style="border: 1px solid #ccc; padding: 4px;">200.61</td><td style="border: 1px solid #ccc; padding: 4px;">0.94</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">OpenSearch-2.19-faiss</td><td style="border: 1px solid #ccc; padding: 4px;">10-1000-1000</td><td style="border: 1px solid #ccc; padding: 4px;">156.81</td><td style="border: 1px solid #ccc; padding: 4px;">58.30</td><td style="border: 1px solid #ccc; padding: 4px;">0.96</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">OpenSearch-2.19-faiss</td><td style="border: 1px solid #ccc; padding: 4px;">10-1500-1500</td><td style="border: 1px solid #ccc; padding: 4px;">181.79</td><td style="border: 1px solid #ccc; padding: 4px;">42.97</td><td style="border: 1px solid #ccc; padding: 4px;">0.96</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">OpenSearch-2.19-faiss</td><td style="border: 1px solid #ccc; padding: 4px;">10-200-200</td><td style="border: 1px solid #ccc; padding: 4px;">47.91</td><td style="border: 1px solid #ccc; padding: 4px;">155.16</td><td style="border: 1px solid #ccc; padding: 4px;">0.95</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">OpenSearch-2.19-faiss</td><td style="border: 1px solid #ccc; padding: 4px;">10-2000-2000</td><td style="border: 1px solid #ccc; padding: 4px;">232.14</td><td style="border: 1px solid #ccc; padding: 4px;">31.84</td><td style="border: 1px solid #ccc; padding: 4px;">0.96</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">OpenSearch-2.19-faiss</td><td style="border: 1px solid #ccc; padding: 4px;">10-40-40</td><td style="border: 1px solid #ccc; padding: 4px;">27.55</td><td style="border: 1px solid #ccc; padding: 4px;">249.25</td><td style="border: 1px solid #ccc; padding: 4px;">0.92</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">OpenSearch-2.19-faiss</td><td style="border: 1px solid #ccc; padding: 4px;">10-50-50</td><td style="border: 1px solid #ccc; padding: 4px;">28.78</td><td style="border: 1px solid #ccc; padding: 4px;">245.14</td><td style="border: 1px solid #ccc; padding: 4px;">0.92</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">OpenSearch-2.19-faiss</td><td style="border: 1px solid #ccc; padding: 4px;">10-500-500</td><td style="border: 1px solid #ccc; padding: 4px;">79.44</td><td style="border: 1px solid #ccc; padding: 4px;">97.06</td><td style="border: 1px solid #ccc; padding: 4px;">0.96</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">OpenSearch-2.19-faiss</td><td style="border: 1px solid #ccc; padding: 4px;">10-750-750</td><td style="border: 1px solid #ccc; padding: 4px;">104.19</td><td style="border: 1px solid #ccc; padding: 4px;">75.49</td><td style="border: 1px solid #ccc; padding: 4px;">0.96</td>
                    </tr>
              </tbody>
            </table>
          <h3>召回 @ 50 - 简化</h3><p>在这种召回水平下，Elasticsearch BBQ 比 OpenSearch FAISS<strong> 快多达 5 倍</strong> （平均快<strong> 4.2 倍），平均 吞吐量多</strong> 3.9 倍 。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/3189ffb330948b35854eeea9ae317d4846c14972-1600x681.png" style="width: 100%;" /><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/607383d674dcf0b8f94bfb1a450063f52fcbeb15-2060x876.png" style="width: 100%;" /><h4>详细结果 - 召回率 @ 50</h4><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/ebfffe0b776aad31dd03d315cfbf5aa098b41226-1600x789.png" style="width: 100%;" /><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/ae20cf870a65c400a2112bbad62eb56e244f549a-1600x799.png" style="width: 100%;" />
            <table style="border-collapse: collapse; margin: 20px 0;">
              <tbody>
                
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;"></td><td style="border: 1px solid #ccc; padding: 4px;">任务</td><td style="border: 1px solid #ccc; padding: 4px;">平均时延</td><td style="border: 1px solid #ccc; padding: 4px;">吞吐量平均值</td><td style="border: 1px solid #ccc; padding: 4px;">平均召回率</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">Elasticsearch-9.0-BBQ</td><td style="border: 1px solid #ccc; padding: 4px;">50-1000-1000</td><td style="border: 1px solid #ccc; padding: 4px;">25.71</td><td style="border: 1px solid #ccc; padding: 4px;">246.44</td><td style="border: 1px solid #ccc; padding: 4px;">0.95</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">Elasticsearch-9.0-BBQ</td><td style="border: 1px solid #ccc; padding: 4px;">50-1200-1200</td><td style="border: 1px solid #ccc; padding: 4px;">28.81</td><td style="border: 1px solid #ccc; padding: 4px;">227.85</td><td style="border: 1px solid #ccc; padding: 4px;">0.95</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">Elasticsearch-9.0-BBQ</td><td style="border: 1px solid #ccc; padding: 4px;">50-150-150</td><td style="border: 1px solid #ccc; padding: 4px;">13.43</td><td style="border: 1px solid #ccc; padding: 4px;">362.90</td><td style="border: 1px solid #ccc; padding: 4px;">0.90</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">Elasticsearch-9.0-BBQ</td><td style="border: 1px solid #ccc; padding: 4px;">50-1500-1500</td><td style="border: 1px solid #ccc; padding: 4px;">33.38</td><td style="border: 1px solid #ccc; padding: 4px;">202.37</td><td style="border: 1px solid #ccc; padding: 4px;">0.95</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">Elasticsearch-9.0-BBQ</td><td style="border: 1px solid #ccc; padding: 4px;">50-200-200</td><td style="border: 1px solid #ccc; padding: 4px;">12.99</td><td style="border: 1px solid #ccc; padding: 4px;">406.30</td><td style="border: 1px solid #ccc; padding: 4px;">0.91</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">Elasticsearch-9.0-BBQ</td><td style="border: 1px solid #ccc; padding: 4px;">50-2000-2000</td><td style="border: 1px solid #ccc; padding: 4px;">42.63</td><td style="border: 1px solid #ccc; padding: 4px;">163.68</td><td style="border: 1px solid #ccc; padding: 4px;">0.95</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">Elasticsearch-9.0-BBQ</td><td style="border: 1px solid #ccc; padding: 4px;">50-250-250</td><td style="border: 1px solid #ccc; padding: 4px;">14.41</td><td style="border: 1px solid #ccc; padding: 4px;">373.21</td><td style="border: 1px solid #ccc; padding: 4px;">0.92</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">Elasticsearch-9.0-BBQ</td><td style="border: 1px solid #ccc; padding: 4px;">50-500-500</td><td style="border: 1px solid #ccc; padding: 4px;">17.15</td><td style="border: 1px solid #ccc; padding: 4px;">341.04</td><td style="border: 1px solid #ccc; padding: 4px;">0.93</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">Elasticsearch-9.0-BBQ</td><td style="border: 1px solid #ccc; padding: 4px;">50-750-750</td><td style="border: 1px solid #ccc; padding: 4px;">31.25</td><td style="border: 1px solid #ccc; padding: 4px;">248.60</td><td style="border: 1px solid #ccc; padding: 4px;">0.94</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">OpenSearch-2.19-faiss</td><td style="border: 1px solid #ccc; padding: 4px;">50-1000-1000</td><td style="border: 1px solid #ccc; padding: 4px;">125.35</td><td style="border: 1px solid #ccc; padding: 4px;">62.53</td><td style="border: 1px solid #ccc; padding: 4px;">0.96</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">OpenSearch-2.19-faiss</td><td style="border: 1px solid #ccc; padding: 4px;">50-1200-1200</td><td style="border: 1px solid #ccc; padding: 4px;">143.87</td><td style="border: 1px solid #ccc; padding: 4px;">54.75</td><td style="border: 1px solid #ccc; padding: 4px;">0.96</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">OpenSearch-2.19-faiss</td><td style="border: 1px solid #ccc; padding: 4px;">50-150-150</td><td style="border: 1px solid #ccc; padding: 4px;">43.64</td><td style="border: 1px solid #ccc; padding: 4px;">130.01</td><td style="border: 1px solid #ccc; padding: 4px;">0.89</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">OpenSearch-2.19-faiss</td><td style="border: 1px solid #ccc; padding: 4px;">50-1500-1500</td><td style="border: 1px solid #ccc; padding: 4px;">169.45</td><td style="border: 1px solid #ccc; padding: 4px;">46.35</td><td style="border: 1px solid #ccc; padding: 4px;">0.96</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">OpenSearch-2.19-faiss</td><td style="border: 1px solid #ccc; padding: 4px;">50-200-200</td><td style="border: 1px solid #ccc; padding: 4px;">48.05</td><td style="border: 1px solid #ccc; padding: 4px;">156.07</td><td style="border: 1px solid #ccc; padding: 4px;">0.91</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">OpenSearch-2.19-faiss</td><td style="border: 1px solid #ccc; padding: 4px;">50-2000-2000</td><td style="border: 1px solid #ccc; padding: 4px;">216.73</td><td style="border: 1px solid #ccc; padding: 4px;">36.38</td><td style="border: 1px solid #ccc; padding: 4px;">0.96</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">OpenSearch-2.19-faiss</td><td style="border: 1px solid #ccc; padding: 4px;">50-250-250</td><td style="border: 1px solid #ccc; padding: 4px;">53.52</td><td style="border: 1px solid #ccc; padding: 4px;">142.44</td><td style="border: 1px solid #ccc; padding: 4px;">0.93</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">OpenSearch-2.19-faiss</td><td style="border: 1px solid #ccc; padding: 4px;">50-500-500</td><td style="border: 1px solid #ccc; padding: 4px;">78.98</td><td style="border: 1px solid #ccc; padding: 4px;">97.82</td><td style="border: 1px solid #ccc; padding: 4px;">0.95</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">OpenSearch-2.19-faiss</td><td style="border: 1px solid #ccc; padding: 4px;">50-750-750</td><td style="border: 1px solid #ccc; padding: 4px;">103.20</td><td style="border: 1px solid #ccc; padding: 4px;">75.86</td><td style="border: 1px solid #ccc; padding: 4px;">0.96</td>
                    </tr>
              </tbody>
            </table>
          <h3>召回 @ 100</h3><p>在这种召回水平下，Elasticsearch BBQ 比 OpenSearch FAISS<strong>快 5 倍 </strong>（平均快 4.6 倍），<strong>吞吐量 </strong>平均高 3.9 倍。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/d3f29db64cbde9956de1fa3ae64a75f15141a2bb-1600x681.png" style="width: 100%;" /><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/10aff7f8c61fdac895b9ba9c5342baf239ba3ffc-2072x864.png" style="width: 100%;" /><h4>详细结果 - Recall @ 100</h4><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/8790fdf9512b850447f6875fb69969f6f1d4da5f-1600x799.png" style="width: 100%;" /><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/d47032fac6c288cd4eedd9f25001e417b2fa9d65-1600x787.png" style="width: 100%;" />
            <table style="border-collapse: collapse; margin: 20px 0;">
              <tbody>
                
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;"></td><td style="border: 1px solid #ccc; padding: 4px;">工作</td><td style="border: 1px solid #ccc; padding: 4px;">latency.mean</td><td style="border: 1px solid #ccc; padding: 4px;">吞吐量平均值</td><td style="border: 1px solid #ccc; padding: 4px;">平均调用次数</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">Elasticsearch-9.0-BBQ</td><td style="border: 1px solid #ccc; padding: 4px;">100-1000-1000</td><td style="border: 1px solid #ccc; padding: 4px;">27.82</td><td style="border: 1px solid #ccc; padding: 4px;">243.22</td><td style="border: 1px solid #ccc; padding: 4px;">0.95</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">Elasticsearch-9.0-BBQ</td><td style="border: 1px solid #ccc; padding: 4px;">100-1200-1200</td><td style="border: 1px solid #ccc; padding: 4px;">31.14</td><td style="border: 1px solid #ccc; padding: 4px;">224.04</td><td style="border: 1px solid #ccc; padding: 4px;">0.95</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">Elasticsearch-9.0-BBQ</td><td style="border: 1px solid #ccc; padding: 4px;">100-1500-1500</td><td style="border: 1px solid #ccc; padding: 4px;">35.98</td><td style="border: 1px solid #ccc; padding: 4px;">193.99</td><td style="border: 1px solid #ccc; padding: 4px;">0.95</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">Elasticsearch-9.0-BBQ</td><td style="border: 1px solid #ccc; padding: 4px;">100-200-200</td><td style="border: 1px solid #ccc; padding: 4px;">14.18</td><td style="border: 1px solid #ccc; padding: 4px;">403.86</td><td style="border: 1px solid #ccc; padding: 4px;">0.88</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">Elasticsearch-9.0-BBQ</td><td style="border: 1px solid #ccc; padding: 4px;">100-2000-2000</td><td style="border: 1px solid #ccc; padding: 4px;">45.36</td><td style="border: 1px solid #ccc; padding: 4px;">159.88</td><td style="border: 1px solid #ccc; padding: 4px;">0.95</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">Elasticsearch-9.0-BBQ</td><td style="border: 1px solid #ccc; padding: 4px;">100-250-250</td><td style="border: 1px solid #ccc; padding: 4px;">14.77</td><td style="border: 1px solid #ccc; padding: 4px;">433.06</td><td style="border: 1px solid #ccc; padding: 4px;">0.90</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">Elasticsearch-9.0-BBQ</td><td style="border: 1px solid #ccc; padding: 4px;">100-300-300</td><td style="border: 1px solid #ccc; padding: 4px;">14.61</td><td style="border: 1px solid #ccc; padding: 4px;">375.54</td><td style="border: 1px solid #ccc; padding: 4px;">0.91</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">Elasticsearch-9.0-BBQ</td><td style="border: 1px solid #ccc; padding: 4px;">100-500-500</td><td style="border: 1px solid #ccc; padding: 4px;">18.88</td><td style="border: 1px solid #ccc; padding: 4px;">340.37</td><td style="border: 1px solid #ccc; padding: 4px;">0.93</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">Elasticsearch-9.0-BBQ</td><td style="border: 1px solid #ccc; padding: 4px;">100-750-750</td><td style="border: 1px solid #ccc; padding: 4px;">23.59</td><td style="border: 1px solid #ccc; padding: 4px;">285.79</td><td style="border: 1px solid #ccc; padding: 4px;">0.94</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">OpenSearch-2.19-faiss</td><td style="border: 1px solid #ccc; padding: 4px;">100-1000-1000</td><td style="border: 1px solid #ccc; padding: 4px;">142.90</td><td style="border: 1px solid #ccc; padding: 4px;">58.48</td><td style="border: 1px solid #ccc; padding: 4px;">0.95</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">OpenSearch-2.19-faiss</td><td style="border: 1px solid #ccc; padding: 4px;">100-1200-1200</td><td style="border: 1px solid #ccc; padding: 4px;">153.03</td><td style="border: 1px solid #ccc; padding: 4px;">51.04</td><td style="border: 1px solid #ccc; padding: 4px;">0.95</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">OpenSearch-2.19-faiss</td><td style="border: 1px solid #ccc; padding: 4px;">100-1500-1500</td><td style="border: 1px solid #ccc; padding: 4px;">181.79</td><td style="border: 1px solid #ccc; padding: 4px;">43.20</td><td style="border: 1px solid #ccc; padding: 4px;">0.96</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">OpenSearch-2.19-faiss</td><td style="border: 1px solid #ccc; padding: 4px;">100-200-200</td><td style="border: 1px solid #ccc; padding: 4px;">50.94</td><td style="border: 1px solid #ccc; padding: 4px;">131.62</td><td style="border: 1px solid #ccc; padding: 4px;">0.83</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">OpenSearch-2.19-faiss</td><td style="border: 1px solid #ccc; padding: 4px;">100-2000-2000</td><td style="border: 1px solid #ccc; padding: 4px;">232.53</td><td style="border: 1px solid #ccc; padding: 4px;">33.67</td><td style="border: 1px solid #ccc; padding: 4px;">0.96</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">OpenSearch-2.19-faiss</td><td style="border: 1px solid #ccc; padding: 4px;">100-250-250</td><td style="border: 1px solid #ccc; padding: 4px;">57.08</td><td style="border: 1px solid #ccc; padding: 4px;">131.23</td><td style="border: 1px solid #ccc; padding: 4px;">0.87</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">OpenSearch-2.19-faiss</td><td style="border: 1px solid #ccc; padding: 4px;">100-300-300</td><td style="border: 1px solid #ccc; padding: 4px;">62.76</td><td style="border: 1px solid #ccc; padding: 4px;">120.10</td><td style="border: 1px solid #ccc; padding: 4px;">0.89</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">OpenSearch-2.19-faiss</td><td style="border: 1px solid #ccc; padding: 4px;">100-500-500</td><td style="border: 1px solid #ccc; padding: 4px;">84.36</td><td style="border: 1px solid #ccc; padding: 4px;">91.54</td><td style="border: 1px solid #ccc; padding: 4px;">0.93</td>
                    </tr>
                    <tr>
                      <td style="border: 1px solid #ccc; padding: 4px;">OpenSearch-2.19-faiss</td><td style="border: 1px solid #ccc; padding: 4px;">100-750-750</td><td style="border: 1px solid #ccc; padding: 4px;">111.33</td><td style="border: 1px solid #ccc; padding: 4px;">69.95</td><td style="border: 1px solid #ccc; padding: 4px;">0.94</td>
                    </tr>
              </tbody>
            </table>
          <h2>烧烤炉的改进</h2><p>自首次发布以来，BBQ 已经取得了长足的进步。在 Elasticsearch 8.16 上，为了便于比较，我们将 8.16 的基准运行与当前的基准运行放在一起，我们可以看到召回率和延迟从那时起有了怎样的改善。</p><img src="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/45a0acfe5985bff28ccada76ec4eca190fe65f72-1600x799.png" style="width: 100%;" /><p>在 Elasticsearch 8.18 和 9.0 中，我们重写了量化向量的核心算法。因此，8.16 版的 BBQ 已经很不错了，而最新版本则更加出色。您可以<a href="https://www.elastic.co/cn/search-labs/blog/optimized-scalar-quantization-elasticsearch">在这里</a>和<a href="https://www.elastic.co/cn/search-labs/blog/scalar-quantization-optimization">这里</a>了解相关信息。简而言之，每个矢量都通过优化的标量量化进行了单独量化。因此，用户可以在不影响性能的情况下获得更高的向量搜索准确性，使 Elasticsearch 的向量检索功能更加强大。</p><h2>结论</h2><p>在 Elasticsearch BBQ 和 OpenSearch FAISS 的性能比较中，Elasticsearch 在矢量搜索方面明显优于 OpenSearch，在各种召回级别中，Elasticsearch 的查询速度平均提高了 5 倍，吞吐量提高了 3.9 倍。</p><p>主要发现包括</p><ul><li><strong>Recall@10</strong>：与 OpenSearch FAISS 相比，Elasticsearch BBQ 的速度快达 5 倍（平均快 3.9 倍），吞吐量平均高出 3.2 倍。</li><li><strong>Recall@50</strong>：与 OpenSearch FAISS 相比，Elasticsearch BBQ 的速度快达 5 倍（平均快 4.2 倍），吞吐量平均高出 3.9 倍。</li><li><strong>Recall@100</strong>：与 OpenSearch FAISS 相比，Elasticsearch BBQ 的速度快达 5 倍（平均快 4.6 倍），吞吐量平均高出 3.9 倍。</li></ul><p>这些结果凸显了 Elasticsearch BBQ 的效率和性能优势，尤其是在高维向量搜索场景中。Elasticsearch 8.16 中引入的更好的二进制量化（BBQ）技术在保持较高排序质量的同时，大幅减少了内存（~95% ），是大规模矢量搜索应用的上佳选择。</p><p>在 Elastic，我们坚持不懈地创新，改进 Apache Lucene 和 Elasticsearch，为搜索和检索用例（包括 RAG（检索增强生成））提供最佳的向量数据库。在 Lucene 10 的基础上，我们<a href="https://www.elastic.co/cn/search-labs/blog/optimized-scalar-quantization-elasticsearch">最近取得的进步</a>大大提高了性能，使矢量搜索比以前更快、更节省空间。本博客就是这种创新的又一例证。</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[如何在不同版本的 Elasticsearch& 集群之间迁移数据]]></title>
            <description><![CDATA[探索在 Elasticsearch 版本和集群之间传输数据的方法。]]></description>
            <link>https://www.elastic.co/search-labs/cn/blog/elasticsearch-migrate-data-versions-clusters</link>
            <guid isPermaLink="true">https://www.elastic.co/search-labs/cn/blog/elasticsearch-migrate-data-versions-clusters</guid>
            <category><![CDATA[elasticsearch-labs]]></category>
            <pubDate>Mon, 14 Apr 2025 00:00:00 GMT</pubDate>
            <enclosure url="https://cdn.sanity.io/images/me0ej585/search-labs-import-testing/01fde3b1d714f12bf8673140c9f2f940d443de31-1440x823.jpg?fm=webp&amp;w=1440&amp;h=754&amp;q=90&amp;fit=crop" length="0" type="image/webp"/>
            <content:encoded><![CDATA[<p>当你想升级 Elasticsearch 集群时，有时创建一个新的、独立的集群并将数据从旧集群转移到新集群会更容易。这样做的好处是，用户可以在新的集群上测试其所有数据和配置以及所有应用程序，而不会有任何停机或数据丢失的风险。</p><p>这种方法的缺点是需要一些重复的硬件，在试图顺利传输和同步所有数据时可能会造成困难。</p><p>如果需要将应用程序从一个数据中心迁移到另一个数据中心，也可能需要执行类似的程序。</p><p>本文将讨论并详细介绍在 Elasticsearch 集群之间传输数据的三种方法。</p><p><strong>如何在 Elasticsearch 集群之间迁移数据？</strong></p><p>在 Elasticsearch 集群之间传输数据有 3 种方法：</p><ol><li><a href="https://www.elastic.co/cn/search-labs/blog/elasticsearch-migrate-data-versions-clusters#1.-reindexing-data-from-a-remote-cluster">从远程群集重新编排索引</a></li><li><a href="https://www.elastic.co/cn/search-labs/blog/elasticsearch-migrate-data-versions-clusters#2.-transferring-data-using-snapshots">使用快照传输数据</a></li><li><a href="https://www.elastic.co/cn/search-labs/blog/elasticsearch-migrate-data-versions-clusters#3.-transferring-data-using-logstash">使用 Logstash 传输数据</a></li></ol><p>使用快照通常是最快速、最可靠的数据传输方法。不过，请记住，您只能将快照还原到相同或更高版本的群集上，而不能还原到相差一个主版本以上的群集上。这意味着您可以将 6.x 快照还原到 7.x 群集上，但不能还原到 8.x 群集上。</p><p>如果需要增加一个以上的主要版本，则需要重新索引或使用 Logstash。</p><p>现在，让我们分别详细了解在 Elasticsearch 集群之间传输数据的三个选项。</p><h2>1.从远程群集重新索引数据</h2><p>在开始重新索引之前，请记住您需要为新群集上的所有索引设置适当的映射。为此，您必须使用适当的映射直接创建索引，或者使用索引模板。</p><h3>从远程重新索引 - 需要配置</h3><p>要从远程重新索引，应将以下配置添加到接收数据的群集的 elasticseearch.yml 文件中，在 Linux 系统中，该文件通常位于此处：/etc/elasticsearch/elasticsearch.yml。要添加的配置如下：</p><pre><code>reindex.remote.whitelist: "192.168.1.11:9200"</code></pre><p>如果使用 SSL，则应将 CA 证书添加到每个节点，并在 elasticsearch.yml 中每个节点的命令中包含以下内容：</p><pre><code>reindex.ssl.certificate_authorities: “/path/to/ca.pem”</code></pre><p>或者，也可以在所有 Elasticsearch 节点上添加下面一行，以禁用 SSL 验证。不过，由于这种方法不如前一种方法安全，因此不太推荐使用：</p><pre><code>reindex.remote.whitelist: "192.168.1.11:9200"
reindex.ssl.verification_mode: none
systemctl restart elasticsearch service </code></pre><p>您需要在每个节点上进行这些修改，并进行滚动重启。有关如何操作的详细信息，请参阅<a href="https://www.elastic.co/cn/guide/en/elasticsearch/reference/8.17/restart-cluster.html#restart-cluster-rolling">我们的指南</a>。</p><h3>重编索引命令</h3><p>在 elasticsearch.yml 文件中定义远程主机，并在必要时添加 SSL 证书后，就可以使用下面的命令开始重新索引数据了：</p><pre><code>POST _reindex
{
  "source": {
    "remote": {
      "host": "http://192.168.1.11:9200",
      "username": "elastic",
      "password": "123456",
     "socket_timeout": "1m",
      "connect_timeout": "1m"

    },
    "index": "companydatabase"
  },
  "dest": {
    "index": "my-new-index-000001"
  }
}</code></pre><p>在此过程中，您可能会遇到超时错误，因此最好为超时设置宽松的值，而不是依赖默认值。</p><p>现在，让我们来看看从远程重新索引时可能会遇到的其他一些常见错误。</p><h3>从远程重新索引时的常见错误</h3><h4>1.重新索引未列入白名单</h4><pre><code>{
  "error": {
    "root_cause": [
      {
        "type": "illegal_argument_exception",
        "reason": "[192.168.1.11:9200] not whitelisted in reindex.remote.whitelist"
      }
    ],
    "type": "illegal_argument_exception",
    "reason": "[192.168.1.11:9200] not whitelisted in reindex.remote.whitelist"
  },
  "status": 400
}</code></pre><p>如果遇到此错误，则表明您没有按照上文所述在 Elasticsearch 中定义远程主机 IP 地址或节点名称 DNS，或者忘记重启 Elasticsearch 服务。</p><p>要解决 Elasticsearch 集群的问题，需要将远程主机添加到所有 Elasticsearch 节点，并重新启动 Elasticsearch 服务。</p><h4>2.SSL 握手异常</h4><pre><code>{
  "error": {
    "root_cause": [
      {
        "type": "s_s_l_handshake_exception",
        "reason": "PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target"
      }
    ],
    "type": "s_s_l_handshake_exception",
    "reason": "PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target",
    "caused_by": {
      "type": "validator_exception",
      "reason": "PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target",
      "caused_by": {
        "type": "sun_cert_path_builder_exception",
        "reason": "unable to find valid certification path to requested target"
      }
    }
  },
  "status": 500
}</code></pre><p>这个错误说明，你忘记按上文所述在 elasticsearch.yml 中添加 reindex.ssl.certificate_authorities。加进去：</p><pre><code>#elasticsearch.yml
reindex.ssl.certificate_authorities: "/path/to/ca.pem"</code></pre><h2>2.使用快照传输数据</h2><p>请记住，如上所述，您只能将快照还原到相同或更高版本的群集上，而不能还原到相差一个主要版本以上的群集上。</p><p>如果需要增加一个以上的主要版本，则需要重新索引或使用 Logstash。</p><p>通过快照传输数据需要以下步骤：</p><p>步骤 1.将版本库插件添加到第一个 Elasticsearch 群集--为了通过快照在群集间传输数据，需要确保新旧群集都能访问版本库。AWS、Google 和 Azure 等云存储库通常是理想的选择。要拍摄快照，请参阅<a href="https://www.elastic.co/cn/guide/en/elasticsearch/reference/current/snapshot-restore.html">我们的指南</a>，并按照其中描述的步骤操作。</p><p>步骤 2.重启 Elasticsearch 服务（滚动重启）。</p><p>第 3 步为第一个 Elasticsearch 集群创建一个存储库。</p><p>步骤 4- 将版本库插件添加到第二个 Elasticsearch 集群。</p><p>第 5 步--向第二个 Elasticsearch 集群添加只读版本库--您需要重复创建第一个 Elasticsearch 集群的相同步骤来添加版本库。</p><p>重要提示：将第二个 Elasticsearch 集群连接到同一个 AWS S3 存储库时，应将存储库定义为只读存储库：</p><pre><code>PUT _snapshot/my_s3_repository
{
  "type": "s3",
  "settings": {
    "bucket": "my-analytic-data",
    "endpoint": "s3.eu-de.cloud-object-storage.appdomain.cloud",
    "readonly": "true"
  }
}</code></pre><p>这一点很重要，因为你要防止在同一个快照存储库中出现混合 Elasticsearch 版本的风险。</p><p>第 6 步 - 将数据还原到第二个 Elasticsearch 群集 - 采取上述步骤后，您可以还原数据并将其传输到新的群集。请按照<a href="https://www.elastic.co/cn/guide/en/elasticsearch/reference/current/snapshot-restore.html">本文</a>所述步骤将数据恢复到新群集。 </p><h2>3.使用 Logstash 传输数据</h2><p>在开始使用 logstash 传输数据之前，请记住您需要为新群集上的所有索引设置适当的映射。为此，您需要直接创建索引或使用索引模板。</p><p>要在两个 Elasticsearch 集群之间传输数据，可以设置一个临时 Logstash 服务器，并用它在两个集群之间传输数据。对于小型集群，2GB 内存的实例就足够了。对于较大的集群，可以使用配备 8GB 内存的四核 CPU。</p><p>有关安装 Logstash 的指导，<a href="https://www.elastic.co/cn/guide/en/logstash/current/installing-logstash.html">请参阅此处</a>。</p><h3>从一个群集向另一个群集传输数据的 Logstash 配置</h3><p>将单个索引从群集 A 复制到群集 B 的基本配置是</p><pre><code>iinput
{
elasticsearch
      {
        hosts => ["192.168.1.11:9200"]
        index => "index_name"
       docinfo => true      
      }
}

output 
{
  elasticsearch {
        hosts => "https://192.168.1.12:9200"
        index => "index_name"
        
  }
}</code></pre><p>对于安全的 elasticsearch，可以使用下面的配置：</p><pre><code>input
{
  elasticsearch
      {
        hosts => ["192.168.1.11:9200"]
        index => "index_name"
        docinfo => true 
        user => "elastic"
        password => "elastic_password"
        ssl => true
        ssl_certificate_verification => false
            
      }
}

output 
{
  elasticsearch {
        hosts => "https://192.168.1.12:9200"
        index => "index_name"
        user => "elastic"
        password => "elastic_password"
        ssl => true
        ssl_certificate_verification => false
  }
}</code></pre><h3>索引元数据</h3><p>上述命令将写入一个命名索引。如果要传输多个索引并保留索引名称，则需要在 Logstash 输出中添加以下一行：</p><pre><code>index => "%{[@metadata][_index]}"</code></pre><p>此外，如果您想保留文件的原始 ID，则需要添加：</p><pre><code>document_id => "%{[@metadata][_id]}"</code></pre><p>请注意，设置文档 ID 会大大降低数据传输的速度，因此只有在必要时才保留原始 ID。</p><h2>同步更新</h2><p>上述所有方法都需要相对较长的时间，您可能会发现在等待过程完成时，原始群集中的数据已被更新。</p><p>有多种策略可以同步数据传输过程中可能出现的任何更新，在开始这一过程之前，你应该考虑一下这些问题。您尤其需要考虑</p><ul><li>您有什么方法来识别自数据传输过程开始以来已更新/添加的任何数据（例如，数据中的 &quot;最后更新时间 &quot;字段）？</li><li>您可以使用什么方法来传输最后一个数据？</li><li>是否存在记录重复的风险？通常会有，除非您使用的方法在重新索引时将文档 ID 设置为已知值）。</li></ul><p>下文介绍了实现同步更新的不同方法。</p><h3>1.使用排队系统</h3><p>有些摄取/更新系统使用队列，可以 &quot;重放 &quot;过去 x 天内收到的数据修改。这可以为同步任何更改提供一种手段。 </p><h3>2.从远程重新索引</h3><p>对 &quot;last_update_time&quot;&gt; x 天前的所有项目重复重新索引过程。您可以在重新索引请求中添加一个 &quot;查询 &quot;参数。</p><h3>3.Logstash</h3><p>在 Logstash 输入中，可以添加一个查询来过滤所有 &quot;last_update_time&quot;&gt; x 天前的项目。不过，除非设置了 document_id，否则这一过程会导致非时间序列数据出现重复。</p><h3>4.快照</h3><p>不可能只恢复索引的一部分，因此必须使用上述其他数据传输方法之一（或脚本）来更新数据传输过程后发生的任何更改。</p><p>不过，快照恢复比重新索引/Logstash 快得多，因此可以在传输快照时短暂暂停更新，以完全避免问题。</p>]]></content:encoded>
        </item>
    </channel>
</rss>