├── .gitignore ├── LICENSE ├── README.md ├── groovy.plugin.security.policy ├── plugin-descriptor.properties ├── pom.xml ├── src ├── main │ ├── assemblies │ │ └── plugin.xml │ ├── java │ │ └── fr │ │ │ └── v3d │ │ │ └── elasticsearch │ │ │ ├── plugin │ │ │ └── multiplemetric │ │ │ │ └── MultipleMetricPlugin.java │ │ │ └── search │ │ │ └── aggregations │ │ │ ├── metrics │ │ │ └── multiplemetric │ │ │ │ ├── CountBuilder.java │ │ │ │ ├── FieldBuilder.java │ │ │ │ ├── InternalMultipleMetric.java │ │ │ │ ├── MultipleMetric.java │ │ │ │ ├── MultipleMetricAggregator.java │ │ │ │ ├── MultipleMetricBuilder.java │ │ │ │ ├── MultipleMetricParam.java │ │ │ │ ├── MultipleMetricParser.java │ │ │ │ ├── ScriptBuilder.java │ │ │ │ └── SumBuilder.java │ │ │ └── support │ │ │ └── MultipleValuesSourceAggregatorFactory.java │ └── resources │ │ └── es-plugin.properties └── test │ ├── java │ └── fr │ │ └── v3d │ │ └── elasticsearch │ │ ├── plugin │ │ └── multiplemetric │ │ │ ├── MultipleMetricAggregationTestCase.java │ │ │ └── MultipleMetricAggregatorTest.java │ │ └── search │ │ └── aggregations │ │ └── metric │ │ └── multiplemetric │ │ └── MultipleMetricParserTest.java │ └── resources │ └── log4j.properties └── testng.xml /.gitignore: -------------------------------------------------------------------------------- 1 | data/ 2 | releases/ 3 | target/ 4 | test-output/ 5 | 6 | .classpath 7 | .project 8 | .settings/ 9 | .idea/ 10 | *iml 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Elie 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Elasticsearch Multiple Metric Aggregation 2 | 3 | This plugin add a multi-value metrics aggregation which can define and reuse several metrics. Each metric value is build either from an aggregator (sum or count) and a numeric field in the document, or generated from a script (using the previous metric defined). Because it's a multi-value metrics aggregation; each returned value can be used to sort a parent terms aggregation. 4 | 5 | ## Installation 6 | 7 | ### Versions 8 | 9 | | elasticsearch version | latest version | 10 | | --------------------- | ------------- | 11 | | 1.4.5+ | 1.4.7 | 12 | | 2.0+ | 2.0.0 | 13 | | 2.2+ | 2.2.0 | 14 | | 2.3.2 | 2.3.1 | 15 | | 2.3.5 | 2.3.5.1 | 16 | | 2.4.0 | 2.4.0.0 | 17 | | 2.4.1 | 2.4.1.0 | 18 | | 2.4.2 | 2.4.2.0 | 19 | | 2.4.3 | 2.4.3.0 | 20 | | 2.4.4 | 2.4.4.0 | 21 | | 2.4.5 | 2.4.5.0 | 22 | 23 | ### Install as plugin 24 | 25 | Up to 2.0.0: 26 | ``` 27 | bin/plugin --url https://github.com/eliep/elasticsearch-multiple-metric-aggregation/releases/download/2.0.0/elasticsearch-multiple-metric-aggregation-2.0.0.zip install elasticsearch-multiple-metric-aggregation 28 | ``` 29 | 30 | After: 31 | ``` 32 | bin/plugin install https://github.com/eliep/elasticsearch-multiple-metric-aggregation/releases/download/2.3.1/elasticsearch-multiple-metric-aggregation-2.3.1.zip 33 | ``` 34 | 35 | ## Examples 36 | 37 | ### Basics 38 | Given these documents: 39 | ``` 40 | { "id": "1", "x": 13, "y": 1, "tag": "a" } 41 | { "id": "1", "x": 15, "y": 4, "tag": "b" } 42 | { "id": "1", "x": 12, "y": 2, "tag": "a" } 43 | { "id": "2", "x": 14, "y": 1, "tag": "a" } 44 | { "id": "2", "x": 15, "y": 3, "tag": "a" } 45 | { "id": "2", "x": 11, "y": 9, "tag": "b" } 46 | ``` 47 | 48 | You can compute the ratio sum(x) / sum(y) with the following aggregation: 49 | ``` 50 | { 51 | "aggs" : { 52 | "ranking" : { 53 | "multiple-metric" : { 54 | "ratio" : { "script": "sum_x / sum_y" }, 55 | "sum_x" : { "sum" : { "field": "x" } } 56 | "sum_y" : { "sum" : { "field": "y" } } 57 | } 58 | } 59 | } 60 | } 61 | ``` 62 | 63 | The above will returns the following: 64 | 65 | ``` 66 | "aggregations": { 67 | "metrics": { 68 | "sum_x": { 69 | "value": 80.0, 70 | "doc_count": 6 71 | }, 72 | "sum_y": { 73 | "value": 20.0, 74 | "doc_count": 6 75 | }, 76 | "ratio": { 77 | "value": 4.0 78 | } 79 | } 80 | } 81 | ``` 82 | 83 | You can also use it to compute a weighted average: 84 | ``` 85 | { 86 | "aggs" : { 87 | "ranking" : { 88 | "multiple-metric" : { 89 | "ratio" : { "script": "sum_x / sum_y" }, 90 | "sum_x" : { "sum" : { "script": "x * y" } } 91 | "sum_y" : { "sum" : { "field": "y" } } 92 | } 93 | } 94 | } 95 | } 96 | ``` 97 | 98 | 99 | ### Ordering a terms aggregation 100 | Because it's a multi-value metrics aggregation, it can be used to order a terms aggregation: 101 | ``` 102 | { 103 | "aggs": { 104 | "group_by": { 105 | "terms": { 106 | "field": "id", 107 | "order": { "ranking.ratio": "asc" } 108 | }, 109 | "aggs" : { 110 | "ranking" : { 111 | "multiple-metric" : { 112 | "ratio" : { "script": "sum_x / sum_y" }, 113 | "sum_x" : { "sum" : { "field": "x" } } 114 | "sum_y" : { "sum" : { "field": "y" } } 115 | } 116 | } 117 | } 118 | } 119 | } 120 | } 121 | ``` 122 | 123 | ### Filtering 124 | It's also possible to add a filter on a metric value computed from a document field: 125 | 126 | ``` 127 | { 128 | "aggs" : { 129 | "ranking" : { 130 | "multiple-metric" : { 131 | "ratio" : { "script": "sum_x / sum_y" }, 132 | "sum_x" : { "sum" : { "field": "x" }, "filter": { "term" : { "tag" : "a" } } }, 133 | "sum_y" : { "sum" : { "field": "y" } } 134 | } 135 | } 136 | } 137 | } 138 | ``` 139 | 140 | 141 | ### Script parameters 142 | Script parameters are also allowed: 143 | 144 | ``` 145 | { 146 | "aggs" : { 147 | "ranking" : { 148 | "multiple-metric" : { 149 | "ratio" : { "script": "sum_x * factor / sum_y", "params" : { "factor": 2 } }, 150 | "sum_x" : { "sum" : { "field": "x" }, "filter": { "term" : { "tag" : "a" } } }, 151 | "sum_y" : { "sum" : { "field": "y" } } 152 | } 153 | } 154 | } 155 | } 156 | ``` 157 | 158 | ## Todo 159 | 160 | * Script metric can only be defined inline 161 | * Add min/max operator -------------------------------------------------------------------------------- /groovy.plugin.security.policy: -------------------------------------------------------------------------------- 1 | grant { // needed to generate runtime classes 2 | permission java.lang.RuntimePermission "createClassLoader"; 3 | // needed by IndyInterface 4 | permission java.lang.RuntimePermission "getClassLoader"; 5 | // needed by groovy engine 6 | permission java.lang.RuntimePermission "accessDeclaredMembers"; 7 | permission java.lang.RuntimePermission "accessClassInPackage.sun.reflect"; 8 | // needed by GroovyScriptEngineService to close its classloader (why?) 9 | permission java.lang.RuntimePermission "closeClassLoader"; 10 | // Allow executing groovy scripts with codesource of /untrusted 11 | permission groovy.security.GroovyCodeSourcePermission "/untrusted"; 12 | 13 | // Standard set of classes 14 | permission org.elasticsearch.script.ClassPermission "<>"; 15 | // groovy runtime (TODO: clean these up if possible) 16 | permission org.elasticsearch.script.ClassPermission "groovy.grape.GrabAnnotationTransformation"; 17 | permission org.elasticsearch.script.ClassPermission "groovy.lang.Binding"; 18 | permission org.elasticsearch.script.ClassPermission "groovy.lang.GroovyObject"; 19 | permission org.elasticsearch.script.ClassPermission "groovy.lang.GString"; 20 | permission org.elasticsearch.script.ClassPermission "groovy.lang.Script"; 21 | permission org.elasticsearch.script.ClassPermission "groovy.util.GroovyCollections"; 22 | permission org.elasticsearch.script.ClassPermission "org.codehaus.groovy.ast.builder.AstBuilderTransformation"; 23 | permission org.elasticsearch.script.ClassPermission "org.codehaus.groovy.reflection.ClassInfo"; 24 | permission org.elasticsearch.script.ClassPermission "org.codehaus.groovy.runtime.GStringImpl"; 25 | permission org.elasticsearch.script.ClassPermission "org.codehaus.groovy.runtime.powerassert.ValueRecorder"; 26 | permission org.elasticsearch.script.ClassPermission "org.codehaus.groovy.runtime.powerassert.AssertionRenderer"; 27 | permission org.elasticsearch.script.ClassPermission "org.codehaus.groovy.runtime.ScriptBytecodeAdapter"; 28 | permission org.elasticsearch.script.ClassPermission "org.codehaus.groovy.runtime.typehandling.DefaultTypeTransformation"; 29 | permission org.elasticsearch.script.ClassPermission "org.codehaus.groovy.vmplugin.v7.IndyInterface"; 30 | permission org.elasticsearch.script.ClassPermission "sun.reflect.ConstructorAccessorImpl"; 31 | permission org.elasticsearch.script.ClassPermission "sun.reflect.MethodAccessorImpl"; 32 | 33 | permission org.elasticsearch.script.ClassPermission "groovy.lang.Closure"; 34 | permission org.elasticsearch.script.ClassPermission "org.codehaus.groovy.runtime.GeneratedClosure"; 35 | permission org.elasticsearch.script.ClassPermission "groovy.lang.MetaClass"; 36 | permission org.elasticsearch.script.ClassPermission "groovy.lang.Range"; 37 | permission org.elasticsearch.script.ClassPermission "groovy.lang.Reference"; 38 | }; -------------------------------------------------------------------------------- /plugin-descriptor.properties: -------------------------------------------------------------------------------- 1 | name=${elasticsearch.plugin.name} 2 | description=${project.description} 3 | version=${project.version} 4 | jvm=${elasticsearch.plugin.jvm} 5 | classname=${elasticsearch.plugin.classname} 6 | java.version=1.8 7 | elasticsearch.version=${elasticsearch.version} -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | Elasticsearch Multiple Metric Aggregation Plugin 7 | Add a multi-value metrics aggregation which can define and reuse several metrics 8 | 9 | 4.0.0 10 | 11 | fr.v3d.elasticsearch.plugin 12 | elasticsearch-multiple-metric-aggregation 13 | 2.4.5.0 14 | 15 | jar 16 | 17 | 18 | UTF-8 19 | 5.5.2 20 | 2.4.5 21 | multiple-metric 22 | true 23 | fr.v3d.elasticsearch.plugin.multiplemetric.MultipleMetricPlugin 24 | 25 | 26 | 27 | 28 | org.elasticsearch 29 | elasticsearch 30 | ${elasticsearch.version} 31 | jar 32 | compile 33 | 34 | 35 | 36 | org.apache.lucene 37 | lucene-test-framework 38 | ${lucene.version} 39 | test 40 | 41 | 42 | 43 | org.elasticsearch 44 | elasticsearch 45 | ${elasticsearch.version} 46 | test 47 | test-jar 48 | 49 | 50 | 51 | org.elasticsearch.module 52 | lang-groovy 53 | ${elasticsearch.version} 54 | test 55 | 56 | 57 | 58 | org.hamcrest 59 | hamcrest-core 60 | 1.3 61 | test 62 | 63 | 64 | 65 | org.hamcrest 66 | hamcrest-library 67 | 1.3 68 | test 69 | 70 | 71 | 72 | junit 73 | junit 74 | 4.11 75 | test 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | src/main/resources 84 | true 85 | 86 | es-plugin.properties 87 | 88 | 89 | 90 | ./ 91 | true 92 | 93 | plugin-descriptor.properties 94 | 95 | 96 | 97 | 98 | 99 | 100 | maven-clean-plugin 101 | 2.4.1 102 | 103 | 104 | 105 | data 106 | 107 | 108 | 109 | 110 | 111 | org.apache.maven.plugins 112 | maven-surefire-plugin 113 | 2.14.1 114 | 115 | -Djava.security.policy=file://${basedir}/groovy.plugin.security.policy 116 | 117 | 118 | 119 | maven-assembly-plugin 120 | 2.4 121 | 122 | false 123 | ${basedir}/releases/ 124 | 125 | ${basedir}/src/main/assemblies/plugin.xml 126 | 127 | 128 | 129 | 130 | package 131 | 132 | single 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | -------------------------------------------------------------------------------- /src/main/assemblies/plugin.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | plugin 4 | 5 | zip 6 | 7 | false 8 | 9 | ${project.basedir} 10 | 11 | plugin-descriptor.properties 12 | 13 | true 14 | true 15 | 16 | 17 | 18 | 19 | / 20 | true 21 | true 22 | 23 | org.elasticsearch:elasticsearch 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/main/java/fr/v3d/elasticsearch/plugin/multiplemetric/MultipleMetricPlugin.java: -------------------------------------------------------------------------------- 1 | package fr.v3d.elasticsearch.plugin.multiplemetric; 2 | 3 | import org.elasticsearch.plugins.Plugin; 4 | import org.elasticsearch.search.SearchModule; 5 | 6 | import fr.v3d.elasticsearch.search.aggregations.metrics.multiplemetric.InternalMultipleMetric; 7 | import fr.v3d.elasticsearch.search.aggregations.metrics.multiplemetric.MultipleMetricParser; 8 | 9 | public class MultipleMetricPlugin extends Plugin { 10 | 11 | public String name() { 12 | return "multiple-metric-aggregation"; 13 | } 14 | 15 | public String description() { 16 | return "Multiple Metric Aggregation for Elasticsearch"; 17 | } 18 | 19 | public void onModule(SearchModule module) { 20 | module.registerAggregatorParser(MultipleMetricParser.class); 21 | InternalMultipleMetric.registerStreams(); 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/fr/v3d/elasticsearch/search/aggregations/metrics/multiplemetric/CountBuilder.java: -------------------------------------------------------------------------------- 1 | package fr.v3d.elasticsearch.search.aggregations.metrics.multiplemetric; 2 | 3 | public class CountBuilder extends FieldBuilder { 4 | 5 | public CountBuilder(String name) { 6 | super(name, MultipleMetricParser.COUNT_OPERATOR); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/fr/v3d/elasticsearch/search/aggregations/metrics/multiplemetric/FieldBuilder.java: -------------------------------------------------------------------------------- 1 | package fr.v3d.elasticsearch.search.aggregations.metrics.multiplemetric; 2 | 3 | import java.io.IOException; 4 | 5 | import org.elasticsearch.common.xcontent.XContentBuilder; 6 | import org.elasticsearch.common.xcontent.ToXContent; 7 | import org.elasticsearch.index.query.QueryBuilder; 8 | import org.elasticsearch.script.Script; 9 | 10 | public class FieldBuilder implements ToXContent { 11 | 12 | private String name; 13 | private String type; 14 | 15 | private String field; 16 | private Script script; 17 | public QueryBuilder filter; 18 | 19 | public FieldBuilder(String name, String type) { 20 | this.name = name; 21 | this.type = type; 22 | } 23 | 24 | public FieldBuilder field(String field) { 25 | this.field = field; 26 | return this; 27 | } 28 | 29 | public FieldBuilder script(Script script) { 30 | this.script = script; 31 | return this; 32 | } 33 | 34 | public FieldBuilder filter(QueryBuilder filter) { 35 | this.filter = filter; 36 | return this; 37 | } 38 | 39 | @Override 40 | public final XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { 41 | builder 42 | .startObject(name) 43 | .startObject(type); 44 | 45 | if (field != null) { 46 | builder.field("field", field); 47 | } 48 | 49 | if (script != null) { 50 | builder.field("script", script); 51 | } 52 | 53 | builder.endObject(); 54 | 55 | if (filter != null) { 56 | builder.field("filter", filter); 57 | } 58 | 59 | return builder.endObject(); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/main/java/fr/v3d/elasticsearch/search/aggregations/metrics/multiplemetric/InternalMultipleMetric.java: -------------------------------------------------------------------------------- 1 | package fr.v3d.elasticsearch.search.aggregations.metrics.multiplemetric; 2 | 3 | import java.io.IOException; 4 | import java.util.HashMap; 5 | import java.util.List; 6 | import java.util.Map; 7 | 8 | import org.elasticsearch.common.io.stream.StreamInput; 9 | import org.elasticsearch.common.io.stream.StreamOutput; 10 | import org.elasticsearch.common.logging.ESLogger; 11 | import org.elasticsearch.common.logging.ESLoggerFactory; 12 | import org.elasticsearch.common.xcontent.XContentBuilder; 13 | import org.elasticsearch.script.Script; 14 | import org.elasticsearch.script.ScriptContext; 15 | import org.elasticsearch.search.aggregations.AggregationStreams; 16 | import org.elasticsearch.search.aggregations.InternalAggregation; 17 | import org.elasticsearch.search.aggregations.metrics.InternalNumericMetricsAggregation; 18 | import org.elasticsearch.search.aggregations.pipeline.PipelineAggregator; 19 | 20 | import static java.lang.Double.NaN; 21 | 22 | public class InternalMultipleMetric extends InternalNumericMetricsAggregation.MultiValue implements MultipleMetric { 23 | 24 | protected final static ESLogger logger = ESLoggerFactory.getLogger("test"); 25 | public final static Type TYPE = new Type("multiple-metric"); 26 | 27 | public Map metricsMap; 28 | public Map paramsMap; 29 | public Map countsMap; 30 | 31 | InternalMultipleMetric() {} // for serialization 32 | 33 | InternalMultipleMetric(String name, Map metricsMap, Map countsMap, 34 | List pipelineAggregators, Map metaData) { 35 | super(name, pipelineAggregators, metaData); 36 | this.metricsMap = metricsMap; 37 | this.countsMap = countsMap; 38 | 39 | this.paramsMap = new HashMap(); 40 | for (Map.Entry entry: metricsMap.entrySet()) 41 | this.paramsMap.put(entry.getKey(), 0.0); 42 | } 43 | 44 | InternalMultipleMetric(String name, Map metricsMap, Map paramsMap, Map countsMap, 45 | List pipelineAggregators, Map metaData) { 46 | super(name, pipelineAggregators, metaData); 47 | this.metricsMap = metricsMap; 48 | this.paramsMap = paramsMap; 49 | this.countsMap = countsMap; 50 | } 51 | 52 | @Override 53 | public Type type() { 54 | return TYPE; 55 | } 56 | 57 | public double getValue(String name) { 58 | return value(name); 59 | } 60 | 61 | public long getDocCount(String name) { 62 | return (countsMap.get(name) != null) ? countsMap.get(name) : 0; 63 | } 64 | 65 | @Override 66 | public double value(String name) { 67 | if (paramsMap.size() == 0) 68 | return 0.0; 69 | 70 | return (paramsMap.get(name) != null && !paramsMap.get(name).isNaN()) ? paramsMap.get(name) : 0.0; 71 | } 72 | 73 | @Override 74 | public InternalMultipleMetric doReduce(List aggregations, ReduceContext reduceContext) { 75 | InternalMultipleMetric reduced = null; 76 | 77 | if (aggregations.size() == 1) { 78 | reduced = (InternalMultipleMetric) aggregations.get(0); 79 | 80 | } else { 81 | 82 | for (InternalAggregation aggregation : aggregations) { 83 | if (reduced == null) { 84 | reduced = (InternalMultipleMetric) aggregation; 85 | } else { 86 | InternalMultipleMetric current = (InternalMultipleMetric) aggregation; 87 | for (Map.Entry entry: current.paramsMap.entrySet()) 88 | reduced.paramsMap.put(entry.getKey(), reduced.paramsMap.get(entry.getKey()) + entry.getValue()); 89 | 90 | for (Map.Entry entry: current.countsMap.entrySet()) 91 | reduced.countsMap.put(entry.getKey(), reduced.countsMap.get(entry.getKey()) + entry.getValue()); 92 | 93 | } 94 | } 95 | 96 | } 97 | 98 | if (reduced == null) 99 | reduced = (InternalMultipleMetric) aggregations.get(0); 100 | 101 | 102 | Map scriptedMap = new HashMap(); 103 | for (Map.Entry entry: metricsMap.entrySet()) { 104 | if (entry.getValue().isScript()) { 105 | MultipleMetricParam metric = entry.getValue(); 106 | 107 | if (reduced.paramsMap.size() == 0) { 108 | scriptedMap.put(entry.getKey(), 0.0); 109 | 110 | } else { 111 | Map scriptParamsMap = metric.scriptParams(); 112 | if (scriptParamsMap == null) 113 | scriptParamsMap = new HashMap(); 114 | scriptParamsMap.putAll(reduced.paramsMap); 115 | 116 | Script script = new Script(metric.script().getScript(), metric.script().getType(), metric.script().getLang(), scriptParamsMap); 117 | Double result = (Double)reduceContext.scriptService().executable(script, ScriptContext.Standard.AGGS, reduceContext, new HashMap()).run(); 118 | 119 | scriptedMap.put(entry.getKey(), result); 120 | } 121 | } 122 | } 123 | 124 | reduced.paramsMap.putAll(scriptedMap); 125 | 126 | return reduced; 127 | } 128 | 129 | @Override 130 | public void doReadFrom(StreamInput in) throws IOException { 131 | name = in.readString(); 132 | if (in.readBoolean()) { 133 | int n = in.readInt(); 134 | paramsMap = new HashMap(); 135 | for (int i = 0; i < n; i++) { 136 | String key = in.readString(); 137 | Double value = in.readDouble(); 138 | paramsMap.put(key, value); 139 | } 140 | } 141 | if (in.readBoolean()) { 142 | int n = in.readInt(); 143 | metricsMap = new HashMap(); 144 | for (int i = 0; i < n; i++) { 145 | String key = in.readString(); 146 | MultipleMetricParam value = MultipleMetricParam.readFrom(in); 147 | metricsMap.put(key, value); 148 | } 149 | } 150 | if (in.readBoolean()) { 151 | int n = in.readInt(); 152 | countsMap = new HashMap(); 153 | for (int i = 0; i < n; i++) { 154 | String key = in.readString(); 155 | Long value = in.readLong(); 156 | countsMap.put(key, value); 157 | } 158 | } 159 | } 160 | 161 | @Override 162 | public void doWriteTo(StreamOutput out) throws IOException { 163 | out.writeString(name); 164 | if (paramsMap != null) { 165 | out.writeBoolean(true); 166 | out.writeInt(paramsMap.size()); 167 | for (Map.Entry entry: paramsMap.entrySet()) { 168 | out.writeString(entry.getKey()); 169 | out.writeDouble(entry.getValue()); 170 | } 171 | } else 172 | out.writeBoolean(false); 173 | 174 | if (metricsMap != null) { 175 | out.writeBoolean(true); 176 | out.writeInt(metricsMap.size()); 177 | for (Map.Entry entry: metricsMap.entrySet()) { 178 | out.writeString(entry.getKey()); 179 | MultipleMetricParam.writeTo(entry.getValue(), out); 180 | } 181 | } else 182 | out.writeBoolean(false); 183 | 184 | if (countsMap != null) { 185 | out.writeBoolean(true); 186 | out.writeInt(countsMap.size()); 187 | for (Map.Entry entry: countsMap.entrySet()) { 188 | out.writeString(entry.getKey()); 189 | out.writeLong(entry.getValue()); 190 | } 191 | } else 192 | out.writeBoolean(false); 193 | 194 | } 195 | 196 | @Override 197 | public XContentBuilder doXContentBody(XContentBuilder builder, Params params) throws IOException { 198 | for (Map.Entry entry : metricsMap.entrySet()) { 199 | String metricName = entry.getKey(); 200 | builder.startObject(metricName); 201 | 202 | Double value = value(metricName); 203 | if (Double.isInfinite(value) || Double.isNaN(value)) 204 | value = null; 205 | builder.field("value", value); 206 | 207 | if (countsMap != null && !entry.getValue().isScript()) 208 | builder.field("doc_count", getDocCount(metricName)); 209 | 210 | builder.endObject(); 211 | } 212 | 213 | return builder; 214 | } 215 | 216 | 217 | 218 | public final static AggregationStreams.Stream STREAM = new AggregationStreams.Stream() { 219 | 220 | public InternalMultipleMetric readResult(StreamInput in) throws IOException { 221 | InternalMultipleMetric result = new InternalMultipleMetric(); 222 | result.readFrom(in); 223 | return result; 224 | } 225 | }; 226 | 227 | public static void registerStreams() { 228 | AggregationStreams.registerStream(STREAM, TYPE.stream()); 229 | } 230 | 231 | } 232 | -------------------------------------------------------------------------------- /src/main/java/fr/v3d/elasticsearch/search/aggregations/metrics/multiplemetric/MultipleMetric.java: -------------------------------------------------------------------------------- 1 | package fr.v3d.elasticsearch.search.aggregations.metrics.multiplemetric; 2 | 3 | public interface MultipleMetric { 4 | public double getValue(String name); 5 | public long getDocCount(String name); 6 | } 7 | -------------------------------------------------------------------------------- /src/main/java/fr/v3d/elasticsearch/search/aggregations/metrics/multiplemetric/MultipleMetricAggregator.java: -------------------------------------------------------------------------------- 1 | package fr.v3d.elasticsearch.search.aggregations.metrics.multiplemetric; 2 | 3 | import java.io.IOException; 4 | import java.util.HashMap; 5 | import java.util.List; 6 | import java.util.Map; 7 | import java.util.Map.Entry; 8 | 9 | import org.apache.lucene.index.LeafReaderContext; 10 | import org.apache.lucene.search.Weight; 11 | import org.apache.lucene.util.Bits; 12 | import org.elasticsearch.common.lease.Releasables; 13 | import org.elasticsearch.common.logging.ESLogger; 14 | import org.elasticsearch.common.logging.ESLoggerFactory; 15 | import org.elasticsearch.common.lucene.Lucene; 16 | import org.elasticsearch.common.util.BigArrays; 17 | import org.elasticsearch.common.util.DoubleArray; 18 | import org.elasticsearch.common.util.LongArray; 19 | import org.elasticsearch.index.fielddata.SortedBinaryDocValues; 20 | import org.elasticsearch.index.fielddata.SortedNumericDoubleValues; 21 | import org.elasticsearch.script.Script; 22 | import org.elasticsearch.script.ScriptContext; 23 | import org.elasticsearch.script.ScriptService; 24 | import org.elasticsearch.search.aggregations.Aggregator; 25 | import org.elasticsearch.search.aggregations.InternalAggregation; 26 | import org.elasticsearch.search.aggregations.LeafBucketCollector; 27 | import org.elasticsearch.search.aggregations.LeafBucketCollectorBase; 28 | import org.elasticsearch.search.aggregations.metrics.NumericMetricsAggregator; 29 | import org.elasticsearch.search.aggregations.pipeline.PipelineAggregator; 30 | import org.elasticsearch.search.aggregations.support.AggregationContext; 31 | import org.elasticsearch.search.aggregations.support.ValuesSource; 32 | import org.elasticsearch.search.aggregations.support.ValuesSourceConfig; 33 | 34 | import fr.v3d.elasticsearch.search.aggregations.support.MultipleValuesSourceAggregatorFactory; 35 | 36 | /** 37 | * 38 | */ 39 | public class MultipleMetricAggregator extends NumericMetricsAggregator.MultiValue { 40 | 41 | protected final static ESLogger logger = ESLoggerFactory.getLogger("test"); 42 | 43 | private final Map valuesSourceMap; 44 | private final Map weightMap = new HashMap(); 45 | 46 | private Map metricParamsMap; 47 | private Map metricValuesMap = new HashMap(); 48 | private Map metricCountsMap = new HashMap(); 49 | 50 | private ScriptService scriptService; 51 | 52 | 53 | 54 | public MultipleMetricAggregator(String name, Map valuesSourceMap, AggregationContext context, 55 | Aggregator parent, List pipelineAggregators, Map metaData, 56 | Map metricsMap) throws IOException { 57 | super(name, context, parent, pipelineAggregators, metaData); 58 | this.valuesSourceMap = valuesSourceMap; 59 | this.metricParamsMap = metricsMap; 60 | 61 | this.scriptService = context.searchContext().scriptService(); 62 | 63 | for (Map.Entry entry: valuesSourceMap.entrySet()) { 64 | ValuesSource valuesSource = entry.getValue(); 65 | String key = entry.getKey(); 66 | if (valuesSource != null) { 67 | metricValuesMap.put(key, context.bigArrays().newDoubleArray(1, true)); 68 | metricCountsMap.put(key, context.bigArrays().newLongArray(1, true)); 69 | } 70 | } 71 | 72 | 73 | for (String metricName: valuesSourceMap.keySet()) { 74 | weightMap.put(metricName, context.searchContext().searcher().createNormalizedWeight(metricParamsMap.get(metricName).filter(), false)); 75 | } 76 | } 77 | 78 | 79 | @Override 80 | public boolean needsScores() { 81 | return false; 82 | } 83 | 84 | @Override 85 | public LeafBucketCollector getLeafCollector(LeafReaderContext ctx, final LeafBucketCollector sub) throws IOException { 86 | 87 | if (valuesSourceMap == null) { 88 | return LeafBucketCollector.NO_OP_COLLECTOR; 89 | } 90 | 91 | final BigArrays bigArrays = context.bigArrays(); 92 | 93 | final Map doubleValuesMap = new HashMap(); 94 | final Map docValuesMap = new HashMap(); 95 | final Map bitsMap = new HashMap(); 96 | 97 | for (Entry entry: valuesSourceMap.entrySet()) { 98 | String key = entry.getKey(); 99 | Bits bits = Lucene.asSequentialAccessBits(ctx.reader().maxDoc(), weightMap.get(key).scorer(ctx)); 100 | bitsMap.put(key, bits); 101 | 102 | if (metricParamsMap.get(key).operator().equals(MultipleMetricParser.COUNT_OPERATOR)) { 103 | SortedBinaryDocValues values = ( entry.getValue() != null) ? entry.getValue().bytesValues(ctx) : null; 104 | docValuesMap.put(key, values); 105 | } else { 106 | SortedNumericDoubleValues values = ( entry.getValue() != null) 107 | ? ((ValuesSource.Numeric)entry.getValue()).doubleValues(ctx) 108 | : null; 109 | doubleValuesMap.put(key, values); 110 | } 111 | } 112 | 113 | return new LeafBucketCollectorBase(sub, null) { 114 | @Override 115 | public void collect(int doc, long bucket) throws IOException { 116 | 117 | for (Entry entry: doubleValuesMap.entrySet() ) { 118 | String key = entry.getKey(); 119 | SortedNumericDoubleValues values = entry.getValue(); 120 | if (values != null && bitsMap.get(key).get(doc)) { 121 | values.setDocument(doc); 122 | metricValuesMap.put(key, bigArrays.grow(metricValuesMap.get(key), bucket + 1)); 123 | double increment = 0; 124 | for (int i = 0; i < values.count(); i++) 125 | increment += values.valueAt(i); 126 | metricValuesMap.get(key).increment(bucket, increment); 127 | 128 | metricCountsMap.put(key, bigArrays.grow(metricCountsMap.get(key), bucket + 1)); 129 | metricCountsMap.get(key).increment(bucket, 1); 130 | } 131 | } 132 | 133 | for (Entry entry: docValuesMap.entrySet() ) { 134 | String key = entry.getKey(); 135 | SortedBinaryDocValues values = entry.getValue(); 136 | if (values != null && bitsMap.get(key).get(doc)) { 137 | values.setDocument(doc); 138 | metricValuesMap.put(key, bigArrays.grow(metricValuesMap.get(key), bucket + 1)); 139 | metricValuesMap.get(key).increment(bucket, values.count()); 140 | 141 | metricCountsMap.put(key, bigArrays.grow(metricCountsMap.get(key), bucket + 1)); 142 | metricCountsMap.get(key).increment(bucket, 1); 143 | } 144 | } 145 | } 146 | }; 147 | } 148 | 149 | private Double getMetricValue(String name, long owningBucketOrdinal) { 150 | return metricValuesMap.containsKey(name) && metricValuesMap.get(name).size() > owningBucketOrdinal 151 | ? metricValuesMap.get(name).get(owningBucketOrdinal) 152 | : 0.0; 153 | } 154 | 155 | private HashMap getScriptParamsMap(long owningBucketOrdinal) { 156 | HashMap scriptParamsMap = new HashMap(); 157 | for (Map.Entry entry: metricParamsMap.entrySet()) 158 | if (!entry.getValue().isScript()) 159 | scriptParamsMap.put(entry.getKey(), getMetricValue(entry.getKey(), owningBucketOrdinal)); 160 | 161 | 162 | return scriptParamsMap; 163 | } 164 | 165 | private Map getCountsMap(long owningBucketOrdinal) { 166 | HashMap countsMap = new HashMap(); 167 | for (Map.Entry entry: metricCountsMap.entrySet()) { 168 | long count = (owningBucketOrdinal >= entry.getValue().size()) 169 | ? 0L 170 | : entry.getValue().get(owningBucketOrdinal); 171 | countsMap.put(entry.getKey(), count); 172 | } 173 | 174 | return countsMap; 175 | } 176 | 177 | private Map getEmptyCountsMap() { 178 | HashMap countsMap = new HashMap(); 179 | for (Map.Entry entry: metricCountsMap.entrySet()) 180 | countsMap.put(entry.getKey(), 0L); 181 | 182 | return countsMap; 183 | } 184 | 185 | @Override 186 | public boolean hasMetric(String name) { 187 | return metricParamsMap.containsKey(name); 188 | } 189 | 190 | @Override 191 | public double metric(String name, long owningBucketOrdinal) { 192 | Double result; 193 | MultipleMetricParam metric = metricParamsMap.get(name); 194 | if (metric.isScript()) { 195 | Map scriptParamsMap = metric.scriptParams(); 196 | if (scriptParamsMap == null) 197 | scriptParamsMap = new HashMap(); 198 | scriptParamsMap.putAll(getScriptParamsMap(owningBucketOrdinal)); 199 | 200 | Script script = new Script(metric.script().getScript(), metric.script().getType(), metric.script().getLang(), scriptParamsMap); 201 | result = (Double)scriptService.executable(script, ScriptContext.Standard.AGGS, context.searchContext(), new HashMap()).run(); 202 | } else { 203 | result = getMetricValue(name, owningBucketOrdinal); 204 | } 205 | 206 | return result; 207 | } 208 | 209 | @Override 210 | public InternalAggregation buildAggregation(long owningBucketOrdinal) { 211 | 212 | HashMap scriptParamsMap = getScriptParamsMap(owningBucketOrdinal); 213 | Map countsMap = getCountsMap(owningBucketOrdinal); 214 | 215 | return new InternalMultipleMetric(name, metricParamsMap, scriptParamsMap, countsMap, pipelineAggregators(), metaData()); 216 | } 217 | 218 | @Override 219 | public InternalAggregation buildEmptyAggregation() { 220 | return new InternalMultipleMetric(name, metricParamsMap, getEmptyCountsMap(), pipelineAggregators(), metaData()); 221 | } 222 | 223 | public static class Factory extends MultipleValuesSourceAggregatorFactory.LeafOnly { 224 | public Map metricsMap; 225 | 226 | public Factory(String name, Map> valueSourceConfigMap, 227 | Map metricsMap) { 228 | super(name, InternalMultipleMetric.TYPE.name(), valueSourceConfigMap); 229 | this.metricsMap = metricsMap; 230 | } 231 | 232 | @Override 233 | protected Aggregator doCreateInternal(Map valuesSourceMap, 234 | AggregationContext aggregationContext, Aggregator parent, boolean collectsFromSingleBucket, 235 | List pipelineAggregators, Map metaData) throws IOException { 236 | return new MultipleMetricAggregator(name, valuesSourceMap, aggregationContext, parent, pipelineAggregators, metaData, this.metricsMap); 237 | } 238 | } 239 | 240 | @Override 241 | public void doClose() { 242 | for (Map.Entry entry: metricValuesMap.entrySet()) 243 | Releasables.close(entry.getValue()); 244 | for (Map.Entry entry: metricCountsMap.entrySet()) 245 | Releasables.close(entry.getValue()); 246 | 247 | metricValuesMap = null; 248 | metricParamsMap = null; 249 | } 250 | 251 | } 252 | -------------------------------------------------------------------------------- /src/main/java/fr/v3d/elasticsearch/search/aggregations/metrics/multiplemetric/MultipleMetricBuilder.java: -------------------------------------------------------------------------------- 1 | package fr.v3d.elasticsearch.search.aggregations.metrics.multiplemetric; 2 | 3 | import java.io.IOException; 4 | import java.util.ArrayList; 5 | import java.util.List; 6 | 7 | import org.elasticsearch.common.xcontent.ToXContent; 8 | import org.elasticsearch.common.xcontent.XContentBuilder; 9 | import org.elasticsearch.search.aggregations.AggregationBuilder; 10 | 11 | public class MultipleMetricBuilder extends AggregationBuilder { 12 | 13 | 14 | private List metrics = new ArrayList(10); 15 | 16 | public MultipleMetricBuilder(String name) { 17 | super(name, InternalMultipleMetric.TYPE.name()); 18 | } 19 | 20 | public MultipleMetricBuilder field(FieldBuilder fieldBuilder) { 21 | this.metrics.add(fieldBuilder); 22 | return this; 23 | } 24 | 25 | public MultipleMetricBuilder script(ScriptBuilder scriptBuilder) { 26 | this.metrics.add(scriptBuilder); 27 | return this; 28 | } 29 | 30 | 31 | @Override 32 | protected XContentBuilder internalXContent(XContentBuilder builder, Params params) throws IOException { 33 | builder.startObject(); 34 | for (ToXContent toXContent: this.metrics) 35 | toXContent.toXContent(builder, params); 36 | 37 | return builder.endObject(); 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/fr/v3d/elasticsearch/search/aggregations/metrics/multiplemetric/MultipleMetricParam.java: -------------------------------------------------------------------------------- 1 | package fr.v3d.elasticsearch.search.aggregations.metrics.multiplemetric; 2 | 3 | import java.io.IOException; 4 | import java.util.HashMap; 5 | import java.util.Map; 6 | 7 | import org.apache.lucene.search.MatchAllDocsQuery; 8 | import org.apache.lucene.search.Query; 9 | import org.elasticsearch.common.ParseField; 10 | import org.elasticsearch.common.io.stream.StreamInput; 11 | import org.elasticsearch.common.io.stream.StreamOutput; 12 | import org.elasticsearch.common.xcontent.XContentParser; 13 | import org.elasticsearch.index.query.ParsedQuery; 14 | import org.elasticsearch.script.Script; 15 | import org.elasticsearch.script.ScriptParameterParser; 16 | import org.elasticsearch.script.ScriptParameterParser.ScriptParameterValue; 17 | import org.elasticsearch.search.SearchParseException; 18 | import org.elasticsearch.search.aggregations.metrics.sum.InternalSum; 19 | import org.elasticsearch.search.aggregations.metrics.valuecount.InternalValueCount; 20 | import org.elasticsearch.search.aggregations.support.ValuesSource; 21 | import org.elasticsearch.search.aggregations.support.ValuesSourceParser; 22 | import org.elasticsearch.search.internal.SearchContext; 23 | 24 | public class MultipleMetricParam { 25 | 26 | public static final String SUM_TOKEN = "sum"; 27 | public static final String COUNT_TOKEN = "count"; 28 | public static final String FILTER_TOKEN = "filter"; 29 | public static final String SCRIPT_TOKEN = "script"; 30 | public static final String PARAMS_TOKEN = "params"; 31 | public static final ParseField SUM_FIELD = new ParseField("sum"); 32 | public static final ParseField COUNT_FIELD = new ParseField("count"); 33 | public static final ParseField FILTER_FIELD = new ParseField("filter"); 34 | public static final ParseField SCRIPT_FIELD = new ParseField("script"); 35 | public static final ParseField PARAMS_FIELD = new ParseField("params"); 36 | 37 | private String operator; 38 | private Query filter; 39 | private Script script; 40 | private Map scriptParams; 41 | private ValuesSourceParser vsParser = null; 42 | 43 | public MultipleMetricParam() {} // for serialization 44 | 45 | public MultipleMetricParam(ValuesSourceParser vsParser, String operator, Query filter, 46 | Script script, Map scriptParams) { 47 | this.vsParser = vsParser; 48 | this.operator = operator; 49 | this.filter = filter; 50 | this.script = script; 51 | this.scriptParams = scriptParams; 52 | } 53 | 54 | public MultipleMetricParam(ValuesSourceParser vsParser, String operator, ParsedQuery parsedFilter, 55 | Script script, Map scriptParams) { 56 | this(vsParser, operator, parsedFilter == null ? new MatchAllDocsQuery() : parsedFilter.query(), 57 | script, scriptParams); 58 | } 59 | 60 | public ValuesSourceParser vsParser() { 61 | return vsParser; 62 | } 63 | 64 | public String operator() { 65 | return operator; 66 | } 67 | 68 | public Query filter() { 69 | return filter; 70 | } 71 | 72 | public Script script() { 73 | return script; 74 | } 75 | 76 | public Map scriptParams() { 77 | return scriptParams; 78 | } 79 | 80 | public boolean isScript() { 81 | return (this.script != null); 82 | } 83 | 84 | public static MultipleMetricParam parse(String aggregationName, XContentParser parser, SearchContext context, String metricName) throws IOException { 85 | 86 | XContentParser.Token token; 87 | String currentFieldName = null; 88 | 89 | String operator = null; 90 | ValuesSourceParser vsParser = null; 91 | ParsedQuery parsedFilter = null; 92 | 93 | ScriptParameterParser scriptParameterParser = new ScriptParameterParser(); 94 | Map scriptParams = null; 95 | Script script = null; 96 | 97 | 98 | while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { 99 | 100 | if (token == XContentParser.Token.FIELD_NAME) { 101 | currentFieldName = parser.currentName(); 102 | } else if (token == XContentParser.Token.VALUE_STRING) { 103 | if (context.parseFieldMatcher().match(currentFieldName, SCRIPT_FIELD)) { 104 | if (!scriptParameterParser.token(currentFieldName, token, parser, context.parseFieldMatcher())) { 105 | throw new SearchParseException(context, "Unknown key for a " + token + " in [" + aggregationName + "]: [" + currentFieldName + "].", parser.getTokenLocation()); 106 | 107 | } else { 108 | ScriptParameterValue scriptValue = scriptParameterParser.getScriptParameterValue(SCRIPT_TOKEN); 109 | if (scriptValue != null) { 110 | script = new Script(scriptValue.script(), scriptValue.scriptType(), scriptParameterParser.lang(), null); 111 | } 112 | } 113 | } 114 | 115 | } else if (token == XContentParser.Token.START_OBJECT) { 116 | if (context.parseFieldMatcher().match(currentFieldName, SUM_FIELD)) { 117 | operator = currentFieldName; 118 | vsParser = parseSum(aggregationName, parser, context); //ValuesSourceParser 119 | 120 | } else if (context.parseFieldMatcher().match(currentFieldName, COUNT_FIELD)) { 121 | operator = currentFieldName; 122 | vsParser = parseCount(aggregationName, parser, context); 123 | 124 | } else if (context.parseFieldMatcher().match(currentFieldName, FILTER_FIELD)) { 125 | parsedFilter = context.queryParserService().parseInnerFilter(parser); 126 | 127 | } else if (context.parseFieldMatcher().match(currentFieldName, SCRIPT_FIELD)) { 128 | script = Script.parse(parser, context.parseFieldMatcher()); 129 | 130 | } else if (context.parseFieldMatcher().match(currentFieldName, PARAMS_FIELD)) { 131 | scriptParams = parser.map(); 132 | } 133 | } 134 | } 135 | 136 | if (script == null && vsParser == null) 137 | throw new SearchParseException(context, "Metric [" + metricName + "] in [" + aggregationName + "] must either define a field or a script.", parser.getTokenLocation()); 138 | 139 | if (script == null && vsParser != null && operator == null) 140 | throw new SearchParseException(context, "Metric [" + metricName + "] in [" + aggregationName + "] must define an aggregator.", parser.getTokenLocation()); 141 | 142 | if (operator != null && !MultipleMetricParser.isValidOperator(operator)) 143 | throw new SearchParseException(context, "Metric [" + metricName + "] in [" + aggregationName + "] define a non valid aggregator: [" + operator + "].", parser.getTokenLocation()); 144 | 145 | return new MultipleMetricParam(vsParser, operator, parsedFilter, script, scriptParams); 146 | } 147 | 148 | public static ValuesSourceParser parseSum(String aggregationName, XContentParser parser, SearchContext context) 149 | throws IOException { 150 | 151 | ValuesSourceParser vsParser = ValuesSourceParser.numeric(aggregationName, InternalSum.TYPE, context) 152 | .build(); 153 | 154 | XContentParser.Token token; 155 | String currentFieldName = null; 156 | while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { 157 | if (token == XContentParser.Token.FIELD_NAME) { 158 | currentFieldName = parser.currentName(); 159 | } else if (!vsParser.token(currentFieldName, token, parser)) { 160 | throw new SearchParseException(context, "Unexpected token " + token + " in [" + aggregationName + "].", parser.getTokenLocation()); 161 | } 162 | } 163 | 164 | return vsParser; 165 | } 166 | 167 | public static ValuesSourceParser parseCount(String aggregationName, XContentParser parser, SearchContext context) 168 | throws IOException { 169 | 170 | ValuesSourceParser vsParser = ValuesSourceParser.any(aggregationName, InternalValueCount.TYPE, context) 171 | .build(); 172 | 173 | XContentParser.Token token; 174 | String currentFieldName = null; 175 | while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { 176 | if (token == XContentParser.Token.FIELD_NAME) { 177 | currentFieldName = parser.currentName(); 178 | } else if (!vsParser.token(currentFieldName, token, parser)) { 179 | throw new SearchParseException(context, "Unexpected token " + token + " in [" + aggregationName + "].", parser.getTokenLocation()); 180 | } 181 | } 182 | 183 | return vsParser; 184 | } 185 | 186 | public static MultipleMetricParam readFrom(StreamInput in) throws IOException { 187 | MultipleMetricParam metric = new MultipleMetricParam(); 188 | boolean hasScript = in.readBoolean(); 189 | if (hasScript) 190 | metric.script = Script.readScript(in); 191 | 192 | boolean hasScriptParams = in.readBoolean(); 193 | if (hasScriptParams) { 194 | int size = in.readInt(); 195 | metric.scriptParams = new HashMap(size); 196 | for (int i=0; i entry: metric.scriptParams.entrySet()) { 217 | out.writeString(entry.getKey()); 218 | out.writeGenericValue(entry.getValue()); 219 | } 220 | } else 221 | out.writeBoolean(false); 222 | } 223 | } 224 | -------------------------------------------------------------------------------- /src/main/java/fr/v3d/elasticsearch/search/aggregations/metrics/multiplemetric/MultipleMetricParser.java: -------------------------------------------------------------------------------- 1 | package fr.v3d.elasticsearch.search.aggregations.metrics.multiplemetric; 2 | 3 | import java.io.IOException; 4 | import java.util.HashMap; 5 | import java.util.Map; 6 | 7 | import org.elasticsearch.common.xcontent.XContentParser; 8 | import org.elasticsearch.search.SearchParseException; 9 | import org.elasticsearch.search.aggregations.Aggregator; 10 | import org.elasticsearch.search.aggregations.AggregatorFactory; 11 | import org.elasticsearch.search.aggregations.support.ValuesSource; 12 | import org.elasticsearch.search.aggregations.support.ValuesSourceConfig; 13 | import org.elasticsearch.search.internal.SearchContext; 14 | 15 | /** 16 | * 17 | */ 18 | public class MultipleMetricParser implements Aggregator.Parser { 19 | 20 | public static final String PARAMS_TOKEN = "params"; 21 | 22 | public static final String SUM_OPERATOR = "sum"; 23 | public static final String COUNT_OPERATOR = "count"; 24 | 25 | public String type() { 26 | return InternalMultipleMetric.TYPE.name(); 27 | } 28 | 29 | public static boolean isValidOperator(String operator) { 30 | return (operator.equals(SUM_OPERATOR) || operator.equals(COUNT_OPERATOR)); 31 | } 32 | 33 | public AggregatorFactory parse(String aggregationName, XContentParser parser, SearchContext context) throws IOException { 34 | 35 | XContentParser.Token token; 36 | String currentFieldName = null; 37 | Map metricsMap = new HashMap(); 38 | Map> configMap = new HashMap>(); 39 | 40 | while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { 41 | if (token == XContentParser.Token.FIELD_NAME) { 42 | currentFieldName = parser.currentName(); 43 | 44 | } else if (token == XContentParser.Token.START_OBJECT) { 45 | MultipleMetricParam metric = MultipleMetricParam.parse(aggregationName, parser, context, currentFieldName); 46 | metricsMap.put(currentFieldName, metric); 47 | if (!metric.isScript()) 48 | configMap.put(currentFieldName, metric.vsParser().config()); 49 | } else { 50 | throw new SearchParseException(context, "Unexpected token " + token + " in [" + aggregationName + "]", parser.getTokenLocation()); 51 | } 52 | } 53 | 54 | 55 | 56 | return new MultipleMetricAggregator.Factory(aggregationName, configMap, metricsMap); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/main/java/fr/v3d/elasticsearch/search/aggregations/metrics/multiplemetric/ScriptBuilder.java: -------------------------------------------------------------------------------- 1 | package fr.v3d.elasticsearch.search.aggregations.metrics.multiplemetric; 2 | 3 | import java.io.IOException; 4 | import java.util.HashMap; 5 | import java.util.Map; 6 | 7 | import org.elasticsearch.common.xcontent.ToXContent; 8 | import org.elasticsearch.common.xcontent.XContentBuilder; 9 | import org.elasticsearch.script.Script; 10 | 11 | public class ScriptBuilder implements ToXContent { 12 | 13 | private String name; 14 | 15 | private Script script; 16 | private Map params; 17 | 18 | public ScriptBuilder(String name) { 19 | this.name = name; 20 | } 21 | 22 | public ScriptBuilder script(Script script) { 23 | this.script = script; 24 | return this; 25 | } 26 | 27 | public ScriptBuilder params(Map params) { 28 | if (this.params == null) { 29 | this.params = params; 30 | } else { 31 | this.params.putAll(params); 32 | } 33 | return this; 34 | } 35 | 36 | public ScriptBuilder param(String name, Object value) { 37 | if (this.params == null) { 38 | this.params = new HashMap(); 39 | } 40 | this.params.put(name, value); 41 | return this; 42 | } 43 | 44 | public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { 45 | builder 46 | .startObject(name); 47 | 48 | if (script != null) { 49 | builder.field(MultipleMetricParam.SCRIPT_FIELD.getPreferredName(), script); 50 | } 51 | 52 | if (this.params != null && !this.params.isEmpty()) { 53 | builder 54 | .field(MultipleMetricParam.PARAMS_FIELD.getPreferredName()) 55 | .map(this.params); 56 | } 57 | 58 | return builder.endObject(); 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /src/main/java/fr/v3d/elasticsearch/search/aggregations/metrics/multiplemetric/SumBuilder.java: -------------------------------------------------------------------------------- 1 | package fr.v3d.elasticsearch.search.aggregations.metrics.multiplemetric; 2 | 3 | public class SumBuilder extends FieldBuilder { 4 | 5 | public SumBuilder(String name) { 6 | super(name, MultipleMetricParser.SUM_OPERATOR); 7 | } 8 | 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/fr/v3d/elasticsearch/search/aggregations/support/MultipleValuesSourceAggregatorFactory.java: -------------------------------------------------------------------------------- 1 | package fr.v3d.elasticsearch.search.aggregations.support; 2 | 3 | import java.io.IOException; 4 | import java.util.HashMap; 5 | import java.util.List; 6 | import java.util.Map; 7 | import java.util.Map.Entry; 8 | 9 | import org.elasticsearch.search.aggregations.AggregationExecutionException; 10 | import org.elasticsearch.search.aggregations.AggregationInitializationException; 11 | import org.elasticsearch.search.aggregations.Aggregator; 12 | import org.elasticsearch.search.aggregations.AggregatorFactories; 13 | import org.elasticsearch.search.aggregations.AggregatorFactory; 14 | import org.elasticsearch.search.aggregations.pipeline.PipelineAggregator; 15 | import org.elasticsearch.search.aggregations.support.AggregationContext; 16 | import org.elasticsearch.search.aggregations.support.ValuesSource; 17 | import org.elasticsearch.search.aggregations.support.ValuesSourceConfig; 18 | 19 | public abstract class MultipleValuesSourceAggregatorFactory extends AggregatorFactory { 20 | 21 | public static abstract class LeafOnly extends MultipleValuesSourceAggregatorFactory { 22 | 23 | protected LeafOnly(String name, String type, Map> valuesSourceConfigMap) { 24 | super(name, type, valuesSourceConfigMap); 25 | } 26 | 27 | @Override 28 | public AggregatorFactory subFactories(AggregatorFactories subFactories) { 29 | throw new AggregationInitializationException("Aggregator [" + name + "] of type [" + type + "] cannot accept sub-aggregations"); 30 | } 31 | } 32 | 33 | protected Map> configMap; 34 | 35 | protected MultipleValuesSourceAggregatorFactory(String name, String type, Map> configMap) { 36 | super(name, type); 37 | this.configMap = configMap; 38 | } 39 | 40 | @Override 41 | public Aggregator createInternal(AggregationContext context, Aggregator parent, boolean collectsFromSingleBucket, 42 | List pipelineAggregators, Map metaData) throws IOException { 43 | Map vsMap = new HashMap(); 44 | for (Entry> entry: this.configMap.entrySet()) { 45 | ValuesSourceConfig config = entry.getValue(); 46 | VS vs = !config.unmapped() ? context.valuesSource(config, context.searchContext()) : null; 47 | vsMap.put(entry.getKey(), vs); 48 | } 49 | return doCreateInternal(vsMap, context, parent, collectsFromSingleBucket, pipelineAggregators, metaData); 50 | } 51 | 52 | protected abstract Aggregator doCreateInternal(Map valuesSourceMap, AggregationContext aggregationContext, Aggregator parent, 53 | boolean collectsFromSingleBucket, List pipelineAggregators, Map metaData) 54 | throws IOException; 55 | 56 | @Override 57 | public void doValidate() { 58 | for (ValuesSourceConfig config: configMap.values()) { 59 | if (config == null || !config.valid()) { 60 | throw new AggregationExecutionException("could not find the appropriate value context to perform aggregation [" + name + "]"); 61 | } 62 | } 63 | } 64 | 65 | } -------------------------------------------------------------------------------- /src/main/resources/es-plugin.properties: -------------------------------------------------------------------------------- 1 | plugin=${elasticsearch.plugin.classname} 2 | version=${project.version} -------------------------------------------------------------------------------- /src/test/java/fr/v3d/elasticsearch/plugin/multiplemetric/MultipleMetricAggregationTestCase.java: -------------------------------------------------------------------------------- 1 | package fr.v3d.elasticsearch.plugin.multiplemetric; 2 | 3 | 4 | import java.util.*; 5 | 6 | import org.elasticsearch.common.logging.ESLogger; 7 | import org.elasticsearch.common.logging.ESLoggerFactory; 8 | import org.elasticsearch.common.settings.Settings; 9 | import org.elasticsearch.plugins.Plugin; 10 | import org.elasticsearch.script.groovy.GroovyPlugin; 11 | import org.elasticsearch.test.ESIntegTestCase; 12 | 13 | public class MultipleMetricAggregationTestCase extends ESIntegTestCase { 14 | 15 | 16 | protected final static ESLogger logger = ESLoggerFactory.getLogger("test"); 17 | 18 | @Override 19 | protected Settings nodeSettings(int nodeOrdinal) { 20 | return Settings.settingsBuilder() 21 | .put(super.nodeSettings(nodeOrdinal)) 22 | .put( "script.inline", "true") 23 | .put( "script.indexed", "true") 24 | .put( "script.file", "true") 25 | .put( "script.engine.groovy.inline.search", "true") 26 | .build(); 27 | } 28 | 29 | @Override 30 | protected Collection> nodePlugins() { 31 | return pluginList(GroovyPlugin.class, MultipleMetricPlugin.class); 32 | } 33 | 34 | public void createIndex(int numberOfShards, String indexName) { 35 | client().admin().indices() 36 | .prepareCreate(indexName) 37 | .setSettings(Settings.settingsBuilder().put("index.number_of_shards", numberOfShards)) 38 | .execute().actionGet(); 39 | } 40 | 41 | public void deleteIndex(String indexName) { 42 | try { 43 | client().admin().indices() 44 | .prepareDelete(indexName) 45 | .execute().actionGet(); 46 | } catch (Exception e) { /* ignoring */ } 47 | } 48 | 49 | public void buildTestDataset(int numberOfShards, String indexName, String typeName, int size, Map termsFactor) { 50 | deleteIndex(indexName); 51 | createIndex(numberOfShards, indexName); 52 | 53 | for (int i=0; i < size; i++) { 54 | for (Map.Entry entry: termsFactor.entrySet()) { 55 | for (int j = 0; j < 10; j++) { 56 | Map doc = new HashMap(); 57 | doc.put("date", new GregorianCalendar(2015, 10, 1).getTime()); 58 | doc.put("field0", entry.getKey()); 59 | doc.put("value1", j * entry.getValue()); 60 | doc.put("value2", j * 10 * entry.getValue()); 61 | client().prepareIndex(indexName, typeName, ("doc"+entry.getKey()+i)+j).setSource(doc).setRefresh(true).execute().actionGet(); 62 | } 63 | } 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/test/java/fr/v3d/elasticsearch/plugin/multiplemetric/MultipleMetricAggregatorTest.java: -------------------------------------------------------------------------------- 1 | package fr.v3d.elasticsearch.plugin.multiplemetric; 2 | 3 | import fr.v3d.elasticsearch.search.aggregations.metrics.multiplemetric.CountBuilder; 4 | import fr.v3d.elasticsearch.search.aggregations.metrics.multiplemetric.MultipleMetric; 5 | import fr.v3d.elasticsearch.search.aggregations.metrics.multiplemetric.MultipleMetricBuilder; 6 | import fr.v3d.elasticsearch.search.aggregations.metrics.multiplemetric.ScriptBuilder; 7 | import fr.v3d.elasticsearch.search.aggregations.metrics.multiplemetric.SumBuilder; 8 | 9 | import static org.elasticsearch.index.query.QueryBuilders.matchAllQuery; 10 | import static org.elasticsearch.index.query.QueryBuilders.termQuery; 11 | 12 | import java.util.HashMap; 13 | import java.util.Map; 14 | 15 | import org.elasticsearch.action.admin.cluster.node.info.NodesInfoResponse; 16 | import org.elasticsearch.action.search.SearchResponse; 17 | import org.elasticsearch.index.query.RangeQueryBuilder; 18 | import org.elasticsearch.index.query.TermQueryBuilder; 19 | import org.elasticsearch.plugins.PluginInfo; 20 | import org.elasticsearch.script.Script; 21 | import org.elasticsearch.search.aggregations.bucket.histogram.DateHistogramBuilder; 22 | import org.elasticsearch.search.aggregations.bucket.histogram.DateHistogramInterval; 23 | import org.elasticsearch.search.aggregations.bucket.histogram.Histogram; 24 | import org.elasticsearch.search.aggregations.bucket.range.Range; 25 | import org.elasticsearch.search.aggregations.bucket.range.RangeBuilder; 26 | import org.elasticsearch.search.aggregations.bucket.terms.Terms; 27 | import org.elasticsearch.search.aggregations.bucket.terms.Terms.Order; 28 | import org.elasticsearch.search.aggregations.bucket.terms.TermsBuilder; 29 | import org.junit.Test; 30 | 31 | public class MultipleMetricAggregatorTest extends MultipleMetricAggregationTestCase { 32 | 33 | @Test 34 | public void assertPluginLoaded() { 35 | NodesInfoResponse nodesInfoResponse = client().admin().cluster().prepareNodesInfo() 36 | .clear().setPlugins(true).get(); 37 | logger.info("{}", nodesInfoResponse); 38 | assertNotNull(nodesInfoResponse.getNodes()[0].getPlugins().getPluginInfos()); 39 | boolean hasGroovy = false; 40 | boolean hasMultipleMetricAggregation = false; 41 | for (PluginInfo info: nodesInfoResponse.getNodes()[0].getPlugins().getPluginInfos()) { 42 | if (info.getName().equals("lang-groovy")) 43 | hasGroovy = true; 44 | if (info.getName().equals("multiple-metric-aggregation")) 45 | hasMultipleMetricAggregation = true; 46 | } 47 | assertTrue(hasGroovy); 48 | assertTrue(hasMultipleMetricAggregation); 49 | } 50 | 51 | @Test 52 | public void assertMultipleMetricAggregation() { 53 | String indexName = "test1"; 54 | int size = 1; 55 | 56 | Map termsFactor = new HashMap(); 57 | termsFactor.put("foo", 1); 58 | 59 | buildTestDataset(1, indexName, "type1", size, termsFactor); 60 | 61 | SearchResponse searchResponse = client().prepareSearch(indexName) 62 | .setQuery(matchAllQuery()) 63 | .addAggregation(new MultipleMetricBuilder("metrics") 64 | .script(new ScriptBuilder("ratio").script(new Script("value1 / value2"))) 65 | .field(new SumBuilder("value1").field("value1")) 66 | .field(new CountBuilder("value2").field("value2"))) 67 | .execute().actionGet(); 68 | 69 | MultipleMetric metrics = searchResponse.getAggregations().get("metrics"); 70 | assertEquals(metrics.getValue("value1"), 45.0 * size, 0.0); 71 | assertEquals(metrics.getValue("value2"), 10.0 * size, 0.0); 72 | assertEquals(metrics.getValue("ratio"), metrics.getValue("value1") / metrics.getValue("value2"), 0.0); 73 | 74 | assertEquals(metrics.getDocCount("value1"), 10); 75 | assertEquals(metrics.getDocCount("value2"), 10); 76 | } 77 | 78 | 79 | @Test 80 | public void assertMultipleMetricAggregationWithEmptyFilter() { 81 | String indexName = "test1"; 82 | int size = 1; 83 | 84 | Map termsFactor = new HashMap(); 85 | termsFactor.put("foo", 1); 86 | termsFactor.put("bar", 1); 87 | 88 | buildTestDataset(1, indexName, "type1", size, termsFactor); 89 | 90 | TermsBuilder termsBuilder = new TermsBuilder("group_by") 91 | .field("field0") 92 | .subAggregation(new MultipleMetricBuilder("metrics") 93 | .field(new SumBuilder("value1") 94 | .field("value1") 95 | .filter(new RangeQueryBuilder("value1").lt(0)) 96 | ) 97 | .field(new CountBuilder("value1c") 98 | .field("value1") 99 | ) 100 | ); 101 | 102 | SearchResponse searchResponse = client().prepareSearch(indexName) 103 | .setQuery(matchAllQuery()) //termQuery("field0", "foo") 104 | .addAggregation(termsBuilder) 105 | .execute().actionGet(); 106 | 107 | Terms terms = searchResponse.getAggregations().get("group_by"); 108 | for (Map.Entry entry: termsFactor.entrySet()) { 109 | String term = entry.getKey(); 110 | assertNotNull(terms.getBucketByKey(term)); 111 | assertNotNull(terms.getBucketByKey(term).getAggregations()); 112 | assertNotNull(terms.getBucketByKey(term).getAggregations().get("metrics")); 113 | 114 | MultipleMetric metrics = terms.getBucketByKey(term).getAggregations().get("metrics"); 115 | assertEquals(metrics.getValue("value1"), 0.0, 0.0); 116 | assertEquals(metrics.getDocCount("value1"), 0); 117 | assertEquals(metrics.getValue("value1c"), 10.0, 0.0); 118 | assertEquals(metrics.getDocCount("value1c"), 10); 119 | } 120 | } 121 | 122 | 123 | @Test 124 | public void assertMultipleMetricAggregationWithParentRangeAggregation() { 125 | String indexName = "test1"; 126 | int size = 1; 127 | 128 | Map termsFactor = new HashMap(); 129 | termsFactor.put("foo", 1); 130 | termsFactor.put("bar", 1); 131 | 132 | buildTestDataset(1, indexName, "type1", size, termsFactor); 133 | 134 | RangeBuilder rangeBuilder = new RangeBuilder("ranges") 135 | .addRange(0, 100).addRange(100, 200).addRange(200, 300).addRange(300, 400).addRange(400, 500) 136 | .field("value2") 137 | .subAggregation(new MultipleMetricBuilder("metrics") 138 | .script(new ScriptBuilder("ratio").script(new Script("value1 / value1c"))) 139 | .field(new SumBuilder("value1") 140 | .field("value1") 141 | ) 142 | .field(new CountBuilder("value1c") 143 | .field("value1") 144 | ) 145 | ); 146 | 147 | DateHistogramBuilder dateHistogramBuilder = new DateHistogramBuilder("dates") 148 | .interval(DateHistogramInterval.DAY) 149 | .field("date") 150 | .subAggregation(rangeBuilder); 151 | 152 | 153 | SearchResponse searchResponse = client().prepareSearch(indexName) 154 | .setQuery(matchAllQuery()) //termQuery("field0", "foo") 155 | .addAggregation(dateHistogramBuilder) 156 | .execute().actionGet(); 157 | 158 | Histogram dates = searchResponse.getAggregations().get("dates"); 159 | for (Histogram.Bucket date: dates.getBuckets() ) { 160 | Range buckets = date.getAggregations().get("ranges"); 161 | 162 | for (Range.Bucket bucket : buckets.getBuckets()) { 163 | MultipleMetric metrics = bucket.getAggregations().get("metrics"); 164 | if (bucket.getFromAsString().equals("0.0") && bucket.getToAsString().equals("100.0")) { 165 | assertEquals(90.0, metrics.getValue("value1"), 0.0); 166 | assertEquals(20.0, metrics.getValue("value1c"), 0.0); 167 | } else { 168 | assertEquals(0.0, metrics.getValue("value1"), 0.0); 169 | assertEquals(0.0, metrics.getValue("value1c"), 0.0); 170 | } 171 | 172 | } 173 | } 174 | } 175 | 176 | @Test 177 | public void assertMultipleMetricAggregationWithScriptError() { 178 | String indexName = "test1"; 179 | int size = 1; 180 | 181 | Map termsFactor = new HashMap(); 182 | termsFactor.put("foo", 1); 183 | 184 | buildTestDataset(1, indexName, "type1", size, termsFactor); 185 | 186 | SearchResponse searchResponse = client().prepareSearch(indexName) 187 | .setQuery(matchAllQuery()) 188 | .addAggregation(new MultipleMetricBuilder("metrics") 189 | .script(new ScriptBuilder("ratio").script(new Script("value1 / value2"))) 190 | .field(new SumBuilder("value1").field("value1")) 191 | .field(new CountBuilder("value2").field("value2").filter(new RangeQueryBuilder("value1").gt(1000)))) 192 | .execute().actionGet(); 193 | 194 | MultipleMetric metrics = searchResponse.getAggregations().get("metrics"); 195 | assertEquals(metrics.getValue("value1"), 45.0 * size, 0.0); 196 | assertEquals(metrics.getValue("value2"), 0.0 * size, 0.0); 197 | assertEquals(metrics.getValue("ratio"), Double.POSITIVE_INFINITY, 0.0); 198 | 199 | assertEquals(metrics.getDocCount("value1"), 10); 200 | assertEquals(metrics.getDocCount("value2"), 0); 201 | } 202 | 203 | @Test 204 | public void assertMultipleMetricAggregationWithScriptedField() { 205 | String indexName = "test1"; 206 | int size = 1; 207 | 208 | Map termsFactor = new HashMap(); 209 | termsFactor.put("foo", 1); 210 | 211 | buildTestDataset(1, indexName, "type1", size, termsFactor); 212 | 213 | SearchResponse searchResponse = client().prepareSearch(indexName) 214 | .setQuery(matchAllQuery()) 215 | .addAggregation(new MultipleMetricBuilder("metrics") 216 | .script(new ScriptBuilder("ratio").script(new Script("value1 / value2"))) 217 | .field(new SumBuilder("value1").script(new Script("doc['value1'].value + doc['value2'].value"))) 218 | .field(new CountBuilder("value2").field("value2"))) 219 | .execute().actionGet(); 220 | 221 | MultipleMetric metrics = searchResponse.getAggregations().get("metrics"); 222 | assertEquals(metrics.getValue("value1"), 495.0 * size, 0.0); 223 | assertEquals(metrics.getValue("value2"), 10.0 * size, 0.0); 224 | assertEquals(metrics.getValue("ratio"), metrics.getValue("value1") / metrics.getValue("value2"), 0.0); 225 | 226 | assertEquals(metrics.getDocCount("value1"), 10); 227 | assertEquals(metrics.getDocCount("value2"), 10); 228 | } 229 | 230 | 231 | @Test 232 | public void assertMultipleMetricAggregationWithScriptParams() { 233 | String indexName = "test1"; 234 | int size = 1; 235 | 236 | Map termsFactor = new HashMap(); 237 | termsFactor.put("foo", 1); 238 | 239 | buildTestDataset(1, indexName, "type1", size, termsFactor); 240 | 241 | SearchResponse searchResponse = client().prepareSearch(indexName) 242 | .setQuery(matchAllQuery()) 243 | .addAggregation(new MultipleMetricBuilder("metrics") 244 | .script(new ScriptBuilder("ratio").script(new Script("value1 * p / value2")).param("p", 2)) 245 | .field(new SumBuilder("value1").script(new Script("doc['value1'].value + doc['value2'].value"))) 246 | .field(new CountBuilder("value2").field("value2"))) 247 | .execute().actionGet(); 248 | 249 | MultipleMetric metrics = searchResponse.getAggregations().get("metrics"); 250 | assertEquals(metrics.getValue("value1"), 495.0 * size, 0.0); 251 | assertEquals(metrics.getValue("value2"), 10.0 * size, 0.0); 252 | assertEquals(metrics.getValue("ratio"), metrics.getValue("value1") * 2 / metrics.getValue("value2"), 0.0); 253 | 254 | assertEquals(metrics.getDocCount("value1"), 10); 255 | assertEquals(metrics.getDocCount("value2"), 10); 256 | } 257 | 258 | @Test 259 | public void assertMultipleMetricAggregationWithFilter() { 260 | String indexName = "test2"; 261 | int size = 1; 262 | 263 | Map termsFactor = new HashMap(); 264 | termsFactor.put("foo", 1); 265 | 266 | buildTestDataset(1, indexName, "type1", size, termsFactor); 267 | 268 | SearchResponse searchResponse = client().prepareSearch(indexName) 269 | .setQuery(matchAllQuery()) 270 | .addAggregation(new MultipleMetricBuilder("metrics") 271 | .script(new ScriptBuilder("ratio").script(new Script("value1 / value2"))) 272 | .field(new SumBuilder("value1").field("value1").filter(new RangeQueryBuilder("value1").gt(5))) 273 | .field(new CountBuilder("value2").field("value2"))) 274 | .execute().actionGet(); 275 | 276 | MultipleMetric metrics = searchResponse.getAggregations().get("metrics"); 277 | assertEquals(metrics.getValue("value1"), 30.0 * size, 0.0); 278 | assertEquals(metrics.getValue("value2"), 10.0 * size, 0.0); 279 | assertEquals(metrics.getValue("ratio"), metrics.getValue("value1") / metrics.getValue("value2"), 0.0); 280 | 281 | assertEquals(metrics.getDocCount("value1"), 4); 282 | assertEquals(metrics.getDocCount("value2"), 10); 283 | } 284 | 285 | @Test 286 | public void assertMultipleMetricAggregationWithUnmappedField() { 287 | String indexName = "test3"; 288 | int size = 1; 289 | 290 | Map termsFactor = new HashMap(); 291 | termsFactor.put("foo", 1); 292 | 293 | buildTestDataset(1, indexName, "type1", size, termsFactor); 294 | 295 | SearchResponse searchResponse = client().prepareSearch(indexName) 296 | .setQuery(matchAllQuery()) 297 | .addAggregation(new MultipleMetricBuilder("metrics") 298 | .script(new ScriptBuilder("ratio").script(new Script("value1 + value2"))) 299 | .field(new SumBuilder("value1").field("value4").filter(new RangeQueryBuilder("value1").gt(5))) 300 | .field(new CountBuilder("value2").field("value5"))) 301 | .execute().actionGet(); 302 | 303 | MultipleMetric metrics = searchResponse.getAggregations().get("metrics"); 304 | assertEquals(metrics.getValue("value1"), 0.0, 0.0); 305 | assertEquals(metrics.getValue("value2"), 0.0, 0.0); 306 | assertEquals(metrics.getValue("ratio"), metrics.getValue("value1") + metrics.getValue("value2"), 0.0); 307 | 308 | assertEquals(metrics.getDocCount("value1"), 0); 309 | assertEquals(metrics.getDocCount("value2"), 0); 310 | } 311 | 312 | @Test 313 | public void assertMultipleMetricAsTermsSubAggregation() { 314 | String indexName = "test4"; 315 | int size = 1; 316 | int numberOfShards = 1; 317 | 318 | Map termsFactor = new HashMap(); 319 | termsFactor.put("foo", 1); 320 | termsFactor.put("bar", 10); 321 | termsFactor.put("baz", 100); 322 | 323 | buildTestDataset(numberOfShards, indexName, "type1", size, termsFactor); 324 | 325 | TermsBuilder termsBuilder = new TermsBuilder("group_by") 326 | .field("field0") 327 | .order(Order.aggregation("metrics.ratio", true)) 328 | .subAggregation(new MultipleMetricBuilder("metrics") 329 | .script(new ScriptBuilder("ratio").script(new Script("value1 / value2"))) 330 | .field(new SumBuilder("value1").field("value1")) 331 | .field(new CountBuilder("value2").field("value2"))); 332 | 333 | SearchResponse searchResponse = client().prepareSearch(indexName) 334 | .setQuery(matchAllQuery()) 335 | .addAggregation(termsBuilder) 336 | .execute().actionGet(); 337 | 338 | Terms terms = searchResponse.getAggregations().get("group_by"); 339 | assertNotNull(terms); 340 | assertEquals(terms.getBuckets().size(), termsFactor.size()); 341 | 342 | for (Map.Entry entry: termsFactor.entrySet()) { 343 | String term = entry.getKey(); 344 | assertNotNull(terms.getBucketByKey(term)); 345 | assertNotNull(terms.getBucketByKey(term).getAggregations()); 346 | assertNotNull(terms.getBucketByKey(term).getAggregations().get("metrics")); 347 | 348 | MultipleMetric metrics = terms.getBucketByKey(term).getAggregations().get("metrics"); 349 | assertEquals(metrics.getValue("value1"), 45.0 * size * entry.getValue(), 0.0); 350 | assertEquals(metrics.getValue("value2"), 10.0 * size, 0.0); 351 | assertEquals(metrics.getValue("ratio"), metrics.getValue("value1") / metrics.getValue("value2"), 0.0); 352 | 353 | assertEquals(metrics.getDocCount("value1"), 10); 354 | assertEquals(metrics.getDocCount("value2"), 10); 355 | } 356 | } 357 | 358 | @Test 359 | public void assertMultipleMetricAsEmptyTermsSubAggregation() { 360 | String indexName = "test4"; 361 | int size = 1; 362 | int numberOfShards = 1; 363 | 364 | Map termsFactor = new HashMap(); 365 | termsFactor.put("foo", 1); 366 | termsFactor.put("bar", 10); 367 | termsFactor.put("baz", 100); 368 | 369 | buildTestDataset(numberOfShards, indexName, "type1", size, termsFactor); 370 | 371 | TermsBuilder termsBuilder = new TermsBuilder("group_by") 372 | .field("field0") 373 | .size(termsFactor.size()) 374 | .minDocCount(0L) // we force empty bucket to be returned 375 | .order(Order.aggregation("metrics.ratio", true)) 376 | .subAggregation(new MultipleMetricBuilder("metrics") 377 | .script(new ScriptBuilder("ratio").script(new Script("value1 / value2"))) 378 | .field(new SumBuilder("value1").field("value1")) 379 | .field(new CountBuilder("value2").field("value2"))); 380 | 381 | SearchResponse searchResponse = client().prepareSearch(indexName) 382 | .setQuery(new TermQueryBuilder("field0", "buz")) 383 | .addAggregation(termsBuilder) 384 | .execute().actionGet(); 385 | 386 | Terms terms = searchResponse.getAggregations().get("group_by"); 387 | assertNotNull(terms); 388 | assertEquals(terms.getBuckets().size(), termsFactor.size()); 389 | 390 | for (Map.Entry entry: termsFactor.entrySet()) { 391 | String term = entry.getKey(); 392 | assertNotNull(terms.getBucketByKey(term)); 393 | assertNotNull(terms.getBucketByKey(term).getAggregations()); 394 | assertNotNull(terms.getBucketByKey(term).getAggregations().get("metrics")); 395 | 396 | MultipleMetric metrics = terms.getBucketByKey(term).getAggregations().get("metrics"); 397 | assertEquals(metrics.getValue("value1"), 0.0, 0.0); 398 | assertEquals(metrics.getValue("value2"), 0.0, 0.0); 399 | assertEquals(0.0, metrics.getValue("ratio"), 0.0); 400 | 401 | assertEquals(metrics.getDocCount("value1"), 0); 402 | assertEquals(metrics.getDocCount("value2"), 0); 403 | } 404 | } 405 | 406 | 407 | } 408 | -------------------------------------------------------------------------------- /src/test/java/fr/v3d/elasticsearch/search/aggregations/metric/multiplemetric/MultipleMetricParserTest.java: -------------------------------------------------------------------------------- 1 | package fr.v3d.elasticsearch.search.aggregations.metric.multiplemetric; 2 | 3 | import org.elasticsearch.action.search.SearchPhaseExecutionException; 4 | import org.elasticsearch.common.xcontent.json.JsonXContent; 5 | import org.junit.Test; 6 | 7 | import fr.v3d.elasticsearch.plugin.multiplemetric.MultipleMetricAggregationTestCase; 8 | 9 | public class MultipleMetricParserTest extends MultipleMetricAggregationTestCase { 10 | 11 | @Test(expected=SearchPhaseExecutionException.class) 12 | public void assertMissingFieldOrScript() throws Exception { 13 | String indexName = "index0"; 14 | int numberOfShards = 1; 15 | 16 | createIndex(numberOfShards, indexName); 17 | 18 | client().prepareSearch("index0").setAggregations(JsonXContent.contentBuilder() 19 | .startObject() 20 | .startObject("metrics") 21 | .startObject("value1") 22 | .startObject("sum") 23 | .field("nofield", "field") 24 | .endObject() 25 | .endObject() 26 | .endObject() 27 | .endObject()).execute().actionGet(); 28 | } 29 | 30 | @Test(expected=SearchPhaseExecutionException.class) 31 | public void assertMissingOperator() throws Exception { 32 | String indexName = "index1"; 33 | int numberOfShards = 1; 34 | 35 | createIndex(numberOfShards, indexName); 36 | 37 | client().prepareSearch("index1").setAggregations(JsonXContent.contentBuilder() 38 | .startObject() 39 | .startObject("metrics") 40 | .startObject("value1") 41 | .startObject("bad-aggregator") 42 | .field("field", "a") 43 | .endObject() 44 | .endObject() 45 | .endObject() 46 | .endObject()).execute().actionGet(); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/test/resources/log4j.properties: -------------------------------------------------------------------------------- 1 | log4j.rootLogger=INFO, out 2 | 3 | log4j.appender.out=org.apache.log4j.ConsoleAppender 4 | log4j.appender.out.layout=org.apache.log4j.PatternLayout 5 | log4j.appender.out.layout.conversionPattern=[%d{ISO8601}][%-5p][%-25c][%t] %m%n 6 | -------------------------------------------------------------------------------- /testng.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | --------------------------------------------------------------------------------