You've already forked immich
mirror of
https://github.com/immich-app/immich.git
synced 2025-08-09 23:17:29 +02:00
feat(server): add /search/statistics resource (#18885)
This commit is contained in:
@@ -11,8 +11,10 @@ import {
|
||||
SearchPeopleDto,
|
||||
SearchPlacesDto,
|
||||
SearchResponseDto,
|
||||
SearchStatisticsResponseDto,
|
||||
SearchSuggestionRequestDto,
|
||||
SmartSearchDto,
|
||||
StatisticsSearchDto,
|
||||
} from 'src/dtos/search.dto';
|
||||
import { Auth, Authenticated } from 'src/middleware/auth.guard';
|
||||
import { SearchService } from 'src/services/search.service';
|
||||
@@ -29,6 +31,13 @@ export class SearchController {
|
||||
return this.service.searchMetadata(auth, dto);
|
||||
}
|
||||
|
||||
@Post('statistics')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Authenticated()
|
||||
searchAssetStatistics(@Auth() auth: AuthDto, @Body() dto: StatisticsSearchDto): Promise<SearchStatisticsResponseDto> {
|
||||
return this.service.searchStatistics(auth, dto);
|
||||
}
|
||||
|
||||
@Post('random')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Authenticated()
|
||||
|
@@ -37,12 +37,6 @@ class BaseSearchDto {
|
||||
@ValidateAssetVisibility({ optional: true })
|
||||
visibility?: AssetVisibility;
|
||||
|
||||
@ValidateBoolean({ optional: true })
|
||||
withDeleted?: boolean;
|
||||
|
||||
@ValidateBoolean({ optional: true })
|
||||
withExif?: boolean;
|
||||
|
||||
@ValidateDate({ optional: true })
|
||||
createdBefore?: Date;
|
||||
|
||||
@@ -92,13 +86,6 @@ class BaseSearchDto {
|
||||
@Optional({ nullable: true, emptyToNull: true })
|
||||
lensModel?: string | null;
|
||||
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
@Max(1000)
|
||||
@Type(() => Number)
|
||||
@Optional()
|
||||
size?: number;
|
||||
|
||||
@ValidateBoolean({ optional: true })
|
||||
isNotInAlbum?: boolean;
|
||||
|
||||
@@ -115,7 +102,22 @@ class BaseSearchDto {
|
||||
rating?: number;
|
||||
}
|
||||
|
||||
export class RandomSearchDto extends BaseSearchDto {
|
||||
class BaseSearchWithResultsDto extends BaseSearchDto {
|
||||
@ValidateBoolean({ optional: true })
|
||||
withDeleted?: boolean;
|
||||
|
||||
@ValidateBoolean({ optional: true })
|
||||
withExif?: boolean;
|
||||
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
@Max(1000)
|
||||
@Type(() => Number)
|
||||
@Optional()
|
||||
size?: number;
|
||||
}
|
||||
|
||||
export class RandomSearchDto extends BaseSearchWithResultsDto {
|
||||
@ValidateBoolean({ optional: true })
|
||||
withStacked?: boolean;
|
||||
|
||||
@@ -179,7 +181,14 @@ export class MetadataSearchDto extends RandomSearchDto {
|
||||
page?: number;
|
||||
}
|
||||
|
||||
export class SmartSearchDto extends BaseSearchDto {
|
||||
export class StatisticsSearchDto extends BaseSearchDto {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@Optional()
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export class SmartSearchDto extends BaseSearchWithResultsDto {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
query!: string;
|
||||
@@ -299,6 +308,11 @@ export class SearchResponseDto {
|
||||
assets!: SearchAssetResponseDto;
|
||||
}
|
||||
|
||||
export class SearchStatisticsResponseDto {
|
||||
@ApiProperty({ type: 'integer' })
|
||||
total!: number;
|
||||
}
|
||||
|
||||
class SearchExploreItem {
|
||||
value!: string;
|
||||
data!: AssetResponseDto;
|
||||
|
@@ -20,6 +20,20 @@ limit
|
||||
offset
|
||||
$7
|
||||
|
||||
-- SearchRepository.searchStatistics
|
||||
select
|
||||
count(*) as "total"
|
||||
from
|
||||
"assets"
|
||||
inner join "exif" on "assets"."id" = "exif"."assetId"
|
||||
where
|
||||
"assets"."visibility" = $1
|
||||
and "assets"."fileCreatedAt" >= $2
|
||||
and "exif"."lensModel" = $3
|
||||
and "assets"."ownerId" = any ($4::uuid[])
|
||||
and "assets"."isFavorite" = $5
|
||||
and "assets"."deletedAt" is null
|
||||
|
||||
-- SearchRepository.searchRandom
|
||||
(
|
||||
select
|
||||
|
@@ -185,6 +185,7 @@ export class SearchRepository {
|
||||
async searchMetadata(pagination: SearchPaginationOptions, options: AssetSearchOptions) {
|
||||
const orderDirection = (options.orderDirection?.toLowerCase() || 'desc') as OrderByDirection;
|
||||
const items = await searchAssetBuilder(this.db, options)
|
||||
.selectAll('assets')
|
||||
.orderBy('assets.fileCreatedAt', orderDirection)
|
||||
.limit(pagination.size + 1)
|
||||
.offset((pagination.page - 1) * pagination.size)
|
||||
@@ -193,6 +194,22 @@ export class SearchRepository {
|
||||
return paginationHelper(items, pagination.size);
|
||||
}
|
||||
|
||||
@GenerateSql({
|
||||
params: [
|
||||
{
|
||||
takenAfter: DummyValue.DATE,
|
||||
lensModel: DummyValue.STRING,
|
||||
isFavorite: true,
|
||||
userIds: [DummyValue.UUID],
|
||||
},
|
||||
],
|
||||
})
|
||||
searchStatistics(options: AssetSearchOptions) {
|
||||
return searchAssetBuilder(this.db, options)
|
||||
.select((qb) => qb.fn.countAll<number>().as('total'))
|
||||
.executeTakeFirstOrThrow();
|
||||
}
|
||||
|
||||
@GenerateSql({
|
||||
params: [
|
||||
100,
|
||||
@@ -209,10 +226,12 @@ export class SearchRepository {
|
||||
const uuid = randomUUID();
|
||||
const builder = searchAssetBuilder(this.db, options);
|
||||
const lessThan = builder
|
||||
.selectAll('assets')
|
||||
.where('assets.id', '<', uuid)
|
||||
.orderBy(sql`random()`)
|
||||
.limit(size);
|
||||
const greaterThan = builder
|
||||
.selectAll('assets')
|
||||
.where('assets.id', '>', uuid)
|
||||
.orderBy(sql`random()`)
|
||||
.limit(size);
|
||||
@@ -241,6 +260,7 @@ export class SearchRepository {
|
||||
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)
|
||||
.selectAll('assets')
|
||||
.innerJoin('smart_search', 'assets.id', 'smart_search.assetId')
|
||||
.orderBy(sql`smart_search.embedding <=> ${options.embedding}`)
|
||||
.limit(pagination.size + 1)
|
||||
|
@@ -10,9 +10,11 @@ import {
|
||||
SearchPeopleDto,
|
||||
SearchPlacesDto,
|
||||
SearchResponseDto,
|
||||
SearchStatisticsResponseDto,
|
||||
SearchSuggestionRequestDto,
|
||||
SearchSuggestionType,
|
||||
SmartSearchDto,
|
||||
StatisticsSearchDto,
|
||||
} from 'src/dtos/search.dto';
|
||||
import { AssetOrder, AssetVisibility } from 'src/enum';
|
||||
import { BaseService } from 'src/services/base.service';
|
||||
@@ -67,6 +69,15 @@ export class SearchService extends BaseService {
|
||||
return this.mapResponse(items, hasNextPage ? (page + 1).toString() : null, { auth });
|
||||
}
|
||||
|
||||
async searchStatistics(auth: AuthDto, dto: StatisticsSearchDto): Promise<SearchStatisticsResponseDto> {
|
||||
const userIds = await this.getUserIdsToSearch(auth);
|
||||
|
||||
return await this.searchRepository.searchStatistics({
|
||||
...dto,
|
||||
userIds,
|
||||
});
|
||||
}
|
||||
|
||||
async searchRandom(auth: AuthDto, dto: RandomSearchDto): Promise<AssetResponseDto[]> {
|
||||
if (dto.visibility === AssetVisibility.LOCKED) {
|
||||
requireElevatedPermission(auth);
|
||||
|
@@ -291,7 +291,6 @@ export function searchAssetBuilder(kysely: Kysely<DB>, options: AssetSearchBuild
|
||||
return kysely
|
||||
.withPlugin(joinDeduplicationPlugin)
|
||||
.selectFrom('assets')
|
||||
.selectAll('assets')
|
||||
.where('assets.visibility', '=', visibility)
|
||||
.$if(!!options.tagIds && options.tagIds.length > 0, (qb) => hasTags(qb, options.tagIds!))
|
||||
.$if(!!options.personIds && options.personIds.length > 0, (qb) => hasPeople(qb, options.personIds!))
|
||||
|
Reference in New Issue
Block a user