mirror of
https://github.com/KevinMidboe/immich.git
synced 2026-03-02 04:00:11 +00:00
feat(server)!: search via typesense (#1778)
* build: add typesense to docker * feat(server): typesense search * feat(web): search * fix(web): show api error response message * chore: search tests * chore: regenerate open api * fix: disable typesense on e2e * fix: number properties for open api (dart) * fix: e2e test * fix: change lat/lng from floats to typesense geopoint * dev: Add smartInfo relation to findAssetById to be able to query against it --------- Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
This commit is contained in:
1
server/libs/domain/src/search/dto/index.ts
Normal file
1
server/libs/domain/src/search/dto/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './search.dto';
|
||||
57
server/libs/domain/src/search/dto/search.dto.ts
Normal file
57
server/libs/domain/src/search/dto/search.dto.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import { AssetType } from '@app/infra/db/entities';
|
||||
import { Transform } from 'class-transformer';
|
||||
import { IsArray, IsBoolean, IsEnum, IsNotEmpty, IsOptional, IsString } from 'class-validator';
|
||||
import { toBoolean } from '../../../../../apps/immich/src/utils/transform.util';
|
||||
|
||||
export class SearchDto {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@IsOptional()
|
||||
query?: string;
|
||||
|
||||
@IsEnum(AssetType)
|
||||
@IsOptional()
|
||||
type?: AssetType;
|
||||
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
@Transform(toBoolean)
|
||||
isFavorite?: boolean;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@IsOptional()
|
||||
'exifInfo.city'?: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@IsOptional()
|
||||
'exifInfo.state'?: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@IsOptional()
|
||||
'exifInfo.country'?: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@IsOptional()
|
||||
'exifInfo.make'?: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@IsOptional()
|
||||
'exifInfo.model'?: string;
|
||||
|
||||
@IsString({ each: true })
|
||||
@IsArray()
|
||||
@IsOptional()
|
||||
@Transform(({ value }) => value.split(','))
|
||||
'smartInfo.objects'?: string[];
|
||||
|
||||
@IsString({ each: true })
|
||||
@IsArray()
|
||||
@IsOptional()
|
||||
@Transform(({ value }) => value.split(','))
|
||||
'smartInfo.tags'?: string[];
|
||||
}
|
||||
4
server/libs/domain/src/search/index.ts
Normal file
4
server/libs/domain/src/search/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export * from './dto';
|
||||
export * from './response-dto';
|
||||
export * from './search.repository';
|
||||
export * from './search.service';
|
||||
2
server/libs/domain/src/search/response-dto/index.ts
Normal file
2
server/libs/domain/src/search/response-dto/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './search-config-response.dto';
|
||||
export * from './search-response.dto';
|
||||
@@ -0,0 +1,3 @@
|
||||
export class SearchConfigResponseDto {
|
||||
enabled!: boolean;
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { AlbumResponseDto } from '../../album';
|
||||
import { AssetResponseDto } from '../../asset';
|
||||
|
||||
class SearchFacetCountResponseDto {
|
||||
@ApiProperty({ type: 'integer' })
|
||||
count!: number;
|
||||
value!: string;
|
||||
}
|
||||
|
||||
class SearchFacetResponseDto {
|
||||
fieldName!: string;
|
||||
counts!: SearchFacetCountResponseDto[];
|
||||
}
|
||||
|
||||
class SearchAlbumResponseDto {
|
||||
@ApiProperty({ type: 'integer' })
|
||||
total!: number;
|
||||
@ApiProperty({ type: 'integer' })
|
||||
count!: number;
|
||||
items!: AlbumResponseDto[];
|
||||
facets!: SearchFacetResponseDto[];
|
||||
}
|
||||
|
||||
class SearchAssetResponseDto {
|
||||
@ApiProperty({ type: 'integer' })
|
||||
total!: number;
|
||||
@ApiProperty({ type: 'integer' })
|
||||
count!: number;
|
||||
items!: AssetResponseDto[];
|
||||
facets!: SearchFacetResponseDto[];
|
||||
}
|
||||
|
||||
export class SearchResponseDto {
|
||||
albums!: SearchAlbumResponseDto;
|
||||
assets!: SearchAssetResponseDto;
|
||||
}
|
||||
60
server/libs/domain/src/search/search.repository.ts
Normal file
60
server/libs/domain/src/search/search.repository.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { AlbumEntity, AssetEntity, AssetType } from '@app/infra/db/entities';
|
||||
|
||||
export enum SearchCollection {
|
||||
ASSETS = 'assets',
|
||||
ALBUMS = 'albums',
|
||||
}
|
||||
|
||||
export interface SearchFilter {
|
||||
id?: string;
|
||||
userId: string;
|
||||
type?: AssetType;
|
||||
isFavorite?: boolean;
|
||||
city?: string;
|
||||
state?: string;
|
||||
country?: string;
|
||||
make?: string;
|
||||
model?: string;
|
||||
objects?: string[];
|
||||
tags?: string[];
|
||||
}
|
||||
|
||||
export interface SearchResult<T> {
|
||||
/** total matches */
|
||||
total: number;
|
||||
/** collection size */
|
||||
count: number;
|
||||
/** current page */
|
||||
page: number;
|
||||
/** items for page */
|
||||
items: T[];
|
||||
facets: SearchFacet[];
|
||||
}
|
||||
|
||||
export interface SearchFacet {
|
||||
fieldName: string;
|
||||
counts: Array<{
|
||||
count: number;
|
||||
value: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
export type SearchCollectionIndexStatus = Record<SearchCollection, boolean>;
|
||||
|
||||
export const ISearchRepository = 'ISearchRepository';
|
||||
|
||||
export interface ISearchRepository {
|
||||
setup(): Promise<void>;
|
||||
checkMigrationStatus(): Promise<SearchCollectionIndexStatus>;
|
||||
|
||||
index(collection: SearchCollection.ASSETS, item: AssetEntity): Promise<void>;
|
||||
index(collection: SearchCollection.ALBUMS, item: AlbumEntity): Promise<void>;
|
||||
|
||||
delete(collection: SearchCollection, id: string): Promise<void>;
|
||||
|
||||
import(collection: SearchCollection.ASSETS, items: AssetEntity[], done: boolean): Promise<void>;
|
||||
import(collection: SearchCollection.ALBUMS, items: AlbumEntity[], done: boolean): Promise<void>;
|
||||
|
||||
search(collection: SearchCollection.ASSETS, query: string, filters: SearchFilter): Promise<SearchResult<AssetEntity>>;
|
||||
search(collection: SearchCollection.ALBUMS, query: string, filters: SearchFilter): Promise<SearchResult<AlbumEntity>>;
|
||||
}
|
||||
317
server/libs/domain/src/search/search.service.spec.ts
Normal file
317
server/libs/domain/src/search/search.service.spec.ts
Normal file
@@ -0,0 +1,317 @@
|
||||
import { BadRequestException } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { plainToInstance } from 'class-transformer';
|
||||
import {
|
||||
albumStub,
|
||||
assetEntityStub,
|
||||
authStub,
|
||||
newAlbumRepositoryMock,
|
||||
newAssetRepositoryMock,
|
||||
newJobRepositoryMock,
|
||||
newSearchRepositoryMock,
|
||||
} from '../../test';
|
||||
import { IAlbumRepository } from '../album/album.repository';
|
||||
import { IAssetRepository } from '../asset/asset.repository';
|
||||
import { JobName } from '../job';
|
||||
import { IJobRepository } from '../job/job.repository';
|
||||
import { SearchDto } from './dto';
|
||||
import { ISearchRepository } from './search.repository';
|
||||
import { SearchService } from './search.service';
|
||||
|
||||
describe(SearchService.name, () => {
|
||||
let sut: SearchService;
|
||||
let albumMock: jest.Mocked<IAlbumRepository>;
|
||||
let assetMock: jest.Mocked<IAssetRepository>;
|
||||
let jobMock: jest.Mocked<IJobRepository>;
|
||||
let searchMock: jest.Mocked<ISearchRepository>;
|
||||
let configMock: jest.Mocked<ConfigService>;
|
||||
|
||||
beforeEach(() => {
|
||||
albumMock = newAlbumRepositoryMock();
|
||||
assetMock = newAssetRepositoryMock();
|
||||
jobMock = newJobRepositoryMock();
|
||||
searchMock = newSearchRepositoryMock();
|
||||
configMock = { get: jest.fn() } as unknown as jest.Mocked<ConfigService>;
|
||||
|
||||
sut = new SearchService(albumMock, assetMock, jobMock, searchMock, configMock);
|
||||
});
|
||||
|
||||
it('should work', () => {
|
||||
expect(sut).toBeDefined();
|
||||
});
|
||||
|
||||
describe('request dto', () => {
|
||||
it('should convert smartInfo.tags to a string list', () => {
|
||||
const instance = plainToInstance(SearchDto, { 'smartInfo.tags': 'a,b,c' });
|
||||
expect(instance['smartInfo.tags']).toEqual(['a', 'b', 'c']);
|
||||
});
|
||||
|
||||
it('should handle empty smartInfo.tags', () => {
|
||||
const instance = plainToInstance(SearchDto, {});
|
||||
expect(instance['smartInfo.tags']).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should convert smartInfo.objects to a string list', () => {
|
||||
const instance = plainToInstance(SearchDto, { 'smartInfo.objects': 'a,b,c' });
|
||||
expect(instance['smartInfo.objects']).toEqual(['a', 'b', 'c']);
|
||||
});
|
||||
|
||||
it('should handle empty smartInfo.objects', () => {
|
||||
const instance = plainToInstance(SearchDto, {});
|
||||
expect(instance['smartInfo.objects']).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('isEnabled', () => {
|
||||
it('should be enabled by default', () => {
|
||||
expect(sut.isEnabled()).toBe(true);
|
||||
});
|
||||
|
||||
it('should be disabled via an env variable', () => {
|
||||
configMock.get.mockReturnValue('false');
|
||||
sut = new SearchService(albumMock, assetMock, jobMock, searchMock, configMock);
|
||||
|
||||
expect(sut.isEnabled()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getConfig', () => {
|
||||
it('should return the config', () => {
|
||||
expect(sut.getConfig()).toEqual({ enabled: true });
|
||||
});
|
||||
|
||||
it('should return the config when search is disabled', () => {
|
||||
configMock.get.mockReturnValue('false');
|
||||
sut = new SearchService(albumMock, assetMock, jobMock, searchMock, configMock);
|
||||
|
||||
expect(sut.getConfig()).toEqual({ enabled: false });
|
||||
});
|
||||
});
|
||||
|
||||
describe(`bootstrap`, () => {
|
||||
it('should skip when search is disabled', async () => {
|
||||
configMock.get.mockReturnValue('false');
|
||||
sut = new SearchService(albumMock, assetMock, jobMock, searchMock, configMock);
|
||||
|
||||
await sut.bootstrap();
|
||||
|
||||
expect(searchMock.setup).not.toHaveBeenCalled();
|
||||
expect(searchMock.checkMigrationStatus).not.toHaveBeenCalled();
|
||||
expect(jobMock.queue).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should skip schema migration if not needed', async () => {
|
||||
searchMock.checkMigrationStatus.mockResolvedValue({ assets: false, albums: false });
|
||||
await sut.bootstrap();
|
||||
|
||||
expect(searchMock.setup).toHaveBeenCalled();
|
||||
expect(jobMock.queue).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should do schema migration if needed', async () => {
|
||||
searchMock.checkMigrationStatus.mockResolvedValue({ assets: true, albums: true });
|
||||
await sut.bootstrap();
|
||||
|
||||
expect(searchMock.setup).toHaveBeenCalled();
|
||||
expect(jobMock.queue.mock.calls).toEqual([
|
||||
[{ name: JobName.SEARCH_INDEX_ASSETS }],
|
||||
[{ name: JobName.SEARCH_INDEX_ALBUMS }],
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('search', () => {
|
||||
it('should throw an error is search is disabled', async () => {
|
||||
configMock.get.mockReturnValue('false');
|
||||
sut = new SearchService(albumMock, assetMock, jobMock, searchMock, configMock);
|
||||
|
||||
await expect(sut.search(authStub.admin, {})).rejects.toBeInstanceOf(BadRequestException);
|
||||
|
||||
expect(searchMock.search).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should search assets and albums', async () => {
|
||||
searchMock.search.mockResolvedValue({
|
||||
total: 0,
|
||||
count: 0,
|
||||
page: 1,
|
||||
items: [],
|
||||
facets: [],
|
||||
});
|
||||
|
||||
await expect(sut.search(authStub.admin, {})).resolves.toEqual({
|
||||
albums: {
|
||||
total: 0,
|
||||
count: 0,
|
||||
page: 1,
|
||||
items: [],
|
||||
facets: [],
|
||||
},
|
||||
assets: {
|
||||
total: 0,
|
||||
count: 0,
|
||||
page: 1,
|
||||
items: [],
|
||||
facets: [],
|
||||
},
|
||||
});
|
||||
|
||||
expect(searchMock.search.mock.calls).toEqual([
|
||||
['assets', '*', { userId: authStub.admin.id }],
|
||||
['albums', '*', { userId: authStub.admin.id }],
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleIndexAssets', () => {
|
||||
it('should skip if search is disabled', async () => {
|
||||
configMock.get.mockReturnValue('false');
|
||||
sut = new SearchService(albumMock, assetMock, jobMock, searchMock, configMock);
|
||||
|
||||
await sut.handleIndexAssets();
|
||||
|
||||
expect(searchMock.import).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should index all the assets', async () => {
|
||||
assetMock.getAll.mockResolvedValue([]);
|
||||
|
||||
await sut.handleIndexAssets();
|
||||
|
||||
expect(searchMock.import).toHaveBeenCalledWith('assets', [], true);
|
||||
});
|
||||
|
||||
it('should log an error', async () => {
|
||||
assetMock.getAll.mockResolvedValue([]);
|
||||
searchMock.import.mockRejectedValue(new Error('import failed'));
|
||||
|
||||
await sut.handleIndexAssets();
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleIndexAsset', () => {
|
||||
it('should skip if search is disabled', async () => {
|
||||
configMock.get.mockReturnValue('false');
|
||||
sut = new SearchService(albumMock, assetMock, jobMock, searchMock, configMock);
|
||||
|
||||
await sut.handleIndexAsset({ asset: assetEntityStub.image });
|
||||
|
||||
expect(searchMock.index).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should index the asset', async () => {
|
||||
await sut.handleIndexAsset({ asset: assetEntityStub.image });
|
||||
|
||||
expect(searchMock.index).toHaveBeenCalledWith('assets', assetEntityStub.image);
|
||||
});
|
||||
|
||||
it('should log an error', async () => {
|
||||
searchMock.index.mockRejectedValue(new Error('index failed'));
|
||||
|
||||
await sut.handleIndexAsset({ asset: assetEntityStub.image });
|
||||
|
||||
expect(searchMock.index).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleIndexAlbums', () => {
|
||||
it('should skip if search is disabled', async () => {
|
||||
configMock.get.mockReturnValue('false');
|
||||
sut = new SearchService(albumMock, assetMock, jobMock, searchMock, configMock);
|
||||
|
||||
await sut.handleIndexAlbums();
|
||||
|
||||
expect(searchMock.import).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should index all the albums', async () => {
|
||||
albumMock.getAll.mockResolvedValue([]);
|
||||
|
||||
await sut.handleIndexAlbums();
|
||||
|
||||
expect(searchMock.import).toHaveBeenCalledWith('albums', [], true);
|
||||
});
|
||||
|
||||
it('should log an error', async () => {
|
||||
albumMock.getAll.mockResolvedValue([]);
|
||||
searchMock.import.mockRejectedValue(new Error('import failed'));
|
||||
|
||||
await sut.handleIndexAlbums();
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleIndexAlbum', () => {
|
||||
it('should skip if search is disabled', async () => {
|
||||
configMock.get.mockReturnValue('false');
|
||||
sut = new SearchService(albumMock, assetMock, jobMock, searchMock, configMock);
|
||||
|
||||
await sut.handleIndexAlbum({ album: albumStub.empty });
|
||||
|
||||
expect(searchMock.index).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should index the album', async () => {
|
||||
await sut.handleIndexAlbum({ album: albumStub.empty });
|
||||
|
||||
expect(searchMock.index).toHaveBeenCalledWith('albums', albumStub.empty);
|
||||
});
|
||||
|
||||
it('should log an error', async () => {
|
||||
searchMock.index.mockRejectedValue(new Error('index failed'));
|
||||
|
||||
await sut.handleIndexAlbum({ album: albumStub.empty });
|
||||
|
||||
expect(searchMock.index).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleRemoveAlbum', () => {
|
||||
it('should skip if search is disabled', async () => {
|
||||
configMock.get.mockReturnValue('false');
|
||||
sut = new SearchService(albumMock, assetMock, jobMock, searchMock, configMock);
|
||||
|
||||
await sut.handleRemoveAlbum({ id: 'album1' });
|
||||
|
||||
expect(searchMock.delete).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should remove the album', async () => {
|
||||
await sut.handleRemoveAlbum({ id: 'album1' });
|
||||
|
||||
expect(searchMock.delete).toHaveBeenCalledWith('albums', 'album1');
|
||||
});
|
||||
|
||||
it('should log an error', async () => {
|
||||
searchMock.delete.mockRejectedValue(new Error('remove failed'));
|
||||
|
||||
await sut.handleRemoveAlbum({ id: 'album1' });
|
||||
|
||||
expect(searchMock.delete).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleRemoveAsset', () => {
|
||||
it('should skip if search is disabled', async () => {
|
||||
configMock.get.mockReturnValue('false');
|
||||
sut = new SearchService(albumMock, assetMock, jobMock, searchMock, configMock);
|
||||
|
||||
await sut.handleRemoveAsset({ id: 'asset1`' });
|
||||
|
||||
expect(searchMock.delete).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should remove the asset', async () => {
|
||||
await sut.handleRemoveAsset({ id: 'asset1' });
|
||||
|
||||
expect(searchMock.delete).toHaveBeenCalledWith('assets', 'asset1');
|
||||
});
|
||||
|
||||
it('should log an error', async () => {
|
||||
searchMock.delete.mockRejectedValue(new Error('remove failed'));
|
||||
|
||||
await sut.handleRemoveAsset({ id: 'asset1' });
|
||||
|
||||
expect(searchMock.delete).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
154
server/libs/domain/src/search/search.service.ts
Normal file
154
server/libs/domain/src/search/search.service.ts
Normal file
@@ -0,0 +1,154 @@
|
||||
import { BadRequestException, Inject, Injectable, Logger } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { IAlbumRepository } from '../album/album.repository';
|
||||
import { IAssetRepository } from '../asset/asset.repository';
|
||||
import { AuthUserDto } from '../auth';
|
||||
import { IAlbumJob, IAssetJob, IDeleteJob, IJobRepository, JobName } from '../job';
|
||||
import { SearchDto } from './dto';
|
||||
import { SearchConfigResponseDto, SearchResponseDto } from './response-dto';
|
||||
import { ISearchRepository, SearchCollection } from './search.repository';
|
||||
|
||||
@Injectable()
|
||||
export class SearchService {
|
||||
private logger = new Logger(SearchService.name);
|
||||
private enabled: boolean;
|
||||
|
||||
constructor(
|
||||
@Inject(IAlbumRepository) private albumRepository: IAlbumRepository,
|
||||
@Inject(IAssetRepository) private assetRepository: IAssetRepository,
|
||||
@Inject(IJobRepository) private jobRepository: IJobRepository,
|
||||
@Inject(ISearchRepository) private searchRepository: ISearchRepository,
|
||||
configService: ConfigService,
|
||||
) {
|
||||
this.enabled = configService.get('TYPESENSE_ENABLED') !== 'false';
|
||||
}
|
||||
|
||||
isEnabled() {
|
||||
return this.enabled;
|
||||
}
|
||||
|
||||
getConfig(): SearchConfigResponseDto {
|
||||
return {
|
||||
enabled: this.enabled,
|
||||
};
|
||||
}
|
||||
|
||||
async bootstrap() {
|
||||
if (!this.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.logger.log('Running bootstrap');
|
||||
await this.searchRepository.setup();
|
||||
|
||||
const migrationStatus = await this.searchRepository.checkMigrationStatus();
|
||||
if (migrationStatus[SearchCollection.ASSETS]) {
|
||||
this.logger.debug('Queueing job to re-index all assets');
|
||||
await this.jobRepository.queue({ name: JobName.SEARCH_INDEX_ASSETS });
|
||||
}
|
||||
if (migrationStatus[SearchCollection.ALBUMS]) {
|
||||
this.logger.debug('Queueing job to re-index all albums');
|
||||
await this.jobRepository.queue({ name: JobName.SEARCH_INDEX_ALBUMS });
|
||||
}
|
||||
}
|
||||
|
||||
async search(authUser: AuthUserDto, dto: SearchDto): Promise<SearchResponseDto> {
|
||||
if (!this.enabled) {
|
||||
throw new BadRequestException('Search is disabled');
|
||||
}
|
||||
|
||||
const query = dto.query || '*';
|
||||
|
||||
return {
|
||||
assets: (await this.searchRepository.search(SearchCollection.ASSETS, query, {
|
||||
userId: authUser.id,
|
||||
...dto,
|
||||
})) as any,
|
||||
albums: (await this.searchRepository.search(SearchCollection.ALBUMS, query, {
|
||||
userId: authUser.id,
|
||||
...dto,
|
||||
})) as any,
|
||||
};
|
||||
}
|
||||
|
||||
async handleIndexAssets() {
|
||||
if (!this.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this.logger.debug(`Running indexAssets`);
|
||||
// TODO: do this in batches based on searchIndexVersion
|
||||
const assets = await this.assetRepository.getAll({ isVisible: true });
|
||||
|
||||
this.logger.log(`Indexing ${assets.length} assets`);
|
||||
await this.searchRepository.import(SearchCollection.ASSETS, assets, true);
|
||||
} catch (error: any) {
|
||||
this.logger.error(`Unable to index all assets`, error?.stack);
|
||||
}
|
||||
}
|
||||
|
||||
async handleIndexAsset(data: IAssetJob) {
|
||||
if (!this.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { asset } = data;
|
||||
|
||||
try {
|
||||
await this.searchRepository.index(SearchCollection.ASSETS, asset);
|
||||
} catch (error: any) {
|
||||
this.logger.error(`Unable to index asset: ${asset.id}`, error?.stack);
|
||||
}
|
||||
}
|
||||
|
||||
async handleIndexAlbums() {
|
||||
if (!this.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const albums = await this.albumRepository.getAll();
|
||||
this.logger.log(`Indexing ${albums.length} albums`);
|
||||
await this.searchRepository.import(SearchCollection.ALBUMS, albums, true);
|
||||
} catch (error: any) {
|
||||
this.logger.error(`Unable to index all albums`, error?.stack);
|
||||
}
|
||||
}
|
||||
|
||||
async handleIndexAlbum(data: IAlbumJob) {
|
||||
if (!this.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { album } = data;
|
||||
|
||||
try {
|
||||
await this.searchRepository.index(SearchCollection.ALBUMS, album);
|
||||
} catch (error: any) {
|
||||
this.logger.error(`Unable to index album: ${album.id}`, error?.stack);
|
||||
}
|
||||
}
|
||||
|
||||
async handleRemoveAlbum(data: IDeleteJob) {
|
||||
await this.handleRemove(SearchCollection.ALBUMS, data);
|
||||
}
|
||||
|
||||
async handleRemoveAsset(data: IDeleteJob) {
|
||||
await this.handleRemove(SearchCollection.ASSETS, data);
|
||||
}
|
||||
|
||||
private async handleRemove(collection: SearchCollection, data: IDeleteJob) {
|
||||
if (!this.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { id } = data;
|
||||
|
||||
try {
|
||||
await this.searchRepository.delete(collection, id);
|
||||
} catch (error: any) {
|
||||
this.logger.error(`Unable to remove ${collection}: ${id}`, error?.stack);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user