From 5e6f4a09f42aa82dc520920641c7ca41f1678b84 Mon Sep 17 00:00:00 2001 From: Cory Bullinger Date: Tue, 16 Dec 2025 12:40:06 -0500 Subject: [PATCH 1/8] Fix MongoDB Atlas Search returning too many results for multi-word queries Change directors, writers, and cast fields from text operator to phrase operator in the search endpoint across all three backend implementations. The text operator with fuzzy matching tokenizes multi-word queries into individual terms and matches using OR logic, causing searches like 'james cameron' to return ~240 results instead of ~10-15. The phrase operator performs exact phrase matching, ensuring that only documents where the full phrase appears are returned. Affected files: - Python FastAPI: mflix/server/python-fastapi/src/routers/movies.py - Express TypeScript: mflix/server/js-express/src/controllers/movieController.ts - Java Spring: mflix/server/java-spring/src/main/java/com/mongodb/samplemflix/service/MovieServiceImpl.java --- .../samplemflix/service/MovieServiceImpl.java | 26 ++++++------------- .../src/controllers/movieController.ts | 12 ++++----- .../python-fastapi/src/routers/movies.py | 24 ++++++----------- 3 files changed, 22 insertions(+), 40 deletions(-) diff --git a/mflix/server/java-spring/src/main/java/com/mongodb/samplemflix/service/MovieServiceImpl.java b/mflix/server/java-spring/src/main/java/com/mongodb/samplemflix/service/MovieServiceImpl.java index a0a8943..6a94d35 100644 --- a/mflix/server/java-spring/src/main/java/com/mongodb/samplemflix/service/MovieServiceImpl.java +++ b/mflix/server/java-spring/src/main/java/com/mongodb/samplemflix/service/MovieServiceImpl.java @@ -634,39 +634,29 @@ public List searchMovies(MovieSearchRequest searchRequest) { )); } - // Add directors search if provided (using text operator with fuzzy matching) + // Add directors search if provided (using phrase operator for exact phrase matching) + // This ensures that searching for "james cameron" only matches documents where + // "James Cameron" appears as an exact phrase, not documents containing "James" OR "Cameron". if (searchRequest.getDirectors() != null && !searchRequest.getDirectors().trim().isEmpty()) { - searchPhrases.add(new Document("text", new Document() + searchPhrases.add(new Document("phrase", new Document() .append("query", searchRequest.getDirectors().trim()) .append("path", Movie.Fields.DIRECTORS) - .append("fuzzy", new Document() - .append("maxEdits", 1) - .append("prefixLength", 5) - ) )); } - // Add writers search if provided (using text operator with fuzzy matching) + // Add writers search if provided (using phrase operator for exact phrase matching) if (searchRequest.getWriters() != null && !searchRequest.getWriters().trim().isEmpty()) { - searchPhrases.add(new Document("text", new Document() + searchPhrases.add(new Document("phrase", new Document() .append("query", searchRequest.getWriters().trim()) .append("path", Movie.Fields.WRITERS) - .append("fuzzy", new Document() - .append("maxEdits", 1) - .append("prefixLength", 5) - ) )); } - // Add cast search if provided (using text operator with fuzzy matching) + // Add cast search if provided (using phrase operator for exact phrase matching) if (searchRequest.getCast() != null && !searchRequest.getCast().trim().isEmpty()) { - searchPhrases.add(new Document("text", new Document() + searchPhrases.add(new Document("phrase", new Document() .append("query", searchRequest.getCast().trim()) .append("path", Movie.Fields.CAST) - .append("fuzzy", new Document() - .append("maxEdits", 1) - .append("prefixLength", 5) - ) )); } diff --git a/mflix/server/js-express/src/controllers/movieController.ts b/mflix/server/js-express/src/controllers/movieController.ts index 86febc3..c2129f1 100644 --- a/mflix/server/js-express/src/controllers/movieController.ts +++ b/mflix/server/js-express/src/controllers/movieController.ts @@ -578,32 +578,32 @@ export async function searchMovies(req: Request, res: Response): Promise { }); } + // The phrase operator performs an exact phrase match on the specified field. + // This ensures that searching for "james cameron" only matches documents where + // "James Cameron" appears as an exact phrase, not documents containing "James" OR "Cameron". if (directors) { searchPhrases.push({ - text: { + phrase: { query: directors, path: "directors", - fuzzy: { maxEdits: 1, prefixLength: 5 }, }, }); } if (writers) { searchPhrases.push({ - text: { + phrase: { query: writers, path: "writers", - fuzzy: { maxEdits: 1, prefixLength: 5 }, }, }); } if (cast) { searchPhrases.push({ - text: { + phrase: { query: cast, path: "cast", - fuzzy: { maxEdits: 1, prefixLength: 5 }, }, }); } diff --git a/mflix/server/python-fastapi/src/routers/movies.py b/mflix/server/python-fastapi/src/routers/movies.py index f15d6bc..6934ee0 100644 --- a/mflix/server/python-fastapi/src/routers/movies.py +++ b/mflix/server/python-fastapi/src/routers/movies.py @@ -154,37 +154,29 @@ async def search_movies( } }) if directors is not None: - # The "fuzzy" option enables typo-tolerant (fuzzy) search within MongoDB Search. - # - maxEdits: The maximum number of single-character edits (insertions, deletions, or substitutions) - # allowed when matching the search term to indexed terms. (Range: 1-2; higher = more tolerant) - # - prefixLength: The number of initial characters that must exactly match before fuzzy matching is applied. - # (Higher values make the search stricter and faster.) - # For more details, see: https://www.mongodb.com/docs/atlas/atlas-search/operators-collectors/text/ - + # The phrase operator performs an exact phrase match on the specified field. + # This ensures that searching for "james cameron" only matches documents where + # "James Cameron" appears as an exact phrase, not documents containing "James" OR "Cameron". search_phrases.append({ - "text": { + "phrase": { "query": directors, "path": "directors", - "fuzzy":{"maxEdits":1, "prefixLength":5} - } }) if writers is not None: - # See comments above regarding fuzzy search options. + # The phrase operator performs an exact phrase match on the specified field. search_phrases.append({ - "text": { + "phrase": { "query": writers, "path": "writers", - "fuzzy":{"maxEdits":1, "prefixLength":5} } }) if cast is not None: - # See comments above regarding fuzzy search options. + # The phrase operator performs an exact phrase match on the specified field. search_phrases.append({ - "text": { + "phrase": { "query": cast, "path": "cast", - "fuzzy":{"maxEdits":1, "prefixLength":5} } }) From 010b1468324ebfc642c6565c526e8dd381030c51 Mon Sep 17 00:00:00 2001 From: Cory Bullinger Date: Tue, 16 Dec 2025 14:56:11 -0500 Subject: [PATCH 2/8] Fix MongoDB Atlas Search returning too many results for multi-word queries Use compound queries with AND logic for directors, writers, and cast fields to require ALL search terms to match, preventing 'james cameron' from matching any director with 'James' OR 'Cameron'. Changes: - Split multi-word queries into individual terms - Wrap terms in compound 'must' clause (AND logic) - Adjust fuzzy settings: maxEdits=1, prefixLength=2 for better typo tolerance without over-matching (e.g., prevents 'james' matching 'jane') Single-word queries continue to use simple text operator with fuzzy matching. Affected files: - Python FastAPI: mflix/server/python-fastapi/src/routers/movies.py - Express TypeScript: mflix/server/js-express/src/controllers/movieController.ts - Express types: mflix/server/js-express/src/types/index.ts - Java Spring: mflix/server/java-spring/src/main/java/com/mongodb/samplemflix/service/MovieServiceImpl.java --- .../samplemflix/service/MovieServiceImpl.java | 98 +++++++++++++--- .../src/controllers/movieController.ts | 93 +++++++++++---- mflix/server/js-express/src/types/index.ts | 9 ++ .../python-fastapi/src/routers/movies.py | 108 ++++++++++++++---- 4 files changed, 247 insertions(+), 61 deletions(-) diff --git a/mflix/server/java-spring/src/main/java/com/mongodb/samplemflix/service/MovieServiceImpl.java b/mflix/server/java-spring/src/main/java/com/mongodb/samplemflix/service/MovieServiceImpl.java index 6a94d35..c0931f2 100644 --- a/mflix/server/java-spring/src/main/java/com/mongodb/samplemflix/service/MovieServiceImpl.java +++ b/mflix/server/java-spring/src/main/java/com/mongodb/samplemflix/service/MovieServiceImpl.java @@ -634,30 +634,94 @@ public List searchMovies(MovieSearchRequest searchRequest) { )); } - // Add directors search if provided (using phrase operator for exact phrase matching) - // This ensures that searching for "james cameron" only matches documents where - // "James Cameron" appears as an exact phrase, not documents containing "James" OR "Cameron". + // Add directors search if provided + // Split multi-word queries into individual terms and require ALL terms to match (AND logic). + // This prevents "james cameron" from matching any director with "James" OR "Cameron", + // while still allowing fuzzy matching for typo tolerance. + // Fuzzy settings: maxEdits=1 allows up to 1 character edit, prefixLength=2 requires + // only the first 2 characters to match exactly before fuzzy matching kicks in. if (searchRequest.getDirectors() != null && !searchRequest.getDirectors().trim().isEmpty()) { - searchPhrases.add(new Document("phrase", new Document() - .append("query", searchRequest.getDirectors().trim()) - .append("path", Movie.Fields.DIRECTORS) - )); + String[] directorTerms = searchRequest.getDirectors().trim().split("\\s+"); + if (directorTerms.length == 1) { + searchPhrases.add(new Document("text", new Document() + .append("query", searchRequest.getDirectors().trim()) + .append("path", Movie.Fields.DIRECTORS) + .append("fuzzy", new Document() + .append("maxEdits", 1) + .append("prefixLength", 2) + ) + )); + } else { + // Use compound must clause to require all terms match (AND logic) + java.util.List mustClauses = java.util.Arrays.stream(directorTerms) + .filter(term -> !term.isEmpty()) + .map(term -> new Document("text", new Document() + .append("query", term) + .append("path", Movie.Fields.DIRECTORS) + .append("fuzzy", new Document() + .append("maxEdits", 1) + .append("prefixLength", 2) + ) + )) + .collect(java.util.stream.Collectors.toList()); + searchPhrases.add(new Document("compound", new Document("must", mustClauses))); + } } - // Add writers search if provided (using phrase operator for exact phrase matching) + // Add writers search if provided (see directors comments for AND logic explanation) if (searchRequest.getWriters() != null && !searchRequest.getWriters().trim().isEmpty()) { - searchPhrases.add(new Document("phrase", new Document() - .append("query", searchRequest.getWriters().trim()) - .append("path", Movie.Fields.WRITERS) - )); + String[] writerTerms = searchRequest.getWriters().trim().split("\\s+"); + if (writerTerms.length == 1) { + searchPhrases.add(new Document("text", new Document() + .append("query", searchRequest.getWriters().trim()) + .append("path", Movie.Fields.WRITERS) + .append("fuzzy", new Document() + .append("maxEdits", 1) + .append("prefixLength", 2) + ) + )); + } else { + java.util.List mustClauses = java.util.Arrays.stream(writerTerms) + .filter(term -> !term.isEmpty()) + .map(term -> new Document("text", new Document() + .append("query", term) + .append("path", Movie.Fields.WRITERS) + .append("fuzzy", new Document() + .append("maxEdits", 1) + .append("prefixLength", 2) + ) + )) + .collect(java.util.stream.Collectors.toList()); + searchPhrases.add(new Document("compound", new Document("must", mustClauses))); + } } - // Add cast search if provided (using phrase operator for exact phrase matching) + // Add cast search if provided (see directors comments for AND logic explanation) if (searchRequest.getCast() != null && !searchRequest.getCast().trim().isEmpty()) { - searchPhrases.add(new Document("phrase", new Document() - .append("query", searchRequest.getCast().trim()) - .append("path", Movie.Fields.CAST) - )); + String[] castTerms = searchRequest.getCast().trim().split("\\s+"); + if (castTerms.length == 1) { + searchPhrases.add(new Document("text", new Document() + .append("query", searchRequest.getCast().trim()) + .append("path", Movie.Fields.CAST) + .append("fuzzy", new Document() + .append("maxEdits", 1) + .append("prefixLength", 2) + ) + )); + } else { + java.util.List mustClauses = java.util.Arrays.stream(castTerms) + .filter(term -> !term.isEmpty()) + .map(term -> new Document("text", new Document() + .append("query", term) + .append("path", Movie.Fields.CAST) + .append("fuzzy", new Document() + .append("maxEdits", 1) + .append("prefixLength", 2) + ) + )) + .collect(java.util.stream.Collectors.toList()); + searchPhrases.add(new Document("compound", new Document("must", mustClauses))); + } } // Build the $search aggregation stage with compound operator diff --git a/mflix/server/js-express/src/controllers/movieController.ts b/mflix/server/js-express/src/controllers/movieController.ts index c2129f1..b346b04 100644 --- a/mflix/server/js-express/src/controllers/movieController.ts +++ b/mflix/server/js-express/src/controllers/movieController.ts @@ -578,34 +578,85 @@ export async function searchMovies(req: Request, res: Response): Promise { }); } - // The phrase operator performs an exact phrase match on the specified field. - // This ensures that searching for "james cameron" only matches documents where - // "James Cameron" appears as an exact phrase, not documents containing "James" OR "Cameron". + // Split multi-word queries into individual terms and require ALL terms to match (AND logic). + // This prevents "james cameron" from matching any director with "James" OR "Cameron", + // while still allowing fuzzy matching for typo tolerance. + // Fuzzy settings: maxEdits=2 allows up to 2 character edits, prefixLength=2 requires + // only the first 2 characters to match exactly before fuzzy matching kicks in. if (directors) { - searchPhrases.push({ - phrase: { - query: directors, - path: "directors", - }, - }); + const directorTerms = directors.split(/\s+/).filter((t) => t.length > 0); + if (directorTerms.length === 1) { + searchPhrases.push({ + text: { + query: directors, + path: "directors", + fuzzy: { maxEdits: 1, prefixLength: 2 }, + }, + }); + } else { + // Use compound must clause to require all terms match (AND logic) + searchPhrases.push({ + compound: { + must: directorTerms.map((term) => ({ + text: { + query: term, + path: "directors", + fuzzy: { maxEdits: 1, prefixLength: 2 }, + }, + })), + }, + }); + } } if (writers) { - searchPhrases.push({ - phrase: { - query: writers, - path: "writers", - }, - }); + const writerTerms = writers.split(/\s+/).filter((t) => t.length > 0); + if (writerTerms.length === 1) { + searchPhrases.push({ + text: { + query: writers, + path: "writers", + fuzzy: { maxEdits: 1, prefixLength: 2 }, + }, + }); + } else { + searchPhrases.push({ + compound: { + must: writerTerms.map((term) => ({ + text: { + query: term, + path: "writers", + fuzzy: { maxEdits: 1, prefixLength: 2 }, + }, + })), + }, + }); + } } if (cast) { - searchPhrases.push({ - phrase: { - query: cast, - path: "cast", - }, - }); + const castTerms = cast.split(/\s+/).filter((t) => t.length > 0); + if (castTerms.length === 1) { + searchPhrases.push({ + text: { + query: cast, + path: "cast", + fuzzy: { maxEdits: 1, prefixLength: 2 }, + }, + }); + } else { + searchPhrases.push({ + compound: { + must: castTerms.map((term) => ({ + text: { + query: term, + path: "cast", + fuzzy: { maxEdits: 1, prefixLength: 2 }, + }, + })), + }, + }); + } } if (searchPhrases.length === 0) { diff --git a/mflix/server/js-express/src/types/index.ts b/mflix/server/js-express/src/types/index.ts index 34e3fc7..04ca6d6 100644 --- a/mflix/server/js-express/src/types/index.ts +++ b/mflix/server/js-express/src/types/index.ts @@ -298,6 +298,15 @@ export interface SearchPhrase { path: string; fuzzy?: { maxEdits: number; prefixLength: number }; }; + compound?: { + must?: Array<{ + text: { + query: string; + path: string; + fuzzy?: { maxEdits: number; prefixLength: number }; + }; + }>; + }; } /** diff --git a/mflix/server/python-fastapi/src/routers/movies.py b/mflix/server/python-fastapi/src/routers/movies.py index 6934ee0..21fda81 100644 --- a/mflix/server/python-fastapi/src/routers/movies.py +++ b/mflix/server/python-fastapi/src/routers/movies.py @@ -154,31 +154,93 @@ async def search_movies( } }) if directors is not None: - # The phrase operator performs an exact phrase match on the specified field. - # This ensures that searching for "james cameron" only matches documents where - # "James Cameron" appears as an exact phrase, not documents containing "James" OR "Cameron". - search_phrases.append({ - "phrase": { - "query": directors, - "path": "directors", - } - }) + # Split multi-word queries into individual terms and require ALL terms to match (AND logic). + # This prevents "james cameron" from matching any director with "James" OR "Cameron". + # The "fuzzy" option enables typo-tolerant search within MongoDB Search. + # - maxEdits: The maximum number of single-character edits (insertions, deletions, or substitutions) + # allowed when matching the search term to indexed terms. (Range: 1-2; higher = more tolerant) + # - prefixLength: The number of initial characters that must exactly match before fuzzy matching is applied. + # (Lower values allow typos earlier in the word but may be slower.) + # For more details, see: https://www.mongodb.com/docs/atlas/atlas-search/operators-collectors/text/ + director_terms = directors.split() + if len(director_terms) == 1: + search_phrases.append({ + "text": { + "query": directors, + "path": "directors", + "fuzzy": {"maxEdits": 1, "prefixLength": 2} + } + }) + else: + # Use compound must clause to require all terms match (AND logic) + search_phrases.append({ + "compound": { + "must": [ + { + "text": { + "query": term, + "path": "directors", + "fuzzy": {"maxEdits": 1, "prefixLength": 2} + } + } + for term in director_terms + ] + } + }) + if writers is not None: - # The phrase operator performs an exact phrase match on the specified field. - search_phrases.append({ - "phrase": { - "query": writers, - "path": "writers", - } - }) + # See comments above regarding fuzzy search and AND logic for multi-word queries. + writer_terms = writers.split() + if len(writer_terms) == 1: + search_phrases.append({ + "text": { + "query": writers, + "path": "writers", + "fuzzy": {"maxEdits": 1, "prefixLength": 2} + } + }) + else: + search_phrases.append({ + "compound": { + "must": [ + { + "text": { + "query": term, + "path": "writers", + "fuzzy": {"maxEdits": 1, "prefixLength": 2} + } + } + for term in writer_terms + ] + } + }) + if cast is not None: - # The phrase operator performs an exact phrase match on the specified field. - search_phrases.append({ - "phrase": { - "query": cast, - "path": "cast", - } - }) + # See comments above regarding fuzzy search and AND logic for multi-word queries. + cast_terms = cast.split() + if len(cast_terms) == 1: + search_phrases.append({ + "text": { + "query": cast, + "path": "cast", + "fuzzy": {"maxEdits": 1, "prefixLength": 2} + } + }) + else: + search_phrases.append({ + "compound": { + "must": [ + { + "text": { + "query": term, + "path": "cast", + "fuzzy": {"maxEdits": 1, "prefixLength": 2} + } + } + for term in cast_terms + ] + } + }) if not search_phrases: raise HTTPException( From b1073cbca541103f27e654a266593133942d56b4 Mon Sep 17 00:00:00 2001 From: Cory Bullinger Date: Tue, 16 Dec 2025 15:15:19 -0500 Subject: [PATCH 3/8] Add fuzzy search hints to directors, writers, and cast input fields - Update placeholder text with example names (e.g. James Cameron) - Add helper text indicating fuzzy matching support for typo tolerance --- .../SearchMovieModal/SearchMovieModal.tsx | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/mflix/client/app/components/SearchMovieModal/SearchMovieModal.tsx b/mflix/client/app/components/SearchMovieModal/SearchMovieModal.tsx index 642ec0b..4fce344 100644 --- a/mflix/client/app/components/SearchMovieModal/SearchMovieModal.tsx +++ b/mflix/client/app/components/SearchMovieModal/SearchMovieModal.tsx @@ -256,9 +256,12 @@ export default function SearchMovieModal({ onChange={(e) => handleInputChange('directors', e.target.value)} className={`${styles.input} ${errors.directors ? styles.inputError : ''}`} disabled={isLoading} - placeholder="Director names" + placeholder="e.g. James Cameron" /> {errors.directors && {errors.directors}} + + Fuzzy matching enabled – tolerates minor typos + {/* Writers Search */} @@ -273,9 +276,12 @@ export default function SearchMovieModal({ onChange={(e) => handleInputChange('writers', e.target.value)} className={`${styles.input} ${errors.writers ? styles.inputError : ''}`} disabled={isLoading} - placeholder="Writer names" + placeholder="e.g. Aaron Sorkin" /> {errors.writers && {errors.writers}} + + Fuzzy matching enabled – tolerates minor typos + {/* Cast Search */} @@ -290,9 +296,12 @@ export default function SearchMovieModal({ onChange={(e) => handleInputChange('cast', e.target.value)} className={`${styles.input} ${errors.cast ? styles.inputError : ''}`} disabled={isLoading} - placeholder="Actor names" + placeholder="e.g. Tom Hanks" /> {errors.cast && {errors.cast}} + + Fuzzy matching enabled – tolerates minor typos + {/* Limit */} From 28d9a1c292cadadcb766456a45dffe4fc1fb3d92 Mon Sep 17 00:00:00 2001 From: Cory Bullinger Date: Tue, 16 Dec 2025 15:20:47 -0500 Subject: [PATCH 4/8] Improve search modal UI layout and aesthetics - Group related fields into visual sections (Plot, People, Options) - Add section headers with uppercase styling - Use 3-column grid for directors/writers/cast fields - Consolidate fuzzy matching hint at section level - Improve spacing, padding, and border-radius - Add gradient styling to primary search button - Softer button styles (outline for Clear, subtle for Close) - Better input hover/focus states and placeholder colors - Improved responsive breakpoints for mobile - Cleaner vector search layout with dedicated section --- .../SearchMovieModal.module.css | 210 +++++++----- .../SearchMovieModal/SearchMovieModal.tsx | 309 +++++++++--------- 2 files changed, 289 insertions(+), 230 deletions(-) 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 4fce344..40944e7 100644 --- a/mflix/client/app/components/SearchMovieModal/SearchMovieModal.tsx +++ b/mflix/client/app/components/SearchMovieModal/SearchMovieModal.tsx @@ -208,186 +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="e.g. James Cameron" - /> - {errors.directors && {errors.directors}} - - Fuzzy matching enabled – tolerates minor typos - + {/* 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="e.g. Aaron Sorkin" - /> - {errors.writers && {errors.writers}} - - Fuzzy matching enabled – tolerates minor typos - -
- - {/* Cast Search */} +
+ + ) : ( + <> + {/* Vector Search Fields */} +
+

Semantic Search

-