Skip to content
 
 

Repository files navigation

Clinical Record Semantic Search — take-home exercise

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.


Quick start

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:8000

In another shell:

make test      # backend and frontend suites
make smoke     # confirm database, seed data, and a real embedding call

The 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-web

make 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.


Implementation status

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

Implementation entry points

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

Session and practice context

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.


Layout

├── 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

Configuration

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

Provided service contracts

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.


Testing

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 typecheck

The 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.


Submitting

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.


Troubleshooting

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.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages