一、背景
1、已知一个项目类型的树形结构,数据格式为:项目类型(理疗) + 属性(健康调理) + 属性值(推拿) 三个节点构成。如下图。
2、一颗项目类型树中,可以有多个项目类型,项目类型不会重复,不同的项目类型,属性和属性值 会重复。
3、ElasticSearch 索引商品库内容(100万数据)每个商品的文档中都维护一个项目类型的树形结构。
4、如下图的红色字体统计结果。
- 理疗:一共100个商品 
- 理疗 + 健康调理:一共50个商品 
- 理疗 + 健康调理 + 推拿:一共10个商品 
二、问题
现使用 ElasticSearch 版本为 7.7.1
1、ES 商品索引中,如何存储这种数据结构?
- 使用 nested 嵌套类型? 
- 使用 join 特殊类型? 
2、ES 如何把所有的商品项目类型树结构整合为一个树?
实现功能有点类似于 solr 的 facet 分面查询。
- ES 使用分组 
如下测试代码中,插入的3条数据,聚合的树结构,应为
亮白美甲 - 款式 - 红色 - 白色 极光美甲 - 颜色 - 红色 - 兰色 - 蓝色 - 产地 - 北京 - 杭州 科技美甲 - 款式 - 红色 - 白色 - 黑色 - 产地 - 北京 - 南京
那么,该如何使用es查询呢?
3、ES 如何统计红色字体的商品数量? 
1、用一条查询实现出上图的红色字体统计结果。
- 答:如下测试代码中的统计 
2、如何只统计出项目类型树中,商品数量 >10 的显示?
- 答:使用 ES 的 min_doc_count 属性控制。 
三、测试
1、创建索引
# 创建索引
PUT /my_demo
{
    "mappings": {
        "properties": {
            "id": { "type": "integer" },
            "productName": { "type": "keyword" },
            "price": { "type": "double" },
            "categoryCode": { "type": "keyword" },
            "projectType": {    // 项目类型
                "type": "nested",
                "properties": {
                    "id": { "type": "integer" },
                    "name": { "type": "keyword" },
                    "attr": {    // 属性
                        "type": "nested",
                        "properties": {
                          "name":{ "type": "keyword" },
                          "value": { "type": "keyword" }    // 属性值
                        }
                    }
                }
            }
        }
    }
}2、禁用动态字段
# 禁用动态字段
PUT /my_demo/_mapping
{
    "dynamic":"strict"
}3、插入数据
# 一个项目,一个属性,多个属性值
PUT /my_demo/_doc/1
{
  "id": 1,
  "productName": "美甲商品1",
  "categoryCode": "tag_mei_jia",
  "projectType":[{
    "id": 101,
    "name": "极光美甲",
    "attr":[{
      "name": "颜色",
      "value":["红色", "兰色"]
    }]
  }]
}
# 一个项目,一个属性,多个属性值
PUT /my_demo/_doc/2
{
  "id": 2,
  "productName": "美甲商品2",
  "categoryCode": "tag_mei_jia",
  "projectType":[{
    "id": 102,
    "name": "亮白美甲",
    "attr":[{
      "name": "款式",
      "value":["红色", "白色"]
    }]
  }]
}
# 多个项目,多个属性,多个属性值
PUT /my_demo/_doc/3
{
  "id": 3,
  "productName": "美甲商品3",
  "categoryCode": "tag_mei_jia",
  "projectType":[{
      "id": 101,
      "name": "极光美甲",
      "attr": [{
          "name": "颜色",
          "value": ["红色", "蓝色"]
      },{
          "name": "产地",
          "value": ["北京", "杭州"]
      }]
    },{
      "id": 103,
      "name": "科技美甲",
      "attr": [{
          "name": "款式",
          "value": ["红色", "白色", "黑色"]
      },{
          "name": "产地",
          "value": ["北京", "南京"]
      }]
  }]
}4、查询测试
查询字段数据
# 存在 项目类型 的数据
GET stg_item/_search
{
  "query": {
    "nested": {
      "path": "projectType",
      "query": {
        "bool": {
          "must": {
            "exists": {
              "field": "projectType"
            }
          }
        }   
      }
    }
  }
}查询条件数据
# 项目类型测试(2条)
GET /my_demo/_search
{
  "query": {
    "nested": {
      "path": "projectType",
      "query": {
        "bool": {
          "must": [
            { "match": { "projectType.name": "极光美甲" }}
          ]
        }
      }
    }
  }
}
# 项目类型 + 属性 测试(2条)
GET /my_demo/_search
{
  "query": {
    "nested": {
      "path": "projectType",
      "query": {
        "bool": {
          "must": [
            { "match": { "projectType.name": "极光美甲" }},
            {
              "nested": {
                "path": "projectType.attr",
                "query": {
                  "bool": {
                    "must": [
                      { "match": {"projectType.attr.name": "颜色"}}
                    ]
                  }
                }
              }
            }
          ]
        }
      }
    }
  }
}
# 项目类型 + 属性 + 属性值测试(1条)
GET /my_demo/_search
{
  "query": {
    "nested": {
      "path": "projectType",
      "query": {
        "bool": {
          "must": [
            { "match": { "projectType.name": "极光美甲" }},
            {
              "nested": {
                "path": "projectType.attr",
                "query": {
                  "bool": {
                    "must": [
                      { "match": {"projectType.attr.name": "颜色"}},
                      { "match": {"projectType.attr.value": "兰色"}}
                    ]
                  }
                }
              }
            }
          ]
        }
      }
    }
  }
}
# 项目类型 + 属性值 测试(1条)
GET /my_demo/_search
{
  "query": {
    "nested": {
      "path": "projectType",
      "query": {
        "bool": {
          "must": [
            { "match": { "projectType.name": "极光美甲" }},
            {
              "nested": {
                "path": "projectType.attr",
                "query": {
                  "bool": {
                    "must": [
                      { "match": {"projectType.attr.value": "蓝色"}}
                    ]
                  }
                }
              }
            }
          ]
        }
      }
    }
  }
}
# 属性 测试(2条)
GET /my_demo/_search
{
  "query": {
    "nested": {
      "path": "projectType",
      "query": {
        "bool": {
          "must": [
            {
              "nested": {
                "path": "projectType.attr",
                "query": {
                  "bool": {
                    "must": [
                      { "match": {"projectType.attr.name": "款式"}}
                    ]
                  }
                }
              }
            }
          ]
        }
      }
    }
  }
}
// 等同于
GET /my_demo/_search
{
    "query": {
        "nested": {
            "path": "projectType.attr",
            "query": {
                "bool": {
                    "must": [{
                        "match": {
                            "projectType.attr.name": "款式"
                        }
                    }]
                }
            }
        }
    }
}
# 属性 + 属性值 测试(1条)
GET /my_demo/_search
{
    "query": {
        "nested": {
            "path": "projectType.attr",
            "query": {
                "bool": {
                    "must": [
                      { "match": { "projectType.attr.name": "款式" }},
                      { "match": { "projectType.attr.value": "黑色" }}
                    ]
                }
            }
        }
    }
}
# 查询属性含有(款式 或 产地)的数据(2条)
GET /my_demo/_search
{
    "query": {
        "nested": {
            "path": "projectType.attr",
            "query": {
                "bool": {
                    "must": [
                      { "terms": { "projectType.attr.name": ["款式", "产地"] }}
                    ]
                }
            }
        }
    }
}
# 遗留问题:查询属性必须含有(款式 和 产地)的数据5、聚合查询测试
# 按照 类目 分组
GET /my_demo/_search
{
  "size": 0,
  "aggs": {
    "group_by_projectType": {
      "terms": {
        "field": "categoryCode"
      }
    }
  }
}
# 按照 类目 分组(文档数量不少于10条)
GET /my_demo/_search
{
  "size": 0,
  "aggs": {
    "group_by_projectType": {
      "terms": {
        "field": "categoryCode",
        "min_doc_count": 10
      }
    }
  }
}
# 按照 项目类型名称 分组
GET /my_demo/_search
{
  "size": 0,
  "aggs": {
    "group_by_projectType": {
      "nested": {
        "path": "projectType"
      }, 
      "aggs": {
        "group_by_projectType_name": {
          "terms": {
            "field": "projectType.name"
          }
        }
      }
    }
  }
}
# 按照 项目类型下的属性名称 分组
GET /my_demo/_search
{
  "size": 0,
  "aggs": {
    "group_by_projectType": {
      "nested": {
        "path": "projectType"
      },
      "aggs": {
        "group_by_projectType_name": {
          "nested": {
            "path": "projectType.attr"
          },
          "aggs": {
            "group_by_projectType_attr_name": {
              "terms": {
                "field": "projectType.attr.name"
              }
            }
          }
        }
      }
    }
  }
}以上是统计了所有的数据,产生的数据结果如下图
接下来是统计:分组后的分组数据
# 统计 各个项目类型下,各个属性的 分组数据
GET /my_demo/_search
{
  "size": 0,
  "aggs": {
    "group_by_projectType": {
      "nested": {
        "path": "projectType"
      },
      "aggs": {
        "group_by_projectType_name": {
          "terms": {
            "field": "projectType.name"
          },
          "aggs": {
            "group_by_projectType_attr": {
              "nested": {
                "path": "projectType.attr"
              }, 
              "aggs": {
                "group_by_projectType_attr_name": {
                  "terms": {
                    "field": "projectType.attr.name"
                  }   
                }
              }
            }
          }
        }
      }
    }
  }
}
# 统计 各个项目类型下,各个属性,各个属性值的 分组数据
GET /my_demo/_search
{
  "size": 0,
  "aggs": {
    "group_by_projectType": {
      "nested": {
        "path": "projectType"
      },
      "aggs": {
        "group_by_projectType_name": {
          "terms": {
            "field": "projectType.name"
          },
          "aggs": {
            "group_by_projectType_attr": {
              "nested": {
                "path": "projectType.attr"
              }, 
              "aggs": {
                "group_by_projectType_attr_name": {
                  "terms": {
                    "field": "projectType.attr.name"
                  },
                  "aggs": {
                    "group_by_projectType_attr_value": {
                      "terms": {
                        "field": "projectType.attr.value"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
  }
}执行结果如下图
Java API 代码实现
@Test
public void nestedAggDemo2() throws Exception {
    int minDocCount = 10;
    // 1、聚合构建
    SearchSourceBuilder builder = new SearchSourceBuilder();
    builder.size(0);
    NestedAggregationBuilder aggregationBuilder = AggregationBuilders.nested("group_project_type", "projects")
            .subAggregation(AggregationBuilders.terms("group_project_type_name").minDocCount(minDocCount).field("projects.typeName")
                    .subAggregation(AggregationBuilders.nested("group_attr", "projects.attrs")
                            .subAggregation(AggregationBuilders.terms("group_attr_name").minDocCount(minDocCount).field("projects.attrs.name")
                                    .subAggregation(AggregationBuilders.terms("group_attr_value").minDocCount(minDocCount).field("projects.attrs.values"))))
            );
    builder.aggregation(aggregationBuilder);
    // 2、查询对象
    SearchRequest request = new SearchRequest(index_name);
    request.source(builder);
    // 3、查询
    SearchResponse response = client.search(request, RequestOptions.DEFAULT);
    ParsedNested projectTypeNested = response.getAggregations().get("group_project_type");
    System.out.println("项目类型:" + projectTypeNested.getDocCount());
    ParsedStringTerms projectTypeTerms = projectTypeNested.getAggregations().get("group_project_type_name");
    if (CollectionUtils.isNotEmpty(projectTypeTerms.getBuckets())) {
        for (Terms.Bucket projectTypeBucket : projectTypeTerms.getBuckets()) {
            System.out.println("- " + projectTypeBucket.getKeyAsString() + ":" + projectTypeBucket.getDocCount());
            ParsedNested attrNested = projectTypeBucket.getAggregations().get("group_attr");
            System.out.println("\t 属性:" + attrNested.getDocCount());
            ParsedStringTerms attrTerms = attrNested.getAggregations().get("group_attr_name");
            if (CollectionUtils.isNotEmpty(attrTerms.getBuckets())) {
                for (Terms.Bucket attrBucket : attrTerms.getBuckets()) {
                    System.out.println("\t -- " + attrBucket.getKeyAsString() + ":" + attrBucket.getDocCount());
                    ParsedStringTerms attrValueTerms = attrBucket.getAggregations().get("group_attr_value");
                    if (CollectionUtils.isNotEmpty(attrValueTerms.getBuckets())) {
                        for (Terms.Bucket valueBucket : attrValueTerms.getBuckets()) {
                            System.out.println("\t\t --- " + valueBucket.getKeyAsString() + ":" + valueBucket.getDocCount());
                        }
                    }
                }
            }
        }
    }
}执行结果
项目类型:969 - 白莲新增项目类型测试:467 属性:467 -- 样式:467 --- 镶钻:467 - HA4D面部:87 属性:261 -- 作用部位:87 --- 面部:87 -- 品牌:87 --- HA4D:87 -- 服务时长:87 --- 50:87 - 白莲看图选购项目类型04:70 属性:70 -- 操作时长:70 --- 60:70 - 明镜描述测试:54 属性:54 -- 服务时长-白:54 --- 60:54 - 馨迪蕊拉局部按摩:26 属性:78 -- 作用部位:26 --- 背部:16 -- 品牌:26 --- 馨迪蕊拉:26 -- 服务时长:26 --- 45:25 - 淡然测试项目类型0511_02:22 属性:22 -- 治疗周期:22 --- 一天:15 - 白莲看图选购项目类型01:22 属性:22 -- 服务时长(分钟):22 --- 70:21 - 天芮面部:21 属性:63 -- 作用部位:21 --- 面部:21 -- 品牌:21 --- 天芮:21 -- 服务时长:21 - 爱仕兰面部:20 属性:60 -- 作用部位:20 --- 面部:20 -- 品牌:20 --- 爱仕兰:20 -- 服务时长:20 --- 20:20 - Refa面部:18 属性:54 -- 作用部位:18 --- 手部:18 --- 脊柱:18 --- 腿部:18 -- 品牌:18 --- 馨迪蕊拉:18 -- 服务时长:18 --- 130:18
未经允许请勿转载:程序喵 » ElasticSearch 存储树形结构数据,统计树形数据。
 程序喵
程序喵