immich/server/src/services/search.service.ts

163 lines
5.8 KiB
TypeScript
Raw Normal View History

import { BadRequestException, Injectable } from '@nestjs/common';
import { AssetMapOptions, AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto';
import { AuthDto } from 'src/dtos/auth.dto';
import { PersonResponseDto } from 'src/dtos/person.dto';
import {
MetadataSearchDto,
PlacesResponseDto,
RandomSearchDto,
SearchPeopleDto,
SearchPlacesDto,
SearchResponseDto,
SearchSuggestionRequestDto,
SearchSuggestionType,
SmartSearchDto,
mapPlaces,
} from 'src/dtos/search.dto';
2024-03-20 16:02:51 -05:00
import { AssetEntity } from 'src/entities/asset.entity';
2024-08-15 06:57:01 -04:00
import { AssetOrder } from 'src/enum';
import { SearchExploreItem } from 'src/interfaces/search.interface';
import { BaseService } from 'src/services/base.service';
2024-06-14 18:29:32 -04:00
import { getMyPartnerIds } from 'src/utils/asset.util';
import { isSmartSearchEnabled } from 'src/utils/misc';
@Injectable()
export class SearchService extends BaseService {
async searchPerson(auth: AuthDto, dto: SearchPeopleDto): Promise<PersonResponseDto[]> {
return this.personRepository.getByName(auth.user.id, dto.name, { withHidden: dto.withHidden });
}
async searchPlaces(dto: SearchPlacesDto): Promise<PlacesResponseDto[]> {
const places = await this.searchRepository.searchPlaces(dto.name);
return places.map((place) => mapPlaces(place));
}
async getExploreData(auth: AuthDto): Promise<SearchExploreItem<AssetResponseDto>[]> {
2023-12-08 11:15:46 -05:00
const options = { maxFields: 12, minAssetsPerField: 5 };
const result = await this.assetRepository.getAssetIdByCity(auth.user.id, options);
const results = [result];
2023-12-08 11:15:46 -05:00
const assetIds = new Set<string>(results.flatMap((field) => field.items.map((item) => item.data)));
const assets = await this.assetRepository.getByIdsWithAllRelations([...assetIds]);
2023-12-08 11:15:46 -05:00
const assetMap = new Map<string, AssetResponseDto>(assets.map((asset) => [asset.id, mapAsset(asset)]));
return results.map(({ fieldName, items }) => ({
fieldName,
2023-12-08 11:15:46 -05:00
items: items.map(({ value, data }) => ({ value, data: assetMap.get(data) as AssetResponseDto })),
}));
}
async searchMetadata(auth: AuthDto, dto: MetadataSearchDto): Promise<SearchResponseDto> {
let checksum: Buffer | undefined;
const userIds = await this.getUserIdsToSearch(auth);
if (dto.checksum) {
const encoding = dto.checksum.length === 28 ? 'base64' : 'hex';
checksum = Buffer.from(dto.checksum, encoding);
}
const page = dto.page ?? 1;
const size = dto.size || 250;
const enumToOrder = { [AssetOrder.ASC]: 'ASC', [AssetOrder.DESC]: 'DESC' } as const;
const { hasNextPage, items } = await this.searchRepository.searchMetadata(
{ page, size },
{
...dto,
checksum,
userIds,
orderDirection: dto.order ? enumToOrder[dto.order] : 'DESC',
},
);
return this.mapResponse(items, hasNextPage ? (page + 1).toString() : null, { auth });
}
async searchRandom(auth: AuthDto, dto: RandomSearchDto): Promise<AssetResponseDto[]> {
const userIds = await this.getUserIdsToSearch(auth);
const items = await this.searchRepository.searchRandom(dto.size || 250, { ...dto, userIds });
return items.map((item) => mapAsset(item, { auth }));
}
async searchSmart(auth: AuthDto, dto: SmartSearchDto): Promise<SearchResponseDto> {
const { machineLearning } = await this.getConfig({ withCache: false });
if (!isSmartSearchEnabled(machineLearning)) {
throw new BadRequestException('Smart search is not enabled');
}
const userIds = await this.getUserIdsToSearch(auth);
const embedding = await this.machineLearningRepository.encodeText(
machineLearning.urls,
dto.query,
machineLearning.clip,
);
const page = dto.page ?? 1;
const size = dto.size || 100;
const { hasNextPage, items } = await this.searchRepository.searchSmart(
{ page, size },
{ ...dto, userIds, embedding },
);
return this.mapResponse(items, hasNextPage ? (page + 1).toString() : null, { auth });
}
async getAssetsByCity(auth: AuthDto): Promise<AssetResponseDto[]> {
const userIds = await this.getUserIdsToSearch(auth);
const assets = await this.searchRepository.getAssetsByCity(userIds);
return assets.map((asset) => mapAsset(asset));
}
async getSearchSuggestions(auth: AuthDto, dto: SearchSuggestionRequestDto) {
const userIds = await this.getUserIdsToSearch(auth);
const suggestions = await this.getSuggestions(userIds, dto);
if (dto.includeNull) {
suggestions.push(null);
}
return suggestions;
}
private getSuggestions(userIds: string[], dto: SearchSuggestionRequestDto) {
switch (dto.type) {
case SearchSuggestionType.COUNTRY: {
return this.searchRepository.getCountries(userIds);
}
case SearchSuggestionType.STATE: {
return this.searchRepository.getStates(userIds, dto);
}
case SearchSuggestionType.CITY: {
return this.searchRepository.getCities(userIds, dto);
}
case SearchSuggestionType.CAMERA_MAKE: {
return this.searchRepository.getCameraMakes(userIds, dto);
}
case SearchSuggestionType.CAMERA_MODEL: {
return this.searchRepository.getCameraModels(userIds, dto);
}
default: {
return [] as (string | null)[];
}
}
}
private async getUserIdsToSearch(auth: AuthDto): Promise<string[]> {
2024-06-14 18:29:32 -04:00
const partnerIds = await getMyPartnerIds({
userId: auth.user.id,
repository: this.partnerRepository,
timelineEnabled: true,
});
return [auth.user.id, ...partnerIds];
}
private mapResponse(assets: AssetEntity[], nextPage: string | null, options: AssetMapOptions): SearchResponseDto {
return {
albums: { total: 0, count: 0, items: [], facets: [] },
assets: {
total: assets.length,
count: assets.length,
items: assets.map((asset) => mapAsset(asset, options)),
facets: [],
nextPage,
},
};
}
}