Elasticsearch 搜索与索引
倒排索引、中文分词与文档检索,以及过滤、排序和聚合的区别。
Elasticsearch 把文档中的内容组织成可查询的索引,提供全文检索、字段过滤和聚合统计。 搜索一段文字时,它会分析查询中的词项,查找匹配文档,并按相关性或指定字段排序。
Elasticsearch 简称 ES,底层使用 Lucene。 Kibana 提供查询和可视化界面;Python 客户端直接访问 ES,不依赖 Kibana。
官网:https://www.elastic.co/elasticsearch
1. 文档与索引
1.1 文档、字段和 Mapping
一篇文章、一件商品或一条日志,都能表示成一份 JSON 文档(Document)。文档中的 title、category 等属性称为字段(Field)。
索引(Index) 组织一组文档,Mapping 规定字段的类型及其处理方式。
正文需要分词,类别需要按完整值匹配,发布时间需要按日期比较。
| 概念 | 本文示例 | 作用 |
|---|---|---|
| 索引 | article_demo_es | 保存用于检索的文章文档 |
| 文档 | ID 为 101 的文章 | 一条完整的检索记录 |
| 字段 | title、text、category | 保存标题、正文和类别 |
| Mapping | text 为文本,category 为关键词 | 决定字段如何索引和查询 |
| 文档 ID | _id="101" | 在同一个索引内标识文档 |
索引与关系数据库的表、文档与行,可以作为理解数据组织的粗略对应。但 ES 的 Mapping 不提供关系表的外键约束,多个文档的写入也不能当成一个数据库事务。
业务数据保存在 MySQL、ES 保存搜索副本时,应用或同步任务需要把业务库的修改写入 ES。
1.2 正排索引与倒排索引
文本经过分析得到的检索单位称为词项(Term)。正排索引和倒排索引保存这些词项与记录的对应关系,区别在于查找方向:
- 正排索引按记录 ID 保存词项列表,回答“一条记录包含哪些词项”。
- 倒排索引按词项保存记录 ID 列表,回答“哪些记录包含这个词项”。
假设商品库有三件商品,每条商品记录是一份 ES 文档。下面按给定词项拆分商品名称,仅演示索引关系,不代表后文
cjk分析器的实际输出。正排索引:按商品编号查看词项。
商品编号 商品名称 词项 P1红色运动鞋 红色、运动鞋 P2蓝色运动鞋 蓝色、运动鞋 P3红色背包 红色、背包 已知编号
P1,能查到“红色、运动鞋”。如果只有这张正排表,想找出所有运动鞋,就需要逐条检查词项列表。倒排索引:按词项查找商品编号。
词项 商品编号 红色 P1、P3蓝色 P2运动鞋 P1、P2背包 P3查“运动鞋”即可找到
P1、P2。如果要求同时匹配“红色”和“运动鞋”,两组编号的交集只剩P1;任意一个词项匹配即可时,并集为P1、P2、P3。
倒排索引让词项查询直接定位候选记录,还能按字段配置保存词频、位置,供相关性评分和短语查询使用。正排索引与倒排索引描述的是数据组织方式,不能直接等同于 MySQL 与 ES 的区别。
词项不一定是完整词语。第 3 节的
cjk分析器会把“全文检索”拆为“全文”“文检”“检索”。
Index 是文档容器,倒排索引是内部检索结构。 日期范围查询、排序和聚合还会使用其他数据结构。
1.3 节点、分片与副本
ES 服务进程称为节点(Node),多个节点组成集群(Cluster)。索引的数据分布在主分片(Primary shard) 中,每个分片是一个独立的 Lucene 索引。
- 主分片拆分索引的数据。一篇文档通过路由定位到其中一个主分片。
- 副本分片保存对应主分片的数据副本,能够服务搜索请求;主分片失效时,符合条件的副本可以被提升为主分片。
- 节点承载分片。一个节点可以保存多个分片,分片数量不等于节点数量。
一个索引配置 2 个主分片、每个主分片 1 个副本,总共需要分配 4 份分片。对应的主分片与副本不能放在同一节点上。
一次搜索通常会从每个主分片及其副本中选择一份执行查询,由接收请求的协调节点汇总结果。
分片或副本越多,不代表查询一定更快。 分片增加协调开销,副本增加存储与复制开销。
副本不能代替备份。 误删除会被复制,恢复历史数据需要单独的快照。
2. 本地部署
2.1 启动服务
示例环境:ES 9.2.0、Python 3.13。Linux 安装 Docker Engine + Compose V2,macOS 安装 Docker Desktop。
Docker Desktop 至少分配 4 GB 内存,本例 ES 容器限用 2 GB。
在新目录中保存 compose.yaml:
| |
这份配置关闭认证,端口仅绑定 127.0.0.1。 自建 ES 默认启用认证与 TLS;远程服务需要按实际证书和凭据连接,不能直接套用这份配置对外开放。
在 compose.yaml 所在目录启动并检查服务:
| |
容器运行中不等于接口已就绪。 curl 返回包含 version.number 的 JSON,表示 HTTP 接口已能响应;这里的版本应为 9.2.0。
9200提供 HTTP API。节点间通信使用的9300无需在这个单节点示例中映射到宿主机。
2.2 Python 连接
创建虚拟环境(python3 为 Python 3.13):
| |
在宿主机连接 ES,后续示例沿用当前 Python 会话:
| |
request_timeout是单次请求的客户端等待时间,单位为秒。max_retries=0关闭客户端自动重试,便于在练习中直接看到失败。
宿主机连接
localhost:9200;同一 Compose 网络中的容器连接elasticsearch:9200。HTTPS 服务需按配置提供 CA 证书和认证信息。
3. 字段与分词
3.1 text 与 keyword
text对文本做分析,适合全文检索。keyword保留完整值形成词项,适合类别、标签和标识符。
本例
keyword未配置额外的规范化规则。
| 字段 | 类型 | 本文用途 |
|---|---|---|
doc_id | keyword | 保存一份业务 ID,作为排序的次级条件 |
title | text | 搜索标题 |
title.raw | keyword | 按完整标题匹配或排序 |
text | text | 搜索正文 |
category | keyword | 按类别过滤、统计数量 |
published_at | date | 按日期筛选和排序 |
title.raw 是 title 的多字段(Multi-field):同一个输入值按两种方式建立索引。写入文档时只传 title,不需要另传 raw。
_id属于文档元数据,不写在 Mapping 的properties中。它不能直接用于字段排序,因此示例另存一份doc_id。
keyword 保留完整值,不等于只能用等号查。 它仍支持前缀、通配符查询;字段存储方式与查询条件是两回事。
3.2 中文分析结果
分析器(Analyzer) 包含字符过滤、分词以及词项过滤等处理。写入 text 字段时,分析器决定保存哪些词项;执行全文查询时,查询文本也会经过分析。
示例选用内置 cjk 分析器,不额外安装插件。它会把连续中文字符组合成相邻的双字词项:
| |
“文检”没有独立词义,但会参与匹配。中文文本能被拆开,不代表切分结果符合业务词汇。 人名、品牌和领域术语等查询,需要单独检查分词效果。
IK 等第三方插件提供不同的中文切分与词典机制,安装时需核对 ES 与插件版本。
修改分词规则不会自动更新已有词项。 调整索引分析规则通常需要新建索引并重新写入数据。
3.3 创建索引
为本文字段显式定义 Mapping,避免首批数据决定的动态类型与后续需求不符:
| |
本文的单节点索引配置 1 个主分片、0 个副本。
若改为 1 个副本,副本不能与主分片放在同一节点上,会处于未分配状态。主分片均正常、但有副本未分配时,集群状态为
yellow。
acknowledged=True表示创建请求已获集群确认,不表示文档已经写入。
创建前确认 article_demo_es 尚不存在;重复练习时更换索引名,并同步用于后续代码。读取已有索引不需要再次创建。
搜索不存在的索引不会自动创建它;符合配置条件的写入请求才可能触发自动创建。
已有字段的类型通常不能直接修改。
category已建成text时,不能直接改为keyword。需要新建索引,按新的 Mapping 重新写入文档。
4. 文档写入与读取
4.1 准备文档
准备六篇示例文档:
| |
六条数据只用于观察接口行为,不能代表大规模搜索性能或实际业务中的分词效果。
4.2 批量写入
helpers.bulk 批量写入文档,每项操作指定索引、文档 ID 和正文:
| |
chunk_size=500限制每批最多 500 个操作,请求大小限制还可能让批次更小。_source是提交的文档内容,_id决定文档在索引内的身份。
index 对新 ID 创建文档,对已有 ID 替换整份文档。 仅允许新增时使用 create,同 ID 已存在会报错。
按示例 ID 重复执行这段写入,不会新增六个 ID,但会把后续修改覆盖回
docs中的内容。
批量写入失败不会回滚已成功的记录。 记录失败时,helpers.bulk 默认抛出 BulkIndexError;只有正常返回时,才能读取 written 和 errors。
4.3 写入与搜索可见性
写入请求成功,不等于搜索接口立即能查到新内容。 ES 的近实时搜索依赖 refresh 让近期变化对搜索可见。
refresh 参数 | 请求行为 | 使用场景 |
|---|---|---|
False,默认值 | 不额外等待搜索可见 | 连续写入,不要求紧接着搜索 |
"wait_for" | 等待这些变化对搜索可见再返回 | 写入完成后立即执行搜索 |
True | 请求立即刷新相关分片 | 明确需要主动刷新时使用,会增加刷新开销 |
本例写入后立即搜索,因此使用
refresh="wait_for"。等待会增加请求耗时,高频写入不必每条都等待。
4.4 按 ID 读取
get 按文档 ID 读取:
| |
GET 按 ID 读取默认是实时的,不依赖搜索 refresh。
写入后,GET 可能已读到新内容,搜索却暂时读不到。ID 不存在时,官方 Python 客户端会抛出
NotFoundError。
5. 搜索与过滤
5.1 全文搜索
Query DSL 用 JSON 对象描述查询,在 Python 中用字典表示:
| |
match 按字段的分析器处理查询,and 要求所有词项匹配,or 允许任意词项匹配。 默认使用 or。
“全文检索”被拆成“全文”“文检”“检索”。设置
operator="and"时,三个词项都要出现,但不要求相邻,匹配101、104;改为"or"或省略参数后,含有“检索”的102也会命中。
size=10是最多返回的命中条数,不是匹配条件。代码将 ID 排序打印,是为了检查命中文档;ES 返回的原始列表仍按相关性排序。
搜索响应中的主要字段:
| 字段 | 含义 |
|---|---|
hits.hits | 本次返回的命中列表 |
hits.total.value | 总命中数量,需结合 relation 判断是否精确 |
hits.total.relation | eq 表示精确值,gte 表示至少这么多 |
_id | 命中文档的 ID |
_score | 该查询下的相关性分数 |
_source | 文档内容,受请求中的返回字段设置影响 |
需要精确总数时,设置 track_total_hits=True;这会增加命中计数的计算量。
5.2 词项匹配与条件过滤
term 匹配指定词项,不会像 match 一样对输入做全文分析。 类别字段是 keyword,完整的“检索”就是该字段的一个词项:
| |
两种查询的区别在于字段存了哪些词项、查询是否分析输入,不能概括成“match 模糊、term 精确”。
对
text执行term查询“全文检索”时,ES 会查找完整的这个词项。但cjk分析器存下的是“全文”“文检”“检索”,没有“全文检索”,因此不会命中。
正文搜索、类别限制和日期范围可以放在同一个 bool 查询中:
| |
must 中的正文查询参与评分,filter 中的类别、日期只决定是否保留文档。
日期范围中,
gte包含下界,lt不包含上界。这里包含 10 月 2 日,不包含 10 月 5 日。
bool 子句 | 对命中的要求 | 在这里是否参与评分 |
|---|---|---|
must | 所有子条件都满足 | 是 |
filter | 所有子条件都满足 | 否 |
must_not | 子条件均不匹配 | 否 |
should | 满足多少项由 minimum_should_match 决定 | 匹配项参与评分 |
should 是否必须命中,取决于同一个 bool 中的其他条件:
- 没有
must或filter:默认至少匹配一项should。 - 已有
must或filter:默认允许所有should都不匹配。
需要固定至少命中几项时,显式设置 minimum_should_match。
类别条件应随查询交给 ES,才能在符合类别的文档中选出前若干条。
如果 ES 返回全量搜索的前 10 条,应用才按类别过滤,结果可能只剩两三条。符合类别、但没进入全量前 10 的文档也会被漏掉。
5.3 相关性、短语与多字段
普通全文查询默认按 _score 降序返回。默认评分方法 BM25 主要考虑词项在当前字段中的出现频率、在文档集合中的常见程度,以及字段长度。
词项越常见,区分作用越弱;在同一篇文档中反复出现,对相关性的贡献会增加,但不会无限线性增长。
_score 没有通用的“合格分数”。 查询、文档集合或字段权重变化,分数都会变化。
短语搜索还会考虑词项的位置。下面的 match_phrase 沿用 cjk 分析器,默认 slop=0,要求分析后的词项按对应顺序与位置匹配:
| |
短语查询检查分析后的词项位置,不等同于原始字符串的逐字比较。
上面的
match_phrase与match加and恰好都命中101、104。若一篇文档包含全部查询词项,但词项位置分散,and仍可能命中,默认的短语查询则不一定命中。
同时搜索标题和正文时,可以使用 multi_match:
| |
title^2提高标题字段的权重,但不保证标题命中的文档排第一。默认的best_fields取最佳匹配字段的分数。
这里的 and 分别作用于各字段的查询,不允许把词项随意分散到标题和正文中凑齐。分数和排序以实际响应为准。
6. 排序、分页与聚合
6.1 字段排序与分页
按发布时间从新到旧读取“检索”类别的文档,时间相同时按业务 ID 排序:
| |
指定字段排序后,结果按字段值排列,默认不计算相关性分数。
from_对应请求中的from,表示跳过的条数。保持上述数据和排序不变,下一页设为from_=2,会返回101。
页码越深,需要处理的候选结果越多。 各分片要提供覆盖 from + size 的候选,协调节点汇总后才能丢弃前面的记录。
默认 index.max_result_window 限制 from + size 不超过 10000,调高上限不会消除这些开销。
连续遍历大量结果时,使用 search_after 携带上一页末条结果的排序值;需要固定读取视图时配合 PIT(Point in time)。
6.2 返回匹配片段
高亮功能返回命中文本的片段,供搜索页面展示。这里把默认 HTML 标签替换为方括号,便于在终端查看:
| |
highlight 与 _source 分开返回,不会改写原文。 片段可能只覆盖部分正文,标记边界取决于分析后的词项。
复杂布尔查询的高亮不一定完整反映全部条件,不能按标记数量判断相关性。
6.3 按类别统计
聚合(Aggregation)对查询范围内的文档做统计。 terms 聚合按字段值分组,返回各组的文档数量:
| |
外层
size=0不返回命中文档,聚合仍会执行;聚合中的size=10限制最多返回 10 个类别。
请求没有 query 时统计全部文档,传入查询条件时只统计匹配的文档。
keyword 字段默认建立 doc values,按列组织字段值,适合读取文档的类别并执行排序、聚合。倒排索引则支持按词项查找文档,两者的访问方向不同。
当前索引只有一个主分片、四个类别,
size=10能完整返回这些类别。
多分片下,terms 的计数可能存在误差。 类别较多时,结果来自各分片候选桶的合并,不一定等于精确的全量分组统计。需要结合 doc_count_error_upper_bound、sum_other_doc_count 和业务要求判断。
7. 更新、删除与数据保存
7.1 更新文档
只修改 104 的类别时,update 接收需要更新的字段:
| |
update 合并指定字段后重新索引文档,保留其他字段。 再次提交相同内容时,可能返回 noop,表示无需修改。
整条替换则使用 index,与第 4 节 Bulk 中的 index 操作语义一致:
| |
局部修改使用 update,完整替换使用 index。
如果只把
{"category": "文本处理"}传给index,旧文档中的标题和正文会被删除。
update 默认要求文档已存在;不存在时需要创建,必须明确设置 upsert 行为。
修改正文后,ES 按字段分析器生成新词项,搜索可见性仍受 refresh 控制。
向量字段若由应用的 Embedding 模型生成,应用还需同步更新向量,普通文本分析不会生成这些向量。
7.2 删除文档
删除 106,并通过搜索检查它是否仍然存在:
| |
删除只影响 ES 文档,docs 列表不变。
按本文顺序更新
104、删除106后,重新执行第 4.2 节的批量写入,会恢复原始六篇文档,并把104的类别改回“检索”。
删除索引会移除全部文档和 Mapping。 重复练习不需要在自动初始化代码中加入删除索引。
7.3 关闭、重启与读取已有数据
关闭客户端只释放连接,不会关闭 ES 服务或删除数据:
| |
在第 2 节的 Compose 目录中停止并重新启动服务:
| |
down保留数据:默认不删除命名卷es-data,相同 Compose 项目启动时会挂载原卷。down -v删除数据卷:会删除项目声明的命名卷,示例数据随之丢失。- 项目名影响所用数据卷:Compose 默认以目录名确定项目名,更换目录或项目名可能挂载新的空卷,不代表原卷数据已消失。
服务恢复响应后,读取已有文档:
| |
这次直接读取已有索引,无需重新创建或写入。数据卷不等于备份。 误删或磁盘损坏造成的数据丢失,需要依靠单独保存的快照恢复。
8. 客户端复用
复用客户端,让多个请求共用连接。 配套类集中封装索引初始化、批量写入和文档操作。
配套代码接收官方 Elasticsearch 实例和索引名,保留原生 Query DSL。调用方提供字段定义和查询条件,搜索不会自动创建索引。
下面读取已有的 article_demo_es,不依赖前面会话中的变量:
| |
第 7 节把
104的类别改成了“文本处理”。这里要求类别为“检索”,因此只返回101。
全文搜索依赖词项匹配,向量检索关注语义接近程度。 两类结果需要融合时,候选范围、排序和效果评估应单独设计。
评论
使用 GitHub 登录后参与讨论,评论将公开保存在 GitHub。
站长管理删除会同步到 GitHub,无法撤销。
阅读到这里时加载评论。