3636import java .util .Optional ;
3737import java .util .stream .Collectors ;
3838
39+ import static org .elasticsearch .common .xcontent .ConstructingObjectParser .constructorArg ;
3940import static org .elasticsearch .common .xcontent .ConstructingObjectParser .optionalConstructorArg ;
4041import static org .elasticsearch .index .rankeval .EvaluationMetric .joinHitsWithRatings ;
4142
@@ -129,26 +130,31 @@ public EvalQueryQuality evaluate(String taskId, SearchHit[] hits,
129130 .collect (Collectors .toList ());
130131 List <RatedSearchHit > ratedHits = joinHitsWithRatings (hits , ratedDocs );
131132 List <Integer > ratingsInSearchHits = new ArrayList <>(ratedHits .size ());
133+ int unratedResults = 0 ;
132134 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
137137 ratingsInSearchHits .add (hit .getRating ().orElse (unknownDocRating ));
138+ if (hit .getRating ().isPresent () == false ) {
139+ unratedResults ++;
140+ }
138141 }
139- double dcg = computeDCG (ratingsInSearchHits );
142+ final double dcg = computeDCG (ratingsInSearchHits );
143+ double result = dcg ;
144+ double idcg = 0 ;
140145
141146 if (normalize ) {
142147 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 ;
146151 } else {
147- dcg = 0 ;
152+ result = 0 ;
148153 }
149154 }
150- EvalQueryQuality evalQueryQuality = new EvalQueryQuality (taskId , dcg );
155+ EvalQueryQuality evalQueryQuality = new EvalQueryQuality (taskId , result );
151156 evalQueryQuality .addHitsAndRatings (ratedHits );
157+ evalQueryQuality .setMetricDetails (new Detail (dcg , idcg , unratedResults ));
152158 return evalQueryQuality ;
153159 }
154160
@@ -167,7 +173,7 @@ private static double computeDCG(List<Integer> ratings) {
167173 private static final ParseField K_FIELD = new ParseField ("k" );
168174 private static final ParseField NORMALIZE_FIELD = new ParseField ("normalize" );
169175 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 ,
171177 args -> {
172178 Boolean normalized = (Boolean ) args [0 ];
173179 Integer optK = (Integer ) args [2 ];
@@ -217,4 +223,118 @@ public final boolean equals(Object obj) {
217223 public final int hashCode () {
218224 return Objects .hash (normalize , unknownDocRating , k );
219225 }
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+ }
220339}
340+
0 commit comments