From 5893afda7936f65ed88d8d7bc2ecd062c0aea60b Mon Sep 17 00:00:00 2001 From: Bill Erickson Date: Fri, 6 Sep 2019 18:10:45 -0400 Subject: [PATCH] elastic-builder / marc search Signed-off-by: Bill Erickson --- .../eg2/src/app/share/catalog/catalog.service.ts | 10 +++ .../eg2/src/app/share/catalog/elastic.service.ts | 97 +++++++++++++++++----- 2 files changed, 88 insertions(+), 19 deletions(-) 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 09d2bfbfbd..10c50ad762 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 @@ -93,6 +93,16 @@ 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'; } 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 feb0690988..26f33a0397 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 @@ -6,7 +6,7 @@ import {NetService} from '@eg/core/net.service'; import {PcrudService} from '@eg/core/pcrud.service'; import {CatalogSearchContext} from './search-context'; import {RequestBodySearch, MatchQuery, MultiMatchQuery, - Sort, BoolQuery, TermQuery} from 'elastic-builder'; + Sort, NestedQuery, BoolQuery, TermQuery, RangeQuery} from 'elastic-builder'; @Injectable() export class ElasticService { @@ -21,6 +21,7 @@ export class ElasticService { ) {} init(): Promise { + if (this.bibFields.length > 0) { return Promise.resolve(); } @@ -42,7 +43,6 @@ export class ElasticService { return false; } - // For API consistency, returns an array of arrays whose first // entry within each sub-array is a record ID. performSearch(ctx: CatalogSearchContext): Promise { @@ -71,17 +71,22 @@ export class ElasticService { compileRequestBody(ctx: CatalogSearchContext): RequestBodySearch { const search = new RequestBodySearch() + search.source(['id']); // only retrieve IDs search.size(ctx.pager.limit) search.from(ctx.pager.offset); - const rootAnd = new BoolQuery(); + const rootNode = new BoolQuery(); - this.compileTermSearch(ctx, rootAnd); - this.addFilters(ctx, rootAnd); + if (ctx.termSearch.isSearchable()) { + this.addTermSearches(ctx, rootNode); + } else if (ctx.marcSearch.isSearchable()) { + this.addMarcSearches(ctx, rootNode); + } + this.addFilters(ctx, rootNode); this.addSort(ctx, search); - search.query(rootAnd); + search.query(rootNode); return search; } @@ -90,43 +95,99 @@ export class ElasticService { if (!ctx.sort) { return; } - // e.g. title, title.descending => [{title => 'desc'}] + // e.g. title, title.descending const parts = ctx.sort.split(/\./); search.sort(new Sort(parts[0], parts[1] ? 'desc' : 'asc')); } - addFilters(ctx: CatalogSearchContext, rootAnd: BoolQuery) { + addFilters(ctx: CatalogSearchContext, rootNode: BoolQuery) { const ts = ctx.termSearch; if (ts.format) { - rootAnd.filter(new TermQuery(ts.formatCtype, ts.format)); + rootNode.filter(new TermQuery(ts.formatCtype, ts.format)); } Object.keys(ts.ccvmFilters).forEach(field => { ts.ccvmFilters[field].forEach(value => { if (value !== '') { - rootAnd.filter(new TermQuery(field, value)); + rootNode.filter(new TermQuery(field, value)); } }); }); ts.facetFilters.forEach(f => { if (f.facetValue !== '') { - rootAnd.filter(new TermQuery( + rootNode.filter(new TermQuery( `${f.facetClass}|${f.facetName}`, f.facetValue)); } }); + + if (ts.date1 && ts.dateOp) { + + if (ts.dateOp === 'is') { + + rootNode.filter(new TermQuery('date1', ts.date1)); + + } else { + + const range = new RangeQuery('date1'); + + switch (ts.dateOp) { + case 'before': + range.lt(ts.date1); + break; + case 'after': + range.gt(ts.date1); + break; + case 'between': + range.gt(ts.date1); + range.lt(ts.date2); + break; + } + + rootNode.filter(range); + } + } + } + + addMarcSearches(ctx: CatalogSearchContext, rootNode: BoolQuery) { + const ms = ctx.marcSearch; + + ms.values.forEach((value, idx) => { + if (value === '' || value === null) { return; } + + const marcQuery = new BoolQuery(); + const tag = ms.tags[idx]; + const subfield = ms.subfields[idx]; + + // Full-text search on the values + const valMatch = new MultiMatchQuery(['marc.value*'], value); + valMatch.operator('and'); + valMatch.type('most_fields'); + marcQuery.must(valMatch); + + if (tag) { + marcQuery.must(new TermQuery('marc.tag', tag)); + } + + if (subfield) { + marcQuery.must(new TermQuery('marc.subfield', subfield)); + } + + rootNode.must(new NestedQuery(marcQuery, 'marc')); + }); } - compileTermSearch(ctx: CatalogSearchContext, rootAnd: BoolQuery) { + addTermSearches(ctx: CatalogSearchContext, rootNode: BoolQuery) { // TODO: boolean OR support. const ts = ctx.termSearch; ts.joinOp.forEach((op, idx) => { + const value = ts.query[idx]; + const fieldClass = ts.fieldClass[idx]; const textIndex = `${fieldClass}|*text*`; - const value = ts.query[idx]; let query; switch (ts.matchOp[idx]) { @@ -135,20 +196,20 @@ export class ElasticService { query = new MultiMatchQuery([textIndex], value); query.operator('and'); query.type('most_fields'); - rootAnd.must(query); + rootNode.must(query); break; case 'phrase': query = new MultiMatchQuery([textIndex], value); query.type('phrase'); - rootAnd.must(query); + rootNode.must(query); break; case 'nocontains': query = new MultiMatchQuery([textIndex], value); query.operator('and'); query.type('most_fields'); - rootAnd.mustNot(query); + rootNode.mustNot(query); break; case 'exact': @@ -173,12 +234,10 @@ export class ElasticService { case 'starts': query = new MultiMatchQuery([textIndex], value); query.type('phrase_prefix'); - rootAnd.must(query); + rootNode.must(query); break; } }); } - - } -- 2.11.0