diff --git a/mobile/lib/modules/search/models/search_result_page_state.model.dart b/mobile/lib/modules/search/models/search_result_page_state.model.dart index 51d557b8c3..f42f2e9cb6 100644 --- a/mobile/lib/modules/search/models/search_result_page_state.model.dart +++ b/mobile/lib/modules/search/models/search_result_page_state.model.dart @@ -5,14 +5,14 @@ class SearchResultPageState { final bool isLoading; final bool isSuccess; final bool isError; - final bool isClip; + final bool isSmart; final List searchResult; SearchResultPageState({ required this.isLoading, required this.isSuccess, required this.isError, - required this.isClip, + required this.isSmart, required this.searchResult, }); @@ -20,21 +20,21 @@ class SearchResultPageState { bool? isLoading, bool? isSuccess, bool? isError, - bool? isClip, + bool? isSmart, List? searchResult, }) { return SearchResultPageState( isLoading: isLoading ?? this.isLoading, isSuccess: isSuccess ?? this.isSuccess, isError: isError ?? this.isError, - isClip: isClip ?? this.isClip, + isSmart: isSmart ?? this.isSmart, searchResult: searchResult ?? this.searchResult, ); } @override String toString() { - return 'SearchresultPageState(isLoading: $isLoading, isSuccess: $isSuccess, isError: $isError, isClip: $isClip, searchResult: $searchResult)'; + return 'SearchresultPageState(isLoading: $isLoading, isSuccess: $isSuccess, isError: $isError, isSmart: $isSmart, searchResult: $searchResult)'; } @override @@ -46,7 +46,7 @@ class SearchResultPageState { other.isLoading == isLoading && other.isSuccess == isSuccess && other.isError == isError && - other.isClip == isClip && + other.isSmart == isSmart && listEquals(other.searchResult, searchResult); } @@ -55,7 +55,7 @@ class SearchResultPageState { return isLoading.hashCode ^ isSuccess.hashCode ^ isError.hashCode ^ - isClip.hashCode ^ + isSmart.hashCode ^ searchResult.hashCode; } } diff --git a/mobile/lib/modules/search/providers/search_result_page.provider.dart b/mobile/lib/modules/search/providers/search_result_page.provider.dart index a481f291ce..e220cc69f0 100644 --- a/mobile/lib/modules/search/providers/search_result_page.provider.dart +++ b/mobile/lib/modules/search/providers/search_result_page.provider.dart @@ -14,13 +14,13 @@ class SearchResultPageNotifier extends StateNotifier { isError: false, isLoading: true, isSuccess: false, - isClip: false, + isSmart: false, ), ); final SearchService _searchService; - Future search(String searchTerm, {bool clipEnable = true}) async { + Future search(String searchTerm, {bool smartSearch = true}) async { state = state.copyWith( searchResult: [], isError: false, @@ -28,10 +28,8 @@ class SearchResultPageNotifier extends StateNotifier { isSuccess: false, ); - List? assets = await _searchService.searchAsset( - searchTerm, - clipEnable: clipEnable, - ); + List? assets = + await _searchService.searchAsset(searchTerm, smartSearch: smartSearch); if (assets != null) { state = state.copyWith( @@ -39,7 +37,7 @@ class SearchResultPageNotifier extends StateNotifier { isError: false, isLoading: false, isSuccess: true, - isClip: clipEnable, + isSmart: smartSearch, ); } else { state = state.copyWith( @@ -47,7 +45,7 @@ class SearchResultPageNotifier extends StateNotifier { isError: true, isLoading: false, isSuccess: false, - isClip: clipEnable, + isSmart: smartSearch, ); } } @@ -63,7 +61,7 @@ final searchRenderListProvider = Provider((ref) { final result = ref.watch(searchResultPageProvider); return ref.watch( renderListProviderWithGrouping( - (result.searchResult, result.isClip ? GroupAssetsBy.none : null), + (result.searchResult, result.isSmart ? GroupAssetsBy.none : null), ), ); }); diff --git a/mobile/lib/modules/search/services/search.service.dart b/mobile/lib/modules/search/services/search.service.dart index 746bb455fa..35249dec5d 100644 --- a/mobile/lib/modules/search/services/search.service.dart +++ b/mobile/lib/modules/search/services/search.service.dart @@ -31,13 +31,13 @@ class SearchService { Future?> searchAsset( String searchTerm, { - bool clipEnable = true, + bool smartSearch = true, }) async { // TODO search in local DB: 1. when offline, 2. to find local assets try { final SearchResponseDto? results = await _apiService.searchApi.search( query: searchTerm, - clip: clipEnable, + smart: smartSearch, ); if (results == null) { return null; diff --git a/mobile/lib/modules/search/views/search_result_page.dart b/mobile/lib/modules/search/views/search_result_page.dart index 3645a81f1b..97df5f10c7 100644 --- a/mobile/lib/modules/search/views/search_result_page.dart +++ b/mobile/lib/modules/search/views/search_result_page.dart @@ -11,17 +11,17 @@ import 'package:immich_mobile/shared/ui/asset_grid/multiselect_grid.dart'; import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart'; class SearchType { - SearchType({required this.isClip, required this.searchTerm}); + SearchType({required this.isSmart, required this.searchTerm}); - final bool isClip; + final bool isSmart; final String searchTerm; } SearchType _getSearchType(String searchTerm) { if (searchTerm.startsWith('m:')) { - return SearchType(isClip: false, searchTerm: searchTerm.substring(2)); + return SearchType(isSmart: false, searchTerm: searchTerm.substring(2)); } else { - return SearchType(isClip: true, searchTerm: searchTerm); + return SearchType(isSmart: true, searchTerm: searchTerm); } } @@ -52,7 +52,7 @@ class SearchResultPage extends HookConsumerWidget { Duration.zero, () => ref .read(searchResultPageProvider.notifier) - .search(searchType.searchTerm, clipEnable: searchType.isClip), + .search(searchType.searchTerm, smartSearch: searchType.isSmart), ); return () => searchFocusNode?.dispose(); }, @@ -67,7 +67,7 @@ class SearchResultPage extends HookConsumerWidget { var searchType = _getSearchType(newSearchTerm); return ref .watch(searchResultPageProvider.notifier) - .search(searchType.searchTerm, clipEnable: searchType.isClip); + .search(searchType.searchTerm, smartSearch: searchType.isSmart); } buildTextField() { diff --git a/mobile/openapi/doc/SearchApi.md b/mobile/openapi/doc/SearchApi.md index 1cd5f60a7c..dcf453b55d 100644 Binary files a/mobile/openapi/doc/SearchApi.md and b/mobile/openapi/doc/SearchApi.md differ diff --git a/mobile/openapi/doc/ServerFeaturesDto.md b/mobile/openapi/doc/ServerFeaturesDto.md index a149d8f484..a1a5be4079 100644 Binary files a/mobile/openapi/doc/ServerFeaturesDto.md and b/mobile/openapi/doc/ServerFeaturesDto.md differ diff --git a/mobile/openapi/lib/api/search_api.dart b/mobile/openapi/lib/api/search_api.dart index d3ce301508..e2bde2a17f 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/lib/model/server_features_dto.dart b/mobile/openapi/lib/model/server_features_dto.dart index e3fd530b17..b3ff58f164 100644 Binary files a/mobile/openapi/lib/model/server_features_dto.dart and b/mobile/openapi/lib/model/server_features_dto.dart differ diff --git a/mobile/openapi/test/search_api_test.dart b/mobile/openapi/test/search_api_test.dart index 50406e90d1..769ad31943 100644 Binary files a/mobile/openapi/test/search_api_test.dart and b/mobile/openapi/test/search_api_test.dart differ diff --git a/mobile/openapi/test/server_features_dto_test.dart b/mobile/openapi/test/server_features_dto_test.dart index ac92a105f7..67cc029d4d 100644 Binary files a/mobile/openapi/test/server_features_dto_test.dart and b/mobile/openapi/test/server_features_dto_test.dart differ diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index bd0b99b9c0..a593ac894a 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -4684,6 +4684,8 @@ "name": "clip", "required": false, "in": "query", + "description": "@deprecated", + "deprecated": true, "schema": { "type": "boolean" } @@ -4720,6 +4722,14 @@ "type": "boolean" } }, + { + "name": "smart", + "required": false, + "in": "query", + "schema": { + "type": "boolean" + } + }, { "name": "type", "required": false, @@ -8919,9 +8929,6 @@ }, "ServerFeaturesDto": { "properties": { - "clipEncode": { - "type": "boolean" - }, "configFile": { "type": "boolean" }, @@ -8949,12 +8956,14 @@ "sidecar": { "type": "boolean" }, + "smartSearch": { + "type": "boolean" + }, "trash": { "type": "boolean" } }, "required": [ - "clipEncode", "configFile", "facialRecognition", "map", @@ -8964,6 +8973,7 @@ "reverseGeocoding", "search", "sidecar", + "smartSearch", "trash" ], "type": "object" diff --git a/open-api/typescript-sdk/client/api.ts b/open-api/typescript-sdk/client/api.ts index 64531eb76b..595187df44 100644 --- a/open-api/typescript-sdk/client/api.ts +++ b/open-api/typescript-sdk/client/api.ts @@ -3069,12 +3069,6 @@ export interface ServerConfigDto { * @interface ServerFeaturesDto */ export interface ServerFeaturesDto { - /** - * - * @type {boolean} - * @memberof ServerFeaturesDto - */ - 'clipEncode': boolean; /** * * @type {boolean} @@ -3129,6 +3123,12 @@ export interface ServerFeaturesDto { * @memberof ServerFeaturesDto */ 'sidecar': boolean; + /** + * + * @type {boolean} + * @memberof ServerFeaturesDto + */ + 'smartSearch': boolean; /** * * @type {boolean} @@ -15206,17 +15206,18 @@ export const SearchApiAxiosParamCreator = function (configuration?: Configuratio }, /** * - * @param {boolean} [clip] + * @param {boolean} [clip] @deprecated * @param {boolean} [motion] * @param {string} [q] * @param {string} [query] * @param {boolean} [recent] + * @param {boolean} [smart] * @param {SearchTypeEnum} [type] * @param {boolean} [withArchived] * @param {*} [options] Override http request option. * @throws {RequiredError} */ - search: async (clip?: boolean, motion?: boolean, q?: string, query?: string, recent?: boolean, type?: SearchTypeEnum, withArchived?: boolean, options: RawAxiosRequestConfig = {}): Promise => { + search: async (clip?: boolean, motion?: boolean, q?: string, query?: string, recent?: boolean, smart?: boolean, type?: SearchTypeEnum, withArchived?: boolean, options: RawAxiosRequestConfig = {}): Promise => { const localVarPath = `/search`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); @@ -15258,6 +15259,10 @@ export const SearchApiAxiosParamCreator = function (configuration?: Configuratio localVarQueryParameter['recent'] = recent; } + if (smart !== undefined) { + localVarQueryParameter['smart'] = smart; + } + if (type !== undefined) { localVarQueryParameter['type'] = type; } @@ -15350,18 +15355,19 @@ export const SearchApiFp = function(configuration?: Configuration) { }, /** * - * @param {boolean} [clip] + * @param {boolean} [clip] @deprecated * @param {boolean} [motion] * @param {string} [q] * @param {string} [query] * @param {boolean} [recent] + * @param {boolean} [smart] * @param {SearchTypeEnum} [type] * @param {boolean} [withArchived] * @param {*} [options] Override http request option. * @throws {RequiredError} */ - async search(clip?: boolean, motion?: boolean, q?: string, query?: string, recent?: boolean, type?: SearchTypeEnum, withArchived?: boolean, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { - const localVarAxiosArgs = await localVarAxiosParamCreator.search(clip, motion, q, query, recent, type, withArchived, options); + async search(clip?: boolean, motion?: boolean, q?: string, query?: string, recent?: boolean, smart?: boolean, type?: SearchTypeEnum, withArchived?: boolean, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.search(clip, motion, q, query, recent, smart, type, withArchived, options); const index = configuration?.serverIndex ?? 0; const operationBasePath = operationServerMap['SearchApi.search']?.[index]?.url; return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); @@ -15404,7 +15410,7 @@ export const SearchApiFactory = function (configuration?: Configuration, basePat * @throws {RequiredError} */ search(requestParameters: SearchApiSearchRequest = {}, options?: RawAxiosRequestConfig): AxiosPromise { - return localVarFp.search(requestParameters.clip, requestParameters.motion, requestParameters.q, requestParameters.query, requestParameters.recent, requestParameters.type, requestParameters.withArchived, options).then((request) => request(axios, basePath)); + return localVarFp.search(requestParameters.clip, requestParameters.motion, requestParameters.q, requestParameters.query, requestParameters.recent, requestParameters.smart, requestParameters.type, requestParameters.withArchived, options).then((request) => request(axios, basePath)); }, /** * @@ -15425,7 +15431,7 @@ export const SearchApiFactory = function (configuration?: Configuration, basePat */ export interface SearchApiSearchRequest { /** - * + * @deprecated * @type {boolean} * @memberof SearchApiSearch */ @@ -15459,6 +15465,13 @@ export interface SearchApiSearchRequest { */ readonly recent?: boolean + /** + * + * @type {boolean} + * @memberof SearchApiSearch + */ + readonly smart?: boolean + /** * * @type {'IMAGE' | 'VIDEO' | 'AUDIO' | 'OTHER'} @@ -15520,7 +15533,7 @@ export class SearchApi extends BaseAPI { * @memberof SearchApi */ public search(requestParameters: SearchApiSearchRequest = {}, options?: RawAxiosRequestConfig) { - return SearchApiFp(this.configuration).search(requestParameters.clip, requestParameters.motion, requestParameters.q, requestParameters.query, requestParameters.recent, requestParameters.type, requestParameters.withArchived, options).then((request) => request(this.axios, this.basePath)); + return SearchApiFp(this.configuration).search(requestParameters.clip, requestParameters.motion, requestParameters.q, requestParameters.query, requestParameters.recent, requestParameters.smart, requestParameters.type, requestParameters.withArchived, options).then((request) => request(this.axios, this.basePath)); } /** diff --git a/server/e2e/api/specs/server-info.e2e-spec.ts b/server/e2e/api/specs/server-info.e2e-spec.ts index d142704346..f5664a11a3 100644 --- a/server/e2e/api/specs/server-info.e2e-spec.ts +++ b/server/e2e/api/specs/server-info.e2e-spec.ts @@ -73,7 +73,7 @@ describe(`${ServerInfoController.name} (e2e)`, () => { const { status, body } = await request(server).get('/server-info/features'); expect(status).toBe(200); expect(body).toEqual({ - clipEncode: true, + smartSearch: true, configFile: false, facialRecognition: true, map: true, diff --git a/server/src/domain/job/job.constants.ts b/server/src/domain/job/job.constants.ts index 8f8c0188f1..fe3c817278 100644 --- a/server/src/domain/job/job.constants.ts +++ b/server/src/domain/job/job.constants.ts @@ -80,9 +80,9 @@ export enum JobName { DELETE_FILES = 'delete-files', CLEAN_OLD_AUDIT_LOGS = 'clean-old-audit-logs', - // clip - QUEUE_ENCODE_CLIP = 'queue-clip-encode', - ENCODE_CLIP = 'clip-encode', + // smart search + QUEUE_SMART_SEARCH = 'queue-smart-search', + SMART_SEARCH = 'smart-search', // XMP sidecars QUEUE_SIDECAR = 'queue-sidecar', @@ -135,9 +135,9 @@ export const JOBS_TO_QUEUE: Record = { [JobName.QUEUE_FACIAL_RECOGNITION]: QueueName.FACIAL_RECOGNITION, [JobName.FACIAL_RECOGNITION]: QueueName.FACIAL_RECOGNITION, - // clip - [JobName.QUEUE_ENCODE_CLIP]: QueueName.SMART_SEARCH, - [JobName.ENCODE_CLIP]: QueueName.SMART_SEARCH, + // smart search + [JobName.QUEUE_SMART_SEARCH]: QueueName.SMART_SEARCH, + [JobName.SMART_SEARCH]: QueueName.SMART_SEARCH, // XMP sidecars [JobName.QUEUE_SIDECAR]: QueueName.SIDECAR, diff --git a/server/src/domain/job/job.service.spec.ts b/server/src/domain/job/job.service.spec.ts index 56e0dfb9f5..4abeb309b6 100644 --- a/server/src/domain/job/job.service.spec.ts +++ b/server/src/domain/job/job.service.spec.ts @@ -159,12 +159,12 @@ describe(JobService.name, () => { expect(jobMock.queue).toHaveBeenCalledWith({ name: JobName.STORAGE_TEMPLATE_MIGRATION }); }); - it('should handle a start clip encoding command', async () => { + it('should handle a start smart search command', async () => { jobMock.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false }); await sut.handleCommand(QueueName.SMART_SEARCH, { command: JobCommand.START, force: false }); - expect(jobMock.queue).toHaveBeenCalledWith({ name: JobName.QUEUE_ENCODE_CLIP, data: { force: false } }); + expect(jobMock.queue).toHaveBeenCalledWith({ name: JobName.QUEUE_SMART_SEARCH, data: { force: false } }); }); it('should handle a start metadata extraction command', async () => { @@ -289,7 +289,7 @@ describe(JobService.name, () => { jobs: [ JobName.GENERATE_WEBP_THUMBNAIL, JobName.GENERATE_THUMBHASH_THUMBNAIL, - JobName.ENCODE_CLIP, + JobName.SMART_SEARCH, JobName.FACE_DETECTION, ], }, @@ -298,7 +298,7 @@ describe(JobService.name, () => { jobs: [ JobName.GENERATE_WEBP_THUMBNAIL, JobName.GENERATE_THUMBHASH_THUMBNAIL, - JobName.ENCODE_CLIP, + JobName.SMART_SEARCH, JobName.FACE_DETECTION, JobName.VIDEO_CONVERSION, ], @@ -308,13 +308,13 @@ describe(JobService.name, () => { jobs: [ JobName.GENERATE_WEBP_THUMBNAIL, JobName.GENERATE_THUMBHASH_THUMBNAIL, - JobName.ENCODE_CLIP, + JobName.SMART_SEARCH, JobName.FACE_DETECTION, JobName.VIDEO_CONVERSION, ], }, { - item: { name: JobName.ENCODE_CLIP, data: { id: 'asset-1' } }, + item: { name: JobName.SMART_SEARCH, data: { id: 'asset-1' } }, jobs: [], }, { @@ -365,7 +365,7 @@ describe(JobService.name, () => { const featureTests: Array<{ queue: QueueName; feature: FeatureFlag; configKey: SystemConfigKey }> = [ { queue: QueueName.SMART_SEARCH, - feature: FeatureFlag.CLIP_ENCODE, + feature: FeatureFlag.SMART_SEARCH, configKey: SystemConfigKey.MACHINE_LEARNING_CLIP_ENABLED, }, { diff --git a/server/src/domain/job/job.service.ts b/server/src/domain/job/job.service.ts index 574bfffb77..a804cf658b 100644 --- a/server/src/domain/job/job.service.ts +++ b/server/src/domain/job/job.service.ts @@ -95,8 +95,8 @@ export class JobService { return this.jobRepository.queue({ name: JobName.QUEUE_MIGRATION }); case QueueName.SMART_SEARCH: - await this.configCore.requireFeature(FeatureFlag.CLIP_ENCODE); - return this.jobRepository.queue({ name: JobName.QUEUE_ENCODE_CLIP, data: { force } }); + await this.configCore.requireFeature(FeatureFlag.SMART_SEARCH); + return this.jobRepository.queue({ name: JobName.QUEUE_SMART_SEARCH, data: { force } }); case QueueName.METADATA_EXTRACTION: return this.jobRepository.queue({ name: JobName.QUEUE_METADATA_EXTRACTION, data: { force } }); @@ -226,7 +226,7 @@ export class JobService { const jobs: JobItem[] = [ { name: JobName.GENERATE_WEBP_THUMBNAIL, data: item.data }, { name: JobName.GENERATE_THUMBHASH_THUMBNAIL, data: item.data }, - { name: JobName.ENCODE_CLIP, data: item.data }, + { name: JobName.SMART_SEARCH, data: item.data }, { name: JobName.FACE_DETECTION, data: item.data }, ]; diff --git a/server/src/domain/repositories/asset.repository.ts b/server/src/domain/repositories/asset.repository.ts index 23d2106025..00a74f8293 100644 --- a/server/src/domain/repositories/asset.repository.ts +++ b/server/src/domain/repositories/asset.repository.ts @@ -93,7 +93,7 @@ export enum WithoutProperty { THUMBNAIL = 'thumbnail', ENCODED_VIDEO = 'encoded-video', EXIF = 'exif', - CLIP_ENCODING = 'clip-embedding', + SMART_SEARCH = 'smart-search', OBJECT_TAGS = 'object-tags', FACES = 'faces', PERSON = 'person', diff --git a/server/src/domain/repositories/job.repository.ts b/server/src/domain/repositories/job.repository.ts index 4a05fa83b2..0b52e14590 100644 --- a/server/src/domain/repositories/job.repository.ts +++ b/server/src/domain/repositories/job.repository.ts @@ -71,9 +71,9 @@ export type JobItem = | { name: JobName.FACIAL_RECOGNITION; data: IDeferrableJob } | { name: JobName.GENERATE_PERSON_THUMBNAIL; data: IEntityJob } - // Clip Embedding - | { name: JobName.QUEUE_ENCODE_CLIP; data: IBaseJob } - | { name: JobName.ENCODE_CLIP; data: IEntityJob } + // Smart Search + | { name: JobName.QUEUE_SMART_SEARCH; data: IBaseJob } + | { name: JobName.SMART_SEARCH; data: IEntityJob } // Filesystem | { name: JobName.DELETE_FILES; data: IDeleteFilesJob } diff --git a/server/src/domain/repositories/search.repository.ts b/server/src/domain/repositories/search.repository.ts index c2a0201efe..5c3497c8ef 100644 --- a/server/src/domain/repositories/search.repository.ts +++ b/server/src/domain/repositories/search.repository.ts @@ -1,7 +1,7 @@ import { AssetType } from '@app/infra/entities'; export enum SearchStrategy { - CLIP = 'CLIP', + SMART = 'SMART', TEXT = 'TEXT', } diff --git a/server/src/domain/search/dto/search.dto.ts b/server/src/domain/search/dto/search.dto.ts index acfb91f0cb..3ddcb3a32c 100644 --- a/server/src/domain/search/dto/search.dto.ts +++ b/server/src/domain/search/dto/search.dto.ts @@ -14,6 +14,12 @@ export class SearchDto { @Optional() query?: string; + @IsBoolean() + @Optional() + @Transform(toBoolean) + smart?: boolean; + + /** @deprecated */ @IsBoolean() @Optional() @Transform(toBoolean) diff --git a/server/src/domain/search/search.service.spec.ts b/server/src/domain/search/search.service.spec.ts index 7b182b9d30..86373ce2d2 100644 --- a/server/src/domain/search/search.service.spec.ts +++ b/server/src/domain/search/search.service.spec.ts @@ -180,14 +180,14 @@ describe(SearchService.name, () => { expect(assetMock.searchMetadata).not.toHaveBeenCalled(); }); - it('should throw an error if clip is requested but disabled', async () => { + 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 - .mockResolvedValueOnce([{ key: SystemConfigKey.MACHINE_LEARNING_ENABLED, value: false }]) - .mockResolvedValueOnce([{ key: SystemConfigKey.MACHINE_LEARNING_CLIP_ENABLED, value: false }]); + configMock.load.mockResolvedValue([{ key, value: false }]); - await expect(sut.search(authStub.user1, dto)).rejects.toThrow('CLIP is not enabled'); - await expect(sut.search(authStub.user1, dto)).rejects.toThrow('CLIP is not enabled'); + await expect(sut.search(authStub.user1, dto)).rejects.toThrow('Smart search is not enabled'); }); }); }); diff --git a/server/src/domain/search/search.service.ts b/server/src/domain/search/search.service.ts index 2bd65daab9..8695c26e0d 100644 --- a/server/src/domain/search/search.service.ts +++ b/server/src/domain/search/search.service.ts @@ -56,23 +56,26 @@ export class SearchService { } 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'); } - const hasClip = machineLearning.enabled && machineLearning.clip.enabled; - if (dto.clip && !hasClip) { - throw new Error('CLIP is not enabled'); + + let strategy = SearchStrategy.TEXT; + if (dto.smart || dto.clip) { + await this.configCore.requireFeature(FeatureFlag.SMART_SEARCH); + strategy = SearchStrategy.SMART; } - const strategy = dto.clip ? SearchStrategy.CLIP : SearchStrategy.TEXT; + const userIds = await this.getUserIdsToSearch(auth); const withArchived = dto.withArchived || false; let assets: AssetEntity[] = []; switch (strategy) { - case SearchStrategy.CLIP: + case SearchStrategy.SMART: const embedding = await this.machineLearning.encodeText( machineLearning.url, { text: query }, diff --git a/server/src/domain/server-info/server-info.dto.ts b/server/src/domain/server-info/server-info.dto.ts index 8470da8bcd..b631bdc7ef 100644 --- a/server/src/domain/server-info/server-info.dto.ts +++ b/server/src/domain/server-info/server-info.dto.ts @@ -93,7 +93,7 @@ export class ServerConfigDto { } export class ServerFeaturesDto implements FeatureFlags { - clipEncode!: boolean; + smartSearch!: boolean; configFile!: boolean; facialRecognition!: boolean; map!: boolean; diff --git a/server/src/domain/server-info/server-info.service.spec.ts b/server/src/domain/server-info/server-info.service.spec.ts index 4bc1b10443..1f1b51055b 100644 --- a/server/src/domain/server-info/server-info.service.spec.ts +++ b/server/src/domain/server-info/server-info.service.spec.ts @@ -174,7 +174,7 @@ describe(ServerInfoService.name, () => { describe('getFeatures', () => { it('should respond the server features', async () => { await expect(sut.getFeatures()).resolves.toEqual({ - clipEncode: true, + smartSearch: true, facialRecognition: true, map: true, reverseGeocoding: true, diff --git a/server/src/domain/smart-info/smart-info.service.spec.ts b/server/src/domain/smart-info/smart-info.service.spec.ts index 7dd21d7a53..373f8da91c 100644 --- a/server/src/domain/smart-info/smart-info.service.spec.ts +++ b/server/src/domain/smart-info/smart-info.service.spec.ts @@ -69,8 +69,8 @@ describe(SmartInfoService.name, () => { await sut.handleQueueEncodeClip({ force: false }); - expect(jobMock.queueAll).toHaveBeenCalledWith([{ name: JobName.ENCODE_CLIP, data: { id: assetStub.image.id } }]); - expect(assetMock.getWithout).toHaveBeenCalledWith({ skip: 0, take: 1000 }, WithoutProperty.CLIP_ENCODING); + expect(jobMock.queueAll).toHaveBeenCalledWith([{ name: JobName.SMART_SEARCH, data: { id: assetStub.image.id } }]); + expect(assetMock.getWithout).toHaveBeenCalledWith({ skip: 0, take: 1000 }, WithoutProperty.SMART_SEARCH); }); it('should queue all the assets', async () => { @@ -81,7 +81,7 @@ describe(SmartInfoService.name, () => { await sut.handleQueueEncodeClip({ force: true }); - expect(jobMock.queueAll).toHaveBeenCalledWith([{ name: JobName.ENCODE_CLIP, data: { id: assetStub.image.id } }]); + expect(jobMock.queueAll).toHaveBeenCalledWith([{ name: JobName.SMART_SEARCH, data: { id: assetStub.image.id } }]); expect(assetMock.getAll).toHaveBeenCalled(); }); }); diff --git a/server/src/domain/smart-info/smart-info.service.ts b/server/src/domain/smart-info/smart-info.service.ts index d247fcf08c..55e4b7080f 100644 --- a/server/src/domain/smart-info/smart-info.service.ts +++ b/server/src/domain/smart-info/smart-info.service.ts @@ -53,11 +53,13 @@ export class SmartInfoService { const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) => { return force ? this.assetRepository.getAll(pagination) - : this.assetRepository.getWithout(pagination, WithoutProperty.CLIP_ENCODING); + : this.assetRepository.getWithout(pagination, WithoutProperty.SMART_SEARCH); }); for await (const assets of assetPagination) { - await this.jobRepository.queueAll(assets.map((asset) => ({ name: JobName.ENCODE_CLIP, data: { id: asset.id } }))); + await this.jobRepository.queueAll( + assets.map((asset) => ({ name: JobName.SMART_SEARCH, data: { id: asset.id } })), + ); } return true; diff --git a/server/src/domain/system-config/system-config.core.ts b/server/src/domain/system-config/system-config.core.ts index 0d0af03df3..926703d0dd 100644 --- a/server/src/domain/system-config/system-config.core.ts +++ b/server/src/domain/system-config/system-config.core.ts @@ -136,7 +136,7 @@ export const defaults = Object.freeze({ }); export enum FeatureFlag { - CLIP_ENCODE = 'clipEncode', + SMART_SEARCH = 'smartSearch', FACIAL_RECOGNITION = 'facialRecognition', MAP = 'map', REVERSE_GEOCODING = 'reverseGeocoding', @@ -178,8 +178,8 @@ export class SystemConfigCore { const hasFeature = await this.hasFeature(feature); if (!hasFeature) { switch (feature) { - case FeatureFlag.CLIP_ENCODE: - throw new BadRequestException('Clip encoding is not enabled'); + case FeatureFlag.SMART_SEARCH: + throw new BadRequestException('Smart search is not enabled'); case FeatureFlag.FACIAL_RECOGNITION: throw new BadRequestException('Facial recognition is not enabled'); case FeatureFlag.SIDECAR: @@ -208,7 +208,7 @@ export class SystemConfigCore { const mlEnabled = config.machineLearning.enabled; return { - [FeatureFlag.CLIP_ENCODE]: mlEnabled && config.machineLearning.clip.enabled, + [FeatureFlag.SMART_SEARCH]: mlEnabled && config.machineLearning.clip.enabled, [FeatureFlag.FACIAL_RECOGNITION]: mlEnabled && config.machineLearning.facialRecognition.enabled, [FeatureFlag.MAP]: config.map.enabled, [FeatureFlag.REVERSE_GEOCODING]: config.reverseGeocoding.enabled, diff --git a/server/src/infra/repositories/asset.repository.ts b/server/src/infra/repositories/asset.repository.ts index 715718f327..226803cfb9 100644 --- a/server/src/infra/repositories/asset.repository.ts +++ b/server/src/infra/repositories/asset.repository.ts @@ -501,7 +501,7 @@ export class AssetRepository implements IAssetRepository { }; break; - case WithoutProperty.CLIP_ENCODING: + case WithoutProperty.SMART_SEARCH: relations = { smartSearch: true, }; diff --git a/server/src/microservices/app.service.ts b/server/src/microservices/app.service.ts index 782f7bb73f..93870583ff 100644 --- a/server/src/microservices/app.service.ts +++ b/server/src/microservices/app.service.ts @@ -46,8 +46,8 @@ export class AppService { [JobName.USER_DELETE_CHECK]: () => this.userService.handleUserDeleteCheck(), [JobName.USER_DELETION]: (data) => this.userService.handleUserDelete(data), [JobName.USER_SYNC_USAGE]: () => this.userService.handleUserSyncUsage(), - [JobName.QUEUE_ENCODE_CLIP]: (data) => this.smartInfoService.handleQueueEncodeClip(data), - [JobName.ENCODE_CLIP]: (data) => this.smartInfoService.handleEncodeClip(data), + [JobName.QUEUE_SMART_SEARCH]: (data) => this.smartInfoService.handleQueueEncodeClip(data), + [JobName.SMART_SEARCH]: (data) => this.smartInfoService.handleEncodeClip(data), [JobName.STORAGE_TEMPLATE_MIGRATION]: () => this.storageTemplateService.handleMigration(), [JobName.STORAGE_TEMPLATE_MIGRATION_SINGLE]: (data) => this.storageTemplateService.handleMigrationSingle(data), [JobName.QUEUE_MIGRATION]: () => this.mediaService.handleQueueMigration(), diff --git a/web/src/lib/components/admin-page/jobs/jobs-panel.svelte b/web/src/lib/components/admin-page/jobs/jobs-panel.svelte index c5387c8958..34604e852c 100644 --- a/web/src/lib/components/admin-page/jobs/jobs-panel.svelte +++ b/web/src/lib/components/admin-page/jobs/jobs-panel.svelte @@ -85,7 +85,7 @@ icon: mdiImageSearch, title: api.getJobName(JobName.SmartSearch), subtitle: 'Run machine learning on assets to support smart search', - disabled: !$featureFlags.clipEncode, + disabled: !$featureFlags.smartSearch, }, [JobName.FaceDetection]: { icon: mdiFaceRecognition, diff --git a/web/src/lib/components/shared-components/search-bar/search-bar.svelte b/web/src/lib/components/shared-components/search-bar/search-bar.svelte index af36786ef2..1d26a368fc 100644 --- a/web/src/lib/components/shared-components/search-bar/search-bar.svelte +++ b/web/src/lib/components/shared-components/search-bar/search-bar.svelte @@ -15,11 +15,11 @@ $: showClearIcon = value.length > 0; function onSearch() { - let clipSearch = 'true'; + let smartSearch = 'true'; let searchValue = value; if (value.slice(0, 2) == 'm:') { - clipSearch = 'false'; + smartSearch = 'false'; searchValue = value.slice(2); } @@ -28,7 +28,7 @@ const params = new URLSearchParams({ q: searchValue, - clip: clipSearch, + smart: smartSearch, }); showBigSearchBar = false; diff --git a/web/src/lib/constants.ts b/web/src/lib/constants.ts index bbbe245900..a1ac68c216 100644 --- a/web/src/lib/constants.ts +++ b/web/src/lib/constants.ts @@ -63,7 +63,7 @@ export const dateFormats = { export enum QueryParameter { ACTION = 'action', ASSET_INDEX = 'assetIndex', - CLIP = 'clip', + SMART_SEARCH = 'smartSearch', MEMORY_INDEX = 'memoryIndex', ONBOARDING_STEP = 'step', OPEN_SETTING = 'openSetting', diff --git a/web/src/lib/stores/server-config.store.ts b/web/src/lib/stores/server-config.store.ts index 2fc17595a2..3f9df3a458 100644 --- a/web/src/lib/stores/server-config.store.ts +++ b/web/src/lib/stores/server-config.store.ts @@ -5,7 +5,7 @@ export type FeatureFlags = ServerFeaturesDto & { loaded: boolean }; export const featureFlags = writable({ loaded: false, - clipEncode: true, + smartSearch: true, facialRecognition: true, sidecar: true, map: true, diff --git a/web/src/routes/(user)/search/+page.svelte b/web/src/routes/(user)/search/+page.svelte index 5c34114451..f49c7a0c58 100644 --- a/web/src/routes/(user)/search/+page.svelte +++ b/web/src/routes/(user)/search/+page.svelte @@ -87,7 +87,7 @@ $: term = (() => { let term = $page.url.searchParams.get(QueryParameter.SEARCH_TERM) || data.term || ''; - const isMetadataSearch = $page.url.searchParams.get(QueryParameter.CLIP) === 'false'; + const isMetadataSearch = $page.url.searchParams.get(QueryParameter.SMART_SEARCH) === 'false'; if (isMetadataSearch && term !== '') { term = `m:${term}`; }