🔥 收藏!Python 异步操作 Elasticsearch 完整指南

引言
在现代后端开发中,无论是做全文搜索引擎日志分析系统,还是企业级知识库Elasticsearch (简称 ES) 都是绕不开的神器。

然而,很多同学在 Python 中使用 ES 时,往往只停留在最基础的增删改查,忽略了 ES 强大的多字段高亮搜索相似推荐以及异步高并发能力。

今天,我为大家带来了一份 Python + Elasticsearch 异步操作的完整 Demo。代码基于 asyncio 编写,涵盖了从索引管理、高级搜索到批量操作的所有核心场景。建议先收藏,再慢慢看! 👇


🛠️ 一、 环境与前置准备

在运行代码前,我们需要准备好本地环境。

1. 安装 Python 依赖
我们使用的是官方提供的异步客户端,支持非阻塞 I/O,非常适合高并发场景。

1
pip install elasticsearch

2. 启动本地 ES 服务
你可以使用 Docker 快速拉起一个单节点的 ES 服务(以 ES 8.x 为例):

1
2
3
4
5
docker run -d --name es_demo \
-p 9200:9200 \
-e "discovery.type=single-node" \
-e "xpack.security.enabled=false" \
elasticsearch:8.9.0

(注:如果你的 ES 开启了安全认证,请修改代码中的 ES_USERES_PASSWORD 配置)


🚀 二、 核心功能拆解

这份 Demo 封装了一个 ESDemo 类,我们将它的核心能力分为 6 大模块:

1️⃣ 索引与 Mapping 管理

在 ES 中,Index 相当于关系型数据库的“表”。我们通过 create_index 创建索引,并定义了严谨的 Mapping(数据结构)。
💡 亮点:将需要分词的字段设为 text(如 title, content),将不需要分词仅用于过滤的字段设为 keyword(如 category, tags),这是 ES 性能优化的第一步!

2️⃣ 文档 CRUD 与 Refresh

增删改查是基本功。需要注意的是,ES 写入数据后默认有 1s 的延迟才能被搜索到。在 Demo 中,我们在写入后手动调用了 refresh,确保数据立即可见(适用于对实时性要求高的测试或特定业务场景)。

3️⃣ 高级搜索(核心亮点 🔥)

  • 多字段模糊搜索 (multi_match):支持在标题和内容中同时搜索,并给标题更高的权重(title^2)。开启 fuzziness: AUTO 还能实现拼写容错
  • 高亮显示 (highlight):搜索命中后,自动给关键词加上高亮标签,前端展示体验极佳。
  • 复合条件过滤 (bool filter):结合全文搜索与精确过滤(如:搜索“学习”,且分类必须是“AI”)。
  • 相似文档推荐 (more_like_this):电商“猜你喜欢”、知识库“相关推荐”的底层实现原理,直接根据文档 ID 提取特征词进行匹配。

4️⃣ 批量操作 (Bulk API)

千万不要在循环里单条写入 ES!Demo 中演示了 bulk_indexbulk_delete。将多个操作打包成一个请求发送给 ES,能让写入性能提升数十倍

5️⃣ 聚合分析 (Aggregations)

不需要把数据拉到 Python 里统计!通过 aggs,我们可以直接让 ES 在集群内部完成 Group By 操作(如统计各个分类下的文章数量、各个作者的产出),速度极快且节省带宽。


💻 三、 完整实战源码

以下是完整的 es_demo.py 代码,你可以直接复制运行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
"""
Elasticsearch 常用操作完整 Demo
依赖: pip install elasticsearch
运行: python es_demo.py
"""

import json
import asyncio
from datetime import datetime
from elasticsearch import AsyncElasticsearch
from elasticsearch.exceptions import NotFoundError

# ==================== 配置 ====================
ES_URL = "http://localhost:9200"
ES_USER = "elastic"
ES_PASSWORD = "elastic123" # 根据实际情况修改
DOC_INDEX = "demo_documents"

class ESDemo:
"""Elasticsearch 常用操作演示类"""

def __init__(self):
auth = (ES_USER, ES_PASSWORD) if ES_USER else None
self.client = AsyncElasticsearch(
hosts=[ES_URL],
http_auth=auth,
verify_certs=False,
ssl_show_warn=False,
)
self.doc_index = DOC_INDEX

async def close(self):
await self.client.close()

# ========== 1. 索引管理 ==========
async def create_index(self, index_name: str, mapping: dict = None):
exists = await self.client.indices.exists(index=index_name)
if exists:
print(f"ℹ️ 索引已存在: {index_name}")
return
if mapping:
await self.client.indices.create(index=index_name, body=mapping)
else:
await self.client.indices.create(index=index_name)
print(f"✅ 创建索引: {index_name}")

async def delete_index(self, index_name: str):
await self.client.indices.delete(index=index_name)
print(f"🗑️ 删除索引: {index_name}")

# ========== 2. 文档 CRUD ==========
async def index_document(self, index: str, doc_id, body: dict):
result = await self.client.index(index=index, id=doc_id, body=body)
print(f"📝 索引文档: id={doc_id}, result={result['result']}")
await self.client.indices.refresh(index=index) # 强制刷新
return result

async def get_document(self, index: str, doc_id):
try:
result = await self.client.get(index=index, id=doc_id)
return result["_source"]
except NotFoundError:
return None

async def update_document(self, index: str, doc_id, update_data: dict):
result = await self.client.update(index=index, id=doc_id, body={"doc": update_data})
await self.client.indices.refresh(index=index)
return result

# ========== 3. 高级搜索 ==========
async def multi_match_search(self, index: str, query: str, fields: list = None):
"""多字段全文搜索 + 高亮 + 模糊匹配"""
if fields is None:
fields = ["title^2", "content"] # 标题权重翻倍

body = {
"query": {
"multi_match": {
"query": query,
"fields": fields,
"type": "best_fields",
"fuzziness": "AUTO" # 开启容错
}
},
"highlight": {
"fields": {f: {"fragment_size": 80} for f in fields}
},
"size": 5
}
result = await self.client.search(index=index, body=body)
print(f"\n🔍 搜索: \"{query}\" (共 {result['hits']['total']['value']} 条结果)")
for hit in result["hits"]["hits"]:
print(f" 🌟 高亮: {hit.get('highlight', {})}")
return result

async def similar_search(self, index: str, doc_id, size: int = 3):
"""相似文档推荐 (More Like This)"""
body = {
"query": {
"more_like_this": {
"fields": ["title", "content"],
"like": [{"_index": index, "_id": doc_id}],
"min_term_freq": 1,
"max_query_terms": 12,
}
},
"size": size
}
result = await self.client.search(index=index, body=body)
print(f"\n🔍 相似文档推荐 (id={doc_id}):")
for hit in result["hits"]["hits"]:
print(f" - [{hit['_score']:.2f}] {hit['_source'].get('title', '')}")

# ========== 4. 聚合统计 ==========
async def aggregate_by_field(self, index: str, field: str):
"""按字段聚合统计 (类似 SQL 的 Group By)"""
body = {
"size": 0,
"aggs": {
f"by_{field}": {"terms": {"field": field, "size": 10}}
}
}
result = await self.client.search(index=index, body=body)
buckets = result["aggregations"][f"by_{field}"]["buckets"]
print(f"\n📊 聚合统计: {field}")
for bucket in buckets:
print(f" - {bucket['key']}: {bucket['doc_count']} 条")

# ========== 完整演示流 ==========
async def run_full_demo(self):
print("="*50)
print("🚀 Elasticsearch 异步操作演示开始")
print("="*50)

# 1. 定义 Mapping
doc_mapping = {
"mappings": {
"properties": {
"title": {"type": "text", "analyzer": "standard"},
"content": {"type": "text", "analyzer": "standard"},
"category": {"type": "keyword"},
"author": {"type": "keyword"},
"price": {"type": "float"},
"created_at": {"type": "date"},
}
},
"settings": {"number_of_shards": 1, "number_of_replicas": 0}
}

await self.create_index(self.doc_index, doc_mapping)

# 2. 写入测试数据
docs = [
{"id": 1, "title": "Python 异步编程指南", "content": "asyncio 是 Python 标准库中的异步 I/O 框架。", "category": "编程", "author": "张三", "price": 39.9},
{"id": 2, "title": "Elasticsearch 权威指南", "content": "ES 是一个分布式搜索和分析引擎。", "category": "编程", "author": "李四", "price": 59.9},
{"id": 3, "title": "机器学习入门", "content": "机器学习是人工智能的一个分支。", "category": "AI", "author": "王五", "price": 49.9},
]
for doc in docs:
body = {k: v for k, v in doc.items() if k != "id"}
body["created_at"] = datetime.now().isoformat()
await self.index_document(self.doc_index, doc["id"], body)

# 3. 体验高级搜索
await self.multi_match_search(self.doc_index, "asyncio 框架")
await self.similar_search(self.doc_index, 1) # 找和 Python 异步相似的书

# 4. 聚合统计
await self.aggregate_by_field(self.doc_index, "category")

# 5. 清理战场
await self.delete_index(self.doc_index)
print("\n🎉 ES 演示完成!")

# ==================== 入口 ====================
async def main():
demo = ESDemo()
try:
await demo.run_full_demo()
finally:
await demo.close()

if __name__ == "__main__":
asyncio.run(main())

🎯 四、 总结与避坑指南

通过上面的代码,我们掌握了 ES 的核心玩法。但在实际生产环境中,还有几点需要特别注意:

  1. Mapping 设计先行:千万不要让 ES 自动推断数据类型(Dynamic Mapping),一定要手动定义 Mapping,否则极易出现内存暴涨和查询性能下降。
  2. 批量写入是王道:使用 Bulk API 时,建议每批数据量控制在 5MB - 15MB 之间,或者 500 - 1000 条文档,这是官方推荐的最佳实践。
  3. 分页深度限制:ES 默认限制 max_result_window 为 10000。如果需要深度翻页,请使用 search_afterScroll API
  4. 异步客户端的优势:在 FastAPI / Sanic 等异步 Web 框架中,使用 AsyncElasticsearch 可以避免阻塞主线程,大幅提升 API 的吞吐量。