ElasticSearch 存储树形结构数据,统计树形数据。

一、背景

1、已知一个项目类型的树形结构,数据格式为:项目类型(理疗) + 属性(健康调理) + 属性值(推拿) 三个节点构成。如下图。

2、一颗项目类型树中,可以有多个项目类型,项目类型不会重复,不同的项目类型,属性和属性值 会重复。

3、ElasticSearch 索引商品库内容(100万数据)每个商品的文档中都维护一个项目类型的树形结构。

4、如下图的红色字体统计结果。

  • 理疗:一共100个商品

  • 理疗 + 健康调理:一共50个商品

  • 理疗 + 健康调理 + 推拿:一共10个商品

Snipaste_2020-08-07_11-43-02.jpg

二、问题

现使用 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"
              }
            }
          }
        }
      }
    }
  }
}

以上是统计了所有的数据,产生的数据结果如下图

Snipaste_2020-08-11_14-06-14.jpg

接下来是统计:分组后的分组数据

# 统计 各个项目类型下,各个属性的 分组数据
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"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
  }
}

执行结果如下图

Snipaste_2020-08-11_14-27-26.jpg

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 存储树形结构数据,统计树形数据。

点  赞 (5) 打  赏
分享到: