diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index 187d8e8fa9..a0872d6f97 100644 Binary files a/mobile/openapi/README.md and b/mobile/openapi/README.md differ diff --git a/mobile/openapi/doc/AssetApi.md b/mobile/openapi/doc/AssetApi.md index 826e91a690..f335ca5868 100644 Binary files a/mobile/openapi/doc/AssetApi.md and b/mobile/openapi/doc/AssetApi.md differ diff --git a/mobile/openapi/doc/SearchApi.md b/mobile/openapi/doc/SearchApi.md index e4ab9ecfd3..b55bd067c2 100644 Binary files a/mobile/openapi/doc/SearchApi.md and b/mobile/openapi/doc/SearchApi.md differ diff --git a/mobile/openapi/lib/api/asset_api.dart b/mobile/openapi/lib/api/asset_api.dart index c6031f6bda..d477cf6bc9 100644 Binary files a/mobile/openapi/lib/api/asset_api.dart and b/mobile/openapi/lib/api/asset_api.dart differ diff --git a/mobile/openapi/lib/api/search_api.dart b/mobile/openapi/lib/api/search_api.dart index 386a2f3536..15654fc066 100644 Binary files a/mobile/openapi/lib/api/search_api.dart and b/mobile/openapi/lib/api/search_api.dart differ diff --git a/mobile/openapi/test/asset_api_test.dart b/mobile/openapi/test/asset_api_test.dart index e64d3acf5e..7c2d71baca 100644 Binary files a/mobile/openapi/test/asset_api_test.dart and b/mobile/openapi/test/asset_api_test.dart differ diff --git a/mobile/openapi/test/search_api_test.dart b/mobile/openapi/test/search_api_test.dart index 801c97a180..a00b1290f4 100644 Binary files a/mobile/openapi/test/search_api_test.dart and b/mobile/openapi/test/search_api_test.dart differ diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index d1e0b6aace..e831c6f3e7 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -1823,423 +1823,6 @@ ] } }, - "/assets": { - "get": { - "deprecated": true, - "operationId": "searchAssets", - "parameters": [ - { - "name": "checksum", - "required": false, - "in": "query", - "schema": { - "type": "string" - } - }, - { - "name": "city", - "required": false, - "in": "query", - "schema": { - "type": "string" - } - }, - { - "name": "country", - "required": false, - "in": "query", - "schema": { - "type": "string" - } - }, - { - "name": "createdAfter", - "required": false, - "in": "query", - "schema": { - "format": "date-time", - "type": "string" - } - }, - { - "name": "createdBefore", - "required": false, - "in": "query", - "schema": { - "format": "date-time", - "type": "string" - } - }, - { - "name": "deviceAssetId", - "required": false, - "in": "query", - "schema": { - "type": "string" - } - }, - { - "name": "deviceId", - "required": false, - "in": "query", - "schema": { - "type": "string" - } - }, - { - "name": "encodedVideoPath", - "required": false, - "in": "query", - "schema": { - "type": "string" - } - }, - { - "name": "id", - "required": false, - "in": "query", - "schema": { - "format": "uuid", - "type": "string" - } - }, - { - "name": "isArchived", - "required": false, - "in": "query", - "schema": { - "type": "boolean" - } - }, - { - "name": "isEncoded", - "required": false, - "in": "query", - "schema": { - "type": "boolean" - } - }, - { - "name": "isExternal", - "required": false, - "in": "query", - "schema": { - "type": "boolean" - } - }, - { - "name": "isFavorite", - "required": false, - "in": "query", - "schema": { - "type": "boolean" - } - }, - { - "name": "isMotion", - "required": false, - "in": "query", - "schema": { - "type": "boolean" - } - }, - { - "name": "isNotInAlbum", - "required": false, - "in": "query", - "schema": { - "type": "boolean" - } - }, - { - "name": "isOffline", - "required": false, - "in": "query", - "schema": { - "type": "boolean" - } - }, - { - "name": "isReadOnly", - "required": false, - "in": "query", - "schema": { - "type": "boolean" - } - }, - { - "name": "isVisible", - "required": false, - "in": "query", - "schema": { - "type": "boolean" - } - }, - { - "name": "lensModel", - "required": false, - "in": "query", - "schema": { - "type": "string" - } - }, - { - "name": "libraryId", - "required": false, - "in": "query", - "schema": { - "format": "uuid", - "type": "string" - } - }, - { - "name": "make", - "required": false, - "in": "query", - "schema": { - "type": "string" - } - }, - { - "name": "model", - "required": false, - "in": "query", - "schema": { - "type": "string" - } - }, - { - "name": "order", - "required": false, - "in": "query", - "schema": { - "$ref": "#/components/schemas/AssetOrder" - } - }, - { - "name": "originalFileName", - "required": false, - "in": "query", - "schema": { - "type": "string" - } - }, - { - "name": "originalPath", - "required": false, - "in": "query", - "schema": { - "type": "string" - } - }, - { - "name": "page", - "required": false, - "in": "query", - "schema": { - "minimum": 1, - "type": "number" - } - }, - { - "name": "personIds", - "required": false, - "in": "query", - "schema": { - "format": "uuid", - "type": "array", - "items": { - "type": "string" - } - } - }, - { - "name": "previewPath", - "required": false, - "in": "query", - "schema": { - "type": "string" - } - }, - { - "name": "resizePath", - "required": false, - "in": "query", - "deprecated": true, - "schema": { - "type": "string" - } - }, - { - "name": "size", - "required": false, - "in": "query", - "schema": { - "minimum": 1, - "maximum": 1000, - "type": "number" - } - }, - { - "name": "state", - "required": false, - "in": "query", - "schema": { - "type": "string" - } - }, - { - "name": "takenAfter", - "required": false, - "in": "query", - "schema": { - "format": "date-time", - "type": "string" - } - }, - { - "name": "takenBefore", - "required": false, - "in": "query", - "schema": { - "format": "date-time", - "type": "string" - } - }, - { - "name": "thumbnailPath", - "required": false, - "in": "query", - "schema": { - "type": "string" - } - }, - { - "name": "trashedAfter", - "required": false, - "in": "query", - "schema": { - "format": "date-time", - "type": "string" - } - }, - { - "name": "trashedBefore", - "required": false, - "in": "query", - "schema": { - "format": "date-time", - "type": "string" - } - }, - { - "name": "type", - "required": false, - "in": "query", - "schema": { - "$ref": "#/components/schemas/AssetTypeEnum" - } - }, - { - "name": "updatedAfter", - "required": false, - "in": "query", - "schema": { - "format": "date-time", - "type": "string" - } - }, - { - "name": "updatedBefore", - "required": false, - "in": "query", - "schema": { - "format": "date-time", - "type": "string" - } - }, - { - "name": "webpPath", - "required": false, - "in": "query", - "deprecated": true, - "schema": { - "type": "string" - } - }, - { - "name": "withArchived", - "required": false, - "in": "query", - "schema": { - "default": false, - "type": "boolean" - } - }, - { - "name": "withDeleted", - "required": false, - "in": "query", - "schema": { - "type": "boolean" - } - }, - { - "name": "withExif", - "required": false, - "in": "query", - "schema": { - "type": "boolean" - } - }, - { - "name": "withPeople", - "required": false, - "in": "query", - "schema": { - "type": "boolean" - } - }, - { - "name": "withStacked", - "required": false, - "in": "query", - "schema": { - "type": "boolean" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "items": { - "$ref": "#/components/schemas/AssetResponseDto" - }, - "type": "array" - } - } - }, - "description": "" - } - }, - "security": [ - { - "bearer": [] - }, - { - "cookie": [] - }, - { - "api_key": [] - } - ], - "tags": [ - "Asset" - ] - } - }, "/audit/deletes": { "get": { "operationId": "getAuditDeletes", @@ -4383,130 +3966,6 @@ ] } }, - "/search": { - "get": { - "deprecated": true, - "operationId": "search", - "parameters": [ - { - "name": "clip", - "required": false, - "in": "query", - "deprecated": true, - "schema": { - "type": "boolean" - } - }, - { - "name": "motion", - "required": false, - "in": "query", - "schema": { - "type": "boolean" - } - }, - { - "name": "page", - "required": false, - "in": "query", - "schema": { - "minimum": 1, - "type": "number" - } - }, - { - "name": "q", - "required": false, - "in": "query", - "schema": { - "type": "string" - } - }, - { - "name": "query", - "required": false, - "in": "query", - "schema": { - "type": "string" - } - }, - { - "name": "recent", - "required": false, - "in": "query", - "schema": { - "type": "boolean" - } - }, - { - "name": "size", - "required": false, - "in": "query", - "schema": { - "minimum": 1, - "maximum": 1000, - "type": "number" - } - }, - { - "name": "smart", - "required": false, - "in": "query", - "schema": { - "type": "boolean" - } - }, - { - "name": "type", - "required": false, - "in": "query", - "schema": { - "enum": [ - "IMAGE", - "VIDEO", - "AUDIO", - "OTHER" - ], - "type": "string" - } - }, - { - "name": "withArchived", - "required": false, - "in": "query", - "schema": { - "type": "boolean" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SearchResponseDto" - } - } - }, - "description": "" - } - }, - "security": [ - { - "bearer": [] - }, - { - "cookie": [] - }, - { - "api_key": [] - } - ], - "tags": [ - "Search" - ] - } - }, "/search/cities": { "get": { "operationId": "getAssetsByCity", diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index 2536afe598..378f77c54a 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -600,31 +600,6 @@ export type FileChecksumResponseDto = { export type FileReportFixDto = { items: FileReportItemDto[]; }; -export type SearchFacetCountResponseDto = { - count: number; - value: string; -}; -export type SearchFacetResponseDto = { - counts: SearchFacetCountResponseDto[]; - fieldName: string; -}; -export type SearchAlbumResponseDto = { - count: number; - facets: SearchFacetResponseDto[]; - items: AlbumResponseDto[]; - total: number; -}; -export type SearchAssetResponseDto = { - count: number; - facets: SearchFacetResponseDto[]; - items: AssetResponseDto[]; - nextPage: string | null; - total: number; -}; -export type SearchResponseDto = { - albums: SearchAlbumResponseDto; - assets: SearchAssetResponseDto; -}; export type SearchExploreItem = { data: AssetResponseDto; value: string; @@ -680,6 +655,31 @@ export type MetadataSearchDto = { withPeople?: boolean; withStacked?: boolean; }; +export type SearchFacetCountResponseDto = { + count: number; + value: string; +}; +export type SearchFacetResponseDto = { + counts: SearchFacetCountResponseDto[]; + fieldName: string; +}; +export type SearchAlbumResponseDto = { + count: number; + facets: SearchFacetResponseDto[]; + items: AlbumResponseDto[]; + total: number; +}; +export type SearchAssetResponseDto = { + count: number; + facets: SearchFacetResponseDto[]; + items: AssetResponseDto[]; + nextPage: string | null; + total: number; +}; +export type SearchResponseDto = { + albums: SearchAlbumResponseDto; + assets: SearchAssetResponseDto; +}; export type PlacesResponseDto = { admin1name?: string; admin2name?: string; @@ -1530,106 +1530,6 @@ export function updateAsset({ id, updateAssetDto }: { body: updateAssetDto }))); } -export function searchAssets({ 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, updatedAfter, updatedBefore, webpPath, withArchived, withDeleted, withExif, withPeople, withStacked }: { - checksum?: string; - city?: string; - country?: string; - createdAfter?: string; - createdBefore?: string; - deviceAssetId?: string; - deviceId?: string; - encodedVideoPath?: string; - id?: string; - isArchived?: boolean; - isEncoded?: boolean; - isExternal?: boolean; - isFavorite?: boolean; - isMotion?: boolean; - isNotInAlbum?: boolean; - isOffline?: boolean; - isReadOnly?: boolean; - isVisible?: boolean; - lensModel?: string; - libraryId?: string; - make?: string; - model?: string; - order?: AssetOrder; - originalFileName?: string; - originalPath?: string; - page?: number; - personIds?: string[]; - previewPath?: string; - resizePath?: string; - size?: number; - state?: string; - takenAfter?: string; - takenBefore?: string; - thumbnailPath?: string; - trashedAfter?: string; - trashedBefore?: string; - $type?: AssetTypeEnum; - updatedAfter?: string; - updatedBefore?: string; - webpPath?: string; - withArchived?: boolean; - withDeleted?: boolean; - withExif?: boolean; - withPeople?: boolean; - withStacked?: boolean; -}, opts?: Oazapfts.RequestOpts) { - return oazapfts.ok(oazapfts.fetchJson<{ - status: 200; - data: AssetResponseDto[]; - }>(`/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 8e446d23f9..9db27998d2 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<AssetResponseDto[]> { - 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 bd10c41a43..df1a44a157 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 eaf45be293..ce0d0f646d 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<SearchResponseDto> { - return this.service.search(auth, dto); - } - @Post('metadata') @HttpCode(HttpStatus.OK) searchMetadata(@Auth() auth: AuthDto, @Body() dto: MetadataSearchDto): Promise<SearchResponseDto> { diff --git a/server/src/dtos/search.dto.ts b/server/src/dtos/search.dto.ts index d96ce0d98a..3304aae8cf 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 9c2ebe3e73..fb6345df7c 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<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[]>; } diff --git a/server/src/interfaces/search.interface.ts b/server/src/interfaces/search.interface.ts index 771b23e9c9..14c70631d6 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<T> { /** total matches */ total: number; diff --git a/server/src/queries/asset.repository.sql b/server/src/queries/asset.repository.sql index 86e9796faa..81dce80d0f 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 ddc666edd3..e2ec6b3274 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<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: [ { diff --git a/server/src/services/search.service.spec.ts b/server/src/services/search.service.spec.ts index a81ea87973..bf4cd7c679 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 b8e9f13fa6..d2636b91cf 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<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); diff --git a/server/test/repositories/asset.repository.mock.ts b/server/test/repositories/asset.repository.mock.ts index 694fc87cc2..f09d6b619e 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<IAssetRepository> => { softDeleteAll: vitest.fn(), getAssetIdByCity: vitest.fn(), getAssetIdByTag: vitest.fn(), - searchMetadata: vitest.fn(), getAllForUserFullSync: vitest.fn(), getChangedDeltaSync: vitest.fn(), };