| 1 | --- |
| 2 | title: Vector search with Vectorize |
| 3 | description: Create a Vectorize index, bind it to a Worker with SearchIndex, and run filtered nearest-neighbor queries over vector embeddings. |
| 4 | sidebar: |
| 5 | order: 21 |
| 6 | --- |
| 7 | |
| 8 | Vectorize is Cloudflare's globally distributed vector database. |
| 9 | In alchemy it's three pieces: a `Vectorize.Index` resource that |
| 10 | holds the vectors, an optional `Vectorize.MetadataIndex` that makes |
| 11 | a metadata property filterable, and a `Vectorize.SearchIndex` |
| 12 | binding that gives a Worker a typed client to insert and query. |
| 13 | |
| 14 | ## Create an index |
| 15 | |
| 16 | Declare the index once at module scope so both the stack and the |
| 17 | Worker can reference it: |
| 18 | |
| 19 | ```typescript |
| 20 | // src/embeddings.ts |
| 21 | import * as Cloudflare from "alchemy/Cloudflare"; |
| 22 | |
| 23 | export const Embeddings = Cloudflare.Vectorize.Index("Embeddings", { |
| 24 | dimensions: 768, |
| 25 | metric: "cosine", |
| 26 | }); |
| 27 | ``` |
| 28 | |
| 29 | `dimensions` must match the embedding model you'll use (768 for |
| 30 | `bge-base`, 1536 for `text-embedding-ada-002`, …) and `metric` is |
| 31 | one of `cosine`, `euclidean`, or `dot-product`. A Vectorize index is |
| 32 | immutable — changing dimensions, metric, preset, or description |
| 33 | triggers a replacement, and the replacement is a fresh, empty index. |
| 34 | |
| 35 | :::note |
| 36 | Instead of `dimensions` + `metric`, you can pass a managed embedding |
| 37 | model preset — `preset: "@cf/baai/bge-base-en-v1.5"` — which fixes |
| 38 | both to match the named model. |
| 39 | ::: |
| 40 | |
| 41 | ## Index a metadata property |
| 42 | |
| 43 | To filter query results by a metadata property, that property needs |
| 44 | a metadata index. Create it after resolving the parent index: |
| 45 | |
| 46 | ```typescript |
| 47 | const index = yield* Embeddings; |
| 48 | |
| 49 | yield* Cloudflare.Vectorize.MetadataIndex("KindMeta", { |
| 50 | indexName: index.indexName, |
| 51 | propertyName: "kind", |
| 52 | indexType: "string", |
| 53 | }); |
| 54 | ``` |
| 55 | |
| 56 | `indexType` is `string`, `number`, or `boolean`, matching the values |
| 57 | stored under `propertyName`. Create metadata indexes **before** |
| 58 | inserting vectors — only vectors inserted after the metadata index |
| 59 | exists are filterable. Like the parent, a metadata index is |
| 60 | immutable, and replacing the parent index replaces it too. |
| 61 | |
| 62 | ## Bind the index to a Worker |
| 63 | |
| 64 | `Cloudflare.Vectorize.SearchIndex(index)` in the Worker's Construction phase |
| 65 | attaches the native `vectorize` binding at deploy time and resolves |
| 66 | to an Effect-native client at runtime: |
| 67 | |
| 68 | ```typescript |
| 69 | // src/worker.ts |
| 70 | import * as Cloudflare from "alchemy/Cloudflare"; |
| 71 | import * as Effect from "effect/Effect"; |
| 72 | import { HttpServerRequest } from "effect/http/HttpServerRequest"; |
| 73 | import * as HttpServerResponse from "effect/http/HttpServerResponse"; |
| 74 | import { Embeddings } from "./embeddings.ts"; |
| 75 | |
| 76 | export default Cloudflare.Worker( |
| 77 | "Search", |
| 78 | { main: import.meta.url }, |
| 79 | Effect.gen(function* () { |
| 80 | const index = yield* Embeddings; |
| 81 | const vec = yield* Cloudflare.Vectorize.SearchIndex(index); |
| 82 | |
| 83 | return { |
| 84 | fetch: Effect.gen(function* () { |
| 85 | const request = yield* HttpServerRequest; |
| 86 | // routes below use `vec` |
| 87 | return HttpServerResponse.text("ok"); |
| 88 | }), |
| 89 | }; |
| 90 | }).pipe(Effect.provide(Cloudflare.Vectorize.SearchIndexBinding)), |
| 91 | ); |
| 92 | ``` |
| 93 | |
| 94 | The client exposes `upsert`, `insert`, `query`, `queryById`, |
| 95 | `getByIds`, `deleteByIds`, and `describe`, plus `raw` for direct |
| 96 | access to the underlying runtime binding. |
| 97 | |
| 98 | ## Insert vectors |
| 99 | |
| 100 | `upsert` writes vectors, replacing any with matching ids |
| 101 | (`insert` instead fails on duplicate ids): |
| 102 | |
| 103 | ```typescript |
| 104 | const mutation = yield* vec.upsert([ |
| 105 | { id: "doc-1", values: embedding, metadata: { kind: "article" } }, |
| 106 | { id: "doc-2", values: other, metadata: { kind: "note" } }, |
| 107 | ]); |
| 108 | // mutation.mutationId identifies the async write |
| 109 | ``` |
| 110 | |
| 111 | Each `values` array must have exactly the index's `dimensions` |
| 112 | entries. `metadata` is an arbitrary JSON object stored alongside the |
| 113 | vector — properties covered by a metadata index become filterable. |
| 114 | |
| 115 | :::note |
| 116 | Vectorize mutations are asynchronous — `upsert` returns a |
| 117 | `mutationId` immediately, and a query right after may not see the |
| 118 | new vectors yet. Poll with a bounded `Effect.repeat` when a test or |
| 119 | follow-up read depends on the write being visible. |
| 120 | ::: |
| 121 | |
| 122 | ## Query nearest neighbors |
| 123 | |
| 124 | `query` finds the vectors closest to the one you pass: |
| 125 | |
| 126 | ```typescript |
| 127 | const results = yield* vec.query(queryEmbedding, { |
| 128 | topK: 5, |
| 129 | returnMetadata: "all", |
| 130 | }); |
| 131 | // results.matches: [{ id, score, metadata }, ...] best match first |
| 132 | ``` |
| 133 | |
| 134 | `topK` caps the number of matches, and `returnMetadata: "all"` |
| 135 | includes each match's stored metadata in the response. |
| 136 | |
| 137 | ## Filter by metadata |
| 138 | |
| 139 | Pass a `filter` to restrict matches to vectors whose metadata |
| 140 | satisfies it — this is what the metadata index enables: |
| 141 | |
| 142 | ```typescript |
| 143 | const articles = yield* vec.query(queryEmbedding, { |
| 144 | topK: 5, |
| 145 | returnMetadata: "all", |
| 146 | filter: { kind: { $eq: "article" } }, |
| 147 | }); |
| 148 | ``` |
| 149 | |
| 150 | Only vectors with `kind === "article"` are considered. A property |
| 151 | without a metadata index cannot be used in a `filter`, so declare |
| 152 | the `MetadataIndex` up front. |
| 153 | |
| 154 | ## Deploy it |
| 155 | |
| 156 | The stack ties it together — index, metadata index, then the Worker |
| 157 | that binds them: |
| 158 | |
| 159 | ```typescript |
| 160 | // alchemy.run.ts |
| 161 | import * as Alchemy from "alchemy"; |
| 162 | import * as Cloudflare from "alchemy/Cloudflare"; |
| 163 | import * as Effect from "effect/Effect"; |
| 164 | import { Embeddings } from "./src/embeddings.ts"; |
| 165 | import Worker from "./src/worker.ts"; |
| 166 | |
| 167 | export default Alchemy.Stack( |
| 168 | "VectorSearch", |
| 169 | { |
| 170 | providers: Cloudflare.providers(), |
| 171 | state: Cloudflare.state(), |
| 172 | }, |
| 173 | Effect.gen(function* () { |
| 174 | const index = yield* Embeddings; |
| 175 | yield* Cloudflare.Vectorize.MetadataIndex("KindMeta", { |
| 176 | indexName: index.indexName, |
| 177 | propertyName: "kind", |
| 178 | indexType: "string", |
| 179 | }); |
| 180 | const worker = yield* Worker; |
| 181 | return { url: worker.url.as<string>() }; |
| 182 | }), |
| 183 | ); |
| 184 | ``` |
| 185 | |
| 186 | ```sh |
| 187 | bun alchemy deploy |
| 188 | ``` |
| 189 | |
| 190 | ## Where next |
| 191 | |
| 192 | - [AI Search](/cloudflare/ai/ai-search) — managed RAG that |
| 193 | handles embedding and indexing for you; reach for Vectorize when |
| 194 | you want control over the vectors themselves. |
| 195 | - [Workers](/cloudflare/compute/workers) — the two-phase Worker model the |
| 196 | `SearchIndex` binding lives inside. |
| 197 | - Reference: [Index](/providers/cloudflare/vectorize/index), |
| 198 | [MetadataIndex](/providers/cloudflare/vectorize/metadataindex), |
| 199 | [SearchIndex](/providers/cloudflare/vectorize/searchindex) |