从卡顿到秒查:Java 项目引入 Elasticsearch 实现亿级地址数据的复杂查询实战

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

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

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

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

1.1 地址数据的特殊性

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

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

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

1.2 传统数据库的局限性

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

从卡顿到秒查:Java 项目引入 Elasticsearch 实现亿级地址数据的复杂查询实战

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

模糊查询效率低下:使用
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='地址别名表';

注意:
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 
© 版权声明

相关文章

暂无评论

您必须登录才能参与评论!
立即登录
none
暂无评论...