search(ctx: CatalogSearchContext): Promise<void> {
ctx.searchState = CatalogSearchState.SEARCHING;
+ if (this.elastic.canSearch(ctx)) {
+ return this.elastic.performSearch(ctx)
+ .then(result => {
+ this.applyResultData(ctx, result);
+ ctx.searchState = CatalogSearchState.COMPLETE;
+ this.onSearchComplete.emit(ctx);
+ });
+ }
+
if (ctx.showBasket) {
return this.basketSearch(ctx);
} else if (ctx.marcSearch.isSearchable()) {
marcSearch(ctx: CatalogSearchContext): Promise<void> {
- if (this.elastic.canSearch(ctx)) {
- return this.elastic.performSearch(ctx)
- .then(result => {
- this.applyResultData(ctx, result);
- ctx.searchState = CatalogSearchState.COMPLETE;
- this.onSearchComplete.emit(ctx);
- });
- }
-
let method = 'open-ils.search.biblio.marc';
if (ctx.isStaff) { method += '.staff'; }
termSearch(ctx: CatalogSearchContext): Promise<void> {
- if (this.elastic.canSearch(ctx)) {
- return this.elastic.performSearch(ctx)
- .then(result => {
- this.applyResultData(ctx, result);
- ctx.searchState = CatalogSearchState.COMPLETE;
- this.onSearchComplete.emit(ctx);
- });
- }
-
let method = 'open-ils.search.biblio.multiclass.query';
let fullQuery;
return facetData;
}
- fetchCcvms(): Promise<void> {
+ fetchCcvms(): Promise<any> {
- if (Object.keys(this.ccvmMap).length) {
- return Promise.resolve();
- }
+ // XXX Putting the elastic initialization call here since
+ // the call is assumed to be run at page load time.
+ // TODO: migrate our fetch calls to generic init call.
- return new Promise((resolve, reject) => {
- this.pcrud.search('ccvm',
+ return this.elastic.init().then(ok => {
+
+ if (Object.keys(this.ccvmMap).length) {
+ return Promise.resolve();
+ }
+
+ return this.pcrud.search('ccvm',
{ctype : CATALOG_CCVM_FILTERS}, {},
{atomic: true, anonymous: true}
- ).subscribe(list => {
- this.compileCcvms(list);
- resolve();
- });
+ ).toPromise().then(list => this.compileCcvms(list));
});
}
+++ /dev/null
-import {IdlObject} from '@eg/core/idl.service';
-import {CatalogSearchContext} from './search-context';
-
-class ElasticSearchParams {
- search_org: number;
- search_depth: number;
- available: boolean;
- sort: any[] = [];
- searches: any[] = [];
- marc_searches: any[] = [];
- filters: any[] = [];
-}
-
-export class ElasticSearchContext extends CatalogSearchContext {
-
- // The UI is ambiguous re: mixing ANDs and ORs.
- // Here booleans are grouped ANDs first, then each OR is given its own node.
- compileTerms(params: ElasticSearchParams) {
-
- const ts = this.termSearch;
-
- ts.joinOp.forEach((op, idx) => {
- let matchOp = 'match';
-
- switch (ts.matchOp[idx]) {
- case 'phrase':
- matchOp = 'match_phrase';
- break;
- case 'nocontains':
- matchOp = 'must_not';
- break;
- case 'exact':
- matchOp = 'term';
- break;
- case 'starts':
- matchOp = 'match_phrase_prefix';
- break;
- }
-
- params.searches.push({
- field: ts.fieldClass[idx],
- match_op: matchOp,
- value: ts.query[idx]
- });
- });
- }
-
- addTermFilter(params: ElasticSearchParams, name: string, value: any) {
- if (value === '' ||
- value === null ||
- value === undefined) { return; }
-
- // Multiple filter values for a single filter are OR'ed.
- for (let idx = 0; idx < params.filters.length; idx++) {
- const filter = params.filters[idx];
-
- if (filter.term && name in filter.term) {
- // Pluralize an existing filter
- filter.terms = {};
- filter.terms[name] = [filter.term[name], value];
- delete filter.term;
- return;
-
- } else if (filter.terms && name in filter.terms) {
- // Append a filter value to an already pluralized filter.
- filter.terms[name].push(value);
- return;
- }
- }
-
- // New filter type
- const node: any = {term: {}};
- node.term[name] = value;
- params.filters.push(node);
- }
-
- compileTermSearchQuery(): any {
- const ts = this.termSearch;
- const params = this.newParams();
-
- params.available = ts.available;
-
- if (ts.date1 && ts.dateOp) {
- const dateFilter: Object = {};
- switch (ts.dateOp) {
- case 'is':
- this.addTermFilter(params, 'date1', ts.date1);
- break;
- case 'before':
- params.filters.push({range: {date1: {lt: ts.date1}}});
- break;
- case 'after':
- params.filters.push({range: {date1: {gt: ts.date1}}});
- break;
- case 'between':
- if (ts.date2) {
- params.filters.push(
- {range: {date1: {gt: ts.date1, lt: ts.date2}}});
- }
- }
- }
-
- this.compileTerms(params);
-
- if (this.global) {
- params.search_depth = this.org.root().ou_type().depth();
- }
-
- // PENDING DEV
- /*
- if (ts.copyLocations[0] !== '') {
- str += ' locations(' + ts.copyLocations + ')';
- }
- */
-
- if (ts.format) {
- this.addTermFilter(params, ts.formatCtype, ts.format);
- }
-
- Object.keys(ts.ccvmFilters).forEach(field => {
- ts.ccvmFilters[field].forEach(value => {
- if (value !== '') {
- this.addTermFilter(params, field, value);
- }
- });
- });
-
- ts.facetFilters.forEach(f => {
- this.addTermFilter(params,
- `${f.facetClass}|${f.facetName}`, f.facetValue);
- });
-
- return params;
- }
-
- newParams(): ElasticSearchParams {
- const params = new ElasticSearchParams();
- /*
- params.limit = this.pager.limit;
- params.offset = this.pager.offset;
- */
- params.search_org = this.searchOrg.id()
-
- if (this.sort) {
- // e.g. title, title.descending => [{title => 'desc'}]
- const parts = this.sort.split(/\./);
- const sort: any = {};
- sort[parts[0]] = parts[1] ? 'desc' : 'asc';
- params.sort = [sort];
- }
-
- return params;
- }
-
- compileMarcSearchArgs(): any {
- const ms = this.marcSearch;
- const params = this.newParams();
-
- ms.values.forEach((val, idx) => {
- if (val !== '') {
- params.marc_searches.push({
- tag: ms.tags[idx],
- subfield: ms.subfields[idx] ? ms.subfields[idx] : null,
- value: ms.values[idx]
- });
- }
- });
-
- return params;
- }
-
- /*
- getApiName(): string {
-
- // Elastic covers only a subset of available search types.
- if (this.marcSearch.isSearchable() ||
- (
- this.termSearch.isSearchable() &&
- !this.termSearch.groupByMetarecord &&
- !this.termSearch.fromMetarecord
- )
- ) {
-
- return this.isStaff ?
- 'open-ils.search.elastic.bib_search.staff' :
- 'open-ils.search.elastic.bib_search';
- }
-
- // Fall back to existing APIs.
- return super.getApiName();
- }
- */
-}
-
import {NetService} from '@eg/core/net.service';
import {PcrudService} from '@eg/core/pcrud.service';
import {CatalogSearchContext} from './search-context';
-import {RequestBodySearch, MatchQuery, MultiMatchQuery,
+import {RequestBodySearch, MatchQuery, MultiMatchQuery, TermsQuery, Query,
Sort, NestedQuery, BoolQuery, TermQuery, RangeQuery} from 'elastic-builder';
@Injectable()
}
canSearch(ctx: CatalogSearchContext): boolean {
-
+
if (ctx.marcSearch.isSearchable()) { return true; }
if ( ctx.termSearch.isSearchable() &&
compileRequestBody(ctx: CatalogSearchContext): RequestBodySearch {
- const search = new RequestBodySearch()
+ const search = new RequestBodySearch();
search.source(['id']); // only retrieve IDs
- search.size(ctx.pager.limit)
+ search.size(ctx.pager.limit);
search.from(ctx.pager.offset);
const rootNode = new BoolQuery();
if (ctx.termSearch.isSearchable()) {
- this.addTermSearches(ctx, rootNode);
+ this.addFieldSearches(ctx, rootNode);
} else if (ctx.marcSearch.isSearchable()) {
this.addMarcSearches(ctx, rootNode);
}
}
Object.keys(ts.ccvmFilters).forEach(field => {
- ts.ccvmFilters[field].forEach(value => {
- if (value !== '') {
- rootNode.filter(new TermQuery(field, value));
- }
- });
+ // TermsQuery required since there may be multiple filter
+ // values for a given CCVM. These are treated like OR filters.
+ const values: string[] = ts.ccvmFilters[field].filter(v => v !== '');
+ if (values.length > 0) {
+ rootNode.filter(new TermsQuery(field, values));
+ }
});
ts.facetFilters.forEach(f => {
rootNode.filter(new TermQuery('date1', ts.date1));
} else {
-
+
const range = new RangeQuery('date1');
switch (ts.dateOp) {
ms.values.forEach((value, idx) => {
if (value === '' || value === null) { return; }
-
+
const marcQuery = new BoolQuery();
const tag = ms.tags[idx];
const subfield = ms.subfields[idx];
});
}
- addTermSearches(ctx: CatalogSearchContext, rootNode: BoolQuery) {
-
- // TODO: boolean OR support.
+ addFieldSearches(ctx: CatalogSearchContext, rootNode: BoolQuery) {
const ts = ctx.termSearch;
+ let boolNode: BoolQuery;
+ const shouldNodes: Query[] = [];
+
+ if (ts.joinOp.filter(op => op === '||').length > 0) {
+ // Searches containing ORs require a series of boolean buckets.
+ boolNode = new BoolQuery();
+ shouldNodes.push(boolNode);
+
+ } else {
+ // Searches composed entirely of ANDed terms can live on the
+ // root boolean AND node.
+ boolNode = rootNode;
+ }
+
ts.joinOp.forEach((op, idx) => {
- const value = ts.query[idx];
-
- const fieldClass = ts.fieldClass[idx];
- const textIndex = `${fieldClass}|*text*`;
- let query;
-
- switch (ts.matchOp[idx]) {
-
- case 'contains':
- query = new MultiMatchQuery([textIndex], value);
- query.operator('and');
- query.type('most_fields');
- rootNode.must(query);
- break;
-
- case 'phrase':
- query = new MultiMatchQuery([textIndex], value);
- query.type('phrase');
- rootNode.must(query);
- break;
-
- case 'nocontains':
- query = new MultiMatchQuery([textIndex], value);
- query.operator('and');
- query.type('most_fields');
- rootNode.mustNot(query);
- break;
-
- case 'exact':
-
- // TODO: these need to be grouped first by field
- // so we can search multiple values on a singel term
- // via 'terms' search.
-
- /*
- const shoulds = [];
- this.bibFields.filter(f => (
- f.search_field() === 't' &&
- f.search_group() === fieldClass
- )).forEach(field => {
- shoulds.push(
- });
-
- const should = new BoolQuery();
- */
- break;
-
- case 'starts':
- query = new MultiMatchQuery([textIndex], value);
- query.type('phrase_prefix');
- rootNode.must(query);
- break;
+ if (op === '||') {
+ // Start a new OR sub-branch
+ // op on the first query term will never be 'or'.
+ boolNode = new BoolQuery();
+ shouldNodes.push(boolNode);
}
+
+ this.addSearchField(ctx, idx, boolNode);
});
+
+ if (shouldNodes.length > 0) {
+ rootNode.should(shouldNodes);
+ }
+ }
+
+
+ addSearchField(ctx: CatalogSearchContext, idx: number, boolNode: BoolQuery) {
+ const ts = ctx.termSearch;
+ const value = ts.query[idx];
+
+ if (value === '' || value === null) { return; }
+
+ const fieldClass = ts.fieldClass[idx];
+ const textIndex = `${fieldClass}|*text*`;
+ let query;
+
+ switch (ts.matchOp[idx]) {
+
+ case 'contains':
+ query = new MultiMatchQuery([textIndex], value);
+ query.operator('and');
+ query.type('most_fields');
+ boolNode.must(query);
+ break;
+
+ case 'phrase':
+ query = new MultiMatchQuery([textIndex], value);
+ query.type('phrase');
+ boolNode.must(query);
+ break;
+
+ case 'nocontains':
+ query = new MultiMatchQuery([textIndex], value);
+ query.operator('and');
+ query.type('most_fields');
+ boolNode.mustNot(query);
+ break;
+
+ case 'exact':
+
+ const shoulds: Query[] = [];
+ this.bibFields.filter(f => (
+ f.search_field() === 't' &&
+ f.search_group() === fieldClass
+ )).forEach(field => {
+ shoulds.push(new TermQuery(field.name, value));
+ });
+
+ boolNode.should(shoulds);
+ break;
+
+ case 'starts':
+ query = new MultiMatchQuery([textIndex], value);
+ query.type('phrase_prefix');
+ boolNode.must(query);
+ break;
+ }
}
}
format: string;
available = false;
- // TODO: configurable
+ // TODO: configurable
// format limiter default to using the search_format filter
formatCtype = 'search_format';
ccvmFilters: {[ccvmCode: string]: string[]};