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
50 changes: 50 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,56 @@ Queries accept at most 128 subvectors. Both `num_vectors * k` and
`refine_factor * k` must be at most 100,000 to bound plan expansion and candidate
allocation. The existing single-vector API and its defaults are unchanged.

## Batch nearest-neighbor search

Use `lance_scanner_nearest_batch` or `Scanner::nearest_batch` to search a
`FixedSizeList<T, dimension>` column with independent query vectors:

```cpp
const float queries[] = {1.0f, 0.0f, 0.0f, 1.0f};
auto scanner = dataset.scan();
scanner.nearest_batch("embedding", queries, 2, 2, 10)
.use_index(false);
```

Here there are two queries of dimension two, each returning up to ten rows.
The result includes the zero-based `query_index` (`int32`) and `_distance`,
alongside the requested dataset columns. A batch of one query still includes
`query_index`. Duplicate query vectors retain distinct indices; queries with no
matching rows produce no result rows. Result row order is not part of the API contract.

This differs from `nearest_multivector`: that API scores one logical query
against a `List<FixedSizeList<...>>` column and returns one ranked result set.
Batch input requires a fixed-size vector column and rejects a dataset with a
reserved `query_index` column. Float16, Float32, Float64, Int8, and UInt8 are accepted;
the query element type must match the column. UInt8 uses the Hamming metric.
Floating-point query values must be finite. Values are copied before the setter
returns, so the caller can immediately release its input buffer.

Configure the query before scanning. Successful nearest-query setters replace
the previous query; an invalid batch request leaves it unchanged. FTS and
nearest queries remain mutually exclusive. Distance ranges currently require a
single-vector query; replacing it with a batch clears its bounds. All queries
share the scanner's metric, filter, index selection, and tuning parameters.
Existing prefilter and postfilter semantics apply; postfiltering can return
fewer than `k` rows.

The scanner's global `limit` and `offset` settings are rejected in batch mode,
in either configuration order: they cannot represent independent result windows.
For per-query pagination, request enough candidates with `k` and apply the window
separately to each `query_index` in the caller.

Bounds are 128 queries, 64 MiB of copied query values, and 100,000 candidates for
`num_queries * k * refine_factor` (use 1 when refinement is unset). Explicit
refinement must be positive. These bounds do not cap total index/payload memory;
callers should also configure scanner resource controls. Arrow streams use the
existing ownership and cancellation contract, including early stream release.

Execution delegates to the pinned Lance scanner. Flat search can share data
scans. Eligible IVF queries can share partition scans; HNSW, refinement, adaptive
partition probing, and other ineligible plans use Lance's per-query fallback.
The batch API does not guarantee shared execution or a particular speedup.

## Building

There are four supported entry points; pick whichever matches your toolchain.
Expand Down
29 changes: 28 additions & 1 deletion include/lance/lance.h
Original file line number Diff line number Diff line change
Expand Up @@ -2131,7 +2131,7 @@ int32_t lance_scanner_nearest(
* Call after lance_scanner_nearest and before starting the scan. Both bounds
* are copied before returning; NULL means unbounded on that side. Passing NULL
* for both clears the range. A successful replacement nearest query also clears
* the range. Multi-vector queries and scans without nearest are rejected.
* the range. Batch/multi-vector queries and scans without nearest are rejected.
*
* Bounds must be finite; negative distances are allowed. When both are present,
* lower_bound must be strictly smaller than upper_bound. Invalid calls leave the
Expand All @@ -2150,6 +2150,33 @@ int32_t lance_scanner_set_distance_range(
const float* upper_bound
);

/**
* Set independent k-NN queries on a FixedSizeList<element_type, dimension> column.
* query_data contains dimension * num_queries aligned, row-major elements, copied
* before returning. Supported element types: Float16/32/64, Int8, and UInt8 (Hamming).
* Floating-point values must be finite and the type/dimension must match the column.
*
* Every query returns up to k rows with a zero-based int32 query_index column and
* _distance, including batches of one query. Duplicate queries retain separate
* indices. A dataset column named query_index is rejected. No output row order
* is guaranteed. Index, metric, filter and search settings are shared by all queries.
*
* dimension and k must be positive; num_queries must be in 1..=128. Copied values
* are bounded to 64 MiB; num_queries * k * refine_factor must be <= 100000, using
* 1 when refinement is unset. Explicit refinement must be positive. These are
* request bounds, not a total index/payload-memory limit.
*
* Must be configured before scanning. Replaces a previous nearest query atomically;
* an invalid request preserves it. Mutually exclusive with FTS and global scanner
* limit/offset, in either configuration order. Apply per-query windows in the caller.
* Streams retain the existing ownership, error and early-release/cancellation contract.
* Returns 0 on success, -1 on error; use lance_last_error_code/message for details.
*/
int32_t lance_scanner_nearest_batch(
LanceScanner* scanner, const char* column, const void* query_data,
size_t dimension, size_t num_queries, LanceDataType element_type, uint32_t k
);

/**
* Set one multi-vector query on a List<FixedSizeList<float16|float32|float64>> column.
* Inner vectors must be non-nullable and contain no null elements; the outer list may be nullable.
Expand Down
16 changes: 16 additions & 0 deletions include/lance/lance.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -1636,6 +1636,22 @@ class Scanner {
return *this;
}

/// Independent queries; each returns up to k rows tagged with query_index.
Scanner& nearest_batch(const std::string& column, const void* query_data,
size_t dimension, size_t num_queries,
LanceDataType element_type, uint32_t k) {
if (lance_scanner_nearest_batch(handle_.get(), column.c_str(), query_data,
dimension, num_queries, element_type, k) != 0)
check_error();
return *this;
}

/// Float32 batch convenience overload; input values are copied before returning.
Scanner& nearest_batch(const std::string& column, const float* query_data,
size_t dimension, size_t num_queries, uint32_t k) {
return nearest_batch(column, query_data, dimension, num_queries, LANCE_DTYPE_FLOAT32, k);
}

/// One multi-vector query, copied from dimension * num_vectors row-major elements.
Scanner& nearest_multivector(const std::string& column, const void* query_data,
size_t dimension, size_t num_vectors,
Expand Down
Loading
Loading