2023-12-14 17:55:40 +01:00
|
|
|
import { Inject, Injectable } from '@nestjs/common';
|
2024-03-20 21:20:38 +01:00
|
|
|
import { FeatureFlag, SystemConfigCore } from 'src/cores/system-config.core';
|
2024-03-20 19:32:04 +01:00
|
|
|
import { AssetResponseDto, mapAsset } from 'src/domain/asset/response-dto/asset-response.dto';
|
|
|
|
import { AuthDto } from 'src/domain/auth/auth.dto';
|
|
|
|
import { PersonResponseDto } from 'src/domain/person/person.dto';
|
|
|
|
import { SearchSuggestionRequestDto, SearchSuggestionType } from 'src/domain/search/dto/search-suggestion.dto';
|
2024-02-24 01:42:37 +01:00
|
|
|
import {
|
|
|
|
MetadataSearchDto,
|
|
|
|
PlacesResponseDto,
|
|
|
|
SearchDto,
|
|
|
|
SearchPeopleDto,
|
|
|
|
SearchPlacesDto,
|
|
|
|
SmartSearchDto,
|
|
|
|
mapPlaces,
|
2024-03-20 19:32:04 +01:00
|
|
|
} from 'src/domain/search/dto/search.dto';
|
|
|
|
import { SearchResponseDto } from 'src/domain/search/response-dto/search-response.dto';
|
2024-03-20 22:02:51 +01:00
|
|
|
import { AssetOrder } from 'src/entities/album.entity';
|
|
|
|
import { AssetEntity } from 'src/entities/asset.entity';
|
2024-03-20 21:42:58 +01:00
|
|
|
import { IAssetRepository } from 'src/interfaces/asset.repository';
|
|
|
|
import { IMachineLearningRepository } from 'src/interfaces/machine-learning.repository';
|
|
|
|
import { IMetadataRepository } from 'src/interfaces/metadata.repository';
|
|
|
|
import { IPartnerRepository } from 'src/interfaces/partner.repository';
|
|
|
|
import { IPersonRepository } from 'src/interfaces/person.repository';
|
|
|
|
import { ISearchRepository, SearchExploreItem, SearchStrategy } from 'src/interfaces/search.repository';
|
|
|
|
import { ISystemConfigRepository } from 'src/interfaces/system-config.repository';
|
2023-03-18 14:44:42 +01:00
|
|
|
|
2023-03-03 03:47:08 +01:00
|
|
|
@Injectable()
|
|
|
|
export class SearchService {
|
2023-08-25 06:15:03 +02:00
|
|
|
private configCore: SystemConfigCore;
|
2023-03-18 14:44:42 +01:00
|
|
|
|
2023-03-03 03:47:08 +01:00
|
|
|
constructor(
|
2023-12-08 17:15:46 +01:00
|
|
|
@Inject(ISystemConfigRepository) configRepository: ISystemConfigRepository,
|
2023-03-18 14:44:42 +01:00
|
|
|
@Inject(IMachineLearningRepository) private machineLearning: IMachineLearningRepository,
|
2023-09-27 22:46:46 +02:00
|
|
|
@Inject(IPersonRepository) private personRepository: IPersonRepository,
|
2024-02-13 02:50:47 +01:00
|
|
|
@Inject(ISearchRepository) private searchRepository: ISearchRepository,
|
2023-12-08 17:15:46 +01:00
|
|
|
@Inject(IAssetRepository) private assetRepository: IAssetRepository,
|
2024-01-01 23:25:22 +01:00
|
|
|
@Inject(IPartnerRepository) private partnerRepository: IPartnerRepository,
|
2024-02-13 20:54:58 +01:00
|
|
|
@Inject(IMetadataRepository) private metadataRepository: IMetadataRepository,
|
2023-03-03 03:47:08 +01:00
|
|
|
) {
|
2023-10-09 02:51:03 +02:00
|
|
|
this.configCore = SystemConfigCore.create(configRepository);
|
2023-03-18 14:44:42 +01:00
|
|
|
}
|
|
|
|
|
2023-12-10 05:34:12 +01:00
|
|
|
async searchPerson(auth: AuthDto, dto: SearchPeopleDto): Promise<PersonResponseDto[]> {
|
|
|
|
return this.personRepository.getByName(auth.user.id, dto.name, { withHidden: dto.withHidden });
|
2023-03-03 03:47:08 +01:00
|
|
|
}
|
|
|
|
|
2024-02-24 01:42:37 +01:00
|
|
|
async searchPlaces(dto: SearchPlacesDto): Promise<PlacesResponseDto[]> {
|
|
|
|
const places = await this.searchRepository.searchPlaces(dto.name);
|
|
|
|
return places.map((place) => mapPlaces(place));
|
|
|
|
}
|
|
|
|
|
2023-12-10 05:34:12 +01:00
|
|
|
async getExploreData(auth: AuthDto): Promise<SearchExploreItem<AssetResponseDto>[]> {
|
2023-08-25 06:15:03 +02:00
|
|
|
await this.configCore.requireFeature(FeatureFlag.SEARCH);
|
2023-12-08 17:15:46 +01:00
|
|
|
const options = { maxFields: 12, minAssetsPerField: 5 };
|
|
|
|
const results = await Promise.all([
|
2023-12-10 05:34:12 +01:00
|
|
|
this.assetRepository.getAssetIdByCity(auth.user.id, options),
|
|
|
|
this.assetRepository.getAssetIdByTag(auth.user.id, options),
|
2023-12-08 17:15:46 +01:00
|
|
|
]);
|
|
|
|
const assetIds = new Set<string>(results.flatMap((field) => field.items.map((item) => item.data)));
|
2024-03-14 06:58:09 +01:00
|
|
|
const assets = await this.assetRepository.getByIdsWithAllRelations([...assetIds]);
|
2023-12-08 17:15:46 +01:00
|
|
|
const assetMap = new Map<string, AssetResponseDto>(assets.map((asset) => [asset.id, mapAsset(asset)]));
|
2023-05-30 19:55:06 +02:00
|
|
|
|
2023-05-28 03:56:17 +02:00
|
|
|
return results.map(({ fieldName, items }) => ({
|
|
|
|
fieldName,
|
2023-12-08 17:15:46 +01:00
|
|
|
items: items.map(({ value, data }) => ({ value, data: assetMap.get(data) as AssetResponseDto })),
|
2023-05-28 03:56:17 +02:00
|
|
|
}));
|
2023-03-05 21:44:31 +01:00
|
|
|
}
|
|
|
|
|
2024-02-13 02:50:47 +01:00
|
|
|
async searchMetadata(auth: AuthDto, dto: MetadataSearchDto): Promise<SearchResponseDto> {
|
|
|
|
let checksum: Buffer | undefined;
|
2024-02-17 18:00:55 +01:00
|
|
|
const userIds = await this.getUserIdsToSearch(auth);
|
2024-02-13 02:50:47 +01:00
|
|
|
|
|
|
|
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,
|
2024-02-17 18:00:55 +01:00
|
|
|
userIds,
|
2024-02-13 02:50:47 +01:00
|
|
|
orderDirection: dto.order ? enumToOrder[dto.order] : 'DESC',
|
|
|
|
},
|
|
|
|
);
|
|
|
|
|
|
|
|
return this.mapResponse(items, hasNextPage ? (page + 1).toString() : null);
|
|
|
|
}
|
|
|
|
|
|
|
|
async searchSmart(auth: AuthDto, dto: SmartSearchDto): Promise<SearchResponseDto> {
|
|
|
|
await this.configCore.requireFeature(FeatureFlag.SMART_SEARCH);
|
|
|
|
const { machineLearning } = await this.configCore.getConfig();
|
|
|
|
const userIds = await this.getUserIdsToSearch(auth);
|
|
|
|
|
|
|
|
const embedding = await this.machineLearning.encodeText(
|
|
|
|
machineLearning.url,
|
|
|
|
{ text: 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);
|
|
|
|
}
|
|
|
|
|
2024-03-20 04:23:57 +01:00
|
|
|
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));
|
|
|
|
}
|
|
|
|
|
|
|
|
getSearchSuggestions(auth: AuthDto, dto: SearchSuggestionRequestDto): Promise<string[]> {
|
|
|
|
switch (dto.type) {
|
|
|
|
case SearchSuggestionType.COUNTRY: {
|
|
|
|
return this.metadataRepository.getCountries(auth.user.id);
|
|
|
|
}
|
|
|
|
case SearchSuggestionType.STATE: {
|
|
|
|
return this.metadataRepository.getStates(auth.user.id, dto.country);
|
|
|
|
}
|
|
|
|
case SearchSuggestionType.CITY: {
|
|
|
|
return this.metadataRepository.getCities(auth.user.id, dto.country, dto.state);
|
|
|
|
}
|
|
|
|
case SearchSuggestionType.CAMERA_MAKE: {
|
|
|
|
return this.metadataRepository.getCameraMakes(auth.user.id, dto.model);
|
|
|
|
}
|
|
|
|
case SearchSuggestionType.CAMERA_MODEL: {
|
|
|
|
return this.metadataRepository.getCameraModels(auth.user.id, dto.make);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-02-13 02:50:47 +01:00
|
|
|
// TODO: remove after implementing new search filters
|
|
|
|
/** @deprecated */
|
2023-12-10 05:34:12 +01:00
|
|
|
async search(auth: AuthDto, dto: SearchDto): Promise<SearchResponseDto> {
|
2024-01-29 15:51:22 +01:00
|
|
|
await this.configCore.requireFeature(FeatureFlag.SEARCH);
|
2023-08-25 06:15:03 +02:00
|
|
|
const { machineLearning } = await this.configCore.getConfig();
|
2023-12-08 17:15:46 +01:00
|
|
|
const query = dto.q || dto.query;
|
|
|
|
if (!query) {
|
|
|
|
throw new Error('Missing query');
|
|
|
|
}
|
2024-01-29 15:51:22 +01:00
|
|
|
|
|
|
|
let strategy = SearchStrategy.TEXT;
|
|
|
|
if (dto.smart || dto.clip) {
|
|
|
|
await this.configCore.requireFeature(FeatureFlag.SMART_SEARCH);
|
|
|
|
strategy = SearchStrategy.SMART;
|
2023-12-08 17:15:46 +01:00
|
|
|
}
|
2024-01-29 15:51:22 +01:00
|
|
|
|
2024-01-01 23:25:22 +01:00
|
|
|
const userIds = await this.getUserIdsToSearch(auth);
|
2024-02-13 02:50:47 +01:00
|
|
|
const page = dto.page ?? 1;
|
2023-12-08 17:15:46 +01:00
|
|
|
|
2024-02-13 02:50:47 +01:00
|
|
|
let nextPage: string | null = null;
|
2023-12-08 17:15:46 +01:00
|
|
|
let assets: AssetEntity[] = [];
|
2023-03-18 14:44:42 +01:00
|
|
|
switch (strategy) {
|
2024-02-02 04:18:00 +01:00
|
|
|
case SearchStrategy.SMART: {
|
2023-12-08 17:15:46 +01:00
|
|
|
const embedding = await this.machineLearning.encodeText(
|
|
|
|
machineLearning.url,
|
|
|
|
{ text: query },
|
|
|
|
machineLearning.clip,
|
|
|
|
);
|
2024-02-13 02:50:47 +01:00
|
|
|
|
|
|
|
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;
|
2023-03-19 14:20:23 +01:00
|
|
|
break;
|
2024-02-02 04:18:00 +01:00
|
|
|
}
|
|
|
|
case SearchStrategy.TEXT: {
|
2024-02-13 02:50:47 +01:00
|
|
|
assets = await this.assetRepository.searchMetadata(query, userIds, { numResults: dto.size || 250 });
|
2024-02-02 04:18:00 +01:00
|
|
|
}
|
|
|
|
default: {
|
2023-03-18 22:30:48 +01:00
|
|
|
break;
|
2024-02-02 04:18:00 +01:00
|
|
|
}
|
2023-03-18 14:44:42 +01:00
|
|
|
}
|
|
|
|
|
2024-02-13 02:50:47 +01:00
|
|
|
return this.mapResponse(assets, nextPage);
|
2023-03-03 03:47:08 +01:00
|
|
|
}
|
2024-01-01 23:25:22 +01:00
|
|
|
|
|
|
|
private async getUserIdsToSearch(auth: AuthDto): Promise<string[]> {
|
|
|
|
const userIds: string[] = [auth.user.id];
|
|
|
|
const partners = await this.partnerRepository.getAll(auth.user.id);
|
|
|
|
const partnersIds = partners
|
|
|
|
.filter((partner) => partner.sharedBy && partner.inTimeline)
|
|
|
|
.map((partner) => partner.sharedById);
|
|
|
|
userIds.push(...partnersIds);
|
|
|
|
return userIds;
|
|
|
|
}
|
2024-02-13 02:50:47 +01:00
|
|
|
|
2024-03-05 23:23:06 +01:00
|
|
|
private mapResponse(assets: AssetEntity[], nextPage: string | null): SearchResponseDto {
|
2024-02-13 02:50:47 +01:00
|
|
|
return {
|
|
|
|
albums: { total: 0, count: 0, items: [], facets: [] },
|
|
|
|
assets: {
|
|
|
|
total: assets.length,
|
|
|
|
count: assets.length,
|
|
|
|
items: assets.map((asset) => mapAsset(asset)),
|
|
|
|
facets: [],
|
|
|
|
nextPage,
|
|
|
|
},
|
|
|
|
};
|
|
|
|
}
|
2023-03-03 03:47:08 +01:00
|
|
|
}
|