先说结论
Odoo 企业版 Knowledge 的搜索体验之所以看起来很“懂权限”,不是因为它只在最后多做了一次 ACL 过滤,而是因为整条链路从一开始就把以下几件事揉在一起了:
- 用户是否有访问权;
- 文章是 visible 还是 hidden;
- 收藏是否应该优先;
- 搜索结果返回后,左侧树应该展开到哪个根祖先;
- 在结果太多时,全文检索并不追求绝对最优,而追求足够快且足够相关。
所以一句话概括:
Knowledge 搜索返回的不是“纯文本匹配结果”,而是“带权限、带导航上下文、带用户偏好的可落地结果”。
这篇主要看哪里
核心源码在:
enterprise/knowledge/models/knowledge_article.py
重点方法:
search_fetch()get_user_sorted_articles()get_sorted_articles()_get_first_accessible_article()_get_accessible_root_ancestors()
这几段放在一起看,才能理解为什么 Knowledge 搜索出来后,体验像“先把你带到正确目录,再展开正文”。
第一层:不是所有搜索都从全文检索开始
Knowledge 里至少有两条入口很容易混淆:
入口 A:普通列表 / web client 排序
这里会走 search_fetch()。
它专门处理一个特殊问题:
- 如果排序里带了
is_user_favorite,就不能粗暴地让数据库按普通顺序查。
源码的做法是分两步:
- 先把当前用户收藏的文章查出来;
- 再用剩余窗口补其他文章。
这意味着:
“收藏优先”不是前端自己调顺序,而是后端专门重写了查询窗口。
因此同样一组 domain,不同排序参数,Knowledge 给你的第一屏结果可能完全不同。
入口 B:Command Palette / 搜索框
这里更常走 get_user_sorted_articles()。
它先决定当前是在:
- visible 模式;
- 还是 hidden 模式;
- 没有搜索词时是不是优先返回 favorites。
只有在真正有搜索词时,才继续下沉到 get_sorted_articles()。
所以“搜索”其实不是一条单一查询,而是“用户上下文分流 + 文本匹配”的组合。
第二层:get_user_sorted_articles() 决定的是“搜哪一类文章”
这段方法最重要的不是全文检索本身,而是它先设了一个 domain:
is_article_visible != hidden_modeuser_has_access = True
也就是说,在真正排序之前,它已经先把两个边界钉死了:
边界 1:当前是在 visible 还是 hidden 空间
Knowledge 的 hidden 文章不是简单“搜索时也顺带搜一下”。
它有独立模式。
边界 2:当前用户必须真有访问权
这里很关键:
- 不是管理员就天然能看到别人的私有知识;
- 不是全文命中就能越权展示。
所以很多人说“Knowledge 搜索像带权限”,其实不够准确。
更准确的说法是:
Knowledge 的搜索范围本身就是权限裁过的,不是最后渲染时才补一刀。
第三层:get_sorted_articles() 不是追求绝对最准,而是追求可控性能
这段源码的注释写得很坦白:
- 会用
knowledge.fts_search_cut_off控制候选上限; - 会按 title+body、title-only、body-only 分层预选候选;
- 候选过多时,不保证穷尽所有匹配,只保证返回“足够相关且响应够快”的结果。
这很重要,因为它揭示了一个产品取舍:
Odoo 在这里优先要“企业知识库可用”
而不是学术意义上的全局最优搜索排序。
换句话说:
- 小库里,它尽量给你 top-k 真正最相关结果;
- 大库里,它更在乎延迟可控,再用预选 + 打分给你一批足够好的结果。
这也是为什么有时你会感觉:
- 搜索结果“很像懂你要什么”;
- 但在超大库、超泛关键词下,又不一定是全库最完美排序。
这是有意设计,不是 bug。
第四层:为什么用户总像是“先回到目录树,再进正文”
这就要看 _get_accessible_root_ancestors() 了。
这段逻辑会从当前文章往上回溯:
- 只要父文章仍然可读,就继续往上走;
- 把沿途可访问的祖先都收集起来;
- 最终形成一条“可访问的根祖先链”。
这意味着:
Knowledge 并不满足于“这篇正文你能看”。它还想知道“应该把你安放到哪一段你看得见的目录树里”。
所以当当前文章本身不是 root,或者上层有隐藏/权限边界时,系统仍会尽量找一条你可见的祖先路径,把侧边树正确展开。
这就是为什么 Knowledge 的体验不像裸搜索引擎,而像“搜索 + 导航定位器”。
第五层:_get_first_accessible_article() 解决的是默认落点问题
用户进 Knowledge 时,系统需要决定“先打开哪篇”。
源码顺序大致是:
- 如果用户有 active favorites,优先取 favorite;
- 否则在可读、非模板、可见的 root 文章里,按
sequence和internal_permission找第一篇。
这说明 Knowledge 的首页默认落点也不是随便选的。
它同时考虑:
- 用户偏好;
- 根目录结构;
- 可访问性;
- 可见状态。
所以“为什么我进来总先看到某篇文章”通常不是前端记忆,而是后端默认入口策略。
第六层:为什么说它不只是权限系统,而是“带导航语义的权限系统”
如果只有权限过滤,Knowledge 完全可以做成:
- 查出所有命中文章;
- 把没权限的去掉;
- 把剩下的直接列出来。
但它没有停在这里。
它还做了:
- favorites 前置;
- visible/hidden 分流;
- accessible root ancestors 回溯;
- 默认入口文章选择。
所以真实体验是:
- 你看到的不只是“哪几篇能读”;
- 还是“这些文章在你的知识树里应该怎么被安放”。
这正是企业知识库和普通全文检索的区别。
实战里最容易误解的几个点
误解 1:搜索排序全靠 PostgreSQL FTS
不对。
FTS 只是底层的一部分,外围还有:
- candidate cut-off;
- favorites 优先;
- visible/hidden domain;
- user access domain。
误解 2:能搜到说明一定在左侧树里可见
不一定。
系统还要通过 _get_accessible_root_ancestors() 找一条你可见的祖先路径,才能把导航树摆正。
误解 3:管理员就天然看到所有 private article
源码里 user_has_access = True 这类限制并不是摆设。Knowledge 明确希望“别人的私有知识”不要被普通搜索体验轻易穿透。
二开时最值得注意的边界
1. 不要只改前端排序,忽略 search_fetch()
你会发现收藏优先、分页窗口和首屏结果都不对。
2. 不要把 hidden/visible 混成一个 domain
用户会得到看似命中、实际无法落地的结果,体验很差。
3. 不要只返回正文结果,不处理祖先展开链
这会让左侧目录树和正文上下文脱节,用户会觉得“搜到了,但不知道它在哪”。
结论
Knowledge 搜索看起来像全文检索,但真实实现更像一套多层协同机制:
- 权限先裁范围;
- 收藏和可见性决定结果入口;
- FTS 决定候选与相关性;
- 根祖先回溯决定目录树怎么展开。
一句话总结:
Odoo Knowledge 搜索返回的不是一串匹配文本,而是一组“当前用户能安全落地、能看懂上下文、还能在目录树里站得住”的文章结果。
DISCUSSION
评论区