← Files Azure Cosmos DBARCHIVED FILE

skills/cosmosdb-best-practices/rules/vector-repository-pattern.md

13.3 KB · Oct 5, 2026 · 18:19 UTC

↓ Download file

---
title: Implement Repository Pattern for Vector Search
impact: HIGH
impactDescription: Provides clean abstraction for vector operations and data access
tags: vector, repository, pattern, architecture, vector-search
---

## Implement Repository Pattern for Vector Search

**Impact: HIGH (Clean abstraction for vector operations)**

When implementing vector search, use a repository pattern to encapsulate Cosmos DB operations. This separates data access logic from business logic and makes vector search operations testable and maintainable.

**Key Methods to Implement:**
1. **insert_document/upsert_document** - Store documents with embeddings
2. **vector_search** - Perform similarity search with VectorDistance()
3. **get_document** - Point read by ID and partition key
4. **delete_document** - Remove documents

**Incorrect (direct container access in application code):**

```python
# Python - BAD: Direct container access scattered throughout app
@app.post("/api/search")
async def search(request: SearchRequest):
    # Vector search logic mixed with API logic
    query = f"""
        SELECT TOP {request.limit} c.title, 
               VectorDistance(c.embedding, @embedding) AS score
        FROM c ORDER BY VectorDistance(c.embedding, @embedding)
    """
    results = container.query_items(query, parameters=[...])
    # No abstraction, hard to test, tightly coupled
```

```csharp
// .NET - BAD: No separation of concerns
public class DocumentService {
    public async Task<List<Doc>> Search(float[] embedding) {
        // Direct container access, no abstraction
        var query = new QueryDefinition(...);
        var iterator = _container.GetItemQueryIterator<Doc>(query);
        // Mixing infrastructure concerns with business logic
    }
}
```

**Correct (repository pattern with clean abstraction):**

```python
# Python - GOOD: Repository pattern
class DocumentRepository:
    """Repository for documents with vector search capabilities"""
    
    def __init__(self, container: ContainerProxy):
        self.container = container
    
    async def insert_document(self, document: DocumentChunk) -> DocumentChunk:
        """Insert document with vector embedding."""
        try:
            doc_dict = document.dict()
            created_item = self.container.upsert_item(body=doc_dict)
            return DocumentChunk(**created_item)
        except CosmosHttpResponseError as e:
            logger.error(f"Failed to insert document: {e.message}")
            raise
    
    async def vector_search(
        self,
        query_embedding: List[float],
        limit: int = 5,
        similarity_threshold: float = 0.0,
        category_filter: Optional[str] = None
    ) -> List[DocumentChunk]:
        """Perform vector similarity search with VectorDistance()."""
        try:
            # Build parameterized query
            query = """
                SELECT TOP @limit 
                    c.id, c.title, c.content, c.category, c.metadata,
                    VectorDistance(c.embedding, @queryVector) AS similarityScore
                FROM c
                WHERE VectorDistance(c.embedding, @queryVector) > @threshold
            """
            
            # Add optional filters
            if category_filter:
                query += " AND c.category = @category"
            
            query += " ORDER BY VectorDistance(c.embedding, @queryVector)"
            
            # Build parameters
            parameters = [
                {"name": "@queryVector", "value": query_embedding},
                {"name": "@limit", "value": limit},
                {"name": "@threshold", "value": similarity_threshold}
            ]
            
            if category_filter:
                parameters.append({"name": "@category", "value": category_filter})
            
            # Execute query
            items = list(self.container.query_items(
                query=query,
                parameters=parameters,
                enable_cross_partition_query=True,
                populate_query_metrics=True
            ))
            
            # Convert to domain models
            results = []
            for item in items:
                score = item.pop('similarityScore', 0.0)
                if 'metadata' not in item:
                    item['metadata'] = {}
                item['metadata']['similarityScore'] = score
                item['embedding'] = []  # Exclude from response for performance
                results.append(DocumentChunk(**item))
            
            return results
            
        except CosmosHttpResponseError as e:
            logger.error(f"Vector search failed: {e.message}")
            raise
    
    async def get_document(self, document_id: str, category: str) -> Optional[DocumentChunk]:
        """Point read with partition key."""
        try:
            item = self.container.read_item(
                item=document_id,
                partition_key=category
            )
            return DocumentChunk(**item)
        except CosmosHttpResponseError as e:
            if e.status_code == 404:
                return None
            raise

# Usage in application
@app.post("/api/search")
async def search(request: SearchRequest):
    results = await document_repo.vector_search(
        query_embedding=request.embedding,
        limit=request.top_k,
        category_filter=request.category
    )
    return {"results": results}
```

```csharp
// .NET - GOOD: Repository pattern
public interface IDocumentRepository
{
    Task<DocumentChunk> InsertDocumentAsync(DocumentChunk document);
    Task<List<DocumentChunk>> VectorSearchAsync(
        float[] queryEmbedding, 
        int limit = 5, 
        double similarityThreshold = 0.0, 
        string? categoryFilter = null);
    Task<DocumentChunk?> GetDocumentAsync(string id, string category);
}

public class DocumentRepository : IDocumentRepository
{
    private readonly Container _container;
    private readonly ILogger<DocumentRepository> _logger;

    public DocumentRepository(Container container, ILogger<DocumentRepository> logger)
    {
        _container = container;
        _logger = logger;
    }

    public async Task<DocumentChunk> InsertDocumentAsync(DocumentChunk document)
    {
        try
        {
            var response = await _container.UpsertItemAsync(
                item: document,
                partitionKey: new PartitionKey(document.Category)
            );
            _logger.LogInformation("Inserted document {Id}", document.Id);
            return response.Resource;
        }
        catch (CosmosException ex)
        {
            _logger.LogError(ex, "Failed to insert document {Id}", document.Id);
            throw;
        }
    }

    public async Task<List<DocumentChunk>> VectorSearchAsync(
        float[] queryEmbedding, 
        int limit = 5,
        double similarityThreshold = 0.0, 
        string? categoryFilter = null)
    {
        try
        {
            // Build query
            var queryText = @"
                SELECT TOP @limit 
                    c.id, c.title, c.content, c.category, c.metadata,
                    VectorDistance(c.embedding, @queryVector) AS similarityScore
                FROM c
                WHERE VectorDistance(c.embedding, @queryVector) > @threshold";

            if (!string.IsNullOrEmpty(categoryFilter))
            {
                queryText += " AND c.category = @category";
            }

            queryText += " ORDER BY VectorDistance(c.embedding, @queryVector)";

            // Build query definition
            var queryDef = new QueryDefinition(queryText)
                .WithParameter("@queryVector", queryEmbedding)
                .WithParameter("@limit", limit)
                .WithParameter("@threshold", similarityThreshold);

            if (!string.IsNullOrEmpty(categoryFilter))
            {
                queryDef = queryDef.WithParameter("@category", categoryFilter);
            }

            // Execute query
            var results = new List<DocumentChunk>();
            using var iterator = _container.GetItemQueryIterator<DocumentChunk>(queryDef);

            while (iterator.HasMoreResults)
            {
                var response = await iterator.ReadNextAsync();
                results.AddRange(response);
                
                // Log RU consumption
                _logger.LogDebug("Vector search consumed {RU} RUs", 
                    response.RequestCharge);
            }

            return results;
        }
        catch (CosmosException ex)
        {
            _logger.LogError(ex, "Vector search failed");
            throw;
        }
    }

    public async Task<DocumentChunk?> GetDocumentAsync(string id, string category)
    {
        try
        {
            var response = await _container.ReadItemAsync<DocumentChunk>(
                id: id,
                partitionKey: new PartitionKey(category)
            );
            return response.Resource;
        }
        catch (CosmosException ex) when (ex.StatusCode == System.Net.HttpStatusCode.NotFound)
        {
            return null;
        }
    }
}

// Usage in service/controller
public class SearchService
{
    private readonly IDocumentRepository _repository;

    public SearchService(IDocumentRepository repository)
    {
        _repository = repository;
    }

    public async Task<List<DocumentChunk>> SearchAsync(SearchRequest request)
    {
        return await _repository.VectorSearchAsync(
            queryEmbedding: request.Embedding,
            limit: request.TopK,
            categoryFilter: request.Category
        );
    }
}
```

```javascript
// JavaScript/TypeScript - GOOD: Repository pattern
class DocumentRepository {
    constructor(private container: Container) {}

    async insertDocument(document: DocumentChunk): Promise<DocumentChunk> {
        try {
            const { resource } = await this.container.items.upsert(document);
            console.log(`Inserted document ${resource.id}`);
            return resource;
        } catch (error) {
            console.error('Failed to insert document:', error);
            throw error;
        }
    }

    async vectorSearch(
        queryEmbedding: number[],
        options: {
            limit?: number;
            similarityThreshold?: number;
            categoryFilter?: string;
        } = {}
    ): Promise<DocumentChunk[]> {
        const { limit = 5, similarityThreshold = 0.0, categoryFilter } = options;

        try {
            let query = `
                SELECT TOP @limit 
                    c.id, c.title, c.content, c.category, c.metadata,
                    VectorDistance(c.embedding, @queryVector) AS similarityScore
                FROM c
                WHERE VectorDistance(c.embedding, @queryVector) > @threshold
            `;

            const parameters = [
                { name: '@queryVector', value: queryEmbedding },
                { name: '@limit', value: limit },
                { name: '@threshold', value: similarityThreshold }
            ];

            if (categoryFilter) {
                query += ' AND c.category = @category';
                parameters.push({ name: '@category', value: categoryFilter });
            }

            query += ' ORDER BY VectorDistance(c.embedding, @queryVector)';

            const { resources } = await this.container.items
                .query({
                    query,
                    parameters
                })
                .fetchAll();

            return resources.map(item => ({
                ...item,
                embedding: [] // Exclude for performance
            }));
        } catch (error) {
            console.error('Vector search failed:', error);
            throw error;
        }
    }

    async getDocument(id: string, category: string): Promise<DocumentChunk | null> {
        try {
            const { resource } = await this.container.item(id, category).read();
            return resource;
        } catch (error: any) {
            if (error.code === 404) {
                return null;
            }
            throw error;
        }
    }
}

// Usage
const documentRepo = new DocumentRepository(container);
const results = await documentRepo.vectorSearch(embedding, { 
    limit: 10, 
    categoryFilter: 'ai' 
});
```

**Benefits:**
- ✅ Testable - Mock repository in unit tests
- ✅ Maintainable - Vector search logic in one place
- ✅ Reusable - Use repository across multiple services
- ✅ Clean separation - Infrastructure vs business logic
- ✅ Easier to optimize - Centralized query performance tuning

**Best Practices:**
1. Use `upsert_item` for idempotent inserts
2. Always parameterize queries (never concatenate embeddings)
3. Include `ORDER BY VectorDistance()` for ranked results
4. Exclude embeddings from SELECT when not needed (performance)
5. Log RU consumption for monitoring
6. Handle 404 errors gracefully (return null, not exception)
7. Use domain models (not raw dictionaries/dynamic)

**Related Rules:**
- vector-distance-query.md - VectorDistance() usage
- query-parameterize.md - Always use parameters
- query-use-projections.md - Exclude unnecessary fields

SHA-256: 71fc52761ef55ac6791fc650fb12a0ef321fb56af5f1f9e6e6d153899b16461c