Skip to content

Refactor search#4179

Merged
ildyria merged 11 commits intomasterfrom
refactor-search
Mar 14, 2026
Merged

Refactor search#4179
ildyria merged 11 commits intomasterfrom
refactor-search

Conversation

@ildyria
Copy link
Copy Markdown
Member

@ildyria ildyria commented Mar 12, 2026

Summary by CodeRabbit

  • New Features

    • Token-based search (modifiers like date:, tag:, color/colour:, ratio:, rating:, type:, field filters) and strategy-driven query handling
    • Advanced Search panel with token assembly, improved search input, and results auto-scroll
    • Color-palette similarity matching with configurable distance and user-specific rating filters plus a 1–5 star control
  • Internationalization

    • Added advanced-search translations for many locales
  • Bug Fixes

    • Improved search accuracy and consistent AND/OR term behavior
  • Documentation

    • Specs, plans and task lists for the refactored search system
  • Tests

    • New unit and feature tests for parser, strategies, and scenarios

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 12, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Replaces plain-term search with a token-based system: adds SearchToken DTO and parser, per-modifier strategy contracts and implementations for photos and albums, request/controller and frontend token integration, colour-distance config migration, many new tests, and i18n strings for an Advanced Search UI.

Changes

Cohort / File(s) Summary
Core token model & parser
app/DTO/Search/SearchToken.php, app/Actions/Search/SearchTokenParser.php, app/Actions/Search/ColourNameMap.php
Adds immutable SearchToken DTO, a robust SearchTokenParser (modifiers, operators, prefixes, rating sub-modifiers, validation), and a ColourNameMap constant for name→hex lookup.
Photo search core & strategies
app/Actions/Search/PhotoSearch.php, app/Contracts/Search/PhotoSearchTokenStrategy.php, app/Actions/Search/Strategies/*
Refactors PhotoSearch to accept arrays of SearchToken and dispatch per-token filters via a strategy registry; introduces many PhotoSearchTokenStrategy implementations (PlainText, Tag, Date, Type, Ratio, Colour, FieldLike, Rating, etc.).
Album search core & strategies
app/Actions/Search/AlbumSearch.php, app/Contracts/Search/AlbumSearchTokenStrategy.php, app/Actions/Search/Strategies/Album/*
Refactors AlbumSearch to accept tokens, adds AlbumSearchTokenStrategy interface and album-specific strategies (title/description FieldLike, AlbumDateStrategy), and updates method signatures to tokens.
Colour strategy & config
app/Actions/Search/Strategies/ColourStrategy.php, database/migrations/2026_03_12_000000_add_search_colour_distance_config.php
Adds ColourStrategy implementing palette-distance EXISTS query logic and a migration/config entry (search_colour_distance) used for colour similarity.
HTTP layer / requests
app/Http/Requests/Search/GetSearchRequest.php, app/Contracts/Http/Requests/HasSearchTokens.php, app/Http/Requests/Traits/HasSearchTokensTrait.php, app/Http/Controllers/Gallery/SearchController.php
Integrates SearchTokenParser into request processing, introduces HasSearchTokens contract/trait, renames request state to tokens, and forwards token arrays to search services.
Frontend token assembler & components
resources/js/composables/useSearchTokenAssembler.ts, resources/js/components/forms/search/AdvancedSearchPanel.vue, resources/js/components/forms/search/SearchInputBar.vue, resources/js/components/forms/search/SearchBox.vue
Adds token assembler/parser composable and AdvancedSearchPanel + SearchInputBar; refactors SearchBox to sync text input, assembled tokens, and advanced panel state and emit tokenized searches.
Rating UI & composables
resources/js/components/forms/basic/rating.vue, resources/js/components/gallery/.../PhotoRatingOverlay.vue, resources/js/components/gallery/.../PhotoRatingWidget.vue, resources/js/composables/photo/useRating.ts
Adds reusable Rating component, swaps inline rating UIs to it, and removes hover state from useRating's public API.
Tests — unit & feature
tests/Unit/Actions/Search/SearchTokenParserTest.php, tests/Feature_v2/Search/PhotoSearchTest.php, tests/Feature_v2/Search/AlbumSearchTest.php
Adds unit tests for the parser and comprehensive feature tests covering photo and album token-search scenarios, validation, and auth-gated rating behaviors.
Docs & roadmap
docs/specs/.../027-search-refactoring/*, docs/specs/.../028-search-ui-refactor/*, docs/specs/4-architecture/open-questions.md, docs/specs/4-architecture/roadmap.md
Adds detailed plan/spec/tasks for the search refactor and UI, plus roadmap and open-question updates documenting architecture, increments, and test matrix.
Localization
lang/*/gallery.php (multiple locales)
Adds Advanced Search translation keys across many locales (title, fields, placeholders, type/orientation/EXIF labels).

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐇 I nibble tokens, tidy and bright,
Tags and dates folded neat in the night.
Strategies hop where wild SQL once sprawled,
Parsers and panels—order installed.
Hop, search, hop—let the results take flight. 🌿

🚥 Pre-merge checks | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 46.72% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

📝 Coding Plan
  • Generate coding plan for human review comments

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.

@codecov
Copy link
Copy Markdown

codecov bot commented Mar 13, 2026

Codecov Report

❌ Patch coverage is 97.27626% with 7 lines in your changes missing coverage. Please review.
✅ Project coverage is 90.19%. Comparing base (4ad8c8c) to head (ee1918a).
⚠️ Report is 3 commits behind head on master.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@ildyria ildyria marked this pull request as ready for review March 14, 2026 00:49
@ildyria ildyria requested a review from a team as a code owner March 14, 2026 00:49
Copy link
Copy Markdown

@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: 11

Caution

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

⚠️ Outside diff range comments (1)
docs/specs/4-architecture/roadmap.md (1)

107-107: ⚠️ Potential issue | 🟡 Minor

Update the roadmap footer timestamp.

Line 107 still shows *Last updated: 2026-03-03* even though this file was modified. Please refresh it to the actual update date.

As per coding guidelines, "**/*.md: At the bottom of documentation files, add an hr line followed by 'Last updated: [date of the update]'."

🧹 Nitpick comments (8)
lang/fa/gallery.php (1)

48-79: Translation values are English placeholders in a Persian locale file.

The new advanced search block adds 32 translation strings, but all values are in English rather than Persian. This follows an existing pattern in the file (e.g., lines 13-20, 87-96), so it's consistent, but these should ideally be translated to Persian for a complete localization.

Examples of expected Persian translations based on existing patterns in this file:

  • 'title''جستجوی پیشرفته' (similar to line 42)
  • 'description''توضیحات' (similar to line 181)
  • 'location''موقعیت' (similar to line 220)
  • 'tags''برچسب‌ها' (similar to line 228)
app/Actions/Search/Strategies/ColourStrategy.php (1)

80-84: Consider validating hex format before returning.

The resolveHex method accepts any string starting with # without validating that it's a valid 6-digit hex colour code. Malformed inputs like #xyz or #ff will pass through and may cause issues in Colour::fromHex().

♻️ Proposed fix to add hex validation
 private function resolveHex(string $value): string
 {
 	if (str_starts_with($value, '#')) {
+		$hex = strtolower($value);
+		if (!preg_match('/^#[0-9a-f]{6}$/', $hex)) {
+			throw ValidationException::withMessages(['term' => "Invalid hex colour '{$value}'. Use format `#rrggbb`."]);
+		}
-		return strtolower($value);
+		return $hex;
 	}
resources/js/components/forms/basic/rating.vue (1)

29-34: Consider using toRef instead of manual watch synchronization.

The current pattern of copying props to local refs and using a watcher to sync them can be simplified with Vue's toRef for reactive prop references, reducing boilerplate.

♻️ Simplified approach using toRef
-import { ref, watch } from "vue";
+import { ref, toRef } from "vue";

 const props = defineProps<{
 	loading: boolean;
 	selectedRating: number | undefined;
 	handleRatingClick: (rating: 1 | 2 | 3 | 4 | 5) => void;
 	amber?: boolean;
 }>();

-const loading = ref(props.loading);
+const isLoading = toRef(props, 'loading');
 const hoverRating = ref<number | null>(null);
-const selectedRating = ref(props.selectedRating);
+const currentRating = toRef(props, 'selectedRating');

 // ... handlers remain the same ...

-watch(
-	() => [props.loading, props.selectedRating],
-	([newLoading, newSelectedRating]) => {
-		loading.value = newLoading as boolean;
-		selectedRating.value = newSelectedRating as number | undefined;
-	},
-);
resources/js/components/forms/search/SearchBox.vue (1)

94-94: Move import statement to the top of the script block.

The watch import on line 94 should be grouped with other Vue imports at line 15 for consistency and readability.

♻️ Proposed fix
-import { ref, onMounted, nextTick } from "vue";
+import { ref, onMounted, nextTick, watch } from "vue";

Then remove line 94:

-import { watch } from "vue";
app/Actions/Search/PhotoSearch.php (1)

95-117: Strategy registry is rebuilt on every search query.

The buildStrategyRegistry() method creates new strategy instances on each call. Since these strategies are stateless, consider caching them as a class property for minor performance improvement in high-traffic scenarios.

♻️ Proposed optimization
 class PhotoSearch
 {
+	/** `@var` array<string, PhotoSearchTokenStrategy>|null */
+	private ?array $strategy_registry = null;
+
 	public function __construct(
 		protected readonly ConfigManager $config_manager,
 		protected PhotoQueryPolicy $photo_query_policy,
 	) {
 	}
 
 	// ... other methods ...
 
 	private function buildStrategyRegistry(): array
 	{
+		if ($this->strategy_registry !== null) {
+			return $this->strategy_registry;
+		}
+
-		return [
+		$this->strategy_registry = [
 			'' => new PlainTextStrategy(),
 			// ... rest of strategies
 		];
+
+		return $this->strategy_registry;
 	}
 }
resources/js/components/forms/search/AdvancedSearchPanel.vue (2)

11-22: Inconsistent label positioning in FloatLabel components.

In lines 11-14, the <label> comes after <InputText>, but in lines 15-18 and 19-22, the <label> comes before <InputText>. While PrimeVue's FloatLabel may handle both, consistency improves maintainability.

♻️ Suggested fix for consistency
 		<FloatLabel variant="on">
-			<label class="text-xs font-medium text-muted-color">{{ $t("gallery.search.advanced.description") }}</label>
 			<InputText v-model="description" `@update`:model-value="onFieldChange" />
+			<label class="text-xs font-medium text-muted-color">{{ $t("gallery.search.advanced.description") }}</label>
 		</FloatLabel>
 		<FloatLabel variant="on">
-			<label class="text-xs font-medium text-muted-color">{{ $t("gallery.search.advanced.location") }}</label>
 			<InputText v-model="location" `@update`:model-value="onFieldChange" />
+			<label class="text-xs font-medium text-muted-color">{{ $t("gallery.search.advanced.location") }}</label>
 		</FloatLabel>

319-320: Specify radix parameter for parseInt.

parseInt without a radix can lead to unexpected results with certain string formats. Always pass 10 as the second argument for decimal parsing.

♻️ Proposed fix
-	ratingMin.value = advanced.ratingMin ? parseInt(advanced.ratingMin) : undefined;
-	ratingOwn.value = advanced.ratingOwn ? parseInt(advanced.ratingOwn) : undefined;
+	ratingMin.value = advanced.ratingMin ? parseInt(advanced.ratingMin, 10) : undefined;
+	ratingOwn.value = advanced.ratingOwn ? parseInt(advanced.ratingOwn, 10) : undefined;
tests/Feature_v2/Search/PhotoSearchTest.php (1)

339-351: Consider guarding against null taken_at.

Line 341 calls $this->photo1->taken_at->format('Y-m-d') without checking if taken_at is null. If the fixture changes, this could cause a null pointer exception.

🛡️ Defensive check
 public function testDateExactMatchesPhotoTakenOnDate(): void
 {
+	$this->assertNotNull($this->photo1->taken_at, 'Fixture photo1 must have taken_at set');
 	$date = $this->photo1->taken_at->format('Y-m-d');

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: de5b245b-e5fa-4a79-b707-583c1397e5dd

📥 Commits

Reviewing files that changed from the base of the PR and between 4ad8c8c and 7d7cb9a.

📒 Files selected for processing (65)
  • app/Actions/Search/AlbumSearch.php
  • app/Actions/Search/ColourNameMap.php
  • app/Actions/Search/PhotoSearch.php
  • app/Actions/Search/SearchTokenParser.php
  • app/Actions/Search/Strategies/Album/AlbumDateStrategy.php
  • app/Actions/Search/Strategies/Album/AlbumFieldLikeStrategy.php
  • app/Actions/Search/Strategies/ColourStrategy.php
  • app/Actions/Search/Strategies/DateStrategy.php
  • app/Actions/Search/Strategies/FieldLikeStrategy.php
  • app/Actions/Search/Strategies/PlainTextStrategy.php
  • app/Actions/Search/Strategies/RatingStrategy.php
  • app/Actions/Search/Strategies/RatioStrategy.php
  • app/Actions/Search/Strategies/TagStrategy.php
  • app/Actions/Search/Strategies/TypeStrategy.php
  • app/Contracts/Http/Requests/HasSearchTokens.php
  • app/Contracts/Search/AlbumSearchTokenStrategy.php
  • app/Contracts/Search/PhotoSearchTokenStrategy.php
  • app/DTO/Search/SearchToken.php
  • app/Http/Controllers/Gallery/SearchController.php
  • app/Http/Requests/Search/GetSearchRequest.php
  • app/Http/Requests/Traits/HasSearchTokensTrait.php
  • database/migrations/2026_03_12_000000_add_search_colour_distance_config.php
  • docs/specs/4-architecture/features/027-search-refactoring/plan.md
  • docs/specs/4-architecture/features/027-search-refactoring/spec.md
  • docs/specs/4-architecture/features/027-search-refactoring/tasks.md
  • docs/specs/4-architecture/features/028-search-ui-refactor/plan.md
  • docs/specs/4-architecture/features/028-search-ui-refactor/spec.md
  • docs/specs/4-architecture/features/028-search-ui-refactor/tasks.md
  • docs/specs/4-architecture/open-questions.md
  • docs/specs/4-architecture/roadmap.md
  • lang/ar/gallery.php
  • lang/bg/gallery.php
  • lang/cz/gallery.php
  • lang/de/gallery.php
  • lang/el/gallery.php
  • lang/en/gallery.php
  • lang/es/gallery.php
  • lang/fa/gallery.php
  • lang/fr/gallery.php
  • lang/hu/gallery.php
  • lang/it/gallery.php
  • lang/ja/gallery.php
  • lang/nl/gallery.php
  • lang/no/gallery.php
  • lang/pl/gallery.php
  • lang/pt/gallery.php
  • lang/ru/gallery.php
  • lang/sk/gallery.php
  • lang/sv/gallery.php
  • lang/vi/gallery.php
  • lang/zh_CN/gallery.php
  • lang/zh_TW/gallery.php
  • resources/js/components/forms/basic/rating.vue
  • resources/js/components/forms/search/AdvancedSearchPanel.vue
  • resources/js/components/forms/search/SearchBox.vue
  • resources/js/components/forms/search/SearchInputBar.vue
  • resources/js/components/gallery/photoModule/PhotoRatingOverlay.vue
  • resources/js/components/gallery/photoModule/PhotoRatingWidget.vue
  • resources/js/components/gallery/searchModule/SearchPanel.vue
  • resources/js/composables/photo/useRating.ts
  • resources/js/composables/useSearchTokenAssembler.ts
  • resources/js/views/gallery-panels/Search.vue
  • tests/Feature_v2/Search/AlbumSearchTest.php
  • tests/Feature_v2/Search/PhotoSearchTest.php
  • tests/Unit/Actions/Search/SearchTokenParserTest.php
💤 Files with no reviewable changes (1)
  • resources/js/composables/photo/useRating.ts

Copy link
Copy Markdown

@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


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 183639e4-e9a3-48c5-bf77-1ce744522bb4

📥 Commits

Reviewing files that changed from the base of the PR and between 7d7cb9a and 9beaa20.

📒 Files selected for processing (3)
  • app/Actions/Search/Strategies/Album/AlbumFieldLikeStrategy.php
  • app/Actions/Search/Strategies/PlainTextStrategy.php
  • resources/js/composables/useSearchTokenAssembler.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • app/Actions/Search/Strategies/PlainTextStrategy.php

Copy link
Copy Markdown

@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.

♻️ Duplicate comments (1)
resources/js/composables/useSearchTokenAssembler.ts (1)

69-72: ⚠️ Potential issue | 🟠 Major

Handle embedded double quotes before assembling tokens.

assembleStringToken still produces malformed tokens when a value contains ", which can break the token grammar and backend parsing. This concern was already raised earlier and appears unresolved in this revision.

🐛 Proposed fix
 function assembleStringToken(modifier: string, value: string): string {
-	const token = `${modifier}:${value}`;
-	return value.includes(" ") ? `"${token}"` : token;
+	// Keep token grammar valid: raw double quotes terminate quoted tokens.
+	const sanitized = value.replace(/"/g, "");
+	const token = `${modifier}:${sanitized}`;
+	return /\s/.test(sanitized) ? `"${token}"` : token;
 }
🧹 Nitpick comments (1)
app/Actions/Search/AlbumSearch.php (1)

107-141: Consider reusing the strategy registry instead of rebuilding it per call.

buildAlbumStrategyRegistry() is invoked on each addSearchCondition() call; caching it on the class would reduce repeated allocations.

♻️ Optional refactor
 class AlbumSearch
 {
+	/** `@var` array<string, AlbumSearchTokenStrategy> */
+	private array $album_strategy_registry;
+
 	public function __construct(
 		protected AlbumQueryPolicy $album_query_policy,
 	) {
+		$this->album_strategy_registry = $this->buildAlbumStrategyRegistry();
 	}
@@
 	private function addSearchCondition(array $tokens, AlbumBuilder|TagAlbumBuilder|FixedQueryBuilder $query): void
 	{
-		$strategies = $this->buildAlbumStrategyRegistry();
+		$strategies = $this->album_strategy_registry;
 		$applied = false;

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 8df85bd2-e23f-4452-9380-59ee00f75f5c

📥 Commits

Reviewing files that changed from the base of the PR and between 9beaa20 and 0a4dd74.

📒 Files selected for processing (4)
  • app/Actions/Search/AlbumSearch.php
  • app/Actions/Search/Strategies/Album/AlbumFieldLikeStrategy.php
  • resources/js/composables/useSearchTokenAssembler.ts
  • tests/Feature_v2/Search/AlbumSearchTest.php
🚧 Files skipped from review as they are similar to previous changes (2)
  • tests/Feature_v2/Search/AlbumSearchTest.php
  • app/Actions/Search/Strategies/Album/AlbumFieldLikeStrategy.php

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Copy link
Copy Markdown

@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: 1


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 05fdb3fb-84d3-406d-867c-7d6e8443d180

📥 Commits

Reviewing files that changed from the base of the PR and between 5ea42f2 and ee1918a.

📒 Files selected for processing (1)
  • app/Actions/Search/Strategies/DateStrategy.php

@ildyria ildyria merged commit 592600f into master Mar 14, 2026
44 checks passed
@ildyria ildyria deleted the refactor-search branch March 14, 2026 22:12
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.

1 participant