elastic service continued
authorBill Erickson <berickxx@gmail.com>
Mon, 9 Sep 2019 16:26:27 +0000 (12:26 -0400)
committerBill Erickson <berickxx@gmail.com>
Fri, 17 Jan 2020 19:36:02 +0000 (14:36 -0500)
Signed-off-by: Bill Erickson <berickxx@gmail.com>
Open-ILS/src/eg2/src/app/share/catalog/catalog.service.ts
Open-ILS/src/eg2/src/app/share/catalog/elastic-search-context.ts [deleted file]
Open-ILS/src/eg2/src/app/share/catalog/elastic.service.ts
Open-ILS/src/eg2/src/app/share/catalog/search-context.ts

index 10c50ad..d9f297f 100644 (file)
@@ -44,6 +44,15 @@ export class CatalogService {
     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()) {
@@ -94,15 +103,6 @@ export class CatalogService {
 
     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'; }
 
@@ -121,15 +121,6 @@ export class CatalogService {
 
     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;
 
@@ -302,20 +293,22 @@ export class CatalogService {
         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));
         });
     }
 
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 (file)
index 6c8ab24..0000000
+++ /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();
-    }
-    */
-}
-
index 26f33a0..ff5e19e 100644 (file)
@@ -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;
+            }
     }
 }
 
index c026eaa..43867fc 100644 (file)
@@ -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[]};