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 */}
+
- {/* 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
+
+
+
- {/* 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
-
- {/* Limit */}
-
-
- {/* Search Operator */}
-
-
- Search Logic
-
-
-
- {searchOperatorOptions.find(opt => opt.value === formData.searchOperator)?.description}
-
-
- >
- ) : (
- <>
- {/* Vector Search Fields */}
-
-
- Search Query
-
-
-
- {/* Limit for Vector Search */}
-
-
- Max Results
-
- handleInputChange('limit', e.target.value)}
- className={`${styles.input} ${errors.limit ? styles.inputError : ''}`}
- disabled={isLoading}
- min="1"
- max="50"
- />
- {errors.limit && {errors.limit}}
-
- Vector search supports up to 50 results
-
-
>
)}
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..75eae9a 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,38 +634,47 @@ public List
searchMovies(MovieSearchRequest searchRequest) {
));
}
- // Add directors search if provided (using text operator with fuzzy matching)
+ // Add directors search if provided
+ // Use matchCriteria: "all" to require ALL terms in the query 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.
+ // For more details, see: https://www.mongodb.com/docs/atlas/atlas-search/operators-collectors/text/
if (searchRequest.getDirectors() != null && !searchRequest.getDirectors().trim().isEmpty()) {
searchPhrases.add(new Document("text", new Document()
.append("query", searchRequest.getDirectors().trim())
.append("path", Movie.Fields.DIRECTORS)
+ .append("matchCriteria", "all")
.append("fuzzy", new Document()
.append("maxEdits", 1)
- .append("prefixLength", 5)
+ .append("prefixLength", 2)
)
));
}
- // Add writers search if provided (using text operator with fuzzy matching)
+ // Add writers search if provided (see directors comments for matchCriteria explanation)
if (searchRequest.getWriters() != null && !searchRequest.getWriters().trim().isEmpty()) {
searchPhrases.add(new Document("text", new Document()
.append("query", searchRequest.getWriters().trim())
.append("path", Movie.Fields.WRITERS)
+ .append("matchCriteria", "all")
.append("fuzzy", new Document()
.append("maxEdits", 1)
- .append("prefixLength", 5)
+ .append("prefixLength", 2)
)
));
}
- // Add cast search if provided (using text operator with fuzzy matching)
+ // Add cast search if provided (see directors comments for matchCriteria explanation)
if (searchRequest.getCast() != null && !searchRequest.getCast().trim().isEmpty()) {
searchPhrases.add(new Document("text", new Document()
.append("query", searchRequest.getCast().trim())
.append("path", Movie.Fields.CAST)
+ .append("matchCriteria", "all")
.append("fuzzy", new Document()
.append("maxEdits", 1)
- .append("prefixLength", 5)
+ .append("prefixLength", 2)
)
));
}
diff --git a/mflix/server/js-express/src/controllers/movieController.ts b/mflix/server/js-express/src/controllers/movieController.ts
index 86febc3..6fe2583 100644
--- a/mflix/server/js-express/src/controllers/movieController.ts
+++ b/mflix/server/js-express/src/controllers/movieController.ts
@@ -578,12 +578,20 @@ export async function searchMovies(req: Request, res: Response): Promise {
});
}
+ // For directors, writers, and cast fields, we use matchCriteria: "all" to require ALL terms
+ // in the query to match (AND logic). This prevents "james cameron" from matching any director
+ // with "James" OR "Cameron" (which would return too many results), 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.
+ // For more details, see: https://www.mongodb.com/docs/atlas/atlas-search/operators-collectors/text/
if (directors) {
searchPhrases.push({
text: {
query: directors,
path: "directors",
- fuzzy: { maxEdits: 1, prefixLength: 5 },
+ matchCriteria: "all",
+ fuzzy: { maxEdits: 1, prefixLength: 2 },
},
});
}
@@ -593,7 +601,8 @@ export async function searchMovies(req: Request, res: Response): Promise {
text: {
query: writers,
path: "writers",
- fuzzy: { maxEdits: 1, prefixLength: 5 },
+ matchCriteria: "all",
+ fuzzy: { maxEdits: 1, prefixLength: 2 },
},
});
}
@@ -603,7 +612,8 @@ export async function searchMovies(req: Request, res: Response): Promise {
text: {
query: cast,
path: "cast",
- fuzzy: { maxEdits: 1, prefixLength: 5 },
+ matchCriteria: "all",
+ fuzzy: { maxEdits: 1, prefixLength: 2 },
},
});
}
diff --git a/mflix/server/js-express/src/types/index.ts b/mflix/server/js-express/src/types/index.ts
index 34e3fc7..1b9d26d 100644
--- a/mflix/server/js-express/src/types/index.ts
+++ b/mflix/server/js-express/src/types/index.ts
@@ -296,6 +296,7 @@ export interface SearchPhrase {
text?: {
query: string;
path: string;
+ matchCriteria?: "any" | "all";
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 f15d6bc..3b1e5a0 100644
--- a/mflix/server/python-fastapi/src/routers/movies.py
+++ b/mflix/server/python-fastapi/src/routers/movies.py
@@ -154,37 +154,42 @@ async def search_movies(
}
})
if directors is not None:
- # The "fuzzy" option enables typo-tolerant (fuzzy) search within MongoDB Search.
+ # Use matchCriteria: "all" to require ALL terms in the query 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.
- # (Higher values make the search stricter and faster.)
+ # (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/
-
search_phrases.append({
"text": {
"query": directors,
"path": "directors",
- "fuzzy":{"maxEdits":1, "prefixLength":5}
-
+ "matchCriteria": "all",
+ "fuzzy": {"maxEdits": 1, "prefixLength": 2}
}
})
+
if writers is not None:
- # See comments above regarding fuzzy search options.
+ # See comments above regarding fuzzy search and matchCriteria for multi-word queries.
search_phrases.append({
"text": {
"query": writers,
"path": "writers",
- "fuzzy":{"maxEdits":1, "prefixLength":5}
+ "matchCriteria": "all",
+ "fuzzy": {"maxEdits": 1, "prefixLength": 2}
}
})
+
if cast is not None:
- # See comments above regarding fuzzy search options.
+ # See comments above regarding fuzzy search and matchCriteria for multi-word queries.
search_phrases.append({
"text": {
"query": cast,
"path": "cast",
- "fuzzy":{"maxEdits":1, "prefixLength":5}
+ "matchCriteria": "all",
+ "fuzzy": {"maxEdits": 1, "prefixLength": 2}
}
})