fix(server): vacuum after deleting people (#18299)

* vacuum after deleting people

* update sql
This commit is contained in:
Mert 2025-05-14 23:13:13 -04:00 committed by GitHub
parent cd03d0c0f2
commit 3a0ddfb92d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 18 additions and 19 deletions

View File

@ -13,12 +13,6 @@ set
"personId" = $1 "personId" = $1
where where
"asset_faces"."sourceType" = $2 "asset_faces"."sourceType" = $2
VACUUM
ANALYZE asset_faces,
face_search,
person
REINDEX TABLE asset_faces
REINDEX TABLE person
-- PersonRepository.delete -- PersonRepository.delete
delete from "person" delete from "person"
@ -29,12 +23,6 @@ where
delete from "asset_faces" delete from "asset_faces"
where where
"asset_faces"."sourceType" = $1 "asset_faces"."sourceType" = $1
VACUUM
ANALYZE asset_faces,
face_search,
person
REINDEX TABLE asset_faces
REINDEX TABLE person
-- PersonRepository.getAllWithoutFaces -- PersonRepository.getAllWithoutFaces
select select

View File

@ -105,8 +105,6 @@ export class PersonRepository {
.set({ personId: null }) .set({ personId: null })
.where('asset_faces.sourceType', '=', sourceType) .where('asset_faces.sourceType', '=', sourceType)
.execute(); .execute();
await this.vacuum({ reindexVectors: false });
} }
@GenerateSql({ params: [DummyValue.UUID] }) @GenerateSql({ params: [DummyValue.UUID] })
@ -121,8 +119,6 @@ export class PersonRepository {
@GenerateSql({ params: [{ sourceType: SourceType.EXIF }] }) @GenerateSql({ params: [{ sourceType: SourceType.EXIF }] })
async deleteFaces({ sourceType }: DeleteFacesOptions): Promise<void> { async deleteFaces({ sourceType }: DeleteFacesOptions): Promise<void> {
await this.db.deleteFrom('asset_faces').where('asset_faces.sourceType', '=', sourceType).execute(); await this.db.deleteFrom('asset_faces').where('asset_faces.sourceType', '=', sourceType).execute();
await this.vacuum({ reindexVectors: sourceType === SourceType.MACHINE_LEARNING });
} }
getAllFaces(options: GetAllFacesOptions = {}) { getAllFaces(options: GetAllFacesOptions = {}) {
@ -519,7 +515,7 @@ export class PersonRepository {
await this.db.updateTable('asset_faces').set({ deletedAt: new Date() }).where('asset_faces.id', '=', id).execute(); await this.db.updateTable('asset_faces').set({ deletedAt: new Date() }).where('asset_faces.id', '=', id).execute();
} }
private async vacuum({ reindexVectors }: { reindexVectors: boolean }): Promise<void> { async vacuum({ reindexVectors }: { reindexVectors: boolean }): Promise<void> {
await sql`VACUUM ANALYZE asset_faces, face_search, person`.execute(this.db); await sql`VACUUM ANALYZE asset_faces, face_search, person`.execute(this.db);
await sql`REINDEX TABLE asset_faces`.execute(this.db); await sql`REINDEX TABLE asset_faces`.execute(this.db);
await sql`REINDEX TABLE person`.execute(this.db); await sql`REINDEX TABLE person`.execute(this.db);

View File

@ -459,6 +459,7 @@ describe(PersonService.name, () => {
await sut.handleQueueDetectFaces({ force: false }); await sut.handleQueueDetectFaces({ force: false });
expect(mocks.assetJob.streamForDetectFacesJob).toHaveBeenCalledWith(false); expect(mocks.assetJob.streamForDetectFacesJob).toHaveBeenCalledWith(false);
expect(mocks.person.vacuum).not.toHaveBeenCalled();
expect(mocks.job.queueAll).toHaveBeenCalledWith([ expect(mocks.job.queueAll).toHaveBeenCalledWith([
{ {
name: JobName.FACE_DETECTION, name: JobName.FACE_DETECTION,
@ -475,6 +476,7 @@ describe(PersonService.name, () => {
expect(mocks.person.deleteFaces).toHaveBeenCalledWith({ sourceType: SourceType.MACHINE_LEARNING }); expect(mocks.person.deleteFaces).toHaveBeenCalledWith({ sourceType: SourceType.MACHINE_LEARNING });
expect(mocks.person.delete).toHaveBeenCalledWith([personStub.withName.id]); expect(mocks.person.delete).toHaveBeenCalledWith([personStub.withName.id]);
expect(mocks.person.vacuum).toHaveBeenCalledWith({ reindexVectors: true });
expect(mocks.storage.unlink).toHaveBeenCalledWith(personStub.withName.thumbnailPath); expect(mocks.storage.unlink).toHaveBeenCalledWith(personStub.withName.thumbnailPath);
expect(mocks.assetJob.streamForDetectFacesJob).toHaveBeenCalledWith(true); expect(mocks.assetJob.streamForDetectFacesJob).toHaveBeenCalledWith(true);
expect(mocks.job.queueAll).toHaveBeenCalledWith([ expect(mocks.job.queueAll).toHaveBeenCalledWith([
@ -492,6 +494,7 @@ describe(PersonService.name, () => {
expect(mocks.person.delete).not.toHaveBeenCalled(); expect(mocks.person.delete).not.toHaveBeenCalled();
expect(mocks.person.deleteFaces).not.toHaveBeenCalled(); expect(mocks.person.deleteFaces).not.toHaveBeenCalled();
expect(mocks.person.vacuum).not.toHaveBeenCalled();
expect(mocks.storage.unlink).not.toHaveBeenCalled(); expect(mocks.storage.unlink).not.toHaveBeenCalled();
expect(mocks.assetJob.streamForDetectFacesJob).toHaveBeenCalledWith(undefined); expect(mocks.assetJob.streamForDetectFacesJob).toHaveBeenCalledWith(undefined);
expect(mocks.job.queueAll).toHaveBeenCalledWith([ expect(mocks.job.queueAll).toHaveBeenCalledWith([
@ -521,6 +524,7 @@ describe(PersonService.name, () => {
]); ]);
expect(mocks.person.delete).toHaveBeenCalledWith([personStub.randomPerson.id]); expect(mocks.person.delete).toHaveBeenCalledWith([personStub.randomPerson.id]);
expect(mocks.storage.unlink).toHaveBeenCalledWith(personStub.randomPerson.thumbnailPath); expect(mocks.storage.unlink).toHaveBeenCalledWith(personStub.randomPerson.thumbnailPath);
expect(mocks.person.vacuum).toHaveBeenCalledWith({ reindexVectors: true });
}); });
}); });
@ -584,6 +588,7 @@ describe(PersonService.name, () => {
expect(mocks.systemMetadata.set).toHaveBeenCalledWith(SystemMetadataKey.FACIAL_RECOGNITION_STATE, { expect(mocks.systemMetadata.set).toHaveBeenCalledWith(SystemMetadataKey.FACIAL_RECOGNITION_STATE, {
lastRun: expect.any(String), lastRun: expect.any(String),
}); });
expect(mocks.person.vacuum).not.toHaveBeenCalled();
}); });
it('should queue all assets', async () => { it('should queue all assets', async () => {
@ -611,6 +616,7 @@ describe(PersonService.name, () => {
expect(mocks.systemMetadata.set).toHaveBeenCalledWith(SystemMetadataKey.FACIAL_RECOGNITION_STATE, { expect(mocks.systemMetadata.set).toHaveBeenCalledWith(SystemMetadataKey.FACIAL_RECOGNITION_STATE, {
lastRun: expect.any(String), lastRun: expect.any(String),
}); });
expect(mocks.person.vacuum).toHaveBeenCalledWith({ reindexVectors: false });
}); });
it('should run nightly if new face has been added since last run', async () => { it('should run nightly if new face has been added since last run', async () => {
@ -629,11 +635,14 @@ describe(PersonService.name, () => {
mocks.person.getAllWithoutFaces.mockResolvedValue([]); mocks.person.getAllWithoutFaces.mockResolvedValue([]);
mocks.person.unassignFaces.mockResolvedValue(); mocks.person.unassignFaces.mockResolvedValue();
await sut.handleQueueRecognizeFaces({ force: true, nightly: true }); await sut.handleQueueRecognizeFaces({ force: false, nightly: true });
expect(mocks.systemMetadata.get).toHaveBeenCalledWith(SystemMetadataKey.FACIAL_RECOGNITION_STATE); expect(mocks.systemMetadata.get).toHaveBeenCalledWith(SystemMetadataKey.FACIAL_RECOGNITION_STATE);
expect(mocks.person.getLatestFaceDate).toHaveBeenCalledOnce(); expect(mocks.person.getLatestFaceDate).toHaveBeenCalledOnce();
expect(mocks.person.getAllFaces).toHaveBeenCalledWith(undefined); expect(mocks.person.getAllFaces).toHaveBeenCalledWith({
personId: null,
sourceType: SourceType.MACHINE_LEARNING,
});
expect(mocks.job.queueAll).toHaveBeenCalledWith([ expect(mocks.job.queueAll).toHaveBeenCalledWith([
{ {
name: JobName.FACIAL_RECOGNITION, name: JobName.FACIAL_RECOGNITION,
@ -643,6 +652,7 @@ describe(PersonService.name, () => {
expect(mocks.systemMetadata.set).toHaveBeenCalledWith(SystemMetadataKey.FACIAL_RECOGNITION_STATE, { expect(mocks.systemMetadata.set).toHaveBeenCalledWith(SystemMetadataKey.FACIAL_RECOGNITION_STATE, {
lastRun: expect.any(String), lastRun: expect.any(String),
}); });
expect(mocks.person.vacuum).not.toHaveBeenCalled();
}); });
it('should skip nightly if no new face has been added since last run', async () => { it('should skip nightly if no new face has been added since last run', async () => {
@ -660,6 +670,7 @@ describe(PersonService.name, () => {
expect(mocks.person.getAllFaces).not.toHaveBeenCalled(); expect(mocks.person.getAllFaces).not.toHaveBeenCalled();
expect(mocks.job.queueAll).not.toHaveBeenCalled(); expect(mocks.job.queueAll).not.toHaveBeenCalled();
expect(mocks.systemMetadata.set).not.toHaveBeenCalled(); expect(mocks.systemMetadata.set).not.toHaveBeenCalled();
expect(mocks.person.vacuum).not.toHaveBeenCalled();
}); });
it('should delete existing people if forced', async () => { it('should delete existing people if forced', async () => {
@ -688,6 +699,7 @@ describe(PersonService.name, () => {
]); ]);
expect(mocks.person.delete).toHaveBeenCalledWith([personStub.randomPerson.id]); expect(mocks.person.delete).toHaveBeenCalledWith([personStub.randomPerson.id]);
expect(mocks.storage.unlink).toHaveBeenCalledWith(personStub.randomPerson.thumbnailPath); expect(mocks.storage.unlink).toHaveBeenCalledWith(personStub.randomPerson.thumbnailPath);
expect(mocks.person.vacuum).toHaveBeenCalledWith({ reindexVectors: false });
}); });
}); });

View File

@ -259,6 +259,7 @@ export class PersonService extends BaseService {
if (force) { if (force) {
await this.personRepository.deleteFaces({ sourceType: SourceType.MACHINE_LEARNING }); await this.personRepository.deleteFaces({ sourceType: SourceType.MACHINE_LEARNING });
await this.handlePersonCleanup(); await this.handlePersonCleanup();
await this.personRepository.vacuum({ reindexVectors: true });
} }
let jobs: JobItem[] = []; let jobs: JobItem[] = [];
@ -409,6 +410,7 @@ export class PersonService extends BaseService {
if (force) { if (force) {
await this.personRepository.unassignFaces({ sourceType: SourceType.MACHINE_LEARNING }); await this.personRepository.unassignFaces({ sourceType: SourceType.MACHINE_LEARNING });
await this.handlePersonCleanup(); await this.handlePersonCleanup();
await this.personRepository.vacuum({ reindexVectors: false });
} else if (waiting) { } else if (waiting) {
this.logger.debug( this.logger.debug(
`Skipping facial recognition queueing because ${waiting} job${waiting > 1 ? 's are' : ' is'} already queued`, `Skipping facial recognition queueing because ${waiting} job${waiting > 1 ? 's are' : ' is'} already queued`,

View File

@ -33,5 +33,6 @@ export const newPersonRepositoryMock = (): Mocked<RepositoryInterface<PersonRepo
createAssetFace: vitest.fn(), createAssetFace: vitest.fn(),
deleteAssetFace: vitest.fn(), deleteAssetFace: vitest.fn(),
softDeleteAssetFaces: vitest.fn(), softDeleteAssetFaces: vitest.fn(),
vacuum: vitest.fn(),
}; };
}; };