|
@@ -36,6 +36,7 @@ import java.util.Objects;
|
|
|
import java.util.Optional;
|
|
|
import java.util.stream.Collectors;
|
|
|
|
|
|
+import static org.elasticsearch.common.xcontent.ConstructingObjectParser.constructorArg;
|
|
|
import static org.elasticsearch.common.xcontent.ConstructingObjectParser.optionalConstructorArg;
|
|
|
import static org.elasticsearch.index.rankeval.EvaluationMetric.joinHitsWithRatings;
|
|
|
|
|
@@ -129,26 +130,31 @@ public class DiscountedCumulativeGain implements EvaluationMetric {
|
|
|
.collect(Collectors.toList());
|
|
|
List<RatedSearchHit> ratedHits = joinHitsWithRatings(hits, ratedDocs);
|
|
|
List<Integer> ratingsInSearchHits = new ArrayList<>(ratedHits.size());
|
|
|
+ int unratedResults = 0;
|
|
|
for (RatedSearchHit hit : ratedHits) {
|
|
|
- // unknownDocRating might be null, which means it will be unrated docs are
|
|
|
- // ignored in the dcg calculation
|
|
|
- // we still need to add them as a placeholder so the rank of the subsequent
|
|
|
- // ratings is correct
|
|
|
+ // unknownDocRating might be null, in which case unrated docs will be ignored in the dcg calculation.
|
|
|
+ // we still need to add them as a placeholder so the rank of the subsequent ratings is correct
|
|
|
ratingsInSearchHits.add(hit.getRating().orElse(unknownDocRating));
|
|
|
+ if (hit.getRating().isPresent() == false) {
|
|
|
+ unratedResults++;
|
|
|
+ }
|
|
|
}
|
|
|
- double dcg = computeDCG(ratingsInSearchHits);
|
|
|
+ final double dcg = computeDCG(ratingsInSearchHits);
|
|
|
+ double result = dcg;
|
|
|
+ double idcg = 0;
|
|
|
|
|
|
if (normalize) {
|
|
|
Collections.sort(allRatings, Comparator.nullsLast(Collections.reverseOrder()));
|
|
|
- double idcg = computeDCG(allRatings.subList(0, Math.min(ratingsInSearchHits.size(), allRatings.size())));
|
|
|
- if (idcg > 0) {
|
|
|
- dcg = dcg / idcg;
|
|
|
+ idcg = computeDCG(allRatings.subList(0, Math.min(ratingsInSearchHits.size(), allRatings.size())));
|
|
|
+ if (idcg != 0) {
|
|
|
+ result = dcg / idcg;
|
|
|
} else {
|
|
|
- dcg = 0;
|
|
|
+ result = 0;
|
|
|
}
|
|
|
}
|
|
|
- EvalQueryQuality evalQueryQuality = new EvalQueryQuality(taskId, dcg);
|
|
|
+ EvalQueryQuality evalQueryQuality = new EvalQueryQuality(taskId, result);
|
|
|
evalQueryQuality.addHitsAndRatings(ratedHits);
|
|
|
+ evalQueryQuality.setMetricDetails(new Detail(dcg, idcg, unratedResults));
|
|
|
return evalQueryQuality;
|
|
|
}
|
|
|
|
|
@@ -167,7 +173,7 @@ public class DiscountedCumulativeGain implements EvaluationMetric {
|
|
|
private static final ParseField K_FIELD = new ParseField("k");
|
|
|
private static final ParseField NORMALIZE_FIELD = new ParseField("normalize");
|
|
|
private static final ParseField UNKNOWN_DOC_RATING_FIELD = new ParseField("unknown_doc_rating");
|
|
|
- private static final ConstructingObjectParser<DiscountedCumulativeGain, Void> PARSER = new ConstructingObjectParser<>("dcg_at", false,
|
|
|
+ private static final ConstructingObjectParser<DiscountedCumulativeGain, Void> PARSER = new ConstructingObjectParser<>("dcg", false,
|
|
|
args -> {
|
|
|
Boolean normalized = (Boolean) args[0];
|
|
|
Integer optK = (Integer) args[2];
|
|
@@ -217,4 +223,118 @@ public class DiscountedCumulativeGain implements EvaluationMetric {
|
|
|
public final int hashCode() {
|
|
|
return Objects.hash(normalize, unknownDocRating, k);
|
|
|
}
|
|
|
+
|
|
|
+ public static final class Detail implements MetricDetail {
|
|
|
+
|
|
|
+ private static ParseField DCG_FIELD = new ParseField("dcg");
|
|
|
+ private static ParseField IDCG_FIELD = new ParseField("ideal_dcg");
|
|
|
+ private static ParseField NDCG_FIELD = new ParseField("normalized_dcg");
|
|
|
+ private static ParseField UNRATED_FIELD = new ParseField("unrated_docs");
|
|
|
+ private final double dcg;
|
|
|
+ private final double idcg;
|
|
|
+ private final int unratedDocs;
|
|
|
+
|
|
|
+ Detail(double dcg, double idcg, int unratedDocs) {
|
|
|
+ this.dcg = dcg;
|
|
|
+ this.idcg = idcg;
|
|
|
+ this.unratedDocs = unratedDocs;
|
|
|
+ }
|
|
|
+
|
|
|
+ Detail(StreamInput in) throws IOException {
|
|
|
+ this.dcg = in.readDouble();
|
|
|
+ this.idcg = in.readDouble();
|
|
|
+ this.unratedDocs = in.readVInt();
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public
|
|
|
+ String getMetricName() {
|
|
|
+ return NAME;
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public XContentBuilder innerToXContent(XContentBuilder builder, Params params) throws IOException {
|
|
|
+ builder.field(DCG_FIELD.getPreferredName(), this.dcg);
|
|
|
+ if (this.idcg != 0) {
|
|
|
+ builder.field(IDCG_FIELD.getPreferredName(), this.idcg);
|
|
|
+ builder.field(NDCG_FIELD.getPreferredName(), this.dcg / this.idcg);
|
|
|
+ }
|
|
|
+ builder.field(UNRATED_FIELD.getPreferredName(), this.unratedDocs);
|
|
|
+ return builder;
|
|
|
+ }
|
|
|
+
|
|
|
+ private static final ConstructingObjectParser<Detail, Void> PARSER = new ConstructingObjectParser<>(NAME, true, args -> {
|
|
|
+ return new Detail((Double) args[0], (Double) args[1] != null ? (Double) args[1] : 0.0d, (Integer) args[2]);
|
|
|
+ });
|
|
|
+
|
|
|
+ static {
|
|
|
+ PARSER.declareDouble(constructorArg(), DCG_FIELD);
|
|
|
+ PARSER.declareDouble(optionalConstructorArg(), IDCG_FIELD);
|
|
|
+ PARSER.declareInt(constructorArg(), UNRATED_FIELD);
|
|
|
+ }
|
|
|
+
|
|
|
+ public static Detail fromXContent(XContentParser parser) {
|
|
|
+ return PARSER.apply(parser, null);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public void writeTo(StreamOutput out) throws IOException {
|
|
|
+ out.writeDouble(this.dcg);
|
|
|
+ out.writeDouble(this.idcg);
|
|
|
+ out.writeVInt(this.unratedDocs);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public String getWriteableName() {
|
|
|
+ return NAME;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * @return the discounted cumulative gain
|
|
|
+ */
|
|
|
+ public double getDCG() {
|
|
|
+ return this.dcg;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * @return the ideal discounted cumulative gain, can be 0 if nothing was computed, e.g. because no normalization was required
|
|
|
+ */
|
|
|
+ public double getIDCG() {
|
|
|
+ return this.idcg;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * @return the normalized discounted cumulative gain, can be 0 if nothing was computed, e.g. because no normalization was required
|
|
|
+ */
|
|
|
+ public double getNDCG() {
|
|
|
+ return (this.idcg != 0) ? this.dcg / this.idcg : 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * @return the number of unrated documents in the search results
|
|
|
+ */
|
|
|
+ public Object getUnratedDocs() {
|
|
|
+ return this.unratedDocs;
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public boolean equals(Object obj) {
|
|
|
+ if (this == obj) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ if (obj == null || getClass() != obj.getClass()) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ DiscountedCumulativeGain.Detail other = (DiscountedCumulativeGain.Detail) obj;
|
|
|
+ return (this.dcg == other.dcg &&
|
|
|
+ this.idcg == other.idcg &&
|
|
|
+ this.unratedDocs == other.unratedDocs);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public int hashCode() {
|
|
|
+ return Objects.hash(this.dcg, this.idcg, this.unratedDocs);
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|
|
|
+
|