1
0
Fork 0
mirror of https://github.com/immich-app/immich.git synced 2025-03-01 15:11:21 +01:00

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
This commit is contained in:
Jason Rasmussen 2023-03-20 16:16:32 -04:00 committed by GitHub
parent deb1e7f41f
commit 73a2063d96
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 423 additions and 20 deletions

View file

@ -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) [[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** # **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'; //defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKeyPrefix = 'Bearer';
final api_instance = SearchApi(); 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 { 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); print(result);
} catch (e) { } catch (e) {
print('Exception when calling SearchApi->search: $e\n'); print('Exception when calling SearchApi->search: $e\n');
@ -144,7 +158,23 @@ try {
``` ```
### Parameters ### 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 ### Return type

View file

@ -110,7 +110,37 @@ class SearchApi {
/// ///
/// ///
/// Note: This method returns the HTTP [Response]. /// 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 // ignore: prefer_const_declarations
final path = r'/search'; final path = r'/search';
@ -121,6 +151,49 @@ class SearchApi {
final headerParams = <String, String>{}; final headerParams = <String, String>{};
final formParams = <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>[]; 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) { if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response)); throw ApiException(response.statusCode, await _decodeBodyBytes(response));
} }

View file

@ -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 { test('test search', () async {
// TODO // TODO
}); });

View file

@ -20,7 +20,7 @@ export class SearchController {
@Get() @Get()
async search( async search(
@GetAuthUser() authUser: AuthUserDto, @GetAuthUser() authUser: AuthUserDto,
@Query(new ValidationPipe({ transform: true })) dto: SearchDto | any, @Query(new ValidationPipe({ transform: true })) dto: SearchDto,
): Promise<SearchResponseDto> { ): Promise<SearchResponseDto> {
return this.searchService.search(authUser, dto); return this.searchService.search(authUser, dto);
} }

View file

@ -620,7 +620,132 @@
"get": { "get": {
"operationId": "search", "operationId": "search",
"description": "", "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": { "responses": {
"200": { "200": {
"description": "", "description": "",

View file

@ -145,7 +145,15 @@ export class SearchService {
// TODO: do this in batches based on searchIndexVersion // TODO: do this in batches based on searchIndexVersion
const assets = this.patchAssets(await this.assetRepository.getAll({ isVisible: true })); const assets = this.patchAssets(await this.assetRepository.getAll({ isVisible: true }));
this.logger.log(`Indexing ${assets.length} assets`); 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'); this.logger.debug('Finished re-indexing all assets');
} catch (error: any) { } catch (error: any) {
this.logger.error(`Unable to index all assets`, error?.stack); this.logger.error(`Unable to index all assets`, error?.stack);

View file

@ -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) { private getAssetFilters(filters: SearchFilter) {
@ -382,6 +386,11 @@ export class TypesenseRepository implements ISearchRepository {
_filters.push(`${item.name}:${value}`); _filters.push(`${item.name}:${value}`);
} }
} }
return _filters.join(' && ');
const result = _filters.join(' && ');
this.logger.debug(`Asset filters are: ${result}`);
return result;
} }
} }

View file

@ -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. * @param {*} [options] Override http request option.
* @throws {RequiredError} * @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`; const localVarPath = `/search`;
// use dummy base URL string because the URL constructor only accepts absolute URLs. // use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
@ -6783,6 +6797,62 @@ export const SearchApiAxiosParamCreator = function (configuration?: Configuratio
// authentication cookie required // 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); 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. * @param {*} [options] Override http request option.
* @throws {RequiredError} * @throws {RequiredError}
*/ */
async search(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<SearchResponseDto>> { 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(options); 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); 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. * @param {*} [options] Override http request option.
* @throws {RequiredError} * @throws {RequiredError}
*/ */
search(options?: any): AxiosPromise<SearchResponseDto> { 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(options).then((request) => request(axios, basePath)); 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. * @param {*} [options] Override http request option.
* @throws {RequiredError} * @throws {RequiredError}
* @memberof SearchApi * @memberof SearchApi
*/ */
public search(options?: AxiosRequestConfig) { 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(options).then((request) => request(this.axios, this.basePath)); 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));
} }
} }

View file

@ -9,7 +9,23 @@ export const load = (async ({ locals, parent, url }) => {
const term = url.searchParams.get('q') || url.searchParams.get('query') || undefined; 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 { return {
user, user,