diff --git a/mobile/openapi/doc/SmartSearchDto.md b/mobile/openapi/doc/SmartSearchDto.md index d4ec1a70f6..d5f4c40256 100644 Binary files a/mobile/openapi/doc/SmartSearchDto.md and b/mobile/openapi/doc/SmartSearchDto.md differ diff --git a/mobile/openapi/lib/model/smart_search_dto.dart b/mobile/openapi/lib/model/smart_search_dto.dart index 664850db82..0b99acdd66 100644 Binary files a/mobile/openapi/lib/model/smart_search_dto.dart and b/mobile/openapi/lib/model/smart_search_dto.dart differ diff --git a/mobile/openapi/test/smart_search_dto_test.dart b/mobile/openapi/test/smart_search_dto_test.dart index 4db3ac0808..5263f7bb6a 100644 Binary files a/mobile/openapi/test/smart_search_dto_test.dart and b/mobile/openapi/test/smart_search_dto_test.dart differ diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index c0e8689850..d5bffc887a 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -9539,6 +9539,12 @@ "page": { "type": "number" }, + "personIds": { + "items": { + "type": "string" + }, + "type": "array" + }, "query": { "type": "string" }, diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index b512bce296..12238e40bc 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -671,6 +671,7 @@ export type SmartSearchDto = { make?: string; model?: string; page?: number; + personIds?: string[]; query: string; size?: number; state?: string; diff --git a/server/src/domain/search/dto/search.dto.ts b/server/src/domain/search/dto/search.dto.ts index 877a494e4d..c529f6887b 100644 --- a/server/src/domain/search/dto/search.dto.ts +++ b/server/src/domain/search/dto/search.dto.ts @@ -122,6 +122,9 @@ class BaseSearchDto { @QueryBoolean({ optional: true }) isNotInAlbum?: boolean; + + @Optional() + personIds?: string[]; } export class MetadataSearchDto extends BaseSearchDto { @@ -173,9 +176,6 @@ export class MetadataSearchDto extends BaseSearchDto { @Optional() @ApiProperty({ enumName: 'AssetOrder', enum: AssetOrder }) order?: AssetOrder; - - @Optional() - personIds?: string[]; } export class SmartSearchDto extends BaseSearchDto { diff --git a/server/src/infra/repositories/search.repository.ts b/server/src/infra/repositories/search.repository.ts index c8dc5070f7..0ff26a4f5f 100644 --- a/server/src/infra/repositories/search.repository.ts +++ b/server/src/infra/repositories/search.repository.ts @@ -22,7 +22,7 @@ import { import { ImmichLogger } from '@app/infra/logger'; import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; +import { Repository, SelectQueryBuilder } from 'typeorm'; import { vectorExt } from '../database.config'; import { DummyValue, GenerateSql } from '../infra.util'; import { asVector, isValidInteger, paginatedBuilder, searchAssetBuilder } from '../infra.utils'; @@ -81,6 +81,14 @@ export class SearchRepository implements ISearchRepository { }); } + private createPersonFilter(builder: SelectQueryBuilder<AssetFaceEntity>, personIds: string[]) { + return builder + .select(`${builder.alias}."assetId"`) + .where(`${builder.alias}."personId" IN (:...personIds)`, { personIds }) + .groupBy(`${builder.alias}."assetId"`) + .having(`COUNT(DISTINCT ${builder.alias}."personId") = :personCount`, { personCount: personIds.length }); + } + @GenerateSql({ params: [ { page: 1, size: 100 }, @@ -96,12 +104,21 @@ export class SearchRepository implements ISearchRepository { }) async searchSmart( pagination: SearchPaginationOptions, - { embedding, userIds, ...options }: SmartSearchOptions, + { embedding, userIds, personIds, ...options }: SmartSearchOptions, ): Paginated<AssetEntity> { let results: PaginationResult<AssetEntity> = { items: [], hasNextPage: false }; await this.assetRepository.manager.transaction(async (manager) => { let builder = manager.createQueryBuilder(AssetEntity, 'asset'); + + if (personIds?.length) { + const assetFaceBuilder = manager.createQueryBuilder(AssetFaceEntity, 'asset_face'); + const cte = this.createPersonFilter(assetFaceBuilder, personIds); + builder + .addCommonTableExpression(cte, 'asset_face_ids') + .innerJoin('asset_face_ids', 'a', 'a."assetId" = asset.id'); + } + builder = searchAssetBuilder(builder, options); builder .innerJoin('asset.smartSearch', 'search') diff --git a/web/src/lib/components/shared-components/search-bar/search-filter-box.svelte b/web/src/lib/components/shared-components/search-bar/search-filter-box.svelte index 8d060f4d0d..b05e2d5a3b 100644 --- a/web/src/lib/components/shared-components/search-bar/search-filter-box.svelte +++ b/web/src/lib/components/shared-components/search-bar/search-filter-box.svelte @@ -22,7 +22,6 @@ <script lang="ts"> import Button from '$lib/components/elements/buttons/button.svelte'; - import { handleError } from '$lib/utils/handle-error'; import { AssetTypeEnum, type SmartSearchDto, type MetadataSearchDto } from '@immich/sdk'; import { createEventDispatcher } from 'svelte'; import { fly } from 'svelte/transition'; @@ -83,14 +82,6 @@ }; const search = () => { - if (filter.context && filter.personIds.size > 0) { - handleError( - new Error('Context search does not support people filter'), - 'Context search does not support people filter', - ); - return; - } - let type: AssetTypeEnum | undefined = undefined; if (filter.mediaType === MediaType.Image) { type = AssetTypeEnum.Image;