Skip to content

Vectorize

Long-term semantic memory. "What did Margaret think about Tom?" — answered by meaning, not by keyword.

What is Vectorize?

Cloudflare Vectorize is a serverless vector database, integrated with Workers.

For AI World Sim, it stores long-term NPC memories as embeddings. When the simulation needs to recall relevant past events for an NPC, it queries by semantic similarity.


Why Vector Memory?

D1 stores memory chronologically. To find "what did Margaret think about Tom?" with D1 alone, you'd have to scan all her diaries.

With Vectorize:

  1. Embed the query: "Margaret's thoughts about Tom"
  2. Query Vectorize for top-K most similar memories
  3. Get back semantically relevant memories instantly

This mirrors how human memory works — we recall by meaning, not by date.


Index Structure

We use one Vectorize index for the entire NPC memory corpus, with metadata filtering per NPC.

Metadata Schema

Each vector has metadata:

ts
interface VectorMetadata {
  npcId: string;
  type: 'event' | 'relationship' | 'opinion' | 'place';
  day: number;
  importance: number;
  decayFactor: number;
  textPreview: string;
}

Embedding Model

We use OpenAI's text-embedding-3-small (1536 dimensions) by default. Optionally, Workers AI's @cf/baai/bge-base-en-v1.5 (768 dimensions) for cost-free local embeddings.


When Memory is Embedded

1. Real-time (during tick)

When a high-importance short-term memory is created (importance > 0.7), it's immediately embedded and upserted to Vectorize.

ts
async function indexMemory(memory: ShortTermMemory, env: Env) {
  const embedding = await embed(memory.content, env);
  await env.VECTORIZE.upsert([{
    id: memory.id,
    values: embedding,
    metadata: {
      npcId: memory.npcId,
      type: 'event',
      day: memory.worldTime.day,
      importance: memory.importance,
      decayFactor: 0,
      textPreview: memory.content.slice(0, 200),
    },
  }]);
}

2. Background consolidation (every 6 hours)

Older daily digests are summarized into 1-2 sentences and embedded.


Retrieval API

Simple query

ts
async function recall(npcId: string, query: string, env: Env, topK = 5) {
  const queryEmbedding = await embed(query, env);

  const results = await env.VECTORIZE.query(queryEmbedding, {
    topK,
    filter: { npcId },
    returnMetadata: true,
  });

  return results.matches.map(m => ({
    text: m.metadata.textPreview,
    importance: m.metadata.importance,
    day: m.metadata.day,
    score: m.score,
  }));
}

Weighted query

For decision-making, we apply decay weighting:

ts
function weightedRecall(matches: Match[], currentDay: number) {
  return matches
    .map(m => ({
      ...m,
      effectiveScore: m.score * m.metadata.importance / (1 + (currentDay - m.metadata.day) * m.metadata.decayFactor),
    }))
    .sort((a, b) => b.effectiveScore - a.effectiveScore);
}

Use Cases

1. Decision Context

When deciding what to do, the NPC queries relevant memories:

ts
const context = await recall(
  npcId,
  `${npc.name} is deciding whether to go to the pub tonight`,
  env
);

2. Diary Generation

When writing a diary, relevant past events are surfaced.

3. Relationship Context

When two NPCs interact, each recalls their history with the other.


Decay & Pruning

Memory is not immortal. Two mechanisms:

Decay factor

Every memory has a decayFactor that grows over time. During retrieval, decay reduces effective score.

Active pruning

A daily job queries all memories with decayFactor > 0.9. Re-embeds important ones, deletes the rest.


Failure Modes

FailureHandling
Embedding API failsMemory is still stored in D1; we retry embedding later
Vectorize query returns emptyDecision module degrades to short-term memory only
Index size limit reachedAggressive pruning + older memories kept only in D1

What's Not Stored Here

  • ❌ Raw diary text (in D1)
  • ❌ Short-term memory (in D1)
  • ❌ Current NPC state (in D1)
  • ❌ World events (in D1)

Only semantic memory lives in Vectorize.

Last updated:

Released under the MIT License.