This project is a lightweight course search and ranking system for UC Irvine built using:
- Python
- SQLite
- WebSOC data
Instead of relying on live API calls, this system uses a locally collected WebSOC dataset to build a searchable database of:
- Courses
- Sections
- Meeting times
- Locations
- Grade distributions / average GPAs
- Instructor names
The goal of this demo is to show:
- Database indexing
- Term filtering
- Course ranking with GPA-based difficulty assessment
- Real meeting data integration
- Personalized recommendations based on user profile
# 1. Clone the repo and enter the project directory
cd searchengine
# 2. (Optional) Create a virtual environment
python -m venv .venv
# Windows:
.venv\Scripts\activate
# macOS/Linux:
source .venv/bin/activate
# 3. Install dependencies
pip install -r requirements.txt
# 4. Build the database (first run downloads data from Anteater API, ~5 min)
cd backend
python index_setup.py
# To force re-download existing JSON data:
python index_setup.py --force
# 5. Start the server
python server.py
# 6. Open http://localhost:8080 in browsersearchengine/
├── backend/
│ ├── index/
│ │ ├── common.py # Shared constants (QUARTERS, GE_CATEGORIES) and utility functions
│ │ ├── index_search.py # CourseSearch class — query courses by major, minor, GE, term, etc.
│ │ └── sql_index.py # Builds the SQLite database: creates tables, inserts all data
│ │
│ ├── tests/
│ │ └── index_query_tests.py # Unit tests for CourseSearch and query functions
│ │
│ ├── data_collection.py # Fetches raw data from Anteater API and saves to JSON files
│ ├── data_categorization.py # Builds standalone JSON indexes by dept, instructor, level, GE
│ ├── index_setup.py # Main setup script: downloads data (if needed) and builds courses.db
│ ├── quick_setup.py # Lightweight setup: builds a minimal DB (no WebSOC term details)
│ ├── server.py # Flask web server — serves frontend and provides REST API endpoints
│ ├── ranking.py # Course ranking logic — multi-factor scoring with explainability
│ ├── user_index.py # Creates user-related tables (Users, UserCompletedCourses, CourseGrades)
│ ├── progress_report_1_demo.py # Demo script used for progress report #1 presentation
│ │
│ ├── all_course_data.json # [Generated] All course data from Anteater API
│ ├── all_major_data.json # [Generated] All majors and their graduation requirements
│ ├── all_minor_data.json # [Generated] All minors and their requirements
│ ├── all_specialization_data.json # [Generated] All specializations and their requirements
│ ├── all_grade_data.json # [Generated] Aggregated grade distributions from Anteater API
│ └── courses.db # [Generated] SQLite database built from the JSON files above
│
├── frontend/
│ ├── LoginPage.html # Login / registration page
│ ├── UserProfilePage.html # Onboarding page: 4-step form for student profile
│ ├── SearchPage.html # Main search page: filters, quick-filter pills, course result cards
│ └── static/
│ ├── css/style.css # All styling for both pages (incl. GPA tag color coding)
│ └── js/
│ ├── UserProfilePage.js # Onboarding form logic, major courses split by lower/upper div
│ └── SearchPage.js # Search page logic: calls API, renders course cards with GPA & instructor
│
├── requirements.txt # Python dependencies (flask, flask-cors, requests)
└── README.md # This file
The SQLite database is built in backend/index/sql_index.py and stores both static catalog metadata and term-specific meeting data.
Stores general course metadata.
| Column | Description |
|---|---|
course_id |
Canonical course id (ex: I&CSCI31) |
department |
Department code / name |
course_number |
Course number |
course_title |
Official course title |
min_units |
Minimum units |
max_units |
Maximum units |
repeatability |
Repeatability metadata |
grading_option |
Grading option |
corequisites |
Corequisite text |
Stores term-specific section and meeting information from WebSOC.
| Column | Description |
|---|---|
course_id |
References Courses.course_id |
section_code |
WebSOC section code |
section_type |
Lecture / discussion / lab / etc. |
year |
Academic year |
quarter |
Quarter code |
building_id |
Building code |
room_number |
Room number |
start_time |
Start time |
end_time |
End time |
days |
Meeting days |
restrictions |
Enrollment restrictions |
max_capacity |
Section capacity |
num_currently_enrolled |
Current enrollment |
waitlist_capacity |
Waitlist capacity |
num_on_waitlist |
Current waitlist count |
is_cancelled |
Cancellation flag |
instructor |
Instructor name(s) |
Stores aggregated grade distribution data per course.
| Column | Description |
|---|---|
course_id |
References Courses.course_id |
average_gpa |
Average GPA across all recorded sections |
grade_a_count |
Number of A grades |
grade_b_count |
Number of B grades |
grade_c_count |
Number of C grades |
grade_d_count |
Number of D grades |
grade_f_count |
Number of F grades |
Stores user account and preference data.
| Column | Description |
|---|---|
username |
Login username |
password |
User password |
display_name |
Display name |
standing |
Academic standing (Freshman / Sophomore / etc.) |
major |
Selected major ID |
preferred_time |
Time of day preference |
workload |
Workload preference (light / balanced / heavy) |
course_format |
Format preference (in-person / online) |
quarter_target |
Target enrollment quarter |
max_units |
Max units per course |
Tracks which courses each user has completed.
Tracks which GE categories each user still needs.
These tables support degree-aware search and ranking.
Stores major metadata.
Stores hierarchical graduation requirement groups for each major.
Maps courses to major requirements and requirement groups.
Stores minor metadata.
Stores minor requirement groups.
Maps courses to minor requirements.
Stores specialization metadata and parent major relationship.
Stores specialization requirement groups.
Maps courses to specialization requirements.
Stores AND / OR prerequisite tree structure.
Stores actual prerequisite courses belonging to each relationship node.
This allows the system to evaluate more complex prerequisite logic than a flat prerequisite list.
Maps courses to UCI GE categories.
Stores token frequencies for course title / department / number search.
Stores token frequencies for major-name search.
Search is handled primarily through CourseSearch in backend/index/index_search.py.
The backend can filter courses by:
- term (
year,quarter) - selected major(s)
- selected minor(s)
- selected specialization(s)
- completed prerequisites
- major / minor / specialization requirement progress
- GE needs
- text query terms through inverted indexes
At a high level, the system does the following:
- Load relevant requirement-linked course ids from the database.
- Intersect them with term availability from
Terms. - Remove already completed courses if requested.
- Check prerequisite satisfaction using the prerequisite relationship tree.
- Return feasible results.
- Optionally apply ranking and sort by score.
from index.index_search import CourseSearch
search = CourseSearch("courses.db")
search.add_major("BS-201")
search.add_prerequisite("MATH1B")
results = search.search(2026, "Spring")
for course in results:
print(course)Ranking is implemented in ranking.py using the function: compute_match_score(…)
The system assigns a score to each course based on multiple factors and returns both:
- a numeric score
- a list of human-readable reasons (for explainability)
Courses are scored using a combination of academic relevance, user preferences, and real-world scheduling constraints.
-
Major requirement (+30)
Courses required for the selected major are prioritized. -
Prerequisite completion (+15 if met / −30 if unmet) Courses with unmet prerequisites are penalized but still shown (so users can see upcoming major courses).
-
Dependency bonus (+ up to 15)
Courses that unlock future required courses are boosted.
- GE requirement match (+20)
Courses satisfying remaining GE categories are prioritized.
-
Preferred time match (+15)
Morning / afternoon / evening preferences. -
Preferred format (+10)
Online vs in-person preference. -
Unit constraint (+10)
Courses within the user's max unit limit. -
Workload preference
- Light workload → prefer low-unit / low-difficulty courses
- Heavy workload → slight boost for higher-unit courses
Optional keyword-based boosts using real GPA data:
"morning","afternoon","evening"→ +10 (if course matches the time slot)"online"→ +15 (only if actually online)"easy"→ GPA-based scoring:- GPA ≥ 3.4: +10 (Easy course)
- GPA 3.0–3.4: +0 (Medium difficulty)
- GPA < 3.0: +0 (Hard course)
- No GPA data: +5
When the user's workload preference is set:
- Light workload + hard course (GPA < 3.0): −15
- Light workload + easy course (GPA ≥ 3.4): +10
- Heavy workload + hard course (GPA < 3.0): +5
- Open seats available (+ up to 6)
- Space on waitlist (noted in explanation)
- No space in class (−30)
These signals prioritize courses that are easier to enroll in.
The ranking system adapts dynamically based on the student's current schedule.
-
Time conflicts (−40)
Courses overlapping with existing schedule are heavily penalized. -
Walking distance penalty (−10)
Consecutive classes in different buildings are penalized. -
Final exam conflict (−15)
Courses with finals on the same day are penalized.
- Each ranked result includes:
{ “course_id”: “I&CSCI32”, “score”: 78, “reasons”: [ “Required for your major”, “Prerequisites completed”, “Matches morning preference”, “Units <= 4”, “3 section option(s)” ] }
-
Explainability
Every score includes reasons to justify ranking decisions. -
Personalization
Results change based on user profile and schedule. -
Feasibility-aware Courses with unmet prerequisites are penalized but still visible, so users can plan ahead.
-
Real-world constraints
Uses actual meeting times, enrollment data, and schedule conflicts.
The ranking system is not static — it dynamically adapts based on:
- academic progress (major / prerequisites / GE)
- user preferences (time, format, workload)
- real schedule constraints (conflicts, walking, finals)
- enrollment feasibility (sections, waitlist)
This produces a more realistic and useful course recommendation experience compared to simple filtering or keyword search.
- Aggregated grade data is fetched from Anteater API (
/v2/rest/grades/aggregateByCourse) and stored in theCourseGradestable. - Each course card displays an Avg GPA tag, color-coded:
- Green (≥ 3.4): Easy course
- Yellow (3.0–3.4): Medium difficulty
- Red (< 3.0): Hard course
- GPA data is used as a ranking signal for the
"easy"keyword and workload preferences.
- Instructor names are extracted from WebSOC section data and stored in the
Termstable. - Displayed on each course card alongside time and location.
- Courses with multiple lecture sections are deduplicated in search results, keeping only the highest-scoring section per course.
- When searching for GE courses (via keyword, dropdown filter, or "GE I Need" pill), major core requirement courses are excluded from results so users only see courses that fulfill GE needs without overlapping with their required major coursework.
- On the profile page, core requirements are split into Lower Division and Upper Division categories, making it easier to find and mark intro-level courses as completed.
- Two quick-filter pills on the search page: My Major (shows only major-required courses) and GE I Need (shows only courses satisfying the user's remaining GE categories).
- Time of Day and Format sidebar filters are connected to the backend API, enabling hard filtering (e.g., selecting "Morning" only returns courses before 12:00 PM).
- Common abbreviations are mapped to canonical terms for search (e.g.,
cs→compsci,maths→math,stats→statistics).
- All Anteater API calls use a universal
_request_with_retry()helper that automatically retries on HTTP 429 rate-limit responses with progressive backoff.