diff --git a/backend/model/sql/GalleryManager.ts b/backend/model/sql/GalleryManager.ts index 5adc361..146ea9e 100644 --- a/backend/model/sql/GalleryManager.ts +++ b/backend/model/sql/GalleryManager.ts @@ -67,7 +67,7 @@ export class GalleryManager implements IGalleryManager, ISQLGalleryManager { // on the fly reindexing Logger.silly(LOG_TAG, 'lazy reindexing reason: cache timeout: lastScanned: ' - + (Date.now() - dir.lastScanned) + ', cachedFolderTimeout:' + Config.Server.indexing.cachedFolderTimeout); + + (Date.now() - dir.lastScanned) + ' ms ago, cachedFolderTimeout:' + Config.Server.indexing.cachedFolderTimeout); ObjectManagerRepository.getInstance().IndexingManager.indexDirectory(relativeDirectoryName).catch((err) => { console.error(err); }); diff --git a/backend/model/sql/SQLConnection.ts b/backend/model/sql/SQLConnection.ts index 671b86b..22a8fee 100644 --- a/backend/model/sql/SQLConnection.ts +++ b/backend/model/sql/SQLConnection.ts @@ -44,7 +44,7 @@ export class SQLConnection { VersionEntity ]; options.synchronize = false; - // options.logging = 'all'; + //options.logging = 'all'; this.connection = await createConnection(options); await SQLConnection.schemeSync(this.connection); } diff --git a/backend/model/sql/SearchManager.ts b/backend/model/sql/SearchManager.ts index 982c072..f6c06fa 100644 --- a/backend/model/sql/SearchManager.ts +++ b/backend/model/sql/SearchManager.ts @@ -8,6 +8,7 @@ import {MediaEntity} from './enitites/MediaEntity'; import {VideoEntity} from './enitites/VideoEntity'; import {PersonEntry} from './enitites/PersonEntry'; import {FaceRegionEntry} from './enitites/FaceRegionEntry'; +import {SelectQueryBuilder} from 'typeorm'; export class SearchManager implements ISearchManager { @@ -24,7 +25,7 @@ export class SearchManager implements ISearchManager { return a; } - async autocomplete(text: string): Promise> { + async autocomplete(text: string): Promise { const connection = await SQLConnection.getConnection(); @@ -122,61 +123,60 @@ export class SearchManager implements ISearchManager { resultOverflow: false }; - let repository = connection.getRepository(MediaEntity); - const faceRepository = connection.getRepository(FaceRegionEntry); + let usedEntity = MediaEntity; if (searchType === SearchTypes.photo) { - repository = connection.getRepository(PhotoEntity); + usedEntity = PhotoEntity; } else if (searchType === SearchTypes.video) { - repository = connection.getRepository(VideoEntity); + usedEntity = VideoEntity; } - const query = repository.createQueryBuilder('media') + const query = await connection.getRepository(usedEntity).createQueryBuilder('media') + .innerJoin(q => { + const subQuery = q.from(usedEntity, 'media') + .select('distinct media.id') + .limit(2000); + + + if (!searchType || searchType === SearchTypes.directory) { + subQuery.leftJoin('media.directory', 'directory') + .orWhere('directory.name LIKE :text COLLATE utf8_general_ci', {text: '%' + text + '%'}); + } + + if (!searchType || searchType === SearchTypes.photo || searchType === SearchTypes.video) { + subQuery.orWhere('media.name LIKE :text COLLATE utf8_general_ci', {text: '%' + text + '%'}); + } + + if (!searchType || searchType === SearchTypes.photo) { + subQuery.orWhere('media.metadata.caption LIKE :text COLLATE utf8_general_ci', {text: '%' + text + '%'}); + } + if (!searchType || searchType === SearchTypes.person) { + subQuery + .leftJoin('media.metadata.faces', 'faces') + .leftJoin('faces.person', 'person') + .orWhere('person.name LIKE :text COLLATE utf8_general_ci', {text: '%' + text + '%'}); + } + + if (!searchType || searchType === SearchTypes.position) { + subQuery.orWhere('media.metadata.positionData.country LIKE :text COLLATE utf8_general_ci', {text: '%' + text + '%'}) + .orWhere('media.metadata.positionData.state LIKE :text COLLATE utf8_general_ci', {text: '%' + text + '%'}) + .orWhere('media.metadata.positionData.city LIKE :text COLLATE utf8_general_ci', {text: '%' + text + '%'}); + + } + if (!searchType || searchType === SearchTypes.keyword) { + subQuery.orWhere('media.metadata.keywords LIKE :text COLLATE utf8_general_ci', {text: '%' + text + '%'}); + } + + return subQuery; + }, + 'innerMedia', + 'media.id=innerMedia.id') .leftJoinAndSelect('media.directory', 'directory') - .leftJoin('media.metadata.faces', 'faces') - .leftJoin('faces.person', 'person') - .orderBy('media.metadata.creationDate', 'ASC'); + .leftJoinAndSelect('media.metadata.faces', 'faces') + .leftJoinAndSelect('faces.person', 'person'); - if (!searchType || searchType === SearchTypes.directory) { - query.orWhere('directory.name LIKE :text COLLATE utf8_general_ci', {text: '%' + text + '%'}); - } - - if (!searchType || searchType === SearchTypes.photo || searchType === SearchTypes.video) { - query.orWhere('media.name LIKE :text COLLATE utf8_general_ci', {text: '%' + text + '%'}); - } - - if (!searchType || searchType === SearchTypes.photo) { - query.orWhere('media.metadata.caption LIKE :text COLLATE utf8_general_ci', {text: '%' + text + '%'}); - } - if (!searchType || searchType === SearchTypes.person) { - query.orWhere('person.name LIKE :text COLLATE utf8_general_ci', {text: '%' + text + '%'}); - } - - if (!searchType || searchType === SearchTypes.position) { - query.orWhere('media.metadata.positionData.country LIKE :text COLLATE utf8_general_ci', {text: '%' + text + '%'}) - .orWhere('media.metadata.positionData.state LIKE :text COLLATE utf8_general_ci', {text: '%' + text + '%'}) - .orWhere('media.metadata.positionData.city LIKE :text COLLATE utf8_general_ci', {text: '%' + text + '%'}); - - } - if (!searchType || searchType === SearchTypes.keyword) { - query.orWhere('media.metadata.keywords LIKE :text COLLATE utf8_general_ci', {text: '%' + text + '%'}); - } - - - result.media = (await query - .limit(5000).getMany()).slice(0, 2001); - - for (let i = 0; i < result.media.length; i++) { - const faces = (await faceRepository - .createQueryBuilder('faces') - .leftJoinAndSelect('faces.person', 'person') - .where('faces.media = :media', {media: result.media[i].id}) - .getMany()).map(fE => ({name: fE.person.name, box: fE.box})); - if (faces.length > 0) { - result.media[i].metadata.faces = faces; - } - } + result.media = await this.loadMediaWithFaces(query); if (result.media.length > 2000) { result.resultOverflow = true; @@ -208,35 +208,30 @@ export class SearchManager implements ISearchManager { resultOverflow: false }; - const faceRepository = connection.getRepository(FaceRegionEntry); + const query = await connection.getRepository(MediaEntity).createQueryBuilder('media') + .innerJoin(q => q.from(MediaEntity, 'media') + .select('distinct media.id') + .limit(10) + .leftJoin('media.directory', 'directory') + .leftJoin('media.metadata.faces', 'faces') + .leftJoin('faces.person', 'person') + .where('media.metadata.keywords LIKE :text COLLATE utf8_general_ci', {text: '%' + text + '%'}) + .orWhere('media.metadata.positionData.country LIKE :text COLLATE utf8_general_ci', {text: '%' + text + '%'}) + .orWhere('media.metadata.positionData.state LIKE :text COLLATE utf8_general_ci', {text: '%' + text + '%'}) + .orWhere('media.metadata.positionData.city LIKE :text COLLATE utf8_general_ci', {text: '%' + text + '%'}) + .orWhere('media.name LIKE :text COLLATE utf8_general_ci', {text: '%' + text + '%'}) + .orWhere('media.metadata.caption LIKE :text COLLATE utf8_general_ci', {text: '%' + text + '%'}) + .orWhere('person.name LIKE :text COLLATE utf8_general_ci', {text: '%' + text + '%'}) + , + 'innerMedia', + 'media.id=innerMedia.id') + .leftJoinAndSelect('media.directory', 'directory') + .leftJoinAndSelect('media.metadata.faces', 'faces') + .leftJoinAndSelect('faces.person', 'person'); - result.media = await connection - .getRepository(MediaEntity) - .createQueryBuilder('media') - .orderBy('media.metadata.creationDate', 'ASC') - .where('media.metadata.keywords LIKE :text COLLATE utf8_general_ci', {text: '%' + text + '%'}) - .orWhere('media.metadata.positionData.country LIKE :text COLLATE utf8_general_ci', {text: '%' + text + '%'}) - .orWhere('media.metadata.positionData.state LIKE :text COLLATE utf8_general_ci', {text: '%' + text + '%'}) - .orWhere('media.metadata.positionData.city LIKE :text COLLATE utf8_general_ci', {text: '%' + text + '%'}) - .orWhere('media.name LIKE :text COLLATE utf8_general_ci', {text: '%' + text + '%'}) - .orWhere('media.metadata.caption LIKE :text COLLATE utf8_general_ci', {text: '%' + text + '%'}) - .orWhere('person.name LIKE :text COLLATE utf8_general_ci', {text: '%' + text + '%'}) - .innerJoinAndSelect('media.directory', 'directory') - .leftJoin('media.metadata.faces', 'faces') - .leftJoin('faces.person', 'person') - .limit(10) - .getMany(); - for (let i = 0; i < result.media.length; i++) { - const faces = (await faceRepository - .createQueryBuilder('faces') - .leftJoinAndSelect('faces.person', 'person') - .where('faces.media = :media', {media: result.media[i].id}) - .getMany()).map(fE => ({name: fE.person.name, box: fE.box})); - if (faces.length > 0) { - result.media[i].metadata.faces = faces; - } - } + result.media = await this.loadMediaWithFaces(query); + result.directories = await connection .getRepository(DirectoryEntity) @@ -255,4 +250,29 @@ export class SearchManager implements ISearchManager { }); return res; } + + private async loadMediaWithFaces(query: SelectQueryBuilder) { + const rawAndEntities = await query.orderBy('media.id').getRawAndEntities(); + const media: MediaEntity[] = rawAndEntities.entities; + + // console.log(rawAndEntities.raw); + let rawIndex = 0; + for (let i = 0; i < media.length; i++) { + if (rawAndEntities.raw[rawIndex].faces_id === null || + rawAndEntities.raw[rawIndex].media_id !== media[i].id) { + delete media[i].metadata.faces; + continue; + } + media[i].metadata.faces = []; + + while (rawAndEntities.raw[rawIndex].media_id === media[i].id) { + media[i].metadata.faces.push(FaceRegionEntry.fromRawToDTO(rawAndEntities.raw[rawIndex])); + rawIndex++; + if (rawIndex >= rawAndEntities.raw.length) { + return media; + } + } + } + return media; + } } diff --git a/backend/model/sql/enitites/FaceRegionEntry.ts b/backend/model/sql/enitites/FaceRegionEntry.ts index f9d1afd..3b569a5 100644 --- a/backend/model/sql/enitites/FaceRegionEntry.ts +++ b/backend/model/sql/enitites/FaceRegionEntry.ts @@ -1,7 +1,7 @@ -import {FaceRegionBox} from '../../../../common/entities/PhotoDTO'; -import {Column, ManyToOne, Entity, PrimaryGeneratedColumn} from 'typeorm'; +import {FaceRegion, FaceRegionBox} from '../../../../common/entities/PhotoDTO'; +import {Column, Entity, ManyToOne, PrimaryGeneratedColumn} from 'typeorm'; import {PersonEntry} from './PersonEntry'; -import {MediaEntity, MediaMetadataEntity} from './MediaEntity'; +import {MediaEntity} from './MediaEntity'; export class FaceRegionBoxEntry implements FaceRegionBox { @Column('int') @@ -35,4 +35,21 @@ export class FaceRegionEntry { person: PersonEntry; name: string; + + public static fromRawToDTO(raw: { + faces_id: number, + faces_mediaId: number, + faces_personId: number, + faces_boxHeight: number, + faces_boxWidth: number, + faces_boxX: number, + faces_boxY: number, + person_id: number, + person_name: string + }): FaceRegion { + return { + box: {width: raw.faces_boxWidth, height: raw.faces_boxHeight, x: raw.faces_boxX, y: raw.faces_boxY}, + name: raw.person_name + }; + } } diff --git a/benchmark/Benchmarks.ts b/benchmark/Benchmarks.ts new file mode 100644 index 0000000..58b34e8 --- /dev/null +++ b/benchmark/Benchmarks.ts @@ -0,0 +1,137 @@ +import {SQLConnection} from '../backend/model/sql/SQLConnection'; +import {Config} from '../common/config/private/Config'; +import {DatabaseType, ReIndexingSensitivity} from '../common/config/private/IPrivateConfig'; +import {ObjectManagerRepository} from '../backend/model/ObjectManagerRepository'; +import {DiskMangerWorker} from '../backend/model/threading/DiskMangerWorker'; +import {IndexingManager} from '../backend/model/sql/IndexingManager'; +import {SearchManager} from '../backend/model/sql/SearchManager'; +import * as fs from 'fs'; +import {SearchTypes} from '../common/entities/AutoCompleteItem'; +import {Utils} from '../common/Utils'; +import {GalleryManager} from '../backend/model/sql/GalleryManager'; +import {DirectoryDTO} from '../common/entities/DirectoryDTO'; + +export interface BenchmarkResult { + duration: number; + directories?: number; + media?: number; + items?: number; +} + +export class BMIndexingManager extends IndexingManager { + + public async saveToDB(scannedDirectory: DirectoryDTO): Promise { + return super.saveToDB(scannedDirectory); + } +} + +export class Benchmarks { + + constructor(public RUNS: number, public dbPath: string) { + + } + + async bmSaveDirectory(): Promise { + await this.resetDB(); + const dir = await DiskMangerWorker.scanDirectory('./'); + const im = new BMIndexingManager(); + return await this.benchmark(() => im.saveToDB(dir), () => this.resetDB()); + } + + async bmScanDirectory(): Promise { + return await this.benchmark(() => DiskMangerWorker.scanDirectory('./')); + } + + async bmListDirectory(): Promise { + const gm = new GalleryManager(); + await this.setupDB(); + Config.Server.indexing.reIndexingSensitivity = ReIndexingSensitivity.low; + return await this.benchmark(() => gm.listDirectory('./')); + } + + async bmAllSearch(text: string): Promise<{ result: BenchmarkResult, searchType: SearchTypes }[]> { + await this.setupDB(); + const types = Utils.enumToArray(SearchTypes).map(a => a.key).concat([null]); + const results: { result: BenchmarkResult, searchType: SearchTypes }[] = []; + const sm = new SearchManager(); + for (let i = 0; i < types.length; i++) { + results.push({result: await this.benchmark(() => sm.search(text, types[i])), searchType: types[i]}); + } + return results; + } + + async bmInstantSearch(text: string): Promise { + await this.setupDB(); + const sm = new SearchManager(); + return await this.benchmark(() => sm.instantSearch(text)); + } + + async bmAutocomplete(text: string): Promise { + await this.setupDB(); + const sm = new SearchManager(); + return await this.benchmark(() => sm.autocomplete(text)); + } + + + private async benchmark(fn: () => Promise<{ media: any[], directories: any[] } | any[] | void>, + beforeEach: () => Promise = null, + afterEach: () => Promise = null) { + const scanned = await fn(); + const start = process.hrtime(); + let skip = 0; + for (let i = 0; i < this.RUNS; i++) { + if (beforeEach) { + const startSkip = process.hrtime(); + await beforeEach(); + const endSkip = process.hrtime(startSkip); + skip += (endSkip[0] * 1000 + endSkip[1] / 1000); + } + await fn(); + if (afterEach) { + const startSkip = process.hrtime(); + await afterEach(); + const endSkip = process.hrtime(startSkip); + skip += (endSkip[0] * 1000 + endSkip[1] / 1000); + } + } + const end = process.hrtime(start); + const duration = (end[0] * 1000 + end[1] / 1000) / this.RUNS; + + if (!scanned) { + return { + duration: duration + }; + } + + if (Array.isArray(scanned)) { + return { + duration: duration, + items: scanned.length + }; + } + + return { + duration: duration, + media: scanned.media.length, + directories: scanned.directories.length + }; + } + + private resetDB = async () => { + await SQLConnection.close(); + if (fs.existsSync(this.dbPath)) { + fs.unlinkSync(this.dbPath); + } + Config.Server.database.type = DatabaseType.sqlite; + Config.Server.database.sqlite.storage = this.dbPath; + await ObjectManagerRepository.InitSQLManagers(); + }; + + private async setupDB() { + const im = new BMIndexingManager(); + await this.resetDB(); + const dir = await DiskMangerWorker.scanDirectory('./'); + await im.saveToDB(dir); + } + +} diff --git a/benchmark/README.md b/benchmark/README.md new file mode 100644 index 0000000..9ace280 --- /dev/null +++ b/benchmark/README.md @@ -0,0 +1,5 @@ +# PiGallery2 performance benchmark results + +These results are created mostly for development, but I'm making them public for curious users. + + diff --git a/benchmark/index.ts b/benchmark/index.ts new file mode 100644 index 0000000..98a6048 --- /dev/null +++ b/benchmark/index.ts @@ -0,0 +1,70 @@ +import {Config} from '../common/config/private/Config'; +import * as path from 'path'; +import {ProjectPath} from '../backend/ProjectPath'; +import {BenchmarkResult, Benchmarks} from './Benchmarks'; +import {SearchTypes} from '../common/entities/AutoCompleteItem'; +import {Utils} from '../common/Utils'; +import {DiskMangerWorker} from '../backend/model/threading/DiskMangerWorker'; + +const config: { path: string, system: string } = require(path.join(__dirname, 'config.json')); +Config.Server.imagesFolder = config.path; +const dbPath = path.join(__dirname, 'test.db'); +ProjectPath.reset(); +const RUNS = 1; + +let resultsText = ''; +const printLine = (text: string) => { + resultsText += text + '\n'; +}; + +const printHeader = async () => { + const dt = new Date(); + printLine('## PiGallery2 v' + require('./../package.json').version + + ', ' + Utils.zeroPrefix(dt.getDay(), 2) + + '.' + Utils.zeroPrefix(dt.getMonth() + 1, 2) + + '.' + dt.getFullYear()); + printLine('**System**: ' + config.system); + const dir = await DiskMangerWorker.scanDirectory('./'); + printLine('**Gallery**: directories:' + dir.directories.length + ' media:' + dir.media.length); +}; + + +const printTableHeader = () => { + printLine('| action | action details | average time | details |'); + printLine('|:------:|:--------------:|:------------:|:-------:|'); +}; +const printResult = (result: BenchmarkResult, action: string, actionDetails: string = '') => { + let details = '-'; + if (result.items) { + details = 'items: ' + result.items; + } + if (result.media) { + details = 'media: ' + result.media + ', directories:' + result.directories; + } + printLine('| ' + action + ' | ' + actionDetails + + ' | ' + (result.duration / 1000).toFixed(2) + 's | ' + details + ' |'); +}; + +const run = async () => { + const bm = new Benchmarks(RUNS, dbPath); + + // header + await printHeader(); + printTableHeader(); + printResult(await bm.bmScanDirectory(), 'Scanning directory'); + printResult(await bm.bmSaveDirectory(), 'Saving directory'); + printResult(await bm.bmListDirectory(), 'Listing Directory'); + (await bm.bmAllSearch('a')).forEach(res => { + if (res.searchType !== null) { + printResult(res.result, 'searching', '`a` as `' + SearchTypes[res.searchType] + '`'); + } else { + printResult(res.result, 'searching', '`a` as `any`'); + } + }); + printResult(await bm.bmInstantSearch('a'), 'instant search', '`a`'); + printResult(await bm.bmAutocomplete('a'), 'auto complete', '`a`'); + console.log(resultsText); +}; + +run(); + diff --git a/common/entities/Error.ts b/common/entities/Error.ts index d64cbba..6a84c06 100644 --- a/common/entities/Error.ts +++ b/common/entities/Error.ts @@ -23,4 +23,8 @@ export enum ErrorCodes { export class ErrorDTO { constructor(public code: ErrorCodes, public message?: string, public details?: any) { } + + toString(): string { + return '[' + ErrorCodes[this.code] + '] ' + this.message + (this.details ? this.details.toString() : ''); + } } diff --git a/test/backend/unit/assets/test image öüóőúéáű-.,.jpg b/test/backend/unit/assets/test image öüóőúéáű-.,.jpg index e82c52e..f0fa299 100644 Binary files a/test/backend/unit/assets/test image öüóőúéáű-.,.jpg and b/test/backend/unit/assets/test image öüóőúéáű-.,.jpg differ diff --git a/test/backend/unit/assets/test image öüóőúéáű-.,.json b/test/backend/unit/assets/test image öüóőúéáű-.,.json index 3d4100d..dd0bd2c 100644 --- a/test/backend/unit/assets/test image öüóőúéáű-.,.json +++ b/test/backend/unit/assets/test image öüóőúéáű-.,.json @@ -13,15 +13,24 @@ "faces": [ { "box": { - "height": 19, - "width": 20, - "x": 82, - "y": 38 + "height": 2, + "width": 2, + "x": 8, + "y": 4 }, "name": "squirrel" + }, + { + "box": { + "height": 3, + "width": 2, + "x": 5, + "y": 5 + }, + "name": "special_chars űáéúőóüío?._:" } ], - "fileSize": 59187, + "fileSize": 39424, "keywords": [ "Berkley", "USA", @@ -39,7 +48,7 @@ "state": "test state őúéáűóöí-.,)(" }, "size": { - "height": 93, - "width": 140 + "height": 10, + "width": 14 } } diff --git a/test/backend/unit/model/sql/SearchManager.ts b/test/backend/unit/model/sql/SearchManager.ts index cefccda..671d758 100644 --- a/test/backend/unit/model/sql/SearchManager.ts +++ b/test/backend/unit/model/sql/SearchManager.ts @@ -4,13 +4,7 @@ import * as path from 'path'; import {Config} from '../../../../../common/config/private/Config'; import {DatabaseType} from '../../../../../common/config/private/IPrivateConfig'; import {SQLConnection} from '../../../../../backend/model/sql/SQLConnection'; -import { - CameraMetadataEntity, - GPSMetadataEntity, - PhotoEntity, - PhotoMetadataEntity, - PositionMetaDataEntity -} from '../../../../../backend/model/sql/enitites/PhotoEntity'; +import {PhotoEntity} from '../../../../../backend/model/sql/enitites/PhotoEntity'; import {SearchManager} from '../../../../../backend/model/sql/SearchManager'; import {AutoCompleteItem, SearchTypes} from '../../../../../common/entities/AutoCompleteItem'; import {SearchResultDTO} from '../../../../../common/entities/SearchResultDTO'; @@ -18,6 +12,9 @@ import {DirectoryEntity} from '../../../../../backend/model/sql/enitites/Directo import {Utils} from '../../../../../common/Utils'; import {TestHelper} from './TestHelper'; import {VideoEntity} from '../../../../../backend/model/sql/enitites/VideoEntity'; +import {PersonEntry} from '../../../../../backend/model/sql/enitites/PersonEntry'; +import {FaceRegionEntry} from '../../../../../backend/model/sql/enitites/FaceRegionEntry'; +import {PhotoDTO} from '../../../../../common/entities/PhotoDTO'; describe('SearchManager', () => { @@ -28,6 +25,9 @@ describe('SearchManager', () => { const dir = TestHelper.getDirectoryEntry(); const p = TestHelper.getPhotoEntry1(dir); const p2 = TestHelper.getPhotoEntry2(dir); + const p_faceLess = TestHelper.getPhotoEntry2(dir); + delete p_faceLess.metadata.faces; + p_faceLess.name = 'fl'; const v = TestHelper.getVideoEntry1(dir); const setUpSqlDB = async () => { @@ -41,13 +41,26 @@ describe('SearchManager', () => { Config.Server.database.type = DatabaseType.sqlite; Config.Server.database.sqlite.storage = dbPath; + const savePhoto = async (photo: PhotoDTO) => { + const savedPhoto = await pr.save(photo); + if (!photo.metadata.faces) { + return; + } + for (let i = 0; i < photo.metadata.faces.length; i++) { + const face = photo.metadata.faces[i]; + const person = await conn.getRepository(PersonEntry).save({name: face.name}); + await conn.getRepository(FaceRegionEntry).save({box: face.box, person: person, media: savedPhoto}); + } + }; const conn = await SQLConnection.getConnection(); const pr = conn.getRepository(PhotoEntity); await conn.getRepository(DirectoryEntity).save(p.directory); - await pr.save(p); - await pr.save(p2); + await savePhoto(p); + await savePhoto(p2); + await savePhoto(p_faceLess); + await conn.getRepository(VideoEntity).save(v); await SQLConnection.close(); @@ -76,6 +89,9 @@ describe('SearchManager', () => { const sm = new SearchManager(); const cmp = (a: AutoCompleteItem, b: AutoCompleteItem) => { + if (a.text === b.text) { + return a.type - b.type; + } return a.text.localeCompare(b.text); }; @@ -89,9 +105,14 @@ describe('SearchManager', () => { expect((await sm.autocomplete('arch'))).eql([new AutoCompleteItem('Research City', SearchTypes.position)]); expect((await sm.autocomplete('a')).sort(cmp)).eql([ new AutoCompleteItem('Boba Fett', SearchTypes.keyword), + new AutoCompleteItem('Boba Fett', SearchTypes.person), new AutoCompleteItem('star wars', SearchTypes.keyword), new AutoCompleteItem('Anakin', SearchTypes.keyword), + new AutoCompleteItem('Anakin Skywalker', SearchTypes.person), + new AutoCompleteItem('Luke Skywalker', SearchTypes.person), + new AutoCompleteItem('Han Solo', SearchTypes.person), new AutoCompleteItem('death star', SearchTypes.keyword), + new AutoCompleteItem('Padmé Amidala', SearchTypes.person), new AutoCompleteItem('Padmé Amidala', SearchTypes.keyword), new AutoCompleteItem('Natalie Portman', SearchTypes.keyword), new AutoCompleteItem('Han Solo\'s dice', SearchTypes.photo), @@ -120,6 +141,15 @@ describe('SearchManager', () => { resultOverflow: false })); + expect(Utils.clone(await sm.search('Boba', null))).to.deep.equal(Utils.clone({ + searchText: 'Boba', + searchType: null, + directories: [], + media: [p], + metaFile: [], + resultOverflow: false + })); + expect(Utils.clone(await sm.search('Tatooine', SearchTypes.position))).to.deep.equal(Utils.clone({ searchText: 'Tatooine', searchType: SearchTypes.position, @@ -133,7 +163,7 @@ describe('SearchManager', () => { searchText: 'ortm', searchType: SearchTypes.keyword, directories: [], - media: [p2], + media: [p2, p_faceLess], metaFile: [], resultOverflow: false })); @@ -142,7 +172,7 @@ describe('SearchManager', () => { searchText: 'ortm', searchType: SearchTypes.keyword, directories: [], - media: [p2], + media: [p2, p_faceLess], metaFile: [], resultOverflow: false })); @@ -151,7 +181,7 @@ describe('SearchManager', () => { searchText: 'wa', searchType: SearchTypes.keyword, directories: [dir], - media: [p, p2], + media: [p, p2, p_faceLess], metaFile: [], resultOverflow: false })); @@ -165,6 +195,15 @@ describe('SearchManager', () => { resultOverflow: false })); + expect(Utils.clone(await sm.search('sw', SearchTypes.video))).to.deep.equal(Utils.clone({ + searchText: 'sw', + searchType: SearchTypes.video, + directories: [], + media: [v], + metaFile: [], + resultOverflow: false + })); + expect(Utils.clone(await sm.search('han', SearchTypes.keyword))).to.deep.equal(Utils.clone({ searchText: 'han', searchType: SearchTypes.keyword, @@ -173,6 +212,15 @@ describe('SearchManager', () => { metaFile: [], resultOverflow: false })); + + expect(Utils.clone(await sm.search('Boba', SearchTypes.person))).to.deep.equal(Utils.clone({ + searchText: 'Boba', + searchType: SearchTypes.person, + directories: [], + media: [p], + metaFile: [], + resultOverflow: false + })); }); @@ -198,23 +246,16 @@ describe('SearchManager', () => { expect(Utils.clone(await sm.instantSearch('ortm'))).to.deep.equal(Utils.clone({ searchText: 'ortm', directories: [], - media: [p2], + media: [p2, p_faceLess], metaFile: [], resultOverflow: false })); - expect(Utils.clone(await sm.instantSearch('ortm'))).to.deep.equal(Utils.clone({ - searchText: 'ortm', - directories: [], - media: [p2], - metaFile: [], - resultOverflow: false - })); expect(Utils.clone(await sm.instantSearch('wa'))).to.deep.equal(Utils.clone({ searchText: 'wa', directories: [dir], - media: [p, p2], + media: [p, p2, p_faceLess], metaFile: [], resultOverflow: false })); @@ -226,6 +267,13 @@ describe('SearchManager', () => { metaFile: [], resultOverflow: false })); + expect(Utils.clone(await sm.instantSearch('Boba'))).to.deep.equal(Utils.clone({ + searchText: 'Boba', + directories: [], + media: [p], + metaFile: [], + resultOverflow: false + })); }); diff --git a/test/backend/unit/model/sql/TestHelper.ts b/test/backend/unit/model/sql/TestHelper.ts index 5c78941..bc9f685 100644 --- a/test/backend/unit/model/sql/TestHelper.ts +++ b/test/backend/unit/model/sql/TestHelper.ts @@ -104,6 +104,20 @@ export class TestHelper { p.metadata.positionData.city = 'Mos Eisley'; p.metadata.positionData.country = 'Tatooine'; p.name = 'sw1'; + + p.metadata.faces = [{ + box: {height: 10, width: 10, x: 10, y: 10}, + name: 'Boba Fett' + }, { + box: {height: 10, width: 10, x: 101, y: 101}, + name: 'Luke Skywalker' + }, { + box: {height: 10, width: 10, x: 101, y: 101}, + name: 'Han Solo' + }, { + box: {height: 10, width: 10, x: 101, y: 101}, + name: 'Unkle Ben' + }]; return p; } @@ -121,6 +135,16 @@ export class TestHelper { p.metadata.positionData.state = 'Research City'; p.metadata.positionData.country = 'Kamino'; p.name = 'sw2'; + p.metadata.faces = [{ + box: {height: 10, width: 10, x: 10, y: 10}, + name: 'Padmé Amidala' + }, { + box: {height: 10, width: 10, x: 101, y: 101}, + name: 'Anakin Skywalker' + }, { + box: {height: 10, width: 10, x: 101, y: 101}, + name: 'Obivan Kenobi' + }]; return p; } diff --git a/test/backend/unit/model/threading/DiskMangerWorker.spec.ts b/test/backend/unit/model/threading/DiskMangerWorker.spec.ts index 641c8d8..b045bb9 100644 --- a/test/backend/unit/model/threading/DiskMangerWorker.spec.ts +++ b/test/backend/unit/model/threading/DiskMangerWorker.spec.ts @@ -11,10 +11,10 @@ describe('DiskMangerWorker', () => { Config.Server.imagesFolder = path.join(__dirname, '/../../assets'); ProjectPath.ImageFolder = path.join(__dirname, '/../../assets'); const dir = await DiskMangerWorker.scanDirectory('/'); - expect(dir.media.length).to.be.equals(2); + expect(dir.media.length).to.be.equals(3); const expected = require(path.join(__dirname, '/../../assets/test image öüóőúéáű-.,.json')); - expect(Utils.clone(dir.media[0].name)).to.be.deep.equal('test image öüóőúéáű-.,.jpg'); - expect(Utils.clone(dir.media[0].metadata)).to.be.deep.equal(expected); + expect(Utils.clone(dir.media[1].name)).to.be.deep.equal('test image öüóőúéáű-.,.jpg'); + expect(Utils.clone(dir.media[1].metadata)).to.be.deep.equal(expected); }); });