mirror of https://github.com/apache/cassandra
Merge a5ad88907e into bdbdf8d710
This commit is contained in:
commit
392fbfa37f
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,620 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
VoyageAI Contextual Embeddings (voyage-context-3) + Apache Cassandra Vector Search
|
||||
|
||||
This example demonstrates REAL contextual retrieval using VoyageAI's voyage-context-3:
|
||||
1. Embedding document chunks with surrounding context for improved retrieval
|
||||
2. Comparing retrieval accuracy: with vs without context
|
||||
3. Storing contextual embeddings in Cassandra
|
||||
4. Implementing RAG (Retrieval-Augmented Generation) with contextual embeddings
|
||||
|
||||
Prerequisites:
|
||||
- Python 3.8+
|
||||
- pip install voyageai cassandra-driver
|
||||
- VoyageAI API key (set as VOYAGE_API_KEY environment variable)
|
||||
- Apache Cassandra 5.0+ with vector search support
|
||||
|
||||
Key Features of voyage-context-3:
|
||||
- Encodes both chunk-level details and global document context
|
||||
- Improved retrieval accuracy over standard embeddings
|
||||
- Seamless drop-in replacement for existing RAG pipelines
|
||||
- Supports documents up to 120K tokens total
|
||||
- Available dimensions: 256, 512, 1024 (default), 2048
|
||||
|
||||
Author: Apache Cassandra Documentation Team
|
||||
License: Apache 2.0
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
from typing import List, Dict, Any, Optional
|
||||
from datetime import datetime
|
||||
import uuid
|
||||
|
||||
try:
|
||||
import voyageai
|
||||
from cassandra.cluster import Cluster, Session
|
||||
from cassandra.auth import PlainTextAuthProvider
|
||||
except ImportError as e:
|
||||
print(f"Error: Missing required dependency - {e}")
|
||||
print("Install dependencies: pip install voyageai cassandra-driver")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Configuration
|
||||
# ============================================================================
|
||||
|
||||
class Config:
|
||||
"""Configuration for contextual vector search."""
|
||||
|
||||
# VoyageAI settings
|
||||
VOYAGE_API_KEY = os.getenv("VOYAGE_API_KEY")
|
||||
CONTEXTUAL_MODEL = "voyage-context-3"
|
||||
EMBEDDING_DIMENSION = 1024 # Options: 256, 512, 1024, 2048
|
||||
|
||||
# Cassandra settings
|
||||
CASSANDRA_HOSTS = os.getenv("CASSANDRA_HOSTS", "127.0.0.1").split(",")
|
||||
CASSANDRA_PORT = int(os.getenv("CASSANDRA_PORT", "9042"))
|
||||
CASSANDRA_KEYSPACE = "contextual_search"
|
||||
CASSANDRA_USERNAME = os.getenv("CASSANDRA_USERNAME")
|
||||
CASSANDRA_PASSWORD = os.getenv("CASSANDRA_PASSWORD")
|
||||
|
||||
@classmethod
|
||||
def validate(cls):
|
||||
"""Validate required configuration."""
|
||||
if not cls.VOYAGE_API_KEY:
|
||||
raise ValueError(
|
||||
"VOYAGE_API_KEY environment variable is required.\n"
|
||||
"Get your API key from: https://dash.voyageai.com/api-keys\n"
|
||||
"Set it with: export VOYAGE_API_KEY='your-api-key-here'"
|
||||
)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Sample Data: Technical Documentation with Context
|
||||
# ============================================================================
|
||||
|
||||
# Simulating a long technical document split into chunks
|
||||
# Each document has multiple chunks that need context
|
||||
|
||||
SAMPLE_DOCUMENTS = [
|
||||
{
|
||||
"doc_id": "apache-cassandra-architecture",
|
||||
"title": "Apache Cassandra Architecture Overview",
|
||||
"chunks": [
|
||||
"Apache Cassandra is a distributed NoSQL database designed for handling large amounts of data across multiple nodes. "
|
||||
"It provides high availability with no single point of failure.",
|
||||
|
||||
"The ring architecture in Cassandra distributes data across nodes using consistent hashing. "
|
||||
"Each node is responsible for a range of tokens on the ring.",
|
||||
|
||||
"Cassandra uses a peer-to-peer architecture where all nodes are equal. "
|
||||
"There are no master-slave relationships, eliminating single points of failure.",
|
||||
|
||||
"Replication in Cassandra is configurable per keyspace. The replication factor determines "
|
||||
"how many copies of data are stored across the cluster for fault tolerance."
|
||||
]
|
||||
},
|
||||
{
|
||||
"doc_id": "vector-search-guide",
|
||||
"title": "Vector Search Implementation Guide",
|
||||
"chunks": [
|
||||
"Vector search enables semantic similarity queries by representing data as high-dimensional vectors. "
|
||||
"These vectors capture semantic meaning rather than just keyword matches.",
|
||||
|
||||
"Storage Attached Indexes (SAI) in Cassandra 5.0+ provide native vector search capabilities. "
|
||||
"SAI indexes support approximate nearest neighbor (ANN) search with configurable similarity functions.",
|
||||
|
||||
"Similarity functions available in Cassandra include COSINE, DOT_PRODUCT, and EUCLIDEAN. "
|
||||
"COSINE similarity is recommended for normalized embeddings from most modern embedding models.",
|
||||
|
||||
"The ANN search query syntax uses 'ORDER BY vector_column ANN OF [query_vector]'. "
|
||||
"This performs fast approximate nearest neighbor search without scanning all rows."
|
||||
]
|
||||
},
|
||||
{
|
||||
"doc_id": "embedding-best-practices",
|
||||
"title": "Embedding Generation Best Practices",
|
||||
"chunks": [
|
||||
"When generating embeddings for documents, use input_type='document' to optimize for storage. "
|
||||
"For search queries, use input_type='query' to optimize for retrieval performance.",
|
||||
|
||||
"Chunk size significantly impacts retrieval quality. Chunks should be large enough to contain "
|
||||
"meaningful context but small enough to match specific queries. Typical sizes range from 200-500 tokens.",
|
||||
|
||||
"Contextual embeddings improve retrieval by encoding both local chunk details and global document context. "
|
||||
"This helps disambiguate chunks that might be unclear when isolated from their document.",
|
||||
|
||||
"Batch processing embeddings reduces API latency and cost. Process multiple chunks in a single API call "
|
||||
"when possible, respecting the model's batch size limits."
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# VoyageAI Contextual Embedder
|
||||
# ============================================================================
|
||||
|
||||
class VoyageContextualEmbedder:
|
||||
"""
|
||||
Handles contextual embedding generation using VoyageAI's voyage-context-3.
|
||||
|
||||
This model embeds chunks while encoding context from other chunks in the same document,
|
||||
improving retrieval accuracy compared to isolated chunk embeddings.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
api_key: str,
|
||||
model: str = "voyage-context-3",
|
||||
dimension: int = 1024
|
||||
):
|
||||
"""
|
||||
Initialize VoyageAI contextual client.
|
||||
|
||||
Args:
|
||||
api_key: VoyageAI API key
|
||||
model: Model name (voyage-context-3)
|
||||
dimension: Output dimension (256, 512, 1024, 2048)
|
||||
"""
|
||||
self.client = voyageai.Client(api_key=api_key)
|
||||
self.model = model
|
||||
self.dimension = dimension
|
||||
print(f"✓ VoyageAI contextual client initialized")
|
||||
print(f" Model: {model}")
|
||||
print(f" Dimension: {dimension}")
|
||||
print(f" Feature: Contextual chunk embeddings")
|
||||
|
||||
def embed_document_chunks_with_context(
|
||||
self,
|
||||
chunks: List[str],
|
||||
input_type: str = "document"
|
||||
) -> List[List[float]]:
|
||||
"""
|
||||
Embed document chunks with context using voyage-context-3.
|
||||
|
||||
All chunks from the same document are passed together so the model
|
||||
can encode context from the entire document into each chunk's embedding.
|
||||
|
||||
Args:
|
||||
chunks: List of text chunks from a single document
|
||||
input_type: "document" or "query"
|
||||
|
||||
Returns:
|
||||
List of contextualized embeddings, one per chunk
|
||||
"""
|
||||
# Pass all chunks together in a list so they share context
|
||||
result = self.client.contextualized_embed(
|
||||
inputs=[chunks], # List of lists - one document with multiple chunks
|
||||
model=self.model,
|
||||
input_type=input_type,
|
||||
output_dimension=self.dimension
|
||||
)
|
||||
|
||||
# Extract embeddings from the result
|
||||
embeddings = result.results[0].embeddings
|
||||
return embeddings
|
||||
|
||||
def embed_document_chunks_without_context(
|
||||
self,
|
||||
chunks: List[str],
|
||||
input_type: str = "document"
|
||||
) -> List[List[float]]:
|
||||
"""
|
||||
Embed document chunks WITHOUT context (using standard embed API).
|
||||
|
||||
This is the baseline approach where each chunk is embedded independently
|
||||
without knowledge of surrounding chunks.
|
||||
|
||||
Args:
|
||||
chunks: List of text chunks from a single document
|
||||
input_type: "document" or "query"
|
||||
|
||||
Returns:
|
||||
List of standard embeddings, one per chunk
|
||||
"""
|
||||
# Use standard embed API - each chunk is independent
|
||||
result = self.client.embed(
|
||||
texts=chunks,
|
||||
model="voyage-3.5", # Use voyage-3.5 for fair comparison
|
||||
input_type=input_type,
|
||||
output_dimension=self.dimension
|
||||
)
|
||||
|
||||
return result.embeddings
|
||||
|
||||
def embed_query(self, query: str) -> List[float]:
|
||||
"""
|
||||
Embed a search query.
|
||||
|
||||
Args:
|
||||
query: Search query text
|
||||
|
||||
Returns:
|
||||
Query embedding vector
|
||||
"""
|
||||
result = self.client.contextualized_embed(
|
||||
inputs=[[query]], # Single query
|
||||
model=self.model,
|
||||
input_type="query",
|
||||
output_dimension=self.dimension
|
||||
)
|
||||
|
||||
return result.results[0].embeddings[0]
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Cassandra Vector Store
|
||||
# ============================================================================
|
||||
|
||||
class ContextualVectorStore:
|
||||
"""Handles contextual vector storage and search in Cassandra."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hosts: List[str],
|
||||
port: int = 9042,
|
||||
username: Optional[str] = None,
|
||||
password: Optional[str] = None
|
||||
):
|
||||
"""Initialize Cassandra connection."""
|
||||
auth_provider = None
|
||||
if username and password:
|
||||
auth_provider = PlainTextAuthProvider(username=username, password=password)
|
||||
|
||||
self.cluster = Cluster(
|
||||
contact_points=hosts,
|
||||
port=port,
|
||||
auth_provider=auth_provider
|
||||
)
|
||||
self.session: Optional[Session] = None
|
||||
print(f"✓ Cassandra cluster initialized (hosts: {', '.join(hosts)})")
|
||||
|
||||
def connect(self):
|
||||
"""Establish connection to Cassandra cluster."""
|
||||
try:
|
||||
self.session = self.cluster.connect()
|
||||
print("✓ Connected to Cassandra cluster")
|
||||
except Exception as e:
|
||||
print(f"Error connecting to Cassandra: {e}")
|
||||
raise
|
||||
|
||||
def close(self):
|
||||
"""Close Cassandra connection."""
|
||||
if self.cluster:
|
||||
self.cluster.shutdown()
|
||||
print("✓ Cassandra connection closed")
|
||||
|
||||
def setup_schema(
|
||||
self,
|
||||
keyspace: str,
|
||||
dimension: int,
|
||||
replication_factor: int = 1
|
||||
):
|
||||
"""
|
||||
Create schema for contextual document storage.
|
||||
|
||||
Creates two tables for comparison:
|
||||
- document_chunks_contextual: Uses contextual embeddings
|
||||
- document_chunks_standard: Uses standard embeddings (baseline)
|
||||
|
||||
Args:
|
||||
keyspace: Keyspace name
|
||||
dimension: Dimension of embeddings
|
||||
replication_factor: Replication factor
|
||||
"""
|
||||
# Create keyspace
|
||||
query = f"""
|
||||
CREATE KEYSPACE IF NOT EXISTS {keyspace}
|
||||
WITH REPLICATION = {{
|
||||
'class': 'SimpleStrategy',
|
||||
'replication_factor': {replication_factor}
|
||||
}}
|
||||
"""
|
||||
self.session.execute(query)
|
||||
print(f"✓ Keyspace '{keyspace}' created")
|
||||
|
||||
self.session.set_keyspace(keyspace)
|
||||
|
||||
# Table for contextual embeddings
|
||||
query = f"""
|
||||
CREATE TABLE IF NOT EXISTS document_chunks_contextual (
|
||||
chunk_id UUID PRIMARY KEY,
|
||||
doc_id TEXT,
|
||||
doc_title TEXT,
|
||||
chunk_text TEXT,
|
||||
chunk_index INT,
|
||||
embedding VECTOR<FLOAT, {dimension}>,
|
||||
created_at TIMESTAMP
|
||||
)
|
||||
"""
|
||||
self.session.execute(query)
|
||||
print(f"✓ Table 'document_chunks_contextual' created")
|
||||
|
||||
# Table for standard embeddings (baseline comparison)
|
||||
query = f"""
|
||||
CREATE TABLE IF NOT EXISTS document_chunks_standard (
|
||||
chunk_id UUID PRIMARY KEY,
|
||||
doc_id TEXT,
|
||||
doc_title TEXT,
|
||||
chunk_text TEXT,
|
||||
chunk_index INT,
|
||||
embedding VECTOR<FLOAT, {dimension}>,
|
||||
created_at TIMESTAMP
|
||||
)
|
||||
"""
|
||||
self.session.execute(query)
|
||||
print(f"✓ Table 'document_chunks_standard' created")
|
||||
|
||||
# Create SAI indexes for both tables
|
||||
for table_name in ["document_chunks_contextual", "document_chunks_standard"]:
|
||||
query = f"""
|
||||
CREATE CUSTOM INDEX IF NOT EXISTS {table_name}_vector_idx
|
||||
ON {table_name}(embedding)
|
||||
USING 'StorageAttachedIndex'
|
||||
WITH OPTIONS = {{'similarity_function': 'COSINE'}}
|
||||
"""
|
||||
self.session.execute(query)
|
||||
print(f"✓ SAI vector index created on {table_name}")
|
||||
|
||||
def insert_chunk(
|
||||
self,
|
||||
keyspace: str,
|
||||
table_name: str,
|
||||
doc_id: str,
|
||||
doc_title: str,
|
||||
chunk_text: str,
|
||||
chunk_index: int,
|
||||
embedding: List[float]
|
||||
):
|
||||
"""Insert a document chunk with its embedding."""
|
||||
self.session.set_keyspace(keyspace)
|
||||
|
||||
query = f"""
|
||||
INSERT INTO {table_name} (
|
||||
chunk_id, doc_id, doc_title, chunk_text, chunk_index,
|
||||
embedding, created_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
"""
|
||||
|
||||
self.session.execute(
|
||||
query,
|
||||
(
|
||||
uuid.uuid4(),
|
||||
doc_id,
|
||||
doc_title,
|
||||
chunk_text,
|
||||
chunk_index,
|
||||
embedding,
|
||||
datetime.utcnow()
|
||||
)
|
||||
)
|
||||
|
||||
def search_similar_chunks(
|
||||
self,
|
||||
keyspace: str,
|
||||
table_name: str,
|
||||
query_vector: List[float],
|
||||
limit: int = 5
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Search for similar document chunks.
|
||||
|
||||
Args:
|
||||
keyspace: Keyspace name
|
||||
table_name: Table to search (contextual or standard)
|
||||
query_vector: Query embedding
|
||||
limit: Maximum results
|
||||
|
||||
Returns:
|
||||
List of matching chunks with similarity scores
|
||||
"""
|
||||
self.session.set_keyspace(keyspace)
|
||||
|
||||
query = f"""
|
||||
SELECT
|
||||
chunk_id, doc_id, doc_title, chunk_text, chunk_index,
|
||||
similarity_cosine(embedding, ?) AS similarity
|
||||
FROM {table_name}
|
||||
ORDER BY embedding ANN OF ?
|
||||
LIMIT ?
|
||||
"""
|
||||
|
||||
rows = self.session.execute(query, (query_vector, query_vector, limit))
|
||||
|
||||
results = []
|
||||
for row in rows:
|
||||
results.append({
|
||||
"chunk_id": str(row.chunk_id),
|
||||
"doc_id": row.doc_id,
|
||||
"doc_title": row.doc_title,
|
||||
"chunk_text": row.chunk_text,
|
||||
"chunk_index": row.chunk_index,
|
||||
"similarity": float(row.similarity) if row.similarity else None
|
||||
})
|
||||
|
||||
return results
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Main Application
|
||||
# ============================================================================
|
||||
|
||||
def main():
|
||||
"""Main application demonstrating contextual embeddings."""
|
||||
|
||||
print("\n" + "="*80)
|
||||
print("VoyageAI Contextual Embeddings (voyage-context-3) + Cassandra")
|
||||
print("="*80 + "\n")
|
||||
|
||||
# Validate configuration
|
||||
try:
|
||||
Config.validate()
|
||||
except ValueError as e:
|
||||
print(f"Configuration error: {e}")
|
||||
return 1
|
||||
|
||||
# Initialize components
|
||||
print("1. Initializing VoyageAI contextual embedder...")
|
||||
print("-" * 80)
|
||||
|
||||
embedder = VoyageContextualEmbedder(
|
||||
api_key=Config.VOYAGE_API_KEY,
|
||||
model=Config.CONTEXTUAL_MODEL,
|
||||
dimension=Config.EMBEDDING_DIMENSION
|
||||
)
|
||||
|
||||
vector_store = ContextualVectorStore(
|
||||
hosts=Config.CASSANDRA_HOSTS,
|
||||
port=Config.CASSANDRA_PORT,
|
||||
username=Config.CASSANDRA_USERNAME,
|
||||
password=Config.CASSANDRA_PASSWORD
|
||||
)
|
||||
|
||||
try:
|
||||
vector_store.connect()
|
||||
|
||||
# Setup schema
|
||||
print("\n2. Setting up Cassandra schema...")
|
||||
print("-" * 80)
|
||||
|
||||
vector_store.setup_schema(
|
||||
keyspace=Config.CASSANDRA_KEYSPACE,
|
||||
dimension=Config.EMBEDDING_DIMENSION,
|
||||
replication_factor=1
|
||||
)
|
||||
|
||||
# Process documents with both contextual and standard embeddings
|
||||
print("\n3. Generating embeddings (contextual vs standard)...")
|
||||
print("-" * 80)
|
||||
|
||||
for doc in SAMPLE_DOCUMENTS:
|
||||
doc_id = doc["doc_id"]
|
||||
doc_title = doc["title"]
|
||||
chunks = doc["chunks"]
|
||||
|
||||
print(f"\nProcessing: {doc_title}")
|
||||
print(f" Chunks: {len(chunks)}")
|
||||
|
||||
# Generate CONTEXTUAL embeddings
|
||||
contextual_embeddings = embedder.embed_document_chunks_with_context(chunks)
|
||||
print(f" ✓ Generated {len(contextual_embeddings)} contextual embeddings")
|
||||
|
||||
# Generate STANDARD embeddings (for comparison)
|
||||
standard_embeddings = embedder.embed_document_chunks_without_context(chunks)
|
||||
print(f" ✓ Generated {len(standard_embeddings)} standard embeddings")
|
||||
|
||||
# Store contextual embeddings
|
||||
for i, (chunk_text, embedding) in enumerate(zip(chunks, contextual_embeddings)):
|
||||
vector_store.insert_chunk(
|
||||
keyspace=Config.CASSANDRA_KEYSPACE,
|
||||
table_name="document_chunks_contextual",
|
||||
doc_id=doc_id,
|
||||
doc_title=doc_title,
|
||||
chunk_text=chunk_text,
|
||||
chunk_index=i,
|
||||
embedding=embedding
|
||||
)
|
||||
|
||||
# Store standard embeddings
|
||||
for i, (chunk_text, embedding) in enumerate(zip(chunks, standard_embeddings)):
|
||||
vector_store.insert_chunk(
|
||||
keyspace=Config.CASSANDRA_KEYSPACE,
|
||||
table_name="document_chunks_standard",
|
||||
doc_id=doc_id,
|
||||
doc_title=doc_title,
|
||||
chunk_text=chunk_text,
|
||||
chunk_index=i,
|
||||
embedding=embedding
|
||||
)
|
||||
|
||||
print(f"\n✓ All documents processed and stored")
|
||||
|
||||
# Perform comparison searches
|
||||
print("\n4. Comparing retrieval: Contextual vs Standard embeddings...")
|
||||
print("-" * 80)
|
||||
|
||||
test_queries = [
|
||||
"How does Cassandra distribute data across nodes?",
|
||||
"What similarity functions are available for vector search?",
|
||||
"What is the recommended chunk size for embeddings?"
|
||||
]
|
||||
|
||||
for query_text in test_queries:
|
||||
print(f"\nQuery: \"{query_text}\"")
|
||||
print("=" * 70)
|
||||
|
||||
# Generate query embedding
|
||||
query_vector = embedder.embed_query(query_text)
|
||||
|
||||
# Search with CONTEXTUAL embeddings
|
||||
print("\n[CONTEXTUAL EMBEDDINGS]")
|
||||
print("-" * 40)
|
||||
contextual_results = vector_store.search_similar_chunks(
|
||||
keyspace=Config.CASSANDRA_KEYSPACE,
|
||||
table_name="document_chunks_contextual",
|
||||
query_vector=query_vector,
|
||||
limit=3
|
||||
)
|
||||
|
||||
for i, result in enumerate(contextual_results, 1):
|
||||
print(f"{i}. {result['doc_title']} (chunk {result['chunk_index']})")
|
||||
print(f" Similarity: {result['similarity']:.4f}")
|
||||
print(f" Text: {result['chunk_text'][:100]}...")
|
||||
print()
|
||||
|
||||
# Search with STANDARD embeddings
|
||||
print("[STANDARD EMBEDDINGS - Baseline]")
|
||||
print("-" * 40)
|
||||
standard_results = vector_store.search_similar_chunks(
|
||||
keyspace=Config.CASSANDRA_KEYSPACE,
|
||||
table_name="document_chunks_standard",
|
||||
query_vector=query_vector,
|
||||
limit=3
|
||||
)
|
||||
|
||||
for i, result in enumerate(standard_results, 1):
|
||||
print(f"{i}. {result['doc_title']} (chunk {result['chunk_index']})")
|
||||
print(f" Similarity: {result['similarity']:.4f}")
|
||||
print(f" Text: {result['chunk_text'][:100]}...")
|
||||
print()
|
||||
|
||||
print("\n" + "="*80)
|
||||
print("SUCCESS: Contextual embeddings demonstration complete!")
|
||||
print("="*80)
|
||||
|
||||
print("\nKey Features Demonstrated:")
|
||||
print("✓ Real VoyageAI voyage-context-3 integration")
|
||||
print("✓ Contextual chunk embeddings with global document context")
|
||||
print("✓ Side-by-side comparison with standard embeddings")
|
||||
print("✓ Improved retrieval accuracy for ambiguous chunks")
|
||||
print("✓ Drop-in replacement for existing RAG pipelines")
|
||||
|
||||
print("\nWhen to Use Contextual Embeddings:")
|
||||
print("- Long documents split into chunks (technical docs, books)")
|
||||
print("- Chunks that need surrounding context for disambiguation")
|
||||
print("- Improved precision for RAG applications")
|
||||
print("- Knowledge bases with interconnected information")
|
||||
|
||||
print("\nBest Practices:")
|
||||
print("- Pass all chunks from same document together")
|
||||
print("- Maintain chunk order for sequential context")
|
||||
print("- Avoid overlapping chunks")
|
||||
print("- Use input_type='document' for chunks, 'query' for searches")
|
||||
|
||||
return 0
|
||||
|
||||
except Exception as e:
|
||||
print(f"\nError: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return 1
|
||||
|
||||
finally:
|
||||
vector_store.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
|
|
@ -0,0 +1,699 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
VoyageAI Multimodal Embeddings (voyage-multimodal-3.5) + Apache Cassandra Vector Search
|
||||
|
||||
This example demonstrates REAL multimodal vector search using VoyageAI's multimodal models:
|
||||
1. Embedding text, images, and video together in a shared vector space
|
||||
2. Storing multimodal vectors in Cassandra (same vector space for all modalities)
|
||||
3. Cross-modal similarity search (text query -> image results, image query -> text results)
|
||||
4. Hybrid search combining text, images, and metadata filters
|
||||
|
||||
Prerequisites:
|
||||
- Python 3.8+
|
||||
- pip install 'voyageai>=0.3.6' cassandra-driver pillow requests
|
||||
- VoyageAI API key (set as VOYAGE_API_KEY environment variable)
|
||||
- Apache Cassandra 5.0+ with vector search support
|
||||
- Sample images (or URLs) for demonstration
|
||||
|
||||
Supported Multimodal Models:
|
||||
|
||||
voyage-multimodal-3.5 (Preview - Default):
|
||||
- Supports text, images, AND video in same vector space
|
||||
- Variable dimensions: 256, 512, 1024 (default), 2048
|
||||
- 32,000 token context length
|
||||
- Images: max 16 million pixels, max 20MB
|
||||
- Video: max 20MB (requires voyageai >= 0.3.6)
|
||||
- Token pricing: 560 image pixels = 1 token, 1120 video pixels = 1 token
|
||||
|
||||
voyage-multimodal-3:
|
||||
- Supports text and images (no video)
|
||||
- Fixed 1024-dimensional embeddings
|
||||
- 32,000 token context length
|
||||
- Images: max 16 million pixels, max 20MB
|
||||
|
||||
Cross-modal search enabled (text finds images/video, images find text)
|
||||
|
||||
Author: Apache Cassandra Documentation Team
|
||||
License: Apache 2.0
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
from typing import List, Dict, Any, Optional, Tuple
|
||||
from datetime import datetime
|
||||
import uuid
|
||||
import json
|
||||
import io
|
||||
|
||||
try:
|
||||
import voyageai
|
||||
from cassandra.cluster import Cluster, Session
|
||||
from cassandra.auth import PlainTextAuthProvider
|
||||
from PIL import Image
|
||||
import requests
|
||||
except ImportError as e:
|
||||
print(f"Error: Missing required dependency - {e}")
|
||||
print("Install dependencies: pip install voyageai cassandra-driver pillow requests")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Configuration
|
||||
# ============================================================================
|
||||
|
||||
class Config:
|
||||
"""Configuration for multimodal vector search."""
|
||||
|
||||
# VoyageAI settings
|
||||
VOYAGE_API_KEY = os.getenv("VOYAGE_API_KEY")
|
||||
# Model options: voyage-multimodal-3 (fixed 1024D), voyage-multimodal-3.5 (256/512/1024/2048D)
|
||||
MULTIMODAL_MODEL = "voyage-multimodal-3.5"
|
||||
# Dimension options for voyage-multimodal-3.5: 256, 512, 1024 (default), 2048
|
||||
# Note: voyage-multimodal-3 only supports fixed 1024D
|
||||
EMBEDDING_DIMENSION = 1024
|
||||
|
||||
# Cassandra settings
|
||||
CASSANDRA_HOSTS = os.getenv("CASSANDRA_HOSTS", "127.0.0.1").split(",")
|
||||
CASSANDRA_PORT = int(os.getenv("CASSANDRA_PORT", "9042"))
|
||||
CASSANDRA_KEYSPACE = "multimodal_search"
|
||||
CASSANDRA_USERNAME = os.getenv("CASSANDRA_USERNAME")
|
||||
CASSANDRA_PASSWORD = os.getenv("CASSANDRA_PASSWORD")
|
||||
|
||||
@classmethod
|
||||
def validate(cls):
|
||||
"""Validate required configuration."""
|
||||
if not cls.VOYAGE_API_KEY:
|
||||
raise ValueError(
|
||||
"VOYAGE_API_KEY environment variable is required.\n"
|
||||
"Get your API key from: https://dash.voyageai.com/api-keys\n"
|
||||
"Set it with: export VOYAGE_API_KEY='your-api-key-here'"
|
||||
)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Sample Data: Multimedia Content Library
|
||||
# ============================================================================
|
||||
|
||||
# Demo images - Using placeholder image URLs for demonstration
|
||||
# In production, replace with your actual images
|
||||
SAMPLE_MEDIA_ITEMS = [
|
||||
{
|
||||
"title": "Mountain Landscape Photography",
|
||||
"description": "Majestic snow-capped mountain peaks at sunset with dramatic lighting",
|
||||
"content_type": "image",
|
||||
"tags": ["nature", "landscape", "mountains", "photography"],
|
||||
"image_url": "https://picsum.photos/800/600?mountain",
|
||||
"has_visual": True
|
||||
},
|
||||
{
|
||||
"title": "Machine Learning Tutorial",
|
||||
"description": "Comprehensive guide to neural networks and deep learning algorithms",
|
||||
"content_type": "article",
|
||||
"tags": ["technology", "machine-learning", "education"],
|
||||
"has_visual": False
|
||||
},
|
||||
{
|
||||
"title": "Ocean Beach Sunset",
|
||||
"description": "Tranquil beach scene with golden sunset over calm ocean waves",
|
||||
"content_type": "image",
|
||||
"tags": ["nature", "ocean", "beach", "sunset"],
|
||||
"image_url": "https://picsum.photos/800/600?ocean",
|
||||
"has_visual": True
|
||||
},
|
||||
{
|
||||
"title": "Modern Architecture Design",
|
||||
"description": "Contemporary building with glass facade and geometric patterns",
|
||||
"content_type": "image",
|
||||
"tags": ["architecture", "design", "modern", "urban"],
|
||||
"image_url": "https://picsum.photos/800/600?architecture",
|
||||
"has_visual": True
|
||||
},
|
||||
{
|
||||
"title": "Python Programming Guide",
|
||||
"description": "Complete Python tutorial covering data structures and algorithms",
|
||||
"content_type": "article",
|
||||
"tags": ["programming", "python", "education", "tutorial"],
|
||||
"has_visual": False
|
||||
},
|
||||
{
|
||||
"title": "Forest Trail Hiking",
|
||||
"description": "Lush green forest path winding through tall trees and vegetation",
|
||||
"content_type": "image",
|
||||
"tags": ["nature", "forest", "hiking", "outdoor"],
|
||||
"image_url": "https://picsum.photos/800/600?forest",
|
||||
"has_visual": True
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# VoyageAI Multimodal Embedder
|
||||
# ============================================================================
|
||||
|
||||
class VoyageMultimodalEmbedder:
|
||||
"""
|
||||
Handles multimodal embedding generation using VoyageAI's multimodal models.
|
||||
|
||||
Supported models:
|
||||
- voyage-multimodal-3.5: Text + Images + Video, variable dimensions (256/512/1024/2048)
|
||||
- voyage-multimodal-3: Text + Images only, fixed 1024-dimensional vectors
|
||||
|
||||
Both models embed content into the same vector space, enabling cross-modal similarity search.
|
||||
"""
|
||||
|
||||
def __init__(self, api_key: str, model: str = "voyage-multimodal-3.5", dimension: int = 1024):
|
||||
"""
|
||||
Initialize VoyageAI multimodal client.
|
||||
|
||||
Args:
|
||||
api_key: VoyageAI API key
|
||||
model: Model name (voyage-multimodal-3.5 or voyage-multimodal-3)
|
||||
dimension: Output dimension (256, 512, 1024, 2048). Only for voyage-multimodal-3.5.
|
||||
voyage-multimodal-3 always produces 1024-dim vectors.
|
||||
"""
|
||||
self.client = voyageai.Client(api_key=api_key)
|
||||
self.model = model
|
||||
|
||||
# voyage-multimodal-3 only supports 1024D, voyage-multimodal-3.5 supports variable
|
||||
if model == "voyage-multimodal-3":
|
||||
self.dimension = 1024
|
||||
if dimension != 1024:
|
||||
print(f" Note: voyage-multimodal-3 only supports 1024D (ignoring {dimension}D)")
|
||||
else:
|
||||
self.dimension = dimension
|
||||
|
||||
modalities = "Text + Images + Video" if "3.5" in model else "Text + Images"
|
||||
print(f"✓ VoyageAI multimodal client initialized")
|
||||
print(f" Model: {model}")
|
||||
print(f" Dimension: {self.dimension}")
|
||||
print(f" Supports: {modalities} in same vector space")
|
||||
|
||||
def load_image_from_url(self, url: str) -> Image.Image:
|
||||
"""
|
||||
Download and load image from URL.
|
||||
|
||||
Args:
|
||||
url: Image URL
|
||||
|
||||
Returns:
|
||||
PIL Image object
|
||||
"""
|
||||
try:
|
||||
response = requests.get(url, timeout=10)
|
||||
response.raise_for_status()
|
||||
return Image.open(io.BytesIO(response.content))
|
||||
except Exception as e:
|
||||
print(f"Warning: Failed to load image from {url}: {e}")
|
||||
# Return a small placeholder image
|
||||
return Image.new('RGB', (100, 100), color='gray')
|
||||
|
||||
def embed_text(self, text: str, input_type: str = "document") -> List[float]:
|
||||
"""
|
||||
Embed text using VoyageAI multimodal model.
|
||||
|
||||
Args:
|
||||
text: Text to embed
|
||||
input_type: "document" or "query"
|
||||
|
||||
Returns:
|
||||
Embedding vector (dimension depends on model/config)
|
||||
"""
|
||||
# Build kwargs - only add output_dimension for voyage-multimodal-3.5
|
||||
kwargs = {
|
||||
"inputs": [[text]],
|
||||
"model": self.model,
|
||||
"input_type": input_type
|
||||
}
|
||||
if "3.5" in self.model:
|
||||
kwargs["output_dimension"] = self.dimension
|
||||
|
||||
result = self.client.multimodal_embed(**kwargs)
|
||||
return result.embeddings[0]
|
||||
|
||||
def embed_image(self, image: Image.Image, caption: Optional[str] = None) -> List[float]:
|
||||
"""
|
||||
Embed image (optionally with caption) using VoyageAI multimodal model.
|
||||
|
||||
Args:
|
||||
image: PIL Image object
|
||||
caption: Optional text caption to embed with image
|
||||
|
||||
Returns:
|
||||
Embedding vector (dimension depends on model/config)
|
||||
"""
|
||||
if caption:
|
||||
# Embed image with caption (interleaved)
|
||||
inputs = [[caption, image]]
|
||||
else:
|
||||
# Embed image only
|
||||
inputs = [[image]]
|
||||
|
||||
# Build kwargs - only add output_dimension for voyage-multimodal-3.5
|
||||
kwargs = {
|
||||
"inputs": inputs,
|
||||
"model": self.model,
|
||||
"input_type": "document"
|
||||
}
|
||||
if "3.5" in self.model:
|
||||
kwargs["output_dimension"] = self.dimension
|
||||
|
||||
result = self.client.multimodal_embed(**kwargs)
|
||||
return result.embeddings[0]
|
||||
|
||||
def embed_multimodal_item(self, item: Dict[str, Any]) -> List[float]:
|
||||
"""
|
||||
Generate embedding for a multimodal item.
|
||||
|
||||
For items with images: embeds image + description together
|
||||
For text-only items: embeds description only
|
||||
|
||||
Args:
|
||||
item: Item dictionary with description and optional image_url
|
||||
|
||||
Returns:
|
||||
Embedding vector (dimension depends on model/config)
|
||||
"""
|
||||
if item.get("has_visual") and item.get("image_url"):
|
||||
# Load image
|
||||
image = self.load_image_from_url(item["image_url"])
|
||||
|
||||
# Embed image with description (cross-modal alignment)
|
||||
embedding = self.embed_image(image, caption=item["description"])
|
||||
|
||||
return embedding
|
||||
else:
|
||||
# Text-only content
|
||||
return self.embed_text(item["description"], input_type="document")
|
||||
|
||||
def embed_query(self, query: str) -> List[float]:
|
||||
"""
|
||||
Embed a search query.
|
||||
|
||||
Can be used to find both text, image, and video content.
|
||||
|
||||
Args:
|
||||
query: Search query text
|
||||
|
||||
Returns:
|
||||
Query embedding vector (dimension depends on model/config)
|
||||
"""
|
||||
return self.embed_text(query, input_type="query")
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Cassandra Multimodal Vector Store
|
||||
# ============================================================================
|
||||
|
||||
class MultimodalVectorStore:
|
||||
"""Handles multimodal vector storage and search in Cassandra."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hosts: List[str],
|
||||
port: int = 9042,
|
||||
username: Optional[str] = None,
|
||||
password: Optional[str] = None
|
||||
):
|
||||
"""Initialize Cassandra connection."""
|
||||
auth_provider = None
|
||||
if username and password:
|
||||
auth_provider = PlainTextAuthProvider(username=username, password=password)
|
||||
|
||||
self.cluster = Cluster(
|
||||
contact_points=hosts,
|
||||
port=port,
|
||||
auth_provider=auth_provider
|
||||
)
|
||||
self.session: Optional[Session] = None
|
||||
print(f"✓ Cassandra cluster initialized (hosts: {', '.join(hosts)})")
|
||||
|
||||
def connect(self):
|
||||
"""Establish connection to Cassandra cluster."""
|
||||
try:
|
||||
self.session = self.cluster.connect()
|
||||
print("✓ Connected to Cassandra cluster")
|
||||
except Exception as e:
|
||||
print(f"Error connecting to Cassandra: {e}")
|
||||
raise
|
||||
|
||||
def close(self):
|
||||
"""Close Cassandra connection."""
|
||||
if self.cluster:
|
||||
self.cluster.shutdown()
|
||||
print("✓ Cassandra connection closed")
|
||||
|
||||
def setup_schema(
|
||||
self,
|
||||
keyspace: str,
|
||||
dimension: int,
|
||||
replication_factor: int = 1
|
||||
):
|
||||
"""
|
||||
Create schema for multimodal content storage.
|
||||
|
||||
Args:
|
||||
keyspace: Keyspace name
|
||||
dimension: Dimension of embeddings (1024 for voyage-multimodal-3)
|
||||
replication_factor: Replication factor
|
||||
"""
|
||||
# Create keyspace
|
||||
query = f"""
|
||||
CREATE KEYSPACE IF NOT EXISTS {keyspace}
|
||||
WITH REPLICATION = {{
|
||||
'class': 'SimpleStrategy',
|
||||
'replication_factor': {replication_factor}
|
||||
}}
|
||||
"""
|
||||
self.session.execute(query)
|
||||
print(f"✓ Keyspace '{keyspace}' created")
|
||||
|
||||
self.session.set_keyspace(keyspace)
|
||||
|
||||
# Create media items table
|
||||
# Note: Single vector column since both text and images use same vector space
|
||||
query = f"""
|
||||
CREATE TABLE IF NOT EXISTS media_items (
|
||||
item_id UUID PRIMARY KEY,
|
||||
title TEXT,
|
||||
description TEXT,
|
||||
content_type TEXT,
|
||||
tags SET<TEXT>,
|
||||
has_visual BOOLEAN,
|
||||
embedding VECTOR<FLOAT, {dimension}>,
|
||||
image_url TEXT,
|
||||
metadata TEXT,
|
||||
created_at TIMESTAMP
|
||||
)
|
||||
"""
|
||||
self.session.execute(query)
|
||||
print(f"✓ Table 'media_items' created with VECTOR<FLOAT, {dimension}>")
|
||||
|
||||
# Create SAI index for vector similarity search
|
||||
query = f"""
|
||||
CREATE CUSTOM INDEX IF NOT EXISTS media_embedding_idx
|
||||
ON media_items(embedding)
|
||||
USING 'StorageAttachedIndex'
|
||||
WITH OPTIONS = {{'similarity_function': 'COSINE'}}
|
||||
"""
|
||||
self.session.execute(query)
|
||||
print("✓ SAI vector index created (COSINE similarity)")
|
||||
|
||||
# Create index on content_type for filtering
|
||||
query = """
|
||||
CREATE CUSTOM INDEX IF NOT EXISTS media_content_type_idx
|
||||
ON media_items(content_type)
|
||||
USING 'StorageAttachedIndex'
|
||||
"""
|
||||
self.session.execute(query)
|
||||
print("✓ SAI index created on content_type")
|
||||
|
||||
def insert_media_item(
|
||||
self,
|
||||
keyspace: str,
|
||||
item_id: uuid.UUID,
|
||||
title: str,
|
||||
description: str,
|
||||
content_type: str,
|
||||
tags: List[str],
|
||||
has_visual: bool,
|
||||
embedding: List[float],
|
||||
image_url: Optional[str] = None,
|
||||
metadata: Optional[Dict[str, Any]] = None
|
||||
):
|
||||
"""Insert a multimodal media item."""
|
||||
self.session.set_keyspace(keyspace)
|
||||
|
||||
query = """
|
||||
INSERT INTO media_items (
|
||||
item_id, title, description, content_type, tags,
|
||||
has_visual, embedding, image_url, metadata, created_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
"""
|
||||
|
||||
self.session.execute(
|
||||
query,
|
||||
(
|
||||
item_id,
|
||||
title,
|
||||
description,
|
||||
content_type,
|
||||
set(tags),
|
||||
has_visual,
|
||||
embedding,
|
||||
image_url,
|
||||
json.dumps(metadata) if metadata else None,
|
||||
datetime.utcnow()
|
||||
)
|
||||
)
|
||||
|
||||
def search_similar(
|
||||
self,
|
||||
keyspace: str,
|
||||
query_vector: List[float],
|
||||
content_type: Optional[str] = None,
|
||||
limit: int = 5
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Search media items using vector similarity.
|
||||
|
||||
Thanks to voyage-multimodal-3, this works for:
|
||||
- Text query -> Text results
|
||||
- Text query -> Image results (cross-modal)
|
||||
- Image query -> Text results (cross-modal)
|
||||
- Image query -> Image results
|
||||
|
||||
Args:
|
||||
keyspace: Keyspace name
|
||||
query_vector: Query embedding
|
||||
content_type: Optional filter by content type
|
||||
limit: Maximum results
|
||||
|
||||
Returns:
|
||||
List of matching media items
|
||||
"""
|
||||
self.session.set_keyspace(keyspace)
|
||||
|
||||
if content_type:
|
||||
query = """
|
||||
SELECT
|
||||
item_id, title, description, content_type, tags,
|
||||
has_visual, image_url,
|
||||
similarity_cosine(embedding, ?) AS similarity
|
||||
FROM media_items
|
||||
WHERE content_type = ?
|
||||
ORDER BY embedding ANN OF ?
|
||||
LIMIT ?
|
||||
"""
|
||||
rows = self.session.execute(query, (query_vector, content_type, query_vector, limit))
|
||||
else:
|
||||
query = """
|
||||
SELECT
|
||||
item_id, title, description, content_type, tags,
|
||||
has_visual, image_url,
|
||||
similarity_cosine(embedding, ?) AS similarity
|
||||
FROM media_items
|
||||
ORDER BY embedding ANN OF ?
|
||||
LIMIT ?
|
||||
"""
|
||||
rows = self.session.execute(query, (query_vector, query_vector, limit))
|
||||
|
||||
return [self._row_to_dict(row) for row in rows]
|
||||
|
||||
@staticmethod
|
||||
def _row_to_dict(row) -> Dict[str, Any]:
|
||||
"""Convert Cassandra row to dictionary."""
|
||||
return {
|
||||
"item_id": str(row.item_id),
|
||||
"title": row.title,
|
||||
"description": row.description,
|
||||
"content_type": row.content_type,
|
||||
"tags": list(row.tags) if row.tags else [],
|
||||
"has_visual": row.has_visual,
|
||||
"image_url": row.image_url,
|
||||
"similarity": float(row.similarity) if hasattr(row, 'similarity') and row.similarity else None
|
||||
}
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Main Application
|
||||
# ============================================================================
|
||||
|
||||
def main():
|
||||
"""Main application demonstrating multimodal vector search."""
|
||||
|
||||
print("\n" + "="*80)
|
||||
print(f"VoyageAI Multimodal ({Config.MULTIMODAL_MODEL}) + Cassandra Vector Search")
|
||||
print("="*80 + "\n")
|
||||
|
||||
# Validate configuration
|
||||
try:
|
||||
Config.validate()
|
||||
except ValueError as e:
|
||||
print(f"Configuration error: {e}")
|
||||
return 1
|
||||
|
||||
# Initialize components
|
||||
print("1. Initializing VoyageAI multimodal embedder...")
|
||||
print("-" * 80)
|
||||
|
||||
embedder = VoyageMultimodalEmbedder(
|
||||
api_key=Config.VOYAGE_API_KEY,
|
||||
model=Config.MULTIMODAL_MODEL,
|
||||
dimension=Config.EMBEDDING_DIMENSION
|
||||
)
|
||||
|
||||
vector_store = MultimodalVectorStore(
|
||||
hosts=Config.CASSANDRA_HOSTS,
|
||||
port=Config.CASSANDRA_PORT,
|
||||
username=Config.CASSANDRA_USERNAME,
|
||||
password=Config.CASSANDRA_PASSWORD
|
||||
)
|
||||
|
||||
try:
|
||||
vector_store.connect()
|
||||
|
||||
# Setup schema
|
||||
print("\n2. Setting up Cassandra schema...")
|
||||
print("-" * 80)
|
||||
|
||||
vector_store.setup_schema(
|
||||
keyspace=Config.CASSANDRA_KEYSPACE,
|
||||
dimension=Config.EMBEDDING_DIMENSION,
|
||||
replication_factor=1
|
||||
)
|
||||
|
||||
# Generate and store embeddings
|
||||
print(f"\n3. Generating multimodal embeddings with {Config.MULTIMODAL_MODEL}...")
|
||||
print("-" * 80)
|
||||
|
||||
for item in SAMPLE_MEDIA_ITEMS:
|
||||
# Generate embedding (handles both text and image content)
|
||||
embedding = embedder.embed_multimodal_item(item)
|
||||
|
||||
item_id = uuid.uuid4()
|
||||
vector_store.insert_media_item(
|
||||
keyspace=Config.CASSANDRA_KEYSPACE,
|
||||
item_id=item_id,
|
||||
title=item["title"],
|
||||
description=item["description"],
|
||||
content_type=item["content_type"],
|
||||
tags=item["tags"],
|
||||
has_visual=item.get("has_visual", False),
|
||||
embedding=embedding,
|
||||
image_url=item.get("image_url"),
|
||||
metadata={}
|
||||
)
|
||||
|
||||
modality = "text + image" if item.get("has_visual") else "text only"
|
||||
print(f" ✓ Embedded: {item['title']} ({modality})")
|
||||
|
||||
print(f"\n✓ Inserted {len(SAMPLE_MEDIA_ITEMS)} multimodal items")
|
||||
|
||||
# Perform multimodal searches
|
||||
print("\n4. Performing cross-modal similarity searches...")
|
||||
print("-" * 80)
|
||||
|
||||
# Search 1: Text query -> Find all content (including images)
|
||||
print("\n[A] Text Query -> All Content: 'beautiful natural scenery'")
|
||||
print("-" * 40)
|
||||
query_vector = embedder.embed_query("beautiful natural scenery")
|
||||
results = vector_store.search_similar(
|
||||
keyspace=Config.CASSANDRA_KEYSPACE,
|
||||
query_vector=query_vector,
|
||||
limit=4
|
||||
)
|
||||
|
||||
for i, result in enumerate(results, 1):
|
||||
visual_tag = " [IMAGE]" if result['has_visual'] else " [TEXT]"
|
||||
print(f"{i}. {result['title']}{visual_tag}")
|
||||
print(f" Type: {result['content_type']} | Similarity: {result['similarity']:.4f}")
|
||||
print(f" Description: {result['description'][:70]}...")
|
||||
print()
|
||||
|
||||
# Search 2: Cross-modal search - Text query -> Images only
|
||||
print("[B] Cross-Modal Search: Text query -> Image results")
|
||||
print(" Query: 'sunset over water'")
|
||||
print("-" * 40)
|
||||
query_vector = embedder.embed_query("sunset over water")
|
||||
results = vector_store.search_similar(
|
||||
keyspace=Config.CASSANDRA_KEYSPACE,
|
||||
query_vector=query_vector,
|
||||
content_type="image", # Filter to images only
|
||||
limit=3
|
||||
)
|
||||
|
||||
for i, result in enumerate(results, 1):
|
||||
print(f"{i}. {result['title']}")
|
||||
print(f" Similarity: {result['similarity']:.4f}")
|
||||
print(f" Tags: {', '.join(result['tags'])}")
|
||||
print()
|
||||
|
||||
# Search 3: Find programming tutorials
|
||||
print("[C] Text Search: 'learning to code and program'")
|
||||
print("-" * 40)
|
||||
query_vector = embedder.embed_query("learning to code and program")
|
||||
results = vector_store.search_similar(
|
||||
keyspace=Config.CASSANDRA_KEYSPACE,
|
||||
query_vector=query_vector,
|
||||
content_type="article",
|
||||
limit=3
|
||||
)
|
||||
|
||||
for i, result in enumerate(results, 1):
|
||||
print(f"{i}. {result['title']}")
|
||||
print(f" Similarity: {result['similarity']:.4f}")
|
||||
print()
|
||||
|
||||
# Search 4: Architecture and design images
|
||||
print("[D] Image Search: 'modern buildings and architecture'")
|
||||
print("-" * 40)
|
||||
query_vector = embedder.embed_query("modern buildings and architecture")
|
||||
results = vector_store.search_similar(
|
||||
keyspace=Config.CASSANDRA_KEYSPACE,
|
||||
query_vector=query_vector,
|
||||
content_type="image",
|
||||
limit=3
|
||||
)
|
||||
|
||||
for i, result in enumerate(results, 1):
|
||||
print(f"{i}. {result['title']}")
|
||||
print(f" Similarity: {result['similarity']:.4f}")
|
||||
print(f" URL: {result.get('image_url', 'N/A')}")
|
||||
print()
|
||||
|
||||
print("\n" + "="*80)
|
||||
print("SUCCESS: Multimodal vector search demonstration complete!")
|
||||
print("="*80)
|
||||
|
||||
print("\nKey Features Demonstrated:")
|
||||
print(f"✓ Real VoyageAI {Config.MULTIMODAL_MODEL} integration")
|
||||
print(f"✓ Text and images embedded in same {Config.EMBEDDING_DIMENSION}-dim vector space")
|
||||
print("✓ Cross-modal search (text queries find images, vice versa)")
|
||||
print("✓ Single vector column for both modalities")
|
||||
print("✓ Content-type filtering for hybrid search")
|
||||
print("✓ COSINE similarity for normalized embeddings")
|
||||
if "3.5" in Config.MULTIMODAL_MODEL:
|
||||
print("✓ Variable dimensions (256, 512, 1024, 2048)")
|
||||
print("✓ Video embedding support (requires voyageai >= 0.3.6)")
|
||||
|
||||
print("\nProduction Use Cases:")
|
||||
print("- E-commerce: Text search returns product images")
|
||||
print("- Media libraries: Find photos by description")
|
||||
print("- Document search: Images in PDFs/slides/documents")
|
||||
print("- Video retrieval: Find video content by text query")
|
||||
print("- Visual Q&A: Natural language queries for visual content")
|
||||
|
||||
return 0
|
||||
|
||||
except Exception as e:
|
||||
print(f"\nError: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return 1
|
||||
|
||||
finally:
|
||||
vector_store.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue