Jelajahi Sumber

fix(server): search and explore issues (#2029)

* fix: send assets to typesense in batches

* fix: run classs transformer on search endpoint

* chore: log typesense filters
Jason Rasmussen 2 tahun lalu
induk
melakukan
73a2063d96

+ 33 - 3
mobile/openapi/doc/SearchApi.md

@@ -113,7 +113,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()
+> SearchResponseDto search(q, query, clip, type, isFavorite, exifInfoPeriodCity, exifInfoPeriodState, exifInfoPeriodCountry, exifInfoPeriodMake, exifInfoPeriodModel, smartInfoPeriodObjects, smartInfoPeriodTags, recent, motion)
 
 
 
@@ -134,9 +134,23 @@ import 'package:openapi/api.dart';
 //defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKeyPrefix = 'Bearer';
 
 final api_instance = SearchApi();
+final q = q_example; // String | 
+final query = query_example; // String | 
+final clip = true; // bool | 
+final type = type_example; // String | 
+final isFavorite = true; // bool | 
+final exifInfoPeriodCity = exifInfoPeriodCity_example; // String | 
+final exifInfoPeriodState = exifInfoPeriodState_example; // String | 
+final exifInfoPeriodCountry = exifInfoPeriodCountry_example; // String | 
+final exifInfoPeriodMake = exifInfoPeriodMake_example; // String | 
+final exifInfoPeriodModel = exifInfoPeriodModel_example; // String | 
+final smartInfoPeriodObjects = []; // List<String> | 
+final smartInfoPeriodTags = []; // List<String> | 
+final recent = true; // bool | 
+final motion = true; // bool | 
 
 try {
-    final result = api_instance.search();
+    final result = api_instance.search(q, query, clip, type, isFavorite, exifInfoPeriodCity, exifInfoPeriodState, exifInfoPeriodCountry, exifInfoPeriodMake, exifInfoPeriodModel, smartInfoPeriodObjects, smartInfoPeriodTags, recent, motion);
     print(result);
 } catch (e) {
     print('Exception when calling SearchApi->search: $e\n');
@@ -144,7 +158,23 @@ try {
 ```
 
 ### Parameters
-This endpoint does not need any parameter.
+
+Name | Type | Description  | Notes
+------------- | ------------- | ------------- | -------------
+ **q** | **String**|  | [optional] 
+ **query** | **String**|  | [optional] 
+ **clip** | **bool**|  | [optional] 
+ **type** | **String**|  | [optional] 
+ **isFavorite** | **bool**|  | [optional] 
+ **exifInfoPeriodCity** | **String**|  | [optional] 
+ **exifInfoPeriodState** | **String**|  | [optional] 
+ **exifInfoPeriodCountry** | **String**|  | [optional] 
+ **exifInfoPeriodMake** | **String**|  | [optional] 
+ **exifInfoPeriodModel** | **String**|  | [optional] 
+ **smartInfoPeriodObjects** | [**List<String>**](String.md)|  | [optional] [default to const []]
+ **smartInfoPeriodTags** | [**List<String>**](String.md)|  | [optional] [default to const []]
+ **recent** | **bool**|  | [optional] 
+ **motion** | **bool**|  | [optional] 
 
 ### Return type
 

+ 106 - 3
mobile/openapi/lib/api/search_api.dart

@@ -110,7 +110,37 @@ class SearchApi {
   /// 
   ///
   /// Note: This method returns the HTTP [Response].
-  Future<Response> searchWithHttpInfo() async {
+  ///
+  /// Parameters:
+  ///
+  /// * [String] q:
+  ///
+  /// * [String] query:
+  ///
+  /// * [bool] clip:
+  ///
+  /// * [String] type:
+  ///
+  /// * [bool] isFavorite:
+  ///
+  /// * [String] exifInfoPeriodCity:
+  ///
+  /// * [String] exifInfoPeriodState:
+  ///
+  /// * [String] exifInfoPeriodCountry:
+  ///
+  /// * [String] exifInfoPeriodMake:
+  ///
+  /// * [String] exifInfoPeriodModel:
+  ///
+  /// * [List<String>] smartInfoPeriodObjects:
+  ///
+  /// * [List<String>] smartInfoPeriodTags:
+  ///
+  /// * [bool] recent:
+  ///
+  /// * [bool] motion:
+  Future<Response> searchWithHttpInfo({ String? q, String? query, bool? clip, String? type, bool? isFavorite, String? exifInfoPeriodCity, String? exifInfoPeriodState, String? exifInfoPeriodCountry, String? exifInfoPeriodMake, String? exifInfoPeriodModel, List<String>? smartInfoPeriodObjects, List<String>? smartInfoPeriodTags, bool? recent, bool? motion, }) async {
     // ignore: prefer_const_declarations
     final path = r'/search';
 
@@ -121,6 +151,49 @@ class SearchApi {
     final headerParams = <String, String>{};
     final formParams = <String, String>{};
 
+    if (q != null) {
+      queryParams.addAll(_queryParams('', 'q', q));
+    }
+    if (query != null) {
+      queryParams.addAll(_queryParams('', 'query', query));
+    }
+    if (clip != null) {
+      queryParams.addAll(_queryParams('', 'clip', clip));
+    }
+    if (type != null) {
+      queryParams.addAll(_queryParams('', 'type', type));
+    }
+    if (isFavorite != null) {
+      queryParams.addAll(_queryParams('', 'isFavorite', isFavorite));
+    }
+    if (exifInfoPeriodCity != null) {
+      queryParams.addAll(_queryParams('', 'exifInfo.city', exifInfoPeriodCity));
+    }
+    if (exifInfoPeriodState != null) {
+      queryParams.addAll(_queryParams('', 'exifInfo.state', exifInfoPeriodState));
+    }
+    if (exifInfoPeriodCountry != null) {
+      queryParams.addAll(_queryParams('', 'exifInfo.country', exifInfoPeriodCountry));
+    }
+    if (exifInfoPeriodMake != null) {
+      queryParams.addAll(_queryParams('', 'exifInfo.make', exifInfoPeriodMake));
+    }
+    if (exifInfoPeriodModel != null) {
+      queryParams.addAll(_queryParams('', 'exifInfo.model', exifInfoPeriodModel));
+    }
+    if (smartInfoPeriodObjects != null) {
+      queryParams.addAll(_queryParams('multi', 'smartInfo.objects', smartInfoPeriodObjects));
+    }
+    if (smartInfoPeriodTags != null) {
+      queryParams.addAll(_queryParams('multi', 'smartInfo.tags', smartInfoPeriodTags));
+    }
+    if (recent != null) {
+      queryParams.addAll(_queryParams('', 'recent', recent));
+    }
+    if (motion != null) {
+      queryParams.addAll(_queryParams('', 'motion', motion));
+    }
+
     const contentTypes = <String>[];
 
 
@@ -136,8 +209,38 @@ class SearchApi {
   }
 
   /// 
-  Future<SearchResponseDto?> search() async {
-    final response = await searchWithHttpInfo();
+  ///
+  /// Parameters:
+  ///
+  /// * [String] q:
+  ///
+  /// * [String] query:
+  ///
+  /// * [bool] clip:
+  ///
+  /// * [String] type:
+  ///
+  /// * [bool] isFavorite:
+  ///
+  /// * [String] exifInfoPeriodCity:
+  ///
+  /// * [String] exifInfoPeriodState:
+  ///
+  /// * [String] exifInfoPeriodCountry:
+  ///
+  /// * [String] exifInfoPeriodMake:
+  ///
+  /// * [String] exifInfoPeriodModel:
+  ///
+  /// * [List<String>] smartInfoPeriodObjects:
+  ///
+  /// * [List<String>] smartInfoPeriodTags:
+  ///
+  /// * [bool] recent:
+  ///
+  /// * [bool] motion:
+  Future<SearchResponseDto?> search({ String? q, String? query, bool? clip, String? type, bool? isFavorite, String? exifInfoPeriodCity, String? exifInfoPeriodState, String? exifInfoPeriodCountry, String? exifInfoPeriodMake, String? exifInfoPeriodModel, List<String>? smartInfoPeriodObjects, List<String>? smartInfoPeriodTags, bool? recent, bool? motion, }) async {
+    final response = await searchWithHttpInfo( q: q, query: query, clip: clip, type: type, isFavorite: isFavorite, exifInfoPeriodCity: exifInfoPeriodCity, exifInfoPeriodState: exifInfoPeriodState, exifInfoPeriodCountry: exifInfoPeriodCountry, exifInfoPeriodMake: exifInfoPeriodMake, exifInfoPeriodModel: exifInfoPeriodModel, smartInfoPeriodObjects: smartInfoPeriodObjects, smartInfoPeriodTags: smartInfoPeriodTags, recent: recent, motion: motion, );
     if (response.statusCode >= HttpStatus.badRequest) {
       throw ApiException(response.statusCode, await _decodeBodyBytes(response));
     }

+ 1 - 1
mobile/openapi/test/search_api_test.dart

@@ -33,7 +33,7 @@ void main() {
 
     // 
     //
-    //Future<SearchResponseDto> search() async
+    //Future<SearchResponseDto> search({ String q, String query, bool clip, String type, bool isFavorite, String exifInfoPeriodCity, String exifInfoPeriodState, String exifInfoPeriodCountry, String exifInfoPeriodMake, String exifInfoPeriodModel, List<String> smartInfoPeriodObjects, List<String> smartInfoPeriodTags, bool recent, bool motion }) async
     test('test search', () async {
       // TODO
     });

+ 1 - 1
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<SearchResponseDto> {
     return this.searchService.search(authUser, dto);
   }

+ 126 - 1
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": "",

+ 9 - 1
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);

+ 11 - 2
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;
   }
 }

+ 119 - 7
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<string>} [smartInfoObjects] 
+         * @param {Array<string>} [smartInfoTags] 
+         * @param {boolean} [recent] 
+         * @param {boolean} [motion] 
          * @param {*} [options] Override http request option.
          * @throws {RequiredError}
          */
-        search: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
+        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<string>, smartInfoTags?: Array<string>, recent?: boolean, motion?: boolean, options: AxiosRequestConfig = {}): 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);
@@ -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<string>} [smartInfoObjects] 
+         * @param {Array<string>} [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<SearchResponseDto>> {
-            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<string>, smartInfoTags?: Array<string>, recent?: boolean, motion?: boolean, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<SearchResponseDto>> {
+            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<string>} [smartInfoObjects] 
+         * @param {Array<string>} [smartInfoTags] 
+         * @param {boolean} [recent] 
+         * @param {boolean} [motion] 
          * @param {*} [options] Override http request option.
          * @throws {RequiredError}
          */
-        search(options?: any): AxiosPromise<SearchResponseDto> {
-            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<string>, smartInfoTags?: Array<string>, recent?: boolean, motion?: boolean, options?: any): AxiosPromise<SearchResponseDto> {
+            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<string>} [smartInfoObjects] 
+     * @param {Array<string>} [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<string>, smartInfoTags?: Array<string>, 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));
     }
 }
 

+ 17 - 1
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,