很多人以为 Odoo 的附件搜索就是:
- 认文件名;
- 认 mimetype;
- 最多再加个附件说明。
但看 /home/ubuntu/odoo-temp/addons/attachment_indexation/models/ir_attachment.py 就会发现,attachment_indexation 想做的是另一件事:
让“文件内容”也能进入搜索面。
也就是说,用户搜索的不只是 invoice_2026.pdf 这种文件名,理论上还能搜到 PDF、Word、表格正文里的字。
一、主入口其实只有一个:_index()
这个模块的核心不是几十个复杂模型,而是把 ir.attachment._index() 扩了一层。
入口逻辑非常清楚:
- 如果传了
checksum,先查 LRU 缓存; - 按
FTYPES = ['docx', 'pptx', 'xlsx', 'opendoc', 'pdf']依次尝试; - 谁先成功提取出文本,就用谁;
- 如果都失败,再回退到父类
_index(); - 最终结果再按 checksum 放回缓存。
这套顺序说明了作者的优先级:
- 先尽可能做深度文本抽取;
- 失败也别把附件索引能力搞挂;
- 同一份文件别重复做重活。
二、它不是“通用解析器”,而是按格式各自拆
模块没有试图发明一个万能文件阅读器,而是每种格式各走自己的提取逻辑。
docx:直接读 word/document.xml
- 用
zipfile打开 docx; - 读取
word/document.xml; - 把
w:p、w:h、text: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:p、text:h、text: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) 看起来很小,但思路很明确。
索引附件正文是重活,尤其是:
- 大表格
- 多页 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
评论区