从卡顿到秒查:Java 工程引入 Elasticsearch 搭建亿级地址数据的复杂查询实战

在当今的 Java 应用中,地址查询功能几乎无处不在 —— 电商平台的收货地址检索、外卖 App 的定位服务、物流系统的路径规划,都离不开高效的地址处理能力。然而,当面对 "上海市浦东新区张江高科技园区博云路 2 号附近 3 公里内的咖啡馆" 这类复杂查询时,传统关系型数据库往往力不从心,查询延迟甚至能达到秒级,严重影响用户体验。

本文将带你从零开始,一步步在 Java 项目中集成 Elasticsearch (以下简称 ES),构建一个能处理亿级地址数据、支持复杂空间查询的高性能系统。我们不仅会讲解技术实现,更会深入底层原理,让你明白 "为什么这么做",真正做到知其然更知其所以然。

一、为什么传统数据库搞不定复杂地址查询?

在引入 ES 之前,我们先来搞清楚一个核心问题:为什么 MySQL 这类传统数据库在复杂地址查询场景下表现拉胯?

1.1 地址数据的特殊性

地址数据不同于普通业务数据,它具有以下特点:

  • 层级性:国家→省→市→区→街道→门牌号,形成天然的层级结构
  • 模糊性:"张江科技园" 和 "张江高科技园区" 指的可能是同一区域
  • 空间性:地址本质上是地球表面的一个点,具有经纬度坐标
  • 多样性:包含拼音、简称、别名等多种表达方式

这些特性使得地址查询需求异常复杂,远超简单的 CRUD 操作。

1.2 传统数据库的局限性

流程图展示传统数据库处理地址查询的困境:

具体来说,传统数据库存在以下瓶颈:

  1. 模糊查询效率低下:使用LIKE '%关键词%'会导致全表扫描,无法利用索引
  2. 多条件组合查询复杂:地址的多维度查询需要大量 JOIN 操作,性能随数据量增长急剧下降
  3. 缺乏空间索引支持:难以高效实现 "附近 X 公里" 这类空间查询
  4. 无法处理语义相似性:无法识别 "张江科技园" 和 "张江高科技园区" 的关联

1.3 Elasticsearch 的优势

相比之下,ES 在地址查询场景中展现出显著优势:

  • 倒排索引:专为全文检索设计,模糊查询性能远超传统数据库
  • 丰富的字段类型:支持 geo_point 等空间类型,原生支持地理位置计算
  • 复杂查询 DSL:灵活组合多种查询条件,轻松实现多维度过滤
  • 分布式架构:天然支持水平扩展,轻松应对亿级数据量
  • 聚合分析:强大的聚合功能支持地址数据的统计分析需求

通过引入 ES,我们可以将复杂地址查询的响应时间从秒级降至毫秒级,同时支持更丰富的查询场景。

二、项目环境搭建与依赖配置

工欲善其事,必先利其器。我们先搭建基础项目环境,选择当前最新的稳定版本组件。

2.1 技术栈选型

组件版本说明
JDK17长期支持版本,性能优异
Spring Boot3.2.0简化 Java 开发的框架
Elasticsearch8.11.0搜索引擎核心
Spring Data Elasticsearch5.2.0Spring 生态的 ES 集成方案
MyBatis-Plus3.5.5增强版 MyBatis,简化 CRUD
MySQL8.0.35存储基础业务数据
Lombok1.18.30简化 Java 代码
Fastjson22.0.32JSON 处理工具
Swagger32.2.0API 文档生成工具

2.2 Maven 依赖配置

创建一个 Spring Boot 项目,在pom.xml中添加以下核心依赖:



    4.0.0
    
        org.springframework.boot
        spring-boot-starter-parent
        3.2.0
        
    
    com.address
    address-search
    0.0.1-SNAPSHOT
    address-search
    Address Search with Elasticsearch
    
        17
        8.11.0
    
    
        
        
            org.springframework.boot
            spring-boot-starter-web
        
        
        
            org.springframework.boot
            spring-boot-starter-data-elasticsearch
        
        
        
            com.baomidou
            mybatis-plus-boot-starter
            3.5.5
        
        
        
            com.mysql
            mysql-connector-j
            runtime
        
        
        
            org.projectlombok
            lombok
            1.18.30
            provided
        
        
        
            com.alibaba.fastjson2
            fastjson2
            2.0.32
        
        
        
            org.springdoc
            springdoc-openapi-starter-webmvc-ui
            2.2.0
        
        
        
            com.google.guava
            guava
            32.1.3-jre
        
        
        
            org.springframework.boot
            spring-boot-starter-test
            test
        
    
    
        
            
                org.springframework.boot
                spring-boot-maven-plugin
                
                    
                        
                            org.projectlombok
                            lombok
                        
                    
                
            
        
    

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='地址别名表';

注意: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
posted on 2025-12-05 18:17  ljbguanli  阅读(0)  评论(0)    收藏  举报