This document describes the system design, component responsibilities, and data flow of embedding-clusters.
The application converts CSV data into interactive 3D embedding
visualizations. It has three running modes, all dispatched from a single
entry point (python -m embedding_cluster):
| Mode | Entry | Purpose |
|---|---|---|
SERVER |
server/app.py |
FastAPI backend + React SPA |
INDEX |
indexer.py |
CLI embedding pipeline |
PLOT |
scatter_plot.py |
CLI cluster visualization |
__main__.py
/ | \
/ | \
INDEX SERVER PLOT
| | |
indexer.py FastAPI scatter_plot.py
| / | \ |
| routes | SPA |
| | |
+--- ChromaDB --------+
All configuration is driven by environment variables, parsed by
pydantic-settings BaseSettings. Each setting has a Field() with a
default value and description. List fields accept JSON-encoded strings
(e.g. '["field1","field2"]').
Responsible for the INDEX mode and also used by the server's indexing route.
- Read CSV rows (with optional start/stop line range)
- Load embedding models:
- SentenceTransformer for text fields
- CLIP (via HuggingFace Transformers) for image URL fields
- Generate embeddings in batches with semaphore-controlled concurrency
- Store embeddings + metadata in ChromaDB collections
- Report progress via callback (used by WebSocket in server mode)
- Support cancellation via
asyncio.Event
Images are downloaded asynchronously with exponential backoff retry
(up to 6 attempts) using a singleton ImageDownloader backed by
aiohttp.ClientSession.
Responsible for the PLOT mode and used by the server's plot route.
- Load embeddings from a ChromaDB collection
- Standardize with
StandardScaler - Reduce dimensions using t-SNE, UMAP, or PCA
- Cluster with KMeans
- Compute silhouette scores, centroids, and per-point distances
- Return structured point and cluster data
Additional capabilities:
- Optimal cluster suggestion — evaluates k=2..30 with inertia and silhouette scores
- Sub-clustering — re-run KMeans within a single cluster or on a selected subset of points
Uses LiteLLM as a universal gateway to call any LLM provider (OpenAI, Google, Anthropic, Ollama) with a single interface. Generates short (max 5 words) descriptive names for clusters based on sampled items.
Persists cluster metadata (name, notes, tags) as JSON sidecar files in
./annotations/, one file per plot job. The AnnotationManager handles
read/write with automatic timestamping.
- Logging — colored console formatter
- ChromaDB helpers — collection creation, batch document initialization
- ImageDownloader — singleton async image fetcher with retry logic
- ID generator — random alphanumeric IDs for jobs and documents
The SERVER mode runs a FastAPI application that serves both the REST API
and the built React SPA.
create_app() assembles the FastAPI app:
- Registers all API route modules under
/api - Adds CORS middleware for frontend dev server (
localhost:5173) - Serves the React SPA from
frontend/dist(if built), with catch-all fallback toindex.htmlfor client-side routing
Long-running operations (indexing, plot computation) run as background
async tasks tracked by an in-memory TaskRegistry:
- Each job gets a unique ID and a
TaskStatewith status, progress dict, result, error, and a cancel event - Status lifecycle:
PENDING→RUNNING→COMPLETED|FAILED|CANCELLED - Clients poll status via REST or subscribe via WebSocket
Manages per-job WebSocket connections for real-time progress streaming. Broadcasts JSON messages (progress, log, heartbeat, completed, error) to all connected clients for a given job ID.
| Route module | Prefix | Responsibility |
|---|---|---|
csv.py |
/api/csv |
Upload and preview CSV files |
index.py |
/api/index |
Start/cancel indexing jobs, WebSocket progress |
collections.py |
/api/collections |
List, detail, delete ChromaDB collections |
plot.py |
/api/plot |
Compute plots, cluster detail, sub-clustering, suggest k |
search.py |
/api/search |
Semantic search (text or image query) |
ai.py |
/api/ai |
LLM cluster naming, connection testing, Ollama proxy |
annotations.py |
/api/annotations |
CRUD for cluster annotations |
All API contracts are defined as Pydantic models. The frontend TypeScript
types in frontend/src/types/index.ts mirror these models.
The frontend is a React 19 SPA built with Vite and Tailwind CSS 4.
Four pages mapped via React Router:
| Path | Page | Purpose |
|---|---|---|
/ |
HomePage |
Collection browser, quick actions |
/index |
IndexPage |
CSV upload, embedding config, progress |
/plot |
PlotPage |
3D visualization, search, annotations |
/settings |
SettingsPage |
AI provider configuration |
- Zustand (
stores/plotStore.ts) — single store for all plot-related state: points, clusters, visibility, search results, drill-down path, annotations, render mode, algorithm parameters - TanStack React Query — server state (collections, plot data polling)
Uses React Three Fiber
(@react-three/fiber) with drei helpers. Three render modes:
- Particles — GPU-accelerated point cloud (default, best performance)
- Sprites — image thumbnails at each point (when image field available)
- Instanced Spheres — 3D sphere meshes with lighting
Typed fetch wrappers organized by domain (client.ts, indexing.ts,
plot.ts, ai.ts, collections.ts, csv.ts). All requests go through
a shared apiFetch<T>() utility with error handling.
useIndexWebSocket— real-time indexing progress with stuck detection (warning after 15s, error after 30s of silence)usePlotData— starts plot computation, polls for results every 2s
Browser Server Storage
| | |
|-- POST /csv/upload ---------->| |
|<---- filename, columns -------| |
| | |
|-- POST /index/start -------->| |
|<---- job_id ------------------| |
| |-- load models |
|== WS /index/ws/{job_id} ====>| |
| |-- read CSV |
|<--- progress messages --------|-- embed rows ---------->|
|<--- log messages -------------|-- store in ChromaDB --->|
|<--- completed message --------| |
Browser Server Storage
| | |
|-- POST /plot/compute -------->| |
|<---- job_id ------------------| |
| |-- load embeddings <----|
|-- GET /plot/data/{id} ------->|-- reduce dimensions |
|<---- ready: false ------------|-- KMeans clustering |
|-- GET /plot/data/{id} ------->|-- compute centroids |
|<---- ready: true, data -------| |
| | |
|-- render 3D scene | |
Browser Server Storage
| | |
|-- POST /search -------------->| |
| |-- infer model type |
| |-- embed query |
| |-- ChromaDB.query() <----|
|<---- results + distances -----| |
| | |
|-- highlight in 3D scene | |
| Directory | Contents | Persistence |
|---|---|---|
./chromadb/ |
Vector database (embeddings + metadata) | Persistent, gitignored |
./uploads/ |
User-uploaded CSV files | Persistent, gitignored |
./annotations/ |
Cluster annotation JSON files | Persistent, gitignored |
ChromaDB provides embedded vector storage with no external dependencies. Collections persist to disk automatically, support metadata filtering, and offer nearest-neighbor search out of the box — exactly what this tool needs without requiring a separate database server.
Rather than coupling to a single LLM provider, LiteLLM provides a unified interface to OpenAI, Google, Anthropic, and Ollama. Users can switch providers from the settings page without code changes.
The 3D visualization needs to render thousands of points interactively. React Three Fiber provides a React-native API over Three.js, enabling declarative scene composition while retaining GPU-level performance through instanced rendering and point clouds.
Embedding generation and plot computation can take seconds to minutes. The task registry pattern decouples request handling from execution, allowing the frontend to poll or subscribe via WebSocket without blocking HTTP connections.