/* * 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.TermInSetQuery; import org.apache.lucene.search.BooleanQuery; import org.apache.lucene.search.ConstantScoreQuery; import org.apache.lucene.search.MatchNoDocsQuery; import org.apache.lucene.search.PointInSetQuery; import org.apache.lucene.search.Query; import org.apache.lucene.util.BytesRef; import org.elasticsearch.ElasticsearchException; import org.elasticsearch.action.get.GetRequest; import org.elasticsearch.action.get.GetResponse; import org.elasticsearch.common.ParsingException; import org.elasticsearch.common.bytes.BytesArray; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentFactory; import org.elasticsearch.index.get.GetResult; import org.elasticsearch.indices.TermsLookup; import org.elasticsearch.search.internal.SearchContext; import org.elasticsearch.test.AbstractQueryTestCase; import org.junit.Before; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Objects; import java.util.stream.Collectors; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.either; import static org.hamcrest.Matchers.instanceOf; public class TermsQueryBuilderTests extends AbstractQueryTestCase<TermsQueryBuilder> { private List<Object> randomTerms; private String termsPath; @Before public void randomTerms() { List<Object> randomTerms = new ArrayList<>(); String[] strings = generateRandomStringArray(10, 10, false, true); for (String string : strings) { randomTerms.add(string); if (rarely()) { randomTerms.add(null); } } this.randomTerms = randomTerms; termsPath = randomAlphaOfLength(10).replace('.', '_'); } @Override protected TermsQueryBuilder doCreateTestQueryBuilder() { TermsQueryBuilder query; // terms query or lookup query if (randomBoolean()) { // make between 0 and 5 different values of the same type String fieldName; do { fieldName = getRandomFieldName(); } while (fieldName.equals(GEO_POINT_FIELD_NAME) || fieldName.equals(GEO_SHAPE_FIELD_NAME) || fieldName.equals(INT_RANGE_FIELD_NAME) || fieldName.equals(DATE_RANGE_FIELD_NAME)); Object[] values = new Object[randomInt(5)]; for (int i = 0; i < values.length; i++) { values[i] = getRandomValueForFieldName(fieldName); } query = new TermsQueryBuilder(fieldName, values); } else { // right now the mock service returns us a list of strings query = new TermsQueryBuilder(randomBoolean() ? randomAlphaOfLengthBetween(1,10) : STRING_FIELD_NAME, randomTermsLookup()); } return query; } private TermsLookup randomTermsLookup() { return new TermsLookup(randomBoolean() ? randomAlphaOfLength(10) : null, randomAlphaOfLength(10), randomAlphaOfLength(10), termsPath).routing(randomBoolean() ? randomAlphaOfLength(10) : null); } @Override protected void doAssertLuceneQuery(TermsQueryBuilder queryBuilder, Query query, SearchContext context) throws IOException { if (queryBuilder.termsLookup() == null && (queryBuilder.values() == null || queryBuilder.values().isEmpty())) { assertThat(query, instanceOf(MatchNoDocsQuery.class)); MatchNoDocsQuery matchNoDocsQuery = (MatchNoDocsQuery) query; assertThat(matchNoDocsQuery.toString(), containsString("No terms supplied for \"terms\" query.")); } else if (queryBuilder.termsLookup() != null && randomTerms.size() == 0){ assertThat(query, instanceOf(MatchNoDocsQuery.class)); MatchNoDocsQuery matchNoDocsQuery = (MatchNoDocsQuery) query; assertThat(matchNoDocsQuery.toString(), containsString("No terms supplied for \"terms\" query.")); } else { assertThat(query, either(instanceOf(TermInSetQuery.class)) .or(instanceOf(PointInSetQuery.class)) .or(instanceOf(ConstantScoreQuery.class))); if (query instanceof ConstantScoreQuery) { assertThat(((ConstantScoreQuery) query).getQuery(), instanceOf(BooleanQuery.class)); } // we only do the check below for string fields (otherwise we'd have to decode the values) if (queryBuilder.fieldName().equals(INT_FIELD_NAME) || queryBuilder.fieldName().equals(DOUBLE_FIELD_NAME) || queryBuilder.fieldName().equals(BOOLEAN_FIELD_NAME) || queryBuilder.fieldName().equals(DATE_FIELD_NAME)) { return; } // expected returned terms depending on whether we have a terms query or a terms lookup query List<Object> terms; if (queryBuilder.termsLookup() != null) { terms = randomTerms; } else { terms = queryBuilder.values(); } TermInSetQuery expected = new TermInSetQuery(queryBuilder.fieldName(), terms.stream().filter(Objects::nonNull).map(Object::toString).map(BytesRef::new).collect(Collectors.toList())); assertEquals(expected, query); } } public void testEmtpyFieldName() { IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> new TermsQueryBuilder(null, "term")); assertEquals("field name cannot be null.", e.getMessage()); e = expectThrows(IllegalArgumentException.class, () -> new TermsQueryBuilder("", "term")); assertEquals("field name cannot be null.", e.getMessage()); } public void testEmtpyTermsLookup() { IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> new TermsQueryBuilder("field", (TermsLookup) null)); assertEquals("No value or termsLookup specified for terms query", e.getMessage()); } public void testNullValues() { IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> new TermsQueryBuilder("field", (String[]) null)); assertThat(e.getMessage(), containsString("No value specified for terms query")); e = expectThrows(IllegalArgumentException.class, () -> new TermsQueryBuilder("field", (int[]) null)); assertThat(e.getMessage(), containsString("No value specified for terms query")); e = expectThrows(IllegalArgumentException.class, () -> new TermsQueryBuilder("field", (long[]) null)); assertThat(e.getMessage(), containsString("No value specified for terms query")); e = expectThrows(IllegalArgumentException.class, () -> new TermsQueryBuilder("field", (float[]) null)); assertThat(e.getMessage(), containsString("No value specified for terms query")); e = expectThrows(IllegalArgumentException.class, () -> new TermsQueryBuilder("field", (double[]) null)); assertThat(e.getMessage(), containsString("No value specified for terms query")); e = expectThrows(IllegalArgumentException.class, () -> new TermsQueryBuilder("field", (Object[]) null)); assertThat(e.getMessage(), containsString("No value specified for terms query")); e = expectThrows(IllegalArgumentException.class, () -> new TermsQueryBuilder("field", (Iterable<?>) null)); assertThat(e.getMessage(), containsString("No value specified for terms query")); } public void testBothValuesAndLookupSet() throws IOException { String query = "{\n" + " \"terms\": {\n" + " \"field\": [\n" + " \"blue\",\n" + " \"pill\"\n" + " ],\n" + " \"field_lookup\": {\n" + " \"index\": \"pills\",\n" + " \"type\": \"red\",\n" + " \"id\": \"3\",\n" + " \"path\": \"white rabbit\"\n" + " }\n" + " }\n" + "}"; ParsingException e = expectThrows(ParsingException.class, () -> parseQuery(query)); assertThat(e.getMessage(), containsString("[" + TermsQueryBuilder.NAME + "] query does not support more than one field.")); } @Override public GetResponse executeGet(GetRequest getRequest) { String json; try { XContentBuilder builder = XContentFactory.jsonBuilder().prettyPrint(); builder.startObject(); builder.array(termsPath, randomTerms.toArray(new Object[randomTerms.size()])); builder.endObject(); json = builder.string(); } catch (IOException ex) { throw new ElasticsearchException("boom", ex); } return new GetResponse(new GetResult(getRequest.index(), getRequest.type(), getRequest.id(), 0, true, new BytesArray(json), null)); } public void testNumeric() throws IOException { { TermsQueryBuilder builder = new TermsQueryBuilder("foo", new int[]{1, 3, 4}); TermsQueryBuilder copy = (TermsQueryBuilder) assertSerialization(builder); List<Object> values = copy.values(); assertEquals(Arrays.asList(1L, 3L, 4L), values); } { TermsQueryBuilder builder = new TermsQueryBuilder("foo", new double[]{1, 3, 4}); TermsQueryBuilder copy = (TermsQueryBuilder) assertSerialization(builder); List<Object> values = copy.values(); assertEquals(Arrays.asList(1d, 3d, 4d), values); } { TermsQueryBuilder builder = new TermsQueryBuilder("foo", new float[]{1, 3, 4}); TermsQueryBuilder copy = (TermsQueryBuilder) assertSerialization(builder); List<Object> values = copy.values(); assertEquals(Arrays.asList(1f, 3f, 4f), values); } { TermsQueryBuilder builder = new TermsQueryBuilder("foo", new long[]{1, 3, 4}); TermsQueryBuilder copy = (TermsQueryBuilder) assertSerialization(builder); List<Object> values = copy.values(); assertEquals(Arrays.asList(1L, 3L, 4L), values); } } public void testTermsQueryWithMultipleFields() throws IOException { String query = XContentFactory.jsonBuilder().startObject() .startObject("terms").array("foo", 123).array("bar", 456).endObject() .endObject().string(); ParsingException e = expectThrows(ParsingException.class, () -> parseQuery(query)); assertEquals("[" + TermsQueryBuilder.NAME + "] query does not support multiple fields", e.getMessage()); } public void testFromJson() throws IOException { String json = "{\n" + " \"terms\" : {\n" + " \"user\" : [ \"kimchy\", \"elasticsearch\" ],\n" + " \"boost\" : 1.0\n" + " }\n" + "}"; TermsQueryBuilder parsed = (TermsQueryBuilder) parseQuery(json); checkGeneratedJson(json, parsed); assertEquals(json, 2, parsed.values().size()); } @Override public void testMustRewrite() throws IOException { TermsQueryBuilder termsQueryBuilder = new TermsQueryBuilder(STRING_FIELD_NAME, randomTermsLookup()); UnsupportedOperationException e = expectThrows(UnsupportedOperationException.class, () -> termsQueryBuilder.toQuery(createShardContext())); assertEquals("query must be rewritten first", e.getMessage()); assertEquals(termsQueryBuilder.rewrite(createShardContext()), new TermsQueryBuilder(STRING_FIELD_NAME, randomTerms.stream().filter(x -> x != null).collect(Collectors.toList()))); // terms lookup removes null values } public void testGeo() throws Exception { assumeTrue("test runs only when at least a type is registered", getCurrentTypes().length > 0); TermsQueryBuilder query = new TermsQueryBuilder(GEO_POINT_FIELD_NAME, "2,3"); QueryShardContext context = createShardContext(); QueryShardException e = expectThrows(QueryShardException.class, () -> query.toQuery(context)); assertEquals("Geo fields do not support exact searching, use dedicated geo queries instead: [mapped_geo_point]", e.getMessage()); } @Override protected boolean isCachable(TermsQueryBuilder queryBuilder) { // even though we use a terms lookup here we do this during rewrite and that means we are cachable on toQuery // that's why we return true here all the time return super.isCachable(queryBuilder); } public void testConversion() { List<Object> list = Arrays.asList(); assertSame(Collections.emptyList(), TermsQueryBuilder.convert(list)); assertEquals(list, TermsQueryBuilder.convertBack(TermsQueryBuilder.convert(list))); list = Arrays.asList("abc"); assertEquals(Arrays.asList(new BytesRef("abc")), TermsQueryBuilder.convert(list)); assertEquals(list, TermsQueryBuilder.convertBack(TermsQueryBuilder.convert(list))); list = Arrays.asList("abc", new BytesRef("def")); assertEquals(Arrays.asList(new BytesRef("abc"), new BytesRef("def")), TermsQueryBuilder.convert(list)); assertEquals(Arrays.asList("abc", "def"), TermsQueryBuilder.convertBack(TermsQueryBuilder.convert(list))); list = Arrays.asList(5, 42L); assertEquals(Arrays.asList(5L, 42L), TermsQueryBuilder.convert(list)); assertEquals(Arrays.asList(5L, 42L), TermsQueryBuilder.convertBack(TermsQueryBuilder.convert(list))); list = Arrays.asList(5, 42d); assertEquals(Arrays.asList(5, 42d), TermsQueryBuilder.convert(list)); assertEquals(Arrays.asList(5, 42d), TermsQueryBuilder.convertBack(TermsQueryBuilder.convert(list))); } }