From 81de979a35fd38b63ea916d05f74fcc9b0acde2f Mon Sep 17 00:00:00 2001 From: "immich-push-o-matic[bot]" <179150890+immich-push-o-matic[bot]@users.noreply.github.com> Date: Mon, 5 Oct 2026 16:23:02 +0000 Subject: [PATCH] fix: people page search (#32108) fix: people page search (#32094) (cherry picked from commit 549a76cd35ec9f63a76174992de50095192afa11) Co-authored-by: Daniel Dietzler <36593685+danieldietzler@users.noreply.github.com> --- open-api/immich-openapi-specs.json | 9 ++ packages/sdk/src/fetch-client.ts | 4 +- server/src/dtos/person.dto.ts | 1 + server/src/queries/person.repository.sql | 12 +++ server/src/repositories/person.repository.ts | 25 ++++- web/src/lib/types.ts | 1 + web/src/routes/(user)/people/+page.svelte | 100 ++++++++----------- web/src/routes/(user)/people/+page.ts | 1 + 8 files changed, 89 insertions(+), 64 deletions(-) diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index dddc0fb09..ffab17f4a 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -9804,6 +9804,15 @@ "type": "boolean" } }, + { + "name": "name", + "required": false, + "in": "query", + "description": "Filter by person name", + "schema": { + "type": "string" + } + }, { "name": "page", "required": false, diff --git a/packages/sdk/src/fetch-client.ts b/packages/sdk/src/fetch-client.ts index fdfb452aa..63d860058 100644 --- a/packages/sdk/src/fetch-client.ts +++ b/packages/sdk/src/fetch-client.ts @@ -6097,11 +6097,12 @@ export function deletePeople({ peopleDeleteDto }: { /** * Get all people */ -export function getAllPeople({ closestAssetId, closestPersonId, isFavorite, isHidden, page, sharedById, sharedWithId, size, withHidden }: { +export function getAllPeople({ closestAssetId, closestPersonId, isFavorite, isHidden, name, page, sharedById, sharedWithId, size, withHidden }: { closestAssetId?: string; closestPersonId?: string; isFavorite?: boolean; isHidden?: boolean; + name?: string; page?: number; sharedById?: string; sharedWithId?: string; @@ -6116,6 +6117,7 @@ export function getAllPeople({ closestAssetId, closestPersonId, isFavorite, isHi closestPersonId, isFavorite, isHidden, + name, page, sharedById, sharedWithId, diff --git a/server/src/dtos/person.dto.ts b/server/src/dtos/person.dto.ts index 909ee064c..0b3237ac5 100644 --- a/server/src/dtos/person.dto.ts +++ b/server/src/dtos/person.dto.ts @@ -69,6 +69,7 @@ const PersonSearchSchema = z sharedWithId: z.uuid().optional().describe('Only include people to which the user was given access'), isFavorite: stringToBool.optional().describe('Filter by favorite status'), isHidden: stringToBool.optional().describe('Filter by hidden status'), + name: z.string().optional().describe('Filter by person name'), }) .meta({ id: 'PersonSearchDto' }); diff --git a/server/src/queries/person.repository.sql b/server/src/queries/person.repository.sql index b9d42755e..551622b72 100644 --- a/server/src/queries/person.repository.sql +++ b/server/src/queries/person.repository.sql @@ -147,6 +147,11 @@ limit 3 -- PersonRepository.getAllForUser +with + "similarity_threshold" as ( + select + set_config('pg_trgm.word_similarity_threshold', '0.5', true) as "thresh" + ) select ( select @@ -234,6 +239,7 @@ select ) as agg ) as "sharedWith" from + "similarity_threshold", "person_group" inner join "person" as "owned" on "owned"."personGroupId" = "person_group"."id" and "owned"."ownerId" = $6 @@ -894,6 +900,11 @@ where ) -- PersonRepository.getNumberOfPeople +with + "similarity_threshold" as ( + select + set_config('pg_trgm.word_similarity_threshold', '0.5', true) as "thresh" + ) select coalesce(count(*), 0) as "total", coalesce( @@ -904,6 +915,7 @@ select 0 ) as "hidden" from + "similarity_threshold", "person_group" left join "person" as "owned" on "owned"."personGroupId" = "person_group"."id" and "owned"."ownerId" = $2 diff --git a/server/src/repositories/person.repository.ts b/server/src/repositories/person.repository.ts index 01b030697..7b12b1f18 100644 --- a/server/src/repositories/person.repository.ts +++ b/server/src/repositories/person.repository.ts @@ -39,6 +39,7 @@ export interface PersonFilterOptions { sharedWithId?: string; isFavorite?: boolean; isHidden?: boolean; + name?: string; } export interface PersonSearchOptions extends PersonFilterOptions { @@ -172,8 +173,13 @@ const withOtherPeopleForPerson = (userId: string) => { }; const withFilters = (userId: string, options: PersonFilterOptions = {}) => { - const { sharedById, sharedWithId, isFavorite, isHidden } = options; - return (eb: ExpressionBuilder) => { + const { sharedById, sharedWithId, isFavorite, isHidden, name } = options; + return ( + eb: ExpressionBuilder< + DB & { owned: DB['person']; similarity_threshold: unknown }, + 'person_group' | 'owned' | 'similarity_threshold' + >, + ) => { const filters: Expression[] = []; if (sharedById || sharedWithId) { @@ -200,6 +206,10 @@ const withFilters = (userId: string, options: PersonFilterOptions = {}) => { filters.push(eb('owned.isHidden', '=', isHidden)); } + if (name !== undefined) { + filters.push(sql`f_unaccent("owned"."name") %> f_unaccent(${name})`); + } + return eb.and(filters); }; }; @@ -429,7 +439,10 @@ export class PersonRepository { @GenerateSql({ params: [{ take: 1, skip: 0 }, DummyValue.UUID] }) async getAllForUser(pagination: PaginationOptions, userId: string, options?: PersonSearchOptions) { const items = await this.db - .selectFrom('person_group') + .with('similarity_threshold', (db) => + db.selectNoFrom(sql`set_config('pg_trgm.word_similarity_threshold', '0.5', true)`.as('thresh')), + ) + .selectFrom(['similarity_threshold', 'person_group']) .innerJoin('person as owned', (join) => join.onRef('owned.personGroupId', '=', 'person_group.id').on('owned.ownerId', '=', userId), ) @@ -502,6 +515,7 @@ export class PersonRepository { // an explicit isHidden filter takes precedence over withHidden .$if(!options?.withHidden && options?.isHidden === undefined, (qb) => qb.where('owned.isHidden', '=', false)) .where(withFilters(userId, options)) + .$if(!!options?.name, (qb) => qb.orderBy(sql`f_unaccent("owned"."name") <->>> f_unaccent(${options!.name!})`)) .offset(pagination.skip ?? 0) .limit(pagination.take + 1) .execute(); @@ -726,7 +740,10 @@ export class PersonRepository { getNumberOfPeople(userId: string, options?: PersonFilterOptions) { const zero = sql.lit(0); return this.db - .selectFrom('person_group') + .with('similarity_threshold', (db) => + db.selectNoFrom(sql`set_config('pg_trgm.word_similarity_threshold', '0.5', true)`.as('thresh')), + ) + .selectFrom(['similarity_threshold', 'person_group']) .leftJoin('person as owned', (join) => join.onRef('owned.personGroupId', '=', 'person_group.id').on('owned.ownerId', '=', userId), ) diff --git a/web/src/lib/types.ts b/web/src/lib/types.ts index 43b044e14..b6865f198 100644 --- a/web/src/lib/types.ts +++ b/web/src/lib/types.ts @@ -81,6 +81,7 @@ export type PeopleFilter = { sharedWithId?: string; isFavorite?: boolean; isHidden?: boolean; + name?: string; }; export type JSONSchemaType = 'string' | 'number' | 'integer' | 'boolean' | 'object'; diff --git a/web/src/routes/(user)/people/+page.svelte b/web/src/routes/(user)/people/+page.svelte index 2af5459a7..7aaac2952 100644 --- a/web/src/routes/(user)/people/+page.svelte +++ b/web/src/routes/(user)/people/+page.svelte @@ -1,26 +1,23 @@