mirror of
https://github.com/immich-app/immich.git
synced 2025-12-16 09:13:13 +03:00
refactor: merge into one query
This commit is contained in:
@@ -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 (
|
||||||
|
|||||||
@@ -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,45 +159,62 @@ 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)]),
|
||||||
eb(
|
)
|
||||||
(eb) =>
|
.selectAll('person')
|
||||||
eb
|
.select((eb) => [
|
||||||
.selectFrom('face_search')
|
eb.fn.count('asset_face.assetId').as('faceCount'),
|
||||||
.select('face_search.embedding')
|
|
||||||
.whereRef('face_search.faceId', '=', 'person.faceAssetId'),
|
eb.fn.countAll().over().as('totalCount'),
|
||||||
'<=>',
|
|
||||||
(eb) =>
|
eb.fn
|
||||||
eb
|
.sum(sql<number>`CASE WHEN ${sql.ref('person.isHidden')} THEN 1 ELSE 0 END`)
|
||||||
.selectFrom('face_search')
|
.over()
|
||||||
.select('face_search.embedding')
|
.as('hiddenCount'),
|
||||||
.where('face_search.faceId', '=', options!.closestFaceAssetId!),
|
]);
|
||||||
),
|
|
||||||
|
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
|
||||||
|
.selectFrom('face_search')
|
||||||
|
.select('face_search.embedding')
|
||||||
|
.whereRef('face_search.faceId', '=', 'person.faceAssetId'),
|
||||||
|
'<=>',
|
||||||
|
(eb) =>
|
||||||
|
eb.selectFrom('face_search').select('face_search.embedding').where('face_search.faceId', '=', closestId),
|
||||||
),
|
),
|
||||||
)
|
);
|
||||||
.$if(!options?.closestFaceAssetId, (qb) =>
|
} else {
|
||||||
qb
|
sorted = sorted
|
||||||
.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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user