在当今的 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.Driver
url: jdbc:mysql://localhost:3306/address_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
username: root
password: root
# Elasticsearch配置
elasticsearch:
uris: http://localhost:9200
username: elastic
password: elastic
# MyBatis-Plus配置
mybatis-plus:
mapper-locations: classpath*:mapper/**/*.xml
type-aliases-package: com.address.search.entity
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
# 日志配置
logging:
level:
com.address.search: debug
org.elasticsearch.client: warn
# 服务器配置
server:
port: 8080
# Swagger配置
springdoc:
api-docs:
path: /api-docs
swagger-ui:
path: /swagger-ui.html
operationsSorter: 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='地址别名表';
注意:
函数需要 MySQL 安装 pinyin 插件,如mysql-pinyin,用于生成地址的拼音,方便后续拼音检索。
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


