From 72e2a18f03e7ad84c60abe0a8610d3c8ff4e4e30 Mon Sep 17 00:00:00 2001 From: timonrieger Date: Fri, 24 Jul 2026 00:30:37 +0200 Subject: [PATCH] page defaults --- server/src/dtos/search.dto.ts | 5 ++- server/src/services/search.service.spec.ts | 36 ++++++++++------- server/src/services/search.service.ts | 14 +++---- .../specs/services/search.service.spec.ts | 39 ++++++++++++------- 4 files changed, 56 insertions(+), 38 deletions(-) diff --git a/server/src/dtos/search.dto.ts b/server/src/dtos/search.dto.ts index 6cf12234ad..6e73a6e64f 100644 --- a/server/src/dtos/search.dto.ts +++ b/server/src/dtos/search.dto.ts @@ -70,12 +70,12 @@ const BaseSearchSchema = z.object({ const BaseSearchWithResultsSchema = BaseSearchSchema.extend({ withDeleted: z.boolean().optional().describe('Include deleted assets').meta(DEPRECATED_FLAT_FIELD), withExif: z.boolean().optional().describe('Include EXIF data in response'), - size: z.int().min(1).max(1000).optional().describe('Number of results to return'), + size: z.int().min(1).max(1000).default(250).describe('Number of results to return'), }); const LargeAssetSearchSchema = BaseSearchWithResultsSchema.extend({ minFileSize: z.coerce.number().int().min(0).optional().describe('Minimum file size in bytes'), - size: z.coerce.number().int().min(1).max(1000).optional().describe('Number of results to return'), + size: z.coerce.number().int().min(1).max(1000).default(250).describe('Number of results to return'), }).meta({ id: 'LargeAssetSearchDto' }); const SearchPlacesSchema = z @@ -372,6 +372,7 @@ const StatisticsSearchSchema = withShapeExclusivity( const SmartSearchSchema = withShapeExclusivity( BaseSearchWithResultsSchema.extend({ + size: z.int().min(1).max(1000).default(100).describe('Number of results to return'), query: z.string().trim().optional().describe('Natural language search query'), queryAssetId: z.uuidv4().optional().describe('Asset ID to use as search reference'), language: z.string().optional().describe('Search language code'), diff --git a/server/src/services/search.service.spec.ts b/server/src/services/search.service.spec.ts index 2ccdc1b80b..9dfe3cfb72 100644 --- a/server/src/services/search.service.spec.ts +++ b/server/src/services/search.service.spec.ts @@ -222,12 +222,12 @@ describe(SearchService.name, () => { const auth = AuthFactory.create(); mocks.search.searchMetadataV3.mockResolvedValue({ hasNextPage: false, items: [] }); - await sut.searchMetadata(auth, { filter: {} }); + await sut.searchMetadata(auth, { size: 250, filter: {} }); expect(mocks.search.searchMetadataV3).toHaveBeenCalled(); expect(mocks.search.searchMetadata).not.toHaveBeenCalled(); mocks.search.searchMetadata.mockResolvedValue({ hasNextPage: false, items: [] }); - await sut.searchMetadata(auth, { city: 'Oslo' }); + await sut.searchMetadata(auth, { size: 250, city: 'Oslo' }); expect(mocks.search.searchMetadata).toHaveBeenCalled(); }); @@ -238,11 +238,11 @@ describe(SearchService.name, () => { await expect(sut.searchStatistics(auth, { filter: {} })).resolves.toEqual({ total: 0 }); mocks.search.searchRandomV3.mockResolvedValue([]); - await expect(sut.searchRandom(auth, { filter: {} })).resolves.toEqual([]); + await expect(sut.searchRandom(auth, { size: 250, filter: {} })).resolves.toEqual([]); mocks.search.searchSmartV3.mockResolvedValue({ hasNextPage: false, items: [] }); mocks.machineLearning.encodeText.mockResolvedValue('[1, 2, 3]'); - await sut.searchSmart(auth, { filter: {}, query: 'test' }); + await sut.searchSmart(auth, { size: 100, filter: {}, query: 'test' }); expect(mocks.search.searchSmartV3).toHaveBeenCalledWith( { size: 100, offset: 0 }, expect.objectContaining({ embedding: '[1, 2, 3]' }), @@ -250,27 +250,29 @@ describe(SearchService.name, () => { }); it('should reject an invalid cursor', async () => { - await expect(sut.searchMetadata(AuthFactory.create(), { cursor: '???' })).rejects.toThrowError( + await expect(sut.searchMetadata(AuthFactory.create(), { size: 250, cursor: '???' })).rejects.toThrowError( new BadRequestException('Invalid cursor'), ); }); it('should reject an unelevated session whose filter could match locked assets', async () => { const filter = { visibility: { in: [AssetVisibility.Locked, AssetVisibility.Timeline] } }; - await expect(sut.searchMetadata(AuthFactory.create(), { filter })).rejects.toBeInstanceOf(UnauthorizedException); + await expect(sut.searchMetadata(AuthFactory.create(), { size: 250, filter })).rejects.toBeInstanceOf( + UnauthorizedException, + ); }); it('should reject a shared link without a top-level album constraint', async () => { const auth = AuthFactory.from().sharedLink().build(); - await expect(sut.searchMetadata(auth, { filter: {} })).rejects.toThrowError( + await expect(sut.searchMetadata(auth, { size: 250, filter: {} })).rejects.toThrowError( new BadRequestException('Shared link access is only allowed in combination with an albumIds filter'), ); const albumId = newUuid(); mocks.access.album.checkSharedLinkAccess.mockResolvedValue(new Set([albumId])); await expect( - sut.searchMetadata(auth, { filter: { or: [{ albumIds: { any: [albumId] } }] } }), + sut.searchMetadata(auth, { size: 250, filter: { or: [{ albumIds: { any: [albumId] } }] } }), ).rejects.toThrowError( new BadRequestException('Shared link access is only allowed in combination with an albumIds filter'), ); @@ -288,7 +290,7 @@ describe(SearchService.name, () => { machineLearning: { enabled: false }, }); - await expect(sut.searchSmart(authStub.user1, { query: 'test' })).rejects.toThrowError( + await expect(sut.searchSmart(authStub.user1, { size: 100, query: 'test' })).rejects.toThrowError( new BadRequestException('Smart search is not enabled'), ); }); @@ -298,13 +300,13 @@ describe(SearchService.name, () => { machineLearning: { clip: { enabled: false } }, }); - await expect(sut.searchSmart(authStub.user1, { query: 'test' })).rejects.toThrowError( + await expect(sut.searchSmart(authStub.user1, { size: 100, query: 'test' })).rejects.toThrowError( new BadRequestException('Smart search is not enabled'), ); }); it('should work', async () => { - await sut.searchSmart(authStub.user1, { query: 'test' }); + await sut.searchSmart(authStub.user1, { size: 100, query: 'test' }); expect(mocks.machineLearning.encodeText).toHaveBeenCalledWith( 'test', @@ -312,7 +314,13 @@ describe(SearchService.name, () => { ); expect(mocks.search.searchSmart).toHaveBeenCalledWith( { page: 1, size: 100 }, - { query: 'test', embedding: '[1, 2, 3]', userIds: [authStub.user1.user.id], visibility: 'not-locked' }, + { + query: 'test', + size: 100, + embedding: '[1, 2, 3]', + userIds: [authStub.user1.user.id], + visibility: 'not-locked', + }, ); }); @@ -334,7 +342,7 @@ describe(SearchService.name, () => { machineLearning: { clip: { modelName: 'ViT-B-16-SigLIP__webli' } }, }); - await sut.searchSmart(authStub.user1, { query: 'test' }); + await sut.searchSmart(authStub.user1, { size: 100, query: 'test' }); expect(mocks.machineLearning.encodeText).toHaveBeenCalledWith( 'test', @@ -343,7 +351,7 @@ describe(SearchService.name, () => { }); it('should use language specified in request', async () => { - await sut.searchSmart(authStub.user1, { query: 'test', language: 'de' }); + await sut.searchSmart(authStub.user1, { size: 100, query: 'test', language: 'de' }); expect(mocks.machineLearning.encodeText).toHaveBeenCalledWith( 'test', diff --git a/server/src/services/search.service.ts b/server/src/services/search.service.ts index ef96e5ce70..736e485feb 100644 --- a/server/src/services/search.service.ts +++ b/server/src/services/search.service.ts @@ -98,7 +98,7 @@ export class SearchService extends BaseService { } const page = dto.page ?? 1; - const size = dto.size || 250; + const size = dto.size; const { hasNextPage, items } = await this.searchRepository.searchMetadata( { page, size }, { @@ -140,7 +140,7 @@ export class SearchService extends BaseService { } const userIds = await this.getUserIdsToSearch(auth, dto.visibility); - const items = await this.searchRepository.searchRandom(dto.size || 250, { + const items = await this.searchRepository.searchRandom(dto.size, { ...dto, visibility: dto.visibility ?? (auth.session?.hasElevatedPermission ? undefined : 'not-locked'), userIds, @@ -154,7 +154,7 @@ export class SearchService extends BaseService { } const userIds = await this.getUserIdsToSearch(auth, dto.visibility); - const items = await this.searchRepository.searchLargeAssets(dto.size || 250, { + const items = await this.searchRepository.searchLargeAssets(dto.size, { ...dto, visibility: dto.visibility ?? (auth.session?.hasElevatedPermission ? undefined : 'not-locked'), userIds, @@ -179,7 +179,7 @@ export class SearchService extends BaseService { const userIds = this.getUserIdsToSearch(auth, dto.visibility); const embedding = await this.resolveEmbedding(auth, dto, machineLearning); const page = dto.page ?? 1; - const size = dto.size || 100; + const size = dto.size; const { hasNextPage, items } = await this.searchRepository.searchSmart( { page, size }, { @@ -252,7 +252,7 @@ export class SearchService extends BaseService { ]); const { offset } = decodeSearchCursor(dto.cursor); - const size = dto.size || 250; + const size = dto.size; const { hasNextPage, items } = await this.searchRepository.searchMetadataV3( { size, offset }, { @@ -275,7 +275,7 @@ export class SearchService extends BaseService { private async searchRandomV3(auth: AuthDto, dto: RandomSearchDto): Promise { const { effectiveFilter, userIds } = await this.resolveSearchScopeV3(auth, dto); - const items = await this.searchRepository.searchRandomV3(dto.size || 250, { + const items = await this.searchRepository.searchRandomV3(dto.size, { filter: effectiveFilter, userIds, withExif: dto.withExif, @@ -297,7 +297,7 @@ export class SearchService extends BaseService { ]); const { offset } = decodeSearchCursor(dto.cursor); - const size = dto.size || 100; + const size = dto.size; const { hasNextPage, items } = await this.searchRepository.searchSmartV3( { size, offset }, { filter: effectiveFilter, userIds, withExif: dto.withExif, embedding }, diff --git a/server/test/medium/specs/services/search.service.spec.ts b/server/test/medium/specs/services/search.service.spec.ts index d21294d906..6d14a5d225 100644 --- a/server/test/medium/specs/services/search.service.spec.ts +++ b/server/test/medium/specs/services/search.service.spec.ts @@ -58,7 +58,7 @@ describe(SearchService.name, () => { const auth = factory.auth({ user: { id: user.id } }); - await expect(sut.searchLargeAssets(auth, {})).resolves.toEqual([ + await expect(sut.searchLargeAssets(auth, { size: 250 })).resolves.toEqual([ expect.objectContaining({ id: assets[2].id }), expect.objectContaining({ id: assets[0].id }), expect.objectContaining({ id: assets[1].id }), @@ -122,7 +122,7 @@ describe(SearchService.name, () => { const auth = factory.auth({ user: { id: user.id } }); - const response = await sut.searchMetadata(auth, { withStacked: false }); + const response = await sut.searchMetadata(auth, { size: 250, withStacked: false }); expect(response.assets.items.length).toBe(1); expect(response.assets.items[0].id).toBe(unstackedAsset.id); @@ -137,7 +137,7 @@ describe(SearchService.name, () => { const auth = factory.auth({ user: { id: user.id } }); - const response = await sut.searchMetadata(auth, { withStacked: false }); + const response = await sut.searchMetadata(auth, { size: 250, withStacked: false }); expect(response.assets.items.length).toBe(0); }); @@ -150,7 +150,7 @@ describe(SearchService.name, () => { const auth = factory.auth({ user: { id: user.id }, session: { hasElevatedPermission: true } }); - const response = await sut.searchMetadata(auth, { withStacked: false }); + const response = await sut.searchMetadata(auth, { size: 250, withStacked: false }); expect(response.assets.items.length).toBe(1); }); @@ -170,7 +170,7 @@ describe(SearchService.name, () => { const auth = factory.auth({ user: { id: user.id } }); - const response = await sut.searchMetadata(auth, { albumIds: [album.id] }); + const response = await sut.searchMetadata(auth, { size: 250, albumIds: [album.id] }); expect(response.assets.items.length).toBe(1); }); @@ -188,7 +188,7 @@ describe(SearchService.name, () => { const auth = factory.auth({ user: { id: user.id } }); - await expect(sut.searchMetadata(auth, { albumIds: [album.id] })).rejects.toThrow( + await expect(sut.searchMetadata(auth, { size: 250, albumIds: [album.id] })).rejects.toThrow( 'Not found or no album.read access', ); }); @@ -224,7 +224,7 @@ describe(SearchService.name, () => { const auth = factory.auth({ user: { id: user.id } }); - const response = await sut.searchRandom(auth, {}); + const response = await sut.searchRandom(auth, { size: 250 }); expect(response.length).toBe(0); }); @@ -240,7 +240,7 @@ describe(SearchService.name, () => { await ctx.newExif({ assetId: other.id, city: 'Bergen' }); const auth = factory.auth({ user: { id: user.id } }); - const response = await sut.searchMetadata(auth, { filter: { city: { eq: 'Oslo' } } }); + const response = await sut.searchMetadata(auth, { size: 250, filter: { city: { eq: 'Oslo' } } }); expect(response.assets.items).toEqual([expect.objectContaining({ id: asset.id })]); expect(response.assets.nextPage).toBeNull(); @@ -257,6 +257,7 @@ describe(SearchService.name, () => { const auth = factory.auth({ user: { id: user.id } }); const response = await sut.searchMetadata(auth, { + size: 250, filter: { or: [{ city: { eq: 'Oslo' } }, { isFavorite: { eq: true } }] }, }); @@ -273,10 +274,13 @@ describe(SearchService.name, () => { const auth = factory.auth({ user: { id: user.id } }); - const topLevel = await sut.searchMetadata(auth, { filter: { albumIds: { any: [album.id] } } }); + const topLevel = await sut.searchMetadata(auth, { size: 250, filter: { albumIds: { any: [album.id] } } }); expect(topLevel.assets.items).toEqual([expect.objectContaining({ id: asset.id })]); - const branchOnly = await sut.searchMetadata(auth, { filter: { or: [{ albumIds: { any: [album.id] } }] } }); + const branchOnly = await sut.searchMetadata(auth, { + size: 250, + filter: { or: [{ albumIds: { any: [album.id] } }] }, + }); expect(branchOnly.assets.items).toEqual([]); }); @@ -288,9 +292,9 @@ describe(SearchService.name, () => { const auth = factory.auth({ user: { id: user.id } }); - await expect(sut.searchMetadata(auth, { filter: { or: [{ albumIds: { none: [album.id] } }] } })).rejects.toThrow( - 'Not found or no album.read access', - ); + await expect( + sut.searchMetadata(auth, { size: 250, filter: { or: [{ albumIds: { none: [album.id] } }] } }), + ).rejects.toThrow('Not found or no album.read access'); }); it('should return locked assets only to an elevated session that asks for them', async () => { @@ -299,11 +303,12 @@ describe(SearchService.name, () => { const { asset: timeline } = await ctx.newAsset({ ownerId: user.id }); const { asset: locked } = await ctx.newAsset({ ownerId: user.id, visibility: AssetVisibility.Locked }); - const unelevated = await sut.searchMetadata(factory.auth({ user: { id: user.id } }), { filter: {} }); + const unelevated = await sut.searchMetadata(factory.auth({ user: { id: user.id } }), { size: 250, filter: {} }); expect(unelevated.assets.items).toEqual([expect.objectContaining({ id: timeline.id })]); const elevatedAuth = factory.auth({ user: { id: user.id }, session: { hasElevatedPermission: true } }); const elevated = await sut.searchMetadata(elevatedAuth, { + size: 250, filter: { visibility: { eq: AssetVisibility.Locked } }, }); expect(elevated.assets.items).toEqual([expect.objectContaining({ id: locked.id })]); @@ -318,7 +323,10 @@ describe(SearchService.name, () => { await ctx.newAsset({ ownerId: partner.id, visibility: AssetVisibility.Locked }); const auth = factory.auth({ user: { id: user.id }, session: { hasElevatedPermission: true } }); - const response = await sut.searchMetadata(auth, { filter: { visibility: { eq: AssetVisibility.Locked } } }); + const response = await sut.searchMetadata(auth, { + size: 250, + filter: { visibility: { eq: AssetVisibility.Locked } }, + }); expect(response.assets.items).toEqual([expect.objectContaining({ id: ownLocked.id })]); }); @@ -358,6 +366,7 @@ describe(SearchService.name, () => { const auth = factory.auth({ user: { id: user.id } }); const response = await sut.searchMetadata(auth, { + size: 250, orderBy: { field: SearchOrderField.FileSizeInBytes, direction: AssetOrder.Asc }, });