【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:报表分析【聚合极快、适合统计数据量(亿级)报表】

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

相关推荐
hashiqimiya2 小时前
后端springboot的接收前端发来的数据反序列化原理
java
cat三三2 小时前
java之异常
java·开发语言
浙江第二深情2 小时前
前端性能优化终极指南
java·maven
养乐多07222 小时前
【Java】IO流
java
俊男无期2 小时前
超效率工作法
java·前端·数据库
中国胖子风清扬3 小时前
SpringAI和 Langchain4j等 AI 框架之间的差异和开发经验
java·数据库·人工智能·spring boot·spring cloud·ai·langchain
月明长歌3 小时前
【码道初阶】牛客TSINGK110:二叉树遍历(较难)如何根据“扩展先序遍历”构建二叉树?
java·数据结构·算法
Elastic 中国社区官方博客3 小时前
Elasticsearch:你是说,用于混合搜索(hybrid search)
大数据·数据库·人工智能·elasticsearch·搜索引擎·ai·全文检索
用户2190326527353 小时前
Spring Boot + Redis 注解极简教程:5分钟搞定CRUD操作
java·后端