A Machine Learning-powered REST API built with FastAPI that provides personalized LeetCode problem recommendations based on a user's recent submission history.
The backend employs an optimized Redis caching strategy to minimize external API latency and handle high concurrency:
graph TD
A["Client GET /recommend/similar?username=awice"] --> B{"Is Redis Enabled?"}
B -- "Yes" --> C{"Cache Check: 'user:submissions:awice'"}
B -- "No (Circuit Breaker Down)" --> F["Fetch from LeetCode GraphQL API"]
C -- "Cache HIT" --> D["Retrieve Solved IDs (100 bytes) from Redis"]
C -- "Cache MISS" --> F
F --> G["Fetch user's recent 20 submissions via GraphQL"]
G --> H["Store Solved IDs in Redis (10-min TTL)"]
H --> I["Slice user embeddings locally in RAM (embeddings.npy)"]
D --> I
I --> J["Compute similarity and hybrid recommendation mix"]
J --> K["Return JSON Recommendations (HIT: ~70ms / MISS: ~1.1s)"]
- Endpoint Access: A client hits
/recommend/similaror/recommend/diffwith a LeetCode username. - Circuit Breaker Check: The system checks if the Redis connection is active. If the connection fails during startup, the caching layer is bypassed automatically (circuit-breaker enabled) to keep the service operational.
- Cache Hit Check:
- Cache HIT: If the user's solved problem IDs list is found in Redis under the key
user:submissions:<username>, the IDs are retrieved. The embedding vectors are sliced instantly from the preloaded local matrix (embeddings.npy) in RAM (<1ms). The recommendations are generated, responding in ~70ms. - Cache MISS: If not found in Redis, the backend initiates the LeetCode GraphQL Pipeline:
- Fetches the 20 most recent solved submission slugs.
- Batch-resolves the slugs to frontend question IDs in a single query using aliases.
- Saves the resolved question IDs list in Redis with a 10-minute (600 seconds) expiration.
- Extracts the embedding vectors from memory and computes recommendations, returning the response in ~1.1s.
- Cache HIT: If the user's solved problem IDs list is found in Redis under the key
The recommendation pipeline ensures high performance and accurate suggestions:
-
Startup Phase:
- Uses FastAPI's
@app.on_event("startup")to load preprocessed datasets (processed.csv) and their precomputed SentenceTransformer vector embeddings (embeddings.npy). - The
/recommend/diffservice also builds a tag → problem ID inverted index at this stage (see Different Problems below). - Loading on startup instead of globally allows fast port binding for deployment health checks.
- Uses FastAPI's
-
Client Request:
- The frontend requests
/recommend/similaror/recommend/diffwith the LeetCode username.
- The frontend requests
-
Data Fetching (GraphQL):
- The backend uses
httpxto interact with the LeetCode GraphQL API (https://leetcode.com/graphql/). - Step 1 — Recent submissions: A single
recentAcSubmissionsquery fetches thetitleSlugof the 20 most recently accepted problems for the given username. - Step 2 — Batched ID lookup: Rather than firing one
questionDataquery per slug (N+1 pattern), all slugs are resolved in a single batched GraphQL request using field aliases:query { q0: question(titleSlug: "two-sum") { questionFrontendId } q1: question(titleSlug: "rotate-image") { questionFrontendId } ... }
- This reduces total HTTP round-trips from N+1 down to 2, regardless of how many unique slugs are returned.
- The backend uses
-
Embedding Preprocessing (Offline):
- The dataset of LeetCode problems (
LeetCode Questions.csv) is preprocessed offline via a dedicated Python script. - The text features (specifically,
difficulty+topics, e.g.,"Medium Array Hash Table") are concatenated. - A lightweight SentenceTransformer model (
paraphrase-MiniLM-L3-v2) processes these combined strings and encodes them into normalized, high-dimensional dense vectors. - These vectors are saved locally as an
embeddings.npyNumPy array file alongside a cleanedprocessed.csv. This means the API never performs expensive NLP inference during runtime.
- The dataset of LeetCode problems (
-
Recommendation Engine:
- Retrieves the exact embedding vectors for the user's recently solved
questionFrontendIds fromembeddings.npy. - Calculates the mathematical average of these vectors to generate a single "User Profile Vector".
- Uses scikit-learn's Cosine Similarity metric to measure the angle between the User Profile Vector and every other problem vector in the dataset. Scores closer to 1.0 indicate high semantic overlap in topic and difficulty.
- Applies a Hybrid Recommendation Logic to the sorted list, ensuring the final recommendations match the user's recent Easy/Medium/Hard difficulty ratio.
A tag-weight-based engine designed to surface problems in topics the user has not yet practiced.
Startup (once): An inverted index is built from the full dataset:
tag (lowercase) → frozenset of problem IDs e.g. "dynamic programming" → frozenset({5, 10, 62, 64, ...}) "graph" → frozenset({200, 207, 210, ...})Per request:
- Extract
seen_tagsfrom the user's last 20 solved problems. - Compute
unseen_tags = all_tags − seen_tags. - Union the inverted index entries for all
unseen_tags→ candidate pool of ~200–400 problem IDs (set union, no full scan). - Score only the candidates using tag weights:
- Seen tag → weight
1(familiar territory, unlikely to be picked) - Unseen tag → weight
100(fresh topic, highly preferred) - A problem's score = sum of its tag weights.
- Seen tag → weight
- Use
pandas.DataFrame.sample(weights=score)to pick 5 problems — high-scoring problems are proportionally more likely to appear.
This is ~10× faster than scoring the full 3,000-row dataset, since only problems reachable through unexplored tags are ever evaluated.
- Retrieves the exact embedding vectors for the user's recently solved
-
Response:
- Returns the top 5 customized problem recommendations in JSON format.
- All floating-point values (
NaN,inf) are sanitized before serialization to prevent JSON encoding errors from missing CSV fields or degenerate embedding vectors.
| Technology | Reason |
|---|---|
| FastAPI | Native async/await, auto-generated Swagger UI, high throughput vs Flask/Django |
SentenceTransformers (paraphrase-MiniLM-L3-v2) |
Lightweight BERT-based model for semantic embeddings without GPU |
Precomputed Embeddings (.npy) |
Offline generation drops inference latency from seconds → milliseconds at runtime |
Asynchronous httpx |
Non-blocking HTTP client; LeetCode GraphQL calls don't stall the event loop |
| Batched GraphQL aliases | Reduces external API round-trips from N+1 to 2 for submission slug resolution |
| Inverted tag index | O(1) candidate lookup per tag; limits scoring to ~200–400 rows instead of 3,000 |
- Navigate to the backend directory:
cd backend - Create and activate a virtual environment:
python -m venv venv # On Windows venv\Scripts\activate # On Mac/Linux source venv/bin/activate
- Install dependencies:
pip install -r requirements.txt
- Configure environment variables:
- Make sure your
.envfile is set up in thebackendfolder.
- Make sure your
- Run the server:
uvicorn app:app --host 0.0.0.0 --port 8000 --reload
- Navigate to the backend directory:
cd backend - Build and run using Docker Compose:
docker compose up --build
- The API will be available at
http://localhost:8000.
While the current architecture handles the existing 3,000+ LeetCode problems easily, scaling to millions of entries would involve:
- Vector Databases: Replacing the in-memory NumPy arrays and O(N) linear Cosine Similarity scan with Approximate Nearest Neighbor (ANN) search using Pinecone, Milvus, or FAISS for O(log N) complexity.
- Database Migration: Moving from static CSV files loaded via Pandas to a distributed relational database like PostgreSQL.
- Caching Layer: Implementing Redis to cache LeetCode GraphQL responses and generated recommendations for frequently active users.
- Persistent Inverted Index: Storing the tag index in Redis or a search engine like Elasticsearch to avoid rebuild cost on cold starts in a multi-worker deployment.