背景
解决了什么问题:解决海量数据下的快速检索、模糊查询、分词搜索、聚合统计问题。
如何解决的:分词+倒排索引
为什么这么解决:倒排索引是全文检索最高效的结构,比数据库 like / 正则快几个数量级
对比mysql

文档是可以搜索的最小单元
ES + kibana
这里是docker容器启动的。
dart
version: '3.8' # Compose文件版本(与Docker版本兼容即可)
services:
# 1. ZooKeeper(Kafka依赖,用于集群协调)
zookeeper:
image: confluentinc/cp-zookeeper:7.3.0 # 稳定版本镜像
container_name: zookeeper
environment:
ZOOKEEPER_CLIENT_PORT: 2181 # 容器内端口
ZOOKEEPER_TICK_TIME: 2000 # 心跳间隔
ports:
- "2181:2181" # 宿主机端口:容器内端口(IDEA通过宿主机端口访问)
restart: always # 容器异常时自动重启
networks:
- app-network # 加入自定义网络
# 2. Kafka(消息队列)
kafka:
image: confluentinc/cp-kafka:7.3.0
container_name: kafka
depends_on:
- zookeeper # 依赖ZooKeeper,启动顺序:先启动ZooKeeper
environment:
KAFKA_BROKER_ID: 1
KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181 # 连接容器内的ZooKeeper(用容器名作为 hostname)
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:9093,PLAINTEXT_HOST://localhost:9092
# 关键配置:PLAINTEXT_HOST映射到宿主机localhost,Java客户端通过 localhost:9092 连接
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
ports:
- "9092:9092" # Kafka宿主机访问端口
restart: always
networks:
- app-network # 加入自定义网络
# 3. Elasticsearch(搜索引擎)
elasticsearch:
image: elasticsearch:7.17.0 # 稳定版本(避免用8.x,默认开启HTTPS,新手易踩坑)
container_name: elasticsearch
environment:
- discovery.type=single-node # 单节点模式(开发环境用)
- ES_JAVA_OPTS=-Xms512m -Xmx512m # 限制JVM内存(避免占满宿主机内存)
- xpack.security.enabled=false # 关闭安全验证(开发环境简化配置)
- http.cors.enabled=true # 开启跨域(elasticvue需要)
- http.cors.allow-origin=* # 允许所有来源跨域(开发环境)
ports:
- "9200:9200" # ES HTTP访问端口(IDEA/Java客户端连接用)
- "9300:9300" # ES集群通信端口(单节点可忽略)
volumes:
- es-data:/usr/share/elasticsearch/data # 数据持久化(容器删除后数据不丢失)
restart: always
networks:
- app-network # 加入自定义网络
# 4. Redis 服务(开发环境配置,带密码+数据持久化)
redis:
image: redis:6.2-alpine # 轻量稳定版本(Alpine镜像体积更小)
container_name: redis
environment:
- REDIS_PASSWORD=123456 # 设置Redis密码(开发环境简化,可自定义)
- REDIS_MAXMEMORY=512mb # 限制最大内存(避免占满宿主机)
- REDIS_MAXMEMORY_POLICY=allkeys-lru # 内存满时淘汰策略(删除最近最少使用的key)
command: redis-server --requirepass 123456 --appendonly yes # 开启AOF持久化(数据不丢失)
ports:
- "6379:6379" # 宿主机端口映射(Java项目通过 localhost:6379 访问)
volumes:
- redis-data:/data # 数据持久化目录(容器删除后数据保留)
restart: always # 异常自动重启
networks:
- app-network # 加入自定义网络
# 5. Kibana(Elasticsearch可视化工具)
kibana:
image: kibana:7.17.0 # 版本必须和Elasticsearch完全一致(7.17.0)
container_name: kibana
depends_on:
- elasticsearch # 依赖ES,确保ES先启动
environment:
- ELASTICSEARCH_HOSTS=http://elasticsearch:9200 # 连接容器内的ES(用容器名访问)
- XPACK_SECURITY_ENABLED=false # 关闭安全验证(和ES保持一致)
- SERVER_PORT=5601 # Kibana服务端口
ports:
- "5601:5601" # 宿主机访问端口(浏览器打开 http://localhost:5601 即可使用)
restart: always
networks:
- app-network # 加入自定义网络,和ES/其他服务互通
# 数据卷声明
volumes:
es-data:
redis-data: # 声明Redis数据卷
# 自定义网络(确保所有服务互通)
networks:
app-network:
driver: bridge # 桥接网络,默认驱动,所有服务(包括Kibana)都加入此网络
增加:
dart
# 创建索引(指定字段类型)
PUT /product_index
{
"mappings": {
"properties": {
"id": { "type": "keyword" }, # 商品ID:精确匹配,不分词
"name": { "type": "text", # 商品名称:支持全文检索
"fields": {
"keyword": { "type": "keyword" } # 子字段:用于精确匹配/聚合。也就是name字段既可以分词匹配,又可以精确匹配
}
},
"price": { "type": "double" }, # 价格:数值类型
"category": { "type": "keyword" }, # 分类:枚举值,精确匹配
"create_time": { "type": "date" } # 创建时间:日期类型
}
}
}
# 验证索引是否创建成功
GET /product_index
# 方式1:指定ID新增(PUT),ID不存在则创建,存在则覆盖
PUT /product_index/_doc/1001
{
"id": "P1001",
"name": "小米14 5G手机",
"price": 3999.0,
"category": "手机",
"create_time": "2026-02-24T10:00:00"
}
# 方式2:自动生成ID新增(POST),适合无固定ID的场景
POST /product_index/_doc
{
"id": "P1002",
"name": "华为Mate60 Pro",
"price": 6999.0,
"category": "手机",
"create_time": "2026-02-24T10:05:00"
}
# 验证新增结果(查询指定ID文档)
GET /product_index/_doc/1001
# 查询所有的数据
GET /product_index/_search
{
"query": { "match_all": {} }
}
删除:
dart
# 方式1:按ID删除单个文档
DELETE /product_index/_doc/1001
# 方式2:按条件批量删除(需先安装Delete By Query插件,ES 7.x+默认支持)
POST /product_index/_delete_by_query
{
"query": {
"term": { "category": "手机" }
}
}
改变:
dart
POST /product_index/_update/1001
{
"doc": {
"price": 3899.0,
"category": "智能手机"
}
}
# 方式2:全量覆盖(PUT),需传入所有字段,否则未传字段会丢失
PUT /product_index/_doc/1001
{
"id": "P1001"
}
查询:
dart
# 场景1:按ID精确查询
GET /product_index/_doc/1001
# 场景2:全文检索(匹配商品名称) match是分词匹配,term是精确匹配
GET /product_index/_search
{
"query": {
"match": {
"name": "小米手机" # 分词后匹配,会命中"小米14 5G手机"
}
}
}
# 场景3:精确匹配+过滤+排序+分页
GET /product_index/_search
{
"query": {
"bool": {
"must": [ { "term": { "category": "手机" } } ], # 精确匹配分类
"filter": [ { "range": { "price": { "lte": 5000 } } } ] # 过滤价格≤5000
}
},
"sort": [ { "price": { "order": "desc" } } ], # 按价格降序
"from": 0, "size": 10, # 分页:第1页,每页10条
"_source": [ "id", "name", "price" ] # 只返回指定字段
}
# 场景4:查询所有文档(慎用,大数据量会性能问题)
GET /product_index/_search
{
"query": { "match_all": {} }
}
Java api
文档对象:
dart
package com.xop.business.middleware.elasticsearch.demo2;
import lombok.Getter;
import lombok.Setter;
import org.springframework.data.annotation.Id;
import org.springframework.data.annotation.Transient;
import org.springframework.data.elasticsearch.annotations.Document;
import org.springframework.data.elasticsearch.annotations.Field;
import org.springframework.data.elasticsearch.annotations.FieldType;
import java.time.LocalDateTime;
/**
* 商品文档类
* 对应 Elasticsearch 的 product_index 索引(id字段与索引Mapping完全对齐)
*/
@Document(indexName = "product_index") // 对应ES的商品索引名
@Getter
@Setter
public class SampleDocument {
/**
* 文档ID(对应ES的_id,Spring Data ES 专用)
*/
@Id
@Transient
private String docId;
/**
* 商品ID(业务唯一标识,对应ES的id字段,keyword类型)
*/
@Field(type = FieldType.Keyword, name = "id") // 显式指定对应ES的id字段
private String id;
/**
* 商品名称(支持中文分词,同时保留keyword子字段用于精确匹配/聚合)
*/
/**
* 商品名称(支持中文分词,同时保留keyword子字段用于精确匹配/聚合)
*/
/**
* 商品名称(支持中文IK分词,同时保留keyword子字段用于精确匹配/聚合)
*/
@Field(
name = "name", // 显式指定对应ES的name字段(与AliasFor的value等效,二选一即可)
type = FieldType.Text,
analyzer = "ik_max_word" // 中文IK分词器(最大粒度拆分)
)
private String name;
/**
* 商品价格(数值类型,对应ES的price字段)
*/
@Field(type = FieldType.Double, name = "price")
private Double price;
/**
* 商品分类(枚举值,对应ES的category字段)
*/
@Field(type = FieldType.Keyword, name = "category")
private String category;
/**
* 创建时间(日期类型,对应ES的create_time字段)
*/
@Field(
type = FieldType.Date,
pattern = "yyyy-MM-dd'T'HH:mm:ss", // ES默认的日期格式
name = "create_time" // 显式指定对应ES的create_time字段
)
private LocalDateTime createTime;
/**
* 无参构造函数(Spring Data ES 序列化/反序列化需要)
*/
public SampleDocument() {
this.createTime = LocalDateTime.now(); // 默认赋值当前时间
}
/**
* 带参构造函数(快速创建对象,字段与ES索引完全对齐)
*/
public SampleDocument(String id, String name, Double price, String category) {
this.id = id; // 商品业务ID(对应ES的id字段)
this.name = name;
this.price = price;
this.category = category;
this.createTime = LocalDateTime.now(); // 自动赋值创建时间
}
/**
* 重写toString,便于日志打印和调试
*/
@Override
public String toString() {
return "ProductDocument{" +
"docId='" + docId + '\'' + // ES文档的_id
", id='" + id + '\'' + // 商品业务ID(ES的id字段)
", name='" + name + '\'' +
", price=" + price +
", category='" + category + '\'' +
", createTime=" + createTime +
'}';
}
}
dart
@Autowired
private ElasticsearchOperations elasticsearchOperations;
elasticsearchOperations.save();
elasticsearchOperations.delete();
elasticsearchOperations.search();
SampleDocument sampleDocument = elasticsearchOperations.get(id, SampleDocument.class); //根据id查询
elasticsearchOperations.delete(id, SampleDocument.class); // 根据id删除
//其他查询
public List<SampleDocument> searchDocuments(String keyword) {
Criteria criteria = new Criteria("name").matches(keyword) // 分词匹配
.or(new Criteria("category").is(keyword)); // 全量匹配
Query query = new CriteriaQuery(criteria);
SearchHits<SampleDocument> searchHits = elasticsearchOperations.search(query, SampleDocument.class);
return searchHits.stream()
.map(SearchHit::getContent)
.collect(Collectors.toList());
}