1
0
Fork 0
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:
jschwalbe 2024-09-23 12:09:26 -04:00 committed by GitHub
parent a7719a94fc
commit 9f8a7e0bea
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 266 additions and 10 deletions

BIN
mobile/openapi/README.md generated

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -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": {

View file

@ -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) {

View file

@ -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);
}

View file

@ -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()

View file

@ -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()

View file

@ -116,6 +116,7 @@ export interface SearchPeopleOptions {
export interface SearchOrderOptions {
orderDirection?: 'ASC' | 'DESC';
random?: boolean;
}
export interface SearchPaginationOptions {

View file

@ -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,

View file

@ -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)) {