mirror of
https://github.com/immich-app/immich.git
synced 2024-12-25 10:43:13 +02: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:
parent
c45e8cc170
commit
6e7b3d6f24
@ -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<AssetEntity[]> {
|
||||
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)
|
||||
|
@ -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
|
||||
|
@ -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) {
|
||||
|
15
server/test/fixtures/search.stub.ts
vendored
15
server/test/fixtures/search.stub.ts
vendored
@ -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<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'] }),
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user