Elasticsearch Workshop Session
文章目录
- Elasticsearch Workshop Session
- 一、概述
- 1.1 什么是全文检索?
- 1.2 Why Elasticsearch?
- 1.2.1 技术选型指南
- 1.2.2 Lucene、Solr、ES
- 1.2.3 ES常用模式
- 二、核心概念
- 2.1 "You Know, for Search"
- 三、实战
- 3.1 搭建本地环境
- 3.2 CRUD
- 3.2.1 Restful的增删查改
- 3.2.2 Java的增删查改
- 3.3 复杂的查询
- 3.3.1 聚合(aggregations)
- 3.3.2 Null值查询
- 3.3.3 分页查询
- 四、踩坑记录
- 4.1 和`JpaRepository`混用
- 4.2 日期格式转换
- 4.2 使用`Jpa`方法名解析查询兼容性
- 4.3 加Field并支持排序
- 4.4 ES中的Result Window
- 4.5 查询条件过多
- 引用
一、概述
1.1 什么是全文检索?
全文检索
以下摘自百度百科:
全文数据库是全文检索系统的主要构成部分。所谓全文数据库是将一个完整的信息源的全部内容转化为计算机可以识别、处理的信息单元而形成的数据集合。全文数据库不仅存储了信息,而且还有对全文数据进行词、字、段落等更深层次的编辑、加工的功能,而且所有全文数据库无一不是海量信息数据库。
上面只是一个空泛的概念
在实际生产环境中全文检索随处可见
搜索引擎(百度、必应、搜狗、Google)
站内搜索(京东、淘宝、最大同性交友网站GitHub)
还有调查bug最常打交道的Kibana,底层就是使用ES作为全文搜索引擎
与传统数据库的区别
试想,如果让你做一个类似于新浪新闻的舆情网站,现在用户有一个需求,检索包含“大连春节放假通知”关键词的所有文章,按照上传日期倒序输出,用传统数据库如何实现?
用text类型的字段去存储文章,然后检索的时候用like ‘%keyword%’ 去搜索,这样检索效率会大打折扣
可以看出,传统数据库善于组织高度结构化的数据,类似于文章这一类非结构化数据,在存储和检索上都不具备优势
Tip
结构化数据和非结构化数据:
是大数据中的常用概念,
结构化数据,可以从名称中看出,是高度组织和整齐格式化的数据。它是可以放入表格和电子表格中的数据类型。它可能不是人们最容易找到的数据类型,但与非结构化数据相比,无疑是两者中人们更容易使用的数据类型。
非结构化数据本质上是结构化数据之外的一切数据。它不符合任何预定义的模型,因此它存储在非关系数据库中,并使用NoSQL进行查询。它可能是文本的或非文本的,也可能是人为的或机器生成的。简单的说,非结构化数据就是字段可变的的数据。
ES起源
许多年前,一个刚结婚的名叫 Shay Banon 的失业开发者,跟着他的妻子去了伦敦,他的妻子在那里学习厨师。 在寻找一个赚钱的工作的时候,为了给他的妻子做一个食谱搜索引擎,他开始使用 Lucene 的一个早期版本。
直接使用 Lucene 是很难的,因此 Shay 开始做一个抽象层,Java 开发者使用它可以很简单的给他们的程序添加搜索功能。 他发布了他的第一个开源项目 Compass。
后来 Shay 获得了一份工作,主要是高性能,分布式环境下的内存数据网格。这个对于高性能,实时,分布式搜索引擎的需求尤为突出, 他决定重写 Compass,把它变为一个独立的服务并取名 Elasticsearch。
第一个公开版本在2010年2月发布,从此以后,Elasticsearch 已经成为了 Github 上最活跃的项目之一,他拥有超过300名 contributors(目前736名 contributors )。 一家公司已经开始围绕 Elasticsearch 提供商业服务,并开发新的特性,但是,Elasticsearch 将永远开源并对所有人可用。
据说,Shay 的妻子还在等着她的食谱搜索引擎…
这个故事告诉我们什么道理?
- 像 Banon这样牛X的程序员也会失业
- 他做食谱搜索引擎可能只是为了学习Lucene
倒排索引
为什么全文检索ES(搜索引擎)比传统数据库更具优势?
核心就是倒排索引
以下是ES官网对倒排索引的简单介绍:
Elasticsearch 使用一种称为 倒排索引 的结构,它适用于快速的全文搜索。一个倒排索引由文档中所有不重复词的列表构成,对于其中每个词,有一个包含它的文档列表。
倒排索引一旦被写入磁盘后永远都不会被修改。
这个我们会在分片章节详细讲解为什么倒排索引不可变。
官网也为我们举了一个例子,觉得枯燥的同学也可以通过阿里云栖上的这篇漫画来了解
分词器也会影响到查询的效果,英文分词相对容易,中文分词较难,不过市面上有很多现成的中文分词插件可以使用
1.2 Why Elasticsearch?
现在一提到搜索引擎、全文检索,首先想到的就是ES
相较于别的全文搜索引擎,ES的魅力在哪里?
1.2.1 技术选型指南
ES是建立在索引上全文搜索引擎,这意味着ES在全文检索的效率上绝对是不二之选
有人常常把ES和MongoDB用来比较,但是他们的初衷是不同的
ES是提供了一个完整的、分布式的搜索引擎,全文检索是其优势
而MongoDB是为了取代RDBMS,被认为是最像关系型数据库的非关系型数据库
ES支持PB级的快速检索!
但是ES也有它的劣势:不支持事务、不支持复杂的关联关系
除此之外,ES支持复杂的聚合查询,这一特点还使得ES非常适合做数据分析使用
如果你已经有一个在运行的复杂的系统,你的需求之一是在现有系统中添加检索服务。一种非常冒险的方式是重构系统以支持ES。而相对安全的方式是:将ES作为新的组件添加到现有系统中。
更多的应用实例我们会在后面说明
比如:你要在现有网站上加全站搜索功能,并且可能面临着将来并发量快速增长,那么天生支持分布式的ES能让你快速扩展出新的节点来应对日益增长的流量,但是放弃RDBMS转向ES显然不是一个特别明智的决定
1.2.2 Lucene、Solr、ES
在ES出现之前,都是用什么做搜索引擎?
-
Lucene
Lucene是Apache基金会下的一个开源项目,是一个全文检索引擎工具包,但它并不是一个完整的全文检索引擎。
-
Solr
Solr是一个高性能,采用Java开发的全文搜索服务。在Lucene的基础上提供了更为丰富的查询语言,同时实现了可配置、可扩展并对查询性能进行了优化,还有一个功能完善的管理界面,是一款非常优秀的全文搜索引擎。
-
ES
ES和Solr一样,都是基于Lucene开发的全文搜索服务,不同于Solr的是,它天然支持分布式,且基于RESTful接口对外提供检索服务。
可见,ES除了基于Lucene提供的接近实时的搜索以外,易用的接口,天然的分布式支持才是在这个数据爆炸的时代快速发展并被广泛使用的核心原因。
1.2.3 ES常用模式
-
整合到已有系统中作为搜索引擎,提供快速检索、高亮检索能力
如同上文所述,将已有的系统迁移到ES上,虽然可以解决查询性能问题,但是risk太大,我们可以建立ES和既有RDBMS的同步机制:
- 可以是存DB的时候同步更新ES,这个方法最简单,同步延时最低,但是代码侵入性较高
- 也可以用ETL工具定时同步到ES,该方法较为复杂,实时性较低,但是侵入性较低,适合统计一类对时间要求不是很高的系统
- 可以通过MQ的方式异步双写
- MySQL可以通过读取binlog的方式同步写到ES,代码侵入性较低的同时也保证了实时性
-
作为新闻网站数据平台,使用ES提供的强大聚合功能构建用户画像和分析用户行为(点击,浏览,收藏,评论)
-
为开源代码网站的代码仓库提供代码搜索功能
-
日志数据分析,最常见的组合就是ELK
-
BI(Business Intelligence 商业智能)系统
二、核心概念
以下的诸多核心概念可以在ES官网找到,这里只是做一个入门的前置知识普及
2.1 “You Know, for Search”
-
“你知道的,为了搜索…”
这是整个ES的开篇词,言简意赅,ES所做的一切都是为了能够快速检索
以下是开篇词的节选
Elasticsearch 是一个开源的搜索引擎,建立在一个全文搜索引擎库 Apache Lucene™ 基础之上。 Lucene 可以说是当下最先进、高性能、全功能的搜索引擎库—无论是开源还是私有。
但是 Lucene 仅仅只是一个库。为了充分发挥其功能,你需要使用 Java 并将 Lucene 直接集成到应用程序中。 更糟糕的是,您可能需要获得信息检索学位才能了解其工作原理。Lucene 非常 复杂。
Elasticsearch 也是使用 Java 编写的,它的内部使用 Lucene 做索引与搜索,但是它的目的是使全文检索变得简单, 通过隐藏 Lucene 的复杂性,取而代之的提供一套简单一致的 RESTful API。
然而,Elasticsearch 不仅仅是 Lucene,并且也不仅仅只是一个全文搜索引擎。 它可以被下面这样准确的形容:
- 一个分布式的实时文档存储,每个字段 可以被索引与搜索
- 一个分布式实时分析搜索引擎
- 能胜任上百个服务节点的扩展,并支持 PB 级别的结构化或者非结构化数据
补充
最近ES因为诸多原因准备放弃开源
庆幸的是AWS站出来生成要发布一个免费的开源ES分支版本
-
Restful风格的接口(这个会在实战环节讲解)
-
面向文档
Elasticsearch 是 面向文档 的,意味着它存储整个对象或 文档。Elasticsearch 不仅存储文档,而且 索引 每个文档的内容,使之可以被检索。在 Elasticsearch 中,我们对文档进行索引、检索、排序和过滤—而不是对行列数据。这是一种完全不同的思考数据的方式,也是 Elasticsearch 能支持复杂全文检索的原因。
-
索引
索引在ES中有多重语境
索引(名词):
一个 索引 类似于传统关系数据库中的一个 数据库 ,是一个存储关系型文档的地方。 索引 (index) 的复数词为 indices 或 indexes 。
索引(动词):
索引一个文档 就是存储一个文档到一个 索引 (名词)中以便被检索和查询。这非常类似于 SQL 语句中的
INSERT
关键词,除了文档已存在时,新文档会替换旧文档情况之外。倒排索引:
关系型数据库通过增加一个 索引 比如一个 B树(B-tree)索引 到指定的列上,以便提升数据检索速度。Elasticsearch 和 Lucene 使用了一个叫做 倒排索引 的结构来达到相同的目的。
-
类型
类型相当于传统关系型数据库中的一张表
-
文档
文档相当于传统关系型数据库中的一条记录
-
Shard(分片)
分片是ES中最小的工作单元,一个索引是若干个分片的集合
一个 分片 是一个底层的 工作单元 ,它仅保存了全部数据中的一部分。
一个查询进来后索引会交由各个分片去执行查询,最后汇总数据。
有点类似ForkJoin的工作原理,将一个大的任务拆分成若干个小任务执行后合并结果。
这也是ES说是“接近实时的搜索”原因之一。
说到Shard一定离不开我们上文提到的倒排索引不变,倒排索引的不变性有以下好处
- 不需要锁。如果你从来不更新索引,你就不需要担心多进程同时修改数据的问题。
- 一旦索引被读入内核的文件系统缓存,便会留在哪里,由于其不变性。只要文件系统缓存中还有足够的空间,那么大部分读请求会直接请求内存,而不会命中磁盘。这提供了很大的性能提升。
- 其它缓存(像filter缓存),在索引的生命周期内始终有效。它们不需要在每次数据改变时被重建,因为数据不会变化。
- 写入单个大的倒排索引允许数据被压缩,减少磁盘 I/O 和 需要被缓存到内存的索引的使用量。
当然,一个不变的索引也有不好的地方。主要事实是它是不可变的! 你不能修改它。如果你需要让一个新的文档 可被搜索,你需要重建整个索引。这要么对一个索引所能包含的数据量造成了很大的限制,要么对索引可被更新的频率造成了很大的限制。
怎样在保留不变性的前提下实现倒排索引的更新?
答案是: 用更多的索引
通过增加新的补充索引来反映新近的修改,而不是直接重写整个倒排索引。每一个倒排索引都会被轮流查询到—从最早的开始—查询完后再对结果进行合并。
更多的细节我们不再深究,感兴趣的同学可以自行学习分片内部原理
三、实战
3.1 搭建本地环境
自行百度安装ES
傻瓜式的下一步即可在本地创建ES的服务
ES启动后我们可以通过http://localhost:9200/来检验我们的ES是否启动成功
如果成功,将会看到以下的画面
由上至下分别是
- name:ES实例的名称,没有特别指定的情况下就是你的计算机名
- cluster_name:集群的名称
- cluster_uuid:集群的uuid
- version:版本相关信息
- tagline:ES最经典的开篇词,“You Know,For Search”
3.2 CRUD
程序员写的最多的肯定就是CRUD了,前面是造火箭,现在就是如何去拧螺丝了
Talk is cheap,show me the code
以下所有的实战环节,都会分为原生Restful接口调用方式和对应的Java两种实现方式
因为我们在日常工作中往往是使用封装好的框架,只要我们有一些JPA的基础知识,都可以在不太了解ES的情况下写出大部分的CRUD。这么做旨在让大家理解框架底层是怎么去调用ES的Restful接口的。
3.2.1 Restful的增删查改
一般本地调用Restful接口有两种方式
- DataGrip装ES插件
- PostMan
本教程采用第二种方式,方便大家使用和理解
-
增
Post{raw/JSON} localhost:9200/my_index/my_type { "name" : "zhangsan", "age" : 18, "gender" : "MALE" } # Response { "_index": "my_index", "_type": "my_type", "_id": "JeqXW3cBzyfdu7sj-tRZ", "_version": 1, "result": "created", "_shards": { "total": 2, "successful": 1, "failed": 0 }, "_seq_no": 0, "_primary_term": 1 }
一般来说不必特地去创建某个index或type,ES会自动根据发送的POST请求去创建对应的index和type
注意:在某些云产品上(如阿里云ES),为了安全起见不能直接创建index,可以到后台手动新建index或通过设置清除这个限制
然后我们可以再加几个属性,发送Post请求
Post{raw/JSON} localhost:9200/my_index/my_type { "name" : "zhangsan", "age" : 18, "gender" : "MALE", "company" : "Accenture", "hobby" : "swimming" } # Response { "_index": "my_index", "_type": "my_type", "_id": "JuqpW3cBzyfdu7sjyNT5", "_version": 1, "result": "created", "_shards": { "total": 2, "successful": 1, "failed": 0 }, "_seq_no": 1, "_primary_term": 1 }
可见相较于传统关系型数据库,ES可以随时改变我们存储的数据结构
-
查
接下来我们查询一下刚才插入的两条数据
很简单
GET localhost:9200/my_index/my_type/_search #Response { "took": 2, "timed_out": false, "_shards": { "total": 1, "successful": 1, "skipped": 0, "failed": 0 }, "hits": { "total": { "value": 2, "relation": "eq" }, "max_score": 1.0, "hits": [ { "_index": "my_index", "_type": "my_type", "_id": "JeqXW3cBzyfdu7sj-tRZ", "_score": 1.0, "_source": { "name": "zhangsan", "age": 18, "gender": "MALE" } }, { "_index": "my_index", "_type": "my_type", "_id": "JuqpW3cBzyfdu7sjyNT5", "_score": 1.0, "_source": { "name": "zhangsan", "age": 18, "gender": "MALE", "company": "Accenture", "hobby": "swimming" } } ] } }
-
改
下面的代码我们实现了把_id为JeqXW3cBzyfdu7sj-tRZ的文档的age field更新为20
POST localhost:9200/my_index/my_type/_update_by_query { "script": { "source": "ctx._source['age'] = 20" }, "query": { "bool": { "must": [ { "match": { "_id": "JeqXW3cBzyfdu7sj-tRZ" } } ] } } }
-
删
DELETE localhost:9200/my_index/my_type/f-HjfHgBk74RyUBNEN-s
删除数据是不需要带请求体的,只需要在url里跟上"_id"即可
3.2.2 Java的增删查改
说明
本工程会在加载的时候默认初始化1000条数据到本地的ES
如果不希望自动生成数据可以将application.yml里的prepare更改为false
如果希望生成更多的测试数据可以更改amount的值
很庆幸spring-data-elasticsearch已经为我们封装了常用的CRUD方法
打开本地elasticsearch工程
在com.workshop.elasticsearch.representation.apis.PersonController中我们可以看到本教程中提到的所有知识点对应的Java实例
如果要尝试StringQuery,可以尝试以下请求(这样我们就可以把编写query的重任交给前端同事了(开个玩笑))
POST http://localhost:8080/people
{
"query_string": {
"query": "name : 'ZHAO' and age : 1"
}
}
注:spring-data 没有专门封装update方法,可以调用save方法,save的时候如果id已存在就会更新对应的记录
3.3 复杂的查询
3.3.1 聚合(aggregations)
聚合这个操作我们可以理解为MySQL中的group by操作
在讲聚合之前不得不说两个概念
- 桶
- 指标
专有名词听起来比较晦涩,我们可以通过MySQL的一条query来理解
SELECT COUNT(color)
FROM table
GROUP BY color
其中:
COUNT(color)
相当于指标
GROUP BY color
相当于桶
我们可以通过对person的job进行聚合来统计不同job各有多少人
GET localhost:9200/person_index/_search
{
"from": 0,
"size": 20,
"aggs": {
"myAggs": {
"terms": {
"field": "job.keyword"
}
}
},
"query": {
"match_all": {}
}
}
返回的结果中有aggregations,里面封装的就是各个“桶”,以及对应的数量
Java实现在工程中
这只是聚合的简单实现,ES为我们提供了聚合的很多度量指标和桶,如果想要深入了解聚合可以参考官方文档
3.3.2 Null值查询
当我们需要查询某个字段为空/不为空的时候,需要用到以下的查询
GET localhost:9200/person_index/_doc/_search
{
"query":{
"bool":{
"must(Not)":{
"exists":{
"field" : "job"
}
}
}
}
}
3.3.3 分页查询
// TODO 普通分页和游标查询
History工程实例讲解
scroll
查询 可以用来对 Elasticsearch 有效地执行大批量的文档查询,而又不用付出深度分页那种代价。游标查询允许我们 先做查询初始化,然后再批量地拉取结果。 这有点儿像传统数据库中的 cursor 。
游标查询会取某个时间点的快照数据。 查询初始化之后索引上的任何变化会被它忽略。 它通过保存旧的数据文件来实现这个特性,结果就像保留初始化时的索引 视图 一样。
深度分页的代价根源是结果集全局排序,如果去掉全局排序的特性的话查询结果的成本就会很低。 游标查询用字段
_doc
来排序。 这个指令让 Elasticsearch 仅仅从还有结果的分片返回下一批结果。启用游标查询可以通过在查询的时候设置参数
scroll
的值为我们期望的游标查询的过期时间。 游标查询的过期时间会在每次做查询的时候刷新,所以这个时间只需要足够处理当前批的结果就可以了,而不是处理查询结果的所有文档的所需时间。 这个过期时间的参数很重要,因为保持这个游标查询窗口需要消耗资源,所以我们期望如果不再需要维护这种资源就该早点儿释放掉。 设置这个超时能够让 Elasticsearch 在稍后空闲的时候自动释放这部分资源。
https://www.elastic.co/guide/cn/elasticsearch/guide/current/scroll.html
四、踩坑记录
4.1 和JpaRepository
混用
如果一个项目同时配置了ES的Repo和JPA的repo
那么这时候需要去配置ES的包扫描范围,否则ES扫描到JPA的repo时会报错
加上@EnableElasticsearchRepositories(basePackages = “com.daimler.otr.servicehistorymanagement.infrastructure.repository.es”)
4.2 日期格式转换
ES存到数据库的时间默认是时间戳的格式,
当我们需要指定日期存储格式的时候
需要加上以下注解
@Field(type = FieldType.Date, format = DateFormat.date_time)
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'")
4.2 使用Jpa
方法名解析查询兼容性
当我们使用JPA解析方法名的方式去查询的时候
JPA只支持最基础的等值查询
比如
findAllByNameAndAge(name, age)
一些特殊的查询比如IsNull或者Between、LessThan等
不是生成不了就是生成的query可定制性不高、效率的不到保障
建议用NativeQuery
4.3 加Field并支持排序
text类型在ES中是比较特殊的一种类型
当我们要对文本类型的field进行排序、聚合的时候
一定要用keyword去做,并且mapping中必须显式声明text支持keyword
4.4 ES中的Result Window
在大数据组件中,常常会提到“深分页”和“浅分页”的问题
缘由是ES不同于传统的RDBMS,需要分页的数据都是一次性取到内存中在内存中进行分页
一旦取的数据量超出ES result window上限,就会报错
访问GET /{index_name}/_settings
能够清楚看到max_result_window即为我们设置的最大结果窗口,默认值为10000
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Q2zfRaLx-1623047845049)(image-20210327175143298.png)]
当然result window不是越大越好,窗口越大意味着更大的内存开销
所以当发现代码里存在“深分页”的时候,务必换做游标查询以防止报错和提升查询性能
4.5 查询条件过多
场景复现
detailEsRepository.findAllByServiceNumberIn(serviceNumbers) --serviceNumbers.size() = 1045条
然后ES报错query clause超过max size = 1024
解决方案
- 改elasticsearch.yml中最大查询数相关配置,然后重启集群
- 相对比较理智的改法,减少每次查询粒度。
总结
参考4.4中result window过大的问题,我们可以得出ES中尽量采用“小步快跑”的方式去做批量查询
- 条件数尽可能控制在500条以内(default max = 1024)
- 单次返回的数量不要超过5000条(default max = 10000)
引用
Elasticsearch详解-简书
Elasticsearch官方中文文档2.x
结构化数据和非结构化数据区别
Lucene、Solr、Elasticsearch区别
漫画Elasticsearch原理
MySQL 数据实时同步到 ES
Mysql 同步到ES的最佳实践
spring-data-elasticsearch method query (8.2.2 query creation)