mirror of
https://github.com/immich-app/immich.git
synced 2025-01-27 22:22:45 +01:00
feat: filter people when using smart search (#7521)
This commit is contained in:
parent
15a4a4aaaa
commit
c89d91e006
8 changed files with 29 additions and 14 deletions
mobile/openapi
open-api
server/src
web/src/lib/components/shared-components/search-bar
BIN
mobile/openapi/doc/SmartSearchDto.md
generated
BIN
mobile/openapi/doc/SmartSearchDto.md
generated
Binary file not shown.
BIN
mobile/openapi/lib/model/smart_search_dto.dart
generated
BIN
mobile/openapi/lib/model/smart_search_dto.dart
generated
Binary file not shown.
BIN
mobile/openapi/test/smart_search_dto_test.dart
generated
BIN
mobile/openapi/test/smart_search_dto_test.dart
generated
Binary file not shown.
|
@ -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…
Reference in a new issue