From a9e270d63e3ef1da41869dc883c697d22a4f859f Mon Sep 17 00:00:00 2001 From: Connor Hindley Date: Mon, 22 Jun 2026 19:08:46 -0500 Subject: [PATCH] feat(vectorize): add Vectorize (V2) binding Adds the Cloudflare Vectorize binding. The bulk is generated from a curated `types/vectorize.d.ts` via ts-gen (same approach as the `email` binding), with a thin hand-written layer for the parts ts-gen can't infer: the `EnvBinding` impl, `env.vectorize(binding)`, and some `f32`/serde conveniences. The curated types are a V2-only subset of upstream. Filters and metadata serialize as plain objects rather than JS `Map`s, which Vectorize silently ignores. Tests are a `/vectorize` route that hits the binding end to end. That's compile coverage on wasm32 only; there's no vitest spec because Vectorize has no local Miniflare emulator. See examples/vectorize for a semantic-search example using Workers AI embeddings. Regenerating the bindings needs the ts-gen string-enum codegen fixes (pending upstream, plus a submodule bump). The committed output already includes them, so the crate compiles against the pinned wasm-bindgen as-is. --- Cargo.lock | 9 + chompfile.toml | 11 +- examples/vectorize/.gitignore | 3 + examples/vectorize/Cargo.toml | 15 + examples/vectorize/README.md | 61 ++++ examples/vectorize/src/lib.rs | 179 +++++++++++ examples/vectorize/wrangler.toml | 19 ++ test/src/lib.rs | 1 + test/src/router.rs | 5 +- test/src/vectorize.rs | 94 ++++++ test/wrangler.toml | 12 + types/vectorize.d.ts | 205 ++++++++++++ worker/src/bindings/mod.rs | 1 + worker/src/bindings/vectorize.rs | 516 +++++++++++++++++++++++++++++++ worker/src/env.rs | 7 + worker/src/lib.rs | 2 + worker/src/vectorize.rs | 149 +++++++++ 17 files changed, 1285 insertions(+), 4 deletions(-) create mode 100644 examples/vectorize/.gitignore create mode 100644 examples/vectorize/Cargo.toml create mode 100644 examples/vectorize/README.md create mode 100644 examples/vectorize/src/lib.rs create mode 100644 examples/vectorize/wrangler.toml create mode 100644 test/src/vectorize.rs create mode 100644 types/vectorize.d.ts create mode 100644 worker/src/bindings/vectorize.rs create mode 100644 worker/src/vectorize.rs diff --git a/Cargo.lock b/Cargo.lock index cb8b17987..dcc5fc6f9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3329,6 +3329,15 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" +[[package]] +name = "vectorize-on-workers" +version = "0.1.0" +dependencies = [ + "serde", + "serde_json", + "worker", +] + [[package]] name = "version_check" version = "0.9.5" diff --git a/chompfile.toml b/chompfile.toml index 036964a85..bb23547f4 100644 --- a/chompfile.toml +++ b/chompfile.toml @@ -12,12 +12,19 @@ deps = ['install:ts-gen'] # `worker::bindings::email::email::EmailMessage`). The file `--export` # is required alongside because once any `--export` is specified the # implicit "every input is its own export" default is disabled. -run = '''ts-gen --input types/email.d.ts --output worker/src/bindings/email.rs \ +# +# Vectorize needs no `--export`/`--external`: every declaration is global +# (no `cloudflare:` module) and nothing references a project-specific type. +run = ''' +ts-gen --input types/email.d.ts --output worker/src/bindings/email.rs \ --errors-as-error \ --export types/email.d.ts \ --export cloudflare:email \ --external "Env=crate::Env" \ - --external "ExecutionContext=crate::Context"''' + --external "ExecutionContext=crate::Context" +ts-gen --input types/vectorize.d.ts --output worker/src/bindings/vectorize.rs \ + --errors-as-error +''' [[task]] name = 'install:ts-gen' diff --git a/examples/vectorize/.gitignore b/examples/vectorize/.gitignore new file mode 100644 index 000000000..f8382ac81 --- /dev/null +++ b/examples/vectorize/.gitignore @@ -0,0 +1,3 @@ +target +build +dist diff --git a/examples/vectorize/Cargo.toml b/examples/vectorize/Cargo.toml new file mode 100644 index 000000000..e97856a02 --- /dev/null +++ b/examples/vectorize/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "vectorize-on-workers" +version = "0.1.0" +edition = "2021" + +[package.metadata.release] +release = false + +[lib] +crate-type = ["cdylib", "rlib"] + +[dependencies] +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +worker.workspace = true diff --git a/examples/vectorize/README.md b/examples/vectorize/README.md new file mode 100644 index 000000000..c3662bbbe --- /dev/null +++ b/examples/vectorize/README.md @@ -0,0 +1,61 @@ +# Vectorize example + +Semantic search over a small corpus using [Cloudflare Vectorize](https://developers.cloudflare.com/vectorize/) and [Workers AI](https://developers.cloudflare.com/workers-ai/) from a Rust Cloudflare Worker. It's the retrieval half of a [RAG](https://developers.cloudflare.com/workers-ai/guides/tutorials/build-a-retrieval-augmented-generation-ai/) pipeline. + +The Worker embeds text with the `@cf/baai/bge-base-en-v1.5` model (768-dim) through the `worker::Ai` binding and stores it in a Vectorize index, keeping the source text as metadata. To search, it embeds the natural-language query and looks for the nearest vectors. + +## Routes + +| Route | Description | +|---|---| +| `POST /insert` | Embeds the built-in corpus and upserts it, keeping `{ text, category }` as metadata. Returns the async mutation id. | +| `GET /query?q=&category=` | Embeds `q`, returns the `topK` nearest documents with full metadata. The optional `category` filter uses the typed `$in` operator. | +| `GET /describe` | Returns the index dimensions and current vector count. | + +## Setup + +Both Workers AI and Vectorize run remotely (Vectorize has no local emulator), so this example must run with `wrangler dev`. + +1. Create a 768-dimensional index. The dimensions must match the embedding model: + + ```sh + npx wrangler vectorize create demo-index --dimensions=768 --metric=cosine + ``` + + The name must match `index_name` under `[[vectorize]]` in `wrangler.toml`. + +2. Create a metadata index on `category` so the `?category=` filter works. + Vectorize only filters on properties that have a metadata index, and it only + indexes vectors written after the index exists, so create it before + inserting: + + ```sh + npx wrangler vectorize create-metadata-index demo-index --property-name=category --type=string + ``` + +3. Start the Worker against the live index + AI: + + ```sh + npx wrangler dev + ``` + +## Testing + +```sh +# Embed and store the corpus (mutations are async — give it a moment to process) +curl -X POST localhost:8787/insert + +# Semantic search +curl "localhost:8787/query?q=famous+towers+in+Europe" + +# Restrict to a metadata category (typed `$in` operator) +curl "localhost:8787/query?q=tall+places&category=geography" + +# Index stats +curl localhost:8787/describe +``` + +> Vectorize processes mutations (`insert` / `upsert` / `deleteByIds`) asynchronously. +> The response carries a `mutationId` that identifies the changeset rather than a +> completed write, so a `query` run right after an `insert` may not see the new +> vectors yet. diff --git a/examples/vectorize/src/lib.rs b/examples/vectorize/src/lib.rs new file mode 100644 index 000000000..39587c4ae --- /dev/null +++ b/examples/vectorize/src/lib.rs @@ -0,0 +1,179 @@ +//! Example: semantic search with Vectorize + Workers AI embeddings. +//! +//! Embeds text with the `@cf/baai/bge-base-en-v1.5` model (768-dim) through the +//! AI binding, stores the vectors in a Vectorize index with the source text as +//! metadata, and answers natural-language queries by embedding the query and +//! searching the index, the retrieval half of a RAG pipeline. +//! +//! Both Workers AI and Vectorize run remotely (Vectorize has no local +//! emulator): +//! +//! ```sh +//! wrangler vectorize create demo-index --dimensions=768 --metric=cosine +//! # Filtering needs a metadata index on the property, created before inserting: +//! wrangler vectorize create-metadata-index demo-index --property-name=category --type=string +//! wrangler dev +//! curl -X POST localhost:8787/insert +//! curl "localhost:8787/query?q=famous+towers+in+Europe" +//! curl "localhost:8787/query?q=tall+places&category=geography" +//! ``` + +use serde::{Deserialize, Serialize}; +use serde_json::{json, Map}; +use worker::{ + event, + vectorize::{ + VectorizeMetadataRetrievalLevel, VectorizeQueryOptions, VectorizeVector, + VectorizeVectorMetadataFilter, VectorizeVectorMetadataFilterCollectionOp, + }, + Context, Env, Request, Response, Result, RouteContext, Router, +}; + +/// Text-embedding model; produces the 768-dim vectors the index is sized for. +const EMBEDDING_MODEL: &str = "@cf/baai/bge-base-en-v1.5"; + +/// Sample corpus: `(id, category, text)`. +const CORPUS: &[(&str, &str, &str)] = &[ + ( + "eiffel", + "landmark", + "The Eiffel Tower is a wrought-iron lattice tower in Paris, France.", + ), + ( + "colosseum", + "landmark", + "The Colosseum is an ancient amphitheatre in the centre of Rome, Italy.", + ), + ( + "everest", + "geography", + "Mount Everest is Earth's highest mountain above sea level.", + ), + ( + "pacific", + "geography", + "The Pacific Ocean is the largest and deepest of Earth's oceans.", + ), +]; + +#[derive(Serialize)] +struct EmbeddingRequest { + text: Vec, +} + +#[derive(Deserialize)] +struct EmbeddingResponse { + /// One embedding vector per input text. + data: Vec>, +} + +#[derive(Serialize, Deserialize)] +struct DocMetadata { + text: String, + category: String, +} + +#[event(fetch)] +async fn main(req: Request, env: Env, _ctx: Context) -> Result { + Router::new() + .post_async("/insert", insert) + .get_async("/query", query) + .get_async("/describe", describe) + .run(req, env) + .await +} + +/// Embed the corpus with Workers AI and upsert it into the index, keeping the +/// source text and category as metadata. Mutations are async, so the returned +/// id identifies the changeset rather than a completed write. +async fn insert(_req: Request, ctx: RouteContext<()>) -> Result { + let ai = ctx.env.ai("AI")?; + let index = ctx.env.vectorize("VECTORIZE")?; + + let text = CORPUS.iter().map(|&(_, _, t)| t.to_string()).collect(); + let embeddings: EmbeddingResponse = ai.run(EMBEDDING_MODEL, EmbeddingRequest { text }).await?; + + let mut vectors = Vec::with_capacity(CORPUS.len()); + for (&(id, category, text), embedding) in CORPUS.iter().zip(&embeddings.data) { + let vector = VectorizeVector::from_f32(id, embedding); + vector.set_metadata_from(&DocMetadata { + text: text.to_string(), + category: category.to_string(), + })?; + vectors.push(vector); + } + + let mutation = index.upsert(&vectors).await?; + Response::ok(format!("queued mutation {}", mutation.mutation_id())) +} + +/// Embed the query text and return the closest documents. +/// +/// `GET /query?q=&category=`. The optional `category` filter is +/// built with the typed `$in` operator. +async fn query(req: Request, ctx: RouteContext<()>) -> Result { + let ai = ctx.env.ai("AI")?; + let index = ctx.env.vectorize("VECTORIZE")?; + + let url = req.url()?; + let question = url + .query_pairs() + .find(|(k, _)| k == "q") + .map(|(_, v)| v.into_owned()) + .unwrap_or_else(|| "famous towers in Europe".to_string()); + + let embeddings: EmbeddingResponse = ai + .run( + EMBEDDING_MODEL, + EmbeddingRequest { + text: vec![question.clone()], + }, + ) + .await?; + let query_vector = embeddings.data.into_iter().next().unwrap_or_default(); + + let mut options = VectorizeQueryOptions::builder() + .top_k(3.0) + .return_metadata_with_vectorize_metadata_retrieval_level( + VectorizeMetadataRetrievalLevel::All, + ); + + // Optional `?category=landmark,geography` + if let Some((_, categories)) = url.query_pairs().find(|(k, _)| k == "category") { + let mut clause = Map::new(); + clause.insert( + VectorizeVectorMetadataFilterCollectionOp::In + .as_str() + .to_string(), + json!(categories.split(',').collect::>()), + ); + let filter = VectorizeVectorMetadataFilter::from_serde(&json!({ "category": clause }))?; + options = options.filter(&filter); + } + + let results = index.query_f32(&query_vector, &options.build()).await?; + + let hits: Vec<_> = results + .matches() + .into_iter() + .map(|hit| { + let metadata: Option = hit.metadata_into().ok().flatten(); + json!({ + "id": hit.id(), + "score": hit.score(), + "text": metadata.map(|m| m.text), + }) + }) + .collect(); + + Response::from_json(&json!({ "query": question, "matches": hits })) +} + +/// Report the index dimensions and current vector count. +async fn describe(_req: Request, ctx: RouteContext<()>) -> Result { + let info = ctx.env.vectorize("VECTORIZE")?.describe().await?; + Response::from_json(&json!({ + "dimensions": info.dimensions(), + "vectorCount": info.vector_count(), + })) +} diff --git a/examples/vectorize/wrangler.toml b/examples/vectorize/wrangler.toml new file mode 100644 index 000000000..57d5247bc --- /dev/null +++ b/examples/vectorize/wrangler.toml @@ -0,0 +1,19 @@ +name = "vectorize-worker" +main = "build/worker/shim.mjs" +compatibility_date = "2026-01-01" + +[build] +command = "cargo install worker-build@^0.8 && worker-build --release" + +# Embeddings come from Workers AI (the `@cf/baai/bge-base-en-v1.5` model). +[ai] +binding = "AI" +remote = true + +# The index must be 768-dimensional to match the embedding model. Create it first: +# wrangler vectorize create demo-index --dimensions=768 --metric=cosine +# wrangler vectorize create-metadata-index demo-index --property-name=category --type=string +[[vectorize]] +binding = "VECTORIZE" +index_name = "demo-index" +remote = true diff --git a/test/src/lib.rs b/test/src/lib.rs index 37f718fd0..79b686a08 100644 --- a/test/src/lib.rs +++ b/test/src/lib.rs @@ -39,6 +39,7 @@ mod socket; mod sql_counter; mod sql_iterator; mod user; +mod vectorize; mod ws; #[derive(Deserialize, Serialize)] diff --git a/test/src/router.rs b/test/src/router.rs index b5a4b56b2..1962c9e24 100644 --- a/test/src/router.rs +++ b/test/src/router.rs @@ -2,8 +2,8 @@ use crate::signal; use crate::{ alarm, analytics_engine, assets, auto_response, cache, container, counter, d1, durable, fetch, form, js_snippets, kv, put_raw, queue, r2, rate_limit, request, secret_store, send_email, - service, socket, sql_counter, sql_iterator, user, ws, SomeSharedData, GLOBAL_SECOND_START, - GLOBAL_STATE, + service, socket, sql_counter, sql_iterator, user, vectorize, ws, SomeSharedData, + GLOBAL_SECOND_START, GLOBAL_STATE, }; #[cfg(feature = "http")] use std::convert::TryInto; @@ -116,6 +116,7 @@ macro_rules! add_routes ( ($obj:ident) => { add_route!($obj, get, sync, "/request", request::handle_a_request); add_route!($obj, get, "/analytics-engine", analytics_engine::handle_analytics_event); + add_route!($obj, get, "/vectorize", vectorize::handle_vectorize); add_route!($obj, get, "/async-request", request::handle_async_request); add_route!($obj, get, format_route!("/asset/{}", "name"), assets::handle_asset); add_route!($obj, get, "/websocket", ws::handle_websocket); diff --git a/test/src/vectorize.rs b/test/src/vectorize.rs new file mode 100644 index 000000000..dd9c179df --- /dev/null +++ b/test/src/vectorize.rs @@ -0,0 +1,94 @@ +use crate::SomeSharedData; +use serde_json::json; +use worker::{ + vectorize::{ + VectorizeMetadataRetrievalLevel, VectorizeQueryOptions, VectorizeVector, + VectorizeVectorMetadataFilter, + }, + Env, Request, Response, Result, +}; + +/// Dimensionality of the bound `test-index`. Must match the index created with +/// `wrangler vectorize create test-index --dimensions=768`. +const INDEX_DIMENSIONS: usize = 768; + +/// A deterministic 768-dim vector, so the handler runs against a real index +/// without needing an embedding model. Real workers embed text via Workers AI; +/// see `examples/vectorize`. +fn demo_vector(seed: f32) -> Vec { + (0..INDEX_DIMENSIONS) + .map(|i| ((i as f32 + seed) * 0.001).sin()) + .collect() +} + +// Exercises the full Vectorize binding surface so it stays compiling against +// real worker code (including the `#[worker::send]` async path). Vectorize has +// no local Miniflare emulator, so this isn't wired into a vitest spec. Pointed +// at a real 768-dim index (`remote = true` in wrangler.toml) it returns the JSON +// summary below; otherwise each step reports its error instead of panicking. +#[worker::send] +pub async fn handle_vectorize(_req: Request, env: Env, _data: SomeSharedData) -> Result { + let index = match env.vectorize("VECTORIZE") { + Ok(index) => index, + Err(err) => return Response::error(format!("no Vectorize binding: {err}"), 500), + }; + + // Build vectors from `f32` embeddings with typed metadata. `doc-2` goes + // through the generated `&[f64]` slice builder to cover that path too. + let first = VectorizeVector::from_f32("doc-1", &demo_vector(1.0)); + first.set_metadata_from(&json!({ "genre": "comedy", "year": 2021 }))?; + let second_values: Vec = demo_vector(2.0).into_iter().map(f64::from).collect(); + let second = VectorizeVector::builder_with_slice("doc-2", &second_values) + .namespace("library") + .build(); + + let insert = index.insert(&[first, second]).await; + let upsert = index + .upsert(&[VectorizeVector::from_f32("doc-1", &demo_vector(1.5))]) + .await; + + // Query by vector with the full option surface, including a metadata filter + // and the typed retrieval level (passed by value). + let options = VectorizeQueryOptions::builder() + .top_k(3.0) + .namespace("library") + .return_values(true) + .return_metadata_with_vectorize_metadata_retrieval_level( + VectorizeMetadataRetrievalLevel::All, + ) + .filter(&VectorizeVectorMetadataFilter::from_serde( + &json!({ "genre": { "$in": ["comedy", "drama"] } }), + )?) + .build(); + let matches = index.query_f32(&demo_vector(1.0), &options).await; + + let scored: Vec<_> = matches + .as_ref() + .map(|m| { + m.matches() + .into_iter() + .map(|hit| { + let metadata: Option = hit.metadata_into().ok().flatten(); + json!({ "id": hit.id(), "score": hit.score(), "metadata": metadata }) + }) + .collect() + }) + .unwrap_or_default(); + + let ids = [String::from("doc-1"), String::from("doc-2")]; + let by_id = index.query_by_id_with_options("doc-1", &options).await; + let fetched = index.get_by_ids(&ids).await; + let deleted = index.delete_by_ids(&ids[1..]).await; + let described = index.describe().await; + + Response::from_json(&json!({ + "insert": insert.map(|m| m.mutation_id()).map_err(|e| e.to_string()).ok(), + "upsert": upsert.map(|m| m.mutation_id()).map_err(|e| e.to_string()).ok(), + "matchCount": matches.as_ref().map(|m| m.count()).unwrap_or(0.0), + "matches": scored, + "queryByIdCount": by_id.map(|m| m.count()).unwrap_or(0.0), + "fetched": fetched.map(|arr| arr.length()).unwrap_or(0), + "deleted": deleted.map(|m| m.mutation_id()).map_err(|e| e.to_string()).ok(), + "dimensions": described.map(|info| info.dimensions()).unwrap_or(0.0), + })) +} diff --git a/test/wrangler.toml b/test/wrangler.toml index 654208df2..cc2ba3809 100644 --- a/test/wrangler.toml +++ b/test/wrangler.toml @@ -37,6 +37,18 @@ bindings = [ dataset = "http" binding = "HTTP_ANALYTICS" +# Vectorize has no local Miniflare emulator, so the `/vectorize` route can't be +# exercised in the vitest suite; this binding documents the config and lets the +# worker resolve `env.vectorize("VECTORIZE")`. Point `index_name` at a real index: +# wrangler vectorize create test-index --dimensions=768 --metric=cosine +# The handler filters on `genre`, which needs a metadata index (create it before +# inserting, since only vectors written afterwards are indexed): +# wrangler vectorize create-metadata-index test-index --property-name=genre --type=string +[[vectorize]] +binding = "VECTORIZE" +index_name = "test-index" +remote = true + [[d1_databases]] binding = 'DB' database_name = 'my_db' diff --git a/types/vectorize.d.ts b/types/vectorize.d.ts new file mode 100644 index 000000000..b75b9194d --- /dev/null +++ b/types/vectorize.d.ts @@ -0,0 +1,205 @@ +/* + * Vectorize (V2) types from @cloudflare/workers-types. Valid as of 22/06/2026. + * This file builds worker/src/bindings/vectorize.rs as auto-generated bindings using ts-gen. + * + * NOTE: All hand edits to the @cloudflare/worker-types are marked with an "EDIT:" comment. + * + * EDIT: This is a V2-only subset of upstream's vectorize.d.ts. The deprecated + * beta `VectorizeIndex` class and its beta-only companions + * (`VectorizeIndexDetails`, `VectorizeVectorMutation`, `VectorizeIndexConfig`, + * `VectorizeDistanceMetric`, `VectorizeError`) have been dropped, since the + * current `Vectorize` binding never surfaces them and generating them would + * only emit dead code. + */ + +/** + * Data types supported for holding vector metadata. + */ +type VectorizeVectorMetadataValue = string | number | boolean | string[]; +/** + * Additional information to associate with a vector. + */ +type VectorizeVectorMetadata = + | VectorizeVectorMetadataValue + | Record; + +type VectorFloatArray = Float32Array | Float64Array; + +/** + * Comparison logic/operation to use for metadata filtering. + * + * This list is expected to grow as support for more operations are released. + */ +type VectorizeVectorMetadataFilterOp = + | '$eq' + | '$ne' + | '$lt' + | '$lte' + | '$gt' + | '$gte'; +type VectorizeVectorMetadataFilterCollectionOp = '$in' | '$nin'; + +/** + * Filter criteria for vector metadata used to limit the retrieved query result set. + */ +type VectorizeVectorMetadataFilter = { + [field: string]: + | Exclude + | null + | { + [Op in VectorizeVectorMetadataFilterOp]?: Exclude< + VectorizeVectorMetadataValue, + string[] + > | null; + } + | { + [Op in VectorizeVectorMetadataFilterCollectionOp]?: Exclude< + VectorizeVectorMetadataValue, + string[] + >[]; + }; +}; + +/** + * Metadata return levels for a Vectorize query. + * + * Default to "none". + * + * @property all Full metadata for the vector return set, including all fields (including those un-indexed) without truncation. This is a more expensive retrieval, as it requires additional fetching & reading of un-indexed data. + * @property indexed Return all metadata fields configured for indexing in the vector return set. This level of retrieval is "free" in that no additional overhead is incurred returning this data. However, note that indexed metadata is subject to truncation (especially for larger strings). + * @property none No indexed metadata will be returned. + */ +type VectorizeMetadataRetrievalLevel = "all" | "indexed" | "none"; + +interface VectorizeQueryOptions { + topK?: number; + namespace?: string; + returnValues?: boolean; + returnMetadata?: boolean | VectorizeMetadataRetrievalLevel; + filter?: VectorizeVectorMetadataFilter; +} + +/** + * Metadata about an existing index. + */ +interface VectorizeIndexInfo { + /** The number of records containing vectors within the index. */ + vectorCount: number; + /** Number of dimensions the index has been configured for. */ + dimensions: number; + /** ISO 8601 datetime of the last processed mutation on in the index. All changes before this mutation will be reflected in the index state. */ + processedUpToDatetime: number; + /** UUIDv4 of the last mutation processed by the index. All changes before this mutation will be reflected in the index state. */ + processedUpToMutation: number; +} + +/** + * Represents a single vector value set along with its associated metadata. + */ +interface VectorizeVector { + /** The ID for the vector. This can be user-defined, and must be unique. It should uniquely identify the object, and is best set based on the ID of what the vector represents. */ + id: string; + /** The vector values */ + values: VectorFloatArray | number[]; + /** The namespace this vector belongs to. */ + namespace?: string; + /** Metadata associated with the vector. Includes the values of other fields and potentially additional details. */ + metadata?: Record; +} + +// EDIT: upstream defines this as +// type VectorizeMatch = Pick, "values"> & +// Omit & { score: number }; +// ts-gen can't resolve the Pick/Omit intersection into concrete fields, so it +// collapses the whole type to an opaque `JsValue` (and `VectorizeMatches.matches` +// to `Vec`). Rewritten here as a plain interface with the identical +// runtime shape so ts-gen emits a real struct with typed getters. +/** + * Represents a matched vector for a query along with its score and (if specified) the matching vector information. + */ +interface VectorizeMatch { + /** The ID for the vector. */ + id: string; + /** The vector values, when `returnValues` was requested. */ + values?: VectorFloatArray | number[]; + /** The namespace this vector belongs to. */ + namespace?: string; + /** Metadata associated with the vector, when `returnMetadata` was requested. */ + metadata?: Record; + /** The score or rank for similarity, when returned as a result */ + score: number; +} + +/** + * A set of matching {@link VectorizeMatch} for a particular query. + */ +interface VectorizeMatches { + matches: VectorizeMatch[]; + count: number; +} + +/** + * Result type indicating a mutation on the Vectorize Index. + * Actual mutations are processed async where the `mutationId` is the unique identifier for the operation. + */ +interface VectorizeAsyncMutation { + /** The unique identifier for the async mutation operation containing the changeset. */ + mutationId: string; +} + +/** + * A Vectorize Vector Search Index for querying vectors/embeddings. + * + * Mutations in this version are async, returning a mutation id. + */ +declare abstract class Vectorize { + /** + * Get information about the currently bound index. + * @returns A promise that resolves with information about the current index. + */ + public describe(): Promise; + /** + * Use the provided vector to perform a similarity search across the index. + * @param vector Input vector that will be used to drive the similarity search. + * @param options Configuration options to massage the returned data. + * @returns A promise that resolves with matched and scored vectors. + */ + public query( + vector: VectorFloatArray | number[], + options?: VectorizeQueryOptions + ): Promise; + /** + * Use the provided vector-id to perform a similarity search across the index. + * @param vectorId Id for a vector in the index against which the index should be queried. + * @param options Configuration options to massage the returned data. + * @returns A promise that resolves with matched and scored vectors. + */ + public queryById( + vectorId: string, + options?: VectorizeQueryOptions + ): Promise; + /** + * Insert a list of vectors into the index dataset. If a provided id exists, an error will be thrown. + * @param vectors List of vectors that will be inserted. + * @returns A promise that resolves with a unique identifier of a mutation containing the insert changeset. + */ + public insert(vectors: VectorizeVector[]): Promise; + /** + * Upsert a list of vectors into the index dataset. If a provided id exists, it will be replaced with the new values. + * @param vectors List of vectors that will be upserted. + * @returns A promise that resolves with a unique identifier of a mutation containing the upsert changeset. + */ + public upsert(vectors: VectorizeVector[]): Promise; + /** + * Delete a list of vectors with a matching id. + * @param ids List of vector ids that should be deleted. + * @returns A promise that resolves with a unique identifier of a mutation containing the delete changeset. + */ + public deleteByIds(ids: string[]): Promise; + /** + * Get a list of vectors with a matching id. + * @param ids List of vector ids that should be returned. + * @returns A promise that resolves with the raw unscored vectors matching the id set. + */ + public getByIds(ids: string[]): Promise; +} diff --git a/worker/src/bindings/mod.rs b/worker/src/bindings/mod.rs index aa5f45d47..c72a60138 100644 --- a/worker/src/bindings/mod.rs +++ b/worker/src/bindings/mod.rs @@ -1 +1,2 @@ pub mod email; +pub mod vectorize; diff --git a/worker/src/bindings/vectorize.rs b/worker/src/bindings/vectorize.rs new file mode 100644 index 000000000..a28c17c2f --- /dev/null +++ b/worker/src/bindings/vectorize.rs @@ -0,0 +1,516 @@ +// Generated by ts-gen. Do not edit. + +#[allow(unused_imports)] +use js_sys::*; +#[allow(unused_imports)] +use wasm_bindgen::prelude::*; +#[allow(dead_code)] +pub type VectorizeVectorMetadataValue = JsValue; +#[allow(dead_code)] +pub type VectorizeVectorMetadata = JsValue; +#[allow(dead_code)] +pub type VectorFloatArray = JsValue; +#[wasm_bindgen] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum VectorizeVectorMetadataFilterOp { + Eq = "$eq", + Ne = "$ne", + Lt = "$lt", + Lte = "$lte", + Gt = "$gt", + Gte = "$gte", +} +#[wasm_bindgen] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum VectorizeVectorMetadataFilterCollectionOp { + In = "$in", + Nin = "$nin", +} +#[wasm_bindgen] +extern "C" { + # [wasm_bindgen (extends = Object)] + #[derive(Debug, Clone, PartialEq, Eq)] + pub type VectorizeVectorMetadataFilter; +} +#[wasm_bindgen] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum VectorizeMetadataRetrievalLevel { + All = "all", + Indexed = "indexed", + None = "none", +} +#[wasm_bindgen] +extern "C" { + # [wasm_bindgen (extends = Object)] + #[derive(Debug, Clone, PartialEq, Eq)] + pub type VectorizeQueryOptions; + #[wasm_bindgen(method, getter, js_name = "topK")] + pub fn top_k(this: &VectorizeQueryOptions) -> Option; + #[wasm_bindgen(method, setter, js_name = "topK")] + pub fn set_top_k(this: &VectorizeQueryOptions, val: f64); + #[wasm_bindgen(method, getter)] + pub fn namespace(this: &VectorizeQueryOptions) -> Option; + #[wasm_bindgen(method, setter)] + pub fn set_namespace(this: &VectorizeQueryOptions, val: &str); + #[wasm_bindgen(method, getter, js_name = "returnValues")] + pub fn return_values(this: &VectorizeQueryOptions) -> Option; + #[wasm_bindgen(method, setter, js_name = "returnValues")] + pub fn set_return_values(this: &VectorizeQueryOptions, val: bool); + #[wasm_bindgen(method, getter, js_name = "returnMetadata")] + pub fn return_metadata(this: &VectorizeQueryOptions) -> Option; + #[wasm_bindgen(method, setter, js_name = "returnMetadata")] + pub fn set_return_metadata(this: &VectorizeQueryOptions, val: bool); + #[wasm_bindgen(method, setter, js_name = "returnMetadata")] + pub fn set_return_metadata_with_vectorize_metadata_retrieval_level( + this: &VectorizeQueryOptions, + val: VectorizeMetadataRetrievalLevel, + ); + #[wasm_bindgen(method, getter)] + pub fn filter(this: &VectorizeQueryOptions) -> Option; + #[wasm_bindgen(method, setter)] + pub fn set_filter(this: &VectorizeQueryOptions, val: &VectorizeVectorMetadataFilter); +} +impl VectorizeQueryOptions { + pub fn new() -> VectorizeQueryOptions { + Self::builder().build() + } + pub fn builder() -> VectorizeQueryOptionsBuilder { + VectorizeQueryOptionsBuilder { + inner: JsCast::unchecked_into(js_sys::Object::new()), + } + } +} +pub struct VectorizeQueryOptionsBuilder { + inner: VectorizeQueryOptions, +} +impl VectorizeQueryOptionsBuilder { + pub fn top_k(self, val: f64) -> Self { + self.inner.set_top_k(val); + self + } + pub fn namespace(self, val: &str) -> Self { + self.inner.set_namespace(val); + self + } + pub fn return_values(self, val: bool) -> Self { + self.inner.set_return_values(val); + self + } + pub fn return_metadata(self, val: bool) -> Self { + self.inner.set_return_metadata(val); + self + } + pub fn return_metadata_with_vectorize_metadata_retrieval_level( + self, + val: VectorizeMetadataRetrievalLevel, + ) -> Self { + self.inner + .set_return_metadata_with_vectorize_metadata_retrieval_level(val); + self + } + pub fn filter(self, val: &VectorizeVectorMetadataFilter) -> Self { + self.inner.set_filter(val); + self + } + pub fn build(self) -> VectorizeQueryOptions { + self.inner + } +} +#[wasm_bindgen] +extern "C" { + # [wasm_bindgen (extends = Object)] + #[derive(Debug, Clone, PartialEq, Eq)] + pub type VectorizeIndexInfo; + #[doc = " The number of records containing vectors within the index."] + #[wasm_bindgen(method, getter, js_name = "vectorCount")] + pub fn vector_count(this: &VectorizeIndexInfo) -> f64; + #[wasm_bindgen(method, setter, js_name = "vectorCount")] + pub fn set_vector_count(this: &VectorizeIndexInfo, val: f64); + #[doc = " Number of dimensions the index has been configured for."] + #[wasm_bindgen(method, getter)] + pub fn dimensions(this: &VectorizeIndexInfo) -> f64; + #[wasm_bindgen(method, setter)] + pub fn set_dimensions(this: &VectorizeIndexInfo, val: f64); + #[doc = " ISO 8601 datetime of the last processed mutation on in the index. All changes before this mutation will be reflected in the index state."] + #[wasm_bindgen(method, getter, js_name = "processedUpToDatetime")] + pub fn processed_up_to_datetime(this: &VectorizeIndexInfo) -> f64; + #[wasm_bindgen(method, setter, js_name = "processedUpToDatetime")] + pub fn set_processed_up_to_datetime(this: &VectorizeIndexInfo, val: f64); + #[doc = " UUIDv4 of the last mutation processed by the index. All changes before this mutation will be reflected in the index state."] + #[wasm_bindgen(method, getter, js_name = "processedUpToMutation")] + pub fn processed_up_to_mutation(this: &VectorizeIndexInfo) -> f64; + #[wasm_bindgen(method, setter, js_name = "processedUpToMutation")] + pub fn set_processed_up_to_mutation(this: &VectorizeIndexInfo, val: f64); +} +impl VectorizeIndexInfo { + #[doc = " * `vector_count` - The number of records containing vectors within the index."] + #[doc = " * `dimensions` - Number of dimensions the index has been configured for."] + #[doc = " * `processed_up_to_datetime` - ISO 8601 datetime of the last processed mutation on in the index. All changes before this mutation will be reflected in the index state."] + #[doc = " * `processed_up_to_mutation` - UUIDv4 of the last mutation processed by the index. All changes before this mutation will be reflected in the index state."] + pub fn new( + vector_count: f64, + dimensions: f64, + processed_up_to_datetime: f64, + processed_up_to_mutation: f64, + ) -> VectorizeIndexInfo { + let inner: VectorizeIndexInfo = JsCast::unchecked_into(js_sys::Object::new()); + inner.set_vector_count(vector_count); + inner.set_dimensions(dimensions); + inner.set_processed_up_to_datetime(processed_up_to_datetime); + inner.set_processed_up_to_mutation(processed_up_to_mutation); + inner + } +} +#[wasm_bindgen] +extern "C" { + # [wasm_bindgen (extends = Object)] + #[derive(Debug, Clone, PartialEq, Eq)] + pub type VectorizeVector; + #[doc = " The ID for the vector. This can be user-defined, and must be unique. It should uniquely identify the object, and is best set based on the ID of what the vector represents."] + #[wasm_bindgen(method, getter)] + pub fn id(this: &VectorizeVector) -> String; + #[wasm_bindgen(method, setter)] + pub fn set_id(this: &VectorizeVector, val: &str); + #[doc = " The vector values"] + #[wasm_bindgen(method, getter)] + pub fn values(this: &VectorizeVector) -> ValuesKind; + #[wasm_bindgen(method, setter)] + pub fn set_values(this: &VectorizeVector, val: &Float32Array); + #[wasm_bindgen(method, setter, js_name = "values")] + pub fn set_values_with_float64_array(this: &VectorizeVector, val: &Float64Array); + #[wasm_bindgen(method, setter, js_name = "values")] + pub fn set_values_with_slice(this: &VectorizeVector, val: &[f64]); + #[doc = " The namespace this vector belongs to."] + #[wasm_bindgen(method, getter)] + pub fn namespace(this: &VectorizeVector) -> Option; + #[wasm_bindgen(method, setter)] + pub fn set_namespace(this: &VectorizeVector, val: &str); + #[doc = " Metadata associated with the vector. Includes the values of other fields and potentially additional details."] + #[wasm_bindgen(method, getter)] + pub fn metadata(this: &VectorizeVector) -> Option; + #[wasm_bindgen(method, setter)] + pub fn set_metadata(this: &VectorizeVector, val: &Object); +} +impl VectorizeVector { + #[doc = " * `id` - The ID for the vector. This can be user-defined, and must be unique. It should uniquely identify the object, and is best set based on the ID of what the vector represents."] + #[doc = " * `values` - The vector values"] + pub fn new(id: &str, values: &Float32Array) -> VectorizeVector { + Self::builder(id, values).build() + } + #[doc = " * `id` - The ID for the vector. This can be user-defined, and must be unique. It should uniquely identify the object, and is best set based on the ID of what the vector represents."] + #[doc = " * `values` - The vector values"] + pub fn new_with_float64_array(id: &str, values: &Float64Array) -> VectorizeVector { + Self::builder_with_float64_array(id, values).build() + } + #[doc = " * `id` - The ID for the vector. This can be user-defined, and must be unique. It should uniquely identify the object, and is best set based on the ID of what the vector represents."] + #[doc = " * `values` - The vector values"] + pub fn new_with_slice(id: &str, values: &[f64]) -> VectorizeVector { + Self::builder_with_slice(id, values).build() + } + #[doc = " * `id` - The ID for the vector. This can be user-defined, and must be unique. It should uniquely identify the object, and is best set based on the ID of what the vector represents."] + #[doc = " * `values` - The vector values"] + pub fn builder(id: &str, values: &Float32Array) -> VectorizeVectorBuilder { + let inner: VectorizeVector = JsCast::unchecked_into(js_sys::Object::new()); + inner.set_id(id); + inner.set_values(values); + VectorizeVectorBuilder { inner } + } + #[doc = " * `id` - The ID for the vector. This can be user-defined, and must be unique. It should uniquely identify the object, and is best set based on the ID of what the vector represents."] + #[doc = " * `values` - The vector values"] + pub fn builder_with_float64_array(id: &str, values: &Float64Array) -> VectorizeVectorBuilder { + let inner: VectorizeVector = JsCast::unchecked_into(js_sys::Object::new()); + inner.set_id(id); + inner.set_values_with_float64_array(values); + VectorizeVectorBuilder { inner } + } + #[doc = " * `id` - The ID for the vector. This can be user-defined, and must be unique. It should uniquely identify the object, and is best set based on the ID of what the vector represents."] + #[doc = " * `values` - The vector values"] + pub fn builder_with_slice(id: &str, values: &[f64]) -> VectorizeVectorBuilder { + let inner: VectorizeVector = JsCast::unchecked_into(js_sys::Object::new()); + inner.set_id(id); + inner.set_values_with_slice(values); + VectorizeVectorBuilder { inner } + } +} +pub struct VectorizeVectorBuilder { + inner: VectorizeVector, +} +impl VectorizeVectorBuilder { + pub fn namespace(self, val: &str) -> Self { + self.inner.set_namespace(val); + self + } + pub fn metadata(self, val: &Object) -> Self { + self.inner.set_metadata(val); + self + } + pub fn build(self) -> VectorizeVector { + self.inner + } +} +#[wasm_bindgen] +extern "C" { + # [wasm_bindgen (extends = Object)] + #[derive(Debug, Clone, PartialEq, Eq)] + pub type VectorizeMatch; + #[doc = " The ID for the vector."] + #[wasm_bindgen(method, getter)] + pub fn id(this: &VectorizeMatch) -> String; + #[wasm_bindgen(method, setter)] + pub fn set_id(this: &VectorizeMatch, val: &str); + #[doc = " The vector values, when `returnValues` was requested."] + #[wasm_bindgen(method, getter)] + pub fn values(this: &VectorizeMatch) -> Option; + #[wasm_bindgen(method, setter)] + pub fn set_values(this: &VectorizeMatch, val: &Float32Array); + #[wasm_bindgen(method, setter, js_name = "values")] + pub fn set_values_with_float64_array(this: &VectorizeMatch, val: &Float64Array); + #[wasm_bindgen(method, setter, js_name = "values")] + pub fn set_values_with_slice(this: &VectorizeMatch, val: &[f64]); + #[doc = " The namespace this vector belongs to."] + #[wasm_bindgen(method, getter)] + pub fn namespace(this: &VectorizeMatch) -> Option; + #[wasm_bindgen(method, setter)] + pub fn set_namespace(this: &VectorizeMatch, val: &str); + #[doc = " Metadata associated with the vector, when `returnMetadata` was requested."] + #[wasm_bindgen(method, getter)] + pub fn metadata(this: &VectorizeMatch) -> Option; + #[wasm_bindgen(method, setter)] + pub fn set_metadata(this: &VectorizeMatch, val: &Object); + #[doc = " The score or rank for similarity, when returned as a result"] + #[wasm_bindgen(method, getter)] + pub fn score(this: &VectorizeMatch) -> f64; + #[wasm_bindgen(method, setter)] + pub fn set_score(this: &VectorizeMatch, val: f64); +} +impl VectorizeMatch { + #[doc = " * `id` - The ID for the vector."] + #[doc = " * `score` - The score or rank for similarity, when returned as a result"] + pub fn new(id: &str, score: f64) -> VectorizeMatch { + Self::builder(id, score).build() + } + #[doc = " * `id` - The ID for the vector."] + #[doc = " * `score` - The score or rank for similarity, when returned as a result"] + pub fn builder(id: &str, score: f64) -> VectorizeMatchBuilder { + let inner: VectorizeMatch = JsCast::unchecked_into(js_sys::Object::new()); + inner.set_id(id); + inner.set_score(score); + VectorizeMatchBuilder { inner } + } +} +pub struct VectorizeMatchBuilder { + inner: VectorizeMatch, +} +impl VectorizeMatchBuilder { + pub fn values(self, val: &Float32Array) -> Self { + self.inner.set_values(val); + self + } + pub fn values_with_float64_array(self, val: &Float64Array) -> Self { + self.inner.set_values_with_float64_array(val); + self + } + pub fn values_with_slice(self, val: &[f64]) -> Self { + self.inner.set_values_with_slice(val); + self + } + pub fn namespace(self, val: &str) -> Self { + self.inner.set_namespace(val); + self + } + pub fn metadata(self, val: &Object) -> Self { + self.inner.set_metadata(val); + self + } + pub fn build(self) -> VectorizeMatch { + self.inner + } +} +#[wasm_bindgen] +extern "C" { + # [wasm_bindgen (extends = Object)] + #[derive(Debug, Clone, PartialEq, Eq)] + pub type VectorizeMatches; + #[wasm_bindgen(method, getter)] + pub fn matches(this: &VectorizeMatches) -> Vec; + #[wasm_bindgen(method, setter, slice_to_array)] + pub fn set_matches(this: &VectorizeMatches, val: &[VectorizeMatch]); + #[wasm_bindgen(method, getter)] + pub fn count(this: &VectorizeMatches) -> f64; + #[wasm_bindgen(method, setter)] + pub fn set_count(this: &VectorizeMatches, val: f64); +} +impl VectorizeMatches { + pub fn new(matches: &[VectorizeMatch], count: f64) -> VectorizeMatches { + let inner: VectorizeMatches = JsCast::unchecked_into(js_sys::Object::new()); + inner.set_matches(matches); + inner.set_count(count); + inner + } +} +#[wasm_bindgen] +extern "C" { + # [wasm_bindgen (extends = Object)] + #[derive(Debug, Clone, PartialEq, Eq)] + pub type VectorizeAsyncMutation; + #[doc = " The unique identifier for the async mutation operation containing the changeset."] + #[wasm_bindgen(method, getter, js_name = "mutationId")] + pub fn mutation_id(this: &VectorizeAsyncMutation) -> String; + #[wasm_bindgen(method, setter, js_name = "mutationId")] + pub fn set_mutation_id(this: &VectorizeAsyncMutation, val: &str); +} +impl VectorizeAsyncMutation { + #[doc = " * `mutation_id` - The unique identifier for the async mutation operation containing the changeset."] + pub fn new(mutation_id: &str) -> VectorizeAsyncMutation { + let inner: VectorizeAsyncMutation = JsCast::unchecked_into(js_sys::Object::new()); + inner.set_mutation_id(mutation_id); + inner + } +} +#[wasm_bindgen] +extern "C" { + # [wasm_bindgen (extends = Object)] + #[derive(Debug, Clone, PartialEq, Eq)] + pub type Vectorize; + #[doc = " Get information about the currently bound index."] + #[doc = ""] + #[doc = " Returns: A promise that resolves with information about the current index."] + #[wasm_bindgen(method, catch)] + pub async fn describe(this: &Vectorize) -> Result; + #[doc = " Use the provided vector to perform a similarity search across the index."] + #[doc = ""] + #[doc = " * `vector` - Input vector that will be used to drive the similarity search."] + #[doc = " * `options` - Configuration options to massage the returned data."] + #[doc = ""] + #[doc = " Returns: A promise that resolves with matched and scored vectors."] + #[wasm_bindgen(method, catch)] + pub async fn query(this: &Vectorize, vector: &Float32Array) -> Result; + #[doc = " Use the provided vector to perform a similarity search across the index."] + #[doc = ""] + #[doc = " * `vector` - Input vector that will be used to drive the similarity search."] + #[doc = " * `options` - Configuration options to massage the returned data."] + #[doc = ""] + #[doc = " Returns: A promise that resolves with matched and scored vectors."] + #[wasm_bindgen(method, catch, js_name = "query")] + pub async fn query_with_float64_array( + this: &Vectorize, + vector: &Float64Array, + ) -> Result; + #[doc = " Use the provided vector to perform a similarity search across the index."] + #[doc = ""] + #[doc = " * `vector` - Input vector that will be used to drive the similarity search."] + #[doc = " * `options` - Configuration options to massage the returned data."] + #[doc = ""] + #[doc = " Returns: A promise that resolves with matched and scored vectors."] + #[wasm_bindgen(method, catch, js_name = "query")] + pub async fn query_with_slice( + this: &Vectorize, + vector: &[f64], + ) -> Result; + #[doc = " Use the provided vector to perform a similarity search across the index."] + #[doc = ""] + #[doc = " * `vector` - Input vector that will be used to drive the similarity search."] + #[doc = " * `options` - Configuration options to massage the returned data."] + #[doc = ""] + #[doc = " Returns: A promise that resolves with matched and scored vectors."] + #[wasm_bindgen(method, catch, js_name = "query")] + pub async fn query_with_float32_array_and_options( + this: &Vectorize, + vector: &Float32Array, + options: &VectorizeQueryOptions, + ) -> Result; + #[doc = " Use the provided vector to perform a similarity search across the index."] + #[doc = ""] + #[doc = " * `vector` - Input vector that will be used to drive the similarity search."] + #[doc = " * `options` - Configuration options to massage the returned data."] + #[doc = ""] + #[doc = " Returns: A promise that resolves with matched and scored vectors."] + #[wasm_bindgen(method, catch, js_name = "query")] + pub async fn query_with_float64_array_and_options( + this: &Vectorize, + vector: &Float64Array, + options: &VectorizeQueryOptions, + ) -> Result; + #[doc = " Use the provided vector to perform a similarity search across the index."] + #[doc = ""] + #[doc = " * `vector` - Input vector that will be used to drive the similarity search."] + #[doc = " * `options` - Configuration options to massage the returned data."] + #[doc = ""] + #[doc = " Returns: A promise that resolves with matched and scored vectors."] + #[wasm_bindgen(method, catch, js_name = "query")] + pub async fn query_with_slice_and_options( + this: &Vectorize, + vector: &[f64], + options: &VectorizeQueryOptions, + ) -> Result; + #[doc = " Use the provided vector-id to perform a similarity search across the index."] + #[doc = ""] + #[doc = " * `vectorId` - Id for a vector in the index against which the index should be queried."] + #[doc = " * `options` - Configuration options to massage the returned data."] + #[doc = ""] + #[doc = " Returns: A promise that resolves with matched and scored vectors."] + #[wasm_bindgen(method, catch, js_name = "queryById")] + pub async fn query_by_id(this: &Vectorize, vector_id: &str) -> Result; + #[doc = " Use the provided vector-id to perform a similarity search across the index."] + #[doc = ""] + #[doc = " * `vectorId` - Id for a vector in the index against which the index should be queried."] + #[doc = " * `options` - Configuration options to massage the returned data."] + #[doc = ""] + #[doc = " Returns: A promise that resolves with matched and scored vectors."] + #[wasm_bindgen(method, catch, js_name = "queryById")] + pub async fn query_by_id_with_options( + this: &Vectorize, + vector_id: &str, + options: &VectorizeQueryOptions, + ) -> Result; + #[doc = " Insert a list of vectors into the index dataset. If a provided id exists, an error will be thrown."] + #[doc = ""] + #[doc = " * `vectors` - List of vectors that will be inserted."] + #[doc = ""] + #[doc = " Returns: A promise that resolves with a unique identifier of a mutation containing the insert changeset."] + #[wasm_bindgen(method, catch, slice_to_array)] + pub async fn insert( + this: &Vectorize, + vectors: &[VectorizeVector], + ) -> Result; + #[doc = " Upsert a list of vectors into the index dataset. If a provided id exists, it will be replaced with the new values."] + #[doc = ""] + #[doc = " * `vectors` - List of vectors that will be upserted."] + #[doc = ""] + #[doc = " Returns: A promise that resolves with a unique identifier of a mutation containing the upsert changeset."] + #[wasm_bindgen(method, catch, slice_to_array)] + pub async fn upsert( + this: &Vectorize, + vectors: &[VectorizeVector], + ) -> Result; + #[doc = " Delete a list of vectors with a matching id."] + #[doc = ""] + #[doc = " * `ids` - List of vector ids that should be deleted."] + #[doc = ""] + #[doc = " Returns: A promise that resolves with a unique identifier of a mutation containing the delete changeset."] + #[wasm_bindgen(method, catch, slice_to_array, js_name = "deleteByIds")] + pub async fn delete_by_ids( + this: &Vectorize, + ids: &[String], + ) -> Result; + #[doc = " Get a list of vectors with a matching id."] + #[doc = ""] + #[doc = " * `ids` - List of vector ids that should be returned."] + #[doc = ""] + #[doc = " Returns: A promise that resolves with the raw unscored vectors matching the id set."] + #[wasm_bindgen(method, catch, slice_to_array, js_name = "getByIds")] + pub async fn get_by_ids( + this: &Vectorize, + ids: &[String], + ) -> Result, Error>; +} +#[wasm_bindgen] +pub enum ReturnMetadataKind { + Bool(bool), + JsString(JsString), +} +#[wasm_bindgen] +pub enum ValuesKind { + VectorFloatArray(JsValue), + VecOfF64(Vec), +} diff --git a/worker/src/env.rs b/worker/src/env.rs index 61d02c879..91e858e2f 100644 --- a/worker/src/env.rs +++ b/worker/src/env.rs @@ -6,6 +6,7 @@ use crate::d1::D1Database; use crate::email::SendEmail; use crate::kv::KvStore; use crate::rate_limit::RateLimiter; +use crate::vectorize::Vectorize; use crate::Ai; #[cfg(feature = "queue")] use crate::Queue; @@ -120,6 +121,12 @@ impl Env { self.get_binding(binding) } + /// Access a [Vectorize](https://developers.cloudflare.com/vectorize/) index + /// by the binding name configured in your wrangler.toml file. + pub fn vectorize(&self, binding: &str) -> Result { + self.get_binding(binding) + } + /// Access a Secret Store by the binding name configured in your wrangler.toml file. pub fn secret_store(&self, binding: &str) -> Result { self.get_binding(binding) diff --git a/worker/src/lib.rs b/worker/src/lib.rs index 604cf0c09..c5b71bbe5 100644 --- a/worker/src/lib.rs +++ b/worker/src/lib.rs @@ -196,6 +196,7 @@ pub use crate::schedule::*; pub use crate::secret_store::SecretStore; pub use crate::socket::*; pub use crate::streams::*; +pub use crate::vectorize::*; pub use crate::version::*; pub use crate::websocket::*; @@ -256,6 +257,7 @@ pub mod signal; mod socket; mod sql; mod streams; +pub mod vectorize; mod version; mod websocket; diff --git a/worker/src/vectorize.rs b/worker/src/vectorize.rs new file mode 100644 index 000000000..16804fa6f --- /dev/null +++ b/worker/src/vectorize.rs @@ -0,0 +1,149 @@ +//! Ergonomics layered over the ts-gen-generated Vectorize bindings. +//! +//! The raw `#[wasm_bindgen]` surface (the [`Vectorize`] index handle plus the +//! [`VectorizeVector`], [`VectorizeQueryOptions`], [`VectorizeMatches`] types +//! and their builders) is generated from `types/vectorize.d.ts` into +//! [`crate::bindings::vectorize`] and re-exported here unchanged. This module +//! adds only what ts-gen can't infer: +//! +//! * the [`EnvBinding`] impl that backs [`Env::vectorize`](crate::Env::vectorize), and +//! * a handful of `f32`/serde conveniences. Embeddings are almost always +//! `f32`, while the generated `number[]` overloads land on `f64`, and the +//! freeform metadata/filter types come through as opaque JS objects. + +use serde::{de::DeserializeOwned, Serialize}; +use wasm_bindgen::{JsCast, JsValue}; + +pub use crate::bindings::vectorize::*; +use crate::{env::EnvBinding, Result}; + +impl EnvBinding for Vectorize { + const TYPE_NAME: &'static str = "VectorizeIndexImpl"; + + // The runtime hands back an instance of workerd's internal + // `VectorizeIndexImpl` class, but that name is an implementation detail + // that has moved across Vectorize versions (the beta exposed + // `VectorizeIndex`). As with `SendEmail`, we trust the object the runtime + // provides for the configured binding and skip the `constructor.name` + // check the default `EnvBinding::get` performs. + fn get(val: JsValue) -> Result { + Ok(val.unchecked_into()) + } +} + +impl Vectorize { + /// Similarity search using an `f32` embedding, the representation almost + /// every model emits. + /// + /// Convenience over [`Vectorize::query_with_float32_array_and_options`] + /// that converts the slice to a `Float32Array` for you. Build `options` + /// with [`VectorizeQueryOptions::builder`] (or pass + /// `&VectorizeQueryOptions::new()` for defaults). + pub async fn query_f32( + &self, + vector: &[f32], + options: &VectorizeQueryOptions, + ) -> Result { + let array = js_sys::Float32Array::from(vector); + Ok(self + .query_with_float32_array_and_options(&array, options) + .await?) + } +} + +impl VectorizeVector { + /// Build a vector from an `f32` embedding slice, the `f32` counterpart to + /// the generated [`VectorizeVector::new_with_slice`] (which takes `&[f64]`). + pub fn from_f32(id: &str, values: &[f32]) -> Self { + Self::new(id, &js_sys::Float32Array::from(values)) + } + + /// Attach metadata serialized from any [`Serialize`] value, rather than + /// hand-building the underlying JS object. + pub fn set_metadata_from(&self, metadata: &T) -> Result<()> { + self.set_metadata(&to_js_object(metadata)?.unchecked_into()); + Ok(()) + } +} + +impl VectorizeMatch { + /// Deserialize this match's metadata into a typed value, when the query + /// requested metadata (`returnMetadata`). + pub fn metadata_into(&self) -> Result> { + match self.metadata() { + Some(obj) => Ok(Some(serde_wasm_bindgen::from_value(obj.into())?)), + None => Ok(None), + } + } +} + +impl VectorizeVectorMetadataFilter { + /// Build a metadata filter from any [`Serialize`] value (e.g. a + /// `serde_json::json!({ "genre": { "$in": ["comedy", "drama"] } })`). + /// + /// The upstream filter type is a TypeScript mapped type, which ts-gen + /// can't translate into fields, so it surfaces as an opaque JS object; + /// this is the ergonomic way to populate it. The operator keys can be + /// written as string literals or via [`VectorizeVectorMetadataFilterOp::as_str`] + /// / [`VectorizeVectorMetadataFilterCollectionOp::as_str`]. + pub fn from_serde(filter: &T) -> Result { + Ok(to_js_object(filter)?.unchecked_into()) + } +} + +/// Serialize a value to a JS value, rendering Rust maps (`serde_json::Map`, +/// `HashMap`, etc.) as plain objects rather than `Map`s. +/// +/// `serde_wasm_bindgen::to_value` defaults to JS `Map`s for map types, but +/// Vectorize metadata and filters must be plain objects. A `Map`-shaped filter +/// is silently ignored by the runtime (the query returns unfiltered results). +/// Structs are unaffected (they always serialize to objects); this only matters +/// for `json!(...)` / map inputs. +fn to_js_object(value: &T) -> Result { + let serializer = serde_wasm_bindgen::Serializer::new().serialize_maps_as_objects(true); + Ok(value.serialize(&serializer)?) +} + +impl VectorizeVectorMetadataFilterOp { + /// The operator as its Vectorize filter key (e.g. `"$eq"`). + /// + /// wasm-bindgen emits a `to_str` for string enums but keeps it private, so + /// we re-expose the mapping here for building filters with typed operators. + pub fn as_str(self) -> &'static str { + match self { + Self::Eq => "$eq", + Self::Ne => "$ne", + Self::Lt => "$lt", + Self::Lte => "$lte", + Self::Gt => "$gt", + Self::Gte => "$gte", + // `__Invalid` (wasm-bindgen's hidden catch-all for unknown JS + // strings) is unreachable for a locally-constructed operator. + _ => "", + } + } +} + +impl VectorizeVectorMetadataFilterCollectionOp { + /// The collection operator as its Vectorize filter key (`"$in"` / `"$nin"`). + pub fn as_str(self) -> &'static str { + match self { + Self::In => "$in", + Self::Nin => "$nin", + _ => "", + } + } +} + +#[cfg(test)] +mod send_check { + // `Vectorize` is a raw `#[wasm_bindgen]` extern type, which wasm-bindgen + // makes `Send + Sync`. This compile-time check guards against an upstream + // regression, since the binding is awaited across `#[worker::send]` boundaries. + use super::Vectorize; + fn _assert_send() {} + #[allow(dead_code)] + fn _check() { + _assert_send::(); + } +}