mirror of
https://github.com/immich-app/immich.git
synced 2024-12-28 22:51:59 +00:00
feat(server): sort assets randomly from the API 'api/search/metadata' endpoint by including 'order': 'rand' in the API call. (#12741)
feat(server): search metadata random sort order Co-authored-by: Jason Rasmussen <jason@rasm.me>
This commit is contained in:
parent
a7719a94fc
commit
9f8a7e0bea
15 changed files with 266 additions and 10 deletions
BIN
mobile/openapi/README.md
generated
BIN
mobile/openapi/README.md
generated
Binary file not shown.
BIN
mobile/openapi/lib/api.dart
generated
BIN
mobile/openapi/lib/api.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/api/assets_api.dart
generated
BIN
mobile/openapi/lib/api/assets_api.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/api/deprecated_api.dart
generated
BIN
mobile/openapi/lib/api/deprecated_api.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/api/search_api.dart
generated
BIN
mobile/openapi/lib/api/search_api.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/api_client.dart
generated
BIN
mobile/openapi/lib/api_client.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/model/random_search_dto.dart
generated
Normal file
BIN
mobile/openapi/lib/model/random_search_dto.dart
generated
Normal file
Binary file not shown.
|
@ -1646,6 +1646,8 @@
|
|||
},
|
||||
"/assets/random": {
|
||||
"get": {
|
||||
"deprecated": true,
|
||||
"description": "This property was deprecated in v1.116.0",
|
||||
"operationId": "getRandom",
|
||||
"parameters": [
|
||||
{
|
||||
|
@ -1685,8 +1687,12 @@
|
|||
}
|
||||
],
|
||||
"tags": [
|
||||
"Assets"
|
||||
]
|
||||
"Assets",
|
||||
"Deprecated"
|
||||
],
|
||||
"x-immich-lifecycle": {
|
||||
"deprecatedAt": "v1.116.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"/assets/statistics": {
|
||||
|
@ -4677,6 +4683,48 @@
|
|||
]
|
||||
}
|
||||
},
|
||||
"/search/random": {
|
||||
"post": {
|
||||
"operationId": "searchRandom",
|
||||
"parameters": [],
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/RandomSearchDto"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/SearchResponseDto"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearer": []
|
||||
},
|
||||
{
|
||||
"cookie": []
|
||||
},
|
||||
{
|
||||
"api_key": []
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
"Search"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/search/smart": {
|
||||
"post": {
|
||||
"operationId": "searchSmart",
|
||||
|
@ -10454,6 +10502,130 @@
|
|||
],
|
||||
"type": "object"
|
||||
},
|
||||
"RandomSearchDto": {
|
||||
"properties": {
|
||||
"city": {
|
||||
"nullable": true,
|
||||
"type": "string"
|
||||
},
|
||||
"country": {
|
||||
"nullable": true,
|
||||
"type": "string"
|
||||
},
|
||||
"createdAfter": {
|
||||
"format": "date-time",
|
||||
"type": "string"
|
||||
},
|
||||
"createdBefore": {
|
||||
"format": "date-time",
|
||||
"type": "string"
|
||||
},
|
||||
"deviceId": {
|
||||
"type": "string"
|
||||
},
|
||||
"isArchived": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"isEncoded": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"isFavorite": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"isMotion": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"isNotInAlbum": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"isOffline": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"isVisible": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"lensModel": {
|
||||
"nullable": true,
|
||||
"type": "string"
|
||||
},
|
||||
"libraryId": {
|
||||
"format": "uuid",
|
||||
"nullable": true,
|
||||
"type": "string"
|
||||
},
|
||||
"make": {
|
||||
"type": "string"
|
||||
},
|
||||
"model": {
|
||||
"nullable": true,
|
||||
"type": "string"
|
||||
},
|
||||
"page": {
|
||||
"minimum": 1,
|
||||
"type": "number"
|
||||
},
|
||||
"personIds": {
|
||||
"items": {
|
||||
"format": "uuid",
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"size": {
|
||||
"maximum": 1000,
|
||||
"minimum": 1,
|
||||
"type": "number"
|
||||
},
|
||||
"state": {
|
||||
"nullable": true,
|
||||
"type": "string"
|
||||
},
|
||||
"takenAfter": {
|
||||
"format": "date-time",
|
||||
"type": "string"
|
||||
},
|
||||
"takenBefore": {
|
||||
"format": "date-time",
|
||||
"type": "string"
|
||||
},
|
||||
"trashedAfter": {
|
||||
"format": "date-time",
|
||||
"type": "string"
|
||||
},
|
||||
"trashedBefore": {
|
||||
"format": "date-time",
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"$ref": "#/components/schemas/AssetTypeEnum"
|
||||
},
|
||||
"updatedAfter": {
|
||||
"format": "date-time",
|
||||
"type": "string"
|
||||
},
|
||||
"updatedBefore": {
|
||||
"format": "date-time",
|
||||
"type": "string"
|
||||
},
|
||||
"withArchived": {
|
||||
"default": false,
|
||||
"type": "boolean"
|
||||
},
|
||||
"withDeleted": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"withExif": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"withPeople": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"withStacked": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"RatingsResponse": {
|
||||
"properties": {
|
||||
"enabled": {
|
||||
|
|
|
@ -837,6 +837,40 @@ export type PlacesResponseDto = {
|
|||
longitude: number;
|
||||
name: string;
|
||||
};
|
||||
export type RandomSearchDto = {
|
||||
city?: string | null;
|
||||
country?: string | null;
|
||||
createdAfter?: string;
|
||||
createdBefore?: string;
|
||||
deviceId?: string;
|
||||
isArchived?: boolean;
|
||||
isEncoded?: boolean;
|
||||
isFavorite?: boolean;
|
||||
isMotion?: boolean;
|
||||
isNotInAlbum?: boolean;
|
||||
isOffline?: boolean;
|
||||
isVisible?: boolean;
|
||||
lensModel?: string | null;
|
||||
libraryId?: string | null;
|
||||
make?: string;
|
||||
model?: string | null;
|
||||
page?: number;
|
||||
personIds?: string[];
|
||||
size?: number;
|
||||
state?: string | null;
|
||||
takenAfter?: string;
|
||||
takenBefore?: string;
|
||||
trashedAfter?: string;
|
||||
trashedBefore?: string;
|
||||
"type"?: AssetTypeEnum;
|
||||
updatedAfter?: string;
|
||||
updatedBefore?: string;
|
||||
withArchived?: boolean;
|
||||
withDeleted?: boolean;
|
||||
withExif?: boolean;
|
||||
withPeople?: boolean;
|
||||
withStacked?: boolean;
|
||||
};
|
||||
export type SmartSearchDto = {
|
||||
city?: string | null;
|
||||
country?: string | null;
|
||||
|
@ -1696,6 +1730,9 @@ export function getMemoryLane({ day, month }: {
|
|||
...opts
|
||||
}));
|
||||
}
|
||||
/**
|
||||
* This property was deprecated in v1.116.0
|
||||
*/
|
||||
export function getRandom({ count }: {
|
||||
count?: number;
|
||||
}, opts?: Oazapfts.RequestOpts) {
|
||||
|
@ -2500,6 +2537,18 @@ export function searchPlaces({ name }: {
|
|||
...opts
|
||||
}));
|
||||
}
|
||||
export function searchRandom({ randomSearchDto }: {
|
||||
randomSearchDto: RandomSearchDto;
|
||||
}, opts?: Oazapfts.RequestOpts) {
|
||||
return oazapfts.ok(oazapfts.fetchJson<{
|
||||
status: 200;
|
||||
data: SearchResponseDto;
|
||||
}>("/search/random", oazapfts.json({
|
||||
...opts,
|
||||
method: "POST",
|
||||
body: randomSearchDto
|
||||
})));
|
||||
}
|
||||
export function searchSmart({ smartSearchDto }: {
|
||||
smartSearchDto: SmartSearchDto;
|
||||
}, opts?: Oazapfts.RequestOpts) {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Put, Query } from '@nestjs/common';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import { EndpointLifecycle } from 'src/decorators';
|
||||
import { AssetResponseDto, MemoryLaneResponseDto } from 'src/dtos/asset-response.dto';
|
||||
import {
|
||||
AssetBulkDeleteDto,
|
||||
|
@ -31,6 +32,7 @@ export class AssetController {
|
|||
|
||||
@Get('random')
|
||||
@Authenticated()
|
||||
@EndpointLifecycle({ deprecatedAt: 'v1.116.0' })
|
||||
getRandom(@Auth() auth: AuthDto, @Query() dto: RandomAssetsDto): Promise<AssetResponseDto[]> {
|
||||
return this.service.getRandom(auth, dto.count ?? 1);
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ import { PersonResponseDto } from 'src/dtos/person.dto';
|
|||
import {
|
||||
MetadataSearchDto,
|
||||
PlacesResponseDto,
|
||||
RandomSearchDto,
|
||||
SearchExploreResponseDto,
|
||||
SearchPeopleDto,
|
||||
SearchPlacesDto,
|
||||
|
@ -28,6 +29,13 @@ export class SearchController {
|
|||
return this.service.searchMetadata(auth, dto);
|
||||
}
|
||||
|
||||
@Post('random')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Authenticated()
|
||||
searchRandom(@Auth() auth: AuthDto, @Body() dto: RandomSearchDto): Promise<SearchResponseDto> {
|
||||
return this.service.searchRandom(auth, dto);
|
||||
}
|
||||
|
||||
@Post('smart')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Authenticated()
|
||||
|
|
|
@ -119,7 +119,15 @@ class BaseSearchDto {
|
|||
personIds?: string[];
|
||||
}
|
||||
|
||||
export class MetadataSearchDto extends BaseSearchDto {
|
||||
export class RandomSearchDto extends BaseSearchDto {
|
||||
@ValidateBoolean({ optional: true })
|
||||
withStacked?: boolean;
|
||||
|
||||
@ValidateBoolean({ optional: true })
|
||||
withPeople?: boolean;
|
||||
}
|
||||
|
||||
export class MetadataSearchDto extends RandomSearchDto {
|
||||
@ValidateUUID({ optional: true })
|
||||
id?: string;
|
||||
|
||||
|
@ -133,12 +141,6 @@ export class MetadataSearchDto extends BaseSearchDto {
|
|||
@Optional()
|
||||
checksum?: string;
|
||||
|
||||
@ValidateBoolean({ optional: true })
|
||||
withStacked?: boolean;
|
||||
|
||||
@ValidateBoolean({ optional: true })
|
||||
withPeople?: boolean;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@Optional()
|
||||
|
|
|
@ -116,6 +116,7 @@ export interface SearchPeopleOptions {
|
|||
|
||||
export interface SearchOrderOptions {
|
||||
orderDirection?: 'ASC' | 'DESC';
|
||||
random?: boolean;
|
||||
}
|
||||
|
||||
export interface SearchPaginationOptions {
|
||||
|
|
|
@ -73,8 +73,13 @@ export class SearchRepository implements ISearchRepository {
|
|||
async searchMetadata(pagination: SearchPaginationOptions, options: AssetSearchOptions): Paginated<AssetEntity> {
|
||||
let builder = this.assetRepository.createQueryBuilder('asset');
|
||||
builder = searchAssetBuilder(builder, options);
|
||||
|
||||
builder.orderBy('asset.fileCreatedAt', options.orderDirection ?? 'DESC');
|
||||
|
||||
if (options.random) {
|
||||
// TODO replace with complicated SQL magic after kysely migration
|
||||
builder.addSelect('RANDOM() as r').orderBy('r');
|
||||
}
|
||||
|
||||
return paginatedBuilder<AssetEntity>(builder, {
|
||||
mode: PaginationMode.SKIP_TAKE,
|
||||
skip: (pagination.page - 1) * pagination.size,
|
||||
|
|
|
@ -6,6 +6,7 @@ import { PersonResponseDto } from 'src/dtos/person.dto';
|
|||
import {
|
||||
MetadataSearchDto,
|
||||
PlacesResponseDto,
|
||||
RandomSearchDto,
|
||||
SearchPeopleDto,
|
||||
SearchPlacesDto,
|
||||
SearchResponseDto,
|
||||
|
@ -93,6 +94,22 @@ export class SearchService {
|
|||
return this.mapResponse(items, hasNextPage ? (page + 1).toString() : null, { auth });
|
||||
}
|
||||
|
||||
async searchRandom(auth: AuthDto, dto: RandomSearchDto): Promise<SearchResponseDto> {
|
||||
const userIds = await this.getUserIdsToSearch(auth);
|
||||
const page = dto.page ?? 1;
|
||||
const size = dto.size || 250;
|
||||
const { hasNextPage, items } = await this.searchRepository.searchMetadata(
|
||||
{ page, size },
|
||||
{
|
||||
...dto,
|
||||
userIds,
|
||||
random: true,
|
||||
},
|
||||
);
|
||||
|
||||
return this.mapResponse(items, hasNextPage ? (page + 1).toString() : null, { auth });
|
||||
}
|
||||
|
||||
async searchSmart(auth: AuthDto, dto: SmartSearchDto): Promise<SearchResponseDto> {
|
||||
const { machineLearning } = await this.configCore.getConfig({ withCache: false });
|
||||
if (!isSmartSearchEnabled(machineLearning)) {
|
||||
|
|
Loading…
Reference in a new issue