From 5a49de5592cfa5e40a85809de0b46a5cd3cc0123 Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Sat, 27 Apr 2024 08:57:39 -0400 Subject: [PATCH] chore(server): remove old asset search (#9104) * chore(server): remove old asset search * chore: remove more unused search code --- mobile/openapi/README.md | Bin 26660 -> 26502 bytes mobile/openapi/doc/AssetApi.md | Bin 46195 -> 39094 bytes mobile/openapi/doc/SearchApi.md | Bin 17401 -> 14676 bytes mobile/openapi/lib/api/asset_api.dart | Bin 47965 -> 35922 bytes mobile/openapi/lib/api/search_api.dart | Bin 16129 -> 12851 bytes mobile/openapi/test/asset_api_test.dart | Bin 4769 -> 3820 bytes mobile/openapi/test/search_api_test.dart | Bin 1752 -> 1520 bytes open-api/immich-openapi-specs.json | 541 ------------------ open-api/typescript-sdk/src/fetch-client.ts | 180 +----- server/src/controllers/asset.controller.ts | 21 +- server/src/controllers/index.ts | 3 +- server/src/controllers/search.controller.ts | 9 +- server/src/dtos/search.dto.ts | 47 -- server/src/interfaces/asset.interface.ts | 5 - server/src/interfaces/search.interface.ts | 23 - server/src/queries/asset.repository.sql | 31 - server/src/repositories/asset.repository.ts | 90 --- server/src/services/search.service.spec.ts | 117 ---- server/src/services/search.service.ts | 57 +- .../repositories/asset.repository.mock.ts | 1 - 20 files changed, 30 insertions(+), 1095 deletions(-) diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index 187d8e8fa9ce92f965805d85bc763c10b1113140..a0872d6f97f5cdad7ee77418ca0b86d91e93cbfc 100644 GIT binary patch delta 19 bcmZ2-fwAp87o83j%i*0t$d14I!TPFyW delta 85 zcmZoW&$#3Q7EQv+Q8IuhKrMZh!LCnPB;?$DK8-;}#izhn@iERETvQccZownHI k1KMoDnhG@vT3YU|AzE4r`VjS#1GGdZ>9B2Hr2WJi04h8kZ2$lO diff --git a/mobile/openapi/doc/AssetApi.md b/mobile/openapi/doc/AssetApi.md index 826e91a69066ca11d8cc5d44dc27c3d1a3622578..f335ca58681d1b31df4c5bdf3927c294b34345fa 100644 GIT binary patch delta 19 bcmezTf@#}ErVX=LHs4`UO50r7_DdH4WB&<> delta 2401 zcmeHI&u<$=6joZhF{!GiB&1N>uuhY1wcXUMK%!A;j8R-A6zWz*AQYmo@s8tZ*1Oj3 zIB6P<`r`Rz<~IVDxo>lq1(zMsvJLLSUsQ$l z`6eT~rZ5<;U57rt*P1^$v7ff=$*&bCxGWD&o? zl#P~cL;}ujFBt;X^5f%^tqyM+qSbA$`40C~l?a`vm0VQpCD`qbEWKbXY_YYDf^Tur zO79501?DlhY1=@jcAYnsb(sYn5B7u6tHNn)?EF4AlN-|mGtL{@l=n^@KGeiJpdn%g zW{J_maa0Wz6UxHeaGaB)_4&OpZFztCy!`X_wPTM@7RnwEVZHopYeBx;JD#U0%Svyu zpoc80y;t|C$%0uN$&40U`B9IL>+wQ!PutY>m)^zwIA%!T55~sSPHF5I#_?nPbrmNb zK>VChE*nJaIx@rfz!=ehL52WUQ$&*n+m!~Yamfw_L>E(I*59s^r6dP99%;hJfw<>@W!#bLDR3SSn@U$h-ZMTF$`D${A+A z>wj2CBnvEPS+EL2;igOM*@Uu1+O(LLa@{U z2xUYkUL0vOxOiUa3*_;{N=C16hH<}KBy+b4dH@Ko_s(}R0F~X*`Q#n zV4-JdVy&RBuaH-o3v?e;Zy`{oaB_l}xG+R9*cBi#kZVBVJP>h+vp}{$O$E6Rr>S6P zgA@xubc5UvR1Bmh$BRh_K?IS#161osrI4AdB_BSSkzZkQqLB0?1;6h5^tx9iS{I p00@}?2?wACHlPNO?`0%Zcu@SUs9`*LzWLe7D-GUmwwDr>2LQ!H#y0=} diff --git a/mobile/openapi/lib/api/asset_api.dart b/mobile/openapi/lib/api/asset_api.dart index c6031f6bdaa6a607b32ed762bb7b8e3e7d9d1489..d477cf6bc904fb19183e9c69130b72882bc397ad 100644 GIT binary patch delta 14 Vcmccnjp@=1rVX}nn>~6(^#C`&23P<9 delta 5255 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 diff --git a/mobile/openapi/test/search_api_test.dart b/mobile/openapi/test/search_api_test.dart index 801c97a180a4e00439a64bc9941d6ae0cfa2da25..a00b1290f44a5aace2d2a898397b30cf9ac5cf55 100644 GIT binary patch delta 12 Tcmcb?`+$yLFRgmYK5fy{2YbkoXi3p2s1aoBr`uxM(`/assets${QS.query(QS.explode({ - checksum, - city, - country, - createdAfter, - createdBefore, - deviceAssetId, - deviceId, - encodedVideoPath, - id, - isArchived, - isEncoded, - isExternal, - isFavorite, - isMotion, - isNotInAlbum, - isOffline, - isReadOnly, - isVisible, - lensModel, - libraryId, - make, - model, - order, - originalFileName, - originalPath, - page, - personIds, - previewPath, - resizePath, - size, - state, - takenAfter, - takenBefore, - thumbnailPath, - trashedAfter, - trashedBefore, - "type": $type, - updatedAfter, - updatedBefore, - webpPath, - withArchived, - withDeleted, - withExif, - withPeople, - withStacked - }))}`, { - ...opts - })); -} export function getAuditDeletes({ after, entityType, userId }: { after: string; entityType: EntityType; @@ -2201,36 +2101,6 @@ export function fixAuditFiles({ fileReportFixDto }: { body: fileReportFixDto }))); } -export function search({ clip, motion, page, q, query, recent, size, smart, $type, withArchived }: { - clip?: boolean; - motion?: boolean; - page?: number; - q?: string; - query?: string; - recent?: boolean; - size?: number; - smart?: boolean; - $type?: "IMAGE" | "VIDEO" | "AUDIO" | "OTHER"; - withArchived?: boolean; -}, opts?: Oazapfts.RequestOpts) { - return oazapfts.ok(oazapfts.fetchJson<{ - status: 200; - data: SearchResponseDto; - }>(`/search${QS.query(QS.explode({ - clip, - motion, - page, - q, - query, - recent, - size, - smart, - "type": $type, - withArchived - }))}`, { - ...opts - })); -} export function getAssetsByCity(opts?: Oazapfts.RequestOpts) { return oazapfts.ok(oazapfts.fetchJson<{ status: 200; diff --git a/server/src/controllers/asset.controller.ts b/server/src/controllers/asset.controller.ts index 8e446d23f..9db27998d 100644 --- a/server/src/controllers/asset.controller.ts +++ b/server/src/controllers/asset.controller.ts @@ -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 { - const { - assets: { items }, - } = await this.searchService.searchMetadata(auth, dto); - return items; - } -} - @ApiTags('Asset') @Controller(Route.ASSET) @Authenticated() diff --git a/server/src/controllers/index.ts b/server/src/controllers/index.ts index bd10c41a4..df1a44a15 100644 --- a/server/src/controllers/index.ts +++ b/server/src/controllers/index.ts @@ -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, diff --git a/server/src/controllers/search.controller.ts b/server/src/controllers/search.controller.ts index eaf45be29..ce0d0f646 100644 --- a/server/src/controllers/search.controller.ts +++ b/server/src/controllers/search.controller.ts @@ -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 { - return this.service.search(auth, dto); - } - @Post('metadata') @HttpCode(HttpStatus.OK) searchMetadata(@Auth() auth: AuthDto, @Body() dto: MetadataSearchDto): Promise { diff --git a/server/src/dtos/search.dto.ts b/server/src/dtos/search.dto.ts index d96ce0d98..3304aae8c 100644 --- a/server/src/dtos/search.dto.ts +++ b/server/src/dtos/search.dto.ts @@ -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() diff --git a/server/src/interfaces/asset.interface.ts b/server/src/interfaces/asset.interface.ts index 9c2ebe3e7..fb6345df7 100644 --- a/server/src/interfaces/asset.interface.ts +++ b/server/src/interfaces/asset.interface.ts @@ -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): Promise; getAssetIdByCity(userId: string, options: AssetExploreFieldOptions): Promise>; getAssetIdByTag(userId: string, options: AssetExploreFieldOptions): Promise>; - searchMetadata(query: string, userIds: string[], options: MetadataSearchOptions): Promise; getAllForUserFullSync(options: AssetFullSyncOptions): Promise; getChangedDeltaSync(options: AssetDeltaSyncOptions): Promise; } diff --git a/server/src/interfaces/search.interface.ts b/server/src/interfaces/search.interface.ts index 771b23e9c..14c70631d 100644 --- a/server/src/interfaces/search.interface.ts +++ b/server/src/interfaces/search.interface.ts @@ -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 { /** total matches */ total: number; diff --git a/server/src/queries/asset.repository.sql b/server/src/queries/asset.repository.sql index 86e9796fa..81dce80d0 100644 --- a/server/src/queries/asset.repository.sql +++ b/server/src/queries/asset.repository.sql @@ -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", diff --git a/server/src/repositories/asset.repository.ts b/server/src/repositories/asset.repository.ts index ddc666edd..e2ec6b327 100644 --- a/server/src/repositories/asset.repository.ts +++ b/server/src/repositories/asset.repository.ts @@ -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 { - 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: [ { diff --git a/server/src/services/search.service.spec.ts b/server/src/services/search.service.spec.ts index a81ea8797..bf4cd7c67 100644 --- a/server/src/services/search.service.spec.ts +++ b/server/src/services/search.service.spec.ts @@ -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'); - }); - }); }); diff --git a/server/src/services/search.service.ts b/server/src/services/search.service.ts index b8e9f13fa..d2636b91c 100644 --- a/server/src/services/search.service.ts +++ b/server/src/services/search.service.ts @@ -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 { - 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 { const userIds: string[] = [auth.user.id]; const partners = await this.partnerRepository.getAll(auth.user.id); diff --git a/server/test/repositories/asset.repository.mock.ts b/server/test/repositories/asset.repository.mock.ts index 694fc87cc..f09d6b619 100644 --- a/server/test/repositories/asset.repository.mock.ts +++ b/server/test/repositories/asset.repository.mock.ts @@ -35,7 +35,6 @@ export const newAssetRepositoryMock = (): Mocked => { softDeleteAll: vitest.fn(), getAssetIdByCity: vitest.fn(), getAssetIdByTag: vitest.fn(), - searchMetadata: vitest.fn(), getAllForUserFullSync: vitest.fn(), getChangedDeltaSync: vitest.fn(), };