Skip to content
Draft
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
9 changes: 9 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 9 additions & 2 deletions chompfile.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
3 changes: 3 additions & 0 deletions examples/vectorize/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
target
build
dist
15 changes: 15 additions & 0 deletions examples/vectorize/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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
61 changes: 61 additions & 0 deletions examples/vectorize/README.md
Original file line number Diff line number Diff line change
@@ -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=<text>&category=<a,b>` | 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.
179 changes: 179 additions & 0 deletions examples/vectorize/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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<String>,
}

#[derive(Deserialize)]
struct EmbeddingResponse {
/// One embedding vector per input text.
data: Vec<Vec<f32>>,
}

#[derive(Serialize, Deserialize)]
struct DocMetadata {
text: String,
category: String,
}

#[event(fetch)]
async fn main(req: Request, env: Env, _ctx: Context) -> Result<Response> {
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<Response> {
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=<text>&category=<a,b>`. The optional `category` filter is
/// built with the typed `$in` operator.
async fn query(req: Request, ctx: RouteContext<()>) -> Result<Response> {
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::<Vec<_>>()),
);
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<DocMetadata> = 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<Response> {
let info = ctx.env.vectorize("VECTORIZE")?.describe().await?;
Response::from_json(&json!({
"dimensions": info.dimensions(),
"vectorCount": info.vector_count(),
}))
}
19 changes: 19 additions & 0 deletions examples/vectorize/wrangler.toml
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions test/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ mod socket;
mod sql_counter;
mod sql_iterator;
mod user;
mod vectorize;
mod ws;

#[derive(Deserialize, Serialize)]
Expand Down
5 changes: 3 additions & 2 deletions test/src/router.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down
Loading