TF-IDF 关键词提取

解读

国内 PHP 面试中,TF-IDF 并不是为了让你现场推导数学公式,而是考察三件事:

  1. 你是否理解“关键词”在业务中的价值(搜索、推荐、标签、SEO);
  2. 能否把算法思想落地到 PHP 生态(无需 C++ 重写,而是用现成扩展或 Composer 包);
  3. 是否具备工程化思维:中文分词、停用词、分布式文本去重、增量索引、性能优化。
    面试官常追问:
  • “1000 万条商品标题,每晚增量 50 万,如何用 PHP 跑 TF-IDF 并 5 分钟内更新索引?”
  • “如果商品标题很短,TF 几乎全是 1,怎么解决?”
  • “IDF 需要全局语料,多机如何共享?”
    答不到工程细节,会被判定为“只会背公式”。

知识点

  1. 经典公式
    TF(t,d) = 词 t 在文档 d 中出现的次数 / 文档 d 总词数
    IDF(t) = log(语料文档总数 N / 包含 t 的文档数 df)
    TF-IDF = TF × IDF
  2. 中文陷阱
    没有天然空格,必须先分词;国内主流:jieba-php(基于 FFI 调用 libjieba)、scws、结巴分词纯 PHP 移植版。
  3. 停用词与归一化
    使用《中文停用词表》+ 业务自定义词(“包邮”“正品”),统一转小写、全角转半角、数字正则归一化。
  4. 稀疏矩阵与性能
    1000 万×50 万维矩阵无法放内存,采用 CSR 压缩或 Elasticsearch 的 “term_vector” 存储,PHP 端只取 Top-K。
  5. 增量更新
    每天新增商品只计算增量 DF,旧 DF 放 Redis Hash,Lua 脚本原子累加;IDF 延迟 24 h 重新计算,避免每次全量。
  6. 短文本平滑
    标题长度 < 8 词时,TF 失去区分度,改用 BM25 或双通道:TF-IDF + TextRank 做融合排序。
  7. 分布式方案
    语料放 Hive,MapReduce 预聚合 DF;PHP 通过 Thrift 读取汇总结果,本地只保留 Top-5 万词库。
  8. 安全与合规
    用户生成内容需先过敏感词过滤(DFA 树),防止“TF-IDF 把违规词排到前面”。

答案

“我去年在电商搜索组负责商品关键词提取,日增量 50 万 SKU,平均标题 12 个词。整体分四步:

  1. 预处理:Nginx 日志异步写到 Kafka,Consumer 用 PHP 进程池拉取,jieba-php 做分词,停用词 1893 个,统一转小写。
  2. 统计 DF:Redis Hash 分 1024 个桶存词→df,Lua 脚本保证原子自增;每晚 Spark 离线合并全量,输出 IDF 表到 MySQL。
  3. 在线计算:PHP-FPM 收到标题后,先在本地 OPcache 缓存的 IDF 数组里查,TF 实时统计,取 TF-IDF Top-5 词,写入 ES 的 keyword 字段。
  4. 性能:引入 FFI 后分词耗时从 18 ms 降到 3 ms;ES 使用 term_vector 跳过 PHP 端排序,整体 P99 延迟 28 ms。
    短文本平滑:标题词长 < 8 时,额外跑一遍 TextRank,与 TF-IDF 分数加权 0.7:0.3,A/B 测试 CTR 提升 4.2%。”

拓展思考

  1. 如果业务扩展到多语言(英文+日文),如何复用同一套 PHP 服务?
    方案:语言字段路由到不同分词器(jieba / elasticsearch-analysis-ik / mecab),DF 与 IDF 按语言维度分表,PHP 端用策略工厂模式切换。
  2. 当语料出现“概念漂移”(突然流行“冰墩墩”),IDF 历史值会压不住新热词,如何实时发现?
    可引入时间衰减 DF:df_t = Σ weight_i,weight_i = exp(−Δt/τ),τ 取 7 天,PHP 每晚跑离线脚本更新,保证新词 3 小时内 IDF 下降 30%,提升召回。
  3. PHP 8.2 的 JIT 对大规模矩阵运算帮助有限,是否考虑把 TF-IDF 批量计算下沉到 Rust 扩展?
    思路:用 ext-php-rs 写 Rust 扩展,暴露 tfidf_batch(array $docs): array,PHP 端仍保持业务编排,实测 10 万 标题 5 秒算完,内存占用降 45%。