搜索建模、Index 与 Mapping:业务模型怎样变成可检索结构
商品名称需要按关键词检索,价格需要排序,颜色和尺码需要成对筛选,租户标识则参与每一次权限过滤。这些要求会把同一个商品拆成几种不同的索引结构。Mapping 负责声明字段类型及处理方式;文档粒度决定哪些数据一起返回、一起更新。
{
"product_id": "p-17",
"tenant_id": "shop-a",
"title": "Blue Cotton Shirt",
"price_cents": 12900,
"variants": [
{"color": "blue", "size": "M"},
{"color": "red", "size": "L"}
]
}这份文档包含蓝色 M 码和红色 L 码两个规格。查询“蓝色 L 码”时,应当没有结果。字段是否保留数组元素之间的配对关系,会直接改变这个答案。
文档和字段怎样承接查询
文档粒度先由查询和更新决定
一份搜索文档通常围绕“查询一次需要拿到的对象”组织。商品列表可以一份文档代表一个商品,将规格内嵌;库存查询若主要按 SKU 更新和筛选,每个 SKU 一份文档更容易控制更新量。文章检索一般把标题、摘要、正文和作者展示信息放在一起,避免每个命中都跨服务拼接。订单分析如果需要逐行商品统计,则要区分“订单文档”和“订单明细文档”的计数口径。
| 组织方式 | 查询与更新特点 | 需要承担的代价 |
|---|---|---|
| 一商品一文档,内嵌规格 | 一次命中能返回商品及其规格;适合商品列表 | 改一个规格也会更新整个父文档及其 nested 文档块 |
| 一 SKU 一文档 | 精确库存与规格过滤直接,单次更新较小 | 商品列表需要分组、折叠或额外投影,商品数与 SKU 数不同 |
| 去规范化展示字段 | 查询时少做远程关联 | 品牌名、组织名等公共字段变化后,需要更新受影响文档 |
| 父子关联或拆索引 | 可让关联对象独立变化 | 查询和路由更复杂;关联能力及限制需要单独评估 |
数据库的一对多关系可以转换成内嵌数组,也可以拆成多份文档,选择取决于筛选方式、返回形态和更新频率。把每天变化一次的品牌名称重复存进商品文档,通常比查询时逐条补数据简单;把每秒变化的库存连同大型商品介绍一起重写,写放大就可能过高。
如果数据库、对象存储或完整事件归档保留了所有必需字段,索引可以由这些数据重新生成。日志只保留在搜索集群、原始文件已经删除时,重建能力就取决于搜索快照等额外副本。是否能够重建,要检查实际保存的数据与保留期。
Index、Shard 与 Segment 各在什么位置
OpenSearch Cluster
├── Node:运行 OpenSearch 的服务进程
└── Index:一组采用同一套 Mapping 的文档
├── Primary Shard 0:负责一部分文档的写入
│ ├── Lucene index
│ │ ├── Segment A
│ │ └── Segment B
│ └── Translog:用于恢复尚未进入 Lucene 提交点的操作
├── Replica of Shard 0:该分片的副本
└── Primary Shard 1:负责另一部分文档Index 是 API 中使用的索引名称,例如 products-v1。它的文档分散在主分片中,每个分片内部使用 Lucene 管理检索结构。Segment 是分片里的数据组织单元,后续写入生成新数据,后台合并将小 Segment 整理成更大的 Segment。
索引设置管理主分片数、副本数、刷新间隔和分析器定义;Mapping 管理字段类型、字段参数和对象结构。主分片数量影响文档分布和查询并行度,创建后不能通过普通 settings 更新任意改成另一个值。扩展分片需要使用满足条件的 split、shrink,或重新建立索引。副本数可以调整,但副本还需要可用节点和磁盘。索引设置
查询涉及多个分片时,协调节点需要收集并合并结果。分片更多会增加并行机会,也会增加请求分发、内存结构、文件与恢复任务的数量。单节点小实验使用一个主分片、零副本,方便观察字段行为;生产分片布局需要结合实际数据量、查询并发和节点故障后的恢复时间。
字段类型应与访问方式对应
| 查询需要 | 常见字段设计 | 关键说明 |
|---|---|---|
| 标题、摘要、正文检索 | text | 文本分析后按词项匹配,可计算相关度 |
| ID、状态、分类、租户过滤 | keyword | 适合精确值、排序、分组;大小写是否归一由 normalizer 等配置决定 |
| 数量、价格、范围筛选 | integer、long、double、scaled_float | 类型范围、精度和单位应固定;金额可用最小货币单位的整数 |
| 时间范围与时间排序 | date | 明确输入格式、时区与精度;不要混用秒和毫秒 |
| 开关状态 | boolean | 使用真正的布尔字段,减少字符串状态歧义 |
| IP 或位置查询 | ip、geo_point 等 | 使用相应类型和查询,而非每次解析普通字符串 |
| 同一数组元素内的条件组合 | nested | 保留对象关联,增加内部文档与更新成本 |
| 不固定、主要用于取回的附加属性 | flat_object 或禁用解析的 object | 需要根据真实检索能力选择,不能代替所有显式字段 |
向量、自动补全、父子关联等还有专门类型。全部字段类型和使用入口可查官方字段类型目录。选型时先判断查询操作,再决定是否需要增加一种索引结构。
同一原始值可以有多个索引表示。例如 title 用于全文检索,title.raw 用于按完整标题排序或分组:
"title": {
"type": "text",
"analyzer": "standard",
"fields": {
"raw": {"type": "keyword"}
}
}应用仍然只写入一个 title。raw 是 Mapping 派生的多字段,不要求应用额外提交 title.raw,也不会自动出现在 _source 中。给既有字段增加多字段后,旧文档还需要重新处理才能建立新增表示。Mapping 参数、修改 Mapping
缺失值、动态字段与输入类型
Mapping 控制索引处理方式,不负责完整业务校验。dynamic:strict 会拒绝未声明字段,却不会要求每份商品都必须有价格、租户和名称。必填项、价格下限、租户归属仍应在应用中验证。
null、缺失字段和空数组通常不产生可供 exists 命中的索引值;空字符串对 keyword 则是一个值。配置 null_value、ignore_above、ignore_malformed 后,_source 中可见的内容与实际索引值还可能不同。排查“字段在 JSON 里却筛不出来”时,要同时看 Mapping 与查询结果。Mapping 参数
动态字段有三种常见策略:
true:根据新输入增加字段 Mapping,适合允许扩展且有字段数量约束的数据。false:未知字段留在_source,不为它建立动态字段 Mapping。strict:遇到未知字段拒绝该文档写入,适合结构已明确的业务投影。
动态模板可以按字段名或推断类型分配 Mapping,适合有规律的扩展字段。模板仍可能生成大量不同字段,不能独自限制任意动态键。dynamic 参数
数值字段默认的类型转换有时会掩盖生产者错误。示例对 price_cents 设置 coerce:false,让 JSON 字符串 "12900" 与整数 12900 得到不同处理。这样,上游字段类型发生变化时会尽早暴露,而不是依赖各消费者各自转换。
写入如何形成可检索结构
分析器把文本转换为词项
分析器由可选的字符过滤器、一个分词器和若干词项过滤器组成:
原始文本
→ char filters:处理字符,例如去 HTML 标签
→ tokenizer:切分并产生 token、位置等信息
→ token filters:小写化、停用词或词干等处理
→ 用于索引与匹配的词项standard 分析器会把 Blue Cotton Shirt 转成 blue、cotton、shirt。它没有因此自动获得“衬衫”“上衣”“shirt”之间的跨语言语义。中文分词、同义词、拼写容错和相关度排序需要各自的词料、配置与评估,不能只凭一条英文命中就决定商品搜索质量。文本分析
索引时使用的词项与查询时产生的词项需要能够匹配。match 会分析查询文本,term 则查找给定词项。对 text 字段发完整短语的 term 查询,常常找不到已经被拆成多个词的标题。需要短语位置关系时使用 match_phrase,需要完整字符串相等时使用 keyword 多字段。
部署额外分析插件前,应确认其适配的 OpenSearch 版本和所有数据节点上的安装情况。已经写入的词项不会因为替换插件文件自动重建。词典、分析器或字段设计变化后,要验证新旧查询行为,并安排数据重新索引。
倒排索引、doc values 与 _source
倒排索引从词项找到包含它的文档。短语查询还需要位置等信息;评分会使用词频、文档长度等统计。对于价格和日期等类型,范围查询还会利用对应的数值索引结构,不能把每一种字段查询都理解成字符串查词表。
title 的倒排关系(简化) price_cents 的列式访问(简化)
blue → p-17, p-23 p-17 → 12900
cotton → p-17 p-23 → 9900
shirt → p-17, p-41 p-41 → 15900排序和聚合需要从文档读取字段值。doc_values 为这种访问建立磁盘上的列式结构,是 keyword、数值、日期等字段常用的表示。text 通常不用它;直接在 text 上开启 fielddata,会将分析后的词项加载到内存,还可能把“完整标题分组”变成“标题单词分组”。一般优先给字段增加 keyword 表示。doc values、fielddata
_source 保存文档的原始 JSON 表示,供结果取回、更新和 reindex 等操作使用。它与倒排、doc values 分别承担不同任务。只是不希望响应返回大型字段时,可以使用 _source filtering;关闭 _source 会影响后续能力,需要先检查更新和重建链路。Reindex 对 _source 的要求
写入确认、实时 GET 与 refresh
默认文档复制路径中,请求按 _id 或指定的 routing 定位主分片,经过字段解析与文本分析,再进入分片的 Lucene 写入与 translog 处理,并复制到副本。返回成功时,还要结合实际复制状态、translog 配置和响应中的分片结果解释其含义。
index.translog.durability=request 是默认配置,确认请求前会按该策略同步 translog;async 则允许后台周期同步,节点故障可能丢失尚未同步的已确认操作。底层磁盘、副本与备份是否可恢复仍是独立条件。把 translog 当快照备份使用,会漏掉磁盘损坏或索引误删后的恢复需求。索引设置
| 操作或对象 | 作用 |
|---|---|
| Translog | 保存操作,帮助恢复尚未进入 Lucene 提交点的写入 |
| Refresh | 更新可搜索视图,让新写入能被 Search 看见 |
| Flush | 建立 Lucene 提交点并处理 translog 代次,减少恢复时需重放的操作 |
| Merge | 整理 Segment、回收不再需要的数据,消耗 CPU 与磁盘 I/O |
| 实时 GET | 按索引与 ID 读取当前文档,默认不需要等待搜索 refresh |
索引更新频繁时,缩短 refresh 间隔会提高新鲜度,也会增加小 Segment 和后续合并负担。批量导入可暂时延长或关闭自动刷新,完成后显式刷新;线上接口若只要求某次写入之后可搜索,可以按 API 支持使用 refresh=wait_for。自动刷新被关闭时,wait_for 也需要有其他刷新动作才能继续。Refresh API、Index Document API
数组对象为何需要 nested
普通 object 数组会将同名字段的值放在一起。开头商品中的规格可能被理解为两组独立值:
variants.color = [blue, red]
variants.size = [M, L]此时“颜色含 blue”与“尺码含 L”都成立,但它们来自不同规格。nested 为对象元素保留独立的内部文档关系,查询时在指定 path 内同时检查两个条件,再返回匹配的父商品。nested 字段
普通对象适合无需保持元素配对的结构;nested 适合“同一规格、同一联系人、同一订单行”的关联条件。nested 元素增加内部 Lucene 文档数量,更新父商品也会影响这组文档。一个商品挂几万条持续变化的明细时,应该重新考虑文档粒度。
对于字段名称任意变化的附加属性,OpenSearch 的 flat_object 可以减少动态字段扩张。但它不提供所有子字段的数值计算、排序和聚合能力,高频过滤字段应提升成明确类型。Elasticsearch 的 flattened 与这里的 flat_object 还需要逐项核对能力,不能只替换类型名。flat_object
在单节点中验证 Mapping
启动隔离的 OpenSearch
环境使用 Linux Bash、Docker Compose、curl、jq、unzip,以及有 Docker 使用权限的普通用户。Windows 开发机可以在 WSL 中使用同样的 Bash 命令。OpenSearch 服务固定为 3.8.0,实验容器限 2 GiB、JVM 堆 512 MiB;这组小数据只验证行为,不用于推导生产容量。官方镜像与安装说明、发行版本
下载 OpenSearch 实验工程 到当前目录,再解压到新建实验目录:
set -euo pipefail
LAB=$(mktemp -d -t search22.XXXXXX)
unzip opensearch-lab.zip -d "$LAB"
cd "$LAB/opensearch-lab"
docker version
docker compose version
curl --version
jq --version
sysctl vm.max_map_countLinux 的 vm.max_map_count 至少需要满足官方要求的 262144。若值过低,应由宿主机管理员按安装说明调整;容器内普通用户无法修改这个宿主内核参数。Docker Desktop 要检查其 Linux 虚拟机的设置,而非只看 Windows 宿主的内存。
docker compose -p search22 up -d --wait --wait-timeout 180
export SEARCH_URL=http://127.0.0.1:19222
curl -q --noproxy '*' --fail-with-body --silent --show-error \
"$SEARCH_URL/" | jq '.version | {distribution, number, lucene_version}'
docker compose -p search22 exec -T opensearch idnumber 应为 3.8.0。Compose 默认从官方 ECR 获取镜像,也可在启动前设置 OPENSEARCH_IMAGE=opensearchproject/opensearch:3.8.0 使用官方 Docker Hub 仓库。服务使用镜像自带的 UID 1000 用户运行,构建测试的身份在后面另行指定。端口只绑定 127.0.0.1:19222,Security plugin 在这个专用实验中被关闭,所以此入口没有认证和 TLS;不要改成公网绑定,也不要写入真实业务数据。共享实例需要使用受信任证书、应用账号和索引权限,可查 OpenSearch 安全部署。
启动失败时先执行 docker compose -p search22 logs --tail 80 opensearch,检查镜像下载、端口占用、内存、数据目录权限和内核参数。网络不能访问 Docker Hub 时,从已批准私有仓库获取相同发行物,或按 Docker 的 save 与 load 流程离线导入,核对来源与摘要后再运行,不能通过换成未知镜像跳过校验。
创建索引并观察词项
工程中的 src/test/resources/mapping/product-index.json 已包含完整 Mapping:一个主分片、零副本、关闭自动刷新、严格字段检查,以及开头商品使用的全部字段。其中规格配置为:
"variants": {
"type": "nested",
"properties": {
"color": {"type": "keyword"},
"size": {"type": "keyword"}
}
}直接发送完整文件,不需要手工拼接各段 JSON:
INDEX=lab22-products
curl -q --noproxy '*' --fail-with-body --silent --show-error \
-X PUT "$SEARCH_URL/$INDEX" -H 'Content-Type: application/json' \
--data-binary @src/test/resources/mapping/product-index.json
curl -q --noproxy '*' --fail-with-body --silent --show-error \
"$SEARCH_URL/$INDEX/_analyze" -H 'Content-Type: application/json' \
-d '{"field":"title","text":"Blue Cotton Shirt"}' \
| jq '[.tokens[].token]'第一条返回 acknowledged:true;第二条得到以下词项:
["blue", "cotton", "shirt"]如果建索引返回 resource_already_exists_exception,说明相同名字已存在,应选择新的专用索引名;不要直接删除不知道归属的索引。Analyze API
比较 GET 与 Search 的可见性
先在空索引上发起查询,再写入商品。刷新间隔被明确设置成 -1,用于控制实验时序。
curl -q --noproxy '*' --fail-with-body --silent --show-error \
"$SEARCH_URL/$INDEX/_search" -H 'Content-Type: application/json' -d '{}' \
| jq '.hits.total'
curl -q --noproxy '*' --fail-with-body --silent --show-error \
-X PUT "$SEARCH_URL/$INDEX/_doc/p-17" -H 'Content-Type: application/json' \
--data-binary @src/test/resources/mapping/product.json
curl -q --noproxy '*' --fail-with-body --silent --show-error \
"$SEARCH_URL/$INDEX/_doc/p-17" | jq '{found, source: ._source.product_id}'
curl -q --noproxy '*' --fail-with-body --silent --show-error \
"$SEARCH_URL/$INDEX/_search" -H 'Content-Type: application/json' -d '{}' \
| jq '.hits.total'按上述顺序运行,实时 GET 的 found 为 true,而 Search 仍是 value:0。随后执行刷新:
curl -q --noproxy '*' --fail-with-body --silent --show-error \
-X POST "$SEARCH_URL/$INDEX/_refresh"
curl -q --noproxy '*' --fail-with-body --silent --show-error \
"$SEARCH_URL/$INDEX/_search" -H 'Content-Type: application/json' \
-d '{"query":{"match":{"title":"BLUE"}}}' \
| jq '{total: .hits.total, ids: [.hits.hits[]._id]}'这次命中 p-17。将 query 换成 {"term":{"title":"Blue Cotton Shirt"}} 得到零条;换成 {"term":{"title.raw":"Blue Cotton Shirt"}} 则命中一条。输入大小写、分析器与字段类型共同决定结果,排查时可以先用 _analyze 把词项展开。
验证规格配对
curl -q --noproxy '*' --fail-with-body --silent --show-error \
"$SEARCH_URL/$INDEX/_search" -H 'Content-Type: application/json' -d '{
"query": {
"nested": {
"path": "variants",
"query": {
"bool": {
"filter": [
{"term": {"variants.color": "blue"}},
{"term": {"variants.size": "L"}}
]
}
}
}
}
}' | jq '.hits.total'蓝色 L 码得到零条;将 L 改成 M 得到一条。工程的 nestedPreservesElementPairs 还会建立普通 object 的对照索引:同一份数据、同一组“blue 与 L”的条件会错误命中。
捕获明确的失败,再修正输入
向严格 Mapping 提交一个未声明字段。这个请求预期失败,分别判断传输结果与 HTTP 状态:
if STATUS=$(curl -q --noproxy '*' --silent --show-error \
-o unknown-field.json -w '%{http_code}' \
-X PUT "$SEARCH_URL/$INDEX/_doc/bad" -H 'Content-Type: application/json' \
-d '{"unexpected":1}'); then
test "$STATUS" = 400 || exit 1
jq -e '.error.type == "strict_dynamic_mapping_exception"' unknown-field.json || exit 1
else
echo '连接失败:尚未得到预期的 HTTP 错误' >&2
exit 1
fi确认这个新字段确实需要查询后,可以先声明 Mapping,再写入正确文档:
curl -q --noproxy '*' --fail-with-body --silent --show-error \
-X PUT "$SEARCH_URL/$INDEX/_mapping" -H 'Content-Type: application/json' \
-d '{"properties":{"category":{"type":"keyword"}}}'
curl -q --noproxy '*' --fail-with-body --silent --show-error \
-X PUT "$SEARCH_URL/$INDEX/_doc/fixed" -H 'Content-Type: application/json' \
-d '{"category":"shirts","price_cents":12900}'如果字段只是拼写错误,应修正生产者,不必给错误名称新增 Mapping。price_cents 提交为字符串也会触发解析失败;改成整数后正常写入。业务字段增加和坏输入修复是两种不同的处理。
执行全部字段实验
测试只删除本轮成功创建且具有随机标识的专用索引,不清理上面手工创建的 lab22-products。以当前普通用户身份创建缓存,再运行 Maven:
mkdir -p .m2-cache
docker run --rm --memory 2g --entrypoint mvn \
--user "$(id -u):$(id -g)" \
--network search22_default \
-e MAVEN_CONFIG=/tmp/maven -e SEARCH_URL=http://opensearch:9200 \
-v "$PWD:/work" -v "$PWD/.m2-cache:/m2" -w /work \
maven:3.9.12-eclipse-temurin-17 \
-B -Duser.home=/tmp -Dmaven.repo.local=/m2 -Dtest=MappingTest clean verify正常结果为 7 项测试通过。七个方法分别检查:分词与多字段、GET/refresh、nested 配对、严格 Mapping 和类型修改、数值转换、字段数量限制、外部版本语义。将镜像改成 maven:3.9.12-eclipse-temurin-25 可以在 Java 25 执行相同测试,编译目标仍为 Java 17。
测试失败时保留具体 HTTP 响应和 Surefire 报告。缺依赖属于 Maven 仓库访问问题,HTTP 连接错误属于实验服务或网络问题;已经得到预期状态与 JSON 内容后,再比较索引行为。
字段与文档结构怎样修改
字段数量增长需要从输入结构处理
把用户 ID、请求 ID 或任意属性名作为 JSON 字段名,可能让 Mapping 持续增长。字段数量增加会消耗集群元数据和查询相关资源。index.mapping.total_fields.limit 提供上限,但调大上限只是延后触发,不能消除无界键空间。字段爆炸
工程将某个独立索引的限制设为 2,先写数值字段 a、b,再写字段 c,返回字段数量超限的 400。对照索引将这些键放进 flat_object,Mapping 中只保留一个显式属性,原值仍能从 _source 取回。
频繁用于过滤、数值范围或聚合的属性,应提升为明确字段。只用于展示和归档的原始对象,可以用 enabled:false 的 object 保存,省去子字段解析。字段上限因此由实际查询需要控制,低频附加属性留在扩展区。object 字段
更新字段类型需要新结构
向已有 long 字段提交改成 keyword 的 Mapping,会返回 illegal_argument_exception。原索引中已经存在的数值结构无法仅靠修改声明解释成另一种字段。
小范围业务变化可以新增另一个字段,例如保留 price_cents 用于计算,增加 price_label 用于展示。若文档粒度或原字段语义必须改变,应建立新索引,将数据按新结构重新写入,检查命中、排序、聚合和删除状态后再切换入口。工程对类型变更的测试包含“原位修改失败→新字段或新索引写入成功”的完整步骤。修改 Mapping
持续写入中的历史回填还需要补齐增量与删除,具体操作见重建索引、Alias、双写与 CDC。仅执行一次 reindex,无法自动捕获快照之后发生的全部变化。
_id、routing 与版本各解决一个问题
_id 定位文档身份,业务 ID 通常适合直接用作确定性文档 ID。自定义 routing 决定文档落到哪个分片,同一文档的读取、修改、删除也需要使用相同路由。routing 可以减少查询触达的分片,不能充当租户认证;不同租户是否可见仍需服务端权限控制。
并发修改时有两类常用约束:
| 控制方式 | 用途 |
|---|---|
if_seq_no 与 if_primary_term | 读取后条件更新:只有文档仍是读到的那个状态,才允许修改 |
version 与 version_type=external | 使用外部系统提供的单调业务版本,拒绝较旧或相同版本 |
version_type=external_gte | 接受较大或相同版本,需要上游保证相同版本对应相同内容 |
source_version 只是文档中的普通字段;仅把它放进 JSON,并不会自动启用上述版本比较。外部版本需要通过写入 API 的 version 和 version_type 传递。Index Document API
工程先用 external 写入版本 5,再写版本 3 和 5,均得到 409。随后用 external_gte 写同一个版本 5、不同标题,服务端接受并覆盖标题。这说明相同版本可以被接受,与相同操作重放后内容不变,是两个条件。生产者必须对同版本载荷保持一致;冲突时不能一律忽略为“重复成功”。
发生输入错误后,示例用更高版本 6 写入修正内容,并读取 _version 与 _source 核对。批量重放、响应丢失、脚本增量和删除墓碑的处理参见客户端、Bulk 与重试。
实验结束,停止本目录对应的 Compose 项目:
docker compose -p search22 down该命令保留实验数据卷,便于继续查看手工索引。后续确实不再需要这些虚构数据时,再确认项目名、卷名与归属后删除对应实验卷;不要对共享 Docker 环境执行全局清理。
权威资料与规范地址
字段参数、分析器和 API 的完整定义可按下列入口查阅。操作环境固定 OpenSearch 3.8.0,升级时应同时复核服务端、分析插件和应用客户端。
版本、部署与索引配置
- OpenSearch Docker 安装:https://docs.opensearch.org/latest/install-and-configure/install-opensearch/docker/
- 发行版本:https://docs.opensearch.org/latest/version-history/
- 索引设置:https://docs.opensearch.org/latest/install-and-configure/configuring-opensearch/index-settings/
- Docker image save:https://docs.docker.com/reference/cli/docker/image/save/
- Docker image load:https://docs.docker.com/reference/cli/docker/image/load/
字段类型与 Mapping 演进
- 字段类型目录:https://docs.opensearch.org/latest/field-types/supported-field-types/
- Mapping 参数:https://docs.opensearch.org/latest/mappings/mapping-parameters/index/
- dynamic:https://docs.opensearch.org/latest/mappings/mapping-parameters/dynamic/
- 修改 Mapping:https://docs.opensearch.org/latest/api-reference/index-apis/put-mapping/
- object:https://docs.opensearch.org/latest/mappings/supported-field-types/object/
- nested:https://docs.opensearch.org/latest/mappings/supported-field-types/nested/
- flat_object:https://docs.opensearch.org/latest/field-types/flat-object/
- 字段爆炸:https://docs.opensearch.org/latest/mappings/mapping-explosion/
文本分析与字段读取结构
- 文本分析:https://docs.opensearch.org/latest/analyzers/
- Analyze API:https://docs.opensearch.org/latest/api-reference/analyze-apis/
- doc values:https://docs.opensearch.org/latest/mappings/mapping-parameters/doc-values/
- fielddata:https://docs.opensearch.org/latest/mappings/mapping-parameters/field-data/
