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 162 additions and 119 deletions

View File

@ -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;
}
}

View File

@ -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),
),
);
});

View File

@ -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;

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';
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() {

View File

@ -66,7 +66,7 @@ This endpoint does not need any parameter.
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
# **search**
> SearchResponseDto search(clip, motion, q, query, recent, type, withArchived)
> SearchResponseDto search(clip, motion, q, query, recent, smart, type, withArchived)
@ -89,16 +89,17 @@ import 'package:openapi/api.dart';
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
final api_instance = SearchApi();
final clip = true; // bool |
final clip = true; // bool | @deprecated
final motion = true; // bool |
final q = q_example; // String |
final query = query_example; // String |
final recent = true; // bool |
final smart = true; // bool |
final type = type_example; // String |
final withArchived = true; // bool |
try {
final result = api_instance.search(clip, motion, q, query, recent, type, withArchived);
final result = api_instance.search(clip, motion, q, query, recent, smart, type, withArchived);
print(result);
} catch (e) {
print('Exception when calling SearchApi->search: $e\n');
@ -109,11 +110,12 @@ try {
Name | Type | Description | Notes
------------- | ------------- | ------------- | -------------
**clip** | **bool**| | [optional]
**clip** | **bool**| @deprecated | [optional]
**motion** | **bool**| | [optional]
**q** | **String**| | [optional]
**query** | **String**| | [optional]
**recent** | **bool**| | [optional]
**smart** | **bool**| | [optional]
**type** | **String**| | [optional]
**withArchived** | **bool**| | [optional]

View File

@ -8,7 +8,6 @@ import 'package:openapi/api.dart';
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**clipEncode** | **bool** | |
**configFile** | **bool** | |
**facialRecognition** | **bool** | |
**map** | **bool** | |
@ -18,6 +17,7 @@ Name | Type | Description | Notes
**reverseGeocoding** | **bool** | |
**search** | **bool** | |
**sidecar** | **bool** | |
**smartSearch** | **bool** | |
**trash** | **bool** | |
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)

View File

@ -64,6 +64,7 @@ class SearchApi {
/// Parameters:
///
/// * [bool] clip:
/// @deprecated
///
/// * [bool] motion:
///
@ -73,10 +74,12 @@ class SearchApi {
///
/// * [bool] recent:
///
/// * [bool] smart:
///
/// * [String] type:
///
/// * [bool] withArchived:
Future<Response> searchWithHttpInfo({ bool? clip, bool? motion, String? q, String? query, bool? recent, String? type, bool? withArchived, }) async {
Future<Response> searchWithHttpInfo({ bool? clip, bool? motion, String? q, String? query, bool? recent, bool? smart, String? type, bool? withArchived, }) async {
// ignore: prefer_const_declarations
final path = r'/search';
@ -102,6 +105,9 @@ class SearchApi {
if (recent != null) {
queryParams.addAll(_queryParams('', 'recent', recent));
}
if (smart != null) {
queryParams.addAll(_queryParams('', 'smart', smart));
}
if (type != null) {
queryParams.addAll(_queryParams('', 'type', type));
}
@ -126,6 +132,7 @@ class SearchApi {
/// Parameters:
///
/// * [bool] clip:
/// @deprecated
///
/// * [bool] motion:
///
@ -135,11 +142,13 @@ class SearchApi {
///
/// * [bool] recent:
///
/// * [bool] smart:
///
/// * [String] type:
///
/// * [bool] withArchived:
Future<SearchResponseDto?> search({ bool? clip, bool? motion, String? q, String? query, bool? recent, String? type, bool? withArchived, }) async {
final response = await searchWithHttpInfo( clip: clip, motion: motion, q: q, query: query, recent: recent, type: type, withArchived: withArchived, );
Future<SearchResponseDto?> search({ bool? clip, bool? motion, String? q, String? query, bool? recent, bool? smart, String? type, bool? withArchived, }) async {
final response = await searchWithHttpInfo( clip: clip, motion: motion, q: q, query: query, recent: recent, smart: smart, type: type, withArchived: withArchived, );
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}

View File

@ -13,7 +13,6 @@ part of openapi.api;
class ServerFeaturesDto {
/// Returns a new [ServerFeaturesDto] instance.
ServerFeaturesDto({
required this.clipEncode,
required this.configFile,
required this.facialRecognition,
required this.map,
@ -23,11 +22,10 @@ class ServerFeaturesDto {
required this.reverseGeocoding,
required this.search,
required this.sidecar,
required this.smartSearch,
required this.trash,
});
bool clipEncode;
bool configFile;
bool facialRecognition;
@ -46,11 +44,12 @@ class ServerFeaturesDto {
bool sidecar;
bool smartSearch;
bool trash;
@override
bool operator ==(Object other) => identical(this, other) || other is ServerFeaturesDto &&
other.clipEncode == clipEncode &&
other.configFile == configFile &&
other.facialRecognition == facialRecognition &&
other.map == map &&
@ -60,12 +59,12 @@ class ServerFeaturesDto {
other.reverseGeocoding == reverseGeocoding &&
other.search == search &&
other.sidecar == sidecar &&
other.smartSearch == smartSearch &&
other.trash == trash;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(clipEncode.hashCode) +
(configFile.hashCode) +
(facialRecognition.hashCode) +
(map.hashCode) +
@ -75,14 +74,14 @@ class ServerFeaturesDto {
(reverseGeocoding.hashCode) +
(search.hashCode) +
(sidecar.hashCode) +
(smartSearch.hashCode) +
(trash.hashCode);
@override
String toString() => 'ServerFeaturesDto[clipEncode=$clipEncode, configFile=$configFile, facialRecognition=$facialRecognition, map=$map, oauth=$oauth, oauthAutoLaunch=$oauthAutoLaunch, passwordLogin=$passwordLogin, reverseGeocoding=$reverseGeocoding, search=$search, sidecar=$sidecar, trash=$trash]';
String toString() => 'ServerFeaturesDto[configFile=$configFile, facialRecognition=$facialRecognition, map=$map, oauth=$oauth, oauthAutoLaunch=$oauthAutoLaunch, passwordLogin=$passwordLogin, reverseGeocoding=$reverseGeocoding, search=$search, sidecar=$sidecar, smartSearch=$smartSearch, trash=$trash]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
json[r'clipEncode'] = this.clipEncode;
json[r'configFile'] = this.configFile;
json[r'facialRecognition'] = this.facialRecognition;
json[r'map'] = this.map;
@ -92,6 +91,7 @@ class ServerFeaturesDto {
json[r'reverseGeocoding'] = this.reverseGeocoding;
json[r'search'] = this.search;
json[r'sidecar'] = this.sidecar;
json[r'smartSearch'] = this.smartSearch;
json[r'trash'] = this.trash;
return json;
}
@ -104,7 +104,6 @@ class ServerFeaturesDto {
final json = value.cast<String, dynamic>();
return ServerFeaturesDto(
clipEncode: mapValueOfType<bool>(json, r'clipEncode')!,
configFile: mapValueOfType<bool>(json, r'configFile')!,
facialRecognition: mapValueOfType<bool>(json, r'facialRecognition')!,
map: mapValueOfType<bool>(json, r'map')!,
@ -114,6 +113,7 @@ class ServerFeaturesDto {
reverseGeocoding: mapValueOfType<bool>(json, r'reverseGeocoding')!,
search: mapValueOfType<bool>(json, r'search')!,
sidecar: mapValueOfType<bool>(json, r'sidecar')!,
smartSearch: mapValueOfType<bool>(json, r'smartSearch')!,
trash: mapValueOfType<bool>(json, r'trash')!,
);
}
@ -162,7 +162,6 @@ class ServerFeaturesDto {
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
'clipEncode',
'configFile',
'facialRecognition',
'map',
@ -172,6 +171,7 @@ class ServerFeaturesDto {
'reverseGeocoding',
'search',
'sidecar',
'smartSearch',
'trash',
};
}

View File

@ -22,7 +22,7 @@ void main() {
// TODO
});
//Future<SearchResponseDto> search({ bool clip, bool motion, String q, String query, bool recent, String type, bool withArchived }) async
//Future<SearchResponseDto> search({ bool clip, bool motion, String q, String query, bool recent, bool smart, String type, bool withArchived }) async
test('test search', () async {
// TODO
});

View File

@ -16,11 +16,6 @@ void main() {
// final instance = ServerFeaturesDto();
group('test ServerFeaturesDto', () {
// bool clipEncode
test('to test the property `clipEncode`', () async {
// TODO
});
// bool configFile
test('to test the property `configFile`', () async {
// TODO
@ -66,6 +61,11 @@ void main() {
// TODO
});
// bool smartSearch
test('to test the property `smartSearch`', () async {
// TODO
});
// bool trash
test('to test the property `trash`', () async {
// TODO

View File

@ -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"

View File

@ -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));
}
/**

View File

@ -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,

View File

@ -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,

View File

@ -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,
},
{

View File

@ -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 },
];

View File

@ -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',

View File

@ -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 }

View File

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

View File

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

View File

@ -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');
});
});
});

View File

@ -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 },

View File

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

View File

@ -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,

View File

@ -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();
});
});

View File

@ -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;

View File

@ -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,

View File

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

View File

@ -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(),

View File

@ -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,

View File

@ -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;

View File

@ -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',

View File

@ -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,

View File

@ -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}`;
}