假设我们有一份用户信息,用户信息有一个字段存储用户的社交网络帐号信息。我们知道现在每个人都会有很多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对象内部。