diff --git a/server/src/queries/person.repository.sql b/server/src/queries/person.repository.sql index a2f3f64442..ac181b6dcb 100644 --- a/server/src/queries/person.repository.sql +++ b/server/src/queries/person.repository.sql @@ -218,11 +218,29 @@ from "person" where "person"."ownerId" = $1 - and f_unaccent ("person"."name") %> f_unaccent ($2) + and exists ( + select + from + "asset_face" + where + "asset_face"."personId" = "person"."id" + and "asset_face"."deletedAt" is null + and "asset_face"."isVisible" = $2 + and exists ( + select + from + "asset" + where + "asset"."id" = "asset_face"."assetId" + and "asset"."visibility" = 'timeline' + and "asset"."deletedAt" is null + ) + ) + and f_unaccent ("person"."name") %> f_unaccent ($3) order by - f_unaccent ("person"."name") <->>> f_unaccent ($3) + f_unaccent ("person"."name") <->>> f_unaccent ($4) limit - $4 + $5 -- PersonRepository.getDistinctNames select distinct diff --git a/server/src/repositories/person.repository.ts b/server/src/repositories/person.repository.ts index 0db03a18c7..863d25e95a 100644 --- a/server/src/repositories/person.repository.ts +++ b/server/src/repositories/person.repository.ts @@ -12,6 +12,25 @@ import { PersonTable } from 'src/schema/tables/person.table'; import { dummy, removeUndefinedKeys, withFilePath } from 'src/utils/database'; import { paginationHelper, PaginationOptions } from 'src/utils/pagination'; +function hasFace(eb: ExpressionBuilder) { + return eb.exists((eb) => + eb + .selectFrom('asset_face') + .whereRef('asset_face.personId', '=', 'person.id') + .where('asset_face.deletedAt', 'is', null) + .where('asset_face.isVisible', '=', true) + .where((eb) => + eb.exists((eb) => + eb + .selectFrom('asset') + .whereRef('asset.id', '=', 'asset_face.assetId') + .where('asset.visibility', '=', sql.lit(AssetVisibility.Timeline)) + .where('asset.deletedAt', 'is', null), + ), + ), + ); +} + export interface PersonSearchOptions { withHidden: boolean; closestFaceAssetId?: string; @@ -325,6 +344,7 @@ export class PersonRepository { .selectFrom(['similarity_threshold', 'person']) .selectAll('person') .where('person.ownerId', '=', userId) + .where(hasFace) .where(() => sql`f_unaccent("person"."name") %> f_unaccent(${personName})`) .orderBy(sql`f_unaccent("person"."name") <->>> f_unaccent(${personName})`) .limit(100) @@ -369,24 +389,7 @@ export class PersonRepository { const zero = sql.lit(0); return this.db .selectFrom('person') - .where((eb) => - eb.exists((eb) => - eb - .selectFrom('asset_face') - .whereRef('asset_face.personId', '=', 'person.id') - .where('asset_face.deletedAt', 'is', null) - .where('asset_face.isVisible', '=', true) - .where((eb) => - eb.exists((eb) => - eb - .selectFrom('asset') - .whereRef('asset.id', '=', 'asset_face.assetId') - .where('asset.visibility', '=', sql.lit(AssetVisibility.Timeline)) - .where('asset.deletedAt', 'is', null), - ), - ), - ), - ) + .where(hasFace) .where('person.ownerId', '=', userId) .select((eb) => eb.fn.coalesce(eb.fn.countAll(), zero).as('total')) .select((eb) => eb.fn.coalesce(eb.fn.countAll().filterWhere('isHidden', '=', true), zero).as('hidden')) diff --git a/server/src/services/search.service.spec.ts b/server/src/services/search.service.spec.ts index 4680b4c9de..54a3ab2d4d 100644 --- a/server/src/services/search.service.spec.ts +++ b/server/src/services/search.service.spec.ts @@ -4,6 +4,7 @@ import { SearchSuggestionType } from 'src/dtos/search.dto'; import { SearchService } from 'src/services/search.service'; import { AssetFactory } from 'test/factories/asset.factory'; import { AuthFactory } from 'test/factories/auth.factory'; +import { PersonFactory } from 'test/factories/person.factory'; import { authStub } from 'test/fixtures/auth.stub'; import { getForAsset } from 'test/mappers'; import { newTestService, ServiceMocks } from 'test/utils'; @@ -39,6 +40,18 @@ describe(SearchService.name, () => { expect(mocks.person.getByName).toHaveBeenCalledWith(auth.user.id, name, { withHidden: true }); }); + + it('should exclude people without faces', async () => { + const auth = AuthFactory.create(); + const withFace = PersonFactory.create({ ownerId: auth.user.id, faceAssetId: 'face-id', name: 'Alice' }); + const withoutFace = PersonFactory.create({ ownerId: auth.user.id, faceAssetId: null, name: 'Alina' }); + + mocks.person.getByName.mockResolvedValue([withFace, withoutFace]); + + const result = await sut.searchPerson(auth, { name: 'Ali', withHidden: false }); + + expect(result).toEqual([expect.objectContaining({ id: withFace.id, name: withFace.name })]); + }); }); describe('searchPlaces', () => {