对于现实中一对多的场景比如一个商品除了标题、描述等基本信息外还会有尺寸颜色等多个属性,一篇博文可能会有多条评论信息,在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对象查询,一次讲明白!其查询性能与嵌套对象、嵌套文档比较是最差的,使用的时候需要注意。

小结

1,嵌套对象通过冗余数据来提高查询性能,适用于读多写少的场景,由于ES会对这种数据对象进行压平处理,这会导致搜索不精准,如果搜索精度要求不高,可以采用这种方式。
2,如果搜索需要精准,则可以采用嵌套文档的方式,需要明白的是,文档每次更新的时候,文档数据会先被删除再插入,写入与查询性能比嵌套对象要低。
3,对于多对多的关联场景,可以采用父子文档,每次更新只会更新单个文档,写入会比嵌套文档更快,缺点是查询速度会比嵌套文档慢。

文章大部分都参考了:如何在 ES 中实现嵌套json对象查询,一次讲明白!
嵌套文档那里结合了实际的工作场景作了相关描述。


步履不停
38 声望13 粉丝

好走的都是下坡路