diff --git a/mobile/openapi/lib/api/people_api.dart b/mobile/openapi/lib/api/people_api.dart index 7df0d66c79..92bd0fdeea 100644 --- a/mobile/openapi/lib/api/people_api.dart +++ b/mobile/openapi/lib/api/people_api.dart @@ -66,6 +66,10 @@ class PeopleApi { /// Performs an HTTP 'GET /people' operation and returns the [Response]. /// Parameters: /// + /// * [String] closestAssetId: + /// + /// * [String] closestPersonId: + /// /// * [num] page: /// Page number for pagination /// @@ -73,7 +77,7 @@ class PeopleApi { /// Number of items per page /// /// * [bool] withHidden: - Future getAllPeopleWithHttpInfo({ num? page, num? size, bool? withHidden, }) async { + Future getAllPeopleWithHttpInfo({ String? closestAssetId, String? closestPersonId, num? page, num? size, bool? withHidden, }) async { // ignore: prefer_const_declarations final path = r'/people'; @@ -84,6 +88,12 @@ class PeopleApi { final headerParams = {}; final formParams = {}; + if (closestAssetId != null) { + queryParams.addAll(_queryParams('', 'closestAssetId', closestAssetId)); + } + if (closestPersonId != null) { + queryParams.addAll(_queryParams('', 'closestPersonId', closestPersonId)); + } if (page != null) { queryParams.addAll(_queryParams('', 'page', page)); } @@ -110,6 +120,10 @@ class PeopleApi { /// Parameters: /// + /// * [String] closestAssetId: + /// + /// * [String] closestPersonId: + /// /// * [num] page: /// Page number for pagination /// @@ -117,8 +131,8 @@ class PeopleApi { /// Number of items per page /// /// * [bool] withHidden: - Future getAllPeople({ num? page, num? size, bool? withHidden, }) async { - final response = await getAllPeopleWithHttpInfo( page: page, size: size, withHidden: withHidden, ); + Future getAllPeople({ String? closestAssetId, String? closestPersonId, num? page, num? size, bool? withHidden, }) async { + final response = await getAllPeopleWithHttpInfo( closestAssetId: closestAssetId, closestPersonId: closestPersonId, page: page, size: size, withHidden: withHidden, ); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index 3afda881cd..2f771b7eec 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -3846,6 +3846,24 @@ "get": { "operationId": "getAllPeople", "parameters": [ + { + "name": "closestAssetId", + "required": false, + "in": "query", + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "name": "closestPersonId", + "required": false, + "in": "query", + "schema": { + "format": "uuid", + "type": "string" + } + }, { "name": "page", "required": false, diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index 7770f0c578..393a47a5df 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -2362,7 +2362,9 @@ export function updatePartner({ id, updatePartnerDto }: { body: updatePartnerDto }))); } -export function getAllPeople({ page, size, withHidden }: { +export function getAllPeople({ closestAssetId, closestPersonId, page, size, withHidden }: { + closestAssetId?: string; + closestPersonId?: string; page?: number; size?: number; withHidden?: boolean; @@ -2371,6 +2373,8 @@ export function getAllPeople({ page, size, withHidden }: { status: 200; data: PeopleResponseDto; }>(`/people${QS.query(QS.explode({ + closestAssetId, + closestPersonId, page, size, withHidden diff --git a/server/src/controllers/person.controller.ts b/server/src/controllers/person.controller.ts index ba9a181c41..c8faf87e62 100644 --- a/server/src/controllers/person.controller.ts +++ b/server/src/controllers/person.controller.ts @@ -31,8 +31,8 @@ export class PersonController { @Get() @Authenticated({ permission: Permission.PERSON_READ }) - getAllPeople(@Auth() auth: AuthDto, @Query() withHidden: PersonSearchDto): Promise { - return this.service.getAll(auth, withHidden); + getAllPeople(@Auth() auth: AuthDto, @Query() options: PersonSearchDto): Promise { + return this.service.getAll(auth, options); } @Post() diff --git a/server/src/dtos/person.dto.ts b/server/src/dtos/person.dto.ts index 94ee52d916..047ef600b8 100644 --- a/server/src/dtos/person.dto.ts +++ b/server/src/dtos/person.dto.ts @@ -67,6 +67,10 @@ export class MergePersonDto { export class PersonSearchDto { @ValidateBoolean({ optional: true }) withHidden?: boolean; + @ValidateUUID({ optional: true }) + closestPersonId?: string; + @ValidateUUID({ optional: true }) + closestAssetId?: string; /** Page number for pagination */ @ApiPropertyOptional() diff --git a/server/src/interfaces/person.interface.ts b/server/src/interfaces/person.interface.ts index b3e2c0990e..dc89f5c1b0 100644 --- a/server/src/interfaces/person.interface.ts +++ b/server/src/interfaces/person.interface.ts @@ -10,6 +10,7 @@ export const IPersonRepository = 'IPersonRepository'; export interface PersonSearchOptions { minimumFaceCount: number; withHidden: boolean; + closestFaceAssetId?: string; } export interface PersonNameSearchOptions { diff --git a/server/src/repositories/person.repository.ts b/server/src/repositories/person.repository.ts index 81958d269d..4229286706 100644 --- a/server/src/repositories/person.repository.ts +++ b/server/src/repositories/person.repository.ts @@ -83,7 +83,11 @@ export class PersonRepository implements IPersonRepository { } @GenerateSql({ params: [{ take: 10, skip: 10 }, DummyValue.UUID] }) - getAllForUser(pagination: PaginationOptions, userId: string, options?: PersonSearchOptions): Paginated { + async getAllForUser( + pagination: PaginationOptions, + userId: string, + options?: PersonSearchOptions, + ): Paginated { const queryBuilder = this.personRepository .createQueryBuilder('person') .innerJoin('person.faces', 'face') @@ -97,10 +101,22 @@ export class PersonRepository implements IPersonRepository { .addOrderBy('person.createdAt') .having("person.name != '' OR COUNT(face.assetId) >= :faces", { faces: options?.minimumFaceCount || 1 }) .groupBy('person.id'); + if (options?.closestFaceAssetId) { + const innerQueryBuilder = this.faceSearchRepository + .createQueryBuilder('face_search') + .select('embedding', 'embedding') + .where('"face_search"."faceId" = "person"."faceAssetId"'); + const faceSelectQueryBuilder = this.faceSearchRepository + .createQueryBuilder('face_search') + .select('embedding', 'embedding') + .where('"face_search"."faceId" = :faceId', { faceId: options.closestFaceAssetId }); + queryBuilder + .orderBy('(' + innerQueryBuilder.getQuery() + ') <=> (' + faceSelectQueryBuilder.getQuery() + ')') + .setParameters(faceSelectQueryBuilder.getParameters()); + } if (!options?.withHidden) { queryBuilder.andWhere('person.isHidden = false'); } - return paginatedBuilder(queryBuilder, { mode: PaginationMode.LIMIT_OFFSET, ...pagination, diff --git a/server/src/services/person.service.ts b/server/src/services/person.service.ts index 79e82bb742..bdec6f88e8 100644 --- a/server/src/services/person.service.ts +++ b/server/src/services/person.service.ts @@ -55,16 +55,25 @@ import { IsNull } from 'typeorm'; @Injectable() export class PersonService extends BaseService { async getAll(auth: AuthDto, dto: PersonSearchDto): Promise { - const { withHidden = false, page, size } = dto; + const { withHidden = false, closestAssetId, closestPersonId, page, size } = dto; + let closestFaceAssetId = closestAssetId; const pagination = { take: size, skip: (page - 1) * size, }; + if (closestPersonId) { + const person = await this.personRepository.getById(closestPersonId); + if (!person?.faceAssetId) { + throw new NotFoundException('Person not found'); + } + closestFaceAssetId = person.faceAssetId; + } const { machineLearning } = await this.getConfig({ withCache: false }); const { items, hasNextPage } = await this.personRepository.getAllForUser(pagination, auth.user.id, { minimumFaceCount: machineLearning.facialRecognition.minFaces, withHidden, + closestFaceAssetId, }); const { total, hidden } = await this.personRepository.getNumberOfPeople(auth.user.id); diff --git a/web/src/lib/components/faces-page/assign-face-side-panel.svelte b/web/src/lib/components/faces-page/assign-face-side-panel.svelte index b6c9beb43a..fe6a454307 100644 --- a/web/src/lib/components/faces-page/assign-face-side-panel.svelte +++ b/web/src/lib/components/faces-page/assign-face-side-panel.svelte @@ -1,8 +1,8 @@