1. 多表关联设计
es多表关联的问题是讨论最多的问题之一。多表关联通常指一对多或者多对多的数据关系,如博客及其评论的关系。
之前讲过的Nested嵌套类型、Join父子文档类型都可以算作多表关联类型。此外,es还支持宽表冗余存储、业务端关联的多表关联方式。
1.1 nested 嵌套类型
Nested类型是es映射定义的对象类型之一。实战中需要注意以下两点。
- 当使用嵌套文档时,使用通用的查询方式是无法进行访问的,必须使用合适的查询方式(nested query、nested filter、nested facet等)。很多场景下,使用嵌套文档的复杂度在于索引阶段对关联关系的组合拼装。
- index.mapping.nested_fields.limit的默认值是50,即一个索引中最大允许拥有50个Nested类型的数据。index.mapping.nested_objects.limit的默认值是10000,即一个文档中所有Nested类型的JSON对象数据的总量是10000。
Nested类型适用于一对少量、子文档偶尔更新、查询频繁的场景。如果需要索引对象数组并保持数组中每个对象的独立性,则应使用Nested数据类型而不是Object数据类型。
Nested类型的优点是Nested文档可以将父子关系的两部分数据关联起来(例如博客与评论),可以基于Nested类型做任何查询。其缺点则是查询相对较慢,更新子文档时需要更新整篇文档。
1.2 Join父子文档类型
Join类型用于在同一索引的文档中创建父子关系。
Join类型适用于子文档数据量明显多于父文档的数据量的场景,该场景存在一对多量的关系,子文档更新频繁。举例来说,一个产品和供应商之间就是一对多的关联关系。当使用父子文档时,使用has_child或者has_parent做父子关联查询。
Join类型的优点是父子文档可独立更新。缺点则是维护Join关系需要占据部分内存,查询较Nested类型更耗资源。
1.3 宽表冗余存储
非规范化数据就是“冗余存储”,即对每个文档保持一定数量的冗余数据以避免访问时进行多表关联。通过Logstash同步关联数据到es时,通常建议先通过视图对MySQL数据做好多表关联,然后同步视图数据到es。此处的视图可以理解为宽表的雏形。
宽表适用于一对多或者多对多的关联关系。
宽表的优点是速度快。因为每个文档都包含了所需的所有信息,当这些信息需要查询并匹配时,并不需要进行昂贵的关联操作,本质是以空间换时间。缺点则是索引更新或删除数据时,应用程序不得不处理宽表的冗余数据;并且由于冗余存储,某些搜索和聚合操作的结果可能不准确。
1.4 业务端关联
普遍使用的技术,即在应用接口层面处理关联关系。一般建议在存储层面使用两个独立索引存储,在实际业务层面这将分为两次请求来完成。
适用于数据量少的多表关联场景。数据量少时,用户体验好;而数据量多时,两次查询耗时肯定会比较长,反而影响用户体验。
1.5 多表关联方案对比
不建议在es中做多表关联操作,尽量在设计时使用扁平的宽表文档模型,或者尽量将业务转化为没有关联关系的文档形式,在文档建模处多下功夫,以提升检索效率。
Nested嵌套类型和Join父子文档类型必须考虑性能问题。Nested类型检索使得检索效率慢几倍,Join父子文档类型检索则会使得检索效率慢几百倍,所以选型时要慎之又慎。
2. 内部数据结构解读
es的特点之一是分布式文档存储。不会将信息存储为类似数据库的行,而是存储为已序列化为JSON文档的复杂数据结构。当集群中有多个es节点时,存储的文档会分布在整个集群中,并且可以从任何节点立即进行访问。
存储文档后,将在1s(默认刷新频率)内近实时地对其进行索引和完全搜索。如何做到快速索引和全文检索呢?es使用倒排索引的数据结构。该结构能实现非常快速的全文本搜索。
倒排索引列出了所有文档中的每个唯一词项,并标识了词项出现的文档。索引可以认为是文档的优化集合,每个文档都是字段的集合,这些字段包含数据的键值对。默认情况下,es会对每个字段中的所有数据建立索引,并且每个索引字段都具有专门的优化数据结构。例如文本字段存储在倒排索引中,而数字字段和地理字段存储在BKD树中.
不同字段具有属于自己字段类型的特定优化数据结构,并具备快速响应返回搜索结果的能力,这使得es搜索速度非常快。
2.1 倒排索引
特点:在索引时创建,序列化到磁盘,全文搜索非常快,不适合排序,默认开启。
适用场景:文本搜索引擎,文档检索系统(图书馆),企业内部搜索
2.2 正排索引
doc_values被定义为“正排索引”。默认情况下每个字段的doc_values都是激活的(除了text类型)。正排索引是在索引时创建的。当字段索引时,es为了能够快速检索,会把字段的值加入倒排索引中,同时会存储该字段的正排索引。
特点:在索引时创建,序列化到磁盘,适合排序、聚合操作,将单个字段的所有值一起存储在单个数据列中,默认情况下除text外的字段均启用正排索引。
适用场景:对一个字段排序、聚合,某些过滤场景
注意:当工作集远小于节点可用内存时,系统自动将所有文档值保存在内存,读写迅速。远大于时则系统自动把doc——values加载到系统的页缓存,避免内存溢出。
对于不需要排序、聚合、脚本计算、地理位置过滤的业务场景,可以考虑禁用doc_values,节约存储。
PUT lwy_test
{
"mappings": {
"properties": {
"title":{
"type": "keyword",
"doc_values": false
}
}
}
}
2.3 fielddata
搜索过程中需要解决“哪个文档包含此词”的问题,可以通过倒排索引实现。然而在排序和聚合时,需要解决一个不同的问题,即根据哪个字段的值对文档进行排序、聚合或统计。这种情况下可以使用正排索引来解决这个问题。
但text类型不支持正排索引。在查询时系统会为其创建基于内存的数据结构fielddata。当text字段被用于聚合、排序或脚本操作时,fielddata会按需构建相应的数据结构。
fielddata从磁盘读取每个字段的完整倒排索引,反转词项与文档之间的关系,并将结果存储在JVM堆的内存中构建的。这使得在进行聚合、排序和脚本操作时,系统能够高效地获取和处理相关字段的数据。
PUT lwy_index
{
"mappings": {
"properties": {
"body": {
"type": "text",
"analyzer": "standard",
"fielddata": true
}
}
}
}
POST lwy_index/_bulk
{"index":{"_id":1}}
{"body":"the quick brown fox"}
{"index": {"_id":2}}
{"body":"quick foxes"}
GET lwy_index/_search
{
"size": 0,
"query": {
"match": {
"body": "brown"
}
},
"aggs": {
"popular_terms": {
"terms": {
"field": "body"
}
}
}
}
特点:仅适用于text字段类型,在查询时创建,基于内存的数据结构,不序列化到磁盘,默认情况禁用(构建昂贵,需要堆内预置)
适用场景:全文统计词频、生成词云,聚合、排序、脚本计算
注意事项:启用字段前需考虑为什么将文本字段用于聚合、排序或脚本操作,除业务特殊需求外启用fielddata没意义(太耗内存),若仅用于全文搜索则不该启用
2.4 _source字段
_source字段包含创建文档时传递的原始JSON文档主体。_source字段本身未构建索引(因此不可搜索),但已存储该字段,以便在执行获取请求(如GET或search请求)时可以将其返回。
PUT lwy_index
{
"mappings": {
"_source": {
"enabled": false // 禁用
}
}
}
禁用_source后,Update、update_by_query和reindex API,以及高亮操作将不可用,所以要在存储空间、业务场景之间权衡利弊后选型。建议开着
2.5 store字段
对于某些特殊场景,比如只想检索单个字段或几个字段的值,而不是整个_source的值,这时store字段就派上用场了。
PUT lwy_index
{
"mappings": {
"_source": {
"enabled": false
},
"properties": {
"title": {
"type": "text",
"store": true // 只存储某些字段
},
"content": {
"type": "text"
}
}
}
}
PUT lwy_index/_doc/1
{
"title": "some short title",
"content": "A very long content"
}
GET lwy_index/_search // 搜不出
GET lwy_index/_search // 搜的出
{
"stored_fields": ["title"]
}
适用场景:例如,采集的新闻数据是带有标题、日期和篇幅巨大的内容字段的文档,则可以只检索标题和日期,而不必从较大的_source字段中提取这些字段。
总结:字段类型不一样,存储不一样。倒排索引默认所有字段都启用,正排索引是对非text类型默认启用,_source(存储原始文档的所有字段的JSON结构数据)和store(存储指定字段的JSON数据)的启用与否需要结合业务实际。
3. null_value
实战中经常会遇到定义空值、检索指定空值数据的情况。
PUT lwy_index
{
"mappings": {
"properties": {
"status_code": {
"type": "keyword"
}
}
}
}
PUT lwy_index/_bulk
{"index": {"_id":1}}
{"status_code":null }
{"index": {"_id":2}}
{"status_code":"" }
{"index": {"_id":3}}
{"status_code":[] }
POST lwy_index/_search
{
"query": {
"term": {
"status_code": null
}
}
}
#检索报错
1)接受一个字符串值替换所有显式的空值,默认为null,这意味着该字段被视为丢失。
2)空值不能被索引或搜索。当字段设置为null(空数组或null值的数组)时,将其视为该字段没有值。
使用null_value可以用指定的值替换显式的空值,以便对其进行索引和搜索
PUT lwy_index
{
"mappings": {
"properties": {
"status_code": {
"type": "keyword",
"null_value": "NULL" // 用指定的值替换显式的空值
}
}
}
}
PUT lwy_index/_bulk
{"index": {"_id":1}}
{"status_code":null }
{"index": {"_id":2}}
{"status_code":"NULL"}
{"index": {"_id":3}}
{"status_code":[]}
POST lwy_index/_search
{
"query": {
"term": {
"status_code": "NULL"
}
}
}
null_value必须和定义的数据类型匹配,如long类型字段不能有string类型的null_value
支持null_value的字段:括Arrays、Boolean、Date、geo_point、IP、Keyword、numeric、point
text类型不支持null_value还想使用怎么办?使用multi_fields,借助keyword和text组合类型达到业务需求
PUT lwy_index
{
"mappings": {
"properties": {
"title":{
"type":"text",
"fields": {
"kw": {
"type": "keyword",
"null_value": "NULL"
}
}
}
}
}
}