Elasticsearch入门与整合
核心定位:Elasticsearch(ES)是分布式全文搜索引擎,用于日志分析、商品搜索、指标监控等场景。
1. Elasticsearch 核心概念
┌─────────────────────────────────────────────┐
│ ES 核心概念 vs 关系数据库 │
├─────────────────────────────────────────────┤
│ Index(索引) ≈ Database(数据库) │
│ Mapping(映射) ≈ Schema(表结构) │
│ Document(文档) ≈ Row(行) │
│ Field(字段) ≈ Column(列) │
│ Shard(分片) ≈ Partition(分区) │
│ Replica(副本) ≈ Slave(从库) │
└─────────────────────────────────────────────┘1.1 倒排索引
正排索引(传统数据库):
文档ID → 文档内容
1 → "Java编程思想"
2 → "Java并发实战"
3 → "Python入门"
倒排索引(ES):
词项 → 文档ID列表
"Java" → [1, 2]
"编程" → [1]
"并发" → [2]
"实战" → [2]
"Python" → [3]
"入门" → [3]
搜索 "Java并发":
1. 分词 → ["Java", "并发"]
2. 查倒排索引 → "Java"→[1,2], "并发"→[2]
3. 交集 → [2]
4. 返回文档 21.2 分词器
┌─────────────────────────────────────────────┐
│ ES 分词器(Analyzer) │
├─────────────────────────────────────────────┤
│ standard(默认): │
│ - 按 Unicode 分词 │
│ - 中文逐字分词(效果差) │
│ "Java编程" → ["Java", "编", "程"] │
│ │
│ ik_smart(中文推荐): │
│ - 智能中文分词 │
│ "Java编程思想" → ["Java", "编程", "思想"] │
│ │
│ ik_max_word(最细粒度): │
│ "Java编程思想" → ["Java", "编程", "思想", │
│ "编", "程", "思", "想"] │
│ │
│ ★ 建议:索引用 ik_max_word,搜索用 ik_smart │
└─────────────────────────────────────────────┘2. Spring Boot 整合 ES
2.1 依赖与配置
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>yaml
spring:
elasticsearch:
uris: http://localhost:9200
username: elastic
password: your_password
connection-timeout: 5s
socket-timeout: 30s2.2 实体与 Mapping
java
@Document(indexName = "product", createIndex = true)
@Setting(shards = 3, replicas = 1)
public class Product {
@Id
@Field(type = FieldType.Long)
private Long id;
@Field(type = FieldType.Text, analyzer = "ik_max_word", searchAnalyzer = "ik_smart")
private String name;
@Field(type = FieldType.Keyword)
private String brand;
@Field(type = FieldType.Double)
private BigDecimal price;
@Field(type = FieldType.Integer)
private Integer stock;
@Field(type = FieldType.Keyword)
private String category;
@Field(type = FieldType.Text, analyzer = "ik_max_word")
private String description;
@Field(type = FieldType.Date, format = DateFormat.date_hour_minute_second)
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime;
@Field(type = FieldType.Boolean)
private Boolean onSale;
@Field(type = FieldType.Nested)
private List<ProductAttr> attributes;
// getter/setter...
}
@Data
public class ProductAttr {
@Field(type = FieldType.Keyword)
private String name;
@Field(type = FieldType.Keyword)
private String value;
}2.3 Repository 接口
java
public interface ProductRepository extends ElasticsearchRepository<Product, Long> {
// 方法名查询
List<Product> findByName(String name);
List<Product> findByBrandAndCategory(String brand, String category);
Page<Product> findByPriceBetween(BigDecimal min, BigDecimal max, Pageable pageable);
List<Product> findByOnSaleTrueOrderByPriceAsc(Boolean onSale);
// @Query 自定义查询
@Query("{\"bool\": {\"must\": [{\"match\": {\"name\": \"?0\"}}]}}")
Page<Product> searchByName(String keyword, Pageable pageable);
}3. DSL 查询实战
java
@Service
public class ProductSearchService {
@Autowired
private ElasticsearchOperations esOperations;
@Autowired
private ProductRepository productRepository;
/**
* ★ 复杂搜索:关键词 + 分类 + 价格范围 + 排序 + 分页
*/
public SearchHits<Product> search(ProductSearchQuery query) {
NativeQueryBuilder builder = NativeQuery.builder();
// 1. bool 查询
BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
// 关键词搜索(在 name 和 description 中搜索)
if (StringUtils.hasText(query.getKeyword())) {
boolQuery.must(QueryBuilders.multiMatchQuery(query.getKeyword())
.field("name", 3.0f) // name 权重更高
.field("description", 1.0f)
.type(MultiMatchQueryBuilder.Type.BEST_FIELDS));
}
// 精确匹配
if (query.getBrand() != null) {
boolQuery.filter(QueryBuilders.termQuery("brand", query.getBrand()));
}
if (query.getCategory() != null) {
boolQuery.filter(QueryBuilders.termQuery("category", query.getCategory()));
}
// 价格范围
if (query.getMinPrice() != null || query.getMaxPrice() != null) {
RangeQueryBuilder rangeQuery = QueryBuilders.rangeQuery("price");
if (query.getMinPrice() != null) rangeQuery.gte(query.getMinPrice().doubleValue());
if (query.getMaxPrice() != null) rangeQuery.lte(query.getMaxPrice().doubleValue());
boolQuery.filter(rangeQuery);
}
// 上架状态
boolQuery.filter(QueryBuilders.termQuery("onSale", true));
builder.withQuery(boolQuery);
// 2. 排序
if ("priceAsc".equals(query.getSort())) {
builder.withSort(Sort.by(Sort.Direction.ASC, "price"));
} else if ("priceDesc".equals(query.getSort())) {
builder.withSort(Sort.by(Sort.Direction.DESC, "price"));
} else {
// 默认按相关度排序
builder.withSort(Sort.by(ScoreSortBuilder.NAME));
}
// 3. 分页
builder.withPageable(PageRequest.of(query.getPage() - 1, query.getSize()));
// 4. 高亮
if (StringUtils.hasText(query.getKeyword())) {
builder.withHighlightQuery(new HighlightQuery(
Highlight.builder()
.field("name")
.field("description")
.preTags("<em style='color:red'>")
.postTags("</em>")
.fragmentSize(100)
.numberOfFragments(1)
.build(),
Product.class));
}
// 5. 聚合(分类统计)
builder.withAggregation("category_agg",
Aggregation.of(a -> a.terms(t -> t.field("category").size(10))));
builder.withAggregation("brand_agg",
Aggregation.of(a -> a.terms(t -> t.field("brand").size(20))));
builder.withAggregation("price_stats",
Aggregation.of(a -> a.stats(s -> s.field("price"))));
return esOperations.search(builder.build(), Product.class);
}
/**
* 自动补全(搜索建议)
*/
public List<String> suggest(String prefix) {
NativeQuery query = NativeQuery.builder()
.withQuery(QueryBuilders.matchQuery("name", prefix))
.withSuggester(Suggester.of(s -> s
.suggest("name_suggest", sg -> sg
.prefix(prefix)
.completion(c -> c
.field("name_suggest")
.size(10)
.skipDuplicates(true)))))
.build();
SearchHits<Product> hits = esOperations.search(query, Product.class);
return hits.getSuggest().getSuggestion("name_suggest")
.getEntries().stream()
.flatMap(e -> e.getOptions().stream())
.map(o -> o.getText())
.collect(Collectors.toList());
}
}4. 与 MySQL 数据同步
┌─────────────────────────────────────────────┐
│ ES 与 MySQL 同步方案 │
├─────────────────────────────────────────────┤
│ 1. 同步双写(最简单) │
│ - 写 MySQL 后同步写 ES │
│ - 缺点:耦合、性能影响 │
│ │
│ 2. ★ 异步同步(MQ) │
│ - 写 MySQL 后发 MQ 消息 │
│ - 消费者异步写 ES │
│ - 优点:解耦、高性能 │
│ │
│ 3. ★ Binlog 订阅(推荐) │
│ - Canal 监听 MySQL binlog │
│ - 自动同步到 ES │
│ - 优点:零侵入 │
│ │
│ 4. 定时全量同步 │
│ - 定时任务全量拉取 MySQL 写 ES │
│ - 适合数据量小的场景 │
└─────────────────────────────────────────────┘java
// 异步同步方案
@Service
public class ProductSyncService {
@Autowired
private ProductMapper productMapper;
@Autowired
private ProductRepository productRepository;
@Autowired
private RocketMQTemplate rocketMQTemplate;
/**
* 商品更新后异步同步到 ES
*/
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
@Async
public void onProductUpdated(ProductUpdatedEvent event) {
Product product = productMapper.selectById(event.getProductId());
productRepository.save(product);
}
/**
* 批量同步(初始化或重建索引)
*/
public void bulkSync() {
int page = 1;
int size = 1000;
while (true) {
List<Product> products = productMapper.selectPage(page, size);
if (products.isEmpty()) break;
List<IndexQuery> queries = products.stream()
.map(p -> new IndexQueryBuilder()
.withId(String.valueOf(p.getId()))
.withObject(p)
.build())
.collect(Collectors.toList());
productRepository.saveAll(products);
// 或批量索引
esOperations.bulkIndex(queries, Product.class);
if (products.size() < size) break;
page++;
}
}
}5. 常见问题
┌─────────────────────────────────────────────┐
│ ES 常见问题 │
├─────────────────────────────────────────────┤
│ 1. 深分页问题 │
│ - from + size 超过 10000 会报错 │
│ - ★ 使用 search_after 或 scroll API │
│ │
│ 2. 字段类型选择 │
│ - Keyword:精确匹配、聚合、排序 │
│ - Text:全文搜索、分词 │
│ - ★ 不要用 Text 做精确匹配 │
│ │
│ 3. 映射不可变 │
│ - 字段类型一旦创建不可修改 │
│ - 修改需要 reindex(重建索引) │
│ │
│ 4. 中文分词 │
│ - ★ 安装 IK 分词器插件 │
│ - 索引用 ik_max_word,搜索用 ik_smart │
│ │
│ 5. 内存设置 │
│ - JVM 堆内存 = 物理内存的 50% │
│ - 不要超过 32GB(指针压缩失效) │
└─────────────────────────────────────────────┘关联笔记
- [[后端/数据库/MySQL/索引与执行计划]]:MySQL 索引原理
- [[后端/SpringBoot/整合数据访问层]]:Spring Boot 数据访问
- [[后端/消息队列/RocketMQ]]:MQ 异步同步方案
练习:把「Elasticsearch入门与整合」变成可验证能力
- 从本文任选一个示例,在可丢弃环境重跑;记录运行时/服务版本、输入、关键输出和清理动作。
- 保持其他条件不变,只制造一个边界:空值、重复请求、并发、超时、断连、权限不足或资源耗尽。
- 用四句话复盘:现象是什么、原理在哪一层、证据是什么、生产中如何止损。
验收清单
- [ ] 示例能从空环境重复执行,必要依赖和版本已写明。
- [ ] 同时保留成功路径与至少一个失败路径,不用“应该没问题”冒充证据。
- [ ] 结论写明适用边界,并能链接回对应学习路线的能力域。
- [ ] 练习结束后已清理临时数据、凭据、端口或进程。
对照 中间件练习答案与复盘尺。先交自己的证据再翻答案;答案是镜子,不是传送门。