1
0
mirror of https://github.com/bpatrik/pigallery2.git synced 2024-12-10 11:10:35 +02:00

Implementing search query parsing and stringifying. #58

This commit is contained in:
Patrik J. Braun 2021-02-14 12:57:05 +01:00
parent 33b6fbf624
commit 47703b6b84
9 changed files with 651 additions and 272 deletions

View File

@ -11,18 +11,21 @@ import {Brackets, SelectQueryBuilder, WhereExpression} from 'typeorm';
import {Config} from '../../../../common/config/private/Config';
import {
ANDSearchQuery,
DateSearch,
DistanceSearch,
FromDateSearch,
MaxRatingSearch,
MaxResolutionSearch,
MinRatingSearch,
MinResolutionSearch,
OrientationSearch,
ORSearchQuery,
RatingSearch,
ResolutionSearch,
SearchListQuery,
SearchQueryDTO,
SearchQueryTypes,
SomeOfSearchQuery,
TextSearch,
TextSearchQueryMatchTypes
TextSearchQueryMatchTypes,
ToDateSearch
} from '../../../../common/entities/SearchQueryDTO';
import {GalleryManager} from './GalleryManager';
import {ObjectManagers} from '../../ObjectManagers';
@ -266,75 +269,100 @@ export class SearchManager implements ISearchManager {
return q;
});
case SearchQueryTypes.date:
case SearchQueryTypes.from_date:
return new Brackets(q => {
if (typeof (<DateSearch>query).before === 'undefined' && typeof (<DateSearch>query).after === 'undefined') {
throw new Error('Invalid search query: Date Query should contain before or after value');
if (typeof (<FromDateSearch>query).value === 'undefined') {
throw new Error('Invalid search query: Date Query should contain from value');
}
const whereFN = (<TextSearch>query).negate ? 'orWhere' : 'andWhere';
const relation = (<TextSearch>query).negate ? '<' : '>=';
const relationRev = (<TextSearch>query).negate ? '>' : '<=';
if (typeof (<DateSearch>query).after !== 'undefined') {
const textParam: any = {};
textParam['after' + paramCounter.value] = (<DateSearch>query).after;
q.where(`media.metadata.creationDate ${relation} :after${paramCounter.value}`, textParam);
}
const textParam: any = {};
textParam['from' + paramCounter.value] = (<FromDateSearch>query).value;
q.where(`media.metadata.creationDate ${relation} :from${paramCounter.value}`, textParam);
if (typeof (<DateSearch>query).before !== 'undefined') {
const textParam: any = {};
textParam['before' + paramCounter.value] = (<DateSearch>query).before;
q[whereFN](`media.metadata.creationDate ${relationRev} :before${paramCounter.value}`, textParam);
}
paramCounter.value++;
return q;
});
case SearchQueryTypes.rating:
case SearchQueryTypes.to_date:
return new Brackets(q => {
if (typeof (<RatingSearch>query).min === 'undefined' && typeof (<RatingSearch>query).max === 'undefined') {
throw new Error('Invalid search query: Rating Query should contain min or max value');
if (typeof (<ToDateSearch>query).value === 'undefined') {
throw new Error('Invalid search query: Date Query should contain to value');
}
const relation = (<TextSearch>query).negate ? '>' : '<=';
const textParam: any = {};
textParam['to' + paramCounter.value] = (<ToDateSearch>query).value;
q.where(`media.metadata.creationDate ${relation} :to${paramCounter.value}`, textParam);
paramCounter.value++;
return q;
});
case SearchQueryTypes.min_rating:
return new Brackets(q => {
if (typeof (<MinRatingSearch>query).value === 'undefined') {
throw new Error('Invalid search query: Rating Query should contain minvalue');
}
const whereFN = (<TextSearch>query).negate ? 'orWhere' : 'andWhere';
const relation = (<TextSearch>query).negate ? '<' : '>=';
const relationRev = (<TextSearch>query).negate ? '>' : '<=';
if (typeof (<RatingSearch>query).min !== 'undefined') {
const textParam: any = {};
textParam['min' + paramCounter.value] = (<RatingSearch>query).min;
q.where(`media.metadata.rating ${relation} :min${paramCounter.value}`, textParam);
const textParam: any = {};
textParam['min' + paramCounter.value] = (<MinRatingSearch>query).value;
q.where(`media.metadata.rating ${relation} :min${paramCounter.value}`, textParam);
paramCounter.value++;
return q;
});
case SearchQueryTypes.max_rating:
return new Brackets(q => {
if (typeof (<MaxRatingSearch>query).value === 'undefined') {
throw new Error('Invalid search query: Rating Query should contain max value');
}
if (typeof (<RatingSearch>query).max !== 'undefined') {
const relation = (<TextSearch>query).negate ? '>' : '<=';
if (typeof (<MaxRatingSearch>query).value !== 'undefined') {
const textParam: any = {};
textParam['max' + paramCounter.value] = (<RatingSearch>query).max;
q[whereFN](`media.metadata.rating ${relationRev} :max${paramCounter.value}`, textParam);
textParam['max' + paramCounter.value] = (<MaxRatingSearch>query).value;
q.where(`media.metadata.rating ${relation} :max${paramCounter.value}`, textParam);
}
paramCounter.value++;
return q;
});
case SearchQueryTypes.resolution:
case SearchQueryTypes.min_resolution:
return new Brackets(q => {
if (typeof (<ResolutionSearch>query).min === 'undefined' && typeof (<ResolutionSearch>query).max === 'undefined') {
if (typeof (<MinResolutionSearch>query).value === 'undefined') {
throw new Error('Invalid search query: Resolution Query should contain min value');
}
const relation = (<TextSearch>query).negate ? '<' : '>=';
const textParam: any = {};
textParam['min' + paramCounter.value] = (<MinResolutionSearch>query).value * 1000 * 1000;
q.where(`media.metadata.size.width * media.metadata.size.height ${relation} :min${paramCounter.value}`, textParam);
paramCounter.value++;
return q;
});
case SearchQueryTypes.max_resolution:
return new Brackets(q => {
if (typeof (<MaxResolutionSearch>query).value === 'undefined') {
throw new Error('Invalid search query: Rating Query should contain min or max value');
}
const whereFN = (<TextSearch>query).negate ? 'orWhere' : 'andWhere';
const relation = (<TextSearch>query).negate ? '<' : '>=';
const relationRev = (<TextSearch>query).negate ? '>' : '<=';
if (typeof (<ResolutionSearch>query).min !== 'undefined') {
const textParam: any = {};
textParam['min' + paramCounter.value] = (<RatingSearch>query).min * 1000 * 1000;
q.where(`media.metadata.size.width * media.metadata.size.height ${relation} :min${paramCounter.value}`, textParam);
}
const relation = (<TextSearch>query).negate ? '>' : '<=';
const textParam: any = {};
textParam['max' + paramCounter.value] = (<MaxResolutionSearch>query).value * 1000 * 1000;
q.where(`media.metadata.size.width * media.metadata.size.height ${relation} :max${paramCounter.value}`, textParam);
if (typeof (<ResolutionSearch>query).max !== 'undefined') {
const textParam: any = {};
textParam['max' + paramCounter.value] = (<RatingSearch>query).max * 1000 * 1000;
q[whereFN](`media.metadata.size.width * media.metadata.size.height ${relationRev} :max${paramCounter.value}`, textParam);
}
paramCounter.value++;
return q;
});

View File

@ -54,6 +54,11 @@ export class Utils {
return ret.substr(ret.length - length);
}
/**
* Checks if the two input (let them be objects or arrays or just primitives) are equal
* @param object
* @param filter
*/
static equalsFilter(object: any, filter: any): boolean {
if (typeof filter !== 'object' || filter == null) {
return object === filter;

View File

@ -1,13 +1,19 @@
import {GPSMetadata} from './PhotoDTO';
import {Utils} from '../Utils';
export enum SearchQueryTypes {
AND = 1, OR, SOME_OF,
// non-text metadata
date = 10,
rating,
// |- range types
from_date = 10,
to_date,
min_rating,
max_rating,
min_resolution,
max_resolution,
distance,
resolution,
orientation,
// TEXT search types
@ -34,24 +40,33 @@ export const TextSearchQueryTypes = [
SearchQueryTypes.person,
SearchQueryTypes.position,
];
export const MinRangeSearchQueryTypes = [
SearchQueryTypes.from_date,
SearchQueryTypes.min_rating,
SearchQueryTypes.min_resolution,
];
export const MaxRangeSearchQueryTypes = [
SearchQueryTypes.to_date,
SearchQueryTypes.max_rating,
SearchQueryTypes.max_resolution
];
export const RangeSearchQueryTypes = MinRangeSearchQueryTypes.concat(MaxRangeSearchQueryTypes);
export const MetadataSearchQueryTypes = [
// non-text metadata
SearchQueryTypes.date,
SearchQueryTypes.rating,
SearchQueryTypes.distance,
SearchQueryTypes.resolution,
SearchQueryTypes.orientation,
SearchQueryTypes.orientation
].concat(RangeSearchQueryTypes)
.concat(TextSearchQueryTypes);
// TEXT search types
SearchQueryTypes.any_text,
SearchQueryTypes.caption,
SearchQueryTypes.directory,
SearchQueryTypes.file_name,
SearchQueryTypes.keyword,
SearchQueryTypes.person,
SearchQueryTypes.position,
];
export const rangedTypePairs: any = {};
rangedTypePairs[SearchQueryTypes.from_date] = SearchQueryTypes.to_date;
rangedTypePairs[SearchQueryTypes.min_rating] = SearchQueryTypes.max_rating;
rangedTypePairs[SearchQueryTypes.min_resolution] = SearchQueryTypes.max_resolution;
// add the other direction too
for (const key of Object.keys(rangedTypePairs)) {
rangedTypePairs[rangedTypePairs[key]] = key;
}
export enum TextSearchQueryMatchTypes {
exact_match = 1, like = 2
@ -59,6 +74,12 @@ export enum TextSearchQueryMatchTypes {
export namespace SearchQueryDTO {
export const getRangedQueryPair = (type: SearchQueryTypes): SearchQueryTypes => {
if (rangedTypePairs[type]) {
return rangedTypePairs[type];
}
throw new Error('Unknown ranged type');
};
export const negate = (query: SearchQueryDTO): SearchQueryDTO => {
switch (query.type) {
case SearchQueryTypes.AND:
@ -74,9 +95,12 @@ export namespace SearchQueryDTO {
(<OrientationSearch>query).landscape = !(<OrientationSearch>query).landscape;
return query;
case SearchQueryTypes.date:
case SearchQueryTypes.rating:
case SearchQueryTypes.resolution:
case SearchQueryTypes.from_date:
case SearchQueryTypes.to_date:
case SearchQueryTypes.min_rating:
case SearchQueryTypes.max_rating:
case SearchQueryTypes.min_resolution:
case SearchQueryTypes.max_resolution:
case SearchQueryTypes.distance:
case SearchQueryTypes.any_text:
case SearchQueryTypes.person:
@ -95,60 +119,211 @@ export namespace SearchQueryDTO {
throw new Error('Unknown type' + query.type);
}
};
export const parse = (str: string): SearchQueryDTO => {
console.log(str);
str = str.replace(/\s\s+/g, ' ') // remove double spaces
.replace(/:\s+/g, ':').replace(/\)(?=\S)/g, ') ').trim();
if (str.charAt(0) === '(' && str.charAt(str.length - 1) === ')') {
str = str.slice(1, str.length - 1);
}
const fistNonBRSpace = () => {
const bracketIn = [];
for (let i = 0; i < str.length; ++i) {
if (str.charAt(i) === '(') {
bracketIn.push(i);
continue;
}
if (str.charAt(i) === ')') {
bracketIn.pop();
continue;
}
if (bracketIn.length === 0 && str.charAt(i) === ' ') {
return i;
}
}
return str.length - 1;
};
// tokenize
const tokenEnd = fistNonBRSpace();
if (tokenEnd !== str.length - 1) {
if (str.startsWith(' and', tokenEnd)) {
return <ANDSearchQuery>{
type: SearchQueryTypes.AND,
list: [SearchQueryDTO.parse(str.slice(0, tokenEnd)), // trim brackets
SearchQueryDTO.parse(str.slice(tokenEnd + 4))]
};
} else {
let padding = 0;
if (str.startsWith(' or', tokenEnd)) {
padding = 3;
}
return <ORSearchQuery>{
type: SearchQueryTypes.OR,
list: [SearchQueryDTO.parse(str.slice(0, tokenEnd)), // trim brackets
SearchQueryDTO.parse(str.slice(tokenEnd + padding))]
};
}
}
if (str.startsWith('some-of:') ||
new RegExp(/^\d*-of:/).test(str)) {
const prefix = str.startsWith('some-of:') ? 'some-of:' : new RegExp(/^\d*-of:/).exec(str)[0];
let tmpList: any = SearchQueryDTO.parse(str.slice(prefix.length + 1, -1)); // trim brackets
const unfoldList = (q: SearchListQuery): SearchQueryDTO[] => {
if (q.list) {
return [].concat.apply([], q.list.map(e => unfoldList(<any>e))); // flatten array
}
return [q];
};
tmpList = unfoldList(<SearchListQuery>tmpList);
const ret = <SomeOfSearchQuery>{
type: SearchQueryTypes.SOME_OF,
list: tmpList
};
if (new RegExp(/^\d*-of:/).test(str)) {
ret.min = parseInt(new RegExp(/^\d*/).exec(str)[0], 10);
}
return ret;
}
if (str.startsWith('from:')) {
return <FromDateSearch>{
type: SearchQueryTypes.from_date,
value: Date.parse(str.slice('from:'.length + 1, str.length - 1))
};
}
if (str.startsWith('to:')) {
return <ToDateSearch>{
type: SearchQueryTypes.to_date,
value: Date.parse(str.slice('to:'.length + 1, str.length - 1))
};
}
if (str.startsWith('min-rating:')) {
return <MinRatingSearch>{
type: SearchQueryTypes.min_rating,
value: parseInt(str.slice('min-rating:'.length), 10)
};
}
if (str.startsWith('max-rating:')) {
return <MaxRatingSearch>{
type: SearchQueryTypes.max_rating,
value: parseInt(str.slice('max-rating:'.length), 10)
};
}
if (str.startsWith('min-resolution:')) {
return <MinResolutionSearch>{
type: SearchQueryTypes.min_resolution,
value: parseInt(str.slice('min-resolution:'.length), 10)
};
}
if (str.startsWith('max-resolution:')) {
return <MaxResolutionSearch>{
type: SearchQueryTypes.max_resolution,
value: parseInt(str.slice('max-resolution:'.length), 10)
};
}
if (new RegExp(/^\d*-km-from:/).test(str)) {
let from = str.slice(new RegExp(/^\d*-km-from:/).exec(str)[0].length);
if (from.charAt(0) === '(' && from.charAt(from.length - 1) === ')') {
from = from.slice(1, from.length - 1);
}
return <DistanceSearch>{
type: SearchQueryTypes.distance,
distance: parseInt(new RegExp(/^\d*/).exec(str)[0], 10),
from: {text: from}
};
}
if (str.startsWith('orientation:')) {
return <OrientationSearch>{
type: SearchQueryTypes.orientation,
landscape: str.slice('orientation:'.length) === 'landscape'
};
}
// parse text search
const tmp = TextSearchQueryTypes.map(type => ({
key: SearchQueryTypes[type] + ':',
queryTemplate: <TextSearch>{type: type, text: ''}
}));
for (let i = 0; i < tmp.length; ++i) {
if (str.startsWith(tmp[i].key)) {
const ret: TextSearch = Utils.clone(tmp[i].queryTemplate);
if (str.charAt(tmp[i].key.length) === '"' && str.charAt(str.length - 1) === '"') {
ret.text = str.slice(tmp[i].key.length + 1, str.length - 1);
ret.matchType = TextSearchQueryMatchTypes.exact_match;
} else if (str.charAt(tmp[i].key.length) === '(' && str.charAt(str.length - 1) === ')') {
ret.text = str.slice(tmp[i].key.length + 1, str.length - 1);
} else {
ret.text = str.slice(tmp[i].key.length);
}
return ret;
}
}
return <TextSearch>{type: SearchQueryTypes.any_text, text: str};
};
export const stringify = (query: SearchQueryDTO): string => {
if (!query || !query.type) {
return '';
}
switch (query.type) {
case SearchQueryTypes.AND:
return '(' + (<SearchListQuery>query).list.map(q => SearchQueryDTO.stringify(q)).join(' AND ') + ')';
return '(' + (<SearchListQuery>query).list.map(q => SearchQueryDTO.stringify(q)).join(' and ') + ')';
case SearchQueryTypes.OR:
return '(' + (<SearchListQuery>query).list.map(q => SearchQueryDTO.stringify(q)).join(' OR ') + ')';
return '(' + (<SearchListQuery>query).list.map(q => SearchQueryDTO.stringify(q)).join(' or ') + ')';
case SearchQueryTypes.SOME_OF:
if ((<SomeOfSearchQuery>query).min) {
return (<SomeOfSearchQuery>query).min + ' OF: (' +
(<SearchListQuery>query).list.map(q => SearchQueryDTO.stringify(q)).join(', ') + ')';
return (<SomeOfSearchQuery>query).min + '-of:(' +
(<SearchListQuery>query).list.map(q => SearchQueryDTO.stringify(q)).join(' ') + ')';
}
return 'SOME OF: (' +
(<SearchListQuery>query).list.map(q => SearchQueryDTO.stringify(q)).join(', ') + ')';
return 'some-of:(' +
(<SearchListQuery>query).list.map(q => SearchQueryDTO.stringify(q)).join(' ') + ')';
case SearchQueryTypes.orientation:
return 'orientation: ' + (<OrientationSearch>query).landscape ? 'landscape' : 'portrait';
return 'orientation:' + ((<OrientationSearch>query).landscape ? 'landscape' : 'portrait');
case SearchQueryTypes.date:
let ret = '';
if ((<DateSearch>query).after) {
ret += 'from: ' + (<DateSearch>query).after;
case SearchQueryTypes.from_date:
if (!(<FromDateSearch>query).value) {
return '';
}
if ((<DateSearch>query).before) {
ret += ' to: ' + (<DateSearch>query).before;
return 'from:(' + new Date((<FromDateSearch>query).value).toLocaleDateString() + ')'.trim();
case SearchQueryTypes.to_date:
if (!(<ToDateSearch>query).value) {
return '';
}
return ret.trim();
case SearchQueryTypes.rating:
let rating = '';
if ((<RatingSearch>query).min) {
rating += 'min-rating: ' + (<RatingSearch>query).min;
}
if ((<RatingSearch>query).max) {
rating += ' max-rating: ' + (<RatingSearch>query).max;
}
return rating.trim();
case SearchQueryTypes.resolution:
let res = '';
if ((<ResolutionSearch>query).min) {
res += 'min-resolution: ' + (<ResolutionSearch>query).min;
}
if ((<RatingSearch>query).max) {
res += ' max-resolution: ' + (<ResolutionSearch>query).max;
}
return res.trim();
return 'to:(' + new Date((<ToDateSearch>query).value).toLocaleDateString() + ')'.trim();
case SearchQueryTypes.min_rating:
return 'min-rating:' + (isNaN((<RangeSearch>query).value) ? '' : (<RangeSearch>query).value);
case SearchQueryTypes.max_rating:
return 'max-rating:' + (isNaN((<RangeSearch>query).value) ? '' : (<RangeSearch>query).value);
case SearchQueryTypes.min_resolution:
return 'min-resolution:' + (isNaN((<RangeSearch>query).value) ? '' : (<RangeSearch>query).value);
case SearchQueryTypes.max_resolution:
return 'max-resolution:' + (isNaN((<RangeSearch>query).value) ? '' : (<RangeSearch>query).value);
case SearchQueryTypes.distance:
return (<DistanceSearch>query).distance + ' km from: ' + (<DistanceSearch>query).from.text;
if ((<DistanceSearch>query).from.text.indexOf(' ') !== -1) {
return (<DistanceSearch>query).distance + '-km-from:(' + (<DistanceSearch>query).from.text + ')';
}
return (<DistanceSearch>query).distance + '-km-from:' + (<DistanceSearch>query).from.text;
case SearchQueryTypes.any_text:
if ((<TextSearch>query).matchType === TextSearchQueryMatchTypes.exact_match) {
return '"' + (<TextSearch>query).text + '"';
} else if ((<TextSearch>query).text.indexOf(' ') !== -1) {
return '(' + (<TextSearch>query).text + ')';
}
return (<TextSearch>query).text;
case SearchQueryTypes.person:
@ -160,6 +335,12 @@ export namespace SearchQueryDTO {
if (!(<TextSearch>query).text) {
return '';
}
if ((<TextSearch>query).matchType === TextSearchQueryMatchTypes.exact_match) {
return SearchQueryTypes[query.type] + ':"' + (<TextSearch>query).text + '"';
} else if ((<TextSearch>query).text.indexOf(' ') !== -1) {
return SearchQueryTypes[query.type] + ':(' + (<TextSearch>query).text + ')';
}
return SearchQueryTypes[query.type] + ':' + (<TextSearch>query).text;
default:
@ -177,11 +358,6 @@ export interface NegatableSearchQuery extends SearchQueryDTO {
negate?: boolean; // if true negates the expression
}
export interface RangeSearchQuery extends SearchQueryDTO {
min?: number;
max?: number;
}
export interface SearchListQuery extends SearchQueryDTO {
list: SearchQueryDTO[];
}
@ -225,22 +401,42 @@ export interface DistanceSearch extends NegatableSearchQuery {
}
export interface DateSearch extends NegatableSearchQuery {
type: SearchQueryTypes.date;
after?: number;
before?: number;
export interface RangeSearch extends NegatableSearchQuery {
value: number;
}
export interface RatingSearch extends RangeSearchQuery, NegatableSearchQuery {
type: SearchQueryTypes.rating;
min?: number;
max?: number;
export interface RangeSearchGroup extends ANDSearchQuery {
list: RangeSearch[];
}
export interface ResolutionSearch extends RangeSearchQuery, NegatableSearchQuery {
type: SearchQueryTypes.resolution;
min?: number; // in megapixels
max?: number; // in megapixels
export interface FromDateSearch extends RangeSearch {
type: SearchQueryTypes.from_date;
value: number;
}
export interface ToDateSearch extends RangeSearch {
type: SearchQueryTypes.to_date;
value: number;
}
export interface MinRatingSearch extends RangeSearch {
type: SearchQueryTypes.min_rating;
value: number;
}
export interface MaxRatingSearch extends RangeSearch {
type: SearchQueryTypes.max_rating;
value: number;
}
export interface MinResolutionSearch extends RangeSearch {
type: SearchQueryTypes.min_resolution;
value: number; // in megapixels
}
export interface MaxResolutionSearch extends RangeSearch {
type: SearchQueryTypes.max_resolution;
value: number; // in megapixels
}
export interface OrientationSearch {

View File

@ -1,6 +1,6 @@
<div class="row mt-1 mb-1" *ngIf="queryEntry">
<ng-container *ngIf="IsListQuery">
<div class="input-group col-md-2">
<div class="input-group col-md-3">
<select
id="listSearchType"
name="listSearchType"
@ -24,20 +24,22 @@
name="someOfMinValue"
id="someOfMinValue"
required="required">
<div class="col-md-3"></div>
<div class="col-md-2"></div>
</ng-container>
<ng-container *ngIf="queryEntry.type != SearchQueryTypes.SOME_OF">
<div class="col-md-9"></div>
<div class="col-md-8"></div>
</ng-container>
<button [ngClass]="true? 'btn-danger':'btn-secondary'"
class="btn float-right col-md-1">
<button [ngClass]="'btn-danger'"
class="btn float-right col-md-1"
(click)="deleteItem()">
<span class="oi oi-trash" aria-hidden="true" aria-label="Delete"></span>
</button>
<div class="container query-list">
<app-gallery-search-query-entry *ngFor="let sq of AsListQuery.list; index as i"
[(ngModel)]="sq"
(delete)="itemDeleted(i)">
(delete)="itemDeleted(i)">
</app-gallery-search-query-entry>
</div>
<button class="btn btn-primary mx-auto" (click)="addQuery()">
@ -45,7 +47,7 @@
</button>
</ng-container>
<ng-container *ngIf="!IsListQuery">
<div class="input-group col-md-2">
<div class="input-group col-md-3">
<select
id="searchType"
name="searchType"
@ -56,18 +58,19 @@
</option>
</select>
</div>
<div class="input-group col-md-9" *ngIf="IsTextQuery">
<div class="input-group col-md-8" *ngIf="IsTextQuery">
<input
id="searchField"
name="searchField"
placeholder="link"
placeholder="Search text"
class="form-control input-md"
[(ngModel)]="AsTextQuery.text"
(change)="onChange(queryEntry)"
(ngModelChange)="onChange(queryEntry)"
type="text"/>
</div>
<ng-container [ngSwitch]="queryEntry.type">
<div *ngSwitchCase="SearchQueryTypes.distance" class="col-md-9 d-flex">
<div *ngSwitchCase="SearchQueryTypes.distance" class="col-md-8 d-flex">
<div class="input-group col-md-4">
<input type="number" class="form-control" placeholder="1"
id="distance"
@ -93,98 +96,94 @@
type="text">
</div>
</div>
<div *ngSwitchCase="SearchQueryTypes.date" class="col-md-9 d-flex">
<div class="input-group col-md-6">
<label class="col-md-4 control-label" for="maxResolution">From:</label>
<input id="afterDate"
name="afterDate"
title="After"
i18n-title
class="form-control input-md"
[(ngModel)]="AsDateQuery.after"
(ngModelChange)="onChange(queryEntry)"
type="date">
</div>
<div class="input-group col-md-6">
<label class="col-md-4 control-label" for="maxResolution">To:</label>
<input id="beforeDate"
name="beforeDate"
title="Before"
i18n-title
[(ngModel)]="AsDateQuery.before"
(ngModelChange)="onChange(queryEntry)"
class="form-control input-md"
type="date">
<!-- Range Search Query -->
<div *ngSwitchCase="SearchQueryTypes.from_date" class="col-md-8 input-group ">
<label class="col-md-4 control-label" for="from_date">From:</label>
<input id="from_date"
name="from_date"
title="From date"
i18n-title
[ngModel]="AsRangeQuery.value | date:'yyyy-MM-dd'"
(ngModelChange)="AsRangeQuery.value = $event; onChange(queryEntry) "
[value]="AsRangeQuery.value | date:'yyyy-MM-dd'" #from_date="ngModel"
class="form-control input-md"
type="date">
</div>
<div *ngSwitchCase="SearchQueryTypes.to_date" class="col-md-8 input-group">
<label class="col-md-4 control-label" for="to_date">To:</label>
<input id="to_date"
name="to_date"
title="To date"
i18n-title
[ngModel]="AsRangeQuery.value | date:'yyyy-MM-dd'"
(ngModelChange)="AsRangeQuery.value = $event; onChange(queryEntry) "
[value]="AsRangeQuery.value | date:'yyyy-MM-dd'" #to_date="ngModel"
class="form-control input-md"
type="date">
</div>
<div *ngSwitchCase="SearchQueryTypes.min_rating" class="col-md-8 input-group">
<label class="col-md-4 control-label" for="maxResolution">Min:</label>
<input id="minRating"
name="minRating"
title="Minimum Rating"
placeholder="0"
i18n-title
min="0"
max="5"
class="form-control input-md"
[(ngModel)]="AsRangeQuery.value"
(ngModelChange)="onChange(queryEntry)"
type="number">
</div>
<div *ngSwitchCase="SearchQueryTypes.max_rating" class="col-md-8 input-group">
<label class="col-md-4 control-label" for="maxResolution">Max:</label>
<input id="maxRating"
name="maxRating"
title="Maximum Rating"
placeholder="5"
i18n-title
min="0"
max="5"
class="form-control input-md"
[(ngModel)]="AsRangeQuery.value"
(ngModelChange)="onChange(queryEntry)"
type="number">
</div>
<div *ngSwitchCase="SearchQueryTypes.min_resolution" class="col-md-8 input-group">
<label class="col-md-4 control-label" for="maxResolution">Min:</label>
<input id="minResolution"
name="minResolution"
title="Minimum Rating"
placeholder="0"
i18n-title
min="0"
class="form-control input-md"
[(ngModel)]="AsRangeQuery.value"
(ngModelChange)="onChange(queryEntry)"
type="number">
<div class="input-group-append">
<span class="input-group-text">Mpx</span>
</div>
</div>
<div *ngSwitchCase="SearchQueryTypes.rating" class="col-md-9 d-flex">
<div class="input-group col-md-6">
<label class="col-md-4 control-label" for="maxResolution">Min:</label>
<input id="minRating"
name="minRating"
title="Minimum Rating"
placeholder="0"
i18n-title
min="0"
[max]="AsRatingQuery.max || 5"
class="form-control input-md"
[(ngModel)]="AsRatingQuery.min"
(ngModelChange)="onChange(queryEntry)"
type="number">
</div>
<div class="input-group col-md-6">
<label class="col-md-4 control-label" for="maxResolution">Max:</label>
<input id="maxRating"
name="maxRating"
title="Maximum Rating"
placeholder="5"
i18n-title
[min]="AsRatingQuery.min || 0"
max="5"
class="form-control input-md"
[(ngModel)]="AsRatingQuery.max"
(ngModelChange)="onChange(queryEntry)"
type="number">
<div *ngSwitchCase="SearchQueryTypes.max_resolution" class="col-md-8 input-group">
<label class="col-md-4 control-label" for="maxResolution">Max:</label>
<input id="maxResolution"
name="maxResolution"
title="Maximum Rating"
placeholder="5"
i18n-title
[min]="0"
class="form-control input-md"
[(ngModel)]="AsRangeQuery.value"
(ngModelChange)="onChange(queryEntry)"
type="number">
<div class="input-group-append">
<span class="input-group-text">Mpx</span>
</div>
</div>
<div *ngSwitchCase="SearchQueryTypes.resolution" class="col-md-9 d-flex">
<div class="input-group col-md-6">
<label class="col-md-4 control-label" for="maxResolution">Min:</label>
<input id="minResolution"
name="minResolution"
title="Minimum Rating"
placeholder="0"
i18n-title
min="0"
class="form-control input-md"
[(ngModel)]="AsResolutionQuery.min"
(ngModelChange)="onChange(queryEntry)"
type="number">
<div class="input-group-append">
<span class="input-group-text">Mpx</span>
</div>
</div>
<div class="input-group col-md-6">
<label class="col-md-4 control-label" for="maxResolution">Max:</label>
<input id="maxResolution"
name="maxResolution"
title="Maximum Rating"
placeholder="5"
i18n-title
[min]="AsResolutionQuery.min || 0"
class="form-control input-md"
[(ngModel)]="AsResolutionQuery.max"
(ngModelChange)="onChange(queryEntry)"
type="number">
<div class="input-group-append">
<span class="input-group-text">Mpx</span>
</div>
</div>
</div>
<div *ngSwitchCase="SearchQueryTypes.orientation" class="col-md-9 d-flex">
<div *ngSwitchCase="SearchQueryTypes.orientation" class="col-md-8 d-flex">
<div class="input-group col-md-6">
<bSwitch
class="switch"
@ -206,11 +205,11 @@
</div>
</div>
</ng-container>
<button [ngClass]="true? 'btn-danger':'btn-secondary'"
<button [ngClass]="'btn-danger'"
class="btn float-right col-md-1"
(click)="deleteItem()">
<span class="oi oi-trash" aria-hidden="true" aria-label="Delete"></span>
</button>
</ng-container>
</div>

View File

@ -1,11 +1,10 @@
import {Component, EventEmitter, forwardRef, OnChanges, Output} from '@angular/core';
import {
DateSearch,
DistanceSearch,
ListSearchQueryTypes,
OrientationSearch,
RatingSearch,
ResolutionSearch,
RangeSearch,
RangeSearchQueryTypes,
SearchListQuery,
SearchQueryDTO,
SearchQueryTypes,
@ -42,6 +41,11 @@ export class GallerySearchQueryEntryComponent implements ControlValueAccessor, V
constructor() {
this.SearchQueryTypesEnum = Utils.enumToArray(SearchQueryTypes);
// Range queries need to be added as AND with min and max sub entry
this.SearchQueryTypesEnum.filter(e => !RangeSearchQueryTypes.includes(e.key));
this.SearchQueryTypesEnum.push({value: 'Date', key: SearchQueryTypes.AND});
this.SearchQueryTypesEnum.push({value: 'Rating', key: SearchQueryTypes.AND});
this.SearchQueryTypesEnum.push({value: 'Resolution', key: SearchQueryTypes.AND});
}
@ -49,6 +53,7 @@ export class GallerySearchQueryEntryComponent implements ControlValueAccessor, V
return this.queryEntry && TextSearchQueryTypes.includes(this.queryEntry.type);
}
get IsListQuery(): boolean {
return this.queryEntry && ListSearchQueryTypes.includes(this.queryEntry.type);
}
@ -57,13 +62,10 @@ export class GallerySearchQueryEntryComponent implements ControlValueAccessor, V
return <any>this.queryEntry;
}
get AsDateQuery(): DateSearch {
get AsRangeQuery(): RangeSearch {
return <any>this.queryEntry;
}
get AsResolutionQuery(): ResolutionSearch {
return <any>this.queryEntry;
}
get AsOrientationQuery(): OrientationSearch {
return <any>this.queryEntry;
@ -73,9 +75,6 @@ export class GallerySearchQueryEntryComponent implements ControlValueAccessor, V
return <any>this.queryEntry;
}
get AsRatingQuery(): RatingSearch {
return <any>this.queryEntry;
}
get AsSomeOfQuery(): SomeOfSearchQuery {
return <any>this.queryEntry;
@ -132,30 +131,35 @@ export class GallerySearchQueryEntryComponent implements ControlValueAccessor, V
}
ngOnChanges(): void {
// console.log('ngOnChanges', this.queryEntry);
// 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);
// console.log('write value', this.queryEntry);
this.ngOnChanges();
}
public registerOnChange(fn: any): void {
// console.log('registerOnChange', fn);
this.onChange = fn;
registerOnChange(fn: (_: any) => void): void {
this.propagateChange = fn;
}
public registerOnTouched(fn: any): void {
this.onTouched = fn;
registerOnTouched(fn: () => void): void {
this.propagateTouch = fn;
}
public onChange(event: any) {
this.propagateChange(this.queryEntry);
}
private propagateChange = (_: any) => {
};
private propagateTouch = (_: any) => {
};
}

View File

@ -8,6 +8,7 @@
(blur)="onFocusLost()"
(focus)="onFocus()"
[(ngModel)]="rawSearchText"
(ngModelChange)="validateRawSearchText()"
#name="ngModel"
size="30"
ngControl="search"
@ -63,8 +64,8 @@
class="form-control"
i18n-placeholder
placeholder="Search"
disabled
[ngModel]="rawSearchText"
[(ngModel)]="rawSearchText"
(ngModelChange)="validateRawSearchText()"
size="30"
name="srch-term-preview"
id="srch-term-preview"
@ -72,6 +73,7 @@
<app-gallery-search-query-entry
[(ngModel)]="searchQueryDTO"
(change)="onQueryChange()"
name="search-root"
(delete)="resetQuery()">

View File

@ -21,6 +21,7 @@ export class GallerySearchComponent implements OnDestroy {
autoCompleteItems: AutoCompleteRenderItem[] = [];
public searchQueryDTO: SearchQueryDTO = <TextSearch>{type: SearchQueryTypes.any_text, text: ''};
public rawSearchText: string;
mouseOverAutoComplete = false;
readonly SearchQueryTypes: typeof SearchQueryTypes;
modalRef: BsModalRef;
@ -48,12 +49,6 @@ export class GallerySearchComponent implements OnDestroy {
});
}
public get rawSearchText(): string {
return SearchQueryDTO.stringify(this.searchQueryDTO);
}
public set rawSearchText(val: string) {
}
get HTMLSearchQuery() {
const searchQuery: any = {};
@ -81,7 +76,6 @@ export class GallerySearchComponent implements OnDestroy {
}
public setMouseOverAutoComplete(value: boolean) {
this.mouseOverAutoComplete = value;
}
@ -111,6 +105,18 @@ export class GallerySearchComponent implements OnDestroy {
this.searchQueryDTO = <TextSearch>{text: '', type: SearchQueryTypes.any_text};
}
onQueryChange() {
this.rawSearchText = SearchQueryDTO.stringify(this.searchQueryDTO);
}
validateRawSearchText() {
try {
this.searchQueryDTO = SearchQueryDTO.parse(this.rawSearchText);
console.log(this.searchQueryDTO);
} catch (e) {
console.error(e);
}
}
private emptyAutoComplete() {
this.autoCompleteItems = [];

View File

@ -5,17 +5,20 @@ import {Utils} from '../../../../../src/common/Utils';
import {SQLTestHelper} from '../../../SQLTestHelper';
import {
ANDSearchQuery,
DateSearch,
DistanceSearch,
FromDateSearch,
MaxRatingSearch,
MaxResolutionSearch,
MinRatingSearch,
MinResolutionSearch,
OrientationSearch,
ORSearchQuery,
RatingSearch,
ResolutionSearch,
SearchQueryDTO,
SearchQueryTypes,
SomeOfSearchQuery,
TextSearch,
TextSearchQueryMatchTypes
TextSearchQueryMatchTypes,
ToDateSearch
} from '../../../../../src/common/entities/SearchQueryDTO';
import {IndexingManager} from '../../../../../src/backend/model/database/sql/IndexingManager';
import {DirectoryDTO} from '../../../../../src/common/entities/DirectoryDTO';
@ -789,7 +792,7 @@ describe('SearchManager', (sqlHelper: SQLTestHelper) => {
it('should search date', async () => {
const sm = new SearchManager();
let query = <DateSearch>{before: 0, after: 0, type: SearchQueryTypes.date};
let query: any = <FromDateSearch>{value: 0, type: SearchQueryTypes.from_date};
expect(Utils.clone(await sm.search(query)))
.to.deep.equalInAnyOrder(removeDir(<SearchResultDTO>{
@ -800,9 +803,8 @@ describe('SearchManager', (sqlHelper: SQLTestHelper) => {
resultOverflow: false
}));
query = <DateSearch>{
before: p.metadata.creationDate,
after: p.metadata.creationDate, type: SearchQueryTypes.date
query = <ToDateSearch>{
value: p.metadata.creationDate, type: SearchQueryTypes.to_date
};
expect(Utils.clone(await sm.search(query)))
@ -814,11 +816,10 @@ describe('SearchManager', (sqlHelper: SQLTestHelper) => {
resultOverflow: false
}));
query = <DateSearch>{
before: p.metadata.creationDate,
after: p.metadata.creationDate,
query = <FromDateSearch>{
value: p.metadata.creationDate,
negate: true,
type: SearchQueryTypes.date
type: SearchQueryTypes.from_date
};
expect(Utils.clone(await sm.search(query)))
@ -830,15 +831,12 @@ describe('SearchManager', (sqlHelper: SQLTestHelper) => {
resultOverflow: false
}));
query = <DateSearch>{
before: p.metadata.creationDate + 1000000000,
after: 0, type: SearchQueryTypes.date
query = <ToDateSearch>{
value: p.metadata.creationDate + 1000000000,
type: SearchQueryTypes.to_date
};
expect(Utils.clone(await sm.search(<DateSearch>{
before: p.metadata.creationDate + 1000000000,
after: 0, type: SearchQueryTypes.date
})))
expect(Utils.clone(await sm.search(query)))
.to.deep.equalInAnyOrder(removeDir(<SearchResultDTO>{
searchQuery: query,
directories: [],
@ -853,7 +851,7 @@ describe('SearchManager', (sqlHelper: SQLTestHelper) => {
it('should search rating', async () => {
const sm = new SearchManager();
let query = <RatingSearch>{min: 0, max: 0, type: SearchQueryTypes.rating};
let query: MinRatingSearch | MaxRatingSearch = <MinRatingSearch>{value: 0, type: SearchQueryTypes.min_rating};
expect(Utils.clone(await sm.search(query)))
@ -865,7 +863,7 @@ describe('SearchManager', (sqlHelper: SQLTestHelper) => {
resultOverflow: false
}));
query = <RatingSearch>{min: 0, max: 5, type: SearchQueryTypes.rating};
query = <MaxRatingSearch>{value: 5, type: SearchQueryTypes.max_rating};
expect(Utils.clone(await sm.search(query)))
.to.deep.equalInAnyOrder(removeDir(<SearchResultDTO>{
searchQuery: query,
@ -875,7 +873,7 @@ describe('SearchManager', (sqlHelper: SQLTestHelper) => {
resultOverflow: false
}));
query = <RatingSearch>{min: 0, max: 5, negate: true, type: SearchQueryTypes.rating};
query = <MaxRatingSearch>{value: 5, negate: true, type: SearchQueryTypes.max_rating};
expect(Utils.clone(await sm.search(query)))
.to.deep.equalInAnyOrder(removeDir(<SearchResultDTO>{
searchQuery: query,
@ -885,7 +883,7 @@ describe('SearchManager', (sqlHelper: SQLTestHelper) => {
resultOverflow: false
}));
query = <RatingSearch>{min: 2, max: 2, type: SearchQueryTypes.rating};
query = <MinRatingSearch>{value: 2, type: SearchQueryTypes.min_rating};
expect(Utils.clone(await sm.search(query)))
.to.deep.equalInAnyOrder(removeDir(<SearchResultDTO>{
searchQuery: query,
@ -895,7 +893,7 @@ describe('SearchManager', (sqlHelper: SQLTestHelper) => {
resultOverflow: false
}));
query = <RatingSearch>{min: 2, max: 2, negate: true, type: SearchQueryTypes.rating};
query = <MinRatingSearch>{value: 2, negate: true, type: SearchQueryTypes.min_rating};
expect(Utils.clone(await sm.search(query)))
.to.deep.equalInAnyOrder(removeDir(<SearchResultDTO>{
searchQuery: query,
@ -910,7 +908,9 @@ describe('SearchManager', (sqlHelper: SQLTestHelper) => {
it('should search resolution', async () => {
const sm = new SearchManager();
let query = <ResolutionSearch>{min: 0, max: 0, type: SearchQueryTypes.resolution};
let query: MinResolutionSearch | MaxResolutionSearch =
<MinResolutionSearch>{value: 0, type: SearchQueryTypes.min_resolution};
expect(Utils.clone(await sm.search(query)))
.to.deep.equalInAnyOrder(removeDir(<SearchResultDTO>{
searchQuery: query,
@ -920,7 +920,7 @@ describe('SearchManager', (sqlHelper: SQLTestHelper) => {
resultOverflow: false
}));
query = <ResolutionSearch>{max: 1, type: SearchQueryTypes.resolution};
query = <MaxResolutionSearch>{value: 1, type: SearchQueryTypes.max_resolution};
expect(Utils.clone(await sm.search(query)))
.to.deep.equalInAnyOrder(removeDir(<SearchResultDTO>{
searchQuery: query,
@ -930,7 +930,7 @@ describe('SearchManager', (sqlHelper: SQLTestHelper) => {
resultOverflow: false
}));
query = <ResolutionSearch>{min: 2, max: 3, type: SearchQueryTypes.resolution};
query = <MinResolutionSearch>{value: 2, type: SearchQueryTypes.min_resolution};
expect(Utils.clone(await sm.search(query)))
.to.deep.equalInAnyOrder(removeDir(<SearchResultDTO>{
searchQuery: query,
@ -940,7 +940,7 @@ describe('SearchManager', (sqlHelper: SQLTestHelper) => {
resultOverflow: false
}));
query = <ResolutionSearch>{min: 2, max: 3, negate: true, type: SearchQueryTypes.resolution};
query = <MaxResolutionSearch>{value: 3, negate: true, type: SearchQueryTypes.max_resolution};
expect(Utils.clone(await sm.search(query)))
.to.deep.equalInAnyOrder(removeDir(<SearchResultDTO>{
searchQuery: query,
@ -950,7 +950,7 @@ describe('SearchManager', (sqlHelper: SQLTestHelper) => {
resultOverflow: false
}));
query = <ResolutionSearch>{min: 3, type: SearchQueryTypes.resolution};
query = <MinResolutionSearch>{value: 3, negate: true, type: SearchQueryTypes.min_resolution};
expect(Utils.clone(await sm.search(query)))
.to.deep.equalInAnyOrder(removeDir(<SearchResultDTO>{
searchQuery: query,
@ -1094,6 +1094,8 @@ describe('SearchManager', (sqlHelper: SQLTestHelper) => {
text: 'xyz',
type: SearchQueryTypes.keyword
};
// tslint:disable-next-line
expect(await sm.getRandomPhoto(query)).to.not.exist;
query = <TextSearch>{

View File

@ -0,0 +1,137 @@
import {expect} from 'chai';
import {
ANDSearchQuery,
DistanceSearch,
FromDateSearch,
MaxRatingSearch,
MaxResolutionSearch,
MinRatingSearch,
MinResolutionSearch,
OrientationSearch,
ORSearchQuery,
SearchQueryDTO,
SearchQueryTypes,
SomeOfSearchQuery,
TextSearch,
ToDateSearch
} from '../../../src/common/entities/SearchQueryDTO';
describe('SearchQueryDTO', () => {
const check = (query: SearchQueryDTO) => {
expect(SearchQueryDTO.parse(SearchQueryDTO.stringify(query))).to.deep.equals(query, SearchQueryDTO.stringify(query));
};
describe('should serialize and deserialize', () => {
it('Text search', () => {
check(<TextSearch>{type: SearchQueryTypes.any_text, text: 'test'});
check(<TextSearch>{type: SearchQueryTypes.person, text: 'person_test'});
check(<TextSearch>{type: SearchQueryTypes.directory, text: 'directory'});
check(<TextSearch>{type: SearchQueryTypes.keyword, text: 'big boom'});
check(<TextSearch>{type: SearchQueryTypes.caption, text: 'caption'});
check(<TextSearch>{type: SearchQueryTypes.file_name, text: 'filename'});
check(<TextSearch>{type: SearchQueryTypes.position, text: 'New York'});
});
it('Date search', () => {
check(<FromDateSearch>{type: SearchQueryTypes.from_date, value: (new Date(2020, 1, 1)).getTime()});
check(<ToDateSearch>{type: SearchQueryTypes.to_date, value: (new Date(2020, 1, 2)).getTime()});
});
it('Rating search', () => {
check(<MinRatingSearch>{type: SearchQueryTypes.min_rating, value: 10});
check(<MaxRatingSearch>{type: SearchQueryTypes.max_rating, value: 1});
});
it('Resolution search', () => {
check(<MinResolutionSearch>{type: SearchQueryTypes.min_resolution, value: 10});
check(<MaxResolutionSearch>{type: SearchQueryTypes.max_resolution, value: 5});
});
it('Distance search', () => {
check(<DistanceSearch>{type: SearchQueryTypes.distance, distance: 10, from: {text: 'New York'}});
});
it('OrientationSearch search', () => {
check(<OrientationSearch>{type: SearchQueryTypes.orientation, landscape: true});
check(<OrientationSearch>{type: SearchQueryTypes.orientation, landscape: false});
});
it('And search', () => {
check(<ANDSearchQuery>{
type: SearchQueryTypes.AND,
list: [
<TextSearch>{type: SearchQueryTypes.keyword, text: 'big boom'},
<TextSearch>{type: SearchQueryTypes.position, text: 'New York'}
]
});
check(<ANDSearchQuery>{
type: SearchQueryTypes.AND,
list: [
<TextSearch>{type: SearchQueryTypes.keyword, text: 'big boom'},
<ANDSearchQuery>{
type: SearchQueryTypes.AND,
list: [
<TextSearch>{type: SearchQueryTypes.caption, text: 'caption'},
<TextSearch>{type: SearchQueryTypes.position, text: 'New York'}
]
}
]
});
});
it('Or search', () => {
check(<ORSearchQuery>{
type: SearchQueryTypes.OR,
list: [
<TextSearch>{type: SearchQueryTypes.keyword, text: 'big boom'},
<TextSearch>{type: SearchQueryTypes.position, text: 'New York'}
]
});
check(<ORSearchQuery>{
type: SearchQueryTypes.OR,
list: [
<ORSearchQuery>{
type: SearchQueryTypes.OR,
list: [
<TextSearch>{type: SearchQueryTypes.keyword, text: 'big boom'},
<TextSearch>{type: SearchQueryTypes.person, text: 'person_test'}
]
},
<TextSearch>{type: SearchQueryTypes.position, text: 'New York'}
]
});
});
it('Some of search', () => {
check(<SomeOfSearchQuery>{
type: SearchQueryTypes.SOME_OF,
list: [
<TextSearch>{type: SearchQueryTypes.keyword, text: 'big boom'},
<TextSearch>{type: SearchQueryTypes.position, text: 'New York'}
]
});
check(<SomeOfSearchQuery>{
type: SearchQueryTypes.SOME_OF,
min: 2,
list: [
<TextSearch>{type: SearchQueryTypes.keyword, text: 'big boom'},
<TextSearch>{type: SearchQueryTypes.position, text: 'New York'},
<TextSearch>{type: SearchQueryTypes.caption, text: 'caption test'}
]
});
check(<SomeOfSearchQuery>{
type: SearchQueryTypes.SOME_OF,
min: 2,
list: [
<TextSearch>{type: SearchQueryTypes.keyword, text: 'big boom'},
<TextSearch>{type: SearchQueryTypes.person, text: 'person_test'},
<ANDSearchQuery>{
type: SearchQueryTypes.AND,
list: [
<TextSearch>{type: SearchQueryTypes.caption, text: 'caption'},
<TextSearch>{type: SearchQueryTypes.position, text: 'New York'}
]
}
]
});
});
});
});