Elasticsearch 学习+SpringBoot实战教程
Posted 桂亭亭
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Elasticsearch 学习+SpringBoot实战教程相关的知识,希望对你有一定的参考价值。
需要学习基础的可参照这两文章
Elasticsearch 学习+SpringBoot实战教程(一)
Elasticsearch 学习+SpringBoot实战教程(一)_桂亭亭的博客-CSDN博客
Elasticsearch 学习+SpringBoot实战教程(二)
前言: 经过了前面2课的学习我们已经大致明白了ES怎么使用,包括原生语句,javaapi等等,现在我们要在业务中使用了,
所以我们选择spring-data作为我们的ORM框架,快速开发代码。
同时需要给规范化操作
目录
1 使用ElasticsearchOperations的方式
0 前置动作
1 Springboot项目引入依赖
注意你的ES版本号
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/com.alibaba/fastjson -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>2.0.20</version>
</dependency>
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-high-level-client</artifactId>
<version>7.10.1</version>
<exclusions>
<exclusion>
<groupId>org.elasticsearch</groupId>
<artifactId>elasticsearch</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.elasticsearch</groupId>
<artifactId>elasticsearch</artifactId>
<version>7.10.1</version>
</dependency>
2 建立目录与文件
3 配置文件
spring:
elasticsearch:
uris: localhost:9200
connection-timeout: 3000
socket-timeout: 5000
4 实体类
package com.example.eslearn.entity;
import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.annotations.Document;
import org.springframework.data.elasticsearch.annotations.Field;
import org.springframework.data.elasticsearch.annotations.FieldType;
import java.io.Serializable;
/**
* Document: 将这个类对象转为 es 中一条文档进行录入
* indexName: 用来指定文档的索引名称
* createIndex: 用来指定是否创建索引,默认为false
*/
@Document(indexName = "user", createIndex = true)
public class UserDocument implements Serializable
@Id // 用来将放入对象id值作为文档_id进行映射
private String id;
@Field(type = FieldType.Keyword) // 字段映射类型
private String name;
private String sex;
private Integer age;
@Field(type = FieldType.Text) // 字段映射类型
private String city;
1 使用ElasticsearchOperations的方式
优点:更想我们的springdata的使用风格,简单,快捷,个人使用
新增文档
private final ElasticsearchOperations ESO;
// set方法注入
@Autowired
public CRUDService2(ElasticsearchOperations elasticsearchOperations)
this.ESO = elasticsearchOperations;
// 新增文档
public String save()
UserDocument user = new UserDocument();
user.setName("说不定看见的");
user.setCity("北京 上海 西安");
user.setAge(22);
user.setSex("男");
UserDocument save = ESO.save(user);
System.out.println(save);
return JSON.toJSONString(save);
使用可视化软件查询,得到下面的结果
更新文档
// 更新文档
public String update()
UserDocument user = new UserDocument();
user.setId("W7w2HYcB32f1ZLmxRwzw");
user.setName("说快来打见的");
user.setCity("北京 上海 西安");
user.setAge(21);
user.setSex("女");
UserDocument save = ESO.save(user);
System.out.println(save);
return JSON.toJSONString(save);
删除文档
// 删除
public String delete()
UserDocument userDocument = new UserDocument();
userDocument.setId("8966e506-1763-4d4b-bf1c-4f5d9bd9b052");
return ESO.delete(userDocument);
查询所有
// 查询所有
public String findAll()
//查询所有
SearchHits<UserDocument> search = ESO.search(Query.findAll(), UserDocument.class);
for (SearchHit<UserDocument> uc : search)
System.out.println(uc.getContent());
return JSON.toJSONString(search);
查询指定id
// 根据id查询文档
public String getById()
UserDocument userDocument = ESO.get("W7w2HYcB32f1ZLmxRwzw", UserDocument.class);
return JSON.toJSONString(userDocument);
分页+指定条件+高亮显示+排序+过滤结果
服务层
//大杂烩,一次学会
public String findSource()
//查询条件构建
MatchQueryBuilder mp=new MatchQueryBuilder("name","妲己");
//排序构建
FieldSortBuilder f = new FieldSortBuilder("age");
//分页构建
Pageable page= PageRequest.of(0,5);
// 高亮构建
HighlightBuilder highlightBuilder = new HighlightBuilder()
.preTags("<span style='color:yellow'>")
.postTags("</span>")
.field("name");
//结果过滤构建,相当于返回那些字段
FetchSourceFilter filter = new FetchSourceFilter(new String[]"name", "city", null);
//查询语句构建
NativeSearchQueryBuilder query = new NativeSearchQueryBuilder()
.withQuery(mp)
.withSorts(f)
.withPageable(page)
.withHighlightBuilder(highlightBuilder)
.withSourceFilter(filter);
//执行查询
SearchHits<UserDocument> search = ESO.search(query.build(), UserDocument.class);
return JSON.toJSONString(search);
控制器
@GetMapping("/findSource")
private String findSource()
return sv.findSource();
2 使用RestHighLevelClient的方式
优点:安全,企业级常用
精确查询
对应的原生查询语句
注意这里的term就是精准查询到 关键字
GET user/_search
"query":
"term":
"city": "上海"
服务层
// 文档搜索
public String searchDocument(String indexName,String city)
//2 构建搜索请求
SearchRequest searchRequest = new SearchRequest().indices(indexName);
//3 构建搜索内容
TermQueryBuilder termQueryBuilder = QueryBuilders.termQuery("city", city);
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
searchSourceBuilder.query(termQueryBuilder);
//4 填充搜索内容
searchRequest.source(searchSourceBuilder);
SearchResponse searchResponse = null;
try
//5 执行搜索操作
searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);
catch (IOException e)
throw new RuntimeException(e);
//6 返回值
return JSON.toJSONString(searchResponse.getHits().getHits());
控制器
@GetMapping("/searchUserByCity")
public String searchUserByCity() throws IOException
return service.searchDocument("user","上海");
访问链接localhost:8080/searchUserByCity
分页查询
GET user/_search
"query":
"term":
"city": "上海"
,
"from":0,
"size":5
服务层
// 文档搜索--分页查询
public String searchDocument2(String indexName,String city)
//2 构建搜索请求
SearchRequest searchRequest = new SearchRequest().indices(indexName);
//3 构建搜索内容
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
//拿到前5条数据
searchSourceBuilder
.query(QueryBuilders.termQuery("city", city))
.from(0)
.size(5);
//4 填充搜索内容
searchRequest.source(searchSourceBuilder);
SearchResponse searchResponse = null;
try
//5 执行搜索操作
searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);
catch (IOException e)
throw new RuntimeException(e);
//6 返回值
return JSON.toJSONString(searchResponse.getHits().getHits());
控制层
@GetMapping("/searchUserByCity2")
public String searchUserByCity2() throws IOException
return service.searchDocument2("user","上海");
访问localhost:8080/searchUserByCity2
字符匹配AND精准查询
term 与matchphrase的比较 term用于精确查找有点像 mysql里面的"=" match是先将查询关键字分词然后再进行查找。term一般用在keywokrd类型的字段上进行精确查找。
注意这里的bool,表示使用布尔查询,其中的must是相当于SQL语句中的and的意思。
所以就是查找name中包含“妲己”并且年龄为22岁的信息,请注意不能写成"妲",因为我们在新建文档的时候是这样新建的“妲己”,那么我们如果匹配“妲”就会匹配不到,加入这样写就可以匹配到了“妲 己”,请注意空格,这是分词的依据之一
ES查询语句。
GET user/_search
"query":
"bool":
"must": [
"match_phrase":
"name": "妲己"
,
"term":
"age": "32"
]
,
"from":0,
"size":10
服务层
// 文档分词搜索+精确查询
public String searchDocument3(String indexName,String name,Integer age)
//2 构建搜索请求
SearchRequest searchRequest = new SearchRequest().indices(indexName);
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
//3 构建复杂的查询语句
BoolQueryBuilder bq=QueryBuilders
.boolQuery()
//分词匹配
.must(QueryBuilders.matchPhraseQuery("name",name))
//精确匹配
.must(QueryBuilders.matchQuery("age",age));
//4 填充搜索语句
searchSourceBuilder
.query(bq)
.from(0)
.size(5);
//4 填充搜索内容
searchRequest.source(searchSourceBuilder);
SearchResponse searchResponse = null;
try
//5 执行搜索操作
searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);
catch (IOException e)
throw new RuntimeException(e);
//6 返回值
return JSON.toJSONString(searchResponse.getHits());
控制层
@GetMapping("/searchUser3")
public String searchUser3() throws IOException
return service.searchDocument3("user","妲己",32);
字符匹配OR精准查询
原始查询语句
GET user/_search
"query":
"bool":
"should": [
"match_phrase":
"name": "妲己"
,
"term":
"age": "32"
]
,
"from":0,
"size":10
服务层
// 文档分词搜索OR精确查询
public String searchDocument4(String indexName,String name,Integer age)
//2 构建搜索请求
SearchRequest searchRequest = new SearchRequest().indices(indexName);
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
//3 构建复杂的查询语句
BoolQueryBuilder bq=QueryBuilders
.boolQuery()
//分词匹配
.should(QueryBuilders.matchPhraseQuery("name",name))
//精确匹配
.should(QueryBuilders.matchQuery("age",age));
//4 填充搜索语句
searchSourceBuilder
.query(bq)
.from(0)
.size(5);
//4 填充搜索内容
searchRequest.source(searchSourceBuilder);
SearchResponse searchResponse = null;
try
//5 执行搜索操作
searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);
catch (IOException e)
throw new RuntimeException(e);
//6 返回值
return JSON.toJSONString(searchResponse.getHits());
控制层
@GetMapping("/searchUser4")
public String searchUser4() throws IOException
return service.searchDocument4("user","妲己",22);
结果
模糊查询
原始语句
GET user/_search
"query":
"wildcard":
"city":
"value": "上*"
// 文档模糊查询
public String searchDocument5(String indexName,String city)
//2 构建搜索请求
SearchRequest searchRequest = new SearchRequest().indices(indexName);
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
//3 构建模糊查询的语句
WildcardQueryBuilder bq=QueryBuilders
.wildcardQuery("city",city);
//4 填充搜索语句
searchSourceBuilder
.query(bq);
searchRequest.source(searchSourceBuilder);
SearchResponse searchResponse = null;
try
//5 执行搜索操作
searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);
catch (IOException e)
throw new RuntimeException(e);
//6 返回值
return JSON.toJSONString(searchResponse.getHits());
@GetMapping("/searchUser5")
public String searchUser5() throws IOException
return service.searchDocument5("user","上*");
结果
Elasticsearch 学习笔记
最近在参与一个基于Elasticsearch作为底层数据框架提供大数据量(亿级)的实时统计查询的方案设计工作,花了些时间学习Elasticsearch的基础理论知识,整理了一下,希望能对Elasticsearch感兴趣/想了解的同学有所帮助。 同时也希望有发现内容不正确或者有疑问的地方,望指明,一起探讨,学习,进步。
介绍
Elasticsearch 是一个分布式可扩展的实时搜索和分析引擎.
Elasticsearch 是一个建立在全文搜索引擎 Apache Lucene(TM) 基础上的搜索引擎. 当然 Elasticsearch 并不仅仅是 Lucene 那么简单,它不仅包括了全文搜索功能,还可以进行以下工作:
- 分布式实时文件存储,并将每一个字段都编入索引,使其可以被搜索。
- 实时分析的分布式搜索引擎。
- 可以扩展到上百台服务器,处理PB级别的结构化或非结构化数据。
基本概念
先说Elasticsearch的文件存储,Elasticsearch是面向文档型数据库,一条数据在这里就是一个文档,用JSON作为文档序列化的格式,比如下面这条用户数据:
{
"name" : "John",
"sex" : "Male",
"age" : 25,
"birthDate": "1990/05/01",
"about" : "I love to go rock climbing",
"interests": [ "sports", "music" ]
}
用Mysql这样的数据库存储就会容易想到建立一张User表,有balabala的字段等,在Elasticsearch里这就是一个 文档 ,当然这个文档会属于一个User的 类型 ,各种各样的类型存在于一个 索引 当中。这里有一份简易的将Elasticsearch和关系型数据术语对照表:
关系数据库 ⇒ 数据库 ⇒ 表 ⇒ 行 ⇒ 列(Columns)
Elasticsearch ⇒ 索引 ⇒ 类型 ⇒ 文档 ⇒ 字段(Fields)
一个 Elasticsearch 集群可以包含多个索引(数据库),也就是说其中包含了很多类型(表)。这些类型中包含了很多的文档(行),然后每个文档中又包含了很多的字段(列)。
Elasticsearch的交互,可以使用Java API,也可以直接使用HTTP的Restful API方式,比如我们打算插入一条记录,可以简单发送一个HTTP的请求:
PUT /megacorp/employee/1
{
"name" : "John",
"sex" : "Male",
"age" : 25,
"about" : "I love to go rock climbing",
"interests": [ "sports", "music" ]
}
更新,查询也是类似这样的操作,具体操作手册可以参见 Elasticsearch权威指南
索引
Elasticsearch最关键的就是提供强大的索引能力了,其实InfoQ的这篇 时间序列数据库的秘密(2)——索引 写的非常好,我这里也是围绕这篇结合自己的理解进一步梳理下,也希望可以帮助大家更好的理解这篇文章。
Elasticsearch索引的精髓:
一切设计都是为了提高搜索的性能
另一层意思:为了提高搜索的性能,难免会牺牲某些其他方面,比如插入/更新,否则其他数据库不用混了:)
前面看到往Elasticsearch里插入一条记录,其实就是直接PUT一个json的对象,这个对象有多个fields,比如上面例子中的 name, sex, age, about, interests ,那么在插入这些数据到Elasticsearch的同时,Elasticsearch还默默 1 的为这些字段建立索引–倒排索引,因为Elasticsearch最核心功能是搜索。
Elasticsearch是如何做到快速索引的
InfoQ那篇文章里说Elasticsearch使用的倒排索引比关系型数据库的B-Tree索引快,为什么呢?
什么是B-Tree索引?
上大学读书时老师教过我们,二叉树查找效率是logN,同时插入新的节点不必移动全部节点,所以用树型结构存储索引,能同时兼顾插入和查询的性能。
因此在这个基础上,再结合磁盘的读取特性(顺序读/随机读),传统关系型数据库采用了B-Tree/B+Tree这样的数据结构:
为了提高查询的效率,减少磁盘寻道次数,将多个值作为一个数组通过连续区间存放,一次寻道读取多个数据,同时也降低树的高度。
什么是倒排索引?
继续上面的例子,假设有这么几条数据(为了简单,去掉about, interests这两个field):
ID | Name | Age | Sex |
---|---|---|---|
1 | Kate | 24 | Female |
2 | John | 24 | Male |
3 | Bill | 29 | Male |
ID是Elasticsearch自建的文档id,那么Elasticsearch建立的索引如下:
Name:
Term | Posting List |
---|---|
Kate | 1 |
John | 2 |
Bill | 3 |
Age:
Term | Posting List |
---|---|
24 | [1,2] |
29 | 3 |
Sex:
Term | Posting List |
---|---|
Female | 1 |
Male | [2,3] |
Posting List
Elasticsearch分别为每个field都建立了一个倒排索引,Kate, John, 24, Female这些叫term,而[1,2]就是 Posting List 。Posting list就是一个int的数组,存储了所有符合某个term的文档id。
看到这里,不要认为就结束了,精彩的部分才刚开始…
通过posting list这种索引方式似乎可以很快进行查找,比如要找age=24的同学,爱回答问题的小明马上就举手回答:我知道,id是1,2的同学。但是,如果这里有上千万的记录呢?如果是想通过name来查找呢?
Term Dictionary
Elasticsearch为了能快速找到某个term,将所有的term排个序,二分法查找term,logN的查找效率,就像通过字典查找一样,这就是 Term Dictionary 。现在再看起来,似乎和传统数据库通过B-Tree的方式类似啊,为什么说比B-Tree的查询快呢?
Term Index
B-Tree通过减少磁盘寻道次数来提高查询性能,Elasticsearch也是采用同样的思路,直接通过内存查找term,不读磁盘,但是如果term太多,term dictionary也会很大,放内存不现实,于是有了 Term Index ,就像字典里的索引页一样,A开头的有哪些term,分别在哪页,可以理解term index是一颗树:
这棵树不会包含所有的term,它包含的是term的一些前缀。通过term index可以快速地定位到term dictionary的某个offset,然后从这个位置再往后顺序查找。
所以term index不需要存下所有的term,而仅仅是他们的一些前缀与Term Dictionary的block之间的映射关系,再结合FST(Finite State Transducers)的压缩技术,可以使term index缓存到内存中。从term index查到对应的term dictionary的block位置之后,再去磁盘上找term,大大减少了磁盘随机读的次数。
这时候爱提问的小明又举手了:”那个FST是神马东东啊?”
一看就知道小明是一个上大学读书的时候跟我一样不认真听课的孩子,数据结构老师一定讲过什么是FST。但没办法,我也忘了,这里再补下课:
FSTs are finite-state machines that map a term (byte sequence) to an arbitraryoutput .
假设我们现在要将mop, moth, pop, star, stop and top(term index里的term前缀)映射到序号:0,1,2,3,4,5(term dictionary的block位置)。最简单的做法就是定义个Map<String, Integer>,大家找到自己的位置对应入座就好了,但从内存占用少的角度想想,有没有更优的办法呢?答案就是: FST ( 理论依据在此,但我相信99%的人不会认真看完的 )
:o:?表示一种状态
–>表示状态的变化过程,上面的字母/数字表示状态变化和权重
将单词分成单个字母通过:o:?和–>表示出来,0权重不显示。如果:o:?后面出现分支,就标记权重,最后整条路径上的权重加起来就是这个单词对应的序号。
FSTs are finite-state machines that map a term ( byte sequence ) to an arbitrary output.
FST以字节的方式存储所有的term,这种压缩方式可以有效的缩减存储空间,使得term index足以放进内存,但这种方式也会导致查找时需要更多的CPU资源。
后面的更精彩,看累了的同学可以喝杯咖啡……
压缩技巧
Elasticsearch里除了上面说到用FST压缩term index外,对posting list也有压缩技巧。 小明喝完咖啡又举手了:”posting list不是已经只存储文档id了吗?还需要压缩?”
嗯,我们再看回最开始的例子,如果Elasticsearch需要对同学的性别进行索引(这时传统关系型数据库已经哭晕在厕所……),会怎样?如果有上千万个同学,而世界上只有男/女这样两个性别,每个posting list都会有至少百万个文档id。 Elasticsearch是如何有效的对这些文档id压缩的呢?
Frame Of Reference
增量编码压缩,将大数变小数,按字节存储
首先,Elasticsearch要求posting list是有序的(为了提高搜索的性能,再任性的要求也得满足),这样做的一个好处是方便压缩,看下面这个图例:
如果数学不是体育老师教的话,还是比较容易看出来这种压缩技巧的。
原理就是通过增量,将原来的大数变成小数仅存储增量值,再精打细算按bit排好队,最后通过字节存储,而不是大大咧咧的尽管是2也是用int(4个字节)来存储。
Roaring bitmaps
说到Roaring bitmaps,就必须先从bitmap说起。Bitmap是一种数据结构,假设有某个posting list:
[1,3,4,7,10]
对应的bitmap就是:
[1,0,1,1,0,0,1,0,0,1]
非常直观,用0/1表示某个值是否存在,比如10这个值就对应第10位,对应的bit值是1,这样用一个字节就可以代表8个文档id,旧版本(5.0之前)的Lucene就是用这样的方式来压缩的,但这样的压缩方式仍然不够高效,如果有1亿个文档,那么需要12.5MB的存储空间,这仅仅是对应一个索引字段(我们往往会有很多个索引字段)。于是有人想出了Roaring bitmaps这样更高效的数据结构。
Bitmap的缺点是存储空间随着文档个数线性增长,Roaring bitmaps需要打破这个魔咒就一定要用到某些指数特性:
将posting list按照65535为界限分块,比如第一块所包含的文档id范围在0~65535之间,第二块的id范围是65536~131071,以此类推。再用<商,余数>的组合表示每一组id,这样每组里的id范围都在0~65535内了,剩下的就好办了,既然每组id不会变得无限大,那么我们就可以通过最有效的方式对这里的id存储。
细心的小明这时候又举手了:”为什么是以65535为界限?”
程序员的世界里除了1024外,65535也是一个经典值,因为它=2^16-1,正好是用2个字节能表示的最大数,一个short的存储单位,注意到上图里的最后一行“If a block has more than 4096 values, encode as a bit set, and otherwise as a simple array using 2 bytes per value”,如果是大块,用节省点用bitset存,小块就豪爽点,2个字节我也不计较了,用一个short[]存着方便。
那为什么用4096来区分大块还是小块呢?
个人理解:都说程序员的世界是二进制的,4096*2bytes = 8192bytes < 1KB, 磁盘一次寻道可以顺序把一个小块的内容都读出来,再大一位就超过1KB了,需要两次读。
联合索引
上面说了半天都是单field索引,如果多个field索引的联合查询,倒排索引如何满足快速查询的要求呢?
- 利用跳表(Skip list)的数据结构快速做“与”运算,或者
- 利用上面提到的bitset按位“与”
先看看跳表的数据结构:
将一个有序链表level0,挑出其中几个元素到level1及level2,每个level越往上,选出来的指针元素越少,查找时依次从高level往低查找,比如55,先找到level2的31,再找到level1的47,最后找到55,一共3次查找,查找效率和2叉树的效率相当,但也是用了一定的空间冗余来换取的。
假设有下面三个posting list需要联合索引:
如果使用跳表,对最短的posting list中的每个id,逐个在另外两个posting list中查找看是否存在,最后得到交集的结果。
如果使用bitset,就很直观了,直接按位与,得到的结果就是最后的交集。
总结和思考
Elasticsearch的索引思路:
将磁盘里的东西尽量搬进内存,减少磁盘随机读取次数(同时也利用磁盘顺序读特性),结合各种奇技淫巧的压缩算法,用及其苛刻的态度使用内存。
所以,对于使用Elasticsearch进行索引时需要注意:
- 不需要索引的字段,一定要明确定义出来,因为默认是自动建索引的
- 同样的道理,对于String类型的字段,不需要analysis的也需要明确定义出来,因为默认也是会analysis的
- 选择有规律的ID很重要,随机性太大的ID(比如java的UUID)不利于查询
关于最后一点,个人认为有多个因素:
其中一个(也许不是最重要的)因素: 上面看到的压缩算法,都是对Posting list里的大量ID进行压缩的,那如果ID是顺序的,或者是有公共前缀等具有一定规律性的ID,压缩比会比较高;
另外一个因素: 可能是最影响查询性能的,应该是最后通过Posting list里的ID到磁盘中查找Document信息的那步,因为Elasticsearch是分Segment存储的,根据ID这个大范围的Term定位到Segment的效率直接影响了最后查询的性能,如果ID是有规律的,可以快速跳过不包含该ID的Segment,从而减少不必要的磁盘读次数,具体可以参考这篇 如何选择一个高效的全局ID方案 (评论也很精彩)
后续再结合实际开发及调优工作分享更多内容,敬请期待!
以上是关于Elasticsearch 学习+SpringBoot实战教程的主要内容,如果未能解决你的问题,请参考以下文章
ElasticSearch-学习笔记04Java客户端操作索引库