从卡顿到秒查:Java 项目引入 Elasticsearch 实现亿级地址数据的复杂查询实战
在当今的 Java 应用中,地址查询功能几乎无处不在 —— 电商平台的收货地址检索、外卖 App 的定位服务、物流系统的路径规划,都离不开高效的地址处理能力。然而,当面对 "上海市浦东新区张江高科技园区博云路 2 号附近 3 公里内的咖啡馆" 这类复杂查询时,传统关系型数据库往往力不从心,查询延迟甚至能达到秒级,严重影响用户体验。
本文将带你从零开始,一步步在 Java 项目中集成 Elasticsearch (以下简称 ES),构建一个能处理亿级地址数据、支持复杂空间查询的高性能系统。我们不仅会讲解技术实现,更会深入底层原理,让你明白 "为什么这么做",真正做到知其然更知其所以然。
一、为什么传统数据库搞不定复杂地址查询?
在引入 ES 之前,我们先来搞清楚一个核心问题:为什么 MySQL 这类传统数据库在复杂地址查询场景下表现拉胯?
1.1 地址数据的特殊性
地址数据不同于普通业务数据,它具有以下特点:
- 层级性:国家→省→市→区→街道→门牌号,形成天然的层级结构
- 模糊性:"张江科技园" 和 "张江高科技园区" 指的可能是同一区域
- 空间性:地址本质上是地球表面的一个点,具有经纬度坐标
- 多样性:包含拼音、简称、别名等多种表达方式
这些特性使得地址查询需求异常复杂,远超简单的 CRUD 操作。
1.2 传统数据库的局限性
流程图展示传统数据库处理地址查询的困境:

具体来说,传统数据库存在以下瓶颈:
- 模糊查询效率低下:使用
LIKE '%关键词%'会导致全表扫描,无法利用索引 - 多条件组合查询复杂:地址的多维度查询需要大量 JOIN 操作,性能随数据量增长急剧下降
- 缺乏空间索引支持:难以高效实现 "附近 X 公里" 这类空间查询
- 无法处理语义相似性:无法识别 "张江科技园" 和 "张江高科技园区" 的关联
1.3 Elasticsearch 的优势
相比之下,ES 在地址查询场景中展现出显著优势:
- 倒排索引:专为全文检索设计,模糊查询性能远超传统数据库
- 丰富的字段类型:支持 geo_point 等空间类型,原生支持地理位置计算
- 复杂查询 DSL:灵活组合多种查询条件,轻松实现多维度过滤
- 分布式架构:天然支持水平扩展,轻松应对亿级数据量
- 聚合分析:强大的聚合功能支持地址数据的统计分析需求
通过引入 ES,我们可以将复杂地址查询的响应时间从秒级降至毫秒级,同时支持更丰富的查询场景。
二、项目环境搭建与依赖配置
工欲善其事,必先利其器。我们先搭建基础项目环境,选择当前最新的稳定版本组件。
2.1 技术栈选型
| 组件 | 版本 | 说明 |
|---|---|---|
| JDK | 17 | 长期支持版本,性能优异 |
| Spring Boot | 3.2.0 | 简化 Java 开发的框架 |
| Elasticsearch | 8.11.0 | 搜索引擎核心 |
| Spring Data Elasticsearch | 5.2.0 | Spring 生态的 ES 集成方案 |
| MyBatis-Plus | 3.5.5 | 增强版 MyBatis,简化 CRUD |
| MySQL | 8.0.35 | 存储基础业务数据 |
| Lombok | 1.18.30 | 简化 Java 代码 |
| Fastjson2 | 2.0.32 | JSON 处理工具 |
| Swagger3 | 2.2.0 | API 文档生成工具 |
2.2 Maven 依赖配置
创建一个 Spring Boot 项目,在pom.xml中添加以下核心依赖:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>3.2.0</version><relativePath/></parent><groupId>com.address</groupId><artifactId>address-search</artifactId><version>0.0.1-SNAPSHOT</version><name>address-search</name><description>Address Search with Elasticsearch</description><properties><java.version>17</java.version><elasticsearch.version>8.11.0</elasticsearch.version></properties><dependencies><!-- Spring Boot核心 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><!-- Elasticsearch --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-elasticsearch</artifactId></dependency><!-- MyBatis-Plus --><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.5.5</version></dependency><!-- MySQL驱动 --><dependency><groupId>com.mysql</groupId><artifactId>mysql-connector-j</artifactId><scope>runtime</scope></dependency><!-- Lombok --><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><version>1.18.30</version><scope>provided</scope></dependency><!-- Fastjson2 --><dependency><groupId>com.alibaba.fastjson2</groupId><artifactId>fastjson2</artifactId><version>2.0.32</version></dependency><!-- Swagger3 --><dependency><groupId>org.springdoc</groupId><artifactId>springdoc-openapi-starter-webmvc-ui</artifactId><version>2.2.0</version></dependency><!-- 谷歌集合工具 --><dependency><groupId>com.google.guava</groupId><artifactId>guava</artifactId><version>32.1.3-jre</version></dependency><!-- 测试 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency></dependencies><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId><configuration><excludes><exclude><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId></exclude></excludes></configuration></plugin></plugins></build>
</project>
2.3 配置文件
创建application.yml配置文件,配置各组件连接信息:
spring:application:name: address-search# 数据库配置datasource:driver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://localhost:3306/address_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghaiusername: rootpassword: root# Elasticsearch配置elasticsearch:uris: http://localhost:9200username: elasticpassword: elastic# MyBatis-Plus配置
mybatis-plus:mapper-locations: classpath*:mapper/**/*.xmltype-aliases-package: com.address.search.entityconfiguration:map-underscore-to-camel-case: truelog-impl: org.apache.ibatis.logging.stdout.StdOutImpl# 日志配置
logging:level:com.address.search: debugorg.elasticsearch.client: warn# 服务器配置
server:port: 8080# Swagger配置
springdoc:api-docs:path: /api-docsswagger-ui:path: /swagger-ui.htmloperationsSorter: method
三、地址数据模型设计
设计合理的数据模型是实现高效地址查询的基础。我们需要同时考虑 MySQL 中的存储模型和 ES 中的索引模型。
3.1 MySQL 表设计
首先设计 MySQL 中的地址表,存储基础地址数据:
-- 地址表
CREATE TABLE `address` (`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',`country` varchar(50) NOT NULL COMMENT '国家',`province` varchar(50) NOT NULL COMMENT '省份',`city` varchar(50) NOT NULL COMMENT '城市',`district` varchar(50) DEFAULT NULL COMMENT '区/县',`town` varchar(50) DEFAULT NULL COMMENT '乡镇/街道',`detail` varchar(200) NOT NULL COMMENT '详细地址',`zip_code` varchar(20) DEFAULT NULL COMMENT '邮政编码',`longitude` decimal(10,6) NOT NULL COMMENT '经度',`latitude` decimal(10,6) NOT NULL COMMENT '纬度',`name` varchar(100) DEFAULT NULL COMMENT '地址名称(如大厦名、小区名)',`pinyin` varchar(200) GENERATED ALWAYS AS (concat(pinyin(country),'',pinyin(province),'',pinyin(city),'',if(district is null,'',pinyin(district)),'',if(town is null,'',pinyin(town)),'',pinyin(detail))) STORED COMMENT '地址拼音',`created_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',`updated_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',`is_deleted` tinyint NOT NULL DEFAULT '0' COMMENT '是否删除(0-未删除,1-已删除)',PRIMARY KEY (`id`),KEY `idx_region` (`country`,`province`,`city`,`district`) COMMENT '地区索引'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='地址信息表';-- 地址别名表
CREATE TABLE `address_alias` (`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',`address_id` bigint NOT NULL COMMENT '地址ID',`alias` varchar(100) NOT NULL COMMENT '别名',`alias_pinyin` varchar(200) GENERATED ALWAYS AS (pinyin(alias)) STORED COMMENT '别名拼音',`created_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',PRIMARY KEY (`id`),KEY `idx_address_id` (`address_id`) COMMENT '地址ID索引',KEY `idx_alias` (`alias`) COMMENT '别名索引'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='地址别名表';
注意:
pinyin()函数需要 MySQL 安装 pinyin 插件,如mysql-pinyin,用于生成地址的拼音,方便后续拼音检索。
3.2 Elasticsearch 索引设计
ES 的索引设计直接影响查询性能和功能支持,需要精心设计。我们将创建一个address索引,包含以下字段:
package com.address.search.entity.es;import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson2.JSONObject;
import com.address.search.entity.Address;
import com.address.search.entity.AddressAlias;
import lombok.Data;
import org.springframework.data.elasticsearch.annotations.Document;
import org.springframework.data.elasticsearch.annotations.Field;
import org.springframework.data.elasticsearch.annotations.FieldType;
import org.springframework.data.elasticsearch.annotations.GeoPointField;
import org.springframework.data.elasticsearch.core.geo.GeoPoint;import java.util.List;
import java.util.stream.Collectors;/*** 地址ES文档实体** @author ken*/
@Data
@Document(indexName = "address", createIndex = false)
public 