跟着这篇走完,你会在本机用一个带 IK 插件的 OpenSearch Docker 容器跑起中文搜索:看懂 ik_max_word 和 ik_smart 两种分析器怎么把同一句中文切成完全不同的结果,再用自定义词典把「三体」这类领域专名锁成整词。整个过程 6 步,每步都有一条命令能验证自己做对了没有。
文中固定用这些值:端口 9200、用户 admin、示例密码 Devfix@2026、索引 books_zh、OpenSearch 版本 2.19.0;你换了就把示例值替换掉。这篇默认你已经跑通了从零跑通 OpenSearch 本地搜索服务,Docker 启动、索引 CRUD、standard 分析器的行为全部复用第一篇,没跑通先回去把服务起了再回来。
有一点得先说清楚:IK 不是 OpenSearch 内置插件。它是 INFINI Labs 维护的第三方插件(GitHub 仓库),需要自己装。好消息是装法很简单,一个 Dockerfile 就搞定。如果你玩过从零跑通 Meilisearch 中文搜索,这篇的思路类似但解法不同:Meilisearch 用 Charabia + jieba 开箱可用,OpenSearch 用 IK 需要手动装但分词粒度更可控。

前置条件
| 项 | 要求 | 说明 |
|---|---|---|
| Docker | 任意近期版本 | 本文用 Docker 起服务,与 local-search 一致 |
| 内存 | Docker Desktop 至少 4 GB | OpenSearch 是 JVM 进程,比较吃内存 |
| 前置文章 | 已跑通 opensearch-local-search |
本文复用其 Docker 启动套路和密码设置;没跑通先回去补 |
| curl | 任意版本 | 全文用 curl 调 REST API |
| 磁盘 | 2 GB 以上空闲 | 自定义镜像 + 索引数据的余量 |
先确认上一篇的容器还在跑(如果已经停了,后面第一步会重新构建镜像启动):
docker ps
如果看到 opensearchproject/opensearch 的容器在运行,先停掉它——后面要用自定义镜像替换:
docker stop <容器ID>
第一步:构建带 IK 插件的 Docker 镜像
IK 插件没有现成的 Docker 镜像,需要自己构建。先创建一个工作目录,在里面写一个 Dockerfile:
mkdir -p opensearch-ik && cd opensearch-ik
cat > Dockerfile <<'EOF'
FROM opensearchproject/opensearch:2.19.0
RUN /usr/share/opensearch/bin/opensearch-plugin install --batch \
https://get.infini.cloud/opensearch/analysis-ik/2.19.0
EOF
这个 Dockerfile 做两件事:以官方 OpenSearch 2.19.0 镜像为基础,然后用 opensearch-plugin install 从 INFINI Labs 的 CDN 下载安装 IK 插件。--batch 跳过交互式确认。
关键决策:插件版本必须与 OpenSearch 版本完全一致。2.19.0 的 OpenSearch 只能装 2.19.0 的 IK 插件,差一个小版本都可能启动失败。INFINI Labs 的发布页列出了所有可用版本,找一个和你 OpenSearch 版本完全匹配的。
开始构建:
docker build -t opensearch-with-ik:2.19.0 .
构建过程会下载插件和依赖,需要一两分钟。看到 Successfully built 和 Successfully tagged opensearch-with-ik:2.19.0 就成了。
验证镜像存在:
docker images opensearch-with-ik
REPOSITORY TAG IMAGE ID CREATED SIZE
opensearch-with-ik 2.19.0 abc123def456 30 seconds ago 1.2GB
看到 opensearch-with-ik 镜像就在列表里。如果构建时报 plugin not found 或 404,多半是版本号写错了——IK 插件版本必须和 OpenSearch 版本完全一致,见常见问题第 1 项。
第二步:启动容器,确认 IK 插件已加载
用自定义镜像启动容器,启动参数和 local-search 一致:
docker run -d -p 9200:9200 -p 9600:9600 \
-e "discovery.type=single-node" \
-e "OPENSEARCH_INITIAL_ADMIN_PASSWORD=Devfix@2026" \
--name opensearch-ik \
opensearch-with-ik:2.19.0
容器起来后 JVM 启动要几十秒,等它就绪。验证服务通了:
curl -sk -u admin:Devfix@2026 "https://localhost:9200"
返回欢迎 JSON 就说明服务通了。现在确认 IK 插件已加载:
curl -sk -u admin:Devfix@2026 "https://localhost:9200/_cat/plugins?v"
name component version
a937e... analysis-ik 2.19.0
看到 analysis-ik 在组件列表里、版本是 2.19.0,插件就加载成功了。如果这行没出现,说明插件没装成功——回第一步检查 Dockerfile 和构建日志。如果容器根本起不来、反复重启,多半是插件版本和 OpenSearch 版本不匹配,见常见问题第 1 项。
第三步:创建中文索引 books_zh,指定 IK 分析器
这篇换一套独立数据集 books_zh,跟 local-search 的 movies 互不干扰。建索引时在 mappings 里给 title 字段指定 IK 分析器——analyzer 用 ik_max_word(索引时最细切词),search_analyzer 用 ik_smart(搜索时最粗切词):
curl -sk -u admin:Devfix@2026 -X PUT "https://localhost:9200/books_zh?pretty" \
-H "Content-Type: application/json" \
-d '{
"settings": {
"index.number_of_shards": 1,
"index.number_of_replicas": 0
},
"mappings": {
"properties": {
"title": { "type": "text", "analyzer": "ik_max_word", "search_analyzer": "ik_smart" },
"author": { "type": "keyword" },
"category": { "type": "keyword" },
"summary": { "type": "text", "analyzer": "ik_max_word", "search_analyzer": "ik_smart" }
}
}
}'
{
"acknowledged" : true,
"shards_acknowledged" : true,
"index" : "books_zh"
}
analyzer: ik_max_word 告诉 OpenSearch 索引文档时用 ik_max_word 把文本切到最细;search_analyzer: ik_smart 告诉它搜索查询时用 ik_smart 切到最粗。这种「索引细切、搜索粗切」是 IK 的经典搭配——索引时切得细,召回率高;搜索时切得粗,精确度高。
验证 mappings 生效:
curl -sk -u admin:Devfix@2026 "https://localhost:9200/books_zh/_mappings?pretty"
返回里应该能看到 title 字段的 analyzer 是 ik_max_word、search_analyzer 是 ik_smart。如果报 analyzer [ik_max_word] not found,说明 IK 插件没加载成功——回第二步确认 _cat/plugins 的输出,见常见问题第 2 项。
第四步:用 _analyze API 看 IK 怎么切中文
这是全文最重要的一步——不看这步,后面搜出来的结果你无法解释。
_analyze API 让你看到分析器实际怎么切词。先试 ik_max_word:
curl -sk -u admin:Devfix@2026 -X POST "https://localhost:9200/_analyze?pretty" \
-H "Content-Type: application/json" \
-d '{
"analyzer": "ik_max_word",
"text": "中华人民共和国国歌"
}'
{
"tokens" : [
{ "token" : "中华人民共和国" },
{ "token" : "中华人民" },
{ "token" : "中华" },
{ "token" : "华人" },
{ "token" : "人民共和国" },
{ "token" : "人民" },
{ "token" : "共和国" },
{ "token" : "共和" },
{ "token" : "国歌" }
]
}
ik_max_word 会把文本拆到最细——「中华人民共和国国歌」被切成 9 个词,从最长的「中华人民共和国」到「共和」「国歌」,尽可能穷举所有有意义的组合。
现在试 ik_smart,同一句文本:
curl -sk -u admin:Devfix@2026 -X POST "https://localhost:9200/_analyze?pretty" \
-H "Content-Type: application/json" \
-d '{
"analyzer": "ik_smart",
"text": "中华人民共和国国歌"
}'
{
"tokens" : [
{ "token" : "中华人民共和国" },
{ "token" : "国歌" }
]
}
ik_smart 只切出 2 个词——「中华人民共和国」和「国歌」,干净利落,不做多余拆分。
放在一起对比:
| 分析器 | 切词结果 | 粒度 | 适合场景 |
|---|---|---|---|
ik_max_word |
中华人民共和国, 中华人民, 中华, 华人, 人民共和国, 人民, 共和国, 共和, 国歌 | 最细,穷举组合 | 索引时:切得细,召回率高 |
ik_smart |
中华人民共和国, 国歌 | 最粗,智能断词 | 搜索时:切得粗,精确度高 |
这里有一个容易搞错的点:ik_smart 不是 ik_max_word 的子集。看另一个例子就明白了——搜「乒乓球拍」时 ik_smart 可能切成「乒乓球拍」一个词,而 ik_max_word 切成「乒乓球」「乒乓」「球拍」,两者的切法完全不同,不是简单的包含关系。
这就是第三步设置 analyzer: ik_max_word, search_analyzer: ik_smart 的理由:索引时细切,让「共和国」也能被搜到;搜索时粗切,让搜「中华人民共和国」时不会匹配到只含「中华」的文档。
验证命令就是上面两条 _analyze。期望输出:ik_max_word 返回 9 个 token,ik_smart 返回 2 个。如果报 analyzer [ik_max_word] not found,见常见问题第 2 项。
第五步:导入中文文档,搜索验证
灌 6 条虚构中文书籍到 books_zh。数据是刻意设计的——后面搜索结果全都能从分词行为解释:
cat > books_zh.ndjson <<'EOF'
{ "index": { "_index": "books_zh", "_id": "1" } }
{ "title": "三体", "author": "刘慈欣", "category": "科幻", "summary": "文化大革命如火如荼时,红岸基地向宇宙发出了第一声啼鸣" }
{ "index": { "_index": "books_zh", "_id": "2" } }
{ "title": "平凡的世界", "author": "路遥", "category": "文学", "summary": "中国农村的变革与年轻人的奋斗" }
{ "index": { "_index": "books_zh", "_id": "3" } }
{ "title": "C++ 编程入门", "author": "张伟", "category": "编程", "summary": "从零开始学习 C++ 编程语言" }
{ "index": { "_index": "books_zh", "_id": "4" } }
{ "title": "C# 高级编程", "author": "李娜", "category": "编程", "summary": "深入理解 C# 和 .NET 框架" }
{ "index": { "_index": "books_zh", "_id": "5" } }
{ "title": "冰激凌与夏天", "author": "林小满", "category": "美食", "summary": "夏日甜品的制作与故事" }
{ "index": { "_index": "books_zh", "_id": "6" } }
{ "title": "未来科技简史", "author": "王强", "category": "科普", "summary": "从人工智能到量子计算的发展历程" }
EOF
bulk 导入:
curl -sk -u admin:Devfix@2026 -X POST "https://localhost:9200/_bulk?pretty" \
-H "Content-Type: application/x-ndjson" \
--data-binary @books_zh.ndjson
响应里 "errors" : false 就说明全部导入成功。用 count 验证:
curl -sk -u admin:Devfix@2026 "https://localhost:9200/books_zh/_count?pretty"
{
"count" : 6,
"_shards" : { "total" : 1, "successful" : 1, "skipped" : 0, "failed" : 0 }
}
count 是 6 就对了。现在搜「三体」:
curl -sk -u admin:Devfix@2026 -X POST "https://localhost:9200/books_zh/_search?pretty" \
-H "Content-Type: application/json" \
-d '{ "query": { "match": { "title": "三体" } } }'
{
"hits" : {
"total" : { "value" : 1, "relation" : "eq" },
"hits" : [
{ "_id" : "1", "_score" : 0.8630, "_source" : { "title" : "三体", "author" : "刘慈欣" } }
]
}
}
命中了。但这里有个问题值得深挖——用 _analyze 看看「三体」在索引时被切成了什么:
curl -sk -u admin:Devfix@2026 -X POST "https://localhost:9200/_analyze?pretty" \
-H "Content-Type: application/json" \
-d '{ "analyzer": "ik_max_word", "text": "三体" }'
{
"tokens" : [
{ "token" : "三体" },
{ "token" : "三" },
{ "token" : "体" }
]
}
ik_max_word 把「三体」切成了 3 个 token:「三体」「三」「体」。这是因为 IK 的默认词典里恰好有「三体」这个词(毕竟太出名了),所以能识别成整词;但同时它也切出了单字「三」和「体」。
换一个 IK 词典里没有的词试试——比如你公司名「未来科技」:
curl -sk -u admin:Devfix@2026 -X POST "https://localhost:9200/_analyze?pretty" \
-H "Content-Type: application/json" \
-d '{ "analyzer": "ik_max_word", "text": "未来科技" }'
{
"tokens" : [
{ "token" : "未来" },
{ "token" : "科技" }
]
}
「未来科技」被切成了「未来」和「科技」两个词——IK 词典里没有这个复合词。这意味着搜「未来科技」时,查询串也会被切成「未来」和「科技」,然后分别匹配——虽然能搜到,但「未来科技」作为一个整体的排名权重不如被识别成整词时高。
怎么让 IK 认识「未来科技」这个专名?用自定义词典。
第六步:加自定义词典,把专名锁成整词
IK 支持自定义词典——把你领域里的专名、新词加进去,IK 就会把它们当整词识别。做法是:在本地创建一个词典文件,挂载到容器里,然后在 IK 配置文件里引用它。
先创建词典文件和配置目录:
mkdir -p ik-config
cat > ik-config/mydict.dic <<'EOF'
未来科技
平凡的世界
冰激凌
EOF
每行一个词,文件必须 UTF-8 编码。然后创建 IK 的配置文件 IKAnalyzer.cfg.xml:
cat > ik-config/IKAnalyzer.cfg.xml <<'EOF'
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
<properties>
<entry key="ext_dict">mydict.dic</entry>
</properties>
EOF
ext_dict 指向自定义词典文件名。多个词典用分号隔开(dict1.dic;dict2.dic)。
现在把配置文件挂载到容器里。先停掉当前容器,用新的启动命令重新跑:
docker stop opensearch-ik && docker rm opensearch-ik
docker run -d -p 9200:9200 -p 9600:9600 \
-e "discovery.type=single-node" \
-e "OPENSEARCH_INITIAL_ADMIN_PASSWORD=Devfix@2026" \
--name opensearch-ik \
-v "$(pwd)/ik-config/IKAnalyzer.cfg.xml:/usr/share/opensearch/plugins/analysis-ik/config/IKAnalyzer.cfg.xml:ro" \
-v "$(pwd)/ik-config/mydict.dic:/usr/share/opensearch/plugins/analysis-ik/config/mydict.dic:ro" \
opensearch-with-ik:2.19.0
-v 把本地文件挂载到容器内 IK 插件的 config 目录。容器启动后 IK 会自动加载这个配置。
等服务起来(等几十秒),验证自定义词典生效:
curl -sk -u admin:Devfix@2026 -X POST "https://localhost:9200/_analyze?pretty" \
-H "Content-Type: application/json" \
-d '{ "analyzer": "ik_max_word", "text": "未来科技" }'
{
"tokens" : [
{ "token" : "未来科技" }
]
}
之前「未来科技」被切成「未来」「科技」两个词,现在是一个整词了。自定义词典生效。
但注意:自定义词典只影响新索引的文档。已有的 books_zh 索引是在加词典之前建的,分词结果不会自动更新。要让新词典对已有数据生效,需要重建索引(reindex):
curl -sk -u admin:Devfix@2026 -X POST "https://localhost:9200/books_zh/_forcemerge?max_num_segments=1&pretty"
或者更彻底的方式——删掉重建:
curl -sk -u admin:Devfix@2026 -X DELETE "https://localhost:9200/books_zh?pretty"
然后重新执行第三步建索引、第五步导入数据。重新导入后,「未来科技」在索引里就是整词了。
验证命令:_analyze 返回「未来科技」一个 token(而不是「未来」「科技」两个)。如果自定义词典不生效,多半是文件编码不是 UTF-8、或者挂载路径不对,见常见问题第 3 项。
核心概念
这张全景图走完就清楚了:写入路径是 curl → IK 分析器(ik_max_word 切词)→ books_zh 索引 → 倒排索引;查询路径是查询串先过 IK 分析器(ik_smart 切词),再查倒排索引定位文档。

| 概念 | 是什么 | 类比 |
|---|---|---|
| IK 分析器 | INFINI Labs 维护的中文分词插件,集成 Lucene IK 到 OpenSearch | 像给 OpenSearch 装了一把中文切词刀 |
ik_max_word |
IK 的最细粒度分析器,穷举所有可能的词组合 | 像把一句话拆到不能再拆,确保每个词都被索引到 |
ik_smart |
IK 的最粗粒度分析器,智能断词 | 像读一句话时只切出最有意义的几个词 |
analyzer vs search_analyzer |
索引时用 analyzer 切词,搜索时用 search_analyzer 切词 |
索引时细切(多存词),搜索时粗切(少匹配) |
| 自定义词典 | 一个 UTF-8 文本文件,每行一个词,IK 启动时加载 | 像给词典补录你领域里的专名 |
| 热更新 | IK 支持通过 HTTP URL 远程加载词典,定时轮询更新 | 像在线词典,改了服务器上的文件就自动生效 |
常见问题
IK 插件版本与 OpenSearch 不匹配,容器启动失败
现象:docker run 后容器反复重启,docker logs 里报 Plugin [analysis-ik] was built for 版本不匹配。
原因:IK 插件版本必须与 OpenSearch 版本完全一致(major.minor.patch)。2.19.0 的 OpenSearch 只能装 2.19.0 的 IK 插件。INFINI Labs 的发布页列出了所有可用版本。
建索引报 analyzer [ik_max_word] not found
现象:PUT /books_zh 时返回 400,错误信息含 analyzer [ik_max_word] not found。
原因:IK 插件没有加载成功。先用 GET /_cat/plugins 确认 analysis-ik 是否在列。如果不在,说明插件没装成功或版本不匹配导致加载失败——回第一步和第二步检查。
自定义词典不生效
现象:加了自定义词典文件,重启容器后 _analyze 返回的分词结果没变化。
原因:三个检查点——① 词典文件必须是 UTF-8 编码(Windows 记事本默认保存的可能是 GBK);② IKAnalyzer.cfg.xml 里的文件名要和实际文件名完全一致(大小写敏感);③ 挂载路径必须是 /usr/share/opensearch/plugins/analysis-ik/config/ 目录。
