在数据量爆炸的今天,传统数据库的查询能力越来越难以满足复杂的检索需求。比如电商平台的商品搜索,需要支持关键词模糊匹配、多条件筛选、热门度排序等功能,这时候 Elasticsearch(简称 ES)就成了最佳选择。作为一款分布式全文搜索引擎,ES 以其强大的检索能力、灵活的扩展性被广泛应用。本文将从 ES 核心概念讲起,结合 Java 实战案例,带你快速掌握 ES 的应用技巧。
一、Elasticsearch 核心概念与优势
在使用 ES 前,我们需要先理清它的核心概念 —— 很多人会把 ES 和数据库类比,这个思路其实很实用。
1.1 ES 与关系型数据库的概念对应
如果把 ES 比作数据库,两者的核心概念可以这样对应:
关系型数据库 | Elasticsearch | 说明 |
Database(数据库) | Index(索引) | 一个索引对应一类数据集合,比如 “商品索引”“用户索引” |
Table(表) | Type(类型) | 早期 ES 用于区分索引内的不同数据类型,7.x 后已废弃,一个索引只对应一种类型 |
Row(行) | Document(文档) | 索引中的一条数据,以 JSON 格式存储 |
Column(列) | Field(字段) | 文档中的一个属性,比如商品的 “名称”“价格” |
Schema(表结构) | Mapping(映射) | 定义文档中字段的类型、分词器等规则 |
1.2 ES 的核心优势
相比传统数据库和其他搜索引擎,ES 的核心竞争力体现在这几点:
- 全文检索能力:支持关键词分词、模糊匹配、同义词扩展等,比如搜索 “手机” 时能匹配 “智能手机”“移动电话”。
- 分布式架构:天然支持分片和副本,数据自动分片存储,副本保证高可用,可轻松扩展至海量数据。
- 近实时搜索:数据写入后秒级可查,兼顾实时性和性能。
- 灵活的聚合分析:支持统计、排序、分组等复杂分析,比如按 “商品分类” 统计销量 Top10。
二、Elasticsearch 核心操作入门
要使用 ES,首先要掌握其基础操作。ES 提供 RESTful API 接口,可通过 HTTP 请求直接操作,也可通过客户端工具(如 Kibana 的 Dev Tools)执行。
2.1 索引相关操作
(1)创建索引及映射
创建一个 “商品索引”,并定义字段映射(类似数据库建表时定义字段类型):
# 创建商品索引
PUT /product_index
{
"mappings": {
"properties": {
"id": { "type": "keyword" }, // 商品ID,精确匹配,不分词
"name": {
"type": "text",
"analyzer": "ik_max_word", // 使用IK分词器(中文分词必备)
"fields": {
"keyword": { "type": "keyword" } // 用于精确查询或排序
}
},
"price": { "type": "double" }, // 价格
"category": { "type": "keyword" }, // 分类,精确匹配
"createTime": { "type": "date", "format": "yyyy-MM-dd HH:mm:ss" } // 创建时间
}
}
}
这里有两个关键注意点:
- text 与 keyword:text类型用于全文检索(会分词),keyword用于精确匹配(不分词),同一个字段可通过fields同时支持两种类型。
- 中文分词:必须使用 IK 分词器(需提前安装),ik_max_word会将文本拆分为最细粒度(如 “华为手机” 拆为 “华为”“手机”),ik_smart为粗粒度拆分。
(2)查看索引映射
GET /product_index/_mapping
(3)删除索引
DELETE /product_index
2.2 文档相关操作
文档是 ES 中最小的数据单元,所有操作围绕文档展开。
(1)新增文档
# 新增文档(指定ID)
PUT /product_index/_doc/1001
{
"id": "1001",
"name": "华为Mate 60 Pro 智能手机",
"price": 6999.00,
"category": "手机",
"createTime": "2024-01-15 09:30:00"
}
# 新增文档(自动生成ID)
POST /product_index/_doc
{
"id": "1002",
"name": "苹果iPhone 15 手机",
"price": 7999.00,
"category": "手机",
"createTime": "2024-01-20 14:20:00"
}
(2)查询文档
ES 的查询能力非常强大,这里列举几种常用场景:
- 精确查询(根据分类查询手机商品):
GET /product_index/_search
{
"query": {
"term": {
"category": { "value": "手机" }
}
}
}
- 全文检索(搜索包含 “华为” 的商品):
GET /product_index/_search
{
"query": {
"match": {
"name": "华为"
}
}
}
- 组合条件查询(价格在 5000-8000,且分类为手机):
GET /product_index/_search
{
"query": {
"bool": {
"must": [
{ "term": { "category": "手机" } },
{ "range": { "price": { "gte": 5000, "lte": 8000 } } }
]
}
},
"sort": [{"createTime": "desc"}], // 按创建时间倒序
"from": 0, "size": 10 // 分页(从第0条开始,取10条)
}
(3)更新与删除文档
- 更新文档(全量更新或局部更新):
# 局部更新(只更新价格)
POST /product_index/_update/1001
{
"doc": {
"price": 6799.00
}
}
- 删除文档:
DELETE /product_index/_doc/1001
三、Java 操作 Elasticsearch 实战
实际开发中,我们很少直接调用 REST API,而是通过官方客户端工具操作。ES 官方推荐的 Java 客户端是 Elasticsearch Java Client(7.x 后替代了旧的 Transport Client)。
3.1 环境准备
(1)引入依赖
在 Maven 项目的pom.xml中添加依赖(版本需与 ES 服务器一致,这里以 8.10.4 为例):
<dependencies>
<!-- Elasticsearch Java Client -->
<dependency>
<groupId>co.elastic.clients</groupId>
<artifactId>elasticsearch-java</artifactId>
<version>8.10.4</version>
</dependency>
<!-- 依赖Jackson处理JSON -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.15.2</version>
</dependency>
<!-- 连接池 -->
<dependency>
<groupId>org.apache.httpcomponents.client5</groupId>
<artifactId>httpclient5</artifactId>
<version>5.3</version>
</dependency>
</dependencies>
(2)初始化客户端
创建 ES 客户端连接(类似数据库连接池):
import co.elastic.clients.elasticsearch.ElasticsearchClient;
import co.elastic.clients.json.jackson.JacksonJsonpMapper;
import co.elastic.clients.transport.ElasticsearchTransport;
import co.elastic.clients.transport.rest_client.RestClientTransport;
import org.apache.http.HttpHost;
import org.elasticsearch.client.RestClient;
public class EsClient {
// 单例客户端
private static ElasticsearchClient client;
static {
// 创建REST客户端(连接ES服务器,可配置多个节点)
RestClient restClient = RestClient.builder(
new HttpHost("localhost", 9200, "http")
).build();
// 创建传输层
ElasticsearchTransport transport = new RestClientTransport(
restClient, new JacksonJsonpMapper()
);
// 创建客户端
client = new ElasticsearchClient(transport);
}
public static ElasticsearchClient getClient() {
return client;
}
}
注意:ES 客户端版本必须与服务器版本保持一致,否则可能出现兼容性问题。
3.2 核心操作实战:商品搜索功能
以 “电商商品搜索” 为例,实现文档新增、条件查询、分页排序等核心功能。
(1)定义商品实体类
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import java.util.Date;
@Data
public class Product {
private String id;
private String name;
private Double price;
private String category;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date createTime;
}
(2)新增商品文档
import co.elastic.clients.elasticsearch.ElasticsearchClient;
import co.elastic.clients.elasticsearch.core.IndexRequest;
import co.elastic.clients.elasticsearch.core.IndexResponse;
import java.io.IOException;
public class ProductService {
private final ElasticsearchClient client = EsClient.getClient();
// 新增商品到ES
public void addProduct(Product product) throws IOException {
// 构建索引请求(指定索引名、文档ID和数据)
IndexRequest<Product> request = IndexRequest.of(b -> b
.index("product_index") // 索引名
.id(product.getId()) // 文档ID(可选,不指定则自动生成)
.document(product) // 文档数据
);
// 执行请求
IndexResponse response = client.index(request);
System.out.println("新增结果:" + response.result());
}
}
(3)多条件查询商品
实现一个带条件、分页、排序的商品搜索功能:
import co.elastic.clients.elasticsearch.core.SearchRequest;
import co.elastic.clients.elasticsearch.core.SearchResponse;
import co.elastic.clients.elasticsearch.core.search.Hit;
import co.elastic.clients.elasticsearch.core.search.TotalHits;
import co.elastic.clients.elasticsearch.core.search.TotalHitsRelation;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
public class ProductService {
// ... 其他代码省略
/**
* 搜索商品
* @param keyword 关键词(搜索商品名称)
* @param category 分类(可选)
* @param minPrice 最低价格(可选)
* @param maxPrice 最高价格(可选)
* @param page 页码(从1开始)
* @param size 每页条数
* @return 商品列表及总条数
*/
public SearchResult<Product> searchProducts(String keyword, String category,
Double minPrice, Double maxPrice,
int page, int size) throws IOException {
// 构建查询条件
SearchRequest request = SearchRequest.of(b -> b
.index("product_index") // 指定索引
.query(q -> q
.bool(bool -> {
// 拼接查询条件
if (keyword != null && !keyword.isEmpty()) {
// 全文检索商品名称
bool.must(m -> m.match(t -> t.field("name").query(keyword)));
}
if (category != null && !category.isEmpty()) {
// 精确匹配分类
bool.filter(f -> f.term(t -> t.field("category").value(category)));
}
if (minPrice != null || maxPrice != null) {
// 价格范围过滤
bool.filter(f -> f.range(r -> {
if (minPrice != null) r.gte(minPrice);
if (maxPrice != null) r.lte(maxPrice);
return r.field("price");
}));
}
return bool;
})
)
.sort(s -> s.field(f -> f.field("createTime").order("desc"))) // 按创建时间倒序
.from((page - 1) * size) // 起始位置(分页)
.size(size) // 每页条数
);
// 执行查询
SearchResponse<Product> response = client.search(request, Product.class);
// 处理结果
List<Product> products = new ArrayList<>();
for (Hit<Product> hit : response.hits().hits()) {
products.add(hit.source()); // 获取文档数据
}
// 总条数
TotalHits totalHits = response.hits().total();
long total = 0;
if (totalHits != null && totalHits.relation() == TotalHitsRelation.Eq) {
total = totalHits.value();
}
return new SearchResult<>(total, products);
}
// 定义结果封装类
@Data
public static class SearchResult<T> {
private long total; // 总条数
private List<T> data; // 数据列表
public SearchResult(long total, List<T> data) {
this.total = total;
this.data = data;
}
}
}
这个方法包含了 ES 查询的核心场景:
- 多条件组合:用bool查询拼接 “必须满足(must)” 和 “过滤(filter)” 条件;
- 全文检索:match查询用于商品名称的关键词搜索;
- 精确匹配与范围过滤:term查询匹配分类,range查询过滤价格;
- 分页与排序:通过from、size实现分页,sort指定排序规则。
3.3 性能优化技巧
在实际应用中,ES 的性能优化不可忽视,尤其是数据量大或查询频繁的场景:
- 合理设计映射:
- 不需要检索的字段设置index: false(如商品详情的大文本);
- 精确匹配字段用keyword类型,避免text类型的不必要分词。
- 优化查询语句:
- 用filter代替must(filter不计算评分,且可缓存结果);
- 避免wildcard前缀匹配(如name:*手机),会导致全表扫描;
- 分页查询时,深分页(如from:10000)改用search_after。
- 索引分片优化:
- 初始分片数设置为 “节点数 ×1~3”(如 3 个节点可设 5 个分片);
- 分片大小控制在 20GB~50GB 之间,过大影响查询性能。
- 批量操作:
// 批量新增示例
BulkRequest.Builder bulk = new BulkRequest.Builder();
for (Product product : productList) {
bulk.operations(op -> op
.index(idx -> idx
.index("product_index")
.id(product.getId())
.document(product)
)
);
}
client.bulk(bulk.build());
- 批量新增 / 更新用BulkRequest,减少网络请求次数;
四、常见问题与解决方案
在 ES 应用中,新手容易遇到一些 “坑”,这里总结几个高频问题:
- 中文分词效果差:
- 原因:未安装 IK 分词器,默认分词器会把中文拆成单字;
- 解决:在 ES 插件目录安装 IK 分词器(版本与 ES 一致),并在映射中指定analyzer: "ik_max_word"。
- 查询结果与预期不符:
- 检查字段类型:如果用term查询text类型字段,可能因分词导致匹配失败(需用match查询);
- 检查分词结果:通过GET /product_index/_analyze测试字段分词效果。
- 写入 / 查询性能慢:
- 写入慢:检查分片副本数(初期可设 1 个副本)、是否开启批量写入;
- 查询慢:查看是否命中索引(通过explain分析查询计划)、是否需要增加节点或优化分片。
五、总结
Elasticsearch 是一款强大的搜索引擎,其核心价值在于全文检索和分布式扩展能力。本文从基础概念出发,通过 “理论 + 实战” 的方式讲解了 ES 的核心操作,并结合 Java 案例实现了商品搜索功能。
要熟练掌握 ES,关键在于:
- 理解索引、文档、映射等核心概念,明确text与keyword的区别;
- 掌握bool组合查询、范围查询、聚合分析等核心语法;
- 结合实际场景优化映射设计和查询语句,避免性能问题。
如果你在 ES 应用中遇到过特殊场景(如大数据量同步、复杂聚合分析),欢迎在评论区分享你的解决方案!