Skip to content

Vector Stores & Retrieval

Overview

Vector stores enable semantic search over memories—finding similar items based on meaning, not just keywords.

This is critical for agents to retrieve relevant past experiences and knowledge.


The Vector Store Concept

How It Works

Step 1: Embedding
 Text: "I analyzed quarterly sales data"
 → Embed → [0.12, 0.89, -0.34,...]

Step 2: Store
 Save embedding in vector DB
 Index for fast retrieval

Step 3: Query
 Query: "What reports did we analyze?"
 → Embed → [0.15, 0.91, -0.30,...]

Step 4: Retrieve
 Find closest embeddings
 Return: "I analyzed quarterly sales data"

Why Embeddings?

Embeddings capture semantic meaning:

Semantic similarity:
 "I analyzed sales data" Similar
 "I examined revenue reports" Similar
 "The weather was nice today" Different

vs Keyword matching:
 "I analyzed sales data" Exact match
 "I examined revenue reports" No match
 "Sales data analysis" Different order

-

1. Chroma

import chromadb

client = chromadb.Client()
collection = client.create_collection("memories")

# Add documents
collection.add(
 documents=["I analyzed quarterly sales"],
 ids=["doc1"],
 embeddings=[0.1, 0.2, 0.3,...](/0.1,-0.2,-0.3,-.../)
)

# Query
results = collection.query(
 query_texts=["What reports did we analyze?"],
 n_results=5
)

# Pros
# Cons

2. Weaviate

from weaviate import Client

client = Client("http://localhost:8080")

# Store
client.data_object.create(
 data_object={
 "content": "I analyzed quarterly sales",
 "timestamp": "2025-01-15"
 },
 class_name="Memory"
)

# Query
response = client.query.get("Memory").with_near_text({
 "concepts": ["reports analysis"]
}).do()

# Pros
# Cons

3. Milvus

from pymilvus import Collection, connections

connections.connect("default", host="localhost", port="19530")
collection = Collection("memories")

# Insert
collection.insert([embeddings])

# Search
results = collection.search(
 query_embeddings,
 "embedding_field",
 search_params={"metric_type": "L2", "params": {"nprobe": 10}},
 limit=5
)

# Pros
# Cons

4. LanceDB

import lancedb

db = lancedb.connect("./data")
table = db.create_table("memories")

# Add data
table.add([
 {"text": "I analyzed sales", "embedding": [...]}
])

# Query
results = table.search([query_embedding]).limit(5).to_list()

# Pros
# Cons

Complete Example

from sentence_transformers import SentenceTransformer
import chromadb

class SemanticMemoryStore:
 def __init__(self):
 # Initialize embedder
 self.encoder = SentenceTransformer('all-MiniLM-L6-v2')

 # Initialize vector store
 self.client = chromadb.Client()
 self.collection = self.client.create_collection("episodic")

 def add_memory(self, text: str, metadata: dict = None):
 """Add memory with automatic embedding"""

 # Embed text
 embedding = self.encoder.encode(text).tolist()

 # Store with metadata
 memory_id = str(uuid.uuid4())

 self.collection.add(
 ids=[memory_id],
 documents=[text],
 embeddings=[embedding],
 metadatas=[metadata or {}]
)

 return memory_id

 def retrieve(self, query: str, k: int = 5):
 """Retrieve similar memories"""

 # Embed query
 query_embedding = self.encoder.encode(query).tolist()

 # Search
 results = self.collection.query(
 query_embeddings=[query_embedding],
 n_results=k
)

 # Format results
 return self._format_results(results)

 def _format_results(self, results):
 """Format raw results nicely"""
 formatted = []

 for i in range(len(results['documents'][0])):
 formatted.append({
 'text': results['documents'][0][i],
 'metadata': results['metadatas'][0][i],
 'distance': results['distances'][0][i]
 })

 # Sort by relevance (lower distance = more similar)
 formatted.sort(key=lambda x: x['distance'])

 return formatted

# Usage
memory_store = SemanticMemoryStore()

# Add memories
memory_store.add_memory(
 "Analyzed quarterly revenue report for Q1 2025",
 {"type": "analysis", "date": "2025-01-15"}
)

# Retrieve
results = memory_store.retrieve("What reports have we analyzed?")
for result in results:
 print(f"Found: {result['text']}")

Retrieval Strategies

Strategy 1: Direct Similarity

# Simple
query = "How do we analyze data?"
top_5 = memory_store.retrieve(query, k=5)

Strategy 2: Multi-Query Retrieval

# Query from multiple angles
queries = [
 "How do we analyze data?",
 "Data analysis methodology",
 "Steps for data processing"
]

all_results = []
for q in queries:
 results = memory_store.retrieve(q, k=3)
 all_results.extend(results)

# Deduplicate and rank
unique_results = deduplicate(all_results)
ranked = rank_by_relevance(unique_results)

Strategy 3: Hybrid Retrieval

# Combine semantic + keyword search
semantic_results = memory_store.retrieve(query, k=5)

keyword_results = memory_store.keyword_search(query, k=5)

# Merge and rank
combined = merge_results(semantic_results, keyword_results)
final = rank_and_deduplicate(combined)

Strategy 4: Metadata Filtering

# Find similar with constraints
results = memory_store.retrieve_with_filter(
 query="data analysis",
 filters={"type": "methodology"},
 date_range=("2024-01-01", "2025-01-01"),
 k=10
)

Advanced: Re-ranking

Sometimes first results aren't best. Re-rank them:

class ReRanker:
 def __init__(self):
 # Use cross-encoder for re-ranking
 self.model = CrossEncoder('ms-marco-MiniLM-L-12-v2')

 def rerank(self, query, initial_results, k=5):
 """Re-rank initial results"""

 # Score query-result pairs
 pairs = [(query, r['text']) for r in initial_results]
 scores = self.model.predict(pairs)

 # Re-rank by score
 ranked = sorted(
 zip(initial_results, scores),
 key=lambda x: x[1],
 reverse=True
)

 # Return top k
 return [r[0] for r in ranked[:k]]

# Usage
initial_results = memory_store.retrieve("data analysis", k=20)
final_results = reranker.rerank("data analysis", initial_results, k=5)

Performance Optimization

Challenge: Latency

1000 memories: 10ms search Good
100,000 memories: 100ms search OK
1,000,000 memories: 1s search Too slow

Solutions

1. Indexing

# Build efficient index
collection.create_index("embeddings")
# Subsequent searches much faster

2. Partitioning

# Split by time
jan_2025 = collection_jan_2025
dec_2024 = collection_dec_2024

# Search recent first, then historical
recent_results = jan_2025.search(query)
if not enough_results:
 historical_results = dec_2024.search(query)
# Use HNSW or IVF for approximate results
# Much faster, slightly less accurate
search_params = {
 "metric_type": "L2",
 "params": {"nprobe": 10} # Approximate
}
results = collection.search(query, search_params)

-

Embedding Model Selection

Different embeddings for different needs:

General Purpose

# Sentence Transformers
model = SentenceTransformer('all-MiniLM-L6-v2')
# Fast, good quality, ~384 dims

Dense & High Quality

model = SentenceTransformer('paraphrase-multilingual-mpnet-base-v2')
# Slower, higher quality, ~768 dims

Domain Specific

# Fine-tune on your domain
model = SentenceTransformer('sentence-transformers/all-MiniLM-L6-v2')
model = model.fine_tune_on_domain(your_data)

-

Practical Tips

Tip 1: Normalize Embeddings

import numpy as np

# Normalize to unit length
embedding = embedding / np.linalg.norm(embedding)
# Makes similarity calculations more efficient

Tip 2: Batch Processing

# Process in batches for efficiency
texts = ["memory1", "memory2",..., "memory_1000"]

embeddings = []
for i in range(0, len(texts), 100):
 batch = texts[i:i+100]
 batch_embeddings = encoder.encode(batch)
 embeddings.extend(batch_embeddings)

Tip 3: Caching

# Cache common queries
@cache(ttl=3600)
def retrieve_cached(query, k=5):
 return memory_store.retrieve(query, k)

Tip 4: Metadata is Key

# Store rich metadata for filtering
memory_store.add_memory(
 text="Analyzed Q1 sales",
 metadata={
 "type": "financial",
 "period": "Q1",
 "year": 2025,
 "department": "sales",
 "confidence": 0.95,
 "source": "internal_report"
 }
)

# Later, filter by metadata
results = memory_store.retrieve_with_filter(
 query="sales",
 filters={"year": 2025, "department": "sales"},
 k=10
)

-

Monitoring Retrieval Quality

class RetrievalEvaluator:
 def evaluate(self, query, gold_results, retrieved_results):
 """Evaluate retrieval quality"""

 # Recall@k
 retrieved_ids = {r['id'] for r in retrieved_results}
 gold_ids = {r['id'] for r in gold_results}

 recall = len(retrieved_ids & gold_ids) / len(gold_ids)

 # MRR (Mean Reciprocal Rank)
 mrr = 0
 for i, result in enumerate(retrieved_results):
 if result in gold_results:
 mrr = 1 / (i + 1)
 break

 return {
 "recall": recall,
 "mrr": mrr,
 "ndcg": self.calculate_ndcg(gold_ids, retrieved_ids)
 }

Key Takeaways

  1. Embeddings capture meaning - Not just keywords
  2. Vector stores enable semantic search - Find similar memories
  3. Multiple retrieval strategies exist - Similarity, hybrid, re-ranking
  4. Performance matters - Indexing, partitioning, approximation
  5. Metadata filtering powerful - Combine semantic + structured search
  6. Monitoring quality critical - Track recall, MRR, NDCG

-

Next Steps

-

Last Updated: August 9, 2026