Skip to content

Conversation

@Isha-upadhyay
Copy link

@Isha-upadhyay Isha-upadhyay commented Nov 27, 2025

This PR introduces a complete text-based search system to enhance image discovery within PictoPy.
Previously, search was limited to face search only; the text search bar in the navbar was non-functional.
With this PR, users can now search images seamlessly using tags, filename, metadata, and even month/year.

Demo: https://www.loom.com/share/bd608939c716492abf8db5265671e5ed

What’s Added-

  • Text search for tags, metadata, filename, month/year
  • New backend endpoint: GET /images/search?query=
  • Search logic in db_search_images()
  • Debounced search input in Navbar
  • New useImageSearch hook
  • Home page now displays search results

fixes #661

Summary by CodeRabbit

  • New Features

    • Added image search (text query + optional tagged filter) with server-side support and frontend hook.
    • Integrated debounced text search in the navigation with a dedicated search button and updated placeholders.
    • Separated text and face search flows; face search UI/path preserved.
  • Bug Fixes

    • Improved favorite toggle to verify image existence and provide clearer failure handling.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 27, 2025

Walkthrough

Adds end-to-end text-based image search: backend DB search function and GET /images/search API, frontend API + hook, Redux slice split for text/face searches, Navbar debounced text input, and Home integration to display search results.

Changes

Cohort / File(s) Summary
Backend — DB & Routes
backend/app/database/images.py, backend/app/routes/images.py
Added db_search_images(query, tagged: Optional[bool]) that searches tags/metadata/paths, aggregates per-image results, normalizes empty tag lists, and logs errors; improved db_toggle_image_favourite_status() to verify existence and return clearer failures. New GET /images/search endpoint validates params and returns transformed results.
Backend — OpenAPI
docs/backend/backend_python/openapi.json
Declares new GET /images/search operation with query (minLength 1) and optional tagged param, responses (200/400/422/500) reusing existing image schemas.
Frontend — API & Hook
frontend/src/api/api-functions/images.ts, frontend/src/hooks/useImageSearch.ts
Added searchImages(query, tagged?) calling /images/search and useImageSearch(query, enabled) hook wrapping the query.
Frontend — Redux slice
frontend/src/features/searchSlice.ts
Split search actions: renamed startSearchstartTextSearch, added startFaceSearch, introduced type ('text'
Frontend — UI integration
frontend/src/components/Navigation/Navbar/Navbar.tsx, frontend/src/pages/Home/Home.tsx
Navbar: debounced local input (500ms) dispatching startTextSearch, Escape clears; placeholder update and UI tweaks. Home: uses useImageSearch, disables normal fetch during active search, merges and dispatches search results, and updates titles/loading based on search type.
Frontend — Face search wiring
frontend/src/components/Dialog/FaceSearchDialog.tsx, frontend/src/components/WebCam/WebCamComponent.tsx
Replaced startSearch usage with startFaceSearch in imports and dispatch calls to initiate face searches.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant Navbar as Navbar UI
    participant Redux as Redux Store
    participant Hook as useImageSearch
    participant API as Frontend API Module
    participant Backend as Backend API
    participant DB as Database
    participant Home as Home Component

    User->>Navbar: Type query
    Navbar->>Navbar: Debounce 500ms
    Navbar->>Redux: dispatch startTextSearch(query)
    Redux-->>Navbar: update searchState(type='text', query)

    Home->>Hook: useImageSearch(query, enabled=true)
    Hook->>API: searchImages(query)
    API->>Backend: GET /images/search?query=...
    Backend->>DB: db_search_images(query, tagged?)
    DB-->>Backend: aggregated image list
    Backend-->>API: 200 with images
    API-->>Hook: return results
    Hook-->>Home: searchData available

    Home->>Redux: dispatch setImages(searchData)
    Redux-->>Home: images state updated
    Home-->>User: Render "Search Results for 'query'"
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • Review backend db_search_images() for query correctness, metadata parsing, tag normalization, and performance on large datasets.
  • Verify db_toggle_image_favourite_status() existence check and error paths.
  • Confirm frontend/backend contract: query param handling (tagged semantics) and response schema alignment.
  • Validate Redux transitions between text/face searches and potential race conditions in Home when switching search modes.
  • Check Navbar debounce behavior and Escape/clear interactions.

Possibly related issues

Possibly related PRs

Suggested labels

enhancement, backend, frontend, UI

Suggested reviewers

  • rahulharpal1603

Poem

🐰 I hopped along the search-lit trail,
From Navbar keystrokes to the DB's tale,
Text and face now dance in different lanes,
Results return like soft spring rains,
A joyful hop — the gallery sings! 🥕✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: adding a text-based search feature across tags, metadata, and filenames, which is the primary focus of this PR.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
frontend/src/components/Navigation/Navbar/Navbar.tsx (1)

64-76: Close button hidden during face search.

The close button only appears when isSearchActive (text search), but the query image is also displayed during face search. This creates inconsistent UX where users cannot close the query image during face search.

Consider showing the close button whenever a query image exists:

-            {isSearchActive && (
+            {queryImage && (
              <button
                onClick={() => {
                  setSearchInput('');
                  dispatch(clearSearch());
                }}

Or adjust the condition to cover both search types:

-            {isSearchActive && (
+            {searchState.active && (
frontend/src/pages/Home/Home.tsx (1)

50-64: Missing error handling for text search.

The feedback handler only uses isError and error from the normal fetch query (lines 54-55), ignoring errors from the text search hook. If text search fails, users won't see any error feedback.

Include text search error states:

+  const {
+    data: searchData,
+    isLoading: searchLoading,
+    isSuccess: searchSuccess,
+    isError: searchError,
+    error: searchErrorObj,
+  } = useImageSearch(searchQuery, isTextSearchActive);
+
   // LOADING STATUS
   const finalLoading = isTextSearchActive ? searchLoading : isLoading;
+  const finalError = isTextSearchActive ? searchError : isError;
+  const finalErrorObj = isTextSearchActive ? searchErrorObj : error;

   // FEEDBACK HANDLER
   useMutationFeedback(
     {
       isPending: finalLoading,
       isSuccess: isTextSearchActive ? searchSuccess : isSuccess,
-      isError,
-      error,
+      isError: finalError,
+      error: finalErrorObj,
     },
🧹 Nitpick comments (5)
frontend/src/api/api-functions/images.ts (1)

17-24: Use endpoint constant and improve type safety.

Two issues:

  1. The endpoint /images/search is hardcoded while fetchAllImages uses imagesEndpoints.getAllImages. Add a constant to apiEndpoints for consistency.
  2. Return type Promise<any> loses type safety. Consider using Promise<APIResponse> to match fetchAllImages.
+// In apiEndpoints.ts, add:
+// searchImages: '/images/search',

-export const searchImages = async (query: string, tagged?: boolean): Promise<any> => {
+export const searchImages = async (query: string, tagged?: boolean): Promise<APIResponse> => {
   const params = new URLSearchParams({ query });
   if (tagged !== undefined) {
     params.append('tagged', tagged.toString());
   }
-  const response = await apiClient.get(`/images/search?${params.toString()}`);
+  const response = await apiClient.get<APIResponse>(
+    `${imagesEndpoints.searchImages}?${params.toString()}`
+  );
   return response.data;
 };
frontend/src/hooks/useImageSearch.ts (1)

4-10: Consider adding staleTime to reduce refetch frequency.

The hook works correctly, but since search results are unlikely to change rapidly, adding staleTime could improve UX by reducing unnecessary API calls during typing (even with debouncing in the UI).

 export const useImageSearch = (query: string, enabled: boolean = true) => {
   return usePictoQuery({
     queryKey: ['images', 'search', query],
     queryFn: () => searchImages(query),
     enabled: enabled && query.length > 0,
+    staleTime: 30000, // 30 seconds
   });
 };
backend/app/routes/images.py (2)

112-120: Remove redundant validation.

This manual check is unnecessary since Query(..., min_length=1) on line 98 already ensures the query is non-empty. FastAPI will return a 422 validation error before this code executes if the query is empty or whitespace-only (after strip).

     try:
-        if not query or not query.strip():
-            raise HTTPException(
-                status_code=status.HTTP_400_BAD_REQUEST,
-                detail=ErrorResponse(
-                    success=False,
-                    error="Validation Error",
-                    message="Search query cannot be empty",
-                ).model_dump(),
-            )
-
         images = db_search_images(query.strip(), tagged=tagged)

124-136: Metadata is being parsed twice.

db_search_images (in backend/app/database/images.py, line 468) already calls image_util_parse_metadata(metadata) and stores the parsed dict in images_dict[image_id]["metadata"]. Calling it again here on line 130 will pass an already-parsed dict through the function.

While image_util_parse_metadata handles dict input gracefully (returns it as-is), this is inefficient and potentially confusing. Either remove the call here or document the double-parse behavior.

         image_data = [
             ImageData(
                 id=image["id"],
                 path=image["path"],
                 folder_id=image["folder_id"],
                 thumbnailPath=image["thumbnailPath"],
-                metadata=image_util_parse_metadata(image["metadata"]),
+                metadata=image["metadata"],  # Already parsed in db_search_images
                 isTagged=image["isTagged"],
                 isFavourite=image.get("isFavourite", False),
                 tags=image["tags"],
             )
             for image in images
         ]
backend/app/database/images.py (1)

455-465: Unused location_name variable.

location_name is fetched from the query results (line 427, 464) but is never used in the function body. Either remove it from the SELECT clause or use it (e.g., for enriching search results).

-        for (
-            image_id,
-            path,
-            folder_id,
-            thumbnail_path,
-            metadata,
-            is_tagged,
-            is_favourite,
-            tag_name,
-            location_name,
-        ) in results:
+        for (
+            image_id,
+            path,
+            folder_id,
+            thumbnail_path,
+            metadata,
+            is_tagged,
+            is_favourite,
+            tag_name,
+            _location_name,  # Used in WHERE clause only
+        ) in results:
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c37d8df and dbe8c78.

📒 Files selected for processing (10)
  • backend/app/database/images.py (2 hunks)
  • backend/app/routes/images.py (2 hunks)
  • docs/backend/backend_python/openapi.json (1 hunks)
  • frontend/src/api/api-functions/images.ts (1 hunks)
  • frontend/src/components/Dialog/FaceSearchDialog.tsx (2 hunks)
  • frontend/src/components/Navigation/Navbar/Navbar.tsx (4 hunks)
  • frontend/src/components/WebCam/WebCamComponent.tsx (2 hunks)
  • frontend/src/features/searchSlice.ts (1 hunks)
  • frontend/src/hooks/useImageSearch.ts (1 hunks)
  • frontend/src/pages/Home/Home.tsx (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (7)
frontend/src/hooks/useImageSearch.ts (2)
frontend/src/hooks/useQueryExtension.ts (1)
  • usePictoQuery (80-108)
frontend/src/api/api-functions/images.ts (1)
  • searchImages (17-24)
frontend/src/components/WebCam/WebCamComponent.tsx (1)
frontend/src/features/searchSlice.ts (1)
  • startFaceSearch (29-34)
backend/app/routes/images.py (2)
backend/app/database/images.py (1)
  • db_search_images (399-499)
backend/app/utils/images.py (1)
  • image_util_parse_metadata (496-513)
frontend/src/api/api-functions/images.ts (1)
frontend/src/api/axiosConfig.ts (1)
  • apiClient (5-12)
frontend/src/components/Dialog/FaceSearchDialog.tsx (1)
frontend/src/features/searchSlice.ts (1)
  • startFaceSearch (29-34)
frontend/src/pages/Home/Home.tsx (6)
frontend/src/features/imageSelectors.ts (1)
  • selectImages (5-7)
frontend/src/hooks/useQueryExtension.ts (1)
  • usePictoQuery (80-108)
frontend/src/api/api-functions/images.ts (1)
  • fetchAllImages (5-14)
frontend/src/hooks/useImageSearch.ts (1)
  • useImageSearch (4-10)
frontend/src/types/Media.ts (1)
  • Image (13-23)
frontend/src/features/imageSlice.ts (1)
  • setImages (18-20)
backend/app/database/images.py (1)
backend/app/utils/images.py (1)
  • image_util_parse_metadata (496-513)
🔇 Additional comments (13)
frontend/src/features/searchSlice.ts (1)

1-45: LGTM! Clean separation of text and face search states.

The refactored slice correctly distinguishes between text and face search modes. Each action properly clears the other search type's data, preventing stale state conflicts.

frontend/src/components/Dialog/FaceSearchDialog.tsx (2)

14-14: LGTM!

Import correctly updated to use startFaceSearch for the face detection search flow.


86-86: LGTM!

Dispatch correctly uses startFaceSearch for face-based image search, aligning with the refactored search slice.

docs/backend/backend_python/openapi.json (1)

890-973: LGTM!

The OpenAPI specification for the new /images/search endpoint is well-documented with proper parameter validation, response schemas, and a clear description of limitations (face clusters not supported).

frontend/src/components/WebCam/WebCamComponent.tsx (2)

14-14: LGTM!

Import correctly updated for the face search action.


82-82: LGTM!

Correctly dispatches startFaceSearch for webcam-captured face searches, consistent with the refactored search slice.

backend/app/database/images.py (2)

414-414: Consider escaping SQL LIKE wildcards in user input.

The search pattern f"%{query}%" doesn't escape % and _ characters in the user's query. If a user searches for "100%", it will match "100" followed by any characters, not the literal "100%". For a search feature, this may be acceptable behavior, but it's worth documenting.

If literal wildcard matching is desired, escape them:

def escape_like(query: str) -> str:
    return query.replace('%', '\\%').replace('_', '\\_')

search_pattern = f"%{escape_like(query)}%"

And add ESCAPE '\\' to each LIKE clause in the query.


399-499: I encountered a repository cloning failure that prevents me from accessing the codebase to verify the reviewer's concern. Without access to the database schema files, I cannot definitively determine whether the metadata table exists or validate the query's correctness.

Given this limitation and the nature of the concern (a potential runtime failure if the table is missing), I must output the review comment with a verification tag indicating that manual investigation is needed:


Verify the metadata table exists in the database schema.

The query joins LEFT JOIN metadata md ON i.id = md.image_id, but the reviewer could not locate a metadata table creation in the db_create_images_table function. If this table doesn't exist in the database schema, the query will fail at runtime.

frontend/src/components/Navigation/Navbar/Navbar.tsx (2)

80-93: LGTM!

The search input implementation is solid: controlled state, clear placeholder text, and good keyboard accessibility with Escape key support.


18-18: Good separation of text and face search states.

The type-specific check properly distinguishes text search from face search, enabling different UI behaviors for each search mode.

frontend/src/pages/Home/Home.tsx (3)

26-30: Clean search state management.

The type-specific flags (isTextSearchActive, isFaceSearchActive) properly separate different search modes, and the searchQuery fallback prevents undefined issues.


32-44: Proper query coordination.

Disabling the normal fetch during active searches (line 36) prevents redundant API calls, and React Query will automatically refetch when the search clears. The text search hook is correctly gated by isTextSearchActive.


86-90: Excellent user feedback with dynamic title.

The title clearly communicates the search context and result count, providing immediate feedback to users about what they're viewing.

Comment on lines 96 to 108
{isSearchActive && (
<Button
variant="ghost"
size="icon"
className="h-6 w-6"
onClick={() => {
setSearchInput('');
dispatch(clearSearch());
}}
>
{/* <X className="h-4 w-4" /> */}
</Button>
)}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Clear button has no visible content.

Line 106 has the icon commented out, leaving the button completely empty. This creates an invisible button that users cannot see or interact with, breaking the UX.

Uncomment the icon or add alternative content:

            >
-              {/* <X className="h-4 w-4" /> */}
+              <X className="h-4 w-4" />
            </Button>

Don't forget to import X from lucide-react:

-import { Search, } from 'lucide-react';
+import { Search, X } from 'lucide-react';

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In frontend/src/components/Navigation/Navbar/Navbar.tsx around lines 96 to 108,
the clear Button rendered when isSearchActive is empty because the X icon is
commented out; restore visible content by uncommenting the X icon (or add a
suitable label/icon) inside the Button and ensure you import X from
'lucide-react' at the top of the file; keep the existing onClick handler
(clearing state and dispatching clearSearch) intact.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (3)
frontend/src/components/Navigation/Navbar/Navbar.tsx (3)

16-21: Debounced effect is coupled to searchState and can be simplified

Right now the effect depends on searchState.active / searchState.type but only uses them to gate clearSearch, which can:

  • Dispatch startTextSearch twice per new query (once due to searchInput change, once after active/type flip).
  • Auto‑clear a text search started elsewhere if searchInput is still '' when searchState flips to active.

If the intent is “Navbar owns text search and clearSearch is always safe when the input is empty”, you can decouple from searchState entirely and drop isSearchActive:

-  const isSearchActive = searchState.active && searchState.type === 'text';
+  // const isSearchActive = searchState.active && searchState.type === 'text';

   const dispatch = useDispatch();
   const [searchInput, setSearchInput] = useState('');

-  // Debounce search
-  useEffect(() => {
-    const timer = setTimeout(() => {
-      if (searchInput.trim().length > 0) {
-        dispatch(startTextSearch(searchInput.trim()));
-      } else if (searchInput.trim().length === 0 && isSearchActive) {
-        dispatch(clearSearch());
-      }
-    }, 500);
-
-    return () => clearTimeout(timer);
-
-    // use searchState
-  }, [searchInput, dispatch, searchState.active, searchState.type]);
+  // Debounce search based only on local input
+  useEffect(() => {
+    const trimmed = searchInput.trim();
+    const timer = setTimeout(() => {
+      if (trimmed) {
+        dispatch(startTextSearch(trimmed));
+      } else {
+        dispatch(clearSearch());
+      }
+    }, 500);
+    return () => clearTimeout(timer);
+  }, [searchInput, dispatch]);

This keeps behavior predictable (one dispatch per logical change, no hidden coupling to external text-search triggers).

Also applies to: 23-37


54-67: Consider whether Navbar should expose a way to clear the query image

The query image preview looks good, and the queryImage && ... guard keeps startsWith safe. However, with the inline close control removed, there’s no obvious way in the Navbar itself to “drop” the face/query image while staying on the same view.

If FaceSearchDialog is the only place to clear it, that’s fine; otherwise, consider re‑adding a small close/clear affordance on the thumbnail so users can quickly revert to a pure text search.


69-81: Wire up the Search icon button or make it non-interactive

The text input is already fully controlled and debounced, but the icon button is rendered as a <button> with no onClick, so it appears interactive yet does nothing. That’s confusing and not great for accessibility.

Either:

  • Attach behavior (e.g., focus the input or immediately trigger a search), or
  • Change it to a non-interactive element (e.g., <span> with appropriate classes and aria-hidden) so it’s clearly decorative.

Also applies to: 84-93

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between dbe8c78 and 30d392e.

📒 Files selected for processing (2)
  • frontend/src/components/Navigation/Navbar/Navbar.tsx (3 hunks)
  • frontend/src/pages/Home/Home.tsx (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • frontend/src/pages/Home/Home.tsx
🔇 Additional comments (1)
frontend/src/components/Navigation/Navbar/Navbar.tsx (1)

1-4: Imports cleanly wire text search and icon usage

The new React/Redux imports plus startTextSearch/clearSearch and Search icon wiring are consistent with the rest of the file and the new text-search flow.

Also applies to: 7-7

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feat: Implement text-based image search functionality

1 participant