在本章中,你将从抽象走向具体,亲自动手学习如何在 OpenSearch 中为你的数据建立索引。你会启动并运行 OpenSearch,并连接到 OpenSearch Dashboards。在 Dashboards 的 Dev Tools 选项卡中,你可以直接向集群发送 REST 请求。你将利用这一入口创建带有不同设置的索引,并通过简单查询观察这些设置的影响。
当完成基础索引操作后,你将深入到更高级的设置与机制,学习如何自动创建带有预置设置的索引。最后,我们将深入解析 mappings(映射) 。
在本章末尾,你将理解:你发送到 OpenSearch 的数据与其用于匹配的内部处理之间的关系;并学会如何准备数据与映射,以最大化发挥 OpenSearch 查询能力的效果。
本章将涵盖以下主题:
- 索引概览
- 动手实践:连接 OpenSearch Dashboards
- 创建索引
- 通过 API 创建索引
- 理解索引设置
- 深入 mappings
- 技术准备
技术准备
本章你将连接到一个在线运行的 OpenSearch 集群。如何搭建集群请参阅第 2 章。你可以在此处获取本章示例源码并克隆仓库:
github.com/PacktPublis…。
ch4 目录下的 listing.txt 包含示例源码。使用你喜欢的文本编辑器打开该文件,然后将示例复制粘贴到 OpenSearch Dashboards 的 Dev Tools 选项卡中执行。
索引概览(Overview of indexing)
OpenSearch 是一种数据库技术,与其他数据库类似,它为你发送的数据提供主要容器以及可应用于数据的模式(schema) ,以控制其内部处理。许多数据库的主要容器是表;在 OpenSearch 中,主要容器是索引(index) 。
每个索引都带有一系列设置,用于控制诸多行为,例如:为支持并行处理而将数据分片、以及索引刷新频率(使新数据可被搜索)。索引还拥有 mappings,用于指定索引中字段的模式定义。
在设计搜索工作负载时,你需要思考哪些实体需要被检索,以及如何将它们组织成搜索文档。文档(document)是搜索引擎的核心单位——它是你提交进行索引的对象,也是查询返回的对象。你会从源数据(通常是事务型数据库或日志采集代理)构建文档,并把它们发送到某个 OpenSearch 索引。你的应用会根据用户在 UI 中的操作对该索引发起查询,并展示可交互的搜索结果。下一节我们将先通过连接 OpenSearch Dashboards,为创建索引、写入与查询数据做好准备。
动手实践:连接 OpenSearch Dashboards
在本书余下部分,你将使用 OpenSearch Dashboards 的管理面板 Dev Tools,便捷地向集群发送并执行命令。如何访问 Dashboards 取决于你的部署方式。出于本章与全书示例的目的,我们使用 Docker Desktop(www.docker.com/products/do…)在本地运行一个 OpenSearch 集群,其中包含 OpenSearch Dashboards 服务器、两个数据节点,以及一个机器学习节点。本章对应的仓库中提供了 docker_compose.yml,可用于在 Docker 中一键部署该集群。本节你将通过浏览器连接到 Dashboards,并执行一组 API 以确认连接正常。
在浏览器中输入 Dashboards 的地址——如果在本机运行,则为 https://localhost:5601。如果尚未配置 SSL 证书,需要接受安全警告并继续访问(具体操作以浏览器提示为准)。你将看到如下登录界面:
图 4.1:OpenSearch Dashboards 登录界面
输入管理员用户名与密码,点击 Log in。若是首次登录,你会看到欢迎界面:
图 4.2:OpenSearch Dashboards 欢迎界面
点击 Explore on my own 关闭对话框,随后会出现外观设置对话框:
图 4.3:OpenSearch Dashboards 新外观设置对话框
小提示:需要查看高清版本图片?请在下一代 Packt Reader 中打开本书,或参阅 PDF/ePub。
点击 Dismiss 后将显示租户(tenant)选择对话框。关于安全与多租户会在本书第 4 部分深入讲解。此处只需了解:OpenSearch Dashboards 支持 Dashboards tenants,用于控制用户对 Dashboards 中对象(如可视化与 dashboard)的访问。保持 Private tenant(私有租户)选中并点击 Confirm。
图 4.4:OpenSearch Dashboards 租户选择对话框
Dashboards 将重新加载以应用你的租户选择,并显示欢迎首页:
图 4.5:OpenSearch Dashboards 首页
该页面包含若干 UI 元素:
- 左上角的**“汉堡菜单”**用于展开导航面板,以探索数据与进行管理操作。
- Dashboards 自带示例数据、dashboard 与可视化,便于上手。
- Dev Tools 提供一个双栏界面,可直接向 OpenSearch 发送 API 调用。
点击右上角第三个图标 Dev Tools 打开开发工具面板。关闭欢迎对话框(可先下拉阅读小提示)。
图 4.6:OpenSearch Dashboards 的 Dev Tools 选项卡及欢迎对话框
你会看到分屏视图:左侧是查询编辑区,右侧为空白的结果区。在左侧输入命令,使用右侧的小三角执行(macOS 为 ⌘ + Enter,其他系统为 Ctrl + Enter)。右侧显示 API 调用结果。
初始时左侧会给出一个示例查询。将光标放到第一行(GET _search)的任意位置后,你会发现其后的请求体背景变浅,表示这些行属于当前高亮的命令。在第一行右侧可以看到一个小三角图标。点击它即可运行当前命令。
图 4.7:在 OpenSearch Dashboards 中运行命令
OpenSearch 将返回结果,Dashboards 在右侧面板展示响应。恭喜!你已成功向 OpenSearch 发送了一条查询。关于该查询与响应的具体含义,下一章会详细说明。自此,你将在本书的大部分实践中,将 Dev Tools 作为与 OpenSearch 交互的主要方式之一。
下一节,我们将学习并创建一个 OpenSearch 索引,并向其中写入一些数据。
创建索引
OpenSearch 让创建索引变得很简单。你只需向 OpenSearch 发送一份文档,它就会创建索引、根据文档推断并生成 schema,并将该索引的分片分布到集群中的数据节点上,使数据可被搜索。让我们通过向 OpenSearch 发送一条文档来创建你的第一个索引。将下面的代码输入或粘贴到 OpenSearch Dashboards 的 Dev Tools 左侧面板中。把光标放在第一行,确认请求体被高亮,然后执行命令:
POST first_index/_doc/1
{
"an_integer_field": 12345,
"a_string_field": "Mary had a little lamb"
}
当你调用 OpenSearch 的 API 来索引单条文档时,需要在 URL 中指定索引名称。若索引已存在,OpenSearch 会向其中新增或更新该文档;若索引不存在,OpenSearch 会自动创建它,并根据字段的取值(JSON 键)动态生成字段的 schema。
在上面的调用中,URI 与 REST 动词指明你正在向名为 first_index 的索引 POST 一条类型为 _doc、ID 为 1 的文档。由于该索引尚不存在,OpenSearch 会创建它,并利用字段值来动态创建字段映射。
文档(DOCUMENTS)
文档是 OpenSearch 的核心实体——你向其写入文档,查询也返回文档。可以把文档类比为关系型数据库表中的一行。文档是 JSON 对象,其键称为字段(fields) ,字段包含用于索引与检索的取值(可以是扁平或嵌套的 JSON)。每条文档都有唯一的 _id 作为标识。你可以像上例那样显式指定文档 ID;若不指定,OpenSearch 会自动生成。
在 Dev Tools 右侧面板,你会看到 OpenSearch 的响应:
{
"_index": "first_index",
"_id": "1",
"_version": 1,
"result": "created",
"_shards": {
"total": 2,
"successful": 1,
"failed": 0
},
"_seq_no": 0,
"_primary_term": 1
}
该响应包含若干元数据字段,用于说明命令结果。它们对调试与理解 OpenSearch 如何处理请求很有帮助。大多数 API 响应都包含其中的一部分字段。
_index 表示 URI 中的索引名;_id 是文档 ID。result 表示文档已创建,同时返回索引中各分片的处理状态。在此例中,你可以看到在总计 2 个分片中(包括副本),主分片已成功写入。OpenSearch 的默认行为是异步复制:当主分片完成写入即返回响应。_seq_no 与 _primary_term 与冲突更新管理相关,此处无需深究。
要验证索引已成功创建,可以使用 OpenSearch 的 _cat API。这是用于集群、索引与分片等信息的管理型 API。复制并执行下面的命令:
GET _cat/indices/first_index?v&h=health,status,index,pri,rep
提示:执行
GET _cat可查看全部 _cat 命令(还有一个小彩蛋)。
OpenSearch 响应类似:
health status index pri rep
green open first_index 1 1
可以看到索引 first_index 处于 open 状态,green 健康度,且拥有 1 个主分片与 1 个副本分片。(注意:如果你只部署了单节点,副本无法分配,索引状态将为 yellow。)
URL 参数 v 与 h 对所有 _cat API 通用,分别用于显示表头与筛选列。去掉 h 参数可查看全部可用列。
索引状态(INDEX STATUS)
索引健康状态有 green / yellow / red:
- green:所有分片(主+副本)均已分配且可用;
- yellow:至少主分片可用,但有副本分片未分配;
- red:至少有一个主分片未分配或不可用。
当某节点不可用时,若副本在其他节点,副本会被提升为主分片,索引转为 yellow;随后 OpenSearch 会重新复制,最终回到 green。
_bulk API
上面的示例是逐条发送文档。在实际生产中,你或你的数据管道通常会使用 _bulk API` 批量发送文档,以减少多次 API 往返与连接开销,并让 OpenSearch 能在更多文档上并行处理。
使用 _bulk API 时,请求体由动作行与数据行交替组成,可指定 create / delete / index / update / upsert 等操作;数据行是 JSON 文档。可在动作行或 URL 中通过元数据(如索引名、文档 ID)进行指定。示例:
POST _bulk
{ "create": { "_index": "first_index", "_id": "2" } }
{ "an_integer_field": 23456, "a_string_field": "the quick brown fox" }
{ "create": { "_index": "first_index", "_id": "3" } }
{ "an_integer_field": 23456, "a_string_field": "Lorem ipsum" }
现在你已经会把数据发送到 OpenSearch,接下来看看 OpenSearch 如何**解释(分析)这些数据。每个索引都有一个mapping(映射)**来声明该索引中数据的 schema。下一节我们将查看并理解不同类型与数据处理方式。
为数据建立映射(Mapping your data)
OpenSearch 的索引 schema 称为 mapping。你可以让 OpenSearch 根据写入的数据自动推断映射,或在创建索引时显式指定。自动推断虽方便,但容易出错;因此在设计与使用 OpenSearch 时,一个关键实践就是显式定义映射。
先看看 first_index 的映射。执行:
GET first_index/_mapping
可能得到如下响应:
{
"first_index": {
"mappings": {
"properties": {
"a_string_field": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
},
"an_integer_field": {
"type": "long"
}
}
}
}
}
返回的 JSON 中包含索引 first_index 的字段映射,字段名按字母序排列。可以看到 an_integer_field 被映射为 long;a_string_field 看上去“多了一层”——默认情况下,对字符串类型,OpenSearch 会创建 type: text 的主字段,并自动加一个 keyword 子字段。
因此你可以用 a_string_field(text 版本)或 a_string_field.keyword(keyword 版本)参与查询/聚合。此次推断是正确的;但如果你曾把 a_string_field 的值写成 1234,OpenSearch 可能会推断为 long,之后再写入字符串会失败,因为字符串无法写入 long 字段。
通过 API 创建索引(Creating your index via an API)
上一节是通过写入数据由 OpenSearch 自动创建索引。当然你也可以显式通过 API 创建索引,请求大致如下:
PUT /<index_name>
{
"settings": {
...
},
"mappings": {
...
},
"aliases": {
...
}
}
settings控制索引在 OpenSearch 中的部署与管理方式;mappings是文档 JSON 字段的schema;aliases允许你用一个别名管理并引用多个索引。
执行下面的命令创建 index_with_mapping 索引:
PUT index_with_mapping
{
"mappings": {
"dynamic": "strict",
"properties": {
"an_integer_field": { "type": "integer" },
"a_string_field": { "type": "text" }
}
}
}
可能返回:
{
"acknowledged": true,
"shards_acknowledged": true,
"index": "index_with_mapping"
}
这个示例中,我们为两个字段显式设置了映射类型,并将 dynamic 设为 strict。在该设置下,任何不在映射中的字段都会被拒绝,即写入会返回错误。
理解索引设置(Understanding index settings)
OpenSearch 的索引设置分为**动态(dynamic)与静态(static)**两类:
-
动态设置可随时更新;
-
静态设置需要先关闭索引后再修改(见官方文档
close-index)。
通常你很少需要改静态设置;而一些动态设置影响较大,需谨慎配置(完整列表见官方文档)。这里介绍几个关键项: -
index.number_of_shards(主分片数) 与index.number_of_replicas(副本数,动态设置) :
主分片数决定数据如何被划分。例如设为10,数据会被分成10个分片。OpenSearch 默认以文档 ID 的哈希来均衡路由(也可自定义 routing key;本章稍后“join 字段”处会提及)。
副本数则控制每个主分片的副本数量。例如number_of_shards: 10且number_of_replicas: 2,总分片数为 30(10 主 + 20 副本) 。注意术语区分:我们将“覆盖所有主分片的一套副本”称为一个副本(a replica) ;而覆盖单个主分片的副本称为副本分片(a replica shard) 。 -
分片数量对性能的影响:
每个分片都是一个独立的 Apache Lucene 索引。分片数实际决定了每个分片的数据量,也影响其查询处理的工作量。- 如果查询通常会过滤掉大部分文档,可降低分片数(单分片更大);
- 如果查询会匹配大量数据,可提高分片数(单分片更小)。
第 14 章会深入分片策略。
-
index.refresh_interval:
控制 Lucene 刷新频率。写入时数据先进入内存段,每到刷新间隔,OpenSearch 会打开新的 IndexReader 并将段刷盘。查询会遍历所有段,因此 Lucene 会周期性合并段以保持性能;合并会消耗 CPU 并可能增加查询延迟。设置该值时需在写后可见延迟与段合并 CPU 开销间权衡,建议设为30 秒或更长。 -
其他值得关注的动态设置:
多数文本抽取与预处理(如.pdf/.doc/.rtf)应在 OpenSearch 外完成;也可使用ingest pipeline在数据节点执行部分处理,可通过index.default_pipeline指定默认管道。
还有一些限额可限制索引时数据节点的工作量;默认值在实用与资源之间做了折中,调整需谨慎。index.query.default_field:当查询未指定字段时,默认参与查询的字段列表;index.routing.rebalance.enable:控制允许再均衡的分片类型(全部/仅主/仅副本/不允许)。
如前所述,分片数对扩展性至关重要。开源 OpenSearch 对未指定分片数的新索引默认 1 主分片;Amazon OpenSearch Service 默认 5。第 14 章会详细讨论扩展,但经验法则是:
- 搜索型:让每个分片约 10–30 GB;
- 日志型:让每个分片约 50 GB。
主分片数只能在创建时指定,后续若要更改需重建索引并重索引数据。下例创建一个1 主 1 副的索引:
PUT index_with_shard_count
{
"settings": {
"index.number_of_shards": 1,
"index.number_of_replicas": 1
},
...
}
Settings 决定 OpenSearch 如何对待“索引”;Mappings 决定 OpenSearch 如何对待“数据”。下一节我们将深入映射类型。
深入映射(Diving into mappings)
所有索引都有关联的 mapping,用于控制字段数据的处理方式。你可以显式设置,也可以使用 动态映射。多数情况下,建议显式静态映射并关闭动态映射。字段映射一旦确定并写入数据,无法就地修改;如需改变,必须新建索引并重索引。
显式设置映射时,使用创建索引 API,并按如下骨架声明:
"mappings": {
"properties": {
"<field name>": {
"type": "<mapping type>",
"<type-specific params>": "...",
"fields": { "<sub-fields>" : "..." }
},
"<field name>": { ... }
}
}
如果你的数据(如电商目录)结构已知,可以按用例制定合适的映射;若字段未知(如用户自定义元数据),则可依赖并定制动态映射。
动态映射(Dynamic mapping)
当未显式声明映射时,OpenSearch 会基于字段值自动推断类型:
true/false→ boolean;- 数值 → long / float;
- 嵌套 JSON → object;
- 数组 → 采用首个元素的类型;
- 字符串:先尝试识别为日期(默认启用),否则可选识别为数值(默认关闭;可在
mappings中设置"numeric_detection": true);若都不匹配,则设为 text 并带 keyword 子字段。
如果想改变默认行为,可在创建索引时于 mappings 中声明动态映射模板(dynamic_templates) ,基于检测到的类型、字段名或字段路径来生成映射:
PUT index_with_dynamic_mapping_templates
{
"mappings": {
"dynamic_templates": [
{
"<name>": {
/* 匹配条件 */,
"mapping": { /* 目标映射 */ }
}
}
]
}
}
可用的匹配条件包括:
match_mapping_type:匹配检测到的字段值类型(如1234→long);match/unmatch/match_pattern:按字段名匹配/排除,match与unmatch支持通配符;match_pattern使用 Java 正则(如^v??_[ts]);path_match/path_unmatch:按完整路径匹配(如"blog.authors.*.location")。
OpenSearch 还会提供 {name} 与 {dynamic_type} 变量,以在 mapping 中引用被检测到的字段名与类型。
索引模板(Index templates)
某些场景下你已知映射类型,但会按时间粒度持续创建新索引(如日志/流式数据按日/周/月/小时建索引,并在索引名中带有日期)。
手动逐次创建既繁琐也易出错。索引模板允许你为匹配通配符的索引名定义settings / mappings / aliases,OpenSearch 在索引创建时自动应用,通过 _index_template API 定义:
PUT _index_template/logs_template
{
"index_patterns": ["logs-*"],
"priority": 0,
"template": {
"mappings": {
"properties": {
"timestamp": {
"type": "date",
"format": "yyyy-MM-dd HH:mm:ss||yyyy-MM-dd||epoch_millis"
},
"value": {
"type": "double"
}
}
}
}
}
当你(或你的摄取工具)向一个不存在的、且名称匹配通配符的索引写入文档时,OpenSearch 会按模板自动创建该索引并应用映射/设置/别名。例如:
POST logs-2024.07.13/_doc/1
{
"timestamp": "2024-07-13 14:17:20",
"host": "10.198.10.16"
}
此时索引名匹配 logs-*,会套用模板。执行 GET logs-2024.07.13/_mapping 可查看结果——即便未写入 value 字段,也会按模板创建为 double。动态映射仍然生效,host 将被自动创建为字符串类型。
你可以创建多个模板且其索引模式可能重叠;OpenSearch 会按 priority 从高到低依次应用。此外可以创建组件模板(component templates) ,再组合出索引模板。下面将前述模板拆分为两个组件:
组件模板 1:为 timestamp 应用 date 类型:
PUT _component_template/component_template_1
{
"template": {
"mappings": {
"properties": {
"timestamp": {
"type": "date",
"format": "yyyy-MM-dd HH:mm:ss||yyyy-MM-dd||epoch_millis"
}
}
}
}
}
组件模板 2:为 value 应用 double 类型:
PUT _component_template/component_template_2
{
"template": {
"mappings": {
"properties": {
"value": { "type": "double" }
}
}
}
}
组合模板:将两者组合并作用于 logs-*:
PUT _index_template/logs_template
{
"index_patterns": ["logs-*"],
"priority": 100,
"composed_of": [
"component_template_1",
"component_template_2"
]
}
此刻你已有一个旧的 logs_template(非组件版)与索引 logs-2024.07.13。请先删除该模板与索引,然后按上面三段代码重建为组件模板版的 logs_template。再删除旧索引:DELETE logs-2024.07.13。重新发送前述文档并检查 mapping,确认 timestamp、host、value 字段均符合预期。
通过 REST 处理已保存对象(HANDLING SAVED OBJECTS WITH REST CALLS)
OpenSearch 会持久化多种对象,如索引、索引模板、ingest pipelines、search pipelines、ML 模型等,并遵循 REST 规范:你可以用 PUT/POST 创建或更新,用 GET 查看,用 DELETE 删除。正如上例所示,使用 DELETE 删除索引,比逐条删除文档要高效得多。
映射类型(Mapping types)
在索引阶段,OpenSearch 会创建用于快速检索的数据结构。**倒排索引(inverted index)将字段取值中的词元(terms)映射到包含这些词元的文档集合(posting lists)。什么被视作“词元”取决于 OpenSearch 对字段的处理方式,而这又由该字段的映射(mapping)**决定。OpenSearch 提供多种基础映射类型:
- 数值(Numeric) :支持
byte、double、float、half_float、integer、long、unsigned_long、scaled_float、boolean、short。 - 日期(Date) :支持毫秒或纳秒粒度(
date与date_nanos)。 - IP 地址(IP address) :使用
ip_address字段类型,可直接匹配地址,也可使用 CIDR 表示法查询。 - 二进制(Binary) :Base64 编码的二进制值。
- 范围(Range) :范围字段可高效查询数值区间。向范围类型字段写入文档时,可使用“小于/小于等于”等端点限定。支持的基础类型包括
integer_range、long_range、double_range、float_range、date_range、ip_range。
对于这些数值类类型,OpenSearch 不会处理或改变字段值;它会使用诸如 BKD 树之类的数据结构来支持对字段值的快速检索。OpenSearch 的深度体现在其对字符串字段的处理。
字符串映射类型(String mapping types)
OpenSearch(其核心为 Lucene)在处理大段非结构化文本(free text)时尤为出色:解析文本并应用语言相关规则,抽取可匹配的最小单元(terms),并构建倒排索引以实现快速匹配。要处理自由文本,可使用 text、match_only_text 与 token_count 字段类型。本章重点关注 text 字段;match_only_text 用更小的存储换取较少的特性支持;token_count 仅存储父字段的词元计数,实际场景中不常用。典型 text 用途包括电影剧情简介、CRM 通话记录、用户生成内容(如商品评价正文)等。
OpenSearch 也可以不解析字符串而进行存储与匹配。对于这类需要精确匹配的文本,使用 keyword 字段类型,例如应用生成的字符串:产品类别、州/省名称、用户性别等。这些文本通常不变且应完全匹配。
无论采用内置字符串处理还是自定义分析器(analyzer),文本分析都是索引工作的核心。如果分析器没有生成正确、可匹配的词元,你的查询永远无法找回期望的文档!请特别留意字符串字段的定义与用法。
下面用 _analyze API 看一些 text 处理示例。回到 OpenSearch Dashboards 的 Dev Tools,执行:
GET _analyze
{
"text": ["OpenSearch standard analyzer 1234.4. For text"],
"analyzer": "standard"
}
观察输出。OpenSearch 返回 6 个词元:opensearch、standard、analyzer、1234.4、for、text。standard 分析器按空白与标点切分,并将词元小写化。注意它能区分 1234.4 中的小数点与句末的句点。
standard 是 text 字段的默认分析器。你也可以应用 34 种语言分析器,支持词干提取(stemming)、停用词移除与同义词应用。试运行:
GET _analyze
{
"text": ["OpenSearch standard analyzer 1234.4. For text"],
"analyzer": "english"
}
观察输出。OpenSearch 返回 5 个词元:opensearch、standard、analyz、1234.4、text。与 standard 类似,english 分析器同样按空白/标点切分并小写化,同时使用词干提取将 analyzer 变为 analyz。词干提取将词形还原到共同词根,使共享词根的词更易匹配。试用字符串 "Analyze, Analyzing, Analyzer",可看到同样产出 analyz(注意有时也会出现 analysi 的结果)。
OpenSearch 的内置分析器包括:
- Standard(默认) :按词边界解析,移除标点,小写化字符
- Simple:在非字母边界切分,移除非字母字符,小写化
- Whitespace:按任意空白切分
- Stop:与 Simple 相同,但还会移除停用词
- Keyword:输出原始字符串并做规范化
- Pattern:基于正则表达式解析;可小写化、移除停用词
- 语言分析器(如 English) :应用语言相关的解析、词干、停用词
- Fingerprint:按非字母字符切分、转 ASCII、小写、排序、去重并连接为一个字符串;可移除停用词
你可以在 Dev Tools 中试用不同分析器以理解其行为。
自定义分析器
你可以创建自定义分析器来控制字符串处理。索引 text 字段时,文本会经历数个阶段:先由**字符过滤器(char filter)增删改字符,再经分词器(tokenizer)将字符流构成词,再由词元过滤器(token filter)**增删改词元(见图 4.8)。
要创建自定义分析器,可在创建索引时的 settings 中声明:
PUT index_with_custom_analyzer
{
"settings": {
"analysis": {
"analyzer": {
"text_with_urls": {
"type": "custom",
"tokenizer": "path_hierarchy",
"char_filter": [ "html_strip" ],
"filter": [ "lowercase" ]
}
}
}
},
"mappings": {
"properties": {
"a_custom_analyzed_text_field": {
"type": "text",
"analyzer": "text_with_urls"
}
}
}
}
试用该分析器:
GET index_with_custom_analyzer/_analyze
{
"text": "https://opensearch.org/project",
"analyzer": "text_with_urls"
}
OpenSearch 提供大量 tokenizer 与 token filter 以帮助你生成合适的词元集合。常用分词器之一是 ngram / edge_ngram。N-gram 是原字符串的子串(每次取 n 个字符),常用于匹配字符串内部片段,及不以空白分词的语言;edge n-gram 从字符串左端开始逐步扩展,常用于前缀匹配/自动补全。试试下面的调用,看看 ngram 对 "OpenSearch" 产生的词元:
GET _analyze
{
"text": "OpenSearch",
"tokenizer": "ngram"
}
当需要将大块文本拆为可匹配的词元时,用 text 分析;而在很多场景(如 UI 文本元素,或品牌/尺码/颜色等元数据)你希望精确匹配,则应将其映射为 keyword 字段。
关键字与规范化字段(Keywords and normalized fields)
text 字段适用于自由文本的高级处理;而对于需要精确匹配的字符串,使用 keyword。典型例子有电影评级、题材等应用特定字符串。keyword 字段不做分析,而是规范化(normalization) :例如仅小写化。与分析器类似,normalizer 也可使用字符过滤器与词元过滤器,但不使用 tokenizer,因为其输出为单个词元。
继续使用你之前创建的 first_index。该索引中 a_string_field 带有 a_string_field.keyword 子字段。你可用 GET first_index/_mapping 查看映射。此前我们向 a_string_field 写入了 "Mary had a little lamb"。先用 text 字段搜索:
GET first_index/_search
{
"query": { "match": {
"a_string_field": "Mary"
}}
}
如果此时去查询 a_string_field.keyword 中的 "Mary"(见本章仓库 listing),你会得到空结果——因为 keyword 必须精确匹配。改为完整查询 "Mary had a little lamb" 即可命中。同样地,默认 normalizer 不会小写化,因此用 "mary had a little lamb"(小写 m)也不会命中。
你也可以为 text 自定义字符串处理方式,方法是定义自定义 normalizer:
PUT index_with_custom_normalizer
{
"settings": {
"analysis": {
"normalizer": {
"normalized_keyword": {
"type": "custom",
"filter": [ "asciifolding", "lowercase" ]
}
}
}
},
"mappings": {
"properties": {
"custom_normalized_keyword": {
"type": "keyword",
"normalizer": "normalized_keyword"
}
}
}
}
写入一条文档:
POST index_with_custom_normalizer/_doc/1
{
"custom_normalized_keyword": "Naïve string"
}
该 normalizer 会将所有字符小写化并进行 ASCII 折叠(asciifolding) ,移除变音符(例如 Naïve 中的分音符)。查询如下:
GET index_with_custom_normalizer/_search
{
"query": { "match": {
"custom_normalized_keyword": "naive string"
}}
}
尽管查询使用了 naive 而不是 Naïve,文档依然能够匹配。
高级映射类型(Advanced mapping types)
OpenSearch 的映射类型除了前文介绍的核心类型,还包含一些更高级的类型,用于向量匹配、对嵌套 JSON 的特殊处理、地理位置数据以及反向匹配(percolation)。本节将逐一介绍这些高级字段类型。
K 近邻(K-Nearest-Neighbor, KNN)
过去十年,自然语言处理取得了长足进展。随着 通用 Transformer(GPTs) 的兴起,大语言模型(LLMs) 能通过上下文关联来捕捉词语语义,并据此生成符合语义的自然语言回复。
LLM 也可以为自然语言文本生成向量表示(embeddings) :在这个向量空间中,相近的点表示语义相似。OpenSearch 可以使用这些向量做语义搜索:对查询生成向量嵌入,然后在索引中的向量空间里寻找其最近邻,最近邻就是与查询语义最相近的文档。第 9 章会更系统地讨论 embeddings、语义搜索与聊天应用。
OpenSearch 提供存储与使用 ML 模型的能力,可通过 ml-commons 插件 或连接 OpenAI、Cohere、Amazon Bedrock、Amazon SageMaker 等第三方服务生成嵌入;底层的向量检索由 KNN 插件 与核心 Apache Lucene 向量引擎提供。
要在 OpenSearch 中使用向量嵌入,需要创建一个将 index.knn 设为 true 的索引;在映射里定义 knn_vector 字段,并指定引擎与算法及参数。例如:
PUT vector_index
{
"settings": {
"index.knn": true
},
"mappings": {
"properties": {
"knn_field": {
"type": "knn_vector",
"dimension": 2,
"method": {
"engine": "faiss",
"space_type": "l2",
"name": "hnsw",
"parameters": {
"ef_construction": 128,
"m": 24
}}}}}}
OpenSearch 同时支持精确最近邻与近似最近邻检索。精确检索会把查询向量与每个文档向量都计算一遍距离,最准确但随向量总量线性变慢;因此实际多用近似算法。
OpenSearch 支持 两种近似最近邻算法与 三种向量引擎:FAISS、NMSLIB(3.0 起弃用)与 Apache Lucene。不同引擎的算法支持不同,但通常可以使用 HNSW 与 IVF 两类存储/检索算法。
写入向量数据:
POST vector_index/_bulk
{ "create": { "_id": "1" } }
{ "knn_field": [0, 0] }
{ "create": { "_id": "2" } }
{ "knn_field": [100, 100] }
最近邻查询示例:
GET vector_index/_search
{
"query": {
"knn": {
"knn_field": {
"vector": [1, 1],
"k": 1
}}}}
此查询会返回距离 [1,1] 最近的文档,即位于原点的文档 1。
第 10 章还会继续介绍 OpenSearch 在语义搜索与 RAG 场景中的用法。
嵌套 JSON(Nested JSON)
OpenSearch 更擅长处理扁平 JSON;它不是关系型存储,天然不擅长强关系查询。但它也能处理字段值为嵌套对象的 JSON:会为子对象建立索引,并允许用点路径查询子字段。例如:
POST index_with_nesting/_doc/1
{ "author": [
{ "first": "Jon", "last": "Smith" },
{ "first": "Jane", "last": "Doe" }
]}
查询子字段:
GET index_with_nesting/_search
{
"query": { "bool": { "must": [
{"match": { "author.first": "Jon" }}
]}}
}
如果查看 author 的映射,会看到其下的 first 与 last 子字段,并带有 text/keyword 两种映射。
注意:当字段是“对象数组”时会出现“交叉匹配”的现象。比如下面的查询会把两个作者的信息“拼”到一起,从而错误命中:
GET index_with_nesting/_search
{
"query": { "bool": { "must": [
{ "match": { "author.first": "Jon" }},
{ "match": { "author.last": "Doe" }}
]}}}
为避免“串场”,应将字段映射为 nested 类型:OpenSearch 会把数组中的每个对象内部转成独立的隐藏文档,从而保证同一对象内字段的关联查询。示例:
DELETE index_with_nesting
PUT index_with_nesting
{
"mappings": {
"properties": {
"author": { "type": "nested" }
}}}
重建文档后,用 nested 查询来限定路径并匹配同一作者对象:
{
"query": {
"nested": {
"path": "author",
"query": { "bool": {
"must": [
{ "match": { "author.first": "Jon" }},
{ "match": { "author.last": "Smith" }}
]}}}}
}
此时 Jon Doe 将不会命中,而 Jon Smith 会命中。
flat_object
与 nested 相关的还有 flat_object。它允许你以点路径搜索嵌套 JSON,但不会为内部字段创建映射,能避免映射爆炸(mapping explosion),在字段结构多变或极多时很有用。限制:无法为子字段设置显式映射或自定义分析。示例:
PUT flat_object_index
{
"mappings": {
"properties": {
"object_field": { "type": "flat_object" }
}}}
POST flat_object_index/_doc/1
{ "object_field": {
"title": "Iron Man",
"release_details": {
"year": 2008,
"mpaa_rating": "PG",
"box_office": 120000000
}}}
按点路径查询:
GET flat_object_index/_search
{
"query": { "match": {
"object_field.release_details.mpaa_rating": "PG"
}}}
查看映射可见只有 object_field 自身被声明为 flat_object,内部字段未生成映射。
关联(Join)
Join 字段可用于描述存在层级关系的数据(不推荐常用)。例如电商目录:product 与其 offer。先定义一个带 join 字段、声明父子关系的索引:
PUT join_index
{
"mappings": {
"properties": {
"product_offers_join": {
"type": "join",
"relations": { "product": "offer" }
}}}}
索引父文档(产品):
POST join_index/_doc/1
{
"product_name": "A1-brand powerful household cleaner",
"product_offers_join": { "name": "product" }
}
索引两个子文档(报价)。父与子必须在同一分片,因此用 _routing 指向父文档 ID:
POST join_index/_doc/2?routing=1
{
"name": "Al's grocery",
"price": 1.47,
"product_offers_join": { "name": "offer", "parent": "1" }
}
POST join_index/_doc/3?routing=1
{
"name": "Jen's online store",
"price": 1.44,
"product_offers_join": { "name": "offer", "parent": "1" }
}
按父/子关系查询,例如查找指定产品的所有报价:
GET join_index/_search
{
"query": {
"has_parent": {
"parent_type": "product",
"query": { "match": { "product_name": "A1-brand powerful household cleaner" } }
}}}
是否应该使用 Join?
Join 功能强大,但应尽量避免。OpenSearch 不是关系型系统,最佳实践是反规范化:例如把产品字段复制到每条报价;或者把报价作为nested存在产品里;或拆成两个索引,查询后在应用层做关联。
地理数据(Geo data)
地理能力可以让应用具备位置感知(如找房、打车、就近服务、日志地理分布)。OpenSearch 支持用经纬度或 geohash 表达点(geo_point),以及任意边界的形状(geo_shape)。
示例(旧金山市政厅与纽约市政厅):
PUT geo_index
{
"mappings": {
"properties": {
"name": { "type": "keyword" },
"pt_location":{ "type": "geo_point" },
"sh_location":{ "type": "geo_shape" }
}}}
POST geo_index/_doc/1
{ "name": "San Francisco City Hall",
"pt_location": { "lat": 37.78, "lon": -122.42 },
"sh_location": {
"type": "polygon",
"coordinates": [[
[37.7799,-122.4199],
[37.7799,-122.4185],
[37.7787,-122.4185],
[37.7787,-122.4199],
[37.7799,-122.4199]
]]}}
POST geo_index/_doc/2
{ "name": "New York City Hall",
"pt_location": "dr5regw2zr49",
"sh_location": {
"type": "polygon",
"coordinates": [[
[40.7123,-74.0066],
[40.7123,-74.0055],
[40.7132,-74.0055],
[40.7132,-74.0066],
[40.7123,-74.0066]
]]}}
做一个美国边界内的矩形范围过滤(geo_bounding_box):
GET geo_index/_search
{
"query": {
"bool": {
"must": [ { "match_all": {} } ],
"filter": [ { "geo_bounding_box": {
"pt_location": {
"top_left": { "lat": 49.5904, "lon": -125.0011 },
"bottom_right":{ "lat": 24.9493, "lon": -66.9326 }
}}}]}}}
两条文档都会命中。
反向匹配(Percolation)
通常是“用查询去找文档”。Percolator 反其道而行之:把查询当作文档索引,之后把文档当作查询提交,来看看新文档会匹配哪些“已保存的查询”。适用于“用户保存检索条件、当有新内容匹配时通知用户”的场景(如房产订阅)。
定义映射,包含一个 percolator 类型的字段及将用于匹配的实际数据字段:
PUT percolate_index
{
"mappings": {
"properties": {
"stored_query": { "type": "percolator" },
"bedrooms": { "type": "integer" },
"street_address":{ "type": "text" }
}}}
索引一条“存储的查询”——匹配卧室 ≥2 且地址包含 cherry 的房源:
POST percolate_index/_doc/1
{
"stored_query": {
"bool": {
"must": [
{ "range": { "bedrooms": { "gte": 2 } } },
{ "match": { "street_address": "cherry" } }
]}}}
用文档来触发 percolate 查询:
GET percolate_index/_search
{
"query": {
"bool": {
"filter": {
"percolate": {
"field": "stored_query",
"document": {
"bedrooms": 4,
"street_address": "123 Cherry Street, apt 4b"
}}}}}}
因为 street_address 被映射为 text,会经过分析且能匹配到 cherry,所以该文档会命中之前保存的查询。
小结
本节你学习了 OpenSearch 的高级映射类型:
- 向量(knn_vector) 支持语义检索与近似最近邻;
- nested 保证对象数组内字段的配对匹配;flat_object 通过不展开映射来避免映射爆炸;
- join 能表达父子层级(但通常建议以反规范化或
nested/多索引替代); - geo_point/geo_shape 支持强大的地理检索;
- percolator 让你“用文档去匹配已存查询”,实现订阅/告警类能力。
下一章将深入 OpenSearch 的查询 API,完善你对其强大检索能力的理解。