Add parent-aggregation to parent-join module (#34210)

Add `parent` aggregation, a special single bucket aggregation that joins children documents to their parent.
This commit is contained in:
Dominik Stadler 2018-11-08 14:13:00 +01:00 committed by Jim Ferenczi
parent 025a0c82e5
commit d351422215
20 changed files with 1488 additions and 98 deletions

View File

@ -628,7 +628,7 @@ public class RestHighLevelClientTests extends ESTestCase {
public void testProvidedNamedXContents() {
List<NamedXContentRegistry.Entry> namedXContents = RestHighLevelClient.getProvidedNamedXContents();
assertEquals(16, namedXContents.size());
assertEquals(17, namedXContents.size());
Map<Class<?>, Integer> categories = new HashMap<>();
List<String> names = new ArrayList<>();
for (NamedXContentRegistry.Entry namedXContent : namedXContents) {
@ -638,8 +638,8 @@ public class RestHighLevelClientTests extends ESTestCase {
categories.put(namedXContent.categoryClass, counter + 1);
}
}
assertEquals(4, categories.size());
assertEquals(Integer.valueOf(2), categories.get(Aggregation.class));
assertEquals("Had: " + categories, 4, categories.size());
assertEquals(Integer.valueOf(3), categories.get(Aggregation.class));
assertTrue(names.contains(ChildrenAggregationBuilder.NAME));
assertTrue(names.contains(MatrixStatsAggregationBuilder.NAME));
assertEquals(Integer.valueOf(4), categories.get(EvaluationMetric.class));

View File

@ -49,6 +49,8 @@ include::bucket/missing-aggregation.asciidoc[]
include::bucket/nested-aggregation.asciidoc[]
include::bucket/parent-aggregation.asciidoc[]
include::bucket/range-aggregation.asciidoc[]
include::bucket/reverse-nested-aggregation.asciidoc[]

View File

@ -0,0 +1,213 @@
[[search-aggregations-bucket-parent-aggregation]]
=== Parent Aggregation
A special single bucket aggregation that selects parent documents that have the specified type, as defined in a <<parent-join,`join` field>>.
This aggregation has a single option:
* `type` - The child type that should be selected.
For example, let's say we have an index of questions and answers. The answer type has the following `join` field in the mapping:
[source,js]
--------------------------------------------------
PUT parent_example
{
"mappings": {
"_doc": {
"properties": {
"join": {
"type": "join",
"relations": {
"question": "answer"
}
}
}
}
}
}
--------------------------------------------------
// CONSOLE
The `question` document contain a tag field and the `answer` documents contain an owner field. With the `parent`
aggregation the owner buckets can be mapped to the tag buckets in a single request even though the two fields exist in
two different kinds of documents.
An example of a question document:
[source,js]
--------------------------------------------------
PUT parent_example/_doc/1
{
"join": {
"name": "question"
},
"body": "<p>I have Windows 2003 server and i bought a new Windows 2008 server...",
"title": "Whats the best way to file transfer my site from server to a newer one?",
"tags": [
"windows-server-2003",
"windows-server-2008",
"file-transfer"
]
}
--------------------------------------------------
// CONSOLE
// TEST[continued]
Examples of `answer` documents:
[source,js]
--------------------------------------------------
PUT parent_example/_doc/2?routing=1
{
"join": {
"name": "answer",
"parent": "1"
},
"owner": {
"location": "Norfolk, United Kingdom",
"display_name": "Sam",
"id": 48
},
"body": "<p>Unfortunately you're pretty much limited to FTP...",
"creation_date": "2009-05-04T13:45:37.030"
}
PUT parent_example/_doc/3?routing=1&refresh
{
"join": {
"name": "answer",
"parent": "1"
},
"owner": {
"location": "Norfolk, United Kingdom",
"display_name": "Troll",
"id": 49
},
"body": "<p>Use Linux...",
"creation_date": "2009-05-05T13:45:37.030"
}
--------------------------------------------------
// CONSOLE
// TEST[continued]
The following request can be built that connects the two together:
[source,js]
--------------------------------------------------
POST parent_example/_search?size=0
{
"aggs": {
"top-names": {
"terms": {
"field": "owner.display_name.keyword",
"size": 10
},
"aggs": {
"to-questions": {
"parent": {
"type" : "answer"
},
"aggs": {
"top-tags": {
"terms": {
"field": "tags.keyword",
"size": 10
}
}
}
}
}
}
}
}
--------------------------------------------------
// CONSOLE
// TEST[continued]
<1> The `type` points to type / mapping with the name `answer`.
The above example returns the top answer owners and per owner the top question tags.
Possible response:
[source,js]
--------------------------------------------------
{
"took": 9,
"timed_out": false,
"_shards": {
"total": 1,
"successful": 1,
"skipped": 0,
"failed": 0
},
"hits": {
"total": 3,
"max_score": null,
"hits": []
},
"aggregations": {
"top-names": {
"doc_count_error_upper_bound": 0,
"sum_other_doc_count": 0,
"buckets": [
{
"key": "Sam",
"doc_count": 1, <1>
"to-questions": {
"doc_count": 1, <2>
"top-tags": {
"doc_count_error_upper_bound": 0,
"sum_other_doc_count": 0,
"buckets": [
{
"key": "file-transfer",
"doc_count": 1
},
{
"key": "windows-server-2003",
"doc_count": 1
},
{
"key": "windows-server-2008",
"doc_count": 1
}
]
}
}
},
{
"key": "Troll",
"doc_count": 1, <1>
"to-questions": {
"doc_count": 1, <2>
"top-tags": {
"doc_count_error_upper_bound": 0,
"sum_other_doc_count": 0,
"buckets": [
{
"key": "file-transfer",
"doc_count": 1
},
{
"key": "windows-server-2003",
"doc_count": 1
},
{
"key": "windows-server-2008",
"doc_count": 1
}
]
}
}
}
]
}
}
}
--------------------------------------------------
// TESTRESPONSE[s/"took": 9/"took": $body.took/]
<1> The number of answer documents with the tag `Sam`, `Troll`, etc.
<2> The number of question documents that are related to answer documents with the tag `Sam`, `Troll`, etc.

View File

@ -22,6 +22,8 @@ package org.elasticsearch.join;
import org.elasticsearch.index.mapper.Mapper;
import org.elasticsearch.join.aggregations.ChildrenAggregationBuilder;
import org.elasticsearch.join.aggregations.InternalChildren;
import org.elasticsearch.join.aggregations.InternalParent;
import org.elasticsearch.join.aggregations.ParentAggregationBuilder;
import org.elasticsearch.join.mapper.ParentJoinFieldMapper;
import org.elasticsearch.join.query.HasChildQueryBuilder;
import org.elasticsearch.join.query.HasParentQueryBuilder;
@ -51,9 +53,11 @@ public class ParentJoinPlugin extends Plugin implements SearchPlugin, MapperPlug
@Override
public List<AggregationSpec> getAggregations() {
return Collections.singletonList(
new AggregationSpec(ChildrenAggregationBuilder.NAME, ChildrenAggregationBuilder::new, ChildrenAggregationBuilder::parse)
.addResultReader(InternalChildren::new)
return Arrays.asList(
new AggregationSpec(ChildrenAggregationBuilder.NAME, ChildrenAggregationBuilder::new, ChildrenAggregationBuilder::parse)
.addResultReader(InternalChildren::new),
new AggregationSpec(ParentAggregationBuilder.NAME, ParentAggregationBuilder::new, ParentAggregationBuilder::parse)
.addResultReader(InternalParent::new)
);
}

View File

@ -0,0 +1,60 @@
/*
* Licensed to Elasticsearch under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.elasticsearch.join.aggregations;
import org.apache.lucene.search.Query;
import org.elasticsearch.common.ParseField;
import org.elasticsearch.search.aggregations.Aggregator;
import org.elasticsearch.search.aggregations.AggregatorFactories;
import org.elasticsearch.search.aggregations.InternalAggregation;
import org.elasticsearch.search.aggregations.bucket.BucketsAggregator;
import org.elasticsearch.search.aggregations.pipeline.PipelineAggregator;
import org.elasticsearch.search.aggregations.support.ValuesSource;
import org.elasticsearch.search.internal.SearchContext;
import java.io.IOException;
import java.util.List;
import java.util.Map;
/**
* A {@link BucketsAggregator} which resolves to the matching parent documents.
*/
public class ChildrenToParentAggregator extends ParentJoinAggregator {
static final ParseField TYPE_FIELD = new ParseField("type");
public ChildrenToParentAggregator(String name, AggregatorFactories factories,
SearchContext context, Aggregator parent, Query childFilter,
Query parentFilter, ValuesSource.Bytes.WithOrdinals valuesSource,
long maxOrd, List<PipelineAggregator> pipelineAggregators, Map<String, Object> metaData) throws IOException {
super(name, factories, context, parent, childFilter, parentFilter, valuesSource, maxOrd, pipelineAggregators, metaData);
}
@Override
public InternalAggregation buildAggregation(long owningBucketOrdinal) throws IOException {
return new InternalParent(name, bucketDocCount(owningBucketOrdinal),
bucketAggregations(owningBucketOrdinal), pipelineAggregators(), metaData());
}
@Override
public InternalAggregation buildEmptyAggregation() {
return new InternalParent(name, 0, buildEmptySubAggregations(), pipelineAggregators(),
metaData());
}
}

View File

@ -0,0 +1,56 @@
/*
* Licensed to Elasticsearch under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.elasticsearch.join.aggregations;
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.search.aggregations.InternalAggregations;
import org.elasticsearch.search.aggregations.bucket.InternalSingleBucketAggregation;
import org.elasticsearch.search.aggregations.pipeline.PipelineAggregator;
import java.io.IOException;
import java.util.List;
import java.util.Map;
/**
* Results of the {@link ChildrenToParentAggregator}.
*/
public class InternalParent extends InternalSingleBucketAggregation implements Parent {
public InternalParent(String name, long docCount, InternalAggregations aggregations, List<PipelineAggregator> pipelineAggregators,
Map<String, Object> metaData) {
super(name, docCount, aggregations, pipelineAggregators, metaData);
}
/**
* Read from a stream.
*/
public InternalParent(StreamInput in) throws IOException {
super(in);
}
@Override
public String getWriteableName() {
return ParentAggregationBuilder.NAME;
}
@Override
protected InternalSingleBucketAggregation newAggregation(String name, long docCount, InternalAggregations subAggregations) {
return new InternalParent(name, docCount, subAggregations, pipelineAggregators(), getMetaData());
}
}

View File

@ -26,4 +26,11 @@ public abstract class JoinAggregationBuilders {
public static ChildrenAggregationBuilder children(String name, String childType) {
return new ChildrenAggregationBuilder(name, childType);
}
/**
* Create a new {@link Parent} aggregation with the given name.
*/
public static ParentAggregationBuilder parent(String name, String childType) {
return new ParentAggregationBuilder(name, childType);
}
}

View File

@ -0,0 +1,28 @@
/*
* Licensed to Elasticsearch under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.elasticsearch.join.aggregations;
import org.elasticsearch.search.aggregations.bucket.SingleBucketAggregation;
/**
* An single bucket aggregation that translates child documents to their parent documents.
*/
public interface Parent extends SingleBucketAggregation {
}

View File

@ -0,0 +1,176 @@
/*
* Licensed to Elasticsearch under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.elasticsearch.join.aggregations;
import org.apache.lucene.search.Query;
import org.elasticsearch.common.ParsingException;
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.StreamOutput;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.index.fielddata.plain.SortedSetDVOrdinalsIndexFieldData;
import org.elasticsearch.index.mapper.MappedFieldType;
import org.elasticsearch.join.mapper.ParentIdFieldMapper;
import org.elasticsearch.join.mapper.ParentJoinFieldMapper;
import org.elasticsearch.search.aggregations.AggregationBuilder;
import org.elasticsearch.search.aggregations.AggregatorFactories.Builder;
import org.elasticsearch.search.aggregations.AggregatorFactory;
import org.elasticsearch.search.aggregations.support.FieldContext;
import org.elasticsearch.search.aggregations.support.ValueType;
import org.elasticsearch.search.aggregations.support.ValuesSource.Bytes.WithOrdinals;
import org.elasticsearch.search.aggregations.support.ValuesSourceAggregationBuilder;
import org.elasticsearch.search.aggregations.support.ValuesSourceAggregatorFactory;
import org.elasticsearch.search.aggregations.support.ValuesSourceConfig;
import org.elasticsearch.search.aggregations.support.ValuesSourceType;
import org.elasticsearch.search.internal.SearchContext;
import java.io.IOException;
import java.util.Map;
import java.util.Objects;
public class ParentAggregationBuilder
extends ValuesSourceAggregationBuilder<WithOrdinals, ParentAggregationBuilder> {
public static final String NAME = "parent";
private final String childType;
private Query parentFilter;
private Query childFilter;
/**
* @param name
* the name of this aggregation
* @param childType
* the type of children documents
*/
public ParentAggregationBuilder(String name, String childType) {
super(name, ValuesSourceType.BYTES, ValueType.STRING);
if (childType == null) {
throw new IllegalArgumentException("[childType] must not be null: [" + name + "]");
}
this.childType = childType;
}
protected ParentAggregationBuilder(ParentAggregationBuilder clone,
Builder factoriesBuilder, Map<String, Object> metaData) {
super(clone, factoriesBuilder, metaData);
this.childType = clone.childType;
this.childFilter = clone.childFilter;
this.parentFilter = clone.parentFilter;
}
@Override
protected AggregationBuilder shallowCopy(Builder factoriesBuilder, Map<String, Object> metaData) {
return new ParentAggregationBuilder(this, factoriesBuilder, metaData);
}
/**
* Read from a stream.
*/
public ParentAggregationBuilder(StreamInput in) throws IOException {
super(in, ValuesSourceType.BYTES, ValueType.STRING);
childType = in.readString();
}
@Override
protected void innerWriteTo(StreamOutput out) throws IOException {
out.writeString(childType);
}
@Override
protected ValuesSourceAggregatorFactory<WithOrdinals, ?> innerBuild(SearchContext context,
ValuesSourceConfig<WithOrdinals> config,
AggregatorFactory<?> parent,
Builder subFactoriesBuilder) throws IOException {
return new ParentAggregatorFactory(name, config, childFilter, parentFilter, context, parent,
subFactoriesBuilder, metaData);
}
@Override
protected ValuesSourceConfig<WithOrdinals> resolveConfig(SearchContext context) {
ValuesSourceConfig<WithOrdinals> config = new ValuesSourceConfig<>(ValuesSourceType.BYTES);
joinFieldResolveConfig(context, config);
return config;
}
private void joinFieldResolveConfig(SearchContext context, ValuesSourceConfig<WithOrdinals> config) {
ParentJoinFieldMapper parentJoinFieldMapper = ParentJoinFieldMapper.getMapper(context.mapperService());
ParentIdFieldMapper parentIdFieldMapper = parentJoinFieldMapper.getParentIdFieldMapper(childType, false);
if (parentIdFieldMapper != null) {
parentFilter = parentIdFieldMapper.getParentFilter();
childFilter = parentIdFieldMapper.getChildFilter(childType);
MappedFieldType fieldType = parentIdFieldMapper.fieldType();
final SortedSetDVOrdinalsIndexFieldData fieldData = context.getForField(fieldType);
config.fieldContext(new FieldContext(fieldType.name(), fieldData, fieldType));
} else {
config.unmapped(true);
}
}
@Override
protected XContentBuilder doXContentBody(XContentBuilder builder, Params params) throws IOException {
builder.field(ChildrenToParentAggregator.TYPE_FIELD.getPreferredName(), childType);
return builder;
}
public static ParentAggregationBuilder parse(String aggregationName, XContentParser parser) throws IOException {
String childType = null;
XContentParser.Token token;
String currentFieldName = null;
while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) {
if (token == XContentParser.Token.FIELD_NAME) {
currentFieldName = parser.currentName();
} else if (token == XContentParser.Token.VALUE_STRING) {
if ("type".equals(currentFieldName)) {
childType = parser.text();
} else {
throw new ParsingException(parser.getTokenLocation(),
"Unknown key for a " + token + " in [" + aggregationName + "]: [" + currentFieldName + "].");
}
} else {
throw new ParsingException(parser.getTokenLocation(), "Unexpected token " + token + " in [" + aggregationName + "].");
}
}
if (childType == null) {
throw new ParsingException(parser.getTokenLocation(),
"Missing [child_type] field for parent aggregation [" + aggregationName + "]");
}
return new ParentAggregationBuilder(aggregationName, childType);
}
@Override
protected int innerHashCode() {
return Objects.hash(childType);
}
@Override
protected boolean innerEquals(Object obj) {
ParentAggregationBuilder other = (ParentAggregationBuilder) obj;
return Objects.equals(childType, other.childType);
}
@Override
public String getType() {
return NAME;
}
}

View File

@ -0,0 +1,83 @@
/*
* Licensed to Elasticsearch under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.elasticsearch.join.aggregations;
import org.apache.lucene.search.Query;
import org.elasticsearch.search.aggregations.Aggregator;
import org.elasticsearch.search.aggregations.AggregatorFactories;
import org.elasticsearch.search.aggregations.AggregatorFactory;
import org.elasticsearch.search.aggregations.InternalAggregation;
import org.elasticsearch.search.aggregations.NonCollectingAggregator;
import org.elasticsearch.search.aggregations.pipeline.PipelineAggregator;
import org.elasticsearch.search.aggregations.support.ValuesSource.Bytes.WithOrdinals;
import org.elasticsearch.search.aggregations.support.ValuesSourceAggregatorFactory;
import org.elasticsearch.search.aggregations.support.ValuesSourceConfig;
import org.elasticsearch.search.internal.SearchContext;
import java.io.IOException;
import java.util.List;
import java.util.Map;
public class ParentAggregatorFactory extends ValuesSourceAggregatorFactory<WithOrdinals, ParentAggregatorFactory> {
private final Query parentFilter;
private final Query childFilter;
public ParentAggregatorFactory(String name,
ValuesSourceConfig<WithOrdinals> config,
Query childFilter,
Query parentFilter,
SearchContext context,
AggregatorFactory<?> parent,
AggregatorFactories.Builder subFactoriesBuilder,
Map<String, Object> metaData) throws IOException {
super(name, config, context, parent, subFactoriesBuilder, metaData);
this.childFilter = childFilter;
this.parentFilter = parentFilter;
}
@Override
protected Aggregator createUnmapped(Aggregator parent,
List<PipelineAggregator> pipelineAggregators, Map<String, Object> metaData) throws IOException {
return new NonCollectingAggregator(name, context, parent, pipelineAggregators, metaData) {
@Override
public InternalAggregation buildEmptyAggregation() {
return new InternalParent(name, 0, buildEmptySubAggregations(), pipelineAggregators(), metaData());
}
};
}
@Override
protected Aggregator doCreateInternal(WithOrdinals valuesSource,
Aggregator children,
boolean collectsFromSingleBucket,
List<PipelineAggregator> pipelineAggregators,
Map<String, Object> metaData) throws IOException {
long maxOrd = valuesSource.globalMaxOrd(context.searcher());
if (collectsFromSingleBucket) {
return new ChildrenToParentAggregator(name, factories, context, children, childFilter,
parentFilter, valuesSource, maxOrd, pipelineAggregators, metaData);
} else {
return asMultiBucketAggregator(this, context, children);
}
}
}

View File

@ -53,5 +53,4 @@ public class ParentToChildrenAggregator extends ParentJoinAggregator {
return new InternalChildren(name, 0, buildEmptySubAggregations(), pipelineAggregators(),
metaData());
}
}

View File

@ -0,0 +1,36 @@
/*
* Licensed to Elasticsearch under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.elasticsearch.join.aggregations;
import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.search.aggregations.bucket.ParsedSingleBucketAggregation;
import java.io.IOException;
public class ParsedParent extends ParsedSingleBucketAggregation implements Parent {
@Override
public String getType() {
return ParentAggregationBuilder.NAME;
}
public static ParsedParent fromXContent(XContentParser parser, final String name) throws IOException {
return parseXContent(parser, new ParsedParent(), name);
}
}

View File

@ -23,20 +23,26 @@ import org.elasticsearch.common.ParseField;
import org.elasticsearch.common.xcontent.ContextParser;
import org.elasticsearch.common.xcontent.NamedXContentRegistry;
import org.elasticsearch.join.aggregations.ChildrenAggregationBuilder;
import org.elasticsearch.join.aggregations.ParentAggregationBuilder;
import org.elasticsearch.join.aggregations.ParsedChildren;
import org.elasticsearch.join.aggregations.ParsedParent;
import org.elasticsearch.plugins.spi.NamedXContentProvider;
import org.elasticsearch.search.aggregations.Aggregation;
import java.util.Arrays;
import java.util.List;
import static java.util.Collections.singletonList;
public class ParentJoinNamedXContentProvider implements NamedXContentProvider {
@Override
public List<NamedXContentRegistry.Entry> getNamedXContentParsers() {
ParseField parseField = new ParseField(ChildrenAggregationBuilder.NAME);
ContextParser<Object, Aggregation> contextParser = (p, name) -> ParsedChildren.fromXContent(p, (String) name);
return singletonList(new NamedXContentRegistry.Entry(Aggregation.class, parseField, contextParser));
ParseField parseFieldChildren = new ParseField(ChildrenAggregationBuilder.NAME);
ParseField parseFieldParent = new ParseField(ParentAggregationBuilder.NAME);
ContextParser<Object, Aggregation> contextParserChildren = (p, name) -> ParsedChildren.fromXContent(p, (String) name);
ContextParser<Object, Aggregation> contextParserParent = (p, name) -> ParsedParent.fromXContent(p, (String) name);
return Arrays.asList(
new NamedXContentRegistry.Entry(Aggregation.class, parseFieldChildren, contextParserChildren),
new NamedXContentRegistry.Entry(Aggregation.class, parseFieldParent, contextParserParent)
);
}
}

View File

@ -0,0 +1,132 @@
/*
* Licensed to Elasticsearch under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.elasticsearch.join.aggregations;
import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertAcked;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.elasticsearch.action.index.IndexRequestBuilder;
import org.elasticsearch.join.query.ParentChildTestCase;
import org.junit.Before;
/**
* Small base test-class which combines stuff used for Children and Parent aggregation tests
*/
public abstract class AbstractParentChildTestCase extends ParentChildTestCase {
protected final Map<String, Control> categoryToControl = new HashMap<>();
protected final Map<String, ParentControl> articleToControl = new HashMap<>();
@Before
public void setupCluster() throws Exception {
assertAcked(
prepareCreate("test")
.addMapping("doc",
addFieldMappings(buildParentJoinFieldMappingFromSimplifiedDef("join_field", true, "article", "comment"),
"commenter", "keyword", "category", "keyword"))
);
List<IndexRequestBuilder> requests = new ArrayList<>();
String[] uniqueCategories = new String[randomIntBetween(1, 25)];
for (int i = 0; i < uniqueCategories.length; i++) {
uniqueCategories[i] = Integer.toString(i);
}
int catIndex = 0;
int numParentDocs = randomIntBetween(uniqueCategories.length, uniqueCategories.length * 5);
for (int i = 0; i < numParentDocs; i++) {
String id = "article-" + i;
// TODO: this array is always of length 1, and testChildrenAggs fails if this is changed
String[] categories = new String[randomIntBetween(1,1)];
for (int j = 0; j < categories.length; j++) {
String category = categories[j] = uniqueCategories[catIndex++ % uniqueCategories.length];
Control control = categoryToControl.computeIfAbsent(category, Control::new);
control.articleIds.add(id);
articleToControl.put(id, new ParentControl(category));
}
IndexRequestBuilder indexRequest = createIndexRequest("test", "article", id, null, "category", categories, "randomized", true);
requests.add(indexRequest);
}
String[] commenters = new String[randomIntBetween(5, 50)];
for (int i = 0; i < commenters.length; i++) {
commenters[i] = Integer.toString(i);
}
int id = 0;
for (Control control : categoryToControl.values()) {
for (String articleId : control.articleIds) {
int numChildDocsPerParent = randomIntBetween(0, 5);
for (int i = 0; i < numChildDocsPerParent; i++) {
String commenter = commenters[id % commenters.length];
String idValue = "comment-" + id++;
control.commentIds.add(idValue);
Set<String> ids = control.commenterToCommentId.computeIfAbsent(commenter, k -> new HashSet<>());
ids.add(idValue);
articleToControl.get(articleId).commentIds.add(idValue);
IndexRequestBuilder indexRequest = createIndexRequest("test", "comment", idValue,
articleId, "commenter", commenter, "randomized", true);
requests.add(indexRequest);
}
}
}
requests.add(createIndexRequest("test", "article", "a", null, "category", new String[]{"a"}, "randomized", false));
requests.add(createIndexRequest("test", "article", "b", null, "category", new String[]{"a", "b"}, "randomized", false));
requests.add(createIndexRequest("test", "article", "c", null, "category", new String[]{"a", "b", "c"}, "randomized", false));
requests.add(createIndexRequest("test", "article", "d", null, "category", new String[]{"c"}, "randomized", false));
requests.add(createIndexRequest("test", "comment", "e", "a"));
requests.add(createIndexRequest("test", "comment", "f", "c"));
indexRandom(true, requests);
ensureSearchable("test");
}
protected static final class Control {
final String category;
final Set<String> articleIds = new HashSet<>();
final Set<String> commentIds = new HashSet<>();
final Map<String, Set<String>> commenterToCommentId = new HashMap<>();
private Control(String category) {
this.category = category;
}
}
protected static final class ParentControl {
final String category;
final Set<String> commentIds = new HashSet<>();
private ParentControl(String category) {
this.category = category;
}
}
}

View File

@ -25,7 +25,6 @@ import org.elasticsearch.action.update.UpdateResponse;
import org.elasticsearch.client.Requests;
import org.elasticsearch.cluster.metadata.IndexMetaData;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.join.query.ParentChildTestCase;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.aggregations.AggregationBuilders;
import org.elasticsearch.search.aggregations.InternalAggregation;
@ -33,11 +32,8 @@ import org.elasticsearch.search.aggregations.bucket.terms.Terms;
import org.elasticsearch.search.aggregations.metrics.Sum;
import org.elasticsearch.search.aggregations.metrics.TopHits;
import org.elasticsearch.search.sort.SortOrder;
import org.junit.Before;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
@ -58,80 +54,7 @@ import static org.hamcrest.Matchers.greaterThan;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.sameInstance;
public class ChildrenIT extends ParentChildTestCase {
private static final Map<String, Control> categoryToControl = new HashMap<>();
@Before
public void setupCluster() throws Exception {
categoryToControl.clear();
assertAcked(
prepareCreate("test")
.addMapping("doc",
addFieldMappings(buildParentJoinFieldMappingFromSimplifiedDef("join_field", true, "article", "comment"),
"commenter", "keyword", "category", "keyword"))
);
List<IndexRequestBuilder> requests = new ArrayList<>();
String[] uniqueCategories = new String[randomIntBetween(1, 25)];
for (int i = 0; i < uniqueCategories.length; i++) {
uniqueCategories[i] = Integer.toString(i);
}
int catIndex = 0;
int numParentDocs = randomIntBetween(uniqueCategories.length, uniqueCategories.length * 5);
for (int i = 0; i < numParentDocs; i++) {
String id = "article-" + i;
// TODO: this array is always of length 1, and testChildrenAggs fails if this is changed
String[] categories = new String[randomIntBetween(1,1)];
for (int j = 0; j < categories.length; j++) {
String category = categories[j] = uniqueCategories[catIndex++ % uniqueCategories.length];
Control control = categoryToControl.get(category);
if (control == null) {
categoryToControl.put(category, control = new Control());
}
control.articleIds.add(id);
}
requests.add(createIndexRequest("test", "article", id, null, "category", categories, "randomized", true));
}
String[] commenters = new String[randomIntBetween(5, 50)];
for (int i = 0; i < commenters.length; i++) {
commenters[i] = Integer.toString(i);
}
int id = 0;
for (Control control : categoryToControl.values()) {
for (String articleId : control.articleIds) {
int numChildDocsPerParent = randomIntBetween(0, 5);
for (int i = 0; i < numChildDocsPerParent; i++) {
String commenter = commenters[id % commenters.length];
String idValue = "comment-" + id++;
control.commentIds.add(idValue);
Set<String> ids = control.commenterToCommentId.get(commenter);
if (ids == null) {
control.commenterToCommentId.put(commenter, ids = new HashSet<>());
}
ids.add(idValue);
requests.add(createIndexRequest("test", "comment", idValue, articleId, "commenter", commenter));
}
}
}
requests.add(createIndexRequest("test", "article", "a", null, "category", new String[]{"a"}, "randomized", false));
requests.add(createIndexRequest("test", "article", "b", null, "category", new String[]{"a", "b"}, "randomized", false));
requests.add(createIndexRequest("test", "article", "c", null, "category", new String[]{"a", "b", "c"}, "randomized", false));
requests.add(createIndexRequest("test", "article", "d", null, "category", new String[]{"c"}, "randomized", false));
requests.add(createIndexRequest("test", "comment", "e", "a"));
requests.add(createIndexRequest("test", "comment", "f", "c"));
indexRandom(true, requests);
ensureSearchable("test");
}
public class ChildrenIT extends AbstractParentChildTestCase {
public void testChildrenAggs() throws Exception {
SearchResponse searchResponse = client().prepareSearch("test")
@ -455,10 +378,4 @@ public class ChildrenIT extends ParentChildTestCase {
children = parents.getBuckets().get(0).getAggregations().get("child_docs");
assertThat(children.getDocCount(), equalTo(2L));
}
private static final class Control {
final Set<String> articleIds = new HashSet<>();
final Set<String> commentIds = new HashSet<>();
final Map<String, Set<String>> commenterToCommentId = new HashMap<>();
}
}

View File

@ -37,8 +37,7 @@ public class ChildrenTests extends BaseAggregationTestCase<ChildrenAggregationBu
protected ChildrenAggregationBuilder createTestAggregatorBuilder() {
String name = randomAlphaOfLengthBetween(3, 20);
String childType = randomAlphaOfLengthBetween(5, 40);
ChildrenAggregationBuilder factory = new ChildrenAggregationBuilder(name, childType);
return factory;
return new ChildrenAggregationBuilder(name, childType);
}
}

View File

@ -0,0 +1,327 @@
/*
* Licensed to Elasticsearch under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.elasticsearch.join.aggregations;
import org.apache.lucene.document.Field;
import org.apache.lucene.document.SortedDocValuesField;
import org.apache.lucene.document.SortedNumericDocValuesField;
import org.apache.lucene.document.StringField;
import org.apache.lucene.index.DirectoryReader;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.index.RandomIndexWriter;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.MatchAllDocsQuery;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.TermInSetQuery;
import org.apache.lucene.store.Directory;
import org.apache.lucene.util.BytesRef;
import org.elasticsearch.Version;
import org.elasticsearch.cluster.metadata.IndexMetaData;
import org.elasticsearch.common.collect.Tuple;
import org.elasticsearch.common.lucene.index.ElasticsearchDirectoryReader;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.index.Index;
import org.elasticsearch.index.mapper.ContentPath;
import org.elasticsearch.index.mapper.IdFieldMapper;
import org.elasticsearch.index.mapper.MappedFieldType;
import org.elasticsearch.index.mapper.Mapper;
import org.elasticsearch.index.mapper.MapperService;
import org.elasticsearch.index.mapper.NumberFieldMapper;
import org.elasticsearch.index.mapper.Uid;
import org.elasticsearch.index.shard.ShardId;
import org.elasticsearch.join.mapper.MetaJoinFieldMapper;
import org.elasticsearch.join.mapper.ParentJoinFieldMapper;
import org.elasticsearch.search.aggregations.Aggregation;
import org.elasticsearch.search.aggregations.AggregationBuilder;
import org.elasticsearch.search.aggregations.AggregatorTestCase;
import org.elasticsearch.search.aggregations.bucket.terms.LongTerms;
import org.elasticsearch.search.aggregations.bucket.terms.TermsAggregationBuilder;
import org.elasticsearch.search.aggregations.metrics.InternalMin;
import org.elasticsearch.search.aggregations.metrics.MinAggregationBuilder;
import org.elasticsearch.search.aggregations.support.ValueType;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.function.Consumer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class ChildrenToParentAggregatorTests extends AggregatorTestCase {
private static final String CHILD_TYPE = "child_type";
private static final String PARENT_TYPE = "parent_type";
public void testNoDocs() throws IOException {
Directory directory = newDirectory();
RandomIndexWriter indexWriter = new RandomIndexWriter(random(), directory);
// intentionally not writing any docs
indexWriter.close();
IndexReader indexReader = DirectoryReader.open(directory);
testCase(new MatchAllDocsQuery(), newSearcher(indexReader, false, true), childrenToParent -> {
assertEquals(0, childrenToParent.getDocCount());
Aggregation parentAggregation = childrenToParent.getAggregations().get("in_parent");
assertEquals(0, childrenToParent.getDocCount());
assertNotNull("Aggregations: " + childrenToParent.getAggregations().asMap(), parentAggregation);
assertEquals(Double.POSITIVE_INFINITY, ((InternalMin) parentAggregation).getValue(), Double.MIN_VALUE);
});
indexReader.close();
directory.close();
}
public void testParentChild() throws IOException {
Directory directory = newDirectory();
RandomIndexWriter indexWriter = new RandomIndexWriter(random(), directory);
final Map<String, Tuple<Integer, Integer>> expectedParentChildRelations = setupIndex(indexWriter);
indexWriter.close();
IndexReader indexReader = ElasticsearchDirectoryReader.wrap(DirectoryReader.open(directory),
new ShardId(new Index("foo", "_na_"), 1));
// TODO set "maybeWrap" to true for IndexSearcher once #23338 is resolved
IndexSearcher indexSearcher = newSearcher(indexReader, false, true);
// verify with all documents
testCase(new MatchAllDocsQuery(), indexSearcher, parent -> {
int expectedTotalParents = 0;
int expectedMinValue = Integer.MAX_VALUE;
for (Tuple<Integer, Integer> expectedValues : expectedParentChildRelations.values()) {
expectedTotalParents++;
expectedMinValue = Math.min(expectedMinValue, expectedValues.v2());
}
assertEquals("Having " + parent.getDocCount() + " docs and aggregation results: " +
parent.getAggregations().asMap(),
expectedTotalParents, parent.getDocCount());
assertEquals(expectedMinValue, ((InternalMin) parent.getAggregations().get("in_parent")).getValue(), Double.MIN_VALUE);
});
// verify for each children
for (String parent : expectedParentChildRelations.keySet()) {
testCase(new TermInSetQuery(IdFieldMapper.NAME, Uid.encodeId("child0_" + parent)),
indexSearcher, aggregation -> {
assertEquals("Expected one result for min-aggregation for parent: " + parent +
", but had aggregation-results: " + aggregation,
1, aggregation.getDocCount());
assertEquals(expectedParentChildRelations.get(parent).v2(),
((InternalMin) aggregation.getAggregations().get("in_parent")).getValue(), Double.MIN_VALUE);
});
}
indexReader.close();
directory.close();
}
public void testParentChildTerms() throws IOException {
Directory directory = newDirectory();
RandomIndexWriter indexWriter = new RandomIndexWriter(random(), directory);
final Map<String, Tuple<Integer, Integer>> expectedParentChildRelations = setupIndex(indexWriter);
indexWriter.close();
SortedMap<Integer, Long> entries = new TreeMap<>();
for (Tuple<Integer, Integer> value : expectedParentChildRelations.values()) {
Long l = entries.computeIfAbsent(value.v2(), integer -> 0L);
entries.put(value.v2(), l+1);
}
List<Map.Entry<Integer, Long>> sortedValues = new ArrayList<>(entries.entrySet());
sortedValues.sort((o1, o2) -> {
// sort larger values first
int ret = o2.getValue().compareTo(o1.getValue());
if(ret != 0) {
return ret;
}
// on equal value, sort by key
return o1.getKey().compareTo(o2.getKey());
});
IndexReader indexReader = ElasticsearchDirectoryReader.wrap(DirectoryReader.open(directory),
new ShardId(new Index("foo", "_na_"), 1));
// TODO set "maybeWrap" to true for IndexSearcher once #23338 is resolved
IndexSearcher indexSearcher = newSearcher(indexReader, false, true);
// verify a terms-aggregation inside the parent-aggregation
testCaseTerms(new MatchAllDocsQuery(), indexSearcher, parent -> {
assertNotNull(parent);
LongTerms valueTerms = parent.getAggregations().get("value_terms");
assertNotNull(valueTerms);
List<LongTerms.Bucket> valueTermsBuckets = valueTerms.getBuckets();
assertNotNull(valueTermsBuckets);
assertEquals("Had: " + parent, sortedValues.size(), valueTermsBuckets.size());
int i = 0;
for (Map.Entry<Integer, Long> entry : sortedValues) {
LongTerms.Bucket bucket = valueTermsBuckets.get(i);
assertEquals(entry.getKey().longValue(), bucket.getKeyAsNumber());
assertEquals(entry.getValue(), (Long)bucket.getDocCount());
i++;
}
});
indexReader.close();
directory.close();
}
public void testTermsParentChildTerms() throws IOException {
Directory directory = newDirectory();
RandomIndexWriter indexWriter = new RandomIndexWriter(random(), directory);
final Map<String, Tuple<Integer, Integer>> expectedParentChildRelations = setupIndex(indexWriter);
indexWriter.close();
SortedMap<Integer, Long> sortedValues = new TreeMap<>();
for (Tuple<Integer, Integer> value : expectedParentChildRelations.values()) {
Long l = sortedValues.computeIfAbsent(value.v2(), integer -> 0L);
sortedValues.put(value.v2(), l+1);
}
IndexReader indexReader = ElasticsearchDirectoryReader.wrap(DirectoryReader.open(directory),
new ShardId(new Index("foo", "_na_"), 1));
// TODO set "maybeWrap" to true for IndexSearcher once #23338 is resolved
IndexSearcher indexSearcher = newSearcher(indexReader, false, true);
// verify a terms-aggregation inside the parent-aggregation which itself is inside a
// terms-aggregation on the child-documents
testCaseTermsParentTerms(new MatchAllDocsQuery(), indexSearcher, longTerms -> {
assertNotNull(longTerms);
for (LongTerms.Bucket bucket : longTerms.getBuckets()) {
assertNotNull(bucket);
assertNotNull(bucket.getKeyAsString());
}
});
indexReader.close();
directory.close();
}
private static Map<String, Tuple<Integer, Integer>> setupIndex(RandomIndexWriter iw) throws IOException {
Map<String, Tuple<Integer, Integer>> expectedValues = new HashMap<>();
int numParents = randomIntBetween(1, 10);
for (int i = 0; i < numParents; i++) {
String parent = "parent" + i;
int randomValue = randomIntBetween(0, 100);
List<Field> parentDocument = createParentDocument(parent, randomValue);
/*long parentDocId =*/ iw.addDocument(parentDocument);
//System.out.println("Parent: " + parent + ": " + randomValue + ", id: " + parentDocId);
int numChildren = randomIntBetween(1, 10);
int minValue = Integer.MAX_VALUE;
for (int c = 0; c < numChildren; c++) {
minValue = Math.min(minValue, randomValue);
int randomSubValue = randomIntBetween(0, 100);
List<Field> childDocument = createChildDocument("child" + c + "_" + parent, parent, randomSubValue);
/*long childDocId =*/ iw.addDocument(childDocument);
//System.out.println("Child: " + "child" + c + "_" + parent + ": " + randomSubValue + ", id: " + childDocId);
}
expectedValues.put(parent, new Tuple<>(numChildren, minValue));
}
return expectedValues;
}
private static List<Field> createParentDocument(String id, int value) {
return Arrays.asList(
new StringField(IdFieldMapper.NAME, Uid.encodeId(id), Field.Store.NO),
new StringField("join_field", PARENT_TYPE, Field.Store.NO),
createJoinField(PARENT_TYPE, id),
new SortedNumericDocValuesField("number", value)
);
}
private static List<Field> createChildDocument(String childId, String parentId, int value) {
return Arrays.asList(
new StringField(IdFieldMapper.NAME, Uid.encodeId(childId), Field.Store.NO),
new StringField("join_field", CHILD_TYPE, Field.Store.NO),
createJoinField(PARENT_TYPE, parentId),
new SortedNumericDocValuesField("subNumber", value)
);
}
private static SortedDocValuesField createJoinField(String parentType, String id) {
return new SortedDocValuesField("join_field#" + parentType, new BytesRef(id));
}
@Override
protected MapperService mapperServiceMock() {
ParentJoinFieldMapper joinFieldMapper = createJoinFieldMapper();
MapperService mapperService = mock(MapperService.class);
MetaJoinFieldMapper.MetaJoinFieldType metaJoinFieldType = mock(MetaJoinFieldMapper.MetaJoinFieldType.class);
when(metaJoinFieldType.getMapper()).thenReturn(joinFieldMapper);
when(mapperService.fullName("_parent_join")).thenReturn(metaJoinFieldType);
return mapperService;
}
private static ParentJoinFieldMapper createJoinFieldMapper() {
Settings settings = Settings.builder().put(IndexMetaData.SETTING_VERSION_CREATED, Version.CURRENT).build();
return new ParentJoinFieldMapper.Builder("join_field")
.addParent(PARENT_TYPE, Collections.singleton(CHILD_TYPE))
.build(new Mapper.BuilderContext(settings, new ContentPath(0)));
}
private void testCase(Query query, IndexSearcher indexSearcher, Consumer<InternalParent> verify)
throws IOException {
ParentAggregationBuilder aggregationBuilder = new ParentAggregationBuilder("_name", CHILD_TYPE);
aggregationBuilder.subAggregation(new MinAggregationBuilder("in_parent").field("number"));
MappedFieldType fieldType = new NumberFieldMapper.NumberFieldType(NumberFieldMapper.NumberType.LONG);
fieldType.setName("number");
InternalParent result = search(indexSearcher, query, aggregationBuilder, fieldType);
verify.accept(result);
}
private void testCaseTerms(Query query, IndexSearcher indexSearcher, Consumer<InternalParent> verify)
throws IOException {
ParentAggregationBuilder aggregationBuilder = new ParentAggregationBuilder("_name", CHILD_TYPE);
aggregationBuilder.subAggregation(new TermsAggregationBuilder("value_terms", ValueType.LONG).field("number"));
MappedFieldType fieldType = new NumberFieldMapper.NumberFieldType(NumberFieldMapper.NumberType.LONG);
fieldType.setName("number");
InternalParent result = search(indexSearcher, query, aggregationBuilder, fieldType);
verify.accept(result);
}
// run a terms aggregation on the number in child-documents, then a parent aggregation and then terms on the parent-number
private void testCaseTermsParentTerms(Query query, IndexSearcher indexSearcher, Consumer<LongTerms> verify)
throws IOException {
AggregationBuilder aggregationBuilder =
new TermsAggregationBuilder("subvalue_terms", ValueType.LONG).field("subNumber").
subAggregation(new ParentAggregationBuilder("to_parent", CHILD_TYPE).
subAggregation(new TermsAggregationBuilder("value_terms", ValueType.LONG).field("number")));
MappedFieldType fieldType = new NumberFieldMapper.NumberFieldType(NumberFieldMapper.NumberType.LONG);
fieldType.setName("number");
MappedFieldType subFieldType = new NumberFieldMapper.NumberFieldType(NumberFieldMapper.NumberType.LONG);
subFieldType.setName("subNumber");
LongTerms result = search(indexSearcher, query, aggregationBuilder, fieldType, subFieldType);
verify.accept(result);
}
}

View File

@ -0,0 +1,65 @@
/*
* Licensed to Elasticsearch under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.elasticsearch.join.aggregations;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import org.elasticsearch.common.ParseField;
import org.elasticsearch.common.io.stream.Writeable.Reader;
import org.elasticsearch.common.xcontent.NamedXContentRegistry.Entry;
import org.elasticsearch.search.aggregations.Aggregation;
import org.elasticsearch.search.aggregations.InternalAggregations;
import org.elasticsearch.search.aggregations.InternalSingleBucketAggregationTestCase;
import org.elasticsearch.search.aggregations.bucket.ParsedSingleBucketAggregation;
import org.elasticsearch.search.aggregations.pipeline.PipelineAggregator;
public class InternalParentTests extends InternalSingleBucketAggregationTestCase<InternalParent> {
@Override
protected List<Entry> getNamedXContents() {
List<Entry> extendedNamedXContents = new ArrayList<>(super.getNamedXContents());
extendedNamedXContents.add(new Entry(Aggregation.class, new ParseField(ParentAggregationBuilder.NAME),
(p, c) -> ParsedParent.fromXContent(p, (String) c)));
return extendedNamedXContents ;
}
@Override
protected InternalParent createTestInstance(String name, long docCount, InternalAggregations aggregations,
List<PipelineAggregator> pipelineAggregators, Map<String, Object> metaData) {
return new InternalParent(name, docCount, aggregations, pipelineAggregators, metaData);
}
@Override
protected void extraAssertReduced(InternalParent reduced, List<InternalParent> inputs) {
// Nothing extra to assert
}
@Override
protected Reader<InternalParent> instanceReader() {
return InternalParent::new;
}
@Override
protected Class<? extends ParsedSingleBucketAggregation> implementationClass() {
return ParsedParent.class;
}
}

View File

@ -0,0 +1,238 @@
/*
* Licensed to Elasticsearch under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.elasticsearch.join.aggregations;
import org.elasticsearch.action.search.SearchRequestBuilder;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.search.aggregations.Aggregation;
import org.elasticsearch.search.aggregations.bucket.MultiBucketsAggregation;
import org.elasticsearch.search.aggregations.bucket.terms.Terms;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static org.elasticsearch.index.query.QueryBuilders.matchQuery;
import static org.elasticsearch.join.aggregations.JoinAggregationBuilders.parent;
import static org.elasticsearch.search.aggregations.AggregationBuilders.terms;
import static org.elasticsearch.search.aggregations.AggregationBuilders.topHits;
import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertSearchResponse;
import static org.hamcrest.Matchers.equalTo;
public class ParentIT extends AbstractParentChildTestCase {
public void testSimpleParentAgg() throws Exception {
final SearchRequestBuilder searchRequest = client().prepareSearch("test")
.setSize(10000)
.setQuery(matchQuery("randomized", true))
.addAggregation(
parent("to_article", "comment")
.subAggregation(
terms("category").field("category").size(10000)));
SearchResponse searchResponse = searchRequest.get();
assertSearchResponse(searchResponse);
long articlesWithComment = articleToControl.values().stream().filter(
parentControl -> !parentControl.commentIds.isEmpty()
).count();
Parent parentAgg = searchResponse.getAggregations().get("to_article");
assertThat("Request: " + searchRequest + "\nResponse: " + searchResponse + "\n",
parentAgg.getDocCount(), equalTo(articlesWithComment));
Terms categoryTerms = parentAgg.getAggregations().get("category");
long categoriesWithComments = categoryToControl.values().stream().filter(
control -> !control.commentIds.isEmpty()).count();
assertThat("Buckets: " + categoryTerms.getBuckets().stream().map(
(Function<Terms.Bucket, String>) MultiBucketsAggregation.Bucket::getKeyAsString).collect(Collectors.toList()) +
"\nCategories: " + categoryToControl.keySet(),
(long)categoryTerms.getBuckets().size(), equalTo(categoriesWithComments));
for (Map.Entry<String, Control> entry : categoryToControl.entrySet()) {
// no children for this category -> no entry in the child to parent-aggregation
if(entry.getValue().commentIds.isEmpty()) {
assertNull(categoryTerms.getBucketByKey(entry.getKey()));
continue;
}
final Terms.Bucket categoryBucket = categoryTerms.getBucketByKey(entry.getKey());
assertNotNull("Failed for category " + entry.getKey(),
categoryBucket);
assertThat("Failed for category " + entry.getKey(),
categoryBucket.getKeyAsString(), equalTo(entry.getKey()));
// count all articles in this category which have at least one comment
long articlesForCategory = articleToControl.values().stream().
// only articles with this category
filter(parentControl -> parentControl.category.equals(entry.getKey())).
// only articles which have comments
filter(parentControl -> !parentControl.commentIds.isEmpty()).
count();
assertThat("Failed for category " + entry.getKey(),
categoryBucket.getDocCount(), equalTo(articlesForCategory));
}
}
public void testParentAggs() throws Exception {
final SearchRequestBuilder searchRequest = client().prepareSearch("test")
.setSize(10000)
.setQuery(matchQuery("randomized", true))
.addAggregation(
terms("to_commenter").field("commenter").size(10000).subAggregation(
parent("to_article", "comment").subAggregation(
terms("to_category").field("category").size(10000).subAggregation(
topHits("top_category")
))
)
);
SearchResponse searchResponse = searchRequest.get();
assertSearchResponse(searchResponse);
final Set<String> commenters = getCommenters();
final Map<String, Set<String>> commenterToComments = getCommenterToComments();
Terms categoryTerms = searchResponse.getAggregations().get("to_commenter");
assertThat("Request: " + searchRequest + "\nResponse: " + searchResponse + "\n",
categoryTerms.getBuckets().size(), equalTo(commenters.size()));
for (Terms.Bucket commenterBucket : categoryTerms.getBuckets()) {
Set<String> comments = commenterToComments.get(commenterBucket.getKeyAsString());
assertNotNull(comments);
assertThat("Failed for commenter " + commenterBucket.getKeyAsString(),
commenterBucket.getDocCount(), equalTo((long)comments.size()));
Parent articleAgg = commenterBucket.getAggregations().get("to_article");
assertThat(articleAgg.getName(), equalTo("to_article"));
// find all articles for the comments for the current commenter
Set<String> articles = articleToControl.values().stream().flatMap(
(Function<ParentControl, Stream<String>>) parentControl -> parentControl.commentIds.stream().
filter(comments::contains)
).collect(Collectors.toSet());
assertThat(articleAgg.getDocCount(), equalTo((long)articles.size()));
Terms categoryAgg = articleAgg.getAggregations().get("to_category");
assertNotNull(categoryAgg);
List<String> categories = categoryToControl.entrySet().
stream().
filter(entry -> entry.getValue().commenterToCommentId.containsKey(commenterBucket.getKeyAsString())).
map(Map.Entry::getKey).
collect(Collectors.toList());
for (String category : categories) {
Terms.Bucket categoryBucket = categoryAgg.getBucketByKey(category);
assertNotNull(categoryBucket);
Aggregation topCategory = categoryBucket.getAggregations().get("top_category");
assertNotNull(topCategory);
}
}
for (String commenter : commenters) {
Terms.Bucket categoryBucket = categoryTerms.getBucketByKey(commenter);
assertThat(categoryBucket.getKeyAsString(), equalTo(commenter));
assertThat(categoryBucket.getDocCount(), equalTo((long) commenterToComments.get(commenter).size()));
Parent childrenBucket = categoryBucket.getAggregations().get("to_article");
assertThat(childrenBucket.getName(), equalTo("to_article"));
}
}
private Set<String> getCommenters() {
return categoryToControl.values().stream().flatMap(
(Function<Control, Stream<String>>) control -> control.commenterToCommentId.keySet().stream()).
collect(Collectors.toSet());
}
private Map<String, Set<String>> getCommenterToComments() {
final Map<String, Set<String>> commenterToComments = new HashMap<>();
for (Control control : categoryToControl.values()) {
for (Map.Entry<String, Set<String>> entry : control.commenterToCommentId.entrySet()) {
final Set<String> comments = commenterToComments.computeIfAbsent(entry.getKey(), s -> new HashSet<>());
comments.addAll(entry.getValue());
}
}
return commenterToComments;
}
public void testNonExistingParentType() throws Exception {
SearchResponse searchResponse = client().prepareSearch("test")
.addAggregation(
parent("non-existing", "xyz")
).get();
assertSearchResponse(searchResponse);
Parent parent = searchResponse.getAggregations().get("non-existing");
assertThat(parent.getName(), equalTo("non-existing"));
assertThat(parent.getDocCount(), equalTo(0L));
}
public void testTermsParentAggTerms() throws Exception {
final SearchRequestBuilder searchRequest = client().prepareSearch("test")
.setSize(10000)
.setQuery(matchQuery("randomized", true))
.addAggregation(
terms("to_commenter").field("commenter").size(10000).subAggregation(
parent("to_article", "comment").subAggregation(
terms("to_category").field("category").size(10000))));
SearchResponse searchResponse = searchRequest.get();
assertSearchResponse(searchResponse);
final Set<String> commenters = getCommenters();
final Map<String, Set<String>> commenterToComments = getCommenterToComments();
Terms commentersAgg = searchResponse.getAggregations().get("to_commenter");
assertThat("Request: " + searchRequest + "\nResponse: " + searchResponse + "\n",
commentersAgg.getBuckets().size(), equalTo(commenters.size()));
for (Terms.Bucket commenterBucket : commentersAgg.getBuckets()) {
Set<String> comments = commenterToComments.get(commenterBucket.getKeyAsString());
assertNotNull(comments);
assertThat("Failed for commenter " + commenterBucket.getKeyAsString(),
commenterBucket.getDocCount(), equalTo((long)comments.size()));
Parent articleAgg = commenterBucket.getAggregations().get("to_article");
assertThat(articleAgg.getName(), equalTo("to_article"));
// find all articles for the comments for the current commenter
Set<String> articles = articleToControl.values().stream().flatMap(
(Function<ParentControl, Stream<String>>) parentControl -> parentControl.commentIds.stream().
filter(comments::contains)
).collect(Collectors.toSet());
assertThat(articleAgg.getDocCount(), equalTo((long)articles.size()));
Terms categoryAgg = articleAgg.getAggregations().get("to_category");
assertNotNull(categoryAgg);
List<String> categories = categoryToControl.entrySet().
stream().
filter(entry -> entry.getValue().commenterToCommentId.containsKey(commenterBucket.getKeyAsString())).
map(Map.Entry::getKey).
collect(Collectors.toList());
for (String category : categories) {
Terms.Bucket categoryBucket = categoryAgg.getBucketByKey(category);
assertNotNull(categoryBucket);
}
}
}
}

View File

@ -0,0 +1,42 @@
/*
* Licensed to Elasticsearch under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.elasticsearch.join.aggregations;
import java.util.Collection;
import java.util.Collections;
import org.elasticsearch.join.ParentJoinPlugin;
import org.elasticsearch.plugins.Plugin;
import org.elasticsearch.search.aggregations.BaseAggregationTestCase;
public class ParentTests extends BaseAggregationTestCase<ParentAggregationBuilder> {
@Override
protected Collection<Class<? extends Plugin>> getPlugins() {
return Collections.singleton(ParentJoinPlugin.class);
}
@Override
protected ParentAggregationBuilder createTestAggregatorBuilder() {
String name = randomAlphaOfLengthBetween(3, 20);
String parentType = randomAlphaOfLengthBetween(5, 40);
return new ParentAggregationBuilder(name, parentType);
}
}