关系型数据库的范式化设计:范式化设计(Normalization)的主要目的是减少不必要的更新,但是一个完全范式化设计的数据会经常面临查询缓慢的问题(数据库越范式化,需要Join的表就越多)

反范式化设计(Denormalization):数据扁平,不使用关联关系,而是在文档中保存冗余的数据拷贝

  • 优点:无需处理Join操作,数据读取性能好(Elasticsearch通过压缩_source字段,减少磁盘的开销)
  • 缺点:不适合在数据频繁修改的场景

关系型数据库一般会考虑Normalize数据,在Elasticsearch,往往考虑Denormalize数据(Denormalize的好处:读的速度快/无需表连接/无需行锁)

Elasticsearch并不擅长处理关联关系,一般采取以下四种方式处理

  • 对象类型
  • 嵌套对象(Nested Object)
  • 父子关联关系(Parent/Child)
  • 应用端关联

对比

Nested Object

Parent/Child

优点

文档存储在一起,读取性能高

父子文档可以独立更新

缺点

更新嵌套子文档时,需要更新整个文档

需要额外的内存维护关系,读取性能相对差

对象类型

案例一:文章和作者的信息(1:1关系)

DELETE articles
#设置articles的mappings信息
PUT /articles  
{  
  "mappings": {  
    "properties": {  
      "content": {  
        "type": "text"  
      },  
      "time": {  
        "type": "date"  
      },  
      "author": {  
        "properties": {
          "userid": {  
            "type": "long"  
          },  
          "username": {  
            "type": "keyword"  
          }  
        }  
      }  
    }  
  }  
} 
#插入一条测试数据
PUT articles/_doc/1  
{  
  "content":"Elasticsearch Helloworld!",  
  "time":"2020-01-01T00:00:00",  
  "author":{  
    "userid":1001,  
    "username":"liu"
  }  
} 
#查询
POST articles/_search  
{  
  "query": {  
    "bool": {  
      "must": [  
        {"match": {  
          "content": "Elasticsearch"  
        }},  
        {"match": {  
          "author.username": "liu"  
        }}  
      ]  
    }  
  }  
}

案例二:文章和作者的信息(1:n关系)(有问题!

DELETE articles
#设置articles的mappings信息
PUT /articles  
{  
  "mappings": {  
    "properties": {  
      "content": {  
        "type": "text"  
      },  
      "time": {  
        "type": "date"  
      },  
      "author": {  
        "properties": {
          "userid": {  
            "type": "long"  
          },  
          "username": {  
            "type": "keyword"  
          }  
        }  
      }  
    }  
  }  
} 
POST articles/_search
#插入一条测试数据
PUT articles/_doc/1  
{  
  "content":"Elasticsearch Helloworld!",  
  "time":"2020-01-01T00:00:00",  
  "author":[{  
    "userid":1001,  
    "username":"liu"
  },{
    "userid":1002,  
    "username":"jia"
  }]
} 
#查询(这样也能查到!为什么出现这种结果呢?)
POST articles/_search  
{  
  "query": {  
    "bool": {  
      "must": [  
        {"match": {  
          "author.userid": "1001"  
        }},  
        {"match": {  
          "author.username": "jia"  
        }}  
      ]  
    }  
  }  
}

当使用对象保存有数组的文档时,我们发现会查询到不需要的结果,原因是什么呢?

存储时,内部对象的边界并没有考虑在内,JSON格式被处理成扁平式键值对的结构,当对多个字段进行查询时,导致了意外的搜索结果

"content":"Elasticsearch Helloworld!"
"time":"2020-01-01T00:00:00"
"author.userid":["1001","1002"]
"author.username":["liu","jia"]

使用嵌套对象(Nested Object)可以解决这个问题

嵌套对象

允许对象数组中的对象被独立索引,使用Nested和properties关键字将所有author索引到多个分隔的文档 ,在内部,Nested文档会被保存在两个Lucene文档中,在查询时做Join处理

案例一:文章和作者的信息(1:n关系)

DELETE articles
#设置articles的mappings信息
PUT /articles  
{  
  "mappings": {  
    "properties": {  
      "content": {  
        "type": "text"  
      },  
      "time": {  
        "type": "date"  
      },  
      "author": {  
        "type": "nested", 
        "properties": {
          "userid": {  
            "type": "long"  
          },  
          "username": {  
            "type": "keyword"  
          }  
        }  
      }  
    }  
  }  
} 
POST articles/_search
#插入一条测试数据
PUT articles/_doc/1  
{  
  "content":"Elasticsearch Helloworld!",  
  "time":"2020-01-01T00:00:00",  
  "author":[{  
    "userid":1001,  
    "username":"liu"
  },{
    "userid":1002,  
    "username":"jia"
  }]
} 
#查询(这样也能查到!为什么出现这种结果呢?)
POST articles/_search  
{  
  "query": {  
    "bool": {  
      "must": [  
        {"nested": {
          "path": "author",
          "query": {  
            "bool": {  
              "must": [  
                {"match": {  
                  "author.userid": "1001"  
                }},  
                {"match": {  
                  "author.username": "jia"  
                }}  
              ]  
            }  
          }
        }}
      ]  
    }  
  }  
}

父子关联关系

对象和Nested对象都存在一定的局限性,每次更新需要重新索引整个对象,Elasticsearch提供了类似关系型数据库中Join的实现,可以通过维护Parent/Child的关系,从而分离两个对象,父文档和子文档是两个独立的文档,更新父文档无需重新索引子文档,子文档被添加,更新或删除也不会影响到父文档和其他的子文档

案例:文章和作者的信息(1:n关系)

DELETE articles
#设置articles的mappings信息
PUT /articles  
{  
  "mappings": {  
    "properties": {  
      "article_author_relation": {  
        "type": "join",  
        "relations": {  
          "article": "author"  
        }
      },
      "content": {  
        "type": "text"  
      },  
      "time": {  
        "type": "date"  
      }
    }  
  }  
} 
#索引父文档
PUT articles/_doc/article1
{  
  "article_author_relation":{
    "name":"article"
  },
  "content":"Elasticsearch Helloworld!",  
  "time":"2020-01-01T00:00:00"
} 
#索引子文档
PUT articles/_doc/author1?routing=article1
{  
  "article_author_relation":{
    "name":"author",
    "parent":"article1"
  },
  "userid":"1001",  
  "username":"jia"
} 
PUT articles/_doc/author2?routing=article1
{  
  "article_author_relation":{
    "name":"author",
    "parent":"article1"
  },
  "userid":"1002",  
  "username":"liu"
} 
GET articles/_doc/article1
POST articles/_search
#根据parent_id父文档id查询子文档
POST articles/_search  
{  
  "query": {
    "parent_id":{
      "type":"author",
      "id":"article1"
    }
  }  
}
#has_child返回父文档
POST articles/_search  
{  
  "query": {
    "has_child":{
      "type":"author",
      "query": {
        "match": {
          "username": "liu"
        }
      }
    }
  }  
}
#has_parent返回子文档
POST articles/_search  
{  
  "query": {
    "has_parent":{
      "parent_type":"article",
      "query": {
        "match": {
          "content": "elasticsearch"
        }
      }
    }
  }  
}