Skip to content

Commit ba3902f

Browse files
committed
Refactor: Use text operator matchCriteria='all' instead of compound queries
Simplify multi-word search logic by using the built-in matchCriteria option instead of manually splitting terms and wrapping in compound must clauses. - matchCriteria: 'all' requires ALL query terms to match (AND logic) - Maintains fuzzy matching support for typo tolerance - Significantly reduces code complexity - Same behavior, cleaner implementation Ref: https://www.mongodb.com/docs/atlas/atlas-search/operators-collectors/text/
1 parent 28d9a1c commit ba3902f

4 files changed

Lines changed: 89 additions & 232 deletions

File tree

mflix/server/java-spring/src/main/java/com/mongodb/samplemflix/service/MovieServiceImpl.java

Lines changed: 31 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -635,93 +635,48 @@ public List<Movie> searchMovies(MovieSearchRequest searchRequest) {
635635
}
636636

637637
// Add directors search if provided
638-
// Split multi-word queries into individual terms and require ALL terms to match (AND logic).
638+
// Use matchCriteria: "all" to require ALL terms in the query to match (AND logic).
639639
// This prevents "james cameron" from matching any director with "James" OR "Cameron",
640640
// while still allowing fuzzy matching for typo tolerance.
641641
// Fuzzy settings: maxEdits=1 allows up to 1 character edit, prefixLength=2 requires
642642
// only the first 2 characters to match exactly before fuzzy matching kicks in.
643+
// For more details, see: https://www.mongodb.com/docs/atlas/atlas-search/operators-collectors/text/
643644
if (searchRequest.getDirectors() != null && !searchRequest.getDirectors().trim().isEmpty()) {
644-
String[] directorTerms = searchRequest.getDirectors().trim().split("\\s+");
645-
if (directorTerms.length == 1) {
646-
searchPhrases.add(new Document("text", new Document()
647-
.append("query", searchRequest.getDirectors().trim())
648-
.append("path", Movie.Fields.DIRECTORS)
649-
.append("fuzzy", new Document()
650-
.append("maxEdits", 1)
651-
.append("prefixLength", 2)
652-
)
653-
));
654-
} else {
655-
// Use compound must clause to require all terms match (AND logic)
656-
java.util.List<Document> mustClauses = java.util.Arrays.stream(directorTerms)
657-
.filter(term -> !term.isEmpty())
658-
.map(term -> new Document("text", new Document()
659-
.append("query", term)
660-
.append("path", Movie.Fields.DIRECTORS)
661-
.append("fuzzy", new Document()
662-
.append("maxEdits", 1)
663-
.append("prefixLength", 2)
664-
)
665-
))
666-
.collect(java.util.stream.Collectors.toList());
667-
searchPhrases.add(new Document("compound", new Document("must", mustClauses)));
668-
}
645+
searchPhrases.add(new Document("text", new Document()
646+
.append("query", searchRequest.getDirectors().trim())
647+
.append("path", Movie.Fields.DIRECTORS)
648+
.append("matchCriteria", "all")
649+
.append("fuzzy", new Document()
650+
.append("maxEdits", 1)
651+
.append("prefixLength", 2)
652+
)
653+
));
669654
}
670655

671-
// Add writers search if provided (see directors comments for AND logic explanation)
656+
// Add writers search if provided (see directors comments for matchCriteria explanation)
672657
if (searchRequest.getWriters() != null && !searchRequest.getWriters().trim().isEmpty()) {
673-
String[] writerTerms = searchRequest.getWriters().trim().split("\\s+");
674-
if (writerTerms.length == 1) {
675-
searchPhrases.add(new Document("text", new Document()
676-
.append("query", searchRequest.getWriters().trim())
677-
.append("path", Movie.Fields.WRITERS)
678-
.append("fuzzy", new Document()
679-
.append("maxEdits", 1)
680-
.append("prefixLength", 2)
681-
)
682-
));
683-
} else {
684-
java.util.List<Document> mustClauses = java.util.Arrays.stream(writerTerms)
685-
.filter(term -> !term.isEmpty())
686-
.map(term -> new Document("text", new Document()
687-
.append("query", term)
688-
.append("path", Movie.Fields.WRITERS)
689-
.append("fuzzy", new Document()
690-
.append("maxEdits", 1)
691-
.append("prefixLength", 2)
692-
)
693-
))
694-
.collect(java.util.stream.Collectors.toList());
695-
searchPhrases.add(new Document("compound", new Document("must", mustClauses)));
696-
}
658+
searchPhrases.add(new Document("text", new Document()
659+
.append("query", searchRequest.getWriters().trim())
660+
.append("path", Movie.Fields.WRITERS)
661+
.append("matchCriteria", "all")
662+
.append("fuzzy", new Document()
663+
.append("maxEdits", 1)
664+
.append("prefixLength", 2)
665+
)
666+
));
697667
}
698668

699-
// Add cast search if provided (see directors comments for AND logic explanation)
669+
// Add cast search if provided (see directors comments for matchCriteria explanation)
700670
if (searchRequest.getCast() != null && !searchRequest.getCast().trim().isEmpty()) {
701-
String[] castTerms = searchRequest.getCast().trim().split("\\s+");
702-
if (castTerms.length == 1) {
703-
searchPhrases.add(new Document("text", new Document()
704-
.append("query", searchRequest.getCast().trim())
705-
.append("path", Movie.Fields.CAST)
706-
.append("fuzzy", new Document()
707-
.append("maxEdits", 1)
708-
.append("prefixLength", 2)
709-
)
710-
));
711-
} else {
712-
java.util.List<Document> mustClauses = java.util.Arrays.stream(castTerms)
713-
.filter(term -> !term.isEmpty())
714-
.map(term -> new Document("text", new Document()
715-
.append("query", term)
716-
.append("path", Movie.Fields.CAST)
717-
.append("fuzzy", new Document()
718-
.append("maxEdits", 1)
719-
.append("prefixLength", 2)
720-
)
721-
))
722-
.collect(java.util.stream.Collectors.toList());
723-
searchPhrases.add(new Document("compound", new Document("must", mustClauses)));
724-
}
671+
searchPhrases.add(new Document("text", new Document()
672+
.append("query", searchRequest.getCast().trim())
673+
.append("path", Movie.Fields.CAST)
674+
.append("matchCriteria", "all")
675+
.append("fuzzy", new Document()
676+
.append("maxEdits", 1)
677+
.append("prefixLength", 2)
678+
)
679+
));
725680
}
726681

727682
// Build the $search aggregation stage with compound operator

mflix/server/js-express/src/controllers/movieController.ts

Lines changed: 30 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -578,85 +578,44 @@ export async function searchMovies(req: Request, res: Response): Promise<void> {
578578
});
579579
}
580580

581-
// Split multi-word queries into individual terms and require ALL terms to match (AND logic).
582-
// This prevents "james cameron" from matching any director with "James" OR "Cameron",
583-
// while still allowing fuzzy matching for typo tolerance.
584-
// Fuzzy settings: maxEdits=2 allows up to 2 character edits, prefixLength=2 requires
581+
// For directors, writers, and cast fields, we use matchCriteria: "all" to require ALL terms
582+
// in the query to match (AND logic). This prevents "james cameron" from matching any director
583+
// with "James" OR "Cameron" (which would return too many results), while still allowing
584+
// fuzzy matching for typo tolerance.
585+
// Fuzzy settings: maxEdits=1 allows up to 1 character edit, prefixLength=2 requires
585586
// only the first 2 characters to match exactly before fuzzy matching kicks in.
587+
// For more details, see: https://www.mongodb.com/docs/atlas/atlas-search/operators-collectors/text/
586588
if (directors) {
587-
const directorTerms = directors.split(/\s+/).filter((t) => t.length > 0);
588-
if (directorTerms.length === 1) {
589-
searchPhrases.push({
590-
text: {
591-
query: directors,
592-
path: "directors",
593-
fuzzy: { maxEdits: 1, prefixLength: 2 },
594-
},
595-
});
596-
} else {
597-
// Use compound must clause to require all terms match (AND logic)
598-
searchPhrases.push({
599-
compound: {
600-
must: directorTerms.map((term) => ({
601-
text: {
602-
query: term,
603-
path: "directors",
604-
fuzzy: { maxEdits: 1, prefixLength: 2 },
605-
},
606-
})),
607-
},
608-
});
609-
}
589+
searchPhrases.push({
590+
text: {
591+
query: directors,
592+
path: "directors",
593+
matchCriteria: "all",
594+
fuzzy: { maxEdits: 1, prefixLength: 2 },
595+
},
596+
});
610597
}
611598

612599
if (writers) {
613-
const writerTerms = writers.split(/\s+/).filter((t) => t.length > 0);
614-
if (writerTerms.length === 1) {
615-
searchPhrases.push({
616-
text: {
617-
query: writers,
618-
path: "writers",
619-
fuzzy: { maxEdits: 1, prefixLength: 2 },
620-
},
621-
});
622-
} else {
623-
searchPhrases.push({
624-
compound: {
625-
must: writerTerms.map((term) => ({
626-
text: {
627-
query: term,
628-
path: "writers",
629-
fuzzy: { maxEdits: 1, prefixLength: 2 },
630-
},
631-
})),
632-
},
633-
});
634-
}
600+
searchPhrases.push({
601+
text: {
602+
query: writers,
603+
path: "writers",
604+
matchCriteria: "all",
605+
fuzzy: { maxEdits: 1, prefixLength: 2 },
606+
},
607+
});
635608
}
636609

637610
if (cast) {
638-
const castTerms = cast.split(/\s+/).filter((t) => t.length > 0);
639-
if (castTerms.length === 1) {
640-
searchPhrases.push({
641-
text: {
642-
query: cast,
643-
path: "cast",
644-
fuzzy: { maxEdits: 1, prefixLength: 2 },
645-
},
646-
});
647-
} else {
648-
searchPhrases.push({
649-
compound: {
650-
must: castTerms.map((term) => ({
651-
text: {
652-
query: term,
653-
path: "cast",
654-
fuzzy: { maxEdits: 1, prefixLength: 2 },
655-
},
656-
})),
657-
},
658-
});
659-
}
611+
searchPhrases.push({
612+
text: {
613+
query: cast,
614+
path: "cast",
615+
matchCriteria: "all",
616+
fuzzy: { maxEdits: 1, prefixLength: 2 },
617+
},
618+
});
660619
}
661620

662621
if (searchPhrases.length === 0) {

mflix/server/js-express/src/types/index.ts

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -296,17 +296,9 @@ export interface SearchPhrase {
296296
text?: {
297297
query: string;
298298
path: string;
299+
matchCriteria?: "any" | "all";
299300
fuzzy?: { maxEdits: number; prefixLength: number };
300301
};
301-
compound?: {
302-
must?: Array<{
303-
text: {
304-
query: string;
305-
path: string;
306-
fuzzy?: { maxEdits: number; prefixLength: number };
307-
};
308-
}>;
309-
};
310302
}
311303

312304
/**

mflix/server/python-fastapi/src/routers/movies.py

Lines changed: 27 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -154,93 +154,44 @@ async def search_movies(
154154
}
155155
})
156156
if directors is not None:
157-
# Split multi-word queries into individual terms and require ALL terms to match (AND logic).
157+
# Use matchCriteria: "all" to require ALL terms in the query to match (AND logic).
158158
# This prevents "james cameron" from matching any director with "James" OR "Cameron".
159159
# The "fuzzy" option enables typo-tolerant search within MongoDB Search.
160160
# - maxEdits: The maximum number of single-character edits (insertions, deletions, or substitutions)
161161
# allowed when matching the search term to indexed terms. (Range: 1-2; higher = more tolerant)
162162
# - prefixLength: The number of initial characters that must exactly match before fuzzy matching is applied.
163163
# (Lower values allow typos earlier in the word but may be slower.)
164164
# For more details, see: https://www.mongodb.com/docs/atlas/atlas-search/operators-collectors/text/
165-
director_terms = directors.split()
166-
if len(director_terms) == 1:
167-
search_phrases.append({
168-
"text": {
169-
"query": directors,
170-
"path": "directors",
171-
"fuzzy": {"maxEdits": 1, "prefixLength": 2}
172-
}
173-
})
174-
else:
175-
# Use compound must clause to require all terms match (AND logic)
176-
search_phrases.append({
177-
"compound": {
178-
"must": [
179-
{
180-
"text": {
181-
"query": term,
182-
"path": "directors",
183-
"fuzzy": {"maxEdits": 1, "prefixLength": 2}
184-
}
185-
}
186-
for term in director_terms
187-
]
188-
}
189-
})
165+
search_phrases.append({
166+
"text": {
167+
"query": directors,
168+
"path": "directors",
169+
"matchCriteria": "all",
170+
"fuzzy": {"maxEdits": 1, "prefixLength": 2}
171+
}
172+
})
190173

191174
if writers is not None:
192-
# See comments above regarding fuzzy search and AND logic for multi-word queries.
193-
writer_terms = writers.split()
194-
if len(writer_terms) == 1:
195-
search_phrases.append({
196-
"text": {
197-
"query": writers,
198-
"path": "writers",
199-
"fuzzy": {"maxEdits": 1, "prefixLength": 2}
200-
}
201-
})
202-
else:
203-
search_phrases.append({
204-
"compound": {
205-
"must": [
206-
{
207-
"text": {
208-
"query": term,
209-
"path": "writers",
210-
"fuzzy": {"maxEdits": 1, "prefixLength": 2}
211-
}
212-
}
213-
for term in writer_terms
214-
]
215-
}
216-
})
175+
# See comments above regarding fuzzy search and matchCriteria for multi-word queries.
176+
search_phrases.append({
177+
"text": {
178+
"query": writers,
179+
"path": "writers",
180+
"matchCriteria": "all",
181+
"fuzzy": {"maxEdits": 1, "prefixLength": 2}
182+
}
183+
})
217184

218185
if cast is not None:
219-
# See comments above regarding fuzzy search and AND logic for multi-word queries.
220-
cast_terms = cast.split()
221-
if len(cast_terms) == 1:
222-
search_phrases.append({
223-
"text": {
224-
"query": cast,
225-
"path": "cast",
226-
"fuzzy": {"maxEdits": 1, "prefixLength": 2}
227-
}
228-
})
229-
else:
230-
search_phrases.append({
231-
"compound": {
232-
"must": [
233-
{
234-
"text": {
235-
"query": term,
236-
"path": "cast",
237-
"fuzzy": {"maxEdits": 1, "prefixLength": 2}
238-
}
239-
}
240-
for term in cast_terms
241-
]
242-
}
243-
})
186+
# See comments above regarding fuzzy search and matchCriteria for multi-word queries.
187+
search_phrases.append({
188+
"text": {
189+
"query": cast,
190+
"path": "cast",
191+
"matchCriteria": "all",
192+
"fuzzy": {"maxEdits": 1, "prefixLength": 2}
193+
}
194+
})
244195

245196
if not search_phrases:
246197
raise HTTPException(

0 commit comments

Comments
 (0)