Skip to the content.

文档索引设计

使用方式和参数见README。索引只接收元数据和文本;文件解析、OCR、源文件授权和任务重试由宿主负责。

倒排索引、段与快照

倒排索引保存“词项到文档及位置”的对应关系,查询先找到对应记录,再读取少量结果字段。Lucene 把写入形成的文件组称为段,后台合并把多个段整理为更少的文件组。查询 reader 固定看到某一版本的段集合,多线程可以共享该快照。

本组件使用 Lucene.NET 4.8.0-beta00018 的 FSDirectory、IndexWriter 和 SearcherManager。一个实例持有一个 writer,Lucene 文件锁阻止同目录第二个 writer。公开写方法经实例锁串行;查询独立 Acquire/Release 共享 reader,不持有写锁。

SearcherManager 基于目录创建,因此 ApplyBatch 的未提交写入对查询不可见。Publish 先 Commit,再 MaybeRefreshBlocking,最后清空待发布版本缓存。提交成功但刷新失败时方法抛错,数据可能已持久;下次发布或重新打开可恢复可见性。查询不执行刷新,避免一次请求承担重新打开成本。

索引提交元数据保存格式标识和随机索引身份。打开已有目录先验证格式及身份,避免把其他 Lucene 索引当成本组件数据改写。身份参与分页游标签名,防止不同目录恰好使用相同 reader 版本时混用游标。

文档身份与版本栅栏

Scope 和 DocumentId 保留原始大小写及 Unicode。将每个字符串的严格 UTF-8 字节以长度前缀拼接后取 SHA-256,形成文档键;Scope 单独哈希为查询过滤字段。长度前缀防止拼接歧义,严格编码拒绝孤立代理项,避免替换字符把不同输入混为同一身份。

每个身份存储版本及内容指纹,指纹覆盖写入/删除类型、名称、路径、正文、大小和 UTC 修改时间。版本比较在同一写锁内进行:先检查待发布缓存,再查询已发布索引。预先计算整批版本决策,因此输入非法或同版本冲突不会写入前面的有效项。同批重复身份也按已计划的最高版本判断。

删除通过 UpdateDocument 写为不参与查询的墓碑,保留身份、版本、指纹和排序值。旧版本无法覆盖墓碑,同版本 Upsert 与 Delete 代表不同内容而冲突;更高版本允许重建。墓碑不会随正常合并消失。若清理墓碑或重建索引,宿主必须先确保权威源的版本及删除信息能阻止旧任务再次发布。

版本栅栏只保护索引状态。宿主必须在读取及提取源文档后检查源版本,不能把旧内容附到新的版本号上提交。

批量写入不是数据库事务。取消或底层异常可能发生在若干 UpdateDocument 已完成之后,成功写入项已经更新待发布缓存;宿主应幂等重放或放弃整个未发布 writer。不同调用者的缓冲变化由下一次 Publish 一并提交,业务需要整体隔离时应在编排层统一批次所有权。

包含、前缀和正文语义

文件名和路径先做 NFC、不变量小写以及路径分隔符归一化。每个字段有固定 1、2、3 Unicode 码点的 NGramTokenizer 索引,保留位置。短查询选同长度字段,较长查询选三元字段并构造零间距 PhraseQuery;顺序和相邻位置必须一致,避免仅交集命中但实际不存在该子串的假阳性。

前缀查询用同样的词项形成 SpanNearQuery,再用 SpanFirstQuery 将匹配锚定在位置零。目录筛选追加末尾斜杠后按前缀处理,区分 /a/ 与 /ab/。这避免对大量唯一完整路径做领先通配符或前缀词典枚举。NGram 字段会放大索引,必须同时观察索引体积和导入成本。

正文用无停用词 StandardAnalyzer,词项输入与查询均经过同样的 NFC/小写处理。它保留中文单字,AllTerms 用去重后的 MUST 词项查询,Phrase 保留原有位置。正文原文单独保存,用于可选的开头摘要;摘要截断不会切开 UTF-16 代理对,不执行匹配高亮。

Scope、live 标记、扩展名、目录和修改时间均作为索引查询条件进入收集器之前。禁止取前 K 条之后再做权限过滤,否则可能遗漏本来应返回的结果。源权限变化到索引发布之间仍有更新窗口,宿主需要更细粒度的实时 ACL 时应在设计其授权模型时一起解决。

Fast、Exact 与全局排序

两种策略使用相同查询和排序。所有 Target 默认 Path,按规范化路径及原始 DocumentId 的 UTF-8 字节字典序排序;正文可显式选择 Relevance,按 BM25 分数排序,分数平局用快照内 Lucene docID。元数据只允许 Path。

SortingMergePolicy 在合并时按 PathSort 整理段并记录排序标记。Fast 首屏使用 EarlyTerminatingSortingCollector,在每个确认排序一致的段收集 K+1 个匹配后停止;未排序的新段回退普通收集。各段的前 K+1 合并足以得到全局前 K+1,所以返回前 K 条后仍能准确判断 HasMore。提前停止得到的收集计数不是真正总数,Fast 永远返回 TotalHits=null。

Exact 不加提前终止包装,返回整个匹配集的 TotalHits。正文相关度不能按路径物理顺序安全提前终止,因此 Fast Relevance 也正常遍历候选,只省略对外总数;不承诺与 Fast Path 相同的加速。

Optimize 是显式导入维护入口,ForceMerge 到一个排序段并发布。正常更新依靠合并策略逐步整理,不在查询、Publish 或每个小批次强制合并。大合并可能占用 CPU、I/O 和额外磁盘空间。

分页、取消与关闭

游标编码索引身份与 reader 版本、查询签名、最后命中的 docID、路径/ID 或分数。签名覆盖 Scope、归一化查询、匹配、排序及筛选条件;允许分页时调整结果数量、摘要长度或是否计数。签名是参数一致性检查,不是授权凭据,Scope 始终由宿主授权确定。

当前快照版本不符时抛出 SearchCursorExpiredException。组件不保留旧快照跨请求的租约,不允许跨更新猜测 docID。Path 的后续页使用 Lucene paging collector 正常遍历,不套早停包装:该 collector 还会接收游标之前的匹配,如果直接按 K+1 早停会遗漏后续页。

取消在入口、批次项、段切换、每 256 次 Collect 及结果读取时检查;Lucene 查询重写、提交、合并和 I/O 不保证可立即中断。关闭先拒绝新操作,再等待在途操作结束;释放 manager 后 Rollback writer,最后关闭分析器和目录。未发布变化被放弃,已发布索引可独立重新打开。关闭不会删除索引目录。

性能证据与适用边界

原型使用精确总数及全局路径排序,在百万元数据的高命中场景出现 10ms 以上延迟;因此不能把 Fast 的新结果解释为旧 Exact 场景已达标。生产基准分别报告两种策略、排序、固定语料、资源、索引体积、导入及显式优化时间。

当前合成正文平均约 753 个 UTF-16 字符且高度重复。热文件缓存、单线程、无并发更新的结果不能证明真实长文档、HTTP 或多用户负载达标。Lucene 是成熟库,仍需按宿主语料验证分析器、资源预算及查询分布;此组件没有实现自研全文引擎。