StackTraces
Meilisearch从零跑通

从零跑通 Meilisearch 排序与自定义排名:7 步,外加 3 个容易卡住的地方

@debug_master2026-10-06

跟着这篇走完,你会在终端把 Meilisearch 的排序跑通:用 sort 参数给搜索结果按评分、价格、日期排序,再把“新品优先”这类规则写进索引设置里常驻生效。整个过程 7 步,每步都有一条命令能验证自己做对了没有。

动手前先交代下环境。localhost:7700 是 Meilisearch 的默认监听地址,master key 固定用 aSampleMasterKey(与系列一致),索引(Meilisearch 的独立文档集合,相当于一张数据表)固定用 books_sort(本篇新建,不动前面的 books_zh),示例文档是 6 条虚构书籍;你要是换了端口、master key 或索引名,把示例值替换掉就行。这篇默认你已经装好并跑通了从零跑通 Meilisearch 本地搜索服务,还没跑起来先回去把服务起了再回来,安装、启动、异步任务轮询的套路全部复用第一篇。

Meilisearch:从零跑通排序与自定义排名的 7 个步骤

前置条件

项 要求 说明
Meilisearch v1.54.3 或更高 与第一篇一致;本篇关键前提是版本 ≥ v1.36(默认规则拆成 attributeRank + wordPosition),太低照做会报错
前置文章 已跑通 meilisearch-local-search 复用其安装/启动/master key/异步任务轮询套路;没跑通先回去补
master key aSampleMasterKey 与系列一致
curl 任意版本 全文用 curl 调 REST API

先跑这条,确认第一篇的服务还活着。

curl http://localhost:7700/health
{"status":"available"}

返回 {"status":"available"} 就满足条件。要是返回 curl: (7) Failed to connect to localhost port 7700,说明服务没起——回第一篇第一步用 meilisearch --master-key="aSampleMasterKey" 重新启动,再回来。

第一步:确认服务在跑、版本对不对

本篇大量依赖一个事实:v1.36 起 Meilisearch 把默认规则里的 attribute 拆成了 attributeRank 和 wordPosition,默认排名规则(rankingRules,决定搜索结果先后顺序的规则列表)从 6 条变成 7 条。后面写排名规则时要按新名单来,所以第一步先把版本确认掉,省得照老教程做了一路、最后在设置接口上报错。

curl http://localhost:7700/version -H "Authorization: Bearer aSampleMasterKey"
{"commitSha":"7ebd97e38c36882b0c78d79e433b9115b4f003b3","commitDate":"2026-10-01T11:51:22.000000000+02:00","pkgVersion":"1.54.3"}

版本号在 pkgVersion 字段里。验证标准就一条:版本 ≥ 1.36 就行。低于 1.36 的旧版默认规则不含 attributeRank/wordPosition,照本篇的命令做会踩到“找不到规则”之类的错,先升级再回来。

第二步:建 books_sort 索引 + 写 6 条书

排序要先有“可排序的字段”,而字段类型直接决定排对排错。这一步把数据底座搭好:rating、price 用 JSON 数字,release_date 用 YYYY-MM-DD 格式的字符串——数字按数值比大小,日期按字符串词典序比,这是后面所有步骤的前提。用独立索引 books_sort,不动第一篇的 books_zh。

索引走显式创建,直接指定主键字段为 id,避免自动推断失败:

curl -X POST http://localhost:7700/indexes \
  -H "Authorization: Bearer aSampleMasterKey" \
  -H "Content-Type: application/json" \
  -d '{"uid":"books_sort","primaryKey":"id"}'
{
  "taskUid": 7,
  "indexUid": "books_sort",
  "status": "enqueued",
  "type": "indexCreation"
}

创建索引是异步任务(task,后台执行的操作单元),返回体里的 taskUid 就是任务编号。全新环境任务号从 0 开始;要是你的环境已经跑过其他任务,以实际返回的 taskUid 为准。

接着写 6 条书:

curl -X POST http://localhost:7700/indexes/books_sort/documents \
  -H "Authorization: Bearer aSampleMasterKey" \
  -H "Content-Type: application/json" \
  --data-binary '[
    {"id": 1, "title": "三体", "author": "刘慈欣", "rating": 9.3, "price": 39.9, "release_date": "2008-01-01", "genres": ["科幻"]},
    {"id": 2, "title": "球状闪电", "author": "刘慈欣", "rating": 8.8, "price": 29.0, "release_date": "2004-06-01", "genres": ["科幻"]},
    {"id": 3, "title": "活着", "author": "余华", "rating": 9.1, "price": 35.0, "release_date": "1993-01-01", "genres": ["文学"]},
    {"id": 4, "title": "许三观卖血记", "author": "余华", "rating": 8.7, "price": 32.0, "release_date": "1995-03-01", "genres": ["文学"]},
    {"id": 5, "title": "明朝那些事儿", "author": "当年明月", "rating": 9.0, "price": 45.5, "release_date": "2006-09-01", "genres": ["历史"]},
    {"id": 6, "title": "万历十五年", "author": "黄仁宇", "rating": 8.9, "price": 28.0, "release_date": "1982-05-01", "genres": ["历史"]}
  ]'
{
  "taskUid": 8,
  "indexUid": "books_sort",
  "status": "enqueued",
  "type": "documentAdditionOrUpdate"
}

文档添加同样是异步任务,POST 返回的 taskUid 只表示入队,文档还没被索引。等对应任务 succeeded 再往下走。

确认数据到位:

curl http://localhost:7700/indexes/books_sort/stats \
  -H "Authorization: Bearer aSampleMasterKey"
{
  "numberOfDocuments": 6,
  "isIndexing": false,
  "fieldDistribution": {
    "author": 6, "genres": 6, "id": 6, "price": 6,
    "rating": 6, "release_date": 6, "title": 6
  }
}

返回里 numberOfDocuments 是 6、isIndexing 是 false,说明 6 条文档全部索引完。要是数量是 0 或 isIndexing 还是 true,多半是任务没等完就查,回第一篇把轮询套路捡起来,见Meilisearch 添加文档后立即搜索返回空结果。

第三步:先撞一次坑——不声明就传 sort

很多人以为排序属性跟在文档里、直接传 sort 就能排。这一步故意不声明任何东西就传 sort,让报错把“前置声明”这个核心约束讲明白:排序依赖为属性预建的内部数据结构,必须在设置里显式声明。

curl -X POST http://localhost:7700/indexes/books_sort/search \
  -H "Authorization: Bearer aSampleMasterKey" \
  -H "Content-Type: application/json" \
  -d '{"q":"","sort":["rating:desc"]}'
{
  "message": "Index `books_sort`: Attribute `rating` is not sortable. This index does not have configured sortable attributes.",
  "code": "invalid_search_sort",
  "type": "invalid_request",
  "link": "https://docs.meilisearch.com/errors#invalid_search_sort"
}

rating 明明在文档里,为什么不能排?因为 filter 有 filterableAttributes、sort 有 sortableAttributes,两套列表相互独立,默认都是空数组。排序靠的是索引阶段为属性建好的列式结构,不能在搜索请求里临时为任意字段排序——把高成本操作前置到索引阶段,是 filter/sort/facet 三者的统一设计。

顺带看一眼默认设置,把排名规则(rankingRules)的默认名单记下来,后面两步要用:

curl http://localhost:7700/indexes/books_sort/settings \
  -H "Authorization: Bearer aSampleMasterKey"
{
  "rankingRules": ["words", "typo", "proximity", "attributeRank", "sort", "wordPosition", "exactness"],
  "sortableAttributes": [],
  "filterableAttributes": []
}

验证就两条:上面请求的返回码是 invalid_search_sort(不是 200);settings 里 sortableAttributes 是 []。要是你看到的是 Attribute "xx" is not filterable 那类报错,说明把 sort 和 filter 的声明搞混了,排查入口见Meilisearch sort 属性未声明报 invalid_search_sort。

第四步:声明 sortableAttributes

第三步的报错说明缺前置声明,这一步补上。把三个要排的字段一次性放进 sortableAttributes(允许参与排序的属性白名单):

curl -X PUT http://localhost:7700/indexes/books_sort/settings/sortable-attributes \
  -H "Authorization: Bearer aSampleMasterKey" \
  -H "Content-Type: application/json" \
  --data-binary '["rating","price","release_date"]'
{
  "taskUid": 9,
  "indexUid": "books_sort",
  "status": "enqueued",
  "type": "settingsUpdate"
}

这一步有个容易忽略的点:设置更新也是异步任务(type 为 settingsUpdate),还会重索引该索引的全部数据。任务没 succeeded 就发带 sort 的搜索,照样报 not sortable——配置已经写进去了,但旧索引还没重建完。

验证一下:

curl http://localhost:7700/indexes/books_sort/settings/sortable-attributes \
  -H "Authorization: Bearer aSampleMasterKey"
["price", "rating", "release_date"]

返回三个属性就成。注意返回顺序是字母序(price/rating/release_date),跟你提交的顺序无关,别拿这个当报错。等不到 succeeded 就搜仍会报 not sortable,见Meilisearch 添加文档后立即搜索返回空结果。

第五步:sort 参数排序

前置声明到位,正式用 sort 参数。它写在搜索请求 body 里,格式是数组,每项 属性:方向(asc 升序 / desc 降序),数组顺序即优先级。先跑单字段:空查询(q 为空,官方叫 placeholder search——不搜关键词、拿全量列表)加排序,就是“榜单/全量列表”场景:

curl -X POST http://localhost:7700/indexes/books_sort/search \
  -H "Authorization: Bearer aSampleMasterKey" \
  -H "Content-Type: application/json" \
  -d '{"q":"","sort":["rating:desc"]}'
hits 顺序(rating 降序):
三体 9.3 → 活着 9.1 → 明朝那些事儿 9.0 → 万历十五年 8.9 → 球状闪电 8.8 → 许三观卖血记 8.7

再传第二个字段做 tiebreaker(打平时用的决胜规则)。前面规则分出胜负就不再往后看,同 rating 的两本才用 price:asc 决定先后:

curl -X POST http://localhost:7700/indexes/books_sort/search \
  -H "Authorization: Bearer aSampleMasterKey" \
  -H "Content-Type: application/json" \
  -d '{"q":"","sort":["rating:desc","price:asc"]}'
hits 顺序(rating 降序,同分按 price 升序):
三体 9.3/39.9 → 活着 9.1/35.0 → 明朝那些事儿 9.0/45.5 → 万历十五年 8.9/28.0 → 球状闪电 8.8/29.0 → 许三观卖血记 8.7/32.0

这两个数据集的 rating 没有同分,看不出 tiebreaker 在动。再补一条查询词 + 排序组合的验证,确认排序在文本查询下也生效:

curl -X POST http://localhost:7700/indexes/books_sort/search \
  -H "Authorization: Bearer aSampleMasterKey" \
  -H "Content-Type: application/json" \
  -d '{"q":"刘慈欣","sort":["rating:desc"]}'
{
  "hits": [
    {"id": 1, "title": "三体", "author": "刘慈欣", "rating": 9.3},
    {"id": 2, "title": "球状闪电", "author": "刘慈欣", "rating": 8.8}
  ],
  "estimatedTotalHits": 2
}

期望输出:三组结果顺序与上面一致——q=刘慈欣 命中两本,三体 9.3 排在球状闪电 8.8 前。要是带了 sort 返回 400 invalid_search_sort,多半是第四步没等完或属性名拼错,见Meilisearch sort 属性未声明报 invalid_search_sort。

第六步:自定义排名规则(release_date:desc)

sort 参数有个局限:每次请求都得传,只对本次生效。而“新品优先”“评分优先”这类是常驻业务规则——产品上希望不传任何参数、每次搜索都按它排。这一步把规则写进索引设置,用自定义排名规则(custom ranking rule)。

语法跟 sort 参数一样是 属性:方向,放进排名规则链(rankingRules)末尾。注意这里必须把默认 7 条全带上:PUT /settings/ranking-rules 是全量覆盖语义,只传新规则会把 words/exactness 全冲掉,相关度排序直接失效。

curl -X PUT http://localhost:7700/indexes/books_sort/settings/ranking-rules \
  -H "Authorization: Bearer aSampleMasterKey" \
  -H "Content-Type: application/json" \
  --data-binary '["words","typo","proximity","attributeRank","sort","wordPosition","exactness","release_date:desc"]'
{
  "taskUid": 10,
  "indexUid": "books_sort",
  "status": "enqueued",
  "type": "settingsUpdate"
}

设置更新同样要等 succeeded,并触发全量重建。

验证常驻生效——不传 sort 参数,直接搜:

curl -X POST http://localhost:7700/indexes/books_sort/search \
  -H "Authorization: Bearer aSampleMasterKey" \
  -H "Content-Type: application/json" \
  -d '{"q":"刘慈欣"}'
{
  "hits": [
    {"id": 1, "title": "三体", "author": "刘慈欣", "release_date": "2008-01-01"},
    {"id": 2, "title": "球状闪电", "author": "刘慈欣", "release_date": "2004-06-01"}
  ],
  "estimatedTotalHits": 2
}

release_date:desc 是链尾规则,作为 tiebreaker 生效:文本相关度先裁决,打平时新书靠前。三体(2008)排到球状闪电(2004)前。要是 PUT 返回 400,检查语法——旧版函数式写法 desc(release_date) 自 v0.22 起废弃,见Meilisearch 自定义排名规则旧语法报 invalid_settings_ranking_rules。

第七步(进阶):规则链位置的影响

自定义规则放链尾是“软排序”,放链首就变成“硬排序”——排名规则从第一条开始裁决,越靠前权重越大。先跑一次基线,记住当前配置(release_date:desc 在链尾)下 q=三 的顺序:

curl -X POST http://localhost:7700/indexes/books_sort/search \
  -H "Authorization: Bearer aSampleMasterKey" \
  -H "Content-Type: application/json" \
  -d '{"q":"三"}'
{
  "hits": [
    {"id": 1, "title": "三体", "rating": 9.3},
    {"id": 4, "title": "许三观卖血记", "rating": 8.7}
  ],
  "estimatedTotalHits": 2
}

三体(9.3)在前、许三观卖血记(8.7)在后。现在把 rating:asc 挪到链首,让评分成为第一裁决规则:

curl -X PUT http://localhost:7700/indexes/books_sort/settings/ranking-rules \
  -H "Authorization: Bearer aSampleMasterKey" \
  -H "Content-Type: application/json" \
  --data-binary '["rating:asc","words","typo","proximity","attributeRank","sort","wordPosition","exactness"]'
{
  "taskUid": 11,
  "indexUid": "books_sort",
  "status": "enqueued",
  "type": "settingsUpdate"
}

等任务 succeeded,再搜同一个 q=三:

curl -X POST http://localhost:7700/indexes/books_sort/search \
  -H "Authorization: Bearer aSampleMasterKey" \
  -H "Content-Type: application/json" \
  -d '{"q":"三"}'
{
  "hits": [
    {"id": 4, "title": "许三观卖血记", "rating": 8.7},
    {"id": 1, "title": "三体", "rating": 9.3}
  ],
  "estimatedTotalHits": 2
}

顺序整个翻转:评分 8.7 的许三观卖血记排到 9.3 的三体前。这就是位置即权重——规则越靠前越贴业务排序、越牺牲相关度。规则链从第一条开始裁决,rating:asc 放链首意味着先按评分升序硬排,words 那套相关度规则只在评分平局时才轮到。

验证完收尾,把排名规则恢复默认,别污染后面系列文章的实验环境:

curl -X DELETE http://localhost:7700/indexes/books_sort/settings/ranking-rules \
  -H "Authorization: Bearer aSampleMasterKey"
{
  "taskUid": 12,
  "indexUid": "books_sort",
  "status": "enqueued",
  "type": "settingsUpdate"
}

等 succeeded 后 GET /settings,rankingRules 应回到默认 7 条。到这里排序与自定义排名就全跑通了。

核心概念

下面这张图是整条调用链路的走向,从左到右就是一次请求的完整路径。

Meilisearch 排序与自定义排名调用链路架构图

概念 是什么 类比
rankingRules(排名规则链) 决定搜索结果先后顺序的规则列表,从头到尾一条条裁决,排前面的规则权重更大 像考试阅卷顺序:先看总分定名次,同分再看单科
sortableAttributes 声明“允许参与排序”的属性列表,不声明就无法用 sort 像 SQL 里 ORDER BY 的字段白名单
sort 参数 搜索请求里临时传的排序条件,如 ["rating:desc"],只对本次请求生效 像在结果页临时点一次“按价格排序”
自定义排名规则 写死在索引设置里的常驻排序规则(release_date:desc),不传 sort 也生效 像平台预置的“新品优先”排序,用户不用点
tiebreaker 前面规则打平时的“决胜规则”,多字段排序里第二个字段就是 tiebreaker 像比赛同分时看净胜球
异步任务(task) 设置/写入类操作后台执行,返回 taskUid,要轮询到 succeeded 才生效 像后台发货:下单先返回订单号,物流到了才算完
全量重建索引 设置变更后 Meilisearch 重算全部文档的内部索引,耗时与数据量成正比 像给大表加索引要重扫一遍数据
placeholder search 空查询(q 为空)的搜索,用来拿“全量列表 + 排序/分页” 像不搜关键词直接翻商品总列表

常见问题

invalid_search_sort:属性没声明就传 sort

现象:搜索请求带 "sort":["rating:desc"] 返回 400,错误码 invalid_search_sort,rating 明明存在于文档里。

原因:排序属性未在 sortableAttributes 里声明。filter 用 filterableAttributes、sort 用 sortableAttributes,两套独立白名单,默认都是空数组。

相关文章:Meilisearch sort 属性未声明报 invalid_search_sort

invalid_settings_ranking_rules:设排名规则返回 400

现象:PUT /settings/ranking-rules 传 ["words",...,"desc(release_date)"] 返回 400,错误码 invalid_settings_ranking_rules。

原因:用了旧版函数式语法 asc()/desc(),自 v0.22 起废弃。现行写法是 属性:asc / 属性:desc,如 "release_date:desc"。

相关文章:Meilisearch 自定义排名规则旧语法报 invalid_settings_ranking_rules

排序不生效:声明了 sortableAttributes 结果还是旧顺序

现象:PUT sortable-attributes 或 ranking-rules 后立即搜索,结果顺序没有任何变化,也不报错。

原因:设置更新是异步任务(settingsUpdate)且触发全量重建索引。任务还在 enqueued/processing 时搜索,用的仍是旧索引。轮询 GET /tasks/{taskUid} 到 status: succeeded 再搜。

相关文章:Meilisearch 添加文档后立即搜索返回空结果

下一步

分面搜索与过滤

给结果页加分面筛选器:声明 filterableAttributes、写 filter 组合与范围条件,拿 facetDistribution 与 facetStats,跟本篇的排序搭配就是完整的“筛选 + 排序”结果页。

Meilisearch filter 报 Attribute is not filterable

中文分词与本地化

本篇的 q=三 只演示了命中,中文书名/标题要搜得更准,得配中文分词与本地化设置。

Meilisearch 中文检索重建索引后未生效

filter 复杂组合

先过滤再排序的业务条件:filter 的 AND/OR 优先级、括号组合,跟 sort 叠加成“条件筛选 + 排序”的完整查询。

Meilisearch filter 组合 AND/OR 优先级坑

卡在哪一步了?

从零跑通的过程中撞到报错?把命令和输出记下来,帮助下一个刚起步的人。

贡献一条解法