很多人调 Odoo 搜索功能时,脑子里只有一句话:
搜索不就是拼个 domain 吗?
这句话不能算错,但它只说中了最后一步。
真正难的部分,是前端如何把这些东西同时组织起来:
- 搜索栏里的 filter
- group by
- favorite
- order by
- 日期区间
- search panel 左侧分类树
- 当前上下文和全局 domain
- 页面刷新后的状态恢复
这也是为什么 addons/web/static/src/search/search_model.js 会这么长。它不是单纯的 domain 工具,而是 Odoo 搜索 UI 的状态中枢。
一、SearchModel 管的不是“一个查询条件”,而是一整套搜索会话
看 load(config) 就能明白,SearchModel 一上来接收的不是一个 domain,而是一整组搜索环境:
resModelcontextdomaingroupByorderBysearchViewArchsearchViewFieldsirFilterssearchMenuTypesdisplay.searchPanelstate
这说明官方设计 SearchModel 时,目标不是“帮你算个查询表达式”,而是:
把当前这一次列表/看板/报表搜索会话完整建模出来。
所以它天然是前端状态容器,而不是后端 domain 包装器。
二、为什么 SearchModel 要自己持有 globalContext / globalDomain / globalGroupBy
在 load() 里,源码会先把:
globalContextglobalDomainglobalGroupByglobalOrderBy
收进模型。
这一步非常关键。它意味着 SearchModel 从一开始就区分两层东西:
1)入口预设
来自 action、菜单、view 配置、当前上下文的默认约束。
2)用户交互后的增量状态
比如临时点亮一个筛选器、改一个 group by、切一个 favorite。
如果不分层,前端很快就会出现两个典型问题:
- 你不知道哪些条件是入口强制给的,哪些是用户刚刚点的;
- 一旦用户“清空筛选”,容易把本该保留的基础约束也一并清掉。
SearchModel 把全局预设先单独存住,本质上是在保护“入口语义”。
三、SearchModel 里的 query / sections / searchItems 才是 UI 真正的语言
源码里有几个很值得注意的状态:
querysearchItemssectionssearchPanelInfo
这几个名字已经暴露了设计思路:Odoo 搜索 UI 不是直接围着 domain 转,而是先围着可交互对象转。
searchItems 是什么
它更像一份“可被启用、关闭、组合的搜索项目录”。
里面的条目可能来自:
- search view 里的 filter
- group by 项
- date filter
- favorite
- 自定义动态项
sections 是什么
它更接近 search panel 左侧的分区结构。源码里甚至专门把 section 分为 category 和 filter 两种。
这说明 search panel 不是普通附属 UI,而是 SearchModel 的内建部分。
query 是什么
它不是单纯字符串,而是当前已激活搜索条件的抽象状态集合。
换句话说,SearchModel 先维护“当前界面上到底激活了哪些搜索语义”,然后再导出 domain / context / groupBy / orderBy 给后续数据层使用。
四、为什么 favorites 在 SearchModel 里不是“另一个过滤器”
源码定义了:
FAVORITE_PRIVATE_GROUP = 1FAVORITE_SHARED_GROUP = 2
这说明 favorite 从设计上就不是普通 filter。
普通 filter 更像“当前这次会话里临时点亮的条件”; favorite 更像“可持久化、可复用、可共享的搜索方案”。
也正因为如此,favorite 在 SearchModel 里要处理的事情更多:
- 保存的不只是 domain
- 还可能带 group by、排序、上下文
- 还要区分私有和共享
- 还要支持恢复为当前 active query
这能解释为什么很多人自己做搜索定制时,临时筛选能跑,收藏搜索却乱掉:
因为你以为自己保存的是一个 domain,实际你在保存一份完整搜索状态快照。
五、日期筛选为什么会单独复杂:它不是常量 filter,而是“运行时生成条件”
SearchModel 专门引入了:
constructDateDomaingetIntervalOptionsgetPeriodOptionsDEFAULT_INTERVALreferenceMoment
这已经说明日期筛选不是普通的静态 filter 节点。
它的复杂度主要来自三点:
1)同一个“本月”会随着时间变化
今天点的“本月”和下个月再恢复这个 favorite,本质上不是同一组固定日期。
所以它需要相对时间语义,而不是只存结果。
2)日期 / datetime 语义不同
时区、边界、粒度都会影响最终 domain。
3)UI 上看起来像一个简单选项,底层却需要序列化成可恢复配置
这就是 SearchModel 必须显式维护 reference moment 和 interval 逻辑的原因。
很多自定义出 bug,不是 domain 拼错,而是把动态时间条件当成静态筛选项来保存。
六、SearchModel 为什么要自己加载 search view,而不是等别人把结果喂给它
load() 里如果发现需要,会调用 viewService.loadViews() 去取 search view。
这说明 SearchModel 并不是一个纯被动容器,它会主动参与“搜索语义解释”的前半段。
原因很简单:
- filter 定义在 search arch 里
- field 信息在 view fields 里
- favorite 也可能依赖 search view 语义
- search panel 是否可显示,和 view 描述也有关
所以 SearchModel 不是拿到一个 domain 后才开始工作,而是会先参与:
- 解析 search view
- 建立搜索项目录
- 恢复 state
- 再产出最终查询语义
这就是它和“普通查询参数对象”的根本区别。
七、状态可恢复,是 SearchModel 最重要也最容易低估的能力
源码里有 mapToArray、arraytoMap、deepCopy 这些工具,以及对 state 的支持。
这背后的真实目标是:
当前搜索界面必须能被序列化、恢复、复制,并在不同交互周期里维持一致。
为什么这一点重要?因为 Odoo Web Client 不是一次性页面:
- 用户会切换视图
- 会刷新页面
- 会保存 favorite
- 会从 action 返回
- 会带着上下文重新进入同一个模型
如果搜索状态不能恢复,用户感知就会非常差。
所以 SearchModel 其实承担了很像“前端会话状态机”的角色。
八、search panel 不是边栏装饰,而是 SearchModel 的第一等公民
源码里专门有:
categoriesLoadIdfiltersLoadIdsearchPanelInfosectionshasValues(section)
这说明左侧 search panel 的加载、分组、计数、层级和可见性,不是临时拼在外面的。
官方是把它视为搜索模型内部的一部分。
这点很重要,因为很多二开喜欢犯一个错误:
- 列表顶部搜索栏自己做一套状态
- 左侧 panel 自己再做一套状态
- 两边最后只靠 domain 勉强对齐
这样短期能用,长期就会出:
- 选中态不同步
- favorite 恢复不完整
- panel 计数与顶部 query 不一致
- 分组条件和 facet 显示打架
SearchModel 的价值,恰好就是把这些状态统一在一个地方。
九、从源码看,SearchModel 解决的是“前端搜索语义组合”问题
如果只从 ORM 角度理解搜索,就会以为搜索系统的核心在:
- domain
- context
- order by
- group by
但 SearchModel 让我们看到,前端其实还有一层更难的问题:
1)UI 项如何映射到查询语义
不是每个搜索项都等价于 domain。
- 有些是 group by
- 有些是 context
- 有些是 favorite 快照
- 有些是 search panel 分类状态
2)多个来源如何合并
- action 默认值
- search view 定义
- 当前用户点击
- 已保存 favorite
- 左侧 panel 当前状态
3)状态如何持久化与恢复
这部分如果没有统一模型,搜索体验就会非常碎。
十、实战里怎么判断自己是不是用错了 SearchModel 思路
如果你遇到下面这些问题,通常就不是简单 domain 问题:
- filter 点亮了,但 favorite 恢复后丢了一半状态
- search panel 左边和顶部 facet 不同步
- group by 改了以后收藏搜索不一致
- 日期筛选下个月恢复时结果完全不对
- 页面回来后顶部 UI 还在,但实际查询条件变了
这些症状大多指向一个共同问题:
你只保存了“查询结果表达式”,没有保存“搜索会话状态”。
而 SearchModel 正是官方给出的那层中介。
十一、结论
Odoo 搜索栏真正复杂的地方,不在“domain 怎么拼”,而在:
- 搜索项如何被建模;
- 不同类型条件如何组合;
- search panel、favorite、date filter、group by 如何保持同一份状态语义;
- 整个搜索会话如何被序列化、恢复和重放。
所以更准确的理解应该是:
SearchModel 不是一个 domain 生成器,而是 Odoo 搜索 UI 的状态操作系统。
理解这一点后,你做搜索二开时就不会只盯着 domain 末端,而会先问:
- 当前状态由谁持有?
- 哪些条件是入口预设?
- 哪些条件是用户激活?
- 哪些东西需要被 favorite 一起持久化?
这才是把 Odoo 搜索功能做稳的关键。
DISCUSSION
评论区