feat(server, web): smart search filtering and pagination (#6525)

* initial pagination impl

* use limit + offset instead of take + skip

* wip web pagination

* working infinite scroll

* update api

* formatting

* fix rebase

* search refactor

* re-add runtime config for vector search

* fix rebase

* fixes

* useless omitBy

* unnecessary handling

* add sql decorator for `searchAssets`

* fixed search builder

* fixed sql

* remove mock method

* linting

* fixed pagination

* fixed unit tests

* formatting

* fix e2e tests

* re-flatten search builder

* refactor endpoints

* clean up dto

* refinements

* don't break everything just yet

* update openapi spec & sql

* update api

* linting

* update sql

* fixes

* optimize web code

* fix typing

* add page limit

* make limit based on asset count

* increase limit

* simpler import
This commit is contained in:
Mert 2024-02-12 20:50:47 -05:00 committed by GitHub
parent f1e4fdf175
commit e334443919
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
54 changed files with 3993 additions and 790 deletions

View File

@ -162,7 +162,9 @@ Class | Method | HTTP request | Description
*PersonApi* | [**updatePerson**](doc//PersonApi.md#updateperson) | **PUT** /person/{id} | *PersonApi* | [**updatePerson**](doc//PersonApi.md#updateperson) | **PUT** /person/{id} |
*SearchApi* | [**getExploreData**](doc//SearchApi.md#getexploredata) | **GET** /search/explore | *SearchApi* | [**getExploreData**](doc//SearchApi.md#getexploredata) | **GET** /search/explore |
*SearchApi* | [**search**](doc//SearchApi.md#search) | **GET** /search | *SearchApi* | [**search**](doc//SearchApi.md#search) | **GET** /search |
*SearchApi* | [**searchMetadata**](doc//SearchApi.md#searchmetadata) | **GET** /search/metadata |
*SearchApi* | [**searchPerson**](doc//SearchApi.md#searchperson) | **GET** /search/person | *SearchApi* | [**searchPerson**](doc//SearchApi.md#searchperson) | **GET** /search/person |
*SearchApi* | [**searchSmart**](doc//SearchApi.md#searchsmart) | **GET** /search/smart |
*ServerInfoApi* | [**getServerConfig**](doc//ServerInfoApi.md#getserverconfig) | **GET** /server-info/config | *ServerInfoApi* | [**getServerConfig**](doc//ServerInfoApi.md#getserverconfig) | **GET** /server-info/config |
*ServerInfoApi* | [**getServerFeatures**](doc//ServerInfoApi.md#getserverfeatures) | **GET** /server-info/features | *ServerInfoApi* | [**getServerFeatures**](doc//ServerInfoApi.md#getserverfeatures) | **GET** /server-info/features |
*ServerInfoApi* | [**getServerInfo**](doc//ServerInfoApi.md#getserverinfo) | **GET** /server-info | *ServerInfoApi* | [**getServerInfo**](doc//ServerInfoApi.md#getserverinfo) | **GET** /server-info |

View File

@ -1034,7 +1034,7 @@ void (empty response body)
[[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)
# **searchAssets** # **searchAssets**
> List<AssetResponseDto> searchAssets(checksum, city, country, createdAfter, createdBefore, deviceAssetId, deviceId, encodedVideoPath, id, isArchived, isEncoded, isExternal, isFavorite, isMotion, isOffline, isReadOnly, isVisible, lensModel, libraryId, make, model, order, originalFileName, originalPath, page, resizePath, size, state, takenAfter, takenBefore, trashedAfter, trashedBefore, type, updatedAfter, updatedBefore, webpPath, withDeleted, withExif, withPeople, withStacked) > List<AssetResponseDto> searchAssets(checksum, city, country, createdAfter, createdBefore, deviceAssetId, deviceId, encodedVideoPath, id, isArchived, isEncoded, isExternal, isFavorite, isMotion, isOffline, isReadOnly, isVisible, lensModel, libraryId, make, model, order, originalFileName, originalPath, page, resizePath, size, state, takenAfter, takenBefore, trashedAfter, trashedBefore, type, updatedAfter, updatedBefore, webpPath, withArchived, withDeleted, withExif, withPeople, withStacked)
@ -1093,13 +1093,14 @@ final type = ; // AssetTypeEnum |
final updatedAfter = 2013-10-20T19:20:30+01:00; // DateTime | final updatedAfter = 2013-10-20T19:20:30+01:00; // DateTime |
final updatedBefore = 2013-10-20T19:20:30+01:00; // DateTime | final updatedBefore = 2013-10-20T19:20:30+01:00; // DateTime |
final webpPath = webpPath_example; // String | final webpPath = webpPath_example; // String |
final withArchived = true; // bool |
final withDeleted = true; // bool | final withDeleted = true; // bool |
final withExif = true; // bool | final withExif = true; // bool |
final withPeople = true; // bool | final withPeople = true; // bool |
final withStacked = true; // bool | final withStacked = true; // bool |
try { try {
final result = api_instance.searchAssets(checksum, city, country, createdAfter, createdBefore, deviceAssetId, deviceId, encodedVideoPath, id, isArchived, isEncoded, isExternal, isFavorite, isMotion, isOffline, isReadOnly, isVisible, lensModel, libraryId, make, model, order, originalFileName, originalPath, page, resizePath, size, state, takenAfter, takenBefore, trashedAfter, trashedBefore, type, updatedAfter, updatedBefore, webpPath, withDeleted, withExif, withPeople, withStacked); final result = api_instance.searchAssets(checksum, city, country, createdAfter, createdBefore, deviceAssetId, deviceId, encodedVideoPath, id, isArchived, isEncoded, isExternal, isFavorite, isMotion, isOffline, isReadOnly, isVisible, lensModel, libraryId, make, model, order, originalFileName, originalPath, page, resizePath, size, state, takenAfter, takenBefore, trashedAfter, trashedBefore, type, updatedAfter, updatedBefore, webpPath, withArchived, withDeleted, withExif, withPeople, withStacked);
print(result); print(result);
} catch (e) { } catch (e) {
print('Exception when calling AssetApi->searchAssets: $e\n'); print('Exception when calling AssetApi->searchAssets: $e\n');
@ -1146,6 +1147,7 @@ Name | Type | Description | Notes
**updatedAfter** | **DateTime**| | [optional] **updatedAfter** | **DateTime**| | [optional]
**updatedBefore** | **DateTime**| | [optional] **updatedBefore** | **DateTime**| | [optional]
**webpPath** | **String**| | [optional] **webpPath** | **String**| | [optional]
**withArchived** | **bool**| | [optional]
**withDeleted** | **bool**| | [optional] **withDeleted** | **bool**| | [optional]
**withExif** | **bool**| | [optional] **withExif** | **bool**| | [optional]
**withPeople** | **bool**| | [optional] **withPeople** | **bool**| | [optional]

View File

@ -11,7 +11,9 @@ Method | HTTP request | Description
------------- | ------------- | ------------- ------------- | ------------- | -------------
[**getExploreData**](SearchApi.md#getexploredata) | **GET** /search/explore | [**getExploreData**](SearchApi.md#getexploredata) | **GET** /search/explore |
[**search**](SearchApi.md#search) | **GET** /search | [**search**](SearchApi.md#search) | **GET** /search |
[**searchMetadata**](SearchApi.md#searchmetadata) | **GET** /search/metadata |
[**searchPerson**](SearchApi.md#searchperson) | **GET** /search/person | [**searchPerson**](SearchApi.md#searchperson) | **GET** /search/person |
[**searchSmart**](SearchApi.md#searchsmart) | **GET** /search/smart |
# **getExploreData** # **getExploreData**
@ -66,7 +68,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(clip, motion, q, query, recent, smart, type, withArchived) > SearchResponseDto search(clip, motion, page, q, query, recent, size, smart, type, withArchived)
@ -91,15 +93,17 @@ import 'package:openapi/api.dart';
final api_instance = SearchApi(); final api_instance = SearchApi();
final clip = true; // bool | @deprecated final clip = true; // bool | @deprecated
final motion = true; // bool | final motion = true; // bool |
final page = 8.14; // num |
final q = q_example; // String | final q = q_example; // String |
final query = query_example; // String | final query = query_example; // String |
final recent = true; // bool | final recent = true; // bool |
final size = 8.14; // num |
final smart = true; // bool | final smart = true; // bool |
final type = type_example; // String | final type = type_example; // String |
final withArchived = true; // bool | final withArchived = true; // bool |
try { try {
final result = api_instance.search(clip, motion, q, query, recent, smart, type, withArchived); final result = api_instance.search(clip, motion, page, q, query, recent, size, smart, type, withArchived);
print(result); print(result);
} catch (e) { } catch (e) {
print('Exception when calling SearchApi->search: $e\n'); print('Exception when calling SearchApi->search: $e\n');
@ -112,9 +116,11 @@ Name | Type | Description | Notes
------------- | ------------- | ------------- | ------------- ------------- | ------------- | ------------- | -------------
**clip** | **bool**| @deprecated | [optional] **clip** | **bool**| @deprecated | [optional]
**motion** | **bool**| | [optional] **motion** | **bool**| | [optional]
**page** | **num**| | [optional]
**q** | **String**| | [optional] **q** | **String**| | [optional]
**query** | **String**| | [optional] **query** | **String**| | [optional]
**recent** | **bool**| | [optional] **recent** | **bool**| | [optional]
**size** | **num**| | [optional]
**smart** | **bool**| | [optional] **smart** | **bool**| | [optional]
**type** | **String**| | [optional] **type** | **String**| | [optional]
**withArchived** | **bool**| | [optional] **withArchived** | **bool**| | [optional]
@ -134,6 +140,141 @@ Name | Type | Description | Notes
[[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)
# **searchMetadata**
> SearchResponseDto searchMetadata(checksum, city, country, createdAfter, createdBefore, deviceAssetId, deviceId, encodedVideoPath, id, isArchived, isEncoded, isExternal, isFavorite, isMotion, isOffline, isReadOnly, isVisible, lensModel, libraryId, make, model, order, originalFileName, originalPath, page, resizePath, size, state, takenAfter, takenBefore, trashedAfter, trashedBefore, type, updatedAfter, updatedBefore, webpPath, withArchived, withDeleted, withExif, withPeople, withStacked)
### Example
```dart
import 'package:openapi/api.dart';
// TODO Configure API key authorization: cookie
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKey = 'YOUR_API_KEY';
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKeyPrefix = 'Bearer';
// TODO Configure API key authorization: api_key
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKey = 'YOUR_API_KEY';
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKeyPrefix = 'Bearer';
// TODO Configure HTTP Bearer authorization: bearer
// Case 1. Use String Token
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken('YOUR_ACCESS_TOKEN');
// Case 2. Use Function which generate token.
// String yourTokenGeneratorFunction() { ... }
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
final api_instance = SearchApi();
final checksum = checksum_example; // String |
final city = city_example; // String |
final country = country_example; // String |
final createdAfter = 2013-10-20T19:20:30+01:00; // DateTime |
final createdBefore = 2013-10-20T19:20:30+01:00; // DateTime |
final deviceAssetId = deviceAssetId_example; // String |
final deviceId = deviceId_example; // String |
final encodedVideoPath = encodedVideoPath_example; // String |
final id = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String |
final isArchived = true; // bool |
final isEncoded = true; // bool |
final isExternal = true; // bool |
final isFavorite = true; // bool |
final isMotion = true; // bool |
final isOffline = true; // bool |
final isReadOnly = true; // bool |
final isVisible = true; // bool |
final lensModel = lensModel_example; // String |
final libraryId = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String |
final make = make_example; // String |
final model = model_example; // String |
final order = ; // AssetOrder |
final originalFileName = originalFileName_example; // String |
final originalPath = originalPath_example; // String |
final page = 8.14; // num |
final resizePath = resizePath_example; // String |
final size = 8.14; // num |
final state = state_example; // String |
final takenAfter = 2013-10-20T19:20:30+01:00; // DateTime |
final takenBefore = 2013-10-20T19:20:30+01:00; // DateTime |
final trashedAfter = 2013-10-20T19:20:30+01:00; // DateTime |
final trashedBefore = 2013-10-20T19:20:30+01:00; // DateTime |
final type = ; // AssetTypeEnum |
final updatedAfter = 2013-10-20T19:20:30+01:00; // DateTime |
final updatedBefore = 2013-10-20T19:20:30+01:00; // DateTime |
final webpPath = webpPath_example; // String |
final withArchived = true; // bool |
final withDeleted = true; // bool |
final withExif = true; // bool |
final withPeople = true; // bool |
final withStacked = true; // bool |
try {
final result = api_instance.searchMetadata(checksum, city, country, createdAfter, createdBefore, deviceAssetId, deviceId, encodedVideoPath, id, isArchived, isEncoded, isExternal, isFavorite, isMotion, isOffline, isReadOnly, isVisible, lensModel, libraryId, make, model, order, originalFileName, originalPath, page, resizePath, size, state, takenAfter, takenBefore, trashedAfter, trashedBefore, type, updatedAfter, updatedBefore, webpPath, withArchived, withDeleted, withExif, withPeople, withStacked);
print(result);
} catch (e) {
print('Exception when calling SearchApi->searchMetadata: $e\n');
}
```
### Parameters
Name | Type | Description | Notes
------------- | ------------- | ------------- | -------------
**checksum** | **String**| | [optional]
**city** | **String**| | [optional]
**country** | **String**| | [optional]
**createdAfter** | **DateTime**| | [optional]
**createdBefore** | **DateTime**| | [optional]
**deviceAssetId** | **String**| | [optional]
**deviceId** | **String**| | [optional]
**encodedVideoPath** | **String**| | [optional]
**id** | **String**| | [optional]
**isArchived** | **bool**| | [optional]
**isEncoded** | **bool**| | [optional]
**isExternal** | **bool**| | [optional]
**isFavorite** | **bool**| | [optional]
**isMotion** | **bool**| | [optional]
**isOffline** | **bool**| | [optional]
**isReadOnly** | **bool**| | [optional]
**isVisible** | **bool**| | [optional]
**lensModel** | **String**| | [optional]
**libraryId** | **String**| | [optional]
**make** | **String**| | [optional]
**model** | **String**| | [optional]
**order** | [**AssetOrder**](.md)| | [optional]
**originalFileName** | **String**| | [optional]
**originalPath** | **String**| | [optional]
**page** | **num**| | [optional]
**resizePath** | **String**| | [optional]
**size** | **num**| | [optional]
**state** | **String**| | [optional]
**takenAfter** | **DateTime**| | [optional]
**takenBefore** | **DateTime**| | [optional]
**trashedAfter** | **DateTime**| | [optional]
**trashedBefore** | **DateTime**| | [optional]
**type** | [**AssetTypeEnum**](.md)| | [optional]
**updatedAfter** | **DateTime**| | [optional]
**updatedBefore** | **DateTime**| | [optional]
**webpPath** | **String**| | [optional]
**withArchived** | **bool**| | [optional]
**withDeleted** | **bool**| | [optional]
**withExif** | **bool**| | [optional]
**withPeople** | **bool**| | [optional]
**withStacked** | **bool**| | [optional]
### Return type
[**SearchResponseDto**](SearchResponseDto.md)
### Authorization
[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer)
### HTTP request headers
- **Content-Type**: Not defined
- **Accept**: application/json
[[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)
# **searchPerson** # **searchPerson**
> List<PersonResponseDto> searchPerson(name, withHidden) > List<PersonResponseDto> searchPerson(name, withHidden)
@ -191,3 +332,118 @@ Name | Type | Description | Notes
[[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)
# **searchSmart**
> SearchResponseDto searchSmart(query, city, country, createdAfter, createdBefore, deviceId, isArchived, isEncoded, isExternal, isFavorite, isMotion, isOffline, isReadOnly, isVisible, lensModel, libraryId, make, model, page, size, state, takenAfter, takenBefore, trashedAfter, trashedBefore, type, updatedAfter, updatedBefore, withArchived, withDeleted, withExif)
### Example
```dart
import 'package:openapi/api.dart';
// TODO Configure API key authorization: cookie
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKey = 'YOUR_API_KEY';
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKeyPrefix = 'Bearer';
// TODO Configure API key authorization: api_key
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKey = 'YOUR_API_KEY';
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKeyPrefix = 'Bearer';
// TODO Configure HTTP Bearer authorization: bearer
// Case 1. Use String Token
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken('YOUR_ACCESS_TOKEN');
// Case 2. Use Function which generate token.
// String yourTokenGeneratorFunction() { ... }
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
final api_instance = SearchApi();
final query = query_example; // String |
final city = city_example; // String |
final country = country_example; // String |
final createdAfter = 2013-10-20T19:20:30+01:00; // DateTime |
final createdBefore = 2013-10-20T19:20:30+01:00; // DateTime |
final deviceId = deviceId_example; // String |
final isArchived = true; // bool |
final isEncoded = true; // bool |
final isExternal = true; // bool |
final isFavorite = true; // bool |
final isMotion = true; // bool |
final isOffline = true; // bool |
final isReadOnly = true; // bool |
final isVisible = true; // bool |
final lensModel = lensModel_example; // String |
final libraryId = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String |
final make = make_example; // String |
final model = model_example; // String |
final page = 8.14; // num |
final size = 8.14; // num |
final state = state_example; // String |
final takenAfter = 2013-10-20T19:20:30+01:00; // DateTime |
final takenBefore = 2013-10-20T19:20:30+01:00; // DateTime |
final trashedAfter = 2013-10-20T19:20:30+01:00; // DateTime |
final trashedBefore = 2013-10-20T19:20:30+01:00; // DateTime |
final type = ; // AssetTypeEnum |
final updatedAfter = 2013-10-20T19:20:30+01:00; // DateTime |
final updatedBefore = 2013-10-20T19:20:30+01:00; // DateTime |
final withArchived = true; // bool |
final withDeleted = true; // bool |
final withExif = true; // bool |
try {
final result = api_instance.searchSmart(query, city, country, createdAfter, createdBefore, deviceId, isArchived, isEncoded, isExternal, isFavorite, isMotion, isOffline, isReadOnly, isVisible, lensModel, libraryId, make, model, page, size, state, takenAfter, takenBefore, trashedAfter, trashedBefore, type, updatedAfter, updatedBefore, withArchived, withDeleted, withExif);
print(result);
} catch (e) {
print('Exception when calling SearchApi->searchSmart: $e\n');
}
```
### Parameters
Name | Type | Description | Notes
------------- | ------------- | ------------- | -------------
**query** | **String**| |
**city** | **String**| | [optional]
**country** | **String**| | [optional]
**createdAfter** | **DateTime**| | [optional]
**createdBefore** | **DateTime**| | [optional]
**deviceId** | **String**| | [optional]
**isArchived** | **bool**| | [optional]
**isEncoded** | **bool**| | [optional]
**isExternal** | **bool**| | [optional]
**isFavorite** | **bool**| | [optional]
**isMotion** | **bool**| | [optional]
**isOffline** | **bool**| | [optional]
**isReadOnly** | **bool**| | [optional]
**isVisible** | **bool**| | [optional]
**lensModel** | **String**| | [optional]
**libraryId** | **String**| | [optional]
**make** | **String**| | [optional]
**model** | **String**| | [optional]
**page** | **num**| | [optional]
**size** | **num**| | [optional]
**state** | **String**| | [optional]
**takenAfter** | **DateTime**| | [optional]
**takenBefore** | **DateTime**| | [optional]
**trashedAfter** | **DateTime**| | [optional]
**trashedBefore** | **DateTime**| | [optional]
**type** | [**AssetTypeEnum**](.md)| | [optional]
**updatedAfter** | **DateTime**| | [optional]
**updatedBefore** | **DateTime**| | [optional]
**withArchived** | **bool**| | [optional]
**withDeleted** | **bool**| | [optional]
**withExif** | **bool**| | [optional]
### Return type
[**SearchResponseDto**](SearchResponseDto.md)
### Authorization
[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer)
### HTTP request headers
- **Content-Type**: Not defined
- **Accept**: application/json
[[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)

View File

@ -11,6 +11,7 @@ Name | Type | Description | Notes
**count** | **int** | | **count** | **int** | |
**facets** | [**List<SearchFacetResponseDto>**](SearchFacetResponseDto.md) | | [default to const []] **facets** | [**List<SearchFacetResponseDto>**](SearchFacetResponseDto.md) | | [default to const []]
**items** | [**List<AssetResponseDto>**](AssetResponseDto.md) | | [default to const []] **items** | [**List<AssetResponseDto>**](AssetResponseDto.md) | | [default to const []]
**nextPage** | **String** | |
**total** | **int** | | **total** | **int** | |
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) [[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

@ -1177,6 +1177,8 @@ class AssetApi {
/// ///
/// * [String] webpPath: /// * [String] webpPath:
/// ///
/// * [bool] withArchived:
///
/// * [bool] withDeleted: /// * [bool] withDeleted:
/// ///
/// * [bool] withExif: /// * [bool] withExif:
@ -1184,7 +1186,7 @@ class AssetApi {
/// * [bool] withPeople: /// * [bool] withPeople:
/// ///
/// * [bool] withStacked: /// * [bool] withStacked:
Future<Response> searchAssetsWithHttpInfo({ String? checksum, String? city, String? country, DateTime? createdAfter, DateTime? createdBefore, String? deviceAssetId, String? deviceId, String? encodedVideoPath, String? id, bool? isArchived, bool? isEncoded, bool? isExternal, bool? isFavorite, bool? isMotion, bool? isOffline, bool? isReadOnly, bool? isVisible, String? lensModel, String? libraryId, String? make, String? model, AssetOrder? order, String? originalFileName, String? originalPath, num? page, String? resizePath, num? size, String? state, DateTime? takenAfter, DateTime? takenBefore, DateTime? trashedAfter, DateTime? trashedBefore, AssetTypeEnum? type, DateTime? updatedAfter, DateTime? updatedBefore, String? webpPath, bool? withDeleted, bool? withExif, bool? withPeople, bool? withStacked, }) async { Future<Response> searchAssetsWithHttpInfo({ String? checksum, String? city, String? country, DateTime? createdAfter, DateTime? createdBefore, String? deviceAssetId, String? deviceId, String? encodedVideoPath, String? id, bool? isArchived, bool? isEncoded, bool? isExternal, bool? isFavorite, bool? isMotion, bool? isOffline, bool? isReadOnly, bool? isVisible, String? lensModel, String? libraryId, String? make, String? model, AssetOrder? order, String? originalFileName, String? originalPath, num? page, String? resizePath, num? size, String? state, DateTime? takenAfter, DateTime? takenBefore, DateTime? trashedAfter, DateTime? trashedBefore, AssetTypeEnum? type, DateTime? updatedAfter, DateTime? updatedBefore, String? webpPath, bool? withArchived, bool? withDeleted, bool? withExif, bool? withPeople, bool? withStacked, }) async {
// ignore: prefer_const_declarations // ignore: prefer_const_declarations
final path = r'/assets'; final path = r'/assets';
@ -1303,6 +1305,9 @@ class AssetApi {
if (webpPath != null) { if (webpPath != null) {
queryParams.addAll(_queryParams('', 'webpPath', webpPath)); queryParams.addAll(_queryParams('', 'webpPath', webpPath));
} }
if (withArchived != null) {
queryParams.addAll(_queryParams('', 'withArchived', withArchived));
}
if (withDeleted != null) { if (withDeleted != null) {
queryParams.addAll(_queryParams('', 'withDeleted', withDeleted)); queryParams.addAll(_queryParams('', 'withDeleted', withDeleted));
} }
@ -1404,6 +1409,8 @@ class AssetApi {
/// ///
/// * [String] webpPath: /// * [String] webpPath:
/// ///
/// * [bool] withArchived:
///
/// * [bool] withDeleted: /// * [bool] withDeleted:
/// ///
/// * [bool] withExif: /// * [bool] withExif:
@ -1411,8 +1418,8 @@ class AssetApi {
/// * [bool] withPeople: /// * [bool] withPeople:
/// ///
/// * [bool] withStacked: /// * [bool] withStacked:
Future<List<AssetResponseDto>?> searchAssets({ String? checksum, String? city, String? country, DateTime? createdAfter, DateTime? createdBefore, String? deviceAssetId, String? deviceId, String? encodedVideoPath, String? id, bool? isArchived, bool? isEncoded, bool? isExternal, bool? isFavorite, bool? isMotion, bool? isOffline, bool? isReadOnly, bool? isVisible, String? lensModel, String? libraryId, String? make, String? model, AssetOrder? order, String? originalFileName, String? originalPath, num? page, String? resizePath, num? size, String? state, DateTime? takenAfter, DateTime? takenBefore, DateTime? trashedAfter, DateTime? trashedBefore, AssetTypeEnum? type, DateTime? updatedAfter, DateTime? updatedBefore, String? webpPath, bool? withDeleted, bool? withExif, bool? withPeople, bool? withStacked, }) async { Future<List<AssetResponseDto>?> searchAssets({ String? checksum, String? city, String? country, DateTime? createdAfter, DateTime? createdBefore, String? deviceAssetId, String? deviceId, String? encodedVideoPath, String? id, bool? isArchived, bool? isEncoded, bool? isExternal, bool? isFavorite, bool? isMotion, bool? isOffline, bool? isReadOnly, bool? isVisible, String? lensModel, String? libraryId, String? make, String? model, AssetOrder? order, String? originalFileName, String? originalPath, num? page, String? resizePath, num? size, String? state, DateTime? takenAfter, DateTime? takenBefore, DateTime? trashedAfter, DateTime? trashedBefore, AssetTypeEnum? type, DateTime? updatedAfter, DateTime? updatedBefore, String? webpPath, bool? withArchived, bool? withDeleted, bool? withExif, bool? withPeople, bool? withStacked, }) async {
final response = await searchAssetsWithHttpInfo( checksum: checksum, city: city, country: country, createdAfter: createdAfter, createdBefore: createdBefore, deviceAssetId: deviceAssetId, deviceId: deviceId, encodedVideoPath: encodedVideoPath, id: id, isArchived: isArchived, isEncoded: isEncoded, isExternal: isExternal, isFavorite: isFavorite, isMotion: isMotion, isOffline: isOffline, isReadOnly: isReadOnly, isVisible: isVisible, lensModel: lensModel, libraryId: libraryId, make: make, model: model, order: order, originalFileName: originalFileName, originalPath: originalPath, page: page, resizePath: resizePath, size: size, state: state, takenAfter: takenAfter, takenBefore: takenBefore, trashedAfter: trashedAfter, trashedBefore: trashedBefore, type: type, updatedAfter: updatedAfter, updatedBefore: updatedBefore, webpPath: webpPath, withDeleted: withDeleted, withExif: withExif, withPeople: withPeople, withStacked: withStacked, ); final response = await searchAssetsWithHttpInfo( checksum: checksum, city: city, country: country, createdAfter: createdAfter, createdBefore: createdBefore, deviceAssetId: deviceAssetId, deviceId: deviceId, encodedVideoPath: encodedVideoPath, id: id, isArchived: isArchived, isEncoded: isEncoded, isExternal: isExternal, isFavorite: isFavorite, isMotion: isMotion, isOffline: isOffline, isReadOnly: isReadOnly, isVisible: isVisible, lensModel: lensModel, libraryId: libraryId, make: make, model: model, order: order, originalFileName: originalFileName, originalPath: originalPath, page: page, resizePath: resizePath, size: size, state: state, takenAfter: takenAfter, takenBefore: takenBefore, trashedAfter: trashedAfter, trashedBefore: trashedBefore, type: type, updatedAfter: updatedAfter, updatedBefore: updatedBefore, webpPath: webpPath, withArchived: withArchived, withDeleted: withDeleted, withExif: withExif, withPeople: withPeople, withStacked: withStacked, );
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

@ -68,18 +68,22 @@ class SearchApi {
/// ///
/// * [bool] motion: /// * [bool] motion:
/// ///
/// * [num] page:
///
/// * [String] q: /// * [String] q:
/// ///
/// * [String] query: /// * [String] query:
/// ///
/// * [bool] recent: /// * [bool] recent:
/// ///
/// * [num] size:
///
/// * [bool] smart: /// * [bool] smart:
/// ///
/// * [String] type: /// * [String] type:
/// ///
/// * [bool] withArchived: /// * [bool] withArchived:
Future<Response> searchWithHttpInfo({ bool? clip, bool? motion, String? q, String? query, bool? recent, bool? smart, String? type, bool? withArchived, }) async { Future<Response> searchWithHttpInfo({ bool? clip, bool? motion, num? page, String? q, String? query, bool? recent, num? size, bool? smart, String? type, bool? withArchived, }) async {
// ignore: prefer_const_declarations // ignore: prefer_const_declarations
final path = r'/search'; final path = r'/search';
@ -96,6 +100,9 @@ class SearchApi {
if (motion != null) { if (motion != null) {
queryParams.addAll(_queryParams('', 'motion', motion)); queryParams.addAll(_queryParams('', 'motion', motion));
} }
if (page != null) {
queryParams.addAll(_queryParams('', 'page', page));
}
if (q != null) { if (q != null) {
queryParams.addAll(_queryParams('', 'q', q)); queryParams.addAll(_queryParams('', 'q', q));
} }
@ -105,6 +112,9 @@ class SearchApi {
if (recent != null) { if (recent != null) {
queryParams.addAll(_queryParams('', 'recent', recent)); queryParams.addAll(_queryParams('', 'recent', recent));
} }
if (size != null) {
queryParams.addAll(_queryParams('', 'size', size));
}
if (smart != null) { if (smart != null) {
queryParams.addAll(_queryParams('', 'smart', smart)); queryParams.addAll(_queryParams('', 'smart', smart));
} }
@ -136,19 +146,354 @@ class SearchApi {
/// ///
/// * [bool] motion: /// * [bool] motion:
/// ///
/// * [num] page:
///
/// * [String] q: /// * [String] q:
/// ///
/// * [String] query: /// * [String] query:
/// ///
/// * [bool] recent: /// * [bool] recent:
/// ///
/// * [num] size:
///
/// * [bool] smart: /// * [bool] smart:
/// ///
/// * [String] type: /// * [String] type:
/// ///
/// * [bool] withArchived: /// * [bool] withArchived:
Future<SearchResponseDto?> search({ bool? clip, bool? motion, String? q, String? query, bool? recent, bool? smart, String? type, bool? withArchived, }) async { Future<SearchResponseDto?> search({ bool? clip, bool? motion, num? page, String? q, String? query, bool? recent, num? size, 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, ); final response = await searchWithHttpInfo( clip: clip, motion: motion, page: page, q: q, query: query, recent: recent, size: size, smart: smart, type: type, withArchived: withArchived, );
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
// When a remote server returns no body with a status of 204, we shall not decode it.
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
// FormatException when trying to decode an empty string.
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'SearchResponseDto',) as SearchResponseDto;
}
return null;
}
/// Performs an HTTP 'GET /search/metadata' operation and returns the [Response].
/// Parameters:
///
/// * [String] checksum:
///
/// * [String] city:
///
/// * [String] country:
///
/// * [DateTime] createdAfter:
///
/// * [DateTime] createdBefore:
///
/// * [String] deviceAssetId:
///
/// * [String] deviceId:
///
/// * [String] encodedVideoPath:
///
/// * [String] id:
///
/// * [bool] isArchived:
///
/// * [bool] isEncoded:
///
/// * [bool] isExternal:
///
/// * [bool] isFavorite:
///
/// * [bool] isMotion:
///
/// * [bool] isOffline:
///
/// * [bool] isReadOnly:
///
/// * [bool] isVisible:
///
/// * [String] lensModel:
///
/// * [String] libraryId:
///
/// * [String] make:
///
/// * [String] model:
///
/// * [AssetOrder] order:
///
/// * [String] originalFileName:
///
/// * [String] originalPath:
///
/// * [num] page:
///
/// * [String] resizePath:
///
/// * [num] size:
///
/// * [String] state:
///
/// * [DateTime] takenAfter:
///
/// * [DateTime] takenBefore:
///
/// * [DateTime] trashedAfter:
///
/// * [DateTime] trashedBefore:
///
/// * [AssetTypeEnum] type:
///
/// * [DateTime] updatedAfter:
///
/// * [DateTime] updatedBefore:
///
/// * [String] webpPath:
///
/// * [bool] withArchived:
///
/// * [bool] withDeleted:
///
/// * [bool] withExif:
///
/// * [bool] withPeople:
///
/// * [bool] withStacked:
Future<Response> searchMetadataWithHttpInfo({ String? checksum, String? city, String? country, DateTime? createdAfter, DateTime? createdBefore, String? deviceAssetId, String? deviceId, String? encodedVideoPath, String? id, bool? isArchived, bool? isEncoded, bool? isExternal, bool? isFavorite, bool? isMotion, bool? isOffline, bool? isReadOnly, bool? isVisible, String? lensModel, String? libraryId, String? make, String? model, AssetOrder? order, String? originalFileName, String? originalPath, num? page, String? resizePath, num? size, String? state, DateTime? takenAfter, DateTime? takenBefore, DateTime? trashedAfter, DateTime? trashedBefore, AssetTypeEnum? type, DateTime? updatedAfter, DateTime? updatedBefore, String? webpPath, bool? withArchived, bool? withDeleted, bool? withExif, bool? withPeople, bool? withStacked, }) async {
// ignore: prefer_const_declarations
final path = r'/search/metadata';
// ignore: prefer_final_locals
Object? postBody;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
if (checksum != null) {
queryParams.addAll(_queryParams('', 'checksum', checksum));
}
if (city != null) {
queryParams.addAll(_queryParams('', 'city', city));
}
if (country != null) {
queryParams.addAll(_queryParams('', 'country', country));
}
if (createdAfter != null) {
queryParams.addAll(_queryParams('', 'createdAfter', createdAfter));
}
if (createdBefore != null) {
queryParams.addAll(_queryParams('', 'createdBefore', createdBefore));
}
if (deviceAssetId != null) {
queryParams.addAll(_queryParams('', 'deviceAssetId', deviceAssetId));
}
if (deviceId != null) {
queryParams.addAll(_queryParams('', 'deviceId', deviceId));
}
if (encodedVideoPath != null) {
queryParams.addAll(_queryParams('', 'encodedVideoPath', encodedVideoPath));
}
if (id != null) {
queryParams.addAll(_queryParams('', 'id', id));
}
if (isArchived != null) {
queryParams.addAll(_queryParams('', 'isArchived', isArchived));
}
if (isEncoded != null) {
queryParams.addAll(_queryParams('', 'isEncoded', isEncoded));
}
if (isExternal != null) {
queryParams.addAll(_queryParams('', 'isExternal', isExternal));
}
if (isFavorite != null) {
queryParams.addAll(_queryParams('', 'isFavorite', isFavorite));
}
if (isMotion != null) {
queryParams.addAll(_queryParams('', 'isMotion', isMotion));
}
if (isOffline != null) {
queryParams.addAll(_queryParams('', 'isOffline', isOffline));
}
if (isReadOnly != null) {
queryParams.addAll(_queryParams('', 'isReadOnly', isReadOnly));
}
if (isVisible != null) {
queryParams.addAll(_queryParams('', 'isVisible', isVisible));
}
if (lensModel != null) {
queryParams.addAll(_queryParams('', 'lensModel', lensModel));
}
if (libraryId != null) {
queryParams.addAll(_queryParams('', 'libraryId', libraryId));
}
if (make != null) {
queryParams.addAll(_queryParams('', 'make', make));
}
if (model != null) {
queryParams.addAll(_queryParams('', 'model', model));
}
if (order != null) {
queryParams.addAll(_queryParams('', 'order', order));
}
if (originalFileName != null) {
queryParams.addAll(_queryParams('', 'originalFileName', originalFileName));
}
if (originalPath != null) {
queryParams.addAll(_queryParams('', 'originalPath', originalPath));
}
if (page != null) {
queryParams.addAll(_queryParams('', 'page', page));
}
if (resizePath != null) {
queryParams.addAll(_queryParams('', 'resizePath', resizePath));
}
if (size != null) {
queryParams.addAll(_queryParams('', 'size', size));
}
if (state != null) {
queryParams.addAll(_queryParams('', 'state', state));
}
if (takenAfter != null) {
queryParams.addAll(_queryParams('', 'takenAfter', takenAfter));
}
if (takenBefore != null) {
queryParams.addAll(_queryParams('', 'takenBefore', takenBefore));
}
if (trashedAfter != null) {
queryParams.addAll(_queryParams('', 'trashedAfter', trashedAfter));
}
if (trashedBefore != null) {
queryParams.addAll(_queryParams('', 'trashedBefore', trashedBefore));
}
if (type != null) {
queryParams.addAll(_queryParams('', 'type', type));
}
if (updatedAfter != null) {
queryParams.addAll(_queryParams('', 'updatedAfter', updatedAfter));
}
if (updatedBefore != null) {
queryParams.addAll(_queryParams('', 'updatedBefore', updatedBefore));
}
if (webpPath != null) {
queryParams.addAll(_queryParams('', 'webpPath', webpPath));
}
if (withArchived != null) {
queryParams.addAll(_queryParams('', 'withArchived', withArchived));
}
if (withDeleted != null) {
queryParams.addAll(_queryParams('', 'withDeleted', withDeleted));
}
if (withExif != null) {
queryParams.addAll(_queryParams('', 'withExif', withExif));
}
if (withPeople != null) {
queryParams.addAll(_queryParams('', 'withPeople', withPeople));
}
if (withStacked != null) {
queryParams.addAll(_queryParams('', 'withStacked', withStacked));
}
const contentTypes = <String>[];
return apiClient.invokeAPI(
path,
'GET',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
/// Parameters:
///
/// * [String] checksum:
///
/// * [String] city:
///
/// * [String] country:
///
/// * [DateTime] createdAfter:
///
/// * [DateTime] createdBefore:
///
/// * [String] deviceAssetId:
///
/// * [String] deviceId:
///
/// * [String] encodedVideoPath:
///
/// * [String] id:
///
/// * [bool] isArchived:
///
/// * [bool] isEncoded:
///
/// * [bool] isExternal:
///
/// * [bool] isFavorite:
///
/// * [bool] isMotion:
///
/// * [bool] isOffline:
///
/// * [bool] isReadOnly:
///
/// * [bool] isVisible:
///
/// * [String] lensModel:
///
/// * [String] libraryId:
///
/// * [String] make:
///
/// * [String] model:
///
/// * [AssetOrder] order:
///
/// * [String] originalFileName:
///
/// * [String] originalPath:
///
/// * [num] page:
///
/// * [String] resizePath:
///
/// * [num] size:
///
/// * [String] state:
///
/// * [DateTime] takenAfter:
///
/// * [DateTime] takenBefore:
///
/// * [DateTime] trashedAfter:
///
/// * [DateTime] trashedBefore:
///
/// * [AssetTypeEnum] type:
///
/// * [DateTime] updatedAfter:
///
/// * [DateTime] updatedBefore:
///
/// * [String] webpPath:
///
/// * [bool] withArchived:
///
/// * [bool] withDeleted:
///
/// * [bool] withExif:
///
/// * [bool] withPeople:
///
/// * [bool] withStacked:
Future<SearchResponseDto?> searchMetadata({ String? checksum, String? city, String? country, DateTime? createdAfter, DateTime? createdBefore, String? deviceAssetId, String? deviceId, String? encodedVideoPath, String? id, bool? isArchived, bool? isEncoded, bool? isExternal, bool? isFavorite, bool? isMotion, bool? isOffline, bool? isReadOnly, bool? isVisible, String? lensModel, String? libraryId, String? make, String? model, AssetOrder? order, String? originalFileName, String? originalPath, num? page, String? resizePath, num? size, String? state, DateTime? takenAfter, DateTime? takenBefore, DateTime? trashedAfter, DateTime? trashedBefore, AssetTypeEnum? type, DateTime? updatedAfter, DateTime? updatedBefore, String? webpPath, bool? withArchived, bool? withDeleted, bool? withExif, bool? withPeople, bool? withStacked, }) async {
final response = await searchMetadataWithHttpInfo( checksum: checksum, city: city, country: country, createdAfter: createdAfter, createdBefore: createdBefore, deviceAssetId: deviceAssetId, deviceId: deviceId, encodedVideoPath: encodedVideoPath, id: id, isArchived: isArchived, isEncoded: isEncoded, isExternal: isExternal, isFavorite: isFavorite, isMotion: isMotion, isOffline: isOffline, isReadOnly: isReadOnly, isVisible: isVisible, lensModel: lensModel, libraryId: libraryId, make: make, model: model, order: order, originalFileName: originalFileName, originalPath: originalPath, page: page, resizePath: resizePath, size: size, state: state, takenAfter: takenAfter, takenBefore: takenBefore, trashedAfter: trashedAfter, trashedBefore: trashedBefore, type: type, updatedAfter: updatedAfter, updatedBefore: updatedBefore, webpPath: webpPath, withArchived: withArchived, withDeleted: withDeleted, withExif: withExif, withPeople: withPeople, withStacked: withStacked, );
if (response.statusCode >= HttpStatus.badRequest) { if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response)); throw ApiException(response.statusCode, await _decodeBodyBytes(response));
} }
@ -220,4 +565,263 @@ class SearchApi {
} }
return null; return null;
} }
/// Performs an HTTP 'GET /search/smart' operation and returns the [Response].
/// Parameters:
///
/// * [String] query (required):
///
/// * [String] city:
///
/// * [String] country:
///
/// * [DateTime] createdAfter:
///
/// * [DateTime] createdBefore:
///
/// * [String] deviceId:
///
/// * [bool] isArchived:
///
/// * [bool] isEncoded:
///
/// * [bool] isExternal:
///
/// * [bool] isFavorite:
///
/// * [bool] isMotion:
///
/// * [bool] isOffline:
///
/// * [bool] isReadOnly:
///
/// * [bool] isVisible:
///
/// * [String] lensModel:
///
/// * [String] libraryId:
///
/// * [String] make:
///
/// * [String] model:
///
/// * [num] page:
///
/// * [num] size:
///
/// * [String] state:
///
/// * [DateTime] takenAfter:
///
/// * [DateTime] takenBefore:
///
/// * [DateTime] trashedAfter:
///
/// * [DateTime] trashedBefore:
///
/// * [AssetTypeEnum] type:
///
/// * [DateTime] updatedAfter:
///
/// * [DateTime] updatedBefore:
///
/// * [bool] withArchived:
///
/// * [bool] withDeleted:
///
/// * [bool] withExif:
Future<Response> searchSmartWithHttpInfo(String query, { String? city, String? country, DateTime? createdAfter, DateTime? createdBefore, String? deviceId, bool? isArchived, bool? isEncoded, bool? isExternal, bool? isFavorite, bool? isMotion, bool? isOffline, bool? isReadOnly, bool? isVisible, String? lensModel, String? libraryId, String? make, String? model, num? page, num? size, String? state, DateTime? takenAfter, DateTime? takenBefore, DateTime? trashedAfter, DateTime? trashedBefore, AssetTypeEnum? type, DateTime? updatedAfter, DateTime? updatedBefore, bool? withArchived, bool? withDeleted, bool? withExif, }) async {
// ignore: prefer_const_declarations
final path = r'/search/smart';
// ignore: prefer_final_locals
Object? postBody;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
if (city != null) {
queryParams.addAll(_queryParams('', 'city', city));
}
if (country != null) {
queryParams.addAll(_queryParams('', 'country', country));
}
if (createdAfter != null) {
queryParams.addAll(_queryParams('', 'createdAfter', createdAfter));
}
if (createdBefore != null) {
queryParams.addAll(_queryParams('', 'createdBefore', createdBefore));
}
if (deviceId != null) {
queryParams.addAll(_queryParams('', 'deviceId', deviceId));
}
if (isArchived != null) {
queryParams.addAll(_queryParams('', 'isArchived', isArchived));
}
if (isEncoded != null) {
queryParams.addAll(_queryParams('', 'isEncoded', isEncoded));
}
if (isExternal != null) {
queryParams.addAll(_queryParams('', 'isExternal', isExternal));
}
if (isFavorite != null) {
queryParams.addAll(_queryParams('', 'isFavorite', isFavorite));
}
if (isMotion != null) {
queryParams.addAll(_queryParams('', 'isMotion', isMotion));
}
if (isOffline != null) {
queryParams.addAll(_queryParams('', 'isOffline', isOffline));
}
if (isReadOnly != null) {
queryParams.addAll(_queryParams('', 'isReadOnly', isReadOnly));
}
if (isVisible != null) {
queryParams.addAll(_queryParams('', 'isVisible', isVisible));
}
if (lensModel != null) {
queryParams.addAll(_queryParams('', 'lensModel', lensModel));
}
if (libraryId != null) {
queryParams.addAll(_queryParams('', 'libraryId', libraryId));
}
if (make != null) {
queryParams.addAll(_queryParams('', 'make', make));
}
if (model != null) {
queryParams.addAll(_queryParams('', 'model', model));
}
if (page != null) {
queryParams.addAll(_queryParams('', 'page', page));
}
queryParams.addAll(_queryParams('', 'query', query));
if (size != null) {
queryParams.addAll(_queryParams('', 'size', size));
}
if (state != null) {
queryParams.addAll(_queryParams('', 'state', state));
}
if (takenAfter != null) {
queryParams.addAll(_queryParams('', 'takenAfter', takenAfter));
}
if (takenBefore != null) {
queryParams.addAll(_queryParams('', 'takenBefore', takenBefore));
}
if (trashedAfter != null) {
queryParams.addAll(_queryParams('', 'trashedAfter', trashedAfter));
}
if (trashedBefore != null) {
queryParams.addAll(_queryParams('', 'trashedBefore', trashedBefore));
}
if (type != null) {
queryParams.addAll(_queryParams('', 'type', type));
}
if (updatedAfter != null) {
queryParams.addAll(_queryParams('', 'updatedAfter', updatedAfter));
}
if (updatedBefore != null) {
queryParams.addAll(_queryParams('', 'updatedBefore', updatedBefore));
}
if (withArchived != null) {
queryParams.addAll(_queryParams('', 'withArchived', withArchived));
}
if (withDeleted != null) {
queryParams.addAll(_queryParams('', 'withDeleted', withDeleted));
}
if (withExif != null) {
queryParams.addAll(_queryParams('', 'withExif', withExif));
}
const contentTypes = <String>[];
return apiClient.invokeAPI(
path,
'GET',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
/// Parameters:
///
/// * [String] query (required):
///
/// * [String] city:
///
/// * [String] country:
///
/// * [DateTime] createdAfter:
///
/// * [DateTime] createdBefore:
///
/// * [String] deviceId:
///
/// * [bool] isArchived:
///
/// * [bool] isEncoded:
///
/// * [bool] isExternal:
///
/// * [bool] isFavorite:
///
/// * [bool] isMotion:
///
/// * [bool] isOffline:
///
/// * [bool] isReadOnly:
///
/// * [bool] isVisible:
///
/// * [String] lensModel:
///
/// * [String] libraryId:
///
/// * [String] make:
///
/// * [String] model:
///
/// * [num] page:
///
/// * [num] size:
///
/// * [String] state:
///
/// * [DateTime] takenAfter:
///
/// * [DateTime] takenBefore:
///
/// * [DateTime] trashedAfter:
///
/// * [DateTime] trashedBefore:
///
/// * [AssetTypeEnum] type:
///
/// * [DateTime] updatedAfter:
///
/// * [DateTime] updatedBefore:
///
/// * [bool] withArchived:
///
/// * [bool] withDeleted:
///
/// * [bool] withExif:
Future<SearchResponseDto?> searchSmart(String query, { String? city, String? country, DateTime? createdAfter, DateTime? createdBefore, String? deviceId, bool? isArchived, bool? isEncoded, bool? isExternal, bool? isFavorite, bool? isMotion, bool? isOffline, bool? isReadOnly, bool? isVisible, String? lensModel, String? libraryId, String? make, String? model, num? page, num? size, String? state, DateTime? takenAfter, DateTime? takenBefore, DateTime? trashedAfter, DateTime? trashedBefore, AssetTypeEnum? type, DateTime? updatedAfter, DateTime? updatedBefore, bool? withArchived, bool? withDeleted, bool? withExif, }) async {
final response = await searchSmartWithHttpInfo(query, city: city, country: country, createdAfter: createdAfter, createdBefore: createdBefore, deviceId: deviceId, isArchived: isArchived, isEncoded: isEncoded, isExternal: isExternal, isFavorite: isFavorite, isMotion: isMotion, isOffline: isOffline, isReadOnly: isReadOnly, isVisible: isVisible, lensModel: lensModel, libraryId: libraryId, make: make, model: model, page: page, size: size, state: state, takenAfter: takenAfter, takenBefore: takenBefore, trashedAfter: trashedAfter, trashedBefore: trashedBefore, type: type, updatedAfter: updatedAfter, updatedBefore: updatedBefore, withArchived: withArchived, withDeleted: withDeleted, withExif: withExif, );
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
// When a remote server returns no body with a status of 204, we shall not decode it.
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
// FormatException when trying to decode an empty string.
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'SearchResponseDto',) as SearchResponseDto;
}
return null;
}
} }

View File

@ -16,6 +16,7 @@ class SearchAssetResponseDto {
required this.count, required this.count,
this.facets = const [], this.facets = const [],
this.items = const [], this.items = const [],
required this.nextPage,
required this.total, required this.total,
}); });
@ -25,6 +26,8 @@ class SearchAssetResponseDto {
List<AssetResponseDto> items; List<AssetResponseDto> items;
String? nextPage;
int total; int total;
@override @override
@ -32,6 +35,7 @@ class SearchAssetResponseDto {
other.count == count && other.count == count &&
_deepEquality.equals(other.facets, facets) && _deepEquality.equals(other.facets, facets) &&
_deepEquality.equals(other.items, items) && _deepEquality.equals(other.items, items) &&
other.nextPage == nextPage &&
other.total == total; other.total == total;
@override @override
@ -40,16 +44,22 @@ class SearchAssetResponseDto {
(count.hashCode) + (count.hashCode) +
(facets.hashCode) + (facets.hashCode) +
(items.hashCode) + (items.hashCode) +
(nextPage == null ? 0 : nextPage!.hashCode) +
(total.hashCode); (total.hashCode);
@override @override
String toString() => 'SearchAssetResponseDto[count=$count, facets=$facets, items=$items, total=$total]'; String toString() => 'SearchAssetResponseDto[count=$count, facets=$facets, items=$items, nextPage=$nextPage, total=$total]';
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final json = <String, dynamic>{}; final json = <String, dynamic>{};
json[r'count'] = this.count; json[r'count'] = this.count;
json[r'facets'] = this.facets; json[r'facets'] = this.facets;
json[r'items'] = this.items; json[r'items'] = this.items;
if (this.nextPage != null) {
json[r'nextPage'] = this.nextPage;
} else {
// json[r'nextPage'] = null;
}
json[r'total'] = this.total; json[r'total'] = this.total;
return json; return json;
} }
@ -65,6 +75,7 @@ class SearchAssetResponseDto {
count: mapValueOfType<int>(json, r'count')!, count: mapValueOfType<int>(json, r'count')!,
facets: SearchFacetResponseDto.listFromJson(json[r'facets']), facets: SearchFacetResponseDto.listFromJson(json[r'facets']),
items: AssetResponseDto.listFromJson(json[r'items']), items: AssetResponseDto.listFromJson(json[r'items']),
nextPage: mapValueOfType<String>(json, r'nextPage'),
total: mapValueOfType<int>(json, r'total')!, total: mapValueOfType<int>(json, r'total')!,
); );
} }
@ -116,6 +127,7 @@ class SearchAssetResponseDto {
'count', 'count',
'facets', 'facets',
'items', 'items',
'nextPage',
'total', 'total',
}; };
} }

View File

@ -110,7 +110,7 @@ void main() {
// TODO // TODO
}); });
//Future<List<AssetResponseDto>> searchAssets({ String checksum, String city, String country, DateTime createdAfter, DateTime createdBefore, String deviceAssetId, String deviceId, String encodedVideoPath, String id, bool isArchived, bool isEncoded, bool isExternal, bool isFavorite, bool isMotion, bool isOffline, bool isReadOnly, bool isVisible, String lensModel, String libraryId, String make, String model, AssetOrder order, String originalFileName, String originalPath, num page, String resizePath, num size, String state, DateTime takenAfter, DateTime takenBefore, DateTime trashedAfter, DateTime trashedBefore, AssetTypeEnum type, DateTime updatedAfter, DateTime updatedBefore, String webpPath, bool withDeleted, bool withExif, bool withPeople, bool withStacked }) async //Future<List<AssetResponseDto>> searchAssets({ String checksum, String city, String country, DateTime createdAfter, DateTime createdBefore, String deviceAssetId, String deviceId, String encodedVideoPath, String id, bool isArchived, bool isEncoded, bool isExternal, bool isFavorite, bool isMotion, bool isOffline, bool isReadOnly, bool isVisible, String lensModel, String libraryId, String make, String model, AssetOrder order, String originalFileName, String originalPath, num page, String resizePath, num size, String state, DateTime takenAfter, DateTime takenBefore, DateTime trashedAfter, DateTime trashedBefore, AssetTypeEnum type, DateTime updatedAfter, DateTime updatedBefore, String webpPath, bool withArchived, bool withDeleted, bool withExif, bool withPeople, bool withStacked }) async
test('test searchAssets', () async { test('test searchAssets', () async {
// TODO // TODO
}); });

View File

@ -22,15 +22,25 @@ void main() {
// TODO // TODO
}); });
//Future<SearchResponseDto> search({ bool clip, bool motion, String q, String query, bool recent, bool smart, String type, bool withArchived }) async //Future<SearchResponseDto> search({ bool clip, bool motion, num page, String q, String query, bool recent, num size, bool smart, String type, bool withArchived }) async
test('test search', () async { test('test search', () async {
// TODO // TODO
}); });
//Future<SearchResponseDto> searchMetadata({ String checksum, String city, String country, DateTime createdAfter, DateTime createdBefore, String deviceAssetId, String deviceId, String encodedVideoPath, String id, bool isArchived, bool isEncoded, bool isExternal, bool isFavorite, bool isMotion, bool isOffline, bool isReadOnly, bool isVisible, String lensModel, String libraryId, String make, String model, AssetOrder order, String originalFileName, String originalPath, num page, String resizePath, num size, String state, DateTime takenAfter, DateTime takenBefore, DateTime trashedAfter, DateTime trashedBefore, AssetTypeEnum type, DateTime updatedAfter, DateTime updatedBefore, String webpPath, bool withArchived, bool withDeleted, bool withExif, bool withPeople, bool withStacked }) async
test('test searchMetadata', () async {
// TODO
});
//Future<List<PersonResponseDto>> searchPerson(String name, { bool withHidden }) async //Future<List<PersonResponseDto>> searchPerson(String name, { bool withHidden }) async
test('test searchPerson', () async { test('test searchPerson', () async {
// TODO // TODO
}); });
//Future<SearchResponseDto> searchSmart(String query, { String city, String country, DateTime createdAfter, DateTime createdBefore, String deviceId, bool isArchived, bool isEncoded, bool isExternal, bool isFavorite, bool isMotion, bool isOffline, bool isReadOnly, bool isVisible, String lensModel, String libraryId, String make, String model, num page, num size, String state, DateTime takenAfter, DateTime takenBefore, DateTime trashedAfter, DateTime trashedBefore, AssetTypeEnum type, DateTime updatedAfter, DateTime updatedBefore, bool withArchived, bool withDeleted, bool withExif }) async
test('test searchSmart', () async {
// TODO
});
}); });
} }

View File

@ -31,6 +31,11 @@ void main() {
// TODO // TODO
}); });
// String nextPage
test('to test the property `nextPage`', () async {
// TODO
});
// int total // int total
test('to test the property `total`', () async { test('to test the property `total`', () async {
// TODO // TODO

View File

@ -2130,6 +2130,7 @@
}, },
"/assets": { "/assets": {
"get": { "get": {
"deprecated": true,
"operationId": "searchAssets", "operationId": "searchAssets",
"parameters": [ "parameters": [
{ {
@ -2430,6 +2431,14 @@
"type": "string" "type": "string"
} }
}, },
{
"name": "withArchived",
"required": false,
"in": "query",
"schema": {
"type": "boolean"
}
},
{ {
"name": "withDeleted", "name": "withDeleted",
"required": false, "required": false,
@ -4354,6 +4363,7 @@
}, },
"/search": { "/search": {
"get": { "get": {
"deprecated": true,
"operationId": "search", "operationId": "search",
"parameters": [ "parameters": [
{ {
@ -4374,6 +4384,14 @@
"type": "boolean" "type": "boolean"
} }
}, },
{
"name": "page",
"required": false,
"in": "query",
"schema": {
"type": "number"
}
},
{ {
"name": "q", "name": "q",
"required": false, "required": false,
@ -4398,6 +4416,14 @@
"type": "boolean" "type": "boolean"
} }
}, },
{
"name": "size",
"required": false,
"in": "query",
"schema": {
"type": "number"
}
},
{ {
"name": "smart", "name": "smart",
"required": false, "required": false,
@ -4492,6 +4518,377 @@
] ]
} }
}, },
"/search/metadata": {
"get": {
"operationId": "searchMetadata",
"parameters": [
{
"name": "checksum",
"required": false,
"in": "query",
"schema": {
"type": "string"
}
},
{
"name": "city",
"required": false,
"in": "query",
"schema": {
"type": "string"
}
},
{
"name": "country",
"required": false,
"in": "query",
"schema": {
"type": "string"
}
},
{
"name": "createdAfter",
"required": false,
"in": "query",
"schema": {
"format": "date-time",
"type": "string"
}
},
{
"name": "createdBefore",
"required": false,
"in": "query",
"schema": {
"format": "date-time",
"type": "string"
}
},
{
"name": "deviceAssetId",
"required": false,
"in": "query",
"schema": {
"type": "string"
}
},
{
"name": "deviceId",
"required": false,
"in": "query",
"schema": {
"type": "string"
}
},
{
"name": "encodedVideoPath",
"required": false,
"in": "query",
"schema": {
"type": "string"
}
},
{
"name": "id",
"required": false,
"in": "query",
"schema": {
"format": "uuid",
"type": "string"
}
},
{
"name": "isArchived",
"required": false,
"in": "query",
"schema": {
"type": "boolean"
}
},
{
"name": "isEncoded",
"required": false,
"in": "query",
"schema": {
"type": "boolean"
}
},
{
"name": "isExternal",
"required": false,
"in": "query",
"schema": {
"type": "boolean"
}
},
{
"name": "isFavorite",
"required": false,
"in": "query",
"schema": {
"type": "boolean"
}
},
{
"name": "isMotion",
"required": false,
"in": "query",
"schema": {
"type": "boolean"
}
},
{
"name": "isOffline",
"required": false,
"in": "query",
"schema": {
"type": "boolean"
}
},
{
"name": "isReadOnly",
"required": false,
"in": "query",
"schema": {
"type": "boolean"
}
},
{
"name": "isVisible",
"required": false,
"in": "query",
"schema": {
"type": "boolean"
}
},
{
"name": "lensModel",
"required": false,
"in": "query",
"schema": {
"type": "string"
}
},
{
"name": "libraryId",
"required": false,
"in": "query",
"schema": {
"format": "uuid",
"type": "string"
}
},
{
"name": "make",
"required": false,
"in": "query",
"schema": {
"type": "string"
}
},
{
"name": "model",
"required": false,
"in": "query",
"schema": {
"type": "string"
}
},
{
"name": "order",
"required": false,
"in": "query",
"schema": {
"$ref": "#/components/schemas/AssetOrder"
}
},
{
"name": "originalFileName",
"required": false,
"in": "query",
"schema": {
"type": "string"
}
},
{
"name": "originalPath",
"required": false,
"in": "query",
"schema": {
"type": "string"
}
},
{
"name": "page",
"required": false,
"in": "query",
"schema": {
"type": "number"
}
},
{
"name": "resizePath",
"required": false,
"in": "query",
"schema": {
"type": "string"
}
},
{
"name": "size",
"required": false,
"in": "query",
"schema": {
"type": "number"
}
},
{
"name": "state",
"required": false,
"in": "query",
"schema": {
"type": "string"
}
},
{
"name": "takenAfter",
"required": false,
"in": "query",
"schema": {
"format": "date-time",
"type": "string"
}
},
{
"name": "takenBefore",
"required": false,
"in": "query",
"schema": {
"format": "date-time",
"type": "string"
}
},
{
"name": "trashedAfter",
"required": false,
"in": "query",
"schema": {
"format": "date-time",
"type": "string"
}
},
{
"name": "trashedBefore",
"required": false,
"in": "query",
"schema": {
"format": "date-time",
"type": "string"
}
},
{
"name": "type",
"required": false,
"in": "query",
"schema": {
"$ref": "#/components/schemas/AssetTypeEnum"
}
},
{
"name": "updatedAfter",
"required": false,
"in": "query",
"schema": {
"format": "date-time",
"type": "string"
}
},
{
"name": "updatedBefore",
"required": false,
"in": "query",
"schema": {
"format": "date-time",
"type": "string"
}
},
{
"name": "webpPath",
"required": false,
"in": "query",
"schema": {
"type": "string"
}
},
{
"name": "withArchived",
"required": false,
"in": "query",
"schema": {
"type": "boolean"
}
},
{
"name": "withDeleted",
"required": false,
"in": "query",
"schema": {
"type": "boolean"
}
},
{
"name": "withExif",
"required": false,
"in": "query",
"schema": {
"type": "boolean"
}
},
{
"name": "withPeople",
"required": false,
"in": "query",
"schema": {
"type": "boolean"
}
},
{
"name": "withStacked",
"required": false,
"in": "query",
"schema": {
"type": "boolean"
}
}
],
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SearchResponseDto"
}
}
},
"description": ""
}
},
"security": [
{
"bearer": []
},
{
"cookie": []
},
{
"api_key": []
}
],
"tags": [
"Search"
]
}
},
"/search/person": { "/search/person": {
"get": { "get": {
"operationId": "searchPerson", "operationId": "searchPerson",
@ -4544,6 +4941,296 @@
] ]
} }
}, },
"/search/smart": {
"get": {
"operationId": "searchSmart",
"parameters": [
{
"name": "city",
"required": false,
"in": "query",
"schema": {
"type": "string"
}
},
{
"name": "country",
"required": false,
"in": "query",
"schema": {
"type": "string"
}
},
{
"name": "createdAfter",
"required": false,
"in": "query",
"schema": {
"format": "date-time",
"type": "string"
}
},
{
"name": "createdBefore",
"required": false,
"in": "query",
"schema": {
"format": "date-time",
"type": "string"
}
},
{
"name": "deviceId",
"required": false,
"in": "query",
"schema": {
"type": "string"
}
},
{
"name": "isArchived",
"required": false,
"in": "query",
"schema": {
"type": "boolean"
}
},
{
"name": "isEncoded",
"required": false,
"in": "query",
"schema": {
"type": "boolean"
}
},
{
"name": "isExternal",
"required": false,
"in": "query",
"schema": {
"type": "boolean"
}
},
{
"name": "isFavorite",
"required": false,
"in": "query",
"schema": {
"type": "boolean"
}
},
{
"name": "isMotion",
"required": false,
"in": "query",
"schema": {
"type": "boolean"
}
},
{
"name": "isOffline",
"required": false,
"in": "query",
"schema": {
"type": "boolean"
}
},
{
"name": "isReadOnly",
"required": false,
"in": "query",
"schema": {
"type": "boolean"
}
},
{
"name": "isVisible",
"required": false,
"in": "query",
"schema": {
"type": "boolean"
}
},
{
"name": "lensModel",
"required": false,
"in": "query",
"schema": {
"type": "string"
}
},
{
"name": "libraryId",
"required": false,
"in": "query",
"schema": {
"format": "uuid",
"type": "string"
}
},
{
"name": "make",
"required": false,
"in": "query",
"schema": {
"type": "string"
}
},
{
"name": "model",
"required": false,
"in": "query",
"schema": {
"type": "string"
}
},
{
"name": "page",
"required": false,
"in": "query",
"schema": {
"type": "number"
}
},
{
"name": "query",
"required": true,
"in": "query",
"schema": {
"type": "string"
}
},
{
"name": "size",
"required": false,
"in": "query",
"schema": {
"type": "number"
}
},
{
"name": "state",
"required": false,
"in": "query",
"schema": {
"type": "string"
}
},
{
"name": "takenAfter",
"required": false,
"in": "query",
"schema": {
"format": "date-time",
"type": "string"
}
},
{
"name": "takenBefore",
"required": false,
"in": "query",
"schema": {
"format": "date-time",
"type": "string"
}
},
{
"name": "trashedAfter",
"required": false,
"in": "query",
"schema": {
"format": "date-time",
"type": "string"
}
},
{
"name": "trashedBefore",
"required": false,
"in": "query",
"schema": {
"format": "date-time",
"type": "string"
}
},
{
"name": "type",
"required": false,
"in": "query",
"schema": {
"$ref": "#/components/schemas/AssetTypeEnum"
}
},
{
"name": "updatedAfter",
"required": false,
"in": "query",
"schema": {
"format": "date-time",
"type": "string"
}
},
{
"name": "updatedBefore",
"required": false,
"in": "query",
"schema": {
"format": "date-time",
"type": "string"
}
},
{
"name": "withArchived",
"required": false,
"in": "query",
"schema": {
"type": "boolean"
}
},
{
"name": "withDeleted",
"required": false,
"in": "query",
"schema": {
"type": "boolean"
}
},
{
"name": "withExif",
"required": false,
"in": "query",
"schema": {
"type": "boolean"
}
}
],
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SearchResponseDto"
}
}
},
"description": ""
}
},
"security": [
{
"bearer": []
},
{
"cookie": []
},
{
"api_key": []
}
],
"tags": [
"Search"
]
}
},
"/server-info": { "/server-info": {
"get": { "get": {
"operationId": "getServerInfo", "operationId": "getServerInfo",
@ -8458,6 +9145,10 @@
}, },
"type": "array" "type": "array"
}, },
"nextPage": {
"nullable": true,
"type": "string"
},
"total": { "total": {
"type": "integer" "type": "integer"
} }
@ -8466,6 +9157,7 @@
"count", "count",
"facets", "facets",
"items", "items",
"nextPage",
"total" "total"
], ],
"type": "object" "type": "object"

File diff suppressed because it is too large Load Diff

View File

@ -588,6 +588,7 @@ export type SearchAssetResponseDto = {
count: number; count: number;
facets: SearchFacetResponseDto[]; facets: SearchFacetResponseDto[];
items: AssetResponseDto[]; items: AssetResponseDto[];
nextPage: string | null;
total: number; total: number;
}; };
export type SearchResponseDto = { export type SearchResponseDto = {
@ -1461,7 +1462,7 @@ export function updateAsset({ id, updateAssetDto }: {
body: updateAssetDto body: updateAssetDto
}))); })));
} }
export function searchAssets({ checksum, city, country, createdAfter, createdBefore, deviceAssetId, deviceId, encodedVideoPath, id, isArchived, isEncoded, isExternal, isFavorite, isMotion, isOffline, isReadOnly, isVisible, lensModel, libraryId, make, model, order, originalFileName, originalPath, page, resizePath, size, state, takenAfter, takenBefore, trashedAfter, trashedBefore, $type, updatedAfter, updatedBefore, webpPath, withDeleted, withExif, withPeople, withStacked }: { export function searchAssets({ checksum, city, country, createdAfter, createdBefore, deviceAssetId, deviceId, encodedVideoPath, id, isArchived, isEncoded, isExternal, isFavorite, isMotion, isOffline, isReadOnly, isVisible, lensModel, libraryId, make, model, order, originalFileName, originalPath, page, resizePath, size, state, takenAfter, takenBefore, trashedAfter, trashedBefore, $type, updatedAfter, updatedBefore, webpPath, withArchived, withDeleted, withExif, withPeople, withStacked }: {
checksum?: string; checksum?: string;
city?: string; city?: string;
country?: string; country?: string;
@ -1498,6 +1499,7 @@ export function searchAssets({ checksum, city, country, createdAfter, createdBef
updatedAfter?: string; updatedAfter?: string;
updatedBefore?: string; updatedBefore?: string;
webpPath?: string; webpPath?: string;
withArchived?: boolean;
withDeleted?: boolean; withDeleted?: boolean;
withExif?: boolean; withExif?: boolean;
withPeople?: boolean; withPeople?: boolean;
@ -1543,6 +1545,7 @@ export function searchAssets({ checksum, city, country, createdAfter, createdBef
updatedAfter, updatedAfter,
updatedBefore, updatedBefore,
webpPath, webpPath,
withArchived,
withDeleted, withDeleted,
withExif, withExif,
withPeople, withPeople,
@ -2047,12 +2050,14 @@ export function getPersonThumbnail({ id }: {
...opts ...opts
})); }));
} }
export function search({ clip, motion, q, query, recent, smart, $type, withArchived }: { export function search({ clip, motion, page, q, query, recent, size, smart, $type, withArchived }: {
clip?: boolean; clip?: boolean;
motion?: boolean; motion?: boolean;
page?: number;
q?: string; q?: string;
query?: string; query?: string;
recent?: boolean; recent?: boolean;
size?: number;
smart?: boolean; smart?: boolean;
$type?: "IMAGE" | "VIDEO" | "AUDIO" | "OTHER"; $type?: "IMAGE" | "VIDEO" | "AUDIO" | "OTHER";
withArchived?: boolean; withArchived?: boolean;
@ -2063,9 +2068,11 @@ export function search({ clip, motion, q, query, recent, smart, $type, withArchi
}>(`/search${QS.query(QS.explode({ }>(`/search${QS.query(QS.explode({
clip, clip,
motion, motion,
page,
q, q,
query, query,
recent, recent,
size,
smart, smart,
"type": $type, "type": $type,
withArchived withArchived
@ -2081,6 +2088,98 @@ export function getExploreData(opts?: Oazapfts.RequestOpts) {
...opts ...opts
})); }));
} }
export function searchMetadata({ checksum, city, country, createdAfter, createdBefore, deviceAssetId, deviceId, encodedVideoPath, id, isArchived, isEncoded, isExternal, isFavorite, isMotion, isOffline, isReadOnly, isVisible, lensModel, libraryId, make, model, order, originalFileName, originalPath, page, resizePath, size, state, takenAfter, takenBefore, trashedAfter, trashedBefore, $type, updatedAfter, updatedBefore, webpPath, withArchived, withDeleted, withExif, withPeople, withStacked }: {
checksum?: string;
city?: string;
country?: string;
createdAfter?: string;
createdBefore?: string;
deviceAssetId?: string;
deviceId?: string;
encodedVideoPath?: string;
id?: string;
isArchived?: boolean;
isEncoded?: boolean;
isExternal?: boolean;
isFavorite?: boolean;
isMotion?: boolean;
isOffline?: boolean;
isReadOnly?: boolean;
isVisible?: boolean;
lensModel?: string;
libraryId?: string;
make?: string;
model?: string;
order?: AssetOrder;
originalFileName?: string;
originalPath?: string;
page?: number;
resizePath?: string;
size?: number;
state?: string;
takenAfter?: string;
takenBefore?: string;
trashedAfter?: string;
trashedBefore?: string;
$type?: AssetTypeEnum;
updatedAfter?: string;
updatedBefore?: string;
webpPath?: string;
withArchived?: boolean;
withDeleted?: boolean;
withExif?: boolean;
withPeople?: boolean;
withStacked?: boolean;
}, opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchJson<{
status: 200;
data: SearchResponseDto;
}>(`/search/metadata${QS.query(QS.explode({
checksum,
city,
country,
createdAfter,
createdBefore,
deviceAssetId,
deviceId,
encodedVideoPath,
id,
isArchived,
isEncoded,
isExternal,
isFavorite,
isMotion,
isOffline,
isReadOnly,
isVisible,
lensModel,
libraryId,
make,
model,
order,
originalFileName,
originalPath,
page,
resizePath,
size,
state,
takenAfter,
takenBefore,
trashedAfter,
trashedBefore,
"type": $type,
updatedAfter,
updatedBefore,
webpPath,
withArchived,
withDeleted,
withExif,
withPeople,
withStacked
}))}`, {
...opts
}));
}
export function searchPerson({ name, withHidden }: { export function searchPerson({ name, withHidden }: {
name: string; name: string;
withHidden?: boolean; withHidden?: boolean;
@ -2095,6 +2194,78 @@ export function searchPerson({ name, withHidden }: {
...opts ...opts
})); }));
} }
export function searchSmart({ city, country, createdAfter, createdBefore, deviceId, isArchived, isEncoded, isExternal, isFavorite, isMotion, isOffline, isReadOnly, isVisible, lensModel, libraryId, make, model, page, query, size, state, takenAfter, takenBefore, trashedAfter, trashedBefore, $type, updatedAfter, updatedBefore, withArchived, withDeleted, withExif }: {
city?: string;
country?: string;
createdAfter?: string;
createdBefore?: string;
deviceId?: string;
isArchived?: boolean;
isEncoded?: boolean;
isExternal?: boolean;
isFavorite?: boolean;
isMotion?: boolean;
isOffline?: boolean;
isReadOnly?: boolean;
isVisible?: boolean;
lensModel?: string;
libraryId?: string;
make?: string;
model?: string;
page?: number;
query: string;
size?: number;
state?: string;
takenAfter?: string;
takenBefore?: string;
trashedAfter?: string;
trashedBefore?: string;
$type?: AssetTypeEnum;
updatedAfter?: string;
updatedBefore?: string;
withArchived?: boolean;
withDeleted?: boolean;
withExif?: boolean;
}, opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchJson<{
status: 200;
data: SearchResponseDto;
}>(`/search/smart${QS.query(QS.explode({
city,
country,
createdAfter,
createdBefore,
deviceId,
isArchived,
isEncoded,
isExternal,
isFavorite,
isMotion,
isOffline,
isReadOnly,
isVisible,
lensModel,
libraryId,
make,
model,
page,
query,
size,
state,
takenAfter,
takenBefore,
trashedAfter,
trashedBefore,
"type": $type,
updatedAfter,
updatedBefore,
withArchived,
withDeleted,
withExif
}))}`, {
...opts
}));
}
export function getServerInfo(opts?: Oazapfts.RequestOpts) { export function getServerInfo(opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchJson<{ return oazapfts.ok(oazapfts.fetchJson<{
status: 200; status: 200;

View File

@ -169,7 +169,11 @@ describe(`${AssetController.name} (e2e)`, () => {
{ {
should: 'should reject size as a string', should: 'should reject size as a string',
query: { size: 'abc' }, query: { size: 'abc' },
expected: ['size must not be less than 1', 'size must be an integer number'], expected: [
'size must not be greater than 1000',
'size must not be less than 1',
'size must be an integer number',
],
}, },
{ {
should: 'should reject an invalid size', should: 'should reject an invalid size',
@ -478,7 +482,7 @@ describe(`${AssetController.name} (e2e)`, () => {
}), }),
}, },
{ {
should: 'sohuld search by make', should: 'should search by make',
deferred: () => ({ deferred: () => ({
query: { make: 'Cannon' }, query: { make: 'Cannon' },
assets: [asset3], assets: [asset3],

View File

@ -1,7 +1,7 @@
import { import {
AssetResponseDto, AssetResponseDto,
IAssetRepository, IAssetRepository,
ISmartInfoRepository, ISearchRepository,
LibraryResponseDto, LibraryResponseDto,
LoginResponseDto, LoginResponseDto,
mapAsset, mapAsset,
@ -20,14 +20,14 @@ describe(`${SearchController.name}`, () => {
let accessToken: string; let accessToken: string;
let libraries: LibraryResponseDto[]; let libraries: LibraryResponseDto[];
let assetRepository: IAssetRepository; let assetRepository: IAssetRepository;
let smartInfoRepository: ISmartInfoRepository; let smartInfoRepository: ISearchRepository;
let asset1: AssetResponseDto; let asset1: AssetResponseDto;
beforeAll(async () => { beforeAll(async () => {
app = await testApp.create(); app = await testApp.create();
server = app.getHttpServer(); server = app.getHttpServer();
assetRepository = app.get<IAssetRepository>(IAssetRepository); assetRepository = app.get<IAssetRepository>(IAssetRepository);
smartInfoRepository = app.get<ISmartInfoRepository>(ISmartInfoRepository); smartInfoRepository = app.get<ISearchRepository>(ISearchRepository);
}); });
afterAll(async () => { afterAll(async () => {

View File

@ -31,8 +31,6 @@ import {
AssetBulkUpdateDto, AssetBulkUpdateDto,
AssetJobName, AssetJobName,
AssetJobsDto, AssetJobsDto,
AssetOrder,
AssetSearchDto,
AssetStatsDto, AssetStatsDto,
MapMarkerDto, MapMarkerDto,
MemoryLaneDto, MemoryLaneDto,
@ -92,34 +90,6 @@ export class AssetService {
this.configCore = SystemConfigCore.create(configRepository); this.configCore = SystemConfigCore.create(configRepository);
} }
search(auth: AuthDto, dto: AssetSearchDto) {
let checksum: Buffer | undefined;
if (dto.checksum) {
const encoding = dto.checksum.length === 28 ? 'base64' : 'hex';
checksum = Buffer.from(dto.checksum, encoding);
}
const enumToOrder = { [AssetOrder.ASC]: 'ASC', [AssetOrder.DESC]: 'DESC' } as const;
const order = dto.order ? enumToOrder[dto.order] : undefined;
return this.assetRepository
.search({
...dto,
order,
checksum,
ownerId: auth.user.id,
})
.then((assets) =>
assets.map((asset) =>
mapAsset(asset, {
stripMetadata: false,
withStack: true,
}),
),
);
}
canUploadFile({ auth, fieldName, file }: UploadRequest): true { canUploadFile({ auth, fieldName, file }: UploadRequest): true {
this.access.requireUploadAccess(auth); this.access.requireUploadAccess(auth);

View File

@ -1,20 +1,16 @@
import { AssetType } from '@app/infra/entities';
import { ApiProperty } from '@nestjs/swagger';
import { Type } from 'class-transformer'; import { Type } from 'class-transformer';
import { import {
IsBoolean, IsBoolean,
IsDateString, IsDateString,
IsEnum,
IsInt, IsInt,
IsLatitude, IsLatitude,
IsLongitude, IsLongitude,
IsNotEmpty, IsNotEmpty,
IsPositive, IsPositive,
IsString, IsString,
Min,
ValidateIf, ValidateIf,
} from 'class-validator'; } from 'class-validator';
import { Optional, QueryBoolean, QueryDate, ValidateUUID } from '../../domain.util'; import { Optional, ValidateUUID } from '../../domain.util';
import { BulkIdsDto } from '../response-dto'; import { BulkIdsDto } from '../response-dto';
export class DeviceIdDto { export class DeviceIdDto {
@ -32,152 +28,6 @@ const hasGPS = (o: { latitude: undefined; longitude: undefined }) =>
o.latitude !== undefined || o.longitude !== undefined; o.latitude !== undefined || o.longitude !== undefined;
const ValidateGPS = () => ValidateIf(hasGPS); const ValidateGPS = () => ValidateIf(hasGPS);
export class AssetSearchDto {
@ValidateUUID({ optional: true })
id?: string;
@ValidateUUID({ optional: true })
libraryId?: string;
@IsString()
@Optional()
deviceAssetId?: string;
@IsString()
@Optional()
deviceId?: string;
@IsEnum(AssetType)
@Optional()
@ApiProperty({ enumName: 'AssetTypeEnum', enum: AssetType })
type?: AssetType;
@IsString()
@Optional()
checksum?: string;
@QueryBoolean({ optional: true })
isArchived?: boolean;
@QueryBoolean({ optional: true })
isEncoded?: boolean;
@QueryBoolean({ optional: true })
isExternal?: boolean;
@QueryBoolean({ optional: true })
isFavorite?: boolean;
@QueryBoolean({ optional: true })
isMotion?: boolean;
@QueryBoolean({ optional: true })
isOffline?: boolean;
@QueryBoolean({ optional: true })
isReadOnly?: boolean;
@QueryBoolean({ optional: true })
isVisible?: boolean;
@QueryBoolean({ optional: true })
withDeleted?: boolean;
@QueryBoolean({ optional: true })
withStacked?: boolean;
@QueryBoolean({ optional: true })
withExif?: boolean;
@QueryBoolean({ optional: true })
withPeople?: boolean;
@QueryDate({ optional: true })
createdBefore?: Date;
@QueryDate({ optional: true })
createdAfter?: Date;
@QueryDate({ optional: true })
updatedBefore?: Date;
@QueryDate({ optional: true })
updatedAfter?: Date;
@QueryDate({ optional: true })
trashedBefore?: Date;
@QueryDate({ optional: true })
trashedAfter?: Date;
@QueryDate({ optional: true })
takenBefore?: Date;
@QueryDate({ optional: true })
takenAfter?: Date;
@IsString()
@Optional()
originalFileName?: string;
@IsString()
@Optional()
originalPath?: string;
@IsString()
@Optional()
resizePath?: string;
@IsString()
@Optional()
webpPath?: string;
@IsString()
@Optional()
encodedVideoPath?: string;
@IsString()
@Optional()
city?: string;
@IsString()
@Optional()
state?: string;
@IsString()
@Optional()
country?: string;
@IsString()
@Optional()
make?: string;
@IsString()
@Optional()
model?: string;
@IsString()
@Optional()
lensModel?: string;
@IsEnum(AssetOrder)
@Optional()
@ApiProperty({ enumName: 'AssetOrder', enum: AssetOrder })
order?: AssetOrder;
@IsInt()
@Min(1)
@Type(() => Number)
@Optional()
page?: number;
@IsInt()
@Min(1)
@Type(() => Number)
@Optional()
size?: number;
}
export class AssetBulkUpdateDto extends BulkIdsDto { export class AssetBulkUpdateDto extends BulkIdsDto {
@Optional() @Optional()
@IsBoolean() @IsBoolean()

View File

@ -137,6 +137,17 @@ export interface PaginationOptions {
skip?: number; skip?: number;
} }
export enum PaginationMode {
LIMIT_OFFSET = 'limit-offset',
SKIP_TAKE = 'skip-take',
}
export interface PaginatedBuilderOptions {
take: number;
skip?: number;
mode?: PaginationMode;
}
export interface PaginationResult<T> { export interface PaginationResult<T> {
items: T[]; items: T[];
hasNextPage: boolean; hasNextPage: boolean;

View File

@ -13,7 +13,7 @@ import {
newMediaRepositoryMock, newMediaRepositoryMock,
newMoveRepositoryMock, newMoveRepositoryMock,
newPersonRepositoryMock, newPersonRepositoryMock,
newSmartInfoRepositoryMock, newSearchRepositoryMock,
newStorageRepositoryMock, newStorageRepositoryMock,
newSystemConfigRepositoryMock, newSystemConfigRepositoryMock,
personStub, personStub,
@ -31,7 +31,7 @@ import {
IMediaRepository, IMediaRepository,
IMoveRepository, IMoveRepository,
IPersonRepository, IPersonRepository,
ISmartInfoRepository, ISearchRepository,
IStorageRepository, IStorageRepository,
ISystemConfigRepository, ISystemConfigRepository,
WithoutProperty, WithoutProperty,
@ -76,7 +76,7 @@ describe(PersonService.name, () => {
let moveMock: jest.Mocked<IMoveRepository>; let moveMock: jest.Mocked<IMoveRepository>;
let personMock: jest.Mocked<IPersonRepository>; let personMock: jest.Mocked<IPersonRepository>;
let storageMock: jest.Mocked<IStorageRepository>; let storageMock: jest.Mocked<IStorageRepository>;
let smartInfoMock: jest.Mocked<ISmartInfoRepository>; let searchMock: jest.Mocked<ISearchRepository>;
let cryptoMock: jest.Mocked<ICryptoRepository>; let cryptoMock: jest.Mocked<ICryptoRepository>;
let sut: PersonService; let sut: PersonService;
@ -90,7 +90,7 @@ describe(PersonService.name, () => {
mediaMock = newMediaRepositoryMock(); mediaMock = newMediaRepositoryMock();
personMock = newPersonRepositoryMock(); personMock = newPersonRepositoryMock();
storageMock = newStorageRepositoryMock(); storageMock = newStorageRepositoryMock();
smartInfoMock = newSmartInfoRepositoryMock(); searchMock = newSearchRepositoryMock();
cryptoMock = newCryptoRepositoryMock(); cryptoMock = newCryptoRepositoryMock();
sut = new PersonService( sut = new PersonService(
accessMock, accessMock,
@ -102,7 +102,7 @@ describe(PersonService.name, () => {
configMock, configMock,
storageMock, storageMock,
jobMock, jobMock,
smartInfoMock, searchMock,
cryptoMock, cryptoMock,
); );
@ -752,7 +752,7 @@ describe(PersonService.name, () => {
it('should create a face with no person and queue recognition job', async () => { it('should create a face with no person and queue recognition job', async () => {
personMock.createFaces.mockResolvedValue([faceStub.face1.id]); personMock.createFaces.mockResolvedValue([faceStub.face1.id]);
machineLearningMock.detectFaces.mockResolvedValue([detectFaceMock]); machineLearningMock.detectFaces.mockResolvedValue([detectFaceMock]);
smartInfoMock.searchFaces.mockResolvedValue([{ face: faceStub.face1, distance: 0.7 }]); searchMock.searchFaces.mockResolvedValue([{ face: faceStub.face1, distance: 0.7 }]);
assetMock.getByIds.mockResolvedValue([assetStub.image]); assetMock.getByIds.mockResolvedValue([assetStub.image]);
const face = { const face = {
assetId: 'asset-id', assetId: 'asset-id',
@ -823,7 +823,7 @@ describe(PersonService.name, () => {
configMock.load.mockResolvedValue([ configMock.load.mockResolvedValue([
{ key: SystemConfigKey.MACHINE_LEARNING_FACIAL_RECOGNITION_MIN_FACES, value: 1 }, { key: SystemConfigKey.MACHINE_LEARNING_FACIAL_RECOGNITION_MIN_FACES, value: 1 },
]); ]);
smartInfoMock.searchFaces.mockResolvedValue(faces); searchMock.searchFaces.mockResolvedValue(faces);
personMock.getFaceByIdWithAssets.mockResolvedValue(faceStub.noPerson1); personMock.getFaceByIdWithAssets.mockResolvedValue(faceStub.noPerson1);
personMock.create.mockResolvedValue(faceStub.primaryFace1.person); personMock.create.mockResolvedValue(faceStub.primaryFace1.person);
@ -850,7 +850,7 @@ describe(PersonService.name, () => {
configMock.load.mockResolvedValue([ configMock.load.mockResolvedValue([
{ key: SystemConfigKey.MACHINE_LEARNING_FACIAL_RECOGNITION_MIN_FACES, value: 1 }, { key: SystemConfigKey.MACHINE_LEARNING_FACIAL_RECOGNITION_MIN_FACES, value: 1 },
]); ]);
smartInfoMock.searchFaces.mockResolvedValue(faces); searchMock.searchFaces.mockResolvedValue(faces);
personMock.getFaceByIdWithAssets.mockResolvedValue(faceStub.noPerson1); personMock.getFaceByIdWithAssets.mockResolvedValue(faceStub.noPerson1);
personMock.create.mockResolvedValue(personStub.withName); personMock.create.mockResolvedValue(personStub.withName);
@ -869,14 +869,14 @@ describe(PersonService.name, () => {
it('should not queue face with no matches', async () => { it('should not queue face with no matches', async () => {
const faces = [{ face: faceStub.noPerson1, distance: 0 }] as FaceSearchResult[]; const faces = [{ face: faceStub.noPerson1, distance: 0 }] as FaceSearchResult[];
smartInfoMock.searchFaces.mockResolvedValue(faces); searchMock.searchFaces.mockResolvedValue(faces);
personMock.getFaceByIdWithAssets.mockResolvedValue(faceStub.noPerson1); personMock.getFaceByIdWithAssets.mockResolvedValue(faceStub.noPerson1);
personMock.create.mockResolvedValue(personStub.withName); personMock.create.mockResolvedValue(personStub.withName);
await sut.handleRecognizeFaces({ id: faceStub.noPerson1.id }); await sut.handleRecognizeFaces({ id: faceStub.noPerson1.id });
expect(jobMock.queue).not.toHaveBeenCalled(); expect(jobMock.queue).not.toHaveBeenCalled();
expect(smartInfoMock.searchFaces).toHaveBeenCalledTimes(1); expect(searchMock.searchFaces).toHaveBeenCalledTimes(1);
expect(personMock.create).not.toHaveBeenCalled(); expect(personMock.create).not.toHaveBeenCalled();
expect(personMock.reassignFaces).not.toHaveBeenCalled(); expect(personMock.reassignFaces).not.toHaveBeenCalled();
}); });
@ -890,7 +890,7 @@ describe(PersonService.name, () => {
configMock.load.mockResolvedValue([ configMock.load.mockResolvedValue([
{ key: SystemConfigKey.MACHINE_LEARNING_FACIAL_RECOGNITION_MIN_FACES, value: 3 }, { key: SystemConfigKey.MACHINE_LEARNING_FACIAL_RECOGNITION_MIN_FACES, value: 3 },
]); ]);
smartInfoMock.searchFaces.mockResolvedValue(faces); searchMock.searchFaces.mockResolvedValue(faces);
personMock.getFaceByIdWithAssets.mockResolvedValue(faceStub.noPerson1); personMock.getFaceByIdWithAssets.mockResolvedValue(faceStub.noPerson1);
personMock.create.mockResolvedValue(personStub.withName); personMock.create.mockResolvedValue(personStub.withName);
@ -900,7 +900,7 @@ describe(PersonService.name, () => {
name: JobName.FACIAL_RECOGNITION, name: JobName.FACIAL_RECOGNITION,
data: { id: faceStub.noPerson1.id, deferred: true }, data: { id: faceStub.noPerson1.id, deferred: true },
}); });
expect(smartInfoMock.searchFaces).toHaveBeenCalledTimes(1); expect(searchMock.searchFaces).toHaveBeenCalledTimes(1);
expect(personMock.create).not.toHaveBeenCalled(); expect(personMock.create).not.toHaveBeenCalled();
expect(personMock.reassignFaces).not.toHaveBeenCalled(); expect(personMock.reassignFaces).not.toHaveBeenCalled();
}); });
@ -914,14 +914,14 @@ describe(PersonService.name, () => {
configMock.load.mockResolvedValue([ configMock.load.mockResolvedValue([
{ key: SystemConfigKey.MACHINE_LEARNING_FACIAL_RECOGNITION_MIN_FACES, value: 3 }, { key: SystemConfigKey.MACHINE_LEARNING_FACIAL_RECOGNITION_MIN_FACES, value: 3 },
]); ]);
smartInfoMock.searchFaces.mockResolvedValueOnce(faces).mockResolvedValueOnce([]); searchMock.searchFaces.mockResolvedValueOnce(faces).mockResolvedValueOnce([]);
personMock.getFaceByIdWithAssets.mockResolvedValue(faceStub.noPerson1); personMock.getFaceByIdWithAssets.mockResolvedValue(faceStub.noPerson1);
personMock.create.mockResolvedValue(personStub.withName); personMock.create.mockResolvedValue(personStub.withName);
await sut.handleRecognizeFaces({ id: faceStub.noPerson1.id, deferred: true }); await sut.handleRecognizeFaces({ id: faceStub.noPerson1.id, deferred: true });
expect(jobMock.queue).not.toHaveBeenCalled(); expect(jobMock.queue).not.toHaveBeenCalled();
expect(smartInfoMock.searchFaces).toHaveBeenCalledTimes(2); expect(searchMock.searchFaces).toHaveBeenCalledTimes(2);
expect(personMock.create).not.toHaveBeenCalled(); expect(personMock.create).not.toHaveBeenCalled();
expect(personMock.reassignFaces).not.toHaveBeenCalled(); expect(personMock.reassignFaces).not.toHaveBeenCalled();
}); });

View File

@ -20,7 +20,7 @@ import {
IMediaRepository, IMediaRepository,
IMoveRepository, IMoveRepository,
IPersonRepository, IPersonRepository,
ISmartInfoRepository, ISearchRepository,
IStorageRepository, IStorageRepository,
ISystemConfigRepository, ISystemConfigRepository,
JobItem, JobItem,
@ -61,7 +61,7 @@ export class PersonService {
@Inject(ISystemConfigRepository) configRepository: ISystemConfigRepository, @Inject(ISystemConfigRepository) configRepository: ISystemConfigRepository,
@Inject(IStorageRepository) private storageRepository: IStorageRepository, @Inject(IStorageRepository) private storageRepository: IStorageRepository,
@Inject(IJobRepository) private jobRepository: IJobRepository, @Inject(IJobRepository) private jobRepository: IJobRepository,
@Inject(ISmartInfoRepository) private smartInfoRepository: ISmartInfoRepository, @Inject(ISearchRepository) private smartInfoRepository: ISearchRepository,
@Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository, @Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository,
) { ) {
this.access = AccessCore.create(accessRepository); this.access = AccessCore.create(accessRepository);
@ -285,15 +285,7 @@ export class PersonService {
const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) => { const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) => {
return force return force
? this.assetRepository.getAll(pagination, { ? this.assetRepository.getAll(pagination, { orderDirection: 'DESC', withFaces: true })
order: 'DESC',
withFaces: true,
withPeople: false,
withSmartInfo: false,
withSmartSearch: false,
withExif: false,
withStacked: false,
})
: this.assetRepository.getWithout(pagination, WithoutProperty.FACES); : this.assetRepository.getWithout(pagination, WithoutProperty.FACES);
}); });

View File

@ -1,4 +1,4 @@
import { SearchExploreItem } from '@app/domain'; import { AssetSearchOptions, SearchExploreItem } from '@app/domain';
import { AssetEntity, AssetJobStatusEntity, AssetType, ExifEntity } from '@app/infra/entities'; import { AssetEntity, AssetJobStatusEntity, AssetType, ExifEntity } from '@app/infra/entities';
import { FindOptionsRelations, FindOptionsSelect } from 'typeorm'; import { FindOptionsRelations, FindOptionsSelect } from 'typeorm';
import { Paginated, PaginationOptions } from '../domain.util'; import { Paginated, PaginationOptions } from '../domain.util';
@ -11,64 +11,6 @@ export interface AssetStatsOptions {
isTrashed?: boolean; isTrashed?: boolean;
} }
export interface AssetSearchOptions {
id?: string;
libraryId?: string;
deviceAssetId?: string;
deviceId?: string;
ownerId?: string;
type?: AssetType;
checksum?: Buffer;
isArchived?: boolean;
isEncoded?: boolean;
isExternal?: boolean;
isFavorite?: boolean;
isMotion?: boolean;
isOffline?: boolean;
isReadOnly?: boolean;
isVisible?: boolean;
withDeleted?: boolean;
withStacked?: boolean;
withExif?: boolean;
withPeople?: boolean;
withSmartInfo?: boolean;
withSmartSearch?: boolean;
withFaces?: boolean;
createdBefore?: Date;
createdAfter?: Date;
updatedBefore?: Date;
updatedAfter?: Date;
trashedBefore?: Date;
trashedAfter?: Date;
takenBefore?: Date;
takenAfter?: Date;
originalFileName?: string;
originalPath?: string;
resizePath?: string;
webpPath?: string;
encodedVideoPath?: string;
city?: string;
state?: string;
country?: string;
make?: string;
model?: string;
lensModel?: string;
/** defaults to 'DESC' */
order?: 'ASC' | 'DESC';
/** defaults to 1 */
page?: number;
/** defaults to 250 */
size?: number;
}
export interface LivePhotoSearchOptions { export interface LivePhotoSearchOptions {
ownerId: string; ownerId: string;
livePhotoCID: string; livePhotoCID: string;
@ -204,7 +146,6 @@ export interface IAssetRepository {
getTimeBucket(timeBucket: string, options: TimeBucketOptions): Promise<AssetEntity[]>; getTimeBucket(timeBucket: string, options: TimeBucketOptions): Promise<AssetEntity[]>;
upsertExif(exif: Partial<ExifEntity>): Promise<void>; upsertExif(exif: Partial<ExifEntity>): Promise<void>;
upsertJobStatus(jobStatus: Partial<AssetJobStatusEntity>): Promise<void>; upsertJobStatus(jobStatus: Partial<AssetJobStatusEntity>): Promise<void>;
search(options: AssetSearchOptions): Promise<AssetEntity[]>;
getAssetIdByCity(userId: string, options: AssetExploreFieldOptions): Promise<SearchExploreItem<string>>; getAssetIdByCity(userId: string, options: AssetExploreFieldOptions): Promise<SearchExploreItem<string>>;
getAssetIdByTag(userId: string, options: AssetExploreFieldOptions): Promise<SearchExploreItem<string>>; getAssetIdByTag(userId: string, options: AssetExploreFieldOptions): Promise<SearchExploreItem<string>>;
searchMetadata(query: string, userIds: string[], options: MetadataSearchOptions): Promise<AssetEntity[]>; searchMetadata(query: string, userIds: string[], options: MetadataSearchOptions): Promise<AssetEntity[]>;

View File

@ -19,7 +19,6 @@ export * from './person.repository';
export * from './search.repository'; export * from './search.repository';
export * from './server-info.repository'; export * from './server-info.repository';
export * from './shared-link.repository'; export * from './shared-link.repository';
export * from './smart-info.repository';
export * from './storage.repository'; export * from './storage.repository';
export * from './system-config.repository'; export * from './system-config.repository';
export * from './system-metadata.repository'; export * from './system-metadata.repository';

View File

@ -1,4 +1,7 @@
import { AssetType } from '@app/infra/entities'; import { AssetEntity, AssetFaceEntity, AssetType, SmartInfoEntity } from '@app/infra/entities';
import { Paginated } from '../domain.util';
export const ISearchRepository = 'ISearchRepository';
export enum SearchStrategy { export enum SearchStrategy {
SMART = 'SMART', SMART = 'SMART',
@ -54,3 +57,122 @@ export interface SearchExploreItem<T> {
fieldName: string; fieldName: string;
items: SearchExploreItemSet<T>; items: SearchExploreItemSet<T>;
} }
export type Embedding = number[];
export interface SearchAssetIDOptions {
checksum?: Buffer;
deviceAssetId?: string;
id?: string;
}
export interface SearchUserIDOptions {
deviceId?: string;
libraryId?: string;
ownerId?: string;
}
export type SearchIDOptions = SearchAssetIDOptions & SearchUserIDOptions;
export interface SearchStatusOptions {
isArchived?: boolean;
isEncoded?: boolean;
isExternal?: boolean;
isFavorite?: boolean;
isMotion?: boolean;
isOffline?: boolean;
isReadOnly?: boolean;
isVisible?: boolean;
type?: AssetType;
withArchived?: boolean;
withDeleted?: boolean;
}
export interface SearchOneToOneRelationOptions {
withExif?: boolean;
withSmartInfo?: boolean;
}
export interface SearchRelationOptions extends SearchOneToOneRelationOptions {
withFaces?: boolean;
withPeople?: boolean;
withStacked?: boolean;
}
export interface SearchDateOptions {
createdBefore?: Date;
createdAfter?: Date;
takenBefore?: Date;
takenAfter?: Date;
trashedBefore?: Date;
trashedAfter?: Date;
updatedBefore?: Date;
updatedAfter?: Date;
}
export interface SearchPathOptions {
encodedVideoPath?: string;
originalFileName?: string;
originalPath?: string;
resizePath?: string;
webpPath?: string;
}
export interface SearchExifOptions {
city?: string;
country?: string;
lensModel?: string;
make?: string;
model?: string;
state?: string;
}
export interface SearchEmbeddingOptions {
embedding: Embedding;
userIds: string[];
}
export interface SearchOrderOptions {
orderDirection?: 'ASC' | 'DESC';
}
export interface SearchPaginationOptions {
page: number;
size: number;
}
export type AssetSearchOptions = SearchDateOptions &
SearchIDOptions &
SearchExifOptions &
SearchOrderOptions &
SearchPathOptions &
SearchRelationOptions &
SearchStatusOptions;
export type AssetSearchBuilderOptions = Omit<AssetSearchOptions, 'orderDirection'>;
export type SmartSearchOptions = SearchDateOptions &
SearchEmbeddingOptions &
SearchExifOptions &
SearchOneToOneRelationOptions &
SearchStatusOptions &
SearchUserIDOptions;
export interface FaceEmbeddingSearch extends SearchEmbeddingOptions {
hasPerson?: boolean;
numResults: number;
maxDistance?: number;
}
export interface FaceSearchResult {
distance: number;
face: AssetFaceEntity;
}
export interface ISearchRepository {
init(modelName: string): Promise<void>;
searchMetadata(pagination: SearchPaginationOptions, options: AssetSearchOptions): Paginated<AssetEntity>;
searchSmart(pagination: SearchPaginationOptions, options: SmartSearchOptions): Paginated<AssetEntity>;
searchFaces(search: FaceEmbeddingSearch): Promise<FaceSearchResult[]>;
upsert(smartInfo: Partial<SmartInfoEntity>, embedding?: Embedding): Promise<void>;
}

View File

@ -1,29 +0,0 @@
import { AssetEntity, AssetFaceEntity, SmartInfoEntity } from '@app/infra/entities';
export const ISmartInfoRepository = 'ISmartInfoRepository';
export type Embedding = number[];
export interface EmbeddingSearch {
userIds: string[];
embedding: Embedding;
numResults: number;
withArchived?: boolean;
}
export interface FaceEmbeddingSearch extends EmbeddingSearch {
maxDistance?: number;
hasPerson?: boolean;
}
export interface FaceSearchResult {
face: AssetFaceEntity;
distance: number;
}
export interface ISmartInfoRepository {
init(modelName: string): Promise<void>;
searchCLIP(search: EmbeddingSearch): Promise<AssetEntity[]>;
searchFaces(search: FaceEmbeddingSearch): Promise<FaceSearchResult[]>;
upsert(smartInfo: Partial<SmartInfoEntity>, embedding?: Embedding): Promise<void>;
}

View File

@ -1,8 +1,184 @@
import { AssetOrder } from '@app/domain/asset/dto/asset.dto';
import { AssetType } from '@app/infra/entities'; import { AssetType } from '@app/infra/entities';
import { Transform } from 'class-transformer'; import { ApiProperty } from '@nestjs/swagger';
import { IsBoolean, IsEnum, IsNotEmpty, IsString } from 'class-validator'; import { Transform, Type } from 'class-transformer';
import { Optional, toBoolean } from '../../domain.util'; import { IsBoolean, IsEnum, IsInt, IsNotEmpty, IsString, Max, Min } from 'class-validator';
import { Optional, QueryBoolean, QueryDate, ValidateUUID, toBoolean } from '../../domain.util';
class BaseSearchDto {
@ValidateUUID({ optional: true })
libraryId?: string;
@IsString()
@IsNotEmpty()
@Optional()
deviceId?: string;
@IsEnum(AssetType)
@Optional()
@ApiProperty({ enumName: 'AssetTypeEnum', enum: AssetType })
type?: AssetType;
@QueryBoolean({ optional: true })
isArchived?: boolean;
@QueryBoolean({ optional: true })
withArchived?: boolean;
@QueryBoolean({ optional: true })
isEncoded?: boolean;
@QueryBoolean({ optional: true })
isExternal?: boolean;
@QueryBoolean({ optional: true })
isFavorite?: boolean;
@QueryBoolean({ optional: true })
isMotion?: boolean;
@QueryBoolean({ optional: true })
isOffline?: boolean;
@QueryBoolean({ optional: true })
isReadOnly?: boolean;
@QueryBoolean({ optional: true })
isVisible?: boolean;
@QueryBoolean({ optional: true })
withDeleted?: boolean;
@QueryBoolean({ optional: true })
withExif?: boolean;
@QueryDate({ optional: true })
createdBefore?: Date;
@QueryDate({ optional: true })
createdAfter?: Date;
@QueryDate({ optional: true })
updatedBefore?: Date;
@QueryDate({ optional: true })
updatedAfter?: Date;
@QueryDate({ optional: true })
trashedBefore?: Date;
@QueryDate({ optional: true })
trashedAfter?: Date;
@QueryDate({ optional: true })
takenBefore?: Date;
@QueryDate({ optional: true })
takenAfter?: Date;
@IsString()
@IsNotEmpty()
@Optional()
city?: string;
@IsString()
@IsNotEmpty()
@Optional()
state?: string;
@IsString()
@IsNotEmpty()
@Optional()
country?: string;
@IsString()
@IsNotEmpty()
@Optional()
make?: string;
@IsString()
@IsNotEmpty()
@Optional()
model?: string;
@IsString()
@IsNotEmpty()
@Optional()
lensModel?: string;
@IsInt()
@Min(1)
@Type(() => Number)
@Optional()
page?: number;
@IsInt()
@Min(1)
@Max(1000)
@Type(() => Number)
@Optional()
size?: number;
}
export class MetadataSearchDto extends BaseSearchDto {
@ValidateUUID({ optional: true })
id?: string;
@IsString()
@IsNotEmpty()
@Optional()
deviceAssetId?: string;
@IsString()
@IsNotEmpty()
@Optional()
checksum?: string;
@QueryBoolean({ optional: true })
withStacked?: boolean;
@QueryBoolean({ optional: true })
withPeople?: boolean;
@IsString()
@IsNotEmpty()
@Optional()
originalFileName?: string;
@IsString()
@IsNotEmpty()
@Optional()
originalPath?: string;
@IsString()
@IsNotEmpty()
@Optional()
resizePath?: string;
@IsString()
@IsNotEmpty()
@Optional()
webpPath?: string;
@IsString()
@IsNotEmpty()
@Optional()
encodedVideoPath?: string;
@IsEnum(AssetOrder)
@Optional()
@ApiProperty({ enumName: 'AssetOrder', enum: AssetOrder })
order?: AssetOrder;
}
export class SmartSearchDto extends BaseSearchDto {
@IsString()
@IsNotEmpty()
query!: string;
}
// TODO: remove after implementing new search filters
/** @deprecated */
export class SearchDto { export class SearchDto {
@IsString() @IsString()
@IsNotEmpty() @IsNotEmpty()
@ -43,6 +219,19 @@ export class SearchDto {
@Optional() @Optional()
@Transform(toBoolean) @Transform(toBoolean)
withArchived?: boolean; withArchived?: boolean;
@IsInt()
@Min(1)
@Type(() => Number)
@Optional()
page?: number;
@IsInt()
@Min(1)
@Max(1000)
@Type(() => Number)
@Optional()
size?: number;
} }
export class SearchPeopleDto { export class SearchPeopleDto {

View File

@ -29,6 +29,7 @@ class SearchAssetResponseDto {
count!: number; count!: number;
items!: AssetResponseDto[]; items!: AssetResponseDto[];
facets!: SearchFacetResponseDto[]; facets!: SearchFacetResponseDto[];
nextPage!: string | null;
} }
export class SearchResponseDto { export class SearchResponseDto {

View File

@ -6,7 +6,7 @@ import {
newMachineLearningRepositoryMock, newMachineLearningRepositoryMock,
newPartnerRepositoryMock, newPartnerRepositoryMock,
newPersonRepositoryMock, newPersonRepositoryMock,
newSmartInfoRepositoryMock, newSearchRepositoryMock,
newSystemConfigRepositoryMock, newSystemConfigRepositoryMock,
personStub, personStub,
} from '@test'; } from '@test';
@ -16,7 +16,7 @@ import {
IMachineLearningRepository, IMachineLearningRepository,
IPartnerRepository, IPartnerRepository,
IPersonRepository, IPersonRepository,
ISmartInfoRepository, ISearchRepository,
ISystemConfigRepository, ISystemConfigRepository,
} from '../repositories'; } from '../repositories';
import { SearchDto } from './dto'; import { SearchDto } from './dto';
@ -30,7 +30,7 @@ describe(SearchService.name, () => {
let configMock: jest.Mocked<ISystemConfigRepository>; let configMock: jest.Mocked<ISystemConfigRepository>;
let machineMock: jest.Mocked<IMachineLearningRepository>; let machineMock: jest.Mocked<IMachineLearningRepository>;
let personMock: jest.Mocked<IPersonRepository>; let personMock: jest.Mocked<IPersonRepository>;
let smartInfoMock: jest.Mocked<ISmartInfoRepository>; let searchMock: jest.Mocked<ISearchRepository>;
let partnerMock: jest.Mocked<IPartnerRepository>; let partnerMock: jest.Mocked<IPartnerRepository>;
beforeEach(() => { beforeEach(() => {
@ -38,9 +38,9 @@ describe(SearchService.name, () => {
configMock = newSystemConfigRepositoryMock(); configMock = newSystemConfigRepositoryMock();
machineMock = newMachineLearningRepositoryMock(); machineMock = newMachineLearningRepositoryMock();
personMock = newPersonRepositoryMock(); personMock = newPersonRepositoryMock();
smartInfoMock = newSmartInfoRepositoryMock(); searchMock = newSearchRepositoryMock();
partnerMock = newPartnerRepositoryMock(); partnerMock = newPartnerRepositoryMock();
sut = new SearchService(configMock, machineMock, personMock, smartInfoMock, assetMock, partnerMock); sut = new SearchService(configMock, machineMock, personMock, searchMock, assetMock, partnerMock);
}); });
it('should work', () => { it('should work', () => {
@ -104,6 +104,7 @@ describe(SearchService.name, () => {
count: 1, count: 1,
items: [mapAsset(assetStub.image)], items: [mapAsset(assetStub.image)],
facets: [], facets: [],
nextPage: null,
}, },
}; };
@ -111,13 +112,13 @@ describe(SearchService.name, () => {
expect(result).toEqual(expectedResponse); expect(result).toEqual(expectedResponse);
expect(assetMock.searchMetadata).toHaveBeenCalledWith(dto.q, [authStub.user1.user.id], { numResults: 250 }); expect(assetMock.searchMetadata).toHaveBeenCalledWith(dto.q, [authStub.user1.user.id], { numResults: 250 });
expect(smartInfoMock.searchCLIP).not.toHaveBeenCalled(); expect(searchMock.searchSmart).not.toHaveBeenCalled();
}); });
it('should search archived photos if `withArchived` option is true', async () => { it('should search archived photos if `withArchived` option is true', async () => {
const dto: SearchDto = { q: 'test query', clip: true, withArchived: true }; const dto: SearchDto = { q: 'test query', clip: true, withArchived: true };
const embedding = [1, 2, 3]; const embedding = [1, 2, 3];
smartInfoMock.searchCLIP.mockResolvedValueOnce([assetStub.image]); searchMock.searchSmart.mockResolvedValueOnce({ items: [assetStub.image], hasNextPage: false });
machineMock.encodeText.mockResolvedValueOnce(embedding); machineMock.encodeText.mockResolvedValueOnce(embedding);
partnerMock.getAll.mockResolvedValueOnce([]); partnerMock.getAll.mockResolvedValueOnce([]);
const expectedResponse = { const expectedResponse = {
@ -132,25 +133,28 @@ describe(SearchService.name, () => {
count: 1, count: 1,
items: [mapAsset(assetStub.image)], items: [mapAsset(assetStub.image)],
facets: [], facets: [],
nextPage: null,
}, },
}; };
const result = await sut.search(authStub.user1, dto); const result = await sut.search(authStub.user1, dto);
expect(result).toEqual(expectedResponse); expect(result).toEqual(expectedResponse);
expect(smartInfoMock.searchCLIP).toHaveBeenCalledWith({ expect(searchMock.searchSmart).toHaveBeenCalledWith(
userIds: [authStub.user1.user.id], { page: 1, size: 100 },
embedding, {
numResults: 100, userIds: [authStub.user1.user.id],
withArchived: true, embedding,
}); withArchived: true,
},
);
expect(assetMock.searchMetadata).not.toHaveBeenCalled(); expect(assetMock.searchMetadata).not.toHaveBeenCalled();
}); });
it('should search by CLIP if `clip` option is true', async () => { it('should search by CLIP if `clip` option is true', async () => {
const dto: SearchDto = { q: 'test query', clip: true }; const dto: SearchDto = { q: 'test query', clip: true };
const embedding = [1, 2, 3]; const embedding = [1, 2, 3];
smartInfoMock.searchCLIP.mockResolvedValueOnce([assetStub.image]); searchMock.searchSmart.mockResolvedValueOnce({ items: [assetStub.image], hasNextPage: false });
machineMock.encodeText.mockResolvedValueOnce(embedding); machineMock.encodeText.mockResolvedValueOnce(embedding);
partnerMock.getAll.mockResolvedValueOnce([]); partnerMock.getAll.mockResolvedValueOnce([]);
const expectedResponse = { const expectedResponse = {
@ -165,18 +169,21 @@ describe(SearchService.name, () => {
count: 1, count: 1,
items: [mapAsset(assetStub.image)], items: [mapAsset(assetStub.image)],
facets: [], facets: [],
nextPage: null,
}, },
}; };
const result = await sut.search(authStub.user1, dto); const result = await sut.search(authStub.user1, dto);
expect(result).toEqual(expectedResponse); expect(result).toEqual(expectedResponse);
expect(smartInfoMock.searchCLIP).toHaveBeenCalledWith({ expect(searchMock.searchSmart).toHaveBeenCalledWith(
userIds: [authStub.user1.user.id], { page: 1, size: 100 },
embedding, {
numResults: 100, userIds: [authStub.user1.user.id],
withArchived: false, embedding,
}); withArchived: false,
},
);
expect(assetMock.searchMetadata).not.toHaveBeenCalled(); expect(assetMock.searchMetadata).not.toHaveBeenCalled();
}); });

View File

@ -1,7 +1,7 @@
import { AssetEntity } from '@app/infra/entities'; import { AssetEntity } from '@app/infra/entities';
import { ImmichLogger } from '@app/infra/logger'; import { ImmichLogger } from '@app/infra/logger';
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { AssetResponseDto, mapAsset } from '../asset'; import { AssetOrder, AssetResponseDto, mapAsset } from '../asset';
import { AuthDto } from '../auth'; import { AuthDto } from '../auth';
import { PersonResponseDto } from '../person'; import { PersonResponseDto } from '../person';
import { import {
@ -9,13 +9,13 @@ import {
IMachineLearningRepository, IMachineLearningRepository,
IPartnerRepository, IPartnerRepository,
IPersonRepository, IPersonRepository,
ISmartInfoRepository, ISearchRepository,
ISystemConfigRepository, ISystemConfigRepository,
SearchExploreItem, SearchExploreItem,
SearchStrategy, SearchStrategy,
} from '../repositories'; } from '../repositories';
import { FeatureFlag, SystemConfigCore } from '../system-config'; import { FeatureFlag, SystemConfigCore } from '../system-config';
import { SearchDto, SearchPeopleDto } from './dto'; import { MetadataSearchDto, SearchDto, SearchPeopleDto, SmartSearchDto } from './dto';
import { SearchResponseDto } from './response-dto'; import { SearchResponseDto } from './response-dto';
@Injectable() @Injectable()
@ -27,7 +27,7 @@ export class SearchService {
@Inject(ISystemConfigRepository) configRepository: ISystemConfigRepository, @Inject(ISystemConfigRepository) configRepository: ISystemConfigRepository,
@Inject(IMachineLearningRepository) private machineLearning: IMachineLearningRepository, @Inject(IMachineLearningRepository) private machineLearning: IMachineLearningRepository,
@Inject(IPersonRepository) private personRepository: IPersonRepository, @Inject(IPersonRepository) private personRepository: IPersonRepository,
@Inject(ISmartInfoRepository) private smartInfoRepository: ISmartInfoRepository, @Inject(ISearchRepository) private searchRepository: ISearchRepository,
@Inject(IAssetRepository) private assetRepository: IAssetRepository, @Inject(IAssetRepository) private assetRepository: IAssetRepository,
@Inject(IPartnerRepository) private partnerRepository: IPartnerRepository, @Inject(IPartnerRepository) private partnerRepository: IPartnerRepository,
) { ) {
@ -55,6 +55,53 @@ export class SearchService {
})); }));
} }
async searchMetadata(auth: AuthDto, dto: MetadataSearchDto): Promise<SearchResponseDto> {
let checksum: Buffer | undefined;
if (dto.checksum) {
const encoding = dto.checksum.length === 28 ? 'base64' : 'hex';
checksum = Buffer.from(dto.checksum, encoding);
}
const page = dto.page ?? 1;
const size = dto.size || 250;
const enumToOrder = { [AssetOrder.ASC]: 'ASC', [AssetOrder.DESC]: 'DESC' } as const;
const { hasNextPage, items } = await this.searchRepository.searchMetadata(
{ page, size },
{
...dto,
checksum,
ownerId: auth.user.id,
orderDirection: dto.order ? enumToOrder[dto.order] : 'DESC',
},
);
return this.mapResponse(items, hasNextPage ? (page + 1).toString() : null);
}
async searchSmart(auth: AuthDto, dto: SmartSearchDto): Promise<SearchResponseDto> {
await this.configCore.requireFeature(FeatureFlag.SMART_SEARCH);
const { machineLearning } = await this.configCore.getConfig();
const userIds = await this.getUserIdsToSearch(auth);
const embedding = await this.machineLearning.encodeText(
machineLearning.url,
{ text: dto.query },
machineLearning.clip,
);
const page = dto.page ?? 1;
const size = dto.size || 100;
const { hasNextPage, items } = await this.searchRepository.searchSmart(
{ page, size },
{ ...dto, userIds, embedding },
);
return this.mapResponse(items, hasNextPage ? (page + 1).toString() : null);
}
// TODO: remove after implementing new search filters
/** @deprecated */
async search(auth: AuthDto, dto: SearchDto): Promise<SearchResponseDto> { async search(auth: AuthDto, dto: SearchDto): Promise<SearchResponseDto> {
await this.configCore.requireFeature(FeatureFlag.SEARCH); await this.configCore.requireFeature(FeatureFlag.SEARCH);
const { machineLearning } = await this.configCore.getConfig(); const { machineLearning } = await this.configCore.getConfig();
@ -70,10 +117,10 @@ export class SearchService {
} }
const userIds = await this.getUserIdsToSearch(auth); const userIds = await this.getUserIdsToSearch(auth);
const withArchived = dto.withArchived || false; const page = dto.page ?? 1;
let nextPage: string | null = null;
let assets: AssetEntity[] = []; let assets: AssetEntity[] = [];
switch (strategy) { switch (strategy) {
case SearchStrategy.SMART: { case SearchStrategy.SMART: {
const embedding = await this.machineLearning.encodeText( const embedding = await this.machineLearning.encodeText(
@ -81,36 +128,30 @@ export class SearchService {
{ text: query }, { text: query },
machineLearning.clip, machineLearning.clip,
); );
assets = await this.smartInfoRepository.searchCLIP({
userIds: userIds, const { hasNextPage, items } = await this.searchRepository.searchSmart(
embedding, { page, size: dto.size || 100 },
numResults: 100, {
withArchived, userIds,
}); embedding,
withArchived: !!dto.withArchived,
},
);
if (hasNextPage) {
nextPage = (page + 1).toString();
}
assets = items;
break; break;
} }
case SearchStrategy.TEXT: { case SearchStrategy.TEXT: {
assets = await this.assetRepository.searchMetadata(query, userIds, { numResults: 250 }); assets = await this.assetRepository.searchMetadata(query, userIds, { numResults: dto.size || 250 });
} }
default: { default: {
break; break;
} }
} }
return { return this.mapResponse(assets, nextPage);
albums: {
total: 0,
count: 0,
items: [],
facets: [],
},
assets: {
total: assets.length,
count: assets.length,
items: assets.map((asset) => mapAsset(asset)),
facets: [],
},
};
} }
private async getUserIdsToSearch(auth: AuthDto): Promise<string[]> { private async getUserIdsToSearch(auth: AuthDto): Promise<string[]> {
@ -122,4 +163,17 @@ export class SearchService {
userIds.push(...partnersIds); userIds.push(...partnersIds);
return userIds; return userIds;
} }
private async mapResponse(assets: AssetEntity[], nextPage: string | null): Promise<SearchResponseDto> {
return {
albums: { total: 0, count: 0, items: [], facets: [] },
assets: {
total: assets.length,
count: assets.length,
items: assets.map((asset) => mapAsset(asset)),
facets: [],
nextPage,
},
};
}
} }

View File

@ -5,7 +5,7 @@ import {
newDatabaseRepositoryMock, newDatabaseRepositoryMock,
newJobRepositoryMock, newJobRepositoryMock,
newMachineLearningRepositoryMock, newMachineLearningRepositoryMock,
newSmartInfoRepositoryMock, newSearchRepositoryMock,
newSystemConfigRepositoryMock, newSystemConfigRepositoryMock,
} from '@test'; } from '@test';
import { JobName } from '../job'; import { JobName } from '../job';
@ -14,7 +14,7 @@ import {
IDatabaseRepository, IDatabaseRepository,
IJobRepository, IJobRepository,
IMachineLearningRepository, IMachineLearningRepository,
ISmartInfoRepository, ISearchRepository,
ISystemConfigRepository, ISystemConfigRepository,
WithoutProperty, WithoutProperty,
} from '../repositories'; } from '../repositories';
@ -31,18 +31,18 @@ describe(SmartInfoService.name, () => {
let assetMock: jest.Mocked<IAssetRepository>; let assetMock: jest.Mocked<IAssetRepository>;
let configMock: jest.Mocked<ISystemConfigRepository>; let configMock: jest.Mocked<ISystemConfigRepository>;
let jobMock: jest.Mocked<IJobRepository>; let jobMock: jest.Mocked<IJobRepository>;
let smartMock: jest.Mocked<ISmartInfoRepository>; let searchMock: jest.Mocked<ISearchRepository>;
let machineMock: jest.Mocked<IMachineLearningRepository>; let machineMock: jest.Mocked<IMachineLearningRepository>;
let databaseMock: jest.Mocked<IDatabaseRepository>; let databaseMock: jest.Mocked<IDatabaseRepository>;
beforeEach(async () => { beforeEach(async () => {
assetMock = newAssetRepositoryMock(); assetMock = newAssetRepositoryMock();
configMock = newSystemConfigRepositoryMock(); configMock = newSystemConfigRepositoryMock();
smartMock = newSmartInfoRepositoryMock(); searchMock = newSearchRepositoryMock();
jobMock = newJobRepositoryMock(); jobMock = newJobRepositoryMock();
machineMock = newMachineLearningRepositoryMock(); machineMock = newMachineLearningRepositoryMock();
databaseMock = newDatabaseRepositoryMock(); databaseMock = newDatabaseRepositoryMock();
sut = new SmartInfoService(assetMock, databaseMock, jobMock, machineMock, smartMock, configMock); sut = new SmartInfoService(assetMock, databaseMock, jobMock, machineMock, searchMock, configMock);
assetMock.getByIds.mockResolvedValue([asset]); assetMock.getByIds.mockResolvedValue([asset]);
}); });
@ -102,12 +102,12 @@ describe(SmartInfoService.name, () => {
await sut.handleEncodeClip({ id: asset.id }); await sut.handleEncodeClip({ id: asset.id });
expect(smartMock.upsert).not.toHaveBeenCalled(); expect(searchMock.upsert).not.toHaveBeenCalled();
expect(machineMock.encodeImage).not.toHaveBeenCalled(); expect(machineMock.encodeImage).not.toHaveBeenCalled();
}); });
it('should save the returned objects', async () => { it('should save the returned objects', async () => {
smartMock.upsert.mockResolvedValue(); searchMock.upsert.mockResolvedValue();
machineMock.encodeImage.mockResolvedValue([0.01, 0.02, 0.03]); machineMock.encodeImage.mockResolvedValue([0.01, 0.02, 0.03]);
await sut.handleEncodeClip({ id: asset.id }); await sut.handleEncodeClip({ id: asset.id });
@ -117,7 +117,7 @@ describe(SmartInfoService.name, () => {
{ imagePath: 'path/to/resize.ext' }, { imagePath: 'path/to/resize.ext' },
{ enabled: true, modelName: 'ViT-B-32__openai' }, { enabled: true, modelName: 'ViT-B-32__openai' },
); );
expect(smartMock.upsert).toHaveBeenCalledWith( expect(searchMock.upsert).toHaveBeenCalledWith(
{ {
assetId: 'asset-1', assetId: 'asset-1',
}, },

View File

@ -8,7 +8,7 @@ import {
IDatabaseRepository, IDatabaseRepository,
IJobRepository, IJobRepository,
IMachineLearningRepository, IMachineLearningRepository,
ISmartInfoRepository, ISearchRepository,
ISystemConfigRepository, ISystemConfigRepository,
WithoutProperty, WithoutProperty,
} from '../repositories'; } from '../repositories';
@ -24,7 +24,7 @@ export class SmartInfoService {
@Inject(IDatabaseRepository) private databaseRepository: IDatabaseRepository, @Inject(IDatabaseRepository) private databaseRepository: IDatabaseRepository,
@Inject(IJobRepository) private jobRepository: IJobRepository, @Inject(IJobRepository) private jobRepository: IJobRepository,
@Inject(IMachineLearningRepository) private machineLearning: IMachineLearningRepository, @Inject(IMachineLearningRepository) private machineLearning: IMachineLearningRepository,
@Inject(ISmartInfoRepository) private repository: ISmartInfoRepository, @Inject(ISearchRepository) private repository: ISearchRepository,
@Inject(ISystemConfigRepository) configRepository: ISystemConfigRepository, @Inject(ISystemConfigRepository) configRepository: ISystemConfigRepository,
) { ) {
this.configCore = SystemConfigCore.create(configRepository); this.configCore = SystemConfigCore.create(configRepository);

View File

@ -15,7 +15,7 @@ import { ImmichLogger } from '@app/infra/logger';
import { BadRequestException } from '@nestjs/common'; import { BadRequestException } from '@nestjs/common';
import { newCommunicationRepositoryMock, newSystemConfigRepositoryMock } from '@test'; import { newCommunicationRepositoryMock, newSystemConfigRepositoryMock } from '@test';
import { QueueName } from '../job'; import { QueueName } from '../job';
import { ICommunicationRepository, ISmartInfoRepository, ISystemConfigRepository, ServerEvent } from '../repositories'; import { ICommunicationRepository, ISearchRepository, ISystemConfigRepository, ServerEvent } from '../repositories';
import { defaults, SystemConfigValidator } from './system-config.core'; import { defaults, SystemConfigValidator } from './system-config.core';
import { SystemConfigService } from './system-config.service'; import { SystemConfigService } from './system-config.service';
@ -146,7 +146,7 @@ describe(SystemConfigService.name, () => {
let sut: SystemConfigService; let sut: SystemConfigService;
let configMock: jest.Mocked<ISystemConfigRepository>; let configMock: jest.Mocked<ISystemConfigRepository>;
let communicationMock: jest.Mocked<ICommunicationRepository>; let communicationMock: jest.Mocked<ICommunicationRepository>;
let smartInfoMock: jest.Mocked<ISmartInfoRepository>; let smartInfoMock: jest.Mocked<ISearchRepository>;
beforeEach(async () => { beforeEach(async () => {
delete process.env.IMMICH_CONFIG_FILE; delete process.env.IMMICH_CONFIG_FILE;

View File

@ -6,7 +6,7 @@ import _ from 'lodash';
import { import {
ClientEvent, ClientEvent,
ICommunicationRepository, ICommunicationRepository,
ISmartInfoRepository, ISearchRepository,
ISystemConfigRepository, ISystemConfigRepository,
ServerEvent, ServerEvent,
} from '../repositories'; } from '../repositories';
@ -32,7 +32,7 @@ export class SystemConfigService {
constructor( constructor(
@Inject(ISystemConfigRepository) private repository: ISystemConfigRepository, @Inject(ISystemConfigRepository) private repository: ISystemConfigRepository,
@Inject(ICommunicationRepository) private communicationRepository: ICommunicationRepository, @Inject(ICommunicationRepository) private communicationRepository: ICommunicationRepository,
@Inject(ISmartInfoRepository) private smartInfoRepository: ISmartInfoRepository, @Inject(ISearchRepository) private smartInfoRepository: ISearchRepository,
) { ) {
this.core = SystemConfigCore.create(repository); this.core = SystemConfigCore.create(repository);
this.communicationRepository.on(ServerEvent.CONFIG_UPDATE, () => this.handleConfigUpdate()); this.communicationRepository.on(ServerEvent.CONFIG_UPDATE, () => this.handleConfigUpdate());

View File

@ -33,7 +33,9 @@ export class TrashService {
async restore(auth: AuthDto): Promise<void> { async restore(auth: AuthDto): Promise<void> {
const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) => const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) =>
this.assetRepository.getByUserId(pagination, auth.user.id, { trashedBefore: DateTime.now().toJSDate() }), this.assetRepository.getByUserId(pagination, auth.user.id, {
trashedBefore: DateTime.now().toJSDate(),
}),
); );
for await (const assets of assetPagination) { for await (const assets of assetPagination) {
@ -44,7 +46,9 @@ export class TrashService {
async empty(auth: AuthDto): Promise<void> { async empty(auth: AuthDto): Promise<void> {
const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) => const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) =>
this.assetRepository.getByUserId(pagination, auth.user.id, { trashedBefore: DateTime.now().toJSDate() }), this.assetRepository.getByUserId(pagination, auth.user.id, {
trashedBefore: DateTime.now().toJSDate(),
}),
); );
for await (const assets of assetPagination) { for await (const assets of assetPagination) {

View File

@ -3,7 +3,6 @@ import {
AssetBulkUpdateDto, AssetBulkUpdateDto,
AssetJobsDto, AssetJobsDto,
AssetResponseDto, AssetResponseDto,
AssetSearchDto,
AssetService, AssetService,
AssetStatsDto, AssetStatsDto,
AssetStatsResponseDto, AssetStatsResponseDto,
@ -14,7 +13,9 @@ import {
MapMarkerResponseDto, MapMarkerResponseDto,
MemoryLaneDto, MemoryLaneDto,
MemoryLaneResponseDto, MemoryLaneResponseDto,
MetadataSearchDto,
RandomAssetsDto, RandomAssetsDto,
SearchService,
TimeBucketAssetDto, TimeBucketAssetDto,
TimeBucketDto, TimeBucketDto,
TimeBucketResponseDto, TimeBucketResponseDto,
@ -23,7 +24,7 @@ import {
UpdateStackParentDto, UpdateStackParentDto,
} from '@app/domain'; } from '@app/domain';
import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Put, Query } from '@nestjs/common'; import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Put, Query } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger'; import { ApiOperation, ApiTags } from '@nestjs/swagger';
import { Auth, Authenticated, SharedLinkRoute } from '../app.guard'; import { Auth, Authenticated, SharedLinkRoute } from '../app.guard';
import { UseValidation } from '../app.utils'; import { UseValidation } from '../app.utils';
import { Route } from '../interceptors'; import { Route } from '../interceptors';
@ -34,11 +35,15 @@ import { UUIDParamDto } from './dto/uuid-param.dto';
@Authenticated() @Authenticated()
@UseValidation() @UseValidation()
export class AssetsController { export class AssetsController {
constructor(private service: AssetService) {} constructor(private searchService: SearchService) {}
@Get() @Get()
searchAssets(@Auth() auth: AuthDto, @Query() dto: AssetSearchDto): Promise<AssetResponseDto[]> { @ApiOperation({ deprecated: true })
return this.service.search(auth, dto); async searchAssets(@Auth() auth: AuthDto, @Query() dto: MetadataSearchDto): Promise<AssetResponseDto[]> {
const {
assets: { items },
} = await this.searchService.searchMetadata(auth, dto);
return items;
} }
} }

View File

@ -1,14 +1,16 @@
import { import {
AuthDto, AuthDto,
MetadataSearchDto,
PersonResponseDto, PersonResponseDto,
SearchDto, SearchDto,
SearchExploreResponseDto, SearchExploreResponseDto,
SearchPeopleDto, SearchPeopleDto,
SearchResponseDto, SearchResponseDto,
SearchService, SearchService,
SmartSearchDto,
} from '@app/domain'; } from '@app/domain';
import { Controller, Get, Query } from '@nestjs/common'; import { Controller, Get, Query } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger'; import { ApiOperation, ApiTags } from '@nestjs/swagger';
import { Auth, Authenticated } from '../app.guard'; import { Auth, Authenticated } from '../app.guard';
import { UseValidation } from '../app.utils'; import { UseValidation } from '../app.utils';
@ -19,7 +21,18 @@ import { UseValidation } from '../app.utils';
export class SearchController { export class SearchController {
constructor(private service: SearchService) {} constructor(private service: SearchService) {}
@Get('metadata')
searchMetadata(@Auth() auth: AuthDto, @Query() dto: MetadataSearchDto): Promise<SearchResponseDto> {
return this.service.searchMetadata(auth, dto);
}
@Get('smart')
searchSmart(@Auth() auth: AuthDto, @Query() dto: SmartSearchDto): Promise<SearchResponseDto> {
return this.service.searchSmart(auth, dto);
}
@Get() @Get()
@ApiOperation({ deprecated: true })
search(@Auth() auth: AuthDto, @Query() dto: SearchDto): Promise<SearchResponseDto> { search(@Auth() auth: AuthDto, @Query() dto: SearchDto): Promise<SearchResponseDto> {
return this.service.search(auth, dto); return this.service.search(auth, dto);
} }

View File

@ -17,9 +17,9 @@ import {
IMoveRepository, IMoveRepository,
IPartnerRepository, IPartnerRepository,
IPersonRepository, IPersonRepository,
ISearchRepository,
IServerInfoRepository, IServerInfoRepository,
ISharedLinkRepository, ISharedLinkRepository,
ISmartInfoRepository,
IStorageRepository, IStorageRepository,
ISystemConfigRepository, ISystemConfigRepository,
ISystemMetadataRepository, ISystemMetadataRepository,
@ -56,9 +56,9 @@ import {
MoveRepository, MoveRepository,
PartnerRepository, PartnerRepository,
PersonRepository, PersonRepository,
SearchRepository,
ServerInfoRepository, ServerInfoRepository,
SharedLinkRepository, SharedLinkRepository,
SmartInfoRepository,
SystemConfigRepository, SystemConfigRepository,
SystemMetadataRepository, SystemMetadataRepository,
TagRepository, TagRepository,
@ -86,7 +86,7 @@ const providers: Provider[] = [
{ provide: IPersonRepository, useClass: PersonRepository }, { provide: IPersonRepository, useClass: PersonRepository },
{ provide: IServerInfoRepository, useClass: ServerInfoRepository }, { provide: IServerInfoRepository, useClass: ServerInfoRepository },
{ provide: ISharedLinkRepository, useClass: SharedLinkRepository }, { provide: ISharedLinkRepository, useClass: SharedLinkRepository },
{ provide: ISmartInfoRepository, useClass: SmartInfoRepository }, { provide: ISearchRepository, useClass: SearchRepository },
{ provide: IStorageRepository, useClass: FilesystemProvider }, { provide: IStorageRepository, useClass: FilesystemProvider },
{ provide: ISystemConfigRepository, useClass: SystemConfigRepository }, { provide: ISystemConfigRepository, useClass: SystemConfigRepository },
{ provide: ISystemMetadataRepository, useClass: SystemMetadataRepository }, { provide: ISystemMetadataRepository, useClass: SystemMetadataRepository },

View File

@ -1,7 +1,19 @@
import { Paginated, PaginationOptions } from '@app/domain'; import { AssetSearchBuilderOptions, Paginated, PaginationOptions } from '@app/domain';
import _ from 'lodash'; import _ from 'lodash';
import { Between, FindManyOptions, LessThanOrEqual, MoreThanOrEqual, ObjectLiteral, Repository } from 'typeorm'; import {
import { chunks, setUnion } from '../domain/domain.util'; Between,
Brackets,
FindManyOptions,
IsNull,
LessThanOrEqual,
MoreThanOrEqual,
Not,
ObjectLiteral,
Repository,
SelectQueryBuilder,
} from 'typeorm';
import { PaginatedBuilderOptions, PaginationMode, PaginationResult, chunks, setUnion } from '../domain/domain.util';
import { AssetEntity } from './entities';
import { DATABASE_PARAMETER_CHUNK_SIZE } from './infra.util'; import { DATABASE_PARAMETER_CHUNK_SIZE } from './infra.util';
/** /**
@ -18,9 +30,21 @@ export function OptionalBetween<T>(from?: T, to?: T) {
} }
} }
export const isValidInteger = (value: number, options: { min?: number; max?: number }): value is number => {
const { min = Number.MIN_SAFE_INTEGER, max = Number.MAX_SAFE_INTEGER } = options;
return Number.isInteger(value) && value >= min && value <= max;
};
function paginationHelper<Entity extends ObjectLiteral>(items: Entity[], take: number): PaginationResult<Entity> {
const hasNextPage = items.length > take;
items.splice(take);
return { items, hasNextPage };
}
export async function paginate<Entity extends ObjectLiteral>( export async function paginate<Entity extends ObjectLiteral>(
repository: Repository<Entity>, repository: Repository<Entity>,
paginationOptions: PaginationOptions, { take, skip }: PaginationOptions,
searchOptions?: FindManyOptions<Entity>, searchOptions?: FindManyOptions<Entity>,
): Paginated<Entity> { ): Paginated<Entity> {
const items = await repository.find( const items = await repository.find(
@ -28,27 +52,33 @@ export async function paginate<Entity extends ObjectLiteral>(
{ {
...searchOptions, ...searchOptions,
// Take one more item to check if there's a next page // Take one more item to check if there's a next page
take: paginationOptions.take + 1, take: take + 1,
skip: paginationOptions.skip, skip,
}, },
_.isUndefined, _.isUndefined,
), ),
); );
const hasNextPage = items.length > paginationOptions.take; return paginationHelper(items, take);
items.splice(paginationOptions.take); }
return { items, hasNextPage }; export async function paginatedBuilder<Entity extends ObjectLiteral>(
qb: SelectQueryBuilder<Entity>,
{ take, skip, mode }: PaginatedBuilderOptions,
): Paginated<Entity> {
if (mode === PaginationMode.LIMIT_OFFSET) {
qb.limit(take + 1).offset(skip);
} else {
qb.take(take + 1).skip(skip);
}
const items = await qb.getMany();
return paginationHelper(items, take);
} }
export const asVector = (embedding: number[], quote = false) => export const asVector = (embedding: number[], quote = false) =>
quote ? `'[${embedding.join(',')}]'` : `[${embedding.join(',')}]`; quote ? `'[${embedding.join(',')}]'` : `[${embedding.join(',')}]`;
export const isValidInteger = (value: number, options: { min?: number; max?: number }): value is number => {
const { min = Number.MIN_SAFE_INTEGER, max = Number.MAX_SAFE_INTEGER } = options;
return Number.isInteger(value) && value >= min && value <= max;
};
/** /**
* Wraps a method that takes a collection of parameters and sequentially calls it with chunks of the collection, * Wraps a method that takes a collection of parameters and sequentially calls it with chunks of the collection,
* to overcome the maximum number of parameters allowed by the database driver. * to overcome the maximum number of parameters allowed by the database driver.
@ -91,3 +121,79 @@ export function ChunkedArray(options?: { paramIndex?: number }): MethodDecorator
export function ChunkedSet(options?: { paramIndex?: number }): MethodDecorator { export function ChunkedSet(options?: { paramIndex?: number }): MethodDecorator {
return Chunked({ ...options, mergeFn: setUnion }); return Chunked({ ...options, mergeFn: setUnion });
} }
export function searchAssetBuilder(
builder: SelectQueryBuilder<AssetEntity>,
options: AssetSearchBuilderOptions,
): SelectQueryBuilder<AssetEntity> {
builder.andWhere(
_.omitBy(
{
createdAt: OptionalBetween(options.createdAfter, options.createdBefore),
updatedAt: OptionalBetween(options.updatedAfter, options.updatedBefore),
deletedAt: OptionalBetween(options.trashedAfter, options.trashedBefore),
fileCreatedAt: OptionalBetween(options.takenAfter, options.takenBefore),
},
_.isUndefined,
),
);
const exifInfo = _.omitBy(_.pick(options, ['city', 'country', 'lensModel', 'make', 'model', 'state']), _.isUndefined);
if (Object.keys(exifInfo).length > 0) {
builder.leftJoin(`${builder.alias}.exifInfo`, 'exifInfo');
builder.andWhere({ exifInfo });
}
const id = _.pick(options, ['checksum', 'deviceAssetId', 'deviceId', 'id', 'libraryId', 'ownerId']);
builder.andWhere(_.omitBy(id, _.isUndefined));
const path = _.pick(options, ['encodedVideoPath', 'originalFileName', 'originalPath', 'resizePath', 'webpPath']);
builder.andWhere(_.omitBy(path, _.isUndefined));
const status = _.pick(options, ['isExternal', 'isFavorite', 'isOffline', 'isReadOnly', 'isVisible', 'type']);
const { isArchived, isEncoded, isMotion, withArchived } = options;
builder.andWhere(
_.omitBy(
{
...status,
isArchived: isArchived ?? withArchived,
encodedVideoPath: isEncoded ? Not(IsNull()) : undefined,
livePhotoVideoId: isMotion ? Not(IsNull()) : undefined,
},
_.isUndefined,
),
);
if (options.withExif) {
builder.leftJoinAndSelect(`${builder.alias}.exifInfo`, 'exifInfo');
}
if (options.withFaces || options.withPeople) {
builder.leftJoinAndSelect(`${builder.alias}.faces`, 'faces');
}
if (options.withPeople) {
builder.leftJoinAndSelect(`${builder.alias}.person`, 'person');
}
if (options.withSmartInfo) {
builder.leftJoinAndSelect(`${builder.alias}.smartInfo`, 'smartInfo');
}
if (options.withStacked) {
builder
.leftJoinAndSelect(`${builder.alias}.stack`, 'stack')
.leftJoinAndSelect('stack.assets', 'stackedAssets')
.andWhere(
new Brackets((qb) => qb.where(`stack.primaryAssetId = ${builder.alias}.id`).orWhere('asset.stackId IS NULL')),
);
}
const withDeleted =
options.withDeleted ?? (options.trashedAfter !== undefined || options.trashedBefore !== undefined);
if (withDeleted) {
builder.withDeleted();
}
return builder;
}

View File

@ -42,7 +42,7 @@ export class ApiKeyRepository implements IKeyRepository {
return this.repository.findOne({ where: { userId, id } }); return this.repository.findOne({ where: { userId, id } });
} }
@GenerateSql({ params: [DummyValue.STRING] }) @GenerateSql({ params: [DummyValue.UUID] })
getByUserId(userId: string): Promise<APIKeyEntity[]> { getByUserId(userId: string): Promise<APIKeyEntity[]> {
return this.repository.find({ where: { userId }, order: { createdAt: 'DESC' } }); return this.repository.find({ where: { userId }, order: { createdAt: 'DESC' } });
} }

View File

@ -12,6 +12,7 @@ import {
MetadataSearchOptions, MetadataSearchOptions,
MonthDay, MonthDay,
Paginated, Paginated,
PaginationMode,
PaginationOptions, PaginationOptions,
SearchExploreItem, SearchExploreItem,
TimeBucketItem, TimeBucketItem,
@ -22,26 +23,21 @@ import {
} from '@app/domain'; } from '@app/domain';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm'; import { InjectRepository } from '@nestjs/typeorm';
import _ from 'lodash';
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
import path from 'node:path'; import path from 'node:path';
import { import {
And,
Brackets, Brackets,
FindOptionsRelations, FindOptionsRelations,
FindOptionsSelect, FindOptionsSelect,
FindOptionsWhere, FindOptionsWhere,
In, In,
IsNull, IsNull,
LessThan,
Not, Not,
Repository, Repository,
} from 'typeorm'; } from 'typeorm';
import { AssetEntity, AssetJobStatusEntity, AssetType, ExifEntity, SmartInfoEntity } from '../entities'; import { AssetEntity, AssetJobStatusEntity, AssetType, ExifEntity, SmartInfoEntity } from '../entities';
import { DummyValue, GenerateSql } from '../infra.util'; import { DummyValue, GenerateSql } from '../infra.util';
import { Chunked, ChunkedArray, OptionalBetween, paginate } from '../infra.utils'; import { Chunked, ChunkedArray, OptionalBetween, paginate, paginatedBuilder, searchAssetBuilder } from '../infra.utils';
const DEFAULT_SEARCH_SIZE = 250;
const truncateMap: Record<TimeBucketSize, string> = { const truncateMap: Record<TimeBucketSize, string> = {
[TimeBucketSize.DAY]: 'day', [TimeBucketSize.DAY]: 'day',
@ -70,142 +66,6 @@ export class AssetRepository implements IAssetRepository {
await this.jobStatusRepository.upsert(jobStatus, { conflictPaths: ['assetId'] }); await this.jobStatusRepository.upsert(jobStatus, { conflictPaths: ['assetId'] });
} }
search(options: AssetSearchOptions): Promise<AssetEntity[]> {
const {
id,
libraryId,
deviceAssetId,
type,
checksum,
ownerId,
isVisible,
isFavorite,
isExternal,
isReadOnly,
isOffline,
isArchived,
isMotion,
isEncoded,
createdBefore,
createdAfter,
updatedBefore,
updatedAfter,
trashedBefore,
trashedAfter,
takenBefore,
takenAfter,
originalFileName,
originalPath,
resizePath,
webpPath,
encodedVideoPath,
city,
state,
country,
make,
model,
lensModel,
withDeleted: _withDeleted,
withExif: _withExif,
withStacked,
withPeople,
withSmartInfo,
order,
} = options;
const withDeleted = _withDeleted ?? (trashedAfter !== undefined || trashedBefore !== undefined);
const page = Math.max(options.page || 1, 1);
const size = Math.min(options.size || DEFAULT_SEARCH_SIZE, DEFAULT_SEARCH_SIZE);
const exifWhere = _.omitBy(
{
city,
state,
country,
make,
model,
lensModel,
},
_.isUndefined,
);
const withExif = Object.keys(exifWhere).length > 0 || _withExif;
const where: FindOptionsWhere<AssetEntity> = _.omitBy(
{
ownerId,
id,
libraryId,
deviceAssetId,
type,
checksum,
isVisible,
isFavorite,
isExternal,
isReadOnly,
isOffline,
isArchived,
livePhotoVideoId: isMotion && Not(IsNull()),
originalFileName,
originalPath,
resizePath,
webpPath,
encodedVideoPath: encodedVideoPath ?? (isEncoded && Not(IsNull())),
createdAt: OptionalBetween(createdAfter, createdBefore),
updatedAt: OptionalBetween(updatedAfter, updatedBefore),
deletedAt: OptionalBetween(trashedAfter, trashedBefore),
fileCreatedAt: OptionalBetween(takenAfter, takenBefore),
exifInfo: Object.keys(exifWhere).length > 0 ? exifWhere : undefined,
},
_.isUndefined,
);
const builder = this.repository.createQueryBuilder('asset');
if (withExif) {
if (_withExif) {
builder.leftJoinAndSelect('asset.exifInfo', 'exifInfo');
} else {
builder.leftJoin('asset.exifInfo', 'exifInfo');
}
}
if (withPeople) {
builder.leftJoinAndSelect('asset.faces', 'faces');
builder.leftJoinAndSelect('faces.person', 'person');
}
if (withSmartInfo) {
builder.leftJoinAndSelect('asset.smartInfo', 'smartInfo');
}
if (withDeleted) {
builder.withDeleted();
}
builder.where(where);
if (withStacked) {
builder
.leftJoinAndSelect('asset.stack', 'stack')
.leftJoinAndSelect('stack.assets', 'stackedAssets')
.andWhere(new Brackets((qb) => qb.where('stack.primaryAssetId = asset.id').orWhere('asset.stackId IS NULL')));
}
return builder
.skip(size * (page - 1))
.take(size)
.orderBy('asset.fileCreatedAt', order ?? 'DESC')
.getMany();
}
create(asset: AssetCreate): Promise<AssetEntity> { create(asset: AssetCreate): Promise<AssetEntity> {
return this.repository.save(asset); return this.repository.save(asset);
} }
@ -316,17 +176,7 @@ export class AssetRepository implements IAssetRepository {
} }
getByUserId(pagination: PaginationOptions, userId: string, options: AssetSearchOptions = {}): Paginated<AssetEntity> { getByUserId(pagination: PaginationOptions, userId: string, options: AssetSearchOptions = {}): Paginated<AssetEntity> {
return paginate(this.repository, pagination, { return this.getAll(pagination, { ...options, id: userId });
where: {
ownerId: userId,
isVisible: options.isVisible,
deletedAt: options.trashedBefore ? And(Not(IsNull()), LessThan(options.trashedBefore)) : undefined,
},
relations: {
exifInfo: true,
},
withDeleted: !!options.trashedBefore,
});
} }
@GenerateSql({ params: [[DummyValue.UUID]] }) @GenerateSql({ params: [[DummyValue.UUID]] })
@ -345,24 +195,13 @@ export class AssetRepository implements IAssetRepository {
} }
getAll(pagination: PaginationOptions, options: AssetSearchOptions = {}): Paginated<AssetEntity> { getAll(pagination: PaginationOptions, options: AssetSearchOptions = {}): Paginated<AssetEntity> {
return paginate(this.repository, pagination, { let builder = this.repository.createQueryBuilder('asset');
where: { builder = searchAssetBuilder(builder, options);
isVisible: options.isVisible, builder.orderBy('asset.createdAt', options.orderDirection ?? 'ASC');
type: options.type, return paginatedBuilder<AssetEntity>(builder, {
deletedAt: options.trashedBefore ? And(Not(IsNull()), LessThan(options.trashedBefore)) : undefined, mode: PaginationMode.SKIP_TAKE,
}, skip: pagination.skip,
relations: { take: pagination.take,
exifInfo: options.withExif !== false,
smartInfo: options.withSmartInfo !== false,
tags: options.withSmartInfo !== false,
faces: options.withFaces !== false,
smartSearch: options.withSmartInfo === true,
},
withDeleted: options.withDeleted ?? !!options.trashedBefore,
order: {
// Ensures correct order when paginating
createdAt: options.order ?? 'ASC',
},
}); });
} }
@ -435,7 +274,7 @@ export class AssetRepository implements IAssetRepository {
await this.repository.remove(asset); await this.repository.remove(asset);
} }
@GenerateSql({ params: [[DummyValue.UUID], DummyValue.BUFFER] }) @GenerateSql({ params: [DummyValue.UUID, DummyValue.BUFFER] })
getByChecksum(userId: string, checksum: Buffer): Promise<AssetEntity | null> { getByChecksum(userId: string, checksum: Buffer): Promise<AssetEntity | null> {
return this.repository.findOne({ where: { ownerId: userId, checksum } }); return this.repository.findOne({ where: { ownerId: userId, checksum } });
} }

View File

@ -17,9 +17,9 @@ export * from './metadata.repository';
export * from './move.repository'; export * from './move.repository';
export * from './partner.repository'; export * from './partner.repository';
export * from './person.repository'; export * from './person.repository';
export * from './search.repository';
export * from './server-info.repository'; export * from './server-info.repository';
export * from './shared-link.repository'; export * from './shared-link.repository';
export * from './smart-info.repository';
export * from './system-config.repository'; export * from './system-config.repository';
export * from './system-metadata.repository'; export * from './system-metadata.repository';
export * from './tag.repository'; export * from './tag.repository';

View File

@ -1,10 +1,15 @@
import { import {
AssetSearchOptions,
DatabaseExtension, DatabaseExtension,
Embedding, Embedding,
EmbeddingSearch,
FaceEmbeddingSearch, FaceEmbeddingSearch,
FaceSearchResult, FaceSearchResult,
ISmartInfoRepository, ISearchRepository,
Paginated,
PaginationMode,
PaginationResult,
SearchPaginationOptions,
SmartSearchOptions,
} from '@app/domain'; } from '@app/domain';
import { getCLIPModelInfo } from '@app/domain/smart-info/smart-info.constant'; import { getCLIPModelInfo } from '@app/domain/smart-info/smart-info.constant';
import { AssetEntity, AssetFaceEntity, SmartInfoEntity, SmartSearchEntity } from '@app/infra/entities'; import { AssetEntity, AssetFaceEntity, SmartInfoEntity, SmartSearchEntity } from '@app/infra/entities';
@ -14,11 +19,11 @@ import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm'; import { Repository } from 'typeorm';
import { vectorExt } from '../database.config'; import { vectorExt } from '../database.config';
import { DummyValue, GenerateSql } from '../infra.util'; import { DummyValue, GenerateSql } from '../infra.util';
import { asVector, isValidInteger } from '../infra.utils'; import { asVector, isValidInteger, paginatedBuilder, searchAssetBuilder } from '../infra.utils';
@Injectable() @Injectable()
export class SmartInfoRepository implements ISmartInfoRepository { export class SearchRepository implements ISearchRepository {
private logger = new ImmichLogger(SmartInfoRepository.name); private logger = new ImmichLogger(SearchRepository.name);
private faceColumns: string[]; private faceColumns: string[];
constructor( constructor(
@ -35,48 +40,74 @@ export class SmartInfoRepository implements ISmartInfoRepository {
async init(modelName: string): Promise<void> { async init(modelName: string): Promise<void> {
const { dimSize } = getCLIPModelInfo(modelName); const { dimSize } = getCLIPModelInfo(modelName);
if (dimSize == null) { const curDimSize = await this.getDimSize();
throw new Error(`Invalid CLIP model name: ${modelName}`); this.logger.verbose(`Current database CLIP dimension size is ${curDimSize}`);
}
const currentDimSize = await this.getDimSize(); if (dimSize != curDimSize) {
this.logger.verbose(`Current database CLIP dimension size is ${currentDimSize}`); this.logger.log(`Dimension size of model ${modelName} is ${dimSize}, but database expects ${curDimSize}.`);
if (dimSize != currentDimSize) {
this.logger.log(`Dimension size of model ${modelName} is ${dimSize}, but database expects ${currentDimSize}.`);
await this.updateDimSize(dimSize); await this.updateDimSize(dimSize);
} }
} }
@GenerateSql({ @GenerateSql({
params: [{ userIds: [DummyValue.UUID], embedding: Array.from({ length: 512 }, Math.random), numResults: 100 }], params: [
{ page: 1, size: 100 },
{
takenAfter: DummyValue.DATE,
lensModel: DummyValue.STRING,
ownerId: DummyValue.UUID,
withStacked: true,
isFavorite: true,
},
],
}) })
async searchCLIP({ userIds, embedding, numResults, withArchived }: EmbeddingSearch): Promise<AssetEntity[]> { async searchMetadata(pagination: SearchPaginationOptions, options: AssetSearchOptions): Paginated<AssetEntity> {
if (!isValidInteger(numResults, { min: 1 })) { let builder = this.assetRepository.createQueryBuilder('asset');
throw new Error(`Invalid value for 'numResults': ${numResults}`); builder = searchAssetBuilder(builder, options);
}
// setting this too low messes with prefilter recall builder.orderBy('asset.fileCreatedAt', options.orderDirection ?? 'DESC');
numResults = Math.max(numResults, 64);
return paginatedBuilder<AssetEntity>(builder, {
mode: PaginationMode.SKIP_TAKE,
skip: (pagination.page - 1) * pagination.size,
take: pagination.size,
});
}
@GenerateSql({
params: [
{ page: 1, size: 100 },
{
takenAfter: DummyValue.DATE,
embedding: Array.from({ length: 512 }, Math.random),
lensModel: DummyValue.STRING,
withStacked: true,
isFavorite: true,
userIds: [DummyValue.UUID],
},
],
})
async searchSmart(
pagination: SearchPaginationOptions,
{ embedding, userIds, ...options }: SmartSearchOptions,
): Paginated<AssetEntity> {
let results: PaginationResult<AssetEntity> = { items: [], hasNextPage: false };
let results: AssetEntity[] = [];
await this.assetRepository.manager.transaction(async (manager) => { await this.assetRepository.manager.transaction(async (manager) => {
const query = manager let builder = manager.createQueryBuilder(AssetEntity, 'asset');
.createQueryBuilder(AssetEntity, 'a') builder = searchAssetBuilder(builder, options);
.innerJoin('a.smartSearch', 's') builder
.leftJoinAndSelect('a.exifInfo', 'e') .innerJoin('asset.smartSearch', 'search')
.where('a.ownerId IN (:...userIds )') .andWhere('asset.ownerId IN (:...userIds )')
.orderBy('s.embedding <=> :embedding') .orderBy('search.embedding <=> :embedding')
.setParameters({ userIds, embedding: asVector(embedding) }); .setParameters({ userIds, embedding: asVector(embedding) });
if (!withArchived) { await manager.query(this.getRuntimeConfig(pagination.size));
query.andWhere('a.isArchived = false'); results = await paginatedBuilder<AssetEntity>(builder, {
} mode: PaginationMode.LIMIT_OFFSET,
query.andWhere('a.isVisible = true').andWhere('a.fileCreatedAt < NOW()'); skip: (pagination.page - 1) * pagination.size,
query.limit(numResults); take: pagination.size,
});
await manager.query(this.getRuntimeConfig(numResults));
results = await query.getMany();
}); });
return results; return results;
@ -135,7 +166,6 @@ export class SmartInfoRepository implements ISmartInfoRepository {
.where('res.distance <= :maxDistance', { maxDistance }) .where('res.distance <= :maxDistance', { maxDistance })
.getRawMany(); .getRawMany();
}); });
return results.map((row) => ({ return results.map((row) => ({
face: this.assetFaceRepository.create(row), face: this.assetFaceRepository.create(row),
distance: row.distance, distance: row.distance,
@ -163,17 +193,14 @@ export class SmartInfoRepository implements ISmartInfoRepository {
throw new Error(`Invalid CLIP dimension size: ${dimSize}`); throw new Error(`Invalid CLIP dimension size: ${dimSize}`);
} }
const currentDimSize = await this.getDimSize(); const curDimSize = await this.getDimSize();
if (currentDimSize === dimSize) { if (curDimSize === dimSize) {
return; return;
} }
this.logger.log(`Updating database CLIP dimension size to ${dimSize}.`); this.logger.log(`Updating database CLIP dimension size to ${dimSize}.`);
await this.smartSearchRepository.manager.transaction(async (manager) => { await this.smartSearchRepository.manager.transaction(async (manager) => {
if (vectorExt === DatabaseExtension.VECTORS) {
await manager.query(`SET vectors.pgvector_compatibility=on`);
}
await manager.query(`DROP TABLE smart_search`); await manager.query(`DROP TABLE smart_search`);
await manager.query(` await manager.query(`
@ -182,12 +209,15 @@ export class SmartInfoRepository implements ISmartInfoRepository {
embedding vector(${dimSize}) NOT NULL )`); embedding vector(${dimSize}) NOT NULL )`);
await manager.query(` await manager.query(`
CREATE INDEX IF NOT EXISTS clip_index ON smart_search CREATE INDEX clip_index ON smart_search
USING hnsw (embedding vector_cosine_ops) USING vectors (embedding vector_cos_ops) WITH (options = $$
WITH (ef_construction = 300, m = 16)`); [indexing.hnsw]
m = 16
ef_construction = 300
$$)`);
}); });
this.logger.log(`Successfully updated database CLIP dimension size from ${currentDimSize} to ${dimSize}.`); this.logger.log(`Successfully updated database CLIP dimension size from ${curDimSize} to ${dimSize}.`);
} }
private async getDimSize(): Promise<number> { private async getDimSize(): Promise<number> {

View File

@ -19,8 +19,8 @@ import {
MoveRepository, MoveRepository,
PartnerRepository, PartnerRepository,
PersonRepository, PersonRepository,
SearchRepository,
SharedLinkRepository, SharedLinkRepository,
SmartInfoRepository,
SystemConfigRepository, SystemConfigRepository,
SystemMetadataRepository, SystemMetadataRepository,
TagRepository, TagRepository,
@ -41,7 +41,7 @@ const repositories = [
PartnerRepository, PartnerRepository,
PersonRepository, PersonRepository,
SharedLinkRepository, SharedLinkRepository,
SmartInfoRepository, SearchRepository,
SystemConfigRepository, SystemConfigRepository,
SystemMetadataRepository, SystemMetadataRepository,
TagRepository, TagRepository,
@ -142,7 +142,7 @@ class SqlGenerator {
this.sqlLogger.clear(); this.sqlLogger.clear();
// errors still generate sql, which is all we care about // errors still generate sql, which is all we care about
await target.apply(instance, params).catch(() => null); await target.apply(instance, params).catch((error: Error) => console.error(`${queryLabel} error: ${error}`));
if (this.sqlLogger.queries.length === 0) { if (this.sqlLogger.queries.length === 0) {
console.warn(`No queries recorded for ${queryLabel}`); console.warn(`No queries recorded for ${queryLabel}`);

View File

@ -0,0 +1,234 @@
-- NOTE: This file is auto generated by ./sql-generator
-- SearchRepository.searchMetadata
SELECT DISTINCT
"distinctAlias"."asset_id" AS "ids_asset_id",
"distinctAlias"."asset_fileCreatedAt"
FROM
(
SELECT
"asset"."id" AS "asset_id",
"asset"."deviceAssetId" AS "asset_deviceAssetId",
"asset"."ownerId" AS "asset_ownerId",
"asset"."libraryId" AS "asset_libraryId",
"asset"."deviceId" AS "asset_deviceId",
"asset"."type" AS "asset_type",
"asset"."originalPath" AS "asset_originalPath",
"asset"."resizePath" AS "asset_resizePath",
"asset"."webpPath" AS "asset_webpPath",
"asset"."thumbhash" AS "asset_thumbhash",
"asset"."encodedVideoPath" AS "asset_encodedVideoPath",
"asset"."createdAt" AS "asset_createdAt",
"asset"."updatedAt" AS "asset_updatedAt",
"asset"."deletedAt" AS "asset_deletedAt",
"asset"."fileCreatedAt" AS "asset_fileCreatedAt",
"asset"."localDateTime" AS "asset_localDateTime",
"asset"."fileModifiedAt" AS "asset_fileModifiedAt",
"asset"."isFavorite" AS "asset_isFavorite",
"asset"."isArchived" AS "asset_isArchived",
"asset"."isExternal" AS "asset_isExternal",
"asset"."isReadOnly" AS "asset_isReadOnly",
"asset"."isOffline" AS "asset_isOffline",
"asset"."checksum" AS "asset_checksum",
"asset"."duration" AS "asset_duration",
"asset"."isVisible" AS "asset_isVisible",
"asset"."livePhotoVideoId" AS "asset_livePhotoVideoId",
"asset"."originalFileName" AS "asset_originalFileName",
"asset"."sidecarPath" AS "asset_sidecarPath",
"asset"."stackId" AS "asset_stackId",
"stack"."id" AS "stack_id",
"stack"."primaryAssetId" AS "stack_primaryAssetId",
"stackedAssets"."id" AS "stackedAssets_id",
"stackedAssets"."deviceAssetId" AS "stackedAssets_deviceAssetId",
"stackedAssets"."ownerId" AS "stackedAssets_ownerId",
"stackedAssets"."libraryId" AS "stackedAssets_libraryId",
"stackedAssets"."deviceId" AS "stackedAssets_deviceId",
"stackedAssets"."type" AS "stackedAssets_type",
"stackedAssets"."originalPath" AS "stackedAssets_originalPath",
"stackedAssets"."resizePath" AS "stackedAssets_resizePath",
"stackedAssets"."webpPath" AS "stackedAssets_webpPath",
"stackedAssets"."thumbhash" AS "stackedAssets_thumbhash",
"stackedAssets"."encodedVideoPath" AS "stackedAssets_encodedVideoPath",
"stackedAssets"."createdAt" AS "stackedAssets_createdAt",
"stackedAssets"."updatedAt" AS "stackedAssets_updatedAt",
"stackedAssets"."deletedAt" AS "stackedAssets_deletedAt",
"stackedAssets"."fileCreatedAt" AS "stackedAssets_fileCreatedAt",
"stackedAssets"."localDateTime" AS "stackedAssets_localDateTime",
"stackedAssets"."fileModifiedAt" AS "stackedAssets_fileModifiedAt",
"stackedAssets"."isFavorite" AS "stackedAssets_isFavorite",
"stackedAssets"."isArchived" AS "stackedAssets_isArchived",
"stackedAssets"."isExternal" AS "stackedAssets_isExternal",
"stackedAssets"."isReadOnly" AS "stackedAssets_isReadOnly",
"stackedAssets"."isOffline" AS "stackedAssets_isOffline",
"stackedAssets"."checksum" AS "stackedAssets_checksum",
"stackedAssets"."duration" AS "stackedAssets_duration",
"stackedAssets"."isVisible" AS "stackedAssets_isVisible",
"stackedAssets"."livePhotoVideoId" AS "stackedAssets_livePhotoVideoId",
"stackedAssets"."originalFileName" AS "stackedAssets_originalFileName",
"stackedAssets"."sidecarPath" AS "stackedAssets_sidecarPath",
"stackedAssets"."stackId" AS "stackedAssets_stackId"
FROM
"assets" "asset"
LEFT JOIN "exif" "exifInfo" ON "exifInfo"."assetId" = "asset"."id"
LEFT JOIN "asset_stack" "stack" ON "stack"."id" = "asset"."stackId"
LEFT JOIN "assets" "stackedAssets" ON "stackedAssets"."stackId" = "stack"."id"
AND ("stackedAssets"."deletedAt" IS NULL)
WHERE
(
"asset"."fileCreatedAt" >= $1
AND "exifInfo"."lensModel" = $2
AND "asset"."ownerId" = $3
AND 1 = 1
AND "asset"."isFavorite" = $4
AND (
"stack"."primaryAssetId" = "asset"."id"
OR "asset"."stackId" IS NULL
)
)
AND ("asset"."deletedAt" IS NULL)
) "distinctAlias"
ORDER BY
"distinctAlias"."asset_fileCreatedAt" DESC,
"asset_id" ASC
LIMIT
101
-- SearchRepository.searchSmart
START TRANSACTION
SET
LOCAL vectors.enable_prefilter = on;
SET
LOCAL vectors.search_mode = vbase;
SET
LOCAL vectors.hnsw_ef_search = 100;
SELECT
"asset"."id" AS "asset_id",
"asset"."deviceAssetId" AS "asset_deviceAssetId",
"asset"."ownerId" AS "asset_ownerId",
"asset"."libraryId" AS "asset_libraryId",
"asset"."deviceId" AS "asset_deviceId",
"asset"."type" AS "asset_type",
"asset"."originalPath" AS "asset_originalPath",
"asset"."resizePath" AS "asset_resizePath",
"asset"."webpPath" AS "asset_webpPath",
"asset"."thumbhash" AS "asset_thumbhash",
"asset"."encodedVideoPath" AS "asset_encodedVideoPath",
"asset"."createdAt" AS "asset_createdAt",
"asset"."updatedAt" AS "asset_updatedAt",
"asset"."deletedAt" AS "asset_deletedAt",
"asset"."fileCreatedAt" AS "asset_fileCreatedAt",
"asset"."localDateTime" AS "asset_localDateTime",
"asset"."fileModifiedAt" AS "asset_fileModifiedAt",
"asset"."isFavorite" AS "asset_isFavorite",
"asset"."isArchived" AS "asset_isArchived",
"asset"."isExternal" AS "asset_isExternal",
"asset"."isReadOnly" AS "asset_isReadOnly",
"asset"."isOffline" AS "asset_isOffline",
"asset"."checksum" AS "asset_checksum",
"asset"."duration" AS "asset_duration",
"asset"."isVisible" AS "asset_isVisible",
"asset"."livePhotoVideoId" AS "asset_livePhotoVideoId",
"asset"."originalFileName" AS "asset_originalFileName",
"asset"."sidecarPath" AS "asset_sidecarPath",
"asset"."stackId" AS "asset_stackId",
"stack"."id" AS "stack_id",
"stack"."primaryAssetId" AS "stack_primaryAssetId",
"stackedAssets"."id" AS "stackedAssets_id",
"stackedAssets"."deviceAssetId" AS "stackedAssets_deviceAssetId",
"stackedAssets"."ownerId" AS "stackedAssets_ownerId",
"stackedAssets"."libraryId" AS "stackedAssets_libraryId",
"stackedAssets"."deviceId" AS "stackedAssets_deviceId",
"stackedAssets"."type" AS "stackedAssets_type",
"stackedAssets"."originalPath" AS "stackedAssets_originalPath",
"stackedAssets"."resizePath" AS "stackedAssets_resizePath",
"stackedAssets"."webpPath" AS "stackedAssets_webpPath",
"stackedAssets"."thumbhash" AS "stackedAssets_thumbhash",
"stackedAssets"."encodedVideoPath" AS "stackedAssets_encodedVideoPath",
"stackedAssets"."createdAt" AS "stackedAssets_createdAt",
"stackedAssets"."updatedAt" AS "stackedAssets_updatedAt",
"stackedAssets"."deletedAt" AS "stackedAssets_deletedAt",
"stackedAssets"."fileCreatedAt" AS "stackedAssets_fileCreatedAt",
"stackedAssets"."localDateTime" AS "stackedAssets_localDateTime",
"stackedAssets"."fileModifiedAt" AS "stackedAssets_fileModifiedAt",
"stackedAssets"."isFavorite" AS "stackedAssets_isFavorite",
"stackedAssets"."isArchived" AS "stackedAssets_isArchived",
"stackedAssets"."isExternal" AS "stackedAssets_isExternal",
"stackedAssets"."isReadOnly" AS "stackedAssets_isReadOnly",
"stackedAssets"."isOffline" AS "stackedAssets_isOffline",
"stackedAssets"."checksum" AS "stackedAssets_checksum",
"stackedAssets"."duration" AS "stackedAssets_duration",
"stackedAssets"."isVisible" AS "stackedAssets_isVisible",
"stackedAssets"."livePhotoVideoId" AS "stackedAssets_livePhotoVideoId",
"stackedAssets"."originalFileName" AS "stackedAssets_originalFileName",
"stackedAssets"."sidecarPath" AS "stackedAssets_sidecarPath",
"stackedAssets"."stackId" AS "stackedAssets_stackId"
FROM
"assets" "asset"
LEFT JOIN "exif" "exifInfo" ON "exifInfo"."assetId" = "asset"."id"
LEFT JOIN "asset_stack" "stack" ON "stack"."id" = "asset"."stackId"
LEFT JOIN "assets" "stackedAssets" ON "stackedAssets"."stackId" = "stack"."id"
AND ("stackedAssets"."deletedAt" IS NULL)
INNER JOIN "smart_search" "search" ON "search"."assetId" = "asset"."id"
WHERE
(
"asset"."fileCreatedAt" >= $1
AND "exifInfo"."lensModel" = $2
AND 1 = 1
AND 1 = 1
AND "asset"."isFavorite" = $3
AND (
"stack"."primaryAssetId" = "asset"."id"
OR "asset"."stackId" IS NULL
)
AND "asset"."ownerId" IN ($4)
)
AND ("asset"."deletedAt" IS NULL)
ORDER BY
"search"."embedding" <= > $5 ASC
LIMIT
101
COMMIT
-- SearchRepository.searchFaces
START TRANSACTION
SET
LOCAL vectors.enable_prefilter = on;
SET
LOCAL vectors.search_mode = vbase;
SET
LOCAL vectors.hnsw_ef_search = 100;
WITH
"cte" AS (
SELECT
"faces"."id" AS "id",
"faces"."assetId" AS "assetId",
"faces"."personId" AS "personId",
"faces"."imageWidth" AS "imageWidth",
"faces"."imageHeight" AS "imageHeight",
"faces"."boundingBoxX1" AS "boundingBoxX1",
"faces"."boundingBoxY1" AS "boundingBoxY1",
"faces"."boundingBoxX2" AS "boundingBoxX2",
"faces"."boundingBoxY2" AS "boundingBoxY2",
"faces"."embedding" <= > $1 AS "distance"
FROM
"asset_faces" "faces"
INNER JOIN "assets" "asset" ON "asset"."id" = "faces"."assetId"
AND ("asset"."deletedAt" IS NULL)
WHERE
"asset"."ownerId" IN ($2)
ORDER BY
"faces"."embedding" <= > $1 ASC
LIMIT
100
)
SELECT
res.*
FROM
"cte" "res"
WHERE
res.distance <= $3
COMMIT

View File

@ -1,129 +0,0 @@
-- NOTE: This file is auto generated by ./sql-generator
-- SmartInfoRepository.searchCLIP
START TRANSACTION
SET
LOCAL vectors.enable_prefilter = on;
SET
LOCAL vectors.search_mode = vbase;
SET
LOCAL vectors.hnsw_ef_search = 100;
SELECT
"a"."id" AS "a_id",
"a"."deviceAssetId" AS "a_deviceAssetId",
"a"."ownerId" AS "a_ownerId",
"a"."libraryId" AS "a_libraryId",
"a"."deviceId" AS "a_deviceId",
"a"."type" AS "a_type",
"a"."originalPath" AS "a_originalPath",
"a"."resizePath" AS "a_resizePath",
"a"."webpPath" AS "a_webpPath",
"a"."thumbhash" AS "a_thumbhash",
"a"."encodedVideoPath" AS "a_encodedVideoPath",
"a"."createdAt" AS "a_createdAt",
"a"."updatedAt" AS "a_updatedAt",
"a"."deletedAt" AS "a_deletedAt",
"a"."fileCreatedAt" AS "a_fileCreatedAt",
"a"."localDateTime" AS "a_localDateTime",
"a"."fileModifiedAt" AS "a_fileModifiedAt",
"a"."isFavorite" AS "a_isFavorite",
"a"."isArchived" AS "a_isArchived",
"a"."isExternal" AS "a_isExternal",
"a"."isReadOnly" AS "a_isReadOnly",
"a"."isOffline" AS "a_isOffline",
"a"."checksum" AS "a_checksum",
"a"."duration" AS "a_duration",
"a"."isVisible" AS "a_isVisible",
"a"."livePhotoVideoId" AS "a_livePhotoVideoId",
"a"."originalFileName" AS "a_originalFileName",
"a"."sidecarPath" AS "a_sidecarPath",
"a"."stackId" AS "a_stackId",
"e"."assetId" AS "e_assetId",
"e"."description" AS "e_description",
"e"."exifImageWidth" AS "e_exifImageWidth",
"e"."exifImageHeight" AS "e_exifImageHeight",
"e"."fileSizeInByte" AS "e_fileSizeInByte",
"e"."orientation" AS "e_orientation",
"e"."dateTimeOriginal" AS "e_dateTimeOriginal",
"e"."modifyDate" AS "e_modifyDate",
"e"."timeZone" AS "e_timeZone",
"e"."latitude" AS "e_latitude",
"e"."longitude" AS "e_longitude",
"e"."projectionType" AS "e_projectionType",
"e"."city" AS "e_city",
"e"."livePhotoCID" AS "e_livePhotoCID",
"e"."autoStackId" AS "e_autoStackId",
"e"."state" AS "e_state",
"e"."country" AS "e_country",
"e"."make" AS "e_make",
"e"."model" AS "e_model",
"e"."lensModel" AS "e_lensModel",
"e"."fNumber" AS "e_fNumber",
"e"."focalLength" AS "e_focalLength",
"e"."iso" AS "e_iso",
"e"."exposureTime" AS "e_exposureTime",
"e"."profileDescription" AS "e_profileDescription",
"e"."colorspace" AS "e_colorspace",
"e"."bitsPerSample" AS "e_bitsPerSample",
"e"."fps" AS "e_fps"
FROM
"assets" "a"
INNER JOIN "smart_search" "s" ON "s"."assetId" = "a"."id"
LEFT JOIN "exif" "e" ON "e"."assetId" = "a"."id"
WHERE
(
"a"."ownerId" IN ($1)
AND "a"."isArchived" = false
AND "a"."isVisible" = true
AND "a"."fileCreatedAt" < NOW()
)
AND ("a"."deletedAt" IS NULL)
ORDER BY
"s"."embedding" <= > $2 ASC
LIMIT
100
COMMIT
-- SmartInfoRepository.searchFaces
START TRANSACTION
SET
LOCAL vectors.enable_prefilter = on;
SET
LOCAL vectors.search_mode = vbase;
SET
LOCAL vectors.hnsw_ef_search = 100;
WITH
"cte" AS (
SELECT
"faces"."id" AS "id",
"faces"."assetId" AS "assetId",
"faces"."personId" AS "personId",
"faces"."imageWidth" AS "imageWidth",
"faces"."imageHeight" AS "imageHeight",
"faces"."boundingBoxX1" AS "boundingBoxX1",
"faces"."boundingBoxY1" AS "boundingBoxY1",
"faces"."boundingBoxX2" AS "boundingBoxX2",
"faces"."boundingBoxY2" AS "boundingBoxY2",
"faces"."embedding" <= > $1 AS "distance"
FROM
"asset_faces" "faces"
INNER JOIN "assets" "asset" ON "asset"."id" = "faces"."assetId"
AND ("asset"."deletedAt" IS NULL)
WHERE
"asset"."ownerId" IN ($2)
ORDER BY
"faces"."embedding" <= > $1 ASC
LIMIT
100
)
SELECT
res.*
FROM
"cte" "res"
WHERE
res.distance <= $3
COMMIT

View File

@ -32,7 +32,6 @@ export const newAssetRepositoryMock = (): jest.Mocked<IAssetRepository> => {
getTimeBuckets: jest.fn(), getTimeBuckets: jest.fn(),
restoreAll: jest.fn(), restoreAll: jest.fn(),
softDeleteAll: jest.fn(), softDeleteAll: jest.fn(),
search: jest.fn(),
getAssetIdByCity: jest.fn(), getAssetIdByCity: jest.fn(),
getAssetIdByTag: jest.fn(), getAssetIdByTag: jest.fn(),
searchMetadata: jest.fn(), searchMetadata: jest.fn(),

View File

@ -15,8 +15,8 @@ export * from './metadata.repository.mock';
export * from './move.repository.mock'; export * from './move.repository.mock';
export * from './partner.repository.mock'; export * from './partner.repository.mock';
export * from './person.repository.mock'; export * from './person.repository.mock';
export * from './search.repository.mock';
export * from './shared-link.repository.mock'; export * from './shared-link.repository.mock';
export * from './smart-info.repository.mock';
export * from './storage.repository.mock'; export * from './storage.repository.mock';
export * from './system-config.repository.mock'; export * from './system-config.repository.mock';
export * from './system-info.repository.mock'; export * from './system-info.repository.mock';

View File

@ -0,0 +1,11 @@
import { ISearchRepository } from '@app/domain';
export const newSearchRepositoryMock = (): jest.Mocked<ISearchRepository> => {
return {
init: jest.fn(),
searchMetadata: jest.fn(),
searchSmart: jest.fn(),
searchFaces: jest.fn(),
upsert: jest.fn(),
};
};

View File

@ -1,10 +0,0 @@
import { ISmartInfoRepository } from '@app/domain';
export const newSmartInfoRepositoryMock = (): jest.Mocked<ISmartInfoRepository> => {
return {
init: jest.fn(),
searchCLIP: jest.fn(),
searchFaces: jest.fn(),
upsert: jest.fn(),
};
};

View File

@ -9,7 +9,7 @@
export let right = 0; export let right = 0;
export let root: HTMLElement | null = null; export let root: HTMLElement | null = null;
let intersecting = false; export let intersecting = false;
let container: HTMLDivElement; let container: HTMLDivElement;
const dispatch = createEventDispatcher<{ const dispatch = createEventDispatcher<{
hidden: HTMLDivElement; hidden: HTMLDivElement;

View File

@ -37,6 +37,7 @@
export let readonly = false; export let readonly = false;
export let showArchiveIcon = false; export let showArchiveIcon = false;
export let showStackedIcon = true; export let showStackedIcon = true;
export let intersecting = false;
let className = ''; let className = '';
export { className as class }; export { className as class };
@ -85,7 +86,7 @@
}; };
</script> </script>
<IntersectionObserver once={false} let:intersecting> <IntersectionObserver once={false} on:intersected bind:intersecting>
<!-- svelte-ignore a11y-no-static-element-interactions --> <!-- svelte-ignore a11y-no-static-element-interactions -->
<div <div
style:width="{width}px" style:width="{width}px"
@ -95,8 +96,8 @@
: 'bg-immich-primary/20 dark:bg-immich-dark-primary/20'}" : 'bg-immich-primary/20 dark:bg-immich-dark-primary/20'}"
class:cursor-not-allowed={disabled} class:cursor-not-allowed={disabled}
class:hover:cursor-pointer={!disabled} class:hover:cursor-pointer={!disabled}
on:mouseenter={() => onMouseEnter()} on:mouseenter={onMouseEnter}
on:mouseleave={() => onMouseLeave()} on:mouseleave={onMouseLeave}
on:click={thumbnailClickedHandler} on:click={thumbnailClickedHandler}
on:keydown={thumbnailKeyDownHandler} on:keydown={thumbnailKeyDownHandler}
> >

View File

@ -8,6 +8,10 @@
import { getThumbnailSize } from '$lib/utils/thumbnail-util'; import { getThumbnailSize } from '$lib/utils/thumbnail-util';
import { assetViewingStore } from '$lib/stores/asset-viewing.store'; import { assetViewingStore } from '$lib/stores/asset-viewing.store';
import { onDestroy } from 'svelte'; import { onDestroy } from 'svelte';
import { createEventDispatcher } from 'svelte';
import type { BucketPosition } from '$lib/stores/assets.store';
const dispatch = createEventDispatcher<{ intersected: { container: HTMLDivElement; position: BucketPosition } }>();
export let assets: AssetResponseDto[]; export let assets: AssetResponseDto[];
export let selectedAssets: Set<AssetResponseDto> = new Set(); export let selectedAssets: Set<AssetResponseDto> = new Set();
@ -18,7 +22,6 @@
let selectedAsset: AssetResponseDto; let selectedAsset: AssetResponseDto;
let currentViewAssetIndex = 0; let currentViewAssetIndex = 0;
let viewWidth: number; let viewWidth: number;
$: thumbnailSize = getThumbnailSize(assets.length, viewWidth); $: thumbnailSize = getThumbnailSize(assets.length, viewWidth);
@ -88,7 +91,7 @@
{#if assets.length > 0} {#if assets.length > 0}
<div class="flex w-full flex-wrap gap-1 pb-20" bind:clientWidth={viewWidth}> <div class="flex w-full flex-wrap gap-1 pb-20" bind:clientWidth={viewWidth}>
{#each assets as asset (asset.id)} {#each assets as asset, i (asset.id)}
<div animate:flip={{ duration: 500 }}> <div animate:flip={{ duration: 500 }}>
<Thumbnail <Thumbnail
{asset} {asset}
@ -97,6 +100,8 @@
format={assets.length < 7 ? ThumbnailFormat.Jpeg : ThumbnailFormat.Webp} format={assets.length < 7 ? ThumbnailFormat.Jpeg : ThumbnailFormat.Webp}
on:click={(e) => (isMultiSelectionMode ? selectAssetHandler(e) : viewAssetHandler(e))} on:click={(e) => (isMultiSelectionMode ? selectAssetHandler(e) : viewAssetHandler(e))}
on:select={selectAssetHandler} on:select={selectAssetHandler}
on:intersected={(event) =>
i === Math.max(1, assets.length - 7) ? dispatch('intersected', event.detail) : undefined}
selected={selectedAssets.has(asset)} selected={selectedAssets.has(asset)}
{showArchiveIcon} {showArchiveIcon}
/> />

View File

@ -32,6 +32,7 @@
const parameters = new URLSearchParams({ const parameters = new URLSearchParams({
q: searchValue, q: searchValue,
smart: smartSearch, smart: smartSearch,
take: '100',
}); });
showHistory = false; showHistory = false;

View File

@ -14,7 +14,6 @@
import ControlAppBar from '$lib/components/shared-components/control-app-bar.svelte'; import ControlAppBar from '$lib/components/shared-components/control-app-bar.svelte';
import GalleryViewer from '$lib/components/shared-components/gallery-viewer/gallery-viewer.svelte'; import GalleryViewer from '$lib/components/shared-components/gallery-viewer/gallery-viewer.svelte';
import SearchBar from '$lib/components/shared-components/search-bar/search-bar.svelte'; import SearchBar from '$lib/components/shared-components/search-bar/search-bar.svelte';
import type { AssetResponseDto } from '@api';
import type { PageData } from './$types'; import type { PageData } from './$types';
import Icon from '$lib/components/elements/icon.svelte'; import Icon from '$lib/components/elements/icon.svelte';
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte'; import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
@ -27,15 +26,20 @@
import { preventRaceConditionSearchBar } from '$lib/stores/search.store'; import { preventRaceConditionSearchBar } from '$lib/stores/search.store';
import { shouldIgnoreShortcut } from '$lib/utils/shortcut'; import { shouldIgnoreShortcut } from '$lib/utils/shortcut';
import { mdiArrowLeft, mdiDotsVertical, mdiImageOffOutline, mdiPlus, mdiSelectAll } from '@mdi/js'; import { mdiArrowLeft, mdiDotsVertical, mdiImageOffOutline, mdiPlus, mdiSelectAll } from '@mdi/js';
import type { AssetResponseDto, SearchResponseDto } from '@immich/sdk';
import { authenticate } from '$lib/utils/auth';
import { api } from '@api';
export let data: PageData; export let data: PageData;
const MAX_ASSET_COUNT = 5000;
let { isViewing: showAssetViewer } = assetViewingStore; let { isViewing: showAssetViewer } = assetViewingStore;
// The GalleryViewer pushes it's own history state, which causes weird // The GalleryViewer pushes it's own history state, which causes weird
// behavior for history.back(). To prevent that we store the previous page // behavior for history.back(). To prevent that we store the previous page
// manually and navigate back to that. // manually and navigate back to that.
let previousRoute = AppRoute.EXPLORE as string; let previousRoute = AppRoute.EXPLORE as string;
$: curPage = data.results?.assets.nextPage;
$: albums = data.results?.albums.items; $: albums = data.results?.albums.items;
const onKeyboardPress = (event: KeyboardEvent) => handleKeyboardPress(event); const onKeyboardPress = (event: KeyboardEvent) => handleKeyboardPress(event);
@ -107,6 +111,33 @@
const handleSelectAll = () => { const handleSelectAll = () => {
selectedAssets = new Set(searchResultAssets); selectedAssets = new Set(searchResultAssets);
}; };
export const loadNextPage = async () => {
if (curPage == null || !term || (searchResultAssets && searchResultAssets.length >= MAX_ASSET_COUNT)) {
return;
}
await authenticate();
let results: SearchResponseDto | null = null;
$page.url.searchParams.set('page', curPage.toString());
const res = await api.searchApi.search({}, { params: $page.url.searchParams });
if (searchResultAssets) {
searchResultAssets.push(...res.data.assets.items);
} else {
searchResultAssets = res.data.assets.items;
}
const assets = {
...res.data.assets,
items: searchResultAssets,
};
results = {
assets,
albums: res.data.albums,
};
data.results = results;
};
</script> </script>
<section> <section>
@ -164,7 +195,12 @@
<section id="search-content" class="relative bg-immich-bg dark:bg-immich-dark-bg"> <section id="search-content" class="relative bg-immich-bg dark:bg-immich-dark-bg">
{#if searchResultAssets && searchResultAssets.length > 0} {#if searchResultAssets && searchResultAssets.length > 0}
<div class="pl-4"> <div class="pl-4">
<GalleryViewer assets={searchResultAssets} bind:selectedAssets showArchiveIcon={true} /> <GalleryViewer
assets={searchResultAssets}
bind:selectedAssets
on:intersected={loadNextPage}
showArchiveIcon={true}
/>
</div> </div>
{:else} {:else}
<div class="flex min-h-[calc(66vh_-_11rem)] w-full place-content-center items-center dark:text-white"> <div class="flex min-h-[calc(66vh_-_11rem)] w-full place-content-center items-center dark:text-white">

View File

@ -1,5 +1,5 @@
import { authenticate } from '$lib/utils/auth'; import { authenticate } from '$lib/utils/auth';
import { type SearchResponseDto, api } from '@api'; import { type AssetResponseDto, type SearchResponseDto, api } from '@api';
import type { PageLoad } from './$types'; import type { PageLoad } from './$types';
import { QueryParameter } from '$lib/constants'; import { QueryParameter } from '$lib/constants';
@ -10,8 +10,18 @@ export const load = (async (data) => {
url.searchParams.get(QueryParameter.SEARCH_TERM) || url.searchParams.get(QueryParameter.QUERY) || undefined; url.searchParams.get(QueryParameter.SEARCH_TERM) || url.searchParams.get(QueryParameter.QUERY) || undefined;
let results: SearchResponseDto | null = null; let results: SearchResponseDto | null = null;
if (term) { if (term) {
const { data } = await api.searchApi.search({}, { params: url.searchParams }); const res = await api.searchApi.search({}, { params: data.url.searchParams });
results = data; let items: AssetResponseDto[] = (data as unknown as { results: SearchResponseDto }).results?.assets.items;
if (items) {
items.push(...res.data.assets.items);
} else {
items = res.data.assets.items;
}
const assets = { ...res.data.assets, items };
results = {
assets,
albums: res.data.albums,
};
} }
return { return {