乐趣区

关于elasticsearch:ES学习三nested对象

对于事实中一对多的场景比方一个商品除了题目、形容等根本信息外还会有尺寸色彩等多个属性,一篇博文可能会有多条评论信息,在 ES 有方法反对这种一对多的数据结构吗?答案是有的,比拟常见的形式有:

  1. 嵌套对象
  2. 嵌套文档 (nested)
嵌套对象

订单场景中会有主单与子单的概念,一个主单可能有多个子单,在 ES 中如果咱们应用嵌套对象进行存储那是怎么的呢?咱们先给出示例索引的 mapping:

{
    "order_index":{
        "mappings":{
            "_doc":{
                "properties":{
                    "orderId":{"type":"keyword"},
                    "orderNo":{"type":"keyword"},
                    "orderUserName":{"type":"keyword"},
                    "orderItems":{
                        "properties":{
                            "orderItemId":{"type":"keyword"},
                            "orderId":{"type":"keyword"},
                            "productName":{"type":"keyword"},
                            "brandName":{"type":"keyword"},
                            "sellPrice":{"type":"keyword"}
                        }
                    }
                }
            }
        }
    }
}

其中 orderItems 是子单列表的详情字段,它本身也是一个对象。增加示例数据后:

[
    {
        "_index":"order_index",
        "_type":"_doc",
        "_id":"1",
        "_score":1,
        "_source":{
            "orderId":"1",
            "orderNo":"123456",
            "orderUserName":"张三",
            "orderItems":[
                {
                    "orderItemId":"12234",
                    "orderId":"1",
                    "productName":"火腿肠",
                    "brandName":"双汇",
                    "sellPrice":"28"
                },
                {
                    "orderItemId":"12235",
                    "orderId":"1",
                    "productName":"果冻",
                    "brandName":"汇源",
                    "sellPrice":"12"
                }
            ]
        }
    }
]

当初咱们想查问商品名称为‘火腿肠’并且品牌为‘汇源’的订单,DSL 语句如下:

POST order_index/_search

{
    "query":{
        "bool":{
            "must":[
                {
                    "match":{"orderItems.productName":"火腿肠"}
                },
                {
                    "match":{"orderItems.brandName":"汇源"}
                }
            ]
        }
    }
}

查问后果:

[
    {
        "_index":"order_index",
        "_type":"_doc",
        "_id":"1",
        "_score":1,
        "_source":{
            "orderId":"1",
            "orderNo":"123456",
            "orderUserName":"张三",
            "orderItems":[
                {
                    "orderItemId":"12234",
                    "orderId":"1",
                    "productName":"火腿肠",
                    "brandName":"双汇",
                    "sellPrice":"28"
                },
                {
                    "orderItemId":"12235",
                    "orderId":"1",
                    "productName":"果冻",
                    "brandName":"汇源",
                    "sellPrice":"12"
                }
            ]
        }
    }
]

很显然咱们的示例数据中是没有商品名称为‘火腿肠’并且品牌为‘汇源’的订单的,但这里会什么会查出来呢?
这里因为 ES 对于 json 对象数组做了压扁解决,下面的示例在 ES 中是这样被存储的:

{"orderId": [ 1],
  "orderItems.productName":["火腿肠","果冻"],
  "orderItems.brandName": ["双汇","汇源"],
  ...
}

很显著,这样的构造失落了商品名称和品牌名称的关联导致查问的时候数据不精确。如果想精确查问到数据,有没其它形式呢?答案是有的,接着咱们来看下嵌套文档。

嵌套文档

针对数组对象被 ES 以扁平化模式存储的状况,能够通过嵌套文档的形式解决。咱们将之前的 mapping 批改下,次要是将 orderItems 的数据类型改成 nested:

{
    "properties":{
        "orderItems":{
            "properties":{....},
            "type":"nested"
        }
  ....
    }
}

咱们还是以同样的条件查问:

POST order_index/_search

{
    "query":{
        "nested":{
            "path":"orderItems",
            "query":{
                "bool":{
                    "must":[
                        {
                            "match":{"orderItems.productName":"火腿肠"}
                        },
                        {
                            "match":{"orderItems.brandName":"汇源"}
                        }
                    ]
                }
            }
        }
    }
}

这里的后果为空,这是正确的。
nested 文档里的子文档在 ES 外部其实是一个独立的文档,咱们在查问的时候,ES 替咱们作了相似数据里的 join 操作,给咱们的感觉是在操作一个文档。
须要留神的是,如果咱们要更新文档里栽个属性的值,ES 的流程是: 将原来的数据删除,再从新插入一条,只不过文档 ID 是雷同的。,所以更新数量较大的话这会对 ES 性能有肯定影响的。

工作中也有应用 nested 文档的场景,参加的我的项目是一个游戏账号交易的商城,这里贴一下相干的 mapping:

"mappings":{
        "goods":{
            "properties":{
                "game_id":{"type":"long"},
                "game_name":{
                    "type":"text",
                    "analyzer":"ik_max_word",
                    "search_analyzer":"ik_max_word",
                    "fields":{
                        "keyword":{"type":"keyword"}
                    }
                },
                "game_other_name":{"type":"keyword"},
                "goods_no":{"type":"long"},
                "goods_title":{
                    "type":"text",
                    "analyzer":"ik_max_word",
                    "search_analyzer":"ik_max_word",
                    "fields":{
                        "keyword":{"type":"keyword"}
                    }
                },
                "goods_status":{"type":"short"},
                "goods_attr":{
                    "type":"nested",
                    "properties":{"parent_attr_id":{"type":"long"},
                        "child_attr_value":{"type":"long"}
                    }
                },
                "goods_tags":{
                    "type":"nested",
                    "properties":{"tag_type":{"type":"short"},
                        "tag_name":{"type":"text"}
                    }
                },
                "del_flag":{"type":"short"},
                "source":{"type":"short"}
            }
        }
    }

这里只是一部分字段,每个游戏都会属性,比方 ’ 王者光荣 ’ 会有‘典藏皮肤’、‘贵族等级’等属性,‘火影忍者’会有‘A 忍’、‘S 忍’等属性,所以这里的游戏属性 goods_attr 应用了 nested,另外思考到有的账号属性会特地多,如果存文字的话占用的空间会特地大,且在更新的深造性降落也很厉害,所以在设计的时候游戏属性内容并不是存的文字内容而是存的 ID,也就是 parent_attr_id 与 child_attr_value 存的都是 ID,这里不好的中央也很显著,就是用户须要搜寻的关键词只能精准而不能含糊匹配属性内容(这里的精准指的通过属性的 ID 值匹配)。
另外这里再记录下针对 nested 文档应用 DSL 查问的一个示例,比方,这里须要查问游戏名为‘英雄联盟’,且价格为 300-1000 元的游戏账号:

{
    "from": 0,
    "size": 15,
    "query": {
        "bool": {
            "must": [{
                "multi_match": {
                    "query": "英雄联盟",
                    "fields": ["game_name^1.0", "game_name.keyword^1.0"],
                    "type": "best_fields",
                    "operator": "OR",
                    "slop": 0,
                    "prefix_length": 0,
                    "max_expansions": 50,
                    "zero_terms_query": "NONE",
                    "auto_generate_synonyms_phrase_query": true,
                    "fuzzy_transpositions": true,
                    "boost": 1.0
                }
            }],
            "filter": [{
                "nested": {
                    "path": "goods_attr",
                    "query": {
                        "bool": {
                            "must": [{
                                    "match": {"goods_attr.parent_attr_id": 101}
                                },
                                {
                                    "range": {
                                        "goods_attr.child_attr_value": {
                                            "from": 300,
                                            "to": 1000,
                                            "include_lower": true,
                                            "include_upper": true,
                                            "boost": 1.0
                                        }
                                    }
                                }
                            ]
                        }
                    }

                }
            }, {
                "term": {
                    "goods_status": {
                        "value": 1,
                        "boost": 1.0
                    }
                }
            }],
            "adjust_pure_negative": true,
            "boost": 1.0
        }
    }
}

这里 goods_attr 里的 parent_attr_id 的值 101 是‘价格’的 ID,child_attr_value 存的则是具体价格范畴。转换成 RestHighLevelClient 的写法令是:

...
for(Map.Entry<Long,Map<String,Integer>> entry : attrRangeMap.entrySet()){TermQueryBuilder termQueryBuilder = QueryBuilders.termQuery(attrTriple.getMiddle(), entry.getKey());
    RangeQueryBuilder rangeQueryBuilder = QueryBuilders.rangeQuery(attrTriple.getRight());
    Map<String, Integer> value = entry.getValue();
    Integer min = value.get("min");
    Integer max = value.get("max");
    rangeQueryBuilder.gte(min);
    rangeQueryBuilder.lte(max);
    BoolQueryBuilder filterBuilder = QueryBuilders.boolQuery().filter(termQueryBuilder).filter(rangeQueryBuilder);//1--------
    NestedQueryBuilder nestedRangeBuilder = QueryBuilders.nestedQuery(attrTriple.getLeft(),filterBuilder,ScoreMode.Avg);
    boolQueryBuilder.filter(nestedRangeBuilder);
}
....

标识 1 那里是两个 filter,一个是依据值过滤一个是依据范畴过滤。

父子文档

下面提到的嵌套文档解决的是一对多的场景,如果是多对多的情景则能够应用父子文档,这里不过多开展,具体可参考如何在 ES 中实现嵌套 json 对象查问,一次讲明确!其查问性能与嵌套对象、嵌套文档比拟是最差的,应用的时候须要留神。

小结

嵌套对象通过冗余数据来进步查问性能,实用于读多写少的场景,因为 ES 会对这种数据对象进行压平解决,这会导致搜寻不精准,如果搜寻精度要求不高,能够采纳这种形式。
如果搜寻须要精准,则能够采纳嵌套文档的形式,须要明确的是,文档每次更新的时候,文档数据会先被删除再插入,写入与查问性能比嵌套对象要低。
对于多对多的关联场景,能够采纳父子文档,每次更新只会更新单个文档,写入会比嵌套文档更快,毛病是查问速度会比嵌套文档慢。

文章大部分都参考了:如何在 ES 中实现嵌套 json 对象查问,一次讲明确!
嵌套文档那里联合了理论的工作场景作了相干形容。

退出移动版