Hotdry.

Article

纯 PHP 全文搜索实战:共享主机环境下的部署参数与维护阈值

探讨 php-fts 在共享主机环境下的工程化实践,提供 BM25 评分配置、碎片化阈值与维护策略的可落地参数。

2026-05-07systems

在 Web 应用开发中,全文搜索通常是 Elasticsearch、Meilisearch 或 Typesense 的领地。这些专业搜索引擎功能强大,但部署成本高、需要独立基础设施,对于运行在共享主机或小型 VPS 上的项目而言并不友好。php-fts 是一个完全使用纯 PHP 实现的全文搜索引擎,无需任何 PHP 扩展依赖,只需要一个可写的目录即可运行,为这类受限场景提供了可行的 FTS 解决方案。

架构核心:三 元语法索引与 BM25 评分

php-fts 采用了三 元语法(Trigram)索引而非传统的单词级倒排索引,这一选择有其特定的技术考量。三 元语法将每个连续字符序列切分为长度为 3 的子串,例如字符串 "leather" 会被分解为 "lea", "eat", "ath", "the", "her"。这种切分方式使得搜索对拼写错误和部分匹配具有天然的容忍度 —— 用户输入 "leathr"(少了一个 e)时,搜索引擎仍然能够通过匹配 "lea"、"eat"、"ath"、"thr" 等三 元语法找到相关文档。

索引数据结构采用固定大小的三 元语法查找表,文件大小约为 810 KB,实现了 O (1) 的查找复杂度,无需树结构遍历。这种设计在数据量较小(数百到数万文档)时表现出色,查询延迟可控制在毫秒级。根据在 OVH 共享主机上的实测数据,10,000 篇文档的搜索延迟中位数为 3.2 毫秒,P95 为 12.5 毫秒,最坏情况为 37.1 毫秒。这一性能对于面向终端用户的搜索接口已经足够。

评分机制使用 BM25 结合 IDF(逆文档频率),与 Lucene 和 Elasticsearch 使用相同的算法。BM25 通过两个参数控制评分行为:k1 控制词频饱和度(默认 1.5),b 控制文档长度归一化程度(默认 0.75)。这些是 Lucene 的标准默认值,意味着某个词在文档中出现 10 次并不比出现 3 次得分高 10 倍,这避免了常见词对评分的过度影响。同时,IDF 机制确保了出现在几乎所有文档中的三 元语法贡献较低分数,而罕见的三 元语法贡献较高分数。最终得分被归一化到 0 到 100 的范围内,结果对象中的 score 字段可直接用于构建分面计数、自定义排序或相关性阈值过滤。

部署场景与适用边界

php-fts 明确自身定位为 “无法部署专用搜索服务时的备选方案”,这一坦诚的定位值得称道。项目文档清晰列出了适用场景与不适用场景,使用者在评估时应当严格对照。

适用场景包括:运行在 OVH、Infomaniak、o2Switch 等共享托管环境;希望保持基础设施零额外开销;文档数量在数百到数万之间;索引构建为离线或定时任务,搜索在运行时执行。这些条件反映了该库的核心设计假设 —— 读多写少、数据量可控、环境受限。

不适用场景包括:需要实时索引且并发写入量大;文档数量达到百万级别;需要地理搜索或多租户隔离。这些限制并非实现缺陷,而是架构权衡的结果。在这些场景下,投入 Elasticsearch 或 Meilisearch 仍然是更理性的选择。

具体的数据规模参考包括:1,000 篇文档索引大小约 2.8 MB;10,000 篇文档约 21.7 MB;50,000 篇文档约 106 MB。插入性能方面,单条插入 10,000 篇文档在 Windows NVMe SSD 上耗时约 53 秒,在 Linux 共享主机上约 63 秒;而批量插入同样数量在两类环境下均可控制在 30 秒左右。生产环境应当优先使用 insertBulk 方法,该方法在整个批次期间仅获取一次文件锁,显著提升批量写入效率。

字段加权与过滤配置

搜索 API 支持字段加权(boosts)功能,允许开发者为不同字段设置不同的权重。例如,商品搜索场景下标题通常比描述更重要,可以如此配置:

$results = $engine->search('leather shoe', limit: 20, boosts: [
    'title'       => 3.0,
    'description' => 1.0,
]);

这一配置使得匹配出现在标题字段中的文档获得更高的相关性得分。权重数值的经验法则是:核心匹配字段设置 2.0 到 5.0,辅助字段保持 1.0,填充字段设置为 0.5 或更低。具体的数值需要根据实际业务场景中的搜索意图分布来调整。

过滤功能支持精确匹配、数值比较、范围查询、IN 和 NOT IN 以及数组字段的 CONTAINS 操作。过滤条件支持 AND 和 OR 组合,但至少需要提供一种。以下是一个典型的复合过滤场景:

$results = $engine->search('shoe', filters: [
    'and' => [
        ['field' => 'active',   'op' => '=',  'value' => true],
        ['field' => 'stock',    'op' => '>',  'value' => 0],
        ['field' => 'price',    'op' => '<=', 'value' => 300],
        ['field' => 'category', 'op' => 'in', 'value' => ['Shoes', 'Sport']],
    ],
    'or' => [
        ['field' => 'brand', 'op' => '=', 'value' => 'Adidas'],
        ['field' => 'brand', 'op' => '=', 'value' => 'Puma'],
    ],
]);

这种组合过滤在电商搜索、产品目录管理等场景中非常实用。需要注意的是,被过滤的字段必须在文档中存在,否则该文档会被排除在结果之外。

维护策略:碎片化监控与压缩阈值

php-fts 使用软删除机制处理文档删除操作。删除文档时,系统并非立即从索引文件中移除数据,而是将文档 ID 记录到 tombstones.bin 中。这种设计允许原子更新操作在单个文件锁内完成先删除再插入的流程,但会导致索引碎片化随时间累积。碎片化率可以通过 fragmentationRate 方法实时监控,该方法返回 0 到 100 之间的百分比数值。

项目文档建议当碎片化率超过 20% 时执行压缩操作(compact)。压缩过程会重建索引文件并清除已删除的文档,将碎片化率重置为 0。这是一个相对保守的阈值,在实际项目中可以根据删除频率和性能监控数据适当调整 —— 如果删除操作不频繁,30% 的阈值也是合理的;但如果系统频繁执行删除和更新,15% 的阈值可能更能保证搜索性能。

维护任务的具体实践建议如下:首先,在应用初始化时检查碎片化率并记录日志;其次,将压缩操作安排在低峰时段执行,因为压缩过程需要锁定索引文件,期间无法处理搜索请求;最后,对于数据量较大的场景,可以考虑实现增量压缩策略,即每次只处理部分碎片化严重的索引文件。

索引文件的便携性是一个实用的特性。由于所有数据存储在本地文件中,索引可以在不同服务器之间直接复制而无需重建。这一特性使得开发环境与生产环境之间的数据同步、灾备恢复以及版本迁移都变得简单直接。

总结与选型建议

php-fts 为 PHP 开发者提供了一个无需扩展依赖的全文搜索解决方案,在共享主机和小型 VPS 场景下具有明显的部署优势。其 BM25+IDF 评分机制确保了搜索结果的相关性,三 元语法索引提供了对拼写错误的部分容忍,完整的过滤和字段加权功能满足了大多数业务搜索需求。

选型时的核心判断标准可以简化为:如果文档数量在数万以内且基础设施不允许运行独立搜索服务,php-fts 是值得考虑的选项;如果数据规模预计会快速增长,或需要实时索引和高并发写入,则应当投入专业搜索引擎的怀抱。两者的边界并非绝对,但明确的场景认知可以帮助做出更理性的技术决策。


参考资料

systems