mirror of
https://github.com/nosqlbench/nosqlbench.git
synced 2026-10-13 09:52:01 -05:00
feat(engine): measure recall against the neighbors that exist
A filtered ground truth pads a query whose predicate matches fewer than k rows with sentinels no search can return, and the strict recall divides by k regardless, so a small slice reads low by construction. attainable_recall leaves the sentinels out of the denominator: the share of the neighbors that exist which were found. A row with nothing attainable scores 1.0; how many such rows there are is a fact about the ground truth, not the search.
This commit is contained in:
1 parent
d557669a80
commit
085e545787
4 files changed
+105
No files matched your search
+24
@@ -135,6 +135,30 @@ public class ComputeFunctions extends NBBaseComponent {
|
||||
return (double) common_indices / (double) k;
|
||||
}
|
||||
|
||||
/// Recall against the neighbors that exist. Of the first `k`
|
||||
/// ground-truth entries, a negative one is a **sentinel** for a
|
||||
/// neighbor that does not exist — a filtered search over a slice too
|
||||
/// small for its predicate leaves such rows padded — and is left out
|
||||
/// of both the numerator and the denominator: the result is the
|
||||
/// share of the attainable neighbors found in the first `k` results.
|
||||
/// A row with no attainable neighbor scores `1.0`, since nothing
|
||||
/// could be found and nothing was missed; how many such rows there
|
||||
/// are is a fact about the ground truth, reported before a run by
|
||||
/// its coverage, not a fact about the search. The strict
|
||||
/// [#recall(int[], int[], int)] divides by `k` regardless and is
|
||||
/// the measure to read on a ground truth with no sentinels.
|
||||
public static double attainableRecall(int[] ground_truth, int[] actual, int k) {
|
||||
if (ground_truth.length < k) {
|
||||
throw new RuntimeException("ground truth size " + ground_truth.length + " is less than k=" + k);
|
||||
}
|
||||
int[] attainable = Arrays.stream(ground_truth, 0, k).filter(entry -> entry >= 0).toArray();
|
||||
if (attainable.length == 0) return 1.0d;
|
||||
int[] found = Arrays.stream(actual, 0, Math.min(k, actual.length)).filter(entry -> entry >= 0).toArray();
|
||||
Arrays.sort(attainable);
|
||||
Arrays.sort(found);
|
||||
return (double) Intersections.count(attainable, found) / (double) attainable.length;
|
||||
}
|
||||
|
||||
public static double precision(int[] relevant, int[] actual) {
|
||||
Arrays.sort(relevant);
|
||||
Arrays.sort(actual);
|
||||
|
||||
+12
@@ -34,6 +34,18 @@ public class RelevancyFunctions {
|
||||
return new Recall(name, k);
|
||||
}
|
||||
|
||||
public static AttainableRecall attainable_recall(String name, int k, Map<String, String> labels) {
|
||||
return new AttainableRecall(name, k, labels);
|
||||
}
|
||||
|
||||
public static AttainableRecall attainable_recall(String name, int k, Object... labels) {
|
||||
return new AttainableRecall(name, k, labels);
|
||||
}
|
||||
|
||||
public static AttainableRecall attainable_recall(String name, int k) {
|
||||
return new AttainableRecall(name, k);
|
||||
}
|
||||
|
||||
public static Precision precision(String name, int k, Map<String, String> labels) {
|
||||
return new Precision(name, k, labels);
|
||||
}
|
||||
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* Copyright (c) nosqlbench
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.nosqlbench.engine.extensions.computefunctions.relavency;
|
||||
|
||||
import io.nosqlbench.engine.extensions.computefunctions.ComputeFunctions;
|
||||
import io.nosqlbench.nb.api.labels.NBLabels;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/// Recall against the neighbors that exist: the share of a query's
|
||||
/// **attainable** ground truth found in the first `k` results, where a
|
||||
/// negative ground-truth entry is a sentinel for a neighbor that does
|
||||
/// not exist and is left out of the denominator. The strict
|
||||
/// [Recall] divides by `k` whatever the row holds, so a filtered search
|
||||
/// whose predicate matches fewer than `k` rows reads low by
|
||||
/// construction; this function reads what the search could have done.
|
||||
/// See [ComputeFunctions#attainableRecall(int[], int[], int)].
|
||||
public class AttainableRecall extends BaseRelevancyFunction {
|
||||
private final int k;
|
||||
|
||||
public AttainableRecall(String name, int k, Object... labels) {
|
||||
super(name, NBLabels.forKV("k", k).and(NBLabels.forKV(labels)));
|
||||
this.k = k;
|
||||
}
|
||||
|
||||
public AttainableRecall(String name, int k, Map<String, String> labels) {
|
||||
super(name, NBLabels.forKV("k", labels).andMap(labels));
|
||||
this.k = k;
|
||||
}
|
||||
|
||||
@Override
|
||||
public double apply(int[] relevant, int[] actual) {
|
||||
return ComputeFunctions.attainableRecall(relevant, actual, k);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUniqueName() {
|
||||
return getName() + "_" + k;
|
||||
}
|
||||
}
|
||||
+15
@@ -57,6 +57,21 @@ class ComputeFunctionsIntTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void attainableRecallLeavesSentinelsOutOfTheDenominator() {
|
||||
int[] padded = new int[]{3, 7, -1, -1, -1};
|
||||
assertThat(ComputeFunctions.recall(padded, int_3_7_1_9_5, 5)).as(
|
||||
"strict recall counts a sentinel as a miss").isCloseTo(0.4d, offset);
|
||||
assertThat(ComputeFunctions.attainableRecall(padded, int_3_7_1_9_5, 5)).as(
|
||||
"both attainable neighbors were found").isCloseTo(1.0d, offset);
|
||||
assertThat(ComputeFunctions.attainableRecall(padded, new int[]{3, 8, -2, -3, -4}, 5)).as(
|
||||
"one of two attainable neighbors found; padded results never match").isCloseTo(0.5d, offset);
|
||||
assertThat(ComputeFunctions.attainableRecall(new int[]{-1, -1, -1, -1, -1}, int_3_7_1_9_5, 5)).as(
|
||||
"nothing attainable, nothing missed").isCloseTo(1.0d, offset);
|
||||
assertThat(ComputeFunctions.attainableRecall(allInts, int_3_7_1_9_5, 5)).as(
|
||||
"with no sentinels it is the strict recall").isCloseTo(ComputeFunctions.recall(allInts, int_3_7_1_9_5, 5), offset);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testProportionalIntersectionAtSomeK() {
|
||||
int[] image = new int[]{101,102,103,104,105};
|
||||
|
||||
Reference in new issue
Block a user