从搜索到结账只需 20 行代码:使用 OpenTelemetry 构建四阶段转化漏斗

作者:来自 Elastic Matthew Adams

将加入购物车和购买跟踪添加到你的搜索分析管道中,并使用 ES|QL 回答每个产品经理都会问的问题:哪些搜索查询带来了最高收入?

你的产品经理想知道哪些搜索带来了最多收入。你可以告诉他们用户搜索了什么(查看我们的第二篇博客),也可以告诉他们用户点击了什么(查看我们的第三篇博客),但你还无法知道他们购买了什么。

新增的两种 span 类型 ------ 加入购物车( add-to-cart )和购买( purchase )------ 补全了从搜索查询到结账的四阶段漏斗。它们基于你从第二篇博客开始一直使用的相同 search.* 属性和 ES|QL 查询。

只需大约 20 行代码,你做出的每一个相关性决策都可以附带一个收入数值。

你将发现什么

在本文中,你将学习如何:

  • 使用能够关联到原始搜索的 search.* 属性,添加转化跟踪(加入购物车和购买 span)。

  • 构建完整的搜索到收入漏斗:搜索 → 点击 → 加入购物车 → 购买。

  • 编写 Elasticsearch Query Language( ES|QL )查询,用于计算转化率、每次查询带来的收入以及平均订单价值。

  • 识别用户在哪些环节流失,以及每个流失点应该由哪个团队负责。

  • 将收入归因到具体搜索查询,从而优先处理相关性优化工作。

你需要准备什么

  • 来自第三篇博客的点击跟踪能力(搜索和点击 span 通过基于 OTel 原生的数据采集方式流入 Elastic)。

  • 用于加入购物车和结账事件的后端接口(提供示例代码)。

  • 对电商转化漏斗有基础理解。

为什么搜索收入归因很重要

你的产品经理走进会议室,问:"哪些搜索带来了最多收入?"

你可以告诉他们用户搜索了什么(第二篇博客),也可以告诉他们用户点击了什么(第三篇博客)。但你无法告诉他们用户最终购买了什么。

"点击了一个结果"和"购买了一个产品"之间的差距,正是搜索投入价值所在,而目前这部分信息是不可见的。

本文将补全这一闭环。通过增加两种 span 类型(加入购物车和购买),你可以构建一个完整的搜索到收入漏斗,并且基于从第二篇博客开始一直使用的相同 search.* 属性和 ES|QL 查询。

搜索转化跟踪可以为你的团队回答哪些问题

下面是转化跟踪能够帮助你回答的问题,以及为什么这些问题对团队中的不同角色很重要。

  • 哪些搜索带来了收入?

    这是产品经理最关心的问题。当你能够将收入归因到具体查询时,就可以根据业务影响来优先安排相关性优化工作。

    一个点击率( CTR )一般但转化价值很高的查询,比一个点击率很高但从未带来购买的查询更重要。

  • 用户在哪些环节流失?

    从搜索到购买的漏斗包含四个阶段:搜索、点击、加入购物车和购买。

    每个流失点都指向不同的问题:

    • 高点击到加入购物车流失率,说明产品页面可能没有足够吸引力。

    • 高购物车到购买流失率,说明问题可能出在结账流程,而不是搜索。

    了解用户在哪里 放弃,可以帮助你确定哪个团队应该负责解决问题。

  • 哪些查询需要保护?

    一旦你知道" laptop bag "这个查询每月产生 12,000 美元的归因收入,你对它的处理方式就会不同。

    任何影响高收入查询的相关性调整,都应该经过更严格的审查。

    你还可以设置监控(即将发布的第六篇博客),当收入最高的搜索查询转化率下降时发出告警。

新增的两个插桩点总共大约需要 20 行代码,并且遵循之前使用的相同模式。你只需要向 span 添加属性,然后使用 ES|QL 查询这些数据。

想跟着代码一起操作?

参考项目已经准备好了转化跟踪功能。

取消 app.pyapp.js 中 Blog 4 部分的注释,重启应用,然后使用以下命令生成流量:

python generate_traffic.py --blog 4

四阶段搜索转化漏斗

在编写任何代码之前,先了解我们正在构建的结构。每个阶段都是一个插桩点,并且每个阶段都会在 traces-generic.otel-default 中创建 span:

ini 复制代码
`

1.  search          →  click              →  cart.add         →  checkout.complete
2.  (Blog 2)           (Blog 3)              (this post)         (this post)
3.  query_id=abc       query_id=abc          query_id=abc        query_id=abc
4.  user_query=...     click_position=1      product_id=...      order_total=$149
5.  result_count=15    product_id=...        quantity=1           item_count=2

`AI写代码

贯穿整个链路的核心是 search.query_id。你在第二篇博客中根据 trace ID 推导出的同一个标识符,并在第三篇博客中用于将点击关联到搜索,现在会继续传递到加入购物车和购买事件中。

这正是实现收入归因的关键:你可以追踪一次购买,回溯到最初开启整个购买过程的搜索。

使用 OpenTelemetry 添加转化跟踪 span

加入购物车 span 插桩

当用户从搜索结果页面添加商品到购物车时(或者从通过搜索进入的商品详情页面添加商品),你需要创建一个 cart.add span。

该 span 用于记录用户意图转变为实际操作的时刻。

less 复制代码
`

1.  @app.post("/api/cart/add")
2.  async def add_to_cart(event: AddToCartRequest):  # reference project uses CartEvent
3.      with tracer.start_as_current_span("cart.add") as span:
4.          span.set_attribute("search.action", "add_to_cart")
5.          span.set_attribute("search.result_click_id", event.object_id)
6.          span.set_attribute("search.result_click_position", event.position)
7.          span.set_attribute("search.query_id", event.query_id)
8.          span.set_attribute("enduser.pseudo.id", event.client_id)
9.          span.set_attribute("cart.quantity", event.quantity)
10.          if event.price is not None:
11.              span.set_attribute("cart.price", event.price)
12.          if event.user_query:
13.              span.set_attribute("search.query", event.user_query)

`AI写代码![](https://csdnimg.cn/release/blogv2/dist/pc/img/runCode/icon-arrowwhite.png)

这遵循第三篇博客中点击跟踪的相同模式;也就是说,这是一个独立的 span,通过 query_id 与原始搜索关联。

新增的属性是 cart.quantitycart.price,它们可以让你在查询时聚合收入。

购买 span 插桩

当用户完成结账时,你需要创建一个 checkout.complete span。

这是收入事件,也是能够回答产品经理问题的关键事件。

less 复制代码
`

1.  @app.post("/api/checkout")
2.  async def checkout(event: CheckoutRequest):  # reference project uses CheckoutEvent
3.      with tracer.start_as_current_span("checkout.complete") as span:
4.          span.set_attribute("search.action", "purchase")
5.          span.set_attribute("checkout.order_id", event.order_id)
6.          span.set_attribute("checkout.total_amount", event.total_amount)
7.          span.set_attribute("checkout.item_count", len(event.items))
8.          span.set_attribute("enduser.pseudo.id", event.client_id)
9.          if event.query_id:  # last search in journey
10.              span.set_attribute("search.query_id", event.query_id)
11.              span.set_attribute("search.query", event.user_query)

`AI写代码![](https://csdnimg.cn/release/blogv2/dist/pc/img/runCode/icon-arrowwhite.png)
  • Span 会自动捕获错误。 这是使用 OTel span 记录转化事件带来的额外好处。如果加入购物车或结账调用抛出未处理异常,span 状态会自动设置为 ERROR,并记录异常详细信息。这些错误对业务至关重要(结账流程故障意味着收入损失),并且会立即显示在 Elastic APM 的错误跟踪、服务地图和告警中。你可以通过同一套插桩同时获得转化分析和运行监控,无需额外代码。

  • checkout.total_amount 是收入数值。 这是你将在 ES|QL 中进行聚合的字段,用于获取按查询统计的收入。它表示订单总金额,而不是单个商品的价格。

  • search.query_id 是有条件的。 并非每次购买都来自搜索。用户可能浏览商品分类、点击促销链接,或者几天后返回购物车完成购买。if event.query_id: 判断确保只有在存在真实关联时,才会将购买归因到搜索。

    没有 query_id 的购买仍然会被记录,只是不会出现在搜索归因查询中。

  • search.query 会同时设置在购物车和购买 span 上。 这是经过设计的数据冗余。你可以通过 query_id 回查搜索记录来获取查询文本,但虽然 ES|QL 支持 LOOKUP JOIN,由于需要优化查询索引,这里可能并不是最佳选择。

    通过直接将查询文本放入转化 span 中,你的"按查询统计收入"和"按查询统计购物车数据"的查询就可以变成简单直接的聚合操作。

购物车和购买事件的 Span 属性

加入购物车属性:

属性 类型 必需 用途
search.action string "add_to_cart"
search.result_click_id string 商品文档 ID
search.result_click_position int 添加商品时在搜索结果中的位置
search.query_id string 关联到原始搜索
enduser.pseudo.id string 客户端/设备标识符( OTel 语义约定 SemConv
cart.quantity int 添加的商品数量
cart.price float 可选 商品价格
search.query string 推荐 搜索查询文本(用于按查询分析购物车数据)

Purchase attributes:

属性 类型 必需 用途
search.action string "purchase"
checkout.order_id string 唯一订单标识符
checkout.total_amount float 订单总金额
checkout.item_count int 购买商品数量
enduser.pseudo.id string 客户端/设备标识符( OTel SemConv )
search.query_id string 推荐 关联到原始搜索(用户旅程中的最后一次搜索)
search.query string 推荐 搜索查询文本(用于按查询分析收入)

这些属性继续沿用了第 2 篇和第 3 篇博客中的 search.* 命名空间,并为转化相关数据新增了 cart.*checkout.* 前缀。

通过基于 OTel 原生的数据采集方式,所有属性都会存储在 attributes.* 下,并保留点号表示法。这意味着不再需要将字符串映射到 labels.*,也不需要将数字映射到 numeric_labels.*

search.query 属性可以通过 attributes.search.query 查询。同样,checkout.total_amount 可以通过 attributes.checkout.total_amount 查询。

用户身份:跨会话连接搜索与购买

你会注意到,在本系列中每种 span 类型都包含 enduser.pseudo.id。它代表最低身份级别,也就是存储在浏览器 localStorage 中的持久化标识符,用于跨多个会话将事件关联到同一个设备。

对于转化跟踪,身份信息变得更加重要。你需要将周一发生的搜索与周二发生的购买关联起来,或者关联跨多个标签页产生的购物车添加事件。

我们的 schema 支持三个身份级别,并与 OTel 语义约定保持一致:

属性 持久性 用途
enduser.pseudo.id 永久( localStorage ) 设备/浏览器标识符( OTel SemConv )
session.id 单次访问( sessionStorage ) 将单次访问中的事件进行分组( OTel SemConv )
user.id 账户级别(认证系统) 已认证用户( OTel SemConv )

对于本文中的漏斗查询,enduser.pseudo.id 已经足够。它可以在浏览器范围内将用户从搜索到购买的完整旅程关联起来。

如果你的用户进行了身份认证,添加 user.id 可以实现跨设备归因(例如在手机上搜索,在桌面设备上购买),并支持更丰富的个性化能力。

session.id 可以帮助区分同一个客户端存在多个活跃会话时的不同访问过程。

这三个属性在交互 span 上都是可选的。你可以从 enduser.pseudo.id 开始,并在使用场景需要时添加其他属性。

关键在于保持一致性:在搜索、点击和转化 span 中使用相同的标识符,这样关联查询( join )才能正常工作。

前端集成

前端需要在整个用户旅程中传递 query_id

当用户点击搜索结果时,你已经可以从搜索响应中获得 query_id第三篇博客)。关键是继续将它传递下去。

php 复制代码
`

1.  // CLIENT_ID: persistent browser identifier from localStorage (set up in Blog 3)
2.  // const CLIENT_ID = localStorage.getItem("search_client_id") || ...

4.  // On add-to-cart from a search result page
5.  fetch('/api/cart/add', {
6.    method: 'POST',
7.    headers: { 'Content-Type': 'application/json' },
8.    body: JSON.stringify({
9.      object_id: product.id,
10.      position: product.resultPosition,  // from search results
11.      query_id: product.queryId,         // from search response
12.      client_id: CLIENT_ID,              // persistent browser identifier → enduser.pseudo.id
13.      quantity: 1,
14.      price: product.price,
15.    })
16.  });

18.  // On checkout completion
19.  fetch('/api/checkout', {
20.    method: 'POST',
21.    headers: { 'Content-Type': 'application/json' },
22.    body: JSON.stringify({
23.      order_id: order.id,
24.      total_amount: order.total,
25.      items: order.items,
26.      client_id: CLIENT_ID,              // same identifier as search and click spans
27.      query_id: lastSearchQueryId,       // last search in session
28.      user_query: lastSearchQuery,
29.    })
30.  });

`AI写代码![](https://csdnimg.cn/release/blogv2/dist/pc/img/runCode/icon-arrowwhite.png)

前端将 client_id 作为 HTTP 字段名发送;后端在设置 span 属性时,将其映射到 OTel 语义约定中的 enduser.pseudo.id

query_id 的传递是关键部分。需要将它与商品一起存储在购物车数据结构中,使其能够在页面之间跳转时继续保留。

我们将在下面的归因部分讨论其中的设计挑战。

正在使用参考项目?

参考应用中的 frontend/app.js 已经接入了加入购物车按钮,但浏览器 UI 中尚未实现结账流程。结账流程通过流量生成器完成。

要模拟转化事件,请运行:

python generate_traffic.py --blog 4 --sessions 100

该命令会向三个后端接口发送真实场景中的搜索、点击、加入购物车和购买事件组合。

验证转化事件是否正在到达

在构建漏斗查询之前,请确认两种 span 类型都已经流入 Elastic:

加入购物车事件:

sql 复制代码
`

1.  FROM traces-generic.otel-default
2.  | WHERE attributes.search.action == "add_to_cart"
3.  | KEEP attributes.search.result_click_id, attributes.search.query_id,
4.         attributes.cart.quantity
5.  | LIMIT 5

`AI写代码

购买事件:

markdown 复制代码
`

1.  FROM traces-generic.otel-default
2.  | WHERE attributes.search.action == "purchase"
3.  | KEEP attributes.checkout.order_id, attributes.checkout.total_amount,
4.         attributes.checkout.item_count, attributes.search.query_id,
5.         attributes.search.query
6.  | LIMIT 5

`AI写代码

如果这些查询返回数据行,说明你的完整漏斗已经完成插桩。如果没有返回结果,请检查一直以来需要检查的内容:OpenTelemetry Protocol( OTLP )端点、认证 token 以及 span 导出。

使用 ES|QL 进行漏斗分析

在单个查询中统计搜索、点击、购物车和购买事件

你可以使用与第三篇博客中 CTR 查询相同的 COUNT(CASE(...)) 模式,在一个 ES|QL 查询中统计完整的四个漏斗阶段:

ini 复制代码
`

1.  FROM traces-generic.otel-default
2.  | WHERE (name == "search" AND attributes.search.query IS NOT NULL)
3.      OR attributes.search.first_click == true
4.      OR attributes.search.action IN ("add_to_cart", "purchase")
5.  | STATS
6.      searches  = COUNT(CASE(name == "search" AND attributes.search.query IS NOT NULL, 1)),
7.      clicked   = COUNT(CASE(attributes.search.first_click == true, 1)),
8.      carts     = COUNT(CASE(attributes.search.action == "add_to_cart", 1)),
9.      purchases = COUNT(CASE(attributes.search.action == "purchase", 1))
10.  | EVAL
11.      click_rate    = ROUND(100.0 * clicked   / searches, 1),
12.      cart_rate     = ROUND(100.0 * carts     / searches, 1),
13.      purchase_rate = ROUND(100.0 * purchases / searches, 1)

`AI写代码![](https://csdnimg.cn/release/blogv2/dist/pc/img/runCode/icon-arrowwhite.png)

示例输出:

searches clicked carts purchases click_rate cart_rate purchase_rate
146 41 28 12 28.1% 19.2% 8.2%

你可以通过一个查询获得四个阶段的计数以及三个转化率。WHERE 子句会将四种 span 类型提取到同一个结果集中,而 COUNT(CASE(...)) 则分别统计每种类型。

这与第三篇博客中让 CTR 查询保持简洁的技术相同。

注意,我们在点击阶段使用 search.first_click,而不是统计所有点击事件。这样可以得到至少收到一次点击的搜索数量(与第三篇博客中 CTR 的定义一致)。

如果不这样处理,一个产生三次点击的搜索会将点击数量增加到三次,而搜索数量仍然只计算一次,从而导致漏斗数据失真。

现在,每个阶段都代表一个唯一的转化过程:

  • 发生了多少次搜索。

  • 其中有多少次获得了点击。

  • 其中有多少次导致了加入购物车。

  • 其中有多少次最终产生购买。

你可以使用 Kibana Lens 将其转换为漏斗可视化。

分别运行每个阶段的计数作为独立的 ES|QL 查询,将它们保存为仪表板面板,然后排列成水平柱状图:

  • Y 轴显示四个阶段。

  • X 轴显示数量。

每一步的流失情况都会立即变得清晰可见。

上面的仪表板(来自该系列第一篇博客)展示了实际效果:

左下角的 Conversion Funnel(转化漏斗) 面板使用水平柱状图展示四个阶段,而旁边的 Top Queries by Revenue(按收入排名的热门查询) 表格展示收入归因。

你可以直接根据本文中的 ES|QL 查询构建这些内容。

流失率告诉你什么,以及每个瓶颈由谁负责

漏斗中的每个转化阶段都会告诉你一些具体信息:

  • 搜索到点击( CTR ):

    你已经在第三篇博客中进行了测量。

    较低的 CTR 表示搜索结果缺乏吸引力。这通常是相关性问题。

  • 点击到加入购物车:

    用户与搜索结果进行了交互,但没有将商品加入购物车。

    这可能意味着:

    • 商品详情页缺乏说服力。

    • 价格缺乏竞争力。

    • 商品缺货。

    这通常不是搜索问题

    搜索可能已经成功了(用户点击了结果),但后续环节导致用户流失。

  • 购物车到购买:

    用户已经决定购买,但没有完成结账。

    复杂的表单、意外的配送费用以及支付问题都会造成这种结账阻力( checkout friction )

    这几乎从来不是搜索问题,但了解漏斗在哪里出现流失非常有价值,这样你就不会在结账环节才是瓶颈时,浪费时间优化搜索相关性。

诊断模式非常直接:

流失点 可能原因 负责团队
搜索到点击 相关性 / 排名 搜索团队
点击到加入购物车 商品页面 / 定价 / 库存情况 商品 / 运营团队
购物车到购买 结账体验 / 支付 / 配送 结账 / 增长团队

这是完整漏斗数据能够带来的最有价值的能力之一:它让你能够定位正确的问题。

当 VP 问:"为什么搜索没有带来转化?"时,你可以明确指出瓶颈到底是在相关性、商品页面还是结账流程。

按搜索查询进行收入归因

下面是你的产品经理真正想要的查询:

sql 复制代码
`

1.  FROM traces-generic.otel-default
2.  | WHERE attributes.search.action == "purchase"
3.    AND attributes.search.query IS NOT NULL
4.  | STATS
5.      purchase_count = COUNT(*),
6.      total_revenue = SUM(attributes.checkout.total_amount)
7.    BY attributes.search.query
8.  | SORT total_revenue DESC

`AI写代码

这会根据搜索查询产生的收入,为你提供一个排名列表。

购买 span 上的 search.query 属性(我们之前讨论过的数据冗余设计)使其成为一个单独的聚合查询,无需使用 join 或子查询。

如何利用搜索收入数据采取行动

  • 保护高收入查询。

    如果"laptop bag"带来了最高收入,那么任何影响该查询的相关性调整都需要更加严格的审查。

    你可以将它加入回归测试集合,或者通过查询规则固定特定结果。

    你甚至可以在其转化率下降时设置告警(第 6 篇博客)。

  • 优先投入相关性优化。

    这个列表顶部的查询,是相关性改进能够带来最大业务影响的地方。

    一个每月产生 500 美元收入的查询,即使 CTR 提升 10%,其价值也高于一个仅产生 20 美元收入但 CTR 提升 50% 的查询。

  • 发现错失机会。

    将其与第二篇博客中的热门查询数据进行交叉分析。

    一个搜索量很高但没有购买归因的查询,可能是:

    • 浏览型查询(信息需求)。

    • 值得进一步调查的转化缺口。

热门收入查询:相关性投入应该关注的地方

为了让相关性团队能够集中精力优化,可以获取产生最高收入的查询:

sql 复制代码
`

1.  FROM traces-generic.otel-default
2.  | WHERE attributes.search.action == "purchase"
3.    AND attributes.search.query IS NOT NULL
4.  | STATS
5.      purchases = COUNT(*),
6.      revenue = SUM(attributes.checkout.total_amount)
7.    BY attributes.search.query
8.  | SORT revenue DESC
9.  | LIMIT 10

`AI写代码

将其与第 3 篇博客中的按查询统计的 CTR 进行交叉分析。

一个高收入但低 CTR 的查询表示表现不佳;即使是小幅度的相关性改进,也可能带来巨大的业务影响。

一个高 CTR 但没有购买归因的查询,可能属于信息型查询(例如用户浏览但没有购买)。

同时出现在两个列表顶部的查询,应该获得你的相关性团队最多的关注。

按搜索查询统计平均订单价值

平均订单价值( AOV )除了告诉你哪些查询能够带来购买之外,还能告诉你这些购买的价值。

具有高 AOV 的查询属于高价值意图搜索;在这些查询上的排名优化能够带来最大的单次购买影响:

ini 复制代码
`

1.  FROM traces-generic.otel-default
2.  | WHERE attributes.search.action == "purchase"
3.    AND attributes.search.query IS NOT NULL
4.  | STATS
5.      purchase_count = COUNT(*),
6.      total_revenue = SUM(attributes.checkout.total_amount),
7.      avg_order_value = ROUND(AVG(attributes.checkout.total_amount), 2)
8.    BY attributes.search.query
9.  | SORT avg_order_value DESC
10.  | LIMIT 10

`AI写代码![](https://csdnimg.cn/release/blogv2/dist/pc/img/runCode/icon-arrowwhite.png)

一个具有高 AOV 但低搜索量的查询,与高搜索量但低 AOV 的查询代表不同的机会。

前者表示来自小众用户群体的高价值意图(可以考虑推荐结果或专门的落地页);后者表示面向大众用户的广泛覆盖(价格敏感性可能比相关性更限制转化)。

搜索收入归因:限制与解决方法

搜索收入归因很有价值,但它并不完美。了解这些限制有助于你设定合理预期,并围绕这些限制进行设计。

多次搜索旅程中的最后触点归因

用户很少只搜索一次就购买。一个典型旅程可能如下:

  1. 搜索 "laptop bag":浏览结果并点击几个商品。

  2. 搜索 "laptop bag leather":细化搜索。

  3. 搜索 "laptop sleeve 15 inch":尝试不同方向。

  4. 从第三次搜索的结果中加入购物车。

  5. 完成购买。

通过上述插桩方式,这次购买会归因到第三次搜索,也就是购物车商品中保存了其 query_id 的那次搜索。

前两次搜索参与了整个购买旅程,但不会获得任何归因。

这就是最后触点归因( last-touch attribution ) ,它是在单个 query_id 关联机制下能够工作的最简单模型。

它并不完美,但具有明确且无歧义的特点。

另一种方式,即跟踪用户会话中的每个 query_id 并分配贡献比例,会显著增加插桩和分析的复杂度。

对于大多数团队来说,最后触点归因是一个很好的起点。

如果未来需要多触点归因,原始数据仍然存在。你可以在指定时间窗口内,根据某个 client_id 查询所有搜索和点击事件,并重建完整的用户旅程:

ini 复制代码
`

1.  FROM traces-generic.otel-default
2.  | WHERE attributes.enduser.pseudo.id == "client-abc-123"
3.    AND (name == "search"
4.      OR attributes.search.action == "click"
5.      OR attributes.search.action == "add_to_cart"
6.      OR attributes.search.action == "purchase")
7.  | KEEP @timestamp, name, attributes.search.action,
8.         attributes.search.query, attributes.search.query_id,
9.         attributes.search.result_click_id
10.  | SORT @timestamp ASC

`AI写代码![](https://csdnimg.cn/release/blogv2/dist/pc/img/runCode/icon-arrowwhite.png)

这会按照时间顺序重建用户完整的搜索旅程。即使你不构建自动化的多触点归因,它对于调试单个会话也很有用。

使用 query_id 进行跨会话归因的限制

用户周一搜索 "wireless headphones",点击了几个结果,离开,然后周三回来购买。

周一搜索产生的 query_id 已经不存在了,因为它只是那个特定搜索请求的属性。

这是基于 query_id 的归因模型的一个基本限制。

它可以在单个会话内工作(更准确地说,是在前端保留 query_id 的范围内工作)。

它无法跨会话工作。

对于跨会话归因,你需要采用不同的方法,通常是建立一个用户级事件存储,将商品浏览、加入购物车和购买事件与持久化用户 ID 关联,然后回溯查找最初触发这些行为的搜索。

这是一个更复杂的分析管道,不属于我们当前构建范围。

实际影响是,你通过搜索归因得到的收入会出现低估

一些实际上受到搜索影响的购买事件可能不会携带 query_id

这对于相对比较来说没有问题(例如:哪些查询比其他查询产生更多收入?),即使绝对数值会比较保守。

如何将 query_id 从搜索持久化到结账

一些实际设计决策会影响你的 query_id 传递范围:

  • 在购物车中存储 query_id

    当用户将商品加入购物车时,将 query_id 与商品一起持久化保存。

    这样,即使用户离开页面,并在稍后(同一会话内)返回结账,归因信息仍然可以保留。

  • 不要在重新搜索时覆盖 query_id

    如果用户从搜索 A 添加了一个商品,然后再次搜索并从搜索 B 添加另一个商品,那么每个购物车商品都应该保留自己的 query_id

    购买事件会携带最后一次搜索的 query_id 作为汇总信息,但基于商品项的归因可以提供更丰富的数据。

  • 接受这些限制。

    并不是每次购买都会有搜索归因。

    直接访问、分类浏览、促销链接以及直接返回购物车购买的老用户,都会产生没有 query_id 的购买事件。

    这是正确的行为,而不是数据缺失。

使用 Span 还是日志事件进行转化跟踪

转化事件可以同时发送 OTel span 和兼容 UBI 的日志事件,这与第 3 篇博客中用于点击跟踪的双信号模式相同。

实际上,对于转化场景,日志事件包含的信息更丰富。

它可以包含完整的商品列表,以及每个商品对应的 query_id 归因,而这些信息无法很好地映射到扁平化的 span 属性中。

span 提供简单、可聚合的视图(例如按查询统计总收入),而日志提供详细的、基于商品项的视图(例如哪些具体商品来自哪些具体搜索)。

对于本文中的漏斗查询,span 已经足够。

如果你需要商品级别的归因分析,那么 logs-generic.otel-default 中的日志事件提供了所需的详细信息。

ES|QL 查询这些日志的方式与查询 span 相同,只是查询的是不同的索引模式。

下一步:将转化数据转化为相关性改进

现在,我们已经拥有完整的插桩体系,包括四种 span 类型:

这些 span 捕获了从查询到购买的完整用户旅程。

每个 span 都存在于 traces-generic.otel-default 中,并且每个指标都可以通过 ES|QL 查询。

search.query_id 这条关联链将整个漏斗连接在一起。

但是,测量漏斗只是整个过程的一半。

真正的价值在于使用这些数据来改进搜索。

在本系列后续的一篇博客中,我们会利用目前构建的一切,并将其转化为相关性改进。

点击位置和转化数据会成为 Learning To Rank 的判断列表,而每个查询的 CTR 和收入会成为用于提升排序的 rank 特征。

此外,高收入查询还可以获得保护性监控。

同时,像 Elasticsearch Relevance Studio 这样的工具,可以为最重要的搜索提供可视化调优界面,并使用你现在正在收集的数据进行优化。

你在第 2--4 篇博客中构建的插桩体系不仅仅用于分析,它还形成了一个反馈循环:

衡量、改进、再次衡量。

开始使用搜索转化跟踪

原文:Search conversion tracking with OpenTelemetry and ES|QL | Elasticsearch Labs

相关推荐
Elasticsearch2 小时前
你的堆内存图表看不到的隐藏压力:AutoOps 现已监控向量堆外内存
elasticsearch
程序员小八77719 小时前
一篇文章讲透 Elasticsearch:从倒排索引到实战落地
大数据·elasticsearch·搜索引擎
Elasticsearch21 小时前
Kibana Dashboards API:适用于所有面板类型的稳定接口,在正式发布前经过 50 多个团队验证
elasticsearch
Elasticsearch21 小时前
Elasticsearch ES|QL 将全文搜索带到你从未建立索引的数据中
elasticsearch
Elasticsearch1 天前
用 start-local 脚本在本地运行 Elastic Stack 并创建 AI agents
elasticsearch
Elasticsearch2 天前
一次编辑,更新所有仪表板:使用 Terraform 大规模管理 Kibana 可观测性配置
elasticsearch
Elasticsearch2 天前
足够接近就是足够快:ES|QL Fast 模式如何让 Kibana 仪表板速度提升最高 100 倍
elasticsearch
Elasticsearch2 天前
一分钟内从提示词生成仪表板,成本降低 5 倍:Kibana 中的 AI 仪表板和自定义 Vega-Lite 图表
elasticsearch
雾屿_Mistisle2 天前
敏感文件泄露
大数据·elasticsearch·搜索引擎