ElasticSearch搜索引擎
什么是搜索引擎
当你的项目中需要一个很强大且快速的搜索功能,并且需求的预期已经超过了关系型数据库能带来的检索能力时,你就需要使用搜索引擎技术。
- 存储并快速检索、分析海量数据
- 提供全文检索
- 一般都提供分布式功能
Lucene和ElasticSearch
Lucene是一个历史悠久的Java搜索引擎库,它的学习曲线陡峭,并且好像不支持分布式集群,ElasticSearch基于Lucene构建,简化了使用并支持集群。而且,ElasticSearch对调用者提供Restful接口和一个DSL(领域特定语言)来进行查询。
反向索引(倒排索引)
搜索引擎的任务是基于特定的关键词来搜索内容,这些关键词可能出现在待检测内容的多个字段上,用博客平台的搜索系统来举例,用户可能会搜索“阮一峰 Python教程”这个字符串,这个字符串中可能从以下的字段中匹配:
- 文章作者的名字:阮一峰
- 文章标题中的子字符串:Python,教程
- 文章内容中的子字符串:Python,教程
- 文章所在的分类:Python
若使用传统的B+树的索引结构,你完全做不到这一点,首先你不知道用户输入的阮一峰
需要匹配数据库中的字段author
,你无法利用B+树索引对文章标题和内容做不符合最左前缀原则的模糊搜索......传统数据库中B+树索引的种种限制让它不适合做这种分析搜索操作,我们需要一种新的索引结构——反向索引
反向索引根据原内容建立很多搜索单词,基于搜索单词建立起索引结构,并让单词指向拥有这个单词的内容的ID,如下图:
这样,用户搜索阮一峰 Python教程
时,用户的搜索会被分成独立的单词,然后与通过到搜索引擎索引的查询,发现[10001, 15192, 40212, 10001, 53021, 02041, 10001, 23014]
这些文章中有待搜索的单词,并且,10001出现的频率最高,所以它在搜索结果中的排名最靠前。
所以可以理解为倒排索引就是通过内容去寻找id,而正向索引是通过id去寻找内容。
分词
倒排索引中很重要的一个东西就是分词,上图中的文章标题和文章内容都被切割成一个一个小的单词插入到索引中了,并且用户的输入也被分词了,这给了搜索引擎基于关键词来搜索的能力。
不过我们仍需注意到,并非所有加入到搜索引擎中的东西都要分词,比如文章的作者名就无需进行分词,分了有什么意义呢?
概念
不管是ElasticSearch还是其它搜索引擎,它们都有一些相通的概念:
- 索引:即一个倒排索引结构,用于对某一种数据进行搜索,比如文章数据需要在搜索引擎中有一个索引结构
- 文档:即一个要插入到索引结构中的待查询的实体,如文章
- 字段:一个实体中的一个属性,比如文章的作者,每一个字段都可能作为搜索引擎建立倒排索引的依据
- Mapping:这个应该是ES特有的概念,是对索引的约束
ElasticSearch的组件简要介绍
- ElasticSearch:ES搜索引擎的服务器
- Kibana:ES搜索引擎的一个Web控制台,让我们更方便的操作它
- 插件:ES搜索引擎可以添加插件来扩展功能
- 分词器:以插件的形式存在的,用于对属性进行分割,对于中文这种没有明确单词边界的语言,可能需要定制特殊的分词器
- 字典:也是对于中文这种没有明确边界的语言,分词需要依据一个字典来分,字典中定义每个单词
- 停止词字典:有一些无意义的词,比如
a
、an
、the
、的
、是
、这
,将它们插入到倒排索引中只能徒增搜索引擎的压力,因为几乎每一篇文章中都有大量的这些词。停止词字典描述了这些不需要被添加到倒排索引中的单词。
DSL操作ES
mapping约束
mapping是描述索引的约束,它定义了文档中的哪些属性要被索引,还有这些属性的类型
下面介绍mapping约束的一些约束项
type
type描述文档中某个属性的类型
字符串类型:
text
:可分词的文本,比如文章标题keyword
:本身就是关键字,不用被分词,比如品牌名
其它类型:
数值
布尔
日期
对象
地理位置
- ...
只有
text
类型需要考虑是否被分词,其它类型都不支持分词
index
index描述该属性是否被插入到倒排索引中,也就是说它是否会被作为搜索依据,默认为true
analyzer
analyzer描述了对该字段使用哪种分词器,只有type=text
的属性才需要考虑这个字段
properties
properties描述了该属性的子属性,这在该属性是一个对象类型的属性时使用,用于描述对象的嵌套
索引的CRUD
添加索引
我们使用Kibana
客户端来操作ES,你也可以直接使用HTTP客户端来发送请求,下面我们发送一个PUT
请求来创建索引,其中请求的路径就是索引名:
PUT /article
{
"mappings": {
"properties": {
"title": {
"type": "text",
"analyzer": "ik_smart"
},
"content": {
"type": "text",
"analyzer": "ik_smart"
},
"head_image": {
"type": "keyword",
"index": "false"
},
"author_name": {
"properties": {
"first_name": {
"type": "keyword"
},
"last_name": {
"type": "keyword"
}
}
}
}
}
}
创建索引中的请求体就是对索引的mapping约束的描述,所以很容易看懂,上面的索引中有四个字段
title
:文章标题,text
类型,使用ik_smart
分词器进行分词content
:文章内容,text
类型,使用ik_smart
分词器head_image
:文章头图URL,keyword
类型,不参加索引author_name
:文章作者名,object
类型,其中描述了它的子属性,子属性都是keyword
类型并参与索引
查询/删除索引
由于ES提供了符合Restful风格的API,所以我们可以轻松的猜测查询和删除索引的请求规则:
GET /article
DELETE /article
更新索引
索引的更新稍微复杂一点,由于ES不支持对已有索引字段进行更改,所以你只能在这之上添加新的索引字段,下面向article
索引的mapping约束中添加了一个author_age
字段,请求的url是:/索引名/_mapping
:
PUT /article/_mapping
{
"properties": {
"author_age": {
"type": "integer"
}
}
}
文档的CRUD
对文档进行操作的url为:/索引名/_doc/文档id
新增文档
POST /article/_doc/1
{
"title": "RabbitMQ&AMQP",
"content": "同步请求&异步请求 同步请求 以下是微服务间使用同步请求调用的示意图: 缺点: 性能低下:支付服务的服务是它所调用的所有服务的服务时间之和 资源浪费:支付服务在等待其它服务时占用系统资源,但实际不工作 紧耦合:当支付动作发生后又要扩展其它的业务时(比如新增赠送优惠券服务),需要更改支付服务的代码 故",
"head_image": "null",
"authorName": {
"firstName": "Haonan",
"lastName": "Yu"
}
}
查询/删除文档
GET /article/_doc/1
DELETE /article/_doc/1
修改文档
修改有两种方式,首先是全量修改,即根据id,先删除对应的文档,再以同样的id插入你携带的新文档。
PUT /article/_doc/1
{
"title": "RabbitMQ&AMQP",
"content": "同步请求&异步请求 同步请求 以下是微服务间使用同步请求调用的示意图: 缺点: 性能低下:支付服务的服务是它所调用的所有服务的服务时间之和 资源浪费:支付服务在等待其它服务时占用系统资源,但实际不工作 紧耦合:当支付动作发生后又要扩展其它的业务时(比如新增赠送优惠券服务),需要更改支付服务的代码 故",
"head_image": "null",
"authorName": {
"firstName": "Doge",
"lastName": "Yu"
}
}
第二种就是增量修改,根据id查找文档,并修改你携带的指定字段:
POST /article/_update/1
{
"doc": {
"head_image": "https://cn.bing.com/ck/a?!&&p=9fef2355fc23882cJmltdHM9MTY1OTkyNDM0NSZpZ3VpZD05YmJlNDU2NC0xOWY0LTQwMWItYTA5NC1mOWRmZWU0OTczMmImaW5zaWQ9NTU3MQ&ptn=7&hsh=3&fclid=9cd9ed9a-16be-11ed-8d03-3ebf7a0556df&u=a1L2ltYWdlcy9zZWFyY2g_cT0lRTUlOUIlQkUlRTclODklODcmRk9STT1JUUZSQkEmaWQ9MzM3RkZFNTA1NzBDQkVBMEUxMTNCRUMxNjk0MUI1QzlCMzJCODQxMg&ntb=1",
"authorName": {
"firstName": "Haonan"
}
}
}
RestHighLevelClient
ElasticSearch针对不同语言提供了不同的客户端,毕竟这东西最后要被程序操纵,而不是通过web客户端人工操作。RestHighLevelClient
是ES给Java提供的一种客户端。
导入
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-high-level-client</artifactId>
</dependency>
如果使用SpringBoot,其父pom中定义了客户端中使用的部分组件的版本,这可能和你所使用的版本不兼容,所以请在你的pom中的properties
替换它:
<elasticsearch.version>7.12.1</elasticsearch.version>
客户端创建
RestHighLevelClient client = new RestHighLevelClient(RestClient.builder(
HttpHost.create("http://localhost:9200")
));
客户端API分析
Client
只能对文档进行操作,如果你想对索引或集群进行操作,有对应的IndicesClient
和ClusterClient
,它们可以通过如下方法获得:
IndicesClient indicesClient = client.indices();
ClusterClient clusterClient = client.cluster();
不管是哪种客户端,你想调用它进行增删改查,它都定义好了你可以直接使用的请求和响应对象。
所以整个操作过程可以分为:
- 构建代表你的操作的请求对象
- 设置请求对象的值
- 调用client的方法,传入请求对象,获取响应对象
同时客户端还提供异步的调用,额外传入一个监听器,并且返回的是Cancellable
对象用于调用者线程对这个请求进行取消:
索引简单操作
增
CreateIndexRequest request = new CreateIndexRequest("hotel");
// MAPPING_TEMPLATE就是mapping约束的json字符串
request.source(MAPPING_TEMPLATE, XContentType.JSON);
client.indices().create(request, RequestOptions.DEFAULT);
删
DeleteIndexRequest deleteIndexRequest = new DeleteIndexRequest("hotel");
AcknowledgedResponse response = client.indices().delete(deleteIndexRequest, RequestOptions.DEFAULT);
System.out.println(response.isAcknowledged());
文档简单操作
新增文档
新增文档的过程就相当于给一个文档在倒排索引中创建索引,所以,ES的客户端搞了这么一个让人迷惑的API:
IndexRequest indexRequest = new IndexRequest("hotel").id("1");
indexRequest.source("{....json object....}", XContentType.JSON);
client.index(indexRequest, RequestOptions.DEFAULT);
一般情况下,我们都是将数据库中的数据放到搜索引擎中一份,数据库中的数据是与数据库模式相匹配的Pojo对象,所以想把它添加到搜索引擎中要经历如下几步:
- 转换pojo:如果数据库Schema和搜索引擎Mapping不符,将pojo对象转换成符合搜索引擎Mapping的pojo
- 序列化成json:ES中接收json格式的数据,你需要通过某种办法将它格式化成json
- 插入到搜索引擎
查询文档
// 索引名,待查文档id
GetRequest request = new GetRequest("hotel", "61083");
// 获取响应
GetResponse response = client.get(request, RequestOptions.DEFAULT);
// 从响应中获取json字符串
String json = response.getSourceAsString();
// 将它转换成pojo
HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class);
System.out.println("hotelDoc = " + hotelDoc);
由于删除和修改和之前的逻辑几乎没区别,所以不记录
批量处理
@Test
void testBulkRequest2() throws IOException {
List<Hotel> hotels = hotelService.list();
BulkRequest bulkRequest = new BulkRequest();
hotels.stream()
.map(HotelDoc::new)
.forEach(
doc -> {
IndexRequest indexRequest = new IndexRequest("hotel").id(doc.getId().toString());
indexRequest.source(JSON.toJSONString(doc), XContentType.JSON);
bulkRequest.add(indexRequest);
}
);
client.bulk(bulkRequest, RequestOptions.DEFAULT);
}