chore(server): remove old asset search (#9104)

* chore(server): remove old asset search

* chore: remove more unused search code
This commit is contained in:
Jason Rasmussen 2024-04-27 08:57:39 -04:00 committed by GitHub
parent 0c60aaf557
commit 5a49de5592
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 30 additions and 1801 deletions

View file

@ -1,5 +1,5 @@
import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Put, Query } from '@nestjs/common';
import { ApiOperation, ApiTags } from '@nestjs/swagger';
import { ApiTags } from '@nestjs/swagger';
import { AssetResponseDto, MemoryLaneResponseDto } from 'src/dtos/asset-response.dto';
import {
AssetBulkDeleteDto,
@ -12,30 +12,13 @@ import {
UpdateAssetDto,
} from 'src/dtos/asset.dto';
import { AuthDto } from 'src/dtos/auth.dto';
import { MapMarkerDto, MapMarkerResponseDto, MemoryLaneDto, MetadataSearchDto } from 'src/dtos/search.dto';
import { MapMarkerDto, MapMarkerResponseDto, MemoryLaneDto } from 'src/dtos/search.dto';
import { UpdateStackParentDto } from 'src/dtos/stack.dto';
import { Auth, Authenticated, SharedLinkRoute } from 'src/middleware/auth.guard';
import { Route } from 'src/middleware/file-upload.interceptor';
import { AssetService } from 'src/services/asset.service';
import { SearchService } from 'src/services/search.service';
import { UUIDParamDto } from 'src/validation';
@ApiTags('Asset')
@Controller('assets')
@Authenticated()
export class AssetsController {
constructor(private searchService: SearchService) {}
@Get()
@ApiOperation({ deprecated: true })
async searchAssets(@Auth() auth: AuthDto, @Query() dto: MetadataSearchDto): Promise<AssetResponseDto[]> {
const {
assets: { items },
} = await this.searchService.searchMetadata(auth, dto);
return items;
}
}
@ApiTags('Asset')
@Controller(Route.ASSET)
@Authenticated()

View file

@ -3,7 +3,7 @@ import { AlbumController } from 'src/controllers/album.controller';
import { APIKeyController } from 'src/controllers/api-key.controller';
import { AppController } from 'src/controllers/app.controller';
import { AssetControllerV1 } from 'src/controllers/asset-v1.controller';
import { AssetController, AssetsController } from 'src/controllers/asset.controller';
import { AssetController } from 'src/controllers/asset.controller';
import { AuditController } from 'src/controllers/audit.controller';
import { AuthController } from 'src/controllers/auth.controller';
import { DownloadController } from 'src/controllers/download.controller';
@ -34,7 +34,6 @@ export const controllers = [
AppController,
AssetController,
AssetControllerV1,
AssetsController,
AuditController,
AuthController,
DownloadController,

View file

@ -1,12 +1,11 @@
import { Body, Controller, Get, HttpCode, HttpStatus, Post, Query } from '@nestjs/common';
import { ApiOperation, ApiTags } from '@nestjs/swagger';
import { ApiTags } from '@nestjs/swagger';
import { AssetResponseDto } from 'src/dtos/asset-response.dto';
import { AuthDto } from 'src/dtos/auth.dto';
import { PersonResponseDto } from 'src/dtos/person.dto';
import {
MetadataSearchDto,
PlacesResponseDto,
SearchDto,
SearchExploreResponseDto,
SearchPeopleDto,
SearchPlacesDto,
@ -23,12 +22,6 @@ import { SearchService } from 'src/services/search.service';
export class SearchController {
constructor(private service: SearchService) {}
@Get()
@ApiOperation({ deprecated: true })
search(@Auth() auth: AuthDto, @Query() dto: SearchDto): Promise<SearchResponseDto> {
return this.service.search(auth, dto);
}
@Post('metadata')
@HttpCode(HttpStatus.OK)
searchMetadata(@Auth() auth: AuthDto, @Body() dto: MetadataSearchDto): Promise<SearchResponseDto> {

View file

@ -199,53 +199,6 @@ export class SmartSearchDto extends BaseSearchDto {
query!: string;
}
// TODO: remove after implementing new search filters
/** @deprecated */
export class SearchDto {
@IsString()
@IsNotEmpty()
@Optional()
q?: string;
@IsString()
@IsNotEmpty()
@Optional()
query?: string;
@ValidateBoolean({ optional: true })
smart?: boolean;
/** @deprecated */
@ValidateBoolean({ optional: true })
clip?: boolean;
@IsEnum(AssetType)
@Optional()
type?: AssetType;
@ValidateBoolean({ optional: true })
recent?: boolean;
@ValidateBoolean({ optional: true })
motion?: boolean;
@ValidateBoolean({ optional: true })
withArchived?: boolean;
@IsInt()
@Min(1)
@Type(() => Number)
@Optional()
page?: number;
@IsInt()
@Min(1)
@Max(1000)
@Type(() => Number)
@Optional()
size?: number;
}
export class SearchPlacesDto {
@IsString()
@IsNotEmpty()

View file

@ -129,10 +129,6 @@ export interface AssetExploreOptions extends AssetExploreFieldOptions {
unnest?: boolean;
}
export interface MetadataSearchOptions {
numResults: number;
}
export interface AssetFullSyncOptions {
ownerId: string;
lastCreationDate?: Date;
@ -188,7 +184,6 @@ export interface IAssetRepository {
upsertJobStatus(jobStatus: Partial<AssetJobStatusEntity>): Promise<void>;
getAssetIdByCity(userId: string, options: AssetExploreFieldOptions): Promise<SearchExploreItem<string>>;
getAssetIdByTag(userId: string, options: AssetExploreFieldOptions): Promise<SearchExploreItem<string>>;
searchMetadata(query: string, userIds: string[], options: MetadataSearchOptions): Promise<AssetEntity[]>;
getAllForUserFullSync(options: AssetFullSyncOptions): Promise<AssetEntity[]>;
getChangedDeltaSync(options: AssetDeltaSyncOptions): Promise<AssetEntity[]>;
}

View file

@ -5,29 +5,6 @@ import { Paginated } from 'src/utils/pagination';
export const ISearchRepository = 'ISearchRepository';
export enum SearchStrategy {
SMART = 'SMART',
TEXT = 'TEXT',
}
export interface SearchFilter {
id?: string;
userId: string;
type?: AssetType;
isFavorite?: boolean;
isArchived?: boolean;
city?: string;
state?: string;
country?: string;
make?: string;
model?: string;
objects?: string[];
tags?: string[];
recent?: boolean;
motion?: boolean;
debug?: boolean;
}
export interface SearchResult<T> {
/** total matches */
total: number;

View file

@ -738,37 +738,6 @@ WHERE
LIMIT
12
-- AssetRepository.searchMetadata
SELECT
asset.*,
e.*,
COALESCE("si"."tags", array[]::text[]) AS "tags",
COALESCE("si"."objects", array[]::text[]) AS "objects"
FROM
"assets" "asset"
INNER JOIN "exif" "e" ON asset."id" = e."assetId"
LEFT JOIN "smart_info" "si" ON si."assetId" = asset."id"
WHERE
(
"asset"."isVisible" = true
AND "asset"."ownerId" IN ($1)
AND "asset"."isArchived" = $2
AND (
(
e."exifTextSearchableColumn" || COALESCE(
si."smartInfoTextSearchableColumn",
to_tsvector('english', '')
)
) @@ PLAINTO_TSQUERY('english', $3)
OR asset."originalFileName" = $4
)
)
AND ("asset"."deletedAt" IS NULL)
ORDER BY
"asset"."fileCreatedAt" DESC
LIMIT
250
-- AssetRepository.getAllForUserFullSync
SELECT
"asset"."id" AS "asset_id",

View file

@ -1,6 +1,5 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import path from 'node:path';
import { Chunked, ChunkedArray, DummyValue, GenerateSql } from 'src/decorators';
import { AlbumEntity, AssetOrder } from 'src/entities/album.entity';
import { AssetJobStatusEntity } from 'src/entities/asset-job-status.entity';
@ -23,7 +22,6 @@ import {
LivePhotoSearchOptions,
MapMarker,
MapMarkerSearchOptions,
MetadataSearchOptions,
MonthDay,
TimeBucketItem,
TimeBucketOptions,
@ -700,94 +698,6 @@ export class AssetRepository implements IAssetRepository {
return builder;
}
@GenerateSql({ params: [DummyValue.STRING, [DummyValue.UUID], { numResults: 250 }] })
async searchMetadata(
query: string,
userIds: string[],
{ numResults }: MetadataSearchOptions,
): Promise<AssetEntity[]> {
const rows = await this.getBuilder({
userIds: userIds,
exifInfo: false,
isArchived: false,
})
.select('asset.*')
.addSelect('e.*')
.addSelect('COALESCE(si.tags, array[]::text[])', 'tags')
.addSelect('COALESCE(si.objects, array[]::text[])', 'objects')
.innerJoin('exif', 'e', 'asset."id" = e."assetId"')
.leftJoin('smart_info', 'si', 'si."assetId" = asset."id"')
.andWhere(
new Brackets((qb) => {
qb.where(
`(e."exifTextSearchableColumn" || COALESCE(si."smartInfoTextSearchableColumn", to_tsvector('english', '')))
@@ PLAINTO_TSQUERY('english', :query)`,
{ query },
).orWhere('asset."originalFileName" = :path', { path: path.parse(query).name });
}),
)
.addOrderBy('asset.fileCreatedAt', 'DESC')
.limit(numResults)
.getRawMany();
return rows.map(
({
tags,
objects,
country,
state,
city,
description,
model,
make,
dateTimeOriginal,
exifImageHeight,
exifImageWidth,
exposureTime,
fNumber,
fileSizeInByte,
focalLength,
iso,
latitude,
lensModel,
longitude,
modifyDate,
projectionType,
timeZone,
...assetInfo
}) =>
({
exifInfo: {
city,
country,
dateTimeOriginal,
description,
exifImageHeight,
exifImageWidth,
exposureTime,
fNumber,
fileSizeInByte,
focalLength,
iso,
latitude,
lensModel,
longitude,
make,
model,
modifyDate,
projectionType,
state,
timeZone,
},
smartInfo: {
tags,
objects,
},
...assetInfo,
}) as AssetEntity,
);
}
@GenerateSql({
params: [
{

View file

@ -1,6 +1,4 @@
import { mapAsset } from 'src/dtos/asset-response.dto';
import { SearchDto } from 'src/dtos/search.dto';
import { SystemConfigKey } from 'src/entities/system-config.entity';
import { IAssetRepository } from 'src/interfaces/asset.interface';
import { ILoggerRepository } from 'src/interfaces/logger.interface';
import { IMachineLearningRepository } from 'src/interfaces/machine-learning.interface';
@ -97,119 +95,4 @@ describe(SearchService.name, () => {
expect(result).toEqual(expectedResponse);
});
});
describe('search', () => {
it('should throw an error if query is missing', async () => {
await expect(sut.search(authStub.user1, { q: '' })).rejects.toThrow('Missing query');
});
it('should search by metadata if `clip` option is false', async () => {
const dto: SearchDto = { q: 'test query', clip: false };
assetMock.searchMetadata.mockResolvedValueOnce([assetStub.image]);
partnerMock.getAll.mockResolvedValueOnce([]);
const expectedResponse = {
albums: {
total: 0,
count: 0,
items: [],
facets: [],
},
assets: {
total: 1,
count: 1,
items: [mapAsset(assetStub.image)],
facets: [],
nextPage: null,
},
};
const result = await sut.search(authStub.user1, dto);
expect(result).toEqual(expectedResponse);
expect(assetMock.searchMetadata).toHaveBeenCalledWith(dto.q, [authStub.user1.user.id], { numResults: 250 });
expect(searchMock.searchSmart).not.toHaveBeenCalled();
});
it('should search archived photos if `withArchived` option is true', async () => {
const dto: SearchDto = { q: 'test query', clip: true, withArchived: true };
const embedding = [1, 2, 3];
searchMock.searchSmart.mockResolvedValueOnce({ items: [assetStub.image], hasNextPage: false });
machineMock.encodeText.mockResolvedValueOnce(embedding);
partnerMock.getAll.mockResolvedValueOnce([]);
const expectedResponse = {
albums: {
total: 0,
count: 0,
items: [],
facets: [],
},
assets: {
total: 1,
count: 1,
items: [mapAsset(assetStub.image)],
facets: [],
nextPage: null,
},
};
const result = await sut.search(authStub.user1, dto);
expect(result).toEqual(expectedResponse);
expect(searchMock.searchSmart).toHaveBeenCalledWith(
{ page: 1, size: 100 },
{
userIds: [authStub.user1.user.id],
embedding,
withArchived: true,
},
);
expect(assetMock.searchMetadata).not.toHaveBeenCalled();
});
it('should search by CLIP if `clip` option is true', async () => {
const dto: SearchDto = { q: 'test query', clip: true };
const embedding = [1, 2, 3];
searchMock.searchSmart.mockResolvedValueOnce({ items: [assetStub.image], hasNextPage: false });
machineMock.encodeText.mockResolvedValueOnce(embedding);
partnerMock.getAll.mockResolvedValueOnce([]);
const expectedResponse = {
albums: {
total: 0,
count: 0,
items: [],
facets: [],
},
assets: {
total: 1,
count: 1,
items: [mapAsset(assetStub.image)],
facets: [],
nextPage: null,
},
};
const result = await sut.search(authStub.user1, dto);
expect(result).toEqual(expectedResponse);
expect(searchMock.searchSmart).toHaveBeenCalledWith(
{ page: 1, size: 100 },
{
userIds: [authStub.user1.user.id],
embedding,
withArchived: false,
},
);
expect(assetMock.searchMetadata).not.toHaveBeenCalled();
});
it.each([
{ key: SystemConfigKey.MACHINE_LEARNING_ENABLED },
{ key: SystemConfigKey.MACHINE_LEARNING_CLIP_ENABLED },
])('should throw an error if clip is requested but disabled', async ({ key }) => {
const dto: SearchDto = { q: 'test query', clip: true };
configMock.load.mockResolvedValue([{ key, value: false }]);
await expect(sut.search(authStub.user1, dto)).rejects.toThrow('Smart search is not enabled');
});
});
});

View file

@ -6,7 +6,6 @@ import { PersonResponseDto } from 'src/dtos/person.dto';
import {
MetadataSearchDto,
PlacesResponseDto,
SearchDto,
SearchPeopleDto,
SearchPlacesDto,
SearchResponseDto,
@ -23,7 +22,7 @@ import { IMachineLearningRepository } from 'src/interfaces/machine-learning.inte
import { IMetadataRepository } from 'src/interfaces/metadata.interface';
import { IPartnerRepository } from 'src/interfaces/partner.interface';
import { IPersonRepository } from 'src/interfaces/person.interface';
import { ISearchRepository, SearchExploreItem, SearchStrategy } from 'src/interfaces/search.interface';
import { ISearchRepository, SearchExploreItem } from 'src/interfaces/search.interface';
import { ISystemConfigRepository } from 'src/interfaces/system-config.interface';
@Injectable()
@ -145,60 +144,6 @@ export class SearchService {
}
}
// TODO: remove after implementing new search filters
/** @deprecated */
async search(auth: AuthDto, dto: SearchDto): Promise<SearchResponseDto> {
await this.configCore.requireFeature(FeatureFlag.SEARCH);
const { machineLearning } = await this.configCore.getConfig();
const query = dto.q || dto.query;
if (!query) {
throw new Error('Missing query');
}
let strategy = SearchStrategy.TEXT;
if (dto.smart || dto.clip) {
await this.configCore.requireFeature(FeatureFlag.SMART_SEARCH);
strategy = SearchStrategy.SMART;
}
const userIds = await this.getUserIdsToSearch(auth);
const page = dto.page ?? 1;
let nextPage: string | null = null;
let assets: AssetEntity[] = [];
switch (strategy) {
case SearchStrategy.SMART: {
const embedding = await this.machineLearning.encodeText(
machineLearning.url,
{ text: query },
machineLearning.clip,
);
const { hasNextPage, items } = await this.searchRepository.searchSmart(
{ page, size: dto.size || 100 },
{
userIds,
embedding,
withArchived: !!dto.withArchived,
},
);
if (hasNextPage) {
nextPage = (page + 1).toString();
}
assets = items;
break;
}
case SearchStrategy.TEXT: {
assets = await this.assetRepository.searchMetadata(query, userIds, { numResults: dto.size || 250 });
}
default: {
break;
}
}
return this.mapResponse(assets, nextPage);
}
private async getUserIdsToSearch(auth: AuthDto): Promise<string[]> {
const userIds: string[] = [auth.user.id];
const partners = await this.partnerRepository.getAll(auth.user.id);