一、什么是 Elasticsearch?为什么我们需要它?

Elasticsearch(简称 ES)是一个开源的、分布式的、RESTful 风格的搜索引擎和数据分析引擎。它基于 Lucene 库构建,提供了简单易用的 API,隐藏了 Lucene 的复杂性。

ES 的核心特点

  • 分布式:自动分片、自动负载均衡、自动故障转移
  • 近实时:数据写入后 1 秒内即可被搜索到
  • 全文检索:支持复杂的全文搜索、模糊搜索、高亮显示
  • 数据分析:支持强大的聚合查询,能快速进行数据统计和分析
  • 高可用:支持主从复制,单个节点故障不影响服务
  • 可扩展:可以轻松扩展到上百个节点,处理 PB 级别的数据

为什么不用 MySQL 做搜索?

很多人会问:MySQL 也有 like 查询,为什么还要用 ES?

答案很简单:MySQL 的 like 查询在大数据量下性能极差,而且不支持全文检索和复杂的数据分析

  • MySQL 的 like '% 关键词 %' 查询会走全表扫描,百万级数据就会明显变慢
  • MySQL 不支持分词,只能做简单的字符串匹配
  • MySQL 不支持复杂的相关性排序
  • MySQL 不支持聚合分析和统计

而 ES 就是为了解决这些问题而生的。它专门针对搜索和分析进行了优化,能在毫秒级返回千万级数据的查询结果。

二、ES 的核心应用场景

ES 的应用场景非常广泛,我总结了最常用的 8 个场景:

  1. 全文检索:商品搜索、文章搜索、用户搜索
  2. 日志分析:ELK(Elasticsearch+Logstash+Kibana)日志收集分析系统
  3. 指标监控:系统指标、业务指标的实时监控和告警
  4. 数据分析:用户行为分析、销售数据分析、运营数据分析
  5. 地理信息搜索:附近的人、附近的商家、地理位置查询
  6. 自动补全:搜索框的输入提示、自动补全功能
  7. 拼写纠错:搜索时的拼写检查和纠错
  8. 数据可视化:配合 Kibana 制作各种数据仪表盘

三、ES 的核心概念

要真正用好 ES,必须先搞懂它的核心概念。很多人学 ES 觉得难,就是因为这些概念和关系型数据库不一样。

我做了一个 ES 和 MySQL 的概念对比表,帮助你快速理解:

表格

Elasticsearch MySQL
Index(索引) Database(数据库)
Document(文档) Row(行)
Field(字段) Column(列)
Mapping(映射) Table Schema(表结构)
Query DSL SQL
Shard(分片) 分表
Replica(副本) 主从复制

核心概念详解

  1. Index(索引):ES 中存储数据的地方,相当于 MySQL 的数据库。一个索引就是一个逻辑上的数据集合。
  2. Document(文档):ES 中的最小数据单元,相当于 MySQL 的一行记录。文档以 JSON 格式存储。
  3. Field(字段):文档中的属性,相当于 MySQL 的列。每个字段都有自己的数据类型。
  4. Mapping(映射):定义索引中字段的数据类型、分词器、索引方式等,相当于 MySQL 的表结构。
  5. Shard(分片):将一个大的索引分成多个小的分片,分布在不同的节点上,实现水平扩展。
  6. Replica(副本):分片的备份,用于提高数据的可用性和查询性能。

四、Spring Boot 集成 ES 完整教程

下面我以 Spring Boot 2.7.x 为例,教你如何快速集成和使用 Elasticsearch。

第一步:引入依赖

xml

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

第二步:配置 ES 连接

yaml

spring:
  elasticsearch:
    rest:
      uris: http://localhost:9200
      username: elastic
      password: 123456
      connection-timeout: 5s
      read-timeout: 30s

第三步:定义实体类和 Mapping

java

运行

@Data
@Document(indexName = "product", shards = 3, replicas = 1)
public class Product {
    @Id
    private Long id;
    
    @Field(type = FieldType.Text, analyzer = "ik_max_word")
    private String name;
    
    @Field(type = FieldType.Text, analyzer = "ik_max_word")
    private String description;
    
    @Field(type = FieldType.Double)
    private Double price;
    
    @Field(type = FieldType.Integer)
    private Integer stock;
    
    @Field(type = FieldType.Keyword)
    private String category;
    
    @Field(type = FieldType.Date, format = DateFormat.date_time)
    private Date createTime;
}

第四步:定义 Repository 接口

java

运行

public interface ProductRepository extends ElasticsearchRepository<Product, Long> {
    // 根据名称搜索
    List<Product> findByName(String name);
    
    // 根据名称和描述搜索
    List<Product> findByNameOrDescription(String name, String description);
    
    // 根据价格区间查询
    List<Product> findByPriceBetween(Double minPrice, Double maxPrice);
}

第五步:使用 Repository 操作 ES

java

运行

@Service
public class ProductService {
    @Autowired
    private ProductRepository productRepository;
    
    // 保存商品
    public void saveProduct(Product product) {
        productRepository.save(product);
    }
    
    // 根据ID查询
    public Product findById(Long id) {
        return productRepository.findById(id).orElse(null);
    }
    
    // 搜索商品
    public List<Product> search(String keyword) {
        return productRepository.findByNameOrDescription(keyword, keyword);
    }
    
    // 删除商品
    public void deleteById(Long id) {
        productRepository.deleteById(id);
    }
}

第六步:复杂查询(Query DSL)

对于复杂的查询,我们需要使用 ElasticsearchRestTemplate 来编写 Query DSL:

java

运行

@Service
public class ProductSearchService {
    @Autowired
    private ElasticsearchRestTemplate elasticsearchTemplate;
    
    public Page<Product> searchProducts(String keyword, String category, 
                                       Double minPrice, Double maxPrice, 
                                       int pageNum, int pageSize) {
        NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
        
        // 构建查询条件
        BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
        
        // 关键词搜索
        if (StringUtils.hasText(keyword)) {
            boolQuery.must(QueryBuilders.multiMatchQuery(keyword, "name", "description")
                    .type(MultiMatchQueryBuilder.Type.BEST_FIELDS)
                    .boost(3.0f));
        }
        
        // 分类过滤
        if (StringUtils.hasText(category)) {
            boolQuery.filter(QueryBuilders.termQuery("category", category));
        }
        
        // 价格区间过滤
        if (minPrice != null && maxPrice != null) {
            boolQuery.filter(QueryBuilders.rangeQuery("price").gte(minPrice).lte(maxPrice));
        }
        
        queryBuilder.withQuery(boolQuery);
        
        // 排序:按价格升序
        queryBuilder.withSort(SortBuilders.fieldSort("price").order(SortOrder.ASC));
        
        // 分页
        queryBuilder.withPageable(PageRequest.of(pageNum - 1, pageSize));
        
        // 高亮显示
        HighlightBuilder highlightBuilder = new HighlightBuilder();
        highlightBuilder.field("name").field("description");
        highlightBuilder.preTags("<em style='color:red'>");
        highlightBuilder.postTags("</em>");
        queryBuilder.withHighlightBuilder(highlightBuilder);
        
        // 执行查询
        SearchHits<Product> searchHits = elasticsearchTemplate.search(
                queryBuilder.build(), Product.class);
        
        // 处理高亮结果
        List<Product> products = searchHits.stream().map(hit -> {
            Product product = hit.getContent();
            // 替换高亮字段
            if (hit.getHighlightFields().containsKey("name")) {
                product.setName(hit.getHighlightFields().get("name").get(0));
            }
            if (hit.getHighlightFields().containsKey("description")) {
                product.setDescription(hit.getHighlightFields().get("description").get(0));
            }
            return product;
        }).collect(Collectors.toList());
        
        return new PageImpl<>(products, PageRequest.of(pageNum - 1, pageSize), 
                searchHits.getTotalHits());
    }
}

五、深入原理:ES 为什么这么快?

ES 的高性能是由多个关键技术共同决定的:

1. 倒排索引

这是 ES 最核心的数据结构。和关系型数据库的正排索引不同,倒排索引记录的是 "关键词 - 文档" 的映射关系。

举个例子:

  • 文档 1:"我爱 Java"
  • 文档 2:"我爱 Elasticsearch"
  • 文档 3:"Java 是最好的编程语言"

倒排索引就是:

plaintext

我 → [1, 2]
爱 → [1, 2]
Java → [1, 3]
Elasticsearch → [2]
是 → [3]
最好的 → [3]
编程语言 → [3]

当你搜索 "Java" 时,ES 直接从倒排索引中找到包含 "Java" 的文档 1 和 3,速度非常快。

2. 分词器

ES 会将文本内容拆分成一个个关键词,然后建立倒排索引。分词器的质量直接决定了搜索结果的准确性。

ES 内置了多种分词器,最常用的中文分词器是 IK 分词器,它支持细粒度分词(ik_max_word)和粗粒度分词(ik_smart)两种模式。

3. 分布式架构

ES 天生就是分布式的,它将数据分成多个分片,分布在不同的节点上。查询时可以并行查询多个分片,然后合并结果,大大提高了查询速度。

4. 内存缓存

ES 将热点数据和索引缓存到内存中,查询时直接从内存获取,避免了磁盘 IO。

六、ES 常见问题与解决方案

在生产环境中使用 ES,最常见的问题有:

问题 1:深分页问题

问题描述:当使用 from+size 进行分页时,from 越大,查询越慢。当 from 超过 10000 时,ES 会直接报错。

原因:ES 需要从所有分片中查询前 from+size 条数据,然后在协调节点进行排序和合并,最后返回 size 条数据。from 越大,这个过程越耗时。

解决方案

  • 使用 search_after:适用于滚动加载场景,不支持跳页
  • 使用 scroll:适用于批量导出数据,不适用于用户交互
  • 限制分页深度:业务上限制用户只能翻到前 100 页

问题 2:索引设计不当导致查询慢

常见错误

  • 给不需要搜索的字段设置为 text 类型
  • 使用默认的分词器处理中文
  • 索引分片数设置不合理
  • 没有设置合理的刷新间隔

解决方案

  • 只给需要搜索的字段设置为 text 类型,其他字段设置为 keyword 或数值类型
  • 使用 IK 分词器处理中文
  • 分片数设置为节点数的 1-3 倍,单个分片大小控制在 10-50GB
  • 将刷新间隔(refresh_interval)设置为 30s 或更长,提高写入性能

问题 3:数据写入慢

问题描述:大量数据写入时,ES 性能下降严重。

解决方案

  • 使用批量写入(bulk API),每次批量写入 1000-5000 条数据
  • 关闭自动刷新,写入完成后再手动刷新
  • 增加副本数为 0,写入完成后再恢复副本
  • 使用 SSD 磁盘
  • 调整 JVM 堆内存大小,设置为物理内存的一半,不超过 32GB

问题 4:集群健康状态异常

常见状态

  • Green:所有主分片和副本都正常运行
  • Yellow:所有主分片正常运行,部分副本不正常
  • Red:部分主分片不正常

解决方案

  • Yellow 状态:检查节点是否正常,增加节点数
  • Red 状态:检查故障节点,恢复数据,必要时重建索引

七、ES 生产环境最佳实践

最后,我分享几个我在生产环境中总结的 ES 最佳实践:

  1. 版本选择:建议使用 7.x 版本,不要使用太新或太旧的版本
  2. 集群规划:至少 3 个节点,避免脑裂问题
  3. 硬件配置:使用 SSD 磁盘,内存越大越好,CPU 核心数不少于 4 核
  4. 索引设计
    • 索引名使用小写字母,用下划线分隔
    • 不要在一个索引中存储太多不同类型的数据
    • 按时间分索引,比如每天一个索引,方便管理和删除
  5. 查询优化
    • 避免使用通配符开头的查询
    • 避免查询大量数据
    • 使用 filter 代替 query 进行过滤,filter 会缓存结果
    • 只返回需要的字段,不要返回整个文档
  6. 安全配置:开启身份认证和权限控制,禁止外网直接访问 ES 集群
  7. 监控告警:监控集群健康状态、节点状态、索引大小、查询延迟等指标
  8. 备份恢复:定期备份数据,防止数据丢失
Logo

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

更多推荐