bigywater.tech
MongoDB · 39

文本索引:$text 的分词与打分

$text 搜索到底怎么分词、怎么打分?这篇用 8 篇短文档验证词干、停用词、短语、排除、权重和限制。

3.55 → 15.55
把 title 权重设成 10 后,标题含 quick fox 的文档得分
0 个
搜停用词 "the" 的结果数
1 个
一个集合上最多能有的文本索引数

文本索引

实测 · 本机执行输出
  test> db.docs.createIndex({title: "text", body: "text"}, {name: "ft"})
  ft
  test> db.docs.getIndexes().filter(i => i.name == "ft")
  [
    {
      v: 2,
      key: { _fts: 'text', _ftsx: 1 },
      name: 'ft',
      weights: { body: 1, title: 1 },
      default_language: 'english',
      language_override: 'language',
      textIndexVersion: 3,
      collation: { locale: 'simple' }
    }
  ]

文本索引的 key 是 {_fts: "text", _ftsx: 1},默认英文,每个字段权重 1。

词干

实测 · 本机执行输出
  test> db.docs.find({$text: {$search: "running"}}, {title: 1}).toArray().map(d => d._id + ":" + d.title)
  [ '2:Running shoes review' ]
  test> $search: "jumped"    // 文档 1 里是 jumps
  [ '1:The quick brown fox' ]
  test> $search: "foxes"    // 文档里有 fox 和 foxes
  [ '1:The quick brown fox', '6:Quick fox facts' ]
  test> $search: "ran"    // 文档 2 里有字面的 ran
  [ '2:Running shoes review' ]
  test> $search: "index"    // 文档里是 Indexes / index / indexing
  [ '7:Database indexing', '8:Fast queries' ]
  test> $search: "queries"    // 文档里有 queries / query
  [ '8:Fast queries', '7:Database indexing' ]

搜索词和文档都做词干化:jumped 能匹配 jumps,foxes 能匹配 fox,index 能匹配 Indexes 和 indexing;不规则变化(ran 与 run)不会归一。

停用词、大小写与重音

实测 · 本机执行输出
  test> $search: "the"
  0
  test> $search: "the quick"    // 等价于 quick
  [ 6, 1, 3 ]
  test> $search: "CAFE"    // 大小写、重音都不敏感
  [ '4:Café guide' ]
  test> {$caseSensitive: true}
  []
  test> {$search: "cafe", $diacriticSensitive: true}
  [ '4:Café guide' ]

the 这样的停用词不进索引,搜它返回 0 个;大小写和重音默认都不敏感,可以用 $caseSensitive、$diacriticSensitive 改变。

查询语法

实测 · 本机执行输出
  test> $search: "fox lazy"    // 多个词按 OR
  [ 1, 5, 6 ]
  test> $search: "\"quick brown\""    // 短语
  [ '1:The quick brown fox' ]
  test> $search: "quick -fox"    // 排除含 fox 的
  [ '3:Brown bread recipe' ]

多个词是 OR,引号里是短语,减号排除含该词的文档。

打分与权重

实测 · 本机执行输出
  test> 默认权重(title 1、body 1)
  [ '6:3.55', '1:1.33', '3:0.57' ]
  test> db.docs.dropIndex("ft"); db.docs.createIndex({title: "text", body: "text"}, {name: "ft2", weights: {title: 10, body: 1}})
  ft2
  test> title 权重 10
  [ '6:15.55', '1:13.33', '3:0.57' ]

用 {$meta: "textScore"} 取分数并排序;权重让某个字段里的命中得分成倍提高。

限制

实测 · 本机执行输出
  test> try { db.docs.createIndex({body: "text"}, {name: "ft3"}) } catch (e) { e.message.slice(0, 230) }
  An equivalent index already exists with a different name and options. Requested index: { v: 2, key: { _fts: "text", _ftsx: 1 }, name: "ft3", weights: { body: 1 }, default
  test> 没有文本索引的集合 $text
  text index required for $text query
  test> $text 不在 pipeline 第一阶段
  $match with $text is only allowed as the first pipeline stage
  test> explain 的计划
  TEXT_MATCH / IXSCAN ft2

一个集合只能有一个文本索引;没有文本索引不能用 $text;聚合里 $text 必须在第一阶段。

分步演示

文本索引
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / MongoDB 9.0.2(Homebrew 的 mongod、mongos)/ mongosh 2.12.0 / pymongo 4.18.3;每个实验在临时目录里起全新的 mongod(独立 dbpath,端口 9900 以上,只监听 127.0.0.1,WiredTiger 缓存 0.25 GB),需要副本集或分片集群时在本机起多个进程;命令输出来自真实的 mongosh,批量写入、计时与多线程用 pymongo;计时用 time.perf_counter,数字每次运行会有小幅抖动,ObjectId、时间戳等每次运行都不同。8 篇短文档,英文;分数取自 textScore
    • 没有验证:其它语言的分词与词干(default_language 为中文时不分词);大量文档上的文本索引大小与性能;textScore 的计算公式;复合文本索引(前缀等值字段);通配符文本索引($**);文本索引在分片集群上的行为;Atlas Search / 全文检索替代方案
    京ICP备2026067502号-1京公网安备11010502063487号