refactor: merge into one query

This commit is contained in:
Yaros
2025-12-12 17:51:34 +01:00
parent 4277b500ac
commit 6ac3d8e4c6
4 changed files with 100 additions and 151 deletions

View File

@@ -23,38 +23,6 @@ where
limit limit
3 3
-- PersonRepository.getAllForUser
select
"person".*
from
"person"
inner join "asset_face" on "asset_face"."personId" = "person"."id"
inner join "asset" on "asset_face"."assetId" = "asset"."id"
and "asset"."visibility" = 'timeline'
and "asset"."deletedAt" is null
where
"person"."ownerId" = $1
and "asset_face"."deletedAt" is null
and "person"."isHidden" = $2
group by
"person"."id"
having
(
"person"."name" != $3
or count("asset_face"."assetId") >= $4
)
order by
"person"."isHidden" asc,
"person"."isFavorite" desc,
NULLIF(person.name, '') is null asc,
count("asset_face"."assetId") desc,
NULLIF(person.name, '') asc nulls last,
"person"."createdAt"
limit
$5
offset
$6
-- PersonRepository.getAllWithoutFaces -- PersonRepository.getAllWithoutFaces
select select
"person".* "person".*
@@ -230,43 +198,6 @@ from
where where
"asset_face"."deletedAt" is null "asset_face"."deletedAt" is null
-- PersonRepository.getNumberOfPeople
select
coalesce(count(*), 0) as "total",
coalesce(
count(*) filter (
where
"isHidden" = $1
),
0
) as "hidden"
from
"person"
where
exists (
select
from
"asset_face"
where
"asset_face"."personId" = "person"."id"
and "asset_face"."deletedAt" is null
and exists (
select
from
"asset"
where
"asset"."id" = "asset_face"."assetId"
and "asset"."visibility" = 'timeline'
and "asset"."deletedAt" is null
)
having
(
"person"."name" != $2
or count("asset_face"."assetId") >= $3
)
)
and "person"."ownerId" = $4
-- PersonRepository.refreshFaces -- PersonRepository.refreshFaces
with with
"added_embeddings" as ( "added_embeddings" as (

View File

@@ -147,10 +147,9 @@ export class PersonRepository {
} }
@GenerateSql({ params: [{ take: 1, skip: 0 }, DummyValue.UUID] }) @GenerateSql({ params: [{ take: 1, skip: 0 }, DummyValue.UUID] })
async getAllForUser(pagination: PaginationOptions, userId: string, options?: PersonSearchOptions) { async getAllForUser(pagination: PaginationOptions, userId: string, options: PersonSearchOptions) {
const items = await this.db const baseQuery = this.db
.selectFrom('person') .selectFrom('person')
.selectAll('person')
.innerJoin('asset_face', 'asset_face.personId', 'person.id') .innerJoin('asset_face', 'asset_face.personId', 'person.id')
.innerJoin('asset', (join) => .innerJoin('asset', (join) =>
join join
@@ -160,17 +159,28 @@ export class PersonRepository {
) )
.where('person.ownerId', '=', userId) .where('person.ownerId', '=', userId)
.where('asset_face.deletedAt', 'is', null) .where('asset_face.deletedAt', 'is', null)
.orderBy('person.isHidden', 'asc')
.orderBy('person.isFavorite', 'desc')
.having((eb) =>
eb.or([
eb('person.name', '!=', ''),
eb((innerEb) => innerEb.fn.count('asset_face.assetId'), '>=', options?.minimumFaceCount || 1),
]),
)
.groupBy('person.id') .groupBy('person.id')
.$if(!!options?.closestFaceAssetId, (qb) => .having((eb) =>
qb.orderBy((eb) => eb.or([eb('person.name', '!=', ''), eb(eb.fn.count('asset_face.assetId'), '>=', options.minimumFaceCount)]),
)
.selectAll('person')
.select((eb) => [
eb.fn.count('asset_face.assetId').as('faceCount'),
eb.fn.countAll().over().as('totalCount'),
eb.fn
.sum(sql<number>`CASE WHEN ${sql.ref('person.isHidden')} THEN 1 ELSE 0 END`)
.over()
.as('hiddenCount'),
]);
let sorted = baseQuery.orderBy('person.isHidden', 'asc').orderBy('person.isFavorite', 'desc');
if (options.closestFaceAssetId) {
const closestId = options.closestFaceAssetId!;
sorted = sorted.orderBy((eb) =>
eb( eb(
(eb) => (eb) =>
eb eb
@@ -179,26 +189,32 @@ export class PersonRepository {
.whereRef('face_search.faceId', '=', 'person.faceAssetId'), .whereRef('face_search.faceId', '=', 'person.faceAssetId'),
'<=>', '<=>',
(eb) => (eb) =>
eb eb.selectFrom('face_search').select('face_search.embedding').where('face_search.faceId', '=', closestId),
.selectFrom('face_search')
.select('face_search.embedding')
.where('face_search.faceId', '=', options!.closestFaceAssetId!),
), ),
), );
) } else {
.$if(!options?.closestFaceAssetId, (qb) => sorted = sorted
qb .orderBy(sql`NULLIF(person.name, '') IS NULL`, 'asc')
.orderBy(sql`NULLIF(person.name, '') is null`, 'asc')
.orderBy((eb) => eb.fn.count('asset_face.assetId'), 'desc') .orderBy((eb) => eb.fn.count('asset_face.assetId'), 'desc')
.orderBy(sql`NULLIF(person.name, '')`, (om) => om.asc().nullsLast()) .orderBy(sql`NULLIF(person.name, '')`, (o) => o.asc().nullsLast())
.orderBy('person.createdAt'), .orderBy('person.createdAt');
) }
.$if(!options?.withHidden, (qb) => qb.where('person.isHidden', '=', false))
if (!options.withHidden) {
sorted = sorted.where('person.isHidden', '=', false);
}
const rows = await sorted
.offset(pagination.skip ?? 0) .offset(pagination.skip ?? 0)
.limit(pagination.take + 1) .limit(pagination.take + 1)
.execute(); .execute();
return paginationHelper(items, pagination.take); const total = rows[0]?.totalCount ?? 0;
const hidden = rows[0]?.hiddenCount ?? 0;
const { items, hasNextPage } = paginationHelper(rows, pagination.take);
return { items, hasNextPage, total, hidden };
} }
@GenerateSql() @GenerateSql()
@@ -357,40 +373,6 @@ export class PersonRepository {
}; };
} }
@GenerateSql({ params: [DummyValue.UUID] })
getNumberOfPeople(userId: string, options?: PersonSearchOptions) {
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)
.having((eb) =>
eb.or([
eb('person.name', '!=', ''),
eb((innerEb) => innerEb.fn.count('asset_face.assetId'), '>=', options?.minimumFaceCount || 1),
]),
)
.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('person.ownerId', '=', userId)
.select((eb) => eb.fn.coalesce(eb.fn.countAll<number>(), zero).as('total'))
.select((eb) => eb.fn.coalesce(eb.fn.countAll<number>().filterWhere('isHidden', '=', true), zero).as('hidden'))
.executeTakeFirstOrThrow();
}
create(person: Insertable<PersonTable>) { create(person: Insertable<PersonTable>) {
return this.db.insertInto('person').values(person).returningAll().executeTakeFirstOrThrow(); return this.db.insertInto('person').values(person).returningAll().executeTakeFirstOrThrow();
} }

View File

@@ -70,11 +70,28 @@ describe(PersonService.name, () => {
describe('getAll', () => { describe('getAll', () => {
it('should get all hidden and visible people with thumbnails', async () => { it('should get all hidden and visible people with thumbnails', async () => {
// Updated stubs with the required aggregated fields
const personWithCounts = {
...personStub.withName,
faceCount: 3,
totalCount: 2,
hiddenCount: 1,
};
const hiddenPersonWithCounts = {
...personStub.hidden,
faceCount: 1,
totalCount: 2,
hiddenCount: 1,
};
mocks.person.getAllForUser.mockResolvedValue({ mocks.person.getAllForUser.mockResolvedValue({
items: [personStub.withName, personStub.hidden], items: [personWithCounts, hiddenPersonWithCounts],
hasNextPage: false, hasNextPage: false,
total: 2,
hidden: 1,
}); });
mocks.person.getNumberOfPeople.mockResolvedValue({ total: 2, hidden: 1 });
await expect(sut.getAll(authStub.admin, { withHidden: true, page: 1, size: 10 })).resolves.toEqual({ await expect(sut.getAll(authStub.admin, { withHidden: true, page: 1, size: 10 })).resolves.toEqual({
hasNextPage: false, hasNextPage: false,
total: 2, total: 2,
@@ -93,18 +110,36 @@ describe(PersonService.name, () => {
}, },
], ],
}); });
expect(mocks.person.getAllForUser).toHaveBeenCalledWith({ skip: 0, take: 10 }, authStub.admin.user.id, { expect(mocks.person.getAllForUser).toHaveBeenCalledWith({ skip: 0, take: 10 }, authStub.admin.user.id, {
minimumFaceCount: 3, minimumFaceCount: 3,
withHidden: true, withHidden: true,
closestFaceAssetId: undefined,
}); });
}); });
it('should get all visible people and favorites should be first in the array', async () => { it('should get all visible people and favorites should be first in the array', async () => {
const favoritePersonWithCounts = {
...personStub.isFavorite,
faceCount: 3,
totalCount: 2,
hiddenCount: 1,
};
const namedPersonWithCounts = {
...personStub.withName,
faceCount: 3,
totalCount: 2,
hiddenCount: 1,
};
mocks.person.getAllForUser.mockResolvedValue({ mocks.person.getAllForUser.mockResolvedValue({
items: [personStub.isFavorite, personStub.withName], items: [favoritePersonWithCounts, namedPersonWithCounts],
hasNextPage: false, hasNextPage: false,
total: 2,
hidden: 1,
}); });
mocks.person.getNumberOfPeople.mockResolvedValue({ total: 2, hidden: 1 });
await expect(sut.getAll(authStub.admin, { withHidden: false, page: 1, size: 10 })).resolves.toEqual({ await expect(sut.getAll(authStub.admin, { withHidden: false, page: 1, size: 10 })).resolves.toEqual({
hasNextPage: false, hasNextPage: false,
total: 2, total: 2,
@@ -123,9 +158,11 @@ describe(PersonService.name, () => {
responseDto, responseDto,
], ],
}); });
expect(mocks.person.getAllForUser).toHaveBeenCalledWith({ skip: 0, take: 10 }, authStub.admin.user.id, { expect(mocks.person.getAllForUser).toHaveBeenCalledWith({ skip: 0, take: 10 }, authStub.admin.user.id, {
minimumFaceCount: 3, minimumFaceCount: 3,
withHidden: false, withHidden: false,
closestFaceAssetId: undefined,
}); });
}); });
}); });

View File

@@ -48,12 +48,13 @@ import { isFacialRecognitionEnabled } from 'src/utils/misc';
export class PersonService extends BaseService { export class PersonService extends BaseService {
async getAll(auth: AuthDto, dto: PersonSearchDto): Promise<PeopleResponseDto> { async getAll(auth: AuthDto, dto: PersonSearchDto): Promise<PeopleResponseDto> {
const { withHidden = false, closestAssetId, closestPersonId, page, size } = dto; const { withHidden = false, closestAssetId, closestPersonId, page, size } = dto;
let closestFaceAssetId = closestAssetId;
const pagination = { const pagination = {
take: size, take: size,
skip: (page - 1) * size, skip: (page - 1) * size,
}; };
let closestFaceAssetId = closestAssetId;
if (closestPersonId) { if (closestPersonId) {
const person = await this.personRepository.getById(closestPersonId); const person = await this.personRepository.getById(closestPersonId);
if (!person?.faceAssetId) { if (!person?.faceAssetId) {
@@ -61,22 +62,20 @@ export class PersonService extends BaseService {
} }
closestFaceAssetId = person.faceAssetId; closestFaceAssetId = person.faceAssetId;
} }
const { machineLearning } = await this.getConfig({ withCache: false }); const { machineLearning } = await this.getConfig({ withCache: false });
const { items, hasNextPage } = await this.personRepository.getAllForUser(pagination, auth.user.id, {
const { items, hasNextPage, total, hidden } = await this.personRepository.getAllForUser(pagination, auth.user.id, {
minimumFaceCount: machineLearning.facialRecognition.minFaces, minimumFaceCount: machineLearning.facialRecognition.minFaces,
withHidden, withHidden,
closestFaceAssetId, closestFaceAssetId,
}); });
const { total, hidden } = await this.personRepository.getNumberOfPeople(auth.user.id, {
minimumFaceCount: machineLearning.facialRecognition.minFaces,
withHidden,
});
return { return {
people: items.map((person) => mapPerson(person)), people: items.map((person) => mapPerson(person)),
hasNextPage, hasNextPage,
total, total: Number(total),
hidden, hidden: Number(hidden),
}; };
} }