1、ElasticSearch
概念特点
全文搜索是对非结构化数据的一种搜索方式,所谓非结构化数据是指相对于结构化数据(如数据库)来说长度不固定或无固定格式的数据,例如文档、邮件等。
对非结构化数据的搜索最常见的方式是顺序扫描法,即对整个文档从头到尾逐字匹配检索,例如Windows的文件搜索或者Linux的grep命令。这种方式适用于数据量较小的文件,当文件量过大时搜索将变得异常缓慢。另一种搜索方式全文搜索对非结构化数据按照一定的索引进行重新组织,从而达到按照索引对数据进行快速搜索的目的。
常用的全文搜索框架是基于Lucene引擎的ElasticSearch(ES)和Solr,相比于支持多种数据格式的Solr,ES只支持JSON数据,但ES的实时搜索效率较高。它的相关概念和特点如下:
- 分布式:节点Node是指单个保存数据和索引的服务器,每个节点在启动的时候会分配一个唯一标识符,也可以自定义名称。多个保存数据的节点就构成了集群,节点可以根据名称加入集群。
- ES中索引Index是相似文档的集合。文档Document是索引的基本单位,ES以json格式保存每个文档。当数据过大超过单个节点的存储空间时,ES会将数据划分为多个分片并保存到不同的节点,并且对分片进行管理。为了提高可用性,还会对分片创建副本,当某几个主机出现问题时,由于副本的存在,系统仍然可用。并且由于分片可副本的存在,可以将搜索负荷分摊到不同的分配或副本,提高了系统的读写吞吐量。
- 近实时:在索引创建之后ES不会立马写入磁盘,而是储存在缓存中,然后根据刷新策略(默认为1秒)定时将索引写入磁盘,因此搜索会存在1s左右的延迟。这是es对写入和查询一个平衡,这样设置既提升了es的索引写入效率同时也使得es能够近实时检索数据。
- 多API:ES除了Java原生接口外,还支持RESTful类型的API
- 面向文档:使用关系型数据库中的表时需要构建对应的实体类,而ES可以直接创建文档并进行存储
- ES也有很多的短处,最明显的就是字段类型无法修改、写入性能较低和高硬件资源消耗
倒排索引 inverted index
如下所示为ElasticSearch实现搜索的方式–inverted index。对于正向索引forward index,我们是在文章中逐个查找关键字;而倒排索引则是利用关键词将文章串起来。例如我们在扫描第1篇文章时拆分为单个词组,其中有关键词Ada,则为Ada添加索引1,然后在扫描第2篇文章时也发现有Ada,同样将其添加到索引中。这样当我们检索Ada关键字时就可以很快地找到对应的文章。
2、SpringBoot中使用
2.1、部署环境&依赖
从ElasticSearch官网下载所需版本的压缩包后在本地解压,对于Windows,在解压后的bin目录下找到elasticsearch.bat点击运行,ElasticSearch会在默认的9200端口启动。
在Spring boot中使用ElasticSearch需要引入spring-boot-starter-data-elasticsearch
、jna
两个依赖项
dependencies {
implementation('org.springframework.boot:spring-boot-starter-data-elasticsearch')
implementation('net.java.dev.jna:jna:5.6.0')
......
}
在spring boot的application.properties中配置ElasticSearch如下
# ElasticSearch服务器地址
spring.data.elasticsearch.client.reactive.endpoints=localhost:9200
# 连接超时时间
spring.data.elasticsearch.client.reactive.connection-timeout=120
2.2、对应Java类
首先创建Article
类用于表示存储的单个文档(Document)对象,通过注解@Document
表示这是一个文档类。该类需要实现Serializable接口以及满足POJO要求(需要包含空构造函数与字段的getter、setter方法),此外增加一个toString方法用于将其打印字符串。我们可以对Article类的title、summary、content的三个字段进行索引,从而实现快速查找。
package com.tory.blog.entity;
import org.springframework.data.elasticsearch.annotations.Document;
import javax.persistence.Id;
import java.io.Serializable;
@Document(indexName = "article")
public class Article implements Serializable {
@Id
private String id;
private String title;
private String summary;
private String content;
public Article() {
}
public Article(String id, String title, String summary, String content) {
this.id = id;
this.title = title;
this.summary = summary;
this.content = content;
}
//getter & setter...
@Override
public String toString() {
return "Article{" +
"id='" + id + '\'' +
", title='" + title + '\'' +
", summary='" + summary + '\'' +
", content='" + content + '\'' +
'}';
}
}
接着定义ArticleRepository
接口用于操作ElasticSearch,该接口继承自ElasticsearchRepository
。和SpringDataJPA一样,我们只需要在接口中按照规则写好方法名而不必手动去实现,例如这里声明了根据文章标题对文档进行检索findDistinctArticleByTitleContaining()
package com.tory.blog.repository;
import com.tory.blog.entity.Article;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;
public interface ArticleRepository extends ElasticsearchRepository<Article, String> {
/**
* 根据Title、Summary、Content分页查询Article
*
* @param title
* @return Page<Article>
*/
Page<Article> findDistinctArticleByTitleContaining(String title, Pageable pageable);
}
2.3、进行搜索
首先通过articleRepository.save()存入三个文档对象
@Autowired
private ArticleRepository articleRepository;
@BeforeEach
void setUp() {
articleRepository.deleteAll();
articleRepository.save(new Article("1", "静夜思", "李白的诗", "床前明月光,疑是地上霜。"));
articleRepository.save(new Article("2", "青花瓷", "周杰伦的歌", "素胚勾勒出青花笔锋浓转淡。"));
articleRepository.save(new Article("3", "青玉案", "辛弃疾的词", "东风夜放花千树,更吹落星如雨。"));
}
之后在Controller中定义searchArticle()方法,通过调用findDistinctArticleByTitleContaining()
根据title对文档进行搜索,注意该方法返回的是分页之后的查询结果,除了title之外还需要pageable
对象作为参数,通过PageRequest.of()创建。
@RequestMapping("article")
@RestController
public class ArticleController {
@Autowired
private ArticleRepository articleRepository;
@GetMapping("search")
public List<Article> searchArticle(@RequestParam("title") String title){
Pageable pageable= PageRequest.of(0,20); //选择第0页,每页20条结果。
Page<Article> blogPage= articleRepository.findDistinctArticleByTitleContaining(title,pageable);
return blogPage.getContent();
}
}
接着启动项目,访问http://localhost:8080/blog/article/search?title=青,对title含有“青”字的文章进行搜索,返回json结果如下: