Python后端中间件专题19:“相关”究竟怎么算——boost、search_after 与聚合
Python后端中间件专题19“相关”究竟怎么算——boost、search_after 与聚合客服搜索“VPN 登录”第一条却是正文长篇提到 VPN 的普通工单标题直接写“VPN 登录故障”的工单排在后面。产品经理说“相关性不对”但后端不能只回答“ES 算的”。我们要能指出哪些字段参加评分、翻到第二页使用哪组排序值、筛选后的统计到底统计了谁。第 18 课已经把租户和状态塞进不计分的filter。本课只改读模型的三件事标题更重要、结果用游标继续取、页面显示当前查询范围内的状态/优先级计数。工单事实和写事务一个字也不用改变。上一课练习答案答案 EX-18-01 [code]把下面保存为query_guard_probe.py从project/运行。这里直接调用生产查询构造器如果有人把空关键词分支里的租户条件丢了两次断言不会一起放过。fromticketflow.search.queryimportSearchFilters,build_search_body with_wordsbuild_search_body(tenant_idtenant-a,query VPN 登录 ,filtersSearchFilters(statuses(open,),priorityhigh),size10,)without_wordsbuild_search_body(tenant_idtenant-a,query ,filtersSearchFilters(),size10,)tenant_filter{term:{tenant_id:tenant-a}}assertwith_words[query][bool][filter][0]tenant_filterassertwithout_words[query][bool][filter][0]tenant_filterassertwith_words[query][bool][must][0][multi_match][query]VPN 登录assertwithout_words[query][bool][must][{match_all:{}}]print(wordsmulti_match:VPN 登录)print(emptymatch_all; tenanttenant-a)从project/运行命令python query_guard_probe.py预期输出wordsmulti_match:VPN 登录 emptymatch_all; tenanttenant-a生产版本在本课会对 title/tags 加权所以这里只断言“查询类型与词项”不把字段权重写死在上课答案里。无论权重如何调租户条件都必须稳定。答案 EX-18-02 [prose]多个文档的updated_at相同若只按它排序ES 可以在不同请求之间以不同内部顺序返回它们追加映射为keyword的唯一ticket_id作为 tie-breaker使同一份静态索引、同一查询条件下有可重复的全序。不要改回_idElasticsearch 8 并不把内部_id暴露成普通可排序字段。但排序键不能冻结索引。翻页期间有新工单写入、旧工单更新分数和时间都可能改变search_after在两次请求里并不自动保存同一快照仍可能重/漏。生产级深分页应固定同一索引生成并使用 Point in TimePIT或其他快照机制还要管理 PIT 生命周期。当前 TicketFlow 教学模块提供的是有界search_after和显式稳定排序不假装已经实现跨更新的快照一致性。相关性是可以解释和验证的决策第 18 课查询字段是title/body现在变成{multi_match:{query:VPN 登录,fields:[title^3,tags^2,body],type:best_fields,operator:and}}^3表示标题在该查询中更重^2让标签次之正文为基准。这并非“标题命中必然第一”的保证分词、词频、字段长度、稀有度和过滤范围仍影响实际_score。真正调参要准备人工标注的代表性查询和期望前几名记录改动前后结果不能只凭一张工单调出“感觉不错”的权重。tags在 Mapping 里是keyword。把它纳入 multi_match 的含义是匹配完整标签词项不是让一个长标签像正文那样分词如果产品要标签模糊匹配应该另设分析字段并验证不能悄悄修改租户/状态等精确字段。为什么from100000不适合翻到很后面传统from size让 ES 为每个分片收集并排序前面大量被跳过的命中。深分页成本随 offset 放大而且更新发生时页边界会漂移。search_after则取上一页最后一条的排序三元组_score desc updated_at desc ticket_id asc 1.75 2026-09-21T08:30:0000:00 ticket-7下一页将这个三元组原样带回查询与排序必须保持一致。TicketFlow 把 JSON 数组做 URL-safe Base64 编码使客户端不必拼 ES DSLfromticketflow.search.queryimportSearchFilters,build_search_body,decode_cursor,encode_cursor cursorencode_cursor([1.75,2026-09-21T08:30:0000:00,ticket-7])assertdecode_cursor(cursor)[1.75,2026-09-21T08:30:0000:00,ticket-7]next_pagebuild_search_body(tenant_idtenant-a,queryVPN,filtersSearchFilters(),size10,cursorcursor,)assertnext_page[search_after]decode_cursor(cursor)print(search_after3 sort values)先保存上述完整片段再从project/运行命令python cursor_probe.py。预期输出search_after3 sort valuesBase64 只是编码不是签名或加密。客户端能改它服务端仍要验证游标形状、强制租户过滤、限制 size。当前decode_cursor只做基础结构校验高风险公网接口还应收紧字段类型与长度、把查询指纹/索引代数绑定到游标必要时签名。尤其 alias 切换后旧游标不能理所当然沿用。聚合到底统计了谁本课查询体同时包含aggs:{status_counts:{terms:{field:status,size:10}},priority_counts:{terms:{field:priority,size:10}}}聚合继承同一个query的词项和过滤范围因此绝不会统计其他租户。但是如果用户已经过滤statusopenstatus_counts也只看到 open。若产品希望选中 open 后依然显示“其他状态各有多少”需要把某些 UI 过滤移动到post_filter、同时保留不可移除的租户边界。那是另一种产品语义不应凭直觉改 DSL。本课先选择“当前结果集统计”并把这一点写在 UI 说明里。TicketFlow 不再停在“请求体里有aggs”。ElasticsearchHTTP.search()会把 ES 的status_counts、priority_counts验证并转换为SearchFacetsFastAPI 响应再输出稳定的key/count列表。例如{total:9,items:[],next_cursor:null,facets:{statuses:[{key:open,count:7},{key:pending,count:2}],priorities:[{key:high,count:4}]}}适配器不会把缺少doc_count的畸形桶悄悄当作 0它会报告SearchUnavailable避免页面展示一组看似可信的假统计。API 测试同时断言聚合桶确实穿过适配器和响应模型而不只是验证 DSL 长得像。验收与可见边界从课程根运行本课 revision verifierpython tools/verify_executable_revisions.py--lesson 19--python python此命令在全新 Python 3.11 venv 安装本课独立 revision并运行 baseline 与 09–19 的全部历史合同不会借用最终project/。它仍不能证明 ES 8 接受排序并真正分页。真实服务检查点test_real_elasticsearch_supports_ticket_id_search_after_and_facets必须单独通过project/中的python scripts/run_remote_selector.py --checkpoint 19 --selector tests/integration/test_search_elasticsearch.py::test_real_elasticsearch_supports_ticket_id_search_after_and_facets运行未获授权或未运行仍是 PENDING。遇到“第二页重复/漏掉”先记录相同查询参数、三元组、索引 generation 和翻页期间更新遇到“状态统计只有 open”先问产品要的是当前结果集统计还是所有状态的可选 facet遇到“标题仍排后面”拿同一组样本比较_explain/评分不是盲目把^3加到^100。本课练习练习 EX-19-01 [code]写cursor_guard_probe.py把[2.5, 2026-09-21T09:00:0000:00, ticket-9]编码再解码传进build_search_body()并断言search_after原样保留另传一个无效游标断言抛InvalidCursor。打印cursor round-trip ok和invalid cursor rejected。第 20 课给出完整答案。练习 EX-19-02 [prose]同一个 alias 正在从 v1 切到 v2用户带着 v1 页面末尾的游标请求下一页。说明为什么即使游标解码成功结果也未必连续分别给出产品上可接受的“重新开始搜索”和技术上更强的 PIT/索引 generation 绑定方案。接下来要追哪条链排序和游标都建立在“索引里已经有正确版本”之上。第 20 课会让 version 3 先到、version 2 后到再看 refresh 前后搜索可见性创建工单成功和搜索立刻命中从来不是同一个事务承诺。本课完整核心模块search/query.py这是lesson-19的完整查询构造器包含本课新增的 boost、游标和聚合。注意完整模块同步的是教学 revision而不是后来可能继续演进的最终源码。from__future__importannotationsimportbase64fromdataclassesimportdataclassimportjsonclassInvalidCursor(ValueError):The opaque search-after cursor failed structural validation.dataclass(frozenTrue,slotsTrue)classSearchFilters:statuses:tuple[str,...]()priority:str|NoneNonetags:tuple[str,...]()defencode_cursor(sort_values:list[object])-str:rawjson.dumps(sort_values,ensure_asciiFalse,separators(,,:)).encode()returnbase64.urlsafe_b64encode(raw).decode().rstrip()defdecode_cursor(cursor:str)-list[object]:try:paddedcursor*(-len(cursor)%4)valuejson.loads(base64.urlsafe_b64decode(padded).decode())except(ValueError,UnicodeError,json.JSONDecodeError)asexc:raiseInvalidCursor(cursor is not valid)fromexcifnotisinstance(value,list)orlen(value)!3:raiseInvalidCursor(cursor is not valid)returnvaluedefbuild_search_body(*,tenant_id:str,query:str,filters:SearchFilters,size:int,cursor:str|NoneNone,)-dict[str,object]:Build a bounded search request with a mandatory tenant boundary.ifnot1size100:raiseValueError(size must be between 1 and 100)exact_filters:list[dict[str,object]][{term:{tenant_id:tenant_id}}]iffilters.statuses:exact_filters.append({terms:{status:list(filters.statuses)}})iffilters.priorityisnotNone:exact_filters.append({term:{priority:filters.priority}})iffilters.tags:exact_filters.append({terms:{tags:list(filters.tags)}})normalized_queryquery.strip()must:list[dict[str,object]]([{multi_match:{query:normalized_query,fields:[title^3,tags^2,body],type:best_fields,operator:and,}}]ifnormalized_queryelse[{match_all:{}}])body:dict[str,object]{query:{bool:{filter:exact_filters,must:must}},sort:[{_score:desc},{updated_at:desc},{ticket_id:asc},],highlight:{fields:{title:{},body:{}}},size:size,track_total_hits:True,# Aggregations inherit the same query/filter scope as hits. In particular,# a facet can never count documents from a different tenant.aggs:{status_counts:{terms:{field:status,size:10}},priority_counts:{terms:{field:priority,size:10}},},}ifcursorisnotNone:body[search_after]decode_cursor(cursor)returnbody

相关新闻

自由学习记录(228)

自由学习记录(228)

xxxxxxxxxxxxxxxxxxx 源码里确认了一个关键顺序:**先稳定地形、水域范围和 PCG 规则,再完成地表与雪的整理,最后录水面。**PCG 会改写 Terrain 材质权重和清雪遮罩;雪的运行材质还会读取这些地表层。水面画面里能看到的河床&…

2026/10/13 6:49:51 阅读更多 →
SpringBoot智能出行系统开发实战

SpringBoot智能出行系统开发实战

1. 项目概述与背景作为一名长期从事Java开发的工程师,我最近完成了一个基于SpringBoot的智能出行系统毕业设计项目。这个系统将拼车和打车功能整合在一个平台上,解决了传统出行方式中信息不对称、流程不规范的问题。在实际开发过程中,我发现这…

2026/10/13 6:49:51 阅读更多 →
Cortex-M4 三级流水线 BL 带返回跳转指令(PC、LB)

Cortex-M4 三级流水线 BL 带返回跳转指令(PC、LB)

一、行业经典疑点:两段官方描述看似冲突在 Cortex‑M4 汇编体系中,关于 BL 指令存在两段标准描述,是嵌入式学习、面试、考试的核心易错点:硬件流水线描述:BL 执行顺序为先将 PC−4 赋值给 LR,再将目标函数入…

2026/10/13 6:49:51 阅读更多 →

最新新闻

SpringBoot+Vue打造装饰工程管理系统:完整实战解析

SpringBoot+Vue打造装饰工程管理系统:完整实战解析

做装饰工程项目管理的系统,很多老牌软件公司都停留在单体架构时代,界面老旧、流程僵硬,项目经理宁可自己拉Excel表格也不愿意用。而新团队上来就搞微服务分布式,对一家中小型装饰公司来说,根本就是杀鸡用牛刀&#xff…

2026/10/13 7:36:08 阅读更多 →
Dalsa Nano相机C++采集例程:从接口配置到稳定抓帧

Dalsa Nano相机C++采集例程:从接口配置到稳定抓帧

简介:面向机器视觉、自动化检测与工业现场应用的 Dalsa Nano 相机 C 使用例程,以 GigE Vision 千兆网通信协议为基础,完整演示了相机从连接初始化、参数配置到实时图像采集、显示与保存的整个流程。压缩包内含 77 个文件,主体为 C…

2026/10/13 7:36:08 阅读更多 →
Hive多DISTINCT性能优化:用COLLECT_SET替代COUNT(DISTINCT)

Hive多DISTINCT性能优化:用COLLECT_SET替代COUNT(DISTINCT)

1. 项目概述:为什么“多个DISTINCT”会成为Hive SQL里的隐形性能杀手你有没有写过这样的Hive SQL?SELECT dt,COUNT(DISTINCT user_id) AS dau,COUNT(DISTINCT CASE WHEN is_paying 1 THEN user_id END) AS paying_users,COUNT(DISTINCT CASE WHEN chann…

2026/10/13 7:36:08 阅读更多 →
Flutter鸿蒙化适配:floor_generator桥接SQLite的完整实践与踩坑记录

Flutter鸿蒙化适配:floor_generator桥接SQLite的完整实践与踩坑记录

上个月在适配一个跨平台进销存项目到鸿蒙平台时,数据库层成了第一个硬骨头。这个项目的十几个业务模块全部建立在 floor 这个 ORM 框架之上,实体类、DAO 接口、查询条件都是现成的,本来以为 Flutter 能在鸿蒙环境跑起来,floor 也能…

2026/10/13 7:36:08 阅读更多 →
虚拟机 ens33 没有 IP?ifconfig 不显示网卡地址的排查与修复

虚拟机 ens33 没有 IP?ifconfig 不显示网卡地址的排查与修复

刚装好的虚拟机,开机之后敲下 ifconfig ,屏幕上来回翻的输出里只有 lo 和那个让人无语的 ens33 ,连个 IPv4 地址都不给。这个画面我见过太多次,第一次踩坑时还以为是虚拟网卡坏了,把虚拟机删了重建两回&#xff…

2026/10/13 7:36:08 阅读更多 →
E2B沙箱实战:AI智能体代码执行失控防护与排障指南

E2B沙箱实战:AI智能体代码执行失控防护与排障指南

1. 先搞清楚:AI 智能体为什么总能把沙箱跑崩上周有个朋友找我,说他的 AI 智能体项目出事了:Agent 在沙箱里跑了一段“看似无害”的 Python 脚本,结果沙箱彻底失联,连宿主机都差点被拖挂,最后只能重启整台机…

2026/10/13 7:35:07 阅读更多 →

日新闻

物联网系统集成避坑指南:从选型到验收的关键要点

物联网系统集成避坑指南:从选型到验收的关键要点

做了这么多年物联网系统集成的项目,我最大的感受是,大部分坑不是出在技术前沿,而是出在一些看起来很平常的地方。设备选了便宜的、协议没问清楚、现场没摸透、验收标准写得含糊——这些细枝末节,最后都会变成延期、超支和扯皮的大…

2026/10/13 0:00:57 阅读更多 →
ESP32舵机控制全攻略:从PWM原理到高级玩法与常见故障排查

ESP32舵机控制全攻略:从PWM原理到高级玩法与常见故障排查

如果你玩ESP32有一阵子了,很难不碰到舵机控制这道坎。做智能小车要转方向,做机械臂要抬关节,做云台要偏航俯仰,背后都是同一个东西在干活:舵机。而我见过太多人卡在“代码能编译、舵机就是不听话”这个阶段&#xff0c…

2026/10/13 0:00:57 阅读更多 →
CAN总线原理与STM32实战:抗干扰、仲裁、错误处理全解析

CAN总线原理与STM32实战:抗干扰、仲裁、错误处理全解析

1. 为什么CAN总线至今仍是工业现场的“硬通货”?——从汽车底盘到智能温室的真实需求你有没有拆过一辆老款燃油车的中控台?或者调试过某高校实验室里那台跑着Modbus协议却频频丢帧的PLC控制柜?又或者,在某个农业物联网项目里&…

2026/10/13 0:00:57 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/13 2:54:56 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/12 0:16:38 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/13 1:58:08 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 21:05:09 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 14:48:49 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 18:11:17 阅读更多 →