Architecture
Component overview, dependency injection, and request lifecycle for the Live Services Engine.
Component map
┌──────────────────────────────────────────────────────┐
│ FastAPI (main.py) │
│ │
│ /exq/search/ ──► SearchService ──► Strategy │
│ /exq/item/ ──► ItemService │
│ /exq/ ──► Admin handlers │
│ /health ──► Health check │
└───────────────────────┬──────────────────────────────┘
│ ApplicationContainer (DI)
┌─────────────┼─────────────────┐
▼ ▼ ▼
ModelManager IndexRepository DatabaseRepository
(CLIP model) (FAISS / Zarr) (SQLite per collection)
Layers
API layer (app/api/)
FastAPI route handlers. Each router module groups related endpoints:
| Module | Prefix | Responsibility |
|---|---|---|
routes/search.py |
/exq/search/ |
Dispatch to search strategies |
routes/items.py |
/exq/item/ |
Item metadata retrieval |
routes/admin.py |
/exq/ |
Session init, filter discovery, audit logging |
Route handlers are thin: they validate the incoming request (via Pydantic schemas), call the appropriate service, schedule audit log entries as background tasks, and return the response.
Service layer (app/services/)
Business logic lives here, decoupled from HTTP concerns.
| Service | Responsibility |
|---|---|
SearchService |
Strategy selection, wall-clock timing, result assembly |
ItemService |
Item metadata queries |
LoggingService |
Async audit log writes via FastAPI BackgroundTasks |
Strategy layer (app/strategies/)
Each search mode is implemented as an independent strategy class that inherits from BaseSearchStrategy.
| Strategy | Class | Description |
|---|---|---|
| CLIP | CLIPSearchStrategy |
Text encoding + index search |
| RF | RFSearchStrategy |
SVM training + index search |
| Faceted | FacetedSearchStrategy |
SQL filter compilation + database query |
SearchService owns one instance of each strategy and routes requests by name.
Repository layer (app/repositories/)
Stateful wrappers around external data stores. Initialised once and held for the process lifetime.
| Repository | Responsibility |
|---|---|
IndexRepository |
Load and query FAISS / Zarr vector indices |
DatabaseRepository |
Open and query SQLite databases |
Core layer (app/core/)
Infrastructure and cross-cutting concerns.
| Module | Responsibility |
|---|---|
config.py |
INI file loading and Pydantic validation |
models.py |
ModelManager (CLIP) and ApplicationContainer (DI) |
indexes.py |
Abstract index interfaces (BaseIndex, FaissIndex, ZarrIndex) |
exceptions.py |
Custom exception hierarchy |
Dependency injection
ApplicationContainer acts as a singleton DI container. It is created once during the FastAPI lifespan and injected into routes via app/api/dependencies.py.
# Simplified lifespan (main.py)
@asynccontextmanager
async def lifespan(app: FastAPI):
container = ApplicationContainer()
container.initialize() # loads config, model, DBs, indices
app.state.container = container
yield
container.shutdown()
Route handlers receive the container (or individual services) via Depends():
@router.post("/clip")
async def clip_search(
request: TextSearchRequest,
service: SearchService = Depends(get_search_service),
):
return await service.search("clip", request)
Request lifecycle
For a CLIP search request the flow is:
POST /exq/search/clip
│
├─ Pydantic validates TextSearchRequest
│
├─ SearchService.search("clip", request)
│ ├─ Record request_timestamp
│ ├─ CLIPSearchStrategy.execute(request)
│ │ ├─ Tokenise + encode text → embedding
│ │ ├─ Build skip set (seen ∪ excluded ∪ filtered)
│ │ ├─ IndexRepository.search_clip(embedding, n, skip)
│ │ └─ DatabaseRepository.get_media_ids(index_ids)
│ └─ Record completion_time
│
├─ BackgroundTask: LoggingService.log_search_request(...)
│
└─ Return SearchResponse
Exception handling
All custom exceptions inherit from LSEException. A global FastAPI exception handler catches them, returns HTTP 400 with a structured JSON body, and logs the event to the audit log:
{
"error": "CollectionNotFound",
"message": "Collection 'unknown' is not enabled"
}
See app/core/exceptions.py for the full hierarchy.
Startup sequence
- Parse
./data/config.ini - Validate all collection configs (Pydantic)
- Select compute device (CUDA → MPS → CPU)
- Load CLIP model (from disk cache or download)
- For each enabled collection:
- Open SQLite database
- Load Zarr embeddings or FAISS index
- Initialise
LoggingService(ifLogDirectoryis set) - Server begins accepting requests
Shutdown sequence
- Signal
LoggingServiceto flush and stop - Close all SQLite connections
- Release GPU memory