Skip to content
GitHubXDiscordRSS

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.

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.

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,
});

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 string
await 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);

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
},
});
src/worker.ts
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);
},
};

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
},
});
src/worker.ts
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).

src/worker.ts
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,
});
},
};

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,
});

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,
});

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"]),
});

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

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).

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.

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 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:

  1. 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.

  2. 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.

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 explicitly
const 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.

PropertyTypeDefaultDescription
namestringauto-generatedInstance name (1-32 characters)
sourceR2Bucket | AiSearchR2Source | AiSearchWebCrawlerSourceData source. Omit for built-in storage (upload-only).
namespacestring | AiSearchNamespace"default"Namespace the instance belongs to
aiSearchModelstring@cf/meta/llama-3.3-70b-instruct-fp8-fastText generation model
embeddingModelstring@cf/baai/bge-m3Embedding model
indexMethod{ vector?: boolean; keyword?: boolean }vector-onlyControls which storage backends are used. Set both to true for hybrid search.
fusionMethod"max" | "rrf""rrf"Fusion method for combining vector and keyword results
chunkbooleantrueEnable document chunking
chunkSizenumber256Chunk size (minimum 64)
chunkOverlapnumber10Overlap between chunks (0-30)
maxNumResultsnumber10Max search results (1-50)
scoreThresholdnumber0.4Minimum match score (0-1)
rerankingbooleanfalseEnable result reranking
rerankingModelstring@cf/baai/bge-reranker-baseReranking model
rewriteQuerybooleanfalseEnable query rewriting
rewriteModelstring@cf/meta/llama-3.3-70b-instruct-fp8-fastQuery rewriting model
cachebooleanfalseEnable similarity caching
cacheThreshold"super_strict_match" | "close_enough" | "flexible_friend" | "anything_goes""close_enough"Cache similarity threshold
metadataRecord<string, unknown>Custom metadata
indexOnCreatebooleantrueIndex source documents on creation (only when source is provided)
tokenAiSearchTokenauto-created for R2Service token (only needed for R2 sources)
deletebooleantrueDelete instance on removal
adoptbooleanfalseAdopt existing instance
PropertyTypeDescription
idstringInstance name on Cloudflare (equal to name)
namestringInstance name (what ai_search bindings emit as instance_name)
namespacestringNamespace the instance lives in (defaults to "default")
createdAtstringCreation timestamp (ISO 8601)
modifiedAtstringLast-modified timestamp
internalIdstringCloudflare-internal identifier for the instance
vectorizeNamestringUnderlying Vectorize index name (legacy instances only)
tokenIdstring | undefinedService-token id (only present for R2-backed instances)
sourcestring | undefinedResolved data source (bucket name for R2, domain for web-crawler, undefined for sourceless)
type"r2" | "web-crawler" | undefinedSource type, or undefined for sourceless (built-in storage) instances

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);
}