Skip to content

Commit 5d54e36

Browse files
committed
Statistical Facet: Allow to compute statistical facets on more than one field, closes elastic#436.
1 parent 2fc0022 commit 5d54e36

File tree

5 files changed

+198
-17
lines changed

5 files changed

+198
-17
lines changed

modules/elasticsearch/src/main/java/org/elasticsearch/search/facets/statistical/StatisticalFacetBuilder.java

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
* @author kimchy (shay.banon)
3131
*/
3232
public class StatisticalFacetBuilder extends AbstractFacetBuilder {
33+
private String[] fieldsNames;
3334
private String fieldName;
3435

3536
public StatisticalFacetBuilder(String name) {
@@ -41,6 +42,14 @@ public StatisticalFacetBuilder field(String field) {
4142
return this;
4243
}
4344

45+
/**
46+
* The fields the terms will be collected from.
47+
*/
48+
public StatisticalFacetBuilder fields(String... fields) {
49+
this.fieldsNames = fields;
50+
return this;
51+
}
52+
4453
public StatisticalFacetBuilder global(boolean global) {
4554
this.global = global;
4655
return this;
@@ -52,13 +61,21 @@ public StatisticalFacetBuilder facetFilter(XContentFilterBuilder filter) {
5261
}
5362

5463
@Override public void toXContent(XContentBuilder builder, Params params) throws IOException {
55-
if (fieldName == null) {
64+
if (fieldName == null && fieldsNames == null) {
5665
throw new SearchSourceBuilderException("field must be set on statistical facet for facet [" + name + "]");
5766
}
5867
builder.startObject(name);
5968

6069
builder.startObject(StatisticalFacetCollectorParser.NAME);
61-
builder.field("field", fieldName);
70+
if (fieldsNames != null) {
71+
if (fieldsNames.length == 1) {
72+
builder.field("field", fieldsNames[0]);
73+
} else {
74+
builder.field("fields", fieldsNames);
75+
}
76+
} else {
77+
builder.field("field", fieldName);
78+
}
6279
builder.endObject();
6380

6481
addFilterFacetAndGlobal(builder, params);

modules/elasticsearch/src/main/java/org/elasticsearch/search/facets/statistical/StatisticalFacetCollectorParser.java

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
package org.elasticsearch.search.facets.statistical;
2121

22+
import org.elasticsearch.common.collect.Lists;
2223
import org.elasticsearch.common.thread.ThreadLocals;
2324
import org.elasticsearch.common.xcontent.XContentParser;
2425
import org.elasticsearch.search.facets.FacetPhaseExecutionException;
@@ -28,6 +29,7 @@
2829

2930
import java.io.IOException;
3031
import java.util.HashMap;
32+
import java.util.List;
3133
import java.util.Map;
3234

3335
/**
@@ -49,12 +51,14 @@ public class StatisticalFacetCollectorParser implements FacetCollectorParser {
4951

5052
@Override public FacetCollector parse(String facetName, XContentParser parser, SearchContext context) throws IOException {
5153
String field = null;
54+
String[] fieldsNames = null;
5255

53-
String currentFieldName = null;
5456
String script = null;
5557
String scriptLang = null;
5658
Map<String, Object> params = cachedParams.get().get();
5759
params.clear();
60+
61+
String currentFieldName = null;
5862
XContentParser.Token token;
5963
while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) {
6064
if (token == XContentParser.Token.FIELD_NAME) {
@@ -63,6 +67,14 @@ public class StatisticalFacetCollectorParser implements FacetCollectorParser {
6367
if ("params".equals(currentFieldName)) {
6468
params = parser.map();
6569
}
70+
} else if (token == XContentParser.Token.START_ARRAY) {
71+
if ("fields".equals(currentFieldName)) {
72+
List<String> fields = Lists.newArrayListWithCapacity(4);
73+
while ((token = parser.nextToken()) != XContentParser.Token.END_ARRAY) {
74+
fields.add(parser.text());
75+
}
76+
fieldsNames = fields.toArray(new String[fields.size()]);
77+
}
6678
} else if (token.isValue()) {
6779
if ("field".equals(currentFieldName)) {
6880
field = parser.text();
@@ -73,6 +85,9 @@ public class StatisticalFacetCollectorParser implements FacetCollectorParser {
7385
}
7486
}
7587
}
88+
if (fieldsNames != null) {
89+
return new StatisticalFieldsFacetCollector(facetName, fieldsNames, context);
90+
}
7691
if (script == null && field == null) {
7792
throw new FacetPhaseExecutionException(facetName, "statistical facet requires either [script] or [field] to be set");
7893
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
/*
2+
* Licensed to Elastic Search and Shay Banon under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. Elastic Search licenses this
6+
* file to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
package org.elasticsearch.search.facets.statistical;
21+
22+
import org.apache.lucene.index.IndexReader;
23+
import org.elasticsearch.common.Strings;
24+
import org.elasticsearch.index.cache.field.data.FieldDataCache;
25+
import org.elasticsearch.index.field.data.FieldDataType;
26+
import org.elasticsearch.index.field.data.NumericFieldData;
27+
import org.elasticsearch.index.mapper.FieldMapper;
28+
import org.elasticsearch.search.facets.Facet;
29+
import org.elasticsearch.search.facets.FacetPhaseExecutionException;
30+
import org.elasticsearch.search.facets.support.AbstractFacetCollector;
31+
import org.elasticsearch.search.internal.SearchContext;
32+
33+
import java.io.IOException;
34+
35+
/**
36+
* @author kimchy (shay.banon)
37+
*/
38+
public class StatisticalFieldsFacetCollector extends AbstractFacetCollector {
39+
40+
private final String[] fieldsNames;
41+
42+
private final String[] indexFieldsNames;
43+
44+
private final FieldDataCache fieldDataCache;
45+
46+
private final FieldDataType[] fieldsDataType;
47+
48+
private NumericFieldData[] fieldsData;
49+
50+
private final StatsProc statsProc = new StatsProc();
51+
52+
public StatisticalFieldsFacetCollector(String facetName, String[] fieldsNames, SearchContext context) {
53+
super(facetName);
54+
this.fieldsNames = fieldsNames;
55+
this.fieldDataCache = context.fieldDataCache();
56+
57+
fieldsDataType = new FieldDataType[fieldsNames.length];
58+
fieldsData = new NumericFieldData[fieldsNames.length];
59+
indexFieldsNames = new String[fieldsNames.length];
60+
61+
62+
for (int i = 0; i < fieldsNames.length; i++) {
63+
FieldMapper mapper = context.mapperService().smartNameFieldMapper(fieldsNames[i]);
64+
if (mapper == null) {
65+
throw new FacetPhaseExecutionException(facetName, "No mapping found for field [" + fieldsNames[i] + "]");
66+
}
67+
indexFieldsNames[i] = mapper.names().indexName();
68+
fieldsDataType[i] = mapper.fieldDataType();
69+
}
70+
}
71+
72+
@Override protected void doCollect(int doc) throws IOException {
73+
for (NumericFieldData fieldData : fieldsData) {
74+
fieldData.forEachValueInDoc(doc, statsProc);
75+
}
76+
}
77+
78+
@Override protected void doSetNextReader(IndexReader reader, int docBase) throws IOException {
79+
for (int i = 0; i < fieldsNames.length; i++) {
80+
fieldsData[i] = (NumericFieldData) fieldDataCache.cache(fieldsDataType[i], reader, indexFieldsNames[i]);
81+
}
82+
}
83+
84+
@Override public Facet facet() {
85+
return new InternalStatisticalFacet(facetName, Strings.arrayToCommaDelimitedString(fieldsNames), statsProc.min(), statsProc.max(), statsProc.total(), statsProc.sumOfSquares(), statsProc.count());
86+
}
87+
88+
public static class StatsProc implements NumericFieldData.DoubleValueInDocProc {
89+
90+
private double min = Double.NaN;
91+
92+
private double max = Double.NaN;
93+
94+
private double total = 0;
95+
96+
private double sumOfSquares = 0.0;
97+
98+
private long count;
99+
100+
@Override public void onValue(int docId, double value) {
101+
if (value < min || Double.isNaN(min)) {
102+
min = value;
103+
}
104+
if (value > max || Double.isNaN(max)) {
105+
max = value;
106+
}
107+
sumOfSquares += value * value;
108+
total += value;
109+
count++;
110+
}
111+
112+
public final double min() {
113+
return min;
114+
}
115+
116+
public final double max() {
117+
return max;
118+
}
119+
120+
public final double total() {
121+
return total;
122+
}
123+
124+
public final long count() {
125+
return count;
126+
}
127+
128+
public final double sumOfSquares() {
129+
return sumOfSquares;
130+
}
131+
}
132+
}

modules/elasticsearch/src/main/java/org/elasticsearch/search/facets/terms/TermsFacetCollectorParser.java

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -47,53 +47,54 @@ public class TermsFacetCollectorParser implements FacetCollectorParser {
4747
String field = null;
4848
int size = 10;
4949

50-
String fieldName = null;
5150
String[] fieldsNames = null;
52-
XContentParser.Token token;
5351
ImmutableSet<String> excluded = ImmutableSet.of();
5452
String regex = null;
5553
String regexFlags = null;
5654
TermsFacet.ComparatorType comparatorType = TermsFacet.ComparatorType.COUNT;
5755
String scriptLang = null;
5856
String script = null;
5957
Map<String, Object> params = null;
58+
59+
String currentFieldName = null;
60+
XContentParser.Token token;
6061
while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) {
6162
if (token == XContentParser.Token.FIELD_NAME) {
62-
fieldName = parser.currentName();
63+
currentFieldName = parser.currentName();
6364
} else if (token == XContentParser.Token.START_OBJECT) {
64-
if ("params".equals(fieldName)) {
65+
if ("params".equals(currentFieldName)) {
6566
params = parser.map();
6667
}
6768
} else if (token == XContentParser.Token.START_ARRAY) {
68-
if ("exclude".equals(fieldName)) {
69+
if ("exclude".equals(currentFieldName)) {
6970
ImmutableSet.Builder<String> builder = ImmutableSet.builder();
7071
while ((token = parser.nextToken()) != XContentParser.Token.END_ARRAY) {
7172
builder.add(parser.text());
7273
}
7374
excluded = builder.build();
74-
} else if ("fields".equals(fieldName)) {
75+
} else if ("fields".equals(currentFieldName)) {
7576
List<String> fields = Lists.newArrayListWithCapacity(4);
7677
while ((token = parser.nextToken()) != XContentParser.Token.END_ARRAY) {
7778
fields.add(parser.text());
7879
}
7980
fieldsNames = fields.toArray(new String[fields.size()]);
8081
}
8182
} else if (token.isValue()) {
82-
if ("field".equals(fieldName)) {
83+
if ("field".equals(currentFieldName)) {
8384
field = parser.text();
84-
} else if ("script_field".equals(fieldName)) {
85+
} else if ("script_field".equals(currentFieldName)) {
8586
script = parser.text();
86-
} else if ("size".equals(fieldName)) {
87+
} else if ("size".equals(currentFieldName)) {
8788
size = parser.intValue();
88-
} else if ("regex".equals(fieldName)) {
89+
} else if ("regex".equals(currentFieldName)) {
8990
regex = parser.text();
90-
} else if ("regex_flags".equals(fieldName) || "regexFlags".equals(fieldName)) {
91+
} else if ("regex_flags".equals(currentFieldName) || "regexFlags".equals(currentFieldName)) {
9192
regexFlags = parser.text();
92-
} else if ("order".equals(fieldName) || "comparator".equals(field)) {
93+
} else if ("order".equals(currentFieldName) || "comparator".equals(field)) {
9394
comparatorType = TermsFacet.ComparatorType.fromString(parser.text());
94-
} else if ("script".equals(fieldName)) {
95+
} else if ("script".equals(currentFieldName)) {
9596
script = parser.text();
96-
} else if ("lang".equals(fieldName)) {
97+
} else if ("lang".equals(currentFieldName)) {
9798
scriptLang = parser.text();
9899
}
99100
}

modules/test/integration/src/test/java/org/elasticsearch/test/integration/search/facets/SimpleFacetsTests.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -560,6 +560,22 @@ protected Client getClient() {
560560
assertThat(facet.mean(), equalTo(3d));
561561
assertThat(facet.sumOfSquares(), equalTo(20d));
562562

563+
// test multi field facet
564+
searchResponse = client.prepareSearch()
565+
.setQuery(matchAllQuery())
566+
.addFacet(statisticalFacet("stats").fields("num", "multi_num"))
567+
.execute().actionGet();
568+
569+
570+
facet = searchResponse.facets().facet("stats");
571+
assertThat(facet.name(), equalTo(facet.name()));
572+
assertThat(facet.count(), equalTo(6l));
573+
assertThat(facet.total(), equalTo(13d));
574+
assertThat(facet.min(), equalTo(1d));
575+
assertThat(facet.max(), equalTo(4d));
576+
assertThat(facet.mean(), equalTo(13d / 6d));
577+
assertThat(facet.sumOfSquares(), equalTo(35d));
578+
563579
// test cross field facet using the same facet name...
564580
searchResponse = client.prepareSearch()
565581
.setQuery(matchAllQuery())

0 commit comments

Comments
 (0)