diff --git a/mobile/openapi/doc/SearchApi.md b/mobile/openapi/doc/SearchApi.md index 2a8a5733b6..8ba8a88088 100644 Binary files a/mobile/openapi/doc/SearchApi.md and b/mobile/openapi/doc/SearchApi.md differ diff --git a/mobile/openapi/lib/api/search_api.dart b/mobile/openapi/lib/api/search_api.dart index c50dc04890..645ccf876b 100644 Binary files a/mobile/openapi/lib/api/search_api.dart and b/mobile/openapi/lib/api/search_api.dart differ diff --git a/mobile/openapi/test/search_api_test.dart b/mobile/openapi/test/search_api_test.dart index ba9adaec59..89037f3c49 100644 Binary files a/mobile/openapi/test/search_api_test.dart and b/mobile/openapi/test/search_api_test.dart differ diff --git a/server/apps/immich/src/controllers/search.controller.ts b/server/apps/immich/src/controllers/search.controller.ts index d172c7592b..2c2248c3fc 100644 --- a/server/apps/immich/src/controllers/search.controller.ts +++ b/server/apps/immich/src/controllers/search.controller.ts @@ -20,7 +20,7 @@ export class SearchController { @Get() async search( @GetAuthUser() authUser: AuthUserDto, - @Query(new ValidationPipe({ transform: true })) dto: SearchDto | any, + @Query(new ValidationPipe({ transform: true })) dto: SearchDto, ): Promise { return this.searchService.search(authUser, dto); } diff --git a/server/immich-openapi-specs.json b/server/immich-openapi-specs.json index 2991174870..2d61b30803 100644 --- a/server/immich-openapi-specs.json +++ b/server/immich-openapi-specs.json @@ -620,7 +620,132 @@ "get": { "operationId": "search", "description": "", - "parameters": [], + "parameters": [ + { + "name": "q", + "required": false, + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "query", + "required": false, + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "clip", + "required": false, + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "type", + "required": false, + "in": "query", + "schema": { + "enum": [ + "IMAGE", + "VIDEO", + "AUDIO", + "OTHER" + ], + "type": "string" + } + }, + { + "name": "isFavorite", + "required": false, + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "exifInfo.city", + "required": false, + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "exifInfo.state", + "required": false, + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "exifInfo.country", + "required": false, + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "exifInfo.make", + "required": false, + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "exifInfo.model", + "required": false, + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "smartInfo.objects", + "required": false, + "in": "query", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + { + "name": "smartInfo.tags", + "required": false, + "in": "query", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + { + "name": "recent", + "required": false, + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "motion", + "required": false, + "in": "query", + "schema": { + "type": "boolean" + } + } + ], "responses": { "200": { "description": "", diff --git a/server/libs/domain/src/search/search.service.ts b/server/libs/domain/src/search/search.service.ts index dab2f5ad3c..a9a18a835f 100644 --- a/server/libs/domain/src/search/search.service.ts +++ b/server/libs/domain/src/search/search.service.ts @@ -145,7 +145,15 @@ export class SearchService { // TODO: do this in batches based on searchIndexVersion const assets = this.patchAssets(await this.assetRepository.getAll({ isVisible: true })); this.logger.log(`Indexing ${assets.length} assets`); - await this.searchRepository.importAssets(assets, true); + + const chunkSize = 1000; + for (let i = 0; i < assets.length; i += chunkSize) { + const end = i + chunkSize; + const chunk = assets.slice(i, end); + const done = end >= assets.length - 1; + await this.searchRepository.importAssets(chunk, done); + } + this.logger.debug('Finished re-indexing all assets'); } catch (error: any) { this.logger.error(`Unable to index all assets`, error?.stack); diff --git a/server/libs/infra/src/search/typesense.repository.ts b/server/libs/infra/src/search/typesense.repository.ts index f5bb23d8eb..91c801c54f 100644 --- a/server/libs/infra/src/search/typesense.repository.ts +++ b/server/libs/infra/src/search/typesense.repository.ts @@ -365,7 +365,11 @@ export class TypesenseRepository implements ISearchRepository { } } - return _filters.join(' && '); + const result = _filters.join(' && '); + + this.logger.debug(`Album filters are: ${result}`); + + return result; } private getAssetFilters(filters: SearchFilter) { @@ -382,6 +386,11 @@ export class TypesenseRepository implements ISearchRepository { _filters.push(`${item.name}:${value}`); } } - return _filters.join(' && '); + + const result = _filters.join(' && '); + + this.logger.debug(`Asset filters are: ${result}`); + + return result; } } diff --git a/web/src/api/open-api/api.ts b/web/src/api/open-api/api.ts index 038ff5e55a..f66c191d9a 100644 --- a/web/src/api/open-api/api.ts +++ b/web/src/api/open-api/api.ts @@ -6761,10 +6761,24 @@ export const SearchApiAxiosParamCreator = function (configuration?: Configuratio }, /** * + * @param {string} [q] + * @param {string} [query] + * @param {boolean} [clip] + * @param {'IMAGE' | 'VIDEO' | 'AUDIO' | 'OTHER'} [type] + * @param {boolean} [isFavorite] + * @param {string} [exifInfoCity] + * @param {string} [exifInfoState] + * @param {string} [exifInfoCountry] + * @param {string} [exifInfoMake] + * @param {string} [exifInfoModel] + * @param {Array} [smartInfoObjects] + * @param {Array} [smartInfoTags] + * @param {boolean} [recent] + * @param {boolean} [motion] * @param {*} [options] Override http request option. * @throws {RequiredError} */ - search: async (options: AxiosRequestConfig = {}): Promise => { + search: async (q?: string, query?: string, clip?: boolean, type?: 'IMAGE' | 'VIDEO' | 'AUDIO' | 'OTHER', isFavorite?: boolean, exifInfoCity?: string, exifInfoState?: string, exifInfoCountry?: string, exifInfoMake?: string, exifInfoModel?: string, smartInfoObjects?: Array, smartInfoTags?: Array, recent?: boolean, motion?: boolean, options: AxiosRequestConfig = {}): Promise => { const localVarPath = `/search`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); @@ -6783,6 +6797,62 @@ export const SearchApiAxiosParamCreator = function (configuration?: Configuratio // authentication cookie required + if (q !== undefined) { + localVarQueryParameter['q'] = q; + } + + if (query !== undefined) { + localVarQueryParameter['query'] = query; + } + + if (clip !== undefined) { + localVarQueryParameter['clip'] = clip; + } + + if (type !== undefined) { + localVarQueryParameter['type'] = type; + } + + if (isFavorite !== undefined) { + localVarQueryParameter['isFavorite'] = isFavorite; + } + + if (exifInfoCity !== undefined) { + localVarQueryParameter['exifInfo.city'] = exifInfoCity; + } + + if (exifInfoState !== undefined) { + localVarQueryParameter['exifInfo.state'] = exifInfoState; + } + + if (exifInfoCountry !== undefined) { + localVarQueryParameter['exifInfo.country'] = exifInfoCountry; + } + + if (exifInfoMake !== undefined) { + localVarQueryParameter['exifInfo.make'] = exifInfoMake; + } + + if (exifInfoModel !== undefined) { + localVarQueryParameter['exifInfo.model'] = exifInfoModel; + } + + if (smartInfoObjects) { + localVarQueryParameter['smartInfo.objects'] = smartInfoObjects; + } + + if (smartInfoTags) { + localVarQueryParameter['smartInfo.tags'] = smartInfoTags; + } + + if (recent !== undefined) { + localVarQueryParameter['recent'] = recent; + } + + if (motion !== undefined) { + localVarQueryParameter['motion'] = motion; + } + setSearchParams(localVarUrlObj, localVarQueryParameter); @@ -6824,11 +6894,25 @@ export const SearchApiFp = function(configuration?: Configuration) { }, /** * + * @param {string} [q] + * @param {string} [query] + * @param {boolean} [clip] + * @param {'IMAGE' | 'VIDEO' | 'AUDIO' | 'OTHER'} [type] + * @param {boolean} [isFavorite] + * @param {string} [exifInfoCity] + * @param {string} [exifInfoState] + * @param {string} [exifInfoCountry] + * @param {string} [exifInfoMake] + * @param {string} [exifInfoModel] + * @param {Array} [smartInfoObjects] + * @param {Array} [smartInfoTags] + * @param {boolean} [recent] + * @param {boolean} [motion] * @param {*} [options] Override http request option. * @throws {RequiredError} */ - async search(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { - const localVarAxiosArgs = await localVarAxiosParamCreator.search(options); + async search(q?: string, query?: string, clip?: boolean, type?: 'IMAGE' | 'VIDEO' | 'AUDIO' | 'OTHER', isFavorite?: boolean, exifInfoCity?: string, exifInfoState?: string, exifInfoCountry?: string, exifInfoMake?: string, exifInfoModel?: string, smartInfoObjects?: Array, smartInfoTags?: Array, recent?: boolean, motion?: boolean, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.search(q, query, clip, type, isFavorite, exifInfoCity, exifInfoState, exifInfoCountry, exifInfoMake, exifInfoModel, smartInfoObjects, smartInfoTags, recent, motion, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, } @@ -6859,11 +6943,25 @@ export const SearchApiFactory = function (configuration?: Configuration, basePat }, /** * + * @param {string} [q] + * @param {string} [query] + * @param {boolean} [clip] + * @param {'IMAGE' | 'VIDEO' | 'AUDIO' | 'OTHER'} [type] + * @param {boolean} [isFavorite] + * @param {string} [exifInfoCity] + * @param {string} [exifInfoState] + * @param {string} [exifInfoCountry] + * @param {string} [exifInfoMake] + * @param {string} [exifInfoModel] + * @param {Array} [smartInfoObjects] + * @param {Array} [smartInfoTags] + * @param {boolean} [recent] + * @param {boolean} [motion] * @param {*} [options] Override http request option. * @throws {RequiredError} */ - search(options?: any): AxiosPromise { - return localVarFp.search(options).then((request) => request(axios, basePath)); + search(q?: string, query?: string, clip?: boolean, type?: 'IMAGE' | 'VIDEO' | 'AUDIO' | 'OTHER', isFavorite?: boolean, exifInfoCity?: string, exifInfoState?: string, exifInfoCountry?: string, exifInfoMake?: string, exifInfoModel?: string, smartInfoObjects?: Array, smartInfoTags?: Array, recent?: boolean, motion?: boolean, options?: any): AxiosPromise { + return localVarFp.search(q, query, clip, type, isFavorite, exifInfoCity, exifInfoState, exifInfoCountry, exifInfoMake, exifInfoModel, smartInfoObjects, smartInfoTags, recent, motion, options).then((request) => request(axios, basePath)); }, }; }; @@ -6897,12 +6995,26 @@ export class SearchApi extends BaseAPI { /** * + * @param {string} [q] + * @param {string} [query] + * @param {boolean} [clip] + * @param {'IMAGE' | 'VIDEO' | 'AUDIO' | 'OTHER'} [type] + * @param {boolean} [isFavorite] + * @param {string} [exifInfoCity] + * @param {string} [exifInfoState] + * @param {string} [exifInfoCountry] + * @param {string} [exifInfoMake] + * @param {string} [exifInfoModel] + * @param {Array} [smartInfoObjects] + * @param {Array} [smartInfoTags] + * @param {boolean} [recent] + * @param {boolean} [motion] * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof SearchApi */ - public search(options?: AxiosRequestConfig) { - return SearchApiFp(this.configuration).search(options).then((request) => request(this.axios, this.basePath)); + public search(q?: string, query?: string, clip?: boolean, type?: 'IMAGE' | 'VIDEO' | 'AUDIO' | 'OTHER', isFavorite?: boolean, exifInfoCity?: string, exifInfoState?: string, exifInfoCountry?: string, exifInfoMake?: string, exifInfoModel?: string, smartInfoObjects?: Array, smartInfoTags?: Array, recent?: boolean, motion?: boolean, options?: AxiosRequestConfig) { + return SearchApiFp(this.configuration).search(q, query, clip, type, isFavorite, exifInfoCity, exifInfoState, exifInfoCountry, exifInfoMake, exifInfoModel, smartInfoObjects, smartInfoTags, recent, motion, options).then((request) => request(this.axios, this.basePath)); } } diff --git a/web/src/routes/(user)/search/+page.server.ts b/web/src/routes/(user)/search/+page.server.ts index 71122f4582..790cceae21 100644 --- a/web/src/routes/(user)/search/+page.server.ts +++ b/web/src/routes/(user)/search/+page.server.ts @@ -9,7 +9,23 @@ export const load = (async ({ locals, parent, url }) => { const term = url.searchParams.get('q') || url.searchParams.get('query') || undefined; - const { data: results } = await locals.api.searchApi.search({ params: url.searchParams }); + const { data: results } = await locals.api.searchApi.search( + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + { params: url.searchParams } + ); return { user,