MeiliSearch 1.55.0 快速入门与集成示例
MeiliSearch 1.55.0 快速入门:创建索引、添加文档、异步任务查询、搜索、过滤、排序、分页与 facets 聚合的 REST API 全流程实操
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
三、前置条件
- MeiliSearch 1.55.0 已安装并运行(安装见《MeiliSearch 1.55.0 Ubuntu 22.04 安装指南》,Compose 部署见同专栏部署指南),健康检查通过:
curl -s http://127.0.0.1:7700/health
实测输出:
{"status":"available"}
- 下载示例数据集到服务器(约 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 [...]。两点实测排障经验:
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)。
- 时间戳条件要用数据实际范围:本文实测用
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 进阶实战与运维》中的同义词/停用词/可搜索字段设置调优相关性。
十、参考链接
- 官方快速入门:https://www.meilisearch.com/docs/learn/getting_started/getting_started_with_meilisearch
- Search API 参考:https://www.meilisearch.com/docs/reference/api/search
- Documents API 参考:https://www.meilisearch.com/docs/reference/api/documents
- Tasks API 参考:https://www.meilisearch.com/docs/reference/api/tasks
- 官方示例数据集:https://github.com/meilisearch/datasets
- 相关文档:《MeiliSearch 1.55.0 Ubuntu 22.04 安装指南》《MeiliSearch 1.55.0 配置参数详解》《MeiliSearch 1.55.0 进阶实战与运维》