mirror of
				https://github.com/immich-app/immich.git
				synced 2025-11-03 19:29:32 -05:00 
			
		
		
		
	feat: filter people when using smart search (#7521)
This commit is contained in:
		
							parent
							
								
									15a4a4aaaa
								
							
						
					
					
						commit
						c89d91e006
					
				
							
								
								
									
										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…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user