Skip to content

Commit 8276d84

Browse files
feat: implement vector search UI (#23)
* feat: add vector search UI * Fix Java vector search NullPointerException and dirty data handling - Replace List.of() with Arrays.asList() to handle null values in MongoDB aggregation pipeline - Add safe type conversion for year field to handle dirty data (e.g., '1994è1998') - Improve error handling with specific catch blocks for IOException and InterruptedException - Add validation for Voyage AI API response structure - Remove debug logging statements * Fix Python vector search dirty data handling and missing import - Add missing import for voyage_ai_available function - Add conditional projection to handle non-integer year values (e.g., '1994è1998') - Return None for invalid year data instead of causing validation errors * add pagination to search results --------- Co-authored-by: cbullinger <[email protected]>
1 parent 55b7c65 commit 8276d84

File tree

9 files changed

+838
-308
lines changed

9 files changed

+838
-308
lines changed

client/app/components/SearchMovieModal/SearchMovieModal.tsx

Lines changed: 248 additions & 145 deletions
Large diffs are not rendered by default.
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
export { default } from './SearchMovieModal';
2-
export type { SearchParams } from './SearchMovieModal';
2+
export type { SearchParams, SearchType } from './SearchMovieModal';

client/app/lib/api.ts

Lines changed: 156 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,7 @@ export async function deleteMovie(id: string): Promise<{ success: boolean; error
183183
*/
184184
export async function createMovie(movieData: Omit<Movie, '_id'>): Promise<{ success: boolean; error?: string; movieId?: string }> {
185185
try {
186-
const response = await fetch(`${API_BASE_URL}/api/movies/`, {
186+
const response = await fetch(`${API_BASE_URL}/api/movies`, {
187187
method: 'POST',
188188
headers: {
189189
'Content-Type': 'application/json',
@@ -276,7 +276,7 @@ export async function deleteMoviesBatch(movieIds: string[]): Promise<{ success:
276276
}
277277
};
278278

279-
const response = await fetch(`${API_BASE_URL}/api/movies/`, {
279+
const response = await fetch(`${API_BASE_URL}/api/movies`, {
280280
method: 'DELETE',
281281
headers: {
282282
'Content-Type': 'application/json',
@@ -326,7 +326,7 @@ export async function updateMoviesBatch(movieIds: string[], updateData: Partial<
326326
}
327327
};
328328

329-
const response = await fetch(`${API_BASE_URL}/api/movies/`, {
329+
const response = await fetch(`${API_BASE_URL}/api/movies`, {
330330
method: 'PATCH',
331331
headers: {
332332
'Content-Type': 'application/json',
@@ -364,80 +364,6 @@ export async function updateMoviesBatch(movieIds: string[], updateData: Partial<
364364
}
365365
}
366366

367-
/**
368-
* Search movies using MongoDB Search across multiple fields with pagination support
369-
*/
370-
export async function searchMovies(searchParams: {
371-
plot?: string;
372-
fullplot?: string;
373-
directors?: string;
374-
writers?: string;
375-
cast?: string;
376-
limit?: number;
377-
skip?: number;
378-
search_operator?: 'must' | 'should' | 'mustNot' | 'filter';
379-
}): Promise<{ success: boolean; error?: string; movies?: Movie[]; hasNextPage?: boolean; hasPrevPage?: boolean; totalCount?: number }> {
380-
try {
381-
// Build query parameters
382-
const limit = searchParams.limit || 20;
383-
const skip = searchParams.skip || 0;
384-
385-
const queryParams = new URLSearchParams();
386-
387-
if (searchParams.plot) queryParams.append('plot', searchParams.plot);
388-
if (searchParams.fullplot) queryParams.append('fullplot', searchParams.fullplot);
389-
if (searchParams.directors) queryParams.append('directors', searchParams.directors);
390-
if (searchParams.writers) queryParams.append('writers', searchParams.writers);
391-
if (searchParams.cast) queryParams.append('cast', searchParams.cast);
392-
queryParams.append('limit', limit.toString());
393-
queryParams.append('skip', skip.toString());
394-
if (searchParams.search_operator) queryParams.append('search_operator', searchParams.search_operator);
395-
396-
const response = await fetch(`${API_BASE_URL}/api/movies/search?${queryParams}`, {
397-
method: 'GET',
398-
headers: {
399-
'Content-Type': 'application/json',
400-
},
401-
});
402-
403-
const result = await response.json();
404-
405-
if (!response.ok) {
406-
return {
407-
success: false,
408-
error: result.message || result.error?.message || `Failed to search movies: ${response.status}`
409-
};
410-
}
411-
412-
if (!result.success) {
413-
return {
414-
success: false,
415-
error: result.message || result.error?.message || 'API returned error response'
416-
};
417-
}
418-
419-
const responseData = result.data || {};
420-
const movies = responseData.movies || [];
421-
const totalCount = responseData.totalCount || 0;
422-
const hasNextPage = skip + limit < totalCount;
423-
const hasPrevPage = skip > 0;
424-
425-
return {
426-
success: true,
427-
movies,
428-
hasNextPage,
429-
hasPrevPage,
430-
totalCount
431-
};
432-
} catch (error) {
433-
console.error('Error searching movies:', error);
434-
return {
435-
success: false,
436-
error: 'Network error occurred while searching movies'
437-
};
438-
}
439-
}
440-
441367
/**
442368
* Aggregation API Functions for Aggregations
443369
*/
@@ -655,3 +581,156 @@ export async function fetchDirectorStats(limit: number = 20): Promise<{ success:
655581
};
656582
}
657583
}
584+
585+
/**
586+
* Search movies using MongoDB Search across multiple fields with pagination support
587+
*/
588+
export async function searchMovies(searchParams: {
589+
plot?: string;
590+
fullplot?: string;
591+
directors?: string;
592+
writers?: string;
593+
cast?: string;
594+
limit?: number;
595+
skip?: number;
596+
search_operator?: 'must' | 'should' | 'mustNot' | 'filter';
597+
}): Promise<{ success: boolean; error?: string; movies?: Movie[]; hasNextPage?: boolean; hasPrevPage?: boolean; totalCount?: number }> {
598+
try {
599+
// Build query parameters
600+
const limit = searchParams.limit || 20;
601+
const skip = searchParams.skip || 0;
602+
603+
const queryParams = new URLSearchParams();
604+
605+
if (searchParams.plot) queryParams.append('plot', searchParams.plot);
606+
if (searchParams.fullplot) queryParams.append('fullplot', searchParams.fullplot);
607+
if (searchParams.directors) queryParams.append('directors', searchParams.directors);
608+
if (searchParams.writers) queryParams.append('writers', searchParams.writers);
609+
if (searchParams.cast) queryParams.append('cast', searchParams.cast);
610+
queryParams.append('limit', limit.toString());
611+
queryParams.append('skip', skip.toString());
612+
if (searchParams.search_operator) queryParams.append('search_operator', searchParams.search_operator);
613+
614+
const response = await fetch(`${API_BASE_URL}/api/movies/search?${queryParams}`, {
615+
method: 'GET',
616+
headers: {
617+
'Content-Type': 'application/json',
618+
},
619+
});
620+
621+
const result = await response.json();
622+
623+
if (!response.ok) {
624+
return {
625+
success: false,
626+
error: result.message || result.error?.message || `Failed to search movies: ${response.status}`
627+
};
628+
}
629+
630+
if (!result.success) {
631+
return {
632+
success: false,
633+
error: result.message || result.error?.message || 'API returned error response'
634+
};
635+
}
636+
637+
const responseData = result.data || {};
638+
const movies = responseData.movies || [];
639+
const totalCount = responseData.totalCount || 0;
640+
const hasNextPage = skip + limit < totalCount;
641+
const hasPrevPage = skip > 0;
642+
643+
return {
644+
success: true,
645+
movies,
646+
hasNextPage,
647+
hasPrevPage,
648+
totalCount
649+
};
650+
} catch (error) {
651+
console.error('Error searching movies:', error);
652+
return {
653+
success: false,
654+
error: 'Network error occurred while searching movies'
655+
};
656+
}
657+
}
658+
659+
/**
660+
* Search movies using MongoDB Vector Search to find movies with similar plots
661+
*/
662+
export async function vectorSearchMovies(searchParams: {
663+
q: string;
664+
limit?: number;
665+
}): Promise<{ success: boolean; error?: string; movies?: Movie[]; results?: any[] }> {
666+
try {
667+
const limit = searchParams.limit || 10;
668+
669+
const queryParams = new URLSearchParams();
670+
queryParams.append('q', searchParams.q);
671+
queryParams.append('limit', limit.toString());
672+
673+
const response = await fetch(`${API_BASE_URL}/api/movies/vector-search?${queryParams}`, {
674+
method: 'GET',
675+
headers: {
676+
'Content-Type': 'application/json',
677+
},
678+
});
679+
680+
const result = await response.json();
681+
682+
if (!response.ok) {
683+
return {
684+
success: false,
685+
error: result.error || `Failed to perform vector search: ${response.status}`
686+
};
687+
}
688+
689+
if (!result.success) {
690+
return {
691+
success: false,
692+
error: result.error || 'API returned error response'
693+
};
694+
}
695+
696+
// Transform VectorSearchResult objects to Movie objects for backend compatibility
697+
const movies: Movie[] = (result.data || []).map((item: any) => {
698+
// Convert VectorSearchResult to Movie format
699+
return {
700+
_id: item._id || item.id, // Handle both _id (Python) and id (Java) field names
701+
title: item.title || '',
702+
plot: item.plot || '',
703+
poster: item.poster,
704+
year: item.year,
705+
genres: item.genres || [],
706+
directors: item.directors || [],
707+
cast: item.cast || [],
708+
// Add default values for fields not included in VectorSearchResult
709+
fullplot: undefined,
710+
released: undefined,
711+
runtime: undefined,
712+
writers: [],
713+
countries: [],
714+
languages: [],
715+
rated: undefined,
716+
awards: undefined,
717+
imdb: undefined,
718+
tomatoes: undefined,
719+
metacritic: undefined,
720+
type: undefined
721+
} as Movie;
722+
});
723+
724+
return {
725+
success: true,
726+
movies,
727+
results: result.data || []
728+
};
729+
} catch (error) {
730+
console.error('Error performing vector search:', error);
731+
return {
732+
success: false,
733+
error: 'Network error occurred while performing vector search'
734+
};
735+
}
736+
}

client/app/movies/movies.module.css

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,17 @@
148148
text-align: center;
149149
}
150150

151+
.searchInfo {
152+
margin: 2rem 0;
153+
padding: 1rem;
154+
background: #f8f9fa;
155+
border: 1px solid #e9ecef;
156+
border-radius: 8px;
157+
text-align: center;
158+
font-size: 0.9rem;
159+
color: #666;
160+
}
161+
151162
.selectAllButton {
152163
padding: 0.5rem 1rem;
153164
background: #6c757d;

0 commit comments

Comments
 (0)