From c89d91e006474dcabc5379d0935ef4e9b6d37306 Mon Sep 17 00:00:00 2001 From: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com> Date: Thu, 29 Feb 2024 22:14:48 +0100 Subject: [PATCH] feat: filter people when using smart search (#7521) --- mobile/openapi/doc/SmartSearchDto.md | 1 + .../openapi/lib/model/smart_search_dto.dart | 11 +++++++++- .../openapi/test/smart_search_dto_test.dart | 5 +++++ open-api/immich-openapi-specs.json | 6 ++++++ open-api/typescript-sdk/src/fetch-client.ts | 1 + server/src/domain/search/dto/search.dto.ts | 6 +++--- .../infra/repositories/search.repository.ts | 21 +++++++++++++++++-- .../search-bar/search-filter-box.svelte | 9 -------- 8 files changed, 45 insertions(+), 15 deletions(-) diff --git a/mobile/openapi/doc/SmartSearchDto.md b/mobile/openapi/doc/SmartSearchDto.md index d4ec1a70f6..d5f4c40256 100644 --- a/mobile/openapi/doc/SmartSearchDto.md +++ b/mobile/openapi/doc/SmartSearchDto.md @@ -27,6 +27,7 @@ Name | Type | Description | Notes **make** | **String** | | [optional] **model** | **String** | | [optional] **page** | **num** | | [optional] +**personIds** | **List** | | [optional] [default to const []] **query** | **String** | | **size** | **num** | | [optional] **state** | **String** | | [optional] diff --git a/mobile/openapi/lib/model/smart_search_dto.dart b/mobile/openapi/lib/model/smart_search_dto.dart index 664850db82..0b99acdd66 100644 --- a/mobile/openapi/lib/model/smart_search_dto.dart +++ b/mobile/openapi/lib/model/smart_search_dto.dart @@ -32,6 +32,7 @@ class SmartSearchDto { this.make, this.model, this.page, + this.personIds = const [], required this.query, this.size, this.state, @@ -199,6 +200,8 @@ class SmartSearchDto { /// num? page; + List personIds; + String query; /// @@ -312,6 +315,7 @@ class SmartSearchDto { other.make == make && other.model == model && other.page == page && + _deepEquality.equals(other.personIds, personIds) && other.query == query && other.size == size && other.state == state && @@ -348,6 +352,7 @@ class SmartSearchDto { (make == null ? 0 : make!.hashCode) + (model == null ? 0 : model!.hashCode) + (page == null ? 0 : page!.hashCode) + + (personIds.hashCode) + (query.hashCode) + (size == null ? 0 : size!.hashCode) + (state == null ? 0 : state!.hashCode) + @@ -363,7 +368,7 @@ class SmartSearchDto { (withExif == null ? 0 : withExif!.hashCode); @override - String toString() => 'SmartSearchDto[city=$city, country=$country, createdAfter=$createdAfter, createdBefore=$createdBefore, deviceId=$deviceId, isArchived=$isArchived, isEncoded=$isEncoded, isExternal=$isExternal, isFavorite=$isFavorite, isMotion=$isMotion, isNotInAlbum=$isNotInAlbum, isOffline=$isOffline, isReadOnly=$isReadOnly, isVisible=$isVisible, lensModel=$lensModel, libraryId=$libraryId, make=$make, model=$model, page=$page, query=$query, size=$size, state=$state, takenAfter=$takenAfter, takenBefore=$takenBefore, trashedAfter=$trashedAfter, trashedBefore=$trashedBefore, type=$type, updatedAfter=$updatedAfter, updatedBefore=$updatedBefore, withArchived=$withArchived, withDeleted=$withDeleted, withExif=$withExif]'; + String toString() => 'SmartSearchDto[city=$city, country=$country, createdAfter=$createdAfter, createdBefore=$createdBefore, deviceId=$deviceId, isArchived=$isArchived, isEncoded=$isEncoded, isExternal=$isExternal, isFavorite=$isFavorite, isMotion=$isMotion, isNotInAlbum=$isNotInAlbum, isOffline=$isOffline, isReadOnly=$isReadOnly, isVisible=$isVisible, lensModel=$lensModel, libraryId=$libraryId, make=$make, model=$model, page=$page, personIds=$personIds, query=$query, size=$size, state=$state, takenAfter=$takenAfter, takenBefore=$takenBefore, trashedAfter=$trashedAfter, trashedBefore=$trashedBefore, type=$type, updatedAfter=$updatedAfter, updatedBefore=$updatedBefore, withArchived=$withArchived, withDeleted=$withDeleted, withExif=$withExif]'; Map toJson() { final json = {}; @@ -462,6 +467,7 @@ class SmartSearchDto { } else { // json[r'page'] = null; } + json[r'personIds'] = this.personIds; json[r'query'] = this.query; if (this.size != null) { json[r'size'] = this.size; @@ -549,6 +555,9 @@ class SmartSearchDto { make: mapValueOfType(json, r'make'), model: mapValueOfType(json, r'model'), page: num.parse('${json[r'page']}'), + personIds: json[r'personIds'] is Iterable + ? (json[r'personIds'] as Iterable).cast().toList(growable: false) + : const [], query: mapValueOfType(json, r'query')!, size: num.parse('${json[r'size']}'), state: mapValueOfType(json, r'state'), diff --git a/mobile/openapi/test/smart_search_dto_test.dart b/mobile/openapi/test/smart_search_dto_test.dart index 4db3ac0808..5263f7bb6a 100644 --- a/mobile/openapi/test/smart_search_dto_test.dart +++ b/mobile/openapi/test/smart_search_dto_test.dart @@ -111,6 +111,11 @@ void main() { // TODO }); + // List personIds (default value: const []) + test('to test the property `personIds`', () async { + // TODO + }); + // String query test('to test the property `query`', () async { // TODO 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, 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 { let results: PaginationResult = { 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 @@