0
Fork 0
mirror of https://github.com/immich-app/immich.git synced 2025-03-11 02:23:09 -05:00

feat: filter people when using smart search (#7521)

This commit is contained in:
Michel Heusschen 2024-02-29 22:14:48 +01:00 committed by GitHub
parent 15a4a4aaaa
commit c89d91e006
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 45 additions and 15 deletions

View file

@ -27,6 +27,7 @@ Name | Type | Description | Notes
**make** | **String** | | [optional]
**model** | **String** | | [optional]
**page** | **num** | | [optional]
**personIds** | **List<String>** | | [optional] [default to const []]
**query** | **String** | |
**size** | **num** | | [optional]
**state** | **String** | | [optional]

View file

@ -32,6 +32,7 @@ class SmartSearchDto {
this.make,
this.model,
this.page,
this.personIds = const [],
required this.query,
this.size,
this.state,
@ -199,6 +200,8 @@ class SmartSearchDto {
///
num? page;
List<String> personIds;
String query;
///
@ -312,6 +315,7 @@ class SmartSearchDto {
other.make == make &&
other.model == model &&
other.page == page &&
_deepEquality.equals(other.personIds, personIds) &&
other.query == query &&
other.size == size &&
other.state == state &&
@ -348,6 +352,7 @@ class SmartSearchDto {
(make == null ? 0 : make!.hashCode) +
(model == null ? 0 : model!.hashCode) +
(page == null ? 0 : page!.hashCode) +
(personIds.hashCode) +
(query.hashCode) +
(size == null ? 0 : size!.hashCode) +
(state == null ? 0 : state!.hashCode) +
@ -363,7 +368,7 @@ class SmartSearchDto {
(withExif == null ? 0 : withExif!.hashCode);
@override
String toString() => 'SmartSearchDto[city=$city, country=$country, createdAfter=$createdAfter, createdBefore=$createdBefore, deviceId=$deviceId, isArchived=$isArchived, isEncoded=$isEncoded, isExternal=$isExternal, isFavorite=$isFavorite, isMotion=$isMotion, isNotInAlbum=$isNotInAlbum, isOffline=$isOffline, isReadOnly=$isReadOnly, isVisible=$isVisible, lensModel=$lensModel, libraryId=$libraryId, make=$make, model=$model, page=$page, query=$query, size=$size, state=$state, takenAfter=$takenAfter, takenBefore=$takenBefore, trashedAfter=$trashedAfter, trashedBefore=$trashedBefore, type=$type, updatedAfter=$updatedAfter, updatedBefore=$updatedBefore, withArchived=$withArchived, withDeleted=$withDeleted, withExif=$withExif]';
String toString() => 'SmartSearchDto[city=$city, country=$country, createdAfter=$createdAfter, createdBefore=$createdBefore, deviceId=$deviceId, isArchived=$isArchived, isEncoded=$isEncoded, isExternal=$isExternal, isFavorite=$isFavorite, isMotion=$isMotion, isNotInAlbum=$isNotInAlbum, isOffline=$isOffline, isReadOnly=$isReadOnly, isVisible=$isVisible, lensModel=$lensModel, libraryId=$libraryId, make=$make, model=$model, page=$page, personIds=$personIds, query=$query, size=$size, state=$state, takenAfter=$takenAfter, takenBefore=$takenBefore, trashedAfter=$trashedAfter, trashedBefore=$trashedBefore, type=$type, updatedAfter=$updatedAfter, updatedBefore=$updatedBefore, withArchived=$withArchived, withDeleted=$withDeleted, withExif=$withExif]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
@ -462,6 +467,7 @@ class SmartSearchDto {
} else {
// json[r'page'] = null;
}
json[r'personIds'] = this.personIds;
json[r'query'] = this.query;
if (this.size != null) {
json[r'size'] = this.size;
@ -549,6 +555,9 @@ class SmartSearchDto {
make: mapValueOfType<String>(json, r'make'),
model: mapValueOfType<String>(json, r'model'),
page: num.parse('${json[r'page']}'),
personIds: json[r'personIds'] is Iterable
? (json[r'personIds'] as Iterable).cast<String>().toList(growable: false)
: const [],
query: mapValueOfType<String>(json, r'query')!,
size: num.parse('${json[r'size']}'),
state: mapValueOfType<String>(json, r'state'),

View file

@ -111,6 +111,11 @@ void main() {
// TODO
});
// List<String> personIds (default value: const [])
test('to test the property `personIds`', () async {
// TODO
});
// String query
test('to test the property `query`', () async {
// TODO

View file

@ -9539,6 +9539,12 @@
"page": {
"type": "number"
},
"personIds": {
"items": {
"type": "string"
},
"type": "array"
},
"query": {
"type": "string"
},

View file

@ -671,6 +671,7 @@ export type SmartSearchDto = {
make?: string;
model?: string;
page?: number;
personIds?: string[];
query: string;
size?: number;
state?: string;

View file

@ -122,6 +122,9 @@ class BaseSearchDto {
@QueryBoolean({ optional: true })
isNotInAlbum?: boolean;
@Optional()
personIds?: string[];
}
export class MetadataSearchDto extends BaseSearchDto {
@ -173,9 +176,6 @@ export class MetadataSearchDto extends BaseSearchDto {
@Optional()
@ApiProperty({ enumName: 'AssetOrder', enum: AssetOrder })
order?: AssetOrder;
@Optional()
personIds?: string[];
}
export class SmartSearchDto extends BaseSearchDto {

View file

@ -22,7 +22,7 @@ import {
import { ImmichLogger } from '@app/infra/logger';
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Repository, SelectQueryBuilder } from 'typeorm';
import { vectorExt } from '../database.config';
import { DummyValue, GenerateSql } from '../infra.util';
import { asVector, isValidInteger, paginatedBuilder, searchAssetBuilder } from '../infra.utils';
@ -81,6 +81,14 @@ export class SearchRepository implements ISearchRepository {
});
}
private createPersonFilter(builder: SelectQueryBuilder<AssetFaceEntity>, personIds: string[]) {
return builder
.select(`${builder.alias}."assetId"`)
.where(`${builder.alias}."personId" IN (:...personIds)`, { personIds })
.groupBy(`${builder.alias}."assetId"`)
.having(`COUNT(DISTINCT ${builder.alias}."personId") = :personCount`, { personCount: personIds.length });
}
@GenerateSql({
params: [
{ page: 1, size: 100 },
@ -96,12 +104,21 @@ export class SearchRepository implements ISearchRepository {
})
async searchSmart(
pagination: SearchPaginationOptions,
{ embedding, userIds, ...options }: SmartSearchOptions,
{ embedding, userIds, personIds, ...options }: SmartSearchOptions,
): Paginated<AssetEntity> {
let results: PaginationResult<AssetEntity> = { items: [], hasNextPage: false };
await this.assetRepository.manager.transaction(async (manager) => {
let builder = manager.createQueryBuilder(AssetEntity, 'asset');
if (personIds?.length) {
const assetFaceBuilder = manager.createQueryBuilder(AssetFaceEntity, 'asset_face');
const cte = this.createPersonFilter(assetFaceBuilder, personIds);
builder
.addCommonTableExpression(cte, 'asset_face_ids')
.innerJoin('asset_face_ids', 'a', 'a."assetId" = asset.id');
}
builder = searchAssetBuilder(builder, options);
builder
.innerJoin('asset.smartSearch', 'search')

View file

@ -22,7 +22,6 @@
<script lang="ts">
import Button from '$lib/components/elements/buttons/button.svelte';
import { handleError } from '$lib/utils/handle-error';
import { AssetTypeEnum, type SmartSearchDto, type MetadataSearchDto } from '@immich/sdk';
import { createEventDispatcher } from 'svelte';
import { fly } from 'svelte/transition';
@ -83,14 +82,6 @@
};
const search = () => {
if (filter.context && filter.personIds.size > 0) {
handleError(
new Error('Context search does not support people filter'),
'Context search does not support people filter',
);
return;
}
let type: AssetTypeEnum | undefined = undefined;
if (filter.mediaType === MediaType.Image) {
type = AssetTypeEnum.Image;