diff --git a/solr/modules/ltr/src/java/org/apache/solr/ltr/feature/FieldValueFeature.java b/solr/modules/ltr/src/java/org/apache/solr/ltr/feature/FieldValueFeature.java index bab34fa8d3e..583e080954c 100644 --- a/solr/modules/ltr/src/java/org/apache/solr/ltr/feature/FieldValueFeature.java +++ b/solr/modules/ltr/src/java/org/apache/solr/ltr/feature/FieldValueFeature.java @@ -129,6 +129,17 @@ public FieldValueFeatureWeight( } } + /** + * Override this method in sub classes that wish to use not an absolute time but an interval + * such as document age or remaining shelf life relative to a specific date or relative to now. + * + * @param val value of the field + * @return value after transformation + */ + protected long readNumericDocValuesDate(long val) { + return val; + } + /** * Return a FeatureScorer that uses docValues or storedFields if no docValues are present * @@ -261,6 +272,8 @@ private float readNumericDocValues() throws IOException { } else if (NumberType.DOUBLE.equals(numberType)) { // handle double value conversion return (float) Double.longBitsToDouble(docValues.longValue()); + } else if (NumberType.DATE.equals(numberType)) { + return readNumericDocValuesDate(docValues.longValue()); } // just take the long value return docValues.longValue(); diff --git a/solr/modules/ltr/src/test/org/apache/solr/ltr/feature/TestFieldValueFeature.java b/solr/modules/ltr/src/test/org/apache/solr/ltr/feature/TestFieldValueFeature.java index 8bed4efe7ed..b10d9d7f952 100644 --- a/solr/modules/ltr/src/test/org/apache/solr/ltr/feature/TestFieldValueFeature.java +++ b/solr/modules/ltr/src/test/org/apache/solr/ltr/feature/TestFieldValueFeature.java @@ -17,8 +17,10 @@ package org.apache.solr.ltr.feature; import java.io.IOException; +import java.time.Instant; import java.util.LinkedHashMap; import java.util.Map; +import java.util.concurrent.TimeUnit; import org.apache.lucene.index.LeafReaderContext; import org.apache.lucene.search.IndexSearcher; import org.apache.lucene.search.Query; @@ -692,6 +694,113 @@ public void testThatDateValuesAreCorrectlyParsed() throws Exception { } } + public static class RelativeDateFieldValueFeature extends FieldValueFeature { + + private boolean since = false; + private boolean until = false; + + public boolean getSince() { + return this.since; + } + + public void setSince(boolean since) { + this.since = since; + } + + public boolean getUntil() { + return this.until; + } + + public void setUntil(boolean until) { + this.until = until; + } + + public RelativeDateFieldValueFeature(String name, Map params) { + super(name, params); + } + + @Override + protected void validate() throws FeatureException { + if (since != until) { + return; + } + throw new FeatureException( + getClass().getSimpleName() + ": exactly one of 'since' and 'until' must be provided"); + } + + @Override + public FeatureWeight createWeight( + IndexSearcher searcher, + boolean needsScores, + SolrQueryRequest request, + Query originalQuery, + Map efi) + throws IOException { + return new FieldValueFeatureWeight(searcher, request, originalQuery, efi) { + private final long timeZero = Instant.parse("2000-01-01T00:00:00.000Z").toEpochMilli(); + + @Override + public long readNumericDocValuesDate(long val) { + if (since) return TimeUnit.MILLISECONDS.toMinutes(val - this.timeZero); + if (until) return TimeUnit.MILLISECONDS.toMinutes(this.timeZero - val); + return 0; + } + }; + } + } + + @Test + public void testRelativeDateFieldValueFeature() throws Exception { + final String field = "dvDateField"; + for (boolean since : new boolean[] {false, true}) { + final String[][] inputsAndTests = { + new String[] { + "2000-01-01T00:00:00.000Z", + "/response/docs/[0]/=={'[fv]':'" + + FeatureLoggerTestUtils.toFeatureVector(field, "0.0") + + "'}" + }, + new String[] { + "2000-01-01T00:01:02.003Z", + "/response/docs/[0]/=={'[fv]':'" + + FeatureLoggerTestUtils.toFeatureVector(field, (since ? "1.0" : "-1.0")) + + "'}" + }, + new String[] { + "2000-01-01T01:02:03.004Z", + "/response/docs/[0]/=={'[fv]':'" + + FeatureLoggerTestUtils.toFeatureVector(field, (since ? "62.0" : "-62.0")) + + "'}" + } + }; + + final String fstore = "testRelativeDateFieldValueFeature" + field + "_" + since; + final String model = fstore + "-model"; + loadFeature( + field, + RelativeDateFieldValueFeature.class.getName(), + fstore, + "{\"field\":\"" + field + "\", \"" + (since ? "since" : "until") + "\": true}"); + loadModel( + model, + LinearModel.class.getName(), + new String[] {field}, + fstore, + "{\"weights\":{\"" + field + "\":1.0}}"); + + for (String[] inputAndTest : inputsAndTests) { + assertU(adoc("id", "21", field, inputAndTest[0])); + assertU(commit()); + + final SolrQuery query = new SolrQuery("id:21"); + query.add("rq", "{!ltr model=" + model + " reRankDocs=4}"); + query.add("fl", "[fv]"); + + assertJQ("/query" + query.toQueryString(), inputAndTest[1]); + } + } + } + /** * This class is used to track which specific FieldValueFeature is used so that we can test, * whether the fallback mechanism works correctly.