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:
Alex 2024-02-17 11:00:55 -06:00 committed by GitHub
parent 60ba37b3a7
commit 69983ff83a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
40 changed files with 3400 additions and 2585 deletions

View file

@ -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;

View file

@ -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 {

View file

@ -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',
},
);

View file

@ -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);
}

View file

@ -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')

View file

@ -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,

View file

@ -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