Skip to content

Commit 7adb35e

Browse files
authored
fix(server): /search/random failing with certain options (#13040)
* fix relation handling, remove pagination * update api, sql * update mock
1 parent 2f13db5 commit 7adb35e

File tree

12 files changed

+250
-62
lines changed

12 files changed

+250
-62
lines changed

Diff for: mobile/openapi/lib/api/search_api.dart

+6-3
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Diff for: mobile/openapi/lib/model/random_search_dto.dart

+1-19
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Diff for: open-api/immich-openapi-specs.json

+4-5
Original file line numberDiff line numberDiff line change
@@ -4615,7 +4615,10 @@
46154615
"content": {
46164616
"application/json": {
46174617
"schema": {
4618-
"$ref": "#/components/schemas/SearchResponseDto"
4618+
"items": {
4619+
"$ref": "#/components/schemas/AssetResponseDto"
4620+
},
4621+
"type": "array"
46194622
}
46204623
}
46214624
},
@@ -10463,10 +10466,6 @@
1046310466
"nullable": true,
1046410467
"type": "string"
1046510468
},
10466-
"page": {
10467-
"minimum": 1,
10468-
"type": "number"
10469-
},
1047010469
"personIds": {
1047110470
"items": {
1047210471
"format": "uuid",

Diff for: open-api/typescript-sdk/src/fetch-client.ts

+1-2
Original file line numberDiff line numberDiff line change
@@ -852,7 +852,6 @@ export type RandomSearchDto = {
852852
libraryId?: string | null;
853853
make?: string;
854854
model?: string | null;
855-
page?: number;
856855
personIds?: string[];
857856
size?: number;
858857
state?: string | null;
@@ -2523,7 +2522,7 @@ export function searchRandom({ randomSearchDto }: {
25232522
}, opts?: Oazapfts.RequestOpts) {
25242523
return oazapfts.ok(oazapfts.fetchJson<{
25252524
status: 200;
2526-
data: SearchResponseDto;
2525+
data: AssetResponseDto[];
25272526
}>("/search/random", oazapfts.json({
25282527
...opts,
25292528
method: "POST",

Diff for: server/src/controllers/search.controller.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ export class SearchController {
3232
@Post('random')
3333
@HttpCode(HttpStatus.OK)
3434
@Authenticated()
35-
searchRandom(@Auth() auth: AuthDto, @Body() dto: RandomSearchDto): Promise<SearchResponseDto> {
35+
searchRandom(@Auth() auth: AuthDto, @Body() dto: RandomSearchDto): Promise<AssetResponseDto[]> {
3636
return this.service.searchRandom(auth, dto);
3737
}
3838

Diff for: server/src/dtos/search.dto.ts

+12-6
Original file line numberDiff line numberDiff line change
@@ -99,12 +99,6 @@ class BaseSearchDto {
9999
@Optional({ nullable: true, emptyToNull: true })
100100
lensModel?: string | null;
101101

102-
@IsInt()
103-
@Min(1)
104-
@Type(() => Number)
105-
@Optional()
106-
page?: number;
107-
108102
@IsInt()
109103
@Min(1)
110104
@Max(1000)
@@ -170,12 +164,24 @@ export class MetadataSearchDto extends RandomSearchDto {
170164
@Optional()
171165
@ApiProperty({ enumName: 'AssetOrder', enum: AssetOrder })
172166
order?: AssetOrder;
167+
168+
@IsInt()
169+
@Min(1)
170+
@Type(() => Number)
171+
@Optional()
172+
page?: number;
173173
}
174174

175175
export class SmartSearchDto extends BaseSearchDto {
176176
@IsString()
177177
@IsNotEmpty()
178178
query!: string;
179+
180+
@IsInt()
181+
@Min(1)
182+
@Type(() => Number)
183+
@Optional()
184+
page?: number;
179185
}
180186

181187
export class SearchPlacesDto {

Diff for: server/src/interfaces/search.interface.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,6 @@ export interface SearchPeopleOptions {
116116

117117
export interface SearchOrderOptions {
118118
orderDirection?: 'ASC' | 'DESC';
119-
random?: boolean;
120119
}
121120

122121
export interface SearchPaginationOptions {
@@ -177,6 +176,7 @@ export interface ISearchRepository {
177176
searchSmart(pagination: SearchPaginationOptions, options: SmartSearchOptions): Paginated<AssetEntity>;
178177
searchDuplicates(options: AssetDuplicateSearch): Promise<AssetDuplicateResult[]>;
179178
searchFaces(search: FaceEmbeddingSearch): Promise<FaceSearchResult[]>;
179+
searchRandom(size: number, options: AssetSearchOptions): Promise<AssetEntity[]>;
180180
upsert(assetId: string, embedding: number[]): Promise<void>;
181181
searchPlaces(placeName: string): Promise<GeodataPlacesEntity[]>;
182182
getAssetsByCity(userIds: string[]): Promise<AssetEntity[]>;

Diff for: server/src/queries/search.repository.sql

+187-2
Original file line numberDiff line numberDiff line change
@@ -77,10 +77,11 @@ FROM
7777
"asset"."fileCreatedAt" >= $1
7878
AND "exifInfo"."lensModel" = $2
7979
AND 1 = 1
80+
AND "asset"."ownerId" IN ($3)
8081
AND 1 = 1
8182
AND (
82-
"asset"."isFavorite" = $3
83-
AND "asset"."isArchived" = $4
83+
"asset"."isFavorite" = $4
84+
AND "asset"."isArchived" = $5
8485
)
8586
)
8687
AND ("asset"."deletedAt" IS NULL)
@@ -91,6 +92,190 @@ ORDER BY
9192
LIMIT
9293
101
9394

95+
-- SearchRepository.searchRandom
96+
SELECT DISTINCT
97+
"distinctAlias"."asset_id" AS "ids_asset_id",
98+
"distinctAlias"."asset_id"
99+
FROM
100+
(
101+
SELECT
102+
"asset"."id" AS "asset_id",
103+
"asset"."deviceAssetId" AS "asset_deviceAssetId",
104+
"asset"."ownerId" AS "asset_ownerId",
105+
"asset"."libraryId" AS "asset_libraryId",
106+
"asset"."deviceId" AS "asset_deviceId",
107+
"asset"."type" AS "asset_type",
108+
"asset"."status" AS "asset_status",
109+
"asset"."originalPath" AS "asset_originalPath",
110+
"asset"."thumbhash" AS "asset_thumbhash",
111+
"asset"."encodedVideoPath" AS "asset_encodedVideoPath",
112+
"asset"."createdAt" AS "asset_createdAt",
113+
"asset"."updatedAt" AS "asset_updatedAt",
114+
"asset"."deletedAt" AS "asset_deletedAt",
115+
"asset"."fileCreatedAt" AS "asset_fileCreatedAt",
116+
"asset"."localDateTime" AS "asset_localDateTime",
117+
"asset"."fileModifiedAt" AS "asset_fileModifiedAt",
118+
"asset"."isFavorite" AS "asset_isFavorite",
119+
"asset"."isArchived" AS "asset_isArchived",
120+
"asset"."isExternal" AS "asset_isExternal",
121+
"asset"."isOffline" AS "asset_isOffline",
122+
"asset"."checksum" AS "asset_checksum",
123+
"asset"."duration" AS "asset_duration",
124+
"asset"."isVisible" AS "asset_isVisible",
125+
"asset"."livePhotoVideoId" AS "asset_livePhotoVideoId",
126+
"asset"."originalFileName" AS "asset_originalFileName",
127+
"asset"."sidecarPath" AS "asset_sidecarPath",
128+
"asset"."stackId" AS "asset_stackId",
129+
"asset"."duplicateId" AS "asset_duplicateId",
130+
"stack"."id" AS "stack_id",
131+
"stack"."ownerId" AS "stack_ownerId",
132+
"stack"."primaryAssetId" AS "stack_primaryAssetId",
133+
"stackedAssets"."id" AS "stackedAssets_id",
134+
"stackedAssets"."deviceAssetId" AS "stackedAssets_deviceAssetId",
135+
"stackedAssets"."ownerId" AS "stackedAssets_ownerId",
136+
"stackedAssets"."libraryId" AS "stackedAssets_libraryId",
137+
"stackedAssets"."deviceId" AS "stackedAssets_deviceId",
138+
"stackedAssets"."type" AS "stackedAssets_type",
139+
"stackedAssets"."status" AS "stackedAssets_status",
140+
"stackedAssets"."originalPath" AS "stackedAssets_originalPath",
141+
"stackedAssets"."thumbhash" AS "stackedAssets_thumbhash",
142+
"stackedAssets"."encodedVideoPath" AS "stackedAssets_encodedVideoPath",
143+
"stackedAssets"."createdAt" AS "stackedAssets_createdAt",
144+
"stackedAssets"."updatedAt" AS "stackedAssets_updatedAt",
145+
"stackedAssets"."deletedAt" AS "stackedAssets_deletedAt",
146+
"stackedAssets"."fileCreatedAt" AS "stackedAssets_fileCreatedAt",
147+
"stackedAssets"."localDateTime" AS "stackedAssets_localDateTime",
148+
"stackedAssets"."fileModifiedAt" AS "stackedAssets_fileModifiedAt",
149+
"stackedAssets"."isFavorite" AS "stackedAssets_isFavorite",
150+
"stackedAssets"."isArchived" AS "stackedAssets_isArchived",
151+
"stackedAssets"."isExternal" AS "stackedAssets_isExternal",
152+
"stackedAssets"."isOffline" AS "stackedAssets_isOffline",
153+
"stackedAssets"."checksum" AS "stackedAssets_checksum",
154+
"stackedAssets"."duration" AS "stackedAssets_duration",
155+
"stackedAssets"."isVisible" AS "stackedAssets_isVisible",
156+
"stackedAssets"."livePhotoVideoId" AS "stackedAssets_livePhotoVideoId",
157+
"stackedAssets"."originalFileName" AS "stackedAssets_originalFileName",
158+
"stackedAssets"."sidecarPath" AS "stackedAssets_sidecarPath",
159+
"stackedAssets"."stackId" AS "stackedAssets_stackId",
160+
"stackedAssets"."duplicateId" AS "stackedAssets_duplicateId"
161+
FROM
162+
"assets" "asset"
163+
LEFT JOIN "exif" "exifInfo" ON "exifInfo"."assetId" = "asset"."id"
164+
LEFT JOIN "asset_stack" "stack" ON "stack"."id" = "asset"."stackId"
165+
LEFT JOIN "assets" "stackedAssets" ON "stackedAssets"."stackId" = "stack"."id"
166+
AND ("stackedAssets"."deletedAt" IS NULL)
167+
WHERE
168+
(
169+
"asset"."fileCreatedAt" >= $1
170+
AND "exifInfo"."lensModel" = $2
171+
AND 1 = 1
172+
AND "asset"."ownerId" IN ($3)
173+
AND 1 = 1
174+
AND (
175+
"asset"."isFavorite" = $4
176+
AND "asset"."isArchived" = $5
177+
)
178+
AND "asset"."id" > $6
179+
)
180+
AND ("asset"."deletedAt" IS NULL)
181+
) "distinctAlias"
182+
ORDER BY
183+
"distinctAlias"."asset_id" ASC,
184+
"asset_id" ASC
185+
LIMIT
186+
100
187+
SELECT DISTINCT
188+
"distinctAlias"."asset_id" AS "ids_asset_id",
189+
"distinctAlias"."asset_id"
190+
FROM
191+
(
192+
SELECT
193+
"asset"."id" AS "asset_id",
194+
"asset"."deviceAssetId" AS "asset_deviceAssetId",
195+
"asset"."ownerId" AS "asset_ownerId",
196+
"asset"."libraryId" AS "asset_libraryId",
197+
"asset"."deviceId" AS "asset_deviceId",
198+
"asset"."type" AS "asset_type",
199+
"asset"."status" AS "asset_status",
200+
"asset"."originalPath" AS "asset_originalPath",
201+
"asset"."thumbhash" AS "asset_thumbhash",
202+
"asset"."encodedVideoPath" AS "asset_encodedVideoPath",
203+
"asset"."createdAt" AS "asset_createdAt",
204+
"asset"."updatedAt" AS "asset_updatedAt",
205+
"asset"."deletedAt" AS "asset_deletedAt",
206+
"asset"."fileCreatedAt" AS "asset_fileCreatedAt",
207+
"asset"."localDateTime" AS "asset_localDateTime",
208+
"asset"."fileModifiedAt" AS "asset_fileModifiedAt",
209+
"asset"."isFavorite" AS "asset_isFavorite",
210+
"asset"."isArchived" AS "asset_isArchived",
211+
"asset"."isExternal" AS "asset_isExternal",
212+
"asset"."isOffline" AS "asset_isOffline",
213+
"asset"."checksum" AS "asset_checksum",
214+
"asset"."duration" AS "asset_duration",
215+
"asset"."isVisible" AS "asset_isVisible",
216+
"asset"."livePhotoVideoId" AS "asset_livePhotoVideoId",
217+
"asset"."originalFileName" AS "asset_originalFileName",
218+
"asset"."sidecarPath" AS "asset_sidecarPath",
219+
"asset"."stackId" AS "asset_stackId",
220+
"asset"."duplicateId" AS "asset_duplicateId",
221+
"stack"."id" AS "stack_id",
222+
"stack"."ownerId" AS "stack_ownerId",
223+
"stack"."primaryAssetId" AS "stack_primaryAssetId",
224+
"stackedAssets"."id" AS "stackedAssets_id",
225+
"stackedAssets"."deviceAssetId" AS "stackedAssets_deviceAssetId",
226+
"stackedAssets"."ownerId" AS "stackedAssets_ownerId",
227+
"stackedAssets"."libraryId" AS "stackedAssets_libraryId",
228+
"stackedAssets"."deviceId" AS "stackedAssets_deviceId",
229+
"stackedAssets"."type" AS "stackedAssets_type",
230+
"stackedAssets"."status" AS "stackedAssets_status",
231+
"stackedAssets"."originalPath" AS "stackedAssets_originalPath",
232+
"stackedAssets"."thumbhash" AS "stackedAssets_thumbhash",
233+
"stackedAssets"."encodedVideoPath" AS "stackedAssets_encodedVideoPath",
234+
"stackedAssets"."createdAt" AS "stackedAssets_createdAt",
235+
"stackedAssets"."updatedAt" AS "stackedAssets_updatedAt",
236+
"stackedAssets"."deletedAt" AS "stackedAssets_deletedAt",
237+
"stackedAssets"."fileCreatedAt" AS "stackedAssets_fileCreatedAt",
238+
"stackedAssets"."localDateTime" AS "stackedAssets_localDateTime",
239+
"stackedAssets"."fileModifiedAt" AS "stackedAssets_fileModifiedAt",
240+
"stackedAssets"."isFavorite" AS "stackedAssets_isFavorite",
241+
"stackedAssets"."isArchived" AS "stackedAssets_isArchived",
242+
"stackedAssets"."isExternal" AS "stackedAssets_isExternal",
243+
"stackedAssets"."isOffline" AS "stackedAssets_isOffline",
244+
"stackedAssets"."checksum" AS "stackedAssets_checksum",
245+
"stackedAssets"."duration" AS "stackedAssets_duration",
246+
"stackedAssets"."isVisible" AS "stackedAssets_isVisible",
247+
"stackedAssets"."livePhotoVideoId" AS "stackedAssets_livePhotoVideoId",
248+
"stackedAssets"."originalFileName" AS "stackedAssets_originalFileName",
249+
"stackedAssets"."sidecarPath" AS "stackedAssets_sidecarPath",
250+
"stackedAssets"."stackId" AS "stackedAssets_stackId",
251+
"stackedAssets"."duplicateId" AS "stackedAssets_duplicateId"
252+
FROM
253+
"assets" "asset"
254+
LEFT JOIN "exif" "exifInfo" ON "exifInfo"."assetId" = "asset"."id"
255+
LEFT JOIN "asset_stack" "stack" ON "stack"."id" = "asset"."stackId"
256+
LEFT JOIN "assets" "stackedAssets" ON "stackedAssets"."stackId" = "stack"."id"
257+
AND ("stackedAssets"."deletedAt" IS NULL)
258+
WHERE
259+
(
260+
"asset"."fileCreatedAt" >= $1
261+
AND "exifInfo"."lensModel" = $2
262+
AND 1 = 1
263+
AND "asset"."ownerId" IN ($3)
264+
AND 1 = 1
265+
AND (
266+
"asset"."isFavorite" = $4
267+
AND "asset"."isArchived" = $5
268+
)
269+
AND "asset"."id" < $6
270+
)
271+
AND ("asset"."deletedAt" IS NULL)
272+
) "distinctAlias"
273+
ORDER BY
274+
"distinctAlias"."asset_id" ASC,
275+
"asset_id" ASC
276+
LIMIT
277+
100
278+
94279
-- SearchRepository.searchSmart
95280
START TRANSACTION
96281
SET

0 commit comments

Comments
 (0)