Elasticsearch

Elasticsearch vs MySQL 模糊匹配性能对比

Elasticsearch 响应延迟:

多个实测项目在重负载或大规模索引 (百万量级文档)下能稳定返回 < 100 ms 级别结果。

MySQL 模糊 查询表现:

  • 小数据量(< 10 万):MySQL LIKE '%关键字%' 或内置全文索引,与 ES 差异不大。

  • 大数据量(> 百万):MySQL 往往要扫描大量行或计算全文向量,响应会从数百毫秒飙升到秒级,且无法并行拆分。

性能差距数量 级:

业内高负载场景中,ES 对全文/模糊搜索的优化可带来10 × 以上 的速度提升------MySQL 需要线上查询所有匹配,ES 只处理 Top N 并行执行

一、Elasticsearch 是什么

1.1 Elasticsearch 是什么?

Elasticsearch(简称 ES)是一个开源、分布式的搜索引擎,本质上是基于 Apache Lucene 构建而成的一个"搜索数据库"。

🔎 你可以把 Elasticsearch 理解为:

📖 一个能存储 JSON 文档,并能超快搜索、模糊匹配、分词、高亮、聚合分析的 NoSQL 数据库!


1.2 Elasticsearch 的典型应用场景

场景 示例描述
商品搜索引擎 淘宝商品关键词模糊搜索、销量排序、价格区间筛选等
日志搜索 ELK 堆栈:Filebeat → Logstash → Elasticsearch + Kibana
知识问答系统 输入"如何使用 Spring Boot",模糊匹配最相关的回答内容
新闻推荐 根据关键词、标签、点击热度推荐相似内容
分析报表系统 动态聚合统计、TopN 排行、趋势图等

1.3 ES 和 MySQL 有什么区别?

特性 MySQL Elasticsearch
存储模型 行(Row) 文档(Document,JSON 格式)
检索方式 精确查找(=)为主 全文检索 + 模糊匹配 + 分词 + 相关性计算
数据结构要求 需要预先定义字段、类型 支持动态映射,JSON 灵活
聚合分析 SQL GROUP BYSUM() 更强大的聚合分析 terms, avg, stats
查询性能(模糊) LIKE/REGEXP 效率低 倒排索引效率高

二、Elasticsearch 底层原理

2.1 ES 的存储核心是「倒排索引」

什么是倒排索引?

你可以这样理解:

📝 正排索引(MySQL):记录 → 字段

🧠 倒排索引(ES):词语 → 哪些文档有这个词


举个例子:

假设你有 3 条文档:

🔁 倒排索引就是这样的结构:

词项(Term) 出现在哪些文档
elasticsearch doc1, doc2, doc3
spring doc3
java doc1
search doc2

🧠 有了这个结构:

  • 你搜索关键词 "elasticsearch" → 不用全表扫,只看这个倒排索引,就知道有哪些文档有这个词!

对比正排 vs 倒排

正排索引(MySQL) 倒排索引(ES)
一条记录所有字段 一个词出现在哪些记录
查询慢(like '%xx%') 查询快(直接查词 → 文档列表)
优化靠索引 + where 条件 内建为搜索引擎,天生支持模糊查

2.2 ES 是怎么执行查询的?

🔍 当你发起一个搜索请求:

  1. ES 把搜索词分词(如 "java 开发" → "java", "开发")

  2. 查倒排索引,看哪些文档包含这些词

  3. 计算"相关性得分"(TF-IDF/BM25)来排序

  4. 返回最匹配的结果列表(支持分页、过滤、高亮)

三、Spring Data Elasticsearch


3.1 添加依赖(适用于 Spring Boot 3.x)

pom.xml 中加入以下依赖:

XML 复制代码
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>

3.2 配置 application.yml

XML 复制代码
spring:
  elasticsearch:
    uris: http://localhost:9200
    username: elastic        # 如果没开启安全认证,可不填
    password: changeme

🔍 说明:

  • uris: 你的 Elasticsearch 服务地址(支持多个,如 ["http://host1:9200", "http://host2:9200"]

  • 如果你用的是 Elastic Stack 8.x,默认启用了用户认证,请在 Kibana 中设置 elastic 用户的密码后填入

  • 云服务(如阿里云、ESCloud)也是支持的,只要能访问对应地址即可


3.3 创建 ES 实体类(支持 MyBatis-Plus 和 Elasticsearch 公用一个实体类)

java 复制代码
@Data
@Document(indexName = "user_index")  // ES索引名
@TableName("user_info")              // MySQL 表名
public class User {

    @Id // ES 文档主键
    @TableId(type = IdType.ASSIGN_ID) // MySQL 主键(雪花算法)
    private Long id;

    @TableField("open_id")
    @Field(type = FieldType.Keyword) // 精确搜索(不分词)
    private String openId;

    @TableField("nick_name")
    @Field(type = FieldType.Text, analyzer = "ik_max_word") // 中文模糊搜索
    private String nickName;

    @TableField("age")
    @Field(type = FieldType.Integer)
    private Integer age;

    @TableField("bio")
    @Field(type = FieldType.Text, analyzer = "ik_smart") // 简略分词搜索
    private String bio;
}

🔍 说明:

  • @Document:表明这是一个 Elasticsearch 文档类

  • @Id:ES 中的 _id 主键字段,字符串或 Long 类型都可以

  • @Field:对应索引的字段类型,控制是否分词、如何存储、是否支持聚合等

在实体类中,只有需要在 Elasticsearch 中被搜索、排序、过滤、聚合的字段才应该加 @Field 注解

其他字段如果仅用于数据库存储、业务展示、不参与搜索功能 ,就可以不加 @Field,从而避免 ES 创建无用索引字段,节省资源

| 问题点 | 正确做法 |
| 两套映射冲突吗? | 不会,MyBatis 和 ES 只认自己注解 |
| 字段名不一致怎么办? | 用 @TableField("列名")@Field(type=...) 来明确指定 |

主键类型不同可以吗? 可以,但建议都使用 Long 类型的雪花算法 ID

3.4 创建 Repository 接口

java 复制代码
public interface UserRepository extends ElasticsearchRepository<User, Long> {

    //  精确匹配(字段必须是 Keyword 类型)
    List<User> findByOpenId(String openId);

    //  模糊匹配(字段必须是 Text 类型)
    List<User> findByNickNameContaining(String keyword);

    //  范围查询(数值或时间)
    List<User> findByAgeBetween(Integer min, Integer max);

    //  多字段联合查询
    List<User> findByNickNameContainingOrBioContaining(String kw1, String kw2);

    //  排序 + 分页(组合使用 PageRequest)
    Page<User> findByAgeGreaterThan(Integer age, Pageable pageable);
}

🔍 说明:

  • ElasticsearchRepository<T, ID> 是 Spring Data 提供的接口,自动实现常用查询

  • 方法命名遵循约定(如 findByXxxContaining → 对应模糊搜索)


3.5 服务层使用示例

java 复制代码
@Service
public class UserService {

    @Autowired
    private UserRepository userRepository;

    //  保存或更新
    public void save(User user) {
        userRepository.save(user);
    }

    //  按主键查询
    public User findById(Long id) {
        return userRepository.findById(id).orElse(null);
    }

    //  删除
    public void delete(Long id) {
        userRepository.deleteById(id);
    }

    //  模糊搜索 + 分页
    public Page<User> searchByKeyword(String keyword, int page, int size) {
        Pageable pageable = PageRequest.of(page, size);
        return userRepository.findByNickNameContaining(keyword, pageable);
    }

    //  多条件联合查询
    public List<User> multiFieldSearch(String keyword) {
        return userRepository.findByNickNameContainingOrBioContaining(keyword, keyword);
    }
}

五、FieldType 设置与映射规则


5.1 常见 MySQL 类型 → ES FieldType 映射

MySQL 类型 ES 类型 推荐 FieldType 场景
varchar / char keyword / text KeywordText 精确匹配 vs 分词搜索
int integer Integer 数值筛选、排序
bigint long Long 用户ID、主键
decimal / float double / float Double 金额、评分
datetime / timestamp date Date 时间区间筛选
text / longtext text Text 全文搜索字段

5.2 Text vs Keyword 的核心区别

特性 Text Keyword
是否分词 ✅ 是 ❌ 否
是否支持模糊搜索 ✅ 是 ❌ 否(只能精确匹配)
是否支持聚合 ❌ 否 ✅ 是(group by 可用)
是否支持排序 ❌ 否 ✅ 是
使用场景 用户名、描述、正文内容等模糊字段 ID、手机号、类别、地区码等

5.3 高级技巧:Text + Keyword 多字段组合映射

java 复制代码
@Field(type = FieldType.Text, analyzer = "ik_max_word")
@MultiField(
  mainField = @Field(type = FieldType.Text),
  otherFields = {
    @InnerField(suffix = "keyword", type = FieldType.Keyword)
  }
)
private String name;

🔍 ES 会创建两个字段:

字段名 类型 用途
name text 分词搜索,适合用户输入模糊匹配
name.keyword keyword 精确匹配、排序、聚合

六、Repository 接口提供的操作

6.1 基础 CRUD 操作

java 复制代码
userRepository.save(user);              // 新增或更新
userRepository.findById("1");           // 按 ID 查找
userRepository.findAll();               // 查询全部
userRepository.existsById("1");         // 判断是否存在
userRepository.deleteById("1");         // 删除
userRepository.count();                 // 总文档数量

❗注意:save()方法如果文档 @Id 主键冲突会执行更新操作。


6.2 分页与排序查询

java 复制代码
Pageable pageable = PageRequest.of(0, 10, Sort.by("age").descending());
Page<User> page = userRepository.findByAgeGreaterThan(18, pageable);

page.getContent();      // 当前页的数据列表
page.getTotalPages();   // 总页数
page.getTotalElements();// 总条数

PageRequest.of(pageIndex, pageSize, Sort...) 是核心入口。


6.3 模糊查询(文本匹配)

方式一:使用命名派生查询(字段类型为 Text

java 复制代码
List<User> users = userRepository.findByNickNameContaining("张");

Containing 相当于 %张% 模糊匹配,底层使用 match 查询。


方式二:中文精准分词(必须设置 analyzer)

实体字段:

java 复制代码
@Field(type = FieldType.Text, analyzer = "ik_max_word", searchAnalyzer = "ik_smart")
private String bio;

搜索方法:

java 复制代码
List<User> users = userRepository.findByBioContaining("Java 开发");

6.4 组合查询(AND / OR)

java 复制代码
List<User> list1 = userRepository.findByNickNameContainingAndBioContaining("张", "程序员"); // AND

List<User> list2 = userRepository.findByNickNameContainingOrBioContaining("张", "开发者");  // OR

6.5 范围查询(年龄、时间等)

数值范围

java 复制代码
List<User> users = userRepository.findByAgeBetween(18, 30);
List<User> users = userRepository.findByAgeGreaterThan(25);
List<User> users = userRepository.findByAgeLessThanEqual(40);

时间范围

java 复制代码
List<LogDoc> logs = logRepository.findByCreateTimeBetween(
    LocalDateTime.now().minusDays(7),
    LocalDateTime.now()
);

⚠️ ES 字段要设置为 @Field(type = FieldType.Date),格式一般为 ISO8601。


6.6 自定义 DSL 查询(复杂条件)

使用 NativeSearchQueryBuilder 构建复杂查询 DSL(非方法名自动派生):

java 复制代码
QueryBuilder query = QueryBuilders
    .boolQuery()
    .must(QueryBuilders.matchQuery("bio", "程序员"))
    .filter(QueryBuilders.rangeQuery("age").gte(20).lte(35));

NativeSearchQuery nativeQuery = new NativeSearchQueryBuilder()
    .withQuery(query)
    .withSort(SortBuilders.fieldSort("age").order(SortOrder.DESC))
    .withPageable(PageRequest.of(0, 10))
    .build();

SearchHits<User> hits = elasticsearchRestTemplate.search(nativeQuery, User.class);
List<User> result = hits.getSearchHits().stream()
    .map(SearchHit::getContent).toList();

6.7 聚合查询(如 group by)

示例:统计每个年龄的人数分布(terms 聚合)

java 复制代码
NativeSearchQuery query = new NativeSearchQueryBuilder()
    .addAggregation(AggregationBuilders.terms("ageAgg").field("age"))
    .build();

Aggregations aggs = elasticsearchRestTemplate.search(query, User.class).getAggregations();
Terms ageAgg = aggs.get("ageAgg");

for (Terms.Bucket bucket : ageAgg.getBuckets()) {
    System.out.println("年龄: " + bucket.getKeyAsString() + " 人数: " + bucket.getDocCount());
}

聚合类型说明:

聚合类型 含义 类似 SQL
Terms 按字段分组 GROUP BY
Avg / Max / Min 平均、最大、最小 AVG(col)
Histogram 范围段分组(如时间/金额段) GROUP BY 时间段
Nested 嵌套字段聚合 子表统计

6.8 删除操作

java 复制代码
userRepository.deleteById("123");

userRepository.deleteAllById(List.of("1", "2", "3"));

userRepository.deleteAll(); // 清空索引数据

6.9 索引操作(索引创建、刷新、存在性检查)

java 复制代码
// 判断索引是否存在
boolean exists = elasticsearchRestTemplate.indexOps(User.class).exists();

// 创建索引(若无)
if (!exists) {
    elasticsearchRestTemplate.indexOps(User.class).create();
    elasticsearchRestTemplate.indexOps(User.class).putMapping(
        elasticsearchRestTemplate.indexOps(User.class).createMapping());
}

// 删除索引
elasticsearchRestTemplate.indexOps(User.class).delete();

方法总结表

类型 方法/关键词
新增或更新 save(user)
删除 deleteById(id), deleteAll()
查询所有 findAll(), findAll(Pageable)
精确查询 findByOpenId(String)
模糊匹配 findByXxxContaining(String)
多字段匹配 findByAContainingAndBContaining(...)
分页排序 PageRequest.of(page, size, Sort.by(...))
数值范围 findByAgeBetween(min, max)
时间范围 findByCreateTimeBetween(start, end)
原生查询 NativeSearchQueryBuilder + QueryBuilders
聚合分析 AggregationBuilders.terms/avg/histogram(...)
判断索引存在 indexOps(clazz).exists()
创建索引 indexOps(clazz).create() + putMapping()
相关推荐
程序员编程指南20 小时前
Qt 开发 IDE 插件开发指南
c语言·c++·ide·qt·elasticsearch
TMT星球20 小时前
好未来披露2026财年Q1财报:净利润3128万美元,同比大增174%
搜索引擎
计算机小手1 天前
提升文档管理:推荐一键Docker部署的全文索引搜索引擎工具
经验分享·搜索引擎·docker·全文检索·开源软件
谷新龙0011 天前
Elasticsearch服务器开发(第2版) - 读书笔记 第二章 索引
服务器·elasticsearch
所念皆为东辞1 天前
elk部署加日志收集
linux·elk·elasticsearch·centos
可曾去过倒悬山2 天前
Mac上优雅简单地使用Git:从入门到高效工作流
git·elasticsearch·macos
Hello.Reader2 天前
用 Go Typed Client 快速上手 Elasticsearch —— 从建索引到聚合的完整实战
elasticsearch·golang·jenkins
Fireworkitte2 天前
es的histogram直方图聚合和terms分组聚合
大数据·elasticsearch·搜索引擎
Stringzhua2 天前
Git踩坑
大数据·git·elasticsearch