feat(server): Merge Faces sorted by Similarity (#14635)

* Merge Faces sorted by Similarity

* Adds face sorting to the side panel face merger

* run make open-api

* Make it one query

* Only have the single order by when sorting by closest face
This commit is contained in:
Lukas 2024-12-16 09:47:11 -05:00 committed by GitHub
parent 8945a5d862
commit 12e55f5bf0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 136 additions and 44 deletions

View file

@ -31,8 +31,8 @@ export class PersonController {
@Get()
@Authenticated({ permission: Permission.PERSON_READ })
getAllPeople(@Auth() auth: AuthDto, @Query() withHidden: PersonSearchDto): Promise<PeopleResponseDto> {
return this.service.getAll(auth, withHidden);
getAllPeople(@Auth() auth: AuthDto, @Query() options: PersonSearchDto): Promise<PeopleResponseDto> {
return this.service.getAll(auth, options);
}
@Post()

View file

@ -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()

View file

@ -10,6 +10,7 @@ export const IPersonRepository = 'IPersonRepository';
export interface PersonSearchOptions {
minimumFaceCount: number;
withHidden: boolean;
closestFaceAssetId?: string;
}
export interface PersonNameSearchOptions {

View file

@ -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<PersonEntity> {
async getAllForUser(
pagination: PaginationOptions,
userId: string,
options?: PersonSearchOptions,
): Paginated<PersonEntity> {
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,

View file

@ -55,16 +55,25 @@ import { IsNull } from 'typeorm';
@Injectable()
export class PersonService extends BaseService {
async getAll(auth: AuthDto, dto: PersonSearchDto): Promise<PeopleResponseDto> {
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);