1
0
Fork 0
mirror of https://github.com/immich-app/immich.git synced 2025-01-01 08:31:59 +00:00

fix(server): fix metadata search not working (#5800)

* don't require ml

* update e2e

* fixes

* fix e2e

* add additional conditions

* select all exif columns

* more fixes

* update sql
This commit is contained in:
Mert 2023-12-17 21:16:08 -05:00 committed by GitHub
parent c45e8cc170
commit 6e7b3d6f24
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 80 additions and 40 deletions

View file

@ -804,23 +804,22 @@ export class AssetRepository implements IAssetRepository {
return builder; return builder;
} }
@GenerateSql({ params: [DummyValue.STRING, DummyValue.UUID, { numResults: 250 }] })
async searchMetadata(query: string, ownerId: string, { numResults }: MetadataSearchOptions): Promise<AssetEntity[]> { async searchMetadata(query: string, ownerId: string, { numResults }: MetadataSearchOptions): Promise<AssetEntity[]> {
const rows = await this.repository const rows = await this.getBuilder({
.createQueryBuilder('assets') userIds: [ownerId],
.select('assets.*') exifInfo: false,
.addSelect('e.country', 'country') isArchived: false,
.addSelect('e.state', 'state') })
.addSelect('e.city', 'city') .select('asset.*')
.addSelect('e.description', 'description') .addSelect('e.*')
.addSelect('e.model', 'model')
.addSelect('e.make', 'make')
.addSelect('COALESCE(si.tags, array[]::text[])', 'tags') .addSelect('COALESCE(si.tags, array[]::text[])', 'tags')
.addSelect('COALESCE(si.objects, array[]::text[])', 'objects') .addSelect('COALESCE(si.objects, array[]::text[])', 'objects')
.innerJoin('smart_info', 'si', 'si."assetId" = assets."id"') .innerJoin('exif', 'e', 'asset."id" = e."assetId"')
.innerJoin('exif', 'e', 'assets."id" = e."assetId"') .leftJoin('smart_info', 'si', 'si."assetId" = asset."id"')
.where('a.ownerId = :ownerId', { ownerId }) .andWhere(
.where( `(e."exifTextSearchableColumn" || COALESCE(si."smartInfoTextSearchableColumn", to_tsvector('english', '')))
'(e."exifTextSearchableColumn" || si."smartInfoTextSearchableColumn") @@ PLAINTO_TSQUERY(\'english\', :query)', @@ PLAINTO_TSQUERY('english', :query)`,
{ query }, { query },
) )
.limit(numResults) .limit(numResults)

View file

@ -668,3 +668,30 @@ WHERE
AND ("asset"."deletedAt" IS NULL) AND ("asset"."deletedAt" IS NULL)
LIMIT LIMIT
12 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

View file

@ -9,7 +9,7 @@ import {
import { SearchController } from '@app/immich'; import { SearchController } from '@app/immich';
import { INestApplication } from '@nestjs/common'; import { INestApplication } from '@nestjs/common';
import { api } from '@test/api'; import { api } from '@test/api';
import { errorStub } from '@test/fixtures'; import { errorStub, searchStub } from '@test/fixtures';
import { generateAsset, testApp } from '@test/test-utils'; import { generateAsset, testApp } from '@test/test-utils';
import request from 'supertest'; import request from 'supertest';
@ -39,32 +39,19 @@ describe(`${SearchController.name}`, () => {
loginResponse = await api.authApi.adminLogin(server); loginResponse = await api.authApi.adminLogin(server);
accessToken = loginResponse.accessToken; accessToken = loginResponse.accessToken;
libraries = await api.libraryApi.getAll(server, 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', () => { describe('GET /search (exif)', () => {
beforeEach(async () => {}); 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 () => { it('should require authentication', async () => {
const { status, body } = await request(server).get('/search'); 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 () => { it('should return assets when searching by object', async () => {
if (!asset1?.smartInfo?.objects) { if (!asset1?.smartInfo?.objects) {

View file

@ -1,5 +1,5 @@
import { SearchResult } from '@app/domain'; import { SearchResult } from '@app/domain';
import { AssetEntity } from '@app/infra/entities'; import { AssetEntity, ExifEntity, SmartInfoEntity } from '@app/infra/entities';
import { assetStub } from '.'; import { assetStub } from '.';
export const searchStub = { export const searchStub = {
@ -20,4 +20,17 @@ export const searchStub = {
facets: [], facets: [],
distances: [], distances: [],
}), }),
exif: Object.freeze<Partial<ExifEntity>>({
latitude: 90,
longitude: 90,
city: 'Immich',
state: 'Nebraska',
country: 'United States',
make: 'Canon',
model: 'EOS Rebel T7',
lensModel: 'Fancy lens',
}),
smartInfo: Object.freeze<Partial<SmartInfoEntity>>({ objects: ['car', 'tree'], tags: ['accident'] }),
}; };