【Java + Elasticsearch全量 & 增量同步实战】

Java + Elasticsearch 全量 & 增量同步实战:打造高性能合同搜索系统

在企业合同管理系统中,我们常常遇到以下挑战:

  • 合同量大,文本内容多,传统数据库查询慢

  • 搜索需求多样:全文搜索、按签署人筛选、分页排序

  • 历史合同也要可搜索,不仅仅是新建合同

  • 统计报表需求:合同签署量、签署人分析等

本文将分享如何使用 Elasticsearch + MySQL + ClickHouse 构建一个高性能合同搜索系统,并提供完整 Java 示例。


一、系统架构概览

合同系统采用"三角架构":

复制代码
                ┌───────────────────────┐
                │       前端 / API       │
                │  (创建 / 修改 / 查询) │
                └─────────┬─────────────┘
                          │
                          ▼
                ┌───────────────────────┐
                │       MySQL 数据库     │
                │  (权威业务数据源)    │
                └─────────┬─────────────┘
                          │
           ┌──────────────┴───────────────┐
           │                              │
   【历史数据全量初始化】            【增量同步 / 实时更新】
           │                              │
           ▼                              ▼
分页 / 批量读取历史合同         新建合同 / 修改合同 / 删除合同
           │                              │
           ▼                              ▼
     转换为 ContractDoc                转换为 ContractDoc
           │                              │
           ▼                              ▼
        ES Bulk API                    ES Index / Update / Delete API
           │                              │
           └───────────┬──────────────────┘
                       ▼
                ┌───────────────┐
                │ Elasticsearch │
                │   contract    │
                │     index     │
                └───────────────┘
                       │
                       ▼
                ┌───────────────┐
                │   查询接口    │
                │(合同列表 / 搜索)│
                └───────────────┘

核心说明

  • MySQL:权威数据源,存储所有合同业务信息

  • ES:用于搜索,支持全文搜索、筛选和排序

  • ClickHouse:用于统计报表,处理大规模合同分析


二、为什么使用宽表

1. 什么是宽表?

宽表 = 把多张业务表的数据提前合并到一张字段很多的表里,用"空间换时间",减少查询时的 join。

传统 MySQL 查询可能涉及多个 join,性能差:

复制代码
SELECT c.*, u.name, e.enterprise_name
FROM contract c
JOIN user u ON c.user_id = u.id
JOIN enterprise e ON c.enterprise_id = e.id
WHERE c.status = 'SIGNED';

宽表设计后,所有信息在一条记录中:

复制代码
{
  "contractId": 10001,
  "contractTitle": "劳动合同",
  "contractStatus": "SIGNED",
  "signTime": "2025-12-01 10:30:00",
  "initiatorId": 2001,
  "initiatorName": "张三",
  "initiatorPhone": "138****",
  "enterpriseId": 3001,
  "enterpriseName": "天津数字认证有限公司",
  "fileHash": "xxxx",
  "signType": "SILENT",
  "source": "OPEN_API"
}
  • 查询无需 join,ES 或 ClickHouse 查询极快

  • 冗余换来性能,是搜索系统的设计常态


三、ES 全量初始化历史数据

1. Java 代码示例(全量导入)

复制代码
@Service
public class ContractEsService {

    @Autowired
    private ContractMapper contractMapper;

    @Autowired
    private RestHighLevelClient esClient;

    /**
     * 全量初始化合同数据到 Elasticsearch
     */
    public void initHistoricalContracts() throws IOException {
        int pageSize = 500;
        int page = 0;

        while (true) {
            List<Contract> contracts = contractMapper.selectHistorical(page * pageSize, pageSize);
            if (contracts.isEmpty()) break;

            BulkRequest bulkRequest = new BulkRequest();
            for (Contract contract : contracts) {
                ContractDoc doc = toContractDoc(contract);
                bulkRequest.add(new IndexRequest("contract_index")
                        .id(String.valueOf(doc.getContractId()))
                        .source(doc.toMap()));
            }
            esClient.bulk(bulkRequest, RequestOptions.DEFAULT);
            page++;
        }
    }

    private ContractDoc toContractDoc(Contract contract) {
        ContractDoc doc = new ContractDoc();
        doc.setContractId(contract.getId());
        doc.setContractTitle(contract.getTitle());
        doc.setContractStatus(contract.getStatus());
        doc.setSignTime(contract.getSignTime());
        doc.setInitiatorId(contract.getUserId());
        doc.setInitiatorName(contract.getUserName());
        doc.setInitiatorPhone(contract.getUserPhone());
        doc.setEnterpriseId(contract.getEnterpriseId());
        doc.setEnterpriseName(contract.getEnterpriseName());
        doc.setFileHash(contract.getFileHash());
        doc.setSignType(contract.getSignType());
        doc.setSource(contract.getSource());
        return doc;
    }
}

说明

  • 分批读取,避免内存爆炸

  • BulkRequest 提高写入性能

  • ContractDoc 为宽表结构,支持全文搜索


四、增量同步

1. 新建合同

复制代码
public void saveContract(Contract contract) throws IOException {
    contractMapper.insert(contract); // 写 MySQL

    ContractDoc doc = toContractDoc(contract);
    esClient.index(new IndexRequest("contract_index")
            .id(String.valueOf(doc.getContractId()))
            .source(doc.toMap()), RequestOptions.DEFAULT); // 写 ES
}

2. 更新合同

复制代码
public void updateContract(Contract contract) throws IOException {
    contractMapper.update(contract);

    ContractDoc doc = toContractDoc(contract);
    esClient.update(new UpdateRequest("contract_index", String.valueOf(doc.getContractId()))
            .doc(doc.toMap()), RequestOptions.DEFAULT);
}

3. 删除合同

复制代码
public void deleteContract(Long contractId) throws IOException {
    contractMapper.delete(contractId);
    esClient.delete(new DeleteRequest("contract_index", String.valueOf(contractId)), RequestOptions.DEFAULT);
}

五、查询示例

复制代码
public List<ContractDoc> searchContracts(String keyword, String status) throws IOException {
    SearchRequest searchRequest = new SearchRequest("contract_index");
    SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();

    BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
    if (keyword != null) {
        boolQuery.must(QueryBuilders.multiMatchQuery(keyword, "contractTitle", "enterpriseName"));
    }
    if (status != null) {
        boolQuery.filter(QueryBuilders.termQuery("contractStatus", status));
    }
    sourceBuilder.query(boolQuery).from(0).size(20);
    searchRequest.source(sourceBuilder);

    SearchResponse response = esClient.search(searchRequest, RequestOptions.DEFAULT);
    List<ContractDoc> results = new ArrayList<>();
    for (SearchHit hit : response.getHits()) {
        results.add(ContractDoc.fromMap(hit.getSourceAsMap()));
    }
    return results;
}
  • 支持全文搜索和条件过滤

  • 支持分页

  • 支持宽表字段查询(无需 join)


六、增量 & 历史数据同步策略总结

数据类型 处理方式
历史合同 全量初始化 → 批量写入 ES
新建合同 实时写入 ES
更新合同 实时更新 ES
删除合同 实时删除 ES

建议

  • 增量同步可结合 消息队列 + CDC,保证最终一致性

  • 历史数据初始化建议在低峰时执行,分批写入


七、总结

  1. 宽表 + ES:提高合同搜索性能,避免 join

  2. 全量初始化历史数据:ES 支持既往合同搜索

  3. 增量同步:保证新数据实时可查

  4. 三角架构(MySQL + ES + ClickHouse):各司其职

    • MySQL:权威数据

    • ES:快速搜索

    • ClickHouse:报表分析【聚合极快、适合统计数据量(亿级)报表】

通过这套设计,合同系统既能秒级响应搜索,又能提供高效报表分析,满足大规模企业业务需求。

相关推荐
之歆4 分钟前
Spring AI入门到实战到原理源码-多模型协作智能客服系统
java·人工智能·spring
yyy(十一月限定版)17 分钟前
c++(3)类和对象(中)
java·开发语言·c++
IT 行者31 分钟前
Spring Security 7 OAuth2 Token 格式选择浅析
java·后端·spring
shughui34 分钟前
最新版IntelliJ IDEA下载+安装+汉化(详细图文)
java·ide·intellij-idea
小罗和阿泽37 分钟前
java 【多线程基础 三】
java·开发语言
想你依然心痛39 分钟前
从x86到ARM的HPC之旅:鲲鹏开发工具链(编译器+数学库+MPI)上手与实战
java·开发语言·arm开发·鲲鹏·昇腾
我的golang之路果然有问题42 分钟前
积累的 java 找工作资源
java·笔记
编程大师哥1 小时前
Java 常见异常(按「运行时 / 编译时」分类)
java·开发语言
SnrtIevg1 小时前
Vavr 用户指南
java·后端