/* * SPDX-License-Identifier: Apache-2.0 * * The OpenSearch Contributors require contributions made to * this file be licensed under the Apache-2.0 license or a * compatible open source license. */ /* * 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. */ /* * Modifications Copyright OpenSearch Contributors. See * GitHub history for details. */ package org.opensearch.index.query; import org.apache.lucene.index.Term; import org.apache.lucene.search.BoostQuery; import org.apache.lucene.search.FuzzyQuery; import org.apache.lucene.search.Query; import org.opensearch.OpenSearchParseException; import org.opensearch.core.common.ParsingException; import org.opensearch.common.unit.Fuzziness; import org.opensearch.test.AbstractQueryTestCase; import java.io.IOException; import java.util.HashMap; import java.util.Map; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.instanceOf; public class FuzzyQueryBuilderTests extends AbstractQueryTestCase { @Override protected FuzzyQueryBuilder doCreateTestQueryBuilder() { String fieldName = randomFrom(TEXT_FIELD_NAME, TEXT_ALIAS_FIELD_NAME); FuzzyQueryBuilder query = new FuzzyQueryBuilder(fieldName, getRandomValueForFieldName(fieldName)); if (randomBoolean()) { query.fuzziness(randomFuzziness(query.fieldName())); } if (randomBoolean()) { query.prefixLength(randomIntBetween(0, 10)); } if (randomBoolean()) { query.maxExpansions(randomIntBetween(1, 10)); } if (randomBoolean()) { query.transpositions(randomBoolean()); } if (randomBoolean()) { query.rewrite(getRandomRewriteMethod()); } return query; } @Override protected Map getAlternateVersions() { Map alternateVersions = new HashMap<>(); FuzzyQueryBuilder fuzzyQuery = new FuzzyQueryBuilder(randomAlphaOfLengthBetween(1, 10), randomAlphaOfLengthBetween(1, 10)); String contentString = "{\n" + " \"fuzzy\" : {\n" + " \"" + fuzzyQuery.fieldName() + "\" : \"" + fuzzyQuery.value() + "\"\n" + " }\n" + "}"; alternateVersions.put(contentString, fuzzyQuery); return alternateVersions; } @Override protected void doAssertLuceneQuery(FuzzyQueryBuilder queryBuilder, Query query, QueryShardContext context) throws IOException { assertThat(query, instanceOf(FuzzyQuery.class)); FuzzyQuery fuzzyQuery = (FuzzyQuery) query; String expectedFieldName = expectedFieldName(queryBuilder.fieldName()); String actualFieldName = fuzzyQuery.getTerm().field(); assertThat(actualFieldName, equalTo(expectedFieldName)); } public void testIllegalArguments() { IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> new FuzzyQueryBuilder(null, "text")); assertEquals("field name cannot be null or empty", e.getMessage()); e = expectThrows(IllegalArgumentException.class, () -> new FuzzyQueryBuilder("", "text")); assertEquals("field name cannot be null or empty", e.getMessage()); e = expectThrows(IllegalArgumentException.class, () -> new FuzzyQueryBuilder("field", null)); assertEquals("query value cannot be null", e.getMessage()); } public void testUnsupportedFuzzinessForStringType() throws IOException { QueryShardContext context = createShardContext(); context.setAllowUnmappedFields(true); FuzzyQueryBuilder fuzzyQueryBuilder = new FuzzyQueryBuilder(TEXT_FIELD_NAME, "text"); fuzzyQueryBuilder.fuzziness(Fuzziness.build(randomFrom("a string which is not auto", "3h", "200s"))); NumberFormatException e = expectThrows(NumberFormatException.class, () -> fuzzyQueryBuilder.toQuery(context)); assertThat(e.getMessage(), containsString("For input string")); } public void testToQueryWithStringField() throws IOException { String query = "{\n" + " \"fuzzy\":{\n" + " \"" + TEXT_FIELD_NAME + "\":{\n" + " \"value\":\"sh\",\n" + " \"fuzziness\": \"AUTO\",\n" + " \"prefix_length\":1,\n" + " \"boost\":2.0\n" + " }\n" + " }\n" + "}"; Query parsedQuery = parseQuery(query).toQuery(createShardContext()); assertThat(parsedQuery, instanceOf(BoostQuery.class)); BoostQuery boostQuery = (BoostQuery) parsedQuery; assertThat(boostQuery.getBoost(), equalTo(2.0f)); assertThat(boostQuery.getQuery(), instanceOf(FuzzyQuery.class)); FuzzyQuery fuzzyQuery = (FuzzyQuery) boostQuery.getQuery(); assertThat(fuzzyQuery.getTerm(), equalTo(new Term(TEXT_FIELD_NAME, "sh"))); assertThat(fuzzyQuery.getMaxEdits(), equalTo(Fuzziness.AUTO.asDistance("sh"))); assertThat(fuzzyQuery.getPrefixLength(), equalTo(1)); } public void testToQueryWithStringFieldDefinedFuzziness() throws IOException { String query = "{\n" + " \"fuzzy\":{\n" + " \"" + TEXT_FIELD_NAME + "\":{\n" + " \"value\":\"sh\",\n" + " \"fuzziness\": \"AUTO:2,5\",\n" + " \"prefix_length\":1,\n" + " \"boost\":2.0\n" + " }\n" + " }\n" + "}"; Query parsedQuery = parseQuery(query).toQuery(createShardContext()); assertThat(parsedQuery, instanceOf(BoostQuery.class)); BoostQuery boostQuery = (BoostQuery) parsedQuery; assertThat(boostQuery.getBoost(), equalTo(2.0f)); assertThat(boostQuery.getQuery(), instanceOf(FuzzyQuery.class)); FuzzyQuery fuzzyQuery = (FuzzyQuery) boostQuery.getQuery(); assertThat(fuzzyQuery.getTerm(), equalTo(new Term(TEXT_FIELD_NAME, "sh"))); assertThat(fuzzyQuery.getMaxEdits(), equalTo(1)); assertThat(fuzzyQuery.getPrefixLength(), equalTo(1)); } public void testToQueryWithStringFieldDefinedWrongFuzziness() throws IOException { String queryMissingFuzzinessUpLimit = "{\n" + " \"fuzzy\":{\n" + " \"" + TEXT_FIELD_NAME + "\":{\n" + " \"value\":\"sh\",\n" + " \"fuzziness\": \"AUTO:2\",\n" + " \"prefix_length\":1,\n" + " \"boost\":2.0\n" + " }\n" + " }\n" + "}"; OpenSearchParseException e = expectThrows( OpenSearchParseException.class, () -> parseQuery(queryMissingFuzzinessUpLimit).toQuery(createShardContext()) ); String msg = "failed to find low and high distance values"; assertTrue(e.getMessage() + " didn't contain: " + msg + " but: " + e.getMessage(), e.getMessage().contains(msg)); String queryHavingNegativeFuzzinessLowLimit = "{\n" + " \"fuzzy\":{\n" + " \"" + TEXT_FIELD_NAME + "\":{\n" + " \"value\":\"sh\",\n" + " \"fuzziness\": \"AUTO:-1,6\",\n" + " \"prefix_length\":1,\n" + " \"boost\":2.0\n" + " }\n" + " }\n" + "}"; String msg2 = "fuzziness wrongly configured"; IllegalArgumentException e2 = expectThrows( IllegalArgumentException.class, () -> parseQuery(queryHavingNegativeFuzzinessLowLimit).toQuery(createShardContext()) ); assertTrue(e2.getMessage() + " didn't contain: " + msg2 + " but: " + e.getMessage(), e.getMessage().contains(msg)); String queryMissingFuzzinessUpLimit2 = "{\n" + " \"fuzzy\":{\n" + " \"" + TEXT_FIELD_NAME + "\":{\n" + " \"value\":\"sh\",\n" + " \"fuzziness\": \"AUTO:1,\",\n" + " \"prefix_length\":1,\n" + " \"boost\":2.0\n" + " }\n" + " }\n" + "}"; e = expectThrows(OpenSearchParseException.class, () -> parseQuery(queryMissingFuzzinessUpLimit2).toQuery(createShardContext())); assertTrue(e.getMessage() + " didn't contain: " + msg + " but: " + e.getMessage(), e.getMessage().contains(msg)); String queryMissingFuzzinessLowLimit = "{\n" + " \"fuzzy\":{\n" + " \"" + TEXT_FIELD_NAME + "\":{\n" + " \"value\":\"sh\",\n" + " \"fuzziness\": \"AUTO:,5\",\n" + " \"prefix_length\":1,\n" + " \"boost\":2.0\n" + " }\n" + " }\n" + "}"; e = expectThrows(OpenSearchParseException.class, () -> parseQuery(queryMissingFuzzinessLowLimit).toQuery(createShardContext())); msg = "failed to parse [AUTO:,5] as a \"auto:int,int\""; assertTrue(e.getMessage() + " didn't contain: " + msg + " but: " + e.getMessage(), e.getMessage().contains(msg)); } public void testToQueryWithNumericField() throws IOException { String query = "{\n" + " \"fuzzy\":{\n" + " \"" + INT_FIELD_NAME + "\":{\n" + " \"value\":12,\n" + " \"fuzziness\":5\n" + " }\n" + " }\n" + "}\n"; IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> parseQuery(query).toQuery(createShardContext())); assertEquals( "Can only use fuzzy queries on keyword and text fields - not on [mapped_int] which is of type [integer]", e.getMessage() ); } public void testFromJson() throws IOException { String json = "{\n" + " \"fuzzy\" : {\n" + " \"user\" : {\n" + " \"value\" : \"ki\",\n" + " \"fuzziness\" : \"2\",\n" + " \"prefix_length\" : 0,\n" + " \"max_expansions\" : 100,\n" + " \"transpositions\" : false,\n" + " \"boost\" : 42.0\n" + " }\n" + " }\n" + "}"; FuzzyQueryBuilder parsed = (FuzzyQueryBuilder) parseQuery(json); checkGeneratedJson(json, parsed); assertEquals(json, 42.0, parsed.boost(), 0.00001); assertEquals(json, 2, parsed.fuzziness().asFloat(), 0f); assertEquals(json, false, parsed.transpositions()); } public void testParseFailsWithMultipleFields() throws IOException { String json1 = "{\n" + " \"fuzzy\" : {\n" + " \"message1\" : {\n" + " \"value\" : \"this is a test\"\n" + " }\n" + " }\n" + "}"; parseQuery(json1); // should be all good String json2 = "{\n" + " \"fuzzy\" : {\n" + " \"message1\" : {\n" + " \"value\" : \"this is a test\"\n" + " },\n" + " \"message2\" : {\n" + " \"value\" : \"this is a test\"\n" + " }\n" + " }\n" + "}"; ParsingException e = expectThrows(ParsingException.class, () -> parseQuery(json2)); assertEquals("[fuzzy] query doesn't support multiple fields, found [message1] and [message2]", e.getMessage()); String shortJson = "{\n" + " \"fuzzy\" : {\n" + " \"message1\" : \"this is a test\",\n" + " \"message2\" : \"value\" : \"this is a test\"\n" + " }\n" + "}"; e = expectThrows(ParsingException.class, () -> parseQuery(shortJson)); assertEquals("[fuzzy] query doesn't support multiple fields, found [message1] and [message2]", e.getMessage()); } public void testParseFailsWithValueArray() { String query = "{\n" + " \"fuzzy\" : {\n" + " \"message1\" : {\n" + " \"value\" : [ \"one\", \"two\", \"three\"]\n" + " }\n" + " }\n" + "}"; ParsingException e = expectThrows(ParsingException.class, () -> parseQuery(query)); assertEquals("[fuzzy] unexpected token [START_ARRAY] after [value]", e.getMessage()); } public void testToQueryWithTranspositions() throws Exception { Query query = new FuzzyQueryBuilder(TEXT_FIELD_NAME, "text").toQuery(createShardContext()); assertThat(query, instanceOf(FuzzyQuery.class)); assertEquals(FuzzyQuery.defaultTranspositions, ((FuzzyQuery) query).getTranspositions()); query = new FuzzyQueryBuilder(TEXT_FIELD_NAME, "text").transpositions(true).toQuery(createShardContext()); assertThat(query, instanceOf(FuzzyQuery.class)); assertEquals(true, ((FuzzyQuery) query).getTranspositions()); query = new FuzzyQueryBuilder(TEXT_FIELD_NAME, "text").transpositions(false).toQuery(createShardContext()); assertThat(query, instanceOf(FuzzyQuery.class)); assertEquals(false, ((FuzzyQuery) query).getTranspositions()); } }