From 7e482a5db01b2f761ee65c8bca6a9fe9a929a20e Mon Sep 17 00:00:00 2001 From: Nik Everett Date: Wed, 15 Jul 2020 08:57:24 -0400 Subject: [PATCH] Remaining queries for script keyword fields Adds the remaining queries for scripted keyword fields and handles a few leftover TODOs, mostly around `hashCode` and `equals`. --- .../mapper/RuntimeKeywordMappedFieldType.java | 73 ++++++++++- ...stractStringScriptFieldAutomatonQuery.java | 48 +++++++ .../query/AbstractStringScriptFieldQuery.java | 17 ++- .../query/StringScriptFieldExistsQuery.java | 11 +- .../query/StringScriptFieldFuzzyQuery.java | 68 ++++++++++ .../query/StringScriptFieldPrefixQuery.java | 69 ++++++++++ .../query/StringScriptFieldRangeQuery.java | 115 +++++++++++++++++ .../query/StringScriptFieldRegexpQuery.java | 68 ++++++++++ .../query/StringScriptFieldTermQuery.java | 7 +- .../query/StringScriptFieldTermsQuery.java | 12 +- .../query/StringScriptFieldWildcardQuery.java | 60 +++++++++ .../RuntimeKeywordMappedFieldTypeTests.java | 74 +++++++++++ ...bstractStringScriptFieldQueryTestCase.java | 40 ++++++ .../StringScriptFieldExistsQueryTests.java | 18 ++- .../StringScriptFieldFuzzyQueryTests.java | 115 +++++++++++++++++ .../StringScriptFieldPrefixQueryTests.java | 75 +++++++++++ .../StringScriptFieldRangeQueryTests.java | 120 ++++++++++++++++++ .../StringScriptFieldRegexpQueryTests.java | 106 ++++++++++++++++ .../StringScriptFieldTermQueryTests.java | 33 ++++- .../StringScriptFieldTermsQueryTests.java | 39 +++++- .../StringScriptFieldWildcardQueryTests.java | 73 +++++++++++ .../test/runtime_fields/10_keyword.yml | 64 ++++++++++ 22 files changed, 1269 insertions(+), 36 deletions(-) create mode 100644 x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/query/AbstractStringScriptFieldAutomatonQuery.java create mode 100644 x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/query/StringScriptFieldFuzzyQuery.java create mode 100644 x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/query/StringScriptFieldPrefixQuery.java create mode 100644 x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/query/StringScriptFieldRangeQuery.java create mode 100644 x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/query/StringScriptFieldRegexpQuery.java create mode 100644 x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/query/StringScriptFieldWildcardQuery.java create mode 100644 x-pack/plugin/runtime-fields/src/test/java/org/elasticsearch/xpack/runtimefields/query/StringScriptFieldFuzzyQueryTests.java create mode 100644 x-pack/plugin/runtime-fields/src/test/java/org/elasticsearch/xpack/runtimefields/query/StringScriptFieldPrefixQueryTests.java create mode 100644 x-pack/plugin/runtime-fields/src/test/java/org/elasticsearch/xpack/runtimefields/query/StringScriptFieldRangeQueryTests.java create mode 100644 x-pack/plugin/runtime-fields/src/test/java/org/elasticsearch/xpack/runtimefields/query/StringScriptFieldRegexpQueryTests.java create mode 100644 x-pack/plugin/runtime-fields/src/test/java/org/elasticsearch/xpack/runtimefields/query/StringScriptFieldWildcardQueryTests.java diff --git a/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/mapper/RuntimeKeywordMappedFieldType.java b/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/mapper/RuntimeKeywordMappedFieldType.java index 16261bbfe3173..18d36697c29a0 100644 --- a/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/mapper/RuntimeKeywordMappedFieldType.java +++ b/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/mapper/RuntimeKeywordMappedFieldType.java @@ -6,9 +6,13 @@ package org.elasticsearch.xpack.runtimefields.mapper; +import org.apache.lucene.search.MultiTermQuery.RewriteMethod; import org.apache.lucene.search.Query; import org.apache.lucene.util.BytesRef; +import org.elasticsearch.common.geo.ShapeRelation; import org.elasticsearch.common.lucene.BytesRefs; +import org.elasticsearch.common.time.DateMathParser; +import org.elasticsearch.common.unit.Fuzziness; import org.elasticsearch.common.xcontent.ToXContent.Params; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.index.mapper.MappedFieldType; @@ -18,10 +22,16 @@ import org.elasticsearch.xpack.runtimefields.StringScriptFieldScript; import org.elasticsearch.xpack.runtimefields.fielddata.ScriptBinaryFieldData; import org.elasticsearch.xpack.runtimefields.query.StringScriptFieldExistsQuery; +import org.elasticsearch.xpack.runtimefields.query.StringScriptFieldFuzzyQuery; +import org.elasticsearch.xpack.runtimefields.query.StringScriptFieldPrefixQuery; +import org.elasticsearch.xpack.runtimefields.query.StringScriptFieldRangeQuery; +import org.elasticsearch.xpack.runtimefields.query.StringScriptFieldRegexpQuery; import org.elasticsearch.xpack.runtimefields.query.StringScriptFieldTermQuery; import org.elasticsearch.xpack.runtimefields.query.StringScriptFieldTermsQuery; +import org.elasticsearch.xpack.runtimefields.query.StringScriptFieldWildcardQuery; import java.io.IOException; +import java.time.ZoneId; import java.util.List; import java.util.Map; import java.util.Objects; @@ -69,18 +79,75 @@ private StringScriptFieldScript.LeafFactory leafFactory(QueryShardContext contex @Override public Query existsQuery(QueryShardContext context) { - return new StringScriptFieldExistsQuery(leafFactory(context), name()); + return new StringScriptFieldExistsQuery(script, leafFactory(context), name()); + } + + @Override + public Query fuzzyQuery( + Object value, + Fuzziness fuzziness, + int prefixLength, + int maxExpansions, + boolean transpositions, + QueryShardContext context + ) { + return StringScriptFieldFuzzyQuery.build( + script, + leafFactory(context), + name(), + BytesRefs.toString(Objects.requireNonNull(value)), + fuzziness.asDistance(BytesRefs.toString(value)), + prefixLength, + transpositions + ); + } + + @Override + public Query prefixQuery(String value, RewriteMethod method, org.elasticsearch.index.query.QueryShardContext context) { + return new StringScriptFieldPrefixQuery(script, leafFactory(context), name(), value); + } + + @Override + public Query rangeQuery( + Object lowerTerm, + Object upperTerm, + boolean includeLower, + boolean includeUpper, + ShapeRelation relation, + ZoneId timeZone, + DateMathParser parser, + QueryShardContext context + ) { + return new StringScriptFieldRangeQuery( + script, + leafFactory(context), + name(), + BytesRefs.toString(Objects.requireNonNull(lowerTerm)), + BytesRefs.toString(Objects.requireNonNull(upperTerm)), + includeLower, + includeUpper + ); + } + + @Override + public Query regexpQuery(String value, int flags, int maxDeterminizedStates, RewriteMethod method, QueryShardContext context) { + return new StringScriptFieldRegexpQuery(script, leafFactory(context), name(), value, flags, maxDeterminizedStates); } @Override public Query termQuery(Object value, QueryShardContext context) { - return new StringScriptFieldTermQuery(leafFactory(context), name(), BytesRefs.toString(Objects.requireNonNull(value))); + return new StringScriptFieldTermQuery(script, leafFactory(context), name(), BytesRefs.toString(Objects.requireNonNull(value))); } @Override public Query termsQuery(List values, QueryShardContext context) { Set terms = values.stream().map(v -> BytesRefs.toString(Objects.requireNonNull(v))).collect(toSet()); - return new StringScriptFieldTermsQuery(leafFactory(context), name(), terms); + return new StringScriptFieldTermsQuery(script, leafFactory(context), name(), terms); + } + + @Override + public Query wildcardQuery(String value, RewriteMethod method, QueryShardContext context) { + return new StringScriptFieldWildcardQuery(script, leafFactory(context), name(), value); } void doXContentBody(XContentBuilder builder, boolean includeDefaults, Params params) throws IOException { diff --git a/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/query/AbstractStringScriptFieldAutomatonQuery.java b/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/query/AbstractStringScriptFieldAutomatonQuery.java new file mode 100644 index 0000000000000..18d7450ca1766 --- /dev/null +++ b/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/query/AbstractStringScriptFieldAutomatonQuery.java @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +package org.elasticsearch.xpack.runtimefields.query; + +import org.apache.lucene.search.QueryVisitor; +import org.apache.lucene.util.BytesRefBuilder; +import org.apache.lucene.util.automaton.ByteRunAutomaton; +import org.elasticsearch.script.Script; +import org.elasticsearch.xpack.runtimefields.StringScriptFieldScript; + +import java.util.List; + +public abstract class AbstractStringScriptFieldAutomatonQuery extends AbstractStringScriptFieldQuery { + private final BytesRefBuilder scratch = new BytesRefBuilder(); + private final ByteRunAutomaton automaton; + + public AbstractStringScriptFieldAutomatonQuery( + Script script, + StringScriptFieldScript.LeafFactory leafFactory, + String fieldName, + ByteRunAutomaton automaton + ) { + super(script, leafFactory, fieldName); + this.automaton = automaton; + } + + @Override + protected final boolean matches(List values) { + for (String value : values) { + scratch.copyChars(value); + if (automaton.run(scratch.bytes(), 0, scratch.length())) { + return true; + } + } + return false; + } + + @Override + public final void visit(QueryVisitor visitor) { + if (visitor.acceptField(fieldName())) { + visitor.consumeTermsMatching(this, fieldName(), () -> automaton); + } + } +} diff --git a/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/query/AbstractStringScriptFieldQuery.java b/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/query/AbstractStringScriptFieldQuery.java index c5d1fdaba5714..d98d8397841b5 100644 --- a/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/query/AbstractStringScriptFieldQuery.java +++ b/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/query/AbstractStringScriptFieldQuery.java @@ -16,6 +16,7 @@ import org.apache.lucene.search.Scorer; import org.apache.lucene.search.TwoPhaseIterator; import org.apache.lucene.search.Weight; +import org.elasticsearch.script.Script; import org.elasticsearch.xpack.runtimefields.StringScriptFieldScript; import org.elasticsearch.xpack.runtimefields.StringScriptFieldScript.LeafFactory; @@ -27,10 +28,12 @@ * Abstract base class for building queries based on {@link StringScriptFieldScript}. */ abstract class AbstractStringScriptFieldQuery extends Query { + private final Script script; private final StringScriptFieldScript.LeafFactory leafFactory; private final String fieldName; - AbstractStringScriptFieldQuery(LeafFactory leafFactory, String fieldName) { + AbstractStringScriptFieldQuery(Script script, LeafFactory leafFactory, String fieldName) { + this.script = script; this.leafFactory = Objects.requireNonNull(leafFactory); this.fieldName = Objects.requireNonNull(fieldName); } @@ -38,7 +41,7 @@ abstract class AbstractStringScriptFieldQuery extends Query { /** * Does the value match this query? */ - public abstract boolean matches(List values); + protected abstract boolean matches(List values); @Override public final Weight createWeight(IndexSearcher searcher, ScoreMode scoreMode, float boost) throws IOException { @@ -69,14 +72,17 @@ public float matchCost() { }; } + final Script script() { + return script; + } + protected final String fieldName() { return fieldName; } @Override public int hashCode() { - // TODO should leafFactory be here? Something about the script probably should be! - return Objects.hash(getClass(), fieldName); + return Objects.hash(getClass(), script, fieldName); } @Override @@ -84,8 +90,7 @@ public boolean equals(Object obj) { if (obj == null || getClass() != obj.getClass()) { return false; } - // TODO should leafFactory be here? Something about the script probably should be! AbstractStringScriptFieldQuery other = (AbstractStringScriptFieldQuery) obj; - return fieldName.equals(other.fieldName); + return script.equals(other.script) && fieldName.equals(other.fieldName); } } diff --git a/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/query/StringScriptFieldExistsQuery.java b/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/query/StringScriptFieldExistsQuery.java index 7a83553cba33c..dedc7ce5d41ae 100644 --- a/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/query/StringScriptFieldExistsQuery.java +++ b/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/query/StringScriptFieldExistsQuery.java @@ -6,26 +6,27 @@ package org.elasticsearch.xpack.runtimefields.query; +import org.elasticsearch.script.Script; import org.elasticsearch.xpack.runtimefields.StringScriptFieldScript; import java.util.List; public class StringScriptFieldExistsQuery extends AbstractStringScriptFieldQuery { - public StringScriptFieldExistsQuery(StringScriptFieldScript.LeafFactory leafFactory, String fieldName) { - super(leafFactory, fieldName); + public StringScriptFieldExistsQuery(Script script, StringScriptFieldScript.LeafFactory leafFactory, String fieldName) { + super(script, leafFactory, fieldName); } @Override - public boolean matches(List values) { + protected boolean matches(List values) { return false == values.isEmpty(); } @Override public final String toString(String field) { if (fieldName().contentEquals(field)) { - return "*"; + return "ScriptFieldExists"; } - return fieldName() + ":*"; + return fieldName() + ":ScriptFieldExists"; } // Superclass's equals and hashCode are great for this class diff --git a/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/query/StringScriptFieldFuzzyQuery.java b/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/query/StringScriptFieldFuzzyQuery.java new file mode 100644 index 0000000000000..ac409c747721c --- /dev/null +++ b/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/query/StringScriptFieldFuzzyQuery.java @@ -0,0 +1,68 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +package org.elasticsearch.xpack.runtimefields.query; + +import org.apache.lucene.index.Term; +import org.apache.lucene.search.FuzzyQuery; +import org.apache.lucene.util.automaton.ByteRunAutomaton; +import org.elasticsearch.script.Script; +import org.elasticsearch.xpack.runtimefields.StringScriptFieldScript; + +import java.util.Objects; + +public class StringScriptFieldFuzzyQuery extends AbstractStringScriptFieldAutomatonQuery { + public static StringScriptFieldFuzzyQuery build( + Script script, + StringScriptFieldScript.LeafFactory leafFactory, + String fieldName, + String term, + int maxEdits, + int prefixLength, + boolean transpositions + ) { + int maxExpansions = 1; // We don't actually expand anything so the value here doesn't matter + FuzzyQuery delegate = new FuzzyQuery(new Term(fieldName, term), maxEdits, prefixLength, maxExpansions, transpositions); + ByteRunAutomaton automaton = delegate.getAutomata().runAutomaton; + return new StringScriptFieldFuzzyQuery(script, leafFactory, fieldName, automaton, delegate); + } + + private final FuzzyQuery delegate; + + private StringScriptFieldFuzzyQuery( + Script script, + StringScriptFieldScript.LeafFactory leafFactory, + String fieldName, + ByteRunAutomaton automaton, + FuzzyQuery delegate + ) { + super(script, leafFactory, fieldName, automaton); + this.delegate = delegate; + } + + @Override + public final String toString(String field) { + return delegate.toString(field); + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), delegate); + } + + @Override + public boolean equals(Object obj) { + if (false == super.equals(obj)) { + return false; + } + StringScriptFieldFuzzyQuery other = (StringScriptFieldFuzzyQuery) obj; + return delegate.equals(other.delegate); + } + + FuzzyQuery delegate() { + return delegate; + } +} diff --git a/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/query/StringScriptFieldPrefixQuery.java b/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/query/StringScriptFieldPrefixQuery.java new file mode 100644 index 0000000000000..e9c3f8b19a8bf --- /dev/null +++ b/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/query/StringScriptFieldPrefixQuery.java @@ -0,0 +1,69 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +package org.elasticsearch.xpack.runtimefields.query; + +import org.apache.lucene.search.PrefixQuery; +import org.apache.lucene.search.QueryVisitor; +import org.apache.lucene.util.BytesRef; +import org.apache.lucene.util.automaton.ByteRunAutomaton; +import org.elasticsearch.script.Script; +import org.elasticsearch.xpack.runtimefields.StringScriptFieldScript; + +import java.util.List; +import java.util.Objects; + +public class StringScriptFieldPrefixQuery extends AbstractStringScriptFieldQuery { + private final String prefix; + + public StringScriptFieldPrefixQuery(Script script, StringScriptFieldScript.LeafFactory leafFactory, String fieldName, String prefix) { + super(script, leafFactory, fieldName); + this.prefix = Objects.requireNonNull(prefix); + } + + @Override + protected boolean matches(List values) { + for (String value : values) { + if (value != null && value.startsWith(prefix)) { + return true; + } + } + return false; + } + + @Override + public void visit(QueryVisitor visitor) { + if (visitor.acceptField(fieldName())) { + visitor.consumeTermsMatching(this, fieldName(), () -> new ByteRunAutomaton(PrefixQuery.toAutomaton(new BytesRef(prefix)))); + } + } + + @Override + public final String toString(String field) { + if (fieldName().contentEquals(field)) { + return prefix + "*"; + } + return fieldName() + ":" + prefix + "*"; + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), prefix); + } + + @Override + public boolean equals(Object obj) { + if (false == super.equals(obj)) { + return false; + } + StringScriptFieldPrefixQuery other = (StringScriptFieldPrefixQuery) obj; + return prefix.equals(other.prefix); + } + + String prefix() { + return prefix; + } +} diff --git a/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/query/StringScriptFieldRangeQuery.java b/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/query/StringScriptFieldRangeQuery.java new file mode 100644 index 0000000000000..acd5b051361ec --- /dev/null +++ b/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/query/StringScriptFieldRangeQuery.java @@ -0,0 +1,115 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +package org.elasticsearch.xpack.runtimefields.query; + +import org.apache.lucene.search.QueryVisitor; +import org.apache.lucene.util.BytesRef; +import org.apache.lucene.util.automaton.Automata; +import org.apache.lucene.util.automaton.ByteRunAutomaton; +import org.elasticsearch.script.Script; +import org.elasticsearch.xpack.runtimefields.StringScriptFieldScript; + +import java.util.List; +import java.util.Objects; + +public class StringScriptFieldRangeQuery extends AbstractStringScriptFieldQuery { + private final String lowerValue; + private final String upperValue; + private final boolean includeLower; + private final boolean includeUpper; + + public StringScriptFieldRangeQuery( + Script script, + StringScriptFieldScript.LeafFactory leafFactory, + String fieldName, + String lowerValue, + String upperValue, + boolean includeLower, + boolean includeUpper + ) { + super(script, leafFactory, fieldName); + this.lowerValue = Objects.requireNonNull(lowerValue); + this.upperValue = Objects.requireNonNull(upperValue); + this.includeLower = includeLower; + this.includeUpper = includeUpper; + assert lowerValue.compareTo(upperValue) <= 0; + } + + @Override + protected boolean matches(List values) { + for (String value : values) { + int lct = lowerValue.compareTo(value); + boolean lowerOk = includeLower ? lct <= 0 : lct < 0; + if (lowerOk) { + int uct = upperValue.compareTo(value); + boolean upperOk = includeUpper ? uct >= 0 : uct > 0; + if (upperOk) { + return true; + } + } + } + return false; + } + + @Override + public void visit(QueryVisitor visitor) { + if (visitor.acceptField(fieldName())) { + visitor.consumeTermsMatching( + this, + fieldName(), + () -> new ByteRunAutomaton( + Automata.makeBinaryInterval(new BytesRef(lowerValue), includeLower, new BytesRef(upperValue), includeUpper) + ) + ); + } + } + + @Override + public final String toString(String field) { + StringBuilder b = new StringBuilder(); + if (false == fieldName().contentEquals(field)) { + b.append(fieldName()).append(':'); + } + b.append(includeLower ? '[' : '{'); + b.append(lowerValue).append(" TO ").append(upperValue); + b.append(includeUpper ? ']' : '}'); + return b.toString(); + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), lowerValue, upperValue, includeLower, includeUpper); + } + + @Override + public boolean equals(Object obj) { + if (false == super.equals(obj)) { + return false; + } + StringScriptFieldRangeQuery other = (StringScriptFieldRangeQuery) obj; + return lowerValue.equals(other.lowerValue) + && upperValue.equals(other.upperValue) + && includeLower == other.includeLower + && includeUpper == other.includeUpper; + } + + String lowerValue() { + return lowerValue; + } + + String upperValue() { + return upperValue; + } + + boolean includeLower() { + return includeLower; + } + + boolean includeUpper() { + return includeUpper; + } +} diff --git a/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/query/StringScriptFieldRegexpQuery.java b/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/query/StringScriptFieldRegexpQuery.java new file mode 100644 index 0000000000000..893add56381fd --- /dev/null +++ b/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/query/StringScriptFieldRegexpQuery.java @@ -0,0 +1,68 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +package org.elasticsearch.xpack.runtimefields.query; + +import org.apache.lucene.util.automaton.ByteRunAutomaton; +import org.apache.lucene.util.automaton.RegExp; +import org.elasticsearch.script.Script; +import org.elasticsearch.xpack.runtimefields.StringScriptFieldScript; + +import java.util.Objects; + +public class StringScriptFieldRegexpQuery extends AbstractStringScriptFieldAutomatonQuery { + private final String pattern; + private final int flags; + + public StringScriptFieldRegexpQuery( + Script script, + StringScriptFieldScript.LeafFactory leafFactory, + String fieldName, + String pattern, + int flags, + int maxDeterminizedStates + ) { + super( + script, + leafFactory, + fieldName, + new ByteRunAutomaton(new RegExp(Objects.requireNonNull(pattern), flags).toAutomaton(maxDeterminizedStates)) + ); + this.pattern = pattern; + this.flags = flags; + } + + @Override + public final String toString(String field) { + StringBuilder b = new StringBuilder(); + if (false == fieldName().contentEquals(field)) { + b.append(fieldName()).append(':'); + } + return b.append('/').append(pattern).append('/').toString(); + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), pattern, flags); + } + + @Override + public boolean equals(Object obj) { + if (false == super.equals(obj)) { + return false; + } + StringScriptFieldRegexpQuery other = (StringScriptFieldRegexpQuery) obj; + return pattern.equals(other.pattern) && flags == other.flags; + } + + String pattern() { + return pattern; + } + + int flags() { + return flags; + } +} diff --git a/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/query/StringScriptFieldTermQuery.java b/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/query/StringScriptFieldTermQuery.java index 097eacdf7cb44..ba8650b966bdb 100644 --- a/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/query/StringScriptFieldTermQuery.java +++ b/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/query/StringScriptFieldTermQuery.java @@ -8,6 +8,7 @@ import org.apache.lucene.index.Term; import org.apache.lucene.search.QueryVisitor; +import org.elasticsearch.script.Script; import org.elasticsearch.xpack.runtimefields.StringScriptFieldScript; import java.util.List; @@ -16,13 +17,13 @@ public class StringScriptFieldTermQuery extends AbstractStringScriptFieldQuery { private final String term; - public StringScriptFieldTermQuery(StringScriptFieldScript.LeafFactory leafFactory, String fieldName, String term) { - super(leafFactory, fieldName); + public StringScriptFieldTermQuery(Script script, StringScriptFieldScript.LeafFactory leafFactory, String fieldName, String term) { + super(script, leafFactory, fieldName); this.term = Objects.requireNonNull(term); } @Override - public boolean matches(List values) { + protected boolean matches(List values) { for (String value : values) { if (term.equals(value)) { return true; diff --git a/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/query/StringScriptFieldTermsQuery.java b/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/query/StringScriptFieldTermsQuery.java index eb09ecbcaf9b2..e5efa08c102cd 100644 --- a/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/query/StringScriptFieldTermsQuery.java +++ b/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/query/StringScriptFieldTermsQuery.java @@ -8,6 +8,7 @@ import org.apache.lucene.index.Term; import org.apache.lucene.search.QueryVisitor; +import org.elasticsearch.script.Script; import org.elasticsearch.xpack.runtimefields.StringScriptFieldScript; import java.util.List; @@ -17,13 +18,18 @@ public class StringScriptFieldTermsQuery extends AbstractStringScriptFieldQuery { private final Set terms; - public StringScriptFieldTermsQuery(StringScriptFieldScript.LeafFactory leafFactory, String fieldName, Set terms) { - super(leafFactory, fieldName); + public StringScriptFieldTermsQuery( + Script script, + StringScriptFieldScript.LeafFactory leafFactory, + String fieldName, + Set terms + ) { + super(script, leafFactory, fieldName); this.terms = terms; } @Override - public boolean matches(List values) { + protected boolean matches(List values) { for (String value : values) { if (terms.contains(value)) { return true; diff --git a/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/query/StringScriptFieldWildcardQuery.java b/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/query/StringScriptFieldWildcardQuery.java new file mode 100644 index 0000000000000..2e342adb5cf61 --- /dev/null +++ b/x-pack/plugin/runtime-fields/src/main/java/org/elasticsearch/xpack/runtimefields/query/StringScriptFieldWildcardQuery.java @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +package org.elasticsearch.xpack.runtimefields.query; + +import org.apache.lucene.index.Term; +import org.apache.lucene.search.WildcardQuery; +import org.apache.lucene.util.automaton.ByteRunAutomaton; +import org.elasticsearch.script.Script; +import org.elasticsearch.xpack.runtimefields.StringScriptFieldScript; + +import java.util.Objects; + +public class StringScriptFieldWildcardQuery extends AbstractStringScriptFieldAutomatonQuery { + private final String pattern; + + public StringScriptFieldWildcardQuery( + Script script, + StringScriptFieldScript.LeafFactory leafFactory, + String fieldName, + String pattern + ) { + super( + script, + leafFactory, + fieldName, + new ByteRunAutomaton(WildcardQuery.toAutomaton(new Term(fieldName, Objects.requireNonNull(pattern)))) + ); + this.pattern = pattern; + } + + @Override + public final String toString(String field) { + if (fieldName().equals(field)) { + return pattern; + } + return fieldName() + ":" + pattern; + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), pattern); + } + + @Override + public boolean equals(Object obj) { + if (false == super.equals(obj)) { + return false; + } + StringScriptFieldWildcardQuery other = (StringScriptFieldWildcardQuery) obj; + return pattern.equals(other.pattern); + } + + String pattern() { + return pattern; + } +} diff --git a/x-pack/plugin/runtime-fields/src/test/java/org/elasticsearch/xpack/runtimefields/mapper/RuntimeKeywordMappedFieldTypeTests.java b/x-pack/plugin/runtime-fields/src/test/java/org/elasticsearch/xpack/runtimefields/mapper/RuntimeKeywordMappedFieldTypeTests.java index 8fdc9188726e1..dcc75356fcb61 100644 --- a/x-pack/plugin/runtime-fields/src/test/java/org/elasticsearch/xpack/runtimefields/mapper/RuntimeKeywordMappedFieldTypeTests.java +++ b/x-pack/plugin/runtime-fields/src/test/java/org/elasticsearch/xpack/runtimefields/mapper/RuntimeKeywordMappedFieldTypeTests.java @@ -18,9 +18,11 @@ import org.apache.lucene.search.ScoreMode; import org.apache.lucene.store.Directory; import org.apache.lucene.util.BytesRef; +import org.apache.lucene.util.automaton.Operations; import org.elasticsearch.Version; import org.elasticsearch.cluster.metadata.IndexMetadata; import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.unit.Fuzziness; import org.elasticsearch.index.IndexSettings; import org.elasticsearch.index.fielddata.SortedBinaryDocValues; import org.elasticsearch.index.mapper.MapperService; @@ -102,6 +104,67 @@ public void testExistsQuery() throws IOException { } } + public void testFuzzyQuery() throws IOException { + try (Directory directory = newDirectory(); RandomIndexWriter iw = new RandomIndexWriter(random(), directory)) { + iw.addDocument(List.of(new StoredField("_source", new BytesRef("{\"foo\": \"cat\"}")))); // No edits, matches + iw.addDocument(List.of(new StoredField("_source", new BytesRef("{\"foo\": \"caat\"}")))); // Single insertion, matches + iw.addDocument(List.of(new StoredField("_source", new BytesRef("{\"foo\": \"cta\"}")))); // Single transposition, matches + iw.addDocument(List.of(new StoredField("_source", new BytesRef("{\"foo\": \"caaat\"}")))); // Two insertions, no match + iw.addDocument(List.of(new StoredField("_source", new BytesRef("{\"foo\": \"dog\"}")))); // Totally wrong, no match + try (DirectoryReader reader = iw.getReader()) { + IndexSearcher searcher = newSearcher(reader); + assertThat( + searcher.count(build("value(source.foo)").fuzzyQuery("cat", Fuzziness.AUTO, 0, 1, true, mockContext())), + equalTo(3) + ); + } + } + } + + public void testPrefixQuery() throws IOException { + try (Directory directory = newDirectory(); RandomIndexWriter iw = new RandomIndexWriter(random(), directory)) { + iw.addDocument(List.of(new StoredField("_source", new BytesRef("{\"foo\": \"cat\"}")))); + iw.addDocument(List.of(new StoredField("_source", new BytesRef("{\"foo\": \"cata\"}")))); + iw.addDocument(List.of(new StoredField("_source", new BytesRef("{\"foo\": \"dog\"}")))); + try (DirectoryReader reader = iw.getReader()) { + IndexSearcher searcher = newSearcher(reader); + assertThat(searcher.count(build("value(source.foo)").prefixQuery("cat", null, mockContext())), equalTo(2)); + } + } + } + + public void testRangeQuery() throws IOException { + try (Directory directory = newDirectory(); RandomIndexWriter iw = new RandomIndexWriter(random(), directory)) { + iw.addDocument(List.of(new StoredField("_source", new BytesRef("{\"foo\": \"cat\"}")))); + iw.addDocument(List.of(new StoredField("_source", new BytesRef("{\"foo\": \"cata\"}")))); + iw.addDocument(List.of(new StoredField("_source", new BytesRef("{\"foo\": \"dog\"}")))); + try (DirectoryReader reader = iw.getReader()) { + IndexSearcher searcher = newSearcher(reader); + assertThat( + searcher.count(build("value(source.foo)").rangeQuery("cat", "d", false, false, null, null, null, mockContext())), + equalTo(1) + ); + } + } + } + + public void testRegexpQuery() throws IOException { + try (Directory directory = newDirectory(); RandomIndexWriter iw = new RandomIndexWriter(random(), directory)) { + iw.addDocument(List.of(new StoredField("_source", new BytesRef("{\"foo\": \"cat\"}")))); + iw.addDocument(List.of(new StoredField("_source", new BytesRef("{\"foo\": \"cata\"}")))); + iw.addDocument(List.of(new StoredField("_source", new BytesRef("{\"foo\": \"dog\"}")))); + try (DirectoryReader reader = iw.getReader()) { + IndexSearcher searcher = newSearcher(reader); + assertThat( + searcher.count( + build("value(source.foo)").regexpQuery("ca.+", 0, Operations.DEFAULT_MAX_DETERMINIZED_STATES, null, mockContext()) + ), + equalTo(2) + ); + } + } + } + public void testTermQuery() throws IOException { try (Directory directory = newDirectory(); RandomIndexWriter iw = new RandomIndexWriter(random(), directory)) { iw.addDocument(List.of(new StoredField("_source", new BytesRef("{\"foo\": 1}")))); @@ -126,6 +189,17 @@ public void testTermsQuery() throws IOException { } } + public void testWildcardQuery() throws IOException { + try (Directory directory = newDirectory(); RandomIndexWriter iw = new RandomIndexWriter(random(), directory)) { + iw.addDocument(List.of(new StoredField("_source", new BytesRef("{\"foo\": \"aab\"}")))); + iw.addDocument(List.of(new StoredField("_source", new BytesRef("{\"foo\": \"b\"}")))); + try (DirectoryReader reader = iw.getReader()) { + IndexSearcher searcher = newSearcher(reader); + assertThat(searcher.count(build("value(source.foo)").wildcardQuery("a*b", null, mockContext())), equalTo(1)); + } + } + } + private RuntimeKeywordMappedFieldType build(String code) throws IOException { Script script = new Script(code); PainlessPlugin painlessPlugin = new PainlessPlugin(); diff --git a/x-pack/plugin/runtime-fields/src/test/java/org/elasticsearch/xpack/runtimefields/query/AbstractStringScriptFieldQueryTestCase.java b/x-pack/plugin/runtime-fields/src/test/java/org/elasticsearch/xpack/runtimefields/query/AbstractStringScriptFieldQueryTestCase.java index 2f3edad1142c4..711b4015e64de 100644 --- a/x-pack/plugin/runtime-fields/src/test/java/org/elasticsearch/xpack/runtimefields/query/AbstractStringScriptFieldQueryTestCase.java +++ b/x-pack/plugin/runtime-fields/src/test/java/org/elasticsearch/xpack/runtimefields/query/AbstractStringScriptFieldQueryTestCase.java @@ -6,11 +6,22 @@ package org.elasticsearch.xpack.runtimefields.query; +import org.apache.lucene.index.Term; +import org.apache.lucene.search.Query; +import org.apache.lucene.search.QueryVisitor; +import org.apache.lucene.util.automaton.ByteRunAutomaton; +import org.elasticsearch.script.Script; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.test.EqualsHashCodeTestUtils; import org.elasticsearch.xpack.runtimefields.StringScriptFieldScript; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Supplier; + import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.sameInstance; import static org.mockito.Mockito.mock; public abstract class AbstractStringScriptFieldQueryTestCase extends ESTestCase { @@ -22,10 +33,16 @@ public abstract class AbstractStringScriptFieldQueryTestCase automata = new ArrayList<>(); + testQuery.visit(new QueryVisitor() { + @Override + public void consumeTerms(Query query, Term... terms) { + fail(); + } + + @Override + public void consumeTermsMatching(Query query, String field, Supplier automaton) { + assertThat(query, sameInstance(testQuery)); + assertThat(field, equalTo(testQuery.fieldName())); + automata.add(automaton.get()); + } + }); + assertThat(automata, hasSize(1)); + return automata.get(0); + } } diff --git a/x-pack/plugin/runtime-fields/src/test/java/org/elasticsearch/xpack/runtimefields/query/StringScriptFieldExistsQueryTests.java b/x-pack/plugin/runtime-fields/src/test/java/org/elasticsearch/xpack/runtimefields/query/StringScriptFieldExistsQueryTests.java index 9eda9a6d84020..f900145251d02 100644 --- a/x-pack/plugin/runtime-fields/src/test/java/org/elasticsearch/xpack/runtimefields/query/StringScriptFieldExistsQueryTests.java +++ b/x-pack/plugin/runtime-fields/src/test/java/org/elasticsearch/xpack/runtimefields/query/StringScriptFieldExistsQueryTests.java @@ -11,6 +11,7 @@ import org.apache.lucene.search.QueryVisitor; import org.apache.lucene.util.automaton.ByteRunAutomaton; +import java.util.List; import java.util.function.Supplier; import static org.hamcrest.Matchers.equalTo; @@ -18,22 +19,31 @@ public class StringScriptFieldExistsQueryTests extends AbstractStringScriptFieldQueryTestCase { @Override protected StringScriptFieldExistsQuery createTestInstance() { - return new StringScriptFieldExistsQuery(leafFactory, randomAlphaOfLength(5)); + return new StringScriptFieldExistsQuery(randomScript(), leafFactory, randomAlphaOfLength(5)); } @Override protected StringScriptFieldExistsQuery copy(StringScriptFieldExistsQuery orig) { - return new StringScriptFieldExistsQuery(leafFactory, orig.fieldName()); + return new StringScriptFieldExistsQuery(orig.script(), leafFactory, orig.fieldName()); } @Override protected StringScriptFieldExistsQuery mutate(StringScriptFieldExistsQuery orig) { - return new StringScriptFieldExistsQuery(leafFactory, orig.fieldName() + "modified"); + if (randomBoolean()) { + new StringScriptFieldExistsQuery(randomValueOtherThan(orig.script(), this::randomScript), leafFactory, orig.fieldName()); + } + return new StringScriptFieldExistsQuery(orig.script(), leafFactory, orig.fieldName() + "modified"); + } + + @Override + public void testMatches() { + assertTrue(createTestInstance().matches(List.of("test"))); + assertFalse(createTestInstance().matches(List.of())); } @Override protected void assertToString(StringScriptFieldExistsQuery query) { - assertThat(query.toString(query.fieldName()), equalTo("*")); + assertThat(query.toString(query.fieldName()), equalTo("ScriptFieldExists")); } @Override diff --git a/x-pack/plugin/runtime-fields/src/test/java/org/elasticsearch/xpack/runtimefields/query/StringScriptFieldFuzzyQueryTests.java b/x-pack/plugin/runtime-fields/src/test/java/org/elasticsearch/xpack/runtimefields/query/StringScriptFieldFuzzyQueryTests.java new file mode 100644 index 0000000000000..014a499abf942 --- /dev/null +++ b/x-pack/plugin/runtime-fields/src/test/java/org/elasticsearch/xpack/runtimefields/query/StringScriptFieldFuzzyQueryTests.java @@ -0,0 +1,115 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +package org.elasticsearch.xpack.runtimefields.query; + +import org.apache.lucene.util.BytesRef; +import org.apache.lucene.util.automaton.ByteRunAutomaton; +import org.elasticsearch.script.Script; + +import java.util.List; + +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; + +public class StringScriptFieldFuzzyQueryTests extends AbstractStringScriptFieldQueryTestCase { + @Override + protected StringScriptFieldFuzzyQuery createTestInstance() { + String term = randomAlphaOfLength(6); + return StringScriptFieldFuzzyQuery.build( + randomScript(), + leafFactory, + randomAlphaOfLength(5), + term, + randomIntBetween(0, 2), + randomIntBetween(0, term.length()), + randomBoolean() + ); + } + + @Override + protected StringScriptFieldFuzzyQuery copy(StringScriptFieldFuzzyQuery orig) { + return StringScriptFieldFuzzyQuery.build( + orig.script(), + leafFactory, + orig.fieldName(), + orig.delegate().getTerm().bytes().utf8ToString(), + orig.delegate().getMaxEdits(), + orig.delegate().getPrefixLength(), + orig.delegate().getTranspositions() + ); + } + + @Override + protected StringScriptFieldFuzzyQuery mutate(StringScriptFieldFuzzyQuery orig) { + Script script = orig.script(); + String fieldName = orig.fieldName(); + String term = orig.delegate().getTerm().bytes().utf8ToString(); + int maxEdits = orig.delegate().getMaxEdits(); + int prefixLength = orig.delegate().getPrefixLength(); + boolean transpositions = orig.delegate().getTranspositions(); + switch (randomInt(5)) { + case 0: + script = randomValueOtherThan(script, this::randomScript); + break; + case 1: + fieldName += "modified"; + break; + case 2: + term += "modified"; + break; + case 3: + maxEdits = randomValueOtherThan(maxEdits, () -> randomIntBetween(0, 2)); + break; + case 4: + prefixLength += 1; + break; + case 5: + transpositions = !transpositions; + break; + default: + fail(); + } + return StringScriptFieldFuzzyQuery.build(script, leafFactory, fieldName, term, maxEdits, prefixLength, transpositions); + } + + @Override + public void testMatches() { + StringScriptFieldFuzzyQuery query = StringScriptFieldFuzzyQuery.build(randomScript(), leafFactory, "test", "foo", 1, 0, false); + assertTrue(query.matches(List.of("foo"))); + assertTrue(query.matches(List.of("foa"))); + assertTrue(query.matches(List.of("foo", "bar"))); + assertFalse(query.matches(List.of("bar"))); + query = StringScriptFieldFuzzyQuery.build(randomScript(), leafFactory, "test", "foo", 0, 0, false); + assertTrue(query.matches(List.of("foo"))); + assertFalse(query.matches(List.of("foa"))); + query = StringScriptFieldFuzzyQuery.build(randomScript(), leafFactory, "test", "foo", 2, 0, false); + assertTrue(query.matches(List.of("foo"))); + assertTrue(query.matches(List.of("foa"))); + assertTrue(query.matches(List.of("faa"))); + assertFalse(query.matches(List.of("faaa"))); + } + + @Override + protected void assertToString(StringScriptFieldFuzzyQuery query) { + assertThat(query.toString(query.fieldName()), equalTo(query.delegate().getTerm().bytes().utf8ToString())); + } + + @Override + public void testVisit() { + StringScriptFieldFuzzyQuery query = createTestInstance(); + ByteRunAutomaton automaton = visitForSingleAutomata(query); + BytesRef term = query.delegate().getTerm().bytes(); + assertThat(automaton.run(term.bytes, term.offset, term.length), is(true)); + term = new BytesRef(query.delegate().getTerm().bytes().utf8ToString() + "a"); + assertThat( + automaton.run(term.bytes, term.offset, term.length), + is(query.delegate().getMaxEdits() > 0 && query.delegate().getPrefixLength() < term.length) + ); + term = new BytesRef(query.delegate().getTerm().bytes().utf8ToString() + "abba"); + assertThat(automaton.run(term.bytes, term.offset, term.length), is(false)); + } +} diff --git a/x-pack/plugin/runtime-fields/src/test/java/org/elasticsearch/xpack/runtimefields/query/StringScriptFieldPrefixQueryTests.java b/x-pack/plugin/runtime-fields/src/test/java/org/elasticsearch/xpack/runtimefields/query/StringScriptFieldPrefixQueryTests.java new file mode 100644 index 0000000000000..ad1efc1d43b2d --- /dev/null +++ b/x-pack/plugin/runtime-fields/src/test/java/org/elasticsearch/xpack/runtimefields/query/StringScriptFieldPrefixQueryTests.java @@ -0,0 +1,75 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +package org.elasticsearch.xpack.runtimefields.query; + +import org.apache.lucene.util.BytesRef; +import org.apache.lucene.util.automaton.ByteRunAutomaton; +import org.elasticsearch.script.Script; + +import java.util.List; + +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; + +public class StringScriptFieldPrefixQueryTests extends AbstractStringScriptFieldQueryTestCase { + @Override + protected StringScriptFieldPrefixQuery createTestInstance() { + return new StringScriptFieldPrefixQuery(randomScript(), leafFactory, randomAlphaOfLength(5), randomAlphaOfLength(6)); + } + + @Override + protected StringScriptFieldPrefixQuery copy(StringScriptFieldPrefixQuery orig) { + return new StringScriptFieldPrefixQuery(orig.script(), leafFactory, orig.fieldName(), orig.prefix()); + } + + @Override + protected StringScriptFieldPrefixQuery mutate(StringScriptFieldPrefixQuery orig) { + Script script = orig.script(); + String fieldName = orig.fieldName(); + String prefix = orig.prefix(); + switch (randomInt(2)) { + case 0: + script = randomValueOtherThan(script, this::randomScript); + break; + case 1: + fieldName += "modified"; + break; + case 2: + prefix += "modified"; + break; + default: + fail(); + } + return new StringScriptFieldPrefixQuery(script, leafFactory, fieldName, prefix); + } + + @Override + public void testMatches() { + StringScriptFieldPrefixQuery query = new StringScriptFieldPrefixQuery(randomScript(), leafFactory, "test", "foo"); + assertTrue(query.matches(List.of("foo"))); + assertTrue(query.matches(List.of("foooo"))); + assertFalse(query.matches(List.of("fo"))); + assertTrue(query.matches(List.of("fo", "foo"))); + } + + @Override + protected void assertToString(StringScriptFieldPrefixQuery query) { + assertThat(query.toString(query.fieldName()), equalTo(query.prefix())); + } + + @Override + public void testVisit() { + StringScriptFieldPrefixQuery query = createTestInstance(); + ByteRunAutomaton automaton = visitForSingleAutomata(query); + BytesRef term = new BytesRef(query.prefix()); + assertThat(automaton.run(term.bytes, term.offset, term.length), is(true)); + term = new BytesRef(query.prefix() + "a"); + assertThat(automaton.run(term.bytes, term.offset, term.length), is(true)); + term = new BytesRef("a" + query.prefix()); + assertThat(automaton.run(term.bytes, term.offset, term.length), is(false)); + } +} diff --git a/x-pack/plugin/runtime-fields/src/test/java/org/elasticsearch/xpack/runtimefields/query/StringScriptFieldRangeQueryTests.java b/x-pack/plugin/runtime-fields/src/test/java/org/elasticsearch/xpack/runtimefields/query/StringScriptFieldRangeQueryTests.java new file mode 100644 index 0000000000000..78d9f477eb7d0 --- /dev/null +++ b/x-pack/plugin/runtime-fields/src/test/java/org/elasticsearch/xpack/runtimefields/query/StringScriptFieldRangeQueryTests.java @@ -0,0 +1,120 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +package org.elasticsearch.xpack.runtimefields.query; + +import org.apache.lucene.util.BytesRef; +import org.apache.lucene.util.automaton.ByteRunAutomaton; +import org.elasticsearch.script.Script; + +import java.util.List; + +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.is; + +public class StringScriptFieldRangeQueryTests extends AbstractStringScriptFieldQueryTestCase { + @Override + protected StringScriptFieldRangeQuery createTestInstance() { + String lower = randomAlphaOfLength(3); + String upper = randomValueOtherThan(lower, () -> randomAlphaOfLength(3)); + if (lower.compareTo(upper) > 0) { + String tmp = lower; + lower = upper; + upper = tmp; + } + return new StringScriptFieldRangeQuery( + randomScript(), + leafFactory, + randomAlphaOfLength(5), + lower, + upper, + randomBoolean(), + randomBoolean() + ); + } + + @Override + protected StringScriptFieldRangeQuery copy(StringScriptFieldRangeQuery orig) { + return new StringScriptFieldRangeQuery( + orig.script(), + leafFactory, + orig.fieldName(), + orig.lowerValue(), + orig.upperValue(), + orig.includeLower(), + orig.includeUpper() + ); + } + + @Override + protected StringScriptFieldRangeQuery mutate(StringScriptFieldRangeQuery orig) { + Script script = orig.script(); + String fieldName = orig.fieldName(); + String lower = orig.lowerValue(); + String upper = orig.upperValue(); + boolean includeLower = orig.includeLower(); + boolean includeUpper = orig.includeUpper(); + switch (randomInt(5)) { + case 0: + script = randomValueOtherThan(script, this::randomScript); + break; + case 1: + fieldName += "modified"; + break; + case 2: + lower += "modified"; + break; + case 3: + upper += "modified"; + break; + case 4: + includeLower = !includeLower; + break; + case 5: + includeUpper = !includeUpper; + break; + default: + fail(); + } + return new StringScriptFieldRangeQuery(script, leafFactory, fieldName, lower, upper, includeLower, includeUpper); + } + + @Override + public void testMatches() { + StringScriptFieldRangeQuery query = new StringScriptFieldRangeQuery(randomScript(), leafFactory, "test", "a", "b", true, true); + assertTrue(query.matches(List.of("a"))); + assertTrue(query.matches(List.of("ab"))); + assertTrue(query.matches(List.of("b"))); + assertFalse(query.matches(List.of("ba"))); + assertTrue(query.matches(List.of("a", "c"))); + query = new StringScriptFieldRangeQuery(randomScript(), leafFactory, "test", "a", "b", false, false); + assertFalse(query.matches(List.of("a"))); + assertTrue(query.matches(List.of("ab"))); + assertFalse(query.matches(List.of("b"))); + assertFalse(query.matches(List.of("ba"))); + } + + @Override + protected void assertToString(StringScriptFieldRangeQuery query) { + assertThat(query.toString(query.fieldName()), containsString(query.includeLower() ? "[" : "{")); + assertThat(query.toString(query.fieldName()), containsString(query.includeUpper() ? "]" : "}")); + assertThat(query.toString(query.fieldName()), containsString(query.lowerValue() + " TO " + query.upperValue())); + } + + @Override + public void testVisit() { + StringScriptFieldRangeQuery query = createTestInstance(); + ByteRunAutomaton automaton = visitForSingleAutomata(query); + BytesRef term = new BytesRef(query.lowerValue() + "a"); + assertThat(automaton.run(term.bytes, term.offset, term.length), is(true)); + term = new BytesRef(query.lowerValue()); + assertThat(automaton.run(term.bytes, term.offset, term.length), is(query.includeLower())); + term = new BytesRef(query.upperValue() + "a"); + assertThat(automaton.run(term.bytes, term.offset, term.length), is(false)); + term = new BytesRef(query.upperValue()); + assertThat(automaton.run(term.bytes, term.offset, term.length), is(query.includeUpper())); + } +} diff --git a/x-pack/plugin/runtime-fields/src/test/java/org/elasticsearch/xpack/runtimefields/query/StringScriptFieldRegexpQueryTests.java b/x-pack/plugin/runtime-fields/src/test/java/org/elasticsearch/xpack/runtimefields/query/StringScriptFieldRegexpQueryTests.java new file mode 100644 index 0000000000000..2f2a28952b4f8 --- /dev/null +++ b/x-pack/plugin/runtime-fields/src/test/java/org/elasticsearch/xpack/runtimefields/query/StringScriptFieldRegexpQueryTests.java @@ -0,0 +1,106 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +package org.elasticsearch.xpack.runtimefields.query; + +import org.apache.lucene.util.BytesRef; +import org.apache.lucene.util.automaton.ByteRunAutomaton; +import org.apache.lucene.util.automaton.Operations; +import org.elasticsearch.script.Script; + +import java.util.List; + +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; + +public class StringScriptFieldRegexpQueryTests extends AbstractStringScriptFieldQueryTestCase { + @Override + protected StringScriptFieldRegexpQuery createTestInstance() { + return new StringScriptFieldRegexpQuery( + randomScript(), + leafFactory, + randomAlphaOfLength(5), + randomAlphaOfLength(6), + randomInt(0xFFFF), + Operations.DEFAULT_MAX_DETERMINIZED_STATES + ); + } + + @Override + protected StringScriptFieldRegexpQuery copy(StringScriptFieldRegexpQuery orig) { + return new StringScriptFieldRegexpQuery( + orig.script(), + leafFactory, + orig.fieldName(), + orig.pattern(), + orig.flags(), + Operations.DEFAULT_MAX_DETERMINIZED_STATES + ); + } + + @Override + protected StringScriptFieldRegexpQuery mutate(StringScriptFieldRegexpQuery orig) { + Script script = orig.script(); + String fieldName = orig.fieldName(); + String pattern = orig.pattern(); + int flags = orig.flags(); + switch (randomInt(3)) { + case 0: + script = randomValueOtherThan(script, this::randomScript); + break; + case 1: + fieldName += "modified"; + break; + case 2: + pattern += "modified"; + break; + case 3: + flags = randomValueOtherThan(flags, () -> randomInt(0xFFFF)); + break; + default: + fail(); + } + return new StringScriptFieldRegexpQuery(script, leafFactory, fieldName, pattern, flags, Operations.DEFAULT_MAX_DETERMINIZED_STATES); + } + + @Override + public void testMatches() { + StringScriptFieldRegexpQuery query = new StringScriptFieldRegexpQuery( + randomScript(), + leafFactory, + "test", + "a.+b", + 0, + Operations.DEFAULT_MAX_DETERMINIZED_STATES + ); + assertTrue(query.matches(List.of("astuffb"))); + assertFalse(query.matches(List.of("fffff"))); + assertFalse(query.matches(List.of("ab"))); + assertFalse(query.matches(List.of("aasdf"))); + assertFalse(query.matches(List.of("dsfb"))); + assertTrue(query.matches(List.of("astuffb", "fffff"))); + } + + @Override + protected void assertToString(StringScriptFieldRegexpQuery query) { + assertThat(query.toString(query.fieldName()), equalTo("/" + query.pattern() + "/")); + } + + @Override + public void testVisit() { + StringScriptFieldRegexpQuery query = new StringScriptFieldRegexpQuery( + randomScript(), + leafFactory, + "test", + "a.+b", + 0, + Operations.DEFAULT_MAX_DETERMINIZED_STATES + ); + ByteRunAutomaton automaton = visitForSingleAutomata(query); + BytesRef term = new BytesRef("astuffb"); + assertThat(automaton.run(term.bytes, term.offset, term.length), is(true)); + } +} diff --git a/x-pack/plugin/runtime-fields/src/test/java/org/elasticsearch/xpack/runtimefields/query/StringScriptFieldTermQueryTests.java b/x-pack/plugin/runtime-fields/src/test/java/org/elasticsearch/xpack/runtimefields/query/StringScriptFieldTermQueryTests.java index d6e444b0923b3..bfdd4eba41089 100644 --- a/x-pack/plugin/runtime-fields/src/test/java/org/elasticsearch/xpack/runtimefields/query/StringScriptFieldTermQueryTests.java +++ b/x-pack/plugin/runtime-fields/src/test/java/org/elasticsearch/xpack/runtimefields/query/StringScriptFieldTermQueryTests.java @@ -10,8 +10,10 @@ import org.apache.lucene.search.Query; import org.apache.lucene.search.QueryVisitor; import org.apache.lucene.util.automaton.ByteRunAutomaton; +import org.elasticsearch.script.Script; import java.util.Arrays; +import java.util.List; import java.util.Set; import java.util.TreeSet; import java.util.function.Supplier; @@ -21,20 +23,41 @@ public class StringScriptFieldTermQueryTests extends AbstractStringScriptFieldQueryTestCase { @Override protected StringScriptFieldTermQuery createTestInstance() { - return new StringScriptFieldTermQuery(leafFactory, randomAlphaOfLength(5), randomAlphaOfLength(6)); + return new StringScriptFieldTermQuery(randomScript(), leafFactory, randomAlphaOfLength(5), randomAlphaOfLength(6)); } @Override protected StringScriptFieldTermQuery copy(StringScriptFieldTermQuery orig) { - return new StringScriptFieldTermQuery(leafFactory, orig.fieldName(), orig.term()); + return new StringScriptFieldTermQuery(orig.script(), leafFactory, orig.fieldName(), orig.term()); } @Override protected StringScriptFieldTermQuery mutate(StringScriptFieldTermQuery orig) { - if (randomBoolean()) { - return new StringScriptFieldTermQuery(leafFactory, orig.fieldName() + "modified", orig.term()); + Script script = orig.script(); + String fieldName = orig.fieldName(); + String term = orig.term(); + switch (randomInt(2)) { + case 0: + script = randomValueOtherThan(script, this::randomScript); + break; + case 1: + fieldName += "modified"; + break; + case 2: + term += "modified"; + break; + default: + fail(); } - return new StringScriptFieldTermQuery(leafFactory, orig.fieldName(), orig.term() + "modified"); + return new StringScriptFieldTermQuery(script, leafFactory, fieldName, term); + } + + @Override + public void testMatches() { + StringScriptFieldTermQuery query = new StringScriptFieldTermQuery(randomScript(), leafFactory, "test", "foo"); + assertTrue(query.matches(List.of("foo"))); + assertFalse(query.matches(List.of("bar"))); + assertTrue(query.matches(List.of("foo", "bar"))); } @Override diff --git a/x-pack/plugin/runtime-fields/src/test/java/org/elasticsearch/xpack/runtimefields/query/StringScriptFieldTermsQueryTests.java b/x-pack/plugin/runtime-fields/src/test/java/org/elasticsearch/xpack/runtimefields/query/StringScriptFieldTermsQueryTests.java index 63ec95c559f40..1e7ed94203ea9 100644 --- a/x-pack/plugin/runtime-fields/src/test/java/org/elasticsearch/xpack/runtimefields/query/StringScriptFieldTermsQueryTests.java +++ b/x-pack/plugin/runtime-fields/src/test/java/org/elasticsearch/xpack/runtimefields/query/StringScriptFieldTermsQueryTests.java @@ -10,8 +10,10 @@ import org.apache.lucene.search.Query; import org.apache.lucene.search.QueryVisitor; import org.apache.lucene.util.automaton.ByteRunAutomaton; +import org.elasticsearch.script.Script; import java.util.Arrays; +import java.util.List; import java.util.Set; import java.util.TreeSet; import java.util.function.Supplier; @@ -23,22 +25,45 @@ public class StringScriptFieldTermsQueryTests extends AbstractStringScriptFieldQ @Override protected StringScriptFieldTermsQuery createTestInstance() { Set terms = new TreeSet<>(Arrays.asList(generateRandomStringArray(4, 6, false, false))); - return new StringScriptFieldTermsQuery(leafFactory, randomAlphaOfLength(5), terms); + return new StringScriptFieldTermsQuery(randomScript(), leafFactory, randomAlphaOfLength(5), terms); } @Override protected StringScriptFieldTermsQuery copy(StringScriptFieldTermsQuery orig) { - return new StringScriptFieldTermsQuery(leafFactory, orig.fieldName(), orig.terms()); + return new StringScriptFieldTermsQuery(orig.script(), leafFactory, orig.fieldName(), orig.terms()); } @Override protected StringScriptFieldTermsQuery mutate(StringScriptFieldTermsQuery orig) { - if (randomBoolean()) { - return new StringScriptFieldTermsQuery(leafFactory, orig.fieldName() + "modified", orig.terms()); + Script script = orig.script(); + String fieldName = orig.fieldName(); + Set terms = orig.terms(); + switch (randomInt(2)) { + case 0: + script = randomValueOtherThan(script, this::randomScript); + break; + case 1: + fieldName += "modified"; + break; + case 2: + terms = new TreeSet<>(orig.terms()); + terms.add(randomAlphaOfLength(7)); + break; + default: + fail(); } - Set terms = new TreeSet<>(orig.terms()); - terms.add(randomAlphaOfLength(7)); - return new StringScriptFieldTermsQuery(leafFactory, orig.fieldName(), terms); + return new StringScriptFieldTermsQuery(script, leafFactory, fieldName, terms); + } + + @Override + public void testMatches() { + StringScriptFieldTermsQuery query = new StringScriptFieldTermsQuery(randomScript(), leafFactory, "test", Set.of("foo", "bar")); + assertTrue(query.matches(List.of("foo"))); + assertTrue(query.matches(List.of("bar"))); + assertFalse(query.matches(List.of("baz"))); + assertTrue(query.matches(List.of("foo", "baz"))); + assertTrue(query.matches(List.of("bar", "baz"))); + assertFalse(query.matches(List.of("baz", "bort"))); } @Override diff --git a/x-pack/plugin/runtime-fields/src/test/java/org/elasticsearch/xpack/runtimefields/query/StringScriptFieldWildcardQueryTests.java b/x-pack/plugin/runtime-fields/src/test/java/org/elasticsearch/xpack/runtimefields/query/StringScriptFieldWildcardQueryTests.java new file mode 100644 index 0000000000000..0841585e07fc1 --- /dev/null +++ b/x-pack/plugin/runtime-fields/src/test/java/org/elasticsearch/xpack/runtimefields/query/StringScriptFieldWildcardQueryTests.java @@ -0,0 +1,73 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +package org.elasticsearch.xpack.runtimefields.query; + +import org.apache.lucene.util.BytesRef; +import org.apache.lucene.util.automaton.ByteRunAutomaton; +import org.elasticsearch.script.Script; + +import java.util.List; + +import static org.hamcrest.Matchers.equalTo; + +public class StringScriptFieldWildcardQueryTests extends AbstractStringScriptFieldQueryTestCase { + @Override + protected StringScriptFieldWildcardQuery createTestInstance() { + return new StringScriptFieldWildcardQuery(randomScript(), leafFactory, randomAlphaOfLength(5), randomAlphaOfLength(6)); + } + + @Override + protected StringScriptFieldWildcardQuery copy(StringScriptFieldWildcardQuery orig) { + return new StringScriptFieldWildcardQuery(orig.script(), leafFactory, orig.fieldName(), orig.pattern()); + } + + @Override + protected StringScriptFieldWildcardQuery mutate(StringScriptFieldWildcardQuery orig) { + Script script = orig.script(); + String fieldName = orig.fieldName(); + String pattern = orig.pattern(); + switch (randomInt(2)) { + case 0: + script = randomValueOtherThan(script, this::randomScript); + break; + case 1: + fieldName += "modified"; + break; + case 2: + pattern += "modified"; + break; + default: + fail(); + } + return new StringScriptFieldWildcardQuery(script, leafFactory, fieldName, pattern); + } + + @Override + public void testMatches() { + StringScriptFieldWildcardQuery query = new StringScriptFieldWildcardQuery(randomScript(), leafFactory, "test", "a*b"); + assertTrue(query.matches(List.of("astuffb"))); + assertFalse(query.matches(List.of("fffff"))); + assertFalse(query.matches(List.of("a"))); + assertFalse(query.matches(List.of("b"))); + assertFalse(query.matches(List.of("aasdf"))); + assertFalse(query.matches(List.of("dsfb"))); + assertTrue(query.matches(List.of("astuffb", "fffff"))); + } + + @Override + protected void assertToString(StringScriptFieldWildcardQuery query) { + assertThat(query.toString(query.fieldName()), equalTo("/" + query.pattern() + "/")); + } + + @Override + public void testVisit() { + StringScriptFieldWildcardQuery query = new StringScriptFieldWildcardQuery(randomScript(), leafFactory, "test", "a*b"); + ByteRunAutomaton automaton = visitForSingleAutomata(query); + BytesRef term = new BytesRef("astuffb"); + assertTrue(automaton.run(term.bytes, term.offset, term.length)); + } +} diff --git a/x-pack/plugin/src/test/resources/rest-api-spec/test/runtime_fields/10_keyword.yml b/x-pack/plugin/src/test/resources/rest-api-spec/test/runtime_fields/10_keyword.yml index e66f8d9b6a32a..2ccd18b13e9f4 100644 --- a/x-pack/plugin/src/test/resources/rest-api-spec/test/runtime_fields/10_keyword.yml +++ b/x-pack/plugin/src/test/resources/rest-api-spec/test/runtime_fields/10_keyword.yml @@ -89,6 +89,58 @@ setup: - match: {hits.hits.0._source.voltage: 4.0} - match: {hits.hits.1._source.voltage: 5.2} +--- +"fuzzy query": + - do: + search: + index: sensor + body: + query: + fuzzy: + day_of_week: + value: Manday + fuzziness: 1 + - match: {hits.total.value: 1} + - match: {hits.hits.0._source.voltage: 5.8} + +--- +"prefix query": + - do: + search: + index: sensor + body: + query: + prefix: + day_of_week: M + - match: {hits.total.value: 1} + - match: {hits.hits.0._source.voltage: 5.8} + +--- +"range query": + - do: + search: + index: sensor + body: + query: + range: + day_of_week: + gt: M + lt: N + - match: {hits.total.value: 1} + - match: {hits.hits.0._source.voltage: 5.8} + +--- +"regexp query": + - do: + search: + index: sensor + body: + query: + regexp: + day_of_week: M[aeiou]nday + - match: {hits.total.value: 1} + - match: {hits.hits.0._source.voltage: 5.8} + --- "term query": - do: @@ -114,3 +166,15 @@ setup: - match: {hits.total.value: 2} - match: {hits.hits.0._source.voltage: 5.8} - match: {hits.hits.1._source.voltage: 5.2} + +--- +"wildcard query": + - do: + search: + index: sensor + body: + query: + wildcard: + day_of_week: M*ay + - match: {hits.total.value: 1} + - match: {hits.hits.0._source.voltage: 5.8}