1
0
mirror of https://github.com/immich-app/immich.git synced 2024-12-25 10:43:13 +02:00

refactor: rename clip -> smart search (#6713)

This commit is contained in:
Mert 2024-01-29 09:51:22 -05:00 committed by GitHub
parent e5a70329c9
commit ae7f174948
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
34 changed files with 128 additions and 96 deletions

View File

@ -5,14 +5,14 @@ class SearchResultPageState {
final bool isLoading; final bool isLoading;
final bool isSuccess; final bool isSuccess;
final bool isError; final bool isError;
final bool isClip; final bool isSmart;
final List<Asset> searchResult; final List<Asset> searchResult;
SearchResultPageState({ SearchResultPageState({
required this.isLoading, required this.isLoading,
required this.isSuccess, required this.isSuccess,
required this.isError, required this.isError,
required this.isClip, required this.isSmart,
required this.searchResult, required this.searchResult,
}); });
@ -20,21 +20,21 @@ class SearchResultPageState {
bool? isLoading, bool? isLoading,
bool? isSuccess, bool? isSuccess,
bool? isError, bool? isError,
bool? isClip, bool? isSmart,
List<Asset>? searchResult, List<Asset>? searchResult,
}) { }) {
return SearchResultPageState( return SearchResultPageState(
isLoading: isLoading ?? this.isLoading, isLoading: isLoading ?? this.isLoading,
isSuccess: isSuccess ?? this.isSuccess, isSuccess: isSuccess ?? this.isSuccess,
isError: isError ?? this.isError, isError: isError ?? this.isError,
isClip: isClip ?? this.isClip, isSmart: isSmart ?? this.isSmart,
searchResult: searchResult ?? this.searchResult, searchResult: searchResult ?? this.searchResult,
); );
} }
@override @override
String toString() { 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 @override
@ -46,7 +46,7 @@ class SearchResultPageState {
other.isLoading == isLoading && other.isLoading == isLoading &&
other.isSuccess == isSuccess && other.isSuccess == isSuccess &&
other.isError == isError && other.isError == isError &&
other.isClip == isClip && other.isSmart == isSmart &&
listEquals(other.searchResult, searchResult); listEquals(other.searchResult, searchResult);
} }
@ -55,7 +55,7 @@ class SearchResultPageState {
return isLoading.hashCode ^ return isLoading.hashCode ^
isSuccess.hashCode ^ isSuccess.hashCode ^
isError.hashCode ^ isError.hashCode ^
isClip.hashCode ^ isSmart.hashCode ^
searchResult.hashCode; searchResult.hashCode;
} }
} }

View File

@ -14,13 +14,13 @@ class SearchResultPageNotifier extends StateNotifier<SearchResultPageState> {
isError: false, isError: false,
isLoading: true, isLoading: true,
isSuccess: false, isSuccess: false,
isClip: false, isSmart: false,
), ),
); );
final SearchService _searchService; final SearchService _searchService;
Future<void> search(String searchTerm, {bool clipEnable = true}) async { Future<void> search(String searchTerm, {bool smartSearch = true}) async {
state = state.copyWith( state = state.copyWith(
searchResult: [], searchResult: [],
isError: false, isError: false,
@ -28,10 +28,8 @@ class SearchResultPageNotifier extends StateNotifier<SearchResultPageState> {
isSuccess: false, isSuccess: false,
); );
List<Asset>? assets = await _searchService.searchAsset( List<Asset>? assets =
searchTerm, await _searchService.searchAsset(searchTerm, smartSearch: smartSearch);
clipEnable: clipEnable,
);
if (assets != null) { if (assets != null) {
state = state.copyWith( state = state.copyWith(
@ -39,7 +37,7 @@ class SearchResultPageNotifier extends StateNotifier<SearchResultPageState> {
isError: false, isError: false,
isLoading: false, isLoading: false,
isSuccess: true, isSuccess: true,
isClip: clipEnable, isSmart: smartSearch,
); );
} else { } else {
state = state.copyWith( state = state.copyWith(
@ -47,7 +45,7 @@ class SearchResultPageNotifier extends StateNotifier<SearchResultPageState> {
isError: true, isError: true,
isLoading: false, isLoading: false,
isSuccess: false, isSuccess: false,
isClip: clipEnable, isSmart: smartSearch,
); );
} }
} }
@ -63,7 +61,7 @@ final searchRenderListProvider = Provider((ref) {
final result = ref.watch(searchResultPageProvider); final result = ref.watch(searchResultPageProvider);
return ref.watch( return ref.watch(
renderListProviderWithGrouping( renderListProviderWithGrouping(
(result.searchResult, result.isClip ? GroupAssetsBy.none : null), (result.searchResult, result.isSmart ? GroupAssetsBy.none : null),
), ),
); );
}); });

View File

@ -31,13 +31,13 @@ class SearchService {
Future<List<Asset>?> searchAsset( Future<List<Asset>?> searchAsset(
String searchTerm, { String searchTerm, {
bool clipEnable = true, bool smartSearch = true,
}) async { }) async {
// TODO search in local DB: 1. when offline, 2. to find local assets // TODO search in local DB: 1. when offline, 2. to find local assets
try { try {
final SearchResponseDto? results = await _apiService.searchApi.search( final SearchResponseDto? results = await _apiService.searchApi.search(
query: searchTerm, query: searchTerm,
clip: clipEnable, smart: smartSearch,
); );
if (results == null) { if (results == null) {
return null; return null;

View File

@ -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'; import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
class SearchType { 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; final String searchTerm;
} }
SearchType _getSearchType(String searchTerm) { SearchType _getSearchType(String searchTerm) {
if (searchTerm.startsWith('m:')) { if (searchTerm.startsWith('m:')) {
return SearchType(isClip: false, searchTerm: searchTerm.substring(2)); return SearchType(isSmart: false, searchTerm: searchTerm.substring(2));
} else { } else {
return SearchType(isClip: true, searchTerm: searchTerm); return SearchType(isSmart: true, searchTerm: searchTerm);
} }
} }
@ -52,7 +52,7 @@ class SearchResultPage extends HookConsumerWidget {
Duration.zero, Duration.zero,
() => ref () => ref
.read(searchResultPageProvider.notifier) .read(searchResultPageProvider.notifier)
.search(searchType.searchTerm, clipEnable: searchType.isClip), .search(searchType.searchTerm, smartSearch: searchType.isSmart),
); );
return () => searchFocusNode?.dispose(); return () => searchFocusNode?.dispose();
}, },
@ -67,7 +67,7 @@ class SearchResultPage extends HookConsumerWidget {
var searchType = _getSearchType(newSearchTerm); var searchType = _getSearchType(newSearchTerm);
return ref return ref
.watch(searchResultPageProvider.notifier) .watch(searchResultPageProvider.notifier)
.search(searchType.searchTerm, clipEnable: searchType.isClip); .search(searchType.searchTerm, smartSearch: searchType.isSmart);
} }
buildTextField() { buildTextField() {

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -4684,6 +4684,8 @@
"name": "clip", "name": "clip",
"required": false, "required": false,
"in": "query", "in": "query",
"description": "@deprecated",
"deprecated": true,
"schema": { "schema": {
"type": "boolean" "type": "boolean"
} }
@ -4720,6 +4722,14 @@
"type": "boolean" "type": "boolean"
} }
}, },
{
"name": "smart",
"required": false,
"in": "query",
"schema": {
"type": "boolean"
}
},
{ {
"name": "type", "name": "type",
"required": false, "required": false,
@ -8919,9 +8929,6 @@
}, },
"ServerFeaturesDto": { "ServerFeaturesDto": {
"properties": { "properties": {
"clipEncode": {
"type": "boolean"
},
"configFile": { "configFile": {
"type": "boolean" "type": "boolean"
}, },
@ -8949,12 +8956,14 @@
"sidecar": { "sidecar": {
"type": "boolean" "type": "boolean"
}, },
"smartSearch": {
"type": "boolean"
},
"trash": { "trash": {
"type": "boolean" "type": "boolean"
} }
}, },
"required": [ "required": [
"clipEncode",
"configFile", "configFile",
"facialRecognition", "facialRecognition",
"map", "map",
@ -8964,6 +8973,7 @@
"reverseGeocoding", "reverseGeocoding",
"search", "search",
"sidecar", "sidecar",
"smartSearch",
"trash" "trash"
], ],
"type": "object" "type": "object"

View File

@ -3069,12 +3069,6 @@ export interface ServerConfigDto {
* @interface ServerFeaturesDto * @interface ServerFeaturesDto
*/ */
export interface ServerFeaturesDto { export interface ServerFeaturesDto {
/**
*
* @type {boolean}
* @memberof ServerFeaturesDto
*/
'clipEncode': boolean;
/** /**
* *
* @type {boolean} * @type {boolean}
@ -3129,6 +3123,12 @@ export interface ServerFeaturesDto {
* @memberof ServerFeaturesDto * @memberof ServerFeaturesDto
*/ */
'sidecar': boolean; 'sidecar': boolean;
/**
*
* @type {boolean}
* @memberof ServerFeaturesDto
*/
'smartSearch': boolean;
/** /**
* *
* @type {boolean} * @type {boolean}
@ -15206,17 +15206,18 @@ export const SearchApiAxiosParamCreator = function (configuration?: Configuratio
}, },
/** /**
* *
* @param {boolean} [clip] * @param {boolean} [clip] @deprecated
* @param {boolean} [motion] * @param {boolean} [motion]
* @param {string} [q] * @param {string} [q]
* @param {string} [query] * @param {string} [query]
* @param {boolean} [recent] * @param {boolean} [recent]
* @param {boolean} [smart]
* @param {SearchTypeEnum} [type] * @param {SearchTypeEnum} [type]
* @param {boolean} [withArchived] * @param {boolean} [withArchived]
* @param {*} [options] Override http request option. * @param {*} [options] Override http request option.
* @throws {RequiredError} * @throws {RequiredError}
*/ */
search: async (clip?: boolean, motion?: boolean, q?: string, query?: string, recent?: boolean, type?: SearchTypeEnum, withArchived?: boolean, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => { search: async (clip?: boolean, motion?: boolean, q?: string, query?: string, recent?: boolean, smart?: boolean, type?: SearchTypeEnum, withArchived?: boolean, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
const localVarPath = `/search`; const localVarPath = `/search`;
// use dummy base URL string because the URL constructor only accepts absolute URLs. // use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
@ -15258,6 +15259,10 @@ export const SearchApiAxiosParamCreator = function (configuration?: Configuratio
localVarQueryParameter['recent'] = recent; localVarQueryParameter['recent'] = recent;
} }
if (smart !== undefined) {
localVarQueryParameter['smart'] = smart;
}
if (type !== undefined) { if (type !== undefined) {
localVarQueryParameter['type'] = type; localVarQueryParameter['type'] = type;
} }
@ -15350,18 +15355,19 @@ export const SearchApiFp = function(configuration?: Configuration) {
}, },
/** /**
* *
* @param {boolean} [clip] * @param {boolean} [clip] @deprecated
* @param {boolean} [motion] * @param {boolean} [motion]
* @param {string} [q] * @param {string} [q]
* @param {string} [query] * @param {string} [query]
* @param {boolean} [recent] * @param {boolean} [recent]
* @param {boolean} [smart]
* @param {SearchTypeEnum} [type] * @param {SearchTypeEnum} [type]
* @param {boolean} [withArchived] * @param {boolean} [withArchived]
* @param {*} [options] Override http request option. * @param {*} [options] Override http request option.
* @throws {RequiredError} * @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<SearchResponseDto>> { 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<SearchResponseDto>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.search(clip, motion, q, query, recent, type, withArchived, options); const localVarAxiosArgs = await localVarAxiosParamCreator.search(clip, motion, q, query, recent, smart, type, withArchived, options);
const index = configuration?.serverIndex ?? 0; const index = configuration?.serverIndex ?? 0;
const operationBasePath = operationServerMap['SearchApi.search']?.[index]?.url; const operationBasePath = operationServerMap['SearchApi.search']?.[index]?.url;
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); 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} * @throws {RequiredError}
*/ */
search(requestParameters: SearchApiSearchRequest = {}, options?: RawAxiosRequestConfig): AxiosPromise<SearchResponseDto> { search(requestParameters: SearchApiSearchRequest = {}, options?: RawAxiosRequestConfig): AxiosPromise<SearchResponseDto> {
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 { export interface SearchApiSearchRequest {
/** /**
* * @deprecated
* @type {boolean} * @type {boolean}
* @memberof SearchApiSearch * @memberof SearchApiSearch
*/ */
@ -15459,6 +15465,13 @@ export interface SearchApiSearchRequest {
*/ */
readonly recent?: boolean readonly recent?: boolean
/**
*
* @type {boolean}
* @memberof SearchApiSearch
*/
readonly smart?: boolean
/** /**
* *
* @type {'IMAGE' | 'VIDEO' | 'AUDIO' | 'OTHER'} * @type {'IMAGE' | 'VIDEO' | 'AUDIO' | 'OTHER'}
@ -15520,7 +15533,7 @@ export class SearchApi extends BaseAPI {
* @memberof SearchApi * @memberof SearchApi
*/ */
public search(requestParameters: SearchApiSearchRequest = {}, options?: RawAxiosRequestConfig) { 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));
} }
/** /**

View File

@ -73,7 +73,7 @@ describe(`${ServerInfoController.name} (e2e)`, () => {
const { status, body } = await request(server).get('/server-info/features'); const { status, body } = await request(server).get('/server-info/features');
expect(status).toBe(200); expect(status).toBe(200);
expect(body).toEqual({ expect(body).toEqual({
clipEncode: true, smartSearch: true,
configFile: false, configFile: false,
facialRecognition: true, facialRecognition: true,
map: true, map: true,

View File

@ -80,9 +80,9 @@ export enum JobName {
DELETE_FILES = 'delete-files', DELETE_FILES = 'delete-files',
CLEAN_OLD_AUDIT_LOGS = 'clean-old-audit-logs', CLEAN_OLD_AUDIT_LOGS = 'clean-old-audit-logs',
// clip // smart search
QUEUE_ENCODE_CLIP = 'queue-clip-encode', QUEUE_SMART_SEARCH = 'queue-smart-search',
ENCODE_CLIP = 'clip-encode', SMART_SEARCH = 'smart-search',
// XMP sidecars // XMP sidecars
QUEUE_SIDECAR = 'queue-sidecar', QUEUE_SIDECAR = 'queue-sidecar',
@ -135,9 +135,9 @@ export const JOBS_TO_QUEUE: Record<JobName, QueueName> = {
[JobName.QUEUE_FACIAL_RECOGNITION]: QueueName.FACIAL_RECOGNITION, [JobName.QUEUE_FACIAL_RECOGNITION]: QueueName.FACIAL_RECOGNITION,
[JobName.FACIAL_RECOGNITION]: QueueName.FACIAL_RECOGNITION, [JobName.FACIAL_RECOGNITION]: QueueName.FACIAL_RECOGNITION,
// clip // smart search
[JobName.QUEUE_ENCODE_CLIP]: QueueName.SMART_SEARCH, [JobName.QUEUE_SMART_SEARCH]: QueueName.SMART_SEARCH,
[JobName.ENCODE_CLIP]: QueueName.SMART_SEARCH, [JobName.SMART_SEARCH]: QueueName.SMART_SEARCH,
// XMP sidecars // XMP sidecars
[JobName.QUEUE_SIDECAR]: QueueName.SIDECAR, [JobName.QUEUE_SIDECAR]: QueueName.SIDECAR,

View File

@ -159,12 +159,12 @@ describe(JobService.name, () => {
expect(jobMock.queue).toHaveBeenCalledWith({ name: JobName.STORAGE_TEMPLATE_MIGRATION }); 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 }); jobMock.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false });
await sut.handleCommand(QueueName.SMART_SEARCH, { command: JobCommand.START, force: 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 () => { it('should handle a start metadata extraction command', async () => {
@ -289,7 +289,7 @@ describe(JobService.name, () => {
jobs: [ jobs: [
JobName.GENERATE_WEBP_THUMBNAIL, JobName.GENERATE_WEBP_THUMBNAIL,
JobName.GENERATE_THUMBHASH_THUMBNAIL, JobName.GENERATE_THUMBHASH_THUMBNAIL,
JobName.ENCODE_CLIP, JobName.SMART_SEARCH,
JobName.FACE_DETECTION, JobName.FACE_DETECTION,
], ],
}, },
@ -298,7 +298,7 @@ describe(JobService.name, () => {
jobs: [ jobs: [
JobName.GENERATE_WEBP_THUMBNAIL, JobName.GENERATE_WEBP_THUMBNAIL,
JobName.GENERATE_THUMBHASH_THUMBNAIL, JobName.GENERATE_THUMBHASH_THUMBNAIL,
JobName.ENCODE_CLIP, JobName.SMART_SEARCH,
JobName.FACE_DETECTION, JobName.FACE_DETECTION,
JobName.VIDEO_CONVERSION, JobName.VIDEO_CONVERSION,
], ],
@ -308,13 +308,13 @@ describe(JobService.name, () => {
jobs: [ jobs: [
JobName.GENERATE_WEBP_THUMBNAIL, JobName.GENERATE_WEBP_THUMBNAIL,
JobName.GENERATE_THUMBHASH_THUMBNAIL, JobName.GENERATE_THUMBHASH_THUMBNAIL,
JobName.ENCODE_CLIP, JobName.SMART_SEARCH,
JobName.FACE_DETECTION, JobName.FACE_DETECTION,
JobName.VIDEO_CONVERSION, JobName.VIDEO_CONVERSION,
], ],
}, },
{ {
item: { name: JobName.ENCODE_CLIP, data: { id: 'asset-1' } }, item: { name: JobName.SMART_SEARCH, data: { id: 'asset-1' } },
jobs: [], jobs: [],
}, },
{ {
@ -365,7 +365,7 @@ describe(JobService.name, () => {
const featureTests: Array<{ queue: QueueName; feature: FeatureFlag; configKey: SystemConfigKey }> = [ const featureTests: Array<{ queue: QueueName; feature: FeatureFlag; configKey: SystemConfigKey }> = [
{ {
queue: QueueName.SMART_SEARCH, queue: QueueName.SMART_SEARCH,
feature: FeatureFlag.CLIP_ENCODE, feature: FeatureFlag.SMART_SEARCH,
configKey: SystemConfigKey.MACHINE_LEARNING_CLIP_ENABLED, configKey: SystemConfigKey.MACHINE_LEARNING_CLIP_ENABLED,
}, },
{ {

View File

@ -95,8 +95,8 @@ export class JobService {
return this.jobRepository.queue({ name: JobName.QUEUE_MIGRATION }); return this.jobRepository.queue({ name: JobName.QUEUE_MIGRATION });
case QueueName.SMART_SEARCH: case QueueName.SMART_SEARCH:
await this.configCore.requireFeature(FeatureFlag.CLIP_ENCODE); await this.configCore.requireFeature(FeatureFlag.SMART_SEARCH);
return this.jobRepository.queue({ name: JobName.QUEUE_ENCODE_CLIP, data: { force } }); return this.jobRepository.queue({ name: JobName.QUEUE_SMART_SEARCH, data: { force } });
case QueueName.METADATA_EXTRACTION: case QueueName.METADATA_EXTRACTION:
return this.jobRepository.queue({ name: JobName.QUEUE_METADATA_EXTRACTION, data: { force } }); return this.jobRepository.queue({ name: JobName.QUEUE_METADATA_EXTRACTION, data: { force } });
@ -226,7 +226,7 @@ export class JobService {
const jobs: JobItem[] = [ const jobs: JobItem[] = [
{ name: JobName.GENERATE_WEBP_THUMBNAIL, data: item.data }, { name: JobName.GENERATE_WEBP_THUMBNAIL, data: item.data },
{ name: JobName.GENERATE_THUMBHASH_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 }, { name: JobName.FACE_DETECTION, data: item.data },
]; ];

View File

@ -93,7 +93,7 @@ export enum WithoutProperty {
THUMBNAIL = 'thumbnail', THUMBNAIL = 'thumbnail',
ENCODED_VIDEO = 'encoded-video', ENCODED_VIDEO = 'encoded-video',
EXIF = 'exif', EXIF = 'exif',
CLIP_ENCODING = 'clip-embedding', SMART_SEARCH = 'smart-search',
OBJECT_TAGS = 'object-tags', OBJECT_TAGS = 'object-tags',
FACES = 'faces', FACES = 'faces',
PERSON = 'person', PERSON = 'person',

View File

@ -71,9 +71,9 @@ export type JobItem =
| { name: JobName.FACIAL_RECOGNITION; data: IDeferrableJob } | { name: JobName.FACIAL_RECOGNITION; data: IDeferrableJob }
| { name: JobName.GENERATE_PERSON_THUMBNAIL; data: IEntityJob } | { name: JobName.GENERATE_PERSON_THUMBNAIL; data: IEntityJob }
// Clip Embedding // Smart Search
| { name: JobName.QUEUE_ENCODE_CLIP; data: IBaseJob } | { name: JobName.QUEUE_SMART_SEARCH; data: IBaseJob }
| { name: JobName.ENCODE_CLIP; data: IEntityJob } | { name: JobName.SMART_SEARCH; data: IEntityJob }
// Filesystem // Filesystem
| { name: JobName.DELETE_FILES; data: IDeleteFilesJob } | { name: JobName.DELETE_FILES; data: IDeleteFilesJob }

View File

@ -1,7 +1,7 @@
import { AssetType } from '@app/infra/entities'; import { AssetType } from '@app/infra/entities';
export enum SearchStrategy { export enum SearchStrategy {
CLIP = 'CLIP', SMART = 'SMART',
TEXT = 'TEXT', TEXT = 'TEXT',
} }

View File

@ -14,6 +14,12 @@ export class SearchDto {
@Optional() @Optional()
query?: string; query?: string;
@IsBoolean()
@Optional()
@Transform(toBoolean)
smart?: boolean;
/** @deprecated */
@IsBoolean() @IsBoolean()
@Optional() @Optional()
@Transform(toBoolean) @Transform(toBoolean)

View File

@ -180,14 +180,14 @@ describe(SearchService.name, () => {
expect(assetMock.searchMetadata).not.toHaveBeenCalled(); 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 }; const dto: SearchDto = { q: 'test query', clip: true };
configMock.load configMock.load.mockResolvedValue([{ key, value: false }]);
.mockResolvedValueOnce([{ key: SystemConfigKey.MACHINE_LEARNING_ENABLED, value: false }])
.mockResolvedValueOnce([{ key: SystemConfigKey.MACHINE_LEARNING_CLIP_ENABLED, value: false }]);
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');
await expect(sut.search(authStub.user1, dto)).rejects.toThrow('CLIP is not enabled');
}); });
}); });
}); });

View File

@ -56,23 +56,26 @@ export class SearchService {
} }
async search(auth: AuthDto, dto: SearchDto): Promise<SearchResponseDto> { async search(auth: AuthDto, dto: SearchDto): Promise<SearchResponseDto> {
await this.configCore.requireFeature(FeatureFlag.SEARCH);
const { machineLearning } = await this.configCore.getConfig(); const { machineLearning } = await this.configCore.getConfig();
const query = dto.q || dto.query; const query = dto.q || dto.query;
if (!query) { if (!query) {
throw new Error('Missing query'); throw new Error('Missing query');
} }
const hasClip = machineLearning.enabled && machineLearning.clip.enabled;
if (dto.clip && !hasClip) { let strategy = SearchStrategy.TEXT;
throw new Error('CLIP is not enabled'); 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 userIds = await this.getUserIdsToSearch(auth);
const withArchived = dto.withArchived || false; const withArchived = dto.withArchived || false;
let assets: AssetEntity[] = []; let assets: AssetEntity[] = [];
switch (strategy) { switch (strategy) {
case SearchStrategy.CLIP: case SearchStrategy.SMART:
const embedding = await this.machineLearning.encodeText( const embedding = await this.machineLearning.encodeText(
machineLearning.url, machineLearning.url,
{ text: query }, { text: query },

View File

@ -93,7 +93,7 @@ export class ServerConfigDto {
} }
export class ServerFeaturesDto implements FeatureFlags { export class ServerFeaturesDto implements FeatureFlags {
clipEncode!: boolean; smartSearch!: boolean;
configFile!: boolean; configFile!: boolean;
facialRecognition!: boolean; facialRecognition!: boolean;
map!: boolean; map!: boolean;

View File

@ -174,7 +174,7 @@ describe(ServerInfoService.name, () => {
describe('getFeatures', () => { describe('getFeatures', () => {
it('should respond the server features', async () => { it('should respond the server features', async () => {
await expect(sut.getFeatures()).resolves.toEqual({ await expect(sut.getFeatures()).resolves.toEqual({
clipEncode: true, smartSearch: true,
facialRecognition: true, facialRecognition: true,
map: true, map: true,
reverseGeocoding: true, reverseGeocoding: true,

View File

@ -69,8 +69,8 @@ describe(SmartInfoService.name, () => {
await sut.handleQueueEncodeClip({ force: false }); await sut.handleQueueEncodeClip({ force: false });
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.getWithout).toHaveBeenCalledWith({ skip: 0, take: 1000 }, WithoutProperty.CLIP_ENCODING); expect(assetMock.getWithout).toHaveBeenCalledWith({ skip: 0, take: 1000 }, WithoutProperty.SMART_SEARCH);
}); });
it('should queue all the assets', async () => { it('should queue all the assets', async () => {
@ -81,7 +81,7 @@ describe(SmartInfoService.name, () => {
await sut.handleQueueEncodeClip({ force: true }); 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(); expect(assetMock.getAll).toHaveBeenCalled();
}); });
}); });

View File

@ -53,11 +53,13 @@ export class SmartInfoService {
const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) => { const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) => {
return force return force
? this.assetRepository.getAll(pagination) ? this.assetRepository.getAll(pagination)
: this.assetRepository.getWithout(pagination, WithoutProperty.CLIP_ENCODING); : this.assetRepository.getWithout(pagination, WithoutProperty.SMART_SEARCH);
}); });
for await (const assets of assetPagination) { 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; return true;

View File

@ -136,7 +136,7 @@ export const defaults = Object.freeze<SystemConfig>({
}); });
export enum FeatureFlag { export enum FeatureFlag {
CLIP_ENCODE = 'clipEncode', SMART_SEARCH = 'smartSearch',
FACIAL_RECOGNITION = 'facialRecognition', FACIAL_RECOGNITION = 'facialRecognition',
MAP = 'map', MAP = 'map',
REVERSE_GEOCODING = 'reverseGeocoding', REVERSE_GEOCODING = 'reverseGeocoding',
@ -178,8 +178,8 @@ export class SystemConfigCore {
const hasFeature = await this.hasFeature(feature); const hasFeature = await this.hasFeature(feature);
if (!hasFeature) { if (!hasFeature) {
switch (feature) { switch (feature) {
case FeatureFlag.CLIP_ENCODE: case FeatureFlag.SMART_SEARCH:
throw new BadRequestException('Clip encoding is not enabled'); throw new BadRequestException('Smart search is not enabled');
case FeatureFlag.FACIAL_RECOGNITION: case FeatureFlag.FACIAL_RECOGNITION:
throw new BadRequestException('Facial recognition is not enabled'); throw new BadRequestException('Facial recognition is not enabled');
case FeatureFlag.SIDECAR: case FeatureFlag.SIDECAR:
@ -208,7 +208,7 @@ export class SystemConfigCore {
const mlEnabled = config.machineLearning.enabled; const mlEnabled = config.machineLearning.enabled;
return { 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.FACIAL_RECOGNITION]: mlEnabled && config.machineLearning.facialRecognition.enabled,
[FeatureFlag.MAP]: config.map.enabled, [FeatureFlag.MAP]: config.map.enabled,
[FeatureFlag.REVERSE_GEOCODING]: config.reverseGeocoding.enabled, [FeatureFlag.REVERSE_GEOCODING]: config.reverseGeocoding.enabled,

View File

@ -501,7 +501,7 @@ export class AssetRepository implements IAssetRepository {
}; };
break; break;
case WithoutProperty.CLIP_ENCODING: case WithoutProperty.SMART_SEARCH:
relations = { relations = {
smartSearch: true, smartSearch: true,
}; };

View File

@ -46,8 +46,8 @@ export class AppService {
[JobName.USER_DELETE_CHECK]: () => this.userService.handleUserDeleteCheck(), [JobName.USER_DELETE_CHECK]: () => this.userService.handleUserDeleteCheck(),
[JobName.USER_DELETION]: (data) => this.userService.handleUserDelete(data), [JobName.USER_DELETION]: (data) => this.userService.handleUserDelete(data),
[JobName.USER_SYNC_USAGE]: () => this.userService.handleUserSyncUsage(), [JobName.USER_SYNC_USAGE]: () => this.userService.handleUserSyncUsage(),
[JobName.QUEUE_ENCODE_CLIP]: (data) => this.smartInfoService.handleQueueEncodeClip(data), [JobName.QUEUE_SMART_SEARCH]: (data) => this.smartInfoService.handleQueueEncodeClip(data),
[JobName.ENCODE_CLIP]: (data) => this.smartInfoService.handleEncodeClip(data), [JobName.SMART_SEARCH]: (data) => this.smartInfoService.handleEncodeClip(data),
[JobName.STORAGE_TEMPLATE_MIGRATION]: () => this.storageTemplateService.handleMigration(), [JobName.STORAGE_TEMPLATE_MIGRATION]: () => this.storageTemplateService.handleMigration(),
[JobName.STORAGE_TEMPLATE_MIGRATION_SINGLE]: (data) => this.storageTemplateService.handleMigrationSingle(data), [JobName.STORAGE_TEMPLATE_MIGRATION_SINGLE]: (data) => this.storageTemplateService.handleMigrationSingle(data),
[JobName.QUEUE_MIGRATION]: () => this.mediaService.handleQueueMigration(), [JobName.QUEUE_MIGRATION]: () => this.mediaService.handleQueueMigration(),

View File

@ -85,7 +85,7 @@
icon: mdiImageSearch, icon: mdiImageSearch,
title: api.getJobName(JobName.SmartSearch), title: api.getJobName(JobName.SmartSearch),
subtitle: 'Run machine learning on assets to support smart search', subtitle: 'Run machine learning on assets to support smart search',
disabled: !$featureFlags.clipEncode, disabled: !$featureFlags.smartSearch,
}, },
[JobName.FaceDetection]: { [JobName.FaceDetection]: {
icon: mdiFaceRecognition, icon: mdiFaceRecognition,

View File

@ -15,11 +15,11 @@
$: showClearIcon = value.length > 0; $: showClearIcon = value.length > 0;
function onSearch() { function onSearch() {
let clipSearch = 'true'; let smartSearch = 'true';
let searchValue = value; let searchValue = value;
if (value.slice(0, 2) == 'm:') { if (value.slice(0, 2) == 'm:') {
clipSearch = 'false'; smartSearch = 'false';
searchValue = value.slice(2); searchValue = value.slice(2);
} }
@ -28,7 +28,7 @@
const params = new URLSearchParams({ const params = new URLSearchParams({
q: searchValue, q: searchValue,
clip: clipSearch, smart: smartSearch,
}); });
showBigSearchBar = false; showBigSearchBar = false;

View File

@ -63,7 +63,7 @@ export const dateFormats = {
export enum QueryParameter { export enum QueryParameter {
ACTION = 'action', ACTION = 'action',
ASSET_INDEX = 'assetIndex', ASSET_INDEX = 'assetIndex',
CLIP = 'clip', SMART_SEARCH = 'smartSearch',
MEMORY_INDEX = 'memoryIndex', MEMORY_INDEX = 'memoryIndex',
ONBOARDING_STEP = 'step', ONBOARDING_STEP = 'step',
OPEN_SETTING = 'openSetting', OPEN_SETTING = 'openSetting',

View File

@ -5,7 +5,7 @@ export type FeatureFlags = ServerFeaturesDto & { loaded: boolean };
export const featureFlags = writable<FeatureFlags>({ export const featureFlags = writable<FeatureFlags>({
loaded: false, loaded: false,
clipEncode: true, smartSearch: true,
facialRecognition: true, facialRecognition: true,
sidecar: true, sidecar: true,
map: true, map: true,

View File

@ -87,7 +87,7 @@
$: term = (() => { $: term = (() => {
let term = $page.url.searchParams.get(QueryParameter.SEARCH_TERM) || data.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 !== '') { if (isMetadataSearch && term !== '') {
term = `m:${term}`; term = `m:${term}`;
} }