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:
parent
e5a70329c9
commit
ae7f174948
@ -5,14 +5,14 @@ class SearchResultPageState {
|
||||
final bool isLoading;
|
||||
final bool isSuccess;
|
||||
final bool isError;
|
||||
final bool isClip;
|
||||
final bool isSmart;
|
||||
final List<Asset> 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<Asset>? 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;
|
||||
}
|
||||
}
|
||||
|
@ -14,13 +14,13 @@ class SearchResultPageNotifier extends StateNotifier<SearchResultPageState> {
|
||||
isError: false,
|
||||
isLoading: true,
|
||||
isSuccess: false,
|
||||
isClip: false,
|
||||
isSmart: false,
|
||||
),
|
||||
);
|
||||
|
||||
final SearchService _searchService;
|
||||
|
||||
Future<void> search(String searchTerm, {bool clipEnable = true}) async {
|
||||
Future<void> search(String searchTerm, {bool smartSearch = true}) async {
|
||||
state = state.copyWith(
|
||||
searchResult: [],
|
||||
isError: false,
|
||||
@ -28,10 +28,8 @@ class SearchResultPageNotifier extends StateNotifier<SearchResultPageState> {
|
||||
isSuccess: false,
|
||||
);
|
||||
|
||||
List<Asset>? assets = await _searchService.searchAsset(
|
||||
searchTerm,
|
||||
clipEnable: clipEnable,
|
||||
);
|
||||
List<Asset>? assets =
|
||||
await _searchService.searchAsset(searchTerm, smartSearch: smartSearch);
|
||||
|
||||
if (assets != null) {
|
||||
state = state.copyWith(
|
||||
@ -39,7 +37,7 @@ class SearchResultPageNotifier extends StateNotifier<SearchResultPageState> {
|
||||
isError: false,
|
||||
isLoading: false,
|
||||
isSuccess: true,
|
||||
isClip: clipEnable,
|
||||
isSmart: smartSearch,
|
||||
);
|
||||
} else {
|
||||
state = state.copyWith(
|
||||
@ -47,7 +45,7 @@ class SearchResultPageNotifier extends StateNotifier<SearchResultPageState> {
|
||||
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),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
@ -31,13 +31,13 @@ class SearchService {
|
||||
|
||||
Future<List<Asset>?> 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;
|
||||
|
@ -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() {
|
||||
|
BIN
mobile/openapi/doc/SearchApi.md
generated
BIN
mobile/openapi/doc/SearchApi.md
generated
Binary file not shown.
BIN
mobile/openapi/doc/ServerFeaturesDto.md
generated
BIN
mobile/openapi/doc/ServerFeaturesDto.md
generated
Binary file not shown.
BIN
mobile/openapi/lib/api/search_api.dart
generated
BIN
mobile/openapi/lib/api/search_api.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/model/server_features_dto.dart
generated
BIN
mobile/openapi/lib/model/server_features_dto.dart
generated
Binary file not shown.
BIN
mobile/openapi/test/search_api_test.dart
generated
BIN
mobile/openapi/test/search_api_test.dart
generated
Binary file not shown.
BIN
mobile/openapi/test/server_features_dto_test.dart
generated
BIN
mobile/openapi/test/server_features_dto_test.dart
generated
Binary file not shown.
@ -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"
|
||||
|
41
open-api/typescript-sdk/client/api.ts
generated
41
open-api/typescript-sdk/client/api.ts
generated
@ -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<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`;
|
||||
// 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<SearchResponseDto>> {
|
||||
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<SearchResponseDto>> {
|
||||
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<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 {
|
||||
/**
|
||||
*
|
||||
* @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));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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,
|
||||
|
@ -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, QueueName> = {
|
||||
[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,
|
||||
|
@ -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,
|
||||
},
|
||||
{
|
||||
|
@ -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 },
|
||||
];
|
||||
|
||||
|
@ -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',
|
||||
|
@ -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 }
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { AssetType } from '@app/infra/entities';
|
||||
|
||||
export enum SearchStrategy {
|
||||
CLIP = 'CLIP',
|
||||
SMART = 'SMART',
|
||||
TEXT = 'TEXT',
|
||||
}
|
||||
|
||||
|
@ -14,6 +14,12 @@ export class SearchDto {
|
||||
@Optional()
|
||||
query?: string;
|
||||
|
||||
@IsBoolean()
|
||||
@Optional()
|
||||
@Transform(toBoolean)
|
||||
smart?: boolean;
|
||||
|
||||
/** @deprecated */
|
||||
@IsBoolean()
|
||||
@Optional()
|
||||
@Transform(toBoolean)
|
||||
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -56,23 +56,26 @@ export class SearchService {
|
||||
}
|
||||
|
||||
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');
|
||||
}
|
||||
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 },
|
||||
|
@ -93,7 +93,7 @@ export class ServerConfigDto {
|
||||
}
|
||||
|
||||
export class ServerFeaturesDto implements FeatureFlags {
|
||||
clipEncode!: boolean;
|
||||
smartSearch!: boolean;
|
||||
configFile!: boolean;
|
||||
facialRecognition!: boolean;
|
||||
map!: boolean;
|
||||
|
@ -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,
|
||||
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
@ -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;
|
||||
|
@ -136,7 +136,7 @@ export const defaults = Object.freeze<SystemConfig>({
|
||||
});
|
||||
|
||||
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,
|
||||
|
@ -501,7 +501,7 @@ export class AssetRepository implements IAssetRepository {
|
||||
};
|
||||
break;
|
||||
|
||||
case WithoutProperty.CLIP_ENCODING:
|
||||
case WithoutProperty.SMART_SEARCH:
|
||||
relations = {
|
||||
smartSearch: true,
|
||||
};
|
||||
|
@ -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(),
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
|
@ -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',
|
||||
|
@ -5,7 +5,7 @@ export type FeatureFlags = ServerFeaturesDto & { loaded: boolean };
|
||||
|
||||
export const featureFlags = writable<FeatureFlags>({
|
||||
loaded: false,
|
||||
clipEncode: true,
|
||||
smartSearch: true,
|
||||
facialRecognition: true,
|
||||
sidecar: true,
|
||||
map: true,
|
||||
|
@ -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}`;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user