从倒排索引到项目实战

前言

在后端开发中,很多业务都会涉及搜索功能,比如商品搜索、文章搜索、日志检索、订单异常排查、用户行为分析等。

如果只是根据主键查一条数据,MySQL 完全够用。但如果需求变成:

用户输入关键词后,要支持模糊搜索、中文分词、条件筛选、排序、高亮、聚合统计,并且数据量还比较大。

这时候继续用 MySQL 的 LIKE '%关键词%' 就会比较吃力。

Elasticsearch,简称 ES,就是专门用来解决搜索和分析问题的分布式搜索引擎。它底层基于 Lucene,核心能力是全文检索、复杂查询和数据聚合分析。

一句话理解:

MySQL 更适合存储准确的业务数据,Elasticsearch 更适合快速搜索和分析数据。


一、Elasticsearch 是什么?

Elasticsearch 是一个分布式搜索和分析引擎,常用于全文检索、日志查询、商品搜索、数据分析等场景。

它可以把一条条业务数据保存成文档,然后通过倒排索引快速查找。

比如一个商品文档可能长这样:

{
  "id": 1001,
  "name": "Sony 蓝牙降噪耳机",
  "brand": "Sony",
  "category": "数码耳机",
  "price": 899,
  "sales": 3200
}

当用户搜索“蓝牙耳机”时,ES 不会像数据库那样从头到尾扫描所有商品,而是通过倒排索引快速找到包含“蓝牙”“耳机”这些词的商品,再根据相关性、销量、价格等条件返回结果。


二、为什么不用 MySQL 做搜索?

MySQL 可以做简单查询,比如:

SELECT * FROM product WHERE name LIKE '%耳机%';

但这种方式在真实业务里会有几个问题。

首先,LIKE '%耳机%' 这种前后模糊匹配很难有效走索引,数据量一大查询就会变慢。

其次,MySQL 对全文检索的支持不如 ES 灵活,尤其是中文搜索。比如用户搜索“蓝牙降噪耳机”,系统应该能理解“蓝牙”“降噪”“耳机”这些关键词,而不是只做简单字符串匹配。

另外,搜索业务通常不只是查关键词,还要支持品牌筛选、价格区间、销量排序、关键词高亮、分类聚合等组合能力。用 MySQL 硬做也能做一部分,但性能和扩展性都不如 ES。

所以在实际项目中,常见架构是:

MySQL:保存核心业务数据
Elasticsearch:提供搜索和分析能力
Redis:缓存热点数据
MQ:异步同步 MySQL 和 ES 数据

三、Elasticsearch 核心概念

1. Document:文档

Document 是 ES 中存储数据的基本单位,类似 MySQL 中的一行记录。

比如一条商品数据、一篇文章、一条日志,都可以作为一个文档。

2. Index:索引

Index 可以理解为一类文档的集合,类似 MySQL 中的表。

例如:

product_index   商品索引
article_index   文章索引
order_log_index 订单日志索引

3. Field:字段

Field 类似 MySQL 中的列。

比如商品文档中有 namebrandpricecategory 等字段。

4. Mapping:映射

Mapping 用来定义字段类型和字段是否分词,类似 MySQL 的表结构设计。

常见字段类型有:

keyword:不分词,适合精确匹配,比如订单号、状态、品牌、标签
text:分词,适合全文检索,比如标题、正文、商品描述
integer / long / double:数字类型
date:日期类型
boolean:布尔类型

Mapping 设计很重要,因为它会直接影响查询效果。

比如订单状态 status 应该用 keyword,因为它只需要精确匹配;商品标题 name 应该用 text,因为它需要分词搜索。


四、Elasticsearch 为什么搜索快?

ES 搜索快的核心原因是倒排索引。

普通数据库更像是:

文档 -> 文档里有哪些词

倒排索引则是:

词 -> 这个词出现在哪些文档中

举个例子,有三条商品数据:

商品1:蓝牙降噪耳机
商品2:蓝牙运动耳机
商品3:机械键盘

建立倒排索引后,大概会变成:

蓝牙 -> 商品1、商品2
降噪 -> 商品1
耳机 -> 商品1、商品2
机械 -> 商品3
键盘 -> 商品3

当用户搜索“蓝牙耳机”时,ES 可以直接根据“蓝牙”“耳机”找到相关商品,而不是扫描所有商品记录。

这就是 ES 适合全文搜索的根本原因。


五、分词器是什么?

分词器的作用是把一段文本拆成多个词。

比如:

我想买一副蓝牙降噪耳机

可能会被拆成:

我 / 想 / 买 / 一副 / 蓝牙 / 降噪 / 耳机

英文可以按空格分词,但中文没有天然空格,所以中文搜索通常需要配置中文分词器,比如 IK 分词器。

分词器会直接影响搜索结果。

如果分词太细,搜索结果可能很多,但不够精准;如果分词太粗,又可能搜不到用户想要的内容。

所以在 ES 中,字段设计通常要区分:

需要全文搜索的字段:text
需要精确匹配的字段:keyword

比如商品名称适合 text,订单状态适合 keyword


六、常见查询方式

1. term 精确查询

term 查询适合查询不分词字段,比如订单状态、订单号、用户 ID、品牌、标签、枚举值等。

这类字段的特点是:查的不是某个关键词,而是一个完整值是否完全相等。

比如订单状态字段 status 可能有这些值:

PAID      已支付
UNPAID    未支付
CANCELLED 已取消
REFUNDED  已退款

如果要查询所有已支付订单,可以这样写:

{
  "query": {
    "term": {
      "status": "PAID"
    }
  }
}

这段查询的意思是:

找出 status 字段值等于 PAID 的文档。

这里不需要分词,也不需要相关性评分,只需要精确匹配。所以 status 通常会在 Mapping 中设计成 keyword 类型。

{
  "mappings": {
    "properties": {
      "status": {
        "type": "keyword"
      }
    }
  }
}

可以简单记住:

term 查完整值,通常配合 keyword 字段使用。


2. match 全文检索

match 查询适合搜索会被分词的文本字段,比如商品标题、文章内容、商品描述等。

例如用户搜索:

蓝牙耳机

ES 会先对这个关键词分词,再去匹配文档中的相关内容。

查询示例:

{
  "query": {
    "match": {
      "name": "蓝牙耳机"
    }
  }
}

如果商品名称是:

Sony 蓝牙降噪耳机

它就有可能被匹配出来。

matchterm 最大区别是:

term:精确匹配完整值
match:先分词,再做全文检索

所以搜索商品标题、文章正文时一般用 match;查状态、订单号、分类 ID 时一般用 term


3. range 范围查询

range 用于查询数字或时间范围。

比如商品搜索中,用户选择价格范围 500 到 1000 元:

{
  "query": {
    "range": {
      "price": {
        "gte": 500,
        "lte": 1000
      }
    }
  }
}

常见场景包括:

价格区间
时间范围
年龄范围
库存数量
订单金额

4. bool 组合查询

真实项目中最常用的是 bool 查询,因为业务搜索通常不是单个条件,而是多个条件组合。

比如用户搜索商品时,条件可能是:

关键词:蓝牙耳机
品牌:Sony
分类:数码产品
价格:500 到 1000

对应 ES 查询可以这样写:

{
  "query": {
    "bool": {
      "must": [
        {
          "match": {
            "name": "蓝牙耳机"
          }
        }
      ],
      "filter": [
        {
          "term": {
            "brand": "Sony"
          }
        },
        {
          "term": {
            "category": "数码产品"
          }
        },
        {
          "range": {
            "price": {
              "gte": 500,
              "lte": 1000
            }
          }
        }
      ]
    }
  }
}

这里可以这样理解:

must:必须匹配,会影响相关性评分,比如关键词搜索
filter:过滤条件,不参与评分,性能更好,比如品牌、分类、价格
must_not:必须不匹配
should:可选匹配,可以提高相关性分数

实际开发中,关键词通常放在 must,筛选条件通常放在 filter


七、典型应用场景

1. 商品搜索

商品搜索是 ES 最典型的使用场景。

以电商平台为例,用户输入:

蓝牙降噪耳机

系统要做的不是简单判断商品名称里有没有这几个字,而是要完成一整套搜索流程。

首先,ES 会对用户输入进行分词,比如拆成:

蓝牙 / 降噪 / 耳机

然后在商品名称、商品描述、品牌、分类等字段中查找相关商品。

比如有一条商品数据:

{
  "name": "Sony 蓝牙降噪耳机",
  "brand": "Sony",
  "category": "数码耳机",
  "price": 899,
  "sales": 3200
}

这条商品就可能因为命中了“蓝牙”“降噪”“耳机”而被搜索出来。

同时,用户可能还会继续筛选:

只看 Sony 品牌
价格在 500 到 1000 元之间
按销量从高到低排序
搜索关键词高亮显示
统计不同品牌下有多少商品

这些能力对应到 ES 中,分别是:

关键词搜索:match
品牌筛选:term
价格筛选:range
销量排序:sort
品牌统计:aggregation
关键词高亮:highlight

所以商品搜索本质上是:

全文检索 + 条件过滤 + 排序 + 聚合统计 + 高亮展示。

这类复杂搜索如果全部用 MySQL 来做,随着商品数量增加,性能和搜索体验都会受到影响。而 ES 的优势就是专门处理这种搜索场景。


2. 日志检索

日志检索也是 ES 很常见的应用。

在分布式系统中,一次请求可能经过多个服务。如果线上出现订单异常,只看单个服务日志很难排查。

如果把日志写入 ES,就可以根据这些条件快速检索:

TraceId
订单号
用户 ID
接口路径
错误级别
时间范围
异常关键字

比如排查订单创建失败,可以根据订单号或 TraceId 搜索整条调用链上的日志,快速定位是库存服务、支付服务还是订单服务出了问题。

这也是很多公司使用 ELK 的原因:

Elasticsearch:存储和检索日志
Logstash / Filebeat:采集日志
Kibana:可视化查看日志

3. 文章和文档搜索

博客、知识库、论坛、帮助中心这类系统,也很适合用 ES。

用户输入关键词后,系统可以从标题、正文、标签中搜索相关内容,并且按照相关性排序。

例如搜索:

Spring Boot Redis 缓存

ES 可以找到标题或正文中包含相关词的文章,并把命中的关键词高亮展示出来。


4. 数据聚合分析

ES 不只能搜索,也能做聚合统计。

比如:

统计每天订单数量
统计每个品牌的商品数量
统计不同状态订单占比
统计最近一小时错误日志数量
统计热门搜索关键词

这类功能可以用 ES 的 aggregation 实现。

不过要注意,ES 适合做搜索和近实时分析,不适合作为强事务数据库使用。


八、Spring Boot 集成 Elasticsearch 示例

1. 引入依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>

2. 定义文档对象

@Document(indexName = "product_index")
public class ProductDocument {

    @Id
    private Long id;

    @Field(type = FieldType.Text, analyzer = "ik_max_word")
    private String name;

    @Field(type = FieldType.Keyword)
    private String brand;

    @Field(type = FieldType.Keyword)
    private String category;

    @Field(type = FieldType.Double)
    private Double price;

    @Field(type = FieldType.Integer)
    private Integer sales;
}

这里有几个点要注意:

name 用 text,因为它需要被搜索和分词
brand 用 keyword,因为品牌通常是精确筛选
category 用 keyword,因为分类一般也是精确过滤
price 用 double,因为价格需要范围查询和排序
sales 用 integer,因为销量需要排序

3. 保存商品文档

@Autowired
private ElasticsearchOperations elasticsearchOperations;

public void saveProduct(ProductDocument product) {
    elasticsearchOperations.save(product);
}

4. 商品搜索示例

public List<ProductDocument> searchProducts(String keyword) {
    NativeQuery query = NativeQuery.builder()
            .withQuery(q -> q.match(m -> m.field("name").query(keyword)))
            .build();

    SearchHits<ProductDocument> hits =
            elasticsearchOperations.search(query, ProductDocument.class);

    return hits.stream()
            .map(SearchHit::getContent)
            .toList();
}

这段代码的作用是:根据用户输入的关键词,在商品名称 name 字段中进行全文检索。

如果要继续加品牌、价格、排序等条件,就可以使用 bool 查询进行组合。


九、项目中常见问题

1. MySQL 和 ES 数据一致性

实际项目中,一般不会把 ES 当成主库。核心业务数据仍然存 MySQL,ES 只是搜索库。

常见同步流程是:

业务数据写入 MySQL
        ↓
发送 MQ 消息
        ↓
消费者同步数据到 ES

比如商品价格修改后,先更新 MySQL,再发送消息通知搜索服务更新 ES 中的商品文档。

这里需要注意几个问题:

MQ 消息发送失败怎么办
ES 更新失败怎么办
重复消费会不会导致数据异常
删除数据时 ES 是否同步删除
是否需要定时任务做数据校验

所以 ES 数据同步通常要配合 MQ、重试、幂等和定时校验一起做。


2. Mapping 设计不合理

Mapping 一旦设计错,后期修改成本比较高。

常见问题包括:

订单号用了 text,导致精确查询不准确
商品名称用了 keyword,导致无法分词搜索
价格字段用了字符串,导致范围查询和排序异常
时间字段格式不统一,导致时间范围查询失败

设计 Mapping 时,要先想清楚每个字段的用途:

是否需要全文搜索
是否需要精确过滤
是否需要排序
是否需要聚合统计
是否需要范围查询

字段不是随便建的,而是要根据查询场景来设计。


3. 深分页问题

普通分页一般是:

{
  "from": 0,
  "size": 10
}

前几页没有问题,但如果用户查到第 10000 页,就会变成:

{
  "from": 100000,
  "size": 10
}

这种深分页性能很差,因为 ES 需要先取出大量数据,再丢弃前面的结果。

解决方式包括:

限制最大翻页页数
使用 search_after
使用 scroll 做批量数据导出
业务上避免无限翻页

搜索类产品一般也不会允许用户无限往后翻。


4. 消息写入和查询性能

ES 写入压力过大时,可能会影响查询性能。

常见优化方式:

批量写入
合理设置刷新间隔
控制字段数量
避免频繁更新大文档
合理设置分片数量
冷热数据分离

查询慢时,则需要重点检查:

是否使用了深分页
是否聚合字段基数太高
是否查询条件过于复杂
Mapping 是否设计合理
分片数量是否合适
集群资源是否不足

十、Elasticsearch 和其他中间件的关系

在后端系统里,ES 通常不是单独使用,而是和 MySQL、Redis、MQ 配合。

可以这样理解:

MySQL:负责核心业务数据,保证事务一致性
Redis:负责热点缓存,提高访问速度
MQ:负责异步解耦和数据同步
Elasticsearch:负责搜索和分析

比如商品系统中:

商品详情页:优先查 Redis,缓存没有再查 MySQL
商品搜索页:查 Elasticsearch
商品修改:先更新 MySQL,再通过 MQ 同步 ES
商品统计分析:可以用 ES 聚合查询

这样每个组件负责自己擅长的事情,系统整体会更清晰。


总结

Elasticsearch 的核心价值是快速搜索和数据分析。

它通过倒排索引提高全文检索效率,通过分词器提升搜索体验,通过 Query DSL 支持复杂查询,通过分片和副本支持分布式扩展。

学习 ES 不能只记概念,更要理解它在项目中解决什么问题:

商品搜索为什么不用 MySQL LIKE
日志检索为什么适合 ES
term 和 match 分别适合什么字段
Mapping 为什么要提前设计
MySQL 和 ES 为什么要做数据同步
深分页为什么慢
ES 为什么不能替代 MySQL

一句话总结:

MySQL 负责存准,Elasticsearch 负责搜快。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐