关系型数据库范式化(Normalize)设计的主要目标是减少不必要的更新,往往会带来一些副作用:
反范式化(Denormalize)的设计不使用关联关系,而是在文档中保存冗余的数据拷贝。
关系型数据库,一般会考虑Normalize 数据;在Elasticsearch,往往考虑Denormalize 数据。
Elasticsearch并不擅长处理关联关系,一般会采用以下四种方法处理关联:
案例1: 博客作者信息
当我们查询博客信息的同时需要知道作者信息,那么把作者信息当成一个对象放到博客信息当中
PUT /blog
{
"mappings": {
"dynamic":"false",
"properties": {
"content":{
"type": "text"
},
"time":{
"type": "date"
},
"user":{
"properties": {
"city":{
"type": "keyword"
},
"userid":{
"type":"long"
},
"username":{
"type":"keyword"
}
}
}
}
},
"settings" : {
"index" : {
"analysis.analyzer.default.type": "ik_max_word"
},
"number_of_replicas": 1,
"number_of_shards": 3
}
}
PUT /blog/_doc/1
{
"content":"I like Elasticsearch",
"time":"2022-06-23T17:20:20",
"user":{
"userid":1,
"username":"lsx",
"city":"厦门"
}
}
# 查询对应作者的文档
GET blog/_search
{
"query": {
"term": {
"user.username": {
"value": "lsx"
}
}
}
}
案例2:包含对象数组的文档
PUT /my_movies
{
"mappings" : {
"properties" : {
"actors" : {
"properties" : {
"first_name" : {
"type" : "keyword"
},
"last_name" : {
"type" : "keyword"
}
}
},
"title" : {
"type" : "text",
"fields" : {
"keyword" : {
"type" : "keyword",
"ignore_above" : 256
}
}
}
}
}
}
# 写入一条电影信息
POST /my_movies/_doc/1
{
"title":"Speed",
"actors":[
{
"first_name":"Keanu",
"last_name":"Reeves"
},
{
"first_name":"Dennis",
"last_name":"Hopper"
}
]
}
# 根据first_name 和 last_name 查询全名是Keanu Hopper
GET /my_movies/_search
{
"query": {
"bool": {
"must": [
{"term": { "actors.first_name": "Keanu"}},
{"term": {"actors.last_name": "Hopper"}}
]
}
}
}
文档里面没有全名是Keanu Hopper 这个人,但是依然查了出来。查询出了不需要的数据。为什么会这样?
存储时,内部对象的边界并没有考虑在内,JSON格式被处理成扁平式键值对的结构。当对多个字段进行查询时,导致了意外的搜索结果。可以用Nested Data Type解决这个问题。
什么是Nested Data Type
DELETE /my_movies
# 创建 Nested 对象 Mapping
PUT /my_movies
{
"mappings" : {
"properties" : {
"actors" : {
"type": "nested",
"properties" : {
"first_name" : {"type" : "keyword"},
"last_name" : {"type" : "keyword"}
}},
"title" : {
"type" : "text",
"fields" : {"keyword":{"type":"keyword","ignore_above":256}}
}
}
}
}
Nested 查询和普通查询不一样,通过上面的查询是查不出结果的
GET /my_movies/_search
{
"query": {
"bool": {
"must": [
{
"nested": {
"path": "actors",
"query": {
"bool": {
"must": [
{"term": {"actors.first_name": "Keanu"}},
{"term": {"actors.last_name": "Reeves"}}
]
}
}
}
}
]
}
}
}
# nested聚合
GET /my_movies/_search
{
"size": 0,
"aggs": {
"actors": {
"nested": {
"path": "actors"
},
"aggs": {
"actor_name": {
"terms": {
"field": "actors.first_name",
"size": 10
}
}
}
}
}
}
设定 Parent/Child Mapping
PUT /my_blogs
{
"settings": {
"number_of_shards": 2
},
"mappings": {
"properties": {
"blog_comments_relation": {
"type": "join",
"relations": {
"blog": "comment"
}
},
"content": {
"type": "text"
},
"title": {
"type": "keyword"
}
}
}
}
索引父文档
#索引父文档
PUT /my_blogs/_doc/blog1
{
"title":"Learning Elasticsearch",
"content":"learning ELK ",
"blog_comments_relation":{
"name":"blog"
}
}
#索引父文档
PUT /my_blogs/_doc/blog2
{
"title":"Learning Hadoop",
"content":"learning Hadoop",
"blog_comments_relation":{
"name":"blog"
}
}
索引子文档
#索引子文档
PUT /my_blogs/_doc/comment1?routing=blog1
{
"comment":"I am learning ELK",
"username":"Jack",
"blog_comments_relation":{
"name":"comment",
"parent":"blog1"
}
}
#索引子文档
PUT /my_blogs/_doc/comment2?routing=blog2
{
"comment":"I like Hadoop!!!!!",
"username":"Jack",
"blog_comments_relation":{
"name":"comment",
"parent":"blog2"
}
}
#索引子文档
PUT /my_blogs/_doc/comment3?routing=blog2
{
"comment":"Hello Hadoop",
"username":"Bob",
"blog_comments_relation":{
"name":"comment",
"parent":"blog2"
}
}
注意:
父文档和子文档必须存在相同的分片上,能够确保查询join 的性能
当指定子文档时候,必须指定它的父文档ld。使用routing参数来保证,分配到相同的分片
查询
# 查询所有文档
GET /my_blogs/_search
#根据父文档ID查看
GET /my_blogs/_doc/blog2
# Parent Id 查询
POST /my_blogs/_search
{
"query": {
"parent_id": {
"type": "comment",
"id": "blog2"
}
}
}
# Has Child 查询,返回父文档
POST /my_blogs/_search
{
"query": {
"has_child": {
"type": "comment",
"query" : {
"term": {
"username.keyword": "Jack"
}
}
}
}
}
# Has Parent 查询,返回相关的子文档
POST /my_blogs/_search
{
"query": {
"has_parent": {
"parent_type": "blog",
"query" : {
"match": {
"title" : "Learning Hadoop"
}
}
}
}
}
#通过ID ,访问子文档
GET /my_blogs/_doc/comment3
#通过ID和routing ,访问子文档
GET /my_blogs/_doc/comment3?routing=blog2
#更新子文档
PUT /my_blogs/_doc/comment3?routing=blog2
{
"comment": "Hello Hadoop??",
"blog_comments_relation": {
"name": "comment",
"parent": "blog2"
}
}
嵌套文档 VS 父子文档
Nested Object | Parent / Child | |
---|---|---|
优点 | 文档存储在一起,读取性能高 | 父子文档可以独立更新 |
缺点 | 更新嵌套的子文档时,需要更新整个文档 | 需要额外的内存维护关系。读取性能相对差 |
适用场景 | 子文档偶尔更新,以查询为主 | 子文档更新频繁 |
应用场景: 修复与增强写入数据
Ingest Node
Elasticsearch 5.0后,引入的一种新的节点类型。默认配置下,每个节点都是Ingest Node:
无需Logstash,就可以进行数据的预处理,例如
Pipeline & Processor
一些内置的Processors:https://www.elastic.co/guide/en/elasticsearch/reference/7.17/ingest-processors.html
Split Processor : 将给定字段值分成一个数组
Remove / Rename Processor :移除一个重命名字段
Append : 为商品增加一个新的标签
Convert:将商品价格,从字符串转换成float 类型
Date / JSON:日期格式转换,字符串转JSON对象
Date lndex Name Processor︰将通过该处理器的文档,分配到指定时间格式的索引中
Fail Processor︰一旦出现异常,该Pipeline 指定的错误信息能返回给用户
Foreach Process︰数组字段,数组的每个元素都会使用到一个相同的处理器
Grok Processor︰日志的日期格式切割)
Gsub / Join / Split︰字符串替换│数组转字符串/字符串转数组
Lowercase / upcase︰大小写转换
测试数据
PUT tech_blogs/_doc/1
{
"title":"Introducing big data......111",
"tags":"hadoop,elasticsearch,spark",
"content":"You konw, for big data"
}
PUT tech_blogs/_doc/2
{
"title":"Introducing big data......222",
"tags":"java,php,js",
"content":"You konw, for big data"
}
创建Pipeline
Pipeline的功能,文档中的tags标签,通过,转成数组对象,并且增加views字段
PUT _ingest/pipeline/testPipeline
{
"description": "test",
# 定义管道处理器
"processors": [
{
# 字符串分割处理器
"split": {
"field": "tags",
"separator": ","
}
},
{
# 字符串分割处理器
"set": {
"field": "views",
"value": "0"
}
}
]
}
#查看所有管道
GET _ingest/pipeline
索引文档时,通过定义的管道加工数据
PUT tech_blogs/_doc/3?pipeline=testPipeline
{
"title":"Introducing big data......3333",
"tags":"mysql,redis,kafka",
"content":"You konw, for big data"
}
PUT tech_blogs/_doc/4?pipeline=testPipeline
{
"title":"Introducing big data......4444",
"tags":"test,text,tttt",
"content":"You konw, for big data"
}
GET tech_blogs/_doc/3
借助update_by_query更新已存在的文档
#增加update_by_query的条件
POST tech_blogs/_update_by_query?pipeline=blog_pipeline
{
"query": {
"bool": {
"must_not": {
"exists": {
"field": "views"
}
}
}
}
}
GET tech_blogs/_search
Ingest Node VS Logstash
Logstash | Ingest Node | |
---|---|---|
数据输入与输出 | 支持从不同的数据源读取,并写入不同的数据源 | 支持从ES REST API获取数据,并且写入Elasticsearch |
数据缓冲 | 实现了简单的数据队列,支持重写 | 不支持缓冲 |
数据处理 | 支持大量的插件,也支持定制开发 | 内置的插件,可以开发Plugin进行扩展(Plugin更新需要重启) |
配置和使用 | 增加了一定的架构复杂度 | 无需额外部署 |
自Elasticsearch 5.x后引入,专门为Elasticsearch 设计,扩展了Java的语法。6.0开始,ES只支持 Painless。Groovy,JavaScript和 Python 都不再支持。Painless支持所有Java 的数据类型及Java API子集。
Painless Script具备以下特性:
Painless的用途:
通过Painless脚本访问字段
上下文 | 语法 |
---|---|
Ingestion | ctx.field_name |
Update | ctx._source.field_name |
Search & Aggregation | doc[“field_name”] |
管道中定义脚本代码
PUT _ingest/pipeline/test2PP
{
"description": "test2",
"processors": [
{
"split": {
"field": "tags",
"separator": ","
}
},
{ # java 脚本代码 计算content长度
"script": {
"source": """
if(ctx.containsKey("content")){
ctx.content_length = ctx.content.length();
}else{
ctx.content_length=0;
}
"""
}
},
{
"set": {
"field": "views",
"value": "0"
}
}
]
}
索引文档的时通过管道执行脚本
PUT tech_blogs/_doc/5?pipeline=test2PP
{
"title":"Introducing big data......555",
"tags":"mysql,redis,kafka",
"content":"You konw, for big data"
}
GET tech_blogs/_doc/5
更新时执行脚本代码
POST tech_blogs/_update/4
{
"script": {
"source": "ctx._source.views += params.new_views",
"params": {
"new_views":100
}
}
}
定义脚本代码
POST _scripts/testScripts
{
"script":{
"lang": "painless",
"source": "ctx._source.views += params.new_views"
}
}
执行定义的脚本代码
POST tech_blogs/_update/4
{
"script": {
"id": "testScripts",
"params": {
"new_views":1000
}
}
}
查询中执行脚本代码
DELETE tech_blogs
PUT tech_blogs/_doc/1
{
"title":"Introducing big data......",
"tags":"hadoop,elasticsearch,spark",
"content":"You konw, for big data",
"views":0
}
GET tech_blogs/_search
{
"script_fields": {
"rnd_views": {
"script": {
"lang": "painless",
"source": """
java.util.Random rnd = new Random();
doc['views'].value+rnd.nextInt(1000);
"""
}
}
},
"query": {
"match_all": {}
}
}
脚本编译的开销较大,Elasticsearch会将脚本编译后缓存在Cache 中
Inline scripts和 Stored Scripts都会被缓存,默认缓存100个脚本
参数 | 说明 |
---|---|
script.cache.max_size | 设置最大缓存数 |
script.cache.expire | 设置缓存超时 |
script.max_compilations_rate | 默认5分钟最多75次编译(75/5m) |
建模建议1:如何处理关联关系
建模建议2: 避免过多字段
一个文档中,最好避免大量的字段
默认最大字段数是1000,可以设置index.mapping.total_fields.limit
限定最大字段数。
建模建议3︰避免正则,通配符,前缀查询
正则,通配符查询,前缀查询属于Term查询,但是性能不够好。特别是将通配符放在开头,会导致性能的灾难
建模建议4︰避免空值引起的聚合不准
# Not Null 解决聚合的问题
DELETE /scores
PUT /scores
{
"mappings": {
"properties": {
"score": {
"type": "float",
"null_value": 0
}
}
}
}
PUT /scores/_doc/1
{
"score": 100
}
PUT /scores/_doc/2
{
"score": null
}
POST /scores/_search
{
"size": 0,
"aggs": {
"avg": {
"avg": {
"field": "score"
}
}
}
}
如果不在mapping 中加上 “null_value”: 0 那么聚合会出现错误
建模建议5: 为索引的Mapping加入Meta 信息
Mappings设置非常重要,需要从两个维度进行考虑
Mappings设置是一个迭代的过程