diff --git a/server/src/infra/repositories/asset.repository.ts b/server/src/infra/repositories/asset.repository.ts index e4ba0e2916..690d0aaf94 100644 --- a/server/src/infra/repositories/asset.repository.ts +++ b/server/src/infra/repositories/asset.repository.ts @@ -804,23 +804,22 @@ export class AssetRepository implements IAssetRepository { return builder; } + @GenerateSql({ params: [DummyValue.STRING, DummyValue.UUID, { numResults: 250 }] }) async searchMetadata(query: string, ownerId: string, { numResults }: MetadataSearchOptions): Promise { - const rows = await this.repository - .createQueryBuilder('assets') - .select('assets.*') - .addSelect('e.country', 'country') - .addSelect('e.state', 'state') - .addSelect('e.city', 'city') - .addSelect('e.description', 'description') - .addSelect('e.model', 'model') - .addSelect('e.make', 'make') + const rows = await this.getBuilder({ + userIds: [ownerId], + exifInfo: false, + isArchived: false, + }) + .select('asset.*') + .addSelect('e.*') .addSelect('COALESCE(si.tags, array[]::text[])', 'tags') .addSelect('COALESCE(si.objects, array[]::text[])', 'objects') - .innerJoin('smart_info', 'si', 'si."assetId" = assets."id"') - .innerJoin('exif', 'e', 'assets."id" = e."assetId"') - .where('a.ownerId = :ownerId', { ownerId }) - .where( - '(e."exifTextSearchableColumn" || si."smartInfoTextSearchableColumn") @@ PLAINTO_TSQUERY(\'english\', :query)', + .innerJoin('exif', 'e', 'asset."id" = e."assetId"') + .leftJoin('smart_info', 'si', 'si."assetId" = asset."id"') + .andWhere( + `(e."exifTextSearchableColumn" || COALESCE(si."smartInfoTextSearchableColumn", to_tsvector('english', ''))) + @@ PLAINTO_TSQUERY('english', :query)`, { query }, ) .limit(numResults) diff --git a/server/src/infra/sql/asset.repository.sql b/server/src/infra/sql/asset.repository.sql index aab8492d2a..8a38c475e6 100644 --- a/server/src/infra/sql/asset.repository.sql +++ b/server/src/infra/sql/asset.repository.sql @@ -668,3 +668,30 @@ WHERE AND ("asset"."deletedAt" IS NULL) 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"."fileCreatedAt" < NOW() + AND "asset"."ownerId" IN ($1) + AND "asset"."isArchived" = $2 + AND ( + e."exifTextSearchableColumn" || COALESCE( + si."smartInfoTextSearchableColumn", + to_tsvector('english', '') + ) + ) @@ PLAINTO_TSQUERY('english', $3) + ) + AND ("asset"."deletedAt" IS NULL) +LIMIT + 250 diff --git a/server/test/e2e/search.e2e-spec.ts b/server/test/e2e/search.e2e-spec.ts index 7ab95776ca..d8668767c0 100644 --- a/server/test/e2e/search.e2e-spec.ts +++ b/server/test/e2e/search.e2e-spec.ts @@ -9,7 +9,7 @@ import { import { SearchController } from '@app/immich'; import { INestApplication } from '@nestjs/common'; import { api } from '@test/api'; -import { errorStub } from '@test/fixtures'; +import { errorStub, searchStub } from '@test/fixtures'; import { generateAsset, testApp } from '@test/test-utils'; import request from 'supertest'; @@ -39,32 +39,19 @@ describe(`${SearchController.name}`, () => { loginResponse = await api.authApi.adminLogin(server); accessToken = loginResponse.accessToken; libraries = await api.libraryApi.getAll(server, accessToken); - - const assetId = (await assetRepository.create(generateAsset(loginResponse.userId, libraries))).id; - await assetRepository.upsertExif({ - assetId, - latitude: 90, - longitude: 90, - city: 'Immich', - state: 'Nebraska', - country: 'United States', - make: 'Canon', - model: 'EOS Rebel T7', - lensModel: 'Fancy lens', - }); - await smartInfoRepository.upsert( - { assetId, objects: ['car', 'tree'], tags: ['accident'] }, - Array.from({ length: 512 }, Math.random), - ); - const assetWithMetadata = await assetRepository.getById(assetId, { exifInfo: true, smartInfo: true }); - if (!assetWithMetadata) { - throw new Error('Asset not found'); - } - asset1 = mapAsset(assetWithMetadata); }); - describe('GET /search', () => { - beforeEach(async () => {}); + describe('GET /search (exif)', () => { + beforeEach(async () => { + const assetId = (await assetRepository.create(generateAsset(loginResponse.userId, libraries))).id; + await assetRepository.upsertExif({ assetId, ...searchStub.exif }); + + const assetWithMetadata = await assetRepository.getById(assetId, { exifInfo: true }); + if (!assetWithMetadata) { + throw new Error('Asset not found'); + } + asset1 = mapAsset(assetWithMetadata); + }); it('should require authentication', async () => { const { status, body } = await request(server).get('/search'); @@ -174,6 +161,20 @@ describe(`${SearchController.name}`, () => { }, }); }); + }); + + describe('GET /search (smart info)', () => { + beforeEach(async () => { + const assetId = (await assetRepository.create(generateAsset(loginResponse.userId, libraries))).id; + await assetRepository.upsertExif({ assetId, ...searchStub.exif }); + await smartInfoRepository.upsert({ assetId, ...searchStub.smartInfo }, Array.from({ length: 512 }, Math.random)); + + const assetWithMetadata = await assetRepository.getById(assetId, { exifInfo: true, smartInfo: true }); + if (!assetWithMetadata) { + throw new Error('Asset not found'); + } + asset1 = mapAsset(assetWithMetadata); + }); it('should return assets when searching by object', async () => { if (!asset1?.smartInfo?.objects) { diff --git a/server/test/fixtures/search.stub.ts b/server/test/fixtures/search.stub.ts index eaf03a9f9b..fc197d94f4 100644 --- a/server/test/fixtures/search.stub.ts +++ b/server/test/fixtures/search.stub.ts @@ -1,5 +1,5 @@ import { SearchResult } from '@app/domain'; -import { AssetEntity } from '@app/infra/entities'; +import { AssetEntity, ExifEntity, SmartInfoEntity } from '@app/infra/entities'; import { assetStub } from '.'; export const searchStub = { @@ -20,4 +20,17 @@ export const searchStub = { facets: [], distances: [], }), + + exif: Object.freeze>({ + latitude: 90, + longitude: 90, + city: 'Immich', + state: 'Nebraska', + country: 'United States', + make: 'Canon', + model: 'EOS Rebel T7', + lensModel: 'Fancy lens', + }), + + smartInfo: Object.freeze>({ objects: ['car', 'tree'], tags: ['accident'] }), };