mirror of
https://github.com/immich-app/immich
synced 2025-11-14 17:36:12 +00:00
feat: enhance search (#7127)
* feat: hybrid search * fixing normal search * building out the query * okla * filters * date * order by date * Remove hybrid search endpoint * remove search hybrid endpoint * faces query * search for person * search and pagination * with exif * with exif * justify gallery viewer * memory view * Fixed userId is null * openapi and styling * searchdto * lint and format * remove term * generate sql * fix test * chips * not showing true * pr feedback * pr feedback * nit name * linting * pr feedback * styling * linting
This commit is contained in:
parent
60ba37b3a7
commit
69983ff83a
40 changed files with 3400 additions and 2585 deletions
|
|
@ -66,13 +66,14 @@ export interface SearchAssetIDOptions {
|
|||
id?: string;
|
||||
}
|
||||
|
||||
export interface SearchUserIDOptions {
|
||||
export interface SearchUserIdOptions {
|
||||
deviceId?: string;
|
||||
libraryId?: string;
|
||||
ownerId?: string;
|
||||
userIds?: string[];
|
||||
}
|
||||
|
||||
export type SearchIDOptions = SearchAssetIDOptions & SearchUserIDOptions;
|
||||
export type SearchIdOptions = SearchAssetIDOptions & SearchUserIdOptions;
|
||||
|
||||
export interface SearchStatusOptions {
|
||||
isArchived?: boolean;
|
||||
|
|
@ -83,6 +84,7 @@ export interface SearchStatusOptions {
|
|||
isOffline?: boolean;
|
||||
isReadOnly?: boolean;
|
||||
isVisible?: boolean;
|
||||
isNotInAlbum?: boolean;
|
||||
type?: AssetType;
|
||||
withArchived?: boolean;
|
||||
withDeleted?: boolean;
|
||||
|
|
@ -132,6 +134,10 @@ export interface SearchEmbeddingOptions {
|
|||
userIds: string[];
|
||||
}
|
||||
|
||||
export interface SearchPeopleOptions {
|
||||
personIds?: string[];
|
||||
}
|
||||
|
||||
export interface SearchOrderOptions {
|
||||
orderDirection?: 'ASC' | 'DESC';
|
||||
}
|
||||
|
|
@ -142,12 +148,14 @@ export interface SearchPaginationOptions {
|
|||
}
|
||||
|
||||
export type AssetSearchOptions = SearchDateOptions &
|
||||
SearchIDOptions &
|
||||
SearchIdOptions &
|
||||
SearchExifOptions &
|
||||
SearchOrderOptions &
|
||||
SearchPathOptions &
|
||||
SearchRelationOptions &
|
||||
SearchStatusOptions;
|
||||
SearchStatusOptions &
|
||||
SearchUserIdOptions &
|
||||
SearchPeopleOptions;
|
||||
|
||||
export type AssetSearchBuilderOptions = Omit<AssetSearchOptions, 'orderDirection'>;
|
||||
|
||||
|
|
@ -156,7 +164,8 @@ export type SmartSearchOptions = SearchDateOptions &
|
|||
SearchExifOptions &
|
||||
SearchOneToOneRelationOptions &
|
||||
SearchStatusOptions &
|
||||
SearchUserIDOptions;
|
||||
SearchUserIdOptions &
|
||||
SearchPeopleOptions;
|
||||
|
||||
export interface FaceEmbeddingSearch extends SearchEmbeddingOptions {
|
||||
hasPerson?: boolean;
|
||||
|
|
|
|||
|
|
@ -169,6 +169,12 @@ export class MetadataSearchDto extends BaseSearchDto {
|
|||
@Optional()
|
||||
@ApiProperty({ enumName: 'AssetOrder', enum: AssetOrder })
|
||||
order?: AssetOrder;
|
||||
|
||||
@QueryBoolean({ optional: true })
|
||||
isNotInAlbum?: boolean;
|
||||
|
||||
@Optional()
|
||||
personIds?: string[];
|
||||
}
|
||||
|
||||
export class SmartSearchDto extends BaseSearchDto {
|
||||
|
|
|
|||
|
|
@ -60,6 +60,7 @@ export class SearchService {
|
|||
|
||||
async searchMetadata(auth: AuthDto, dto: MetadataSearchDto): Promise<SearchResponseDto> {
|
||||
let checksum: Buffer | undefined;
|
||||
const userIds = await this.getUserIdsToSearch(auth);
|
||||
|
||||
if (dto.checksum) {
|
||||
const encoding = dto.checksum.length === 28 ? 'base64' : 'hex';
|
||||
|
|
@ -74,7 +75,7 @@ export class SearchService {
|
|||
{
|
||||
...dto,
|
||||
checksum,
|
||||
ownerId: auth.user.id,
|
||||
userIds,
|
||||
orderDirection: dto.order ? enumToOrder[dto.order] : 'DESC',
|
||||
},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import {
|
|||
SmartSearchDto,
|
||||
} from '@app/domain';
|
||||
import { SearchSuggestionRequestDto } from '@app/domain/search/dto/search-suggestion.dto';
|
||||
import { Controller, Get, Query } from '@nestjs/common';
|
||||
import { Body, Controller, Get, Post, Query } from '@nestjs/common';
|
||||
import { ApiOperation, ApiTags } from '@nestjs/swagger';
|
||||
import { Auth, Authenticated } from '../app.guard';
|
||||
import { UseValidation } from '../app.utils';
|
||||
|
|
@ -22,13 +22,13 @@ import { UseValidation } from '../app.utils';
|
|||
export class SearchController {
|
||||
constructor(private service: SearchService) {}
|
||||
|
||||
@Get('metadata')
|
||||
searchMetadata(@Auth() auth: AuthDto, @Query() dto: MetadataSearchDto): Promise<SearchResponseDto> {
|
||||
@Post('metadata')
|
||||
searchMetadata(@Auth() auth: AuthDto, @Body() dto: MetadataSearchDto): Promise<SearchResponseDto> {
|
||||
return this.service.searchMetadata(auth, dto);
|
||||
}
|
||||
|
||||
@Get('smart')
|
||||
searchSmart(@Auth() auth: AuthDto, @Query() dto: SmartSearchDto): Promise<SearchResponseDto> {
|
||||
@Post('smart')
|
||||
searchSmart(@Auth() auth: AuthDto, @Body() dto: SmartSearchDto): Promise<SearchResponseDto> {
|
||||
return this.service.searchSmart(auth, dto);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -139,14 +139,27 @@ export function searchAssetBuilder(
|
|||
);
|
||||
|
||||
const exifInfo = _.omitBy(_.pick(options, ['city', 'country', 'lensModel', 'make', 'model', 'state']), _.isUndefined);
|
||||
if (Object.keys(exifInfo).length > 0) {
|
||||
builder.leftJoin(`${builder.alias}.exifInfo`, 'exifInfo');
|
||||
const hasExifQuery = Object.keys(exifInfo).length > 0;
|
||||
|
||||
if (options.withExif && !hasExifQuery) {
|
||||
builder.leftJoinAndSelect(`${builder.alias}.exifInfo`, 'exifInfo');
|
||||
}
|
||||
|
||||
if (hasExifQuery) {
|
||||
options.withExif
|
||||
? builder.leftJoinAndSelect(`${builder.alias}.exifInfo`, 'exifInfo')
|
||||
: builder.leftJoin(`${builder.alias}.exifInfo`, 'exifInfo');
|
||||
|
||||
builder.andWhere({ exifInfo });
|
||||
}
|
||||
|
||||
const id = _.pick(options, ['checksum', 'deviceAssetId', 'deviceId', 'id', 'libraryId', 'ownerId']);
|
||||
const id = _.pick(options, ['checksum', 'deviceAssetId', 'deviceId', 'id', 'libraryId']);
|
||||
builder.andWhere(_.omitBy(id, _.isUndefined));
|
||||
|
||||
if (options.userIds) {
|
||||
builder.andWhere(`${builder.alias}.ownerId IN (:...userIds)`, { userIds: options.userIds });
|
||||
}
|
||||
|
||||
const path = _.pick(options, ['encodedVideoPath', 'originalFileName', 'originalPath', 'resizePath', 'webpPath']);
|
||||
builder.andWhere(_.omitBy(path, _.isUndefined));
|
||||
|
||||
|
|
@ -164,8 +177,8 @@ export function searchAssetBuilder(
|
|||
),
|
||||
);
|
||||
|
||||
if (options.withExif) {
|
||||
builder.leftJoinAndSelect(`${builder.alias}.exifInfo`, 'exifInfo');
|
||||
if (options.isNotInAlbum) {
|
||||
builder.leftJoin(`${builder.alias}.albums`, 'albums').andWhere('albums.id IS NULL');
|
||||
}
|
||||
|
||||
if (options.withFaces || options.withPeople) {
|
||||
|
|
@ -180,6 +193,18 @@ export function searchAssetBuilder(
|
|||
builder.leftJoinAndSelect(`${builder.alias}.smartInfo`, 'smartInfo');
|
||||
}
|
||||
|
||||
if (options.personIds && options.personIds.length > 0) {
|
||||
builder
|
||||
.leftJoin(`${builder.alias}.faces`, 'faces')
|
||||
.andWhere('faces.personId IN (:...personIds)', { personIds: options.personIds })
|
||||
.addGroupBy(`${builder.alias}.id`)
|
||||
.having('COUNT(faces.id) = :personCount', { personCount: options.personIds.length });
|
||||
|
||||
if (options.withExif) {
|
||||
builder.addGroupBy('exifInfo.assetId');
|
||||
}
|
||||
}
|
||||
|
||||
if (options.withStacked) {
|
||||
builder
|
||||
.leftJoinAndSelect(`${builder.alias}.stack`, 'stack')
|
||||
|
|
|
|||
|
|
@ -58,6 +58,7 @@ export class SearchRepository implements ISearchRepository {
|
|||
ownerId: DummyValue.UUID,
|
||||
withStacked: true,
|
||||
isFavorite: true,
|
||||
ownerIds: [DummyValue.UUID],
|
||||
},
|
||||
],
|
||||
})
|
||||
|
|
@ -66,7 +67,6 @@ export class SearchRepository implements ISearchRepository {
|
|||
builder = searchAssetBuilder(builder, options);
|
||||
|
||||
builder.orderBy('asset.fileCreatedAt', options.orderDirection ?? 'DESC');
|
||||
|
||||
return paginatedBuilder<AssetEntity>(builder, {
|
||||
mode: PaginationMode.SKIP_TAKE,
|
||||
skip: (pagination.page - 1) * pagination.size,
|
||||
|
|
|
|||
|
|
@ -77,9 +77,9 @@ FROM
|
|||
(
|
||||
"asset"."fileCreatedAt" >= $1
|
||||
AND "exifInfo"."lensModel" = $2
|
||||
AND "asset"."ownerId" = $3
|
||||
AND 1 = 1
|
||||
AND "asset"."isFavorite" = $4
|
||||
AND 1 = 1
|
||||
AND "asset"."isFavorite" = $3
|
||||
AND (
|
||||
"stack"."primaryAssetId" = "asset"."id"
|
||||
OR "asset"."stackId" IS NULL
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue