reuse pagination options

This commit is contained in:
timonrieger 2026-07-25 18:47:17 +02:00
parent 34c25b43fc
commit 8981d5ac29
No known key found for this signature in database
5 changed files with 40 additions and 45 deletions

View file

@ -20,7 +20,7 @@ import {
withExifInner,
withSearchOrder,
} from 'src/utils/database';
import { paginationHelper } from 'src/utils/pagination';
import { paginationHelper, PaginationOptions } from 'src/utils/pagination';
import z from 'zod';
export interface SearchAssetIdOptions {
@ -149,11 +149,6 @@ export interface AssetSearchBuilderV3Options {
order?: SearchOrder;
}
export interface AssetSearchPaginationV3Options {
size: number;
offset?: number;
}
export type SmartSearchOptions = SearchDateOptions &
SearchEmbeddingOptions &
SearchExifOptions &
@ -532,16 +527,16 @@ export class SearchRepository {
// TODO(v4): drop the V3 suffix once the legacy methods are removed
@GenerateSql(...searchMetadataV3Examples)
async searchMetadataV3(
pagination: AssetSearchPaginationV3Options,
pagination: PaginationOptions,
options: AssetSearchBuilderV3Options,
scope: AssetSearchScope,
) {
const items = await withSearchOrder(searchAssetBuilder(this.db, options, scope), options.order)
.select(columns.searchAsset)
.limit(pagination.size + 1)
.offset(pagination.offset ?? 0)
.limit(pagination.take + 1)
.offset(pagination.skip ?? 0)
.execute();
return paginationHelper(items, pagination.size);
return paginationHelper(items, pagination.take);
}
// TODO(v4): drop the V3 suffix once the legacy methods are removed
@ -561,7 +556,7 @@ export class SearchRepository {
// TODO(v4): drop the V3 suffix once the legacy methods are removed
@GenerateSql(...searchSmartV3Examples)
searchSmartV3(
pagination: AssetSearchPaginationV3Options,
pagination: PaginationOptions,
options: Omit<AssetSearchBuilderV3Options, 'order'> & { embedding: string },
scope: AssetSearchScope,
) {
@ -572,10 +567,10 @@ export class SearchRepository {
.innerJoin('smart_search', 'asset.id', 'smart_search.assetId')
.orderBy(sql`smart_search.embedding <=> ${options.embedding}`)
.orderBy('asset.id', 'asc')
.limit(pagination.size + 1)
.offset(pagination.offset ?? 0)
.limit(pagination.take + 1)
.offset(pagination.skip ?? 0)
.execute();
return paginationHelper(items, pagination.size);
return paginationHelper(items, pagination.take);
});
}

View file

@ -244,7 +244,7 @@ describe(SearchService.name, () => {
mocks.machineLearning.encodeText.mockResolvedValue('[1, 2, 3]');
await sut.searchSmart(auth, { size: 100, filter: {}, query: 'test' });
expect(mocks.search.searchSmartV3).toHaveBeenCalledWith(
{ size: 100 },
{ take: 100 },
expect.objectContaining({ embedding: '[1, 2, 3]' }),
expect.objectContaining({ lockedOwnerId: expect.any(String) }),
);

View file

@ -240,7 +240,7 @@ export class SearchService extends BaseService {
const { offset } = decodeSearchCursor(dto.cursor);
const size = dto.size;
const { hasNextPage, items } = await this.searchRepository.searchMetadataV3(
{ size, offset },
{ take: size, skip: offset },
{
filter,
withExif: dto.withExif,
@ -287,7 +287,7 @@ export class SearchService extends BaseService {
// no cursor until a rank-aware pagination strategy for smart search is decided
const { items } = await this.searchRepository.searchSmartV3(
{ size: dto.size },
{ take: dto.size },
{ filter, withExif: dto.withExif, embedding },
scope,
);

View file

@ -817,12 +817,12 @@ export function withSearchOrder(qb: ReturnType<typeof searchAssetBuilder>, order
const scopeExample: AssetSearchScope = { userIds: [DummyValue.UUID], lockedOwnerId: DummyValue.UUID };
export const searchMetadataV3Examples: GenerateSqlQueries[] = [
{ name: 'baseline', params: [{ size: 100 }, {}, scopeExample] },
{ name: 'empty', params: [{ size: 100 }, {}, { lockedOwnerId: DummyValue.UUID }] },
{ name: 'baseline', params: [{ take: 100 }, {}, scopeExample] },
{ name: 'empty', params: [{ take: 100 }, {}, { lockedOwnerId: DummyValue.UUID }] },
{
name: 'or-exif-only',
params: [
{ size: 100 },
{ take: 100 },
{
filter: { or: [{ city: { eq: DummyValue.STRING } }] },
},
@ -831,12 +831,12 @@ export const searchMetadataV3Examples: GenerateSqlQueries[] = [
},
{
name: 'string-eq-null',
params: [{ size: 100 }, { filter: { city: { eq: null } } }, scopeExample],
params: [{ take: 100 }, { filter: { city: { eq: null } } }, scopeExample],
},
{
name: 'string-pattern-like',
params: [
{ size: 100 },
{ take: 100 },
{
filter: { description: { like: DummyValue.STRING } },
},
@ -846,7 +846,7 @@ export const searchMetadataV3Examples: GenerateSqlQueries[] = [
{
name: 'string-pattern-notLike',
params: [
{ size: 100 },
{ take: 100 },
{
filter: { description: { notLike: DummyValue.STRING } },
},
@ -856,7 +856,7 @@ export const searchMetadataV3Examples: GenerateSqlQueries[] = [
{
name: 'string-pattern-startsWith',
params: [
{ size: 100 },
{ take: 100 },
{
filter: { originalFileName: { startsWith: DummyValue.STRING } },
},
@ -865,16 +865,16 @@ export const searchMetadataV3Examples: GenerateSqlQueries[] = [
},
{
name: 'string-similarity-ocr',
params: [{ size: 100 }, { filter: { ocr: { matches: DummyValue.STRING } } }, scopeExample],
params: [{ take: 100 }, { filter: { ocr: { matches: DummyValue.STRING } } }, scopeExample],
},
{
name: 'ids-any',
params: [{ size: 100 }, { filter: { albumIds: { any: [DummyValue.UUID] } } }, scopeExample],
params: [{ take: 100 }, { filter: { albumIds: { any: [DummyValue.UUID] } } }, scopeExample],
},
{
name: 'ids-all',
params: [
{ size: 100 },
{ take: 100 },
{
filter: { personIds: { all: [DummyValue.UUID, DummyValue.UUID_1] } },
},
@ -883,16 +883,16 @@ export const searchMetadataV3Examples: GenerateSqlQueries[] = [
},
{
name: 'ids-all-single',
params: [{ size: 100 }, { filter: { albumIds: { all: [DummyValue.UUID] } } }, scopeExample],
params: [{ take: 100 }, { filter: { albumIds: { all: [DummyValue.UUID] } } }, scopeExample],
},
{
name: 'ids-none',
params: [{ size: 100 }, { filter: { tagIds: { none: [DummyValue.UUID] } } }, scopeExample],
params: [{ take: 100 }, { filter: { tagIds: { none: [DummyValue.UUID] } } }, scopeExample],
},
{
name: 'ids-tags-all',
params: [
{ size: 100 },
{ take: 100 },
{
filter: { tagIds: { all: [DummyValue.UUID, DummyValue.UUID_1] } },
},
@ -901,16 +901,16 @@ export const searchMetadataV3Examples: GenerateSqlQueries[] = [
},
{
name: 'has-albums-false',
params: [{ size: 100 }, { filter: { hasAlbums: { eq: false } } }, scopeExample],
params: [{ take: 100 }, { filter: { hasAlbums: { eq: false } } }, scopeExample],
},
{
name: 'is-encoded',
params: [{ size: 100 }, { filter: { isEncoded: { eq: true } } }, scopeExample],
params: [{ take: 100 }, { filter: { isEncoded: { eq: true } } }, scopeExample],
},
{
name: 'number-range',
params: [
{ size: 100 },
{ take: 100 },
{
filter: { fileSizeInBytes: { gte: 100, lte: 1000 } },
},
@ -919,12 +919,12 @@ export const searchMetadataV3Examples: GenerateSqlQueries[] = [
},
{
name: 'date-eq',
params: [{ size: 100 }, { filter: { takenAt: { eq: DummyValue.DATE } } }, scopeExample],
params: [{ take: 100 }, { filter: { takenAt: { eq: DummyValue.DATE } } }, scopeExample],
},
{
name: 'date-range',
params: [
{ size: 100 },
{ take: 100 },
{
filter: { takenAt: { gte: DummyValue.DATE, lt: DummyValue.DATE } },
},
@ -934,7 +934,7 @@ export const searchMetadataV3Examples: GenerateSqlQueries[] = [
{
name: 'order-fileSize-noExif',
params: [
{ size: 100 },
{ take: 100 },
{
order: { field: SearchOrderField.FileSizeInBytes, direction: AssetOrder.Desc },
withExif: false,
@ -945,7 +945,7 @@ export const searchMetadataV3Examples: GenerateSqlQueries[] = [
{
name: 'order-rating-withExif',
params: [
{ size: 100 },
{ take: 100 },
{
order: { field: SearchOrderField.Rating, direction: AssetOrder.Asc },
withExif: true,
@ -956,7 +956,7 @@ export const searchMetadataV3Examples: GenerateSqlQueries[] = [
{
name: 'or-branches',
params: [
{ size: 100 },
{ take: 100 },
{
filter: {
or: [{ isFavorite: { eq: true } }, { personIds: { any: [DummyValue.UUID] } }],
@ -968,7 +968,7 @@ export const searchMetadataV3Examples: GenerateSqlQueries[] = [
{
name: 'or-with-top-level',
params: [
{ size: 100 },
{ take: 100 },
{
filter: {
takenAt: { gte: DummyValue.DATE, lt: DummyValue.DATE },
@ -980,7 +980,7 @@ export const searchMetadataV3Examples: GenerateSqlQueries[] = [
},
{
name: 'cursor-offset',
params: [{ size: 100, offset: 100 }, { filter: { isFavorite: { eq: true } } }, scopeExample],
params: [{ take: 100, skip: 100 }, { filter: { isFavorite: { eq: true } } }, scopeExample],
},
];
@ -995,12 +995,12 @@ export const searchRandomV3Examples: GenerateSqlQueries[] = [
export const searchSmartV3Examples: GenerateSqlQueries[] = [
{
name: 'baseline',
params: [{ size: 100 }, { embedding: DummyValue.VECTOR }, scopeExample],
params: [{ take: 100 }, { embedding: DummyValue.VECTOR }, scopeExample],
},
{
name: 'with-filter',
params: [
{ size: 100 },
{ take: 100 },
{
embedding: DummyValue.VECTOR,
filter: { takenAt: { gte: DummyValue.DATE, lt: DummyValue.DATE } },
@ -1010,7 +1010,7 @@ export const searchSmartV3Examples: GenerateSqlQueries[] = [
},
{
name: 'cursor-offset',
params: [{ size: 100, offset: 100 }, { embedding: DummyValue.VECTOR }, scopeExample],
params: [{ take: 100, skip: 100 }, { embedding: DummyValue.VECTOR }, scopeExample],
},
];

View file

@ -406,12 +406,12 @@ describe(SearchService.name, () => {
const options = { filter: {}, embedding: unitVector(0) };
const scope = { userIds: [user.id], lockedOwnerId: user.id };
const firstPage = await searchRepository.searchSmartV3({ size: 2 }, options, scope);
const firstPage = await searchRepository.searchSmartV3({ take: 2 }, options, scope);
expect(firstPage.items.length).toBe(2);
expect(firstPage.items[0].id).toBe(assetIds[0]);
expect(firstPage.hasNextPage).toBe(true);
const secondPage = await searchRepository.searchSmartV3({ size: 2, offset: 2 }, options, scope);
const secondPage = await searchRepository.searchSmartV3({ take: 2, skip: 2 }, options, scope);
expect(secondPage.items.length).toBe(1);
expect(secondPage.hasNextPage).toBe(false);
});