mirror of
https://github.com/immich-app/immich
synced 2025-11-14 17:36:12 +00:00
chore(server): remove old asset search (#9104)
* chore(server): remove old asset search * chore: remove more unused search code
This commit is contained in:
parent
0c60aaf557
commit
5a49de5592
20 changed files with 30 additions and 1801 deletions
|
|
@ -1,5 +1,5 @@
|
|||
import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Put, Query } from '@nestjs/common';
|
||||
import { ApiOperation, ApiTags } from '@nestjs/swagger';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import { AssetResponseDto, MemoryLaneResponseDto } from 'src/dtos/asset-response.dto';
|
||||
import {
|
||||
AssetBulkDeleteDto,
|
||||
|
|
@ -12,30 +12,13 @@ import {
|
|||
UpdateAssetDto,
|
||||
} from 'src/dtos/asset.dto';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import { MapMarkerDto, MapMarkerResponseDto, MemoryLaneDto, MetadataSearchDto } from 'src/dtos/search.dto';
|
||||
import { MapMarkerDto, MapMarkerResponseDto, MemoryLaneDto } from 'src/dtos/search.dto';
|
||||
import { UpdateStackParentDto } from 'src/dtos/stack.dto';
|
||||
import { Auth, Authenticated, SharedLinkRoute } from 'src/middleware/auth.guard';
|
||||
import { Route } from 'src/middleware/file-upload.interceptor';
|
||||
import { AssetService } from 'src/services/asset.service';
|
||||
import { SearchService } from 'src/services/search.service';
|
||||
import { UUIDParamDto } from 'src/validation';
|
||||
|
||||
@ApiTags('Asset')
|
||||
@Controller('assets')
|
||||
@Authenticated()
|
||||
export class AssetsController {
|
||||
constructor(private searchService: SearchService) {}
|
||||
|
||||
@Get()
|
||||
@ApiOperation({ deprecated: true })
|
||||
async searchAssets(@Auth() auth: AuthDto, @Query() dto: MetadataSearchDto): Promise<AssetResponseDto[]> {
|
||||
const {
|
||||
assets: { items },
|
||||
} = await this.searchService.searchMetadata(auth, dto);
|
||||
return items;
|
||||
}
|
||||
}
|
||||
|
||||
@ApiTags('Asset')
|
||||
@Controller(Route.ASSET)
|
||||
@Authenticated()
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { AlbumController } from 'src/controllers/album.controller';
|
|||
import { APIKeyController } from 'src/controllers/api-key.controller';
|
||||
import { AppController } from 'src/controllers/app.controller';
|
||||
import { AssetControllerV1 } from 'src/controllers/asset-v1.controller';
|
||||
import { AssetController, AssetsController } from 'src/controllers/asset.controller';
|
||||
import { AssetController } from 'src/controllers/asset.controller';
|
||||
import { AuditController } from 'src/controllers/audit.controller';
|
||||
import { AuthController } from 'src/controllers/auth.controller';
|
||||
import { DownloadController } from 'src/controllers/download.controller';
|
||||
|
|
@ -34,7 +34,6 @@ export const controllers = [
|
|||
AppController,
|
||||
AssetController,
|
||||
AssetControllerV1,
|
||||
AssetsController,
|
||||
AuditController,
|
||||
AuthController,
|
||||
DownloadController,
|
||||
|
|
|
|||
|
|
@ -1,12 +1,11 @@
|
|||
import { Body, Controller, Get, HttpCode, HttpStatus, Post, Query } from '@nestjs/common';
|
||||
import { ApiOperation, ApiTags } from '@nestjs/swagger';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import { AssetResponseDto } from 'src/dtos/asset-response.dto';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import { PersonResponseDto } from 'src/dtos/person.dto';
|
||||
import {
|
||||
MetadataSearchDto,
|
||||
PlacesResponseDto,
|
||||
SearchDto,
|
||||
SearchExploreResponseDto,
|
||||
SearchPeopleDto,
|
||||
SearchPlacesDto,
|
||||
|
|
@ -23,12 +22,6 @@ import { SearchService } from 'src/services/search.service';
|
|||
export class SearchController {
|
||||
constructor(private service: SearchService) {}
|
||||
|
||||
@Get()
|
||||
@ApiOperation({ deprecated: true })
|
||||
search(@Auth() auth: AuthDto, @Query() dto: SearchDto): Promise<SearchResponseDto> {
|
||||
return this.service.search(auth, dto);
|
||||
}
|
||||
|
||||
@Post('metadata')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
searchMetadata(@Auth() auth: AuthDto, @Body() dto: MetadataSearchDto): Promise<SearchResponseDto> {
|
||||
|
|
|
|||
|
|
@ -199,53 +199,6 @@ export class SmartSearchDto extends BaseSearchDto {
|
|||
query!: string;
|
||||
}
|
||||
|
||||
// TODO: remove after implementing new search filters
|
||||
/** @deprecated */
|
||||
export class SearchDto {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@Optional()
|
||||
q?: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@Optional()
|
||||
query?: string;
|
||||
|
||||
@ValidateBoolean({ optional: true })
|
||||
smart?: boolean;
|
||||
|
||||
/** @deprecated */
|
||||
@ValidateBoolean({ optional: true })
|
||||
clip?: boolean;
|
||||
|
||||
@IsEnum(AssetType)
|
||||
@Optional()
|
||||
type?: AssetType;
|
||||
|
||||
@ValidateBoolean({ optional: true })
|
||||
recent?: boolean;
|
||||
|
||||
@ValidateBoolean({ optional: true })
|
||||
motion?: boolean;
|
||||
|
||||
@ValidateBoolean({ optional: true })
|
||||
withArchived?: boolean;
|
||||
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
@Type(() => Number)
|
||||
@Optional()
|
||||
page?: number;
|
||||
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
@Max(1000)
|
||||
@Type(() => Number)
|
||||
@Optional()
|
||||
size?: number;
|
||||
}
|
||||
|
||||
export class SearchPlacesDto {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
|
|
|
|||
|
|
@ -129,10 +129,6 @@ export interface AssetExploreOptions extends AssetExploreFieldOptions {
|
|||
unnest?: boolean;
|
||||
}
|
||||
|
||||
export interface MetadataSearchOptions {
|
||||
numResults: number;
|
||||
}
|
||||
|
||||
export interface AssetFullSyncOptions {
|
||||
ownerId: string;
|
||||
lastCreationDate?: Date;
|
||||
|
|
@ -188,7 +184,6 @@ export interface IAssetRepository {
|
|||
upsertJobStatus(jobStatus: Partial<AssetJobStatusEntity>): Promise<void>;
|
||||
getAssetIdByCity(userId: string, options: AssetExploreFieldOptions): Promise<SearchExploreItem<string>>;
|
||||
getAssetIdByTag(userId: string, options: AssetExploreFieldOptions): Promise<SearchExploreItem<string>>;
|
||||
searchMetadata(query: string, userIds: string[], options: MetadataSearchOptions): Promise<AssetEntity[]>;
|
||||
getAllForUserFullSync(options: AssetFullSyncOptions): Promise<AssetEntity[]>;
|
||||
getChangedDeltaSync(options: AssetDeltaSyncOptions): Promise<AssetEntity[]>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,29 +5,6 @@ import { Paginated } from 'src/utils/pagination';
|
|||
|
||||
export const ISearchRepository = 'ISearchRepository';
|
||||
|
||||
export enum SearchStrategy {
|
||||
SMART = 'SMART',
|
||||
TEXT = 'TEXT',
|
||||
}
|
||||
|
||||
export interface SearchFilter {
|
||||
id?: string;
|
||||
userId: string;
|
||||
type?: AssetType;
|
||||
isFavorite?: boolean;
|
||||
isArchived?: boolean;
|
||||
city?: string;
|
||||
state?: string;
|
||||
country?: string;
|
||||
make?: string;
|
||||
model?: string;
|
||||
objects?: string[];
|
||||
tags?: string[];
|
||||
recent?: boolean;
|
||||
motion?: boolean;
|
||||
debug?: boolean;
|
||||
}
|
||||
|
||||
export interface SearchResult<T> {
|
||||
/** total matches */
|
||||
total: number;
|
||||
|
|
|
|||
|
|
@ -738,37 +738,6 @@ WHERE
|
|||
LIMIT
|
||||
12
|
||||
|
||||
-- AssetRepository.searchMetadata
|
||||
SELECT
|
||||
asset.*,
|
||||
e.*,
|
||||
COALESCE("si"."tags", array[]::text[]) AS "tags",
|
||||
COALESCE("si"."objects", array[]::text[]) AS "objects"
|
||||
FROM
|
||||
"assets" "asset"
|
||||
INNER JOIN "exif" "e" ON asset."id" = e."assetId"
|
||||
LEFT JOIN "smart_info" "si" ON si."assetId" = asset."id"
|
||||
WHERE
|
||||
(
|
||||
"asset"."isVisible" = true
|
||||
AND "asset"."ownerId" IN ($1)
|
||||
AND "asset"."isArchived" = $2
|
||||
AND (
|
||||
(
|
||||
e."exifTextSearchableColumn" || COALESCE(
|
||||
si."smartInfoTextSearchableColumn",
|
||||
to_tsvector('english', '')
|
||||
)
|
||||
) @@ PLAINTO_TSQUERY('english', $3)
|
||||
OR asset."originalFileName" = $4
|
||||
)
|
||||
)
|
||||
AND ("asset"."deletedAt" IS NULL)
|
||||
ORDER BY
|
||||
"asset"."fileCreatedAt" DESC
|
||||
LIMIT
|
||||
250
|
||||
|
||||
-- AssetRepository.getAllForUserFullSync
|
||||
SELECT
|
||||
"asset"."id" AS "asset_id",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import path from 'node:path';
|
||||
import { Chunked, ChunkedArray, DummyValue, GenerateSql } from 'src/decorators';
|
||||
import { AlbumEntity, AssetOrder } from 'src/entities/album.entity';
|
||||
import { AssetJobStatusEntity } from 'src/entities/asset-job-status.entity';
|
||||
|
|
@ -23,7 +22,6 @@ import {
|
|||
LivePhotoSearchOptions,
|
||||
MapMarker,
|
||||
MapMarkerSearchOptions,
|
||||
MetadataSearchOptions,
|
||||
MonthDay,
|
||||
TimeBucketItem,
|
||||
TimeBucketOptions,
|
||||
|
|
@ -700,94 +698,6 @@ export class AssetRepository implements IAssetRepository {
|
|||
return builder;
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.STRING, [DummyValue.UUID], { numResults: 250 }] })
|
||||
async searchMetadata(
|
||||
query: string,
|
||||
userIds: string[],
|
||||
{ numResults }: MetadataSearchOptions,
|
||||
): Promise<AssetEntity[]> {
|
||||
const rows = await this.getBuilder({
|
||||
userIds: userIds,
|
||||
exifInfo: false,
|
||||
isArchived: false,
|
||||
})
|
||||
.select('asset.*')
|
||||
.addSelect('e.*')
|
||||
.addSelect('COALESCE(si.tags, array[]::text[])', 'tags')
|
||||
.addSelect('COALESCE(si.objects, array[]::text[])', 'objects')
|
||||
.innerJoin('exif', 'e', 'asset."id" = e."assetId"')
|
||||
.leftJoin('smart_info', 'si', 'si."assetId" = asset."id"')
|
||||
.andWhere(
|
||||
new Brackets((qb) => {
|
||||
qb.where(
|
||||
`(e."exifTextSearchableColumn" || COALESCE(si."smartInfoTextSearchableColumn", to_tsvector('english', '')))
|
||||
@@ PLAINTO_TSQUERY('english', :query)`,
|
||||
{ query },
|
||||
).orWhere('asset."originalFileName" = :path', { path: path.parse(query).name });
|
||||
}),
|
||||
)
|
||||
.addOrderBy('asset.fileCreatedAt', 'DESC')
|
||||
.limit(numResults)
|
||||
.getRawMany();
|
||||
|
||||
return rows.map(
|
||||
({
|
||||
tags,
|
||||
objects,
|
||||
country,
|
||||
state,
|
||||
city,
|
||||
description,
|
||||
model,
|
||||
make,
|
||||
dateTimeOriginal,
|
||||
exifImageHeight,
|
||||
exifImageWidth,
|
||||
exposureTime,
|
||||
fNumber,
|
||||
fileSizeInByte,
|
||||
focalLength,
|
||||
iso,
|
||||
latitude,
|
||||
lensModel,
|
||||
longitude,
|
||||
modifyDate,
|
||||
projectionType,
|
||||
timeZone,
|
||||
...assetInfo
|
||||
}) =>
|
||||
({
|
||||
exifInfo: {
|
||||
city,
|
||||
country,
|
||||
dateTimeOriginal,
|
||||
description,
|
||||
exifImageHeight,
|
||||
exifImageWidth,
|
||||
exposureTime,
|
||||
fNumber,
|
||||
fileSizeInByte,
|
||||
focalLength,
|
||||
iso,
|
||||
latitude,
|
||||
lensModel,
|
||||
longitude,
|
||||
make,
|
||||
model,
|
||||
modifyDate,
|
||||
projectionType,
|
||||
state,
|
||||
timeZone,
|
||||
},
|
||||
smartInfo: {
|
||||
tags,
|
||||
objects,
|
||||
},
|
||||
...assetInfo,
|
||||
}) as AssetEntity,
|
||||
);
|
||||
}
|
||||
|
||||
@GenerateSql({
|
||||
params: [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,6 +1,4 @@
|
|||
import { mapAsset } from 'src/dtos/asset-response.dto';
|
||||
import { SearchDto } from 'src/dtos/search.dto';
|
||||
import { SystemConfigKey } from 'src/entities/system-config.entity';
|
||||
import { IAssetRepository } from 'src/interfaces/asset.interface';
|
||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||
import { IMachineLearningRepository } from 'src/interfaces/machine-learning.interface';
|
||||
|
|
@ -97,119 +95,4 @@ describe(SearchService.name, () => {
|
|||
expect(result).toEqual(expectedResponse);
|
||||
});
|
||||
});
|
||||
|
||||
describe('search', () => {
|
||||
it('should throw an error if query is missing', async () => {
|
||||
await expect(sut.search(authStub.user1, { q: '' })).rejects.toThrow('Missing query');
|
||||
});
|
||||
|
||||
it('should search by metadata if `clip` option is false', async () => {
|
||||
const dto: SearchDto = { q: 'test query', clip: false };
|
||||
assetMock.searchMetadata.mockResolvedValueOnce([assetStub.image]);
|
||||
partnerMock.getAll.mockResolvedValueOnce([]);
|
||||
const expectedResponse = {
|
||||
albums: {
|
||||
total: 0,
|
||||
count: 0,
|
||||
items: [],
|
||||
facets: [],
|
||||
},
|
||||
assets: {
|
||||
total: 1,
|
||||
count: 1,
|
||||
items: [mapAsset(assetStub.image)],
|
||||
facets: [],
|
||||
nextPage: null,
|
||||
},
|
||||
};
|
||||
|
||||
const result = await sut.search(authStub.user1, dto);
|
||||
|
||||
expect(result).toEqual(expectedResponse);
|
||||
expect(assetMock.searchMetadata).toHaveBeenCalledWith(dto.q, [authStub.user1.user.id], { numResults: 250 });
|
||||
expect(searchMock.searchSmart).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should search archived photos if `withArchived` option is true', async () => {
|
||||
const dto: SearchDto = { q: 'test query', clip: true, withArchived: true };
|
||||
const embedding = [1, 2, 3];
|
||||
searchMock.searchSmart.mockResolvedValueOnce({ items: [assetStub.image], hasNextPage: false });
|
||||
machineMock.encodeText.mockResolvedValueOnce(embedding);
|
||||
partnerMock.getAll.mockResolvedValueOnce([]);
|
||||
const expectedResponse = {
|
||||
albums: {
|
||||
total: 0,
|
||||
count: 0,
|
||||
items: [],
|
||||
facets: [],
|
||||
},
|
||||
assets: {
|
||||
total: 1,
|
||||
count: 1,
|
||||
items: [mapAsset(assetStub.image)],
|
||||
facets: [],
|
||||
nextPage: null,
|
||||
},
|
||||
};
|
||||
|
||||
const result = await sut.search(authStub.user1, dto);
|
||||
|
||||
expect(result).toEqual(expectedResponse);
|
||||
expect(searchMock.searchSmart).toHaveBeenCalledWith(
|
||||
{ page: 1, size: 100 },
|
||||
{
|
||||
userIds: [authStub.user1.user.id],
|
||||
embedding,
|
||||
withArchived: true,
|
||||
},
|
||||
);
|
||||
expect(assetMock.searchMetadata).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should search by CLIP if `clip` option is true', async () => {
|
||||
const dto: SearchDto = { q: 'test query', clip: true };
|
||||
const embedding = [1, 2, 3];
|
||||
searchMock.searchSmart.mockResolvedValueOnce({ items: [assetStub.image], hasNextPage: false });
|
||||
machineMock.encodeText.mockResolvedValueOnce(embedding);
|
||||
partnerMock.getAll.mockResolvedValueOnce([]);
|
||||
const expectedResponse = {
|
||||
albums: {
|
||||
total: 0,
|
||||
count: 0,
|
||||
items: [],
|
||||
facets: [],
|
||||
},
|
||||
assets: {
|
||||
total: 1,
|
||||
count: 1,
|
||||
items: [mapAsset(assetStub.image)],
|
||||
facets: [],
|
||||
nextPage: null,
|
||||
},
|
||||
};
|
||||
|
||||
const result = await sut.search(authStub.user1, dto);
|
||||
|
||||
expect(result).toEqual(expectedResponse);
|
||||
expect(searchMock.searchSmart).toHaveBeenCalledWith(
|
||||
{ page: 1, size: 100 },
|
||||
{
|
||||
userIds: [authStub.user1.user.id],
|
||||
embedding,
|
||||
withArchived: false,
|
||||
},
|
||||
);
|
||||
expect(assetMock.searchMetadata).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ key: SystemConfigKey.MACHINE_LEARNING_ENABLED },
|
||||
{ key: SystemConfigKey.MACHINE_LEARNING_CLIP_ENABLED },
|
||||
])('should throw an error if clip is requested but disabled', async ({ key }) => {
|
||||
const dto: SearchDto = { q: 'test query', clip: true };
|
||||
configMock.load.mockResolvedValue([{ key, value: false }]);
|
||||
|
||||
await expect(sut.search(authStub.user1, dto)).rejects.toThrow('Smart search is not enabled');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import { PersonResponseDto } from 'src/dtos/person.dto';
|
|||
import {
|
||||
MetadataSearchDto,
|
||||
PlacesResponseDto,
|
||||
SearchDto,
|
||||
SearchPeopleDto,
|
||||
SearchPlacesDto,
|
||||
SearchResponseDto,
|
||||
|
|
@ -23,7 +22,7 @@ import { IMachineLearningRepository } from 'src/interfaces/machine-learning.inte
|
|||
import { IMetadataRepository } from 'src/interfaces/metadata.interface';
|
||||
import { IPartnerRepository } from 'src/interfaces/partner.interface';
|
||||
import { IPersonRepository } from 'src/interfaces/person.interface';
|
||||
import { ISearchRepository, SearchExploreItem, SearchStrategy } from 'src/interfaces/search.interface';
|
||||
import { ISearchRepository, SearchExploreItem } from 'src/interfaces/search.interface';
|
||||
import { ISystemConfigRepository } from 'src/interfaces/system-config.interface';
|
||||
|
||||
@Injectable()
|
||||
|
|
@ -145,60 +144,6 @@ export class SearchService {
|
|||
}
|
||||
}
|
||||
|
||||
// TODO: remove after implementing new search filters
|
||||
/** @deprecated */
|
||||
async search(auth: AuthDto, dto: SearchDto): Promise<SearchResponseDto> {
|
||||
await this.configCore.requireFeature(FeatureFlag.SEARCH);
|
||||
const { machineLearning } = await this.configCore.getConfig();
|
||||
const query = dto.q || dto.query;
|
||||
if (!query) {
|
||||
throw new Error('Missing query');
|
||||
}
|
||||
|
||||
let strategy = SearchStrategy.TEXT;
|
||||
if (dto.smart || dto.clip) {
|
||||
await this.configCore.requireFeature(FeatureFlag.SMART_SEARCH);
|
||||
strategy = SearchStrategy.SMART;
|
||||
}
|
||||
|
||||
const userIds = await this.getUserIdsToSearch(auth);
|
||||
const page = dto.page ?? 1;
|
||||
|
||||
let nextPage: string | null = null;
|
||||
let assets: AssetEntity[] = [];
|
||||
switch (strategy) {
|
||||
case SearchStrategy.SMART: {
|
||||
const embedding = await this.machineLearning.encodeText(
|
||||
machineLearning.url,
|
||||
{ text: query },
|
||||
machineLearning.clip,
|
||||
);
|
||||
|
||||
const { hasNextPage, items } = await this.searchRepository.searchSmart(
|
||||
{ page, size: dto.size || 100 },
|
||||
{
|
||||
userIds,
|
||||
embedding,
|
||||
withArchived: !!dto.withArchived,
|
||||
},
|
||||
);
|
||||
if (hasNextPage) {
|
||||
nextPage = (page + 1).toString();
|
||||
}
|
||||
assets = items;
|
||||
break;
|
||||
}
|
||||
case SearchStrategy.TEXT: {
|
||||
assets = await this.assetRepository.searchMetadata(query, userIds, { numResults: dto.size || 250 });
|
||||
}
|
||||
default: {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return this.mapResponse(assets, nextPage);
|
||||
}
|
||||
|
||||
private async getUserIdsToSearch(auth: AuthDto): Promise<string[]> {
|
||||
const userIds: string[] = [auth.user.id];
|
||||
const partners = await this.partnerRepository.getAll(auth.user.id);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue