refactor: remove smart search entity (#17447)

refactor: smart search entity
This commit is contained in:
Jason Rasmussen
2025-04-08 09:56:45 -04:00
committed by GitHub
parent 2b131fe935
commit fdbe6d649f
10 changed files with 125 additions and 134 deletions

View File

@@ -1,4 +1,4 @@
import { JobName, JobStatus } from 'src/enum';
import { AssetFileType, AssetType, JobName, JobStatus } from 'src/enum';
import { WithoutProperty } from 'src/repositories/asset.repository';
import { DuplicateService } from 'src/services/duplicate.service';
import { SearchService } from 'src/services/search.service';
@@ -9,6 +9,33 @@ import { beforeEach, vitest } from 'vitest';
vitest.useFakeTimers();
const hasEmbedding = {
id: 'asset-1',
ownerId: 'user-id',
files: [
{
assetId: 'asset-1',
createdAt: new Date(),
id: 'file-1',
path: 'preview.jpg',
type: AssetFileType.PREVIEW,
updatedAt: new Date(),
updateId: 'update-1',
},
],
isVisible: true,
stackId: null,
type: AssetType.IMAGE,
duplicateId: null,
embedding: '[1, 2, 3, 4]',
};
const hasDupe = {
...hasEmbedding,
id: 'asset-2',
duplicateId: 'duplicate-id',
};
describe(SearchService.name, () => {
let sut: DuplicateService;
let mocks: ServiceMocks;
@@ -25,16 +52,16 @@ describe(SearchService.name, () => {
it('should get duplicates', async () => {
mocks.asset.getDuplicates.mockResolvedValue([
{
duplicateId: assetStub.hasDupe.duplicateId!,
assets: [assetStub.hasDupe, assetStub.hasDupe],
duplicateId: 'duplicate-id',
assets: [assetStub.image, assetStub.image],
},
]);
await expect(sut.getDuplicates(authStub.admin)).resolves.toEqual([
{
duplicateId: assetStub.hasDupe.duplicateId,
duplicateId: 'duplicate-id',
assets: [
expect.objectContaining({ id: assetStub.hasDupe.id }),
expect.objectContaining({ id: assetStub.hasDupe.id }),
expect.objectContaining({ id: assetStub.image.id }),
expect.objectContaining({ id: assetStub.image.id }),
],
},
]);
@@ -175,7 +202,7 @@ describe(SearchService.name, () => {
it('should skip if asset is part of stack', async () => {
const id = assetStub.primaryImage.id;
mocks.asset.getById.mockResolvedValue(assetStub.primaryImage);
mocks.asset.getAssetForSearchDuplicatesJob.mockResolvedValue({ ...hasEmbedding, stackId: 'stack-id' });
const result = await sut.handleSearchDuplicates({ id });
@@ -185,7 +212,7 @@ describe(SearchService.name, () => {
it('should skip if asset is not visible', async () => {
const id = assetStub.livePhotoMotionAsset.id;
mocks.asset.getById.mockResolvedValue(assetStub.livePhotoMotionAsset);
mocks.asset.getAssetForSearchDuplicatesJob.mockResolvedValue({ ...hasEmbedding, isVisible: false });
const result = await sut.handleSearchDuplicates({ id });
@@ -194,7 +221,7 @@ describe(SearchService.name, () => {
});
it('should fail if asset is missing preview image', async () => {
mocks.asset.getById.mockResolvedValue(assetStub.noResizePath);
mocks.asset.getAssetForSearchDuplicatesJob.mockResolvedValue({ ...hasEmbedding, files: [] });
const result = await sut.handleSearchDuplicates({ id: assetStub.noResizePath.id });
@@ -203,7 +230,7 @@ describe(SearchService.name, () => {
});
it('should fail if asset is missing embedding', async () => {
mocks.asset.getById.mockResolvedValue(assetStub.image);
mocks.asset.getAssetForSearchDuplicatesJob.mockResolvedValue({ ...hasEmbedding, embedding: null });
const result = await sut.handleSearchDuplicates({ id: assetStub.image.id });
@@ -212,21 +239,21 @@ describe(SearchService.name, () => {
});
it('should search for duplicates and update asset with duplicateId', async () => {
mocks.asset.getById.mockResolvedValue(assetStub.hasEmbedding);
mocks.asset.getAssetForSearchDuplicatesJob.mockResolvedValue(hasEmbedding);
mocks.search.searchDuplicates.mockResolvedValue([
{ assetId: assetStub.image.id, distance: 0.01, duplicateId: null },
]);
const expectedAssetIds = [assetStub.image.id, assetStub.hasEmbedding.id];
const expectedAssetIds = [assetStub.image.id, hasEmbedding.id];
const result = await sut.handleSearchDuplicates({ id: assetStub.hasEmbedding.id });
const result = await sut.handleSearchDuplicates({ id: hasEmbedding.id });
expect(result).toBe(JobStatus.SUCCESS);
expect(mocks.search.searchDuplicates).toHaveBeenCalledWith({
assetId: assetStub.hasEmbedding.id,
embedding: assetStub.hasEmbedding.smartSearch!.embedding,
assetId: hasEmbedding.id,
embedding: hasEmbedding.embedding,
maxDistance: 0.01,
type: assetStub.hasEmbedding.type,
userIds: [assetStub.hasEmbedding.ownerId],
type: hasEmbedding.type,
userIds: [hasEmbedding.ownerId],
});
expect(mocks.asset.updateDuplicates).toHaveBeenCalledWith({
assetIds: expectedAssetIds,
@@ -239,24 +266,24 @@ describe(SearchService.name, () => {
});
it('should use existing duplicate ID among matched duplicates', async () => {
const duplicateId = assetStub.hasDupe.duplicateId;
mocks.asset.getById.mockResolvedValue(assetStub.hasEmbedding);
mocks.search.searchDuplicates.mockResolvedValue([{ assetId: assetStub.hasDupe.id, distance: 0.01, duplicateId }]);
const expectedAssetIds = [assetStub.hasEmbedding.id];
const duplicateId = hasDupe.duplicateId;
mocks.asset.getAssetForSearchDuplicatesJob.mockResolvedValue(hasEmbedding);
mocks.search.searchDuplicates.mockResolvedValue([{ assetId: hasDupe.id, distance: 0.01, duplicateId }]);
const expectedAssetIds = [hasEmbedding.id];
const result = await sut.handleSearchDuplicates({ id: assetStub.hasEmbedding.id });
const result = await sut.handleSearchDuplicates({ id: hasEmbedding.id });
expect(result).toBe(JobStatus.SUCCESS);
expect(mocks.search.searchDuplicates).toHaveBeenCalledWith({
assetId: assetStub.hasEmbedding.id,
embedding: assetStub.hasEmbedding.smartSearch!.embedding,
assetId: hasEmbedding.id,
embedding: hasEmbedding.embedding,
maxDistance: 0.01,
type: assetStub.hasEmbedding.type,
userIds: [assetStub.hasEmbedding.ownerId],
type: hasEmbedding.type,
userIds: [hasEmbedding.ownerId],
});
expect(mocks.asset.updateDuplicates).toHaveBeenCalledWith({
assetIds: expectedAssetIds,
targetDuplicateId: assetStub.hasDupe.duplicateId,
targetDuplicateId: duplicateId,
duplicateIds: [],
});
expect(mocks.asset.upsertJobStatus).toHaveBeenCalledWith(
@@ -265,15 +292,15 @@ describe(SearchService.name, () => {
});
it('should remove duplicateId if no duplicates found and asset has duplicateId', async () => {
mocks.asset.getById.mockResolvedValue(assetStub.hasDupe);
mocks.asset.getAssetForSearchDuplicatesJob.mockResolvedValue(hasDupe);
mocks.search.searchDuplicates.mockResolvedValue([]);
const result = await sut.handleSearchDuplicates({ id: assetStub.hasDupe.id });
const result = await sut.handleSearchDuplicates({ id: hasDupe.id });
expect(result).toBe(JobStatus.SUCCESS);
expect(mocks.asset.update).toHaveBeenCalledWith({ id: assetStub.hasDupe.id, duplicateId: null });
expect(mocks.asset.update).toHaveBeenCalledWith({ id: hasDupe.id, duplicateId: null });
expect(mocks.asset.upsertJobStatus).toHaveBeenCalledWith({
assetId: assetStub.hasDupe.id,
assetId: hasDupe.id,
duplicatesDetectedAt: expect.any(Date),
});
});

View File

@@ -4,7 +4,6 @@ import { OnJob } from 'src/decorators';
import { mapAsset } from 'src/dtos/asset-response.dto';
import { AuthDto } from 'src/dtos/auth.dto';
import { DuplicateResponseDto } from 'src/dtos/duplicate.dto';
import { AssetEntity } from 'src/entities/asset.entity';
import { AssetFileType, JobName, JobStatus, QueueName } from 'src/enum';
import { WithoutProperty } from 'src/repositories/asset.repository';
import { AssetDuplicateResult } from 'src/repositories/search.repository';
@@ -53,7 +52,7 @@ export class DuplicateService extends BaseService {
return JobStatus.SKIPPED;
}
const asset = await this.assetRepository.getById(id, { files: true, smartSearch: true });
const asset = await this.assetRepository.getAssetForSearchDuplicatesJob(id);
if (!asset) {
this.logger.error(`Asset ${id} not found`);
return JobStatus.FAILED;
@@ -75,14 +74,14 @@ export class DuplicateService extends BaseService {
return JobStatus.FAILED;
}
if (!asset.smartSearch?.embedding) {
if (!asset.embedding) {
this.logger.debug(`Asset ${id} is missing embedding`);
return JobStatus.FAILED;
}
const duplicateAssets = await this.searchRepository.searchDuplicates({
assetId: asset.id,
embedding: asset.smartSearch.embedding,
embedding: asset.embedding,
maxDistance: machineLearning.duplicateDetection.maxDistance,
type: asset.type,
userIds: [asset.ownerId],
@@ -105,7 +104,10 @@ export class DuplicateService extends BaseService {
return JobStatus.SUCCESS;
}
private async updateDuplicates(asset: AssetEntity, duplicateAssets: AssetDuplicateResult[]): Promise<string[]> {
private async updateDuplicates(
asset: { id: string; duplicateId: string | null },
duplicateAssets: AssetDuplicateResult[],
): Promise<string[]> {
const duplicateIds = [
...new Set(
duplicateAssets