inner_hits: Nested parent field should be resolved based on the parent inner hit definition, instead of the nested parent field in the mapping.

The behaviour is better in the case someone has multiple levels of nested object fields defined in the mapping and like to define a single inner_hits definition that is two or more levels deep.

If someone wants inner hits on a nested field that is 2 levels deep the following would need to be defined:

```
{
  ...
  "inner_hits" : {
     "path" : {
        "level1" : {
            "inner_hits" : {
               "path" : {
                  "level2" : {
                     "query" : { .... }
                  }
               }
            }
        }
     }
  }
}
```

With this change the above can be defined as:

```
{
  ...
  "inner_hits" : {
     "path" : {
        "level1.level2" : {
            "query" : { .... }
        }
     }
  }
}
```

Closes #9251
This commit is contained in:
Martijn van Groningen 2015-01-12 13:11:46 +01:00
parent 8e58c0b7a8
commit 4393939f5e
6 changed files with 56 additions and 14 deletions

View File

@ -152,6 +152,28 @@ An important default is that the `_source` returned in hits inside `inner_hits`
So in the above example only the comment part is returned per nested hit and not the entire source of the top level
document that contained the the comment.
[[hierarchical-nested-inner-hits]]
==== Hierarchical levels of nested object fields and inner hits.
If a mapping has multiple levels of hierarchical nested object fields each level can be accessed via dot notated path.
For example if there is a `comments` nested field that contains a `votes` nested field and votes should directly be returned
with the the root hits then the following path can be defined:
[source,js]
--------------------------------------------------
{
"query" : {
"nested" : {
"path" : "comments.votes",
"query" : { ... },
"inner_hits" : {}
}
}
}
--------------------------------------------------
This indirect referencing is only supported for nested inner hits.
[[parent-child-inner-hits]]
==== Parent/child inner hits

View File

@ -30,7 +30,6 @@ import org.elasticsearch.common.Strings;
import org.elasticsearch.common.collect.Tuple;
import org.elasticsearch.common.inject.Inject;
import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.index.mapper.object.ObjectMapper;
import org.elasticsearch.index.query.support.InnerHitsQueryParserHelper;
import org.elasticsearch.index.query.support.NestedInnerQueryParseSupport;
import org.elasticsearch.search.fetch.innerhits.InnerHitsContext;
@ -149,8 +148,7 @@ public class NestedQueryParser implements QueryParser {
}
if (innerHits != null) {
ObjectMapper parentObjectMapper = childDocumentMapper.findParentObjectMapper(nestedObjectMapper);
InnerHitsContext.NestedInnerHits nestedInnerHits = new InnerHitsContext.NestedInnerHits(innerHits.v2(), getInnerQuery(), null, parentObjectMapper, nestedObjectMapper);
InnerHitsContext.NestedInnerHits nestedInnerHits = new InnerHitsContext.NestedInnerHits(innerHits.v2(), getInnerQuery(), null, getParentObjectMapper(), nestedObjectMapper);
String name = innerHits.v1() != null ? innerHits.v1() : path;
parseContext.addInnerHits(name, nestedInnerHits);
}

View File

@ -60,6 +60,7 @@ public class NestedInnerQueryParseSupport {
protected DocumentMapper childDocumentMapper;
protected ObjectMapper nestedObjectMapper;
private ObjectMapper parentObjectMapper;
public NestedInnerQueryParseSupport(XContentParser parser, SearchContext searchContext) {
parseContext = searchContext.queryParserService().getParseContext();
@ -187,6 +188,10 @@ public class NestedInnerQueryParseSupport {
return filterFound;
}
public ObjectMapper getParentObjectMapper() {
return parentObjectMapper;
}
private void setPathLevel() {
ObjectMapper objectMapper = parseContext.nestedScope().getObjectMapper();
if (objectMapper == null) {
@ -195,7 +200,7 @@ public class NestedInnerQueryParseSupport {
parentFilter = parseContext.bitsetFilter(objectMapper.nestedTypeFilter());
}
childFilter = parseContext.bitsetFilter(nestedObjectMapper.nestedTypeFilter());
parseContext.nestedScope().nextLevel(nestedObjectMapper);
parentObjectMapper = parseContext.nestedScope().nextLevel(nestedObjectMapper);
}
private void resetPathLevel() {

View File

@ -39,17 +39,19 @@ public final class NestedScope {
}
/**
* Sets the new current nested level and moves old current nested level down
* Sets the new current nested level and pushes old current nested level down the stack returns that level.
*/
public void nextLevel(ObjectMapper level) {
public ObjectMapper nextLevel(ObjectMapper level) {
ObjectMapper previous = levelStack.peek();
levelStack.push(level);
return previous;
}
/**
* Sets the previous nested level as current nested level and removes the current nested level.
* Sets the previous nested level as current nested level and removes and returns the current nested level.
*/
public void previousLevel() {
ObjectMapper level = levelStack.pop();
public ObjectMapper previousLevel() {
return levelStack.pop();
}
}

View File

@ -163,12 +163,10 @@ public class InnerHitsParseElement implements SearchParseElement {
if (!childObjectMapper.nested().isNested()) {
throw new ElasticsearchIllegalArgumentException("path [" + nestedPath +"] isn't nested");
}
DocumentMapper childDocumentMapper = smartNameObjectMapper.docMapper();
parseContext.nestedScope().nextLevel(childObjectMapper);
ObjectMapper parentObjectMapper = parseContext.nestedScope().nextLevel(childObjectMapper);
ParseResult parseResult = parseSubSearchContext(searchContext, parseContext, parser);
parseContext.nestedScope().previousLevel();
ObjectMapper parentObjectMapper = childDocumentMapper.findParentObjectMapper(childObjectMapper);
return new InnerHitsContext.NestedInnerHits(parseResult.context(), parseResult.query(), parseResult.childInnerHits(), parentObjectMapper, childObjectMapper);
}

View File

@ -618,13 +618,30 @@ public class InnerHitsTests extends ElasticsearchIntegrationTest {
assertThat(innerHits.getAt(0).getNestedIdentity().getChild().getField().string(), equalTo("remarks"));
assertThat(innerHits.getAt(0).getNestedIdentity().getChild().getOffset(), equalTo(0));
// Directly refer to the second level:
response = client().prepareSearch("articles")
.setQuery(nestedQuery("comments.remarks", matchQuery("comments.remarks.message", "bad")).innerHit(new QueryInnerHitBuilder()))
.get();
assertNoFailures(response);
assertHitCount(response, 1);
assertSearchHit(response, 1, hasId("2"));
assertThat(response.getHits().getAt(0).getInnerHits().size(), equalTo(1));
innerHits = response.getHits().getAt(0).getInnerHits().get("comments.remarks");
assertThat(innerHits.totalHits(), equalTo(1l));
assertThat(innerHits.getHits().length, equalTo(1));
assertThat(innerHits.getAt(0).getId(), equalTo("2"));
assertThat(innerHits.getAt(0).getNestedIdentity().getField().string(), equalTo("comments"));
assertThat(innerHits.getAt(0).getNestedIdentity().getOffset(), equalTo(0));
assertThat(innerHits.getAt(0).getNestedIdentity().getChild().getField().string(), equalTo("remarks"));
assertThat(innerHits.getAt(0).getNestedIdentity().getChild().getOffset(), equalTo(0));
response = client().prepareSearch("articles")
.setQuery(nestedQuery("comments", nestedQuery("comments.remarks", matchQuery("comments.remarks.message", "bad"))))
.addInnerHit("comment", new InnerHitsBuilder.InnerHit()
.setPath("comments")
.setQuery(nestedQuery("comments.remarks", matchQuery("comments.remarks.message", "bad")))
.addInnerHit("remark", new InnerHitsBuilder.InnerHit().setPath("comments.remarks").setQuery(matchQuery("comments.remarks.message", "bad")))
).get();
.addInnerHit("remark", new InnerHitsBuilder.InnerHit().setPath("comments.remarks").setQuery(matchQuery("comments.remarks.message", "bad"))))
.get();
assertNoFailures(response);
assertHitCount(response, 1);
assertSearchHit(response, 1, hasId("2"));