From 2c289fb538cecb15423db9f358f7902980037316 Mon Sep 17 00:00:00 2001 From: Lee Hinman Date: Thu, 9 Jan 2014 15:20:06 -0700 Subject: [PATCH] Add the ability to retrieve fields from field data Adds a new FetchSubPhase, FieldDataFieldsFetchSubPhase, which loads the field data cache for a field and returns an array of values for the field. Also removes `doc['']` and `_source.` workaround no longer needed in field name resolving. Closes #4492 --- docs/reference/search/request-body.asciidoc | 5 +- .../search/request/fielddata-fields.asciidoc | 21 ++++ .../action/search/SearchRequestBuilder.java | 11 ++ .../index/get/ShardGetService.java | 101 +++++------------- .../percolator/PercolateContext.java | 11 ++ .../elasticsearch/search/SearchModule.java | 4 +- .../search/builder/SearchSourceBuilder.java | 20 ++++ .../search/fetch/FetchPhase.java | 5 +- .../search/fetch/FieldsParseElement.java | 19 +--- .../fielddata/FieldDataFieldsContext.java | 55 ++++++++++ .../FieldDataFieldsFetchSubPhase.java | 93 ++++++++++++++++ .../FieldDataFieldsParseElement.java | 46 ++++++++ .../search/internal/DefaultSearchContext.java | 13 +++ .../search/internal/SearchContext.java | 5 + .../document/DocumentActionsTests.java | 4 +- .../explain/ExplainActionTests.java | 11 +- .../index/search/child/TestSearchContext.java | 11 ++ .../search/fields/SearchFieldsTests.java | 68 +++++++++++- 18 files changed, 397 insertions(+), 106 deletions(-) create mode 100644 docs/reference/search/request/fielddata-fields.asciidoc create mode 100644 src/main/java/org/elasticsearch/search/fetch/fielddata/FieldDataFieldsContext.java create mode 100644 src/main/java/org/elasticsearch/search/fetch/fielddata/FieldDataFieldsFetchSubPhase.java create mode 100644 src/main/java/org/elasticsearch/search/fetch/fielddata/FieldDataFieldsParseElement.java diff --git a/docs/reference/search/request-body.asciidoc b/docs/reference/search/request-body.asciidoc index 493c65df085..4a4a70a54d7 100644 --- a/docs/reference/search/request-body.asciidoc +++ b/docs/reference/search/request-body.asciidoc @@ -31,7 +31,7 @@ And here is a sample response: { "_index" : "twitter", "_type" : "tweet", - "_id" : "1", + "_id" : "1", "_source" : { "user" : "kimchy", "postDate" : "2009-11-15T14:12:12", @@ -87,6 +87,8 @@ include::request/fields.asciidoc[] include::request/script-fields.asciidoc[] +include::request/fielddata-fields.asciidoc[] + include::request/post-filter.asciidoc[] include::request/highlighting.asciidoc[] @@ -108,4 +110,3 @@ include::request/index-boost.asciidoc[] include::request/min-score.asciidoc[] include::request/named-queries-and-filters.asciidoc[] - diff --git a/docs/reference/search/request/fielddata-fields.asciidoc b/docs/reference/search/request/fielddata-fields.asciidoc new file mode 100644 index 00000000000..6547c168284 --- /dev/null +++ b/docs/reference/search/request/fielddata-fields.asciidoc @@ -0,0 +1,21 @@ +[[search-request-fielddata-fields]] +=== Field Data Fields + +Allows to return the field data representiation of a field for each hit, for +example: + +[source,js] +-------------------------------------------------- +{ + "query" : { + ... + }, + "fielddata_fields" : ["test1", "test2"] +} +-------------------------------------------------- + +Field data fields can work on fields that are not stored. + +It's important to understand that using the `fielddata_fields` parameter will +cause the terms for that field to be loaded to memory (cached), which will +result in more memory consumption. diff --git a/src/main/java/org/elasticsearch/action/search/SearchRequestBuilder.java b/src/main/java/org/elasticsearch/action/search/SearchRequestBuilder.java index 7b9c29b7eec..8fe4c2df66a 100644 --- a/src/main/java/org/elasticsearch/action/search/SearchRequestBuilder.java +++ b/src/main/java/org/elasticsearch/action/search/SearchRequestBuilder.java @@ -417,6 +417,17 @@ public class SearchRequestBuilder extends ActionRequestBuilder x = docMapper.mappers().smartNameFieldMapper(field); + if (x == null) { + if (docMapper.objectMappers().get(field) != null) { + // Only fail if we know it is a object field, missing paths / fields shouldn't fail. + throw new ElasticsearchIllegalArgumentException("field [" + field + "] isn't a leaf field"); } - } else { - if (searchLookup == null) { - searchLookup = new SearchLookup(mapperService, fieldDataService, new String[]{type}); - searchLookup.source().setNextSource(source.source); - } - - FieldMapper x = docMapper.mappers().smartNameFieldMapper(field); - if (x == null) { - if (docMapper.objectMappers().get(field) != null) { - // Only fail if we know it is a object field, missing paths / fields shouldn't fail. - throw new ElasticsearchIllegalArgumentException("field [" + field + "] isn't a leaf field"); - } - } else if (docMapper.sourceMapper().enabled() || x.fieldType().stored()) { - List values = searchLookup.source().extractRawValues(field); - if (!values.isEmpty()) { - for (int i = 0; i < values.size(); i++) { - values.set(i, x.valueForSearch(values.get(i))); - } - value = values; + } else if (docMapper.sourceMapper().enabled() || x.fieldType().stored()) { + List values = searchLookup.source().extractRawValues(field); + if (!values.isEmpty()) { + for (int i = 0; i < values.size(); i++) { + values.set(i, x.valueForSearch(values.get(i))); } + value = values; } } } @@ -368,46 +343,26 @@ public class ShardGetService extends AbstractIndexShardComponent { SearchLookup searchLookup = null; for (String field : gFields) { Object value = null; - if (field.contains("_source.") || field.contains("doc[")) { + FieldMappers x = docMapper.mappers().smartName(field); + if (x == null) { + if (docMapper.objectMappers().get(field) != null) { + // Only fail if we know it is a object field, missing paths / fields shouldn't fail. + throw new ElasticsearchIllegalArgumentException("field [" + field + "] isn't a leaf field"); + } + } else if (!x.mapper().fieldType().stored()) { if (searchLookup == null) { searchLookup = new SearchLookup(mapperService, fieldDataService, new String[]{type}); - searchLookup.source().setNextSource(source); searchLookup.setNextReader(docIdAndVersion.context); + searchLookup.source().setNextSource(source); searchLookup.setNextDocId(docIdAndVersion.docId); } - SearchScript searchScript = scriptService.search(searchLookup, "mvel", field, null); - searchScript.setNextReader(docIdAndVersion.context); - searchScript.setNextDocId(docIdAndVersion.docId); - try { - value = searchScript.run(); - } catch (RuntimeException e) { - if (logger.isTraceEnabled()) { - logger.trace("failed to execute get request script field [{}]", e, field); - } - // ignore - } - } else { - FieldMappers x = docMapper.mappers().smartName(field); - if (x == null) { - if (docMapper.objectMappers().get(field) != null) { - // Only fail if we know it is a object field, missing paths / fields shouldn't fail. - throw new ElasticsearchIllegalArgumentException("field [" + field + "] isn't a leaf field"); - } - } else if (!x.mapper().fieldType().stored()) { - if (searchLookup == null) { - searchLookup = new SearchLookup(mapperService, fieldDataService, new String[]{type}); - searchLookup.setNextReader(docIdAndVersion.context); - searchLookup.source().setNextSource(source); - searchLookup.setNextDocId(docIdAndVersion.docId); - } - List values = searchLookup.source().extractRawValues(field); - if (!values.isEmpty()) { - for (int i = 0; i < values.size(); i++) { - values.set(i, x.mapper().valueForSearch(values.get(i))); - } - value = values; + List values = searchLookup.source().extractRawValues(field); + if (!values.isEmpty()) { + for (int i = 0; i < values.size(); i++) { + values.set(i, x.mapper().valueForSearch(values.get(i))); } + value = values; } } diff --git a/src/main/java/org/elasticsearch/percolator/PercolateContext.java b/src/main/java/org/elasticsearch/percolator/PercolateContext.java index c90d594bd6b..628ca337966 100644 --- a/src/main/java/org/elasticsearch/percolator/PercolateContext.java +++ b/src/main/java/org/elasticsearch/percolator/PercolateContext.java @@ -61,6 +61,7 @@ import org.elasticsearch.search.dfs.DfsSearchResult; import org.elasticsearch.search.facet.SearchContextFacets; import org.elasticsearch.search.fetch.FetchSearchResult; import org.elasticsearch.search.fetch.FetchSubPhase; +import org.elasticsearch.search.fetch.fielddata.FieldDataFieldsContext; import org.elasticsearch.search.fetch.partial.PartialFieldsContext; import org.elasticsearch.search.fetch.script.ScriptFieldsContext; import org.elasticsearch.search.fetch.source.FetchSourceContext; @@ -407,6 +408,16 @@ public class PercolateContext extends SearchContext { throw new UnsupportedOperationException(); } + @Override + public boolean hasFieldDataFields() { + throw new UnsupportedOperationException(); + } + + @Override + public FieldDataFieldsContext fieldDataFields() { + throw new UnsupportedOperationException(); + } + @Override public boolean hasScriptFields() { throw new UnsupportedOperationException(); diff --git a/src/main/java/org/elasticsearch/search/SearchModule.java b/src/main/java/org/elasticsearch/search/SearchModule.java index d4875b45358..1092e0e842e 100644 --- a/src/main/java/org/elasticsearch/search/SearchModule.java +++ b/src/main/java/org/elasticsearch/search/SearchModule.java @@ -24,8 +24,6 @@ import org.elasticsearch.common.inject.AbstractModule; import org.elasticsearch.common.inject.Module; import org.elasticsearch.common.inject.SpawnModules; import org.elasticsearch.index.query.functionscore.FunctionScoreModule; -import org.elasticsearch.indices.fielddata.breaker.CircuitBreakerService; -import org.elasticsearch.indices.fielddata.breaker.InternalCircuitBreakerService; import org.elasticsearch.search.action.SearchServiceTransportAction; import org.elasticsearch.search.aggregations.AggregationModule; import org.elasticsearch.search.controller.SearchPhaseController; @@ -33,6 +31,7 @@ import org.elasticsearch.search.dfs.DfsPhase; import org.elasticsearch.search.facet.FacetModule; import org.elasticsearch.search.fetch.FetchPhase; import org.elasticsearch.search.fetch.explain.ExplainFetchSubPhase; +import org.elasticsearch.search.fetch.fielddata.FieldDataFieldsFetchSubPhase; import org.elasticsearch.search.fetch.matchedqueries.MatchedQueriesFetchSubPhase; import org.elasticsearch.search.fetch.partial.PartialFieldsFetchSubPhase; import org.elasticsearch.search.fetch.script.ScriptFieldsFetchSubPhase; @@ -62,6 +61,7 @@ public class SearchModule extends AbstractModule implements SpawnModules { bind(FetchPhase.class).asEagerSingleton(); bind(ExplainFetchSubPhase.class).asEagerSingleton(); + bind(FieldDataFieldsFetchSubPhase.class).asEagerSingleton(); bind(ScriptFieldsFetchSubPhase.class).asEagerSingleton(); bind(PartialFieldsFetchSubPhase.class).asEagerSingleton(); bind(FetchSourceSubPhase.class).asEagerSingleton(); diff --git a/src/main/java/org/elasticsearch/search/builder/SearchSourceBuilder.java b/src/main/java/org/elasticsearch/search/builder/SearchSourceBuilder.java index 31eff16622f..fc4f4a8a267 100644 --- a/src/main/java/org/elasticsearch/search/builder/SearchSourceBuilder.java +++ b/src/main/java/org/elasticsearch/search/builder/SearchSourceBuilder.java @@ -98,6 +98,7 @@ public class SearchSourceBuilder implements ToXContent { private long timeoutInMillis = -1; private List fieldNames; + private List fieldDataFields; private List scriptFields; private List partialFields; private FetchSourceContext fetchSourceContext; @@ -568,6 +569,17 @@ public class SearchSourceBuilder implements ToXContent { return this; } + /** + * Adds a field to load from the field data cache and return as part of the search request. + */ + public SearchSourceBuilder fieldDataField(String name) { + if (fieldDataFields == null) { + fieldDataFields = new ArrayList(); + } + fieldDataFields.add(name); + return this; + } + /** * Adds a script field under the given name with the provided script. * @@ -769,6 +781,14 @@ public class SearchSourceBuilder implements ToXContent { } } + if (fieldDataFields != null) { + builder.startArray("fielddata_fields"); + for (String fieldName : fieldDataFields) { + builder.value(fieldName); + } + builder.endArray(); + } + if (partialFields != null) { builder.startObject("partial_fields"); for (PartialField partialField : partialFields) { diff --git a/src/main/java/org/elasticsearch/search/fetch/FetchPhase.java b/src/main/java/org/elasticsearch/search/fetch/FetchPhase.java index a97dd3c3683..7dc1929bcee 100644 --- a/src/main/java/org/elasticsearch/search/fetch/FetchPhase.java +++ b/src/main/java/org/elasticsearch/search/fetch/FetchPhase.java @@ -34,6 +34,7 @@ import org.elasticsearch.search.SearchHitField; import org.elasticsearch.search.SearchParseElement; import org.elasticsearch.search.SearchPhase; import org.elasticsearch.search.fetch.explain.ExplainFetchSubPhase; +import org.elasticsearch.search.fetch.fielddata.FieldDataFieldsFetchSubPhase; import org.elasticsearch.search.fetch.matchedqueries.MatchedQueriesFetchSubPhase; import org.elasticsearch.search.fetch.partial.PartialFieldsFetchSubPhase; import org.elasticsearch.search.fetch.script.ScriptFieldsFetchSubPhase; @@ -61,9 +62,9 @@ public class FetchPhase implements SearchPhase { @Inject public FetchPhase(HighlightPhase highlightPhase, ScriptFieldsFetchSubPhase scriptFieldsPhase, PartialFieldsFetchSubPhase partialFieldsPhase, MatchedQueriesFetchSubPhase matchedQueriesPhase, ExplainFetchSubPhase explainPhase, VersionFetchSubPhase versionPhase, - FetchSourceSubPhase fetchSourceSubPhase) { + FetchSourceSubPhase fetchSourceSubPhase, FieldDataFieldsFetchSubPhase fieldDataFieldsFetchSubPhase) { this.fetchSubPhases = new FetchSubPhase[]{scriptFieldsPhase, partialFieldsPhase, matchedQueriesPhase, explainPhase, highlightPhase, - fetchSourceSubPhase, versionPhase}; + fetchSourceSubPhase, versionPhase, fieldDataFieldsFetchSubPhase}; } @Override diff --git a/src/main/java/org/elasticsearch/search/fetch/FieldsParseElement.java b/src/main/java/org/elasticsearch/search/fetch/FieldsParseElement.java index 6cc2fdb3339..140d60d4eeb 100644 --- a/src/main/java/org/elasticsearch/search/fetch/FieldsParseElement.java +++ b/src/main/java/org/elasticsearch/search/fetch/FieldsParseElement.java @@ -37,27 +37,16 @@ public class FieldsParseElement implements SearchParseElement { boolean added = false; while ((token = parser.nextToken()) != XContentParser.Token.END_ARRAY) { String name = parser.text(); - if (name.contains("_source.") || name.contains("doc[")) { - // script field to load from source - SearchScript searchScript = context.scriptService().search(context.lookup(), "mvel", name, null); - context.scriptFields().add(new ScriptFieldsContext.ScriptField(name, searchScript, true)); - } else { - added = true; - context.fieldNames().add(name); - } + added = true; + context.fieldNames().add(name); } if (!added) { context.emptyFieldNames(); } } else if (token == XContentParser.Token.VALUE_STRING) { String name = parser.text(); - if (name.contains("_source.") || name.contains("doc[")) { - // script field to load from source - SearchScript searchScript = context.scriptService().search(context.lookup(), "mvel", name, null); - context.scriptFields().add(new ScriptFieldsContext.ScriptField(name, searchScript, true)); - } else { - context.fieldNames().add(name); - } + context.fieldNames().add(name); + } } } diff --git a/src/main/java/org/elasticsearch/search/fetch/fielddata/FieldDataFieldsContext.java b/src/main/java/org/elasticsearch/search/fetch/fielddata/FieldDataFieldsContext.java new file mode 100644 index 00000000000..427fd8632ca --- /dev/null +++ b/src/main/java/org/elasticsearch/search/fetch/fielddata/FieldDataFieldsContext.java @@ -0,0 +1,55 @@ +/* + * 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.search.fetch.fielddata; + +import com.google.common.collect.Lists; + +import java.util.List; + +/** + * All the required context to pull a field from the field data cache. + */ +public class FieldDataFieldsContext { + + public static class FieldDataField { + private final String name; + + public FieldDataField(String name) { + this.name = name; + } + + public String name() { + return name; + } + } + + private List fields = Lists.newArrayList(); + + public FieldDataFieldsContext() { + } + + public void add(FieldDataField field) { + this.fields.add(field); + } + + public List fields() { + return this.fields; + } +} diff --git a/src/main/java/org/elasticsearch/search/fetch/fielddata/FieldDataFieldsFetchSubPhase.java b/src/main/java/org/elasticsearch/search/fetch/fielddata/FieldDataFieldsFetchSubPhase.java new file mode 100644 index 00000000000..0e1b561cbe9 --- /dev/null +++ b/src/main/java/org/elasticsearch/search/fetch/fielddata/FieldDataFieldsFetchSubPhase.java @@ -0,0 +1,93 @@ +/* + * 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.search.fetch.fielddata; + +import com.google.common.collect.ImmutableMap; +import org.elasticsearch.ElasticsearchException; +import org.elasticsearch.common.inject.Inject; +import org.elasticsearch.index.fielddata.AtomicFieldData; +import org.elasticsearch.index.fielddata.ScriptDocValues; +import org.elasticsearch.index.mapper.FieldMapper; +import org.elasticsearch.search.SearchHitField; +import org.elasticsearch.search.SearchParseElement; +import org.elasticsearch.search.fetch.FetchSubPhase; +import org.elasticsearch.search.internal.InternalSearchHit; +import org.elasticsearch.search.internal.InternalSearchHitField; +import org.elasticsearch.search.internal.SearchContext; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; + +/** + * Query sub phase which pulls data from field data (using the cache if + * available, building it if not). + * + * Specifying {@code "fielddata_fields": ["field1", "field2"]} + */ +public class FieldDataFieldsFetchSubPhase implements FetchSubPhase { + + @Inject + public FieldDataFieldsFetchSubPhase() { + } + + @Override + public Map parseElements() { + ImmutableMap.Builder parseElements = ImmutableMap.builder(); + parseElements.put("fielddata_fields", new FieldDataFieldsParseElement()) + .put("fielddataFields", new FieldDataFieldsParseElement()); + return parseElements.build(); + } + + @Override + public boolean hitsExecutionNeeded(SearchContext context) { + return false; + } + + @Override + public void hitsExecute(SearchContext context, InternalSearchHit[] hits) throws ElasticsearchException { + } + + @Override + public boolean hitExecutionNeeded(SearchContext context) { + return context.hasFieldDataFields(); + } + + @Override + public void hitExecute(SearchContext context, HitContext hitContext) throws ElasticsearchException { + for (FieldDataFieldsContext.FieldDataField field : context.fieldDataFields().fields()) { + if (hitContext.hit().fieldsOrNull() == null) { + hitContext.hit().fields(new HashMap(2)); + } + SearchHitField hitField = hitContext.hit().fields().get(field.name()); + if (hitField == null) { + hitField = new InternalSearchHitField(field.name(), new ArrayList(2)); + hitContext.hit().fields().put(field.name(), hitField); + } + FieldMapper mapper = context.mapperService().smartNameFieldMapper(field.name()); + if (mapper != null) { + AtomicFieldData data = context.fieldData().getForField(mapper).load(hitContext.readerContext()); + ScriptDocValues values = data.getScriptValues(); + values.setNextDocId(hitContext.docId()); + hitField.values().addAll(values.getValues()); + } + } + } +} diff --git a/src/main/java/org/elasticsearch/search/fetch/fielddata/FieldDataFieldsParseElement.java b/src/main/java/org/elasticsearch/search/fetch/fielddata/FieldDataFieldsParseElement.java new file mode 100644 index 00000000000..93c2ce98f40 --- /dev/null +++ b/src/main/java/org/elasticsearch/search/fetch/fielddata/FieldDataFieldsParseElement.java @@ -0,0 +1,46 @@ +/* + * 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.search.fetch.fielddata; + +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.search.SearchParseElement; +import org.elasticsearch.search.internal.SearchContext; + +/** + * Parses field name values from the {@code fielddata_fields} parameter in a + * search request. + * + *
+ * {
+ *   "query": {...},
+ *   "fielddata_fields" : ["field1", "field2"]
+ * }
+ * 
+ */ +public class FieldDataFieldsParseElement implements SearchParseElement { + @Override + public void parse(XContentParser parser, SearchContext context) throws Exception { + XContentParser.Token token; + while ((token = parser.nextToken()) != XContentParser.Token.END_ARRAY) { + String fieldName = parser.text(); + context.fieldDataFields().add(new FieldDataFieldsContext.FieldDataField(fieldName)); + } + } +} diff --git a/src/main/java/org/elasticsearch/search/internal/DefaultSearchContext.java b/src/main/java/org/elasticsearch/search/internal/DefaultSearchContext.java index 21219d65f19..b860b20e1cb 100644 --- a/src/main/java/org/elasticsearch/search/internal/DefaultSearchContext.java +++ b/src/main/java/org/elasticsearch/search/internal/DefaultSearchContext.java @@ -58,6 +58,7 @@ import org.elasticsearch.search.aggregations.SearchContextAggregations; import org.elasticsearch.search.dfs.DfsSearchResult; import org.elasticsearch.search.facet.SearchContextFacets; import org.elasticsearch.search.fetch.FetchSearchResult; +import org.elasticsearch.search.fetch.fielddata.FieldDataFieldsContext; import org.elasticsearch.search.fetch.partial.PartialFieldsContext; import org.elasticsearch.search.fetch.script.ScriptFieldsContext; import org.elasticsearch.search.fetch.source.FetchSourceContext; @@ -122,6 +123,7 @@ public class DefaultSearchContext extends SearchContext { private boolean version = false; // by default, we don't return versions private List fieldNames; + private FieldDataFieldsContext fieldDataFields; private ScriptFieldsContext scriptFields; private PartialFieldsContext partialFields; private FetchSourceContext fetchSourceContext; @@ -348,6 +350,17 @@ public class DefaultSearchContext extends SearchContext { this.rescore = rescore; } + public boolean hasFieldDataFields() { + return fieldDataFields != null; + } + + public FieldDataFieldsContext fieldDataFields() { + if (fieldDataFields == null) { + fieldDataFields = new FieldDataFieldsContext(); + } + return this.fieldDataFields; + } + public boolean hasScriptFields() { return scriptFields != null; } diff --git a/src/main/java/org/elasticsearch/search/internal/SearchContext.java b/src/main/java/org/elasticsearch/search/internal/SearchContext.java index 4ca8da1f26a..6ec1cfbd9ac 100644 --- a/src/main/java/org/elasticsearch/search/internal/SearchContext.java +++ b/src/main/java/org/elasticsearch/search/internal/SearchContext.java @@ -47,6 +47,7 @@ import org.elasticsearch.search.aggregations.SearchContextAggregations; import org.elasticsearch.search.dfs.DfsSearchResult; import org.elasticsearch.search.facet.SearchContextFacets; import org.elasticsearch.search.fetch.FetchSearchResult; +import org.elasticsearch.search.fetch.fielddata.FieldDataFieldsContext; import org.elasticsearch.search.fetch.partial.PartialFieldsContext; import org.elasticsearch.search.fetch.script.ScriptFieldsContext; import org.elasticsearch.search.fetch.source.FetchSourceContext; @@ -139,6 +140,10 @@ public abstract class SearchContext implements Releasable { public abstract void rescore(RescoreSearchContext rescore); + public abstract boolean hasFieldDataFields(); + + public abstract FieldDataFieldsContext fieldDataFields(); + public abstract boolean hasScriptFields(); public abstract ScriptFieldsContext scriptFields(); diff --git a/src/test/java/org/elasticsearch/document/DocumentActionsTests.java b/src/test/java/org/elasticsearch/document/DocumentActionsTests.java index 73754e7fa8d..68b4d3e849e 100644 --- a/src/test/java/org/elasticsearch/document/DocumentActionsTests.java +++ b/src/test/java/org/elasticsearch/document/DocumentActionsTests.java @@ -107,11 +107,11 @@ public class DocumentActionsTests extends ElasticsearchIntegrationTest { logger.info("Get [type1/1] with script"); for (int i = 0; i < 5; i++) { - getResult = client().prepareGet("test", "type1", "1").setFields("_source.type1.name").execute().actionGet(); + getResult = client().prepareGet("test", "type1", "1").setFields("type1.name").execute().actionGet(); assertThat(getResult.getIndex(), equalTo(getConcreteIndexName())); assertThat(getResult.isExists(), equalTo(true)); assertThat(getResult.getSourceAsBytes(), nullValue()); - assertThat(getResult.getField("_source.type1.name").getValues().get(0).toString(), equalTo("test")); + assertThat(getResult.getField("type1.name").getValues().get(0).toString(), equalTo("test")); } logger.info("Get [type1/2] (should be empty)"); diff --git a/src/test/java/org/elasticsearch/explain/ExplainActionTests.java b/src/test/java/org/elasticsearch/explain/ExplainActionTests.java index 9500ef9dbe7..31473ee0d33 100644 --- a/src/test/java/org/elasticsearch/explain/ExplainActionTests.java +++ b/src/test/java/org/elasticsearch/explain/ExplainActionTests.java @@ -151,15 +151,14 @@ public class ExplainActionTests extends ElasticsearchIntegrationTest { response = client().prepareExplain("test", "test", "1") .setQuery(QueryBuilders.matchAllQuery()) - .setFields("_source.obj1") + .setFields("obj1.field1", "obj1.field2") .execute().actionGet(); assertNotNull(response); assertTrue(response.isMatch()); - assertThat(response.getGetResult().getFields().size(), equalTo(1)); - Map fields = (Map) response.getGetResult().field("_source.obj1").getValue(); - assertThat(fields.size(), equalTo(2)); - assertThat(fields.get("field1"), equalTo("value1")); - assertThat(fields.get("field2"), equalTo("value2")); + String v1 = (String) response.getGetResult().field("obj1.field1").getValue(); + String v2 = (String) response.getGetResult().field("obj1.field2").getValue(); + assertThat(v1, equalTo("value1")); + assertThat(v2, equalTo("value2")); } @SuppressWarnings("unchecked") diff --git a/src/test/java/org/elasticsearch/index/search/child/TestSearchContext.java b/src/test/java/org/elasticsearch/index/search/child/TestSearchContext.java index 6e8ed6244f1..6e5a1e1cd0c 100644 --- a/src/test/java/org/elasticsearch/index/search/child/TestSearchContext.java +++ b/src/test/java/org/elasticsearch/index/search/child/TestSearchContext.java @@ -47,6 +47,7 @@ import org.elasticsearch.search.aggregations.SearchContextAggregations; import org.elasticsearch.search.dfs.DfsSearchResult; import org.elasticsearch.search.facet.SearchContextFacets; import org.elasticsearch.search.fetch.FetchSearchResult; +import org.elasticsearch.search.fetch.fielddata.FieldDataFieldsContext; import org.elasticsearch.search.fetch.partial.PartialFieldsContext; import org.elasticsearch.search.fetch.script.ScriptFieldsContext; import org.elasticsearch.search.fetch.source.FetchSourceContext; @@ -212,6 +213,16 @@ class TestSearchContext extends SearchContext { public void rescore(RescoreSearchContext rescore) { } + @Override + public boolean hasFieldDataFields() { + return false; + } + + @Override + public FieldDataFieldsContext fieldDataFields() { + return null; + } + @Override public boolean hasScriptFields() { return false; diff --git a/src/test/java/org/elasticsearch/search/fields/SearchFieldsTests.java b/src/test/java/org/elasticsearch/search/fields/SearchFieldsTests.java index 314c235cf38..c397b8a6cce 100644 --- a/src/test/java/org/elasticsearch/search/fields/SearchFieldsTests.java +++ b/src/test/java/org/elasticsearch/search/fields/SearchFieldsTests.java @@ -19,6 +19,7 @@ package org.elasticsearch.search.fields; +import org.elasticsearch.action.search.SearchRequestBuilder; import org.elasticsearch.action.search.SearchResponse; import org.elasticsearch.common.Base64; import org.elasticsearch.common.Priority; @@ -199,7 +200,6 @@ public class SearchFieldsTests extends ElasticsearchIntegrationTest { SearchResponse response = client().prepareSearch() .setQuery(matchAllQuery()) - .addField("_source.obj1") // we also automatically detect _source in fields .addScriptField("s_obj1", "_source.obj1") .addScriptField("s_obj1_test", "_source.obj1.test") .addScriptField("s_obj2", "_source.obj2") @@ -209,11 +209,9 @@ public class SearchFieldsTests extends ElasticsearchIntegrationTest { assertThat("Failures " + Arrays.toString(response.getShardFailures()), response.getShardFailures().length, equalTo(0)); - Map sObj1 = response.getHits().getAt(0).field("_source.obj1").value(); - assertThat(sObj1.get("test").toString(), equalTo("something")); assertThat(response.getHits().getAt(0).field("s_obj1_test").value().toString(), equalTo("something")); - sObj1 = response.getHits().getAt(0).field("s_obj1").value(); + Map sObj1 = response.getHits().getAt(0).field("s_obj1").value(); assertThat(sObj1.get("test").toString(), equalTo("something")); assertThat(response.getHits().getAt(0).field("s_obj1_test").value().toString(), equalTo("something")); @@ -423,4 +421,66 @@ public class SearchFieldsTests extends ElasticsearchIntegrationTest { assertThat(searchResponse.getHits().getAt(0).field(field).getValues().get(0).toString(), equalTo("value1")); assertThat(searchResponse.getHits().getAt(0).field(field).getValues().get(1).toString(), equalTo("value2")); } + + @Test + public void testFieldsPulledFromFieldData() throws Exception { + createIndex("test"); + client().admin().cluster().prepareHealth().setWaitForEvents(Priority.LANGUID).setWaitForYellowStatus().execute().actionGet(); + + String mapping = XContentFactory.jsonBuilder().startObject().startObject("type1").startObject("properties") + .startObject("_source").field("enabled", false).endObject() + .startObject("string_field").field("type", "string").endObject() + .startObject("byte_field").field("type", "byte").endObject() + .startObject("short_field").field("type", "short").endObject() + .startObject("integer_field").field("type", "integer").endObject() + .startObject("long_field").field("type", "long").endObject() + .startObject("float_field").field("type", "float").endObject() + .startObject("double_field").field("type", "double").endObject() + .startObject("date_field").field("type", "date").endObject() + .startObject("boolean_field").field("type", "boolean").endObject() + .startObject("binary_field").field("type", "binary").endObject() + .endObject().endObject().endObject().string(); + + client().admin().indices().preparePutMapping().setType("type1").setSource(mapping).execute().actionGet(); + + client().prepareIndex("test", "type1", "1").setSource(jsonBuilder().startObject() + .field("string_field", "foo") + .field("byte_field", (byte) 1) + .field("short_field", (short) 2) + .field("integer_field", 3) + .field("long_field", 4l) + .field("float_field", 5.0f) + .field("double_field", 6.0d) + .field("date_field", Joda.forPattern("dateOptionalTime").printer().print(new DateTime(2012, 3, 22, 0, 0, DateTimeZone.UTC))) + .field("boolean_field", true) + .endObject()).execute().actionGet(); + + client().admin().indices().prepareRefresh().execute().actionGet(); + + SearchRequestBuilder builder = client().prepareSearch().setQuery(matchAllQuery()) + .addFieldDataField("string_field") + .addFieldDataField("byte_field") + .addFieldDataField("short_field") + .addFieldDataField("integer_field") + .addFieldDataField("long_field") + .addFieldDataField("float_field") + .addFieldDataField("double_field") + .addFieldDataField("date_field") + .addFieldDataField("boolean_field"); + SearchResponse searchResponse = builder.execute().actionGet(); + + assertThat(searchResponse.getHits().getTotalHits(), equalTo(1l)); + assertThat(searchResponse.getHits().hits().length, equalTo(1)); + assertThat(searchResponse.getHits().getAt(0).fields().size(), equalTo(9)); + + assertThat(searchResponse.getHits().getAt(0).fields().get("byte_field").value().toString(), equalTo("1")); + assertThat(searchResponse.getHits().getAt(0).fields().get("short_field").value().toString(), equalTo("2")); + assertThat(searchResponse.getHits().getAt(0).fields().get("integer_field").value(), equalTo((Object) 3l)); + assertThat(searchResponse.getHits().getAt(0).fields().get("long_field").value(), equalTo((Object) 4l)); + assertThat(searchResponse.getHits().getAt(0).fields().get("float_field").value(), equalTo((Object) 5.0)); + assertThat(searchResponse.getHits().getAt(0).fields().get("double_field").value(), equalTo((Object) 6.0d)); + assertThat(searchResponse.getHits().getAt(0).fields().get("date_field").value(), equalTo((Object) 1332374400000L)); + assertThat(searchResponse.getHits().getAt(0).fields().get("boolean_field").value().toString(), equalTo("T")); + + } }