作者:来自 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.py 和 app.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写代码
这遵循第三篇博客中点击跟踪的相同模式;也就是说,这是一个独立的 span,通过 query_id 与原始搜索关联。
新增的属性是 cart.quantity 和 cart.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写代码
-
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写代码
前端将 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写代码
示例输出:
| 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写代码
一个具有高 AOV 但低搜索量的查询,与高搜索量但低 AOV 的查询代表不同的机会。
前者表示来自小众用户群体的高价值意图(可以考虑推荐结果或专门的落地页);后者表示面向大众用户的广泛覆盖(价格敏感性可能比相关性更限制转化)。
搜索收入归因:限制与解决方法
搜索收入归因很有价值,但它并不完美。了解这些限制有助于你设定合理预期,并围绕这些限制进行设计。
多次搜索旅程中的最后触点归因
用户很少只搜索一次就购买。一个典型旅程可能如下:
-
搜索 "laptop bag":浏览结果并点击几个商品。
-
搜索 "laptop bag leather":细化搜索。
-
搜索 "laptop sleeve 15 inch":尝试不同方向。
-
从第三次搜索的结果中加入购物车。
-
完成购买。
通过上述插桩方式,这次购买会归因到第三次搜索,也就是购物车商品中保存了其 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写代码
这会按照时间顺序重建用户完整的搜索旅程。即使你不构建自动化的多触点归因,它对于调试单个会话也很有用。
使用 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 篇博客中构建的插桩体系不仅仅用于分析,它还形成了一个反馈循环:
衡量、改进、再次衡量。
开始使用搜索转化跟踪
-
参考项目:
整个博客系列的完整代码;克隆、配置并运行。
-
Elastic Distribution of OpenTelemetry for Python:
GitHub - elastic/elastic-otel-python · GitHub
用于 Python 的 EDOT。
-
OpenTelemetry with Elastic:
Use OpenTelemetry with Elastic APM | Elastic Docs
了解如何将 OTel 数据发送到 Elastic APM。
-
ES|QL 文档:
ES|QL reference | Elasticsearch Reference
查询语言参考。
-
UBI Standard:
搜索事件结构的参考 schema。
-
查询规则:
针对特定查询固定、提升或排除结果。
原文:Search conversion tracking with OpenTelemetry and ES|QL | Elasticsearch Labs