import { BadRequestException, Injectable } from '@nestjs/common'; import { AssetMapOptions, AssetResponseDto, MapAsset, mapAsset } from 'src/dtos/asset-response.dto'; import { AuthDto } from 'src/dtos/auth.dto'; import { mapPerson, PersonResponseDto } from 'src/dtos/person.dto'; import { mapPlaces, MetadataSearchDto, PlacesResponseDto, RandomSearchDto, SearchPeopleDto, SearchPlacesDto, SearchResponseDto, SearchSuggestionRequestDto, SearchSuggestionType, SmartSearchDto, } from 'src/dtos/search.dto'; import { AssetOrder, AssetVisibility } from 'src/enum'; import { BaseService } from 'src/services/base.service'; import { requireElevatedPermission } from 'src/utils/access'; 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 { const people = await this.personRepository.getByName(auth.user.id, dto.name, { withHidden: dto.withHidden }); return people.map((person) => mapPerson(person)); } async searchPlaces(dto: SearchPlacesDto): Promise { const places = await this.searchRepository.searchPlaces(dto.name); return places.map((place) => mapPlaces(place)); } async getExploreData(auth: AuthDto) { const options = { maxFields: 12, minAssetsPerField: 5 }; const cities = await this.assetRepository.getAssetIdByCity(auth.user.id, options); const assets = await this.assetRepository.getByIdsWithAllRelationsButStacks(cities.items.map(({ data }) => data)); const items = assets.map((asset) => ({ value: asset.exifInfo!.city!, data: mapAsset(asset, { auth }) })); return [{ fieldName: cities.fieldName, items }]; } async searchMetadata(auth: AuthDto, dto: MetadataSearchDto): Promise { if (dto.visibility === AssetVisibility.LOCKED) { requireElevatedPermission(auth); } let checksum: Buffer | undefined; 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 userIds = await this.getUserIdsToSearch(auth); const { hasNextPage, items } = await this.searchRepository.searchMetadata( { page, size }, { ...dto, checksum, userIds, orderDirection: dto.order ?? AssetOrder.DESC, }, ); return this.mapResponse(items, hasNextPage ? (page + 1).toString() : null, { auth }); } async searchRandom(auth: AuthDto, dto: RandomSearchDto): Promise { if (dto.visibility === AssetVisibility.LOCKED) { requireElevatedPermission(auth); } 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 { if (dto.visibility === AssetVisibility.LOCKED) { requireElevatedPermission(auth); } 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, { modelName: machineLearning.clip.modelName, language: dto.language, }); 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 { 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): Promise> { 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 Promise.resolve([]); } } } private async getUserIdsToSearch(auth: AuthDto): Promise { const partnerIds = await getMyPartnerIds({ userId: auth.user.id, repository: this.partnerRepository, timelineEnabled: true, }); return [auth.user.id, ...partnerIds]; } private mapResponse(assets: MapAsset[], 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, }, }; } }