Make a corpus of synthetic clinical records semantically searchable, end to end.
A user types a natural-language description:
recurring headaches preceded by flashing lights, nausea, and sensitivity to light
and gets a ranked list of patients in their own practice whose existing notes and reports are relevant, each with the specific document and passage that explains the match.
This is retrieval. It must not generate a diagnosis, infer a condition that is not already in the records, or present a similarity score as clinical confidence.
Read docs/TAKE_HOME_DESIGN.md first. It is the
specification; this file is the operating manual.
Requirements: Docker with Compose v2. Nothing else — Python, Node, pnpm, and the embedding model all live inside the images.
cp .env.example .env
make setup # build images, start services, apply migrations
make seed # load the synthetic dataset
make index # build or incrementally refresh the semantic index
make dev # web on http://localhost:3000, API on http://localhost:8000In another shell:
make test # backend and frontend suites
make smoke # confirm database, seed data, and a real embedding callThe first make index run embeds the full dataset and can take several minutes on CPU.
Later runs skip unchanged documents and only rebuild new, changed, or version-stale records.
Without make (Windows PowerShell, or a bare shell)
Every target is one or two Compose commands.
Copy-Item .env.example .env
docker compose build
docker compose up -d db embedding api
docker compose run --rm api python -m app.scripts.wait_for_dependencies
docker compose exec -T api python -m app.scripts.migrate
docker compose exec -T api python -m app.scripts.migrate --database test
docker compose exec -T api python -m app.scripts.seed
docker compose up # make dev
docker compose exec -T api pytest -q # make test-api
docker compose run --rm --no-deps web pnpm test # make test-webmake help lists every target with its description.
The first build downloads the embedding model (~90 MB) into the image. Later builds are cached, and nothing reaches the network at run time.
| Area | State |
|---|---|
| Next.js 16 app shell, layout, navigation, design-system primitives | Provided |
| Mock session with a practice switcher | Provided |
Patient detail route /patients/[patientId] |
Provided and working |
/search route |
Implemented with validation, loading, empty, result, and failure states |
| FastAPI service, config, connection pool, error envelope, request logging | Provided |
| Migration runner, seed loader, health endpoint | Provided |
| Embedding service (ONNX MiniLM, 384-dim) | Provided; extended with exact, untruncated token counts |
| Embedding client with batching and error translation | Extended for token counting and contract checks |
| Synthetic dataset: 3 practices, 715 patients, 2,400 documents | Provided |
| Test harness, fixtures, deterministic embedding stub | Provided |
Base tables practices, users, patients, clinical_documents |
Provided |
| Chunk and embedding storage | Implemented in migration 0002 with pgvector |
| Indexing workflow | Implemented as an incremental, idempotent make index command |
POST /api/clinical-search |
Implemented with practice-scoped patient aggregation |
| Acceptance tests | Implemented for indexing, isolation, aggregation, and search quality |
| Task | Start here |
|---|---|
| Chunk and embedding schema | database/migrations/README.md |
| Indexing workflow | services/api/app/features/indexing/README.md |
| Search endpoint | services/api/app/features/search/README.md |
| Search UI | apps/web/features/search/README.md |
| Tests | services/api/tests/acceptance/README.md |
| Synthetic dataset | docs/DATASET.md |
The provided mock session associates each demo user with one practice. The search request must not accept a client-selected practice identifier, and results must remain isolated to the authenticated user's current practice.
Use the dropdown in the header to switch between the supplied demo identities while testing. The implementation of the search boundary is part of the exercise.
├── apps/web/ Next.js 16, App Router, Tailwind v4
│ ├── app/ routes: /, /search, /patients/[id], /api/demo-session
│ ├── components/ui/ design-system primitives
│ ├── features/ feature-local search, session, and patient code
│ └── lib/ server-only API client, types, formatting
├── services/
│ ├── api/ FastAPI
│ │ ├── app/features/ health, session, patients, search, indexing
│ │ ├── app/clients/ embedding client
│ │ ├── app/scripts/ migrate, seed, index, smoke, wait_for_dependencies
│ │ └── tests/ unit, integration, and acceptance coverage
│ └── embedding/ local embedding and exact token-count service
├── database/
│ ├── init/ first-boot extension and test database
│ ├── migrations/ 0001 base schema; 0002 chunk/vector storage
│ └── seed/ generator plus committed CSVs
└── docs/ design document and dataset notes
Copy .env.example to .env. It holds no credentials worth protecting and is safe to
commit as an example; .env itself is git-ignored.
| Variable | Default | Notes |
|---|---|---|
DATABASE_URL |
postgresql://clinical:local_dev_only@db:5432/clinical_search |
|
TEST_DATABASE_URL |
…/clinical_search_test |
Created on first boot |
EMBEDDING_SERVICE_URL |
http://embedding:8080 |
|
EMBEDDING_MAX_BATCH_SIZE |
64 |
Service rejects larger batches |
SEARCH_DEFAULT_LIMIT |
10 |
|
SEARCH_MAX_LIMIT |
25 |
|
SEARCH_MAX_QUERY_LENGTH |
500 |
|
SEARCH_MIN_RELEVANCE_SCORE |
0.35 |
Minimum cosine similarity returned |
EMBEDDING_FAILURE_RATE |
0.0 |
Set to 1.0 to exercise the failure UI |
API_BASE_URL |
http://api:8000 |
Server-side only |
Review services/embedding/README.md for the embedding and
exact token-count APIs, input limits, and failure responses. Both indexing and search use
the same pinned tokenizer so inputs cannot be silently truncated past 256 tokens.
make test # API tests in the running API container + a disposable web test container
make test-api # requires the API and database started by make setup
make test-web # starts a disposable web test container
make test-integration # needs the real embedding container
make lint # ruff, eslint
make typecheckThe default backend suite currently contains 85 passing tests, the real-embedding integration
suite contains 8, and the web suite contains 28. The integration tests are skipped by the
default pytest marker unless make test-integration is run explicitly.
Open a pull request against main and complete the
PULL_REQUEST_TEMPLATE.md provided beside this README. It asks
for architecture, tradeoffs, chunking and ranking decisions, limitations, and AI-tool
disclosure.
If you run out of time, submit what works and be explicit about what is incomplete. That is read as good judgement. Polish does not substitute for practice isolation, source evidence, idempotent indexing, or a working end-to-end flow.
make setup cannot reach the Docker daemon — start Docker Desktop and retry.
Database container exits immediately — the volume was created by an older Postgres.
make destroy then make setup.
pgvector types are not available — migrations have not run. make migrate.
Search returns no results after seeding — run make index. Search only considers chunks
created by the current chunking version and embedding model.
Search returns 422 for a query shorter than 500 characters — the embedding tokenizer can still produce more than 256 tokens. Shorten the query using fewer or broader clinical terms.
Search returns 503 — check docker compose ps embedding and
docker compose logs embedding; token counting and vector generation both depend on it.
Integration tests skip — the embedding container is not up.
docker compose up -d embedding, then wait for its health check.
Web shows "The API is not reachable" — check docker compose logs api. The API needs a
migrated database to answer /api/session.
pnpm install warns about ignored build scripts — already handled via
onlyBuiltDependencies in apps/web/pnpm-workspace.yaml; run pnpm install once more.