mirror of
https://github.com/immich-app/immich.git
synced 2024-12-29 15:11:58 +00:00
fix(server): /search/random
failing with certain options (#13040)
* fix relation handling, remove pagination * update api, sql * update mock
This commit is contained in:
parent
2f13db51df
commit
7adb35e59e
12 changed files with 243 additions and 40 deletions
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/model/random_search_dto.dart
generated
BIN
mobile/openapi/lib/model/random_search_dto.dart
generated
Binary file not shown.
|
@ -4615,7 +4615,10 @@
|
||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/components/schemas/SearchResponseDto"
|
"items": {
|
||||||
|
"$ref": "#/components/schemas/AssetResponseDto"
|
||||||
|
},
|
||||||
|
"type": "array"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -10463,10 +10466,6 @@
|
||||||
"nullable": true,
|
"nullable": true,
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"page": {
|
|
||||||
"minimum": 1,
|
|
||||||
"type": "number"
|
|
||||||
},
|
|
||||||
"personIds": {
|
"personIds": {
|
||||||
"items": {
|
"items": {
|
||||||
"format": "uuid",
|
"format": "uuid",
|
||||||
|
|
|
@ -852,7 +852,6 @@ export type RandomSearchDto = {
|
||||||
libraryId?: string | null;
|
libraryId?: string | null;
|
||||||
make?: string;
|
make?: string;
|
||||||
model?: string | null;
|
model?: string | null;
|
||||||
page?: number;
|
|
||||||
personIds?: string[];
|
personIds?: string[];
|
||||||
size?: number;
|
size?: number;
|
||||||
state?: string | null;
|
state?: string | null;
|
||||||
|
@ -2523,7 +2522,7 @@ export function searchRandom({ randomSearchDto }: {
|
||||||
}, opts?: Oazapfts.RequestOpts) {
|
}, opts?: Oazapfts.RequestOpts) {
|
||||||
return oazapfts.ok(oazapfts.fetchJson<{
|
return oazapfts.ok(oazapfts.fetchJson<{
|
||||||
status: 200;
|
status: 200;
|
||||||
data: SearchResponseDto;
|
data: AssetResponseDto[];
|
||||||
}>("/search/random", oazapfts.json({
|
}>("/search/random", oazapfts.json({
|
||||||
...opts,
|
...opts,
|
||||||
method: "POST",
|
method: "POST",
|
||||||
|
|
|
@ -32,7 +32,7 @@ export class SearchController {
|
||||||
@Post('random')
|
@Post('random')
|
||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.OK)
|
||||||
@Authenticated()
|
@Authenticated()
|
||||||
searchRandom(@Auth() auth: AuthDto, @Body() dto: RandomSearchDto): Promise<SearchResponseDto> {
|
searchRandom(@Auth() auth: AuthDto, @Body() dto: RandomSearchDto): Promise<AssetResponseDto[]> {
|
||||||
return this.service.searchRandom(auth, dto);
|
return this.service.searchRandom(auth, dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -99,12 +99,6 @@ class BaseSearchDto {
|
||||||
@Optional({ nullable: true, emptyToNull: true })
|
@Optional({ nullable: true, emptyToNull: true })
|
||||||
lensModel?: string | null;
|
lensModel?: string | null;
|
||||||
|
|
||||||
@IsInt()
|
|
||||||
@Min(1)
|
|
||||||
@Type(() => Number)
|
|
||||||
@Optional()
|
|
||||||
page?: number;
|
|
||||||
|
|
||||||
@IsInt()
|
@IsInt()
|
||||||
@Min(1)
|
@Min(1)
|
||||||
@Max(1000)
|
@Max(1000)
|
||||||
|
@ -170,12 +164,24 @@ export class MetadataSearchDto extends RandomSearchDto {
|
||||||
@Optional()
|
@Optional()
|
||||||
@ApiProperty({ enumName: 'AssetOrder', enum: AssetOrder })
|
@ApiProperty({ enumName: 'AssetOrder', enum: AssetOrder })
|
||||||
order?: AssetOrder;
|
order?: AssetOrder;
|
||||||
|
|
||||||
|
@IsInt()
|
||||||
|
@Min(1)
|
||||||
|
@Type(() => Number)
|
||||||
|
@Optional()
|
||||||
|
page?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class SmartSearchDto extends BaseSearchDto {
|
export class SmartSearchDto extends BaseSearchDto {
|
||||||
@IsString()
|
@IsString()
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
query!: string;
|
query!: string;
|
||||||
|
|
||||||
|
@IsInt()
|
||||||
|
@Min(1)
|
||||||
|
@Type(() => Number)
|
||||||
|
@Optional()
|
||||||
|
page?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class SearchPlacesDto {
|
export class SearchPlacesDto {
|
||||||
|
|
|
@ -116,7 +116,6 @@ export interface SearchPeopleOptions {
|
||||||
|
|
||||||
export interface SearchOrderOptions {
|
export interface SearchOrderOptions {
|
||||||
orderDirection?: 'ASC' | 'DESC';
|
orderDirection?: 'ASC' | 'DESC';
|
||||||
random?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SearchPaginationOptions {
|
export interface SearchPaginationOptions {
|
||||||
|
@ -177,6 +176,7 @@ export interface ISearchRepository {
|
||||||
searchSmart(pagination: SearchPaginationOptions, options: SmartSearchOptions): Paginated<AssetEntity>;
|
searchSmart(pagination: SearchPaginationOptions, options: SmartSearchOptions): Paginated<AssetEntity>;
|
||||||
searchDuplicates(options: AssetDuplicateSearch): Promise<AssetDuplicateResult[]>;
|
searchDuplicates(options: AssetDuplicateSearch): Promise<AssetDuplicateResult[]>;
|
||||||
searchFaces(search: FaceEmbeddingSearch): Promise<FaceSearchResult[]>;
|
searchFaces(search: FaceEmbeddingSearch): Promise<FaceSearchResult[]>;
|
||||||
|
searchRandom(size: number, options: AssetSearchOptions): Promise<AssetEntity[]>;
|
||||||
upsert(assetId: string, embedding: number[]): Promise<void>;
|
upsert(assetId: string, embedding: number[]): Promise<void>;
|
||||||
searchPlaces(placeName: string): Promise<GeodataPlacesEntity[]>;
|
searchPlaces(placeName: string): Promise<GeodataPlacesEntity[]>;
|
||||||
getAssetsByCity(userIds: string[]): Promise<AssetEntity[]>;
|
getAssetsByCity(userIds: string[]): Promise<AssetEntity[]>;
|
||||||
|
|
|
@ -77,10 +77,11 @@ FROM
|
||||||
"asset"."fileCreatedAt" >= $1
|
"asset"."fileCreatedAt" >= $1
|
||||||
AND "exifInfo"."lensModel" = $2
|
AND "exifInfo"."lensModel" = $2
|
||||||
AND 1 = 1
|
AND 1 = 1
|
||||||
|
AND "asset"."ownerId" IN ($3)
|
||||||
AND 1 = 1
|
AND 1 = 1
|
||||||
AND (
|
AND (
|
||||||
"asset"."isFavorite" = $3
|
"asset"."isFavorite" = $4
|
||||||
AND "asset"."isArchived" = $4
|
AND "asset"."isArchived" = $5
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
AND ("asset"."deletedAt" IS NULL)
|
AND ("asset"."deletedAt" IS NULL)
|
||||||
|
@ -91,6 +92,190 @@ ORDER BY
|
||||||
LIMIT
|
LIMIT
|
||||||
101
|
101
|
||||||
|
|
||||||
|
-- SearchRepository.searchRandom
|
||||||
|
SELECT DISTINCT
|
||||||
|
"distinctAlias"."asset_id" AS "ids_asset_id",
|
||||||
|
"distinctAlias"."asset_id"
|
||||||
|
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"."status" AS "asset_status",
|
||||||
|
"asset"."originalPath" AS "asset_originalPath",
|
||||||
|
"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"."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",
|
||||||
|
"asset"."duplicateId" AS "asset_duplicateId",
|
||||||
|
"stack"."id" AS "stack_id",
|
||||||
|
"stack"."ownerId" AS "stack_ownerId",
|
||||||
|
"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"."status" AS "stackedAssets_status",
|
||||||
|
"stackedAssets"."originalPath" AS "stackedAssets_originalPath",
|
||||||
|
"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"."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",
|
||||||
|
"stackedAssets"."duplicateId" AS "stackedAssets_duplicateId"
|
||||||
|
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 1 = 1
|
||||||
|
AND "asset"."ownerId" IN ($3)
|
||||||
|
AND 1 = 1
|
||||||
|
AND (
|
||||||
|
"asset"."isFavorite" = $4
|
||||||
|
AND "asset"."isArchived" = $5
|
||||||
|
)
|
||||||
|
AND "asset"."id" > $6
|
||||||
|
)
|
||||||
|
AND ("asset"."deletedAt" IS NULL)
|
||||||
|
) "distinctAlias"
|
||||||
|
ORDER BY
|
||||||
|
"distinctAlias"."asset_id" ASC,
|
||||||
|
"asset_id" ASC
|
||||||
|
LIMIT
|
||||||
|
100
|
||||||
|
SELECT DISTINCT
|
||||||
|
"distinctAlias"."asset_id" AS "ids_asset_id",
|
||||||
|
"distinctAlias"."asset_id"
|
||||||
|
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"."status" AS "asset_status",
|
||||||
|
"asset"."originalPath" AS "asset_originalPath",
|
||||||
|
"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"."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",
|
||||||
|
"asset"."duplicateId" AS "asset_duplicateId",
|
||||||
|
"stack"."id" AS "stack_id",
|
||||||
|
"stack"."ownerId" AS "stack_ownerId",
|
||||||
|
"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"."status" AS "stackedAssets_status",
|
||||||
|
"stackedAssets"."originalPath" AS "stackedAssets_originalPath",
|
||||||
|
"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"."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",
|
||||||
|
"stackedAssets"."duplicateId" AS "stackedAssets_duplicateId"
|
||||||
|
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 1 = 1
|
||||||
|
AND "asset"."ownerId" IN ($3)
|
||||||
|
AND 1 = 1
|
||||||
|
AND (
|
||||||
|
"asset"."isFavorite" = $4
|
||||||
|
AND "asset"."isArchived" = $5
|
||||||
|
)
|
||||||
|
AND "asset"."id" < $6
|
||||||
|
)
|
||||||
|
AND ("asset"."deletedAt" IS NULL)
|
||||||
|
) "distinctAlias"
|
||||||
|
ORDER BY
|
||||||
|
"distinctAlias"."asset_id" ASC,
|
||||||
|
"asset_id" ASC
|
||||||
|
LIMIT
|
||||||
|
100
|
||||||
|
|
||||||
-- SearchRepository.searchSmart
|
-- SearchRepository.searchSmart
|
||||||
START TRANSACTION
|
START TRANSACTION
|
||||||
SET
|
SET
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { randomUUID } from 'node:crypto';
|
||||||
import { getVectorExtension } from 'src/database.config';
|
import { getVectorExtension } from 'src/database.config';
|
||||||
import { DummyValue, GenerateSql } from 'src/decorators';
|
import { DummyValue, GenerateSql } from 'src/decorators';
|
||||||
import { AssetFaceEntity } from 'src/entities/asset-face.entity';
|
import { AssetFaceEntity } from 'src/entities/asset-face.entity';
|
||||||
|
@ -63,22 +64,15 @@ export class SearchRepository implements ISearchRepository {
|
||||||
{
|
{
|
||||||
takenAfter: DummyValue.DATE,
|
takenAfter: DummyValue.DATE,
|
||||||
lensModel: DummyValue.STRING,
|
lensModel: DummyValue.STRING,
|
||||||
ownerId: DummyValue.UUID,
|
|
||||||
withStacked: true,
|
withStacked: true,
|
||||||
isFavorite: true,
|
isFavorite: true,
|
||||||
ownerIds: [DummyValue.UUID],
|
userIds: [DummyValue.UUID],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
async searchMetadata(pagination: SearchPaginationOptions, options: AssetSearchOptions): Paginated<AssetEntity> {
|
async searchMetadata(pagination: SearchPaginationOptions, options: AssetSearchOptions): Paginated<AssetEntity> {
|
||||||
let builder = this.assetRepository.createQueryBuilder('asset');
|
let builder = this.assetRepository.createQueryBuilder('asset');
|
||||||
builder = searchAssetBuilder(builder, options);
|
builder = searchAssetBuilder(builder, options).orderBy('asset.fileCreatedAt', options.orderDirection ?? 'DESC');
|
||||||
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, {
|
return paginatedBuilder<AssetEntity>(builder, {
|
||||||
mode: PaginationMode.SKIP_TAKE,
|
mode: PaginationMode.SKIP_TAKE,
|
||||||
|
@ -87,6 +81,35 @@ export class SearchRepository implements ISearchRepository {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GenerateSql({
|
||||||
|
params: [
|
||||||
|
100,
|
||||||
|
{
|
||||||
|
takenAfter: DummyValue.DATE,
|
||||||
|
lensModel: DummyValue.STRING,
|
||||||
|
withStacked: true,
|
||||||
|
isFavorite: true,
|
||||||
|
userIds: [DummyValue.UUID],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
async searchRandom(size: number, options: AssetSearchOptions): Promise<AssetEntity[]> {
|
||||||
|
const builder1 = searchAssetBuilder(this.assetRepository.createQueryBuilder('asset'), options);
|
||||||
|
const builder2 = builder1.clone();
|
||||||
|
|
||||||
|
const uuid = randomUUID();
|
||||||
|
builder1.andWhere('asset.id > :uuid', { uuid }).orderBy('asset.id').take(size);
|
||||||
|
builder2.andWhere('asset.id < :uuid', { uuid }).orderBy('asset.id').take(size);
|
||||||
|
|
||||||
|
const [assets1, assets2] = await Promise.all([builder1.getMany(), builder2.getMany()]);
|
||||||
|
const missingCount = size - assets1.length;
|
||||||
|
for (let i = 0; i < missingCount && i < assets2.length; i++) {
|
||||||
|
assets1.push(assets2[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return assets1;
|
||||||
|
}
|
||||||
|
|
||||||
private createPersonFilter(builder: SelectQueryBuilder<AssetFaceEntity>, personIds: string[]) {
|
private createPersonFilter(builder: SelectQueryBuilder<AssetFaceEntity>, personIds: string[]) {
|
||||||
return builder
|
return builder
|
||||||
.select(`${builder.alias}."assetId"`)
|
.select(`${builder.alias}."assetId"`)
|
||||||
|
|
|
@ -94,20 +94,10 @@ export class SearchService {
|
||||||
return this.mapResponse(items, hasNextPage ? (page + 1).toString() : null, { auth });
|
return this.mapResponse(items, hasNextPage ? (page + 1).toString() : null, { auth });
|
||||||
}
|
}
|
||||||
|
|
||||||
async searchRandom(auth: AuthDto, dto: RandomSearchDto): Promise<SearchResponseDto> {
|
async searchRandom(auth: AuthDto, dto: RandomSearchDto): Promise<AssetResponseDto[]> {
|
||||||
const userIds = await this.getUserIdsToSearch(auth);
|
const userIds = await this.getUserIdsToSearch(auth);
|
||||||
const page = dto.page ?? 1;
|
const items = await this.searchRepository.searchRandom(dto.size || 250, { ...dto, userIds });
|
||||||
const size = dto.size || 250;
|
return items.map((item) => mapAsset(item, { auth }));
|
||||||
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> {
|
async searchSmart(auth: AuthDto, dto: SmartSearchDto): Promise<SearchResponseDto> {
|
||||||
|
|
|
@ -120,7 +120,7 @@ export function searchAssetBuilder(
|
||||||
}
|
}
|
||||||
|
|
||||||
if (withPeople) {
|
if (withPeople) {
|
||||||
builder.leftJoinAndSelect(`${builder.alias}.person`, 'person');
|
builder.leftJoinAndSelect('faces.person', 'person');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (withSmartInfo) {
|
if (withSmartInfo) {
|
||||||
|
|
|
@ -7,6 +7,7 @@ export const newSearchRepositoryMock = (): Mocked<ISearchRepository> => {
|
||||||
searchSmart: vitest.fn(),
|
searchSmart: vitest.fn(),
|
||||||
searchDuplicates: vitest.fn(),
|
searchDuplicates: vitest.fn(),
|
||||||
searchFaces: vitest.fn(),
|
searchFaces: vitest.fn(),
|
||||||
|
searchRandom: vitest.fn(),
|
||||||
upsert: vitest.fn(),
|
upsert: vitest.fn(),
|
||||||
searchPlaces: vitest.fn(),
|
searchPlaces: vitest.fn(),
|
||||||
getAssetsByCity: vitest.fn(),
|
getAssetsByCity: vitest.fn(),
|
||||||
|
|
Loading…
Reference in a new issue