假设我们有一份用户信息,用户信息有一个字段存储用户的社交网络帐号信息。我们知道现在每个人都会有很多SNS账户,例如:QQ,微信,微博,知乎之类。具体到每个SNS帐号,有可以包含很多信息,例如:
snsType int #标识sns类型
snsName string #该sns的名称,如QQ,微信
nickname string #用户昵称
fansCnt long #该用户在该平台上的粉丝/好友数
regDate date #帐号创建时间
所以每个帐号我们会有这样一个列表,加上用户的基本信息,每一个用户的信息格式可能形如:
{
"uid":"u0001",
"countryCode":"CN",
"province":"安徽",
"city":"合肥",
"snsInfoList":
[
{
"snsType" : 1,
"snsName" : "QQ",
"nickname" : "小人物",
"fansCnt" : "0",
"regDate" : "2009-01-02 12:21:00"
},
{
"snsType" : 3,
"snsName" : "微博",
"nickname" : "**工作室",
"fansCnt" : "1292",
"regDate" : "2004-11-02 00:21:00"
},
]
}
假如,还有另外一个用户
{
"uid":"u0002",
"countryCode":"CN",
"province":"浙江",
"city":"杭州",
"snsInfoList":
[
{
"snsType" : 1,
"snsName" : "QQ",
"nickname" : "风清扬",
"fansCnt" : "30",
"regDate" : "2001-01-02 11:00:00"
},
{
"snsType" : 3,
"snsName" : "微博",
"nickname" : "大自然守护者",
"fansCnt" : "129002",
"regDate" : "2005-11-02 11:21:00"
},
]
}
我们知道,这时候snsInfoList不是一个基本数据类型,为了方便起见,我们在声明mapping的时候,将其指定为Object类型,那么我们查询的时候就会遇到问题.如果我们想查找这样的用户:
有QQ账号 and ==QQ注册时间在2005年之后
这是一个简单的Bool查询,所以用Java Api的话,可以写作:
BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
boolQuery.must(QueryBuilders.termQuery("snsType", 1);
boolQuery.must(QueryBuilders.rangeQuery("regDate").gte("2015-01-01 00:00:00")
实际的查询结果会让你大吃一惊:两个用户都命中了!因为在这种mapping下,每个用户的所有资料都会在一篇doc的一级属性里。上面的布尔查询实际的匹配的文档是:
有QQ帐号 and 有任何sns帐号注册时间在2005年之后
造成这种问题的原因是因为ES没有把同一个用户的两个SNS帐号信息分成两个对象来保存,而是混在了一起,通一个属性下面两个sns的不同字段组成了一个array来存储。
为了解决这个问题,就需要引入 嵌套查询
首先在声明Mapping的时候,需要指定类型为nested,一个简单的例子如下:
private XContentBuilder getMapping(){
XContentBuilder mapping = null;
try{
mapping = XContentFactory.jsonBuilder()
.startObject()
.startObject(Expert.IndexType)
.startObject("properties")
.startObject(Field.AUIDDIGEST.getName()).field("type", "string").field("index", "not_analyzed").endObject()
.startObject(Field.SNSINFOLIST.getName()).field("type", "nested")
.startObject("properties")
.startObject("snsType").field("type", "long").endObject()
.startObject("snsName").field("type", "string").field("index", "not_analyzed").endObject()
.startObject("fansCnt").field("type", "long").field("index", "not_analyzed").endObject()
.startObject("nickname").field("type", "string").field("index", "analyzed").endObject()
.startObject("regDate").field("type", "date").field("format","yyyy-MM-dd HH:mm:ss").endObject()
.endObject()
.endObject()
.startObject(Field.COUNTRYCODE.getName()).field("type", "string").field("index", "not_analyzed").endObject()
.startObject(Field.PROVINCE.getName()).field("type", "string").field("index", "not_analyzed").endObject()
.startObject(Field.CITY.getName()).field("type", "string").field("index", "not_analyzed").endObject()
.endObject()
.endObject()
.endObject();
}catch(Exception e) {
}finally {
return mapping;
}
}
我们把SNSINFOLIST这个属性声明为嵌套类型,每个嵌套类型内部的类型声明跟正常的Mapping一样,同样,还可以循环嵌套。
在查询的时候,需要指定嵌套类型名,告诉es本次查询条件发生作用的范围,还是以上面的例子为例:
BoolQueryBuilder queryBuilder = QueryBuilders.boolQuery();
QueryBuilder nestedQuery = QueryBuilders.nestedQuery("snsInfoList", QueryBuilders.boolQuery()
.must(QueryBuilders.termQuery(Field.SNSINFOLIST_TYPE.getName(), snsType))
.must(QueryBuilders.rangeQuery(Field.SNSINFOLIST_REGDATE.getName()).gt(dateFloor).lt(dateCell)));
queryBuilder.must(nestedQuery);
这样就实现了,把条件限制在某一个snsinfo对象内部。