🎯 Elasticsearch简介
Elasticsearch是一个基于Lucene的分布式搜索和分析引擎,能够快速地存储、搜索和分析大量数据。它提供了RESTful API,支持多种查询方式,广泛应用于全文搜索、日志分析、实时数据分析等场景。
核心概念
- Index: 索引,类似于数据库中的数据库
- Type: 类型,类似于数据库中的表(ES 7.x后已废弃)
- Document: 文档,类似于数据库中的行记录
- Field: 字段,类似于数据库中的列
- Mapping: 映射,定义文档字段的数据类型和索引方式
- Shard: 分片,索引的物理分割单元
- Replica: 副本,分片的备份
🚀 快速开始
1. 添加依赖
xml
<dependencies>
<!-- SpringBoot Starter Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- SpringBoot Elasticsearch Starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
<!-- JSON处理 -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<!-- 测试依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
2. Elasticsearch配置
yaml
spring:
# Elasticsearch配置
elasticsearch:
# Elasticsearch服务器地址
uris: http://localhost:9200
# 用户名(如果启用了安全认证)
username: elastic
# 密码(如果启用了安全认证)
password: password
# 连接超时时间
connection-timeout: 10s
# 读取超时时间
socket-timeout: 30s
# 数据源配置(用于对比)
datasource:
url: jdbc:mysql://localhost:3306/demo_db?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
# 日志配置
logging:
level:
org.springframework.data.elasticsearch: DEBUG
org.elasticsearch: DEBUG
🔧 Elasticsearch配置类
java
package com.example.demo.config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.elasticsearch.client.ClientConfiguration;
import org.springframework.data.elasticsearch.client.elc.ElasticsearchConfiguration;
import org.springframework.data.elasticsearch.repository.config.EnableElasticsearchRepositories;
@Configuration
@EnableElasticsearchRepositories(basePackages = "com.example.demo.repository")
public class ElasticsearchConfig extends ElasticsearchConfiguration {
@Value("${spring.elasticsearch.uris}")
private String elasticsearchUrl;
@Value("${spring.elasticsearch.username:}")
private String username;
@Value("${spring.elasticsearch.password:}")
private String password;
@Override
public ClientConfiguration clientConfiguration() {
ClientConfiguration.Builder builder = ClientConfiguration.builder()
.connectedTo(elasticsearchUrl.replace("http://", ""))
.withConnectTimeout(10000)
.withSocketTimeout(30000);
// 如果配置了用户名和密码
if (!username.isEmpty() && !password.isEmpty()) {
builder.withBasicAuth(username, password);
}
return builder.build();
}
}
📊 文档实体类
1. 产品文档
java
package com.example.demo.document;
import com.fasterxml.jackson.annotation.JsonFormat;
import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.annotations.*;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
@Document(indexName = "products")
@Setting(numberOfShards = 1, numberOfReplicas = 0)
public class ProductDocument {
@Id
private String id;
@Field(type = FieldType.Text, analyzer = "ik_max_word", searchAnalyzer = "ik_smart")
private String name;
@Field(type = FieldType.Text, analyzer = "ik_max_word", searchAnalyzer = "ik_smart")
private String description;
@Field(type = FieldType.Keyword)
private String category;
@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 List<String> tags;
@Field(type = FieldType.Boolean)
private Boolean available;
@Field(type = FieldType.Double)
private Double rating;
@Field(type = FieldType.Integer)
private Integer salesCount;
@Field(type = FieldType.Date, format = DateFormat.date_hour_minute_second)
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime;
@Field(type = FieldType.Date, format = DateFormat.date_hour_minute_second)
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime updateTime;
// 构造函数
public ProductDocument() {}
public ProductDocument(String name, String description, String category, String brand, BigDecimal price) {
this.name = name;
this.description = description;
this.category = category;
this.brand = brand;
this.price = price;
this.available = true;
this.rating = 0.0;
this.salesCount = 0;
this.createTime = LocalDateTime.now();
this.updateTime = LocalDateTime.now();
}
// Getter和Setter方法
public String getId() { return id; }
public void setId(String id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getDescription() { return description; }
public void setDescription(String description) { this.description = description; }
public String getCategory() { return category; }
public void setCategory(String category) { this.category = category; }
public String getBrand() { return brand; }
public void setBrand(String brand) { this.brand = brand; }
public BigDecimal getPrice() { return price; }
public void setPrice(BigDecimal price) { this.price = price; }
public Integer getStock() { return stock; }
public void setStock(Integer stock) { this.stock = stock; }
public List<String> getTags() { return tags; }
public void setTags(List<String> tags) { this.tags = tags; }
public Boolean getAvailable() { return available; }
public void setAvailable(Boolean available) { this.available = available; }
public Double getRating() { return rating; }
public void setRating(Double rating) { this.rating = rating; }
public Integer getSalesCount() { return salesCount; }
public void setSalesCount(Integer salesCount) { this.salesCount = salesCount; }
public LocalDateTime getCreateTime() { return createTime; }
public void setCreateTime(LocalDateTime createTime) { this.createTime = createTime; }
public LocalDateTime getUpdateTime() { return updateTime; }
public void setUpdateTime(LocalDateTime updateTime) { this.updateTime = updateTime; }
@Override
public String toString() {
return "ProductDocument{" +
"id='" + id + '\'' +
", name='" + name + '\'' +
", description='" + description + '\'' +
", category='" + category + '\'' +
", brand='" + brand + '\'' +
", price=" + price +
", stock=" + stock +
", available=" + available +
", rating=" + rating +
", salesCount=" + salesCount +
'}';
}
}
2. 文章文档
java
package com.example.demo.document;
import com.fasterxml.jackson.annotation.JsonFormat;
import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.annotations.*;
import java.time.LocalDateTime;
import java.util.List;
@Document(indexName = "articles")
@Setting(numberOfShards = 1, numberOfReplicas = 0)
public class ArticleDocument {
@Id
private String id;
@Field(type = FieldType.Text, analyzer = "ik_max_word", searchAnalyzer = "ik_smart")
private String title;
@Field(type = FieldType.Text, analyzer = "ik_max_word", searchAnalyzer = "ik_smart")
private String content;
@Field(type = FieldType.Text, analyzer = "ik_max_word", searchAnalyzer = "ik_smart")
private String summary;
@Field(type = FieldType.Keyword)
private String author;
@Field(type = FieldType.Keyword)
private String category;
@Field(type = FieldType.Keyword)
private List<String> tags;
@Field(type = FieldType.Integer)
private Integer viewCount;
@Field(type = FieldType.Integer)
private Integer likeCount;
@Field(type = FieldType.Boolean)
private Boolean published;
@Field(type = FieldType.Date, format = DateFormat.date_hour_minute_second)
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime;
@Field(type = FieldType.Date, format = DateFormat.date_hour_minute_second)
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime updateTime;
// 构造函数
public ArticleDocument() {}
public ArticleDocument(String title, String content, String author, String category) {
this.title = title;
this.content = content;
this.author = author;
this.category = category;
this.published = false;
this.viewCount = 0;
this.likeCount = 0;
this.createTime = LocalDateTime.now();
this.updateTime = LocalDateTime.now();
}
// Getter和Setter方法
public String getId() { return id; }
public void setId(String id) { this.id = id; }
public String getTitle() { return title; }
public void setTitle(String title) { this.title = title; }
public String getContent() { return content; }
public void setContent(String content) { this.content = content; }
public String getSummary() { return summary; }
public void setSummary(String summary) { this.summary = summary; }
public String getAuthor() { return author; }
public void setAuthor(String author) { this.author = author; }
public String getCategory() { return category; }
public void setCategory(String category) { this.category = category; }
public List<String> getTags() { return tags; }
public void setTags(List<String> tags) { this.tags = tags; }
public Integer getViewCount() { return viewCount; }
public void setViewCount(Integer viewCount) { this.viewCount = viewCount; }
public Integer getLikeCount() { return likeCount; }
public void setLikeCount(Integer likeCount) { this.likeCount = likeCount; }
public Boolean getPublished() { return published; }
public void setPublished(Boolean published) { this.published = published; }
public LocalDateTime getCreateTime() { return createTime; }
public void setCreateTime(LocalDateTime createTime) { this.createTime = createTime; }
public LocalDateTime getUpdateTime() { return updateTime; }
public void setUpdateTime(LocalDateTime updateTime) { this.updateTime = updateTime; }
}
🔍 Repository接口
1. 产品Repository
java
package com.example.demo.repository;
import com.example.demo.document.ProductDocument;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.elasticsearch.annotations.Query;
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;
import org.springframework.stereotype.Repository;
import java.math.BigDecimal;
import java.util.List;
@Repository
public interface ProductRepository extends ElasticsearchRepository<ProductDocument, String> {
// 根据名称查找产品
List<ProductDocument> findByName(String name);
// 根据名称模糊查找产品
List<ProductDocument> findByNameContaining(String name);
// 根据分类查找产品
List<ProductDocument> findByCategory(String category);
// 根据品牌查找产品
List<ProductDocument> findByBrand(String brand);
// 根据价格范围查找产品
List<ProductDocument> findByPriceBetween(BigDecimal minPrice, BigDecimal maxPrice);
// 根据可用性查找产品
List<ProductDocument> findByAvailable(Boolean available);
// 根据评分范围查找产品
List<ProductDocument> findByRatingGreaterThanEqual(Double rating);
// 复合查询:根据分类和价格范围查找产品
List<ProductDocument> findByCategoryAndPriceBetween(String category, BigDecimal minPrice, BigDecimal maxPrice);
// 分页查询:根据分类查找产品
Page<ProductDocument> findByCategory(String category, Pageable pageable);
// 自定义查询:多字段搜索
@Query("{\"multi_match\": {\"query\": \"?0\", \"fields\": [\"name^2\", \"description\", \"category\", \"brand\"]}}")
List<ProductDocument> searchByMultiFields(String keyword);
// 自定义查询:根据标签搜索
@Query("{\"terms\": {\"tags\": [\"?0\"]}}")
List<ProductDocument> findByTag(String tag);
// 自定义查询:热门产品(根据销量和评分)
@Query("{\"bool\": {\"must\": [{\"range\": {\"salesCount\": {\"gte\": ?0}}}, {\"range\": {\"rating\": {\"gte\": ?1}}}]}}")
List<ProductDocument> findPopularProducts(Integer minSales, Double minRating);
}
2. 文章Repository
java
package com.example.demo.repository;
import com.example.demo.document.ArticleDocument;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.elasticsearch.annotations.Query;
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface ArticleRepository extends ElasticsearchRepository<ArticleDocument, String> {
// 根据标题查找文章
List<ArticleDocument> findByTitleContaining(String title);
// 根据作者查找文章
List<ArticleDocument> findByAuthor(String author);
// 根据分类查找文章
List<ArticleDocument> findByCategory(String category);
// 根据发布状态查找文章
List<ArticleDocument> findByPublished(Boolean published);
// 根据作者和发布状态查找文章
List<ArticleDocument> findByAuthorAndPublished(String author, Boolean published);
// 分页查询:根据分类查找已发布文章
Page<ArticleDocument> findByCategoryAndPublished(String category, Boolean published, Pageable pageable);
// 自定义查询:全文搜索
@Query("{\"multi_match\": {\"query\": \"?0\", \"fields\": [\"title^3\", \"content\", \"summary^2\"]}}")
List<ArticleDocument> searchByContent(String keyword);
// 自定义查询:根据标签搜索
@Query("{\"terms\": {\"tags\": [\"?0\"]}}")
List<ArticleDocument> findByTag(String tag);
// 自定义查询:热门文章
@Query("{\"bool\": {\"must\": [{\"term\": {\"published\": true}}, {\"range\": {\"viewCount\": {\"gte\": ?0}}}], \"should\": [{\"range\": {\"likeCount\": {\"gte\": ?1}}}]}}")
List<ArticleDocument> findPopularArticles(Integer minViews, Integer minLikes);
}
🏗️ Service层实现
1. 产品搜索服务
java
package com.example.demo.service;
import com.example.demo.document.ProductDocument;
import com.example.demo.repository.ProductRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.elasticsearch.core.ElasticsearchOperations;
import org.springframework.data.elasticsearch.core.SearchHit;
import org.springframework.data.elasticsearch.core.SearchHits;
import org.springframework.data.elasticsearch.core.query.NativeSearchQuery;
import org.springframework.data.elasticsearch.core.query.NativeSearchQueryBuilder;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import static org.elasticsearch.index.query.QueryBuilders.*;
@Service
public class ProductSearchService {
@Autowired
private ProductRepository productRepository;
@Autowired
private ElasticsearchOperations elasticsearchOperations;
/**
* 保存产品
*/
public ProductDocument save(ProductDocument product) {
return productRepository.save(product);
}
/**
* 根据ID查找产品
*/
public Optional<ProductDocument> findById(String id) {
return productRepository.findById(id);
}
/**
* 查找所有产品
*/
public Iterable<ProductDocument> findAll() {
return productRepository.findAll();
}
/**
* 删除产品
*/
public void deleteById(String id) {
productRepository.deleteById(id);
}
/**
* 根据名称搜索产品
*/
public List<ProductDocument> searchByName(String name) {
return productRepository.findByNameContaining(name);
}
/**
* 根据分类搜索产品
*/
public List<ProductDocument> searchByCategory(String category) {
return productRepository.findByCategory(category);
}
/**
* 根据价格范围搜索产品
*/
public List<ProductDocument> searchByPriceRange(BigDecimal minPrice, BigDecimal maxPrice) {
return productRepository.findByPriceBetween(minPrice, maxPrice);
}
/**
* 多字段搜索
*/
public List<ProductDocument> multiFieldSearch(String keyword) {
return productRepository.searchByMultiFields(keyword);
}
/**
* 分页搜索产品
*/
public Page<ProductDocument> searchByCategory(String category, int page, int size) {
Pageable pageable = PageRequest.of(page, size, Sort.by("createTime").descending());
return productRepository.findByCategory(category, pageable);
}
/**
* 复杂搜索:使用ElasticsearchOperations
*/
public List<ProductDocument> complexSearch(String keyword, String category, BigDecimal minPrice, BigDecimal maxPrice) {
NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
// 构建复合查询
var boolQuery = boolQuery();
// 关键词搜索
if (keyword != null && !keyword.isEmpty()) {
boolQuery.must(multiMatchQuery(keyword, "name", "description", "brand"));
}
// 分类过滤
if (category != null && !category.isEmpty()) {
boolQuery.filter(termQuery("category", category));
}
// 价格范围过滤
if (minPrice != null && maxPrice != null) {
boolQuery.filter(rangeQuery("price").gte(minPrice).lte(maxPrice));
}
// 只搜索可用产品
boolQuery.filter(termQuery("available", true));
NativeSearchQuery searchQuery = queryBuilder
.withQuery(boolQuery)
.withSort(Sort.by("salesCount").descending())
.withSort(Sort.by("rating").descending())
.build();
SearchHits<ProductDocument> searchHits = elasticsearchOperations.search(searchQuery, ProductDocument.class);
return searchHits.stream()
.map(SearchHit::getContent)
.collect(Collectors.toList());
}
/**
* 搜索热门产品
*/
public List<ProductDocument> searchPopularProducts(Integer minSales, Double minRating) {
return productRepository.findPopularProducts(minSales, minRating);
}
/**
* 根据标签搜索产品
*/
public List<ProductDocument> searchByTag(String tag) {
return productRepository.findByTag(tag);
}
/**
* 聚合搜索:按分类统计产品数量
*/
public void aggregateByCategory() {
// 这里可以实现聚合查询的逻辑
// 由于篇幅限制,这里只是一个示例方法
}
}
🎮 Controller层
java
package com.example.demo.controller;
import com.example.demo.document.ProductDocument;
import com.example.demo.service.ProductSearchService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.math.BigDecimal;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
@RestController
@RequestMapping("/api/search")
@CrossOrigin(origins = "*")
public class SearchController {
@Autowired
private ProductSearchService productSearchService;
/**
* 创建产品
*/
@PostMapping("/products")
public ResponseEntity<ProductDocument> createProduct(@RequestBody ProductDocument product) {
ProductDocument savedProduct = productSearchService.save(product);
return ResponseEntity.ok(savedProduct);
}
/**
* 根据ID获取产品
*/
@GetMapping("/products/{id}")
public ResponseEntity<ProductDocument> getProduct(@PathVariable String id) {
Optional<ProductDocument> product = productSearchService.findById(id);
return product.map(ResponseEntity::ok).orElse(ResponseEntity.notFound().build());
}
/**
* 获取所有产品
*/
@GetMapping("/products")
public ResponseEntity<Iterable<ProductDocument>> getAllProducts() {
Iterable<ProductDocument> products = productSearchService.findAll();
return ResponseEntity.ok(products);
}
/**
* 根据名称搜索产品
*/
@GetMapping("/products/search/name")
public ResponseEntity<List<ProductDocument>> searchByName(@RequestParam String name) {
List<ProductDocument> products = productSearchService.searchByName(name);
return ResponseEntity.ok(products);
}
/**
* 根据分类搜索产品
*/
@GetMapping("/products/search/category")
public ResponseEntity<List<ProductDocument>> searchByCategory(@RequestParam String category) {
List<ProductDocument> products = productSearchService.searchByCategory(category);
return ResponseEntity.ok(products);
}
/**
* 根据价格范围搜索产品
*/
@GetMapping("/products/search/price")
public ResponseEntity<List<ProductDocument>> searchByPriceRange(
@RequestParam BigDecimal minPrice,
@RequestParam BigDecimal maxPrice) {
List<ProductDocument> products = productSearchService.searchByPriceRange(minPrice, maxPrice);
return ResponseEntity.ok(products);
}
/**
* 多字段搜索
*/
@GetMapping("/products/search/multi")
public ResponseEntity<List<ProductDocument>> multiFieldSearch(@RequestParam String keyword) {
List<ProductDocument> products = productSearchService.multiFieldSearch(keyword);
return ResponseEntity.ok(products);
}
/**
* 分页搜索
*/
@GetMapping("/products/search/page")
public ResponseEntity<Page<ProductDocument>> searchByPage(
@RequestParam String category,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size) {
Page<ProductDocument> products = productSearchService.searchByCategory(category, page, size);
return ResponseEntity.ok(products);
}
/**
* 复杂搜索
*/
@GetMapping("/products/search/complex")
public ResponseEntity<List<ProductDocument>> complexSearch(
@RequestParam(required = false) String keyword,
@RequestParam(required = false) String category,
@RequestParam(required = false) BigDecimal minPrice,
@RequestParam(required = false) BigDecimal maxPrice) {
List<ProductDocument> products = productSearchService.complexSearch(keyword, category, minPrice, maxPrice);
return ResponseEntity.ok(products);
}
/**
* 搜索热门产品
*/
@GetMapping("/products/search/popular")
public ResponseEntity<List<ProductDocument>> searchPopularProducts(
@RequestParam(defaultValue = "100") Integer minSales,
@RequestParam(defaultValue = "4.0") Double minRating) {
List<ProductDocument> products = productSearchService.searchPopularProducts(minSales, minRating);
return ResponseEntity.ok(products);
}
/**
* 根据标签搜索产品
*/
@GetMapping("/products/search/tag")
public ResponseEntity<List<ProductDocument>> searchByTag(@RequestParam String tag) {
List<ProductDocument> products = productSearchService.searchByTag(tag);
return ResponseEntity.ok(products);
}
/**
* 删除产品
*/
@DeleteMapping("/products/{id}")
public ResponseEntity<Map<String, String>> deleteProduct(@PathVariable String id) {
productSearchService.deleteById(id);
Map<String, String> response = new HashMap<>();
response.put("status", "success");
response.put("message", "产品删除成功");
return ResponseEntity.ok(response);
}
}
📊 最佳实践
1. 索引设计
- 合理设置分片和副本数量
- 选择合适的字段类型
- 使用合适的分析器
- 定期优化索引
2. 查询优化
- 使用过滤器而非查询进行精确匹配
- 合理使用聚合查询
- 避免深度分页
- 使用缓存提高性能
3. 数据同步
- 实现数据库与ES的同步机制
- 处理数据一致性问题
- 监控同步状态
- 实现故障恢复
本文关键词: Elasticsearch, 全文搜索, 分布式搜索, 数据分析, 实时搜索, Lucene