36
36
import java .util .Optional ;
37
37
import java .util .stream .Collectors ;
38
38
39
+ import static org .elasticsearch .common .xcontent .ConstructingObjectParser .constructorArg ;
39
40
import static org .elasticsearch .common .xcontent .ConstructingObjectParser .optionalConstructorArg ;
40
41
import static org .elasticsearch .index .rankeval .EvaluationMetric .joinHitsWithRatings ;
41
42
@@ -129,26 +130,31 @@ public EvalQueryQuality evaluate(String taskId, SearchHit[] hits,
129
130
.collect (Collectors .toList ());
130
131
List <RatedSearchHit > ratedHits = joinHitsWithRatings (hits , ratedDocs );
131
132
List <Integer > ratingsInSearchHits = new ArrayList <>(ratedHits .size ());
133
+ int unratedResults = 0 ;
132
134
for (RatedSearchHit hit : ratedHits ) {
133
- // unknownDocRating might be null, which means it will be unrated docs are
134
- // ignored in the dcg calculation
135
- // we still need to add them as a placeholder so the rank of the subsequent
136
- // ratings is correct
135
+ // unknownDocRating might be null, in which case unrated docs will be ignored in the dcg calculation.
136
+ // we still need to add them as a placeholder so the rank of the subsequent ratings is correct
137
137
ratingsInSearchHits .add (hit .getRating ().orElse (unknownDocRating ));
138
+ if (hit .getRating ().isPresent () == false ) {
139
+ unratedResults ++;
140
+ }
138
141
}
139
- double dcg = computeDCG (ratingsInSearchHits );
142
+ final double dcg = computeDCG (ratingsInSearchHits );
143
+ double result = dcg ;
144
+ double idcg = 0 ;
140
145
141
146
if (normalize ) {
142
147
Collections .sort (allRatings , Comparator .nullsLast (Collections .reverseOrder ()));
143
- double idcg = computeDCG (allRatings .subList (0 , Math .min (ratingsInSearchHits .size (), allRatings .size ())));
144
- if (idcg > 0 ) {
145
- dcg = dcg / idcg ;
148
+ idcg = computeDCG (allRatings .subList (0 , Math .min (ratingsInSearchHits .size (), allRatings .size ())));
149
+ if (idcg != 0 ) {
150
+ result = dcg / idcg ;
146
151
} else {
147
- dcg = 0 ;
152
+ result = 0 ;
148
153
}
149
154
}
150
- EvalQueryQuality evalQueryQuality = new EvalQueryQuality (taskId , dcg );
155
+ EvalQueryQuality evalQueryQuality = new EvalQueryQuality (taskId , result );
151
156
evalQueryQuality .addHitsAndRatings (ratedHits );
157
+ evalQueryQuality .setMetricDetails (new Detail (dcg , idcg , unratedResults ));
152
158
return evalQueryQuality ;
153
159
}
154
160
@@ -167,7 +173,7 @@ private static double computeDCG(List<Integer> ratings) {
167
173
private static final ParseField K_FIELD = new ParseField ("k" );
168
174
private static final ParseField NORMALIZE_FIELD = new ParseField ("normalize" );
169
175
private static final ParseField UNKNOWN_DOC_RATING_FIELD = new ParseField ("unknown_doc_rating" );
170
- private static final ConstructingObjectParser <DiscountedCumulativeGain , Void > PARSER = new ConstructingObjectParser <>("dcg_at " , false ,
176
+ private static final ConstructingObjectParser <DiscountedCumulativeGain , Void > PARSER = new ConstructingObjectParser <>("dcg " , false ,
171
177
args -> {
172
178
Boolean normalized = (Boolean ) args [0 ];
173
179
Integer optK = (Integer ) args [2 ];
@@ -217,4 +223,118 @@ public final boolean equals(Object obj) {
217
223
public final int hashCode () {
218
224
return Objects .hash (normalize , unknownDocRating , k );
219
225
}
226
+
227
+ public static final class Detail implements MetricDetail {
228
+
229
+ private static ParseField DCG_FIELD = new ParseField ("dcg" );
230
+ private static ParseField IDCG_FIELD = new ParseField ("ideal_dcg" );
231
+ private static ParseField NDCG_FIELD = new ParseField ("normalized_dcg" );
232
+ private static ParseField UNRATED_FIELD = new ParseField ("unrated_docs" );
233
+ private final double dcg ;
234
+ private final double idcg ;
235
+ private final int unratedDocs ;
236
+
237
+ Detail (double dcg , double idcg , int unratedDocs ) {
238
+ this .dcg = dcg ;
239
+ this .idcg = idcg ;
240
+ this .unratedDocs = unratedDocs ;
241
+ }
242
+
243
+ Detail (StreamInput in ) throws IOException {
244
+ this .dcg = in .readDouble ();
245
+ this .idcg = in .readDouble ();
246
+ this .unratedDocs = in .readVInt ();
247
+ }
248
+
249
+ @ Override
250
+ public
251
+ String getMetricName () {
252
+ return NAME ;
253
+ }
254
+
255
+ @ Override
256
+ public XContentBuilder innerToXContent (XContentBuilder builder , Params params ) throws IOException {
257
+ builder .field (DCG_FIELD .getPreferredName (), this .dcg );
258
+ if (this .idcg != 0 ) {
259
+ builder .field (IDCG_FIELD .getPreferredName (), this .idcg );
260
+ builder .field (NDCG_FIELD .getPreferredName (), this .dcg / this .idcg );
261
+ }
262
+ builder .field (UNRATED_FIELD .getPreferredName (), this .unratedDocs );
263
+ return builder ;
264
+ }
265
+
266
+ private static final ConstructingObjectParser <Detail , Void > PARSER = new ConstructingObjectParser <>(NAME , true , args -> {
267
+ return new Detail ((Double ) args [0 ], (Double ) args [1 ] != null ? (Double ) args [1 ] : 0.0d , (Integer ) args [2 ]);
268
+ });
269
+
270
+ static {
271
+ PARSER .declareDouble (constructorArg (), DCG_FIELD );
272
+ PARSER .declareDouble (optionalConstructorArg (), IDCG_FIELD );
273
+ PARSER .declareInt (constructorArg (), UNRATED_FIELD );
274
+ }
275
+
276
+ public static Detail fromXContent (XContentParser parser ) {
277
+ return PARSER .apply (parser , null );
278
+ }
279
+
280
+ @ Override
281
+ public void writeTo (StreamOutput out ) throws IOException {
282
+ out .writeDouble (this .dcg );
283
+ out .writeDouble (this .idcg );
284
+ out .writeVInt (this .unratedDocs );
285
+ }
286
+
287
+ @ Override
288
+ public String getWriteableName () {
289
+ return NAME ;
290
+ }
291
+
292
+ /**
293
+ * @return the discounted cumulative gain
294
+ */
295
+ public double getDCG () {
296
+ return this .dcg ;
297
+ }
298
+
299
+ /**
300
+ * @return the ideal discounted cumulative gain, can be 0 if nothing was computed, e.g. because no normalization was required
301
+ */
302
+ public double getIDCG () {
303
+ return this .idcg ;
304
+ }
305
+
306
+ /**
307
+ * @return the normalized discounted cumulative gain, can be 0 if nothing was computed, e.g. because no normalization was required
308
+ */
309
+ public double getNDCG () {
310
+ return (this .idcg != 0 ) ? this .dcg / this .idcg : 0 ;
311
+ }
312
+
313
+ /**
314
+ * @return the number of unrated documents in the search results
315
+ */
316
+ public Object getUnratedDocs () {
317
+ return this .unratedDocs ;
318
+ }
319
+
320
+ @ Override
321
+ public boolean equals (Object obj ) {
322
+ if (this == obj ) {
323
+ return true ;
324
+ }
325
+ if (obj == null || getClass () != obj .getClass ()) {
326
+ return false ;
327
+ }
328
+ DiscountedCumulativeGain .Detail other = (DiscountedCumulativeGain .Detail ) obj ;
329
+ return (this .dcg == other .dcg &&
330
+ this .idcg == other .idcg &&
331
+ this .unratedDocs == other .unratedDocs );
332
+ }
333
+
334
+ @ Override
335
+ public int hashCode () {
336
+ return Objects .hash (this .dcg , this .idcg , this .unratedDocs );
337
+ }
338
+ }
220
339
}
340
+
0 commit comments