Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,9 +160,9 @@ from the record it has already read rather than from a second lookup.
`RowStoringIndex.isDeleted()` recognises a tombstone. A structure holding its
own form of a row records the tombstone there too: the matrix index in the
unilateral values it scores against, and an inverted index in the indexed form
verification reads. The unilateral values an inverted index orders its lists by are left
alone, since candidate generation prunes against them and an order it cannot
compare would stop pruning working.
verification reads. The unilateral values an inverted index orders its lists by
are left alone, since candidate generation prunes against them and an order it
cannot compare would stop pruning working.

An update is a delete of the old version followed by an insert of the new one
under the same `rowNum`, so the active cache always holds the latest version.
Expand Down Expand Up @@ -597,6 +597,12 @@ the bound, so one adding no more rows than the bound never builds a queue. The
dot products of a query are reused rather than allocated for each query, since
they are as long as the matrix has rows.

Selecting raises its minimum similarity as its heap fills, as a scan does, so a
row that cannot reach what the heap already holds is rejected before a result
is made for it. A query asking for the best rows of the whole matrix otherwise
makes one result per row and lets the heap discard almost all of them, which
was measured to cost more than the multiply that produced the dot products.

Selecting runs on the thread that asked for the query, not on the thread that
performed the multiply, so several queries select at once and overlap the
following multiply. Performing it inside the multiply would serialize the one
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/* AUTHOR: Ahmed Metwally (ametwally@uber.com) */
package com.uber.ussi.searchablestructure.index.matrix;

import com.uber.ussi.searchablestructure.result.ResultHeaps;
import com.uber.ussi.searchablestructure.result.RowNumAndSimilarity;
import com.uber.ussi.utils.BoundedSizeMaxHeap;

Expand All @@ -12,18 +13,25 @@ private DotProductRows() {}
/**
* Adds every row reaching the minimum similarity. A deleted row reaches nothing, since the
* similarity derived from its unilateral value is not a number.
*
* <p>The minimum rises as the heap fills, so a row that cannot reach what the heap already
* holds is rejected before a result is made for it. Without that, a query asking for the best
* rows of the whole matrix makes one result per row and lets the heap discard almost all of
* them, which was measured to cost more than the multiply that produced the dot products.
*/
static void addRows(
float[] dotProducts,
MatrixRows rows,
RowSelection selection,
BoundedSizeMaxHeap<RowNumAndSimilarity> into) {
float minSimilarity = selection.getMinSimilarity();
for (int matrixRowIndex = 0; matrixRowIndex < rows.getNumRows(); ++matrixRowIndex) {
float similarity =
rows.getSimilarity(
dotProducts[matrixRowIndex], selection.getQueryUniValue(), matrixRowIndex);
if (similarity >= selection.getMinSimilarity()) {
if (similarity >= minSimilarity) {
into.add(new RowNumAndSimilarity(rows.getRowNum(matrixRowIndex), similarity));
minSimilarity = ResultHeaps.tightenedMinSimilarity(into, minSimilarity);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package com.uber.ussi.searchablestructure.index.matrix;

import static org.junit.jupiter.api.Assertions.assertEquals;

import com.uber.ussi.searchablestructure.result.ResultHeaps;
import com.uber.ussi.searchablestructure.result.RowNumAndSimilarity;
import com.uber.ussi.utils.BoundedSizeMaxHeap;
import java.util.List;
import org.junit.jupiter.api.Test;

/** Rising the minimum as the heap fills must change what it costs, never what it keeps. */
class DotProductRowsTest {

@Test
void keepsTheBestRows() {
List<Long> kept = addRows(new float[] {1f, 5f, 3f, 4f, 2f}, /* maxResults */ 2);

assertEquals(List.of(1L, 3L), kept, "the two strongest rows, by row number");
}

/**
* The minimum the heap proves is one step below its weakest row, so a row scoring exactly as
* well is still offered to the heap rather than skipped, and the heap keeps as many as it is
* bounded by. The result is what it would be with no minimum at all.
*/
@Test
void keepsTheBoundWhenRowsTieWithTheWeakestKept() {
BoundedSizeMaxHeap<RowNumAndSimilarity> into = ResultHeaps.newTopResults(2);

DotProductRows.addRows(
new float[] {1f, 4f, 4f, 4f}, TestMatrixRows.of(4),
new RowSelection(0, Float.NEGATIVE_INFINITY, 2), into);

List<Float> kept =
into.toList().stream().map(RowNumAndSimilarity::getSimilarity).sorted().toList();
assertEquals(List.of(4f, 4f), kept);
}

@Test
void keepsNothingBelowTheQuerysOwnMinimum() {
BoundedSizeMaxHeap<RowNumAndSimilarity> into = ResultHeaps.newTopResults(10);
float[] dotProducts = {1f, 2f, 3f};

DotProductRows.addRows(
dotProducts, TestMatrixRows.of(3), new RowSelection(0, /* minSimilarity */ 2.5f, 10),
into);

assertEquals(1, into.toList().size());
}

private static List<Long> addRows(float[] dotProducts, int maxResults) {
BoundedSizeMaxHeap<RowNumAndSimilarity> into = ResultHeaps.newTopResults(maxResults);

DotProductRows.addRows(
dotProducts,
TestMatrixRows.of(dotProducts.length),
new RowSelection(0, Float.NEGATIVE_INFINITY, maxResults),
into);

return into.toList().stream().map(RowNumAndSimilarity::getRowNum).sorted().toList();
}
}
Loading