mirror of
https://github.com/immich-app/immich.git
synced 2026-07-28 14:47:30 -07:00
split search opts from scope
This commit is contained in:
@@ -449,14 +449,18 @@ from
|
||||
left join "asset_exif" on "asset"."id" = "asset_exif"."assetId"
|
||||
where
|
||||
"asset"."ownerId" = any ($1::uuid[])
|
||||
and (
|
||||
"asset"."visibility" != $2
|
||||
or "asset"."ownerId" = $3
|
||||
)
|
||||
and true
|
||||
order by
|
||||
"asset"."fileCreatedAt" desc,
|
||||
"asset"."id" desc
|
||||
limit
|
||||
$2
|
||||
$4
|
||||
offset
|
||||
$3
|
||||
$5
|
||||
|
||||
-- SearchRepository.searchMetadataV3 (empty)
|
||||
select
|
||||
@@ -492,14 +496,18 @@ from
|
||||
"asset"
|
||||
left join "asset_exif" on "asset"."id" = "asset_exif"."assetId"
|
||||
where
|
||||
true
|
||||
(
|
||||
"asset"."visibility" != $1
|
||||
or "asset"."ownerId" = $2
|
||||
)
|
||||
and true
|
||||
order by
|
||||
"asset"."fileCreatedAt" desc,
|
||||
"asset"."id" desc
|
||||
limit
|
||||
$1
|
||||
$3
|
||||
offset
|
||||
$2
|
||||
$4
|
||||
|
||||
-- SearchRepository.searchMetadataV3 (or-exif-only)
|
||||
select
|
||||
@@ -536,14 +544,18 @@ from
|
||||
left join "asset_exif" on "asset"."id" = "asset_exif"."assetId"
|
||||
where
|
||||
"asset"."ownerId" = any ($1::uuid[])
|
||||
and "asset_exif"."city" = $2
|
||||
and (
|
||||
"asset"."visibility" != $2
|
||||
or "asset"."ownerId" = $3
|
||||
)
|
||||
and "asset_exif"."city" = $4
|
||||
order by
|
||||
"asset"."fileCreatedAt" desc,
|
||||
"asset"."id" desc
|
||||
limit
|
||||
$3
|
||||
$5
|
||||
offset
|
||||
$4
|
||||
$6
|
||||
|
||||
-- SearchRepository.searchMetadataV3 (string-eq-null)
|
||||
select
|
||||
@@ -580,14 +592,18 @@ from
|
||||
left join "asset_exif" on "asset"."id" = "asset_exif"."assetId"
|
||||
where
|
||||
"asset"."ownerId" = any ($1::uuid[])
|
||||
and (
|
||||
"asset"."visibility" != $2
|
||||
or "asset"."ownerId" = $3
|
||||
)
|
||||
and "asset_exif"."city" is null
|
||||
order by
|
||||
"asset"."fileCreatedAt" desc,
|
||||
"asset"."id" desc
|
||||
limit
|
||||
$2
|
||||
$4
|
||||
offset
|
||||
$3
|
||||
$5
|
||||
|
||||
-- SearchRepository.searchMetadataV3 (string-pattern-like)
|
||||
select
|
||||
@@ -624,14 +640,18 @@ from
|
||||
left join "asset_exif" on "asset"."id" = "asset_exif"."assetId"
|
||||
where
|
||||
"asset"."ownerId" = any ($1::uuid[])
|
||||
and f_unaccent ("asset_exif"."description") ilike ('%' || f_unaccent ($2) || '%')
|
||||
and (
|
||||
"asset"."visibility" != $2
|
||||
or "asset"."ownerId" = $3
|
||||
)
|
||||
and f_unaccent ("asset_exif"."description") ilike ('%' || f_unaccent ($4) || '%')
|
||||
order by
|
||||
"asset"."fileCreatedAt" desc,
|
||||
"asset"."id" desc
|
||||
limit
|
||||
$3
|
||||
$5
|
||||
offset
|
||||
$4
|
||||
$6
|
||||
|
||||
-- SearchRepository.searchMetadataV3 (string-pattern-notLike)
|
||||
select
|
||||
@@ -668,14 +688,18 @@ from
|
||||
left join "asset_exif" on "asset"."id" = "asset_exif"."assetId"
|
||||
where
|
||||
"asset"."ownerId" = any ($1::uuid[])
|
||||
and f_unaccent ("asset_exif"."description") not ilike ('%' || f_unaccent ($2) || '%')
|
||||
and (
|
||||
"asset"."visibility" != $2
|
||||
or "asset"."ownerId" = $3
|
||||
)
|
||||
and f_unaccent ("asset_exif"."description") not ilike ('%' || f_unaccent ($4) || '%')
|
||||
order by
|
||||
"asset"."fileCreatedAt" desc,
|
||||
"asset"."id" desc
|
||||
limit
|
||||
$3
|
||||
$5
|
||||
offset
|
||||
$4
|
||||
$6
|
||||
|
||||
-- SearchRepository.searchMetadataV3 (string-pattern-startsWith)
|
||||
select
|
||||
@@ -712,14 +736,18 @@ from
|
||||
left join "asset_exif" on "asset"."id" = "asset_exif"."assetId"
|
||||
where
|
||||
"asset"."ownerId" = any ($1::uuid[])
|
||||
and f_unaccent ("asset"."originalFileName") ilike (f_unaccent ($2) || '%')
|
||||
and (
|
||||
"asset"."visibility" != $2
|
||||
or "asset"."ownerId" = $3
|
||||
)
|
||||
and f_unaccent ("asset"."originalFileName") ilike (f_unaccent ($4) || '%')
|
||||
order by
|
||||
"asset"."fileCreatedAt" desc,
|
||||
"asset"."id" desc
|
||||
limit
|
||||
$3
|
||||
$5
|
||||
offset
|
||||
$4
|
||||
$6
|
||||
|
||||
-- SearchRepository.searchMetadataV3 (string-similarity-ocr)
|
||||
select
|
||||
@@ -756,21 +784,25 @@ from
|
||||
left join "asset_exif" on "asset"."id" = "asset_exif"."assetId"
|
||||
where
|
||||
"asset"."ownerId" = any ($1::uuid[])
|
||||
and (
|
||||
"asset"."visibility" != $2
|
||||
or "asset"."ownerId" = $3
|
||||
)
|
||||
and exists (
|
||||
select
|
||||
from
|
||||
"ocr_search"
|
||||
where
|
||||
"ocr_search"."assetId" = "asset"."id"
|
||||
and f_unaccent (ocr_search.text) %>> f_unaccent ($2)
|
||||
and f_unaccent (ocr_search.text) %>> f_unaccent ($4)
|
||||
)
|
||||
order by
|
||||
"asset"."fileCreatedAt" desc,
|
||||
"asset"."id" desc
|
||||
limit
|
||||
$3
|
||||
$5
|
||||
offset
|
||||
$4
|
||||
$6
|
||||
|
||||
-- SearchRepository.searchMetadataV3 (ids-any)
|
||||
select
|
||||
@@ -807,21 +839,25 @@ from
|
||||
left join "asset_exif" on "asset"."id" = "asset_exif"."assetId"
|
||||
where
|
||||
"asset"."ownerId" = any ($1::uuid[])
|
||||
and (
|
||||
"asset"."visibility" != $2
|
||||
or "asset"."ownerId" = $3
|
||||
)
|
||||
and exists (
|
||||
select
|
||||
from
|
||||
"album_asset"
|
||||
where
|
||||
"album_asset"."assetId" = "asset"."id"
|
||||
and "album_asset"."albumId" = any ($2::uuid[])
|
||||
and "album_asset"."albumId" = any ($4::uuid[])
|
||||
)
|
||||
order by
|
||||
"asset"."fileCreatedAt" desc,
|
||||
"asset"."id" desc
|
||||
limit
|
||||
$3
|
||||
$5
|
||||
offset
|
||||
$4
|
||||
$6
|
||||
|
||||
-- SearchRepository.searchMetadataV3 (ids-all)
|
||||
select
|
||||
@@ -858,6 +894,10 @@ from
|
||||
left join "asset_exif" on "asset"."id" = "asset_exif"."assetId"
|
||||
where
|
||||
"asset"."ownerId" = any ($1::uuid[])
|
||||
and (
|
||||
"asset"."visibility" != $2
|
||||
or "asset"."ownerId" = $3
|
||||
)
|
||||
and exists (
|
||||
select
|
||||
"asset_face"."assetId"
|
||||
@@ -866,20 +906,20 @@ where
|
||||
where
|
||||
"asset_face"."assetId" = "asset"."id"
|
||||
and "asset_face"."deletedAt" is null
|
||||
and "asset_face"."isVisible" = $2
|
||||
and "asset_face"."personId" = any ($3::uuid[])
|
||||
and "asset_face"."isVisible" = $4
|
||||
and "asset_face"."personId" = any ($5::uuid[])
|
||||
group by
|
||||
"asset_face"."assetId"
|
||||
having
|
||||
count(distinct "asset_face"."personId") = $4
|
||||
count(distinct "asset_face"."personId") = $6
|
||||
)
|
||||
order by
|
||||
"asset"."fileCreatedAt" desc,
|
||||
"asset"."id" desc
|
||||
limit
|
||||
$5
|
||||
$7
|
||||
offset
|
||||
$6
|
||||
$8
|
||||
|
||||
-- SearchRepository.searchMetadataV3 (ids-all-single)
|
||||
select
|
||||
@@ -916,21 +956,25 @@ from
|
||||
left join "asset_exif" on "asset"."id" = "asset_exif"."assetId"
|
||||
where
|
||||
"asset"."ownerId" = any ($1::uuid[])
|
||||
and (
|
||||
"asset"."visibility" != $2
|
||||
or "asset"."ownerId" = $3
|
||||
)
|
||||
and exists (
|
||||
select
|
||||
from
|
||||
"album_asset"
|
||||
where
|
||||
"album_asset"."assetId" = "asset"."id"
|
||||
and "album_asset"."albumId" = any ($2::uuid[])
|
||||
and "album_asset"."albumId" = any ($4::uuid[])
|
||||
)
|
||||
order by
|
||||
"asset"."fileCreatedAt" desc,
|
||||
"asset"."id" desc
|
||||
limit
|
||||
$3
|
||||
$5
|
||||
offset
|
||||
$4
|
||||
$6
|
||||
|
||||
-- SearchRepository.searchMetadataV3 (ids-none)
|
||||
select
|
||||
@@ -967,6 +1011,10 @@ from
|
||||
left join "asset_exif" on "asset"."id" = "asset_exif"."assetId"
|
||||
where
|
||||
"asset"."ownerId" = any ($1::uuid[])
|
||||
and (
|
||||
"asset"."visibility" != $2
|
||||
or "asset"."ownerId" = $3
|
||||
)
|
||||
and not exists (
|
||||
select
|
||||
from
|
||||
@@ -974,15 +1022,15 @@ where
|
||||
inner join "tag_closure" on "tag_asset"."tagId" = "tag_closure"."id_descendant"
|
||||
where
|
||||
"tag_asset"."assetId" = "asset"."id"
|
||||
and "tag_closure"."id_ancestor" = any ($2::uuid[])
|
||||
and "tag_closure"."id_ancestor" = any ($4::uuid[])
|
||||
)
|
||||
order by
|
||||
"asset"."fileCreatedAt" desc,
|
||||
"asset"."id" desc
|
||||
limit
|
||||
$3
|
||||
$5
|
||||
offset
|
||||
$4
|
||||
$6
|
||||
|
||||
-- SearchRepository.searchMetadataV3 (ids-tags-all)
|
||||
select
|
||||
@@ -1019,6 +1067,10 @@ from
|
||||
left join "asset_exif" on "asset"."id" = "asset_exif"."assetId"
|
||||
where
|
||||
"asset"."ownerId" = any ($1::uuid[])
|
||||
and (
|
||||
"asset"."visibility" != $2
|
||||
or "asset"."ownerId" = $3
|
||||
)
|
||||
and exists (
|
||||
select
|
||||
"tag_asset"."assetId"
|
||||
@@ -1027,19 +1079,19 @@ where
|
||||
inner join "tag_closure" on "tag_asset"."tagId" = "tag_closure"."id_descendant"
|
||||
where
|
||||
"tag_asset"."assetId" = "asset"."id"
|
||||
and "tag_closure"."id_ancestor" = any ($2::uuid[])
|
||||
and "tag_closure"."id_ancestor" = any ($4::uuid[])
|
||||
group by
|
||||
"tag_asset"."assetId"
|
||||
having
|
||||
count(distinct "tag_closure"."id_ancestor") = $3
|
||||
count(distinct "tag_closure"."id_ancestor") = $5
|
||||
)
|
||||
order by
|
||||
"asset"."fileCreatedAt" desc,
|
||||
"asset"."id" desc
|
||||
limit
|
||||
$4
|
||||
$6
|
||||
offset
|
||||
$5
|
||||
$7
|
||||
|
||||
-- SearchRepository.searchMetadataV3 (has-albums-false)
|
||||
select
|
||||
@@ -1076,6 +1128,10 @@ from
|
||||
left join "asset_exif" on "asset"."id" = "asset_exif"."assetId"
|
||||
where
|
||||
"asset"."ownerId" = any ($1::uuid[])
|
||||
and (
|
||||
"asset"."visibility" != $2
|
||||
or "asset"."ownerId" = $3
|
||||
)
|
||||
and not exists (
|
||||
select
|
||||
from
|
||||
@@ -1087,9 +1143,9 @@ order by
|
||||
"asset"."fileCreatedAt" desc,
|
||||
"asset"."id" desc
|
||||
limit
|
||||
$2
|
||||
$4
|
||||
offset
|
||||
$3
|
||||
$5
|
||||
|
||||
-- SearchRepository.searchMetadataV3 (is-encoded)
|
||||
select
|
||||
@@ -1126,21 +1182,25 @@ from
|
||||
left join "asset_exif" on "asset"."id" = "asset_exif"."assetId"
|
||||
where
|
||||
"asset"."ownerId" = any ($1::uuid[])
|
||||
and (
|
||||
"asset"."visibility" != $2
|
||||
or "asset"."ownerId" = $3
|
||||
)
|
||||
and exists (
|
||||
select
|
||||
from
|
||||
"asset_file"
|
||||
where
|
||||
"asset_file"."assetId" = "asset"."id"
|
||||
and "asset_file"."type" = $2
|
||||
and "asset_file"."type" = $4
|
||||
)
|
||||
order by
|
||||
"asset"."fileCreatedAt" desc,
|
||||
"asset"."id" desc
|
||||
limit
|
||||
$3
|
||||
$5
|
||||
offset
|
||||
$4
|
||||
$6
|
||||
|
||||
-- SearchRepository.searchMetadataV3 (number-range)
|
||||
select
|
||||
@@ -1178,16 +1238,20 @@ from
|
||||
where
|
||||
"asset"."ownerId" = any ($1::uuid[])
|
||||
and (
|
||||
"asset_exif"."fileSizeInByte" <= $2
|
||||
and "asset_exif"."fileSizeInByte" >= $3
|
||||
"asset"."visibility" != $2
|
||||
or "asset"."ownerId" = $3
|
||||
)
|
||||
and (
|
||||
"asset_exif"."fileSizeInByte" <= $4
|
||||
and "asset_exif"."fileSizeInByte" >= $5
|
||||
)
|
||||
order by
|
||||
"asset"."fileCreatedAt" desc,
|
||||
"asset"."id" desc
|
||||
limit
|
||||
$4
|
||||
$6
|
||||
offset
|
||||
$5
|
||||
$7
|
||||
|
||||
-- SearchRepository.searchMetadataV3 (date-eq)
|
||||
select
|
||||
@@ -1224,14 +1288,18 @@ from
|
||||
left join "asset_exif" on "asset"."id" = "asset_exif"."assetId"
|
||||
where
|
||||
"asset"."ownerId" = any ($1::uuid[])
|
||||
and "asset"."fileCreatedAt" = $2
|
||||
and (
|
||||
"asset"."visibility" != $2
|
||||
or "asset"."ownerId" = $3
|
||||
)
|
||||
and "asset"."fileCreatedAt" = $4
|
||||
order by
|
||||
"asset"."fileCreatedAt" desc,
|
||||
"asset"."id" desc
|
||||
limit
|
||||
$3
|
||||
$5
|
||||
offset
|
||||
$4
|
||||
$6
|
||||
|
||||
-- SearchRepository.searchMetadataV3 (date-range)
|
||||
select
|
||||
@@ -1269,16 +1337,20 @@ from
|
||||
where
|
||||
"asset"."ownerId" = any ($1::uuid[])
|
||||
and (
|
||||
"asset"."fileCreatedAt" < $2
|
||||
and "asset"."fileCreatedAt" >= $3
|
||||
"asset"."visibility" != $2
|
||||
or "asset"."ownerId" = $3
|
||||
)
|
||||
and (
|
||||
"asset"."fileCreatedAt" < $4
|
||||
and "asset"."fileCreatedAt" >= $5
|
||||
)
|
||||
order by
|
||||
"asset"."fileCreatedAt" desc,
|
||||
"asset"."id" desc
|
||||
limit
|
||||
$4
|
||||
$6
|
||||
offset
|
||||
$5
|
||||
$7
|
||||
|
||||
-- SearchRepository.searchMetadataV3 (order-fileSize-noExif)
|
||||
select
|
||||
@@ -1315,14 +1387,18 @@ from
|
||||
left join "asset_exif" on "asset"."id" = "asset_exif"."assetId"
|
||||
where
|
||||
"asset"."ownerId" = any ($1::uuid[])
|
||||
and (
|
||||
"asset"."visibility" != $2
|
||||
or "asset"."ownerId" = $3
|
||||
)
|
||||
and true
|
||||
order by
|
||||
"asset_exif"."fileSizeInByte" desc nulls last,
|
||||
"asset"."id" desc
|
||||
limit
|
||||
$2
|
||||
$4
|
||||
offset
|
||||
$3
|
||||
$5
|
||||
|
||||
-- SearchRepository.searchMetadataV3 (order-rating-withExif)
|
||||
select
|
||||
@@ -1360,14 +1436,18 @@ from
|
||||
left join "asset_exif" on "asset"."id" = "asset_exif"."assetId"
|
||||
where
|
||||
"asset"."ownerId" = any ($1::uuid[])
|
||||
and (
|
||||
"asset"."visibility" != $2
|
||||
or "asset"."ownerId" = $3
|
||||
)
|
||||
and true
|
||||
order by
|
||||
"asset_exif"."rating" asc nulls last,
|
||||
"asset"."id" asc
|
||||
limit
|
||||
$2
|
||||
$4
|
||||
offset
|
||||
$3
|
||||
$5
|
||||
|
||||
-- SearchRepository.searchMetadataV3 (or-branches)
|
||||
select
|
||||
@@ -1405,7 +1485,11 @@ from
|
||||
where
|
||||
"asset"."ownerId" = any ($1::uuid[])
|
||||
and (
|
||||
"asset"."isFavorite" = $2
|
||||
"asset"."visibility" != $2
|
||||
or "asset"."ownerId" = $3
|
||||
)
|
||||
and (
|
||||
"asset"."isFavorite" = $4
|
||||
or exists (
|
||||
select
|
||||
from
|
||||
@@ -1413,17 +1497,17 @@ where
|
||||
where
|
||||
"asset_face"."assetId" = "asset"."id"
|
||||
and "asset_face"."deletedAt" is null
|
||||
and "asset_face"."isVisible" = $3
|
||||
and "asset_face"."personId" = any ($4::uuid[])
|
||||
and "asset_face"."isVisible" = $5
|
||||
and "asset_face"."personId" = any ($6::uuid[])
|
||||
)
|
||||
)
|
||||
order by
|
||||
"asset"."fileCreatedAt" desc,
|
||||
"asset"."id" desc
|
||||
limit
|
||||
$5
|
||||
$7
|
||||
offset
|
||||
$6
|
||||
$8
|
||||
|
||||
-- SearchRepository.searchMetadataV3 (or-with-top-level)
|
||||
select
|
||||
@@ -1461,17 +1545,21 @@ from
|
||||
where
|
||||
"asset"."ownerId" = any ($1::uuid[])
|
||||
and (
|
||||
"asset"."fileCreatedAt" < $2
|
||||
and "asset"."fileCreatedAt" >= $3
|
||||
"asset"."visibility" != $2
|
||||
or "asset"."ownerId" = $3
|
||||
)
|
||||
and (
|
||||
"asset"."fileCreatedAt" < $4
|
||||
and "asset"."fileCreatedAt" >= $5
|
||||
and (
|
||||
"asset"."isFavorite" = $4
|
||||
"asset"."isFavorite" = $6
|
||||
or exists (
|
||||
select
|
||||
from
|
||||
"album_asset"
|
||||
where
|
||||
"album_asset"."assetId" = "asset"."id"
|
||||
and "album_asset"."albumId" = any ($5::uuid[])
|
||||
and "album_asset"."albumId" = any ($7::uuid[])
|
||||
)
|
||||
)
|
||||
)
|
||||
@@ -1479,9 +1567,9 @@ order by
|
||||
"asset"."fileCreatedAt" desc,
|
||||
"asset"."id" desc
|
||||
limit
|
||||
$6
|
||||
$8
|
||||
offset
|
||||
$7
|
||||
$9
|
||||
|
||||
-- SearchRepository.searchMetadataV3 (cursor-offset)
|
||||
select
|
||||
@@ -1518,14 +1606,18 @@ from
|
||||
left join "asset_exif" on "asset"."id" = "asset_exif"."assetId"
|
||||
where
|
||||
"asset"."ownerId" = any ($1::uuid[])
|
||||
and "asset"."isFavorite" = $2
|
||||
and (
|
||||
"asset"."visibility" != $2
|
||||
or "asset"."ownerId" = $3
|
||||
)
|
||||
and "asset"."isFavorite" = $4
|
||||
order by
|
||||
"asset"."fileCreatedAt" desc,
|
||||
"asset"."id" desc
|
||||
limit
|
||||
$3
|
||||
$5
|
||||
offset
|
||||
$4
|
||||
$6
|
||||
|
||||
-- SearchRepository.searchRandomV3 (baseline)
|
||||
select
|
||||
@@ -1562,11 +1654,15 @@ from
|
||||
left join "asset_exif" on "asset"."id" = "asset_exif"."assetId"
|
||||
where
|
||||
"asset"."ownerId" = any ($1::uuid[])
|
||||
and (
|
||||
"asset"."visibility" != $2
|
||||
or "asset"."ownerId" = $3
|
||||
)
|
||||
and true
|
||||
order by
|
||||
random()
|
||||
limit
|
||||
$2
|
||||
$4
|
||||
|
||||
-- SearchRepository.searchRandomV3 (with-filter)
|
||||
select
|
||||
@@ -1603,11 +1699,15 @@ from
|
||||
left join "asset_exif" on "asset"."id" = "asset_exif"."assetId"
|
||||
where
|
||||
"asset"."ownerId" = any ($1::uuid[])
|
||||
and "asset"."isFavorite" = $2
|
||||
and (
|
||||
"asset"."visibility" != $2
|
||||
or "asset"."ownerId" = $3
|
||||
)
|
||||
and "asset"."isFavorite" = $4
|
||||
order by
|
||||
random()
|
||||
limit
|
||||
$3
|
||||
$5
|
||||
|
||||
-- SearchRepository.searchSmartV3 (baseline)
|
||||
begin
|
||||
@@ -1648,14 +1748,18 @@ from
|
||||
inner join "smart_search" on "asset"."id" = "smart_search"."assetId"
|
||||
where
|
||||
"asset"."ownerId" = any ($1::uuid[])
|
||||
and (
|
||||
"asset"."visibility" != $2
|
||||
or "asset"."ownerId" = $3
|
||||
)
|
||||
and true
|
||||
order by
|
||||
smart_search.embedding <=> $2,
|
||||
smart_search.embedding <=> $4,
|
||||
"asset"."id" asc
|
||||
limit
|
||||
$3
|
||||
$5
|
||||
offset
|
||||
$4
|
||||
$6
|
||||
commit
|
||||
|
||||
-- SearchRepository.searchSmartV3 (with-filter)
|
||||
@@ -1698,16 +1802,20 @@ from
|
||||
where
|
||||
"asset"."ownerId" = any ($1::uuid[])
|
||||
and (
|
||||
"asset"."fileCreatedAt" < $2
|
||||
and "asset"."fileCreatedAt" >= $3
|
||||
"asset"."visibility" != $2
|
||||
or "asset"."ownerId" = $3
|
||||
)
|
||||
and (
|
||||
"asset"."fileCreatedAt" < $4
|
||||
and "asset"."fileCreatedAt" >= $5
|
||||
)
|
||||
order by
|
||||
smart_search.embedding <=> $4,
|
||||
smart_search.embedding <=> $6,
|
||||
"asset"."id" asc
|
||||
limit
|
||||
$5
|
||||
$7
|
||||
offset
|
||||
$6
|
||||
$8
|
||||
commit
|
||||
|
||||
-- SearchRepository.searchSmartV3 (cursor-offset)
|
||||
@@ -1749,14 +1857,18 @@ from
|
||||
inner join "smart_search" on "asset"."id" = "smart_search"."assetId"
|
||||
where
|
||||
"asset"."ownerId" = any ($1::uuid[])
|
||||
and (
|
||||
"asset"."visibility" != $2
|
||||
or "asset"."ownerId" = $3
|
||||
)
|
||||
and true
|
||||
order by
|
||||
smart_search.embedding <=> $2,
|
||||
smart_search.embedding <=> $4,
|
||||
"asset"."id" asc
|
||||
limit
|
||||
$3
|
||||
$5
|
||||
offset
|
||||
$4
|
||||
$6
|
||||
commit
|
||||
|
||||
-- SearchRepository.searchStatisticsV3 (baseline)
|
||||
@@ -1767,6 +1879,10 @@ from
|
||||
left join "asset_exif" on "asset"."id" = "asset_exif"."assetId"
|
||||
where
|
||||
"asset"."ownerId" = any ($1::uuid[])
|
||||
and (
|
||||
"asset"."visibility" != $2
|
||||
or "asset"."ownerId" = $3
|
||||
)
|
||||
and true
|
||||
|
||||
-- SearchRepository.searchStatisticsV3 (with-filter)
|
||||
@@ -1778,9 +1894,13 @@ from
|
||||
where
|
||||
"asset"."ownerId" = any ($1::uuid[])
|
||||
and (
|
||||
"asset_exif"."fileSizeInByte" >= $2
|
||||
and "asset"."fileCreatedAt" < $3
|
||||
and "asset"."fileCreatedAt" >= $4
|
||||
"asset"."visibility" != $2
|
||||
or "asset"."ownerId" = $3
|
||||
)
|
||||
and (
|
||||
"asset_exif"."fileSizeInByte" >= $4
|
||||
and "asset"."fileCreatedAt" < $5
|
||||
and "asset"."fileCreatedAt" >= $6
|
||||
)
|
||||
|
||||
-- SearchRepository.searchStatisticsV3 (with-or)
|
||||
@@ -1792,7 +1912,11 @@ from
|
||||
where
|
||||
"asset"."ownerId" = any ($1::uuid[])
|
||||
and (
|
||||
"asset"."isFavorite" = $2
|
||||
"asset"."visibility" != $2
|
||||
or "asset"."ownerId" = $3
|
||||
)
|
||||
and (
|
||||
"asset"."isFavorite" = $4
|
||||
or not exists (
|
||||
select
|
||||
from
|
||||
|
||||
@@ -135,10 +135,13 @@ export type AssetSearchOptions = Omit<BaseAssetSearchOptions, 'visibility'> &
|
||||
|
||||
export type AssetSearchBuilderOptions = Omit<AssetSearchOptions, 'orderDirection'>;
|
||||
|
||||
export interface AssetSearchScope {
|
||||
userIds?: string[];
|
||||
lockedOwnerId: string;
|
||||
}
|
||||
|
||||
export interface AssetSearchBuilderV3Options {
|
||||
filter?: SearchFilter;
|
||||
/** Server-derived ownership scope. Never client-controlled. */
|
||||
userIds?: string[];
|
||||
withExif?: boolean;
|
||||
withFaces?: boolean;
|
||||
withPeople?: boolean;
|
||||
@@ -528,8 +531,12 @@ export class SearchRepository {
|
||||
|
||||
// TODO(v4): drop the V3 suffix once the legacy methods are removed
|
||||
@GenerateSql(...searchMetadataV3Examples)
|
||||
async searchMetadataV3(pagination: AssetSearchPaginationV3Options, options: AssetSearchBuilderV3Options) {
|
||||
const items = await withSearchOrder(searchAssetBuilder(this.db, options), options.order)
|
||||
async searchMetadataV3(
|
||||
pagination: AssetSearchPaginationV3Options,
|
||||
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)
|
||||
@@ -539,8 +546,12 @@ export class SearchRepository {
|
||||
|
||||
// TODO(v4): drop the V3 suffix once the legacy methods are removed
|
||||
@GenerateSql(...searchRandomV3Examples)
|
||||
searchRandomV3(size: number, options: Omit<AssetSearchBuilderV3Options, 'order'>): Promise<MapAsset[]> {
|
||||
return searchAssetBuilder(this.db, options)
|
||||
searchRandomV3(
|
||||
size: number,
|
||||
options: Omit<AssetSearchBuilderV3Options, 'order'>,
|
||||
scope: AssetSearchScope,
|
||||
): Promise<MapAsset[]> {
|
||||
return searchAssetBuilder(this.db, options, scope)
|
||||
.select(columns.searchAsset)
|
||||
.orderBy(sql`random()`)
|
||||
.limit(size)
|
||||
@@ -552,10 +563,11 @@ export class SearchRepository {
|
||||
searchSmartV3(
|
||||
pagination: AssetSearchPaginationV3Options,
|
||||
options: Omit<AssetSearchBuilderV3Options, 'order'> & { embedding: string },
|
||||
scope: AssetSearchScope,
|
||||
) {
|
||||
return this.db.transaction().execute(async (trx) => {
|
||||
await sql`set local vchordrq.probes = ${sql.lit(probes[VectorIndex.Clip])}`.execute(trx);
|
||||
const items = await searchAssetBuilder(trx, options)
|
||||
const items = await searchAssetBuilder(trx, options, scope)
|
||||
.select(columns.searchAsset)
|
||||
.innerJoin('smart_search', 'asset.id', 'smart_search.assetId')
|
||||
.orderBy(sql`smart_search.embedding <=> ${options.embedding}`)
|
||||
@@ -569,8 +581,8 @@ export class SearchRepository {
|
||||
|
||||
// TODO(v4): drop the V3 suffix once the legacy methods are removed
|
||||
@GenerateSql(...searchStatisticsV3Examples)
|
||||
searchStatisticsV3(options: AssetSearchBuilderV3Options) {
|
||||
return searchAssetBuilder(this.db, options)
|
||||
searchStatisticsV3(options: AssetSearchBuilderV3Options, scope: AssetSearchScope) {
|
||||
return searchAssetBuilder(this.db, options, scope)
|
||||
.select((qb) => qb.fn.countAll<number>().as('total'))
|
||||
.executeTakeFirstOrThrow();
|
||||
}
|
||||
|
||||
@@ -246,6 +246,7 @@ describe(SearchService.name, () => {
|
||||
expect(mocks.search.searchSmartV3).toHaveBeenCalledWith(
|
||||
{ size: 100, offset: 0 },
|
||||
expect.objectContaining({ embedding: '[1, 2, 3]' }),
|
||||
expect.objectContaining({ lockedOwnerId: expect.any(String) }),
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
StatisticsSearchDto,
|
||||
} from 'src/dtos/search.dto';
|
||||
import { AssetOrder, AssetVisibility, Permission } from 'src/enum';
|
||||
import { AssetSearchScope } from 'src/repositories/search.repository';
|
||||
import { BaseService } from 'src/services/base.service';
|
||||
import { requireElevatedPermission } from 'src/utils/access';
|
||||
import { getMyPartnerIds } from 'src/utils/asset.util';
|
||||
@@ -31,7 +32,6 @@ import {
|
||||
applyLockedVisibilityPolicy,
|
||||
collectFilterIds,
|
||||
hasTopLevelPositiveIdsConstraint,
|
||||
isLockedOnlyFilter,
|
||||
} from 'src/utils/search-filter';
|
||||
|
||||
@Injectable()
|
||||
@@ -235,6 +235,71 @@ export class SearchService extends BaseService {
|
||||
}
|
||||
|
||||
private async searchMetadataV3(auth: AuthDto, dto: MetadataSearchDto): Promise<SearchResponseDto> {
|
||||
const { filter, scope } = await this.resolveSearchScopeV3(auth, dto);
|
||||
|
||||
const { offset } = decodeSearchCursor(dto.cursor);
|
||||
const size = dto.size;
|
||||
const { hasNextPage, items } = await this.searchRepository.searchMetadataV3(
|
||||
{ size, offset },
|
||||
{
|
||||
filter,
|
||||
withExif: dto.withExif,
|
||||
withPeople: dto.withPeople,
|
||||
withStacked: dto.withStacked,
|
||||
order: dto.orderBy,
|
||||
},
|
||||
scope,
|
||||
);
|
||||
|
||||
return this.mapResponse(items, { auth }, { nextCursor: hasNextPage ? encodeSearchCursor(offset + size) : null });
|
||||
}
|
||||
|
||||
private async searchStatisticsV3(auth: AuthDto, dto: StatisticsSearchDto): Promise<SearchStatisticsResponseDto> {
|
||||
const { filter, scope } = await this.resolveSearchScopeV3(auth, dto);
|
||||
return this.searchRepository.searchStatisticsV3({ filter }, scope);
|
||||
}
|
||||
|
||||
private async searchRandomV3(auth: AuthDto, dto: RandomSearchDto): Promise<AssetResponseDto[]> {
|
||||
const { filter, scope } = await this.resolveSearchScopeV3(auth, dto);
|
||||
const items = await this.searchRepository.searchRandomV3(
|
||||
dto.size,
|
||||
{
|
||||
filter,
|
||||
withExif: dto.withExif,
|
||||
withPeople: dto.withPeople,
|
||||
withStacked: dto.withStacked,
|
||||
},
|
||||
scope,
|
||||
);
|
||||
return items.map((item) => mapAsset(item, { auth }));
|
||||
}
|
||||
|
||||
private async searchSmartV3(auth: AuthDto, dto: SmartSearchDto): Promise<SearchResponseDto> {
|
||||
const { machineLearning } = await this.getConfig({ withCache: false });
|
||||
if (!isSmartSearchEnabled(machineLearning)) {
|
||||
throw new BadRequestException('Smart search is not enabled');
|
||||
}
|
||||
|
||||
const [{ filter, scope }, embedding] = await Promise.all([
|
||||
this.resolveSearchScopeV3(auth, dto),
|
||||
this.resolveEmbedding(auth, dto, machineLearning),
|
||||
]);
|
||||
|
||||
const { offset } = decodeSearchCursor(dto.cursor);
|
||||
const size = dto.size;
|
||||
const { hasNextPage, items } = await this.searchRepository.searchSmartV3(
|
||||
{ size, offset },
|
||||
{ filter, withExif: dto.withExif, embedding },
|
||||
scope,
|
||||
);
|
||||
|
||||
return this.mapResponse(items, { auth }, { nextCursor: hasNextPage ? encodeSearchCursor(offset + size) : null });
|
||||
}
|
||||
|
||||
private async resolveSearchScopeV3(
|
||||
auth: AuthDto,
|
||||
dto: { filter?: SearchFilter },
|
||||
): Promise<{ filter: SearchFilter; scope: AssetSearchScope }> {
|
||||
const filter = dto.filter ?? {};
|
||||
const effectiveFilter = applyLockedVisibilityPolicy(auth, filter);
|
||||
|
||||
@@ -247,75 +312,11 @@ export class SearchService extends BaseService {
|
||||
|
||||
const albumIds = collectFilterIds(filter, 'albumIds');
|
||||
const [userIds] = await Promise.all([
|
||||
hasTopLevelAlbums ? undefined : this.getUserIdsToSearchV3(auth, filter),
|
||||
hasTopLevelAlbums ? undefined : this.getUserIdsToSearch(auth),
|
||||
albumIds.length > 0 ? this.requireAccess({ auth, ids: albumIds, permission: Permission.AlbumRead }) : undefined,
|
||||
]);
|
||||
|
||||
const { offset } = decodeSearchCursor(dto.cursor);
|
||||
const size = dto.size;
|
||||
const { hasNextPage, items } = await this.searchRepository.searchMetadataV3(
|
||||
{ size, offset },
|
||||
{
|
||||
filter: effectiveFilter,
|
||||
userIds,
|
||||
withExif: dto.withExif,
|
||||
withPeople: dto.withPeople,
|
||||
withStacked: dto.withStacked,
|
||||
order: dto.orderBy,
|
||||
},
|
||||
);
|
||||
|
||||
return this.mapResponse(items, { auth }, { nextCursor: hasNextPage ? encodeSearchCursor(offset + size) : null });
|
||||
}
|
||||
|
||||
private async searchStatisticsV3(auth: AuthDto, dto: StatisticsSearchDto): Promise<SearchStatisticsResponseDto> {
|
||||
const { effectiveFilter, userIds } = await this.resolveSearchScopeV3(auth, dto);
|
||||
return this.searchRepository.searchStatisticsV3({ filter: effectiveFilter, userIds });
|
||||
}
|
||||
|
||||
private async searchRandomV3(auth: AuthDto, dto: RandomSearchDto): Promise<AssetResponseDto[]> {
|
||||
const { effectiveFilter, userIds } = await this.resolveSearchScopeV3(auth, dto);
|
||||
const items = await this.searchRepository.searchRandomV3(dto.size, {
|
||||
filter: effectiveFilter,
|
||||
userIds,
|
||||
withExif: dto.withExif,
|
||||
withPeople: dto.withPeople,
|
||||
withStacked: dto.withStacked,
|
||||
});
|
||||
return items.map((item) => mapAsset(item, { auth }));
|
||||
}
|
||||
|
||||
private async searchSmartV3(auth: AuthDto, dto: SmartSearchDto): Promise<SearchResponseDto> {
|
||||
const { machineLearning } = await this.getConfig({ withCache: false });
|
||||
if (!isSmartSearchEnabled(machineLearning)) {
|
||||
throw new BadRequestException('Smart search is not enabled');
|
||||
}
|
||||
|
||||
const [{ effectiveFilter, userIds }, embedding] = await Promise.all([
|
||||
this.resolveSearchScopeV3(auth, dto),
|
||||
this.resolveEmbedding(auth, dto, machineLearning),
|
||||
]);
|
||||
|
||||
const { offset } = decodeSearchCursor(dto.cursor);
|
||||
const size = dto.size;
|
||||
const { hasNextPage, items } = await this.searchRepository.searchSmartV3(
|
||||
{ size, offset },
|
||||
{ filter: effectiveFilter, userIds, withExif: dto.withExif, embedding },
|
||||
);
|
||||
|
||||
return this.mapResponse(items, { auth }, { nextCursor: hasNextPage ? encodeSearchCursor(offset + size) : null });
|
||||
}
|
||||
|
||||
private async resolveSearchScopeV3(auth: AuthDto, dto: { filter?: SearchFilter }) {
|
||||
const filter = dto.filter ?? {};
|
||||
return {
|
||||
effectiveFilter: applyLockedVisibilityPolicy(auth, filter),
|
||||
userIds: await this.getUserIdsToSearchV3(auth, filter),
|
||||
};
|
||||
}
|
||||
|
||||
private getUserIdsToSearchV3(auth: AuthDto, filter: SearchFilter): Promise<string[]> {
|
||||
return this.getUserIdsToSearch(auth, isLockedOnlyFilter(filter) ? AssetVisibility.Locked : undefined);
|
||||
return { filter: effectiveFilter, scope: { userIds, lockedOwnerId: auth.user.id } };
|
||||
}
|
||||
|
||||
private async resolveEmbedding(
|
||||
|
||||
@@ -38,7 +38,11 @@ import {
|
||||
ExifOrientation,
|
||||
SearchOrderField,
|
||||
} from 'src/enum';
|
||||
import { AssetSearchBuilderOptions, AssetSearchBuilderV3Options } from 'src/repositories/search.repository';
|
||||
import {
|
||||
AssetSearchBuilderOptions,
|
||||
AssetSearchBuilderV3Options,
|
||||
AssetSearchScope,
|
||||
} from 'src/repositories/search.repository';
|
||||
import { DB } from 'src/schema';
|
||||
import { AssetExifTable } from 'src/schema/tables/asset-exif.table';
|
||||
import { AudioStreamInfo, VectorExtension, VideoFormat, VideoPacketInfo, VideoStreamInfo } from 'src/types';
|
||||
@@ -762,7 +766,7 @@ function branchPredicates(eb: AssetExpressionBuilder, branch: SearchFilterBranch
|
||||
|
||||
// ordering is deliberately left to the caller so aggregate-only consumers (counts, stats)
|
||||
// can compose the same filters without stripping an order by
|
||||
export function searchAssetBuilder(kysely: Kysely<DB>, options: AssetSearchBuilderV3Options) {
|
||||
export function searchAssetBuilder(kysely: Kysely<DB>, options: AssetSearchBuilderV3Options, scope: AssetSearchScope) {
|
||||
const filter = options.filter ?? {};
|
||||
|
||||
return (
|
||||
@@ -772,8 +776,9 @@ export function searchAssetBuilder(kysely: Kysely<DB>, options: AssetSearchBuild
|
||||
// postgres eliminates the left join when no exif column is referenced, so unused joins are free
|
||||
.leftJoin('asset_exif', 'asset.id', 'asset_exif.assetId')
|
||||
.$if(!!options.withExif, (qb) => qb.select(selectExifInfo))
|
||||
.$if(!!options.userIds && options.userIds.length > 0, (qb) =>
|
||||
qb.where('asset.ownerId', '=', anyUuid(options.userIds!)),
|
||||
.$if(!!scope.userIds && scope.userIds.length > 0, (qb) => qb.where('asset.ownerId', '=', anyUuid(scope.userIds!)))
|
||||
.where((eb) =>
|
||||
eb.or([eb('asset.visibility', '!=', AssetVisibility.Locked), eb('asset.ownerId', '=', scope.lockedOwnerId)]),
|
||||
)
|
||||
.$if(!!(options.withFaces || options.withPeople), (qb) => qb.select(withFacesAndPeople))
|
||||
.$if(options.withStacked === false, (qb) => qb.where('asset.stackId', 'is', null))
|
||||
@@ -809,86 +814,121 @@ 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 }, { userIds: [DummyValue.UUID] }] },
|
||||
{ name: 'empty', params: [{ size: 100 }, {}] },
|
||||
{ name: 'baseline', params: [{ size: 100 }, {}, scopeExample] },
|
||||
{ name: 'empty', params: [{ size: 100 }, {}, { lockedOwnerId: DummyValue.UUID }] },
|
||||
{
|
||||
name: 'or-exif-only',
|
||||
params: [{ size: 100 }, { userIds: [DummyValue.UUID], filter: { or: [{ city: { eq: DummyValue.STRING } }] } }],
|
||||
params: [
|
||||
{ size: 100 },
|
||||
{
|
||||
filter: { or: [{ city: { eq: DummyValue.STRING } }] },
|
||||
},
|
||||
scopeExample,
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'string-eq-null',
|
||||
params: [{ size: 100 }, { userIds: [DummyValue.UUID], filter: { city: { eq: null } } }],
|
||||
params: [{ size: 100 }, { filter: { city: { eq: null } } }, scopeExample],
|
||||
},
|
||||
{
|
||||
name: 'string-pattern-like',
|
||||
params: [{ size: 100 }, { userIds: [DummyValue.UUID], filter: { description: { like: DummyValue.STRING } } }],
|
||||
params: [
|
||||
{ size: 100 },
|
||||
{
|
||||
filter: { description: { like: DummyValue.STRING } },
|
||||
},
|
||||
scopeExample,
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'string-pattern-notLike',
|
||||
params: [{ size: 100 }, { userIds: [DummyValue.UUID], filter: { description: { notLike: DummyValue.STRING } } }],
|
||||
params: [
|
||||
{ size: 100 },
|
||||
{
|
||||
filter: { description: { notLike: DummyValue.STRING } },
|
||||
},
|
||||
scopeExample,
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'string-pattern-startsWith',
|
||||
params: [
|
||||
{ size: 100 },
|
||||
{ userIds: [DummyValue.UUID], filter: { originalFileName: { startsWith: DummyValue.STRING } } },
|
||||
{
|
||||
filter: { originalFileName: { startsWith: DummyValue.STRING } },
|
||||
},
|
||||
scopeExample,
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'string-similarity-ocr',
|
||||
params: [{ size: 100 }, { userIds: [DummyValue.UUID], filter: { ocr: { matches: DummyValue.STRING } } }],
|
||||
params: [{ size: 100 }, { filter: { ocr: { matches: DummyValue.STRING } } }, scopeExample],
|
||||
},
|
||||
{
|
||||
name: 'ids-any',
|
||||
params: [{ size: 100 }, { userIds: [DummyValue.UUID], filter: { albumIds: { any: [DummyValue.UUID] } } }],
|
||||
params: [{ size: 100 }, { filter: { albumIds: { any: [DummyValue.UUID] } } }, scopeExample],
|
||||
},
|
||||
{
|
||||
name: 'ids-all',
|
||||
params: [
|
||||
{ size: 100 },
|
||||
{ userIds: [DummyValue.UUID], filter: { personIds: { all: [DummyValue.UUID, DummyValue.UUID_1] } } },
|
||||
{
|
||||
filter: { personIds: { all: [DummyValue.UUID, DummyValue.UUID_1] } },
|
||||
},
|
||||
scopeExample,
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'ids-all-single',
|
||||
params: [{ size: 100 }, { userIds: [DummyValue.UUID], filter: { albumIds: { all: [DummyValue.UUID] } } }],
|
||||
params: [{ size: 100 }, { filter: { albumIds: { all: [DummyValue.UUID] } } }, scopeExample],
|
||||
},
|
||||
{
|
||||
name: 'ids-none',
|
||||
params: [{ size: 100 }, { userIds: [DummyValue.UUID], filter: { tagIds: { none: [DummyValue.UUID] } } }],
|
||||
params: [{ size: 100 }, { filter: { tagIds: { none: [DummyValue.UUID] } } }, scopeExample],
|
||||
},
|
||||
{
|
||||
name: 'ids-tags-all',
|
||||
params: [
|
||||
{ size: 100 },
|
||||
{ userIds: [DummyValue.UUID], filter: { tagIds: { all: [DummyValue.UUID, DummyValue.UUID_1] } } },
|
||||
{
|
||||
filter: { tagIds: { all: [DummyValue.UUID, DummyValue.UUID_1] } },
|
||||
},
|
||||
scopeExample,
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'has-albums-false',
|
||||
params: [{ size: 100 }, { userIds: [DummyValue.UUID], filter: { hasAlbums: { eq: false } } }],
|
||||
params: [{ size: 100 }, { filter: { hasAlbums: { eq: false } } }, scopeExample],
|
||||
},
|
||||
{
|
||||
name: 'is-encoded',
|
||||
params: [{ size: 100 }, { userIds: [DummyValue.UUID], filter: { isEncoded: { eq: true } } }],
|
||||
params: [{ size: 100 }, { filter: { isEncoded: { eq: true } } }, scopeExample],
|
||||
},
|
||||
{
|
||||
name: 'number-range',
|
||||
params: [{ size: 100 }, { userIds: [DummyValue.UUID], filter: { fileSizeInBytes: { gte: 100, lte: 1000 } } }],
|
||||
params: [
|
||||
{ size: 100 },
|
||||
{
|
||||
filter: { fileSizeInBytes: { gte: 100, lte: 1000 } },
|
||||
},
|
||||
scopeExample,
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'date-eq',
|
||||
params: [{ size: 100 }, { userIds: [DummyValue.UUID], filter: { takenAt: { eq: DummyValue.DATE } } }],
|
||||
params: [{ size: 100 }, { filter: { takenAt: { eq: DummyValue.DATE } } }, scopeExample],
|
||||
},
|
||||
{
|
||||
name: 'date-range',
|
||||
params: [
|
||||
{ size: 100 },
|
||||
{
|
||||
userIds: [DummyValue.UUID],
|
||||
filter: { takenAt: { gte: DummyValue.DATE, lt: DummyValue.DATE } },
|
||||
},
|
||||
scopeExample,
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -896,10 +936,10 @@ export const searchMetadataV3Examples: GenerateSqlQueries[] = [
|
||||
params: [
|
||||
{ size: 100 },
|
||||
{
|
||||
userIds: [DummyValue.UUID],
|
||||
order: { field: SearchOrderField.FileSizeInBytes, direction: AssetOrder.Desc },
|
||||
withExif: false,
|
||||
},
|
||||
scopeExample,
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -907,10 +947,10 @@ export const searchMetadataV3Examples: GenerateSqlQueries[] = [
|
||||
params: [
|
||||
{ size: 100 },
|
||||
{
|
||||
userIds: [DummyValue.UUID],
|
||||
order: { field: SearchOrderField.Rating, direction: AssetOrder.Asc },
|
||||
withExif: true,
|
||||
},
|
||||
scopeExample,
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -918,11 +958,11 @@ export const searchMetadataV3Examples: GenerateSqlQueries[] = [
|
||||
params: [
|
||||
{ size: 100 },
|
||||
{
|
||||
userIds: [DummyValue.UUID],
|
||||
filter: {
|
||||
or: [{ isFavorite: { eq: true } }, { personIds: { any: [DummyValue.UUID] } }],
|
||||
},
|
||||
},
|
||||
scopeExample,
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -930,76 +970,73 @@ export const searchMetadataV3Examples: GenerateSqlQueries[] = [
|
||||
params: [
|
||||
{ size: 100 },
|
||||
{
|
||||
userIds: [DummyValue.UUID],
|
||||
filter: {
|
||||
takenAt: { gte: DummyValue.DATE, lt: DummyValue.DATE },
|
||||
or: [{ isFavorite: { eq: true } }, { albumIds: { any: [DummyValue.UUID] } }],
|
||||
},
|
||||
},
|
||||
scopeExample,
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'cursor-offset',
|
||||
params: [
|
||||
{ size: 100, offset: 100 },
|
||||
{ userIds: [DummyValue.UUID], filter: { isFavorite: { eq: true } } },
|
||||
],
|
||||
params: [{ size: 100, offset: 100 }, { filter: { isFavorite: { eq: true } } }, scopeExample],
|
||||
},
|
||||
];
|
||||
|
||||
export const searchRandomV3Examples: GenerateSqlQueries[] = [
|
||||
{ name: 'baseline', params: [100, { userIds: [DummyValue.UUID] }] },
|
||||
{ name: 'baseline', params: [100, {}, scopeExample] },
|
||||
{
|
||||
name: 'with-filter',
|
||||
params: [100, { userIds: [DummyValue.UUID], filter: { isFavorite: { eq: true } } }],
|
||||
params: [100, { filter: { isFavorite: { eq: true } } }, scopeExample],
|
||||
},
|
||||
];
|
||||
|
||||
export const searchSmartV3Examples: GenerateSqlQueries[] = [
|
||||
{ name: 'baseline', params: [{ size: 100 }, { userIds: [DummyValue.UUID], embedding: DummyValue.VECTOR }] },
|
||||
{
|
||||
name: 'baseline',
|
||||
params: [{ size: 100 }, { embedding: DummyValue.VECTOR }, scopeExample],
|
||||
},
|
||||
{
|
||||
name: 'with-filter',
|
||||
params: [
|
||||
{ size: 100 },
|
||||
{
|
||||
userIds: [DummyValue.UUID],
|
||||
embedding: DummyValue.VECTOR,
|
||||
filter: { takenAt: { gte: DummyValue.DATE, lt: DummyValue.DATE } },
|
||||
},
|
||||
scopeExample,
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'cursor-offset',
|
||||
params: [
|
||||
{ size: 100, offset: 100 },
|
||||
{ userIds: [DummyValue.UUID], embedding: DummyValue.VECTOR },
|
||||
],
|
||||
params: [{ size: 100, offset: 100 }, { embedding: DummyValue.VECTOR }, scopeExample],
|
||||
},
|
||||
];
|
||||
|
||||
export const searchStatisticsV3Examples: GenerateSqlQueries[] = [
|
||||
{ name: 'baseline', params: [{ userIds: [DummyValue.UUID] }] },
|
||||
{ name: 'baseline', params: [{}, scopeExample] },
|
||||
{
|
||||
name: 'with-filter',
|
||||
params: [
|
||||
{
|
||||
userIds: [DummyValue.UUID],
|
||||
filter: {
|
||||
takenAt: { gte: DummyValue.DATE, lt: DummyValue.DATE },
|
||||
fileSizeInBytes: { gte: 100 },
|
||||
},
|
||||
},
|
||||
scopeExample,
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'with-or',
|
||||
params: [
|
||||
{
|
||||
userIds: [DummyValue.UUID],
|
||||
filter: {
|
||||
or: [{ isFavorite: { eq: true } }, { hasAlbums: { eq: false } }],
|
||||
},
|
||||
},
|
||||
scopeExample,
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
applyLockedVisibilityPolicy,
|
||||
collectFilterIds,
|
||||
hasTopLevelPositiveIdsConstraint,
|
||||
isLockedOnlyFilter,
|
||||
} from 'src/utils/search-filter';
|
||||
import { AuthFactory } from 'test/factories/auth.factory';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
@@ -67,25 +66,6 @@ describe(applyLockedVisibilityPolicy.name, () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe(isLockedOnlyFilter.name, () => {
|
||||
it('should detect provably locked-only filters', () => {
|
||||
expect(isLockedOnlyFilter({ visibility: { eq: AssetVisibility.Locked } })).toBe(true);
|
||||
expect(isLockedOnlyFilter({ visibility: { in: [AssetVisibility.Locked] } })).toBe(true);
|
||||
expect(
|
||||
isLockedOnlyFilter({
|
||||
visibility: { notIn: [AssetVisibility.Timeline, AssetVisibility.Archive, AssetVisibility.Hidden] },
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('should not match other filters', () => {
|
||||
expect(isLockedOnlyFilter({ visibility: { ne: AssetVisibility.Locked } })).toBe(false);
|
||||
expect(isLockedOnlyFilter({ visibility: { in: [AssetVisibility.Locked, AssetVisibility.Timeline] } })).toBe(false);
|
||||
expect(isLockedOnlyFilter({ or: [{ visibility: { eq: AssetVisibility.Locked } }] })).toBe(false);
|
||||
expect(isLockedOnlyFilter({})).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe(collectFilterIds.name, () => {
|
||||
it('should union and dedupe ids across operators and branches', () => {
|
||||
const [albumA, albumB, albumC] = [
|
||||
|
||||
@@ -18,9 +18,6 @@ const canMatch = <T>(condition: EnumCondition<T>, value: T): boolean =>
|
||||
(condition.in === undefined || condition.in.includes(value)) &&
|
||||
(condition.notIn === undefined || !condition.notIn.includes(value));
|
||||
|
||||
const matchesOnly = <T>(condition: EnumCondition<T>, value: T, values: T[]): boolean =>
|
||||
values.every((other) => other === value || !canMatch(condition, other));
|
||||
|
||||
/**
|
||||
* The conditions that decide which `field` values the filter can return. A top-level condition decides alone, otherwise each branch itself.
|
||||
*/
|
||||
@@ -54,10 +51,6 @@ export const applyLockedVisibilityPolicy = (auth: AuthDto, filter: SearchFilter)
|
||||
return { ...filter, visibility: { ne: AssetVisibility.Locked } };
|
||||
};
|
||||
|
||||
/** Whether the top-level visibility condition limits the filter to locked assets only. */
|
||||
export const isLockedOnlyFilter = ({ visibility }: SearchFilter): boolean =>
|
||||
visibility !== undefined && matchesOnly(visibility, AssetVisibility.Locked, Object.values(AssetVisibility));
|
||||
|
||||
export const collectFilterIds = (filter: SearchFilter, field: IdsFilterField): string[] => {
|
||||
const ids = new Set<string>();
|
||||
|
||||
|
||||
@@ -331,6 +331,25 @@ describe(SearchService.name, () => {
|
||||
expect(response.assets.items).toEqual([expect.objectContaining({ id: ownLocked.id })]);
|
||||
});
|
||||
|
||||
it('should never return partner locked assets, even for locked-matching mixed filters', async () => {
|
||||
const { sut, ctx } = setup();
|
||||
const { user } = await ctx.newUser();
|
||||
const { user: partner } = await ctx.newUser();
|
||||
await ctx.newPartner({ sharedById: partner.id, sharedWithId: user.id });
|
||||
const { asset: ownLocked } = await ctx.newAsset({ ownerId: user.id, visibility: AssetVisibility.Locked });
|
||||
const { asset: partnerTimeline } = await ctx.newAsset({ ownerId: partner.id });
|
||||
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, {
|
||||
size: 250,
|
||||
filter: { visibility: { in: [AssetVisibility.Locked, AssetVisibility.Timeline] } },
|
||||
});
|
||||
|
||||
const ids = response.assets.items.map(({ id }) => id);
|
||||
expect(ids.toSorted()).toEqual([ownLocked.id, partnerTimeline.id].toSorted());
|
||||
});
|
||||
|
||||
it('should paginate with an opaque cursor', async () => {
|
||||
const { sut, ctx } = setup();
|
||||
const { user } = await ctx.newUser();
|
||||
@@ -385,13 +404,14 @@ describe(SearchService.name, () => {
|
||||
assetIds.push(asset.id);
|
||||
}
|
||||
|
||||
const options = { filter: {}, userIds: [user.id], embedding: unitVector(0) };
|
||||
const firstPage = await searchRepository.searchSmartV3({ size: 2 }, options);
|
||||
const options = { filter: {}, embedding: unitVector(0) };
|
||||
const scope = { userIds: [user.id], lockedOwnerId: user.id };
|
||||
const firstPage = await searchRepository.searchSmartV3({ size: 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);
|
||||
const secondPage = await searchRepository.searchSmartV3({ size: 2, offset: 2 }, options, scope);
|
||||
expect(secondPage.items.length).toBe(1);
|
||||
expect(secondPage.hasNextPage).toBe(false);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user