Skip to content

Commit 81dbcfb

Browse files
committed
Wildcard intervals (#43691)
This commit adds a wildcard intervals source, similar to the prefix. It also changes the term parameter in prefix to read prefix, to bring it in to line with the pattern parameter in wildcard. Closes #43198
1 parent 74dd6e4 commit 81dbcfb

File tree

5 files changed

+258
-15
lines changed

5 files changed

+258
-15
lines changed

docs/reference/query-dsl/intervals-query.asciidoc

+28
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,34 @@ If specified, then match intervals from this field rather than the top-level fie
101101
The `prefix` will be normalized using the search analyzer from this field, unless
102102
`analyzer` is specified separately.
103103

104+
[[intervals-wildcard]]
105+
==== `wildcard`
106+
107+
The `wildcard` rule finds terms that match a wildcard pattern. The pattern will
108+
expand to match at most 128 terms; if there are more matching terms in the index,
109+
then an error will be returned.
110+
111+
[horizontal]
112+
`pattern`::
113+
Find terms matching this pattern
114+
+
115+
--
116+
This parameter supports two wildcard operators:
117+
118+
* `?`, which matches any single character
119+
* `*`, which can match zero or more characters, including an empty one
120+
121+
WARNING: Avoid beginning patterns with `*` or `?`. This can increase
122+
the iterations needed to find matching terms and slow search performance.
123+
--
124+
`analyzer`::
125+
Which analyzer should be used to normalize the `pattern`. By default, the
126+
search analyzer of the top-level field will be used.
127+
`use_field`::
128+
If specified, then match intervals from this field rather than the top-level field.
129+
The `pattern` will be normalized using the search analyzer from this field, unless
130+
`analyzer` is specified separately.
131+
104132
[[intervals-all_of]]
105133
==== `all_of`
106134

rest-api-spec/src/main/resources/rest-api-spec/test/search/230_interval_query.yml

+20
Original file line numberDiff line numberDiff line change
@@ -407,3 +407,23 @@ setup:
407407
prefix: out
408408
- match: { hits.total.value: 3 }
409409

410+
---
411+
"Test wildcard":
412+
- skip:
413+
version: " - 8.0.0"
414+
reason: "TODO: change to 7.3 in backport"
415+
- do:
416+
search:
417+
index: test
418+
body:
419+
query:
420+
intervals:
421+
text:
422+
all_of:
423+
intervals:
424+
- match:
425+
query: cold
426+
- wildcard:
427+
pattern: out?ide
428+
- match: { hits.total.value: 3 }
429+

server/src/main/java/org/elasticsearch/index/query/IntervalsSourceProvider.java

+132-11
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,12 @@
1919

2020
package org.elasticsearch.index.query;
2121

22+
import org.apache.lucene.index.IndexOptions;
2223
import org.apache.lucene.search.intervals.FilteredIntervalsSource;
2324
import org.apache.lucene.search.intervals.IntervalIterator;
2425
import org.apache.lucene.search.intervals.Intervals;
2526
import org.apache.lucene.search.intervals.IntervalsSource;
27+
import org.apache.lucene.util.BytesRef;
2628
import org.elasticsearch.Version;
2729
import org.elasticsearch.common.ParseField;
2830
import org.elasticsearch.common.ParsingException;
@@ -80,6 +82,8 @@ public static IntervalsSourceProvider fromXContent(XContentParser parser) throws
8082
return Combine.fromXContent(parser);
8183
case "prefix":
8284
return Prefix.fromXContent(parser);
85+
case "wildcard":
86+
return Wildcard.fromXContent(parser);
8387
}
8488
throw new ParsingException(parser.getTokenLocation(),
8589
"Unknown interval type [" + parser.currentName() + "], expecting one of [match, any_of, all_of, prefix]");
@@ -446,18 +450,18 @@ public static class Prefix extends IntervalsSourceProvider {
446450

447451
public static final String NAME = "prefix";
448452

449-
private final String term;
453+
private final String prefix;
450454
private final String analyzer;
451455
private final String useField;
452456

453-
public Prefix(String term, String analyzer, String useField) {
454-
this.term = term;
457+
public Prefix(String prefix, String analyzer, String useField) {
458+
this.prefix = prefix;
455459
this.analyzer = analyzer;
456460
this.useField = useField;
457461
}
458462

459463
public Prefix(StreamInput in) throws IOException {
460-
this.term = in.readString();
464+
this.prefix = in.readString();
461465
this.analyzer = in.readOptionalString();
462466
this.useField = in.readOptionalString();
463467
}
@@ -472,10 +476,10 @@ public IntervalsSource getSource(QueryShardContext context, MappedFieldType fiel
472476
if (useField != null) {
473477
fieldType = context.fieldMapper(useField);
474478
assert fieldType != null;
475-
source = Intervals.fixField(useField, fieldType.intervals(term, 0, false, analyzer, true));
479+
source = Intervals.fixField(useField, fieldType.intervals(prefix, 0, false, analyzer, true));
476480
}
477481
else {
478-
source = fieldType.intervals(term, 0, false, analyzer, true);
482+
source = fieldType.intervals(prefix, 0, false, analyzer, true);
479483
}
480484
return source;
481485
}
@@ -492,14 +496,14 @@ public boolean equals(Object o) {
492496
if (this == o) return true;
493497
if (o == null || getClass() != o.getClass()) return false;
494498
Prefix prefix = (Prefix) o;
495-
return Objects.equals(term, prefix.term) &&
499+
return Objects.equals(this.prefix, prefix.prefix) &&
496500
Objects.equals(analyzer, prefix.analyzer) &&
497501
Objects.equals(useField, prefix.useField);
498502
}
499503

500504
@Override
501505
public int hashCode() {
502-
return Objects.hash(term, analyzer, useField);
506+
return Objects.hash(prefix, analyzer, useField);
503507
}
504508

505509
@Override
@@ -509,15 +513,15 @@ public String getWriteableName() {
509513

510514
@Override
511515
public void writeTo(StreamOutput out) throws IOException {
512-
out.writeString(term);
516+
out.writeString(prefix);
513517
out.writeOptionalString(analyzer);
514518
out.writeOptionalString(useField);
515519
}
516520

517521
@Override
518522
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
519523
builder.startObject(NAME);
520-
builder.field("term", term);
524+
builder.field("prefix", prefix);
521525
if (analyzer != null) {
522526
builder.field("analyzer", analyzer);
523527
}
@@ -535,7 +539,7 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws
535539
return new Prefix(term, analyzer, useField);
536540
});
537541
static {
538-
PARSER.declareString(constructorArg(), new ParseField("term"));
542+
PARSER.declareString(constructorArg(), new ParseField("prefix"));
539543
PARSER.declareString(optionalConstructorArg(), new ParseField("analyzer"));
540544
PARSER.declareString(optionalConstructorArg(), new ParseField("use_field"));
541545
}
@@ -545,6 +549,123 @@ public static Prefix fromXContent(XContentParser parser) throws IOException {
545549
}
546550
}
547551

552+
public static class Wildcard extends IntervalsSourceProvider {
553+
554+
public static final String NAME = "wildcard";
555+
556+
private final String pattern;
557+
private final String analyzer;
558+
private final String useField;
559+
560+
public Wildcard(String pattern, String analyzer, String useField) {
561+
this.pattern = pattern;
562+
this.analyzer = analyzer;
563+
this.useField = useField;
564+
}
565+
566+
public Wildcard(StreamInput in) throws IOException {
567+
this.pattern = in.readString();
568+
this.analyzer = in.readOptionalString();
569+
this.useField = in.readOptionalString();
570+
}
571+
572+
@Override
573+
public IntervalsSource getSource(QueryShardContext context, MappedFieldType fieldType) {
574+
NamedAnalyzer analyzer = fieldType.searchAnalyzer();
575+
if (this.analyzer != null) {
576+
analyzer = context.getMapperService().getIndexAnalyzers().get(this.analyzer);
577+
}
578+
IntervalsSource source;
579+
if (useField != null) {
580+
fieldType = context.fieldMapper(useField);
581+
assert fieldType != null;
582+
checkPositions(fieldType);
583+
if (this.analyzer == null) {
584+
analyzer = fieldType.searchAnalyzer();
585+
}
586+
BytesRef normalizedTerm = analyzer.normalize(useField, pattern);
587+
// TODO Intervals.wildcard() should take BytesRef
588+
source = Intervals.fixField(useField, Intervals.wildcard(normalizedTerm.utf8ToString()));
589+
}
590+
else {
591+
checkPositions(fieldType);
592+
BytesRef normalizedTerm = analyzer.normalize(fieldType.name(), pattern);
593+
source = Intervals.wildcard(normalizedTerm.utf8ToString());
594+
}
595+
return source;
596+
}
597+
598+
private void checkPositions(MappedFieldType type) {
599+
if (type.indexOptions().compareTo(IndexOptions.DOCS_AND_FREQS_AND_POSITIONS) < 0) {
600+
throw new IllegalArgumentException("Cannot create intervals over field [" + type.name() + "] with no positions indexed");
601+
}
602+
}
603+
604+
@Override
605+
public void extractFields(Set<String> fields) {
606+
if (useField != null) {
607+
fields.add(useField);
608+
}
609+
}
610+
611+
@Override
612+
public boolean equals(Object o) {
613+
if (this == o) return true;
614+
if (o == null || getClass() != o.getClass()) return false;
615+
Prefix prefix = (Prefix) o;
616+
return Objects.equals(pattern, prefix.prefix) &&
617+
Objects.equals(analyzer, prefix.analyzer) &&
618+
Objects.equals(useField, prefix.useField);
619+
}
620+
621+
@Override
622+
public int hashCode() {
623+
return Objects.hash(pattern, analyzer, useField);
624+
}
625+
626+
@Override
627+
public String getWriteableName() {
628+
return NAME;
629+
}
630+
631+
@Override
632+
public void writeTo(StreamOutput out) throws IOException {
633+
out.writeString(pattern);
634+
out.writeOptionalString(analyzer);
635+
out.writeOptionalString(useField);
636+
}
637+
638+
@Override
639+
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
640+
builder.startObject(NAME);
641+
builder.field("pattern", pattern);
642+
if (analyzer != null) {
643+
builder.field("analyzer", analyzer);
644+
}
645+
if (useField != null) {
646+
builder.field("use_field", useField);
647+
}
648+
builder.endObject();
649+
return builder;
650+
}
651+
652+
private static final ConstructingObjectParser<Wildcard, Void> PARSER = new ConstructingObjectParser<>(NAME, args -> {
653+
String term = (String) args[0];
654+
String analyzer = (String) args[1];
655+
String useField = (String) args[2];
656+
return new Wildcard(term, analyzer, useField);
657+
});
658+
static {
659+
PARSER.declareString(constructorArg(), new ParseField("pattern"));
660+
PARSER.declareString(optionalConstructorArg(), new ParseField("analyzer"));
661+
PARSER.declareString(optionalConstructorArg(), new ParseField("use_field"));
662+
}
663+
664+
public static Wildcard fromXContent(XContentParser parser) throws IOException {
665+
return PARSER.parse(parser, null);
666+
}
667+
}
668+
548669
static class ScriptFilterSource extends FilteredIntervalsSource {
549670

550671
final IntervalFilterScript script;

server/src/main/java/org/elasticsearch/search/SearchModule.java

+2
Original file line numberDiff line numberDiff line change
@@ -849,6 +849,8 @@ private void registerIntervalsSourceProviders() {
849849
IntervalsSourceProvider.Disjunction.NAME, IntervalsSourceProvider.Disjunction::new));
850850
namedWriteables.add(new NamedWriteableRegistry.Entry(IntervalsSourceProvider.class,
851851
IntervalsSourceProvider.Prefix.NAME, IntervalsSourceProvider.Prefix::new));
852+
namedWriteables.add(new NamedWriteableRegistry.Entry(IntervalsSourceProvider.class,
853+
IntervalsSourceProvider.Wildcard.NAME, IntervalsSourceProvider.Wildcard::new));
852854
}
853855

854856
private void registerQuery(QuerySpec<?> spec) {

0 commit comments

Comments
 (0)