Skip to content

Commit

Permalink
feat: add searching by tags
Browse files Browse the repository at this point in the history
  • Loading branch information
dav-wolff committed Jan 17, 2025
1 parent 3a2bf91 commit 8d65b47
Show file tree
Hide file tree
Showing 11 changed files with 200 additions and 6 deletions.
11 changes: 10 additions & 1 deletion mobile/openapi/lib/model/metadata_search_dto.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 10 additions & 1 deletion mobile/openapi/lib/model/random_search_dto.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 10 additions & 1 deletion mobile/openapi/lib/model/smart_search_dto.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

21 changes: 21 additions & 0 deletions open-api/immich-openapi-specs.json
Original file line number Diff line number Diff line change
Expand Up @@ -10032,6 +10032,13 @@
"nullable": true,
"type": "string"
},
"tagIds": {
"items": {
"format": "uuid",
"type": "string"
},
"type": "array"
},
"takenAfter": {
"format": "date-time",
"type": "string"
Expand Down Expand Up @@ -10645,6 +10652,13 @@
"nullable": true,
"type": "string"
},
"tagIds": {
"items": {
"format": "uuid",
"type": "string"
},
"type": "array"
},
"takenAfter": {
"format": "date-time",
"type": "string"
Expand Down Expand Up @@ -11560,6 +11574,13 @@
"nullable": true,
"type": "string"
},
"tagIds": {
"items": {
"format": "uuid",
"type": "string"
},
"type": "array"
},
"takenAfter": {
"format": "date-time",
"type": "string"
Expand Down
3 changes: 3 additions & 0 deletions open-api/typescript-sdk/src/fetch-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -792,6 +792,7 @@ export type MetadataSearchDto = {
previewPath?: string;
size?: number;
state?: string | null;
tagIds?: string[];
takenAfter?: string;
takenBefore?: string;
thumbnailPath?: string;
Expand Down Expand Up @@ -858,6 +859,7 @@ export type RandomSearchDto = {
personIds?: string[];
size?: number;
state?: string | null;
tagIds?: string[];
takenAfter?: string;
takenBefore?: string;
trashedAfter?: string;
Expand Down Expand Up @@ -893,6 +895,7 @@ export type SmartSearchDto = {
query: string;
size?: number;
state?: string | null;
tagIds?: string[];
takenAfter?: string;
takenBefore?: string;
trashedAfter?: string;
Expand Down
3 changes: 3 additions & 0 deletions server/src/dtos/search.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,9 @@ class BaseSearchDto {

@ValidateUUID({ each: true, optional: true })
personIds?: string[];

@ValidateUUID({ each: true, optional: true })
tagIds?: string[];
}

export class RandomSearchDto extends BaseSearchDto {
Expand Down
26 changes: 25 additions & 1 deletion server/src/entities/asset.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,19 @@ export function hasPeopleCte(db: Kysely<DB>, personIds: string[]) {
);
}

/** Adds a `has_tags` CTE that can be inner joined on to filter out assets */
export function hasTagsCte(db: Kysely<DB>, tagIds: string[]) {
return db.with('has_tags', (qb) =>
qb
.selectFrom('tag_asset')
.select('assetsId')
.innerJoin('tags_closure', 'tag_asset.tagsId', 'tags_closure.id_descendant')
.where('tags_closure.id_ancestor', '=', anyUuid(tagIds))
.groupBy('assetsId')
.having((eb) => eb.fn.count('tags_closure.id_ancestor').distinct(), '>=', tagIds.length),
);
}

export function hasPeople(db: Kysely<DB>, personIds?: string[]) {
return personIds && personIds.length > 0
? hasPeopleCte(db, personIds).selectFrom('assets').innerJoin('has_people', 'has_people.assetId', 'assets.id')
Expand Down Expand Up @@ -358,8 +371,19 @@ const joinDeduplicationPlugin = new DeduplicateJoinsPlugin();
export function searchAssetBuilder(kysely: Kysely<DB>, options: AssetSearchBuilderOptions) {
options.isArchived ??= options.withArchived ? undefined : false;
options.withDeleted ||= !!(options.trashedAfter || options.trashedBefore);
return hasPeople(kysely.withPlugin(joinDeduplicationPlugin), options.personIds)

const db = (
options.tagIds && options.tagIds.length > 0
? hasTagsCte(kysely.withPlugin(joinDeduplicationPlugin), options.tagIds)
: kysely.withPlugin(joinDeduplicationPlugin)
) as Kysely<DB>;
return hasPeople(db, options.personIds)
.selectAll('assets')
.$if(!!options.tagIds && options.tagIds.length > 0, (qb) =>
qb.innerJoin(sql.table('has_tags').as('has_tags'), (join) =>
join.onRef(sql`has_tags."assetsId"`, '=', 'assets.id'),
),
)
.$if(!!options.createdBefore, (qb) => qb.where('assets.createdAt', '<=', options.createdBefore!))
.$if(!!options.createdAfter, (qb) => qb.where('assets.createdAt', '>=', options.createdAfter!))
.$if(!!options.updatedBefore, (qb) => qb.where('assets.updatedAt', '<=', options.updatedBefore!))
Expand Down
10 changes: 8 additions & 2 deletions server/src/interfaces/search.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,10 @@ export interface SearchPeopleOptions {
personIds?: string[];
}

export interface SearchTagOptions {
tagIds?: string[];
}

export interface SearchOrderOptions {
orderDirection?: 'asc' | 'desc';
}
Expand All @@ -128,7 +132,8 @@ type BaseAssetSearchOptions = SearchDateOptions &
SearchPathOptions &
SearchStatusOptions &
SearchUserIdOptions &
SearchPeopleOptions;
SearchPeopleOptions &
SearchTagOptions;

export type AssetSearchOptions = BaseAssetSearchOptions & SearchRelationOptions;

Expand All @@ -142,7 +147,8 @@ export type SmartSearchOptions = SearchDateOptions &
SearchOneToOneRelationOptions &
SearchStatusOptions &
SearchUserIdOptions &
SearchPeopleOptions;
SearchPeopleOptions &
SearchTagOptions;

export interface FaceEmbeddingSearch extends SearchEmbeddingOptions {
hasPerson?: boolean;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
query: string;
queryType: 'smart' | 'metadata';
personIds: SvelteSet<string>;
tagIds: SvelteSet<string>;
location: SearchLocationFilter;
camera: SearchCameraFilter;
date: SearchDateFilter;
Expand All @@ -20,6 +21,7 @@
import { Button } from '@immich/ui';
import { AssetTypeEnum, type SmartSearchDto, type MetadataSearchDto } from '@immich/sdk';
import SearchPeopleSection from './search-people-section.svelte';
import SearchTagsSection from './search-tags-section.svelte';
import SearchLocationSection from './search-location-section.svelte';
import SearchCameraSection, { type SearchCameraFilter } from './search-camera-section.svelte';
import SearchDateSection from './search-date-section.svelte';
Expand Down Expand Up @@ -54,6 +56,7 @@
query: 'query' in searchQuery ? searchQuery.query : searchQuery.originalFileName || '',
queryType: 'query' in searchQuery ? 'smart' : 'metadata',
personIds: new SvelteSet('personIds' in searchQuery ? searchQuery.personIds : []),
tagIds: new SvelteSet('tagIds' in searchQuery ? searchQuery.tagIds : []),
location: {
country: withNullAsUndefined(searchQuery.country),
state: withNullAsUndefined(searchQuery.state),
Expand Down Expand Up @@ -85,6 +88,7 @@
query: '',
queryType: 'smart',
personIds: new SvelteSet(),
tagIds: new SvelteSet(),
location: {},
camera: {},
date: {},
Expand Down Expand Up @@ -117,6 +121,7 @@
isFavorite: filter.display.isFavorite || undefined,
isNotInAlbum: filter.display.isNotInAlbum || undefined,
personIds: filter.personIds.size > 0 ? [...filter.personIds] : undefined,
tagIds: filter.tagIds.size > 0 ? [...filter.tagIds] : undefined,
type,
};
Expand All @@ -143,6 +148,9 @@
<!-- TEXT -->
<SearchTextSection bind:query={filter.query} bind:queryType={filter.queryType} />

<!-- TAGS -->
<SearchTagsSection bind:selectedTags={filter.tagIds} />

<!-- LOCATION -->
<SearchLocationSection bind:filters={filter.location} />

Expand Down
Loading

0 comments on commit 8d65b47

Please sign in to comment.