/* * 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.index.query; import org.apache.lucene.search.join.ScoreMode; import org.elasticsearch.common.Strings; import org.elasticsearch.common.io.stream.BytesStreamOutput; import org.elasticsearch.common.io.stream.NamedWriteableRegistry; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.xcontent.NamedXContentRegistry; import org.elasticsearch.common.xcontent.ToXContent; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentFactory; import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.index.query.functionscore.FunctionScoreQueryBuilder; import org.elasticsearch.script.Script; import org.elasticsearch.script.ScriptType; import org.elasticsearch.search.SearchModule; import org.elasticsearch.search.builder.SearchSourceBuilder; import org.elasticsearch.search.fetch.subphase.FetchSourceContext; import org.elasticsearch.search.fetch.subphase.InnerHitsContext; import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilderTests; import org.elasticsearch.search.internal.SearchContext; import org.elasticsearch.search.internal.ShardSearchLocalRequest; import org.elasticsearch.search.sort.SortBuilder; import org.elasticsearch.search.sort.SortBuilders; import org.elasticsearch.search.sort.SortOrder; import org.elasticsearch.test.ESTestCase; import org.junit.AfterClass; import org.junit.BeforeClass; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.function.Supplier; import static java.util.Collections.emptyList; import static org.elasticsearch.test.EqualsHashCodeTestUtils.checkEqualsAndHashCode; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.notNullValue; import static org.hamcrest.Matchers.sameInstance; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; public class InnerHitBuilderTests extends ESTestCase { private static final int NUMBER_OF_TESTBUILDERS = 20; private static NamedWriteableRegistry namedWriteableRegistry; private static NamedXContentRegistry xContentRegistry; @BeforeClass public static void init() { SearchModule searchModule = new SearchModule(Settings.EMPTY, false, emptyList()); namedWriteableRegistry = new NamedWriteableRegistry(searchModule.getNamedWriteables()); xContentRegistry = new NamedXContentRegistry(searchModule.getNamedXContents()); } @AfterClass public static void afterClass() throws Exception { namedWriteableRegistry = null; xContentRegistry = null; } @Override protected NamedXContentRegistry xContentRegistry() { return xContentRegistry; } public void testSerialization() throws Exception { for (int runs = 0; runs < NUMBER_OF_TESTBUILDERS; runs++) { InnerHitBuilder original = randomInnerHits(); InnerHitBuilder deserialized = serializedCopy(original); assertEquals(deserialized, original); assertEquals(deserialized.hashCode(), original.hashCode()); assertNotSame(deserialized, original); } } /** * Test that if we serialize and deserialize an object, further * serialization leads to identical bytes representation. * * This is necessary to ensure because we use the serialized BytesReference * of this builder as part of the cacheKey in * {@link ShardSearchLocalRequest} (via * {@link SearchSourceBuilder#collapse(org.elasticsearch.search.collapse.CollapseBuilder)}) */ public void testSerializationOrder() throws Exception { for (int runs = 0; runs < NUMBER_OF_TESTBUILDERS; runs++) { InnerHitBuilder original = randomInnerHits(); InnerHitBuilder deserialized = serializedCopy(original); assertEquals(deserialized, original); assertEquals(deserialized.hashCode(), original.hashCode()); assertNotSame(deserialized, original); BytesStreamOutput out1 = new BytesStreamOutput(); BytesStreamOutput out2 = new BytesStreamOutput(); original.writeTo(out1); deserialized.writeTo(out2); assertEquals(out1.bytes(), out2.bytes()); } } public void testFromAndToXContent() throws Exception { for (int runs = 0; runs < NUMBER_OF_TESTBUILDERS; runs++) { InnerHitBuilder innerHit = randomInnerHits(true, false); XContentBuilder builder = XContentFactory.contentBuilder(randomFrom(XContentType.values())); innerHit.toXContent(builder, ToXContent.EMPTY_PARAMS); //fields is printed out as an object but parsed into a List where order matters, we disable shuffling XContentBuilder shuffled = shuffleXContent(builder, "fields"); XContentParser parser = createParser(shuffled); QueryParseContext context = new QueryParseContext(parser); InnerHitBuilder secondInnerHits = InnerHitBuilder.fromXContent(context); assertThat(innerHit, not(sameInstance(secondInnerHits))); assertThat(innerHit, equalTo(secondInnerHits)); assertThat(innerHit.hashCode(), equalTo(secondInnerHits.hashCode())); } } public void testEqualsAndHashcode() { for (int runs = 0; runs < NUMBER_OF_TESTBUILDERS; runs++) { checkEqualsAndHashCode(randomInnerHits(), InnerHitBuilderTests::serializedCopy, InnerHitBuilderTests::mutate); } } public void testInlineLeafInnerHitsNestedQuery() throws Exception { InnerHitBuilder leafInnerHits = randomInnerHits(); NestedQueryBuilder nestedQueryBuilder = new NestedQueryBuilder("path", new MatchAllQueryBuilder(), ScoreMode.None); nestedQueryBuilder.innerHit(leafInnerHits, false); Map<String, InnerHitBuilder> innerHitBuilders = new HashMap<>(); nestedQueryBuilder.extractInnerHitBuilders(innerHitBuilders); assertThat(innerHitBuilders.get(leafInnerHits.getName()), notNullValue()); } public void testInlineLeafInnerHitsNestedQueryViaBoolQuery() { InnerHitBuilder leafInnerHits = randomInnerHits(); NestedQueryBuilder nestedQueryBuilder = new NestedQueryBuilder("path", new MatchAllQueryBuilder(), ScoreMode.None) .innerHit(leafInnerHits, false); BoolQueryBuilder boolQueryBuilder = new BoolQueryBuilder().should(nestedQueryBuilder); Map<String, InnerHitBuilder> innerHitBuilders = new HashMap<>(); boolQueryBuilder.extractInnerHitBuilders(innerHitBuilders); assertThat(innerHitBuilders.get(leafInnerHits.getName()), notNullValue()); } public void testInlineLeafInnerHitsNestedQueryViaConstantScoreQuery() { InnerHitBuilder leafInnerHits = randomInnerHits(); NestedQueryBuilder nestedQueryBuilder = new NestedQueryBuilder("path", new MatchAllQueryBuilder(), ScoreMode.None) .innerHit(leafInnerHits, false); ConstantScoreQueryBuilder constantScoreQueryBuilder = new ConstantScoreQueryBuilder(nestedQueryBuilder); Map<String, InnerHitBuilder> innerHitBuilders = new HashMap<>(); constantScoreQueryBuilder.extractInnerHitBuilders(innerHitBuilders); assertThat(innerHitBuilders.get(leafInnerHits.getName()), notNullValue()); } public void testInlineLeafInnerHitsNestedQueryViaBoostingQuery() { InnerHitBuilder leafInnerHits1 = randomInnerHits(); NestedQueryBuilder nestedQueryBuilder1 = new NestedQueryBuilder("path", new MatchAllQueryBuilder(), ScoreMode.None) .innerHit(leafInnerHits1, false); InnerHitBuilder leafInnerHits2 = randomInnerHits(); NestedQueryBuilder nestedQueryBuilder2 = new NestedQueryBuilder("path", new MatchAllQueryBuilder(), ScoreMode.None) .innerHit(leafInnerHits2, false); BoostingQueryBuilder constantScoreQueryBuilder = new BoostingQueryBuilder(nestedQueryBuilder1, nestedQueryBuilder2); Map<String, InnerHitBuilder> innerHitBuilders = new HashMap<>(); constantScoreQueryBuilder.extractInnerHitBuilders(innerHitBuilders); assertThat(innerHitBuilders.get(leafInnerHits1.getName()), notNullValue()); assertThat(innerHitBuilders.get(leafInnerHits2.getName()), notNullValue()); } public void testInlineLeafInnerHitsNestedQueryViaFunctionScoreQuery() { InnerHitBuilder leafInnerHits = randomInnerHits(); NestedQueryBuilder nestedQueryBuilder = new NestedQueryBuilder("path", new MatchAllQueryBuilder(), ScoreMode.None) .innerHit(leafInnerHits, false); FunctionScoreQueryBuilder functionScoreQueryBuilder = new FunctionScoreQueryBuilder(nestedQueryBuilder); Map<String, InnerHitBuilder> innerHitBuilders = new HashMap<>(); ((AbstractQueryBuilder<?>) functionScoreQueryBuilder).extractInnerHitBuilders(innerHitBuilders); assertThat(innerHitBuilders.get(leafInnerHits.getName()), notNullValue()); } public void testBuildIgnoreUnmappedNestQuery() throws Exception { QueryShardContext queryShardContext = mock(QueryShardContext.class); when(queryShardContext.getObjectMapper("path")).thenReturn(null); SearchContext searchContext = mock(SearchContext.class); when(searchContext.getQueryShardContext()).thenReturn(queryShardContext); InnerHitBuilder leafInnerHits = randomInnerHits(); NestedQueryBuilder query1 = new NestedQueryBuilder("path", new MatchAllQueryBuilder(), ScoreMode.None); query1.innerHit(leafInnerHits, false); expectThrows(IllegalStateException.class, () -> query1.innerHit().build(searchContext, new InnerHitsContext())); NestedQueryBuilder query2 = new NestedQueryBuilder("path", new MatchAllQueryBuilder(), ScoreMode.None); query2.innerHit(leafInnerHits, true); InnerHitsContext innerHitsContext = new InnerHitsContext(); query2.innerHit().build(searchContext, innerHitsContext); assertThat(innerHitsContext.getInnerHits().size(), equalTo(0)); } public static InnerHitBuilder randomInnerHits() { return randomInnerHits(true, true); } public static InnerHitBuilder randomInnerHits(boolean recursive, boolean includeQueryTypeOrPath) { InnerHitBuilder innerHits = new InnerHitBuilder(); innerHits.setName(randomAlphaOfLengthBetween(1, 16)); innerHits.setFrom(randomIntBetween(0, 128)); innerHits.setSize(randomIntBetween(0, 128)); innerHits.setExplain(randomBoolean()); innerHits.setVersion(randomBoolean()); innerHits.setTrackScores(randomBoolean()); if (randomBoolean()) { innerHits.setStoredFieldNames(randomListStuff(16, () -> randomAlphaOfLengthBetween(1, 16))); } innerHits.setDocValueFields(randomListStuff(16, () -> randomAlphaOfLengthBetween(1, 16))); // Random script fields deduped on their field name. Map<String, SearchSourceBuilder.ScriptField> scriptFields = new HashMap<>(); for (SearchSourceBuilder.ScriptField field: randomListStuff(16, InnerHitBuilderTests::randomScript)) { scriptFields.put(field.fieldName(), field); } innerHits.setScriptFields(new HashSet<>(scriptFields.values())); FetchSourceContext randomFetchSourceContext; int randomInt = randomIntBetween(0, 2); if (randomInt == 0) { randomFetchSourceContext = new FetchSourceContext(true, Strings.EMPTY_ARRAY, Strings.EMPTY_ARRAY); } else if (randomInt == 1) { randomFetchSourceContext = new FetchSourceContext(true, generateRandomStringArray(12, 16, false), generateRandomStringArray(12, 16, false) ); } else { randomFetchSourceContext = new FetchSourceContext(randomBoolean()); } innerHits.setFetchSourceContext(randomFetchSourceContext); if (randomBoolean()) { innerHits.setSorts(randomListStuff(16, () -> SortBuilders.fieldSort(randomAlphaOfLengthBetween(5, 20)).order(randomFrom(SortOrder.values()))) ); } innerHits.setHighlightBuilder(HighlightBuilderTests.randomHighlighterBuilder()); if (recursive && randomBoolean()) { int size = randomIntBetween(1, 16); for (int i = 0; i < size; i++) { innerHits.addChildInnerHit(randomInnerHits(false, includeQueryTypeOrPath)); } } if (includeQueryTypeOrPath) { QueryBuilder query = new MatchQueryBuilder(randomAlphaOfLengthBetween(1, 16), randomAlphaOfLengthBetween(1, 16)); if (randomBoolean()) { return new InnerHitBuilder(innerHits, randomAlphaOfLength(8), query, randomBoolean()); } else { return new InnerHitBuilder(innerHits, query, randomAlphaOfLength(8), randomBoolean()); } } else { return innerHits; } } public void testCopyConstructor() throws Exception { InnerHitBuilder original = randomInnerHits(); InnerHitBuilder copy = original.getNestedPath() != null ? new InnerHitBuilder(original, original.getNestedPath(), original.getQuery(), original.isIgnoreUnmapped()) : new InnerHitBuilder(original, original.getQuery(), original.getParentChildType(), original.isIgnoreUnmapped()); assertThat(copy, equalTo(original)); copy = mutate(copy); assertThat(copy, not(equalTo(original))); } static InnerHitBuilder mutate(InnerHitBuilder original) throws IOException { final InnerHitBuilder copy = serializedCopy(original); List<Runnable> modifiers = new ArrayList<>(12); modifiers.add(() -> copy.setFrom(randomValueOtherThan(copy.getFrom(), () -> randomIntBetween(0, 128)))); modifiers.add(() -> copy.setSize(randomValueOtherThan(copy.getSize(), () -> randomIntBetween(0, 128)))); modifiers.add(() -> copy.setExplain(!copy.isExplain())); modifiers.add(() -> copy.setVersion(!copy.isVersion())); modifiers.add(() -> copy.setTrackScores(!copy.isTrackScores())); modifiers.add(() -> copy.setName(randomValueOtherThan(copy.getName(), () -> randomAlphaOfLengthBetween(1, 16)))); modifiers.add(() -> { if (randomBoolean()) { copy.setDocValueFields(randomValueOtherThan(copy.getDocValueFields(), () -> { return randomListStuff(16, () -> randomAlphaOfLengthBetween(1, 16)); })); } else { copy.addDocValueField(randomAlphaOfLengthBetween(1, 16)); } }); modifiers.add(() -> { if (randomBoolean()) { copy.setScriptFields(randomValueOtherThan(copy.getScriptFields(), () -> { return new HashSet<>(randomListStuff(16, InnerHitBuilderTests::randomScript)); })); } else { SearchSourceBuilder.ScriptField script = randomScript(); copy.addScriptField(script.fieldName(), script.script()); } }); modifiers.add(() -> copy.setFetchSourceContext(randomValueOtherThan(copy.getFetchSourceContext(), () -> { FetchSourceContext randomFetchSourceContext; if (randomBoolean()) { randomFetchSourceContext = new FetchSourceContext(randomBoolean()); } else { randomFetchSourceContext = new FetchSourceContext(true, generateRandomStringArray(12, 16, false), generateRandomStringArray(12, 16, false)); } return randomFetchSourceContext; }))); modifiers.add(() -> { if (randomBoolean()) { final List<SortBuilder<?>> sortBuilders = randomValueOtherThan(copy.getSorts(), () -> { List<SortBuilder<?>> builders = randomListStuff(16, () -> SortBuilders.fieldSort(randomAlphaOfLengthBetween(5, 20)).order(randomFrom(SortOrder.values()))); return builders; }); copy.setSorts(sortBuilders); } else { copy.addSort(SortBuilders.fieldSort(randomAlphaOfLengthBetween(5, 20))); } }); modifiers.add(() -> copy .setHighlightBuilder(randomValueOtherThan(copy.getHighlightBuilder(), HighlightBuilderTests::randomHighlighterBuilder))); modifiers.add(() -> { if (copy.getStoredFieldsContext() == null || randomBoolean()) { List<String> previous = copy.getStoredFieldsContext() == null ? Collections.emptyList() : copy.getStoredFieldsContext().fieldNames(); List<String> newValues = randomValueOtherThan(previous, () -> randomListStuff(1, 16, () -> randomAlphaOfLengthBetween(1, 16))); copy.setStoredFieldNames(newValues); } else { copy.getStoredFieldsContext().addFieldName(randomAlphaOfLengthBetween(1, 16)); } }); randomFrom(modifiers).run(); return copy; } static SearchSourceBuilder.ScriptField randomScript() { ScriptType randomScriptType = randomFrom(ScriptType.values()); Map<String, Object> randomMap = new HashMap<>(); if (randomBoolean()) { int numEntries = randomIntBetween(0, 32); for (int i = 0; i < numEntries; i++) { randomMap.put(String.valueOf(i), randomAlphaOfLength(16)); } } Script script = new Script(randomScriptType, randomScriptType == ScriptType.STORED ? null : randomAlphaOfLengthBetween(1, 4), randomAlphaOfLength(128), randomMap); return new SearchSourceBuilder.ScriptField(randomAlphaOfLengthBetween(1, 32), script, randomBoolean()); } static <T> List<T> randomListStuff(int maxSize, Supplier<T> valueSupplier) { return randomListStuff(0, maxSize, valueSupplier); } static <T> List<T> randomListStuff(int minSize, int maxSize, Supplier<T> valueSupplier) { int size = randomIntBetween(minSize, maxSize); List<T> list = new ArrayList<>(size); for (int i = 0; i < size; i++) { list.add(valueSupplier.get()); } return list; } private static InnerHitBuilder serializedCopy(InnerHitBuilder original) throws IOException { return ESTestCase.copyWriteable(original, namedWriteableRegistry, InnerHitBuilder::new); } }