Skip to main content

KMeans

UDTF: cuvs_kmeans

Official cuVS reference: C API

K-means clustering assignments for dense vector rows.

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 id, cluster_id
FROM cuvs_kmeans(
input => (SELECT id, d0, d1 FROM input_vectors),
n_clusters => 8
)
ORDER BY row_ordinal;

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.

RoleRequiredValidation referenceDescription
inputyestableDense-vector rows consumed by this operation.

Vector element types​

Element typeValid metrics
Float32l2_expanded, l2_sqrt_expanded
Float64l2_expanded, l2_sqrt_expanded

Arguments and options​

Scalar SQL arguments​

ArgumentTypeRequiredDescription
n_clustersintegeryesNumber of clusters fitted for this one statement.
metricenum ("l2_expanded", "l2_sqrt_expanded")noL2 distance form used while fitting and assigning the current input relation.
max_iterintegernoMaximum fitting iterations for this invocation.
tolnumbernoNon-negative convergence tolerance used by the fitted model.
n_initintegernoNumber of initialization attempts performed within this call.
initenum ("kmeans++", "random")noCentroid initialization strategy for this fitted model.

SQL value argument schemas​

ArgumentRequiredLiteral shapeDefaultConstraintsDescription
initnostring"kmeans++"one of "kmeans++", "random"Centroid initialization strategy for this fitted model.
max_iternointeger100minimum 1; maximum 2147483647Maximum fitting iterations for this invocation.
metricnostring"l2_expanded"one of "l2_expanded", "l2_sqrt_expanded"L2 distance form used while fitting and assigning the current input relation. Supported element/metric combinations: Float32: l2_expanded, l2_sqrt_expanded; Float64: l2_expanded, l2_sqrt_expanded.
n_clustersyesintegerNo defaultminimum 1; maximum 2147483647Number of clusters fitted for this one statement.
n_initnointeger1minimum 1; maximum 2147483647Number of initialization attempts performed within this call.
tolnonumber0.0001minimum 0Non-negative convergence tolerance used by the fitted model.

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, Float64) 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​

ColumnTypeNullableDescription
row_ordinalUInt64noZero-based ordinal of the evaluated input row; it disambiguates duplicate IDs. Follows the input's top-level ORDER BY on output columns; unspecified without one.
idsame_as_input.idnoLogical ID copied from the input relation.
cluster_idInt32noQuery-local numeric assignment label, not a stable business or topic identifier.

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​

This example uses the Amazon Reviews demo dataset.

Which products have a changed mix of low-rating reviews?​

A support analyst wants to inspect products whose complaints changed between 2021 and 2022. The query selects 99,539 sampled reviews rated at most two stars of headphones and earbuds, groups their BGE-M3 vectors into 24 clusters, and counts each product's reviews in those clusters by year. Accessories are excluded before fitting.

A windowed sum supplies each product's annual denominator without fitting a second model. The result keeps products with at least 30 sampled low-rating reviews in each year and a cluster with at least ten reviews in 2022 whose share rose by more than ten percentage points. There are 155 products with enough sampled reviews in both years. The result joins back to the catalog and an original review.

WITH assignments AS (
SELECT id, cluster_id
FROM cuvs_kmeans(
input => (SELECT v.id, v.vector
FROM semantic_reviews_vectors v
JOIN semantic_reviews_text r ON r.id = v.id
WHERE r.category_l3 = 'Headphones & Earbuds'
ORDER BY v.id),
n_clusters => 24, n_init => 5, max_iter => 100, tol => 0.0001)
), issues AS (
SELECT r.product_id, a.cluster_id,
SUM(CASE WHEN EXTRACT(YEAR FROM r.review_time) = 2021 THEN 1 ELSE 0 END) AS n2021,
SUM(CASE WHEN EXTRACT(YEAR FROM r.review_time) = 2022 THEN 1 ELSE 0 END) AS n2022,
MIN(CASE WHEN EXTRACT(YEAR FROM r.review_time) = 2022 THEN r.id END) AS example_id
FROM assignments a JOIN semantic_reviews_text r ON r.id = a.id
GROUP BY r.product_id, a.cluster_id
), totals AS (
SELECT *, SUM(n2021) OVER (PARTITION BY product_id) AS total2021,
SUM(n2022) OVER (PARTITION BY product_id) AS total2022
FROM issues
)
SELECT p.product_id, p.title, t.cluster_id,
t.n2021, t.total2021, t.n2022, t.total2022,
100.0 * t.n2021 / t.total2021 AS share2021_pct,
100.0 * t.n2022 / t.total2022 AS share2022_pct,
100.0 * t.n2022 / t.total2022 - 100.0 * t.n2021 / t.total2021 AS change_pp,
t.example_id, r.embedding_text AS example_review
FROM totals t
JOIN products p ON p.product_id = t.product_id
JOIN semantic_reviews_text r ON r.id = t.example_id
WHERE t.total2021 >= 30 AND t.total2022 >= 30 AND t.n2022 >= 10
AND 1.0 * t.n2022 / t.total2022 - 1.0 * t.n2021 / t.total2021 > 0.10
ORDER BY change_pp DESC, p.product_id, t.cluster_id
LIMIT 10;

The first three rows captured on 2026-09-28 were:

Product (shortened)Cluster2021 cluster / product total2022 cluster / product totalChange
Altec Lansing NanoPods, royal blue71 / 42 (2.38%)10 / 33 (30.30%)+27.92 pp
Jabra Talk 65104 / 73 (5.48%)14 / 58 (24.14%)+18.66 pp
Sony WH-CH510, blue86 / 67 (8.96%)14 / 55 (25.45%)+16.50 pp

The NanoPods result links to review 14707192, which says “Second set will not charge.” A support analyst now has a product, a measured change within the sampled low-rating reviews, and an original report to investigate. The saved rows include the full text and product IDs for each result.

The query took a median 0.752 seconds over three runs. This capture used mixed execution: the vector input and KMeans ran on the GPU, with part of the subsequent relational work in DataFusion. See the capture conditions.

example_review is the 2022 review with the smallest ID for that product and cluster. It provides an audit link; it is not selected as a cluster's most representative review. Several reviews should be read before assigning a topic name.

The denominator contains sampled low-rating reviews. A ten-point increase in a cluster's share does not imply a ten-point increase in defects or in all customers' complaints. The model assigns one cluster per review even when the text discusses multiple issues. Cluster IDs are local to this fit, and the input ordering does not guarantee identical membership across GPU runs.

For a composition that derives its numerical features directly from raw review text, see KMeans followed by Top-K.

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.
  • Fits a model and returns assignments within one statement; it does not return a reusable model, centroids, or inertia.
  • The evaluated input must be non-empty and n_clusters must not exceed its row count.
  • cluster_id values are query-local labels. Do not attach permanent business meaning to their numeric values.

To dry-run validate relation metadata, column types, and options without execution, see gpu_validate_call.