<?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[Articles and tutorials from the Search team at Elastic]]></description>
    <copyright><![CDATA[© 2026. Elasticsearch B.V. All Rights Reserved]]></copyright>
    <image>
      <title><![CDATA[开发者体验 - Elasticsearch Labs]]></title>
      <url>https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt1121c0bf0e8a6e65/6a88da6340a1841030ef456f/search-labs-thumbnail.png</url>
      <link>https://www.elastic.co/cn/search-labs/blog/category/developer-experience</link>
    </image>
    <link>https://www.elastic.co/cn/search-labs/blog/category/developer-experience</link>
    <atom:link href="https://www.elastic.co/cn/search-labs/rss/category/developer-experience.xml" rel="self" type="application/rss+xml"/>
    <language><![CDATA[cn]]></language>
    <lastBuildDate>Tue, 29 Sep 2026 00:39:26 GMT</lastBuildDate>
  <item>
    <title><![CDATA[Kibana 仪表板 API：为每种面板类型提供稳定的 API 规范，正式发布前已经过 50 多个团队测试]]></title>
    <description><![CDATA[以代码形式管理 Kibana 仪表板：提交至 Git、跨环境发布，并借助 Kibana API 和 Terraform 实现部署自动化。]]></description>
    <content:encoded><![CDATA[<p><a href="https://dashboardsapispec.kibana.dev/dashboards#tag/Dashboards">Kibana 仪表板和可视化 API</a> 在 Elastic 9.5 中已可用于生产环境，所有订阅级别均可使用，并提供完全向后兼容性。以 JSON 格式定义仪表板，将其提交到 Git，然后使用持续集成和持续部署 (CI/CD) 管道、<a href="https://registry.terraform.io/providers/elastic/elasticstack/latest/docs/resources/kibana_dashboard">Terraform</a> 或您现有的任何工具，将其部署到不同环境。在 <a href="https://www.elastic.co/search-labs/blog/kibana-dashboards-as-code-terraform-api">9.4 技术预览期间</a>，50 多个团队测试了该 API，其中一些团队已将其用于生产环境。9.5 版本还新增了用于<a href="https://dashboardsapispec.kibana.dev/tags.html">标签</a>的终端（技术预览阶段）；<a href="https://dashboardsapispec.kibana.dev/markdowns.html">Markdown</a> 和<a href="https://dashboardsapispec.kibana.dev/links.html#tag/Links">链接</a>面板终端现已在 Elastic Cloud Serverless 中提供，并将在 9.6 中推出。</p><h2>Kibana 仪表板 API 中的向后兼容性意味着什么</h2><p>在技术预览期间，API 结构可能会随版本发生变化。[1]现在已不再如此。正式发布 (GA) 意味着：</p><ul><li><p><strong>完全向后兼容。</strong>新字段和面板类型将陆续添加，但现有字段和行为保持不变。未来如需引入任何破坏性变更，都会经过慎重评估，并且只会在新的 Elastic Stack 主版本中引入。</p></li><li><p><strong>可用于生产环境，并提供全面支持。</strong>该 API 享有 Elastic 提供的全面支持保障。您可以放心地在生产环境中使用该 API，进行自动化部署、跨环境发布以及以编程方式管理仪表板。</p></li></ul><h2>Kibana 新增用于标签、Markdown 和链接面板的 API 终端</h2><p>Elastic 9.5 还新增了一个用于<a href="https://dashboardsapispec.kibana.dev/tags.html"><strong>标签</strong></a>的独立终端，让您可以对仪表板进行分类和筛选。现在，您可以通过专用 CRUD 终端以编程方式管理这些标签，从而更轻松地跨环境大规模组织仪表板。	</p><p>新的 <a href="https://dashboardsapispec.kibana.dev/markdowns.html"><strong>Markdown</strong></a> 和<a href="https://dashboardsapispec.kibana.dev/links.html#tag/Links"><strong>链接</strong></a>面板终端现已在 Serverless 中提供，并将在下一个 Elastic Stack 版本 (9.6) 中推出。</p><h2>Kibana 仪表板 API 支持哪些面板类型？</h2><p>仪表板 API 支持 9.5 中的所有<em>按值</em>面板（即直接在仪表板中定义的面板，而非保存以供重复使用的库面板）。每种受支持的面板类型都有一个类型化且经过验证的架构。</p><p><strong>面板类型</strong></p><p><strong>状态</strong></p><p>XY 图表</p><p>支持</p><p>指标</p><p>支持</p><p>饼图</p><p>支持</p><p>仪表盘图</p><p>支持</p><p>热图</p><p>支持</p><p>数据表</p><p>支持</p><p>树状图</p><p>支持</p><p>Discover 会话</p><p>支持</p><p>控件</p><p>支持</p><p>Markdown</p><p>支持</p><p>链接</p><p>支持</p><p>ML 面板</p><p>支持</p><p>Observability 面板</p><p>支持</p><p>Maps</p><p>即将推出</p><p>Vega</p><p>即将推出</p><h2>如何以代码形式管理 Kibana 仪表板</h2><p>仪表板 API 支持一套完整的以代码形式管理仪表板的工作流：将仪表板导出为简洁、可进行差异比较的 JSON，将其提交到 Git 并作为唯一可信来源，在拉取请求中审查更改，然后将同一定义部署到开发、预发布和生产环境。一旦以代码形式管理仪表板，就应将 Git 作为唯一可信来源：下次部署会覆盖直接在 UI 中所做的更改。</p><p>在空间、集群或阶段之间迁移仪表板时，主要挑战在于仪表板会按 ID 引用 Data view、库可视化等对象。由于这些 ID 是自动生成的，且不同环境中的 ID 各不相同，因此从一个环境导出的仪表板可能会引用另一个环境中不存在的对象。有三种方法可以处理这个问题，以下按自动化程度从高到低列出：</p><ul><li><p><strong>使用 Terraform。</strong><a href="https://registry.terraform.io/providers/elastic/elasticstack/latest/docs/resources/kibana_dashboard">Elastic Stack Terraform 提供程序</a>会跟踪每项资源，并自动映射各环境中的 ID，因此当您将仪表板从开发环境发布到生产环境时，引用可保持一致。</p></li><li><p><strong>定义按值的 </strong><a href="https://www.elastic.co/docs/explore-analyze/visualize/esorql"><strong>Elasticsearch 查询语言 (ES|QL) 面板</strong></a><strong>。</strong>构建面板时，可移植性最高的方法是在仪表板中直接使用 ES|QL 定义其可视化。<a href="https://www.elastic.co/docs/explore-analyze/query-filter/languages/esql-kibana">ES|QL</a> 查询会从查询中指定的索引读取数据，因此面板不会包含对 Data view 或库对象的外部引用。这样便可获得一个完全自包含、可移植的仪表板。</p></li><li><p><strong>分配一致的 ID。</strong>如果您引用 Data view、库可视化等已保存对象，请使用 PUT (upsert) 并指定 ID 来创建这些对象，而不要使用会自动生成 ID 的 POST。使用易读的 ID（例如 logs-prod），这样更便于在不同环境中重复使用和识别。</p></li></ul><p><a href="https://www.elastic.co/docs/explore-analyze/dashboards/manage-dashboards-as-code#dashboards-as-code-portability">有关这些可移植性模式以及完整的以代码形式管理仪表板的工作流的详细介绍，请参阅“以代码形式管理仪表板”文档。</a></p><h3>使用 PUT 通过仪表板 API 创建 Kibana 仪表板</h3><p>下面是一个简单示例：使用 PUT 而非 POST 创建包含指标面板的仪表板，并以仪表板名称 (service-health-overview) 作为自定义 ID。同样的逻辑也适用于创建保存到库中的独立可视化。</p>PUT kbn:/api/dashboards/service-health-overview
{
  "title": "服务运行状况概览",
  "description": "通过 API 管理的关键服务指标",
  "tags": [
    "production",
    "sre-team"
  ],
  "panels": [
    {
      "type": "vis",
      "grid": {
        "x": 0,
        "y": 0,
        "w": 12,
        "h": 8
      },
      "config": {
        "title": "错误率 (5xx)",
        "type": "metric",
        "data_source": {
          "type": "esql",
          "query": "FROM logs-* | WHERE http.response.status_code &gt;= 500 | STATS error_rate=count(*) BY host.name"
        },
        "metrics": [
          {
            "type": "primary",
            "column": "count"
          }
        ]
      }
    }
  ]
}<h2>Kibana 仪表板 API 路线图：Maps、Vega 和独立终端</h2><p>我们正在积极扩展 API 的功能范围。下一步将支持 Maps 和 Vega 面板，并为其添加类型化架构。我们还在为 Discover 会话（除了现有的仪表板面板支持之外）、Vega、Maps 和注释构建独立的 CRUD 终端，使其与仪表板生命周期解耦。</p><p>有关完整的架构定义，请参阅<a href="https://dashboardsapispec.kibana.dev/dashboards#tag/Dashboards">仪表板 API 文档</a>。对于 Terraform 用户，<a href="https://registry.terraform.io/providers/elastic/elasticstack/latest/docs/resources/kibana_dashboard">Elastic Stack Terraform 提供程序</a>支持正式发布 (GA) 的仪表板 API。</p><h2>注意</h2><ol><li><p>核心终端自技术预览以来未发生变化。如果您基于 9.4 构建了集成，这些集成在 9.5 中也可正常运行。仅有两项轻微的破坏性变更，分别影响仪表板列表和时长单位格式，详见<a href="https://www.elastic.co/docs/release-notes/kibana/breaking-changes">此处</a>。</p></li></ol>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/dashboards-as-code-kibana-api</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/dashboards-as-code-kibana-api</guid>
    <category><![CDATA[Kibana]]></category>
    <category><![CDATA[开发者体验]]></category>
    <category><![CDATA[集成]]></category>
    <dc:creator><![CDATA[Teresa Alvarez Soler]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt8ed7e33de291f255/6a730619c8b7ac02b251f9d3/image1.png" length="0" type="image/png"/>
    <pubDate>Wed, 05 Aug 2026 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[为 Elastic Cloud Serverless 和 Elasticsearch 引入统一的 API 密钥。]]></title>
    <description><![CDATA[了解 Elastic 如何通过全局分布式 IAM 架构，在 Serverless 中统一控制平面与数据平面的身份验证。使用同一 API 密钥访问 Cloud 和 Elasticsearch API。]]></description>
    <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</strong><strong> 和 Kibana</strong> API 进行身份验证。您现在可以使用单一凭证来管理组织的资源<em>并</em>执行数据操作，例如 Elasticsearch 查询语言 (ES|QL) 查询、数据摄取和告警。</p><p>下面我们来看看我们构建这一功能的原因、如何设计全局分布式身份层来实现这一目标，以及它如何为跨项目搜索奠定基础。</p><h2>秘密管理负担</h2><p>围绕数据平台构建可靠的 CI/CD 管道、GitOps 工作流或 Terraform 自动化，都伴随着一项隐性成本：秘密信息蔓延。</p><p>在旧模式下，开发人员面临割裂的身份验证体验：</p><ul><li><p><strong>控制平面 (Elastic Cloud API 密钥)：</strong>组织级密钥，组织级作用域的密钥，用于通过 <a href="https://www.elastic.co/docs/api/doc/cloud/">Elastic Cloud API</a> 创建项目、邀请用户和管理计费。</p></li><li><p><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 进行交互。</p></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><p><strong>以前：</strong>Elastic Cloud API 密钥严格来说是控制平面令牌。它可以创建项目、管理计费和邀请用户，但存在一个硬边界：它不能用于调用这些项目内部的 Elasticsearch 或 Kibana API。您始终需要第二个特定于项目的密钥来执行数据操作。</p></li><li><p><strong>现在：</strong> 在创建 Elastic Cloud API 密钥时，选择 <strong>Cloud、Elasticsearch 和 Kibana API</strong> 访问权限，Serverless 的硬边界就被移除了。该 API 密钥成为一个真正统一的凭证。它保留了管理组织基础架构的能力，同时获得了跨任何已授权 Serverless 项目进行查询、摄取和分析数据的原生访问能力。</p></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://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltda0a18945295aa84/6a1707bd509168fab4e1ba19/c4f802f130655290cd474b283001a954d14c3088-2801x1681.png" alt="Elastic Cloud 界面显示 API 密钥页面，其中包含一个打开的创建 API 密钥模态窗口，包括名称、过期日期和角色分配字段。" /><p>下面说明如何使用 Elastic Cloud API 以编程方式创建统一密钥。请注意 <code>application_roles</code> 数组，正是它授予了密钥对 Elasticsearch 数据平面的原生访问权限：</p>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"]
          }
        ]
      }
    }
  }'<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://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltf4fa84c3f33f88f7/6a1707baacf088989abe9a8e/3e38d7a862b9981523c5393c441b92eae13aeb90-2401x1351.webp" alt="序列图：展示客户端请求携带 Cloud API 密钥，流经 Elasticsearch Serverless、区域 IAM 服务以及分布式数据库副本，最终返回结果的过程。" /><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><p>请参阅 <a href="https://www.elastic.co/docs/deploy-manage/api-keys/elastic-cloud-api-keys">Elastic Cloud API 密钥文档</a>，了解如何分配堆栈访问权限。</p></li><li><p>请参考 <a href="https://www.elastic.co/docs/api/doc/cloud/operation/operation-create-api-key">Create API key（Elastic Cloud API）</a> 文档，以实现密钥自动生成。</p></li><li><p>查看 <a href="https://www.elastic.co/docs/deploy-manage/api-keys">Elastic API 密钥</a>，了解 Elastic 平台中各类密钥的完整对比。</p></li></ul><p>立即开始在 <a href="https://cloud.elastic.co/registration">Elastic Cloud</a> 上构建或继续您的构建之旅。</p><h2>免责声明</h2><p>本文中描述的任何功能或功能性的发布和时间均由 Elastic 自行决定。当前尚未发布的任何功能或功能性可能无法按时提供或根本无法提供。</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/elastic-cloud-api-keys-unified-serverless</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/elastic-cloud-api-keys-unified-serverless</guid>
    <category><![CDATA[Elastic Cloud Serverless]]></category>
    <category><![CDATA[开发者体验]]></category>
    <dc:creator><![CDATA[ Alex Chalkias]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt16ca1a6af7e5bab8/6a1707b7a6c2b900abe7965b/864e229f00eb2018084f13dd7f0e390e18383ed4-1980x1188.png" length="0" type="image/png"/>
    <pubDate>Mon, 20 Apr 2026 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[使用 Elastic 工作流监测 Kibana 仪表板的浏览情况]]></title>
    <description><![CDATA[了解如何使用 Elastic 工作流每 30 分钟收集一次 Kibana 仪表板视图指标，并将其索引到 Elasticsearch 中，以便您可以基于自己的数据构建自定义分析和可视化。]]></description>
    <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><p>运行 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>集群</p></li><li><p><a href="https://www.elastic.co/docs/explore-analyze/workflows/get-started#workflows-prerequisites">已启用工作流</a>（高级设置）</p></li></ul><h2>步骤 1：在 <a href="https://www.elastic.co/docs/explore-analyze/query-filter/tools/console">开发工具</a> 中探索原始数据</h2><p>在开始构建之前，我们先了解目前有哪些数据。Kibana 将其大部分配置和元数据作为<a href="https://www.elastic.co/docs/api/doc/kibana/group/endpoint-saved-objects">已保存对象</a>存储在专用的内部索引中。Kibana 通过这种方式跟踪的事项之一是仪表板查看次数，它使用一种名为“使用计数器”的特殊保存对象类型来实现。您可以在开发工具中直接查询它们：</p>GET kbn:/api/saved_objects/_find?type=usage-counter&amp;filter=usage-counter.attributes.domainId:"dashboard"%20and%20usage-counter.attributes.counterType:"viewed"&amp;per_page=10000<p>响应类似如下：</p>{
  "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
      },
      ...
    }
  ]<p><code>counterName</code> 字段是仪表板 ID，而 <code>count</code> 是该仪表板在特定日期的累计查看次数。Kibana 每天会为每个仪表板创建一个计数器对象；您可以在对象 ID 中看到日期后缀 (...viewed:server:20260310)。随着用户打开仪表板，计数在一天中不断增长。</p><p>我们不会在索引中复制这种每日文档模型，而是为每个工作流执行创建一个文档。每份文档都记录了该仪表板在捕获时当天的累计浏览量。</p><h2>步骤 2：创建目标索引</h2><p>我们需要一个索引来存储仪表板视图快照。以下命令创建了明确的映射，以便我们稍后进行聚合和可视化。在开发工具中运行此命令：</p>PUT dashboard-views
{
  "mappings": {
    "properties": {
      "captured_at": {
        "type": "date"
      },
      "dashboard_id": {
        "type": "keyword"
      },
      "dashboard_name": {
        "type": "keyword"
      },
      "view_count": {
        "type": "integer"
      }
    }
  }
}<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>name: dashboard-views-ingestion
triggers:
  - type: scheduled
    with:
      every: 30m

steps:
  - name: fetch_dashboard_views
    type: kibana.request
    with:
      method: GET
      path: &gt;-
        /api/saved_objects/_find?type=usage-counter&amp;per_page=10000&amp;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' }}"<p>在下一节中，我们将逐步分解工作流。</p><h3>工作流如何运作</h3><h4>触发</h4><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt7672aa533b4bc9ed/6a17dc5b420229d07c29f4d4/5670991d65c64ee833924225c2d375a1be868b13-325x162.png" alt=" 计划触发器" /><p>工作流每 30 分钟按计划触发运行一次。这样我们就能获得时序数据，而不会对 API 造成过多压力。</p><h4>fetch_dashboard_views</h4><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltab2a16f1f11304ea/6a17dc5d25daab26f608a117/66eaec147c3d01c524c67cf1c7f663ac56a3259d-812x215.png" alt=" 获取仪表板" /><p>使用 <code>kibana.request</code> 调用 Kibana 已保存对象 API。无需进行身份验证设置：工作流引擎会根据执行上下文自动附加正确的标头。</p><h4>index_each_dashboard（循环）</h4><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blte6b2611b0216555e/6a17dc5f445de95b584cffe5/aad45e8aed8dc81ded6260cd6199ff78dcffe3b4-1892x290.png" alt="为每个仪表板建立索引" /><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. </strong><strong><code>fetch_dashboard_name</code></strong><strong>：</strong></p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltb3733b3c24629abf/6a17dc60a292993fe3d02b75/db21ec5094b743018b9cd66c5052681f14c7d7e3-1999x431.png" alt="获取仪表板名称" /><p>通过调用 <code>GET /api/saved_objects/dashboard/{id}</code> 来解决人类可读的仪表板标题。我们添加了 <code>on-failure: continue: true</code>，以便如果仪表板被删除但仍有浏览计数器，循环就会继续，而不是导致整个执行失败。</p><p><strong>2. </strong><strong><code>index_doc</code></strong><strong>：</strong></p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt385c1f29d717c280/6a17dc62faa91353cb93c759/f49dd0c9f0817bb1e1e5d9f4a2b05d13ef331054-1999x626.png" alt=" Elasticsearch 请求" /><p>使用 <code>POST /dashboard-views/_doc</code>（无显式 ID）为每个文档建立索引，这样 Elasticsearch 就能自动生成 ID。这样，每次运行时都会创建一个新文档，从而随着时间推移构建浏览次数的历史记录，而不是覆盖之前的快照。</p><p>有两点值得注意：</p><ul><li><p><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 不会将其映射为日期。</p></li><li><p><code>view_count</code> 使用 <code>${{ }}</code> 语法和 <code>| plus: 0</code> 来保留数值类型。使用<code>{{ }}</code> 会将其显示为字符串，这将阻止在仪表板中进行数学运算。</p></li></ul><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt3b94bb6c22c0253e/6a17dc6425daab37a508a11b/6d48c8784d5df6192e8b5175e69dbab5098194bc-919x774.png" alt="" /><p><em>UI 允许您可以很好地对每个工作流步骤进行故障排查。</em></p><h2>第 4 步：构建统计仪表板</h2><p>一旦工作流运行了几次并收集了数据，使用 dashboard-views Data view在 Kibana 中创建一个新的仪表板。</p><p>一些入门面板：</p><ul><li><p><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>。这将显示每个仪表板当前的每日浏览量。</p></li><li><p><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> 细分。由于每次运行都会添加一个新文档，因此使用最后一个值来获取每个时间分桶的峰值计数，而不是重复计数的总和。</p></li><li><p><strong>当前快照：</strong>使用<a href="https://www.elastic.co/docs/explore-analyze/visualize/charts/tables"><strong>数据表</strong></a>和最新的 <code>captured_at</code> 来显示所有仪表板上最新的浏览量。</p></li></ul><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt18d0390e0526b215/6a17dc65a292991da7d02b79/e245b95f67daf76a2aaf4cb9df2c75ef4cfef582-1462x747.png" alt="" /><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><p><a href="https://www.elastic.co/docs/explore-analyze/workflows">Elastic 工作流</a></p></li><li><p><a href="https://www.elastic.co/docs/api/doc/kibana/">Kibana API</a></p></li></ul>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/monitor-kibana-dashboard-views-elastic-workflows</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/monitor-kibana-dashboard-views-elastic-workflows</guid>
    <category><![CDATA[开发者体验]]></category>
    <dc:creator><![CDATA[Gustavo Llermaly]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltef604bbb6dee6be0/6a17dc67a29299db23d02b7d/0ed94ce00962287b5507f45c92ecb60fdcbf2718-1280x720.png" length="0" type="image/png"/>
    <pubDate>Fri, 03 Apr 2026 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Kubernetes 上的依赖管理]]></title>
    <description><![CDATA[如何使用 Renovate CLI 和 Argo 工作流简化 Kubernetes 上的依赖管理。]]></description>
    <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://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltc99617fc3eed538d/6a170ea9964cea459d08bc67/e14d9f98d4eccaa08a335d5bd23d88e5debbb344-1600x1103.png" alt="Elastic 的依赖管理" /><h3><strong>挑战：我们如何在拥有大量存储库的大型组织中扩展依赖管理平台？</strong></h3><p>我们的依赖管理平台一次只能处理一个存储库，由于我们拥有大量存储库，这种顺序处理模型已无法跟上需求。我们已经确定，问题在于我们的依赖管理工具的<strong>单个实例</strong>无法处理我们庞大且不断增长的存储库列表这一概念。存储库在队列中等待，有时长达数小时。我们超过 50% 的存储库甚至没有每天进行处理。这意味着超过 50% 的存储库扫描间隔时间超过 24 小时。</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt0d205fd379e3c07a/6a170eab961e691e1fc4cfca/45ade5bda08f82bed0b3d0d3736cbd6f056e7a4e-1312x816.jpg" alt="依赖管理问题" /><p>大型存储库由于代码库规模庞大且有多个开放 PR，因此会造成更大的瓶颈。GitHub webhook 事件打乱了顺序。由于扫描时间无法预测，自动合并变得不可靠。我们曾向用户承诺扫描频率，但未能兑现。</p><h3><strong>决定内部构建：满足 Elastic 独特的扩展和安全需求</strong></h3><p>虽然我们考虑了商业选项，包括 <strong>Mend 的 Renovate 自托管企业版</strong>，但在 Elastic 内部，我们有几个关键计划正在加速推进。</p><p>我们决定构建一个内部平台，这一决定源于我们认识到，只有深度定制的解决方案才能满足 Elastic 不可妥协的特殊要求：</p><ol><li><p><strong>投资我们的内部开发者平台：</strong>当时，我们已经开始大力投资内部开发者平台。我们正在讨论和设计每项服务都能融入其中的方法。这意味着我们希望为我们的依赖管理平台测试我们自己的规则和做法。除此之外，新的指南即将出台，我们希望在此之前设计好平台。</p></li><li><p><strong>本地集成和工作流程定制：</strong>我们需要与内部工具和内部流程直接集成。例如，我们希望通过服务目录（后台）将配置集中为代码。我们对后台的使用有特殊需求，希望我们的平台能与之兼容。因此，尽管可以将 Renovate 自托管 API 与我们的后台自动化结合使用，但这并不能完全覆盖我们的内部流程。</p></li><li><p><strong>针对 Elastic 的深度防御安全：</strong>我们严格的安全合规要求为我们的生态系统量身定制安全机制。我们正在努力<a href="https://entro.security/blog/how-elastic-scaled-secrets-nhi-security-elastics-playbook-from-visibility-to-automation/">强化对“非人类身份”的使用。</a>这种访问权限的强化方式意味着，如果工具不支持 GitHub 内部的这种实现方式，那么非标准的身份验证方法将无法使用现成的工具。我们的工作流包括实施父子工作流密钥加密模式，并使用临时的一次性 GitHub 令牌。在我们复杂的多云环境中，内部构建是嵌入这些独特的安全层并最大限度减少攻击面的唯一实用方法。</p></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://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt3548539ab52fbb79/6a170eac0c48573da601ab26/5560ed20e2bd9ecdd574a9c835126d12b24c332f-1600x1157.png" alt="Kubernetes 中的依赖管理工作流概述" /><p><strong>亮点：</strong>我们正以一种创新的方式使用经过实战验证的开源项目，为所有这些项目提供新的工作示例，同时为我们的团队提高开发速度并减少 CVE。</p><h2><strong>依赖管理架构：四个微服务</strong></h2><p>该平台由四个定制组件构成：</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt6451ff19da4db511/6a170eaec1e8a562e0f88378/2b3d4046c05bb261e45d40c59f864eb51fb9eaa9-1217x1600.png" alt="Kubernetes 中的依赖管理组件" /><h3><strong>工作流 Operator (Go/Kubebuilder)</strong></h3><p>Kubernetes Operator 通过三个自定义资源定义 (CRD) 管理工作流生命周期：</p><ul><li><p><strong>RepoConfig CRD：</strong>存储库配置的单一事实来源。</p></li></ul><p>这就是在 Operator 中定义 RepoConfig 的方式：</p>// 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"`
}<p>这就是 RepoConfig 实例的样子：</p>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<ul><li><p><strong>父级 CRD：</strong>管理用于计划扫描的 CronWorkflow。</p></li></ul><p>在父控制器的协调循环内部，我们确保创建并保持工作流设置的最新状态，甚至在必要时将其删除。</p><p>首先，它会获取一些全局配置的工作流设置：</p>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",
	}<p>它确保互斥 configmap 是最新的，以防止类似的工作流同时运行：</p>	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)<p>然后创建工作流管理器，该结构将创建或更新 CronWorkflows 和工作流模板：</p>	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<ul><li><p><strong>子 CRD：</strong>使用每个存储库的资源管理 WorkflowTemplate。</p></li></ul><p>子控制器与父控制器有类似的协调职责，但这次它负责子命名空间中将由父工作流触发的工作流模板。</p>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
}<img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blta735156ba6e370ef/6a170eaf7d8d6706fd70e7e4/7ac70492a1266ba02cb8afbafc5a486cb38a0edc-1600x1290.png" alt="Kubernetes 中的依赖管理工作流" /><p>多控制器模式提供了明确的分隔：RepoConfig 控制器处理加入/退出，父控制器管理调度，子控制器处理执行模板。</p><h3><strong>GitHub 事件网关 (Go)</strong></h3><p>一个安全的 Webhook 代理，用于接收 GitHub 的 Webhook，验证签名，按组织/存储库进行筛选，并将其路由到 Argo Events。我们构建了 10 个不同的传感器，分别对依赖仪表板交互、PR 事件和软件包更新做出响应。</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt7e748ceb93c13a5a/6a170eb1a6c2b908f8e797b4/4828625456cbd6efa8020a20f10d23f294f98a02-1306x1600.png" alt="Kubernetes 上的依赖仪表板交互" /><p>此网关可通过以下方式与 GitHub 应用集成：</p><ul><li><p>验证传入的 GitHub Webhook 签名以确保安全。</p></li><li><p>将有效事件转发给 Argo Events EventSource，并附上所有相关标头和身份验证。</p></li><li><p>我们还在 EventSource 上配置了一个 authSecret，并在转发的请求中将其作为 Bearer 标头提供。</p></li><li><p>提供日志记录、指标和重试逻辑。</p></li></ul><p>它对每个 GitHub 事件请求执行各种验证。</p><p>它确保某些 HTTP 属性存在：</p>// 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
}<p>同时，它还会验证每个请求的签名及其组织。</p>// 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, &amp;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
}<p>最后，它会根据事件类型路由到 Argo Events：</p>	// 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<p>在 Argo Events 方面，有 10 个传感器在监视 Argo Events EventBus 上的新事件。</p>apiVersion: argoproj.io/v1alpha1
kind: Sensor
metadata:
  name: {{ .Values.sensors.packageUpdateOnDefaultBranch.name }}
  namespace: {{ .Release.Namespace }}
spec:
  eventBusName: {{ .Values.eventBus.name }}<p>然后，脚本会应用每个传感器的逻辑：</p>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<h3><strong>后台同步器 (Go)</strong></h3><p>此过程将轮询我们的服务目录（后台）以获取存储库真实资源实体，将其转换为 RepoConfig CRD，并使平台与配置更改保持同步。更改将在三分钟内生效。</p>repoMap := make(map[string]map[string]interface{})
			for i := range entities {
				entity := &amp;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, &amp;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, &amp;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)<p>最后，它将数据写入 RepoConfig 实例。</p><h3><strong>工作流基础（混合：JavaScript、Go、Helm）</strong></h3><p>基础层包含 Helm 图表、JavaScript 配置、带有加密支持的适用于 Renovate CLI 的 Go 封装器，以及适用于 Alpine 软件包的自定义 APK 索引器。</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt4c1b5b840854ddf5/6a170eb47d8d67694e70e7e8/908d19278face3ce1119dbee9146c1264b6e2f30-1600x873.png" alt=" 用于 Kubernetes 上依赖管理的基础组件" /><h2><strong>自助服务配置</strong></h2><p>团队通过后台声明式配置其存储库：</p>spec:
  renovate:
    enabled: true
    config:
      resourceGroup: LARGE      # SMALL | MEDIUM | LARGE  
      runFrequency: "0 */4 * * *"  # Every 4 hours<p>资源组根据存储库大小分配 CPU 和内存：</p><ul><li><p><strong>小型：</strong>500m CPU，1Gi 内存。</p></li><li><p><strong>中型：</strong>1000m CPU，2Gi 内存。</p></li><li><p><strong>大型：</strong>2000m CPU，4Gi 内存。</p></li></ul><p>配置受版本控制、可审计并自动应用。</p><h2><strong>父子模式</strong></h2><p>执行模型采用父子工作流模式：</p><ul><li><p><strong>父工作流：</strong>按计划运行的轻量级 CronWorkflow。加密机密，确定是否应运行扫描，将配置传递给子项。</p></li><li><p><strong>子工作流：</strong>运行 Renovate CLI 的临时 Pod。动态分配资源，在隔离环境中解密机密，完成后终止。</p></li></ul><p>这种分离提供了安全性（在父级加密机密）、资源优化（父级使用最少资源）以及可扩展性（子级并行运行）。</p><h2><strong>结果</strong></h2><h3><strong>性能转换</strong></h3><ul><li><p><strong>之前：</strong>每次处理一个存储库，有些存储库可能一天甚至更长时间都无法得到处理，每天扫描量不足 1000 次。</p></li><li><p><strong>之后：</strong>超过 100 次并发扫描，通常每天 8,000 次扫描，最多可达 10000 次记录的扫描，仅受我们愿意投入的资源数量以及处理 GitHub 速率限制的方式的限制。</p></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><p>指标</p><p>场景 A（工作流）</p><p>场景 B（长时间运行的单个 pod）</p><p>设置</p><p>8,000 个 pod（1 个 vCPU / 2GB）</p><p>1 个 pod（8 个 vCPU / 16 GB）*</p><p>持续时间</p><p>每次 10 分钟</p><p>连续 12 天</p><p>总工作时间</p><p>1,333 计算小时</p><p>288 个计算小时</p><p>总成本</p><p>$65.83</p><p>$113.75</p><p>不过，我们应考虑到，我们的工作负载默认设置为“小型”，绝大多数工作负载在 0.5 CPU 和 1G RAM 的情况下成功运行，只有少数需要更改为中型或大型。让我们看看，如果 60% 的工作负载运行在“小型”级别，30% 运行在“中型”级别，10% 运行在“大型”级别会发生什么情况，这更接近实际情况。</p><p>指标</p><p>场景 A（混合群）</p><p>场景 B（长时间运行）</p><p>战略</p><p>8,000 个 Pod（混合尺寸）</p><p>1 个 pod（8 个 vCPU / 16 GB）*</p><p>持续时间</p><p>每次 10 分钟</p><p>连续 12 天</p><p>总成本</p><p>$52.66</p><p>$113.75</p><p>保存</p><p>61.09 美元（便宜 54%）</p><p>—</p><p>我们可以看到，在相同的输出下，我们目前的配置成本效益要高得多。</p><h3><strong>增强安全</strong></h3><ul><li><p>临时 GitHub 令牌（暴露时间为几分钟而不是几天）。</p></li><li><p>通过基于角色的访问控制 (RBAC) 边界实现命名空间隔离。</p></li><li><p>父工作流中的机密数据静态加密。</p></li><li><p>移除了直接访问金库的权限。</p></li></ul><h3><strong>可预测的性能</strong></h3><p>有了有保障的扫描频率，我们终于可以设定服务水平目标 (SLO)。自动合并功能运行可靠。团队信任平台能够兑现承诺。</p><h2><strong>关键架构决策</strong></h2><p>以下是一些塑造平台外观的里程碑式设计决策。</p><ul><li><p><strong>为何采用父子工作流？</strong></p></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><p><strong>为什么采用可自助服务？</strong></p></li></ul><p>消除团队在存储库配置方面的瓶颈是一项关键要求。我们的使命是构建一个可扩展的<strong>自助服务平台</strong>，能够支持各种用例。我们认识到，鉴于存储库的庞大数量，为每项配置更改充当“<strong>守门员</strong>”的做法是不可持续的。相反，我们采取了一种赋能的理念：提供“轨道”（基础设施和<strong>保障措施</strong>），同时赋予用户驾驶“列车”（执行和自定义）的权力。我们相信，这种向<strong>团队自主权</strong>的转变，能让用户根据自己的具体运营需求来定制系统，从而显著提高了生产率。</p><ul><li><p><strong>为什么使用 Kubernetes Operator 模式？</strong></p></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><p><strong>为什么要设计 GitHub 事件网关？</strong></p></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><p><strong>客户至上：</strong>平台为用户而构建。因此，将用户需求放在首位非常重要。这将平台塑造成高效设计的基础设施和应用程序，从而减少与用户的摩擦，简化平台的扩展，并使其易于采用。</p></li><li><p><strong>空间与时间：</strong>有时，最顺畅的道路也会通向<strong>变幻莫测的沙漠</strong>。我们最初尝试优化现有的顺序处理模型，但这并未解决我们的问题；事实上，它只是引入了更多复杂性和未解决的问题。<strong>重新构建</strong>并行处理平台的大胆决定需要大量的前期工作。然而，它最终为可持续的平台增长铺平了道路，并几乎消除了繁琐的日常管理工作。</p></li><li><p><strong>视情况而定：</strong>平台无法孤立运作；其成功取决于它与更广泛的生态系统的整合程度。在我们的案例中，与<strong>后台</strong>的集成至关重要，因为它是无缝服务导入的真实来源。同样，连接到 <strong>Artifactory</strong> 使我们能够高效地管理私有包更新，而且这些重要的集成远不止于此。</p></li><li><p><strong>进步，简单即完美：</strong>在整个实施过程中，我们不断对最初的假设进行压力测试，并在新障碍出现时进行调整。我们没有被完美主义所束缚，而是采取<strong>迭代的方法</strong>，逐一解决挑战，并根据实际情况调整迁移策略。</p></li></ol><h2><strong>未来发展</strong></h2><p>该平台的交付使我们能够开展更有意义的工作，这将有助于我们改善平台的用户体验和效率。一些示例包括：
</p><ul><li><p><strong>增加并规范自动合并的采用</strong></p></li></ul><p>自动合并功能通过消除繁琐的手动任务，显著加快了团队的工作进度。然而，我们需要确保设立严格的<strong>防护措施</strong>，确保这种速度提升不以牺牲安全性为代价。
</p><ul><li><p><strong>改善围绕最终用户体验的可观测性</strong></p></li></ul><p>我们路线图的一个重要优先事项是增强可观测性，不仅是在平台层面，而且特别是从<strong>最终用户的角度</strong>。虽然捕获基础设施指标很简单，但要理解实际的用户体验需要更深入的见解。我们正在努力定义以用户为中心的核心关键性能指标 (KPI)，以便我们的遥测技术能够在问题升级为用户投诉<strong>之前</strong>检测到摩擦点和性能问题。</p><ul><li><p><strong>消除障碍以促进更广泛的应用</strong></p></li></ul><p>展望未来，我们的首要任务是找出并消除任何阻碍平台采用的障碍。无论这需要开发新的集成还是部署特定的功能集，我们都致力于数据驱动的规划。我们已成功构建了一个专为扩展而设计的平台；现在我们的重点转向<strong>最大限度地发挥其潜力</strong>。
</p><h2><strong>了解全貌</strong></h2><p>依赖管理工作流项目展示了一个更广泛的原则：<strong>当您需要将开源工具扩展到其默认部署模型之外时，原生的 Kubernetes 模式提供了前进的道路</strong>。</p><p>通过拥抱：</p><ul><li><p>用于配置的 CRD。</p></li><li><p>适用于生命周期管理的 Operator。</p></li><li><p>用于响应的事件驱动架构</p></li><li><p>用于部署的 GitOps。</p></li></ul><p>我们构建了可独立于所管理的存储库数量进行扩展的编排。无论管理的是 100 个还是 1,000 个存储库，扫描单个存储库的性能都相同。</p><p>公布关键的 CVE 时，我们现在能在几分钟内给出答案，而不是几小时。这就是瓶颈和竞争优势的区别。</p><h2><strong>致谢</strong></h2><p>该平台建立在优秀的开源工具之上：</p><ul><li><p><strong>Kubebuilder：</strong>用于启动 Kubernetes Operator 的开源框架，这些 Operator 可引导和编排工作流。[<a href="https://github.com/kubernetes-sigs/kubebuilder">1</a>][<a href="https://book.kubebuilder.io/">2</a>]</p></li><li><p><strong>后台：</strong>构建服务目录所基于的开源框架，也是我们获取事实依据的来源。[<a href="https://github.com/backstage/backstage">1</a>][<a href="https://backstage.io/">2</a>]</p></li><li><p><strong>Argo 工作流和 Argo 事件：</strong>用于编排复杂流程并基于事件添加动态处理的开源套件。[1][<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>]</p></li><li><p><strong>Renovate CLI：</strong>处理存储库的开源依赖管理工具。[<a href="https://github.com/renovatebot/renovate">1</a>][<a href="https://docs.renovatebot.com/getting-started/running/">2</a>]</p></li></ul><p>*尽管我们的工作负载不一定在 AWS 上运行，而是在完整的 Kubernetes 集群上运行，但我们还是以 AWS Fargate 的定价模型作为单个 Pod 成本的参考。</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/dependency-management-kubernetes</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/dependency-management-kubernetes</guid>
    <category><![CDATA[开发者体验]]></category>
    <dc:creator><![CDATA[Nikos Fotiou]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt6033995d6660d149/6a170eb5839dfa63f1dcff9b/00519840e6eec7101c1fb096afcae976ee0c454e-1280x720.png" length="0" type="image/png"/>
    <pubDate>Thu, 19 Feb 2026 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[介绍 Kibana 中的 Elasticsearch 查询规则用户界面]]></title>
    <description><![CDATA[了解如何使用 Elasticsearch 查询规则用户界面，在 Kibana 中使用可定制的规则集从搜索查询中添加或排除文档，而不影响有机排名。]]></description>
    <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><p><strong>突出显示促销列表或销售</strong>：在顶部显示促销或赞助商品。</p></li><li><p><strong>根据上下文或地理位置排除</strong>：当当地法规不允许显示某些项目时，隐藏这些项目。</p></li><li><p><strong>优先处理关键结果</strong>：确保热门搜索或固定搜索始终排在前面，无论有机搜索排名如何。</p></li></ul><p>要访问界面并与这些工具互动，需要点击 Kibana 侧边菜单，然后转到相关性下的<strong>查询规则</strong> <strong>：</strong></p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltac12541cddd58e36/6a170853a29299941cd00fc2/242e33e89d1a07ffa0e76009c46b3a9236722741-458x1010.png" alt="在相关性下访问 Elasticsearch 中的查询规则" /><p>查询规则菜单弹出后，点击<strong>创建第一个规则集：</strong></p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltcc28329c0f3c3aa9/6a17085547d49c67e22d893b/30b3a91bbbf243d314cf38298e01ca5cff784430-1600x945.png" alt="在 Elasticsearch 中创建第一个查询规则集" /><p>接下来，您需要为规则集命名。</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltb37d271297a4f148/6a170856a29299782cd00fc6/26c5462f88678867776f933b5655ca0df0d72a16-708x446.png" alt="在 Elasticsearch 中命名查询规则集" /><p>定义每条规则的表格有三个关键部分：</p><ul><li><p><strong>标准</strong>：适用规则必须满足的条件。例如，"当 query_string 字段包含<em>Christmas</em>值时 "或 "当 country 字段为<em>CO 时"。</em></p></li><li><p><strong>行动</strong>：这是您希望在条件满足时发生的事情。它可以被固定（将文档固定到顶部结果）或排除（隐藏文档）。</p></li><li><p><strong>元数据</strong>：这些字段在查询运行时会随查询一起出现。它们可以包括用户信息（如位置或语言）以及搜索数据（query_string）。这些值是标准用于决定是否应用规则的值。</p></li></ul><h2>例如：热门项目</h2><p>假设我们有一个电子商务网站，上面有不同的商品。在查看这些指标时，我们注意到在游戏机类别中，"DualShock 4 无线控制器 "是销售量最大的商品之一，尤其是当用户搜索关键词 "PS4 "或 "PlayStation 4 "时。因此，我们决定在用户搜索这些关键词时，将该产品放在搜索结果的顶部。</p><p>首先，让我们使用批量 API 请求为每个项目的文档建立索引：</p>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 }<p>如果我们不干预查询，该项目通常会出现在第四位。问题是这样的</p>GET products/_search
{
 "query": {
   "match": {
     "name": "PlayStation 4"
   }
 }
}<p>结果如下</p>{
 "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
       }
     }
   ]
 }
}<p>让我们创建一个查询规则来改变这种情况。首先，让我们像这样把它添加到规则集中：</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt1576d4f4a2e60548/6a170858cdacbfccb07d298d/fdc42646fb3e76a09bca7d19047a76efe343f7a2-1600x650.png" alt="如何在 Elasticsearch 中编辑查询规则集" /><p>或相应的<a href="https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-query-rules-put-ruleset">API 请求</a>：</p>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"
          }
        ]
      }
    }
  ]
}<p>要在查询中使用<strong>规则集 </strong>，我们必须使用查询规则类型。这种查询主要由两部分组成：</p>GET /products/_search
{
 "retriever": {
   "rule": {
     "retriever": {
       "standard": {
         "query": {
           "match": { "name": "PlayStation 4" }
         }
       }
     },
     "match_criteria": {
       "query_string": "PlayStation 4"
     },
     "ruleset_ids": ["my-rules"]
   }
 }
}<ul><li><p><strong>匹配标准</strong>：这些是用于与用户查询进行比较的元数据。在本例中，当 query_string 字段的值为 "PlayStation 4 "时，规则集被激活。</p></li><li><p><strong>query</strong>：实际查询，用于搜索和获取有机结果。</p></li></ul><p>这样，首先运行有机查询，然后 Elasticsearch 应用规则集中的规则：</p>{
 "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
       }
     }
   ]
 }
}<h2>示例：基于用户的元数据</h2><p>查询规则的另一个有趣应用是使用元数据，根据用户或网页的上下文信息显示特定文档。</p><p>例如，假设我们想根据用户的忠诚度（用数值表示）来突出显示商品或定制销售。</p><p>我们可以直接将这些元数据导入查询，这样当所述值满足特定条件时，规则就会激活。</p><p>首先，我们将为一份只有忠诚度高的用户才能看到的文档建立索引：</p>POST _bulk
{ "index": { "_index": "products", "_id": "6" } }
{ "id": "6", "name": "PlayStation Plus Deluxe Card - 12 months", "category": "membership", "brand": "Sony", "price": 300 }<p>现在，让我们在同一规则集内创建一条新规则，这样当忠诚度_级别等于或高于 80 时，项目就会出现在结果的顶部。</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt158578005df8c76d/6a17085aab7f086dc0db9de3/58de12dff93305440608f51465462fcc68653a08-1421x496.png" alt="如何在 Elasticsearch 中编辑查询规则集" /><p>保存规则和规则集。</p><p>以下是相应的 REST 请求：</p>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"
          }
        ]
      }
    }
  ]
}<p>现在，在运行查询时，我们需要在元数据中包含新参数<strong>loyalty_level </strong>。如果满足规则中的条件，新文档将出现在结果的顶部。</p><p>例如，在发送忠诚度级别为 80 的查询时：</p>POST /products/_search
{
  "retriever": {
    "rule": {
      "retriever": {
        "standard": {
          "query": {
            "match": {
              "name": "PlayStation"
            }
          }
        }
      },
      "match_criteria": {
        "query_string": "PlayStation",
        "loyalty_level": 80
      },
      "ruleset_ids": ["my-rules"]
    }
  }
}<p>我们将在结果上方看到忠诚度文件：</p>{
  "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
        }
      }
    ]
  }
}<p>在下面的例子中，由于忠诚度等级为 70，因此不符合规则，物品不应出现在顶部：</p>POST /products/_search
{
  "retriever": {
    "rule": {
      "retriever": {
        "standard": {
          "query": {
            "match": {
              "name": "PlayStation"
            }
          }
        }
      },
      "match_criteria": {
        "query_string": "PlayStation",
        "loyalty_level": 70
      },
      "ruleset_ids": ["my-rules"]
    }
  }
}<p>结果如下：</p>{
  "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
        }
      }
    ]
  }
}<h2>例如：立即排除</h2><p>假设我们的<strong>DualShock 4 无线控制器（ID 2）</strong>暂时缺货，无法出售。因此，业务团队决定在此期间将其从搜索结果中删除，而不是手动删除文档或等待某些数据流程启动。</p><p>我们将使用与刚才应用于热门项目类似的过程，但这次我们不选择 "<em>已固定"</em>，而是选择 "<em>排除</em>"。这条规则就像一个黑名单。将条件改为 "<strong>始终"</strong>，这样每次运行查询时，排除都会起作用。</p><p>规则应该是这样的</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt38564c0b7f4a6ee2/6a17085c1949f78692e7a989/f10971e4f1bc9520105111adfa3a476581a27130-1600x623.png" alt="Elasticsearch 中立即排除规则集的示例" /><p>保存规则和规则集以应用更改。以下是相应的 REST 请求：</p>PUT _query_rules/my-rules
{
  "rules": [
    {
      "rule_id": "rule-6358",
      "type": "pinned",
      "criteria": [
        {
          "type": "always"
        }
      ],
      "actions": {
        "docs": [
          {
            "_index": "products",
            "_id": "2"
          }
        ]
      }
    }
  ]
}<p>现在，当我们再次运行查询时，你会发现结果中不再有该项目，尽管之前的规则是将其固定。这是因为<strong>排除结果的优先级高于钉牢结果</strong>。</p>{
 "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
       }
     }
   ]
 }
}<h2>结论</h2><p><strong>查询规则</strong>使调整相关性变得非常容易，无需修改任何代码。新的<strong>Kibana</strong> <strong>UI </strong>允许在几秒钟内做出这些更改，让您和您的业务团队对搜索结果拥有更多控制权。</p><p>除电子商务外，查询规则还能支持许多其他应用场景：在支持门户中突出显示故障排除指南，在知识库中显示关键的内部文档，在新闻网站中宣传突发事件，或过滤掉过期的职位或内容列表。它们甚至可以执行合规规则，如根据用户角色或地区隐藏受限资料。</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/elasticsearch-query-rules-ui-introduction</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/elasticsearch-query-rules-ui-introduction</guid>
    <category><![CDATA[基础功能]]></category>
    <category><![CDATA[开发者体验]]></category>
    <dc:creator><![CDATA[Jhon Guzmán]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt565ed0eb407e098d/6a17085d8b73cb363d189fb1/1fb10bd31c509cc9b9bb4f71f49970f140e6c36f-1600x945.png" length="0" type="image/png"/>
    <pubDate>Fri, 07 Nov 2025 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[使用 Mastra 和 Elasticsearch 构建具有语义召回功能的知识代理]]></title>
    <description><![CDATA[了解如何使用 Mastra 和 Elasticsearch 作为记忆和信息检索的向量存储，构建具有语义调用功能的知识代理。]]></description>
    <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><p>节点 v18+</p></li><li><p>Elasticsearch（8.15 或更新版本）</p></li><li><p>Elasticsearch API 密钥</p></li><li><p><a href="https://help.openai.com/en/articles/4936850-where-do-i-find-my-openai-api-key">OpenAI API 密钥</a></p></li></ul><p>注意：您需要这个是因为演示使用了 OpenAI 提供商，但 Mastra 支持其他人工智能 SDK 和社区模型提供商，因此您可以根据自己的设置轻松更换。</p><h2>构建 Mastra 项目</h2><p>我们将使用 Mastra 内置的 CLI 为我们的项目提供脚手架。运行该命令：</p>npm create mastra@latest<p>您将收到一组提示，首先是</p><p>1.为项目命名。</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt87f941f654d03827/6a16f7af67045b214d45bfa1/2b9fe559e0276140dd539e24f916a73c60870405-620x84.png" alt="在 Mastra 应用程序中命名提示符" /><p>2.我们可以保留默认值，也可以不填。</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt7dbb3d4f27435cac/6a16f7b0cdacbf29497d27de/e04729eb03bce8499e973e18c28642402340d0e5-852x68.png" alt="告诉 mastra 保存提示文件的位置" /><p>3.在本项目中，我们将使用 OpenAI 提供的模型。</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt1f654f6cb9397e94/6a16f7b2964cea899a08b942/a86596a469a71bdf8bd99cbaf528d0f0cf7272c0-436x222.png" alt="在 Mastra 中选择 OpenAI 提供的模型" /><p>4.选择 "暂时跳过 "选项，因为我们将把所有环境变量存储在一个".env "文件中，稍后再进行配置。</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltff106117521a3519/6a16f7b3c1e8a5031af880d8/02b19ccc34af0bdacf52fd94b519d036540ca2e6-426x114.png" alt="为 OpenAI 密钥选择暂时跳过" /><p>5.我们也可以跳过该选项。</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltcda7d9c51c3878d7/6a16f7b450916809dbe1b892/b3fe63d19d270bc2e0de1dd92033bf8b26750819-990x208.png" alt="" /><p>初始化完成后，我们就可以进入下一步。</p><h3>安装依赖项</h3><p>接下来，我们需要安装一些依赖项：</p>npm install ai @ai-sdk/openai @elastic/elasticsearch dotenv<ul><li><p><code>ai</code> - 核心人工智能 SDK 软件包，提供用于在 JavaScript/TypeScript 中管理人工智能模型、提示和工作流程的工具。Mastra 是在 Vercel 的<a href="https://ai-sdk.dev/">人工智能 SDK</a>基础上构建的，因此我们需要依赖它来实现模型与代理的交互。</p></li><li><p><code>@ai-sdk/openai</code> - 将 AI SDK 连接到 OpenAI 模型（如 GPT-4、GPT-4o 等）的插件，可使用 OpenAI API 密钥进行 API 调用。</p></li><li><p><code>@elastic/elasticsearch</code> -<a href="https://www.elastic.co/docs/reference/elasticsearch/clients/javascript">Node.js 的官方 Elasticsearch 客户端</a>、用于连接到弹性云或本地集群，以进行索引、搜索和矢量操作。</p></li><li><p><code>dotenv</code> - 从 .env 文件中加载环境变量文件到 process.env 文件中、允许您安全地注入 API 密钥和 Elasticsearch 端点等凭证。</p></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>ELASTICSEARCH_ENDPOINT="your-endpoint-here"
ELASTICSEARCH_API_KEY="your-key-here"
OPENAI_API_KEY="your-key-here"<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>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;
    }
}<ul><li><p><code>ElasticVectorConfig extends ClientOptions</code>:这将创建一个新的配置接口，继承所有 Elasticsearch 客户端选项（如<code>node</code>,<code>auth</code>,<code>requestTimeout</code> ）并添加我们的自定义属性。这意味着用户可以通过任何有效的 Elasticsearch 配置和我们的无服务器特定选项。</p></li><li><p><code>extends MastraVector</code>:这样，<code>ElasticVector</code> 就可以继承 Mastra 的基础<code>MastraVector</code> 类，这是所有矢量存储集成都要遵守的通用接口。这可以确保从代理的角度来看，Elasticsearch 的行为与其他任何 Mastra 向量后端一样。</p></li><li><p><code>private client: Client</code>:这是一个私有属性，用于保存 Elasticsearch JavaScript 客户端的实例。这样，班级就可以直接与群集对话。</p></li><li><p><code>isServerless</code> 和<code>deploymentChecked</code> ：这些属性共同作用，以检测和缓存我们连接的是无服务器还是标准 Elasticsearch 部署。首次使用时会自动检测，也可以明确配置。</p></li><li><p><code>constructor(config: ClientOptions)</code>:该构造函数接收一个配置对象（包含 Elasticsearch 凭据和可选的无服务器设置），并使用它在<code>this.client = new Client(config)</code> 行中初始化客户端。</p></li><li><p><code>super()</code>:它调用 Mastra 的基本构造函数，因此继承了日志记录、验证助手和其他内部钩子。</p></li></ul><p>此时，Mastra 知道有一个名为 <code>ElasticVector</code></p><h3>检测部署类型</h3><p>在创建索引之前，适配器会自动检测您使用的是标准 Elasticsearch 还是 Elasticsearch Serverless。这一点很重要，因为无服务器部署不允许手动配置分片。</p>private async detectServerless(): Promise&lt;boolean&gt; {
    // 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;
    }
}<p>发生了什么？</p><ul><li><p>首先检查您是否在配置中明确设置了<code>isServerless</code> （跳过自动检测）。</p></li><li><p>调用 Elasticsearch 的<code>info()</code> API 获取群集信息</p></li><li><p>检查<code>build_flavor field</code> （无服务器部署返回<code>serverless</code>)</p></li><li><p>如果没有 "构建味道"，则退回到检查标语阶段</p></li><li><p>缓存结果，避免重复调用应用程序接口</p></li><li><p>如果检测失败，则默认为标准部署</p></li></ul><p> 使用示例</p>// 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
});<h3>在 Elasticsearch 中创建 "内存 "存储</h3><p>下面的函数设置了一个 Elasticsearch 索引，用于存储嵌入式内容。它会检查索引是否已经存在。如果没有，它就会用下面的映射创建一个，其中包含一个<code>dense_vector</code> 字段，用于存储嵌入和自定义相似度度量。</p><p>有些事情需要注意：</p><ul><li><p><code>dimension</code> 参数是每个嵌入向量的长度，这取决于你使用的嵌入模型。在我们的例子中，我们将使用 OpenAI 的<code>text-embedding-3-small</code> 模型生成嵌入，该模型输出大小为<code>1536</code> 的向量。我们将以此作为默认值。</p></li><li><p>下面的映射中使用的<code>similarity</code> 变量是由辅助函数 c<code>onst similarity = this.mapMetricToSimilarity(metric)</code> 定义的，该函数接收<code>metric</code> 参数的值，并将其转换为与 Elasticsearch 兼容的关键字，用于所选的距离度量。</p><ul><li><p>例如Mastra 使用<code>cosine</code>,<code>euclidean</code>, 和<code>dotproduct</code> 等一般术语来表示向量相似性。如果我们直接将度量<code>euclidean</code> 传递到 Elasticsearch 映射中，就会出现错误，因为 Elasticsearch 希望关键字<code>l2_norm</code> 代表欧氏距离。</p></li></ul></li><li><p>无服务器兼容性：代码会自动省略无服务器部署的分片和副本设置，因为 Elasticsearch Serverless 会自动管理这些设置。</p></li></ul>async createIndex(params: CreateIndexParams): Promise&lt;void&gt; {
    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}`);
    }
}<h3>互动后存储新的记忆或笔记</h3><p>该函数接收每次交互后生成的新嵌入以及元数据，然后使用 Elastic 的<code>bulk</code> API 将其插入或更新到索引中。<code>bulk</code> API 将多个写入操作合并为一个请求；索引性能的提升确保了在代理内存不断增长的情况下，更新仍能保持高效。</p>async upsert(params: UpsertVectorParams): Promise&lt;string[]&gt; {
    const { indexName, vectors, metadata = [], ids } = params;

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

        const operations = vectors.flatMap((vec, index) =&gt; [
            { 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) =&gt; item.index?.error);
            const erroredIds = erroredItems.map((item: any) =&gt; item.index?._id);
            const errorDetails = erroredItems.slice(0, 3).map((item: any) =&gt; ({
                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) =&gt; 
                !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}`);
    }
}<h3>查询相似向量以实现语义召回</h3><p>该功能是语义召回功能的核心。代理使用向量搜索，在我们的索引中找到类似的存储嵌入。</p>async query(params: QueryVectorParams&lt;any&gt;): Promise&lt;QueryResult[]&gt; {
    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) =&gt; ({
            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}`);
    }
}<p>引擎盖下</p><ul><li><p>使用 Elasticsearch 中的<code>knn</code> API 运行<a href="https://www.elastic.co/docs/solutions/search/vector/knn">kNN</a>（k-近邻）查询。</p></li><li><p>检索与输入查询向量最相似的 K 个向量。</p></li><li><p>可选择应用元数据过滤器来缩小搜索结果范围（例如，仅在特定类别或时间范围内进行搜索）</p></li><li><p>返回结构化结果，包括文档 ID、相似性得分和存储的元数据。</p></li></ul><h2>创建知识代理</h2><p>现在，我们已经通过<code>ElasticVector</code> 集成看到了 Mastra 和 Elasticsearch 之间的连接，让我们来创建知识代理本身。</p><p>在<code>agents</code> 文件夹中，创建一个名为<code>knowledge-agent.ts</code> 的文件。我们可以从连接环境变量和初始化 Elasticsearch 客户端开始。</p>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,
});<p>在这里，我们</p><ul><li><p>使用<code>dotenv</code> 从<code>.env</code> 文件中加载变量。</p></li><li><p>检查 Elasticsearch 凭据是否被正确注入，我们是否能成功建立与客户端的连接。</p></li><li><p>在<code>ElasticVector</code> 构造函数中输入 Elasticsearch 端点和 API 密钥，以创建我们之前定义的向量存储实例。</p></li><li><p>如果使用 Elasticsearch Serverless，可选择指定<code>isServerless: true</code> 。这样可以跳过自动检测步骤，缩短启动时间。如果省略，适配器将在首次使用时自动检测您的部署类型。</p></li></ul><p>接下来，我们可以使用 Mastra 的<code>Agent</code> 类来定义代理。</p>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',
            },
        },
    }),
});<p>我们可以定义的字段有</p><ul><li><p><code>name</code> 和<code>instructions</code> ：赋予其特性和主要功能。</p></li><li><p><code>model</code>:我们通过<code>@ai-sdk/openai</code> 软件包使用 OpenAI 的<code>gpt-4o</code> 。</p></li><li><p><code>memory</code>:</p><ul><li><p><code>vector</code>:指向我们的 Elasticsearch 存储库，因此嵌入式会从那里存储和检索。</p></li><li><p><code>embedder</code>:使用哪种模型生成嵌入模型</p></li><li><p><code>semanticRecall</code> 选项决定召回如何进行：</p><ul><li><p><code>topK</code>:检索多少条语义相似的信息。</p></li><li><p><code>messageRange</code>:每场比赛应包括多少对话内容。</p></li><li><p><code>scope</code>:定义内存边界。</p></li></ul></li></ul></li></ul><p>快好了我们只需将新创建的代理添加到 Mastra 配置中。在名为<a href="http://index.ts/"><code>index.ts</code></a> 的文件中，导入知识代理并将其插入<code>agents</code> 字段。</p>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 }, 
  },
});<p>其他领域包括</p><ul><li><p><code>storage</code>:这是 Mastra 的内部数据存储，用于存储运行历史、可观察性指标、分数和缓存。有关 Mastra 存储的更多信息，请访问<a href="https://mastra.ai/docs/server-db/storage">此处</a>。</p></li><li><p><code>logger</code>:Mastra 使用<a href="https://github.com/pinojs/pino">Pino</a>，这是一个轻量级结构化 JSON 日志记录器。它可捕捉代理启动和停止、工具调用和结果、错误以及 LLM 响应时间等事件。</p></li><li><p><code>observability</code>:控制人工智能跟踪和代理执行的可见性。它可以跟踪</p><ul><li><p>每个推理步骤的开始/结束。</p></li><li><p>使用了哪种模式或工具。</p></li><li><p>输入和输出。</p></li><li><p>分数和评估</p></li></ul></li></ul><h3>使用 Mastra Studio 测试代理</h3><p>祝贺你如果您已经到达这里，那么您就可以运行这个代理，测试它的语义回忆能力了。幸运的是，Mastra 提供了一个内置的聊天用户界面，这样我们就不必自己创建了。</p><p>要启动 Mastra 开发服务器，请打开终端并运行以下命令：</p>npm run dev<p>在初始捆绑和启动服务器后，它应该会为你提供一个 Playground 的地址。</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blte5f857fddc74ffc9/6a16f7b6a6c2b995d5e794c0/8b045f70008d26aec4d2e6b59d61085555b9c5b2-686x116.png" alt="Playground 服务器地址" /><p>将此地址粘贴到浏览器中，您将看到 Mastra Studio。</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltc7fdda6ce46ce068/6a16f7b7b0367d4f7672bacf/69bc80fe8486edd9e0cf91d87b39f465aeb23111-1600x438.png" alt="粘贴 Playground 地址以访问 Mastra Studio" /><p>选择<code>knowledgeAgent</code> ，然后开始聊天。</p><p>为了快速测试一切接线是否正确，请给它提供一些信息，如 "团队宣布 10 月份的销售业绩增长了 12% ，主要是由企业续订驱动的。下一步是扩大对中端市场客户的拓展"。下一步，开始新的聊天，并提问："我们说过下一步需要关注哪个客户群？知识代理应该能够回忆起您在第一次聊天中提供给它的信息。您应该会看到类似的回复：</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltfec3266e81a7213b/6a16f7b92b835f6f70f4afe2/da8ebddad89874023ed440a8f1ad2cb04ed043f4-1070x288.png" alt="在 Mastra Studio 中与知识代理聊天--代理可以调用信息" /><p>看到这样的响应，意味着代理成功地将我们之前的信息以嵌入的形式存储在 Elasticsearch 中，并在稍后使用向量搜索进行检索。</p><h3>检查代理的长期记忆存储</h3><p>在 Mastra Studio 的代理配置中，前往<code>memory</code> 选项卡。这可以让您了解您的代理随着时间的推移学到了什么。嵌入并存储在 Elasticsearch 中的每一条消息、响应和交互都会成为长期记忆的一部分。您可以对过去的交互进行语义搜索，以快速找到代理之前了解到的信息或上下文。这与代理在语义回想时使用的机制基本相同，但在这里你可以直接检查它。在下面的示例中，我们搜索 "销售 "一词，并返回所有包含销售内容的互动。</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blte428134d7bf2a43a/6a16f7bbb0367d185872bad3/3decaa0c332d288c5ae0b11c25f592c7d50c2f0f-1104x1320.png" alt="如何检查知识代理的长期记忆存储" /><h2>结论</h2><p>通过连接 Mastra 和 Elasticsearch，我们可以为代理提供内存，这是上下文工程的关键层。有了语义记忆功能，代理可以随着时间的推移建立上下文，将他们的反应建立在所学知识的基础上。这意味着更准确、更可靠、更自然的互动。</p><p>早期的整合只是一个起点。同样的模式可以让支持代理记住过去的票单，让内部机器人检索相关文档，或者让人工智能助理在对话中回忆起客户的详细信息。我们还在努力实现与 Mastra 的正式集成，以便在不久的将来使这种搭配更加完美。</p><p>我们很期待看到您的下一个作品。试试吧，探索<a href="https://mastra.ai/">Mastra</a>及其内存功能，并随时与社区分享您的发现。</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/knowledge-agent-semantic-recall-mastra-elasticsearch</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/knowledge-agent-semantic-recall-mastra-elasticsearch</guid>
    <category><![CDATA[智能体 AI]]></category>
    <category><![CDATA[开发者体验]]></category>
    <category><![CDATA[集成]]></category>
    <dc:creator><![CDATA[JD Armada]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt09afdbff05603865/6a16f7bd839dfabbf2dcfcb5/b8d51c2726d5573385c9246a7821d12ade4f1b0e-720x420.jpg" length="0" type="image/jpeg"/>
    <pubDate>Thu, 06 Nov 2025 00:00:00 GMT</pubDate>
  </item>
  </channel>
</rss>