매핑은 아래와 같다
post의 자식으로 comment가있고, post에는 언급된 사람들이 배열로 있다.
(7버전 아래에서는 mappings 하위에 _doc으로 감싸줘야함.)
PUT blog
{
"mappings": {
"properties": {
"post": {
"properties": {
"postId": {
"type": "keyword"
},
"mentionedPeople": {
"type": "keyword"
}
}
},
"comment": {
"properties": {
"commentId": {
"type": "keyword"
},
"content": {
"type": "text"
}
}
},
"join": {
"type": "join",
"relations": {
"post": "comment"
}
}
}
}
}
데이터 구분을 위해 모든 댓글의 내용(comment.content)는 "hello"이고, 26번만 " goodbye"로 지정했다.
아래와같이 데이터를 넣었다.
### 부모 포스트 2개 넣음
PUT blog/_doc/1
{
"post":{
"postId" :1,
"mentionedPeople":["a","b","c"]
},
"join":"post"
}
PUT blog/_doc/2
{
"post":{
"postId" :2,
"mentionedPeople":["b","c","d","e"]
},
"join":"post"
}
### 아래와 같이 11개의 댓글을 넣었다.
PUT blog/_doc/11?routing=1
{
"comment": {
"commentId": 11,
"content": "hello"
},
"join": {
"name": "comment",
"parent": 1
}
}
## 2의 6번째 댓글만 content를 bye로 넣었다.
PUT blog/_doc/26?routing=2
{
"comment": {
"commentId": 26,
"content": "goodbye"
},
"join": {
"name": "comment",
"parent": 2
}
}
하고싶은것은, post에는 여러 언급된 사람들이있고, 또 post에는 여러 comment가 있는데,
언급된 사람 > post > 특정 조건의 comment를, 사람별로 집계하고 싶은것이다.
즉 b는 1번과 2번 포스트에 언급되어 11개의 모든 댓글을 가지지만, 그중 내용이 hello인 10개를 가진다는 것을 알고싶고, a는 1번 포스트에 언급되어 내용이 hello인 5개의 포스트를 가진다는 것을 집계하고 싶은것이다.
comment는 자식이기때문에 aggregation시 children aggr를 써줘야한다.
GET blog/_search
{
"query": {
"has_child": {
"type": "comment",
"inner_hits": {
"_source": false,
"size": 0
},
"query": {
"bool": {
"should": [
{
"match": {
"comment.content": "hello"
}
}
]
}
}
}
},
"_source": false,
"aggs": {
"byworkspaceId": {
"terms": {
"field": "post.mentionedPeople"
},
"aggs": {
"commentCount": {
"children": {
"type": "comment"
},
"aggs": {
"commentHowMany": {
"value_count": {
"field": "comment.commentId"
}
}
}
}
}
}
}
}
위 결과는 아래와 같다.
inner_hit에 보면, hello인 것의 개수가 a포스트 5개, b포스트 5개이지만
bucket에보면 a사람에 11개, b사람에 11개로 goodbye인 것이 제외되지 않고 나온다.
{
"took": 1,
"timed_out": false,
"_shards": {
"total": 4,
"successful": 4,
"skipped": 0,
"failed": 0
},
"hits": {
"total": 2,
"max_score": 1,
"hits": [
{
"_index": "blog",
"_type": "_doc",
"_id": "2",
"_score": 1,
"inner_hits": {
"comment": {
"hits": {
"total": 5,
"max_score": 0,
"hits": []
}
}
}
},
{
"_index": "blog",
"_type": "_doc",
"_id": "1",
"_score": 1,
"inner_hits": {
"comment": {
"hits": {
"total": 5,
"max_score": 0,
"hits": []
}
}
}
}
]
},
"aggregations": {
"byworkspaceId": {
"doc_count_error_upper_bound": 0,
"sum_other_doc_count": 0,
"buckets": [
{
"key": "b",
"doc_count": 2,
"commentCount": {
"doc_count": 11,
"commentHowMany": {
"value": 11
}
}
},
{
"key": "c",
"doc_count": 2,
"commentCount": {
"doc_count": 11,
"commentHowMany": {
"value": 11
}
}
},
{
"key": "a",
"doc_count": 1,
"commentCount": {
"doc_count": 5,
"commentHowMany": {
"value": 5
}
}
},
{
"key": "d",
"doc_count": 1,
"commentCount": {
"doc_count": 6,
"commentHowMany": {
"value": 6
}
}
},
{
"key": "e",
"doc_count": 1,
"commentCount": {
"doc_count": 6,
"commentHowMany": {
"value": 6
}
}
}
]
}
}
}
GET blog/_search
{
"query": {
"has_child": {
"type": "comment",
"inner_hits": {
"_source": false,
"size": 0
},
"query": {
"bool": {
"should": [
{
"match": {
"comment.content": "hello"
}
}
]
}
}
}
},
"_source": false,
"aggs": {
"byworkspaceId": {
"terms": {
"field": "post.mentionedPeople",
"size" : 5
},
"aggs": {
"childCount": {
"children": {
"type": "comment"
},
"aggs": {
"inner_filter": {
"filter": {
"bool": {
"should": [
{
"match": {
"comment.content": "hello"
}
}
]
}
},
"aggs": {
"commentHowMany": {
"value_count": {
"field": "comment.commentId"
}
}
}
}
}
}
}
}
}
}
내부 aggr에 filter aggr를 걸었더니, goodbye인것이 제외되고 잘 집계된다. 결과는 아래와같다.
{
"took": 1,
"timed_out": false,
"_shards": {
"total": 4,
"successful": 4,
"skipped": 0,
"failed": 0
},
"hits": {
"total": 2,
"max_score": 1,
"hits": [
{
"_index": "blog",
"_type": "_doc",
"_id": "2",
"_score": 1,
"inner_hits": {
"comment": {
"hits": {
"total": 5,
"max_score": 0,
"hits": []
}
}
}
},
{
"_index": "blog",
"_type": "_doc",
"_id": "1",
"_score": 1,
"inner_hits": {
"comment": {
"hits": {
"total": 5,
"max_score": 0,
"hits": []
}
}
}
}
]
},
"aggregations": {
"byworkspaceId": {
"doc_count_error_upper_bound": 0,
"sum_other_doc_count": 0,
"buckets": [
{
"key": "b",
"doc_count": 2,
"childCount": {
"doc_count": 11,
"inner_filter": {
"doc_count": 10,
"commentHowMany": {
"value": 10
}
}
}
},
{
"key": "c",
"doc_count": 2,
"childCount": {
"doc_count": 11,
"inner_filter": {
"doc_count": 10,
"commentHowMany": {
"value": 10
}
}
}
},
{
"key": "a",
"doc_count": 1,
"childCount": {
"doc_count": 5,
"inner_filter": {
"doc_count": 5,
"commentHowMany": {
"value": 5
}
}
}
},
{
"key": "d",
"doc_count": 1,
"childCount": {
"doc_count": 6,
"inner_filter": {
"doc_count": 5,
"commentHowMany": {
"value": 5
}
}
}
},
{
"key": "e",
"doc_count": 1,
"childCount": {
"doc_count": 6,
"inner_filter": {
"doc_count": 5,
"commentHowMany": {
"value": 5
}
}
}
}
]
}
}
}
문제는, 위 쿼리에 보면 아래와같이 size를 5개까지만 명시했는데, 명시하지 않으면 10개가 default이다.
"aggs": {
"byworkspaceId": {
"terms": {
"field": "post.mentionedPeople",
"size" : 5
},
문제는 mentionedPeople이 만명, 십만명 이라면?
bucket이 만개 십만개 생길것이고, 이를 한번의 response로 내리면 메모리 문제가 생길 수 있다.
composite aggr은 조합 집계를 할때쓰이지만, 페이징처리를 통해 모든 집계를 가져오기 위해서도 쓰인다.
아래와같이 composite 을 추가하면 paging처리되어 aggr를 가져오는 것이 가능하다.
GET blog/_search
{
"query": {
"has_child": {
"type": "comment",
"inner_hits": {
"_source": false,
"size": 0
},
"query": {
"bool": {
"should": [
{
"match": {
"comment.content": "hello"
}
}
]
}
}
}
},
"size": 0,
"aggs": {
"my_compostie": {
"composite": {
"sources": [
{
"byMentionedPeople": {
"terms": {
"field": "post.mentionedPeople"
}
}
}
],
"size": 2
},
"aggs": {
"childCount": {
"children": {
"type": "comment"
},
"aggs": {
"inner_filter": {
"filter": {
"bool": {
"should": [
{
"match": {
"comment.content": "hello"
}
}
]
}
},
"aggs": {
"commentHowMAny": {
"value_count": {
"field": "comment.commentId"
}
}
}
}
}
}
}
}
}
}
위 쿼리의 결과에서는 a,b가 나왔다.
결과에서 아래 "after_key"를 주목하자.
"aggregations": {
"my_compostie": {
"after_key": {
"byMentionedPeople": "b"
},
다음 쿼리시, composite의 after에 위 맵을 그대로 넘겨주면, 그 이후로 조회 가능하다.
"aggs": {
"my_compostie": {
"composite": {
"sources": [
{
"byMentionedPeople": {
"terms": {
"field": "post.mentionedPeople"
}
}
}
],
"size": 2,
"after": {
"byMentionedPeople": "b"
}
},
위와같이 b 이후 부터 조회하겠다라고 하면 c와 d를 보여준다.
RestHighLevelClient metaEs = ESClient.getClient("localhost", 9200);
BoolQueryBuilder shouldMatchHello = QueryBuilders.boolQuery().should(QueryBuilders.matchQuery("comment.content","hello"));
HasChildQueryBuilder hasChildQueryBuilder = JoinQueryBuilders.hasChildQuery("comment", shouldMatchHello, ScoreMode.None);
hasChildQueryBuilder.innerHit();
List<CompositeValuesSourceBuilder<?>> sources = new ArrayList<>();
sources.add(new TermsValuesSourceBuilder("byMentionedPoeple").field("post.mentionedPeople"));
CompositeAggregationBuilder compositeAggregation = new CompositeAggregationBuilder("my_composite", sources).size(2);
compositeAggregation.subAggregation(
new ChildrenAggregationBuilder("childCount", "comment")
.subAggregation(AggregationBuilders.filter("inner_filter", shouldMatchHello))
);
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
sourceBuilder.query(hasChildQueryBuilder);
sourceBuilder.aggregation(compositeAggregation);
sourceBuilder.size(0);
SearchRequest searchRequest = new SearchRequest();
searchRequest.indices("blog");
searchRequest.source(sourceBuilder);
try {
SearchResponse response = metaEs.search(searchRequest, RequestOptions.DEFAULT);
System.out.println(searchRequest);
System.out.println(response);
} catch (IOException e) {
System.out.println(e);
}
사실 inner_filter를 추가할때부터 그 내부의 value_count aggs는 의미가 없어졌다.
이미 filter의 결과에 doc_count가 있기 때문이다. 어쨋든.. 복잡한 요건이 해결되었다.