使用 Elasticsearch 构建搜索引擎
很多人对 Elasticsearch 的第一印象是“搜索神器”,但它本质上是一个基于 Apache Lucene 优化的分布式实时文档存储+分析引擎——存储只是基础,检索才是核心优势。这篇文章会带你快速上手,用 Python 客户端搭建一个基础的中文全文检索系统原型。
1. 先理清核心逻辑:类比relational-database
很多新手容易被 ES 的一堆专有名词绕晕,这里先用大家熟悉的数据库概念做个简单但够用的对应(注意不是严格等价):
简单来说,可以把一个 Index 想象成一张表,里面存着一堆 JSON 格式的文档,每个文档就是一行数据。接下来的操作都会围绕 Index 和 Document 展开。
2. 5 分钟搭好本地测试环境
不用纠结复杂的集群配置,单节点 Docker 容器是新手入门最快的方式。
2.1 拉取并启动 ES
# 拉取 8.12.0 版本(如果版本更新,后面的 IK 分词器也要对应)
docker pull docker.elastic.co/elasticsearch/elasticsearch:8.12.0
# 启动单节点(关闭集群发现、禁用安全配置方便测试,生产环境绝对不能这么做!)
docker run -d \
-p 9200:9200 \
-p 9300:9300 \
-e "discovery.type=single-node" \
-e "xpack.security.enabled=false" \
-e "xpack.security.enrollment.enabled=false" \
docker.elastic.co/elasticsearch/elasticsearch:8.12.0
启动后等待 30 秒左右,访问 http://localhost:9200,如果返回类似 JSON 的集群信息,说明 ES 已经跑起来了。
2.2 安装 Python 客户端
官方推荐的 elasticsearch-py 7.x+ 同时支持 ES 7.x 和 8.x 系列,直接 pip 安装即可:
pip install elasticsearch==8.12.0 # 建议和 ES 版本号一致,避免兼容性问题
3. 准备工作:索引定义(Mapping)
ES 可以根据你插入的第一条文档自动推断字段的类型。但中文全文检索场景最好手动指定 Mapping——例如给文本字段加上中文分词器,给 URL、状态码这种不需要分词的字段设为 keyword 类型,避免被“拆碎”。
这里我们以新闻搜索为场景,先安装 IK 分词器(默认 ES 只会英文分词,中文会被拆成单个汉字):
# 先找到容器 ID:docker ps
docker exec -it <你的容器ID> bin/elasticsearch-plugin install \
https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v8.12.0/elasticsearch-analysis-ik-8.12.0.zip
# 安装后重启容器
docker restart <你的容器ID>
重启后,用 Python 创建索引:
from elasticsearch import Elasticsearch
# 连接本地 ES
es = Elasticsearch("http://localhost:9200")
if not es.ping():
raise ConnectionError("无法连接到 Elasticsearch,请检查容器是否正常运行!")
# 定义索引 Mapping
news_mapping = {
"mappings": {
"properties": {
"title": { # 新闻标题:需要细粒度分词,支持“中国高考”“高考政策”等组合搜索
"type": "text",
"analyzer": "ik_max_word", # 索引时最细粒度分词
"search_analyzer": "ik_smart" # 搜索时粗粒度分词
},
"content": { # 新闻内容:同上
"type": "text",
"analyzer": "ik_max_word",
"search_analyzer": "ik_smart"
},
"url": { # 新闻链接:不需要分词,精准匹配即可
"type": "keyword"
},
"publish_date": { # 发布时间:需要范围过滤
"type": "date"
},
"category": { # 分类:精准匹配
"type": "keyword"
}
}
}
}
# 创建索引,ignore=400 表示如果索引已存在就跳过
response = es.indices.create(index="chinese_news", body=news_mapping, ignore=400)
print("创建索引结果:", response)
ik_max_word 会对文本进行最细粒度拆词(比如“中国学生”会被拆成“中国、国中、学生、国学生、中国学生”),索引内容更多,但有利于召回;ik_smart 则进行粗粒度分词(例如拆成“中国、学生”),搜索时更精准。这种一粗一细的组合,可以兼顾查全率和查准率。
4. 核心功能实战
4.1 文档操作
ES 同时支持单条和批量操作,生产环境优先用 bulk API 批量插入/更新/删除,性能比逐条操作快十几倍甚至几十倍。
单条操作
# 1. 指定 ID 插入(ID 已存在会报错)
doc1 = {
"title": "2024年高考报名时间公布:多省提前启动",
"content": "近日,教育部发布通知,2024年全国普通高等学校招生统一考试报名工作将在部分省份提前启动...",
"url": "https://example.com/news/20240520/1",
"publish_date": "2024-05-20",
"category": "教育"
}
res = es.create(index="chinese_news", id="20240520_1", body=doc1)
print("指定ID插入结果:", res["result"]) # 成功返回 created
# 2. 自动生成 ID 插入(幂等性较差,不建议用于核心数据)
doc2 = doc1.copy()
doc2["url"] = "https://example.com/news/20240520/2"
res = es.index(index="chinese_news", body=doc2)
print("自动ID插入结果:", res["result"], res["_id"])
# 3. 更新文档(部分更新只需传要改的字段)
update_body = {
"doc": {
"category": "高考政策"
}
}
res = es.update(index="chinese_news", id="20240520_1", body=update_body)
print("部分更新结果:", res["result"]) # 成功返回 updated
# 4. 删除文档
res = es.delete(index="chinese_news", id=res["_id"]) # 删除刚才自动生成的那条
print("删除结果:", res["result"])
create 要求指定 ID,并且 ID 不能重复。
index 会覆盖已有文档,个人测试更灵活,但生产环境要小心。
update 支持“部分更新”,只修改指定字段,不会丢掉其他数据。
4.2 搜索功能
搜索是 ES 的灵魂,这里介绍 3 个最常用的场景。
场景1:简单的全文匹配
搜索“2024高考报名”,ES 会返回标题或内容里包含相关关键词的文档,并按相关性分数排序:
simple_query = {
"query": {
"multi_match": { # 多字段匹配
"query": "2024高考报名",
"fields": ["title^3", "content"] # title 的权重是 content 的 3 倍
}
},
"size": 10, # 只返回前 10 条(默认也是 10)
"_source": ["title", "url", "publish_date", "category"] # 只返回需要的字段
}
res = es.search(index="chinese_news", body=simple_query)
print("搜索到的文档数:", res["hits"]["total"]["value"])
for hit in res["hits"]["hits"]:
print(f"标题:{hit['_source']['title']} | 相关性分数:{hit['_score']}")
multi_match 会自动对多个字段做搜索,^3 表示将标题的权重提升到 3 倍,让标题更匹配的文档排名更靠前。
场景2:带条件的布尔查询
布尔查询是 ES 最灵活的查询方式,支持 must(必须满足,参与评分)、must_not(必须不包含,不影响评分)、should(满足则可加分)、filter(必须满足,不参与评分但会缓存,性能最好)。
假设我们要找:2023‑2024 年发布的,分类是‘高考政策’或‘教育’,标题或内容含有‘报名’但不包含‘成人高考’的新闻,并按发布时间倒序排列:
bool_query = {
"query": {
"bool": {
"must": [
{"multi_match": {"query": "报名", "fields": ["title^3", "content"]}}
],
"must_not": [
{"match": {"title": "成人高考"}}
],
"should": [
{"term": {"category": "高考政策"}}, # term 用于 keyword 类型精准匹配
{"term": {"category": "教育"}}
],
"filter": [
{"range": {"publish_date": {"gte": "2023-01-01", "lte": "2024-12-31"}}}
],
"minimum_should_match": 1 # should 里至少满足 1 个
}
},
"sort": [
{"publish_date": {"order": "desc"}}, # 先按发布时间倒序
{"_score": {"order": "desc"}} # 再按相关性分数倒序
],
"size": 10,
"_source": ["title", "url", "publish_date", "category"]
}
res = es.search(index="chinese_news", body=bool_query)
print("过滤后的搜索结果:")
for hit in res["hits"]["hits"]:
print(f"{hit['_source']['publish_date']} | {hit['_source']['category']} | {hit['_source']['title']}")
filter 不会影响文档的得分,还能被 ES 自动缓存,适合用于时间范围、固定状态等条件过滤。
5. 新手必看的 3 个最佳实践
-
索引设计要前置规划
Mapping 一旦创建,除了新增字段,其他类型或分词器的修改都需要重建索引。推荐使用别名(Alias)管理索引:重建时在后台新建索引并切回别名,业务代码无需任何改动。
-
性能优化从细节入手
- 避免单个文档过大(>100 MB),可以考虑拆分为多个小文档;
- 批量操作时,每个
bulk 请求的大小建议控制在 5–15 MB;
- 多用
filter 而非 must,充分利用 ES 的缓存机制。
-
生产环境务必开启安全配置
本文为了快速测试关闭了 xpack.security,生产环境一定要启用 HTTPS、用户名密码认证和 RBAC 访问控制,避免数据裸奔。
6. 总结与扩展
这篇文章带你走完了 ES 的核心概念、environment-setup、索引设计、文档操作和常见搜索场景。用 Python 几行代码,就能跑起一个基本的中文全文检索原型。实际项目中,还可以结合高亮、聚合分析、分页导航等功能,进一步丰富搜索体验。
如果想深入探索,推荐以下资源:
祝你用 ES 构建出又快又准的搜索引擎!