Skip to content

Commit ef3cdec

Browse files
author
Christoph Büscher
committed
Fix NDCG for empty search results (#29267)
Fixes and edge case where DiscountedCumulativeGain can return NaN as result of the quality metric calculation. This can happen when the search result set is empty and normalization is used. We should return 0 in this case. Also adding related unit tests to the other two metrics.
1 parent 10e1b2c commit ef3cdec

File tree

5 files changed

+51
-3
lines changed

5 files changed

+51
-3
lines changed

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

+6-3
Original file line numberDiff line numberDiff line change
@@ -140,9 +140,12 @@ public EvalQueryQuality evaluate(String taskId, SearchHit[] hits,
140140

141141
if (normalize) {
142142
Collections.sort(allRatings, Comparator.nullsLast(Collections.reverseOrder()));
143-
double idcg = computeDCG(
144-
allRatings.subList(0, Math.min(ratingsInSearchHits.size(), allRatings.size())));
145-
dcg = dcg / idcg;
143+
double idcg = computeDCG(allRatings.subList(0, Math.min(ratingsInSearchHits.size(), allRatings.size())));
144+
if (idcg > 0) {
145+
dcg = dcg / idcg;
146+
} else {
147+
dcg = 0;
148+
}
146149
}
147150
EvalQueryQuality evalQueryQuality = new EvalQueryQuality(taskId, dcg);
148151
evalQueryQuality.addHitsAndRatings(ratedHits);

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

+4
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,10 @@ public String getWriteableName() {
228228
return NAME;
229229
}
230230

231+
/**
232+
* the ranking of the first relevant document, or -1 if no relevant document was
233+
* found
234+
*/
231235
int getFirstRelevantRank() {
232236
return firstRelevantRank;
233237
}

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

+26
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,32 @@ public void testDCGAtFourMoreRatings() {
205205
assertEquals(12.392789260714371 / 13.347184833073591, dcg.evaluate("id", hits, ratedDocs).getQualityLevel(), DELTA);
206206
}
207207

208+
/**
209+
* test that metric returns 0.0 when there are no search results
210+
*/
211+
public void testNoResults() throws Exception {
212+
Integer[] relevanceRatings = new Integer[] { 3, 2, 3, null, 1, null };
213+
List<RatedDocument> ratedDocs = new ArrayList<>();
214+
for (int i = 0; i < 6; i++) {
215+
if (i < relevanceRatings.length) {
216+
if (relevanceRatings[i] != null) {
217+
ratedDocs.add(new RatedDocument("index", Integer.toString(i), relevanceRatings[i]));
218+
}
219+
}
220+
}
221+
SearchHit[] hits = new SearchHit[0];
222+
DiscountedCumulativeGain dcg = new DiscountedCumulativeGain();
223+
EvalQueryQuality result = dcg.evaluate("id", hits, ratedDocs);
224+
assertEquals(0.0d, result.getQualityLevel(), DELTA);
225+
assertEquals(0, filterUnknownDocuments(result.getHitsAndRatings()).size());
226+
227+
// also check normalized
228+
dcg = new DiscountedCumulativeGain(true, null, 10);
229+
result = dcg.evaluate("id", hits, ratedDocs);
230+
assertEquals(0.0d, result.getQualityLevel(), DELTA);
231+
assertEquals(0, filterUnknownDocuments(result.getHitsAndRatings()).size());
232+
}
233+
208234
public void testParseFromXContent() throws IOException {
209235
assertParsedCorrect("{ \"unknown_doc_rating\": 2, \"normalize\": true, \"k\" : 15 }", 2, true, 15);
210236
assertParsedCorrect("{ \"normalize\": false, \"k\" : 15 }", null, false, 15);

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

+7
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,13 @@ public void testEvaluationNoRelevantInResults() {
158158
assertEquals(0.0, evaluation.getQualityLevel(), Double.MIN_VALUE);
159159
}
160160

161+
public void testNoResults() throws Exception {
162+
SearchHit[] hits = new SearchHit[0];
163+
EvalQueryQuality evaluated = (new MeanReciprocalRank()).evaluate("id", hits, Collections.emptyList());
164+
assertEquals(0.0d, evaluated.getQualityLevel(), 0.00001);
165+
assertEquals(-1, ((MeanReciprocalRank.Breakdown) evaluated.getMetricDetails()).getFirstRelevantRank());
166+
}
167+
161168
public void testXContentRoundtrip() throws IOException {
162169
MeanReciprocalRank testItem = createTestItem();
163170
XContentBuilder builder = XContentFactory.contentBuilder(randomFrom(XContentType.values()));

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

+8
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,14 @@ public void testNoRatedDocs() throws Exception {
142142
assertEquals(0, ((PrecisionAtK.Breakdown) evaluated.getMetricDetails()).getRetrieved());
143143
}
144144

145+
public void testNoResults() throws Exception {
146+
SearchHit[] hits = new SearchHit[0];
147+
EvalQueryQuality evaluated = (new PrecisionAtK()).evaluate("id", hits, Collections.emptyList());
148+
assertEquals(0.0d, evaluated.getQualityLevel(), 0.00001);
149+
assertEquals(0, ((PrecisionAtK.Breakdown) evaluated.getMetricDetails()).getRelevantRetrieved());
150+
assertEquals(0, ((PrecisionAtK.Breakdown) evaluated.getMetricDetails()).getRetrieved());
151+
}
152+
145153
public void testParseFromXContent() throws IOException {
146154
String xContent = " {\n" + " \"relevant_rating_threshold\" : 2" + "}";
147155
try (XContentParser parser = createParser(JsonXContent.jsonXContent, xContent)) {

0 commit comments

Comments
 (0)