From 88015cc33e0e00635998a078e2ff4f78563c0c3b Mon Sep 17 00:00:00 2001 From: "Patrik J. Braun" Date: Sun, 31 Jan 2021 12:22:56 +0100 Subject: [PATCH] Implementing adv. search query builder #58 --- benchmark/BenchmarkRunner.ts | 25 +- benchmark/index.ts | 1 - src/backend/middlewares/GalleryMWs.ts | 4 +- .../model/database/memory/SearchManager.ts | 5 + .../model/database/sql/SearchManager.ts | 6 +- src/common/QueryParams.ts | 1 - src/common/entities/SearchQueryDTO.ts | 45 +++- src/frontend/app/app.module.ts | 2 + src/frontend/app/app.routing.ts | 2 +- .../app/ui/faces/face/face.component.html | 2 +- .../app/ui/faces/face/face.component.ts | 11 +- .../app/ui/gallery/cache.gallery.service.ts | 21 +- .../app/ui/gallery/gallery.component.ts | 18 +- .../app/ui/gallery/gallery.service.ts | 98 ++------ .../photo/photo.grid.gallery.component.ts | 10 +- .../controls.lightbox.gallery.component.ts | 6 +- .../info-panel.lightbox.gallery.component.ts | 10 +- .../navigator/navigator.gallery.component.ts | 4 +- .../query-entry.search.gallery.component.css | 6 + .../query-entry.search.gallery.component.html | 216 ++++++++++++++++++ .../query-entry.search.gallery.component.ts | 161 +++++++++++++ .../search/search.gallery.component.html | 57 ++++- .../search/search.gallery.component.ts | 76 ++++-- test/backend/unit/model/sql/SearchManager.ts | 26 +-- 24 files changed, 603 insertions(+), 210 deletions(-) create mode 100644 src/frontend/app/ui/gallery/search/query-enrty/query-entry.search.gallery.component.css create mode 100644 src/frontend/app/ui/gallery/search/query-enrty/query-entry.search.gallery.component.html create mode 100644 src/frontend/app/ui/gallery/search/query-enrty/query-entry.search.gallery.component.ts diff --git a/benchmark/BenchmarkRunner.ts b/benchmark/BenchmarkRunner.ts index b476d6ba..cd7f9e29 100644 --- a/benchmark/BenchmarkRunner.ts +++ b/benchmark/BenchmarkRunner.ts @@ -4,7 +4,6 @@ import {DiskMangerWorker} from '../src/backend/model/threading/DiskMangerWorker' import {IndexingManager} from '../src/backend/model/database/sql/IndexingManager'; import * as path from 'path'; import * as fs from 'fs'; -import {SearchTypes} from '../src/common/entities/AutoCompleteItem'; import {Utils} from '../src/common/Utils'; import {DirectoryDTO} from '../src/common/entities/DirectoryDTO'; import {ServerConfig} from '../src/common/config/private/PrivateConfig'; @@ -21,6 +20,7 @@ import {GalleryRouter} from '../src/backend/routes/GalleryRouter'; import {Express} from 'express'; import {PersonRouter} from '../src/backend/routes/PersonRouter'; import {QueryParams} from '../src/common/QueryParams'; +import {SearchQueryTypes, TextSearch} from '../src/common/entities/SearchQueryDTO'; export interface BenchmarkResult { @@ -48,10 +48,6 @@ class BMGalleryRouter extends GalleryRouter { GalleryRouter.addSearch(app); } - public static addInstantSearch(app: Express) { - GalleryRouter.addInstantSearch(app); - } - public static addAutoComplete(app: Express) { GalleryRouter.addAutoComplete(app); } @@ -128,16 +124,15 @@ export class BenchmarkRunner { return await bm.run(this.RUNS); } - async bmAllSearch(text: string): Promise<{ result: BenchmarkResult, searchType: SearchTypes }[]> { + async bmAllSearch(text: string): Promise<{ result: BenchmarkResult, searchType: SearchQueryTypes }[]> { await this.setupDB(); - const types = Utils.enumToArray(SearchTypes).map(a => a.key).concat([null]); - const results: { result: BenchmarkResult, searchType: SearchTypes }[] = []; + const types = Utils.enumToArray(SearchQueryTypes).map(a => a.key).concat([null]); + const results: { result: BenchmarkResult, searchType: SearchQueryTypes }[] = []; for (let i = 0; i < types.length; i++) { const req = Utils.clone(this.requestTemplate); - req.params.text = text; - req.query[QueryParams.gallery.search.type] = types[i]; - const bm = new Benchmark('Searching for `' + text + '` as `' + (types[i] ? SearchTypes[types[i]] : 'any') + '`', req); + req.query[QueryParams.gallery.search.query] = {type: types[i], text: text}; + const bm = new Benchmark('Searching for `' + text + '` as `' + (types[i] ? SearchQueryTypes[types[i]] : 'any') + '`', req); BMGalleryRouter.addSearch(bm.BmExpressApp); results.push({result: await bm.run(this.RUNS), searchType: types[i]}); @@ -145,14 +140,6 @@ export class BenchmarkRunner { return results; } - async bmInstantSearch(text: string): Promise { - await this.setupDB(); - const req = Utils.clone(this.requestTemplate); - req.params.text = text; - const bm = new Benchmark('Instant search for `' + text + '`', req); - BMGalleryRouter.addInstantSearch(bm.BmExpressApp); - return await bm.run(this.RUNS); - } async bmAutocomplete(text: string): Promise { await this.setupDB(); diff --git a/benchmark/index.ts b/benchmark/index.ts index d500ab4a..8d959da2 100644 --- a/benchmark/index.ts +++ b/benchmark/index.ts @@ -71,7 +71,6 @@ const run = async () => { printResult(await bm.bmListDirectory()); printResult(await bm.bmListPersons()); (await bm.bmAllSearch('a')).forEach(res => printResult(res.result)); - printResult(await bm.bmInstantSearch('a')); printResult(await bm.bmAutocomplete('a')); printLine('*Measurements run ' + RUNS + ' times and an average was calculated.'); console.log(resultsText); diff --git a/src/backend/middlewares/GalleryMWs.ts b/src/backend/middlewares/GalleryMWs.ts index 46dd6c73..d29a8e29 100644 --- a/src/backend/middlewares/GalleryMWs.ts +++ b/src/backend/middlewares/GalleryMWs.ts @@ -162,7 +162,7 @@ export class GalleryMWs { return next(); } - const query: SearchQueryDTO = req.query[QueryParams.gallery.search.type]; + const query: SearchQueryDTO = req.query[QueryParams.gallery.search.query]; try { const result = await ObjectManagers.getInstance().SearchManager.search(query); @@ -203,7 +203,7 @@ export class GalleryMWs { return next(); } try { - const query: SearchQueryDTO = req.query[QueryParams.gallery.search.type]; + const query: SearchQueryDTO = req.query[QueryParams.gallery.search.query]; const photo = await ObjectManagers.getInstance() .SearchManager.getRandomPhoto(query); diff --git a/src/backend/model/database/memory/SearchManager.ts b/src/backend/model/database/memory/SearchManager.ts index efbed5b3..b8d27588 100644 --- a/src/backend/model/database/memory/SearchManager.ts +++ b/src/backend/model/database/memory/SearchManager.ts @@ -2,8 +2,13 @@ import {AutoCompleteItem} from '../../../../common/entities/AutoCompleteItem'; import {ISearchManager} from '../interfaces/ISearchManager'; import {SearchResultDTO} from '../../../../common/entities/SearchResultDTO'; import {SearchQueryDTO, SearchQueryTypes} from '../../../../common/entities/SearchQueryDTO'; +import {PhotoDTO} from '../../../../common/entities/PhotoDTO'; export class SearchManager implements ISearchManager { + getRandomPhoto(queryFilter: SearchQueryDTO): Promise { + throw new Error('Method not implemented.'); + } + autocomplete(text: string, type: SearchQueryTypes): Promise { throw new Error('Method not implemented.'); } diff --git a/src/backend/model/database/sql/SearchManager.ts b/src/backend/model/database/sql/SearchManager.ts index d7094b05..fff48388 100644 --- a/src/backend/model/database/sql/SearchManager.ts +++ b/src/backend/model/database/sql/SearchManager.ts @@ -22,7 +22,7 @@ import { SearchQueryTypes, SomeOfSearchQuery, TextSearch, - TextSearchQueryTypes + TextSearchQueryMatchTypes } from '../../../../common/entities/SearchQueryDTO'; import {GalleryManager} from './GalleryManager'; import {ObjectManagers} from '../../ObjectManagers'; @@ -359,7 +359,7 @@ export class SearchManager implements ISearchManager { return new Brackets((q: WhereExpression) => { const createMatchString = (str: string) => { - return (query).matchType === TextSearchQueryTypes.exact_match ? str : `%${str}%`; + return (query).matchType === TextSearchQueryMatchTypes.exact_match ? str : `%${str}%`; }; const LIKE = (query).negate ? 'NOT LIKE' : 'LIKE'; @@ -416,7 +416,7 @@ export class SearchManager implements ISearchManager { // Matching for array type fields const matchArrayField = (fieldName: string) => { q[whereFN](new Brackets(qbr => { - if ((query).matchType !== TextSearchQueryTypes.exact_match) { + if ((query).matchType !== TextSearchQueryMatchTypes.exact_match) { qbr[whereFN](`${fieldName} ${LIKE} :text${paramCounter.value} COLLATE utf8_general_ci`, textParam); } else { diff --git a/src/common/QueryParams.ts b/src/common/QueryParams.ts index ba7c52e4..d1764e21 100644 --- a/src/common/QueryParams.ts +++ b/src/common/QueryParams.ts @@ -16,7 +16,6 @@ export const QueryParams = { photo: 'p', sharingKey_query: 'sk', sharingKey_params: 'sharingKey', - searchText: 'searchText', directory: 'directory', knownLastModified: 'klm', knownLastScanned: 'kls' diff --git a/src/common/entities/SearchQueryDTO.ts b/src/common/entities/SearchQueryDTO.ts index 7121999d..dc0d4203 100644 --- a/src/common/entities/SearchQueryDTO.ts +++ b/src/common/entities/SearchQueryDTO.ts @@ -12,15 +12,48 @@ export enum SearchQueryTypes { // TEXT search types any_text = 100, - person, - keyword, - position, caption, - file_name, directory, + file_name, + keyword, + person, + position, } -export enum TextSearchQueryTypes { +export const ListSearchQueryTypes = [ + SearchQueryTypes.AND, + SearchQueryTypes.OR, + SearchQueryTypes.SOME_OF, +]; +export const TextSearchQueryTypes = [ + SearchQueryTypes.any_text, + SearchQueryTypes.caption, + SearchQueryTypes.directory, + SearchQueryTypes.file_name, + SearchQueryTypes.keyword, + SearchQueryTypes.person, + SearchQueryTypes.position, +]; + +export const MetadataSearchQueryTypes = [ + // non-text metadata + SearchQueryTypes.date, + SearchQueryTypes.rating, + SearchQueryTypes.distance, + SearchQueryTypes.resolution, + SearchQueryTypes.orientation, + + // TEXT search types + SearchQueryTypes.any_text, + SearchQueryTypes.caption, + SearchQueryTypes.directory, + SearchQueryTypes.file_name, + SearchQueryTypes.keyword, + SearchQueryTypes.person, + SearchQueryTypes.position, +]; + +export enum TextSearchQueryMatchTypes { exact_match = 1, like = 2 } @@ -104,7 +137,7 @@ export interface TextSearch extends NegatableSearchQuery { SearchQueryTypes.caption | SearchQueryTypes.file_name | SearchQueryTypes.directory; - matchType: TextSearchQueryTypes; + matchType: TextSearchQueryMatchTypes; text: string; } diff --git a/src/frontend/app/app.module.ts b/src/frontend/app/app.module.ts index 5685d09b..09ac3a51 100644 --- a/src/frontend/app/app.module.ts +++ b/src/frontend/app/app.module.ts @@ -93,6 +93,7 @@ import {JobButtonComponent} from './ui/settings/jobs/button/job-button.settings. import {ErrorInterceptor} from './model/network/helper/error.interceptor'; import {CSRFInterceptor} from './model/network/helper/csrf.interceptor'; import {SettingsEntryComponent} from './ui/settings/_abstract/settings-entry/settings-entry.component'; +import {GallerySearchQueryEntryComponent} from './ui/gallery/search/query-enrty/query-entry.search.gallery.component'; @Injectable() @@ -174,6 +175,7 @@ export function translationsFactory(locale: string) { GalleryMapLightboxComponent, FrameComponent, GallerySearchComponent, + GallerySearchQueryEntryComponent, GalleryShareComponent, GalleryNavigatorComponent, GalleryPhotoComponent, diff --git a/src/frontend/app/app.routing.ts b/src/frontend/app/app.routing.ts index a11c0b7a..5bd6ecfe 100644 --- a/src/frontend/app/app.routing.ts +++ b/src/frontend/app/app.routing.ts @@ -27,7 +27,7 @@ export function galleryMatcherFunction( } if (path === 'search') { if (segments.length > 1) { - posParams[QueryParams.gallery.searchText] = segments[1]; + posParams[QueryParams.gallery.search.query] = segments[1]; } return {consumed: segments.slice(0, Math.min(segments.length, 2)), posParams}; } diff --git a/src/frontend/app/ui/faces/face/face.component.html b/src/frontend/app/ui/faces/face/face.component.html index 3d2a134f..fa2b011d 100644 --- a/src/frontend/app/ui/faces/face/face.component.html +++ b/src/frontend/app/ui/faces/face/face.component.html @@ -1,4 +1,4 @@ - diff --git a/src/frontend/app/ui/faces/face/face.component.ts b/src/frontend/app/ui/faces/face/face.component.ts index 7e642759..26ffd374 100644 --- a/src/frontend/app/ui/faces/face/face.component.ts +++ b/src/frontend/app/ui/faces/face/face.component.ts @@ -1,12 +1,13 @@ import {Component, Input, OnDestroy, OnInit} from '@angular/core'; import {RouterLink} from '@angular/router'; import {PersonDTO} from '../../../../../common/entities/PersonDTO'; -import {SearchTypes} from '../../../../../common/entities/AutoCompleteItem'; import {DomSanitizer} from '@angular/platform-browser'; import {PersonThumbnail, ThumbnailManagerService} from '../../gallery/thumbnailManager.service'; import {FacesService} from '../faces.service'; import {AuthenticationService} from '../../../model/network/authentication.service'; import {Config} from '../../../../../common/config/public/Config'; +import {SearchQueryTypes, TextSearch, TextSearchQueryMatchTypes} from '../../../../../common/entities/SearchQueryDTO'; +import {QueryParams} from '../../../../../common/QueryParams'; @Component({ selector: 'app-face', @@ -19,7 +20,7 @@ export class FaceComponent implements OnInit, OnDestroy { @Input() size: number; thumbnail: PersonThumbnail = null; - SearchTypes = SearchTypes; + public searchQuery: any; constructor(private thumbnailService: ThumbnailManagerService, private _sanitizer: DomSanitizer, @@ -34,6 +35,12 @@ export class FaceComponent implements OnInit, OnDestroy { ngOnInit() { this.thumbnail = this.thumbnailService.getPersonThumbnail(this.person); + this.searchQuery = {}; + this.searchQuery[QueryParams.gallery.search.query] = { + type: SearchQueryTypes.person, + text: this.person.name, + matchType: TextSearchQueryMatchTypes.exact_match + }; } diff --git a/src/frontend/app/ui/gallery/cache.gallery.service.ts b/src/frontend/app/ui/gallery/cache.gallery.service.ts index 3d7acb06..1d80228f 100644 --- a/src/frontend/app/ui/gallery/cache.gallery.service.ts +++ b/src/frontend/app/ui/gallery/cache.gallery.service.ts @@ -2,11 +2,12 @@ import {Injectable} from '@angular/core'; import {DirectoryDTO} from '../../../../common/entities/DirectoryDTO'; import {Utils} from '../../../../common/Utils'; import {Config} from '../../../../common/config/public/Config'; -import {AutoCompleteItem, SearchTypes} from '../../../../common/entities/AutoCompleteItem'; +import {AutoCompleteItem} from '../../../../common/entities/AutoCompleteItem'; import {SearchResultDTO} from '../../../../common/entities/SearchResultDTO'; import {MediaDTO} from '../../../../common/entities/MediaDTO'; import {SortingMethods} from '../../../../common/entities/SortingMethods'; import {VersionService} from '../../model/version.service'; +import {SearchQueryDTO} from '../../../../common/entities/SearchQueryDTO'; interface CacheItem { timestamp: number; @@ -21,7 +22,6 @@ export class GalleryCacheService { private static readonly INSTANT_SEARCH_PREFIX = 'instant_search:'; private static readonly SEARCH_PREFIX = 'search:'; private static readonly SORTING_PREFIX = 'sorting:'; - private static readonly SEARCH_TYPE_PREFIX = ':type:'; private static readonly VERSION = 'version'; constructor(private versionService: VersionService) { @@ -40,7 +40,7 @@ export class GalleryCacheService { const tmp = localStorage.getItem(key); if (tmp != null) { const value: CacheItem = JSON.parse(tmp); - if (value.timestamp < Date.now() - Config.Client.Search.instantSearchCacheTimeout) { + if (value.timestamp < Date.now() - Config.Client.Search.searchCacheTimeout) { localStorage.removeItem(key); return null; } @@ -158,19 +158,15 @@ export class GalleryCacheService { } } - public getSearch(text: string, type?: SearchTypes): SearchResultDTO { + public getSearch(query: SearchQueryDTO): SearchResultDTO { if (Config.Client.Other.enableCache === false) { return null; } - let key = GalleryCacheService.SEARCH_PREFIX + text; - if (typeof type !== 'undefined' && type !== null) { - key += GalleryCacheService.SEARCH_TYPE_PREFIX + type; - } - + const key = GalleryCacheService.SEARCH_PREFIX + JSON.stringify(query); return GalleryCacheService.loadCacheItem(key); } - public setSearch(text: string, type: SearchTypes, searchResult: SearchResultDTO): void { + public setSearch(query: SearchQueryDTO, searchResult: SearchResultDTO): void { if (Config.Client.Other.enableCache === false) { return; } @@ -178,10 +174,7 @@ export class GalleryCacheService { timestamp: Date.now(), item: searchResult }; - let key = GalleryCacheService.SEARCH_PREFIX + text; - if (typeof type !== 'undefined' && type !== null) { - key += GalleryCacheService.SEARCH_TYPE_PREFIX + type; - } + const key = GalleryCacheService.SEARCH_PREFIX + JSON.stringify(query); try { localStorage.setItem(key, JSON.stringify(tmp)); } catch (e) { diff --git a/src/frontend/app/ui/gallery/gallery.component.ts b/src/frontend/app/ui/gallery/gallery.component.ts index 03321946..f3b8ba29 100644 --- a/src/frontend/app/ui/gallery/gallery.component.ts +++ b/src/frontend/app/ui/gallery/gallery.component.ts @@ -3,7 +3,6 @@ import {AuthenticationService} from '../../model/network/authentication.service' import {ActivatedRoute, Params, Router} from '@angular/router'; import {GalleryService} from './gallery.service'; import {GalleryGridComponent} from './grid/grid.gallery.component'; -import {SearchTypes} from '../../../../common/entities/AutoCompleteItem'; import {Config} from '../../../../common/config/public/Config'; import {DirectoryDTO} from '../../../../common/entities/DirectoryDTO'; import {SearchResultDTO} from '../../../../common/entities/SearchResultDTO'; @@ -18,7 +17,7 @@ import {PhotoDTO} from '../../../../common/entities/PhotoDTO'; import {QueryParams} from '../../../../common/QueryParams'; import {SeededRandomService} from '../../model/seededRandom.service'; import {take} from 'rxjs/operators'; -import {FileDTO} from '../../../../common/entities/FileDTO'; +import {SearchQueryTypes} from '../../../../common/entities/SearchQueryDTO'; @Component({ selector: 'app-gallery', @@ -37,7 +36,7 @@ export class GalleryComponent implements OnInit, OnDestroy { public isPhotoWithLocation = false; public countDown: { day: number, hour: number, minute: number, second: number } = null; public readonly mapEnabled: boolean; - readonly SearchTypes: typeof SearchTypes; + readonly SearchTypes: typeof SearchQueryTypes; private $counter: Observable; private subscription: { [key: string]: Subscription } = { content: null, @@ -54,7 +53,7 @@ export class GalleryComponent implements OnInit, OnDestroy { private _navigation: NavigationService, private rndService: SeededRandomService) { this.mapEnabled = Config.Client.Map.enabled; - this.SearchTypes = SearchTypes; + this.SearchTypes = SearchQueryTypes; PageHelper.showScrollY(); } @@ -115,15 +114,10 @@ export class GalleryComponent implements OnInit, OnDestroy { } private onRoute = async (params: Params) => { - const searchText = params[QueryParams.gallery.searchText]; - if (searchText && searchText !== '') { - const typeString: string = params[QueryParams.gallery.search.type]; - let type: SearchTypes = null; - if (typeString && typeString !== '') { - type = SearchTypes[typeString]; - } + const searchQuery = params[QueryParams.gallery.search.query]; + if (searchQuery) { - this._galleryService.search(searchText, type).catch(console.error); + this._galleryService.search(searchQuery).catch(console.error); return; } diff --git a/src/frontend/app/ui/gallery/gallery.service.ts b/src/frontend/app/ui/gallery/gallery.service.ts index b73197b9..55c51949 100644 --- a/src/frontend/app/ui/gallery/gallery.service.ts +++ b/src/frontend/app/ui/gallery/gallery.service.ts @@ -2,7 +2,6 @@ import {Injectable} from '@angular/core'; import {NetworkService} from '../../model/network/network.service'; import {ContentWrapper} from '../../../../common/entities/ConentWrapper'; import {DirectoryDTO} from '../../../../common/entities/DirectoryDTO'; -import {SearchTypes} from '../../../../common/entities/AutoCompleteItem'; import {GalleryCacheService} from './cache.gallery.service'; import {BehaviorSubject} from 'rxjs'; import {Config} from '../../../../common/config/public/Config'; @@ -11,6 +10,7 @@ import {NavigationService} from '../../model/navigation.service'; import {SortingMethods} from '../../../../common/entities/SortingMethods'; import {QueryParams} from '../../../../common/QueryParams'; import {PG2ConfMap} from '../../../../common/PG2ConfMap'; +import {SearchQueryDTO} from '../../../../common/entities/SearchQueryDTO'; @Injectable() @@ -23,15 +23,7 @@ export class GalleryService { }; private lastDirectory: DirectoryDTO; private searchId: any; - private ongoingSearch: { - text: string, - type: SearchTypes - } = null; - private ongoingInstantSearch: { - text: string, - type: SearchTypes - } = null; - private runInstantSearchFor: string; + private ongoingSearch: SearchQueryDTO = null; constructor(private networkService: NetworkService, private galleryCacheService: GalleryCacheService, @@ -124,99 +116,35 @@ export class GalleryService { } } - public async search(text: string, type?: SearchTypes): Promise { + public async search(query: SearchQueryDTO): Promise { if (this.searchId != null) { clearTimeout(this.searchId); } - if (text === null || text === '' || text.trim() === '.') { - return null; - } - this.ongoingSearch = {text: text, type: type}; + this.ongoingSearch = query; this.setContent(new ContentWrapper()); const cw = new ContentWrapper(); - cw.searchResult = this.galleryCacheService.getSearch(text, type); + cw.searchResult = this.galleryCacheService.getSearch(query); if (cw.searchResult == null) { - if (this.runInstantSearchFor === text && !type) { - await this.instantSearch(text, type); - return; - } const params: { [key: string]: any } = {}; - if (typeof type !== 'undefined' && type !== null) { - params[QueryParams.gallery.search.type] = type; - } - cw.searchResult = (await this.networkService.getJson('/search/' + text, params)).searchResult; - if (this.ongoingSearch && - (this.ongoingSearch.text !== text || this.ongoingSearch.type !== type)) { - return; - } - this.galleryCacheService.setSearch(text, type, cw.searchResult); + params[QueryParams.gallery.search.query] = query; + cw.searchResult = (await this.networkService.getJson('/search', params)).searchResult; + this.galleryCacheService.setSearch(query, cw.searchResult); } + + if (this.ongoingSearch !== query) { + return; + } + this.setContent(cw); } - public async instantSearch(text: string, type?: SearchTypes): Promise { - if (text === null || text === '' || text.trim() === '.') { - const content = new ContentWrapper(this.lastDirectory); - this.setContent(content); - if (this.searchId != null) { - clearTimeout(this.searchId); - } - if (!this.lastDirectory) { - this.loadDirectory('/').catch(console.error); - } - return null; - } - - if (this.searchId != null) { - clearTimeout(this.searchId); - } - this.runInstantSearchFor = null; - this.ongoingInstantSearch = {text: text, type: type}; - - - const cw = new ContentWrapper(); - cw.directory = null; - cw.searchResult = this.galleryCacheService.getSearch(text); - if (cw.searchResult == null) { - // If result is not search cache, try to load more - this.searchId = setTimeout(() => { - this.search(text, type).catch(console.error); - this.searchId = null; - }, Config.Client.Search.InstantSearchTimeout); - - cw.searchResult = this.galleryCacheService.getInstantSearch(text); - - if (cw.searchResult == null) { - cw.searchResult = (await this.networkService.getJson('/instant-search/' + text)).searchResult; - if (this.ongoingInstantSearch && - (this.ongoingInstantSearch.text !== text || this.ongoingInstantSearch.type !== type)) { - return; - } - this.galleryCacheService.setInstantSearch(text, cw.searchResult); - } - } - this.setContent(cw); - - // if instant search do not have a result, do not do a search - if (cw.searchResult.media.length === 0 && cw.searchResult.directories.length === 0) { - if (this.searchId != null) { - clearTimeout(this.searchId); - } - } - return cw; - - } - isSearchResult(): boolean { return !!this.content.value.searchResult; } - runInstantSearch(searchText: string) { - this.runInstantSearchFor = searchText; - } } diff --git a/src/frontend/app/ui/gallery/grid/photo/photo.grid.gallery.component.ts b/src/frontend/app/ui/gallery/grid/photo/photo.grid.gallery.component.ts index 1806d26f..cc79e9d7 100644 --- a/src/frontend/app/ui/gallery/grid/photo/photo.grid.gallery.component.ts +++ b/src/frontend/app/ui/gallery/grid/photo/photo.grid.gallery.component.ts @@ -1,12 +1,12 @@ import {Component, ElementRef, Input, OnDestroy, OnInit, ViewChild} from '@angular/core'; import {Dimension, IRenderable} from '../../../../model/IRenderable'; import {GridMedia} from '../GridMedia'; -import {SearchTypes} from '../../../../../../common/entities/AutoCompleteItem'; import {RouterLink} from '@angular/router'; import {Thumbnail, ThumbnailManagerService} from '../../thumbnailManager.service'; import {Config} from '../../../../../../common/config/public/Config'; import {PageHelper} from '../../../../model/page.helper'; import {PhotoDTO, PhotoMetadata} from '../../../../../../common/entities/PhotoDTO'; +import {SearchQueryTypes} from '../../../../../../common/entities/SearchQueryDTO'; @Component({ selector: 'app-gallery-grid-photo', @@ -20,11 +20,11 @@ export class GalleryPhotoComponent implements IRenderable, OnInit, OnDestroy { @ViewChild('photoContainer', {static: true}) container: ElementRef; thumbnail: Thumbnail; - keywords: { value: string, type: SearchTypes }[] = null; + keywords: { value: string, type: SearchQueryTypes }[] = null; infoBarVisible = false; animationTimer: number = null; - readonly SearchTypes: typeof SearchTypes = SearchTypes; + readonly SearchQueryTypes: typeof SearchQueryTypes = SearchQueryTypes; searchEnabled = true; wasInView: boolean = null; @@ -60,9 +60,9 @@ export class GalleryPhotoComponent implements IRenderable, OnInit, OnDestroy { if (Config.Client.Faces.enabled) { const names: string[] = (metadata.faces || []).map(f => f.name); this.keywords = names.filter((name, index) => names.indexOf(name) === index) - .map(n => ({value: n, type: SearchTypes.person})); + .map(n => ({value: n, type: SearchQueryTypes.person})); } - this.keywords = this.keywords.concat((metadata.keywords || []).map(k => ({value: k, type: SearchTypes.keyword}))); + this.keywords = this.keywords.concat((metadata.keywords || []).map(k => ({value: k, type: SearchQueryTypes.keyword}))); } } diff --git a/src/frontend/app/ui/gallery/lightbox/controls/controls.lightbox.gallery.component.ts b/src/frontend/app/ui/gallery/lightbox/controls/controls.lightbox.gallery.component.ts index 08fb5c38..39859d71 100644 --- a/src/frontend/app/ui/gallery/lightbox/controls/controls.lightbox.gallery.component.ts +++ b/src/frontend/app/ui/gallery/lightbox/controls/controls.lightbox.gallery.component.ts @@ -6,8 +6,8 @@ import {Observable, Subscription, timer} from 'rxjs'; import {filter} from 'rxjs/operators'; import {PhotoDTO} from '../../../../../../common/entities/PhotoDTO'; import {GalleryLightboxMediaComponent} from '../media/media.lightbox.gallery.component'; -import {SearchTypes} from '../../../../../../common/entities/AutoCompleteItem'; import {Config} from '../../../../../../common/config/public/Config'; +import {SearchQueryTypes} from '../../../../../../common/entities/SearchQueryDTO'; export enum PlayBackStates { Paused = 1, @@ -46,13 +46,13 @@ export class ControlsLightboxComponent implements OnDestroy, OnInit, OnChanges { public controllersAlwaysOn = false; public controllersVisible = true; public drag = {x: 0, y: 0}; - public SearchTypes = SearchTypes; + public SearchQueryTypes = SearchQueryTypes; private visibilityTimer: number = null; private timer: Observable; private timerSub: Subscription; private prevDrag = {x: 0, y: 0}; private prevZoom = 1; - private faceContainerDim = {width: 0, height: 0}; + public faceContainerDim = {width: 0, height: 0}; constructor(public fullScreenService: FullScreenService) { } diff --git a/src/frontend/app/ui/gallery/lightbox/infopanel/info-panel.lightbox.gallery.component.ts b/src/frontend/app/ui/gallery/lightbox/infopanel/info-panel.lightbox.gallery.component.ts index 6923ffdb..60b243b6 100644 --- a/src/frontend/app/ui/gallery/lightbox/infopanel/info-panel.lightbox.gallery.component.ts +++ b/src/frontend/app/ui/gallery/lightbox/infopanel/info-panel.lightbox.gallery.component.ts @@ -6,7 +6,7 @@ import {VideoDTO, VideoMetadata} from '../../../../../../common/entities/VideoDT import {Utils} from '../../../../../../common/Utils'; import {QueryService} from '../../../../model/query.service'; import {MapService} from '../../map/map.service'; -import {SearchTypes} from '../../../../../../common/entities/AutoCompleteItem'; +import {SearchQueryTypes} from '../../../../../../common/entities/SearchQueryDTO'; @Component({ selector: 'app-info-panel', @@ -19,8 +19,8 @@ export class InfoPanelLightboxComponent implements OnInit { public readonly mapEnabled: boolean; public readonly searchEnabled: boolean; - keywords: { value: string, type: SearchTypes }[] = null; - readonly SearchTypes: typeof SearchTypes = SearchTypes; + keywords: { value: string, type: SearchQueryTypes }[] = null; + readonly SearchQueryTypes: typeof SearchQueryTypes = SearchQueryTypes; constructor(public queryService: QueryService, public mapService: MapService) { @@ -59,9 +59,9 @@ export class InfoPanelLightboxComponent implements OnInit { if (Config.Client.Faces.enabled) { const names: string[] = (metadata.faces || []).map(f => f.name); this.keywords = names.filter((name, index) => names.indexOf(name) === index) - .map(n => ({value: n, type: SearchTypes.person})); + .map(n => ({value: n, type: SearchQueryTypes.person})); } - this.keywords = this.keywords.concat((metadata.keywords || []).map(k => ({value: k, type: SearchTypes.keyword}))); + this.keywords = this.keywords.concat((metadata.keywords || []).map(k => ({value: k, type: SearchQueryTypes.keyword}))); } } diff --git a/src/frontend/app/ui/gallery/navigator/navigator.gallery.component.ts b/src/frontend/app/ui/gallery/navigator/navigator.gallery.component.ts index e0633e59..5b16d8a9 100644 --- a/src/frontend/app/ui/gallery/navigator/navigator.gallery.component.ts +++ b/src/frontend/app/ui/gallery/navigator/navigator.gallery.component.ts @@ -10,7 +10,7 @@ import {Utils} from '../../../../../common/Utils'; import {SortingMethods} from '../../../../../common/entities/SortingMethods'; import {Config} from '../../../../../common/config/public/Config'; import {SearchResultDTO} from '../../../../../common/entities/SearchResultDTO'; -import {SearchTypes} from '../../../../../common/entities/AutoCompleteItem'; +import {SearchQueryTypes} from '../../../../../common/entities/SearchQueryDTO'; @Component({ selector: 'app-gallery-navbar', @@ -27,7 +27,7 @@ export class GalleryNavigatorComponent implements OnChanges { sortingMethodsType: { key: number; value: string }[] = []; config = Config; DefaultSorting = Config.Client.Other.defaultPhotoSortingMethod; - readonly SearchTypes = SearchTypes; + readonly SearchQueryTypes = SearchQueryTypes; private readonly RootFolderName: string; constructor(private _authService: AuthenticationService, diff --git a/src/frontend/app/ui/gallery/search/query-enrty/query-entry.search.gallery.component.css b/src/frontend/app/ui/gallery/search/query-enrty/query-entry.search.gallery.component.css new file mode 100644 index 00000000..de5a3ff5 --- /dev/null +++ b/src/frontend/app/ui/gallery/search/query-enrty/query-entry.search.gallery.component.css @@ -0,0 +1,6 @@ +.query-list{ + padding-left: 25px; +} +label{ + margin-top: 0.3rem; +} diff --git a/src/frontend/app/ui/gallery/search/query-enrty/query-entry.search.gallery.component.html b/src/frontend/app/ui/gallery/search/query-enrty/query-entry.search.gallery.component.html new file mode 100644 index 00000000..e86db9cc --- /dev/null +++ b/src/frontend/app/ui/gallery/search/query-enrty/query-entry.search.gallery.component.html @@ -0,0 +1,216 @@ +
+ +
+ +
+ + + +
+
+ + +
+
+ +
+ + +
+ +
+ +
+ +
+
+ +
+ +
+
+ +
+ km +
+
+
+ + +
+
+
+
+ + +
+
+ + +
+
+
+
+ + +
+ +
+ + +
+
+
+ +
+ + +
+ Mpx +
+
+ +
+ + +
+ Mpx +
+
+
+
+
+ + +
+
+
+ + +
+
diff --git a/src/frontend/app/ui/gallery/search/query-enrty/query-entry.search.gallery.component.ts b/src/frontend/app/ui/gallery/search/query-enrty/query-entry.search.gallery.component.ts new file mode 100644 index 00000000..a3c004ab --- /dev/null +++ b/src/frontend/app/ui/gallery/search/query-enrty/query-entry.search.gallery.component.ts @@ -0,0 +1,161 @@ +import {Component, EventEmitter, forwardRef, OnChanges, Output} from '@angular/core'; +import { + DateSearch, + DistanceSearch, + ListSearchQueryTypes, + OrientationSearch, + RatingSearch, + ResolutionSearch, + SearchListQuery, + SearchQueryDTO, + SearchQueryTypes, + SomeOfSearchQuery, + TextSearch, + TextSearchQueryTypes +} from '../../../../../../common/entities/SearchQueryDTO'; +import {Utils} from '../../../../../../common/Utils'; +import {ControlValueAccessor, FormControl, NG_VALIDATORS, NG_VALUE_ACCESSOR, ValidationErrors, Validator} from '@angular/forms'; + + +@Component({ + selector: 'app-gallery-search-query-entry', + templateUrl: './query-entry.search.gallery.component.html', + styleUrls: ['./query-entry.search.gallery.component.css'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => GallerySearchQueryEntryComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => GallerySearchQueryEntryComponent), + multi: true + } + ] +}) +export class GallerySearchQueryEntryComponent implements ControlValueAccessor, Validator, OnChanges { + public queryEntry: SearchQueryDTO; + public SearchQueryTypesEnum: { value: string; key: SearchQueryTypes }[]; + public SearchQueryTypes = SearchQueryTypes; + @Output() delete = new EventEmitter(); + + constructor() { + this.SearchQueryTypesEnum = Utils.enumToArray(SearchQueryTypes); + + } + + get IsTextQuery(): boolean { + return this.queryEntry && TextSearchQueryTypes.includes(this.queryEntry.type); + } + + get IsListQuery(): boolean { + return this.queryEntry && ListSearchQueryTypes.includes(this.queryEntry.type); + } + + get AsListQuery(): SearchListQuery { + return this.queryEntry; + } + + get AsDateQuery(): DateSearch { + return this.queryEntry; + } + + get AsResolutionQuery(): ResolutionSearch { + return this.queryEntry; + } + + get AsOrientationQuery(): OrientationSearch { + return this.queryEntry; + } + + get AsDistanceQuery(): DistanceSearch { + return this.queryEntry; + } + + get AsRatingQuery(): RatingSearch { + return this.queryEntry; + } + + get AsSomeOfQuery(): SomeOfSearchQuery { + return this.queryEntry; + } + + get AsTextQuery(): TextSearch { + return this.queryEntry; + } + + validate(control: FormControl): ValidationErrors { + return {required: true}; + } + + addQuery(): void { + console.log('clicked', this.IsListQuery); + if (!this.IsListQuery) { + return; + } + this.AsListQuery.list.push({type: SearchQueryTypes.any_text, text: ''}); + } + + onChangeType($event: any) { + if (this.IsListQuery) { + delete this.AsTextQuery.text; + this.AsListQuery.list = this.AsListQuery.list || [ + {type: SearchQueryTypes.any_text, text: ''}, + {type: SearchQueryTypes.any_text, text: ''} + ]; + } else { + delete this.AsListQuery.list; + } + if (this.queryEntry.type === SearchQueryTypes.distance) { + this.AsDistanceQuery.from = {text: ''}; + this.AsDistanceQuery.distance = 1; + } else { + delete this.AsDistanceQuery.from; + delete this.AsDistanceQuery.distance; + } + + if (this.queryEntry.type === SearchQueryTypes.orientation) { + this.AsOrientationQuery.landscape = true; + } else { + delete this.AsOrientationQuery.landscape; + } + this.onChange(this.queryEntry); + } + + deleteItem() { + this.delete.emit(); + } + + itemDeleted(i: number) { + this.AsListQuery.list.splice(i, 1); + } + + ngOnChanges(): void { + // console.log('ngOnChanges', this.queryEntry); + + } + + public onChange(value: any): void { + // console.log('onChange', this.queryEntry); + } + + public onTouched(): void { + } + + public writeValue(obj: any): void { + this.queryEntry = obj; + // console.log('write value', this.queryEntry); + this.ngOnChanges(); + } + + public registerOnChange(fn: any): void { + // console.log('registerOnChange', fn); + this.onChange = fn; + } + + public registerOnTouched(fn: any): void { + this.onTouched = fn; + } +} + diff --git a/src/frontend/app/ui/gallery/search/search.gallery.component.html b/src/frontend/app/ui/gallery/search/search.gallery.component.html index df0bd6fc..8d5c0c3a 100644 --- a/src/frontend/app/ui/gallery/search/search.gallery.component.html +++ b/src/frontend/app/ui/gallery/search/search.gallery.component.html @@ -7,7 +7,7 @@ (keyup)="onSearchChange($event)" (blur)="onFocusLost()" (focus)="onFocus()" - [(ngModel)]="searchText" + [(ngModel)]="rawSearchText" #name="ngModel" size="30" ngControl="search" @@ -19,26 +19,63 @@
+ +
+ +
+ + + + + + diff --git a/src/frontend/app/ui/gallery/search/search.gallery.component.ts b/src/frontend/app/ui/gallery/search/search.gallery.component.ts index 6c6815e2..76c56e44 100644 --- a/src/frontend/app/ui/gallery/search/search.gallery.component.ts +++ b/src/frontend/app/ui/gallery/search/search.gallery.component.ts @@ -1,12 +1,15 @@ -import {Component, OnDestroy} from '@angular/core'; +import {Component, OnDestroy, TemplateRef} from '@angular/core'; import {AutoCompleteService} from './autocomplete.service'; -import {AutoCompleteItem, SearchTypes} from '../../../../../common/entities/AutoCompleteItem'; +import {AutoCompleteItem} from '../../../../../common/entities/AutoCompleteItem'; import {ActivatedRoute, Params, RouterLink} from '@angular/router'; import {GalleryService} from '../gallery.service'; import {Subscription} from 'rxjs'; import {Config} from '../../../../../common/config/public/Config'; import {NavigationService} from '../../../model/navigation.service'; import {QueryParams} from '../../../../../common/QueryParams'; +import {MetadataSearchQueryTypes, SearchQueryDTO, SearchQueryTypes, TextSearch} from '../../../../../common/entities/SearchQueryDTO'; +import {BsModalService} from 'ngx-bootstrap/modal'; +import {BsModalRef} from 'ngx-bootstrap/modal/bs-modal-ref.service'; @Component({ selector: 'app-gallery-search', @@ -17,31 +20,48 @@ import {QueryParams} from '../../../../../common/QueryParams'; export class GallerySearchComponent implements OnDestroy { autoCompleteItems: AutoCompleteRenderItem[] = []; - public searchText = ''; + public searchQueryDTO: SearchQueryDTO = {type: SearchQueryTypes.any_text, text: ''}; + mouseOverAutoComplete = false; + readonly SearchQueryTypes: typeof SearchQueryTypes; + modalRef: BsModalRef; + public readonly MetadataSearchQueryTypes: { value: string; key: SearchQueryTypes }[]; private cache = { lastAutocomplete: '', lastInstantSearch: '' }; - mouseOverAutoComplete = false; - - readonly SearchTypes: typeof SearchTypes; private readonly subscription: Subscription = null; constructor(private _autoCompleteService: AutoCompleteService, private _galleryService: GalleryService, private navigationService: NavigationService, - private _route: ActivatedRoute) { + private _route: ActivatedRoute, + private modalService: BsModalService) { - this.SearchTypes = SearchTypes; + this.SearchQueryTypes = SearchQueryTypes; + this.MetadataSearchQueryTypes = MetadataSearchQueryTypes.map(v => ({key: v, value: SearchQueryTypes[v]})); this.subscription = this._route.params.subscribe((params: Params) => { - const searchText = params[QueryParams.gallery.searchText]; - if (searchText && searchText !== '') { - this.searchText = searchText; + const searchQuery = params[QueryParams.gallery.search.query]; + if (searchQuery) { + this.searchQueryDTO = searchQuery; } }); } + public get rawSearchText() { + return JSON.stringify(this.searchQueryDTO); + } + + public set rawSearchText(val: any) { + + } + + get HTMLSearchQuery() { + const searchQuery: any = {}; + searchQuery[QueryParams.gallery.search.query] = this.searchQueryDTO; + return searchQuery; + } + ngOnDestroy() { if (this.subscription !== null) { @@ -57,19 +77,9 @@ export class GallerySearchComponent implements OnDestroy { if (Config.Client.Search.AutoComplete.enabled && this.cache.lastAutocomplete !== searchText) { this.cache.lastAutocomplete = searchText; - this.autocomplete(searchText).catch(console.error); + // this.autocomplete(searchText).catch(console.error); } - if (Config.Client.Search.instantSearchEnabled && - this.cache.lastInstantSearch !== searchText) { - this.cache.lastInstantSearch = searchText; - if (searchText === '') { - return this.navigationService.toGallery().catch(console.error); - } - this._galleryService.runInstantSearch(searchText); - this.navigationService.search(searchText).catch(console.error); - - } } @@ -84,9 +94,25 @@ export class GallerySearchComponent implements OnDestroy { } public onFocus() { - this.autocomplete(this.searchText).catch(console.error); + // TODO: implement autocomplete + // this.autocomplete(this.searchText).catch(console.error); } + public async openModal(template: TemplateRef) { + this.modalRef = this.modalService.show(template, {class: 'modal-lg'}); + document.body.style.paddingRight = '0px'; + } + + public hideModal() { + this.modalRef.hide(); + this.modalRef = null; + } + + resetQuery() { + this.searchQueryDTO = {text: '', type: SearchQueryTypes.any_text}; + } + + private emptyAutoComplete() { this.autoCompleteItems = []; } @@ -126,9 +152,9 @@ class AutoCompleteRenderItem { public preText = ''; public highLightText = ''; public postText = ''; - public type: SearchTypes; + public type: SearchQueryTypes; - constructor(public text: string, searchText: string, type: SearchTypes) { + constructor(public text: string, searchText: string, type: SearchQueryTypes) { const preIndex = text.toLowerCase().indexOf(searchText.toLowerCase()); if (preIndex > -1) { this.preText = text.substring(0, preIndex); diff --git a/test/backend/unit/model/sql/SearchManager.ts b/test/backend/unit/model/sql/SearchManager.ts index 9f959714..14bac8d8 100644 --- a/test/backend/unit/model/sql/SearchManager.ts +++ b/test/backend/unit/model/sql/SearchManager.ts @@ -15,7 +15,7 @@ import { SearchQueryTypes, SomeOfSearchQuery, TextSearch, - TextSearchQueryTypes + TextSearchQueryMatchTypes } from '../../../../../src/common/entities/SearchQueryDTO'; import {IndexingManager} from '../../../../../src/backend/model/database/sql/IndexingManager'; import {DirectoryDTO} from '../../../../../src/common/entities/DirectoryDTO'; @@ -482,7 +482,7 @@ describe('SearchManager', (sqlHelper: SQLTestHelper) => { query = { text: 'Boba', type: SearchQueryTypes.any_text, - matchType: TextSearchQueryTypes.exact_match + matchType: TextSearchQueryMatchTypes.exact_match }; expect(Utils.clone(await sm.search(query))) .to.deep.equalInAnyOrder(removeDir({ @@ -496,7 +496,7 @@ describe('SearchManager', (sqlHelper: SQLTestHelper) => { query = { text: 'Boba Fett', type: SearchQueryTypes.any_text, - matchType: TextSearchQueryTypes.exact_match + matchType: TextSearchQueryMatchTypes.exact_match }; expect(Utils.clone(await sm.search(query))) @@ -571,7 +571,7 @@ describe('SearchManager', (sqlHelper: SQLTestHelper) => { query = { text: 'star wars', - matchType: TextSearchQueryTypes.exact_match, + matchType: TextSearchQueryMatchTypes.exact_match, type: SearchQueryTypes.keyword }; @@ -585,7 +585,7 @@ describe('SearchManager', (sqlHelper: SQLTestHelper) => { query = { text: 'wookiees', - matchType: TextSearchQueryTypes.exact_match, + matchType: TextSearchQueryMatchTypes.exact_match, type: SearchQueryTypes.keyword }; @@ -684,14 +684,14 @@ describe('SearchManager', (sqlHelper: SQLTestHelper) => { query = { text: '/wars dir', - matchType: TextSearchQueryTypes.exact_match, + matchType: TextSearchQueryMatchTypes.exact_match, type: SearchQueryTypes.directory }; expect(Utils.clone(await sm.search({ text: '/wars dir', - matchType: TextSearchQueryTypes.exact_match, + matchType: TextSearchQueryMatchTypes.exact_match, type: SearchQueryTypes.directory }))).to.deep.equalInAnyOrder(removeDir({ searchQuery: query, @@ -704,7 +704,7 @@ describe('SearchManager', (sqlHelper: SQLTestHelper) => { query = { text: '/wars dir/Return of the Jedi', - matchType: TextSearchQueryTypes.exact_match, + matchType: TextSearchQueryMatchTypes.exact_match, type: SearchQueryTypes.directory }; @@ -718,7 +718,7 @@ describe('SearchManager', (sqlHelper: SQLTestHelper) => { query = { text: '/wars dir/Return of the Jedi', - matchType: TextSearchQueryTypes.exact_match, + matchType: TextSearchQueryMatchTypes.exact_match, type: SearchQueryTypes.directory }; @@ -752,7 +752,7 @@ describe('SearchManager', (sqlHelper: SQLTestHelper) => { query = { text: 'Boba', type: SearchQueryTypes.person, - matchType: TextSearchQueryTypes.exact_match + matchType: TextSearchQueryMatchTypes.exact_match }; expect(Utils.clone(await sm.search(query))).to.deep.equalInAnyOrder(removeDir({ @@ -766,13 +766,13 @@ describe('SearchManager', (sqlHelper: SQLTestHelper) => { query = { text: 'Boba Fett', type: SearchQueryTypes.person, - matchType: TextSearchQueryTypes.exact_match + matchType: TextSearchQueryMatchTypes.exact_match }; expect(Utils.clone(await sm.search({ text: 'Boba Fett', type: SearchQueryTypes.person, - matchType: TextSearchQueryTypes.exact_match + matchType: TextSearchQueryMatchTypes.exact_match }))).to.deep.equalInAnyOrder(removeDir({ searchQuery: query, directories: [], @@ -1098,7 +1098,7 @@ describe('SearchManager', (sqlHelper: SQLTestHelper) => { query = { text: 'wookiees', - matchType: TextSearchQueryTypes.exact_match, + matchType: TextSearchQueryMatchTypes.exact_match, type: SearchQueryTypes.keyword }; expect(Utils.clone(await sm.getRandomPhoto(query))).to.deep.equalInAnyOrder(searchifyMedia(p_faceLess));