使用 Elasticsearch 构建搜索引擎

很多人对 Elasticsearch 的第一印象是“搜索神器”,但它本质上是一个基于 Apache Lucene 优化的分布式实时文档存储+分析引擎——存储只是基础,检索才是核心优势。这篇文章会带你快速上手,用 Python 客户端搭建一个基础的中文全文检索系统原型。


1. 先理清核心逻辑:类比relational-database

很多新手容易被 ES 的一堆专有名词绕晕,这里先用大家熟悉的数据库概念做个简单但够用的对应(注意不是严格等价):

relational-database结构Elasticsearch 对应简单说明
数据库(Database)索引(Index)数据的顶层管理容器,必须全小写
表(Table)7.x+ 已完全弃用 Type,现在一个 Index 只存一类文档
行(Row)文档(Document)JSON 格式的核心数据单元,就是一条条记录
列(Column)字段(Field)JSON 中的键值对,支持文本、数值、日期等多种类型
主键(Primary Key)文档ID(Document ID)可以自动生成,也可以由业务方指定(比如订单号、新闻编号)

简单来说,可以把一个 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 个最佳实践

  1. 索引设计要前置规划
    Mapping 一旦创建,除了新增字段,其他类型或分词器的修改都需要重建索引。推荐使用别名(Alias)管理索引:重建时在后台新建索引并切回别名,业务代码无需任何改动。

  2. 性能优化从细节入手

    • 避免单个文档过大(>100 MB),可以考虑拆分为多个小文档;
    • 批量操作时,每个 bulk 请求的大小建议控制在 5–15 MB
    • 多用 filter 而非 must,充分利用 ES 的缓存机制。
  3. 生产环境务必开启安全配置
    本文为了快速测试关闭了 xpack.security,生产环境一定要启用 HTTPS、用户名密码认证和 RBAC 访问控制,避免数据裸奔。


6. 总结与扩展

这篇文章带你走完了 ES 的核心概念、environment-setup、索引设计、文档操作和常见搜索场景。用 Python 几行代码,就能跑起一个基本的中文全文检索原型。实际项目中,还可以结合高亮、聚合分析、分页导航等功能,进一步丰富搜索体验。

如果想深入探索,推荐以下资源:

祝你用 ES 构建出又快又准的搜索引擎!