Elastic Stack简介
官方文档:https://www.elastic.co/cn/
Elastic Stack的组件
Elastic Stack核心产品包括 Elasticsearch、Kibana、Beats 和 Logstash(也称为 ELK Stack)等等。能够安全可靠地从任何来源获取任何格式的数据,然后对数据进行搜索、分析和可视化。
而Elasticsearch是Elastic Stack核心的核心。
Elasticsearch 是一个分布式、RESTful 风格的搜索和数据分析引擎,能够解决不断涌现出的各种用例。作为 Elastic Stack 的核心,Elasticsearch 会集中存储您的数据,让您飞快完成搜索,微调相关性,进行强大的分析,并轻松缩放规模。
Kibana 可以将数据转变为结果、响应和解决方案,是 Elastic Stack 的可视化工具。使用 Kibana 针对大规模数据快速运行数据分析,以实现可观测性、安全和搜索。对来自任何来源的任何数据进行全面透彻的分析,从威胁情报到搜索分析,从日志到应用程序监测,不一而足。 我赞同Kibana文档中所说的一句话“一张图片胜过千万行日志”。
Logstash 是 Elastic Stack 的数据收集和转换工具,用于将不同来源的数据导入 Elasticsearch。
Beats 是轻量级的数据采集器,用于采集各种类型的数据并将其发送到 Elasticsearch 或 Logstash。
Elasticsearch快速入门
MySQL 与 Elasticsearch 的概念简单类比
倒排索引和正排索引
倒排索引是用于提高数据检索速度的一种数据结构,空间消耗比较大。倒排索引首先将检索文档进行分词得到多个词语/词条,然后将词语和文档 ID 建立关联,从而提高检索效率。
正排索引将文档 ID 和分词建立关联,根据词语查询时,必须先逐条获取每个文档,然后判断文档中是否包含所需要的词语,查询效率较低。
Java操作Elasticsearch
Spring Data Elasticsearch
官方文档:https://spring.io/projects/spring-data-elasticsearch/
集成与配置的基本示例
1.添加以下依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
2.配置 Elasticsearch 连接
spring:
data:
elasticsearch:
cluster-name: elasticsearch
cluster-nodes: localhost:9200
3.创建实体类
import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.annotations.Document;
@Document(indexName = "my_index", type = "my_type")
public class MyEntity {
@Id
private String id;
private String name;
private String description;
// 省略构造函数和 getter/setter 方法
}
4.创建 Elasticsearch Repository
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;
public interface MyEntityRepository extends ElasticsearchRepository<MyEntity, String> {
// 可以添加自定义查询方法
}
5.使用 Spring Data Elasticsearch
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class MyEntityService {
private final MyEntityRepository repository;
@Autowired
public MyEntityService(MyEntityRepository repository) {
this.repository = repository;
}
public MyEntity save(MyEntity entity) {
return repository.save(entity);
}
public MyEntity findById(String id) {
return repository.findById(id).orElse(null);
}
public List<MyEntity> findByName(String name) {
return repository.findByName(name);
}
// 其他操作...
}
Elasticsearch Java API Client
官方文档:https://www.elastic.co/guide/en/elasticsearch/client/java-api-client/current/introduction.html
集成与配置示例(学习自优秀的Elasticsearch的实践:https://docs.xxyopen.com/course/novel/4.html#%E6%90%9C%E7%B4%A2%E5%BC%95%E6%93%8E-elasticsearch-%E9%9B%86%E6%88%90%E4%B8%8E%E9%85%8D%E7%BD%AE)
1.Kibana 中创建索引
PUT /book
{
"mappings" : {
"properties" : {
"id" : {
"type" : "long"
},
"authorId" : {
"type" : "long"
},
"authorName" : {
"type" : "text",
"analyzer": "ik_smart"
},
"bookName" : {
"type" : "text",
"analyzer": "ik_smart"
},
"bookDesc" : {
"type" : "text",
"analyzer": "ik_smart"
},
"bookStatus" : {
"type" : "short"
},
"categoryId" : {
"type" : "integer"
},
"categoryName" : {
"type" : "text",
"analyzer": "ik_smart"
},
"lastChapterId" : {
"type" : "long"
},
"lastChapterName" : {
"type" : "text",
"analyzer": "ik_smart"
},
"lastChapterUpdateTime" : {
"type": "long"
},
"picUrl" : {
"type" : "keyword",
"index" : false,
"doc_values" : false
},
"score" : {
"type" : "integer"
},
"wordCount" : {
"type" : "integer"
},
"workDirection" : {
"type" : "short"
},
"visitCount" : {
"type": "long"
}
}
}
}
2.项目添加依赖
<dependencies>
<dependency>
<groupId>co.elastic.clients</groupId>
<artifactId>elasticsearch-java</artifactId>
<version>8.2.0</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.12.3</version>
</dependency>
</dependencies>
3.application.yml 中配置连接信息
spring:
elasticsearch:
uris:
- https://my-deployment-ce7ca3.es.us-central1.gcp.cloud.es.io:9243
username: elastic
password: qTjgYVKSuExX
4.配置 JacksonJsonpMapper
/**
* elasticsearch 相关配置
*/
@Configuration
public class EsConfig {
/**
* 解决 ElasticsearchClientConfigurations 修改默认 ObjectMapper 配置的问题
*/
@Bean
JacksonJsonpMapper jacksonJsonpMapper() {
return new JacksonJsonpMapper();
}
}
5.新建搜索服务类
/**
* 搜索 服务类
*/
public interface SearchService {
/**
* 小说搜索
*
* @param condition 搜索条件
* @return 搜索结果
*/
RestResp<PageRespDto<BookInfoRespDto>> searchBooks(BookSearchReqDto condition);
}
6.新建数据库搜索服务实现类
/**
* Elasticsearch 搜索 服务实现类
*/
@ConditionalOnProperty(prefix = "spring.elasticsearch", name = "enable", havingValue = "true")
@Service
@RequiredArgsConstructor
@Slf4j
public class EsSearchServiceImpl implements SearchService {
private final ElasticsearchClient esClient;
@SneakyThrows
@Override
public RestResp<PageRespDto<BookInfoRespDto>> searchBooks(BookSearchReqDto condition) {
SearchResponse<EsBookDto> response = esClient.search(s -> {
SearchRequest.Builder searchBuilder = s.index(EsConsts.IndexEnum.BOOK.getName());
buildSearchCondition(condition, searchBuilder);
// 排序
if (!StringUtils.isBlank(condition.getSort())) {
searchBuilder.sort(o ->
o.field(f -> f.field(StringUtils
.underlineToCamel(condition.getSort().split(" ")[0]))
.order(SortOrder.Desc))
);
}
// 分页
searchBuilder.from((condition.getPageNum() - 1) * condition.getPageSize())
.size(condition.getPageSize());
return searchBuilder;
},
EsBookDto.class
);
TotalHits total = response.hits().total();
List<BookInfoRespDto> list = new ArrayList<>();
List<Hit<EsBookDto>> hits = response.hits().hits();
for (Hit<EsBookDto> hit : hits) {
EsBookDto book = hit.source();
assert book != null;
list.add(BookInfoRespDto.builder()
.id(book.getId())
.bookName(book.getBookName())
.categoryId(book.getCategoryId())
.categoryName(book.getCategoryName())
.authorId(book.getAuthorId())
.authorName(book.getAuthorName())
.wordCount(book.getWordCount())
.lastChapterName(book.getLastChapterName())
.build());
}
assert total != null;
return RestResp.ok(PageRespDto.of(condition.getPageNum(), condition.getPageSize(), total.value(), list));
}
/**
* 构建查询条件
*/
private void buildSearchCondition(BookSearchReqDto condition, SearchRequest.Builder searchBuilder) {
BoolQuery boolQuery = BoolQuery.of(b -> {
if (!StringUtils.isBlank(condition.getKeyword())) {
// 关键词匹配
b.must((q -> q.multiMatch(t -> t
.fields("bookName^2","authorName^1.8","bookDesc^0.1")
.query(condition.getKeyword())
)
));
}
// 精确查询
if (Objects.nonNull(condition.getWorkDirection())) {
b.must(TermQuery.of(m -> m
.field("workDirection")
.value(condition.getWorkDirection())
)._toQuery());
}
if (Objects.nonNull(condition.getCategoryId())) {
b.must(TermQuery.of(m -> m
.field("categoryId")
.value(condition.getCategoryId())
)._toQuery());
}
// 范围查询
if (Objects.nonNull(condition.getWordCountMin())) {
b.must(RangeQuery.of(m -> m
.field("wordCount")
.gte(JsonData.of(condition.getWordCountMin()))
)._toQuery());
}
if (Objects.nonNull(condition.getWordCountMax())) {
b.must(RangeQuery.of(m -> m
.field("wordCount")
.lt(JsonData.of(condition.getWordCountMax()))
)._toQuery());
}
if (Objects.nonNull(condition.getUpdateTimeMin())) {
b.must(RangeQuery.of(m -> m
.field("lastChapterUpdateTime")
.gte(JsonData.of(condition.getUpdateTimeMin().getTime()))
)._toQuery());
}
return b;
});
searchBuilder.query(q -> q.bool(boolQuery));
}
}
7.BookController 中注入 SearchService bean,调用searchBooks方法实现按配置动态切换搜索引擎的功能
public class BookController {
private final SearchService searchService;
/**
* 小说搜索接口
*/
@GetMapping("search_list")
public RestResp<PageRespDto<BookInfoRespDto>> searchBooks(BookSearchReqDto condition) {
return searchService.searchBooks(condition);
}
}
两个API对比
Spring Data Elasticsearch提供了更高级别的抽象,隐藏了许多底层细节,可以更快速地开始使用。通过定义存储库接口来轻松地执行常见的CRUD操作和查询。它提供了一些预定义的方法,如保存、查找和删除,以及通过方法命名约定自动生成查询。
Elasticsearch Java API Client提供了更低级别的控制,可以更精确地控制请求和响应的细节。它适用于需要更多自定义和特定操作的场景,但也需要更多的代码编写和管理。