Skip to content

Commit e680d8d

Browse files
author
Christoph Büscher
committed
Forbid expensive query parts in ranking evaluation (#30151)
Currently the ranking evaluation API accepts the full query syntax for the queries specified in the evaluation set and executes them via multi search. This potentially runs costly aggregations and suggestions too. This change adds checks that forbid using aggregations, suggesters, highlighters and the explain and profile options in the queries that are run as part of the ranking evaluation since they are irrelevent in the context of this API.
1 parent ccad537 commit e680d8d

File tree

5 files changed

+171
-51
lines changed

5 files changed

+171
-51
lines changed

modules/rank-eval/src/main/java/org/elasticsearch/index/rankeval/RankEvalSpec.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ public class RankEvalSpec implements Writeable, ToXContentObject {
5757
/** Default max number of requests. */
5858
private static final int MAX_CONCURRENT_SEARCHES = 10;
5959
/** optional: Templates to base test requests on */
60-
private Map<String, Script> templates = new HashMap<>();
60+
private final Map<String, Script> templates = new HashMap<>();
6161

6262
public RankEvalSpec(List<RatedRequest> ratedRequests, EvaluationMetric metric, Collection<ScriptWithId> templates) {
6363
this.metric = Objects.requireNonNull(metric, "Cannot evaluate ranking if no evaluation metric is provided.");
@@ -68,8 +68,8 @@ public RankEvalSpec(List<RatedRequest> ratedRequests, EvaluationMetric metric, C
6868
this.ratedRequests = ratedRequests;
6969
if (templates == null || templates.isEmpty()) {
7070
for (RatedRequest request : ratedRequests) {
71-
if (request.getTestRequest() == null) {
72-
throw new IllegalStateException("Cannot evaluate ranking if neither template nor test request is "
71+
if (request.getEvaluationRequest() == null) {
72+
throw new IllegalStateException("Cannot evaluate ranking if neither template nor evaluation request is "
7373
+ "provided. Seen for request id: " + request.getId());
7474
}
7575
}

modules/rank-eval/src/main/java/org/elasticsearch/index/rankeval/RatedRequest.java

Lines changed: 69 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -75,9 +75,12 @@ public class RatedRequest implements Writeable, ToXContentObject {
7575
private final String id;
7676
private final List<String> summaryFields;
7777
private final List<RatedDocument> ratedDocs;
78-
// Search request to execute for this rated request. This can be null if template and corresponding parameters are supplied.
78+
/**
79+
* Search request to execute for this rated request. This can be null in
80+
* case the query is supplied as a template with corresponding parameters
81+
*/
7982
@Nullable
80-
private SearchSourceBuilder testRequest;
83+
private final SearchSourceBuilder evaluationRequest;
8184
/**
8285
* Map of parameters to use for filling a query template, can be used
8386
* instead of providing testRequest.
@@ -86,27 +89,49 @@ public class RatedRequest implements Writeable, ToXContentObject {
8689
@Nullable
8790
private String templateId;
8891

89-
private RatedRequest(String id, List<RatedDocument> ratedDocs, SearchSourceBuilder testRequest,
92+
/**
93+
* Create a rated request with template ids and parameters.
94+
*
95+
* @param id a unique name for this rated request
96+
* @param ratedDocs a list of document ratings
97+
* @param params template parameters
98+
* @param templateId a templare id
99+
*/
100+
public RatedRequest(String id, List<RatedDocument> ratedDocs, Map<String, Object> params,
101+
String templateId) {
102+
this(id, ratedDocs, null, params, templateId);
103+
}
104+
105+
/**
106+
* Create a rated request using a {@link SearchSourceBuilder} to define the
107+
* evaluated query.
108+
*
109+
* @param id a unique name for this rated request
110+
* @param ratedDocs a list of document ratings
111+
* @param evaluatedQuery the query that is evaluated
112+
*/
113+
public RatedRequest(String id, List<RatedDocument> ratedDocs, SearchSourceBuilder evaluatedQuery) {
114+
this(id, ratedDocs, evaluatedQuery, new HashMap<>(), null);
115+
}
116+
117+
private RatedRequest(String id, List<RatedDocument> ratedDocs, SearchSourceBuilder evaluatedQuery,
90118
Map<String, Object> params, String templateId) {
91-
if (params != null && (params.size() > 0 && testRequest != null)) {
119+
if (params != null && (params.size() > 0 && evaluatedQuery != null)) {
92120
throw new IllegalArgumentException(
93-
"Ambiguous rated request: Set both, verbatim test request and test request "
94-
+ "template parameters.");
121+
"Ambiguous rated request: Set both, verbatim test request and test request " + "template parameters.");
95122
}
96-
if (templateId != null && testRequest != null) {
123+
if (templateId != null && evaluatedQuery != null) {
97124
throw new IllegalArgumentException(
98-
"Ambiguous rated request: Set both, verbatim test request and test request "
99-
+ "template parameters.");
125+
"Ambiguous rated request: Set both, verbatim test request and test request " + "template parameters.");
100126
}
101-
if ((params == null || params.size() < 1) && testRequest == null) {
102-
throw new IllegalArgumentException(
103-
"Need to set at least test request or test request template parameters.");
127+
if ((params == null || params.size() < 1) && evaluatedQuery == null) {
128+
throw new IllegalArgumentException("Need to set at least test request or test request template parameters.");
104129
}
105130
if ((params != null && params.size() > 0) && templateId == null) {
106-
throw new IllegalArgumentException(
107-
"If template parameters are supplied need to set id of template to apply "
108-
+ "them to too.");
131+
throw new IllegalArgumentException("If template parameters are supplied need to set id of template to apply " + "them to too.");
109132
}
133+
validateEvaluatedQuery(evaluatedQuery);
134+
110135
// check that not two documents with same _index/id are specified
111136
Set<DocumentKey> docKeys = new HashSet<>();
112137
for (RatedDocument doc : ratedDocs) {
@@ -118,7 +143,7 @@ private RatedRequest(String id, List<RatedDocument> ratedDocs, SearchSourceBuild
118143
}
119144

120145
this.id = id;
121-
this.testRequest = testRequest;
146+
this.evaluationRequest = evaluatedQuery;
122147
this.ratedDocs = new ArrayList<>(ratedDocs);
123148
if (params != null) {
124149
this.params = new HashMap<>(params);
@@ -129,18 +154,30 @@ private RatedRequest(String id, List<RatedDocument> ratedDocs, SearchSourceBuild
129154
this.summaryFields = new ArrayList<>();
130155
}
131156

132-
public RatedRequest(String id, List<RatedDocument> ratedDocs, Map<String, Object> params,
133-
String templateId) {
134-
this(id, ratedDocs, null, params, templateId);
135-
}
136-
137-
public RatedRequest(String id, List<RatedDocument> ratedDocs, SearchSourceBuilder testRequest) {
138-
this(id, ratedDocs, testRequest, new HashMap<>(), null);
157+
static void validateEvaluatedQuery(SearchSourceBuilder evaluationRequest) {
158+
// ensure that testRequest, if set, does not contain aggregation, suggest or highlighting section
159+
if (evaluationRequest != null) {
160+
if (evaluationRequest.suggest() != null) {
161+
throw new IllegalArgumentException("Query in rated requests should not contain a suggest section.");
162+
}
163+
if (evaluationRequest.aggregations() != null) {
164+
throw new IllegalArgumentException("Query in rated requests should not contain aggregations.");
165+
}
166+
if (evaluationRequest.highlighter() != null) {
167+
throw new IllegalArgumentException("Query in rated requests should not contain a highlighter section.");
168+
}
169+
if (evaluationRequest.explain() != null && evaluationRequest.explain()) {
170+
throw new IllegalArgumentException("Query in rated requests should not use explain.");
171+
}
172+
if (evaluationRequest.profile()) {
173+
throw new IllegalArgumentException("Query in rated requests should not use profile.");
174+
}
175+
}
139176
}
140177

141-
public RatedRequest(StreamInput in) throws IOException {
178+
RatedRequest(StreamInput in) throws IOException {
142179
this.id = in.readString();
143-
testRequest = in.readOptionalWriteable(SearchSourceBuilder::new);
180+
evaluationRequest = in.readOptionalWriteable(SearchSourceBuilder::new);
144181

145182
int intentSize = in.readInt();
146183
ratedDocs = new ArrayList<>(intentSize);
@@ -159,7 +196,7 @@ public RatedRequest(StreamInput in) throws IOException {
159196
@Override
160197
public void writeTo(StreamOutput out) throws IOException {
161198
out.writeString(id);
162-
out.writeOptionalWriteable(testRequest);
199+
out.writeOptionalWriteable(evaluationRequest);
163200

164201
out.writeInt(ratedDocs.size());
165202
for (RatedDocument ratedDoc : ratedDocs) {
@@ -173,8 +210,8 @@ public void writeTo(StreamOutput out) throws IOException {
173210
out.writeOptionalString(this.templateId);
174211
}
175212

176-
public SearchSourceBuilder getTestRequest() {
177-
return testRequest;
213+
public SearchSourceBuilder getEvaluationRequest() {
214+
return evaluationRequest;
178215
}
179216

180217
/** return the user supplied request id */
@@ -240,8 +277,8 @@ public static RatedRequest fromXContent(XContentParser parser) {
240277
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
241278
builder.startObject();
242279
builder.field(ID_FIELD.getPreferredName(), this.id);
243-
if (testRequest != null) {
244-
builder.field(REQUEST_FIELD.getPreferredName(), this.testRequest);
280+
if (evaluationRequest != null) {
281+
builder.field(REQUEST_FIELD.getPreferredName(), this.evaluationRequest);
245282
}
246283
builder.startArray(RATINGS_FIELD.getPreferredName());
247284
for (RatedDocument doc : this.ratedDocs) {
@@ -285,7 +322,7 @@ public final boolean equals(Object obj) {
285322

286323
RatedRequest other = (RatedRequest) obj;
287324

288-
return Objects.equals(id, other.id) && Objects.equals(testRequest, other.testRequest)
325+
return Objects.equals(id, other.id) && Objects.equals(evaluationRequest, other.evaluationRequest)
289326
&& Objects.equals(summaryFields, other.summaryFields)
290327
&& Objects.equals(ratedDocs, other.ratedDocs)
291328
&& Objects.equals(params, other.params)
@@ -294,7 +331,7 @@ public final boolean equals(Object obj) {
294331

295332
@Override
296333
public final int hashCode() {
297-
return Objects.hash(id, testRequest, summaryFields, ratedDocs, params,
334+
return Objects.hash(id, evaluationRequest, summaryFields, ratedDocs, params,
298335
templateId);
299336
}
300337
}

modules/rank-eval/src/main/java/org/elasticsearch/index/rankeval/TransportRankEvalAction.java

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
import java.util.concurrent.ConcurrentHashMap;
5353

5454
import static org.elasticsearch.common.xcontent.XContentHelper.createParser;
55+
import static org.elasticsearch.index.rankeval.RatedRequest.validateEvaluatedQuery;
5556

5657
/**
5758
* Instances of this class execute a collection of search intents (read: user
@@ -99,15 +100,17 @@ protected void doExecute(RankEvalRequest request, ActionListener<RankEvalRespons
99100
msearchRequest.maxConcurrentSearchRequests(evaluationSpecification.getMaxConcurrentSearches());
100101
List<RatedRequest> ratedRequestsInSearch = new ArrayList<>();
101102
for (RatedRequest ratedRequest : ratedRequests) {
102-
SearchSourceBuilder ratedSearchSource = ratedRequest.getTestRequest();
103-
if (ratedSearchSource == null) {
103+
SearchSourceBuilder evaluationRequest = ratedRequest.getEvaluationRequest();
104+
if (evaluationRequest == null) {
104105
Map<String, Object> params = ratedRequest.getParams();
105106
String templateId = ratedRequest.getTemplateId();
106107
TemplateScript.Factory templateScript = scriptsWithoutParams.get(templateId);
107108
String resolvedRequest = templateScript.newInstance(params).execute();
108109
try (XContentParser subParser = createParser(namedXContentRegistry,
109110
LoggingDeprecationHandler.INSTANCE, new BytesArray(resolvedRequest), XContentType.JSON)) {
110-
ratedSearchSource = SearchSourceBuilder.fromXContent(subParser, false);
111+
evaluationRequest = SearchSourceBuilder.fromXContent(subParser, false);
112+
// check for parts that should not be part of a ranking evaluation request
113+
validateEvaluatedQuery(evaluationRequest);
111114
} catch (IOException e) {
112115
// if we fail parsing, put the exception into the errors map and continue
113116
errors.put(ratedRequest.getId(), e);
@@ -116,17 +119,17 @@ LoggingDeprecationHandler.INSTANCE, new BytesArray(resolvedRequest), XContentTyp
116119
}
117120

118121
if (metric.forcedSearchSize().isPresent()) {
119-
ratedSearchSource.size(metric.forcedSearchSize().get());
122+
evaluationRequest.size(metric.forcedSearchSize().get());
120123
}
121124

122125
ratedRequestsInSearch.add(ratedRequest);
123126
List<String> summaryFields = ratedRequest.getSummaryFields();
124127
if (summaryFields.isEmpty()) {
125-
ratedSearchSource.fetchSource(false);
128+
evaluationRequest.fetchSource(false);
126129
} else {
127-
ratedSearchSource.fetchSource(summaryFields.toArray(new String[summaryFields.size()]), new String[0]);
130+
evaluationRequest.fetchSource(summaryFields.toArray(new String[summaryFields.size()]), new String[0]);
128131
}
129-
SearchRequest searchRequest = new SearchRequest(request.indices(), ratedSearchSource);
132+
SearchRequest searchRequest = new SearchRequest(request.indices(), evaluationRequest);
130133
searchRequest.indicesOptions(request.indicesOptions());
131134
msearchRequest.add(searchRequest);
132135
}

modules/rank-eval/src/test/java/org/elasticsearch/index/rankeval/RatedRequestsTests.java

Lines changed: 51 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,11 @@
3333
import org.elasticsearch.index.query.MatchAllQueryBuilder;
3434
import org.elasticsearch.index.query.QueryBuilder;
3535
import org.elasticsearch.search.SearchModule;
36+
import org.elasticsearch.search.aggregations.AggregationBuilders;
3637
import org.elasticsearch.search.builder.SearchSourceBuilder;
38+
import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder;
39+
import org.elasticsearch.search.suggest.SuggestBuilder;
40+
import org.elasticsearch.search.suggest.SuggestBuilders;
3741
import org.elasticsearch.test.ESTestCase;
3842
import org.junit.AfterClass;
3943
import org.junit.BeforeClass;
@@ -165,7 +169,7 @@ public void testEqualsAndHash() throws IOException {
165169

166170
private static RatedRequest mutateTestItem(RatedRequest original) {
167171
String id = original.getId();
168-
SearchSourceBuilder testRequest = original.getTestRequest();
172+
SearchSourceBuilder evaluationRequest = original.getEvaluationRequest();
169173
List<RatedDocument> ratedDocs = original.getRatedDocs();
170174
Map<String, Object> params = original.getParams();
171175
List<String> summaryFields = original.getSummaryFields();
@@ -177,11 +181,11 @@ private static RatedRequest mutateTestItem(RatedRequest original) {
177181
id = randomValueOtherThan(id, () -> randomAlphaOfLength(10));
178182
break;
179183
case 1:
180-
if (testRequest != null) {
181-
int size = randomValueOtherThan(testRequest.size(), () -> randomInt(Integer.MAX_VALUE));
182-
testRequest = new SearchSourceBuilder();
183-
testRequest.size(size);
184-
testRequest.query(new MatchAllQueryBuilder());
184+
if (evaluationRequest != null) {
185+
int size = randomValueOtherThan(evaluationRequest.size(), () -> randomInt(Integer.MAX_VALUE));
186+
evaluationRequest = new SearchSourceBuilder();
187+
evaluationRequest.size(size);
188+
evaluationRequest.query(new MatchAllQueryBuilder());
185189
} else {
186190
if (randomBoolean()) {
187191
Map<String, Object> mutated = new HashMap<>();
@@ -204,10 +208,10 @@ private static RatedRequest mutateTestItem(RatedRequest original) {
204208
}
205209

206210
RatedRequest ratedRequest;
207-
if (testRequest == null) {
211+
if (evaluationRequest == null) {
208212
ratedRequest = new RatedRequest(id, ratedDocs, params, templateId);
209213
} else {
210-
ratedRequest = new RatedRequest(id, ratedDocs, testRequest);
214+
ratedRequest = new RatedRequest(id, ratedDocs, evaluationRequest);
211215
}
212216
ratedRequest.addSummaryFields(summaryFields);
213217

@@ -258,6 +262,44 @@ public void testSettingTemplateIdNoParamsThrows() {
258262
expectThrows(IllegalArgumentException.class, () -> new RatedRequest("id", ratedDocs, null, "templateId"));
259263
}
260264

265+
public void testAggsNotAllowed() {
266+
List<RatedDocument> ratedDocs = Arrays.asList(new RatedDocument("index1", "id1", 1));
267+
SearchSourceBuilder query = new SearchSourceBuilder();
268+
query.aggregation(AggregationBuilders.terms("fieldName"));
269+
IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> new RatedRequest("id", ratedDocs, query));
270+
assertEquals("Query in rated requests should not contain aggregations.", e.getMessage());
271+
}
272+
273+
public void testSuggestionsNotAllowed() {
274+
List<RatedDocument> ratedDocs = Arrays.asList(new RatedDocument("index1", "id1", 1));
275+
SearchSourceBuilder query = new SearchSourceBuilder();
276+
query.suggest(new SuggestBuilder().addSuggestion("id", SuggestBuilders.completionSuggestion("fieldname")));
277+
IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> new RatedRequest("id", ratedDocs, query));
278+
assertEquals("Query in rated requests should not contain a suggest section.", e.getMessage());
279+
}
280+
281+
public void testHighlighterNotAllowed() {
282+
List<RatedDocument> ratedDocs = Arrays.asList(new RatedDocument("index1", "id1", 1));
283+
SearchSourceBuilder query = new SearchSourceBuilder();
284+
query.highlighter(new HighlightBuilder().field("field"));
285+
IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> new RatedRequest("id", ratedDocs, query));
286+
assertEquals("Query in rated requests should not contain a highlighter section.", e.getMessage());
287+
}
288+
289+
public void testExplainNotAllowed() {
290+
List<RatedDocument> ratedDocs = Arrays.asList(new RatedDocument("index1", "id1", 1));
291+
IllegalArgumentException e = expectThrows(IllegalArgumentException.class,
292+
() -> new RatedRequest("id", ratedDocs, new SearchSourceBuilder().explain(true)));
293+
assertEquals("Query in rated requests should not use explain.", e.getMessage());
294+
}
295+
296+
public void testProfileNotAllowed() {
297+
List<RatedDocument> ratedDocs = Arrays.asList(new RatedDocument("index1", "id1", 1));
298+
IllegalArgumentException e = expectThrows(IllegalArgumentException.class,
299+
() -> new RatedRequest("id", ratedDocs, new SearchSourceBuilder().profile(true)));
300+
assertEquals("Query in rated requests should not use profile.", e.getMessage());
301+
}
302+
261303
/**
262304
* test that modifying the order of index/docId to make sure it doesn't
263305
* matter for parsing xContent
@@ -287,7 +329,7 @@ public void testParseFromXContent() throws IOException {
287329
try (XContentParser parser = createParser(JsonXContent.jsonXContent, querySpecString)) {
288330
RatedRequest specification = RatedRequest.fromXContent(parser);
289331
assertEquals("my_qa_query", specification.getId());
290-
assertNotNull(specification.getTestRequest());
332+
assertNotNull(specification.getEvaluationRequest());
291333
List<RatedDocument> ratedDocs = specification.getRatedDocs();
292334
assertEquals(3, ratedDocs.size());
293335
for (int i = 0; i < 3; i++) {

0 commit comments

Comments
 (0)