AiSearch
Learn how to create and configure Cloudflare AI Search instances for RAG-powered semantic search using Alchemy.
The AiSearch resource lets you create and manage Cloudflare AI Search instances (formerly AutoRAG). AI Search automatically indexes your data from R2 buckets, web crawlers, or manual file uploads, creates vector embeddings, and provides natural language search with AI-generated responses.
Query shapes
Section titled “Query shapes”The search / chatCompletions APIs accept two equivalent request shapes:
query: string— a simple text query, ideal for one-off lookups.messages: Array<{role, content}>— a chat-style conversation array, ideal for RAG and multi-turn context.
You can pass either query OR messages, not both. The examples in this page mix both styles.
Minimal Example
Section titled “Minimal Example”Create an AI Search instance backed by an R2 bucket. Just pass the bucket directly as the source - Alchemy automatically handles service token management:
import { AiSearch, R2Bucket } from "alchemy/cloudflare";
const bucket = await R2Bucket("docs", { name: "my-docs" });
const search = await AiSearch("docs-search", { source: bucket,});Built-in Storage (No Source)
Section titled “Built-in Storage (No Source)”Create an AI Search instance with built-in storage for manual file uploads via the binding. No source or token is needed:
import { AiSearch } from "alchemy/cloudflare";
const search = await AiSearch("docs-search", { name: "my-knowledge-base",});Items can be uploaded through the AI Search binding at runtime. The upload() method accepts a ReadableStream, Blob, or string for content:
// In your Worker (where DOCS is bound to the AiSearch instance)// Upload a markdown document as a stringawait env.DOCS.items.upload("faq.md", "# FAQ\n\nQ: ...");
// Upload a Blob (e.g. from a fetch response or form data)const file = (await request.formData()).get("file") as File;await env.DOCS.items.upload(file.name, file);Using AI Search Bindings (Recommended)
Section titled “Using AI Search Bindings (Recommended)”AI Search instances can be bound directly to Workers as ai_search bindings, providing first-class access to search, chat, items, and jobs APIs:
import { Worker, AiSearch } from "alchemy/cloudflare";
const search = await AiSearch("docs-search", { name: "my-docs",});
await Worker("api", { entrypoint: "./src/worker.ts", bindings: { DOCS: search, // Direct AI Search binding },});export default { async fetch(request, env) { // Simple lookup: pass `query`. For multi-turn / chat-style context, // pass `messages: [{ role, content }, …]` instead (see Query shapes above). const results = await env.DOCS.search({ query: "How does caching work?", }); return Response.json(results); },};Namespaces
Section titled “Namespaces”AI Search instances belong to namespaces. By default, instances are created in the "default" namespace. Use the namespace prop to place instances in a specific namespace:
import { AiSearch, AiSearchNamespace } from "alchemy/cloudflare";
const ns = await AiSearchNamespace("production", { name: "production",});
const search = await AiSearch("docs-search", { source: bucket, namespace: ns, // or namespace: "production"});See AiSearchNamespace for managing namespaces.
Using AI Search via the AI Binding (Legacy)
Section titled “Using AI Search via the AI Binding (Legacy)”AI Search instances can also be accessed through the AI binding using env.AI.autorag(name). This is scoped to the default namespace only:
import { Worker, Ai, AiSearch, R2Bucket } from "alchemy/cloudflare";
const bucket = await R2Bucket("docs", { name: "my-docs" });
const search = await AiSearch("docs-search", { source: bucket,});
await Worker("api", { entrypoint: "./src/worker.ts", bindings: { AI: Ai(), // AI binding required to access AI Search RAG_ID: search.id, // Pass the actual instance name },});export default { async fetch(request, env) { const url = new URL(request.url); const query = url.searchParams.get("q") || "";
// Use search() for vector similarity search only const searchResults = await env.AI.autorag(env.RAG_ID).search({ query, max_num_results: 10, });
return Response.json({ results: searchResults.data, }); },};RAG Response Generation (legacy AI binding)
Section titled “RAG Response Generation (legacy AI binding)”Use the legacy AI binding’s autorag(id).aiSearch() to get AI-generated responses along with source documents. For the modern ai_search / ai_search_namespaces bindings, use search() / chatCompletions() on the binding directly instead (see the docs).
export default { async fetch(request, env) { const { question } = await request.json();
// Use aiSearch() for RAG - returns AI response + sources const result = await env.AI.autorag(env.RAG_ID).aiSearch({ query: question, model: "@cf/meta/llama-3.3-70b-instruct-fp8-fast", max_num_results: 5, reranking: { enabled: true, model: "@cf/baai/bge-reranker-base", }, });
return Response.json({ answer: result.response, sources: result.data, }); },};With Custom Models and Chunking
Section titled “With Custom Models and Chunking”Configure an AI Search instance with custom embedding and generation models:
import { AiSearch, R2Bucket } from "alchemy/cloudflare";
const bucket = await R2Bucket("docs", { name: "my-docs" });
const search = await AiSearch("custom-search", { source: bucket, aiSearchModel: "@cf/meta/llama-3.3-70b-instruct-fp8-fast", embeddingModel: "@cf/baai/bge-m3", chunkSize: 512, chunkOverlap: 20, maxNumResults: 15,});With Reranking and Query Rewriting
Section titled “With Reranking and Query Rewriting”Enable advanced retrieval features for better search results:
import { AiSearch, R2Bucket } from "alchemy/cloudflare";
const bucket = await R2Bucket("docs", { name: "my-docs" });
const search = await AiSearch("advanced-search", { source: bucket, reranking: true, rerankingModel: "@cf/baai/bge-reranker-base", rewriteQuery: true, scoreThreshold: 0.3,});Web Crawler Source
Section titled “Web Crawler Source”For crawling websites, use the AiCrawler helper to build the source configuration from URLs:
import { AiSearch, AiCrawler } from "alchemy/cloudflare";
const search = await AiSearch("docs-search", { source: AiCrawler(["https://docs.example.com"]),});Crawl Specific Paths
Section titled “Crawl Specific Paths”Provide multiple URLs to crawl specific sections of a site:
import { AiSearch, AiCrawler } from "alchemy/cloudflare";
const search = await AiSearch("blog-search", { source: AiCrawler([ "https://example.com/blog", "https://example.com/news", ]),});Domain Requirements
The domain must be:
- Added as a zone in your Cloudflare account
- Have active nameservers pointing to Cloudflare
- All URLs must be from the same domain
R2 Source with Paths and Jurisdiction
Section titled “R2 Source with Paths and Jurisdiction”When using an R2 source object instead of a bucket directly, you can set jurisdiction, prefix, and path filters:
import { AiSearch, R2Bucket } from "alchemy/cloudflare";
const bucket = await R2Bucket("docs", { name: "my-docs" });
const search = await AiSearch("docs-search", { source: { type: "r2", bucket, jurisdiction: "eu", // or "default" prefix: "public/", includePaths: ["**/*.md", "**/docs/**"], excludePaths: ["**/draft/**"], },});Path patterns support wildcards: * matches any characters except /, ** matches any characters including / (up to 10 patterns each for include and exclude).
Low-Level Web Crawler Configuration
Section titled “Low-Level Web Crawler Configuration”For more control, configure the web-crawler source directly:
import { AiSearch } from "alchemy/cloudflare";
const search = await AiSearch("docs-search", { source: { type: "web-crawler", domain: "docs.example.com", // Just the domain, not a URL includePaths: ["**/docs/**", "**/blog/**"], excludePaths: ["**/api/**"], parseType: "sitemap", // or "feed-rss" parseOptions: { include_images: true, use_browser_rendering: false, specific_sitemaps: ["https://docs.example.com/sitemap.xml"], }, storeOptions: { storage_id: "my-r2-bucket-name", jurisdiction: "default", storage_type: "r2", }, },});Path patterns support wildcards (up to 10 each). parseOptions can include include_headers, include_images, specific_sitemaps (when parseType is "sitemap"), and use_browser_rendering. Use storeOptions to send crawled content to an R2 bucket.
With Caching
Section titled “With Caching”Enable similarity caching to improve latency on repeated queries:
import { AiSearch, R2Bucket } from "alchemy/cloudflare";
const bucket = await R2Bucket("docs", { name: "my-docs" });
const search = await AiSearch("cached-search", { source: bucket, cache: true, cacheThreshold: "close_enough", // or "super_strict_match" | "flexible_friend" | "anything_goes"});Service Token
Section titled “Service Token”Service tokens are only required for R2 bucket sources. Web crawler and built-in storage instances do not need tokens.
When using an R2 source, Alchemy handles token management automatically:
-
If tokens already exist: Alchemy detects existing AI Search service tokens in your account and lets AI Search auto-select one. No new token is created.
-
If no tokens exist: Alchemy creates an account API token with the required permissions and registers it with AI Search.
The automatically created token has:
- AI Search Index Engine permission
- Workers R2 Storage Write permission
When the AI Search instance is destroyed, the token is automatically cleaned up.
Using an Explicit Token
Section titled “Using an Explicit Token”For advanced use cases (e.g., sharing a token across multiple instances), you can create an AiSearchToken explicitly:
import { AiSearch, AiSearchToken, R2Bucket } from "alchemy/cloudflare";
const bucket = await R2Bucket("docs", { name: "my-docs" });
// Create a token resource explicitlyconst token = await AiSearchToken("my-token", { name: "docs-search-token",});
const search = await AiSearch("docs-search", { source: { type: "r2", bucket, }, token, // Use the explicit token});See AiSearchToken for more details.
Configuration Options
Section titled “Configuration Options”| Property | Type | Default | Description |
|---|---|---|---|
name | string | auto-generated | Instance name (1-32 characters) |
source | R2Bucket | AiSearchR2Source | AiSearchWebCrawlerSource | — | Data source. Omit for built-in storage (upload-only). |
namespace | string | AiSearchNamespace | "default" | Namespace the instance belongs to |
aiSearchModel | string | @cf/meta/llama-3.3-70b-instruct-fp8-fast | Text generation model |
embeddingModel | string | @cf/baai/bge-m3 | Embedding model |
indexMethod | { vector?: boolean; keyword?: boolean } | vector-only | Controls which storage backends are used. Set both to true for hybrid search. |
fusionMethod | "max" | "rrf" | "rrf" | Fusion method for combining vector and keyword results |
chunk | boolean | true | Enable document chunking |
chunkSize | number | 256 | Chunk size (minimum 64) |
chunkOverlap | number | 10 | Overlap between chunks (0-30) |
maxNumResults | number | 10 | Max search results (1-50) |
scoreThreshold | number | 0.4 | Minimum match score (0-1) |
reranking | boolean | false | Enable result reranking |
rerankingModel | string | @cf/baai/bge-reranker-base | Reranking model |
rewriteQuery | boolean | false | Enable query rewriting |
rewriteModel | string | @cf/meta/llama-3.3-70b-instruct-fp8-fast | Query rewriting model |
cache | boolean | false | Enable similarity caching |
cacheThreshold | "super_strict_match" | "close_enough" | "flexible_friend" | "anything_goes" | "close_enough" | Cache similarity threshold |
metadata | Record<string, unknown> | — | Custom metadata |
indexOnCreate | boolean | true | Index source documents on creation (only when source is provided) |
token | AiSearchToken | auto-created for R2 | Service token (only needed for R2 sources) |
delete | boolean | true | Delete instance on removal |
adopt | boolean | false | Adopt existing instance |
Output Properties
Section titled “Output Properties”| Property | Type | Description |
|---|---|---|
id | string | Instance name on Cloudflare (equal to name) |
name | string | Instance name (what ai_search bindings emit as instance_name) |
namespace | string | Namespace the instance lives in (defaults to "default") |
createdAt | string | Creation timestamp (ISO 8601) |
modifiedAt | string | Last-modified timestamp |
internalId | string | Cloudflare-internal identifier for the instance |
vectorizeName | string | Underlying Vectorize index name (legacy instances only) |
tokenId | string | undefined | Service-token id (only present for R2-backed instances) |
source | string | undefined | Resolved data source (bucket name for R2, domain for web-crawler, undefined for sourceless) |
type | "r2" | "web-crawler" | undefined | Source type, or undefined for sourceless (built-in storage) instances |
Type Guard
Section titled “Type Guard”To narrow an unknown binding or resource to an AiSearch, use isAiSearch:
import { isAiSearch } from "alchemy/cloudflare";
if (isAiSearch(binding)) { // binding is narrowed to AiSearch console.log(binding.name, binding.namespace);}