Cloudflare Vectorize — vector database for semantic search and RAG.
Vectorize is a vector database built into the Cloudflare edge. It stores float[] embeddings and supports approximate nearest-neighbour (ANN) search, filtered by metadata.
Free tier: 5M vector dimensions stored (e.g. ~6,500 vectors at 768d). Paid: $0.01 per 1M vector dimensions queried, $0.05 per 1M stored.
[[vectorize]]
binding = "SKILLS_INDEX"
index_name = "skills-embeddings"
# Create index — choose dimensions to match your embedding model
wrangler vectorize create skills-embeddings --dimensions=768 --metric=cosine
# BGE-base = 768, BGE-large = 1024, OpenAI ada-002 = 1536
// Step 1: Embed the user query
async function searchSkills(query: string, env: Env): Promise<string> {
// Generate embedding for the query
const { data } = await env.AI.run('@cf/baai/bge-base-en-v1.5', { text: [query] });
const queryVector = data[0];
// Step 2: Find top-K similar skill vectors
const results = await env.SKILLS_INDEX.query(queryVector, {
topK: 5,
returnMetadata: 'all',
filter: { domain: 'tech' }, // optional metadata filter
});
// Step 3: Build context from retrieved skills
const context = results.matches
.filter(m => m.score > 0.75) // similarity threshold
.map(m => m.metadata?.content as string)
.join('\n\n---\n\n');
return context;
}
// Step 4: Pass context to Claude (via AI Gateway)
async function answerWithRAG(question: string, env: Env): Promise<string> {
const context = await searchSkills(question, env);
const message = await client.messages.create({
model: 'claude-sonnet-4-6',
max_tokens: 1024,
system: `Answer using this context from the 2nth.ai skills library:\n\n${context}`,
messages: [{ role: 'user', content: question }],
});
return message.content[0].text;
}
// Embed and store a SKILL.md file
async function indexSkill(skillPath: string, content: string, env: Env): Promise<void> {
const { data } = await env.AI.run('@cf/baai/bge-base-en-v1.5', { text: [content] });
await env.SKILLS_INDEX.upsert([{
id: skillPath.replace(/\//g, '_'), // 'biz_erp_sage-x3'
values: data[0],
metadata: {
path: skillPath,
domain: skillPath.split('/')[0],
content: content.slice(0, 1000), // truncated for metadata (max 10KB)
githubUrl: `https://github.com/2nth-ai/skills/blob/main/${skillPath}/SKILL.md`,
},
}]);
}
// Batch upsert multiple skills at once (more efficient)
await env.SKILLS_INDEX.upsert(skillsToIndex.map(s => ({
id: s.path.replace(/\//g, '_'),
values: s.embedding,
metadata: { path: s.path, domain: s.domain, content: s.excerpt },
})));
// Filter by domain and minimum score
const results = await env.SKILLS_INDEX.query(queryVector, {
topK: 10,
returnMetadata: 'all',
filter: {
domain: { $in: ['biz', 'fin'] }, // only business + finance skills
},
});
// Filter by status
const results = await env.SKILLS_INDEX.query(queryVector, {
topK: 5,
filter: { status: 'production' },
});
# Separate indexes per client keeps data isolated
[[vectorize]]
binding = "CLIENT_INDEX"
index_name = "client-proximity-green-docs"
// Delete by ID
await env.SKILLS_INDEX.deleteByIds(['biz_erp_sage-x3', 'tech_cloudflare_workers']);
// Upsert overwrites existing vectors with same ID
await env.SKILLS_INDEX.upsert([{ id: 'biz_erp_sage-x3', values: newEmbedding, metadata: {} }]);
// Get info about the index
const info = await env.SKILLS_INDEX.describe();
console.log(info.vectorsCount, info.dimensions);
score > threshold.returnMetadata: 'all' costs more: Only request metadata when you need it. Use 'none' if you only need IDs.