QueryShardContextTests.java 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404
  1. /*
  2. * Licensed to Elasticsearch under one or more contributor
  3. * license agreements. See the NOTICE file distributed with
  4. * this work for additional information regarding copyright
  5. * ownership. Elasticsearch licenses this file to you under
  6. * the Apache License, Version 2.0 (the "License"); you may
  7. * not use this file except in compliance with the License.
  8. * 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. package org.elasticsearch.index.query;
  20. import org.apache.lucene.document.Field;
  21. import org.apache.lucene.document.StringField;
  22. import org.apache.lucene.index.DirectoryReader;
  23. import org.apache.lucene.index.LeafReaderContext;
  24. import org.apache.lucene.index.RandomIndexWriter;
  25. import org.apache.lucene.index.Term;
  26. import org.apache.lucene.search.Collector;
  27. import org.apache.lucene.search.IndexSearcher;
  28. import org.apache.lucene.search.LeafCollector;
  29. import org.apache.lucene.search.MatchAllDocsQuery;
  30. import org.apache.lucene.search.Query;
  31. import org.apache.lucene.search.Scorable;
  32. import org.apache.lucene.search.ScoreMode;
  33. import org.apache.lucene.search.TermQuery;
  34. import org.apache.lucene.store.Directory;
  35. import org.elasticsearch.Version;
  36. import org.elasticsearch.cluster.metadata.IndexMetadata;
  37. import org.elasticsearch.common.TriFunction;
  38. import org.elasticsearch.common.io.stream.NamedWriteableRegistry;
  39. import org.elasticsearch.common.io.stream.StreamOutput;
  40. import org.elasticsearch.common.settings.Settings;
  41. import org.elasticsearch.common.util.BigArrays;
  42. import org.elasticsearch.common.xcontent.NamedXContentRegistry;
  43. import org.elasticsearch.common.xcontent.XContentBuilder;
  44. import org.elasticsearch.index.IndexSettings;
  45. import org.elasticsearch.index.fielddata.IndexFieldData;
  46. import org.elasticsearch.index.fielddata.LeafFieldData;
  47. import org.elasticsearch.index.fielddata.ScriptDocValues;
  48. import org.elasticsearch.index.fielddata.plain.AbstractLeafOrdinalsFieldData;
  49. import org.elasticsearch.index.mapper.IndexFieldMapper;
  50. import org.elasticsearch.index.mapper.MappedFieldType;
  51. import org.elasticsearch.index.mapper.MapperService;
  52. import org.elasticsearch.index.mapper.TextFieldMapper;
  53. import org.elasticsearch.search.lookup.LeafDocLookup;
  54. import org.elasticsearch.search.lookup.LeafSearchLookup;
  55. import org.elasticsearch.search.lookup.SearchLookup;
  56. import org.elasticsearch.test.ESTestCase;
  57. import java.io.IOException;
  58. import java.util.ArrayList;
  59. import java.util.Collections;
  60. import java.util.List;
  61. import java.util.function.BiFunction;
  62. import java.util.function.Supplier;
  63. import static org.hamcrest.Matchers.equalTo;
  64. import static org.hamcrest.Matchers.instanceOf;
  65. import static org.hamcrest.Matchers.notNullValue;
  66. import static org.hamcrest.Matchers.nullValue;
  67. import static org.hamcrest.Matchers.sameInstance;
  68. import static org.mockito.Matchers.any;
  69. import static org.mockito.Mockito.mock;
  70. import static org.mockito.Mockito.when;
  71. public class QueryShardContextTests extends ESTestCase {
  72. public void testFailIfFieldMappingNotFound() {
  73. QueryShardContext context = createQueryShardContext(IndexMetadata.INDEX_UUID_NA_VALUE, null);
  74. context.setAllowUnmappedFields(false);
  75. MappedFieldType fieldType = new TextFieldMapper.TextFieldType("text");
  76. MappedFieldType result = context.failIfFieldMappingNotFound("name", fieldType);
  77. assertThat(result, sameInstance(fieldType));
  78. QueryShardException e = expectThrows(QueryShardException.class, () -> context.failIfFieldMappingNotFound("name", null));
  79. assertEquals("No field mapping can be found for the field with name [name]", e.getMessage());
  80. context.setAllowUnmappedFields(true);
  81. result = context.failIfFieldMappingNotFound("name", fieldType);
  82. assertThat(result, sameInstance(fieldType));
  83. result = context.failIfFieldMappingNotFound("name", null);
  84. assertThat(result, nullValue());
  85. context.setAllowUnmappedFields(false);
  86. context.setMapUnmappedFieldAsString(true);
  87. result = context.failIfFieldMappingNotFound("name", fieldType);
  88. assertThat(result, sameInstance(fieldType));
  89. result = context.failIfFieldMappingNotFound("name", null);
  90. assertThat(result, notNullValue());
  91. assertThat(result, instanceOf(TextFieldMapper.TextFieldType.class));
  92. assertThat(result.name(), equalTo("name"));
  93. }
  94. public void testToQueryFails() {
  95. QueryShardContext context = createQueryShardContext(IndexMetadata.INDEX_UUID_NA_VALUE, null);
  96. Exception exc = expectThrows(Exception.class,
  97. () -> context.toQuery(new AbstractQueryBuilder() {
  98. @Override
  99. public String getWriteableName() {
  100. return null;
  101. }
  102. @Override
  103. protected void doWriteTo(StreamOutput out) throws IOException {
  104. }
  105. @Override
  106. protected void doXContent(XContentBuilder builder, Params params) throws IOException {
  107. }
  108. @Override
  109. protected Query doToQuery(QueryShardContext context) throws IOException {
  110. throw new RuntimeException("boom");
  111. }
  112. @Override
  113. protected boolean doEquals(AbstractQueryBuilder other) {
  114. return false;
  115. }
  116. @Override
  117. protected int doHashCode() {
  118. return 0;
  119. }
  120. }));
  121. assertThat(exc.getMessage(), equalTo("failed to create query: boom"));
  122. }
  123. public void testClusterAlias() throws IOException {
  124. final String clusterAlias = randomBoolean() ? null : "remote_cluster";
  125. QueryShardContext context = createQueryShardContext(IndexMetadata.INDEX_UUID_NA_VALUE, clusterAlias);
  126. IndexFieldMapper mapper = new IndexFieldMapper();
  127. IndexFieldData<?> forField = context.getForField(mapper.fieldType());
  128. String expected = clusterAlias == null ? context.getIndexSettings().getIndexMetadata().getIndex().getName()
  129. : clusterAlias + ":" + context.getIndexSettings().getIndex().getName();
  130. assertEquals(expected, ((AbstractLeafOrdinalsFieldData)forField.load(null)).getOrdinalsValues().lookupOrd(0).utf8ToString());
  131. }
  132. public void testGetFullyQualifiedIndex() {
  133. String clusterAlias = randomAlphaOfLengthBetween(5, 10);
  134. String indexUuid = randomAlphaOfLengthBetween(3, 10);
  135. QueryShardContext shardContext = createQueryShardContext(indexUuid, clusterAlias);
  136. assertThat(shardContext.getFullyQualifiedIndex().getName(), equalTo(clusterAlias + ":index"));
  137. assertThat(shardContext.getFullyQualifiedIndex().getUUID(), equalTo(indexUuid));
  138. }
  139. public void testIndexSortedOnField() {
  140. Settings settings = Settings.builder()
  141. .put(IndexMetadata.SETTING_VERSION_CREATED, Version.CURRENT)
  142. .put(IndexMetadata.SETTING_NUMBER_OF_SHARDS, 1)
  143. .put(IndexMetadata.SETTING_NUMBER_OF_REPLICAS, 1)
  144. .put("index.sort.field", "sort_field")
  145. .build();
  146. IndexMetadata indexMetadata = new IndexMetadata.Builder("index")
  147. .settings(settings)
  148. .build();
  149. IndexSettings indexSettings = new IndexSettings(indexMetadata, settings);
  150. QueryShardContext context = new QueryShardContext(
  151. 0, indexSettings, BigArrays.NON_RECYCLING_INSTANCE, null, null,
  152. null, null, null, NamedXContentRegistry.EMPTY, new NamedWriteableRegistry(Collections.emptyList()),
  153. null, null, () -> 0L, null, null, () -> true, null);
  154. assertTrue(context.indexSortedOnField("sort_field"));
  155. assertFalse(context.indexSortedOnField("second_sort_field"));
  156. assertFalse(context.indexSortedOnField("non_sort_field"));
  157. }
  158. public void testFielddataLookupSelfReference() {
  159. QueryShardContext queryShardContext = createQueryShardContext("uuid", null, (field, leafLookup, docId) -> {
  160. //simulate a runtime field that depends on itself e.g. field: doc['field']
  161. return leafLookup.doc().get(field).toString();
  162. });
  163. IllegalArgumentException iae = expectThrows(IllegalArgumentException.class, () -> collect("field", queryShardContext));
  164. assertEquals("Cyclic dependency detected while resolving runtime fields: field -> field", iae.getMessage());
  165. }
  166. public void testFielddataLookupLooseLoop() {
  167. QueryShardContext queryShardContext = createQueryShardContext("uuid", null, (field, leafLookup, docId) -> {
  168. //simulate a runtime field cycle: 1: doc['2'] 2: doc['3'] 3: doc['4'] 4: doc['1']
  169. if (field.equals("4")) {
  170. return leafLookup.doc().get("1").toString();
  171. }
  172. return leafLookup.doc().get(Integer.toString(Integer.parseInt(field) + 1)).toString();
  173. });
  174. IllegalArgumentException iae = expectThrows(IllegalArgumentException.class, () -> collect("1", queryShardContext));
  175. assertEquals("Cyclic dependency detected while resolving runtime fields: 1 -> 2 -> 3 -> 4 -> 1", iae.getMessage());
  176. }
  177. public void testFielddataLookupTerminatesInLoop() {
  178. QueryShardContext queryShardContext = createQueryShardContext("uuid", null, (field, leafLookup, docId) -> {
  179. //simulate a runtime field cycle: 1: doc['2'] 2: doc['3'] 3: doc['4'] 4: doc['4']
  180. if (field.equals("4")) {
  181. return leafLookup.doc().get("4").toString();
  182. }
  183. return leafLookup.doc().get(Integer.toString(Integer.parseInt(field) + 1)).toString();
  184. });
  185. IllegalArgumentException iae = expectThrows(IllegalArgumentException.class, () -> collect("1", queryShardContext));
  186. assertEquals("Cyclic dependency detected while resolving runtime fields: 1 -> 2 -> 3 -> 4 -> 4", iae.getMessage());
  187. }
  188. public void testFielddataLookupSometimesLoop() throws IOException {
  189. QueryShardContext queryShardContext = createQueryShardContext("uuid", null, (field, leafLookup, docId) -> {
  190. if (docId == 0) {
  191. return field + "_" + docId;
  192. } else {
  193. assert docId == 1;
  194. if (field.equals("field4")) {
  195. return leafLookup.doc().get("field1").toString();
  196. }
  197. int i = Integer.parseInt(field.substring(field.length() - 1));
  198. return leafLookup.doc().get("field" + (i + 1)).toString();
  199. }
  200. });
  201. List<String> values = collect("field1", queryShardContext, new TermQuery(new Term("indexed_field", "first")));
  202. assertEquals(List.of("field1_0"), values);
  203. IllegalArgumentException iae = expectThrows(IllegalArgumentException.class, () -> collect("field1", queryShardContext));
  204. assertEquals("Cyclic dependency detected while resolving runtime fields: field1 -> field2 -> field3 -> field4 -> field1",
  205. iae.getMessage());
  206. }
  207. public void testFielddataLookupBeyondMaxDepth() {
  208. QueryShardContext queryShardContext = createQueryShardContext("uuid", null, (field, leafLookup, docId) -> {
  209. int i = Integer.parseInt(field);
  210. return leafLookup.doc().get(Integer.toString(i + 1)).toString();
  211. });
  212. IllegalArgumentException iae = expectThrows(IllegalArgumentException.class, () -> collect("1", queryShardContext));
  213. assertEquals("Field requires resolving too many dependent fields: 1 -> 2 -> 3 -> 4 -> 5 -> 6", iae.getMessage());
  214. }
  215. public void testFielddataLookupReferencesBelowMaxDepth() throws IOException {
  216. QueryShardContext queryShardContext = createQueryShardContext("uuid", null, (field, leafLookup, docId) -> {
  217. int i = Integer.parseInt(field.substring(field.length() - 1));
  218. if (i == 5) {
  219. return "test";
  220. } else {
  221. ScriptDocValues<?> scriptDocValues = leafLookup.doc().get("field" + (i + 1));
  222. return scriptDocValues.get(0).toString() + docId;
  223. }
  224. });
  225. assertEquals(List.of("test0000", "test1111"), collect("field1", queryShardContext));
  226. }
  227. public void testFielddataLookupOneFieldManyReferences() throws IOException {
  228. int numFields = randomIntBetween(5, 20);
  229. QueryShardContext queryShardContext = createQueryShardContext("uuid", null, (field, leafLookup, docId) -> {
  230. if (field.equals("field")) {
  231. StringBuilder value = new StringBuilder();
  232. for (int i = 0; i < numFields; i++) {
  233. value.append(leafLookup.doc().get("field" + i).get(0));
  234. }
  235. return value.toString();
  236. } else {
  237. return "test" + docId;
  238. }
  239. });
  240. StringBuilder expectedFirstDoc = new StringBuilder();
  241. StringBuilder expectedSecondDoc = new StringBuilder();
  242. for (int i = 0; i < numFields; i++) {
  243. expectedFirstDoc.append("test0");
  244. expectedSecondDoc.append("test1");
  245. }
  246. assertEquals(List.of(expectedFirstDoc.toString(), expectedSecondDoc.toString()), collect("field", queryShardContext));
  247. }
  248. public static QueryShardContext createQueryShardContext(String indexUuid, String clusterAlias) {
  249. return createQueryShardContext(indexUuid, clusterAlias, null);
  250. }
  251. private static QueryShardContext createQueryShardContext(String indexUuid, String clusterAlias,
  252. TriFunction<String, LeafSearchLookup, Integer, String> runtimeDocValues) {
  253. IndexMetadata.Builder indexMetadataBuilder = new IndexMetadata.Builder("index");
  254. indexMetadataBuilder.settings(Settings.builder().put("index.version.created", Version.CURRENT)
  255. .put("index.number_of_shards", 1)
  256. .put("index.number_of_replicas", 1)
  257. .put(IndexMetadata.SETTING_INDEX_UUID, indexUuid)
  258. );
  259. IndexMetadata indexMetadata = indexMetadataBuilder.build();
  260. IndexSettings indexSettings = new IndexSettings(indexMetadata, Settings.EMPTY);
  261. MapperService mapperService = mock(MapperService.class);
  262. when(mapperService.getIndexSettings()).thenReturn(indexSettings);
  263. when(mapperService.index()).thenReturn(indexMetadata.getIndex());
  264. if (runtimeDocValues != null) {
  265. when(mapperService.fieldType(any())).thenAnswer(fieldTypeInv -> {
  266. String fieldName = (String)fieldTypeInv.getArguments()[0];
  267. return mockFieldType(fieldName, (leafSearchLookup, docId) -> runtimeDocValues.apply(fieldName, leafSearchLookup, docId));
  268. });
  269. }
  270. final long nowInMillis = randomNonNegativeLong();
  271. return new QueryShardContext(
  272. 0, indexSettings, BigArrays.NON_RECYCLING_INSTANCE, null,
  273. (mappedFieldType, idxName, searchLookup) -> mappedFieldType.fielddataBuilder(idxName, searchLookup).build(null, null, null),
  274. mapperService, null, null, NamedXContentRegistry.EMPTY, new NamedWriteableRegistry(Collections.emptyList()),
  275. null, null, () -> nowInMillis, clusterAlias, null, () -> true, null);
  276. }
  277. private static MappedFieldType mockFieldType(String fieldName, BiFunction<LeafSearchLookup, Integer, String> runtimeDocValues) {
  278. MappedFieldType fieldType = mock(MappedFieldType.class);
  279. when(fieldType.name()).thenReturn(fieldName);
  280. when(fieldType.fielddataBuilder(any(), any())).thenAnswer(builderInv -> {
  281. @SuppressWarnings("unchecked")
  282. Supplier<SearchLookup> searchLookup = ((Supplier<SearchLookup>) builderInv.getArguments()[1]);
  283. IndexFieldData<?> indexFieldData = mock(IndexFieldData.class);
  284. when(indexFieldData.load(any())).thenAnswer(loadArgs -> {
  285. LeafReaderContext leafReaderContext = (LeafReaderContext) loadArgs.getArguments()[0];
  286. LeafFieldData leafFieldData = mock(LeafFieldData.class);
  287. when(leafFieldData.getScriptValues()).thenAnswer(scriptValuesArgs -> new ScriptDocValues<String>() {
  288. String value;
  289. @Override
  290. public int size() {
  291. return 1;
  292. }
  293. @Override
  294. public String get(int index) {
  295. assert index == 0;
  296. return value;
  297. }
  298. @Override
  299. public void setNextDocId(int docId) {
  300. assert docId >= 0;
  301. LeafSearchLookup leafLookup = searchLookup.get().getLeafSearchLookup(leafReaderContext);
  302. leafLookup.setDocument(docId);
  303. value = runtimeDocValues.apply(leafLookup, docId);
  304. }
  305. });
  306. return leafFieldData;
  307. });
  308. IndexFieldData.Builder builder = mock(IndexFieldData.Builder.class);
  309. when(builder.build(any(), any(), any())).thenAnswer(buildInv -> indexFieldData);
  310. return builder;
  311. });
  312. return fieldType;
  313. }
  314. private static List<String> collect(String field, QueryShardContext queryShardContext) throws IOException {
  315. return collect(field, queryShardContext, new MatchAllDocsQuery());
  316. }
  317. private static List<String> collect(String field, QueryShardContext queryShardContext, Query query) throws IOException {
  318. List<String> result = new ArrayList<>();
  319. try (Directory directory = newDirectory(); RandomIndexWriter indexWriter = new RandomIndexWriter(random(), directory)) {
  320. indexWriter.addDocument(List.of(new StringField("indexed_field", "first", Field.Store.NO)));
  321. indexWriter.addDocument(List.of(new StringField("indexed_field", "second", Field.Store.NO)));
  322. try (DirectoryReader reader = indexWriter.getReader()) {
  323. IndexSearcher searcher = newSearcher(reader);
  324. MappedFieldType fieldType = queryShardContext.fieldMapper(field);
  325. IndexFieldData<?> indexFieldData;
  326. if (randomBoolean()) {
  327. indexFieldData = queryShardContext.getForField(fieldType);
  328. } else {
  329. indexFieldData = queryShardContext.lookup().doc().getForField(fieldType);
  330. }
  331. searcher.search(query, new Collector() {
  332. @Override
  333. public ScoreMode scoreMode() {
  334. return ScoreMode.COMPLETE_NO_SCORES;
  335. }
  336. @Override
  337. public LeafCollector getLeafCollector(LeafReaderContext context) {
  338. ScriptDocValues<?> scriptValues = indexFieldData.load(context).getScriptValues();
  339. return new LeafCollector() {
  340. @Override
  341. public void setScorer(Scorable scorer) {}
  342. @Override
  343. public void collect(int doc) throws IOException {
  344. ScriptDocValues<?> scriptDocValues;
  345. if(randomBoolean()) {
  346. LeafDocLookup leafDocLookup = queryShardContext.lookup().doc().getLeafDocLookup(context);
  347. leafDocLookup.setDocument(doc);
  348. scriptDocValues = leafDocLookup.get(field);
  349. } else {
  350. scriptDocValues = scriptValues;
  351. }
  352. scriptDocValues.setNextDocId(doc);
  353. for (int i = 0; i < scriptDocValues.size(); i++) {
  354. result.add(scriptDocValues.get(i).toString());
  355. }
  356. }
  357. };
  358. }
  359. });
  360. }
  361. return result;
  362. }
  363. }
  364. }