/* * 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.index.Term; import org.apache.lucene.search.BooleanClause; import org.apache.lucene.search.BooleanQuery; import org.apache.lucene.search.BoostQuery; import org.apache.lucene.search.FuzzyQuery; import org.apache.lucene.search.MatchAllDocsQuery; import org.apache.lucene.search.MatchNoDocsQuery; import org.apache.lucene.search.PrefixQuery; import org.apache.lucene.search.Query; import org.apache.lucene.search.TermQuery; import org.apache.lucene.util.TestUtil; import org.elasticsearch.cluster.metadata.MetaData; import org.elasticsearch.common.ParsingException; import org.elasticsearch.index.mapper.MapperService; import org.elasticsearch.search.internal.SearchContext; import org.elasticsearch.test.AbstractQueryTestCase; import java.io.IOException; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.Locale; import java.util.Map; import java.util.Set; import static org.hamcrest.Matchers.anyOf; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.notNullValue; public class SimpleQueryStringBuilderTests extends AbstractQueryTestCase<SimpleQueryStringBuilder> { @Override protected SimpleQueryStringBuilder doCreateTestQueryBuilder() { SimpleQueryStringBuilder result = new SimpleQueryStringBuilder(randomAlphaOfLengthBetween(1, 10)); if (randomBoolean()) { result.analyzeWildcard(randomBoolean()); } if (randomBoolean()) { result.minimumShouldMatch(randomMinimumShouldMatch()); } if (randomBoolean()) { result.analyzer(randomAnalyzer()); } if (randomBoolean()) { result.defaultOperator(randomFrom(Operator.values())); } if (randomBoolean()) { result.quoteFieldSuffix(TestUtil.randomSimpleString(random())); } if (randomBoolean()) { Set<SimpleQueryStringFlag> flagSet = new HashSet<>(); int size = randomIntBetween(0, SimpleQueryStringFlag.values().length); for (int i = 0; i < size; i++) { flagSet.add(randomFrom(SimpleQueryStringFlag.values())); } if (flagSet.size() > 0) { result.flags(flagSet.toArray(new SimpleQueryStringFlag[flagSet.size()])); } } int fieldCount = randomIntBetween(0, 10); Map<String, Float> fields = new HashMap<>(); for (int i = 0; i < fieldCount; i++) { if (randomBoolean()) { fields.put(randomAlphaOfLengthBetween(1, 10), AbstractQueryBuilder.DEFAULT_BOOST); } else { fields.put(randomBoolean() ? STRING_FIELD_NAME : randomAlphaOfLengthBetween(1, 10), 2.0f / randomIntBetween(1, 20)); } } result.fields(fields); return result; } public void testDefaults() { SimpleQueryStringBuilder qb = new SimpleQueryStringBuilder("The quick brown fox."); assertEquals("Wrong default default boost.", AbstractQueryBuilder.DEFAULT_BOOST, qb.boost(), 0.001); assertEquals("Wrong default default boost field.", AbstractQueryBuilder.DEFAULT_BOOST, SimpleQueryStringBuilder.DEFAULT_BOOST, 0.001); assertEquals("Wrong default flags.", SimpleQueryStringFlag.ALL.value, qb.flags()); assertEquals("Wrong default flags field.", SimpleQueryStringFlag.ALL.value(), SimpleQueryStringBuilder.DEFAULT_FLAGS); assertEquals("Wrong default default operator.", Operator.OR, qb.defaultOperator()); assertEquals("Wrong default default operator field.", Operator.OR, SimpleQueryStringBuilder.DEFAULT_OPERATOR); assertEquals("Wrong default default analyze_wildcard.", false, qb.analyzeWildcard()); assertEquals("Wrong default default analyze_wildcard field.", false, SimpleQueryStringBuilder.DEFAULT_ANALYZE_WILDCARD); assertEquals("Wrong default default lenient.", false, qb.lenient()); assertEquals("Wrong default default lenient field.", false, SimpleQueryStringBuilder.DEFAULT_LENIENT); } public void testDefaultNullComplainFlags() { SimpleQueryStringBuilder qb = new SimpleQueryStringBuilder("The quick brown fox."); qb.flags((SimpleQueryStringFlag[]) null); assertEquals("Setting flags to null should result in returning to default value.", SimpleQueryStringBuilder.DEFAULT_FLAGS, qb.flags()); } public void testDefaultEmptyComplainFlags() { SimpleQueryStringBuilder qb = new SimpleQueryStringBuilder("The quick brown fox."); qb.flags(new SimpleQueryStringFlag[]{}); assertEquals("Setting flags to empty should result in returning to default value.", SimpleQueryStringBuilder.DEFAULT_FLAGS, qb.flags()); } public void testDefaultNullComplainOp() { SimpleQueryStringBuilder qb = new SimpleQueryStringBuilder("The quick brown fox."); qb.defaultOperator(null); assertEquals("Setting operator to null should result in returning to default value.", SimpleQueryStringBuilder.DEFAULT_OPERATOR, qb.defaultOperator()); } // Check operator handling, and default field handling. public void testDefaultOperatorHandling() throws IOException { SimpleQueryStringBuilder qb = new SimpleQueryStringBuilder("The quick brown fox.").field(STRING_FIELD_NAME); QueryShardContext shardContext = createShardContext(); shardContext.setAllowUnmappedFields(true); // to avoid occasional cases // in setup where we didn't // add types but strict field // resolution BooleanQuery boolQuery = (BooleanQuery) qb.toQuery(shardContext); assertThat(shouldClauses(boolQuery), is(4)); qb.defaultOperator(Operator.AND); boolQuery = (BooleanQuery) qb.toQuery(shardContext); assertThat(shouldClauses(boolQuery), is(0)); qb.defaultOperator(Operator.OR); boolQuery = (BooleanQuery) qb.toQuery(shardContext); assertThat(shouldClauses(boolQuery), is(4)); } public void testIllegalConstructorArg() { expectThrows(IllegalArgumentException.class, () -> new SimpleQueryStringBuilder((String) null)); } public void testFieldCannotBeNull() { SimpleQueryStringBuilder qb = createTestQueryBuilder(); IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> qb.field(null)); assertEquals("supplied field is null or empty", e.getMessage()); } public void testFieldCannotBeNullAndWeighted() { SimpleQueryStringBuilder qb = createTestQueryBuilder(); IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> qb.field(null, AbstractQueryBuilder.DEFAULT_BOOST)); assertEquals("supplied field is null or empty", e.getMessage()); } public void testFieldCannotBeEmpty() { SimpleQueryStringBuilder qb = createTestQueryBuilder(); IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> qb.field("")); assertEquals("supplied field is null or empty", e.getMessage()); } public void testFieldCannotBeEmptyAndWeighted() { SimpleQueryStringBuilder qb = createTestQueryBuilder(); IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> qb.field("", AbstractQueryBuilder.DEFAULT_BOOST)); assertEquals("supplied field is null or empty", e.getMessage()); } /** * The following should fail fast - never silently set the map containing * fields and weights to null but refuse to accept null instead. * */ public void testFieldsCannotBeSetToNull() { SimpleQueryStringBuilder qb = createTestQueryBuilder(); NullPointerException e = expectThrows(NullPointerException.class, () -> qb.fields(null)); assertEquals("fields cannot be null", e.getMessage()); } public void testDefaultFieldParsing() throws IOException { String query = randomAlphaOfLengthBetween(1, 10).toLowerCase(Locale.ROOT); String contentString = "{\n" + " \"simple_query_string\" : {\n" + " \"query\" : \"" + query + "\"" + " }\n" + "}"; SimpleQueryStringBuilder queryBuilder = (SimpleQueryStringBuilder) parseQuery(contentString); assertThat(queryBuilder.value(), equalTo(query)); assertThat(queryBuilder.fields(), notNullValue()); assertThat(queryBuilder.fields().size(), equalTo(0)); QueryShardContext shardContext = createShardContext(); // the remaining tests requires either a mapping that we register with types in base test setup if (getCurrentTypes().length > 0) { Query luceneQuery = queryBuilder.toQuery(shardContext); assertThat(luceneQuery, instanceOf(BooleanQuery.class)); } } /* * This assumes that Lucene query parsing is being checked already, adding * checks only for our parsing extensions. * * Also this relies on {@link SimpleQueryStringTests} to test most of the * actual functionality of query parsing. */ @Override protected void doAssertLuceneQuery(SimpleQueryStringBuilder queryBuilder, Query query, SearchContext context) throws IOException { assertThat(query, notNullValue()); if ("".equals(queryBuilder.value())) { assertThat(query, instanceOf(MatchNoDocsQuery.class)); } else if (queryBuilder.fields().size() > 1) { assertThat(query, instanceOf(BooleanQuery.class)); BooleanQuery boolQuery = (BooleanQuery) query; for (BooleanClause clause : boolQuery.clauses()) { if (clause.getQuery() instanceof TermQuery) { TermQuery inner = (TermQuery) clause.getQuery(); assertThat(inner.getTerm().bytes().toString(), is(inner.getTerm().bytes().toString().toLowerCase(Locale.ROOT))); } } assertThat(boolQuery.clauses().size(), equalTo(queryBuilder.fields().size())); Iterator<Map.Entry<String, Float>> fieldsIterator = queryBuilder.fields().entrySet().iterator(); for (BooleanClause booleanClause : boolQuery) { Map.Entry<String, Float> field = fieldsIterator.next(); assertTermOrBoostQuery(booleanClause.getQuery(), field.getKey(), queryBuilder.value(), field.getValue()); } /** * TODO: * Test disabled because we cannot check min should match consistently: * https://github.com/elastic/elasticsearch/issues/23966 * if (queryBuilder.minimumShouldMatch() != null && !boolQuery.isCoordDisabled()) { assertThat(boolQuery.getMinimumNumberShouldMatch(), greaterThan(0)); } * **/ } else if (queryBuilder.fields().size() == 1) { Map.Entry<String, Float> field = queryBuilder.fields().entrySet().iterator().next(); assertTermOrBoostQuery(query, field.getKey(), queryBuilder.value(), field.getValue()); } else if (queryBuilder.fields().size() == 0) { MapperService ms = context.mapperService(); if (ms.allEnabled()) { assertTermQuery(query, MetaData.ALL, queryBuilder.value()); } else { assertThat(query.getClass(), anyOf(equalTo(BooleanQuery.class), equalTo(MatchNoDocsQuery.class))); } } else { fail("Encountered lucene query type we do not have a validation implementation for in our " + SimpleQueryStringBuilderTests.class.getSimpleName()); } } private static int shouldClauses(BooleanQuery query) { int result = 0; for (BooleanClause c : query.clauses()) { if (c.getOccur() == BooleanClause.Occur.SHOULD) { result++; } } return result; } public void testToQueryBoost() throws IOException { assumeTrue("test runs only when at least a type is registered", getCurrentTypes().length > 0); QueryShardContext shardContext = createShardContext(); SimpleQueryStringBuilder simpleQueryStringBuilder = new SimpleQueryStringBuilder("test"); simpleQueryStringBuilder.field(STRING_FIELD_NAME, 5); Query query = simpleQueryStringBuilder.toQuery(shardContext); assertThat(query, instanceOf(BoostQuery.class)); BoostQuery boostQuery = (BoostQuery) query; assertThat(boostQuery.getBoost(), equalTo(5f)); assertThat(boostQuery.getQuery(), instanceOf(TermQuery.class)); simpleQueryStringBuilder = new SimpleQueryStringBuilder("test"); simpleQueryStringBuilder.field(STRING_FIELD_NAME, 5); simpleQueryStringBuilder.boost(2); query = simpleQueryStringBuilder.toQuery(shardContext); boostQuery = (BoostQuery) query; assertThat(boostQuery.getBoost(), equalTo(2f)); assertThat(boostQuery.getQuery(), instanceOf(BoostQuery.class)); boostQuery = (BoostQuery) boostQuery.getQuery(); assertThat(boostQuery.getBoost(), equalTo(5f)); assertThat(boostQuery.getQuery(), instanceOf(TermQuery.class)); } public void testNegativeFlags() throws IOException { String query = "{\"simple_query_string\": {\"query\": \"foo bar\", \"flags\": -1}}"; SimpleQueryStringBuilder builder = new SimpleQueryStringBuilder("foo bar"); builder.flags(SimpleQueryStringFlag.ALL); assertParsedQuery(query, builder); SimpleQueryStringBuilder otherBuilder = new SimpleQueryStringBuilder("foo bar"); otherBuilder.flags(-1); assertThat(builder, equalTo(otherBuilder)); } public void testFromJson() throws IOException { String json = "{\n" + " \"simple_query_string\" : {\n" + " \"query\" : \"\\\"fried eggs\\\" +(eggplant | potato) -frittata\",\n" + " \"fields\" : [ \"_all^1.0\", \"body^5.0\" ],\n" + " \"analyzer\" : \"snowball\",\n" + " \"flags\" : -1,\n" + " \"default_operator\" : \"and\",\n" + " \"lenient\" : false,\n" + " \"analyze_wildcard\" : false,\n" + " \"quote_field_suffix\" : \".quote\",\n" + " \"boost\" : 1.0\n" + " }\n" + "}"; SimpleQueryStringBuilder parsed = (SimpleQueryStringBuilder) parseQuery(json); checkGeneratedJson(json, parsed); assertEquals(json, "\"fried eggs\" +(eggplant | potato) -frittata", parsed.value()); assertEquals(json, 2, parsed.fields().size()); assertEquals(json, "snowball", parsed.analyzer()); assertEquals(json, ".quote", parsed.quoteFieldSuffix()); } @AwaitsFix(bugUrl = "Waiting on fix for minimumShouldMatch https://github.com/elastic/elasticsearch/issues/23966") public void testMinimumShouldMatch() throws IOException { QueryShardContext shardContext = createShardContext(); int numberOfTerms = randomIntBetween(1, 4); StringBuilder queryString = new StringBuilder(); for (int i = 0; i < numberOfTerms; i++) { queryString.append("t" + i + " "); } SimpleQueryStringBuilder simpleQueryStringBuilder = new SimpleQueryStringBuilder(queryString.toString().trim()); if (randomBoolean()) { simpleQueryStringBuilder.defaultOperator(Operator.AND); } int numberOfFields = randomIntBetween(1, 4); for (int i = 0; i < numberOfFields; i++) { simpleQueryStringBuilder.field("f" + i); } int percent = randomIntBetween(1, 100); simpleQueryStringBuilder.minimumShouldMatch(percent + "%"); Query query = simpleQueryStringBuilder.toQuery(shardContext); // check special case: one term & one field should get simplified to a TermQuery if (numberOfFields * numberOfTerms == 1) { assertThat(query, instanceOf(TermQuery.class)); } else { assertThat(query, instanceOf(BooleanQuery.class)); BooleanQuery boolQuery = (BooleanQuery) query; int expectedMinimumShouldMatch = numberOfTerms * percent / 100; if (numberOfTerms == 1 || simpleQueryStringBuilder.defaultOperator().equals(Operator.AND)) { expectedMinimumShouldMatch = 0; } assertEquals(expectedMinimumShouldMatch, boolQuery.getMinimumNumberShouldMatch()); } } public void testIndexMetaField() throws IOException { QueryShardContext shardContext = createShardContext(); SimpleQueryStringBuilder simpleQueryStringBuilder = new SimpleQueryStringBuilder(getIndex().getName()); simpleQueryStringBuilder.field("_index"); Query query = simpleQueryStringBuilder.toQuery(shardContext); assertThat(query, notNullValue()); if (getCurrentTypes().length > 0) { assertThat(query, instanceOf(MatchAllDocsQuery.class)); } } public void testExpandedTerms() throws Exception { // Prefix Query query = new SimpleQueryStringBuilder("aBc*") .field(STRING_FIELD_NAME) .analyzer("whitespace") .toQuery(createShardContext()); assertEquals(new PrefixQuery(new Term(STRING_FIELD_NAME, "aBc")), query); query = new SimpleQueryStringBuilder("aBc*") .field(STRING_FIELD_NAME) .analyzer("standard") .toQuery(createShardContext()); assertEquals(new PrefixQuery(new Term(STRING_FIELD_NAME, "abc")), query); // Fuzzy query = new SimpleQueryStringBuilder("aBc~1") .field(STRING_FIELD_NAME) .analyzer("whitespace") .toQuery(createShardContext()); FuzzyQuery expected = new FuzzyQuery(new Term(STRING_FIELD_NAME, "aBc"), 1); assertEquals(expected, query); query = new SimpleQueryStringBuilder("aBc~1") .field(STRING_FIELD_NAME) .analyzer("standard") .toQuery(createShardContext()); expected = new FuzzyQuery(new Term(STRING_FIELD_NAME, "abc"), 1); assertEquals(expected, query); } public void testAllFieldsWithFields() throws IOException { String json = "{\n" + " \"simple_query_string\" : {\n" + " \"query\" : \"this that thus\",\n" + " \"fields\" : [\"foo\"],\n" + " \"all_fields\" : true\n" + " }\n" + "}"; ParsingException e = expectThrows(ParsingException.class, () -> parseQuery(json)); assertThat(e.getMessage(), containsString("cannot use [all_fields] parameter in conjunction with [fields]")); } }