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:
parent
15a4a4aaaa
commit
c89d91e006
8 changed files with 45 additions and 15 deletions
1
mobile/openapi/doc/SmartSearchDto.md
generated
1
mobile/openapi/doc/SmartSearchDto.md
generated
|
@ -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]
|
||||
|
|
11
mobile/openapi/lib/model/smart_search_dto.dart
generated
11
mobile/openapi/lib/model/smart_search_dto.dart
generated
|
@ -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'),
|
||||
|
|
5
mobile/openapi/test/smart_search_dto_test.dart
generated
5
mobile/openapi/test/smart_search_dto_test.dart
generated
|
@ -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
|
||||
|
|
|
@ -9539,6 +9539,12 @@
|
|||
"page": {
|
||||
"type": "number"
|
||||
},
|
||||
"personIds": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"query": {
|
||||
"type": "string"
|
||||
},
|
||||
|
|
|
@ -671,6 +671,7 @@ export type SmartSearchDto = {
|
|||
make?: string;
|
||||
model?: string;
|
||||
page?: number;
|
||||
personIds?: string[];
|
||||
query: string;
|
||||
size?: number;
|
||||
state?: string;
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Add table
Reference in a new issue