一、 引言
全文搜索属于最常见的需求,开源的 Elasticsearch (以下简称 Elastic)是目前全文搜索引擎的首选。
它可以快速地储存、搜索和分析海量数据。维基百科、Stack Overflow、Github 都采用它。
Elastic 的底层是开源库 Lucene。但是,你没法直接用 Lucene,必须自己写代码去调用它的接口。Elastic 是 Lucene 的封装,提供了 REST API 的操作接口,开箱即用。
Elasticsearch 在分布式开源搜索和分析引擎中处于领先地位,能在短时间内搜索、分析大量数据,并作为查询数据的存储系统。不过,在使用Elasticsearch的时候,我们需要注意避开以下坑点。
二、如何使用Elasticsearch设计表结构
ES是基于索引(倒排索引)的设计,因此没法像MySQL一样使用join查询,因此使用ES查询数据的时候需要把每条主数据及关联子表的数据全部整合在一条记录中。
比如,MySQL中有一个订单数据,使用ES查询的时候,我们会把每条主数据及关联的数据全部整合在下表中:
从上表可以看到,使用ES存储数据时并不会设计多个表,而是将所有表的相关字段汇聚在一个Document中,即一个完整的文档结构,类似这样(这里使用json),代码示例:
{
"order_id": {
"order_id": "O2020103115214521",
"order_invoice": {
},
"user": {
"user_id": "U1099",
"user_name": "李大侠"
},
"order_product_item": [
{
"product_name": "乒乓球拍",
"product_count": 1,
"product_price": 149
},
{
"product_name": "纸巾",
"product_count": 2,
"product_price": 1.4
}
],
"total_amount": 20
}
}
你可能会问,为什么所有表汇聚在一个Document中,而不是设计成多个表?为什么ES不需要关联查询?
要解答这个问题,我们现在需要了解ES的存储结构原理。
ES的存储结构
ES 是一个分布式的查询系统,每一个节点都是一个基于Lucene的查询引擎。
1)索引(Index)
Elasticsearch数据管理的顶层单位叫做Index(索引),相当于关系型数据库的数据库的概念。每个Index的名字必须是小写。
2)文档(Document)
Index里面单条的记录称为Document(文档)。许多Document构成了一个Index。Document使用JSON格式表示。同一个Index里面的Document,不要有有相同的结构(scheme),但是最好保持相同,这样有利于提高搜索效率。
3)类型(Type)
Document 可以分组,比如employee这个 Index 里面,可以按部门分组,也可以按职级分组。这种分组就叫做 Type,它是虚拟的逻辑分组,用来过滤 Document,类似关系型数据库中的数据表。
不同的 Type 应该有相似的结构(Schema),性质完全不同的数据(比如 products 和 logs)应该存成两个 Index,而不是一个 Index 里面的两个 Type(虽然可以做到)。
4)文档元数据(Document metadata)
文档元数据为_index, _type, _id, 这三者可以唯一表示一个文档,_index表示文档在哪存放,_type表示文档的对象类别,_id为文档的唯一标识。
5)字段(Fields)
每个Document都类似一个JSON结构,它包含了许多字段,每个字段都有其对应的值,多个字段组成了一个 Document,可以类比关系型数据库数据表中的字段。
|| Lucene与MySQL的概念比对
Lucene是一个索引系统,通过从易到难的方式,把Lucene与MySQL的一些概念简单做映射:
|| 无结构文档的倒排索引(Index)
Lucene使用的是倒排索引的结构。举个例子:
倒排索引后,显示结果如下:
我们发现:无结构的文档经过简单的倒排索引后,字典表主要存放关键字,而倒排表存放该关键字所在的文档ID。
通过上面简单的例子,我们已经明白倒排索引的结构,但是表数据往往是有结构的,并不是一篇篇文章。如果一个文档有结构呢,该怎么办?
|| 有结构文档的倒排索引(Index)
举例:每个Doc 都有Field,Field有不同的值(包含不同的Term),倒排索引的结构参考如下图所示:
也就是说:有结构的文档经过倒排索引后,字段中的每个值都是一个关键字,存放在左边的 Term Dictionary(词汇表)中,且每个关键字都有对应地址指向所在文档。
|| ES的Document怎么定义结构体和字段格式
由于ES是基于索引的设计,并不需要像MySQL关联表,而是把所有相关数据汇聚在1个Document中。例子:
我们直接将刚刚order的Json文档转成一个ES定义文档命令(注意:SQL的子表数据,在ES中需要以嵌入式对象的格式存储),代码示例:
{
"mappings":{
"doc":{
"properties":{
"order_id":{
"type":"text"
},
"order_invoice":{
"type":"nested"
},
"order_product_item":{
"type":"nested",
"properties":{
"product_count":{
"type":"long"
},
"product_name":{
"type":"text"
},
"product_price":{
"type":"float"
}
}
},
"total_amount":{
"type":"long"
},
"user":{
"properties":{
"user_id":{
"type":"text"
},
"user_name":{
"type":"text"
}
}
}
}
}
}
}
三、Elasticsearch如何修改表结构
实际业务中,ES支持直接增加新的字段,但ES不支持修改原来的字段类型。需要注意,修改字段的类型会导致索引失效。
如果你想修改字段的映射,首先需要新建一个索引,然后使用ES的reindex功能将旧索引拷贝到新索引中。
reindex是ES自带的API:
POST _reIndex
{
"source": {
"Index": "my-Index-000001"
},
"dest": {
"Index": "my-new-Index-000001"
}
}
不过,直接重命名字段时,我们使用reindex功能会导致原来保存的旧字段名的索引数据失效,这种情况该如何解决?
此时我们可以使用alias索引功能,代码示例:
PUT trips
{
"mappings": {
"properties": {
"distance": {
"type": "long"
},
"route_length_miles": {
"type": "alias",
"path": "distance"
},
"transit_mode": {
"type": "keyword"
}
}
}
}
MySQL使用时,不建议直接修改字段的类型、改名或者删字段。因为,更新版本后,都要做好回滚的打算,因此设计每个版本对应数据库时,尽量兼容前面版本的代码。
ES的结构基于MySQL设计,两者之间存在对应关系,因此不建议直接修改ES的表结构。
问:如果一定要有修改的需求呢?应该如何做?
答:先保留旧的字段,然后直接添加并使用新的字段,直到新版本的代码全部稳定工作后,再找时间清理旧的不用的字段。(分2个版本完成修改需求)