Skip to content

Commit ef0180a

Browse files
Disallow boosts on inner span queries (#35967)
1 parent 6595ded commit ef0180a

18 files changed

+475
-47
lines changed

docs/reference/migration/migrate_6_6.asciidoc

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,11 @@ rest of Elasticsearc's APIs and Elasticsearch will raise a deprecation warning
3333
if those are used on any APIs. We plan to drop support for `_source_exclude` and
3434
`_source_include` in 7.0.
3535

36+
[float]
37+
==== Boosts on inner span queries are not allowed.
38+
39+
Attempts to set `boost` on inner span queries will now throw a parsing exception.
40+
3641
[float]
3742
==== Deprecate `.values` and `.getValues()` on doc values in scripts
3843

docs/reference/query-dsl/span-queries.asciidoc

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@ Span queries are low-level positional queries which provide expert control
55
over the order and proximity of the specified terms. These are typically used
66
to implement very specific queries on legal documents or patents.
77

8+
It is only allowed to set boost on an outer span query. Compound span queries,
9+
like span_near, only use the list of matching spans of inner span queries in
10+
order to find their own spans, which they then use to produce a score. Scores
11+
are never computed on inner span queries, which is the reason why boosts are not
12+
allowed: they only influence the way scores are computed, not spans.
13+
814
Span queries cannot be mixed with non-span queries (with the exception of the `span_multi` query).
915

1016
The queries in this group are:
@@ -67,4 +73,4 @@ include::span-containing-query.asciidoc[]
6773

6874
include::span-within-query.asciidoc[]
6975

70-
include::span-field-masking-query.asciidoc[]
76+
include::span-field-masking-query.asciidoc[]

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@
3232
import java.io.IOException;
3333
import java.util.Objects;
3434

35+
import static org.elasticsearch.index.query.SpanQueryBuilder.SpanQueryBuilderUtil.checkNoBoost;
36+
3537
/**
3638
* Builder for {@link org.apache.lucene.search.spans.SpanContainingQuery}.
3739
*/
@@ -117,12 +119,14 @@ public static SpanContainingQueryBuilder fromXContent(XContentParser parser) thr
117119
throw new ParsingException(parser.getTokenLocation(), "span_containing [big] must be of type span query");
118120
}
119121
big = (SpanQueryBuilder) query;
122+
checkNoBoost(NAME, currentFieldName, parser, big);
120123
} else if (LITTLE_FIELD.match(currentFieldName, parser.getDeprecationHandler())) {
121124
QueryBuilder query = parseInnerQueryBuilder(parser);
122125
if (query instanceof SpanQueryBuilder == false) {
123126
throw new ParsingException(parser.getTokenLocation(), "span_containing [little] must be of type span query");
124127
}
125128
little = (SpanQueryBuilder) query;
129+
checkNoBoost(NAME, currentFieldName, parser, little);
126130
} else {
127131
throw new ParsingException(parser.getTokenLocation(),
128132
"[span_containing] query does not support [" + currentFieldName + "]");

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

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@
3232
import java.io.IOException;
3333
import java.util.Objects;
3434

35+
import static org.elasticsearch.index.query.SpanQueryBuilder.SpanQueryBuilderUtil.checkNoBoost;
36+
3537
public class SpanFirstQueryBuilder extends AbstractQueryBuilder<SpanFirstQueryBuilder> implements SpanQueryBuilder {
3638
public static final String NAME = "span_first";
3739

@@ -115,9 +117,10 @@ public static SpanFirstQueryBuilder fromXContent(XContentParser parser) throws I
115117
if (MATCH_FIELD.match(currentFieldName, parser.getDeprecationHandler())) {
116118
QueryBuilder query = parseInnerQueryBuilder(parser);
117119
if (query instanceof SpanQueryBuilder == false) {
118-
throw new ParsingException(parser.getTokenLocation(), "spanFirst [match] must be of type span query");
120+
throw new ParsingException(parser.getTokenLocation(), "span_first [match] must be of type span query");
119121
}
120122
match = (SpanQueryBuilder) query;
123+
checkNoBoost(NAME, currentFieldName, parser, match);
121124
} else {
122125
throw new ParsingException(parser.getTokenLocation(), "[span_first] query does not support [" + currentFieldName + "]");
123126
}
@@ -134,10 +137,10 @@ public static SpanFirstQueryBuilder fromXContent(XContentParser parser) throws I
134137
}
135138
}
136139
if (match == null) {
137-
throw new ParsingException(parser.getTokenLocation(), "spanFirst must have [match] span query clause");
140+
throw new ParsingException(parser.getTokenLocation(), "span_first must have [match] span query clause");
138141
}
139142
if (end == null) {
140-
throw new ParsingException(parser.getTokenLocation(), "spanFirst must have [end] set for it");
143+
throw new ParsingException(parser.getTokenLocation(), "span_first must have [end] set for it");
141144
}
142145
SpanFirstQueryBuilder queryBuilder = new SpanFirstQueryBuilder(match, end);
143146
queryBuilder.boost(boost).queryName(queryName);

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

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@
3838
import java.util.List;
3939
import java.util.Objects;
4040

41+
import static org.elasticsearch.index.query.SpanQueryBuilder.SpanQueryBuilderUtil.checkNoBoost;
42+
4143
/**
4244
* Matches spans which are near one another. One can specify slop, the maximum number
4345
* of intervening unmatched positions, as well as whether matches are required to be in-order.
@@ -166,9 +168,11 @@ public static SpanNearQueryBuilder fromXContent(XContentParser parser) throws IO
166168
while ((token = parser.nextToken()) != XContentParser.Token.END_ARRAY) {
167169
QueryBuilder query = parseInnerQueryBuilder(parser);
168170
if (query instanceof SpanQueryBuilder == false) {
169-
throw new ParsingException(parser.getTokenLocation(), "spanNear [clauses] must be of type span query");
171+
throw new ParsingException(parser.getTokenLocation(), "span_near [clauses] must be of type span query");
170172
}
171-
clauses.add((SpanQueryBuilder) query);
173+
final SpanQueryBuilder clause = (SpanQueryBuilder) query;
174+
checkNoBoost(NAME, currentFieldName, parser, clause);
175+
clauses.add(clause);
172176
}
173177
} else {
174178
throw new ParsingException(parser.getTokenLocation(), "[span_near] query does not support [" + currentFieldName + "]");

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

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@
3232
import java.io.IOException;
3333
import java.util.Objects;
3434

35+
import static org.elasticsearch.index.query.SpanQueryBuilder.SpanQueryBuilderUtil.checkNoBoost;
36+
3537
public class SpanNotQueryBuilder extends AbstractQueryBuilder<SpanNotQueryBuilder> implements SpanQueryBuilder {
3638
public static final String NAME = "span_not";
3739

@@ -181,15 +183,17 @@ public static SpanNotQueryBuilder fromXContent(XContentParser parser) throws IOE
181183
if (INCLUDE_FIELD.match(currentFieldName, parser.getDeprecationHandler())) {
182184
QueryBuilder query = parseInnerQueryBuilder(parser);
183185
if (query instanceof SpanQueryBuilder == false) {
184-
throw new ParsingException(parser.getTokenLocation(), "spanNot [include] must be of type span query");
186+
throw new ParsingException(parser.getTokenLocation(), "span_not [include] must be of type span query");
185187
}
186188
include = (SpanQueryBuilder) query;
189+
checkNoBoost(NAME, currentFieldName, parser, include);
187190
} else if (EXCLUDE_FIELD.match(currentFieldName, parser.getDeprecationHandler())) {
188191
QueryBuilder query = parseInnerQueryBuilder(parser);
189192
if (query instanceof SpanQueryBuilder == false) {
190-
throw new ParsingException(parser.getTokenLocation(), "spanNot [exclude] must be of type span query");
193+
throw new ParsingException(parser.getTokenLocation(), "span_not [exclude] must be of type span query");
191194
}
192195
exclude = (SpanQueryBuilder) query;
196+
checkNoBoost(NAME, currentFieldName, parser, exclude);
193197
} else {
194198
throw new ParsingException(parser.getTokenLocation(), "[span_not] query does not support [" + currentFieldName + "]");
195199
}
@@ -210,13 +214,13 @@ public static SpanNotQueryBuilder fromXContent(XContentParser parser) throws IOE
210214
}
211215
}
212216
if (include == null) {
213-
throw new ParsingException(parser.getTokenLocation(), "spanNot must have [include] span query clause");
217+
throw new ParsingException(parser.getTokenLocation(), "span_not must have [include] span query clause");
214218
}
215219
if (exclude == null) {
216-
throw new ParsingException(parser.getTokenLocation(), "spanNot must have [exclude] span query clause");
220+
throw new ParsingException(parser.getTokenLocation(), "span_not must have [exclude] span query clause");
217221
}
218222
if (dist != null && (pre != null || post != null)) {
219-
throw new ParsingException(parser.getTokenLocation(), "spanNot can either use [dist] or [pre] & [post] (or none)");
223+
throw new ParsingException(parser.getTokenLocation(), "span_not can either use [dist] or [pre] & [post] (or none)");
220224
}
221225

222226
SpanNotQueryBuilder spanNotQuery = new SpanNotQueryBuilder(include, exclude);

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

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@
3535
import java.util.List;
3636
import java.util.Objects;
3737

38+
import static org.elasticsearch.index.query.SpanQueryBuilder.SpanQueryBuilderUtil.checkNoBoost;
39+
3840
/**
3941
* Span query that matches the union of its clauses. Maps to {@link SpanOrQuery}.
4042
*/
@@ -113,9 +115,11 @@ public static SpanOrQueryBuilder fromXContent(XContentParser parser) throws IOEx
113115
while ((token = parser.nextToken()) != XContentParser.Token.END_ARRAY) {
114116
QueryBuilder query = parseInnerQueryBuilder(parser);
115117
if (query instanceof SpanQueryBuilder == false) {
116-
throw new ParsingException(parser.getTokenLocation(), "spanOr [clauses] must be of type span query");
118+
throw new ParsingException(parser.getTokenLocation(), "span_or [clauses] must be of type span query");
117119
}
118-
clauses.add((SpanQueryBuilder) query);
120+
final SpanQueryBuilder clause = (SpanQueryBuilder) query;
121+
checkNoBoost(NAME, currentFieldName, parser, clause);
122+
clauses.add(clause);
119123
}
120124
} else {
121125
throw new ParsingException(parser.getTokenLocation(), "[span_or] query does not support [" + currentFieldName + "]");
@@ -132,7 +136,7 @@ public static SpanOrQueryBuilder fromXContent(XContentParser parser) throws IOEx
132136
}
133137

134138
if (clauses.isEmpty()) {
135-
throw new ParsingException(parser.getTokenLocation(), "spanOr must include [clauses]");
139+
throw new ParsingException(parser.getTokenLocation(), "span_or must include [clauses]");
136140
}
137141

138142
SpanOrQueryBuilder queryBuilder = new SpanOrQueryBuilder(clauses.get(0));

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

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,37 @@
1919

2020
package org.elasticsearch.index.query;
2121

22+
import org.elasticsearch.common.ParsingException;
23+
import org.elasticsearch.common.xcontent.XContentParser;
24+
2225
/**
23-
* Marker interface for a specific type of {@link QueryBuilder} that allows to build span queries
26+
* Marker interface for a specific type of {@link QueryBuilder} that allows to build span queries.
2427
*/
2528
public interface SpanQueryBuilder extends QueryBuilder {
2629

30+
class SpanQueryBuilderUtil {
31+
private SpanQueryBuilderUtil() {
32+
// utility class
33+
}
34+
35+
/**
36+
* Checks boost value of a nested span clause is equal to {@link AbstractQueryBuilder#DEFAULT_BOOST}.
37+
*
38+
* @param queryName a query name
39+
* @param fieldName a field name
40+
* @param parser a parser
41+
* @param clause a span query builder
42+
* @throws ParsingException if query boost value isn't equal to {@link AbstractQueryBuilder#DEFAULT_BOOST}
43+
*/
44+
static void checkNoBoost(String queryName, String fieldName, XContentParser parser, SpanQueryBuilder clause) {
45+
try {
46+
if (clause.boost() != AbstractQueryBuilder.DEFAULT_BOOST) {
47+
throw new ParsingException(parser.getTokenLocation(), queryName + " [" + fieldName + "] " +
48+
"as a nested span clause can't have non-default boost value [" + clause.boost() + "]");
49+
}
50+
} catch (UnsupportedOperationException ignored) {
51+
// if boost is unsupported it can't have been set
52+
}
53+
}
54+
}
2755
}

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@
3232
import java.io.IOException;
3333
import java.util.Objects;
3434

35+
import static org.elasticsearch.index.query.SpanQueryBuilder.SpanQueryBuilderUtil.checkNoBoost;
36+
3537
/**
3638
* Builder for {@link org.apache.lucene.search.spans.SpanWithinQuery}.
3739
*/
@@ -122,12 +124,14 @@ public static SpanWithinQueryBuilder fromXContent(XContentParser parser) throws
122124
throw new ParsingException(parser.getTokenLocation(), "span_within [big] must be of type span query");
123125
}
124126
big = (SpanQueryBuilder) query;
127+
checkNoBoost(NAME, currentFieldName, parser, big);
125128
} else if (LITTLE_FIELD.match(currentFieldName, parser.getDeprecationHandler())) {
126129
QueryBuilder query = parseInnerQueryBuilder(parser);
127130
if (query instanceof SpanQueryBuilder == false) {
128131
throw new ParsingException(parser.getTokenLocation(), "span_within [little] must be of type span query");
129132
}
130133
little = (SpanQueryBuilder) query;
134+
checkNoBoost(NAME, currentFieldName, parser, little);
131135
} else {
132136
throw new ParsingException(parser.getTokenLocation(),
133137
"[span_within] query does not support [" + currentFieldName + "]");

server/src/test/java/org/elasticsearch/index/query/SpanContainingQueryBuilderTests.java

Lines changed: 90 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,13 @@
2121

2222
import org.apache.lucene.search.Query;
2323
import org.apache.lucene.search.spans.SpanContainingQuery;
24+
import org.elasticsearch.common.ParsingException;
2425
import org.elasticsearch.search.internal.SearchContext;
2526
import org.elasticsearch.test.AbstractQueryTestCase;
2627

2728
import java.io.IOException;
2829

30+
import static org.hamcrest.CoreMatchers.equalTo;
2931
import static org.hamcrest.CoreMatchers.instanceOf;
3032

3133
public class SpanContainingQueryBuilderTests extends AbstractQueryTestCase<SpanContainingQueryBuilder> {
@@ -80,7 +82,7 @@ public void testFromJson() throws IOException {
8082
" }\n" +
8183
" }\n" +
8284
" },\n" +
83-
" \"boost\" : 1.0\n" +
85+
" \"boost\" : 2.0\n" +
8486
" }\n" +
8587
"}";
8688

@@ -89,5 +91,92 @@ public void testFromJson() throws IOException {
8991

9092
assertEquals(json, 2, ((SpanNearQueryBuilder) parsed.bigQuery()).clauses().size());
9193
assertEquals(json, "foo", ((SpanTermQueryBuilder) parsed.littleQuery()).value());
94+
assertEquals(json, 2.0, parsed.boost(), 0.0);
95+
}
96+
97+
public void testFromJsoWithNonDefaultBoostInBigQuery() {
98+
String json =
99+
"{\n" +
100+
" \"span_containing\" : {\n" +
101+
" \"big\" : {\n" +
102+
" \"span_near\" : {\n" +
103+
" \"clauses\" : [ {\n" +
104+
" \"span_term\" : {\n" +
105+
" \"field1\" : {\n" +
106+
" \"value\" : \"bar\",\n" +
107+
" \"boost\" : 1.0\n" +
108+
" }\n" +
109+
" }\n" +
110+
" }, {\n" +
111+
" \"span_term\" : {\n" +
112+
" \"field1\" : {\n" +
113+
" \"value\" : \"baz\",\n" +
114+
" \"boost\" : 1.0\n" +
115+
" }\n" +
116+
" }\n" +
117+
" } ],\n" +
118+
" \"slop\" : 5,\n" +
119+
" \"in_order\" : true,\n" +
120+
" \"boost\" : 2.0\n" +
121+
" }\n" +
122+
" },\n" +
123+
" \"little\" : {\n" +
124+
" \"span_term\" : {\n" +
125+
" \"field1\" : {\n" +
126+
" \"value\" : \"foo\",\n" +
127+
" \"boost\" : 1.0\n" +
128+
" }\n" +
129+
" }\n" +
130+
" },\n" +
131+
" \"boost\" : 1.0\n" +
132+
" }\n" +
133+
"}";
134+
135+
Exception exception = expectThrows(ParsingException.class, () -> parseQuery(json));
136+
assertThat(exception.getMessage(),
137+
equalTo("span_containing [big] as a nested span clause can't have non-default boost value [2.0]"));
138+
}
139+
140+
public void testFromJsonWithNonDefaultBoostInLittleQuery() {
141+
String json =
142+
"{\n" +
143+
" \"span_containing\" : {\n" +
144+
" \"little\" : {\n" +
145+
" \"span_near\" : {\n" +
146+
" \"clauses\" : [ {\n" +
147+
" \"span_term\" : {\n" +
148+
" \"field1\" : {\n" +
149+
" \"value\" : \"bar\",\n" +
150+
" \"boost\" : 1.0\n" +
151+
" }\n" +
152+
" }\n" +
153+
" }, {\n" +
154+
" \"span_term\" : {\n" +
155+
" \"field1\" : {\n" +
156+
" \"value\" : \"baz\",\n" +
157+
" \"boost\" : 1.0\n" +
158+
" }\n" +
159+
" }\n" +
160+
" } ],\n" +
161+
" \"slop\" : 5,\n" +
162+
" \"in_order\" : true,\n" +
163+
" \"boost\" : 2.0\n" +
164+
" }\n" +
165+
" },\n" +
166+
" \"big\" : {\n" +
167+
" \"span_term\" : {\n" +
168+
" \"field1\" : {\n" +
169+
" \"value\" : \"foo\",\n" +
170+
" \"boost\" : 1.0\n" +
171+
" }\n" +
172+
" }\n" +
173+
" },\n" +
174+
" \"boost\" : 1.0\n" +
175+
" }\n" +
176+
"}";
177+
178+
Exception exception = expectThrows(ParsingException.class, () -> parseQuery(json));
179+
assertThat(exception.getMessage(),
180+
equalTo("span_containing [little] as a nested span clause can't have non-default boost value [2.0]"));
92181
}
93182
}

0 commit comments

Comments
 (0)