Vector Top-K
UDTF: cuvs_top_k
Official cuVS reference: C++ API
Select the smallest or largest values in every dense vector row.
Quickstart
The call below expects the registered relation input_vectors (the input role), passed as a parenthesized SELECT subquery. Substitute your own relations and column names.
SELECT *
FROM cuvs_top_k(
input => (SELECT id, d0, d1 FROM input_vectors),
k => 1,
select => 'max'
)
ORDER BY row_ordinal, rank;
Inputs
Each relation argument is a parenthesized SELECT subquery that the planner keeps as a real child; metadata validation resolves a registered table or view for the same role instead. See Vector Inputs for the relation identity rules and the ID, dense-vector type, null, finite-value, and runtime-dimension contract.
Vector element types
Arguments and options
Scalar SQL arguments
SQL value argument schemas
Vector binding shapes
Each relation subquery must project a non-null id field followed by either one or more non-null feature fields of a supported element type (Float32) or one non-null list vector field named vector.
For wide vectors, the projection order defines the feature dimensions. A list vector relation must contain no feature field beside id and vector.
Output
Concrete schemas are call-specific. Run gpu_validate_call against registered relations to inspect the output schema after the actual ID types and literal options are validated.
Examples
- Amazon Reviews
- EMBER2024
- MerRec marketplace
This example uses the raw products and reviews tables from the
Amazon Reviews demo dataset. It does not require the
semantic embedding preparation.
Which review themes describe each group of products?
A product analyst wants a short description of groups with similar complaint profiles. This query filters 2022 low-rating reviews in the headphones and accessories category, calculates eight keyword-mention rates per product, and keeps products with at least ten reviews. KMeans groups the resulting 2,384 products into six clusters. SQL then averages each cluster's product profiles, and Top-K returns its three largest rates.
Both algorithms run in the statement below. The numerical features come from the raw text through visible SQL expressions; they are not stored embeddings or predefined cluster labels.
WITH complaint_features AS (
SELECT
p.product_id AS id,
COUNT(*) AS negative_reviews,
CAST(AVG(CASE WHEN LOWER(r.text) LIKE '%battery%' THEN 1.0 ELSE 0.0 END) AS REAL) AS battery,
CAST(AVG(CASE WHEN LOWER(r.text) LIKE '%connect%' OR LOWER(r.text) LIKE '%bluetooth%' THEN 1.0 ELSE 0.0 END) AS REAL) AS connection,
CAST(AVG(CASE WHEN LOWER(r.text) LIKE '%sound%' OR LOWER(r.text) LIKE '%volume%' THEN 1.0 ELSE 0.0 END) AS REAL) AS sound,
CAST(AVG(CASE WHEN LOWER(r.text) LIKE '%comfort%' OR LOWER(r.text) LIKE '%fit%' THEN 1.0 ELSE 0.0 END) AS REAL) AS comfort,
CAST(AVG(CASE WHEN LOWER(r.text) LIKE '%broke%' OR LOWER(r.text) LIKE '%stopped working%' THEN 1.0 ELSE 0.0 END) AS REAL) AS durability,
CAST(AVG(CASE WHEN LOWER(r.text) LIKE '%charg%' THEN 1.0 ELSE 0.0 END) AS REAL) AS charging,
CAST(AVG(CASE WHEN LOWER(r.text) LIKE '%microphone%' OR LOWER(r.text) LIKE '%mic %' THEN 1.0 ELSE 0.0 END) AS REAL) AS microphone,
CAST(AVG(CASE WHEN LOWER(r.text) LIKE '%return%' OR LOWER(r.text) LIKE '%refund%' THEN 1.0 ELSE 0.0 END) AS REAL) AS returns
FROM reviews AS r
JOIN products AS p
ON r.product_id = p.product_id
WHERE p.category_l2 = 'Headphones, Earbuds & Accessories'
AND r.review_time >= TIMESTAMP '2022-01-01 00:00:00'
AND r.review_time < TIMESTAMP '2023-01-01 00:00:00'
AND r.rating <= 2
AND r.text IS NOT NULL
GROUP BY p.product_id
HAVING COUNT(*) >= 10
),
clusters AS (
SELECT
id,
cluster_id
FROM cuvs_kmeans(
input => (
SELECT
id,
COALESCE(battery, CAST(0 AS REAL)) AS battery,
COALESCE(connection, CAST(0 AS REAL)) AS connection,
COALESCE(sound, CAST(0 AS REAL)) AS sound,
COALESCE(comfort, CAST(0 AS REAL)) AS comfort,
COALESCE(durability, CAST(0 AS REAL)) AS durability,
COALESCE(charging, CAST(0 AS REAL)) AS charging,
COALESCE(microphone, CAST(0 AS REAL)) AS microphone,
COALESCE(returns, CAST(0 AS REAL)) AS returns
FROM complaint_features
ORDER BY id
),
n_clusters => 6,
n_init => 5
)
),
profiles AS (
SELECT
CAST(k.cluster_id AS BIGINT) AS id,
COALESCE(CAST(AVG(c.battery) AS REAL), CAST(0 AS REAL)) AS battery,
COALESCE(CAST(AVG(c.connection) AS REAL), CAST(0 AS REAL)) AS connection,
COALESCE(CAST(AVG(c.sound) AS REAL), CAST(0 AS REAL)) AS sound,
COALESCE(CAST(AVG(c.comfort) AS REAL), CAST(0 AS REAL)) AS comfort,
COALESCE(CAST(AVG(c.durability) AS REAL), CAST(0 AS REAL)) AS durability,
COALESCE(CAST(AVG(c.charging) AS REAL), CAST(0 AS REAL)) AS charging,
COALESCE(CAST(AVG(c.microphone) AS REAL), CAST(0 AS REAL)) AS microphone,
COALESCE(CAST(AVG(c.returns) AS REAL), CAST(0 AS REAL)) AS returns
FROM clusters AS k
JOIN complaint_features AS c
ON k.id = c.id
GROUP BY k.cluster_id
)
SELECT
t.id AS cluster_id,
t.rank,
CASE t.position
WHEN 0 THEN 'battery'
WHEN 1 THEN 'connection'
WHEN 2 THEN 'sound'
WHEN 3 THEN 'comfort'
WHEN 4 THEN 'durability'
WHEN 5 THEN 'charging'
WHEN 6 THEN 'microphone'
WHEN 7 THEN 'returns'
END AS issue,
t.value AS mean_product_mention_rate
FROM cuvs_top_k(
input => (
SELECT
id,
battery,
connection,
sound,
comfort,
durability,
charging,
microphone,
returns
FROM profiles
),
k => 3,
select => 'max'
) AS t
ORDER BY
t.id,
t.rank;
The 2026-09-28 capture returned 18 rows in a median 0.776 seconds over three
runs. Its cluster 0 has the following profile:
For cluster 1, sound leads at 45.10%, followed by charging at 24.20% and
connection at 24.09%. Those profiles give an analyst different starting
points for reading reviews: breakage in one product group, audio and
charging reports in another. The complete saved output
contains all six groups; capture conditions
include raw-text feature calculation and both algorithm calls.
Top-K selects dimensions within each cluster profile. Here position = 5
means the charging dimension, because charging is the sixth projected
feature. An ORDER BY ... LIMIT 3 over the whole result would answer a
different question.
Each rate counts reviews containing an English substring. Negation is not resolved, and a review can mention several themes. The profile is an average of product-level rates: a product with ten reviews has the same weight as a product with a thousand. Mentions of returns do not establish a completed return transaction.
This retrospective triage example uses the EMBER2024 demo dataset.
Which high-scoring challenge files should an analyst review first?
The LightGBM model scores all 6,315 challenge files. A score threshold of
0.99 selects 1,616 rows before the query's LIMIT 30; the output keeps the
highest-scoring rows with their three largest positive feature-block
contributions. Challenge labels are not inputs to scoring or ranking. They are
all positive, so this review queue cannot estimate a false-positive rate or
score calibration.
The feature-block values are SHAP contributions in raw-margin (log-odds)
units. The attribution table was prepared separately on CPU with
fixture/demo/ember2024/scripts/research_attributions.py: LightGBM
pred_contrib processed 6,315 rows in 13.311 seconds. The script then summed
the 2,568 feature contributions into 12 blocks. The SQL calls cuml_forest_predict for
GPU scoring and cuvs_top_k over those precomputed grouped values. The CPU
attribution preparation is not GPU work and is not included in the SQL run.
The CPU LightGBM score comparison passed for all 6,315 rows, with maximum
absolute error 2.9801783152372252e-8; threshold decisions agreed for every
row at the checked thresholds, including 0.99. Numerical agreement verifies
the imported model's scoring implementation. Detection metrics require the
separately evaluated test labels.
On the separate 6,102-file sample from the later test period, the same 0.99
threshold detected 2,167 of 3,016 malicious files (71.85%) and produced no
false positives among 3,086 benign files. The Wilson 95% interval for that
observed false-positive rate extends up to 0.1243%. The challenge selection
contains 1,616 of 6,315 positives (25.59%). These cohort-specific measurements
describe the coverage lost at a high review threshold; a zero count in this
test sample does not establish a zero operational false-positive rate.
The table shows the first six rows of the 30-row capture. Top-K ranks the 12 signed feature-block values and SQL keeps only positive values. Negative feature groups and the model bias are omitted, so these rows are not complete explanations. Positive SHAP contributions describe the model's score calculation; they do not establish causal risk effects. Family labels are not used by the query.
Download the forest review capture for all 30 result rows, the SQL, query ID, CPU comparison, and preparation conditions.
SELECT p.id, m.sha256, m.file_type, m.week_id, p.prediction,
t.rank, g.group_name, t.value AS positive_log_odds_contribution
FROM cuml_forest_predict(
input => (
SELECT id, vector
FROM ember_research_vectors_challenge
ORDER BY id
),
model => 'EMBER2024_all.model',
model_format => 'lightgbm'
) p
JOIN cuvs_top_k(
input => (SELECT id, vector FROM ember_attributions ORDER BY id),
k => 3,
select => 'max'
) t ON t.id = p.id
JOIN ember_feature_groups g ON g.group_index = t.position
JOIN ember_metadata_challenge m ON m.id = p.id
WHERE p.prediction >= 0.99 AND t.value > 0
ORDER BY p.prediction DESC, p.id, t.rank
LIMIT 30;
The query was captured on a dedicated EMBER server with native disposition.
The CPU attribution step ran before the statement; its duration is recorded
separately in the capture.
This capture uses the MerRec marketplace interaction sample. The preparation
query retained 1,919 users with at least 20 item_view events before the
exclusive cutoff of 2023-10-21. Each of the 17 input features is the square
root of that user's category item_view event share.
The MerRec dataset page describes the
snapshot preparation, including the mr_tastes relation.
Which category-specific campaign variants could be worth testing?
The query fits KMeans with eight clusters and n_init = 5 on those 17
transformed features. SQL squares the features before averaging them within
each fitted group, then Top-K returns the three largest mean category shares.
Each percentage below is an equal-user mean of that category's share of
item_view events. It is not a share of all events in a group or a proportion
of users.
Cluster 0 is weighted toward Men at 62.3%, with Women at 15.1%. Cluster 5 is weighted toward Kids at 69.7%, with Women at 13.5%. Cluster 4 has a more distributed profile: its leading category, Toys & Collectibles, is 22.4%. These differences can guide which category-specific campaign variants a marketplace team tests. This capture has no conversion outcomes and measures no campaign uplift.
The query returns no group sizes or per-user assignments. Cluster IDs belong to this fit and are not durable audience identifiers. A separate comparison against saved KMeans assignments matched all eight top-three profile means exactly after cluster-label matching. That cross-fit consistency supports this profile table only; it does not establish that the independent fits assigned each user to the same cluster.
The captured plan reported five native plan fragments and zero host DataFusion
boundaries. Server query statistics reported 246 ms elapsed. The client Flight
elapsed_s was 0.25340579298790544 seconds, including planning and receipt of
the full result while excluding preparation. Both are single observations from
a shared server, not isolated benchmarks. The JSON capture
preserves the timing boundaries, query ID, exact SQL, output schema, all 24
result rows, and source hashes.
Exact captured SQL
-- Fit one query-local KMeans model, then rank each fitted group's mean category shares.
WITH assignments AS (
SELECT id, cluster_id
FROM cuvs_kmeans(
input => (
SELECT
id,
COALESCE(women, CAST(0 AS REAL)) AS women,
COALESCE(toys, CAST(0 AS REAL)) AS toys,
COALESCE(kids, CAST(0 AS REAL)) AS kids,
COALESCE(men, CAST(0 AS REAL)) AS men,
COALESCE(home, CAST(0 AS REAL)) AS home,
COALESCE(electronics, CAST(0 AS REAL)) AS electronics,
COALESCE(beauty, CAST(0 AS REAL)) AS beauty,
COALESCE(vintage, CAST(0 AS REAL)) AS vintage,
COALESCE(books, CAST(0 AS REAL)) AS books,
COALESCE(sports, CAST(0 AS REAL)) AS sports,
COALESCE(other, CAST(0 AS REAL)) AS other,
COALESCE(handmade, CAST(0 AS REAL)) AS handmade,
COALESCE(garden, CAST(0 AS REAL)) AS garden,
COALESCE(arts, CAST(0 AS REAL)) AS arts,
COALESCE(pets, CAST(0 AS REAL)) AS pets,
COALESCE(office, CAST(0 AS REAL)) AS office,
COALESCE(tools, CAST(0 AS REAL)) AS tools
FROM mr_tastes
ORDER BY id
),
n_clusters => 8,
n_init => 5
)
), profiles AS (
SELECT
a.cluster_id AS id,
COALESCE(CAST(AVG(COALESCE(t.women, CAST(0 AS REAL)) * COALESCE(t.women, CAST(0 AS REAL))) AS REAL), CAST(0 AS REAL)) AS women,
COALESCE(CAST(AVG(COALESCE(t.toys, CAST(0 AS REAL)) * COALESCE(t.toys, CAST(0 AS REAL))) AS REAL), CAST(0 AS REAL)) AS toys,
COALESCE(CAST(AVG(COALESCE(t.kids, CAST(0 AS REAL)) * COALESCE(t.kids, CAST(0 AS REAL))) AS REAL), CAST(0 AS REAL)) AS kids,
COALESCE(CAST(AVG(COALESCE(t.men, CAST(0 AS REAL)) * COALESCE(t.men, CAST(0 AS REAL))) AS REAL), CAST(0 AS REAL)) AS men,
COALESCE(CAST(AVG(COALESCE(t.home, CAST(0 AS REAL)) * COALESCE(t.home, CAST(0 AS REAL))) AS REAL), CAST(0 AS REAL)) AS home,
COALESCE(CAST(AVG(COALESCE(t.electronics, CAST(0 AS REAL)) * COALESCE(t.electronics, CAST(0 AS REAL))) AS REAL), CAST(0 AS REAL)) AS electronics,
COALESCE(CAST(AVG(COALESCE(t.beauty, CAST(0 AS REAL)) * COALESCE(t.beauty, CAST(0 AS REAL))) AS REAL), CAST(0 AS REAL)) AS beauty,
COALESCE(CAST(AVG(COALESCE(t.vintage, CAST(0 AS REAL)) * COALESCE(t.vintage, CAST(0 AS REAL))) AS REAL), CAST(0 AS REAL)) AS vintage,
COALESCE(CAST(AVG(COALESCE(t.books, CAST(0 AS REAL)) * COALESCE(t.books, CAST(0 AS REAL))) AS REAL), CAST(0 AS REAL)) AS books,
COALESCE(CAST(AVG(COALESCE(t.sports, CAST(0 AS REAL)) * COALESCE(t.sports, CAST(0 AS REAL))) AS REAL), CAST(0 AS REAL)) AS sports,
COALESCE(CAST(AVG(COALESCE(t.other, CAST(0 AS REAL)) * COALESCE(t.other, CAST(0 AS REAL))) AS REAL), CAST(0 AS REAL)) AS other,
COALESCE(CAST(AVG(COALESCE(t.handmade, CAST(0 AS REAL)) * COALESCE(t.handmade, CAST(0 AS REAL))) AS REAL), CAST(0 AS REAL)) AS handmade,
COALESCE(CAST(AVG(COALESCE(t.garden, CAST(0 AS REAL)) * COALESCE(t.garden, CAST(0 AS REAL))) AS REAL), CAST(0 AS REAL)) AS garden,
COALESCE(CAST(AVG(COALESCE(t.arts, CAST(0 AS REAL)) * COALESCE(t.arts, CAST(0 AS REAL))) AS REAL), CAST(0 AS REAL)) AS arts,
COALESCE(CAST(AVG(COALESCE(t.pets, CAST(0 AS REAL)) * COALESCE(t.pets, CAST(0 AS REAL))) AS REAL), CAST(0 AS REAL)) AS pets,
COALESCE(CAST(AVG(COALESCE(t.office, CAST(0 AS REAL)) * COALESCE(t.office, CAST(0 AS REAL))) AS REAL), CAST(0 AS REAL)) AS office,
COALESCE(CAST(AVG(COALESCE(t.tools, CAST(0 AS REAL)) * COALESCE(t.tools, CAST(0 AS REAL))) AS REAL), CAST(0 AS REAL)) AS tools
FROM assignments a
JOIN mr_tastes t ON t.id = a.id
GROUP BY a.cluster_id
)
SELECT
ranked.id AS cluster_id,
ranked.rank,
CASE ranked.position
WHEN 0 THEN 'Women'
WHEN 1 THEN 'Toys & Collectibles'
WHEN 2 THEN 'Kids'
WHEN 3 THEN 'Men'
WHEN 4 THEN 'Home'
WHEN 5 THEN 'Electronics'
WHEN 6 THEN 'Beauty'
WHEN 7 THEN 'Vintage & collectibles'
WHEN 8 THEN 'Books'
WHEN 9 THEN 'Sports & outdoors'
WHEN 10 THEN 'Other'
WHEN 11 THEN 'Handmade'
WHEN 12 THEN 'Garden & Outdoor'
WHEN 13 THEN 'Arts & Crafts'
WHEN 14 THEN 'Pet Supplies'
WHEN 15 THEN 'Office'
WHEN 16 THEN 'Tools'
END AS category,
ranked.value AS mean_view_share
FROM cuvs_top_k(
input => (
SELECT id, women, toys, kids, men, home, electronics, beauty, vintage,
books, sports, other, handmade, garden, arts, pets, office, tools
FROM profiles
ORDER BY id
),
k => 3,
select => 'max'
) ranked
ORDER BY cluster_id, rank;
Limits
- Validation resolves named tables or views and reads schemas only; it does not execute relation scans or GPU work.
- Execution relation arguments require parenthesized subqueries; dry-run validation accepts registered named relations only.
- k must not exceed the vector dimension. Empty input returns an empty result with the stable schema.
- Equal selected values are ordered by position; when ties straddle rank k, the selected positions among them are unspecified.
To dry-run validate relation metadata, column types, and options without execution, see gpu_validate_call.