Vectorize Semantic Search
Vectorize provides semantic search over encrypted memories. Index the plaintext (before encryption), store embeddings with record IDs.
Security Model
┌─────────────────────────────────────────────────────────────────┐
│ SEARCH FLOW │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 1. STORE │
│ Content → Embed (plaintext) → Store vector + record ID │
│ Content → Encrypt → Store ciphertext in D1 │
│ │
│ 2. SEARCH │
│ Query → Embed → Vectorize search → Record IDs │
│ Record IDs → Fetch from D1 → Decrypt → Return plaintext │
│ │
│ ⚠️ Vectorize stores embeddings, NOT content │
│ ⚠️ Embeddings can leak semantic information │
│ │
└─────────────────────────────────────────────────────────────────┘
Index Configuration
# wrangler.toml
[[vectorize]]
binding = "VECTORIZE"
index_name = "agent-memory"
Create index via CLI:
wrangler vectorize create agent-memory \
--dimensions=768 \
--metric=cosine
Dimension depends on embedding model:
@cf/baai/bge-base-en-v1.5: 768@cf/baai/bge-large-en-v1.5: 1024- OpenAI
text-embedding-3-small: 1536
Generate Embeddings
Using Cloudflare AI:
async function embed(
ai: Ai,
text: string
): Promise<number[]> {
const result = await ai.run('@cf/baai/bge-base-en-v1.5', {
text: [text]
})
return result.data[0]
}
async function embedBatch(
ai: Ai,
texts: string[]
): Promise<number[][]> {
const result = await ai.run('@cf/baai/bge-base-en-v1.5', {
text: texts
})
return result.data
}
Index Memory
async function indexMemory(
vectorize: VectorizeIndex,
ai: Ai,
record: {
id: string
did: string
collection: string
content: MemoryContent // Plaintext before encryption
}
): Promise<void> {
// Create searchable text from content
const text = extractSearchableText(record.content)
// Generate embedding
const embedding = await embed(ai, text)
// Upsert to Vectorize
await vectorize.upsert([{
id: record.id,
values: embedding,
metadata: {
did: record.did,
collection: record.collection,
tags: record.content.tags?.join(','),
createdAt: record.content.createdAt
}
}])
}
function extractSearchableText(content: MemoryContent): string {
const parts: string[] = []
if (content.summary) parts.push(content.summary)
if (content.text) parts.push(content.text)
if (content.tags) parts.push(content.tags.join(' '))
return parts.join('\n')
}
Semantic Search
interface SearchOptions {
did?: string // Filter by agent
collection?: string // Filter by collection
limit?: number // Max results (default 10)
minScore?: number // Minimum similarity (default 0.5)
}
async function searchMemory(
vectorize: VectorizeIndex,
ai: Ai,
query: string,
options: SearchOptions = {}
): Promise<VectorizeMatch[]> {
// Embed query
const queryEmbedding = await embed(ai, query)
// Build filter
const filter: VectorizeFilter = {}
if (options.did) filter.did = options.did
if (options.collection) filter.collection = options.collection
// Search
const results = await vectorize.query(queryEmbedding, {
topK: options.limit || 10,
filter,
returnMetadata: true
})
// Filter by score
const minScore = options.minScore ?? 0.5
return results.matches.filter(m => m.score >= minScore)
}
Full Search Flow
async function recallMemories(
env: Env,
identity: AgentIdentity,
query: string,
options: SearchOptions = {}
): Promise<DecryptedMemory[]> {
// 1. Semantic search to get record IDs
const matches = await searchMemory(
env.VECTORIZE,
env.AI,
query,
{ ...options, did: identity.did }
)
if (matches.length === 0) return []
// 2. Fetch encrypted records from D1
const ids = matches.map(m => m.id)
const placeholders = ids.map(() => '?').join(',')
const rows = await env.DB.prepare(
`SELECT * FROM records WHERE id IN (${placeholders})`
).bind(...ids).all()
// 3. Decrypt each record
const memories: DecryptedMemory[] = []
for (const row of rows.results) {
const record = rowToRecord(row)
const content = await decryptRecord(record, identity)
const match = matches.find(m => m.id === row.id)
memories.push({
id: row.id as string,
content,
score: match?.score || 0,
metadata: match?.metadata
})
}
// 4. Sort by score (highest first)
return memories.sort((a, b) => b.score - a.score)
}
Batch Indexing
async function batchIndex(
vectorize: VectorizeIndex,
ai: Ai,
records: Array<{ id: string; text: string; metadata: Record<string, string> }>
): Promise<void> {
// Batch embed (max 100 at a time)
const batchSize = 100
for (let i = 0; i < records.length; i += batchSize) {
const batch = records.slice(i, i + batchSize)
const texts = batch.map(r => r.text)
const embeddings = await embedBatch(ai, texts)
const vectors = batch.map((r, j) => ({
id: r.id,
values: embeddings[j],
metadata: r.metadata
}))
await vectorize.upsert(vectors)
}
}
Delete from Index
async function deleteFromIndex(
vectorize: VectorizeIndex,
ids: string[]
): Promise<void> {
await vectorize.deleteByIds(ids)
}
Metadata Filtering
Vectorize supports filtering on metadata:
// Filter by multiple conditions
const results = await vectorize.query(embedding, {
topK: 10,
filter: {
did: 'did:cf:abc123',
collection: 'agent.memory.note'
}
})
// Note: Metadata values must be strings
// Store tags as comma-separated: "tag1,tag2,tag3"
Wrangler Configuration
[[vectorize]]
binding = "VECTORIZE"
index_name = "agent-memory"
[ai]
binding = "AI"