diff --git a/server/src/main/java/org/elasticsearch/index/query/IntervalFilter.java b/server/src/main/java/org/elasticsearch/index/query/IntervalFilter.java new file mode 100644 index 0000000000000..7e68ed0dad953 --- /dev/null +++ b/server/src/main/java/org/elasticsearch/index/query/IntervalFilter.java @@ -0,0 +1,55 @@ +/* + * 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.queries.intervals.IntervalsSource; +import org.elasticsearch.common.ParsingException; +import org.elasticsearch.common.io.stream.NamedWriteable; +import org.elasticsearch.common.xcontent.NamedObjectNotFoundException; +import org.elasticsearch.common.xcontent.ToXContentObject; +import org.elasticsearch.common.xcontent.XContentLocation; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.index.mapper.MappedFieldType; + +import java.io.IOException; + +public abstract class IntervalFilter implements NamedWriteable, ToXContentObject { + @Override + public abstract int hashCode(); + + @Override + public abstract boolean equals(Object other); + + public abstract IntervalsSource filter(IntervalsSource input, QueryShardContext context, MappedFieldType fieldType) + throws IOException; + + public static IntervalFilter fromXContent(XContentParser parser) throws IOException { + if (parser.nextToken() != XContentParser.Token.FIELD_NAME) { + throw new ParsingException(parser.getTokenLocation(), "Expected [FIELD_NAME] but got [" + parser.currentToken() + "]"); + } + String type = parser.currentName(); + try { + return parser.namedObject(IntervalFilter.class, type, null); + } catch (NamedObjectNotFoundException e) { + throw new ParsingException( + new XContentLocation(e.getLineNumber(), e.getColumnNumber()), "Unknown interval filter [" + type + "]", e); + } + } +} diff --git a/server/src/main/java/org/elasticsearch/index/query/IntervalFilterScript.java b/server/src/main/java/org/elasticsearch/index/query/IntervalFilterScript.java index 1f86179dca73c..2e5bcc99e458d 100644 --- a/server/src/main/java/org/elasticsearch/index/query/IntervalFilterScript.java +++ b/server/src/main/java/org/elasticsearch/index/query/IntervalFilterScript.java @@ -23,7 +23,7 @@ import org.elasticsearch.script.ScriptContext; /** - * Base class for scripts used as interval filters, see {@link IntervalsSourceProvider.IntervalFilter} + * Base class for scripts used as interval filters, see {@link IntervalFilter} */ public abstract class IntervalFilterScript { diff --git a/server/src/main/java/org/elasticsearch/index/query/IntervalsSourceProvider.java b/server/src/main/java/org/elasticsearch/index/query/IntervalsSourceProvider.java index 81cc1524549a8..4e4ca9db7fa84 100644 --- a/server/src/main/java/org/elasticsearch/index/query/IntervalsSourceProvider.java +++ b/server/src/main/java/org/elasticsearch/index/query/IntervalsSourceProvider.java @@ -32,20 +32,18 @@ import org.elasticsearch.common.io.stream.NamedWriteable; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; -import org.elasticsearch.common.io.stream.Writeable; import org.elasticsearch.common.xcontent.ConstructingObjectParser; +import org.elasticsearch.common.xcontent.NamedObjectNotFoundException; import org.elasticsearch.common.xcontent.ToXContentFragment; -import org.elasticsearch.common.xcontent.ToXContentObject; import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentLocation; import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.index.analysis.NamedAnalyzer; import org.elasticsearch.index.mapper.MappedFieldType; -import org.elasticsearch.script.Script; import java.io.IOException; import java.util.ArrayList; import java.util.List; -import java.util.Locale; import java.util.Objects; import java.util.Set; @@ -74,23 +72,16 @@ public abstract class IntervalsSourceProvider implements NamedWriteable, ToXCont public static IntervalsSourceProvider fromXContent(XContentParser parser) throws IOException { assert parser.currentToken() == XContentParser.Token.FIELD_NAME; - switch (parser.currentName()) { - case "match": - return Match.fromXContent(parser); - case "any_of": - return Disjunction.fromXContent(parser); - case "all_of": - return Combine.fromXContent(parser); - case "prefix": - return Prefix.fromXContent(parser); - case "wildcard": - return Wildcard.fromXContent(parser); - } - throw new ParsingException(parser.getTokenLocation(), - "Unknown interval type [" + parser.currentName() + "], expecting one of [match, any_of, all_of, prefix]"); + String name = parser.currentName(); + try { + return parser.namedObject(IntervalsSourceProvider.class, name, null); + } catch (NamedObjectNotFoundException e) { + throw new ParsingException( + new XContentLocation(e.getLineNumber(), e.getColumnNumber()), "Unknown interval type [" + name + "]", e); + } } - private static IntervalsSourceProvider parseInnerIntervals(XContentParser parser) throws IOException { + public static IntervalsSourceProvider parseInnerIntervals(XContentParser parser) throws IOException { if (parser.nextToken() != XContentParser.Token.FIELD_NAME) { throw new ParsingException(parser.getTokenLocation(), "Expected [FIELD_NAME] but got [" + parser.currentToken() + "]"); } @@ -126,7 +117,7 @@ public Match(StreamInput in) throws IOException { this.maxGaps = in.readVInt(); this.ordered = in.readBoolean(); this.analyzer = in.readOptionalString(); - this.filter = in.readOptionalWriteable(IntervalFilter::new); + this.filter = in.readOptionalNamedWriteable(IntervalFilter.class); if (in.getVersion().onOrAfter(Version.V_7_2_0)) { this.useField = in.readOptionalString(); } @@ -192,7 +183,7 @@ public void writeTo(StreamOutput out) throws IOException { out.writeVInt(maxGaps); out.writeBoolean(ordered); out.writeOptionalString(analyzer); - out.writeOptionalWriteable(filter); + out.writeOptionalNamedWriteable(filter); if (out.getVersion().onOrAfter(Version.V_7_2_0)) { out.writeOptionalString(useField); } @@ -255,7 +246,7 @@ public Disjunction(List subSources, IntervalFilter filt public Disjunction(StreamInput in) throws IOException { this.subSources = in.readNamedWriteableList(IntervalsSourceProvider.class); - this.filter = in.readOptionalWriteable(IntervalFilter::new); + this.filter = in.readOptionalNamedWriteable(IntervalFilter.class); } @Override @@ -299,7 +290,7 @@ public String getWriteableName() { @Override public void writeTo(StreamOutput out) throws IOException { out.writeNamedWriteableList(subSources); - out.writeOptionalWriteable(filter); + out.writeOptionalNamedWriteable(filter); } @Override @@ -357,7 +348,7 @@ public Combine(StreamInput in) throws IOException { this.ordered = in.readBoolean(); this.subSources = in.readNamedWriteableList(IntervalsSourceProvider.class); this.maxGaps = in.readInt(); - this.filter = in.readOptionalWriteable(IntervalFilter::new); + this.filter = in.readOptionalNamedWriteable(IntervalFilter.class); } @Override @@ -404,7 +395,7 @@ public void writeTo(StreamOutput out) throws IOException { out.writeBoolean(ordered); out.writeNamedWriteableList(subSources); out.writeInt(maxGaps); - out.writeOptionalWriteable(filter); + out.writeOptionalNamedWriteable(filter); } @Override @@ -667,12 +658,12 @@ public static Wildcard fromXContent(XContentParser parser) throws IOException { } } - static class ScriptFilterSource extends FilteredIntervalsSource { + public static class ScriptFilterSource extends FilteredIntervalsSource { final IntervalFilterScript script; IntervalFilterScript.Interval interval = new IntervalFilterScript.Interval(); - ScriptFilterSource(IntervalsSource in, String name, IntervalFilterScript script) { + public ScriptFilterSource(IntervalsSource in, String name, IntervalFilterScript script) { super("FILTER(" + name + ")", in); this.script = script; } @@ -683,133 +674,4 @@ protected boolean accept(IntervalIterator it) { return script.execute(interval); } } - - public static class IntervalFilter implements ToXContentObject, Writeable { - - public static final String NAME = "filter"; - - private final String type; - private final IntervalsSourceProvider filter; - private final Script script; - - public IntervalFilter(IntervalsSourceProvider filter, String type) { - this.filter = filter; - this.type = type.toLowerCase(Locale.ROOT); - this.script = null; - } - - IntervalFilter(Script script) { - this.script = script; - this.type = "script"; - this.filter = null; - } - - public IntervalFilter(StreamInput in) throws IOException { - this.type = in.readString(); - this.filter = in.readOptionalNamedWriteable(IntervalsSourceProvider.class); - if (in.readBoolean()) { - this.script = new Script(in); - } - else { - this.script = null; - } - } - - public IntervalsSource filter(IntervalsSource input, QueryShardContext context, MappedFieldType fieldType) throws IOException { - if (script != null) { - IntervalFilterScript ifs = context.getScriptService().compile(script, IntervalFilterScript.CONTEXT).newInstance(); - return new ScriptFilterSource(input, script.getIdOrCode(), ifs); - } - IntervalsSource filterSource = filter.getSource(context, fieldType); - switch (type) { - case "containing": - return Intervals.containing(input, filterSource); - case "contained_by": - return Intervals.containedBy(input, filterSource); - case "not_containing": - return Intervals.notContaining(input, filterSource); - case "not_contained_by": - return Intervals.notContainedBy(input, filterSource); - case "overlapping": - return Intervals.overlapping(input, filterSource); - case "not_overlapping": - return Intervals.nonOverlapping(input, filterSource); - case "before": - return Intervals.before(input, filterSource); - case "after": - return Intervals.after(input, filterSource); - default: - throw new IllegalArgumentException("Unknown filter type [" + type + "]"); - } - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - IntervalFilter that = (IntervalFilter) o; - return Objects.equals(type, that.type) && - Objects.equals(filter, that.filter); - } - - @Override - public int hashCode() { - return Objects.hash(type, filter); - } - - @Override - public void writeTo(StreamOutput out) throws IOException { - out.writeString(type); - out.writeOptionalNamedWriteable(filter); - if (script == null) { - out.writeBoolean(false); - } - else { - out.writeBoolean(true); - script.writeTo(out); - } - } - - @Override - public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { - builder.startObject(); - builder.field(type); - builder.startObject(); - filter.toXContent(builder, params); - builder.endObject(); - builder.endObject(); - return builder; - } - - public static IntervalFilter fromXContent(XContentParser parser) throws IOException { - if (parser.nextToken() != XContentParser.Token.FIELD_NAME) { - throw new ParsingException(parser.getTokenLocation(), "Expected [FIELD_NAME] but got [" + parser.currentToken() + "]"); - } - String type = parser.currentName(); - if (Script.SCRIPT_PARSE_FIELD.match(type, parser.getDeprecationHandler())) { - Script script = Script.parse(parser); - if (parser.nextToken() != XContentParser.Token.END_OBJECT) { - throw new ParsingException(parser.getTokenLocation(), "Expected [END_OBJECT] but got [" + parser.currentToken() + "]"); - } - return new IntervalFilter(script); - } - if (parser.nextToken() != XContentParser.Token.START_OBJECT) { - throw new ParsingException(parser.getTokenLocation(), "Expected [START_OBJECT] but got [" + parser.currentToken() + "]"); - } - if (parser.nextToken() != XContentParser.Token.FIELD_NAME) { - throw new ParsingException(parser.getTokenLocation(), "Expected [FIELD_NAME] but got [" + parser.currentToken() + "]"); - } - IntervalsSourceProvider intervals = IntervalsSourceProvider.fromXContent(parser); - if (parser.nextToken() != XContentParser.Token.END_OBJECT) { - throw new ParsingException(parser.getTokenLocation(), "Expected [END_OBJECT] but got [" + parser.currentToken() + "]"); - } - if (parser.nextToken() != XContentParser.Token.END_OBJECT) { - throw new ParsingException(parser.getTokenLocation(), "Expected [END_OBJECT] but got [" + parser.currentToken() + "]"); - } - return new IntervalFilter(intervals, type); - } - } - - - } diff --git a/server/src/main/java/org/elasticsearch/index/query/intervals/AfterIntervalFilter.java b/server/src/main/java/org/elasticsearch/index/query/intervals/AfterIntervalFilter.java new file mode 100644 index 0000000000000..c41944d7b8201 --- /dev/null +++ b/server/src/main/java/org/elasticsearch/index/query/intervals/AfterIntervalFilter.java @@ -0,0 +1,49 @@ +/* + * 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.intervals; + +import org.apache.lucene.queries.intervals.Intervals; +import org.apache.lucene.queries.intervals.IntervalsSource; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.index.query.IntervalsSourceProvider; + +import java.io.IOException; + +public class AfterIntervalFilter extends SourceProviderIntervalFilter { + public static final String NAME = "after"; + + public AfterIntervalFilter(final IntervalsSourceProvider filter) { + super(NAME, filter); + } + + public AfterIntervalFilter(final StreamInput in) throws IOException { + super(NAME, in); + } + + @Override + public IntervalsSource getIntervalsSource(final IntervalsSource input, final IntervalsSource filterSource) throws IOException { + return Intervals.after(input, filterSource); + } + + public static AfterIntervalFilter fromXContent(XContentParser parser) throws IOException { + return fromXContent(parser, AfterIntervalFilter::new); + } +} diff --git a/server/src/main/java/org/elasticsearch/index/query/intervals/BeforeIntervalFilter.java b/server/src/main/java/org/elasticsearch/index/query/intervals/BeforeIntervalFilter.java new file mode 100644 index 0000000000000..aefd6b6c8cb44 --- /dev/null +++ b/server/src/main/java/org/elasticsearch/index/query/intervals/BeforeIntervalFilter.java @@ -0,0 +1,49 @@ +/* + * 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.intervals; + +import org.apache.lucene.queries.intervals.Intervals; +import org.apache.lucene.queries.intervals.IntervalsSource; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.index.query.IntervalsSourceProvider; + +import java.io.IOException; + +public class BeforeIntervalFilter extends SourceProviderIntervalFilter { + public static final String NAME = "before"; + + public BeforeIntervalFilter(final IntervalsSourceProvider filter) { + super(NAME, filter); + } + + public BeforeIntervalFilter(final StreamInput in) throws IOException { + super(NAME, in); + } + + @Override + public IntervalsSource getIntervalsSource(final IntervalsSource input, final IntervalsSource filterSource) throws IOException { + return Intervals.before(input, filterSource); + } + + public static BeforeIntervalFilter fromXContent(XContentParser parser) throws IOException { + return fromXContent(parser, BeforeIntervalFilter::new); + } +} diff --git a/server/src/main/java/org/elasticsearch/index/query/intervals/ContainedByIntervalFilter.java b/server/src/main/java/org/elasticsearch/index/query/intervals/ContainedByIntervalFilter.java new file mode 100644 index 0000000000000..d57bd9444956d --- /dev/null +++ b/server/src/main/java/org/elasticsearch/index/query/intervals/ContainedByIntervalFilter.java @@ -0,0 +1,49 @@ +/* + * 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.intervals; + +import org.apache.lucene.queries.intervals.Intervals; +import org.apache.lucene.queries.intervals.IntervalsSource; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.index.query.IntervalsSourceProvider; + +import java.io.IOException; + +public class ContainedByIntervalFilter extends SourceProviderIntervalFilter { + public static final String NAME = "contained_by"; + + public ContainedByIntervalFilter(final IntervalsSourceProvider filter) { + super(NAME, filter); + } + + public ContainedByIntervalFilter(final StreamInput in) throws IOException { + super(NAME, in); + } + + @Override + public IntervalsSource getIntervalsSource(final IntervalsSource input, final IntervalsSource filterSource) throws IOException { + return Intervals.containedBy(input, filterSource); + } + + public static ContainedByIntervalFilter fromXContent(XContentParser parser) throws IOException { + return fromXContent(parser, ContainedByIntervalFilter::new); + } +} diff --git a/server/src/main/java/org/elasticsearch/index/query/intervals/ContainingIntervalFilter.java b/server/src/main/java/org/elasticsearch/index/query/intervals/ContainingIntervalFilter.java new file mode 100644 index 0000000000000..cfd163572f5be --- /dev/null +++ b/server/src/main/java/org/elasticsearch/index/query/intervals/ContainingIntervalFilter.java @@ -0,0 +1,49 @@ +/* + * 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.intervals; + +import org.apache.lucene.queries.intervals.Intervals; +import org.apache.lucene.queries.intervals.IntervalsSource; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.index.query.IntervalsSourceProvider; + +import java.io.IOException; + +public class ContainingIntervalFilter extends SourceProviderIntervalFilter { + public static final String NAME = "containing"; + + public ContainingIntervalFilter(final IntervalsSourceProvider filter) { + super(NAME, filter); + } + + public ContainingIntervalFilter(final StreamInput in) throws IOException { + super(NAME, in); + } + + @Override + public IntervalsSource getIntervalsSource(final IntervalsSource input, final IntervalsSource filterSource) throws IOException { + return Intervals.containing(input, filterSource); + } + + public static ContainingIntervalFilter fromXContent(XContentParser parser) throws IOException { + return fromXContent(parser, ContainingIntervalFilter::new); + } +} diff --git a/server/src/main/java/org/elasticsearch/index/query/intervals/NotContainedByIntervalFilter.java b/server/src/main/java/org/elasticsearch/index/query/intervals/NotContainedByIntervalFilter.java new file mode 100644 index 0000000000000..1c01f8b2e445b --- /dev/null +++ b/server/src/main/java/org/elasticsearch/index/query/intervals/NotContainedByIntervalFilter.java @@ -0,0 +1,49 @@ +/* + * 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.intervals; + +import org.apache.lucene.queries.intervals.Intervals; +import org.apache.lucene.queries.intervals.IntervalsSource; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.index.query.IntervalsSourceProvider; + +import java.io.IOException; + +public class NotContainedByIntervalFilter extends SourceProviderIntervalFilter { + public static final String NAME = "not_contained_by"; + + public NotContainedByIntervalFilter(final IntervalsSourceProvider filter) { + super(NAME, filter); + } + + public NotContainedByIntervalFilter(final StreamInput in) throws IOException { + super(NAME, in); + } + + @Override + public IntervalsSource getIntervalsSource(final IntervalsSource input, final IntervalsSource filterSource) throws IOException { + return Intervals.notContainedBy(input, filterSource); + } + + public static NotContainedByIntervalFilter fromXContent(XContentParser parser) throws IOException { + return fromXContent(parser, NotContainedByIntervalFilter::new); + } +} diff --git a/server/src/main/java/org/elasticsearch/index/query/intervals/NotContainingIntervalFilter.java b/server/src/main/java/org/elasticsearch/index/query/intervals/NotContainingIntervalFilter.java new file mode 100644 index 0000000000000..a0f7d6e1e3889 --- /dev/null +++ b/server/src/main/java/org/elasticsearch/index/query/intervals/NotContainingIntervalFilter.java @@ -0,0 +1,49 @@ +/* + * 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.intervals; + +import org.apache.lucene.queries.intervals.Intervals; +import org.apache.lucene.queries.intervals.IntervalsSource; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.index.query.IntervalsSourceProvider; + +import java.io.IOException; + +public class NotContainingIntervalFilter extends SourceProviderIntervalFilter { + public static final String NAME = "not_containing"; + + public NotContainingIntervalFilter(final IntervalsSourceProvider filter) { + super(NAME, filter); + } + + public NotContainingIntervalFilter(final StreamInput in) throws IOException { + super(NAME, in); + } + + @Override + public IntervalsSource getIntervalsSource(final IntervalsSource input, final IntervalsSource filterSource) throws IOException { + return Intervals.notContaining(input, filterSource); + } + + public static NotContainingIntervalFilter fromXContent(XContentParser parser) throws IOException { + return fromXContent(parser, NotContainingIntervalFilter::new); + } +} diff --git a/server/src/main/java/org/elasticsearch/index/query/intervals/NotOverlappingIntervalFilter.java b/server/src/main/java/org/elasticsearch/index/query/intervals/NotOverlappingIntervalFilter.java new file mode 100644 index 0000000000000..9402f2fcefb57 --- /dev/null +++ b/server/src/main/java/org/elasticsearch/index/query/intervals/NotOverlappingIntervalFilter.java @@ -0,0 +1,49 @@ +/* + * 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.intervals; + +import org.apache.lucene.queries.intervals.Intervals; +import org.apache.lucene.queries.intervals.IntervalsSource; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.index.query.IntervalsSourceProvider; + +import java.io.IOException; + +public class NotOverlappingIntervalFilter extends SourceProviderIntervalFilter { + public static final String NAME = "not_overlapping"; + + public NotOverlappingIntervalFilter(final IntervalsSourceProvider filter) { + super(NAME, filter); + } + + public NotOverlappingIntervalFilter(final StreamInput in) throws IOException { + super(NAME, in); + } + + @Override + public IntervalsSource getIntervalsSource(final IntervalsSource input, final IntervalsSource filterSource) throws IOException { + return Intervals.nonOverlapping(input, filterSource); + } + + public static NotOverlappingIntervalFilter fromXContent(XContentParser parser) throws IOException { + return fromXContent(parser, NotOverlappingIntervalFilter::new); + } +} diff --git a/server/src/main/java/org/elasticsearch/index/query/intervals/OverlappingIntervalFilter.java b/server/src/main/java/org/elasticsearch/index/query/intervals/OverlappingIntervalFilter.java new file mode 100644 index 0000000000000..4ea6d0c8a61c2 --- /dev/null +++ b/server/src/main/java/org/elasticsearch/index/query/intervals/OverlappingIntervalFilter.java @@ -0,0 +1,49 @@ +/* + * 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.intervals; + +import org.apache.lucene.queries.intervals.Intervals; +import org.apache.lucene.queries.intervals.IntervalsSource; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.index.query.IntervalsSourceProvider; + +import java.io.IOException; + +public class OverlappingIntervalFilter extends SourceProviderIntervalFilter { + public static final String NAME = "overlapping"; + + public OverlappingIntervalFilter(final IntervalsSourceProvider filter) { + super(NAME, filter); + } + + public OverlappingIntervalFilter(final StreamInput in) throws IOException { + super(NAME, in); + } + + @Override + public IntervalsSource getIntervalsSource(final IntervalsSource input, final IntervalsSource filterSource) throws IOException { + return Intervals.overlapping(input, filterSource); + } + + public static OverlappingIntervalFilter fromXContent(XContentParser parser) throws IOException { + return fromXContent(parser, OverlappingIntervalFilter::new); + } +} diff --git a/server/src/main/java/org/elasticsearch/index/query/intervals/ScriptIntervalFilter.java b/server/src/main/java/org/elasticsearch/index/query/intervals/ScriptIntervalFilter.java new file mode 100644 index 0000000000000..feb788b474ddb --- /dev/null +++ b/server/src/main/java/org/elasticsearch/index/query/intervals/ScriptIntervalFilter.java @@ -0,0 +1,113 @@ +/* + * 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.intervals; + +import org.apache.lucene.queries.intervals.IntervalsSource; +import org.elasticsearch.Version; +import org.elasticsearch.common.ParsingException; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.index.mapper.MappedFieldType; +import org.elasticsearch.index.query.IntervalFilter; +import org.elasticsearch.index.query.IntervalFilterScript; +import org.elasticsearch.index.query.IntervalsSourceProvider; +import org.elasticsearch.index.query.QueryShardContext; +import org.elasticsearch.script.Script; + +import java.io.IOException; +import java.util.Objects; + +public class ScriptIntervalFilter extends IntervalFilter { + public static final String NAME = "script"; + + private final Script script; + + public ScriptIntervalFilter(Script script) { + this.script = script; + } + + public ScriptIntervalFilter(StreamInput in) throws IOException { + if (in.getVersion().onOrAfter(Version.CURRENT)) { + this.script = new Script(in); + } else { + // back-compat + // name already read due to being named writable + in.readOptionalNamedWriteable(IntervalsSourceProvider.class); // no-op, just here to read the null filter + in.readBoolean(); + this.script = new Script(in); + } + } + + @Override + public IntervalsSource filter(final IntervalsSource input, final QueryShardContext context, final MappedFieldType fieldType) + throws IOException { + IntervalFilterScript ifs = context.getScriptService().compile(script, IntervalFilterScript.CONTEXT).newInstance(); + return new IntervalsSourceProvider.ScriptFilterSource(input, script.getIdOrCode(), ifs); + } + + @Override + public XContentBuilder toXContent(final XContentBuilder builder, final Params params) throws IOException { + builder.startObject(); + builder.field(getWriteableName(), script, params); + builder.endObject(); + return builder; + } + + @Override + public void writeTo(final StreamOutput out) throws IOException { + if (out.getVersion().onOrAfter(Version.CURRENT)) { + script.writeTo(out); + } else { + // back-compat + // name already written due to being a named writable + out.writeOptionalNamedWriteable(null); // filter is was always null for script filter + out.writeBoolean(true); + script.writeTo(out); + } + } + + @Override + public String getWriteableName() { + return NAME; + } + + @Override + public int hashCode() { + return Objects.hash(script, NAME); + } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ScriptIntervalFilter other = (ScriptIntervalFilter) o; + return Objects.equals(script, other.script); + } + + public static ScriptIntervalFilter fromXContent(XContentParser parser) throws IOException { + Script script = Script.parse(parser); + if (parser.nextToken() != XContentParser.Token.END_OBJECT) { + throw new ParsingException(parser.getTokenLocation(), "Expected [END_OBJECT] but got [" + parser.currentToken() + "]"); + } + return new ScriptIntervalFilter(script); + } +} diff --git a/server/src/main/java/org/elasticsearch/index/query/intervals/SourceProviderIntervalFilter.java b/server/src/main/java/org/elasticsearch/index/query/intervals/SourceProviderIntervalFilter.java new file mode 100644 index 0000000000000..735d127d45fa3 --- /dev/null +++ b/server/src/main/java/org/elasticsearch/index/query/intervals/SourceProviderIntervalFilter.java @@ -0,0 +1,131 @@ +/* + * 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.intervals; + +import org.apache.lucene.queries.intervals.IntervalsSource; +import org.elasticsearch.Version; +import org.elasticsearch.common.ParsingException; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.index.mapper.MappedFieldType; +import org.elasticsearch.index.query.IntervalFilter; +import org.elasticsearch.index.query.IntervalsSourceProvider; +import org.elasticsearch.index.query.QueryShardContext; + +import java.io.IOException; +import java.util.Objects; +import java.util.function.Function; + +public abstract class SourceProviderIntervalFilter extends IntervalFilter { + private final IntervalsSourceProvider filter; + private final String name; + + public SourceProviderIntervalFilter(final String name, final IntervalsSourceProvider filter) { + this.name = name; + this.filter = filter; + } + + public SourceProviderIntervalFilter(final String name, final StreamInput in) throws IOException { + this.name = name; + if (in.getVersion().onOrAfter(Version.CURRENT)) { + this.filter = in.readNamedWriteable(IntervalsSourceProvider.class); + } else { + // back-compat + // name already read due to being a named writable now + this.filter = in.readOptionalNamedWriteable(IntervalsSourceProvider.class); // optional previously + in.readBoolean(); // no-op to read that we never had a script + } + } + + public abstract IntervalsSource getIntervalsSource(IntervalsSource input, IntervalsSource filterSource) throws IOException; + + @Override + public IntervalsSource filter(final IntervalsSource input, final QueryShardContext context, final MappedFieldType fieldType) + throws IOException { + IntervalsSource filterSource = filter.getSource(context, fieldType); + return getIntervalsSource(input, filterSource); + } + + @Override + public XContentBuilder toXContent(final XContentBuilder builder, final Params params) throws IOException { + builder.startObject(); + builder.field(name); + builder.startObject(); + filter.toXContent(builder, params); + builder.endObject(); + builder.endObject(); + return builder; + } + + @Override + public void writeTo(final StreamOutput out) throws IOException { + if (out.getVersion().onOrAfter(Version.CURRENT)) { + out.writeNamedWriteable(filter); + } else { + // back-compat + // name already written due to being a named writable now + out.writeOptionalNamedWriteable(filter); + out.writeBoolean(false); // always no scripts + } + } + + @Override + public String getWriteableName() { + return name; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + SourceProviderIntervalFilter fltr = (SourceProviderIntervalFilter) o; + return Objects.equals(filter, fltr.filter) && + Objects.equals(name, fltr.name); + } + + @Override + public int hashCode() { + return Objects.hash(filter, name); + } + + protected static T fromXContent(XContentParser parser, + Function filter) + throws IOException { + + if (parser.nextToken() != XContentParser.Token.START_OBJECT) { + throw new ParsingException(parser.getTokenLocation(), + "Expected [START_OBJECT] but got [" + parser.currentToken() + "]"); + } + if (parser.nextToken() != XContentParser.Token.FIELD_NAME) { + throw new ParsingException(parser.getTokenLocation(), "Expected [FIELD_NAME] but got [" + parser.currentToken() + "]"); + } + IntervalsSourceProvider intervals = IntervalsSourceProvider.fromXContent(parser); + if (parser.nextToken() != XContentParser.Token.END_OBJECT) { + throw new ParsingException(parser.getTokenLocation(), "Expected [END_OBJECT] but got [" + parser.currentToken() + "]"); + } + if (parser.nextToken() != XContentParser.Token.END_OBJECT) { + throw new ParsingException(parser.getTokenLocation(), "Expected [END_OBJECT] but got [" + parser.currentToken() + "]"); + } + + return filter.apply(intervals); + } +} diff --git a/server/src/main/java/org/elasticsearch/plugins/SearchPlugin.java b/server/src/main/java/org/elasticsearch/plugins/SearchPlugin.java index c1152cf015b12..720b43ad48e60 100644 --- a/server/src/main/java/org/elasticsearch/plugins/SearchPlugin.java +++ b/server/src/main/java/org/elasticsearch/plugins/SearchPlugin.java @@ -28,6 +28,8 @@ import org.elasticsearch.common.lucene.search.function.ScoreFunction; import org.elasticsearch.common.xcontent.XContent; import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.index.query.IntervalFilter; +import org.elasticsearch.index.query.IntervalsSourceProvider; import org.elasticsearch.index.query.QueryBuilder; import org.elasticsearch.index.query.QueryParser; import org.elasticsearch.index.query.functionscore.ScoreFunctionBuilder; @@ -105,6 +107,18 @@ default List> getSuggesters() { default List> getQueries() { return emptyList(); } + /** + * The new {@link IntervalsSourceProvider}s defined by this plugin. + */ + default List> getIntervalsSources() { + return emptyList(); + } + /** + * The new {@link IntervalFilter}s defined by this plugin. + */ + default List> getIntervalFilters() { + return emptyList(); + } /** * The new {@link Aggregation}s added by this plugin. */ @@ -199,6 +213,77 @@ public Writeable.Reader getSuggestionReader() { } } + /** + * Specification for a {@link IntervalsSourceProvider}. + */ + class IntervalsSourceSpec + extends SearchExtensionSpec> { + + /** + * Specification of custom {@link IntervalsSourceProvider}. + * + * @param name holds the names by which this interval source might be parsed. The {@link ParseField#getPreferredName()} is special + * as it is the name by under which the reader is registered. So it is the name that the interval source should use as + * its {@link NamedWriteable#getWriteableName()} too. + * @param reader the reader registered for this interval source's builder. Typically a reference to a constructor that takes a + * {@link StreamInput} + * @param parser the parser the reads the interval source from xcontent + */ + public IntervalsSourceSpec(final ParseField name, final Writeable.Reader reader, + final CheckedFunction parser) { + super(name, reader, parser); + } + + /** + * Specification of custom {@link IntervalsSourceProvider}. + * + * @param name the name by which this interval source might be parsed or deserialized. Make sure that the interval source returns + * this name for {@link NamedWriteable#getWriteableName()}. + * @param reader the reader registered for this interval source. Typically a reference to a constructor that takes a + * {@link StreamInput} + * @param parser the parser the reads the interval source from xcontent + */ + public IntervalsSourceSpec(final String name, final Writeable.Reader reader, + final CheckedFunction parser) { + super(name, reader, parser); + } + } + + /** + * Specification for a {@link IntervalFilter}. + */ + class IntervalFilterSpec extends SearchExtensionSpec> { + + /** + * Specification of custom {@link IntervalFilter}. + * + * @param name holds the names by which this interval filter might be parsed. The {@link ParseField#getPreferredName()} is special + * as it is the name by under which the reader is registered. So it is the name that the interval filter should use as + * its {@link NamedWriteable#getWriteableName()} too. + * @param reader the reader registered for this interval filter's builder. Typically a reference to a constructor that takes a + * {@link StreamInput} + * @param parser the parser the reads the interval filter from xcontent + */ + public IntervalFilterSpec(final ParseField name, final Writeable.Reader reader, + final CheckedFunction parser) { + super(name, reader, parser); + } + + /** + * Specification of custom {@link IntervalFilter}. + * + * @param name the name by which this interval filter might be parsed or deserialized. Make sure that the interval filter returns + * this name for {@link NamedWriteable#getWriteableName()}. + * @param reader the reader registered for this interval filter. Typically a reference to a constructor that takes a + * {@link StreamInput} + * @param parser the parser the reads the interval filter from xcontent + */ + public IntervalFilterSpec(final String name, final Writeable.Reader reader, + final CheckedFunction parser) { + super(name, reader, parser); + } + } + /** * Specification of custom {@link Query}. */ diff --git a/server/src/main/java/org/elasticsearch/search/SearchModule.java b/server/src/main/java/org/elasticsearch/search/SearchModule.java index cdfd28760869c..758f29875892c 100644 --- a/server/src/main/java/org/elasticsearch/search/SearchModule.java +++ b/server/src/main/java/org/elasticsearch/search/SearchModule.java @@ -44,6 +44,7 @@ import org.elasticsearch.index.query.GeoPolygonQueryBuilder; import org.elasticsearch.index.query.GeoShapeQueryBuilder; import org.elasticsearch.index.query.IdsQueryBuilder; +import org.elasticsearch.index.query.IntervalFilter; import org.elasticsearch.index.query.IntervalQueryBuilder; import org.elasticsearch.index.query.IntervalsSourceProvider; import org.elasticsearch.index.query.MatchAllQueryBuilder; @@ -85,9 +86,20 @@ import org.elasticsearch.index.query.functionscore.ScriptScoreFunctionBuilder; import org.elasticsearch.index.query.functionscore.ScriptScoreQueryBuilder; import org.elasticsearch.index.query.functionscore.WeightBuilder; +import org.elasticsearch.index.query.intervals.AfterIntervalFilter; +import org.elasticsearch.index.query.intervals.BeforeIntervalFilter; +import org.elasticsearch.index.query.intervals.ContainedByIntervalFilter; +import org.elasticsearch.index.query.intervals.ContainingIntervalFilter; +import org.elasticsearch.index.query.intervals.NotContainedByIntervalFilter; +import org.elasticsearch.index.query.intervals.NotContainingIntervalFilter; +import org.elasticsearch.index.query.intervals.NotOverlappingIntervalFilter; +import org.elasticsearch.index.query.intervals.OverlappingIntervalFilter; +import org.elasticsearch.index.query.intervals.ScriptIntervalFilter; import org.elasticsearch.plugins.SearchPlugin; import org.elasticsearch.plugins.SearchPlugin.AggregationSpec; import org.elasticsearch.plugins.SearchPlugin.FetchPhaseConstructionContext; +import org.elasticsearch.plugins.SearchPlugin.IntervalFilterSpec; +import org.elasticsearch.plugins.SearchPlugin.IntervalsSourceSpec; import org.elasticsearch.plugins.SearchPlugin.PipelineAggregationSpec; import org.elasticsearch.plugins.SearchPlugin.QuerySpec; import org.elasticsearch.plugins.SearchPlugin.RescorerSpec; @@ -315,7 +327,8 @@ public SearchModule(Settings settings, List plugins) { registerFetchSubPhases(plugins); registerSearchExts(plugins); registerShapes(); - registerIntervalsSourceProviders(); + registerIntervalsSourceProviders(plugins); + registerIntervalFilters(plugins); } public List getNamedWriteables() { @@ -793,25 +806,61 @@ private void registerQueryParsers(List plugins) { registerFromPlugin(plugins, SearchPlugin::getQueries, this::registerQuery); } - private void registerIntervalsSourceProviders() { - namedWriteables.add(new NamedWriteableRegistry.Entry(IntervalsSourceProvider.class, - IntervalsSourceProvider.Match.NAME, IntervalsSourceProvider.Match::new)); - namedWriteables.add(new NamedWriteableRegistry.Entry(IntervalsSourceProvider.class, - IntervalsSourceProvider.Combine.NAME, IntervalsSourceProvider.Combine::new)); - namedWriteables.add(new NamedWriteableRegistry.Entry(IntervalsSourceProvider.class, - IntervalsSourceProvider.Disjunction.NAME, IntervalsSourceProvider.Disjunction::new)); - namedWriteables.add(new NamedWriteableRegistry.Entry(IntervalsSourceProvider.class, - IntervalsSourceProvider.Prefix.NAME, IntervalsSourceProvider.Prefix::new)); - namedWriteables.add(new NamedWriteableRegistry.Entry(IntervalsSourceProvider.class, - IntervalsSourceProvider.Wildcard.NAME, IntervalsSourceProvider.Wildcard::new)); - } - private void registerQuery(QuerySpec spec) { namedWriteables.add(new NamedWriteableRegistry.Entry(QueryBuilder.class, spec.getName().getPreferredName(), spec.getReader())); namedXContents.add(new NamedXContentRegistry.Entry(QueryBuilder.class, spec.getName(), (p, c) -> spec.getParser().fromXContent(p))); } + private void registerIntervalsSourceProviders(List plugins) { + registerIntervalsSource(new IntervalsSourceSpec<>(IntervalsSourceProvider.Match.NAME, IntervalsSourceProvider.Match::new, + IntervalsSourceProvider.Match::fromXContent)); + registerIntervalsSource(new IntervalsSourceSpec<>(IntervalsSourceProvider.Combine.NAME, IntervalsSourceProvider.Combine::new, + IntervalsSourceProvider.Combine::fromXContent)); + registerIntervalsSource(new IntervalsSourceSpec<>(IntervalsSourceProvider.Disjunction.NAME, + IntervalsSourceProvider.Disjunction::new, IntervalsSourceProvider.Disjunction::fromXContent)); + registerIntervalsSource(new IntervalsSourceSpec<>(IntervalsSourceProvider.Prefix.NAME, IntervalsSourceProvider.Prefix::new, + IntervalsSourceProvider.Prefix::fromXContent)); + registerIntervalsSource(new IntervalsSourceSpec<>(IntervalsSourceProvider.Wildcard.NAME, IntervalsSourceProvider.Wildcard::new, + IntervalsSourceProvider.Wildcard::fromXContent)); + + registerFromPlugin(plugins, SearchPlugin::getIntervalsSources, this::registerIntervalsSource); + } + + private void registerIntervalsSource(IntervalsSourceSpec spec) { + namedWriteables.add(new NamedWriteableRegistry.Entry(IntervalsSourceProvider.class, spec.getName().getPreferredName(), + spec.getReader())); + namedXContents.add(new NamedXContentRegistry.Entry(IntervalsSourceProvider.class, spec.getName(), spec.getParser())); + } + + private void registerIntervalFilters(List plugins) { + registerIntervalFilter(new IntervalFilterSpec<>(AfterIntervalFilter.NAME, AfterIntervalFilter::new, + AfterIntervalFilter::fromXContent)); + registerIntervalFilter(new IntervalFilterSpec<>(BeforeIntervalFilter.NAME, BeforeIntervalFilter::new, + BeforeIntervalFilter::fromXContent)); + registerIntervalFilter(new IntervalFilterSpec<>(ContainedByIntervalFilter.NAME, ContainedByIntervalFilter::new, + ContainedByIntervalFilter::fromXContent)); + registerIntervalFilter(new IntervalFilterSpec<>(ContainingIntervalFilter.NAME, ContainingIntervalFilter::new, + ContainingIntervalFilter::fromXContent)); + registerIntervalFilter(new IntervalFilterSpec<>(NotContainedByIntervalFilter.NAME, NotContainedByIntervalFilter::new, + NotContainedByIntervalFilter::fromXContent)); + registerIntervalFilter(new IntervalFilterSpec<>(NotContainingIntervalFilter.NAME, NotContainingIntervalFilter::new, + NotContainingIntervalFilter::fromXContent)); + registerIntervalFilter(new IntervalFilterSpec<>(NotOverlappingIntervalFilter.NAME, NotOverlappingIntervalFilter::new, + NotOverlappingIntervalFilter::fromXContent)); + registerIntervalFilter(new IntervalFilterSpec<>(OverlappingIntervalFilter.NAME, OverlappingIntervalFilter::new, + OverlappingIntervalFilter::fromXContent)); + registerIntervalFilter(new IntervalFilterSpec<>(ScriptIntervalFilter.NAME, ScriptIntervalFilter::new, + ScriptIntervalFilter::fromXContent)); + + registerFromPlugin(plugins, SearchPlugin::getIntervalFilters, this::registerIntervalFilter); + } + + private void registerIntervalFilter(IntervalFilterSpec spec) { + namedWriteables.add(new NamedWriteableRegistry.Entry(IntervalFilter.class, spec.getName().getPreferredName(), spec.getReader())); + namedXContents.add(new NamedXContentRegistry.Entry(IntervalFilter.class, spec.getName(), spec.getParser())); + } + public FetchPhase getFetchPhase() { return new FetchPhase(fetchSubPhases); } diff --git a/server/src/test/java/org/elasticsearch/index/MergeSchedulerSettingsTests.java b/server/src/test/java/org/elasticsearch/index/MergeSchedulerSettingsTests.java index 490c89485d317..1f717aa34bc2f 100644 --- a/server/src/test/java/org/elasticsearch/index/MergeSchedulerSettingsTests.java +++ b/server/src/test/java/org/elasticsearch/index/MergeSchedulerSettingsTests.java @@ -144,6 +144,7 @@ private static IndexMetaData createMetaData(int maxThreadCount, int maxMergeCoun return newIndexMeta("index", builder.build()); } + @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/pull/49490") public void testMaxThreadAndMergeCount() { IllegalArgumentException exc = expectThrows(IllegalArgumentException.class, diff --git a/server/src/test/java/org/elasticsearch/index/query/IntervalQueryBuilderTests.java b/server/src/test/java/org/elasticsearch/index/query/IntervalQueryBuilderTests.java index da1da5ce54b69..8a78cbfaee73f 100644 --- a/server/src/test/java/org/elasticsearch/index/query/IntervalQueryBuilderTests.java +++ b/server/src/test/java/org/elasticsearch/index/query/IntervalQueryBuilderTests.java @@ -22,17 +22,36 @@ import org.apache.lucene.queries.XIntervals; import org.apache.lucene.queries.intervals.IntervalQuery; import org.apache.lucene.queries.intervals.Intervals; +import org.apache.lucene.queries.intervals.IntervalsSource; import org.apache.lucene.search.BoostQuery; import org.apache.lucene.search.MatchNoDocsQuery; import org.apache.lucene.search.Query; import org.apache.lucene.util.BytesRef; +import org.elasticsearch.common.ParseField; import org.elasticsearch.common.ParsingException; import org.elasticsearch.common.Strings; import org.elasticsearch.common.compress.CompressedXContent; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.util.BigArrays; +import org.elasticsearch.common.xcontent.ConstructingObjectParser; import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentParseException; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.index.mapper.MappedFieldType; import org.elasticsearch.index.mapper.MapperService; +import org.elasticsearch.index.query.intervals.AfterIntervalFilter; +import org.elasticsearch.index.query.intervals.BeforeIntervalFilter; +import org.elasticsearch.index.query.intervals.ContainedByIntervalFilter; +import org.elasticsearch.index.query.intervals.ContainingIntervalFilter; +import org.elasticsearch.index.query.intervals.NotContainedByIntervalFilter; +import org.elasticsearch.index.query.intervals.NotContainingIntervalFilter; +import org.elasticsearch.index.query.intervals.NotOverlappingIntervalFilter; +import org.elasticsearch.index.query.intervals.OverlappingIntervalFilter; +import org.elasticsearch.index.query.intervals.SourceProviderIntervalFilter; +import org.elasticsearch.plugins.Plugin; +import org.elasticsearch.plugins.SearchPlugin; import org.elasticsearch.script.Script; import org.elasticsearch.script.ScriptContext; import org.elasticsearch.script.ScriptService; @@ -40,24 +59,153 @@ import java.io.IOException; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.function.Function; +import static org.elasticsearch.common.xcontent.ConstructingObjectParser.constructorArg; import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder; +import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.instanceOf; public class IntervalQueryBuilderTests extends AbstractQueryTestCase { + /** + * To test pluggability of {@link IntervalFilter}'s. + */ + private static class UnorderedNoOverlapsIntervalFilter extends SourceProviderIntervalFilter { + public static final String NAME = "unordered_no_overlaps"; + + UnorderedNoOverlapsIntervalFilter(final IntervalsSourceProvider filter) { + super(NAME, filter); + } + + UnorderedNoOverlapsIntervalFilter(final StreamInput in) throws IOException { + super(NAME, in); + } + + @Override + public IntervalsSource getIntervalsSource(final IntervalsSource input, final IntervalsSource filterSource) throws IOException { + return Intervals.unorderedNoOverlaps(input, filterSource); + } + + public static UnorderedNoOverlapsIntervalFilter fromXContent(XContentParser parser) throws IOException { + return fromXContent(parser, UnorderedNoOverlapsIntervalFilter::new); + } + } + + /** + * To test pluggability of {@link IntervalsSourceProvider}'s. + */ + private static class TermIntervalsSource extends IntervalsSourceProvider { + public static final String NAME = "term"; + + private final String term; + + TermIntervalsSource(String term) { + this.term = term; + } + + TermIntervalsSource(StreamInput in) throws IOException { + this.term = in.readString(); + } + + @Override + public IntervalsSource getSource(final QueryShardContext context, final MappedFieldType fieldType) throws IOException { + return Intervals.term(term); + } + + @Override + public void extractFields(final Set fields) { + } + + @Override + public int hashCode() { + return Objects.hash(term); + } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + TermIntervalsSource other = (TermIntervalsSource) o; + return Objects.equals(term, other.term); + } + + @Override + public String getWriteableName() { + return NAME; + } + + @Override + public void writeTo(final StreamOutput out) throws IOException { + out.writeString(term); + } + + @Override + public XContentBuilder toXContent(final XContentBuilder builder, final Params params) throws IOException { + builder.field(NAME); + builder.startObject(); + builder.field("term", term); + return builder.endObject(); + } + + private static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>(NAME, args -> { + String term = (String) args[0]; + return new TermIntervalsSource(term); + }); + static { + PARSER.declareString(constructorArg(), new ParseField("term")); + } + + public static TermIntervalsSource fromXContent(XContentParser parser) { + return PARSER.apply(parser, null); + } + } + + /** + * Plugin to inject our custom interval source and filter. + */ + public static class IntervalPlugin extends Plugin implements SearchPlugin { + @Override + public List> getIntervalsSources() { + return Collections.singletonList(new IntervalsSourceSpec<>(TermIntervalsSource.NAME, TermIntervalsSource::new, + TermIntervalsSource::fromXContent)); + } + + @Override + public List> getIntervalFilters() { + return Collections.singletonList(new IntervalFilterSpec<>(UnorderedNoOverlapsIntervalFilter.NAME, + UnorderedNoOverlapsIntervalFilter::new, UnorderedNoOverlapsIntervalFilter::fromXContent)); + } + } + + @Override + protected Collection> getPlugins() { + return Collections.singletonList(IntervalPlugin.class); + } + @Override protected IntervalQueryBuilder doCreateTestQueryBuilder() { return new IntervalQueryBuilder(STRING_FIELD_NAME, createRandomSource(0)); } - private static final String[] filters = new String[]{ - "containing", "contained_by", "not_containing", "not_contained_by", - "overlapping", "not_overlapping", "before", "after" - }; + private static final List> filters = Arrays.asList( + ContainingIntervalFilter::new, + ContainedByIntervalFilter::new, + NotContainingIntervalFilter::new, + NotContainedByIntervalFilter::new, + OverlappingIntervalFilter::new, + NotOverlappingIntervalFilter::new, + BeforeIntervalFilter::new, + AfterIntervalFilter::new, + UnorderedNoOverlapsIntervalFilter::new + ); private static final String MASKED_FIELD = "masked_field"; private static final String NO_POSITIONS_FIELD = "no_positions_field"; @@ -105,16 +253,20 @@ private IntervalsSourceProvider createRandomSource(int depth) { } boolean ordered = randomBoolean(); int maxGaps = randomInt(5) - 1; - IntervalsSourceProvider.IntervalFilter filter = createRandomFilter(depth + 1); + IntervalFilter filter = createRandomFilter(depth + 1); return new IntervalsSourceProvider.Combine(subSources, ordered, maxGaps, filter); + case 4: + case 5: + return new TermIntervalsSource(randomRealisticUnicodeOfLengthBetween(4, 20)); default: return createRandomMatch(depth + 1); } } - private IntervalsSourceProvider.IntervalFilter createRandomFilter(int depth) { + private IntervalFilter createRandomFilter(int depth) { if (depth < 3 && randomInt(20) > 18) { - return new IntervalsSourceProvider.IntervalFilter(createRandomSource(depth + 1), randomFrom(filters)); + IntervalsSourceProvider provider = createRandomSource(depth + 1); + return randomFrom(filters).apply(provider); } return null; } @@ -138,6 +290,53 @@ protected void doAssertLuceneQuery(IntervalQueryBuilder queryBuilder, Query quer assertThat(query, instanceOf(IntervalQuery.class)); } + public void testUnknownIntervalsSource() throws IOException { + String json = "{ \"intervals\" : " + + "{ \"" + STRING_FIELD_NAME + "\" : { \"unknown_source\" : { \"query\" : \"blah\" } } } }"; + + Exception e = expectThrows(ParsingException.class, () -> parseQuery(json)); + assertThat(e.getMessage(), equalTo("Unknown interval type [unknown_source]")); + } + + public void testUnknownIntervalFilter() throws IOException { + String json = "{ \"intervals\" : " + + "{ \"" + STRING_FIELD_NAME + "\" : { " + + " \"match\" : { " + + " \"query\" : \"blah\"," + + " \"filter\" : {" + + " \"unknown_filter\" : {" + + " \"match\" : { \"query\" : \"blah\" } } } } } } }"; + Exception e = expectThrows(XContentParseException.class, () -> parseQuery(json)); + assertThat(e.getMessage(), containsString("failed to parse field [filter]")); + assertNotNull(e.getCause()); + assertThat(e.getCause().getMessage(), equalTo("Unknown interval filter [unknown_filter]")); + } + + public void testIntervalsSourceFromPlugin() throws IOException { + String json = "{ \"intervals\" : " + + "{ \"" + STRING_FIELD_NAME + "\" : { \"term\" : { \"term\" : \"NOT_ANALYZED\" } } } }"; + + IntervalQueryBuilder builder = (IntervalQueryBuilder) parseQuery(json); + Query expected = new IntervalQuery(STRING_FIELD_NAME, Intervals.term("NOT_ANALYZED")); + assertEquals(expected, builder.toQuery(createShardContext())); + } + + public void testIntervalFilterFromPlugin() throws IOException { + String json = "{ \"intervals\" : { \"" + STRING_FIELD_NAME + "\": {" + + " \"any_of\" : { " + + " \"intervals\" : [" + + " { \"match\" : { \"query\" : \"one\" } }," + + " { \"match\" : { \"query\" : \"two\" } } ]," + + " \"filter\" : {" + + " \"unordered_no_overlaps\" : { \"match\" : { \"query\" : \"three\" } } } } } } }"; + IntervalQueryBuilder builder = (IntervalQueryBuilder) parseQuery(json); + Query expected = new IntervalQuery(STRING_FIELD_NAME, + Intervals.unorderedNoOverlaps( + Intervals.or(Intervals.term("one"), Intervals.term("two")), + Intervals.term("three"))); + assertEquals(expected, builder.toQuery(createShardContext())); + } + public void testMatchInterval() throws IOException { String json = "{ \"intervals\" : " +