From: Bill Erickson Date: Mon, 9 Sep 2019 16:26:27 +0000 (-0400) Subject: elastic service continued X-Git-Url: https://old-git.evergreen-ils.org/?a=commitdiff_plain;h=e19d89fa3d99f727da569f04251fedfbc9343dac;p=working%2FEvergreen.git elastic service continued Signed-off-by: Bill Erickson --- diff --git a/Open-ILS/src/eg2/src/app/share/catalog/catalog.service.ts b/Open-ILS/src/eg2/src/app/share/catalog/catalog.service.ts index 10c50ad762..d9f297f7fc 100644 --- a/Open-ILS/src/eg2/src/app/share/catalog/catalog.service.ts +++ b/Open-ILS/src/eg2/src/app/share/catalog/catalog.service.ts @@ -44,6 +44,15 @@ export class CatalogService { search(ctx: CatalogSearchContext): Promise { 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()) { @@ -94,15 +103,6 @@ export class CatalogService { marcSearch(ctx: CatalogSearchContext): Promise { - 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'; } @@ -121,15 +121,6 @@ export class CatalogService { termSearch(ctx: CatalogSearchContext): Promise { - 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; @@ -302,20 +293,22 @@ export class CatalogService { return facetData; } - fetchCcvms(): Promise { + fetchCcvms(): Promise { - 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)); }); } diff --git a/Open-ILS/src/eg2/src/app/share/catalog/elastic-search-context.ts b/Open-ILS/src/eg2/src/app/share/catalog/elastic-search-context.ts deleted file mode 100644 index 6c8ab247a8..0000000000 --- a/Open-ILS/src/eg2/src/app/share/catalog/elastic-search-context.ts +++ /dev/null @@ -1,194 +0,0 @@ -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(); - } - */ -} - diff --git a/Open-ILS/src/eg2/src/app/share/catalog/elastic.service.ts b/Open-ILS/src/eg2/src/app/share/catalog/elastic.service.ts index 26f33a0397..ff5e19e8b2 100644 --- a/Open-ILS/src/eg2/src/app/share/catalog/elastic.service.ts +++ b/Open-ILS/src/eg2/src/app/share/catalog/elastic.service.ts @@ -5,7 +5,7 @@ import {OrgService} from '@eg/core/org.service'; 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() @@ -32,7 +32,7 @@ export class ElasticService { } canSearch(ctx: CatalogSearchContext): boolean { - + if (ctx.marcSearch.isSearchable()) { return true; } if ( ctx.termSearch.isSearchable() && @@ -70,16 +70,16 @@ export class ElasticService { 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); } @@ -108,11 +108,12 @@ export class ElasticService { } 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 => { @@ -129,7 +130,7 @@ export class ElasticService { rootNode.filter(new TermQuery('date1', ts.date1)); } else { - + const range = new RangeQuery('date1'); switch (ts.dateOp) { @@ -155,7 +156,7 @@ export class ElasticService { ms.values.forEach((value, idx) => { if (value === '' || value === null) { return; } - + const marcQuery = new BoolQuery(); const tag = ms.tags[idx]; const subfield = ms.subfields[idx]; @@ -178,66 +179,91 @@ export class ElasticService { }); } - 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; + } } } diff --git a/Open-ILS/src/eg2/src/app/share/catalog/search-context.ts b/Open-ILS/src/eg2/src/app/share/catalog/search-context.ts index c026eaaf14..43867fc1c7 100644 --- a/Open-ILS/src/eg2/src/app/share/catalog/search-context.ts +++ b/Open-ILS/src/eg2/src/app/share/catalog/search-context.ts @@ -187,7 +187,7 @@ export class CatalogTermContext { format: string; available = false; - // TODO: configurable + // TODO: configurable // format limiter default to using the search_format filter formatCtype = 'search_format'; ccvmFilters: {[ccvmCode: string]: string[]};