1---
2title: Add AI Search (AutoRAG)
3description: Stand up a Cloudflare AI Search (AutoRAG) pipeline over an R2 bucket with one Search call, bind it into your Worker as a typed Effect client, and answer questions over your own documents.
4sidebar:
5 order: 7
6---
7
8Cloudflare **AI Search** indexes the documents in an
9R2 bucket and answers questions over them with retrieval-augmented
10generation — chunking, embedding, vector search, reranking, and
11generation, all managed by Cloudflare.
12
13A working pipeline needs more than one resource: the instance itself,
14plus a scoped API token so the indexer can read your R2 bucket. The
15`Cloudflare.AI.Search` helper wires all of that up in a single call.
16We'll use it here and unpack exactly what it creates.
17
18## Create the R2 bucket
19
20AI Search indexes objects from an R2 bucket, so that's the source.
21Create `src/Docs.ts` with a bucket definition.
22
23```typescript
24// src/Docs.ts
25import * as Cloudflare from "alchemy/Cloudflare";
26
27export const Docs = Cloudflare.R2.Bucket("Docs", {});
28```
29
30This is the bucket you'll drop Markdown, PDFs, or text files into —
31AI Search picks them up on its next sync.
32
33## Declare the AI Search pipeline
34
35Add a `Search` construct and pass the `Docs` bucket as `source`. That
36field selects R2 as the data source.
37
38```typescript
39// src/Search.ts
40import * as Cloudflare from "alchemy/Cloudflare";
41import { Docs } from "./Docs.ts";
42
43export const Search = Cloudflare.AI.Search("Search", {
44 source: Docs,
45});
46```
47
48`Search` is a **construct**, not a single resource. Calling it expands
49into several resources so you don't have to wire them up by hand.
50
51:::note[Crawling a website instead]
52Passing a URL as `source` (instead of a bucket) indexes a website by
53crawling it, with optional `parse` / `crawl` / `store` options and no
54service token. This tutorial focuses on the R2 source — see the
55[`Search` reference](/providers/cloudflare/ai/search) for the
56web-crawler options.
57:::
58
59## What the helper creates
60
61For an R2 source, the indexer needs a service token to read your bucket.
62Cloudflare only creates that token for you through the dashboard or
63Wrangler — never on a programmatic API create — so `Search` provisions
64it as stable children of the construct:
65
66- a least-privilege `Cloudflare.ApiToken.AccountApiToken` scoped to the
67 **AI Search Index Engine** permission group,
68- a `Cloudflare.AI.SearchToken` that wraps that API token into the
69 service-token shape AI Search expects,
70- the `Cloudflare.AI.SearchInstance` itself, with the service token's
71 id wired into its `tokenId`.
72
73So the single `Search("Search", …)` call above is shorthand for
74roughly this:
75
76```typescript
77const apiToken = yield* Cloudflare.ApiToken.AccountApiToken("Token", {
78 policies: [
79 {
80 effect: "allow",
81 permissionGroups: ["AI Search Index Engine"],
82 resources: { [`com.cloudflare.api.account.${accountId}`]: "*" },
83 },
84 ],
85});
86const serviceToken = yield* Cloudflare.AI.SearchToken("Token", {
87 cfApiId: apiToken.tokenId,
88 cfApiKey: apiToken.value,
89});
90const instance = yield* Cloudflare.AI.SearchInstance("Instance", {
91 source: Docs.bucketName,
92 tokenId: serviceToken.id,
93});
94```
95
96`Search` translates the `source` bucket into the instance's `type: "r2"` +
97`source: bucket.bucketName` for you. The value it returns **is** the
98`SearchInstance` (with the managed `serviceToken` attached), so you can pass
99it straight to anything that expects a `SearchInstance` — no destructuring
100needed.
101
102## Index only part of the bucket
103
104By default AI Search indexes every object in the bucket. Pass `prefix` to
105scope indexing to one key prefix, and `include` / `exclude` glob patterns
106for finer control.
107
108```diff lang="typescript"
109export const Search = Cloudflare.AI.Search("Search", {
110 source: Docs,
111+ prefix: "docs/",
112+ include: ["/docs/**"],
113+ exclude: ["/docs/drafts/**"],
114});
115```
116
117`prefix` limits indexing to keys under `docs/`. `include` / `exclude` are
118[micromatch](https://github.com/micromatch/micromatch) glob patterns (`*`
119matches within a path segment, `**` across segments; up to 10 each) — only
120objects matching an `include` pattern are indexed, and `exclude` wins over
121`include`.
122
123## Reuse an existing token
124
125If your account already has a registered AI Search service token, pass
126its `tokenId` and `Search` skips minting the `AccountApiToken` +
127`SearchToken` children, wiring your token into the instance instead.
128
129```typescript
130export const Search = Cloudflare.AI.Search("Search", {
131 source: Docs,
132 tokenId: existingToken.id,
133});
134```
135
136Otherwise the default — letting the construct provision a least-privilege
137token that lives in your stack's state and is torn down on `destroy` — is
138the simplest path.
139
140## Group pipelines under a namespace
141
142Pass a `SearchNamespace` resource as `namespace` to group related
143pipelines under it instead of the account-provided `default` namespace.
144
145```typescript
146const Knowledge = Cloudflare.AI.SearchNamespace("Knowledge", {});
147
148export const Search = Cloudflare.AI.Search("Search", {
149 source: Docs,
150 namespace: Knowledge,
151});
152```
153
154Passing the namespace resource (not its name) lets the engine order the
155pipeline after the namespace on deploy and tear it down before the
156namespace on destroy. The namespace is immutable — changing it replaces
157the pipeline.
158
159## Add both to the stack
160
161```diff lang="typescript"
162// alchemy.run.ts
163+import { Docs } from "./src/Docs.ts";
164+import { Search } from "./src/Search.ts";
165import Api from "./src/Api.ts";
166
167export default Alchemy.Stack(
168 "CloudflareWorkerExample",
169 { providers: Cloudflare.providers(), state: Cloudflare.state() },
170 Effect.gen(function* () {
171 const api = yield* Api;
172+ const docs = yield* Docs;
173+ const search = yield* Search;
174
175 return {
176 url: api.url.as<string>(),
177+ bucket: docs.bucketName,
178+ search: search.instanceId,
179 };
180 }),
181);
182```
183
184`yield* Search` registers the construct — the bucket, both tokens, and
185the instance all get created on the next deploy, in dependency order.
186The value is the `SearchInstance` itself, so `search.instanceId` reads
187straight off it.
188
189## Bind the instance into the Worker
190
191`Cloudflare.AI.QuerySearch(search)` attaches the
192single-instance `ai_search` binding and returns a typed Effect client.
193Call it during the Worker's Construction phase.
194
195```diff lang="typescript"
196// src/Api.ts
197import * as Cloudflare from "alchemy/Cloudflare";
198import * as Effect from "effect/Effect";
199+import { Search } from "./Search.ts";
200
201export default class Api extends Cloudflare.Worker<Api>()(
202 "Api",
203 { main: import.meta.url },
204 Effect.gen(function* () {
205+ const aiSearch = yield* Search;
206+ const search = yield* Cloudflare.AI.QuerySearch(aiSearch);
207
208 return {
209 fetch: Effect.gen(function* () {
210 // …existing routes
211 }),
212 };
213- }),
214+ }).pipe(Effect.provide(Cloudflare.AI.QuerySearchBinding)),
215) {}
216```
217
218`Cloudflare.AI.QuerySearchBinding` is the runtime side of the
219binding. Provide it once at the bottom of the constructor's layer chain and the
220`QuerySearch(...)` above resolves to a live `SearchInstance` handle at runtime.
221
222## Answer questions on `/ask`
223
224`search.chatCompletions({ messages })` runs the full RAG pipeline —
225retrieve the relevant chunks, then answer with the configured generation
226model. It returns an Effect, so call it like any other.
227
228```diff lang="typescript"
229+import { HttpServerRequest } from "effect/http/HttpServerRequest";
230+import * as HttpServerResponse from "effect/http/HttpServerResponse";
231
232 return {
233 fetch: Effect.gen(function* () {
234 const request = yield* HttpServerRequest;
235+ const url = new URL(request.url, "http://api");
236
237+ if (url.pathname === "/ask") {
238+ const query = url.searchParams.get("q") ?? "What is this about?";
239+ const answer = yield* search
240+ .chatCompletions({ messages: [{ role: "user", content: query }] })
241+ .pipe(Effect.orDie);
242+ return yield* HttpServerResponse.json({
243+ response: answer.choices[0]?.message.content,
244+ sources: answer.chunks.map((c) => c.item.key),
245+ });
246+ }
247
248 return HttpServerResponse.text("Not Found", { status: 404 });
249 }),
250 };
251```
252
253`answer.choices[0].message.content` is the generated text; `answer.chunks`
254are the source chunks it drew from, so we surface their item keys as
255citations. `Effect.orDie` turns an `AiSearchError` into a 500 — swap in
256`Effect.catchTag("AiSearchError", …)` if you want typed handling.
257
258## Retrieve chunks without generation on `/search`
259
260When you only want the matching chunks (e.g. to render your own UI or
261feed another model), use `search.search({ query })` — same retrieval,
262no generation step, so it's cheaper and faster.
263
264```diff lang="typescript"
265+ if (url.pathname === "/search") {
266+ const query = url.searchParams.get("q") ?? "";
267+ const hits = yield* search.search({ query }).pipe(Effect.orDie);
268+ return yield* HttpServerResponse.json(hits.chunks);
269+ }
270```
271
272`hits.chunks` is the ranked list of chunks with their scores and source
273metadata — no generated answer, because nothing was generated.
274
275## Try it
276
277Deploy, upload a document to the bucket, and ask a question.
278
279```sh
280bun alchemy deploy
281
282# upload a doc into the indexed bucket (via wrangler)
283echo "Alchemy deploys with 'bun alchemy deploy'." > faq.txt
284bunx wrangler r2 object put "$(bun alchemy stack output bucket)/faq.txt" --file faq.txt --remote
285
286# ask over it
287curl "$(bun alchemy stack output url)/ask?q=How%20do%20I%20deploy%3F"
288```
289
290You can also drag files straight into the bucket from the Cloudflare
291dashboard → **R2** → your bucket — anything in there gets indexed.
292
293Indexing is asynchronous — a freshly uploaded file takes a short while
294to appear in results. Open the Cloudflare dashboard → **AI** →
295**AI Search** → your instance to watch the sync status and confirm the
296document was indexed before querying.
297
298## Drop down to the low-level resources
299
300The `Search` helper is the fast path for the common R2 case. Reach
301for the underlying resources directly when you need to:
302
303- **share one token across many instances** — create the
304 `SearchToken` once and pass its `id` as `tokenId` to each
305 `SearchInstance`,
306- **adopt an existing instance or token** rather than create one.
307
308Everything the helper does is just these resources composed — there's
309no behavior you lose by dropping down.
310
311## What you have now
312
313- An R2 bucket whose contents are indexed for retrieval.
314- A managed, least-privilege API token + AI Search service token, both
315 owned by your stack and torn down on `destroy`.
316- A `SearchInstance` bound into your Worker as a typed Effect client.
317- `/ask` (full RAG) and `/search` (retrieval only) routes over your own
318 documents.
319
320For the full prop surface — embedding/generation models, R2 prefix and
321include/exclude filters, reranking, query rewriting, and similarity
322caching — see the [AI Search resource reference](/providers/cloudflare/ai/search).