diff --git a/.github/workflows/run-python-tests.yml b/.github/workflows/run-python-tests.yml index ecbd341..4a24fe6 100644 --- a/.github/workflows/run-python-tests.yml +++ b/.github/workflows/run-python-tests.yml @@ -38,9 +38,58 @@ jobs: - name: Download sample data run: curl https://atlas-education.s3.amazonaws.com/sampledata.archive -o sampledata.archive - - name: Add sample data to database - run: mongorestore --archive=sampledata.archive --port=27017 + - name: Setup Database (Data & Indexes) + run: | + # 1. Restore the data + mongorestore --archive=sampledata.archive --port=27017 + + # 2. Prepare the Search Index Definition + echo '{ + "name": "movieSearchIndex", + "database": "sample_mflix", + "collectionName": "movies", + "mappings": { + "dynamic": false, + "fields": { + "plot": {"type": "string", "analyzer": "lucene.standard"}, + "fullplot": {"type": "string", "analyzer": "lucene.standard"}, + "directors": {"type": "string", "analyzer": "lucene.standard"}, + "writers": {"type": "string", "analyzer": "lucene.standard"}, + "cast": {"type": "string", "analyzer": "lucene.standard"} + } + } + }' > search_index.json + + # 3. Create the Search Index + atlas deployments search indexes create \ + --deploymentName myLocalRs1 \ + --file search_index.json + + # 4. Prepare the Vector Index Definition + echo '{ + "name": "vector_index", + "database": "sample_mflix", + "collectionName": "embedded_movies", + "type": "vectorSearch", + "fields": [ + { + "type": "vector", + "path": "plot_embedding_voyage_3_large", + "numDimensions": 2048, + "similarity": "cosine" + } + ] + }' > vector_index.json + # 5. Create the Vector Index + atlas deployments search indexes create \ + --deploymentName myLocalRs1 \ + --file vector_index.json + + # 6. Wait for indexes to build + echo "Waiting for indexes to build..." + sleep 20 + - name: Set up Python uses: actions/setup-python@v5 with: @@ -63,10 +112,10 @@ jobs: - name: Run integration tests working-directory: mflix/server/python-fastapi - run: pytest -m integration --verbose --tb=short --junit-xml=test-results-integration.xml || true + run: pytest -m integration --verbose --tb=short --junit-xml=test-results-integration.xml env: - MONGO_URI: mongodb://localhost:27017 - MONGO_DB: sample_mflix + MONGO_URI: mongodb://localhost:27017/?directConnection=true + MONGO_DB: sample_mflix - name: Upload test results uses: actions/upload-artifact@v4 diff --git a/mflix/client/app/aggregations/page.tsx b/mflix/client/app/aggregations/page.tsx index a211141..00b8d16 100644 --- a/mflix/client/app/aggregations/page.tsx +++ b/mflix/client/app/aggregations/page.tsx @@ -1,6 +1,6 @@ import React from 'react'; -import { fetchMoviesWithComments, fetchMoviesByYear, fetchDirectorStats } from '../lib/api'; -import { MovieWithComments, YearlyStats, DirectorStats } from '../types/aggregations'; +import { fetchMoviesWithComments, fetchMoviesByYear, fetchDirectorStats } from '@/lib/api'; +import { MovieWithComments, YearlyStats, DirectorStats } from '@/types/aggregations'; import styles from './aggregations.module.css'; export default async function AggregationsPage() { diff --git a/mflix/client/app/components/MovieCard/MovieCard.module.css b/mflix/client/app/components/MovieCard/MovieCard.module.css index bfce2e7..c19765d 100644 --- a/mflix/client/app/components/MovieCard/MovieCard.module.css +++ b/mflix/client/app/components/MovieCard/MovieCard.module.css @@ -105,6 +105,17 @@ font-size: 14px; } +.vectorScore { + margin: 0 0 4px 0; + color: #0066cc; + font-size: 13px; + font-weight: 500; + background: #e6f0ff; + padding: 4px 8px; + border-radius: 4px; + display: inline-block; +} + .movieGenres { margin: 0; color: #888; diff --git a/mflix/client/app/components/MovieCard/MovieCard.tsx b/mflix/client/app/components/MovieCard/MovieCard.tsx index 7d9ceaf..1ff2c9d 100644 --- a/mflix/client/app/components/MovieCard/MovieCard.tsx +++ b/mflix/client/app/components/MovieCard/MovieCard.tsx @@ -69,6 +69,11 @@ export default function MovieCard({ movie, isSelected = false, onSelectionChange {movie.year && (

({movie.year})

)} + {movie.score !== undefined && ( +

+ 🎯 Vector Score: {movie.score.toFixed(4)} +

+ )} {movie.imdb?.rating && (

⭐ {movie.imdb.rating}/10

)} diff --git a/mflix/client/app/components/SearchMovieModal/SearchMovieModal.module.css b/mflix/client/app/components/SearchMovieModal/SearchMovieModal.module.css index 6805e8c..b757b2f 100644 --- a/mflix/client/app/components/SearchMovieModal/SearchMovieModal.module.css +++ b/mflix/client/app/components/SearchMovieModal/SearchMovieModal.module.css @@ -1,45 +1,47 @@ /** * Search Movie Modal Styles - * + * * CSS Module for the search movie modal component. * Provides consistent styling with the rest of the application. */ .formContainer { background: white; - border-radius: 12px; - padding: 2rem; - box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); + border-radius: 16px; + padding: 2.5rem; + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12); margin-bottom: 2rem; + max-width: 800px; + margin-left: auto; + margin-right: auto; } .formTitle { - font-size: 1.75rem; - font-weight: bold; - color: #333; - margin: 0 0 1.5rem 0; + font-size: 1.875rem; + font-weight: 700; + color: #1a1a2e; + margin: 0 0 0.5rem 0; text-align: center; } .batchDescription { - background-color: #f8f9fa; - border: 1px solid #e9ecef; - border-radius: 8px; - padding: 1rem; - margin-bottom: 1.5rem; - color: #495057; - font-size: 0.9rem; - line-height: 1.4; + background-color: transparent; + border: none; + padding: 0; + margin-bottom: 2rem; + color: #6b7280; + font-size: 1rem; + line-height: 1.5; text-align: center; } .generalError { - background-color: #f8d7da; - border: 1px solid #f5c6cb; - color: #721c24; - padding: 0.75rem 1rem; - border-radius: 8px; - margin-bottom: 1rem; + background-color: #fef2f2; + border: 1px solid #fecaca; + color: #dc2626; + padding: 0.875rem 1.25rem; + border-radius: 10px; + margin-bottom: 1.5rem; font-size: 0.9rem; text-align: center; } @@ -48,138 +50,183 @@ width: 100%; } +/* Section styling for grouped fields */ +.fieldSection { + background: #f8fafc; + border-radius: 12px; + padding: 1.5rem; + margin-bottom: 1.5rem; +} + +.sectionTitle { + font-size: 0.8rem; + font-weight: 600; + color: #64748b; + text-transform: uppercase; + letter-spacing: 0.05em; + margin: 0 0 1rem 0; + padding-bottom: 0.75rem; + border-bottom: 1px solid #e2e8f0; +} + .formGrid { display: grid; - grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); - gap: 1.5rem; + grid-template-columns: repeat(2, 1fr); + gap: 1.25rem; margin-bottom: 1.5rem; } +.formGridThreeCol { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 1.25rem; +} + .formGroup { display: flex; flex-direction: column; - gap: 0.5rem; + gap: 0.375rem; +} + +.formGroupFullWidth { + grid-column: 1 / -1; } .label { - font-weight: 500; - color: #333; - font-size: 0.9rem; + font-weight: 600; + color: #374151; + font-size: 0.875rem; } .input, .textarea { - padding: 0.75rem; - border: 2px solid #e1e5e9; - border-radius: 8px; - font-size: 1rem; - transition: border-color 0.2s ease, box-shadow 0.2s ease; + padding: 0.875rem 1rem; + border: 1.5px solid #e2e8f0; + border-radius: 10px; + font-size: 0.95rem; + transition: border-color 0.2s ease, box-shadow 0.2s ease, background-color 0.2s ease; background: white; } +.input::placeholder, +.textarea::placeholder { + color: #9ca3af; +} + +.input:hover:not(:disabled), +.textarea:hover:not(:disabled) { + border-color: #cbd5e1; +} + .input:focus, .textarea:focus { outline: none; - border-color: #0070f3; - box-shadow: 0 0 0 3px rgba(0, 112, 243, 0.1); + border-color: #3b82f6; + box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.15); + background: white; } .input:disabled, .textarea:disabled { - background: #f8f9fa; - color: #6c757d; + background: #f1f5f9; + color: #94a3b8; cursor: not-allowed; } .inputError { - border-color: #dc2626 !important; + border-color: #ef4444 !important; } .inputError:focus { - box-shadow: 0 0 0 3px rgba(220, 38, 38, 0.1) !important; + box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.15) !important; } .error { - color: #dc2626; - font-size: 0.875rem; + color: #ef4444; + font-size: 0.8rem; margin-top: 0.25rem; } .searchOperatorDescription { - color: #6c757d; - font-size: 0.875rem; + color: #64748b; + font-size: 0.8rem; margin-top: 0.25rem; display: block; + line-height: 1.4; } .formActions { display: flex; - gap: 1rem; + gap: 0.75rem; justify-content: flex-end; - padding-top: 1.5rem; - border-top: 1px solid #e1e5e9; + padding-top: 1.75rem; + margin-top: 0.5rem; + border-top: 1px solid #e2e8f0; } .button { padding: 0.75rem 1.5rem; border: none; - border-radius: 8px; - font-size: 1rem; - font-weight: 500; + border-radius: 10px; + font-size: 0.95rem; + font-weight: 600; cursor: pointer; transition: all 0.2s ease; - min-width: 120px; + min-width: 100px; } .button:disabled { - opacity: 0.6; + opacity: 0.5; cursor: not-allowed; transform: none; } .saveButton { - background: #0070f3; + background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%); color: white; - border: 1px solid #0070f3; + border: none; + min-width: 140px; } .saveButton:hover:not(:disabled) { - background: #0051cc; - border-color: #0051cc; - transform: translateY(-1px); - box-shadow: 0 4px 8px rgba(0, 112, 243, 0.3); + background: linear-gradient(135deg, #2563eb 0%, #1d4ed8 100%); + transform: translateY(-2px); + box-shadow: 0 6px 16px rgba(37, 99, 235, 0.35); } .cancelButton { - background: #6c757d; - color: white; - border: 1px solid #6c757d; + background: #f1f5f9; + color: #475569; + border: 1.5px solid #e2e8f0; } .cancelButton:hover:not(:disabled) { - background: #5a6268; - border-color: #5a6268; + background: #e2e8f0; + border-color: #cbd5e1; transform: translateY(-1px); - box-shadow: 0 4px 8px rgba(108, 117, 125, 0.3); } .clearButton { - background: #6c757d; - color: white; - border: 1px solid #6c757d; + background: transparent; + color: #64748b; + border: 1.5px solid #e2e8f0; } .clearButton:hover:not(:disabled) { - background: #5a6268; - border-color: #5a6268; - transform: translateY(-1px); - box-shadow: 0 4px 8px rgba(108, 117, 125, 0.3); + background: #f8fafc; + border-color: #cbd5e1; + color: #475569; } /* Responsive Design */ @media (max-width: 768px) { .formContainer { - padding: 1.5rem; + padding: 1.75rem; + border-radius: 12px; + } + + .fieldSection { + padding: 1.25rem; } .formGrid { @@ -187,32 +234,43 @@ gap: 1rem; } + .formGridThreeCol { + grid-template-columns: 1fr; + gap: 1rem; + } + .formActions { flex-direction: column-reverse; - gap: 0.75rem; + gap: 0.625rem; } .button { width: 100%; + padding: 0.875rem 1.5rem; } } @media (max-width: 480px) { .formContainer { - padding: 1rem; + padding: 1.25rem; } .formTitle { font-size: 1.5rem; } - .formGrid { - gap: 0.75rem; + .fieldSection { + padding: 1rem; + } + + .formGrid, + .formGridThreeCol { + gap: 0.875rem; } .input, .textarea { - padding: 0.625rem; + padding: 0.75rem; font-size: 0.9rem; } diff --git a/mflix/client/app/components/SearchMovieModal/SearchMovieModal.tsx b/mflix/client/app/components/SearchMovieModal/SearchMovieModal.tsx index 642ec0b..40944e7 100644 --- a/mflix/client/app/components/SearchMovieModal/SearchMovieModal.tsx +++ b/mflix/client/app/components/SearchMovieModal/SearchMovieModal.tsx @@ -208,177 +208,187 @@ export default function SearchMovieModal({ {/* Conditional Form Fields */} {formData.searchType === 'mongodb-search' ? ( <> - {/* MongoDB Search Fields */} -
- {/* Plot Search */} -
- - handleInputChange('plot', e.target.value)} - className={`${styles.input} ${errors.plot ? styles.inputError : ''}`} - disabled={isLoading} - placeholder="Exact phrase search in plot summaries" - /> - {errors.plot && {errors.plot}} + {/* Plot Search Section */} +
+

Plot Search

+
+
+ + handleInputChange('plot', e.target.value)} + className={`${styles.input} ${errors.plot ? styles.inputError : ''}`} + disabled={isLoading} + placeholder="Search in short plot summaries" + /> + {errors.plot && {errors.plot}} +
+ +
+ + handleInputChange('fullplot', e.target.value)} + className={`${styles.input} ${errors.fullplot ? styles.inputError : ''}`} + disabled={isLoading} + placeholder="Search in detailed plot descriptions" + /> + {errors.fullplot && {errors.fullplot}} +
+
- {/* Full Plot Search */} -
- - handleInputChange('fullplot', e.target.value)} - className={`${styles.input} ${errors.fullplot ? styles.inputError : ''}`} - disabled={isLoading} - placeholder="Search in full plot descriptions" - /> - {errors.fullplot && {errors.fullplot}} + {/* People Search Section */} +
+

People Search

+ + Fuzzy matching enabled – tolerates minor typos + +
+
+ + handleInputChange('directors', e.target.value)} + className={`${styles.input} ${errors.directors ? styles.inputError : ''}`} + disabled={isLoading} + placeholder="e.g. James Cameron" + /> + {errors.directors && {errors.directors}} +
+ +
+ + handleInputChange('writers', e.target.value)} + className={`${styles.input} ${errors.writers ? styles.inputError : ''}`} + disabled={isLoading} + placeholder="e.g. Aaron Sorkin" + /> + {errors.writers && {errors.writers}} +
+ +
+ + handleInputChange('cast', e.target.value)} + className={`${styles.input} ${errors.cast ? styles.inputError : ''}`} + disabled={isLoading} + placeholder="e.g. Tom Hanks" + /> + {errors.cast && {errors.cast}} +
+
- {/* Directors Search */} -
- - handleInputChange('directors', e.target.value)} - className={`${styles.input} ${errors.directors ? styles.inputError : ''}`} - disabled={isLoading} - placeholder="Director names" - /> - {errors.directors && {errors.directors}} + {/* Search Options Section */} +
+

Search Options

+
+
+ + + + {searchOperatorOptions.find(opt => opt.value === formData.searchOperator)?.description} + +
+ +
+ + handleInputChange('limit', e.target.value)} + className={`${styles.input} ${errors.limit ? styles.inputError : ''}`} + disabled={isLoading} + min="1" + max="100" + /> + {errors.limit && {errors.limit}} +
- - {/* Writers Search */} -
- - handleInputChange('writers', e.target.value)} - className={`${styles.input} ${errors.writers ? styles.inputError : ''}`} - disabled={isLoading} - placeholder="Writer names" - /> - {errors.writers && {errors.writers}} -
- - {/* Cast Search */} +
+ + ) : ( + <> + {/* Vector Search Fields */} +
+

Semantic Search

-