mirror of
				https://github.com/immich-app/immich.git
				synced 2025-11-04 03:39:37 -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] 
 | 
					**make** | **String** |  | [optional] 
 | 
				
			||||||
**model** | **String** |  | [optional] 
 | 
					**model** | **String** |  | [optional] 
 | 
				
			||||||
**page** | **num** |  | [optional] 
 | 
					**page** | **num** |  | [optional] 
 | 
				
			||||||
 | 
					**personIds** | **List<String>** |  | [optional] [default to const []]
 | 
				
			||||||
**query** | **String** |  | 
 | 
					**query** | **String** |  | 
 | 
				
			||||||
**size** | **num** |  | [optional] 
 | 
					**size** | **num** |  | [optional] 
 | 
				
			||||||
**state** | **String** |  | [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.make,
 | 
				
			||||||
    this.model,
 | 
					    this.model,
 | 
				
			||||||
    this.page,
 | 
					    this.page,
 | 
				
			||||||
 | 
					    this.personIds = const [],
 | 
				
			||||||
    required this.query,
 | 
					    required this.query,
 | 
				
			||||||
    this.size,
 | 
					    this.size,
 | 
				
			||||||
    this.state,
 | 
					    this.state,
 | 
				
			||||||
@ -199,6 +200,8 @@ class SmartSearchDto {
 | 
				
			|||||||
  ///
 | 
					  ///
 | 
				
			||||||
  num? page;
 | 
					  num? page;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  List<String> personIds;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  String query;
 | 
					  String query;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  ///
 | 
					  ///
 | 
				
			||||||
@ -312,6 +315,7 @@ class SmartSearchDto {
 | 
				
			|||||||
    other.make == make &&
 | 
					    other.make == make &&
 | 
				
			||||||
    other.model == model &&
 | 
					    other.model == model &&
 | 
				
			||||||
    other.page == page &&
 | 
					    other.page == page &&
 | 
				
			||||||
 | 
					    _deepEquality.equals(other.personIds, personIds) &&
 | 
				
			||||||
    other.query == query &&
 | 
					    other.query == query &&
 | 
				
			||||||
    other.size == size &&
 | 
					    other.size == size &&
 | 
				
			||||||
    other.state == state &&
 | 
					    other.state == state &&
 | 
				
			||||||
@ -348,6 +352,7 @@ class SmartSearchDto {
 | 
				
			|||||||
    (make == null ? 0 : make!.hashCode) +
 | 
					    (make == null ? 0 : make!.hashCode) +
 | 
				
			||||||
    (model == null ? 0 : model!.hashCode) +
 | 
					    (model == null ? 0 : model!.hashCode) +
 | 
				
			||||||
    (page == null ? 0 : page!.hashCode) +
 | 
					    (page == null ? 0 : page!.hashCode) +
 | 
				
			||||||
 | 
					    (personIds.hashCode) +
 | 
				
			||||||
    (query.hashCode) +
 | 
					    (query.hashCode) +
 | 
				
			||||||
    (size == null ? 0 : size!.hashCode) +
 | 
					    (size == null ? 0 : size!.hashCode) +
 | 
				
			||||||
    (state == null ? 0 : state!.hashCode) +
 | 
					    (state == null ? 0 : state!.hashCode) +
 | 
				
			||||||
@ -363,7 +368,7 @@ class SmartSearchDto {
 | 
				
			|||||||
    (withExif == null ? 0 : withExif!.hashCode);
 | 
					    (withExif == null ? 0 : withExif!.hashCode);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @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() {
 | 
					  Map<String, dynamic> toJson() {
 | 
				
			||||||
    final json = <String, dynamic>{};
 | 
					    final json = <String, dynamic>{};
 | 
				
			||||||
@ -462,6 +467,7 @@ class SmartSearchDto {
 | 
				
			|||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
    //  json[r'page'] = null;
 | 
					    //  json[r'page'] = null;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					      json[r'personIds'] = this.personIds;
 | 
				
			||||||
      json[r'query'] = this.query;
 | 
					      json[r'query'] = this.query;
 | 
				
			||||||
    if (this.size != null) {
 | 
					    if (this.size != null) {
 | 
				
			||||||
      json[r'size'] = this.size;
 | 
					      json[r'size'] = this.size;
 | 
				
			||||||
@ -549,6 +555,9 @@ class SmartSearchDto {
 | 
				
			|||||||
        make: mapValueOfType<String>(json, r'make'),
 | 
					        make: mapValueOfType<String>(json, r'make'),
 | 
				
			||||||
        model: mapValueOfType<String>(json, r'model'),
 | 
					        model: mapValueOfType<String>(json, r'model'),
 | 
				
			||||||
        page: num.parse('${json[r'page']}'),
 | 
					        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')!,
 | 
					        query: mapValueOfType<String>(json, r'query')!,
 | 
				
			||||||
        size: num.parse('${json[r'size']}'),
 | 
					        size: num.parse('${json[r'size']}'),
 | 
				
			||||||
        state: mapValueOfType<String>(json, r'state'),
 | 
					        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
 | 
					      // TODO
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // List<String> personIds (default value: const [])
 | 
				
			||||||
 | 
					    test('to test the property `personIds`', () async {
 | 
				
			||||||
 | 
					      // TODO
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // String query
 | 
					    // String query
 | 
				
			||||||
    test('to test the property `query`', () async {
 | 
					    test('to test the property `query`', () async {
 | 
				
			||||||
      // TODO
 | 
					      // TODO
 | 
				
			||||||
 | 
				
			|||||||
@ -9539,6 +9539,12 @@
 | 
				
			|||||||
          "page": {
 | 
					          "page": {
 | 
				
			||||||
            "type": "number"
 | 
					            "type": "number"
 | 
				
			||||||
          },
 | 
					          },
 | 
				
			||||||
 | 
					          "personIds": {
 | 
				
			||||||
 | 
					            "items": {
 | 
				
			||||||
 | 
					              "type": "string"
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            "type": "array"
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
          "query": {
 | 
					          "query": {
 | 
				
			||||||
            "type": "string"
 | 
					            "type": "string"
 | 
				
			||||||
          },
 | 
					          },
 | 
				
			||||||
 | 
				
			|||||||
@ -671,6 +671,7 @@ export type SmartSearchDto = {
 | 
				
			|||||||
    make?: string;
 | 
					    make?: string;
 | 
				
			||||||
    model?: string;
 | 
					    model?: string;
 | 
				
			||||||
    page?: number;
 | 
					    page?: number;
 | 
				
			||||||
 | 
					    personIds?: string[];
 | 
				
			||||||
    query: string;
 | 
					    query: string;
 | 
				
			||||||
    size?: number;
 | 
					    size?: number;
 | 
				
			||||||
    state?: string;
 | 
					    state?: string;
 | 
				
			||||||
 | 
				
			|||||||
@ -122,6 +122,9 @@ class BaseSearchDto {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  @QueryBoolean({ optional: true })
 | 
					  @QueryBoolean({ optional: true })
 | 
				
			||||||
  isNotInAlbum?: boolean;
 | 
					  isNotInAlbum?: boolean;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Optional()
 | 
				
			||||||
 | 
					  personIds?: string[];
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export class MetadataSearchDto extends BaseSearchDto {
 | 
					export class MetadataSearchDto extends BaseSearchDto {
 | 
				
			||||||
@ -173,9 +176,6 @@ export class MetadataSearchDto extends BaseSearchDto {
 | 
				
			|||||||
  @Optional()
 | 
					  @Optional()
 | 
				
			||||||
  @ApiProperty({ enumName: 'AssetOrder', enum: AssetOrder })
 | 
					  @ApiProperty({ enumName: 'AssetOrder', enum: AssetOrder })
 | 
				
			||||||
  order?: AssetOrder;
 | 
					  order?: AssetOrder;
 | 
				
			||||||
 | 
					 | 
				
			||||||
  @Optional()
 | 
					 | 
				
			||||||
  personIds?: string[];
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export class SmartSearchDto extends BaseSearchDto {
 | 
					export class SmartSearchDto extends BaseSearchDto {
 | 
				
			||||||
 | 
				
			|||||||
@ -22,7 +22,7 @@ import {
 | 
				
			|||||||
import { ImmichLogger } from '@app/infra/logger';
 | 
					import { ImmichLogger } from '@app/infra/logger';
 | 
				
			||||||
import { Injectable } from '@nestjs/common';
 | 
					import { Injectable } from '@nestjs/common';
 | 
				
			||||||
import { InjectRepository } from '@nestjs/typeorm';
 | 
					import { InjectRepository } from '@nestjs/typeorm';
 | 
				
			||||||
import { Repository } from 'typeorm';
 | 
					import { Repository, SelectQueryBuilder } from 'typeorm';
 | 
				
			||||||
import { vectorExt } from '../database.config';
 | 
					import { vectorExt } from '../database.config';
 | 
				
			||||||
import { DummyValue, GenerateSql } from '../infra.util';
 | 
					import { DummyValue, GenerateSql } from '../infra.util';
 | 
				
			||||||
import { asVector, isValidInteger, paginatedBuilder, searchAssetBuilder } from '../infra.utils';
 | 
					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({
 | 
					  @GenerateSql({
 | 
				
			||||||
    params: [
 | 
					    params: [
 | 
				
			||||||
      { page: 1, size: 100 },
 | 
					      { page: 1, size: 100 },
 | 
				
			||||||
@ -96,12 +104,21 @@ export class SearchRepository implements ISearchRepository {
 | 
				
			|||||||
  })
 | 
					  })
 | 
				
			||||||
  async searchSmart(
 | 
					  async searchSmart(
 | 
				
			||||||
    pagination: SearchPaginationOptions,
 | 
					    pagination: SearchPaginationOptions,
 | 
				
			||||||
    { embedding, userIds, ...options }: SmartSearchOptions,
 | 
					    { embedding, userIds, personIds, ...options }: SmartSearchOptions,
 | 
				
			||||||
  ): Paginated<AssetEntity> {
 | 
					  ): Paginated<AssetEntity> {
 | 
				
			||||||
    let results: PaginationResult<AssetEntity> = { items: [], hasNextPage: false };
 | 
					    let results: PaginationResult<AssetEntity> = { items: [], hasNextPage: false };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    await this.assetRepository.manager.transaction(async (manager) => {
 | 
					    await this.assetRepository.manager.transaction(async (manager) => {
 | 
				
			||||||
      let builder = manager.createQueryBuilder(AssetEntity, 'asset');
 | 
					      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 = searchAssetBuilder(builder, options);
 | 
				
			||||||
      builder
 | 
					      builder
 | 
				
			||||||
        .innerJoin('asset.smartSearch', 'search')
 | 
					        .innerJoin('asset.smartSearch', 'search')
 | 
				
			||||||
 | 
				
			|||||||
@ -22,7 +22,6 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
<script lang="ts">
 | 
					<script lang="ts">
 | 
				
			||||||
  import Button from '$lib/components/elements/buttons/button.svelte';
 | 
					  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 { AssetTypeEnum, type SmartSearchDto, type MetadataSearchDto } from '@immich/sdk';
 | 
				
			||||||
  import { createEventDispatcher } from 'svelte';
 | 
					  import { createEventDispatcher } from 'svelte';
 | 
				
			||||||
  import { fly } from 'svelte/transition';
 | 
					  import { fly } from 'svelte/transition';
 | 
				
			||||||
@ -83,14 +82,6 @@
 | 
				
			|||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const search = () => {
 | 
					  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;
 | 
					    let type: AssetTypeEnum | undefined = undefined;
 | 
				
			||||||
    if (filter.mediaType === MediaType.Image) {
 | 
					    if (filter.mediaType === MediaType.Image) {
 | 
				
			||||||
      type = AssetTypeEnum.Image;
 | 
					      type = AssetTypeEnum.Image;
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user