rankingScoreThreshold(0.0–1.0)是停扫语义:搜索结果按相关度降序排,引擎扫到第一个分数低于阈值的文档就停,其后全部排除。被排除的文档不计入estimatedTotalHits、totalHits,也不计入 facet 分布——计数骤降不是索引坏了,是阈值把低分档整体切掉了。
Issue & Env(案发现场与环境)
- 业务场景:给搜索框加「低于 0.5 分的结果不展示」的质量阀门,加上
rankingScoreThreshold: 0.5后发现:结果数从 2 掉到 1,前端统计的总数和分面筛选器的数字一起缩水,误以为索引丢数据或 filter 失效。 - 环境:Meilisearch v1.54+,搜索请求带
rankingScoreThreshold参数。 - 示例索引用
books_rank(6 条书籍文档,含title_en、summary、genres字段),查询词dark forest:
curl -X POST http://localhost:7700/indexes/books_rank/search \
-H "Authorization: Bearer aSampleMasterKey" \
-H "Content-Type: application/json" \
-d '{"q":"dark forest","facets":["genres"]}'
无阈值时两个分档都在:《黑暗森林》(title_en 命中 dark + forest,words 2/2)和《影之森》(title_en 只命中 forest,words 1/2):
{
"hits": ["黑暗森林 9.2", "影之森 8.5"],
"estimatedTotalHits": 2,
"facetDistribution": { "genres": { "科幻": 1, "悬疑": 1 } }
}
(返回体为关键节选;具体分值以本地实测为准。)
StackTrace(无报错,计数骤降)
加上阈值再搜(0.5 取在两档分数之间;先开 showRankingScore 看分数分布再选):
curl -X POST http://localhost:7700/indexes/books_rank/search \
-H "Authorization: Bearer aSampleMasterKey" \
-H "Content-Type: application/json" \
-d '{"q":"dark forest","facets":["genres"],"rankingScoreThreshold":0.5}'
{
"hits": ["黑暗森林 9.2"],
"estimatedTotalHits": 1,
"facetDistribution": { "genres": { "科幻": 1 } }
}
《影之森》从 hits 里消失,estimatedTotalHits 从 2 变 1,facetDistribution 里「悬疑」整个不见了。返回码 200,没有任何报错——但前端的总数、分面计数全都「变少」了。
Root Cause(根因分析)
停扫是设计,不是过滤
官方 search API 文档对 rankingScoreThreshold 的描述:在第一个分数低于该值的文档处停止搜索,其后所有文档排除。这能成立的前提是结果已按相关度降序排列——词典序保证了后面的分数只会更低,扫到第一个低分就可以收工。所以它不是「逐条过滤后如实统计」,而是「提前收工,后面的不看了」。
计数与分面随之缩水
官方文档同一段明确:被排除的命中不计入 estimatedTotalHits、totalHits 与 facet 分布。所以:
estimatedTotalHits变小 ≠ 索引数据变少,它统计的是「阈值之上」的命中;- 分面筛选器里某个选项消失,只说明该选项的文档全在低分档,不代表分类数据丢失;
- 想要「全量命中数 + 阈值内命中数」两个数字,得发两次请求(一次带阈值、一次不带),引擎不会一次给全。
与 page/hitsPerPage 同用会变慢
文档同时提示:rankingScoreThreshold 与 page/hitsPerPage 一起用时可能降低性能——exhaustive 分页要求拿到精确总数,停扫优化直接失效,引擎必须给全部命中文档打分。
The Fix(解决方案)
第一步:先看分数分布,再定阈值
curl -X POST http://localhost:7700/indexes/books_rank/search \
-H "Authorization: Bearer aSampleMasterKey" \
-H "Content-Type: application/json" \
-d '{"q":"dark forest","showRankingScore":true,"showRankingScoreDetails":true}'
用 _rankingScore(0.0–1.0,1.0 为完美匹配)观察你的数据在典型查询下的分数落点,把阈值定在「好结果档」与「差结果档」的分界上——不同数据、不同查询的分数分布差异很大,别照搬别人的 0.9。
第二步:前端别把 estimatedTotalHits 当全量计数
带阈值的响应里,estimatedTotalHits 的语义就是「阈值之上的命中数」。UI 文案写「约 N 条结果」时要么接受这个口径,要么单独发一次不带阈值的请求拿全量数字。
排查建议
- 计数骤降 + 无报错 + 只有带阈值请求复现 → 先怀疑阈值,
GET一遍请求体里的rankingScoreThreshold是不是被上游默认值或 SDK 常量塞进去了。 - 分面选项消失时,去掉阈值重发一次:数字回来就是阈值切的,不是数据丢了。
- 需要精确计数(页码导航)时用
page/hitsPerPage体系并接受性能代价;阈值和精确计数本来就是反向优化,二者只取其一。
References(参考来源)
- Search API reference(rankingScoreThreshold)— Meilisearch 官方文档meilisearch-docs
- Ranking score — Meilisearch 官方文档meilisearch-docs
