MeiliSearch 1.55.0 快速入门与集成示例

MeiliSearch 1.55.0 快速入门:创建索引、添加文档、异步任务查询、搜索、过滤、排序、分页与 facets 聚合的 REST API 全流程实操

MeiliSearchMeiliSearchLinux搜索引擎2026/10/17

MeiliSearch 1.55.0 快速入门与集成示例

目标:通过 MeiliSearch 原生 REST API 完成从创建索引、批量添加文档、查询异步任务到搜索、过滤、排序、分页与 facets 聚合的完整入门流程,全部命令可直接复制执行,回显均为 1.55.0 实例实测输出。已在 Ubuntu 22.04 + MeiliSearch 1.55.0(production 模式)环境逐条实测通过。

一、环境信息

本文所有命令与回显均在以下环境中实测:

  • 操作系统:Ubuntu 22.04.5 LTS(x86_64)
  • MeiliSearch 版本:1.55.0(production 模式,监听 127.0.0.1:7700)
  • Master Key:q1w2e3r4t5y6u7i8o0p(演示值,生产环境必须更换)
  • 示例数据:官方 movies 数据集(31968 条电影记录,含 id/title/overview/genres/poster/release_date 字段),下载地址 https://raw.githubusercontent.com/meilisearch/datasets/main/datasets/movies/movies.json
  • HTTP 客户端:curl 7.81.0

为便于复制,先定义环境变量(下文命令均直接写出实际值,此变量仅为减少重复):

export MS=http://127.0.0.1:7700
export KEY='Authorization: Bearer q1w2e3r4t5y6u7i8o0p'

二、概览:API 模型与异步任务

MeiliSearch 只暴露一个 HTTP 端口(默认 7700),核心对象是「索引(index)—文档(document)—任务(task)」。除搜索外,写操作(建索引、写文档、改设置)全部是异步的:请求立即返回一个 taskUid,引擎在后台排队执行,需要轮询 GET /tasks/{taskUid} 确认结果——这是 MeiliSearch 与传统同步 API 最大的差异点,也是新手最容易踩的坑(写入后立即搜索查不到数据)。

💡 技术拓展:「写操作异步化」不止 MeiliSearch 一种形态,常见异步任务模型对比:

模型代表系统特点
任务队列 + 轮询MeiliSearch返回 taskUid,客户端轮询任务端点
写操作同步阻塞传统 RDBMS写完即生效,大写入阻塞连接
日志重放(追加提交)Elasticsearch(近实时)写入后 refresh 间隔后才可搜(默认 1s)

MeiliSearch 采用任务队列模型,单个任务通常亚秒级完成(本文实测 3 万文档索引仅 2.27 秒),配合轮询可获得既快又稳的写入体验。

鉴权方式:除 GET /health 外的所有路由都需要 Authorization: Bearer <master-key> 请求头。无凭据访问的实测:

curl -s http://127.0.0.1:7700/indexes -w '\nHTTP %{http_code}\n'

实测输出:

{"message":"The Authorization header is missing. It must use the bearer authorization method.","code":"missing_authorization_header","type":"auth","link":"https://docs.meilisearch.com/errors#missing_authorization_header"}
HTTP 401

三、前置条件

  1. MeiliSearch 1.55.0 已安装并运行(安装见《MeiliSearch 1.55.0 Ubuntu 22.04 安装指南》,Compose 部署见同专栏部署指南),健康检查通过:
curl -s http://127.0.0.1:7700/health

实测输出:

{"status":"available"}
  1. 下载示例数据集到服务器(约 16 MB):
curl -fLo /tmp/movies.json https://raw.githubusercontent.com/meilisearch/datasets/main/datasets/movies/movies.json
ls -lh /tmp/movies.json

实测输出:

-rw-r--r-- 1 root root 16M Oct 16 15:30 /tmp/movies.json

查看第一条记录的结构(数据集为 JSON 数组,每条记录含 6 个字段):

python3 -c "import json; d=json.load(open('/tmp/movies.json')); print('docs:', len(d)); print(json.dumps(d[0], ensure_ascii=False)[:220])"

实测输出:

docs: 31968
{"id": 2, "title": "Ariel", "overview": "Taisto Kasurinen is a Finnish coal miner whose father has just committed suicide and who is framed for a crime he did not commit. In jail, he starts to dream about leaving the country and starting a new life. He escapes from prison but things don't go as planned...", "genres": ["Drama", "Crime", "Comedy"], "poster": "https://image.tmdb.org/t/p/w500/ojDg0PGvs6R9xYFodRct2kdI6wC.jpg", "release_date": 593395200}

四、创建索引与添加文档

4.1 创建索引

创建名为 movies 的索引,主键为 id(每条文档的唯一标识字段):

curl -s -X POST http://127.0.0.1:7700/indexes -H 'Authorization: Bearer q1w2e3r4t5y6u7i8o0p' -H 'Content-Type: application/json' --data-binary '{"uid": "movies", "primaryKey": "id"}'

实测输出:

{"taskUid":0,"indexUid":"movies","status":"enqueued","type":"indexCreation","enqueuedAt":"2026-10-16T15:30:32.854908963Z"}

返回的 taskUid: 0 就是这次建索引任务的编号。轮询任务确认结果:

curl -s http://127.0.0.1:7700/tasks/0 -H 'Authorization: Bearer q1w2e3r4t5y6u7i8o0p'

实测输出:

{"uid":0,"batchUid":0,"indexUid":"movies","status":"succeeded","type":"indexCreation","canceledBy":null,"details":{"primaryKey":"id"},"error":null,"duration":"PT0.003879949S","enqueuedAt":"2026-10-16T15:30:32.854908963Z","startedAt":"2026-10-16T15:30:32.855870044Z","finishedAt":"2026-10-16T15:30:32.859749993Z"}

关键字段:status: succeeded 表示任务成功;error: null 无错误;duration 显示仅耗时 3.8 毫秒。若 status 为 failed,看 error 字段的具体信息。

4.2 批量添加文档

用 POST /indexes/{index}/documents 推送文档数组,primaryKey 查询参数可省略(建索引时已指定)。数据集是 JSON 数组,Content-Type 用 application/json:

curl -s -X POST 'http://127.0.0.1:7700/indexes/movies/documents?primaryKey=id' -H 'Authorization: Bearer q1w2e3r4t5y6u7i8o0p' -H 'Content-Type: application/json' --data-binary @/tmp/movies.json

实测输出:

{"taskUid":1,"indexUid":"movies","status":"enqueued","type":"documentAdditionOrUpdate","enqueuedAt":"2026-10-16T15:30:55.956582673Z"}

等待索引完成后查询任务 1:

curl -s http://127.0.0.1:7700/tasks/1 -H 'Authorization: Bearer q1w2e3r4t5y6u7i8o0p' | python3 -m json.tool

实测输出(节选):

{
    "uid": 1,
    "status": "succeeded",
    "type": "documentAdditionOrUpdate",
    "details": {
        "receivedDocuments": 31968,
        "indexedDocuments": 31944
    },
    "error": null,
    "duration": "PT2.270223129S"
}

31968 条文档 2.27 秒索引完成(receivedDocuments 与 indexedDocuments 的差值来自数据集中主键重复的记录,重复记录按主键去重)。

⚠️ 注意:若把 JSON 数组配上 Content-Type: application/x-ndjson 上传,会直接报错(实测):

{"message":"invalid type: sequence, expected a map at line 1 column 0","code":"internal","type":"internal","link":"https://docs.meilisearch.com/errors#internal"}

两种格式二选一:JSON 数组(application/json,--data-binary @文件.json)或 NDJSON(application/x-ndjson,每行一个独立 JSON 对象)。NDJSON 适合流式逐行追加,JSON 数组适合整批上传。

确认索引统计:

curl -s http://127.0.0.1:7700/indexes/movies/stats -H 'Authorization: Bearer q1w2e3r4t5y6u7i8o0p' | python3 -m json.tool | head -8

实测输出(节选):

{
    "numberOfDocuments": 31944,
    "isIndexing": false,
    "fieldDistribution": {
        "genres": 31944,
        "id": 31944,
        "overview": 31944,
        "poster": 31944,
        "release_date": 31944,
        "title": 31944
    }
}

4.3 更新与删除文档

更新是「按主键部分更新」:只提交要改的字段,其余字段保留。实测把 id=258480 的标题改掉:

curl -s -X PUT 'http://127.0.0.1:7700/indexes/movies/documents?primaryKey=id' -H 'Authorization: Bearer q1w2e3r4t5y6u7i8o0p' -H 'Content-Type: application/json' --data-binary '[{"id": 258480, "title": "Carol (Updated)"}]'

实测输出:

{"taskUid":2,"indexUid":"movies","status":"enqueued","type":"documentAdditionOrUpdate","enqueuedAt":"2026-10-16T15:48:32.260176266Z"}
sleep 3 && curl -s http://127.0.0.1:7700/indexes/movies/documents/258480 -H 'Authorization: Bearer q1w2e3r4t5y6u7i8o0p' | python3 -c "import json,sys; d=json.load(sys.stdin); print('title now:', d.get('title'), '| genres still:', d.get('genres'))"

实测输出:

title now: Carol (Updated) | genres still: ['Romance', 'Drama']

标题已更新而 genres 保留,证明部分更新语义。删除单条文档:

curl -s -X DELETE 'http://127.0.0.1:7700/indexes/movies/documents/258480' -H 'Authorization: Bearer q1w2e3r4t5y6u7i8o0p'

实测输出:

{"taskUid":3,"indexUid":"movies","status":"enqueued","type":"documentDeletion","enqueuedAt":"2026-10-16T15:48:35.280157362Z"}

删除后再次读取返回 404:

sleep 3 && curl -s http://127.0.0.1:7700/indexes/movies/documents/258480 -H 'Authorization: Bearer q1w2e3r4t5y6u7i8o0p' -w '\nHTTP %{http_code}\n'

实测输出:

{"message":"Document `258480` not found.","code":"document_not_found","type":"invalid_request","link":"https://docs.meilisearch.com/errors#document_not_found"}
HTTP 404

五、搜索

5.1 基础搜索

搜索入口是 POST /indexes/{index}/search,请求体 JSON 传查询参数。搜 carol:

curl -s -X POST 'http://127.0.0.1:7700/indexes/movies/search' -H 'Authorization: Bearer q1w2e3r4t5y6u7i8o0p' -H 'Content-Type: application/json' --data-binary '{"q": "carol", "limit": 3}' | python3 -c "import json,sys; d=json.load(sys.stdin); print('estimatedTotalHits:', d['estimatedTotalHits']); [print(h['id'], h['title'], h['genres']) for h in d['hits']]"

实测输出:

estimatedTotalHits: 408
258480 Carol ['Romance', 'Drama']
10306 Poltergeist III ['Horror', 'Thriller']
32646 Safe ['Drama']

命中 408 条(overview 正文里含 carol/Carol 一词的也计入),第一条就是标题为 Carol 的电影。

5.2 容错搜索(typo tolerance)

MeiliSearch 默认开启拼写容错:搜 batmn(少一个 a)也能命中 Batman 相关电影:

curl -s -X POST 'http://127.0.0.1:7700/indexes/movies/search' -H 'Authorization: Bearer q1w2e3r4t5y6u7i8o0p' -H 'Content-Type: application/json' --data-binary '{"q": "batmn", "limit": 3}' | python3 -c "import json,sys; d=json.load(sys.stdin); [print(h['id'], h['title']) for h in d['hits']]"

实测输出:

155 The Dark Knight
268 Batman
272 Batman Begins

容错规则默认值:1 个错字对长度 ≥5 的词生效、2 个错字对长度 ≥9 的词生效(可通过 typoTolerance 设置调整,见《MeiliSearch 1.55.0 进阶实战与运维》)。

5.3 搜索结果字段说明

搜索响应的常用字段(上两节的完整响应均可看到):

字段含义
hits命中的文档数组(默认 20 条)
estimatedTotalHits全量估算命中数(受分页上限影响,见 7.3)
query原样返回的查询词
processingTimeMs服务端处理耗时(毫秒)
limit / offset本次请求的分页参数回显

六、过滤与排序

6.1 声明可过滤/可排序字段

过滤与排序的字段必须先加入 filterableAttributes / sortableAttributes,否则请求会报错(filter 用在未声明字段上时报 Attribute genres is not filterable...)。声明 genres 与 release_date 可过滤、release_date 可排序:

curl -s -X PUT 'http://127.0.0.1:7700/indexes/movies/settings/filterable-attributes' -H 'Authorization: Bearer q1w2e3r4t5y6u7i8o0p' -H 'Content-Type: application/json' --data-binary '["genres", "release_date"]'

实测输出:

{"taskUid":2,"indexUid":"movies","status":"enqueued","type":"settingsUpdate","enqueuedAt":"2026-10-16T15:31:48.149292388Z"}
curl -s -X PUT 'http://127.0.0.1:7700/indexes/movies/settings/sortable-attributes' -H 'Authorization: Bearer q1w2e3r4t5y6u7i8o0p' -H 'Content-Type: application/json' --data-binary '["release_date"]'

实测输出:

{"taskUid":3,"indexUid":"movies","status":"enqueued","type":"settingsUpdate","enqueuedAt":"2026-10-16T15:31:51.156819241Z"

两个设置任务完成后(轮询 taskUid 确认 status: succeeded),再进行下面的过滤/排序请求。

6.2 过滤表达式

数据集里 release_date 是 Unix 秒时间戳。过滤「2022 年(含)之后的动作片」——2022-01-01 的时间戳为 1640995200:

curl -s -X POST 'http://127.0.0.1:7700/indexes/movies/search' -H 'Authorization: Bearer q1w2e3r4t5y6u7i8o0p' -H 'Content-Type: application/json' --data-binary '{"q": "", "filter": "genres = Action AND release_date >= 1640995200", "limit": 3}' | python3 -c "import json,sys; d=json.load(sys.stdin); print('estimatedTotalHits:', d['estimatedTotalHits']); [print(h['id'], h['title'], h['release_date']) for h in d['hits']]"

实测输出:

estimatedTotalHits: 8
76600 Avatar 2 1670976000
298618 The Flash 1667433600
335787 Uncharted 1644451200

⚠️ 注意:过滤表达式支持 AND / OR / NOT、= != > >= < <=、TO(区间)与 IN [...]。两点实测排障经验:

  1. IN 列表里带空格的值必须用双引号包裹,否则解析报错。实测 genres IN [Science Fiction, Horror](不加引号)返回:
{"message":"Expected only comma-separated field names inside `IN[..]` but instead found `Fiction, Horror]`. ...","code":"invalid_search_filter","type":"invalid_request","link":"https://docs.meilisearch.com/errors#invalid_search_filter"}

正确写法(JSON 转义后)"filter": "genres IN [\"Science Fiction\", Horror]",实测命中 6591 条(见 6.4)。

  1. 时间戳条件要用数据实际范围:本文实测用 release_date >= 1735689600(2025 年)过滤时返回 0 命中,因为数据集最新电影是 2022 年末的。

6.3 排序

sort 数组按字段名加排序方向(asc/desc)传入,字段必须已在 sortableAttributes 中声明。按上映时间倒序:

curl -s -X POST 'http://127.0.0.1:7700/indexes/movies/search' -H 'Authorization: Bearer q1w2e3r4t5y6u7i8o0p' -H 'Content-Type: application/json' --data-binary '{"q": "", "sort": ["release_date:desc"], "limit": 3}' | python3 -c "import json,sys; d=json.load(sys.stdin); [print(h['id'], h['title'], h['release_date']) for h in d['hits']]"

实测输出:

76600 Avatar 2 1670976000
298618 The Flash 1667433600
436270 Black Adam 1659052800

6.4 IN 过滤与 facets 分布

组合 IN 过滤与 facets 聚合,一次拿到命中数与各分类计数:

curl -s -X POST 'http://127.0.0.1:7700/indexes/movies/search' -H 'Authorization: Bearer q1w2e3r4t5y6u7i8o0p' -H 'Content-Type: application/json' --data-binary '{"q": "", "filter": "genres IN [\"Science Fiction\", Horror]", "facets": ["genres"], "limit": 2}' | python3 -c "import json,sys; d=json.load(sys.stdin); print('estimatedTotalHits:', d['estimatedTotalHits']); fd=d.get('facetDistribution',{}); print('facet genres top5:', dict(list(fd['genres'].items())[:5])); [print(h['id'], h['title'], h['genres']) for h in d['hits']]"

实测输出:

estimatedTotalHits: 6591
facet genres top5: {'Action': 1440, 'Adventure': 737, 'Animation': 379, 'Comedy': 1107, 'Crime': 237}
11 Star Wars ['Adventure', 'Action', 'Science Fiction']
17 The Dark ['Horror', 'Thriller', 'Mystery']

facetDistribution 返回命中集合中各 genre 的计数(按字母序默认排序),可直接用于前端侧边栏的分类筛选计数。

七、分页

7.1 limit 与 offset

默认每页 20 条,用 limit 与 offset 翻页。取第 3 页(每页 2 条):

curl -s -X POST 'http://127.0.0.1:7700/indexes/movies/search' -H 'Authorization: Bearer q1w2e3r4t5y6u7i8o0p' -H 'Content-Type: application/json' --data-binary '{"q": "", "limit": 2, "offset": 2}' | python3 -c "import json,sys; d=json.load(sys.stdin); print('estimatedTotalHits:', d['estimatedTotalHits']); [print(h['id'], h['title']) for h in d['hits']]"

实测输出:

estimatedTotalHits: 31944
6 Judgment Night
11 Star Wars

7.2 分页深度的默认上限(maxTotalHits)

分页能到达的深度受设置项 pagination.maxTotalHits 约束,默认 1000。实测行为(1.55.0):

  • 单次 limit 大于 1000 不报错:limit: 2000 正常返回 HTTP 200;
  • 但 offset + limit 超过 1000 的部分拿不到数据:offset: 990, limit: 100(覆盖区间 990~1089)只返回 10 条——即 990 到 999(1000 以内)的部分。

实测回显:

curl -s -X POST 'http://127.0.0.1:7700/indexes/movies/search' -H 'Authorization: Bearer q1w2e3r4t5y6u7i8o0p' -H 'Content-Type: application/json' --data-binary '{"q": "", "offset": 990, "limit": 100}' | python3 -c "import json,sys; d=json.load(sys.stdin); print('hits len:', len(d['hits']), '; estimatedTotalHits:', d['estimatedTotalHits'])"

实测输出:

hits len: 10 ; estimatedTotalHits: 31943

超过总量的 offset 不报错、返回空页(实测 offset: 31000, limit: 1000 返回 0 条,estimatedTotalHits 仍为 31943)。

💡 说明:maxTotalHits 是保护性上限(深翻页代价高),需要更深分页时调大该设置(PUT /indexes/movies/settings/pagination),或在应用层改用「深筛选 + 浅分页」的交互设计。

八、集成示例:curl 脚本封装

把上述调用封装成可直接运行的 bash 函数(真实服务器上保存为 ms-search.sh 即可复用):

#!/bin/bash
# ms-search.sh: MeiliSearch 命令行搜索工具
MS=http://127.0.0.1:7700
KEY='Authorization: Bearer q1w2e3r4t5y6u7i8o0p'
# 用法: ./ms-search.sh "查询词" [limit]
Q="$1"
L="$2"
[ -z "$Q" ] && Q=batman
[ -z "$L" ] && L=5
curl -s -X POST "$MS/indexes/movies/search" -H "$KEY" -H 'Content-Type: application/json' \
  --data-binary "{\"q\": \"$Q\", \"limit\": $L}" \
  | python3 -c "import json,sys; d=json.load(sys.stdin); [print(h['id'], h['title']) for h in d['hits']]"

实测执行(数据集已删除 id=258480,与 4.3 节一致):

chmod +x ms-search.sh && ./ms-search.sh "batman" 3

实测输出:

155 The Dark Knight
268 Batman
272 Batman Begins

其他语言集成仅需一个 HTTP 客户端:请求 POST {host}/indexes/{index}/search,头部带 Authorization: Bearer <master-key>,响应为 JSON。官方为 JavaScript / Python / PHP / Rust / Go / Swift / Kotlin / Dart / .NET / Java 等提供 SDK(仓库列表见 https://github.com/meilisearch 组织下各 meilisearch-* 仓库),SDK 的核心调用同样是「创建索引 → addDocuments → search」三步,与本文 curl 流程一一对应。

九、注意事项与常见问题

Q1:写入文档后立刻搜索查不到?

写操作是异步任务:POST documents 返回 taskUid 后索引仍在进行。轮询 GET /tasks/{taskUid} 到 status: succeeded 再搜索(本文 4.2 的实测等待约 3 秒、3 万文档 2.27 秒完成)。批量写入后建议统一轮询 GET /tasks?statuses=enqueued,processing 确认队列清空。

Q2:filter 报 Attribute genres is not filterable。

字段未声明进 filterableAttributes(或设置任务尚未完成)。先 PUT settings/filterable-attributes 声明字段,轮询任务成功后再发起带 filter 的搜索(见 6.1)。sortable 同理。

Q3:IN [...] 过滤报 invalid_search_filter。

带空格的值要用双引号包裹(见 6.2 实测报错与正确写法)。

Q4:任务 status 是 failed 怎么排查?

读任务对象里的 error 字段(message/code/type/link),常见原因:主键字段类型不一致(如既有数字又有字符串 id)、JSON 格式错误、索引不存在。错误码速查表见 https://docs.meilisearch.com/errors。

Q5:搜索响应里 estimatedTotalHits 与实际 hits 数不一致?

estimatedTotalHits 是估算全量命中数;本次返回的条数受 limit/offset 与 pagination.maxTotalHits(默认 1000,见 7.2)约束,两者本就不相等。

Q6:中文搜索效果如何?

MeiliSearch 内置多语言分词(含拉丁语系空格分词与 CJK 处理),本文数据集为英文;中文场景建议直接实测业务数据,并用《MeiliSearch 1.55.0 进阶实战与运维》中的同义词/停用词/可搜索字段设置调优相关性。

十、参考链接