This document explains the internal design of VectorDB-Q, focusing on the service layer architecture and how components work together.
VectorDB-Q follows SOLID principles and Domain-Driven Design:
- Single Responsibility - Each service handles one domain
- Separation of Concerns - Clear boundaries between layers
- Dependency Injection - Services receive dependencies, not create them
- Testability - All services can be tested with mocked dependencies
┌─────────────────────────────────────────────────────────┐
│ API Routes │
│ • Handle HTTP requests/responses │
│ • Validate input │
│ • Call appropriate service │
└────────────────────┬────────────────────────────────────┘
│ Dependency Injection
┌────────────────────▼────────────────────────────────────┐
│ Service Layer │
│ • LibraryService - Library CRUD + index creation │
│ • ChunkService - Chunk CRUD + index updates │
│ • SearchService - Vector search + metadata filtering │
└────────────────────┬────────────────────────────────────┘
│ Data Access
┌────────────────────▼────────────────────────────────────┐
│ Repository Layer │
│ • LibraryRepository - Library data persistence │
│ • ChunkRepository - Chunk/document storage │
│ • IndexRepository - Vector index management │
└─────────────────────────────────────────────────────────┘
Purpose: Manage libraries and their indexes
Dependencies:
LibraryRepository- Store/retrieve library dataIndexRepository- Create/manage vector indexesChunkRepository- Get documents for library hydration
Key Methods:
create_library(library_create) -> Library
• Creates library in repository
• Creates corresponding vector index
• Returns library with index type
get_library(library_id) -> Library | None
• Retrieves library from repository
• Hydrates with documents from ChunkRepository
• Returns complete library object
list_libraries() -> List[Library]
• Gets all libraries
• Hydrates each with their documents
• Returns complete library list
update_library(library_id, update_data) -> Library | None
• Updates library metadata
• If index type changes, recreates index
• Returns updated library
delete_library(library_id) -> bool
• Removes library from repository
• Deletes associated index
• Returns success statusPurpose: Manage text chunks and keep indexes in sync
Dependencies:
ChunkRepository- Store/retrieve chunksLibraryService- Validate library existsIndexRepository- Update vector indexes
Key Methods:
create_chunk(library_id, chunk_create) -> Chunk | None
• Validates library exists
• Creates chunk in repository
• Adds vector to library's index
• Returns created chunk
get_chunk(library_id, chunk_id) -> Chunk | None
• Retrieves chunk from repository
• Returns chunk or None if not found
list_chunks(library_id) -> List[Chunk]
• Gets all chunks in library
• Returns list (empty if none)
update_chunk(library_id, chunk_id, update_data) -> Chunk | None
• Updates chunk in repository
• If embedding changed, updates index
• Returns updated chunk
delete_chunk(library_id, chunk_id) -> bool
• Removes chunk from repository
• Removes vector from index
• Returns success statusPurpose: Perform semantic search with filtering
Dependencies:
LibraryService- Validate library existsChunkRepository- Get chunk detailsIndexRepository- Search vector index
Key Methods:
search(library_id, search_query) -> List[SearchResult]
• Validates library exists
• Searches index with query vector
• Retrieves chunk details for results
• Applies metadata filters
• Sorts by relevance score
• Returns top K results
_matches_filters(chunk, filters) -> bool
• Helper to check metadata filters
• Supports dot notation (e.g., "custom.category")
• Case-insensitive string matching
• Returns True if all filters matchAll services are created with dependency injection using FastAPI's dependency system:
# app/api/dependencies.py
# Repository Singletons (one instance per app)
@lru_cache()
def get_library_repository() -> LibraryRepository:
return LibraryRepository(persistence_path=settings.persistence_path)
@lru_cache()
def get_chunk_repository() -> ChunkRepository:
return ChunkRepository(persistence_path=settings.persistence_path)
@lru_cache()
def get_index_repository() -> IndexRepository:
return IndexRepository()
# Service Factory Functions (new instance per request)
def get_library_service() -> LibraryService:
return LibraryService(
repository=get_library_repository(),
index_repository=get_index_repository(),
chunk_repository=get_chunk_repository()
)
def get_chunk_service() -> ChunkService:
return ChunkService(
repository=get_chunk_repository(),
library_service=get_library_service(),
index_repository=get_index_repository()
)
def get_search_service() -> SearchService:
return SearchService(
library_service=get_library_service(),
chunk_repository=get_chunk_repository(),
index_repository=get_index_repository()
)Why This Works:
- Repositories are singletons (
@lru_cache) - Only one instance per app - Services are created per request - Fresh instance with injected dependencies
- No global state - Dependencies passed explicitly
- Easy to test - Can inject mocks instead of real repositories
Storage: In-memory dictionary {library_id: Library}
Persistence: Saves to data/vectordb/libraries.pkl
Thread-Safe: Uses RWLock for concurrent access
Key Features:
- CRUD operations for libraries
- Atomic file writes (temp file + rename)
- Automatic persistence on changes
Storage: Nested dictionaries {library_id: {chunk_id: Chunk}}
Documents: Separate storage {library_id: [Document]}
Persistence: Saves to data/vectordb/chunks.pkl
Thread-Safe: Uses RWLock for concurrent access
Key Features:
- CRUD operations for chunks
- Document management per library
- Batch operations for efficiency
- Atomic file writes
Storage: In-memory dictionary {library_id: VectorIndex}
Persistence: No persistence (rebuilt from chunks on startup)
Thread-Safe: Uses RWLock for concurrent access
Key Features:
- Creates Flat or IVF indexes
- Add/update/remove vectors dynamically
- Rebuild indexes from source data
- K-NN search with cosine or euclidean distance
Client Request → API Route → ChunkService
│
├─→ LibraryService.get_library()
│ └─→ LibraryRepository.get()
│
├─→ ChunkRepository.create_chunk()
│ └─→ Save to pickle file
│
└─→ IndexRepository.add_vector()
└─→ Update in-memory index
Response ← Chunk Object
Client Request → API Route → SearchService
│
├─→ LibraryService.get_library()
│ └─→ LibraryRepository.get()
│
├─→ IndexRepository.search_library()
│ └─→ K-NN search in index
│
├─→ ChunkRepository.get_chunk() (for each result)
│ └─→ Get chunk details
│
└─→ Apply filters, sort by score
Response ← List[SearchResult]
Client Request → API Route → LibraryService.get_library()
│
├─→ LibraryRepository.get()
│ └─→ Get library object
│
└─→ _hydrate_library()
└─→ ChunkRepository.get_documents_for_library()
└─→ Populate library.documents field
Response ← Library (with documents)
Each service has comprehensive unit tests with mocked dependencies:
# tests/services/test_library_service.py
@pytest.fixture
def mock_library_repository():
return Mock() # Mock repository
@pytest.fixture
def library_service(mock_library_repository, ...):
return LibraryService(
repository=mock_library_repository,
index_repository=mock_index_repository,
chunk_repository=mock_chunk_repository
)
def test_create_library_success(library_service, mock_library_repository):
# Arrange: Set up mocks
mock_library_repository.create_library.return_value = sample_library
# Act: Call service method
result = library_service.create_library(library_create)
# Assert: Verify behavior
assert result is not None
mock_library_repository.create_library.assert_called_once()- LibraryService: 13 tests (create, get, list, update, delete + edge cases)
- ChunkService: 13 tests (CRUD + index synchronization)
- SearchService: 15 tests (search, filtering, metrics, edge cases)
- Total: 41 passing tests with 100% service layer coverage
Each service does one thing:
- LibraryService → Libraries
- ChunkService → Chunks
- SearchService → Search
Mock dependencies and test in isolation:
mock_repo = Mock()
service = ChunkService(repository=mock_repo, ...)
# Test service logic without touching databaseWant to add a new feature? Add it to the right service:
- Batch chunk upload? → Add to
ChunkService - Advanced library search? → Add to
LibraryService - New search algorithm? → Add to
SearchService
Know exactly where to look:
- Bug in library creation? →
LibraryService - Search returning wrong results? →
SearchService - Data not persisting? → Repository layer
Each file has one clear purpose:
library_service.py- ~150 lineschunk_service.py- ~130 linessearch_service.py- ~150 lines
Compare to old monolithic vectordb_service.py (~400+ lines)!
Before: One VectorDBService handled everything
After: Three focused services, each with clear responsibility
Reason: Easier to understand, test, and modify
Before: Services created their own repositories
After: Dependencies passed in via constructor
Reason: Can inject mocks for testing, easier to swap implementations
Before: New repository instance per request
After: One repository instance per app
Reason: Repositories manage shared state (in-memory data), should be shared
Before: Libraries didn't include documents
After: Service layer hydrates libraries with documents
Reason: API responses need complete data, but storage is separated
Question: Where do I add library export functionality?
-
Service Layer (
app/services/library_service.py):def export_library(self, library_id: UUID) -> Dict: library = self.repository.get_library(library_id) documents = self.chunk_repository.get_documents_for_library(library_id) return {"library": library, "documents": documents}
-
API Route (
app/api/v1/libraries.py):@router.get("/{library_id}/export") def export_library( library_id: UUID, service: LibraryService = Depends(get_library_service) ): return service.export_library(library_id)
-
Test (
tests/services/test_library_service.py):def test_export_library(library_service, mock_repository): # Test the export logic with mocks
Issue: Search returns wrong results
-
Start at Service (
app/services/search_service.py):- Check
search()method logic - Verify filter matching in
_matches_filters()
- Check
-
Check Repository (
app/repositories/index_repository.py):- Verify
search_library()implementation - Check index building in
rebuild_index()
- Verify
-
Verify Data (
app/repositories/chunk_repository.py):- Ensure chunks are stored correctly
- Check embeddings are valid
- README.md - Project overview and quick start
- API_REFERENCE.md - Complete API documentation
- TESTING_GUIDE.md - How to run and write tests
- Services = Business Logic - Each service handles one domain
- Repositories = Data Access - Each repository manages one type of data
- Dependency Injection - Dependencies passed in, not created
- Testability - Mock dependencies to test in isolation
- SOLID Principles - Single Responsibility, easy to maintain
This architecture makes VectorDB-Q easy to understand, test, and extend! 🚀