Embedding Guide¶
Overview¶
Bruno-LLM provides powerful embedding capabilities through various providers. Embeddings convert text into numerical vectors that capture semantic meaning, enabling similarity search, clustering, classification, and retrieval-augmented generation (RAG) systems.
Supported Embedding Providers¶
OpenAI Embeddings¶
- Models:
text-embedding-ada-002,text-embedding-3-small,text-embedding-3-large - Features: High-quality embeddings, batch processing, cost optimization
- Use Cases: Production applications, high-accuracy requirements
Ollama Embeddings¶
- Models:
nomic-embed-text,mxbai-embed-large,snowflake-arctic-embed - Features: Local processing, privacy-focused, no usage costs
- Use Cases: Privacy-sensitive applications, offline processing
Quick Start¶
Basic Usage¶
from bruno_llm.embedding_factory import EmbeddingFactory
from bruno_llm.providers.openai import OpenAIEmbeddingProvider
from bruno_llm.providers.ollama import OllamaEmbeddingProvider
# OpenAI embeddings (cloud-based)
openai_embedder = OpenAIEmbeddingProvider(
api_key="your-api-key",
model="text-embedding-3-small"
)
# Ollama embeddings (local)
ollama_embedder = OllamaEmbeddingProvider(
base_url="http://localhost:11434",
model="nomic-embed-text"
)
# Single text embedding
text = "Machine learning transforms how we process information"
embedding = await openai_embedder.embed_text(text)
print(f"Embedding dimension: {len(embedding)}")
# Batch processing
texts = [
"Artificial intelligence is reshaping technology",
"Machine learning enables pattern recognition",
"Deep learning uses neural networks"
]
embeddings = await ollama_embedder.embed_texts(texts)
print(f"Generated {len(embeddings)} embeddings")
Factory Pattern¶
from bruno_llm.embedding_factory import EmbeddingFactory
# Create from configuration
embedder = EmbeddingFactory.create(
provider="openai",
config={
"api_key": "your-api-key",
"model": "text-embedding-3-small"
}
)
# Create from environment variables
embedder = EmbeddingFactory.create_from_env("ollama")
# Auto-selection with fallback
embedder = EmbeddingFactory.create_with_fallback(
providers=["openai", "ollama"],
configs=[openai_config, ollama_config]
)
Embedding Models Comparison¶
OpenAI Models¶
| Model | Dimensions | Max Input | Price/1M tokens | Use Case |
|---|---|---|---|---|
text-embedding-ada-002 |
1536 | 8191 | $0.10 | General purpose |
text-embedding-3-small |
1536 | 8191 | $0.02 | Cost-effective |
text-embedding-3-large |
3072 | 8191 | $0.13 | High performance |
Ollama Models¶
| Model | Dimensions | Context Length | Description |
|---|---|---|---|
nomic-embed-text |
768 | 2048 | Fast, general purpose |
mxbai-embed-large |
1024 | 512 | High quality embeddings |
snowflake-arctic-embed |
1024 | 512 | Specialized for retrieval |
all-minilm |
384 | 256 | Compact, efficient |
Advanced Usage¶
Similarity Search¶
import numpy as np
from typing import List, Tuple
class SemanticSearch:
"""Semantic search using embeddings."""
def __init__(self, embedder):
self.embedder = embedder
self.documents = []
self.embeddings = []
async def add_documents(self, texts: List[str]):
"""Add documents to the search index."""
# Generate embeddings for all documents
embeddings = await self.embedder.embed_texts(texts)
self.documents.extend(texts)
self.embeddings.extend(embeddings)
print(f"Added {len(texts)} documents to index")
async def search(self, query: str, top_k: int = 5) -> List[Tuple[str, float]]:
"""Search for similar documents."""
if not self.embeddings:
return []
# Get query embedding
query_embedding = await self.embedder.embed_text(query)
# Calculate similarities
similarities = []
for i, doc_embedding in enumerate(self.embeddings):
similarity = self.embedder.calculate_similarity(query_embedding, doc_embedding)
similarities.append((self.documents[i], similarity))
# Sort by similarity and return top results
similarities.sort(key=lambda x: x[1], reverse=True)
return similarities[:top_k]
# Usage
embedder = EmbeddingFactory.create_from_env("openai")
search = SemanticSearch(embedder)
# Build index
documents = [
"Python is a versatile programming language",
"Machine learning algorithms learn from data",
"Neural networks are inspired by the brain",
"Data science combines statistics and programming",
"APIs enable software communication"
]
await search.add_documents(documents)
# Search
results = await search.search("programming languages", top_k=3)
for doc, score in results:
print(f"Score: {score:.3f} - {doc}")
Document Clustering¶
from sklearn.cluster import KMeans
import numpy as np
async def cluster_documents(embedder, texts: List[str], n_clusters: int = 3):
"""Cluster documents based on their embeddings."""
# Generate embeddings
print("Generating embeddings...")
embeddings = await embedder.embed_texts(texts)
# Convert to numpy array
embedding_matrix = np.array(embeddings)
# Perform clustering
print(f"Clustering into {n_clusters} groups...")
kmeans = KMeans(n_clusters=n_clusters, random_state=42)
labels = kmeans.fit_predict(embedding_matrix)
# Group documents by cluster
clusters = {}
for i, label in enumerate(labels):
if label not in clusters:
clusters[label] = []
clusters[label].append(texts[i])
# Print results
for cluster_id, cluster_docs in clusters.items():
print(f"\n📁 Cluster {cluster_id}:")
for doc in cluster_docs:
print(f" • {doc}")
return clusters
# Usage
embedder = EmbeddingFactory.create_from_env("ollama")
documents = [
"Python programming tutorial",
"JavaScript web development",
"Machine learning with scikit-learn",
"Deep learning neural networks",
"React frontend framework",
"Vue.js component system",
"Supervised learning algorithms",
"Unsupervised learning techniques"
]
clusters = await cluster_documents(embedder, documents, n_clusters=3)
RAG (Retrieval-Augmented Generation)¶
from bruno_llm.factory import LLMFactory
from bruno_llm.embedding_factory import EmbeddingFactory
from bruno_core.models import Message, MessageRole
class RAGSystem:
"""Complete RAG system with embeddings and LLM."""
def __init__(self, llm_provider: str = "openai", embedding_provider: str = "openai"):
self.llm = LLMFactory.create_from_env(llm_provider)
self.embedder = EmbeddingFactory.create_from_env(embedding_provider)
self.knowledge_base = [] # (text, embedding) pairs
async def add_knowledge(self, documents: List[str]):
"""Add documents to the knowledge base."""
print(f"Processing {len(documents)} documents...")
# Generate embeddings in batches
embeddings = await self.embedder.embed_texts(documents)
# Store documents and embeddings
for doc, embedding in zip(documents, embeddings):
self.knowledge_base.append((doc, embedding))
print(f"Knowledge base now contains {len(self.knowledge_base)} documents")
async def retrieve_relevant(self, query: str, top_k: int = 3) -> List[str]:
"""Retrieve most relevant documents for a query."""
if not self.knowledge_base:
return []
# Get query embedding
query_embedding = await self.embedder.embed_text(query)
# Calculate similarities
similarities = []
for doc, doc_embedding in self.knowledge_base:
similarity = self.embedder.calculate_similarity(query_embedding, doc_embedding)
similarities.append((doc, similarity))
# Sort and return top documents
similarities.sort(key=lambda x: x[1], reverse=True)
return [doc for doc, _ in similarities[:top_k]]
async def answer_question(self, question: str) -> str:
"""Answer a question using RAG."""
# Retrieve relevant context
context_docs = await self.retrieve_relevant(question, top_k=3)
if not context_docs:
return "I don't have relevant information to answer this question."
# Build context
context = "\n\n".join(context_docs)
# Generate answer
messages = [
Message(role=MessageRole.SYSTEM, content=
"Answer the question based on the provided context. "
"If the context doesn't contain enough information, say so clearly."),
Message(role=MessageRole.USER, content=
f"Context:\n{context}\n\nQuestion: {question}")
]
response = await self.llm.generate(messages, temperature=0.1)
return response
# Usage
rag = RAGSystem(llm_provider="openai", embedding_provider="openai")
# Build knowledge base
knowledge = [
"Bruno-LLM is a Python package for LLM provider integration.",
"It supports OpenAI, Ollama, and other providers through a unified interface.",
"The factory pattern makes it easy to switch between providers.",
"Embedding providers enable semantic search and RAG applications.",
"All providers implement async interfaces for high performance."
]
await rag.add_knowledge(knowledge)
# Ask questions
answer = await rag.answer_question("What is Bruno-LLM and what does it support?")
print(f"Answer: {answer}")
Performance Optimization¶
Batch Processing¶
async def efficient_batch_processing(embedder, texts: List[str], batch_size: int = 32):
"""Process large text collections efficiently."""
total_texts = len(texts)
all_embeddings = []
print(f"Processing {total_texts} texts in batches of {batch_size}...")
for i in range(0, total_texts, batch_size):
batch = texts[i:i + batch_size]
try:
# Process batch
batch_embeddings = await embedder.embed_texts(batch)
all_embeddings.extend(batch_embeddings)
# Progress update
processed = len(all_embeddings)
progress = processed / total_texts * 100
print(f"Progress: {progress:.1f}% ({processed}/{total_texts})")
# Rate limiting for API providers
if hasattr(embedder, 'api_key'): # Likely an API provider
await asyncio.sleep(0.1) # Small delay
except Exception as e:
print(f"Error processing batch starting at index {i}: {e}")
# Add empty embeddings to maintain alignment
all_embeddings.extend([None] * len(batch))
print(f"✅ Completed processing {len(all_embeddings)} embeddings")
return all_embeddings
Caching for Performance¶
import hashlib
import json
import os
from pathlib import Path
class EmbeddingCache:
"""Cache embeddings to avoid recomputation."""
def __init__(self, cache_dir: str = "embedding_cache"):
self.cache_dir = Path(cache_dir)
self.cache_dir.mkdir(exist_ok=True)
def _get_cache_key(self, text: str, model: str) -> str:
"""Generate cache key for text and model."""
content = f"{model}:{text}"
return hashlib.sha256(content.encode()).hexdigest()
def get_embedding(self, text: str, model: str) -> List[float]:
"""Get embedding from cache."""
cache_key = self._get_cache_key(text, model)
cache_file = self.cache_dir / f"{cache_key}.json"
if cache_file.exists():
with open(cache_file, 'r') as f:
return json.load(f)
return None
def store_embedding(self, text: str, model: str, embedding: List[float]):
"""Store embedding in cache."""
cache_key = self._get_cache_key(text, model)
cache_file = self.cache_dir / f"{cache_key}.json"
with open(cache_file, 'w') as f:
json.dump(embedding, f)
def clear_cache(self):
"""Clear all cached embeddings."""
for cache_file in self.cache_dir.glob("*.json"):
cache_file.unlink()
class CachedEmbedder:
"""Embedder with caching support."""
def __init__(self, embedder, cache_dir: str = "embedding_cache"):
self.embedder = embedder
self.cache = EmbeddingCache(cache_dir)
self.model = getattr(embedder, 'model', 'unknown')
async def embed_text(self, text: str) -> List[float]:
"""Get embedding with caching."""
# Check cache first
cached = self.cache.get_embedding(text, self.model)
if cached is not None:
return cached
# Generate new embedding
embedding = await self.embedder.embed_text(text)
# Store in cache
self.cache.store_embedding(text, self.model, embedding)
return embedding
async def embed_texts(self, texts: List[str]) -> List[List[float]]:
"""Batch embedding with caching."""
embeddings = []
uncached_texts = []
uncached_indices = []
# Check cache for each text
for i, text in enumerate(texts):
cached = self.cache.get_embedding(text, self.model)
if cached is not None:
embeddings.append(cached)
else:
embeddings.append(None) # Placeholder
uncached_texts.append(text)
uncached_indices.append(i)
# Generate embeddings for uncached texts
if uncached_texts:
print(f"Generating {len(uncached_texts)} new embeddings...")
new_embeddings = await self.embedder.embed_texts(uncached_texts)
# Store new embeddings and fill placeholders
for idx, embedding in zip(uncached_indices, new_embeddings):
self.cache.store_embedding(texts[idx], self.model, embedding)
embeddings[idx] = embedding
return embeddings
# Usage
base_embedder = EmbeddingFactory.create_from_env("openai")
cached_embedder = CachedEmbedder(base_embedder)
# First call generates embeddings
embeddings1 = await cached_embedder.embed_texts(["Hello world", "AI is amazing"])
# Second call uses cache
embeddings2 = await cached_embedder.embed_texts(["Hello world", "New text"]) # Only "New text" computed
Memory-Efficient Processing¶
async def memory_efficient_embeddings(embedder, texts: List[str], max_memory_mb: int = 1000):
"""Process embeddings while managing memory usage."""
import sys
def estimate_memory_usage(num_embeddings: int, embedding_dim: int) -> int:
"""Estimate memory usage in MB."""
# Each float is 4 bytes
bytes_per_embedding = embedding_dim * 4
total_bytes = num_embeddings * bytes_per_embedding
return total_bytes / (1024 * 1024) # Convert to MB
# Get embedding dimension from a sample
sample_embedding = await embedder.embed_text("sample")
embedding_dim = len(sample_embedding)
# Calculate optimal batch size
max_embeddings_in_memory = int(max_memory_mb / (embedding_dim * 4 / 1024 / 1024))
batch_size = min(len(texts), max_embeddings_in_memory)
print(f"Processing with batch size: {batch_size}")
print(f"Estimated memory per batch: {estimate_memory_usage(batch_size, embedding_dim):.1f} MB")
all_embeddings = []
for i in range(0, len(texts), batch_size):
batch = texts[i:i + batch_size]
# Process batch
batch_embeddings = await embedder.embed_texts(batch)
# Store results (could write to disk for very large datasets)
all_embeddings.extend(batch_embeddings)
# Optional: Force garbage collection
import gc
gc.collect()
print(f"Processed batch {i//batch_size + 1}/{(len(texts)-1)//batch_size + 1}")
return all_embeddings
Error Handling¶
Robust Embedding Generation¶
from bruno_llm.exceptions import LLMError, LLMTimeoutError
import asyncio
async def robust_embed_texts(embedder, texts: List[str], max_retries: int = 3):
"""Generate embeddings with error handling and retries."""
results = []
for i, text in enumerate(texts):
success = False
for attempt in range(max_retries):
try:
embedding = await embedder.embed_text(text)
results.append(embedding)
success = True
break
except LLMTimeoutError:
print(f"Timeout for text {i}, attempt {attempt + 1}")
if attempt < max_retries - 1:
await asyncio.sleep(2 ** attempt) # Exponential backoff
except LLMError as e:
print(f"Error for text {i}: {e}")
if attempt < max_retries - 1:
await asyncio.sleep(1)
else:
# Add placeholder for failed embedding
results.append(None)
success = True # Don't retry further
break
if not success:
results.append(None) # Final fallback
# Count successful embeddings
successful = sum(1 for r in results if r is not None)
print(f"Successfully generated {successful}/{len(texts)} embeddings")
return results
Integration Patterns¶
Vector Database Integration¶
# Example with a simple in-memory vector store
import numpy as np
from typing import Dict, Any
class SimpleVectorStore:
"""Simple vector database for embeddings."""
def __init__(self):
self.vectors = []
self.metadata = []
self.ids = []
self._id_counter = 0
def add_vectors(self, embeddings: List[List[float]], metadata: List[Dict[str, Any]]):
"""Add vectors with metadata."""
for embedding, meta in zip(embeddings, metadata):
self.vectors.append(np.array(embedding))
self.metadata.append(meta)
self.ids.append(self._id_counter)
self._id_counter += 1
def search(self, query_vector: List[float], top_k: int = 5) -> List[Tuple[int, float, Dict[str, Any]]]:
"""Search for similar vectors."""
if not self.vectors:
return []
query_array = np.array(query_vector)
similarities = []
for i, vector in enumerate(self.vectors):
# Cosine similarity
similarity = np.dot(query_array, vector) / (np.linalg.norm(query_array) * np.linalg.norm(vector))
similarities.append((self.ids[i], similarity, self.metadata[i]))
# Sort by similarity
similarities.sort(key=lambda x: x[1], reverse=True)
return similarities[:top_k]
# Usage with embeddings
async def build_vector_database():
"""Build and query a vector database."""
embedder = EmbeddingFactory.create_from_env("openai")
vector_store = SimpleVectorStore()
# Sample documents
documents = [
{"text": "Python is a programming language", "category": "programming"},
{"text": "Machine learning uses algorithms", "category": "AI"},
{"text": "Databases store structured data", "category": "data"},
{"text": "APIs connect different systems", "category": "programming"},
{"text": "Neural networks learn patterns", "category": "AI"}
]
# Generate embeddings
texts = [doc["text"] for doc in documents]
embeddings = await embedder.embed_texts(texts)
# Add to vector store
vector_store.add_vectors(embeddings, documents)
# Query the database
query = "What are programming concepts?"
query_embedding = await embedder.embed_text(query)
results = vector_store.search(query_embedding, top_k=3)
print(f"Query: {query}")
print("Results:")
for doc_id, similarity, metadata in results:
print(f" {similarity:.3f}: {metadata['text']} (category: {metadata['category']})")
await build_vector_database()
Best Practices¶
1. Provider Selection¶
def choose_embedding_provider(use_case: str, budget: str, privacy: str):
"""Choose the best embedding provider for your needs."""
if privacy == "high" or budget == "free":
return "ollama" # Local, private, free
elif budget == "low":
return "openai", "text-embedding-3-small" # Cheap OpenAI option
elif use_case == "high_accuracy":
return "openai", "text-embedding-3-large" # Best performance
else:
return "openai", "text-embedding-ada-002" # Balanced choice
provider_name, model = choose_embedding_provider(
use_case="general",
budget="medium",
privacy="medium"
)
2. Text Preprocessing¶
def preprocess_text_for_embeddings(text: str) -> str:
"""Prepare text for optimal embedding generation."""
# Remove excessive whitespace
text = " ".join(text.split())
# Truncate if too long (model-dependent)
max_length = 8000 # Adjust based on model
if len(text) > max_length:
text = text[:max_length].rsplit(' ', 1)[0] # Cut at word boundary
# Optional: Remove special characters, normalize case, etc.
# Be careful not to lose important semantic information
return text.strip()
3. Monitoring and Metrics¶
import time
from dataclasses import dataclass
from typing import Optional
@dataclass
class EmbeddingMetrics:
"""Track embedding generation metrics."""
total_texts: int = 0
successful_embeddings: int = 0
failed_embeddings: int = 0
total_time: float = 0.0
total_tokens: int = 0
@property
def success_rate(self) -> float:
return self.successful_embeddings / self.total_texts if self.total_texts > 0 else 0.0
@property
def average_time_per_embedding(self) -> float:
return self.total_time / self.successful_embeddings if self.successful_embeddings > 0 else 0.0
class MetricsTracker:
"""Track embedding metrics."""
def __init__(self):
self.metrics = EmbeddingMetrics()
async def track_embedding(self, embedder, text: str) -> Optional[List[float]]:
"""Generate embedding and track metrics."""
self.metrics.total_texts += 1
start_time = time.time()
try:
embedding = await embedder.embed_text(text)
self.metrics.successful_embeddings += 1
self.metrics.total_time += time.time() - start_time
self.metrics.total_tokens += len(text.split()) # Rough estimate
return embedding
except Exception as e:
self.metrics.failed_embeddings += 1
print(f"Embedding failed: {e}")
return None
def print_summary(self):
"""Print metrics summary."""
print(f"📊 Embedding Metrics Summary:")
print(f" Total texts: {self.metrics.total_texts}")
print(f" Success rate: {self.metrics.success_rate:.1%}")
print(f" Average time: {self.metrics.average_time_per_embedding:.3f}s per embedding")
print(f" Total time: {self.metrics.total_time:.2f}s")
# Usage
tracker = MetricsTracker()
embedder = EmbeddingFactory.create_from_env("openai")
texts = ["Sample text 1", "Sample text 2", "Sample text 3"]
for text in texts:
embedding = await tracker.track_embedding(embedder, text)
tracker.print_summary()
For production deployments and advanced integration patterns, see the Integration Examples and Deployment Guide.