ES批量写入数据:从兼容旧版到适配ES8的最佳实践

在处理海量数据时,单条插入Elasticsearch(ES)就像用勺子舀水灌入大海,效率极低。批量写入(Bulk API)才是我们需要的"消防水管"。

最近在做数据迁移,需要将现有数据从ES6.7同步到ES8.13.4,并将分词器从Jieba切换到IK。在这个过程中,踩了一些关于版本兼容性的坑。今天就来分享一下ES8下如何高效进行批量写入 ,以及新版Python客户端的代码适配技巧

一、 为什么必须用批量写入?

简单算笔账:

  • 单条写入 :每次写入都需要建立TCP连接 -> 认证 -> 解析请求 -> 写入磁盘 -> 返回响应。假设耗时10ms,写入10万条数据需要 1000秒(约16分钟)
  • 批量写入 :将多条数据打包成一个请求,减少了网络IO和握手开销。同样的数据可能只需 10-30秒

性能提升不仅是倍数级的,还能显著降低ES集群的CPU压力。

二、 ES8.x 的重大变化:告别 _type

如果你也是从ES6.7升级上来的,最大的痛点在于:Mapping Types 被彻底移除了

特性 ES 6.7 及之前 ES 7.x / 8.x
URL结构 /index/type/_doc/id /index/_doc/id
Mapping 支持多种Type (如 user, blog) 整个索引只有一种隐含Type (_doc)
Bulk Action 需指定 _type 禁止指定 _type

这意味着,旧代码里的 "_type": "doc_type" 必须彻底删除,否则ES8会直接报错 MapperParsingException

三、 实战代码:适配ES8 + IK分词 + 批量写入

下面是一套完整的生产级代码示例,包含索引创建(IK分词版)批量写入工具类

1. 索引创建:切换为 IK 分词

我们要创建一个支持IK分词、同义词和停用词的索引。注意 settingsmappings 的结构变化。

python 复制代码
from elasticsearch import Elasticsearch

# 连接ES8
es = Elasticsearch(
    hosts=["https://your-es-host:9200"],
    basic_auth=("user", "password"),
    verify_certs=False # 如果是自签名证书
)

index_name = "my_blog_index"

# 删除旧索引(如果存在)
if es.indices.exists(index=index_name):
    es.indices.delete(index=index_name)

# 定义IK分词配置
body = {
    "settings": {
        "analysis": {
            "filter": {
                "ik_stop_filter": {
                    "type": "stop",
                    "stopwords_path": "analysis/ik/stopwords.txt" # 需上传到ESconfig目录
                },
                "ik_synonym_filter": {
                    "type": "synonym",
                    "synonyms_path": "analysis/ik/synonyms.txt" # 需上传到ESconfig目录
                }
            },
            "analyzer": {
                "ik_index_analyzer": {
                    "tokenizer": "ik_max_word",
                    "filter": ["ik_stop_filter", "ik_synonym_filter", "lowercase"]
                },
                "ik_search_analyzer": {
                    "tokenizer": "ik_smart",
                    "filter": ["ik_stop_filter", "ik_synonym_filter", "lowercase"]
                }
            }
        }
    },
    "mappings": {
        "properties": { # 注意:这里直接是 properties,没有 doc_type
            "title": {
                "type": "text",
                "analyzer": "ik_index_analyzer",
                "search_analyzer": "ik_search_analyzer"
            },
            "content": {
                "type": "text",
                "analyzer": "ik_index_analyzer",
                "search_analyzer": "ik_search_analyzer"
            },
            "blog_id": {
                "type": "keyword"
            }
            # ... 其他字段
        }
    }
}

es.indices.create(index=index_name, body=body)
2. 批量写入工具类(重点)

这是核心部分。我们封装一个类,自动处理ID生成和错误重试。

python 复制代码
from elasticsearch import helpers
from elasticsearch.exceptions import BulkIndexError

class ESBulkWriter:
    def __init__(self, es_client):
        self.es = es_client

    def bulk_insert(self, index_name, data_list, id_field=None):
        """
        ES8 兼容的批量写入方法
        
        :param index_name: 索引名
        :param data_list: 数据列表 [{}, {}, ...]
        :param id_field: 指定作为文档ID的字段名(如 "blog_id"),若为None则由ES自动生成
        """
        actions = []
        
        for data in data_list:
            # 1. 构建基础Action
            action = {
                "_index": index_name,
                "_source": data
                # "_type": "_doc"  <-- ES8 Python客户端会自动处理,无需显式写出!
            }
            
            # 2. 处理ID:如果指定了id_field且数据中存在该字段,则使用它作为文档ID
            if id_field and id_field in data:
                action["_id"] = str(data[id_field]) # ES8建议ID转为字符串
            
            actions.append(action)

        # 3. 执行批量写入(带基础错误统计)
        try:
            success_count, failed_count = helpers.bulk(
                self.es, 
                actions, 
                stats_only=True, # 只返回统计信息,不抛异常
                raise_on_error=False, # 遇到错误继续执行
                request_timeout=60
            )
            print(f"写入完成: 成功 {success_count} 条, 失败 {failed_count} 条")
            
            # 如果需要详细错误信息,可以设置 raise_on_error=True 或遍历失败项
            
        except BulkIndexError as e:
            print(f"批量写入发生严重错误: {e}")
            # 这里可以添加逻辑:记录失败的actions到日志文件以便重试
        except Exception as e:
            print(f"发生未知异常: {e}")

# 使用示例
writer = ESBulkWriter(es)
my_data = [
    {"blog_id": "1001", "title": "Elasticsearch 8.0 发布", "content": "..."},
    {"blog_id": "1002", "title": "IK分词器使用指南", "content": "..."}
]

# 使用 blog_id 作为文档ID,避免重复
writer.bulk_insert("my_blog_index", my_data, id_field="blog_id")

四、 进阶技巧:提升写入稳定性

如果数据量达到百万级,上面的基础版可能还不够。建议增加以下机制:

  1. 分块批量(Chunking):不要一次性把100万条数据丢进内存。每1000-5000条切分为一个Chunk进行写入。
  2. 重试机制 :遇到ES集群繁忙(如 429 Too Many Requests 或队列满)时,使用指数退避算法进行重试。
  3. 刷新间隔 :在大量导入期间,临时将 index.refresh_interval 设置为 -1(禁止刷新)或 30s,导入完成后再改回 5s,能大幅减少段合并的压力。

总结

从ES6迁移到ES8,批量写入的代码修改主要就是**"做减法"**:

  1. 减掉 _type 字段。
  2. 减掉复杂的异常捕获(helpers.bulk 已经很强大)。
  3. 加上对 id_field 的灵活处理。

掌握了这套模板,无论是做数据迁移还是日常业务开发,都能让你的数据写入效率飞起来!


如果觉得有帮助,欢迎点赞收藏!关于IK分词的配置文件细节,我会在下一篇文章详细讲解。

相关推荐
成长之路5142 小时前
【工具变量】国地税合并DID数据(2009-2023年)
大数据
痕忆丶2 小时前
Git_Rebase_Conflict_Resolution
大数据·git
白帽子黑客罗哥2 小时前
举例说明在真实业务场景中,如何平衡安全防御方案与系统性能、用户体验的关系?
大数据·安全·ux
AC赳赳老秦2 小时前
CSV大文件处理全流程:数据清洗、去重与格式标准化深度实践
大数据·开发语言·人工智能·python·算法·机器学习·deepseek
珑哥说自养号采购2 小时前
Temu、Shein、速卖通,全托半托管模式下怎样通过测评补单破解店铺流量困局?
大数据
雨大王5122 小时前
工业AI驱动汽车供应链:效率提升的秘密武器
大数据·人工智能
kdniao12 小时前
问答FAQ|快递鸟对接系统/小程序常见问题解答产品篇(二)
大数据·小程序
武子康2 小时前
Java-215 RocketMQ 消费模式:Push vs Pull 的本质、长轮询机制与 Offset/积压调优要
java·大数据·分布式·消息队列·rocketmq·java-rocketmq·mq
zhang_xiaoyu582 小时前
安徽省宣城市国控集团党委书记、董事长钱邦青一行到访国联股份卫多多
大数据·人工智能