附件索引

Odoo 附件搜索为什么不是只看文件名:attachment_indexation 的文本提取、缓存与回退链路讲透

基于 attachment_indexation 源码,讲清 Odoo 如何从 docx、pptx、xlsx、OpenDocument 与 PDF 中抽取文本,为什么会按 checksum 做缓存,以及提取失败时系统如何优雅回退。

Odoo 开发 其他
进阶 开发者 1 分钟阅读
0 评论 0 点赞 0 收藏 8 阅读

很多人以为 Odoo 的附件搜索就是:

  • 认文件名;
  • 认 mimetype;
  • 最多再加个附件说明。

但看 /home/ubuntu/odoo-temp/addons/attachment_indexation/models/ir_attachment.py 就会发现,attachment_indexation 想做的是另一件事:

让“文件内容”也能进入搜索面。

也就是说,用户搜索的不只是 invoice_2026.pdf 这种文件名,理论上还能搜到 PDF、Word、表格正文里的字。

一、主入口其实只有一个:_index()

这个模块的核心不是几十个复杂模型,而是把 ir.attachment._index() 扩了一层。

入口逻辑非常清楚:

  1. 如果传了 checksum,先查 LRU 缓存;
  2. FTYPES = ['docx', 'pptx', 'xlsx', 'opendoc', 'pdf'] 依次尝试;
  3. 谁先成功提取出文本,就用谁;
  4. 如果都失败,再回退到父类 _index()
  5. 最终结果再按 checksum 放回缓存。

这套顺序说明了作者的优先级:

  • 先尽可能做深度文本抽取;
  • 失败也别把附件索引能力搞挂;
  • 同一份文件别重复做重活。

二、它不是“通用解析器”,而是按格式各自拆

模块没有试图发明一个万能文件阅读器,而是每种格式各走自己的提取逻辑。

docx:直接读 word/document.xml

  • zipfile 打开 docx;
  • 读取 word/document.xml
  • w:pw:htext:list 里的文本递归拼出来。

这意味着 Odoo 并不是渲染 Word 文档,而是直接去拿 Office Open XML 里的正文节点。

pptx:逐页读取 slide XML

  • 先找 ppt/slides/slide*.xml
  • 再从 a:t 文本节点里抽字。

所以演示文稿的“可搜内容”本质是每一页 slide 上的文本框内容,而不是视觉布局本身。

xlsx:按工作表转成接近 CSV 的文本

这个实现很实用。

  • 依赖 openpyxl
  • 每个 sheet 都会带上 sheet 名;
  • 每行单元格拼成 CSV 风格文本;
  • 空行会被跳过;
  • 最后再做一次 _clean_text_content() 归一化。

这意味着 Odoo 并不是要“理解 Excel 公式”,而是尽量把人眼能读到的表格值变成可检索文本。

OpenDocument:区分文本文档和电子表格

_index_opendoc() 做得比很多人想象得更细:

  • 如果 mimetype 指向 spreadsheet,就按表格抽;
  • 否则按 text:ptext:htext:list-item 抽普通文本;
  • 还特意限制行列重复展开次数,避免超大重复节点把索引撑爆。

这体现的是很典型的工程思路:

全文索引追求“够用的正文”,不是百分百忠实重建原文件结构。

PDF:交给 pdfminer.six

PDF 这条链路最现实:

  • 先检查文件头是不是 %PDF-
  • 再检查 pdfminer.high_level 是否存在;
  • 存在才继续跑解析;
  • 最后还要用 _clean_text_content() 清理 NUL、制表符和空白。

而且模块初始化时就会提醒:如果系统没装 pdfminer.six,PDF 索引不可用。

这也是为什么很多人会遇到:

  • 上传 PDF 没报错;
  • 但正文搜不到。

根本原因往往不是 Odoo 坏了,而是解析依赖根本没装。

三、_clean_text_content() 很不起眼,却直接决定搜索质量

全文索引最怕的不是“没抽到字”,而是抽出一堆脏文本。

这个清洗函数做了几件很值钱的小事:

  • 去掉 NUL 字符;
  • 把 CRLF/CR 归一到 LF;
  • 把 tab 变空格;
  • 折叠多余空白;
  • 最多保留一个空行层级。

这意味着模块关注的不只是“提取成功”,还关注:

提取后的文本是否适合被索引、比对和搜索。

四、为什么要按 checksum 做 LRU 缓存

index_content_cache = LRU(1) 看起来很小,但思路很明确。

索引附件正文是重活,尤其是:

  • PDF
  • 大表格
  • 多页 PPT

如果同一份文件刚索引完又被复制、重试或者重复访问,没必要再跑一遍全文抽取。

因此 _index() 在有 checksum 时会:

  • 先读缓存;
  • 最终再写缓存;
  • copy() 时还主动把原附件的 index_content 预热进缓存。

也就是说,作者默认认为“相同内容的附件会短时间重复出现”,所以值得用 checksum 做一个超轻量缓存。

五、提取失败为什么几乎都被吞掉

这个模块很多分支都写成:

  • try 解析;
  • except Exception: pass

这不是偷懒,而是边界意识。

因为附件索引是增强能力,不是主交易链路。

如果用户上传一个坏文档,Odoo 更希望结果是:

  • 附件照样能存;
  • 只是正文提取不到;
  • 而不是整次上传或业务操作失败。

所以这里的设计哲学很明确:

索引可以降级,但附件主流程不能被拖死。

六、测试文件只测 PDF,但反而说明了设计重点

test_indexation.py 很短,只验证一件事:

  • 给定测试 PDF;
  • _index() 应该抽出 TestContent!!

这说明作者最在意的不是“每种格式都做复杂集成测试”,而是先把最脆弱、最依赖外部库的 PDF 链路守住。

因为 PDF 往往最常见,也最容易因为外部依赖、编码和内容噪音出问题。

七、实战里最容易误解的 4 件事

1)附件能上传,不代表正文一定可搜

上传链路和正文索引链路是两回事。

2)搜不到,不一定是搜索引擎问题

也可能是对应格式的提取器没装、文件内部结构异常,或者正文被清洗后几乎没剩内容。

3)这个模块不是 OCR

它抽的是文件里已有的结构化文本。

如果 PDF 本质是扫描图片,没有 OCR 文本层,pdfminer 也帮不了你。

4)缓存不是为了长期保存,而是为了短路重复解析

这里的 LRU 很小,目标是“最近一次别白算”,不是做长期全文索引数据库。

总结

attachment_indexation 的核心价值不是“让附件支持更多格式”。

它真正做的是:

  • 对不同文档格式走定制文本提取;
  • 用清洗步骤把结果变成适合搜索的正文;
  • 用 checksum 缓存减少重复重解析;
  • 在失败时优雅回退,而不是中断附件主流程。

如果只记一句,可以记这句:

Odoo 附件索引的重点不是“懂文件”,而是“尽量把文件里的可读正文变成低风险、可搜索的文本”。

DISCUSSION

评论区

想参与讨论?先 登录 再发表评论。
还没有评论,你可以成为第一个留言的人。