fixing searching

This commit is contained in:
Patrik J. Braun 2019-01-26 18:03:40 -05:00
parent 1f58bffa2c
commit 0445c499e8
13 changed files with 445 additions and 111 deletions

View File

@ -67,7 +67,7 @@ export class GalleryManager implements IGalleryManager, ISQLGalleryManager {
// on the fly reindexing // on the fly reindexing
Logger.silly(LOG_TAG, 'lazy reindexing reason: cache timeout: lastScanned: ' 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) => { ObjectManagerRepository.getInstance().IndexingManager.indexDirectory(relativeDirectoryName).catch((err) => {
console.error(err); console.error(err);
}); });

View File

@ -44,7 +44,7 @@ export class SQLConnection {
VersionEntity VersionEntity
]; ];
options.synchronize = false; options.synchronize = false;
// options.logging = 'all'; //options.logging = 'all';
this.connection = await createConnection(options); this.connection = await createConnection(options);
await SQLConnection.schemeSync(this.connection); await SQLConnection.schemeSync(this.connection);
} }

View File

@ -8,6 +8,7 @@ import {MediaEntity} from './enitites/MediaEntity';
import {VideoEntity} from './enitites/VideoEntity'; import {VideoEntity} from './enitites/VideoEntity';
import {PersonEntry} from './enitites/PersonEntry'; import {PersonEntry} from './enitites/PersonEntry';
import {FaceRegionEntry} from './enitites/FaceRegionEntry'; import {FaceRegionEntry} from './enitites/FaceRegionEntry';
import {SelectQueryBuilder} from 'typeorm';
export class SearchManager implements ISearchManager { export class SearchManager implements ISearchManager {
@ -24,7 +25,7 @@ export class SearchManager implements ISearchManager {
return a; return a;
} }
async autocomplete(text: string): Promise<Array<AutoCompleteItem>> { async autocomplete(text: string): Promise<AutoCompleteItem[]> {
const connection = await SQLConnection.getConnection(); const connection = await SQLConnection.getConnection();
@ -122,61 +123,60 @@ export class SearchManager implements ISearchManager {
resultOverflow: false resultOverflow: false
}; };
let repository = connection.getRepository(MediaEntity); let usedEntity = MediaEntity;
const faceRepository = connection.getRepository(FaceRegionEntry);
if (searchType === SearchTypes.photo) { if (searchType === SearchTypes.photo) {
repository = connection.getRepository(PhotoEntity); usedEntity = PhotoEntity;
} else if (searchType === SearchTypes.video) { } 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') .leftJoinAndSelect('media.directory', 'directory')
.leftJoin('media.metadata.faces', 'faces') .leftJoinAndSelect('media.metadata.faces', 'faces')
.leftJoin('faces.person', 'person') .leftJoinAndSelect('faces.person', 'person');
.orderBy('media.metadata.creationDate', 'ASC');
if (!searchType || searchType === SearchTypes.directory) { result.media = await this.loadMediaWithFaces(query);
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;
}
}
if (result.media.length > 2000) { if (result.media.length > 2000) {
result.resultOverflow = true; result.resultOverflow = true;
@ -208,35 +208,30 @@ export class SearchManager implements ISearchManager {
resultOverflow: false 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++) { result.media = await this.loadMediaWithFaces(query);
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.directories = await connection result.directories = await connection
.getRepository(DirectoryEntity) .getRepository(DirectoryEntity)
@ -255,4 +250,29 @@ export class SearchManager implements ISearchManager {
}); });
return res; return res;
} }
private async loadMediaWithFaces(query: SelectQueryBuilder<MediaEntity>) {
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(<any>FaceRegionEntry.fromRawToDTO(rawAndEntities.raw[rawIndex]));
rawIndex++;
if (rawIndex >= rawAndEntities.raw.length) {
return media;
}
}
}
return media;
}
} }

View File

@ -1,7 +1,7 @@
import {FaceRegionBox} from '../../../../common/entities/PhotoDTO'; import {FaceRegion, FaceRegionBox} from '../../../../common/entities/PhotoDTO';
import {Column, ManyToOne, Entity, PrimaryGeneratedColumn} from 'typeorm'; import {Column, Entity, ManyToOne, PrimaryGeneratedColumn} from 'typeorm';
import {PersonEntry} from './PersonEntry'; import {PersonEntry} from './PersonEntry';
import {MediaEntity, MediaMetadataEntity} from './MediaEntity'; import {MediaEntity} from './MediaEntity';
export class FaceRegionBoxEntry implements FaceRegionBox { export class FaceRegionBoxEntry implements FaceRegionBox {
@Column('int') @Column('int')
@ -35,4 +35,21 @@ export class FaceRegionEntry {
person: PersonEntry; person: PersonEntry;
name: string; 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
};
}
} }

137
benchmark/Benchmarks.ts Normal file
View File

@ -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<void> {
return super.saveToDB(scannedDirectory);
}
}
export class Benchmarks {
constructor(public RUNS: number, public dbPath: string) {
}
async bmSaveDirectory(): Promise<BenchmarkResult> {
await this.resetDB();
const dir = await DiskMangerWorker.scanDirectory('./');
const im = new BMIndexingManager();
return await this.benchmark(() => im.saveToDB(dir), () => this.resetDB());
}
async bmScanDirectory(): Promise<BenchmarkResult> {
return await this.benchmark(() => DiskMangerWorker.scanDirectory('./'));
}
async bmListDirectory(): Promise<BenchmarkResult> {
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<BenchmarkResult> {
await this.setupDB();
const sm = new SearchManager();
return await this.benchmark(() => sm.instantSearch(text));
}
async bmAutocomplete(text: string): Promise<BenchmarkResult> {
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<any> = null,
afterEach: () => Promise<any> = 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);
}
}

5
benchmark/README.md Normal file
View File

@ -0,0 +1,5 @@
# PiGallery2 performance benchmark results
These results are created mostly for development, but I'm making them public for curious users.

70
benchmark/index.ts Normal file
View File

@ -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();

View File

@ -23,4 +23,8 @@ export enum ErrorCodes {
export class ErrorDTO { export class ErrorDTO {
constructor(public code: ErrorCodes, public message?: string, public details?: any) { constructor(public code: ErrorCodes, public message?: string, public details?: any) {
} }
toString(): string {
return '[' + ErrorCodes[this.code] + '] ' + this.message + (this.details ? this.details.toString() : '');
}
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 38 KiB

View File

@ -13,15 +13,24 @@
"faces": [ "faces": [
{ {
"box": { "box": {
"height": 19, "height": 2,
"width": 20, "width": 2,
"x": 82, "x": 8,
"y": 38 "y": 4
}, },
"name": "squirrel" "name": "squirrel"
},
{
"box": {
"height": 3,
"width": 2,
"x": 5,
"y": 5
},
"name": "special_chars űáéúőóüío?._:"
} }
], ],
"fileSize": 59187, "fileSize": 39424,
"keywords": [ "keywords": [
"Berkley", "Berkley",
"USA", "USA",
@ -39,7 +48,7 @@
"state": "test state őúéáűóöí-.,)(" "state": "test state őúéáűóöí-.,)("
}, },
"size": { "size": {
"height": 93, "height": 10,
"width": 140 "width": 14
} }
} }

View File

@ -4,13 +4,7 @@ import * as path from 'path';
import {Config} from '../../../../../common/config/private/Config'; import {Config} from '../../../../../common/config/private/Config';
import {DatabaseType} from '../../../../../common/config/private/IPrivateConfig'; import {DatabaseType} from '../../../../../common/config/private/IPrivateConfig';
import {SQLConnection} from '../../../../../backend/model/sql/SQLConnection'; import {SQLConnection} from '../../../../../backend/model/sql/SQLConnection';
import { import {PhotoEntity} from '../../../../../backend/model/sql/enitites/PhotoEntity';
CameraMetadataEntity,
GPSMetadataEntity,
PhotoEntity,
PhotoMetadataEntity,
PositionMetaDataEntity
} from '../../../../../backend/model/sql/enitites/PhotoEntity';
import {SearchManager} from '../../../../../backend/model/sql/SearchManager'; import {SearchManager} from '../../../../../backend/model/sql/SearchManager';
import {AutoCompleteItem, SearchTypes} from '../../../../../common/entities/AutoCompleteItem'; import {AutoCompleteItem, SearchTypes} from '../../../../../common/entities/AutoCompleteItem';
import {SearchResultDTO} from '../../../../../common/entities/SearchResultDTO'; import {SearchResultDTO} from '../../../../../common/entities/SearchResultDTO';
@ -18,6 +12,9 @@ import {DirectoryEntity} from '../../../../../backend/model/sql/enitites/Directo
import {Utils} from '../../../../../common/Utils'; import {Utils} from '../../../../../common/Utils';
import {TestHelper} from './TestHelper'; import {TestHelper} from './TestHelper';
import {VideoEntity} from '../../../../../backend/model/sql/enitites/VideoEntity'; 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', () => { describe('SearchManager', () => {
@ -28,6 +25,9 @@ describe('SearchManager', () => {
const dir = TestHelper.getDirectoryEntry(); const dir = TestHelper.getDirectoryEntry();
const p = TestHelper.getPhotoEntry1(dir); const p = TestHelper.getPhotoEntry1(dir);
const p2 = TestHelper.getPhotoEntry2(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 v = TestHelper.getVideoEntry1(dir);
const setUpSqlDB = async () => { const setUpSqlDB = async () => {
@ -41,13 +41,26 @@ describe('SearchManager', () => {
Config.Server.database.type = DatabaseType.sqlite; Config.Server.database.type = DatabaseType.sqlite;
Config.Server.database.sqlite.storage = dbPath; 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 conn = await SQLConnection.getConnection();
const pr = conn.getRepository(PhotoEntity); const pr = conn.getRepository(PhotoEntity);
await conn.getRepository(DirectoryEntity).save(p.directory); await conn.getRepository(DirectoryEntity).save(p.directory);
await pr.save(p); await savePhoto(p);
await pr.save(p2); await savePhoto(p2);
await savePhoto(p_faceLess);
await conn.getRepository(VideoEntity).save(v); await conn.getRepository(VideoEntity).save(v);
await SQLConnection.close(); await SQLConnection.close();
@ -76,6 +89,9 @@ describe('SearchManager', () => {
const sm = new SearchManager(); const sm = new SearchManager();
const cmp = (a: AutoCompleteItem, b: AutoCompleteItem) => { const cmp = (a: AutoCompleteItem, b: AutoCompleteItem) => {
if (a.text === b.text) {
return a.type - b.type;
}
return a.text.localeCompare(b.text); 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('arch'))).eql([new AutoCompleteItem('Research City', SearchTypes.position)]);
expect((await sm.autocomplete('a')).sort(cmp)).eql([ expect((await sm.autocomplete('a')).sort(cmp)).eql([
new AutoCompleteItem('Boba Fett', SearchTypes.keyword), new AutoCompleteItem('Boba Fett', SearchTypes.keyword),
new AutoCompleteItem('Boba Fett', SearchTypes.person),
new AutoCompleteItem('star wars', SearchTypes.keyword), new AutoCompleteItem('star wars', SearchTypes.keyword),
new AutoCompleteItem('Anakin', 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('death star', SearchTypes.keyword),
new AutoCompleteItem('Padmé Amidala', SearchTypes.person),
new AutoCompleteItem('Padmé Amidala', SearchTypes.keyword), new AutoCompleteItem('Padmé Amidala', SearchTypes.keyword),
new AutoCompleteItem('Natalie Portman', SearchTypes.keyword), new AutoCompleteItem('Natalie Portman', SearchTypes.keyword),
new AutoCompleteItem('Han Solo\'s dice', SearchTypes.photo), new AutoCompleteItem('Han Solo\'s dice', SearchTypes.photo),
@ -120,6 +141,15 @@ describe('SearchManager', () => {
resultOverflow: false resultOverflow: false
})); }));
expect(Utils.clone(await sm.search('Boba', null))).to.deep.equal(Utils.clone(<SearchResultDTO>{
searchText: 'Boba',
searchType: null,
directories: [],
media: [p],
metaFile: [],
resultOverflow: false
}));
expect(Utils.clone(await sm.search('Tatooine', SearchTypes.position))).to.deep.equal(Utils.clone(<SearchResultDTO>{ expect(Utils.clone(await sm.search('Tatooine', SearchTypes.position))).to.deep.equal(Utils.clone(<SearchResultDTO>{
searchText: 'Tatooine', searchText: 'Tatooine',
searchType: SearchTypes.position, searchType: SearchTypes.position,
@ -133,7 +163,7 @@ describe('SearchManager', () => {
searchText: 'ortm', searchText: 'ortm',
searchType: SearchTypes.keyword, searchType: SearchTypes.keyword,
directories: [], directories: [],
media: [p2], media: [p2, p_faceLess],
metaFile: [], metaFile: [],
resultOverflow: false resultOverflow: false
})); }));
@ -142,7 +172,7 @@ describe('SearchManager', () => {
searchText: 'ortm', searchText: 'ortm',
searchType: SearchTypes.keyword, searchType: SearchTypes.keyword,
directories: [], directories: [],
media: [p2], media: [p2, p_faceLess],
metaFile: [], metaFile: [],
resultOverflow: false resultOverflow: false
})); }));
@ -151,7 +181,7 @@ describe('SearchManager', () => {
searchText: 'wa', searchText: 'wa',
searchType: SearchTypes.keyword, searchType: SearchTypes.keyword,
directories: [dir], directories: [dir],
media: [p, p2], media: [p, p2, p_faceLess],
metaFile: [], metaFile: [],
resultOverflow: false resultOverflow: false
})); }));
@ -165,6 +195,15 @@ describe('SearchManager', () => {
resultOverflow: false resultOverflow: false
})); }));
expect(Utils.clone(await sm.search('sw', SearchTypes.video))).to.deep.equal(Utils.clone(<SearchResultDTO>{
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(<SearchResultDTO>{ expect(Utils.clone(await sm.search('han', SearchTypes.keyword))).to.deep.equal(Utils.clone(<SearchResultDTO>{
searchText: 'han', searchText: 'han',
searchType: SearchTypes.keyword, searchType: SearchTypes.keyword,
@ -173,6 +212,15 @@ describe('SearchManager', () => {
metaFile: [], metaFile: [],
resultOverflow: false resultOverflow: false
})); }));
expect(Utils.clone(await sm.search('Boba', SearchTypes.person))).to.deep.equal(Utils.clone(<SearchResultDTO>{
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({ expect(Utils.clone(await sm.instantSearch('ortm'))).to.deep.equal(Utils.clone({
searchText: 'ortm', searchText: 'ortm',
directories: [], directories: [],
media: [p2], media: [p2, p_faceLess],
metaFile: [], metaFile: [],
resultOverflow: false 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({ expect(Utils.clone(await sm.instantSearch('wa'))).to.deep.equal(Utils.clone({
searchText: 'wa', searchText: 'wa',
directories: [dir], directories: [dir],
media: [p, p2], media: [p, p2, p_faceLess],
metaFile: [], metaFile: [],
resultOverflow: false resultOverflow: false
})); }));
@ -226,6 +267,13 @@ describe('SearchManager', () => {
metaFile: [], metaFile: [],
resultOverflow: false resultOverflow: false
})); }));
expect(Utils.clone(await sm.instantSearch('Boba'))).to.deep.equal(Utils.clone({
searchText: 'Boba',
directories: [],
media: [p],
metaFile: [],
resultOverflow: false
}));
}); });

View File

@ -104,6 +104,20 @@ export class TestHelper {
p.metadata.positionData.city = 'Mos Eisley'; p.metadata.positionData.city = 'Mos Eisley';
p.metadata.positionData.country = 'Tatooine'; p.metadata.positionData.country = 'Tatooine';
p.name = 'sw1'; p.name = 'sw1';
p.metadata.faces = [<any>{
box: {height: 10, width: 10, x: 10, y: 10},
name: 'Boba Fett'
}, <any>{
box: {height: 10, width: 10, x: 101, y: 101},
name: 'Luke Skywalker'
}, <any>{
box: {height: 10, width: 10, x: 101, y: 101},
name: 'Han Solo'
}, <any>{
box: {height: 10, width: 10, x: 101, y: 101},
name: 'Unkle Ben'
}];
return p; return p;
} }
@ -121,6 +135,16 @@ export class TestHelper {
p.metadata.positionData.state = 'Research City'; p.metadata.positionData.state = 'Research City';
p.metadata.positionData.country = 'Kamino'; p.metadata.positionData.country = 'Kamino';
p.name = 'sw2'; p.name = 'sw2';
p.metadata.faces = [<any>{
box: {height: 10, width: 10, x: 10, y: 10},
name: 'Padmé Amidala'
}, <any>{
box: {height: 10, width: 10, x: 101, y: 101},
name: 'Anakin Skywalker'
}, <any>{
box: {height: 10, width: 10, x: 101, y: 101},
name: 'Obivan Kenobi'
}];
return p; return p;
} }

View File

@ -11,10 +11,10 @@ describe('DiskMangerWorker', () => {
Config.Server.imagesFolder = path.join(__dirname, '/../../assets'); Config.Server.imagesFolder = path.join(__dirname, '/../../assets');
ProjectPath.ImageFolder = path.join(__dirname, '/../../assets'); ProjectPath.ImageFolder = path.join(__dirname, '/../../assets');
const dir = await DiskMangerWorker.scanDirectory('/'); 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')); 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[1].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].metadata)).to.be.deep.equal(expected);
}); });
}); });