LP1844418 ES rebasing
authorBill Erickson <berickxx@gmail.com>
Wed, 27 Oct 2021 17:21:52 +0000 (13:21 -0400)
committerBill Erickson <berickxx@gmail.com>
Mon, 13 Jun 2022 20:02:46 +0000 (16:02 -0400)
Signed-off-by: Bill Erickson <berickxx@gmail.com>
Open-ILS/src/eg2/src/app/share/catalog/elastic.service.ts

index 903dd0c..434ebdd 100644 (file)
@@ -9,10 +9,20 @@ import {RequestBodySearch, MatchQuery, MultiMatchQuery, TermsQuery, Query, Sort,
     PrefixQuery, NestedQuery, BoolQuery, TermQuery, WildcardQuery, RangeQuery,
     QueryStringQuery} from 'elastic-builder';
 
+const INDEX_SHORTCUTS_MAP = {
+    'au:': 'author\\*:',
+    'ti:': 'title\\*:',
+    'su:': 'subject\\*:',
+    'kw:': 'keyword\\*:',
+    'se:': 'series\\*:',
+    'pb:': 'identifier|publisher\\*:'
+};
+
 @Injectable()
 export class ElasticService {
 
     enabled: boolean;
+    ebfMap: {[id: number]: IdlObject} = {};
 
     constructor(
         private idl: IdlService,
@@ -21,6 +31,10 @@ export class ElasticService {
         private pcrud: PcrudService
     ) {}
 
+    init(): Promise<any> {
+        return Promise.resolve();
+    }
+
     // Returns true if Elastic can provide search results.
     canSearch(ctx: CatalogSearchContext): boolean {
         if (!this.enabled) { return false; }
@@ -32,8 +46,9 @@ export class ElasticService {
             return true;
         }
 
-        if (ctx.identSearch.isSearchable()
-            && ctx.identSearch.queryType !== 'item_barcode') {
+        if (ctx.identSearch.isSearchable() &&
+            ctx.identSearch.queryType !== 'item_barcode' &&
+            ctx.identSearch.queryType !== 'identifier|tcn') {
             return true;
         }
 
@@ -48,7 +63,7 @@ export class ElasticService {
 
         let method = ctx.termSearch.isMetarecordSearch() ?
             'open-ils.search.elastic.bib_search.metabib' :
-            'open-ils.search.elastic.bib_search'
+            'open-ils.search.elastic.bib_search';
 
         if (ctx.isStaff) { method += '.staff'; }
 
@@ -107,6 +122,9 @@ export class ElasticService {
             // Sort by match score by default.
             search.sort(new Sort('_score', 'desc'));
         }
+
+        // Apply a tie-breaker sort on bib ID.
+        search.sort(new Sort('id', 'desc'));
     }
 
     addFilters(ctx: CatalogSearchContext, rootNode: BoolQuery) {
@@ -128,7 +146,7 @@ export class ElasticService {
         ts.facetFilters.forEach(f => {
             if (f.facetValue !== '') {
                 rootNode.filter(new TermQuery(
-                    `${f.facetClass}|${f.facetName}.facet`, f.facetValue));
+                    `${f.facetClass}|${f.facetName}|facet`, f.facetValue));
             }
         });
 
@@ -186,12 +204,10 @@ export class ElasticService {
             const marcQuery = new BoolQuery();
             const tag = ms.tags[idx];
             const subfield = ms.subfields[idx];
+            const matchOp = ms.matchOp[idx];
 
-            // Full-text search on the values
-            const valMatch = new MultiMatchQuery(['marc.value*'], value);
-            valMatch.operator('and');
-            valMatch.type('most_fields');
-            marcQuery.must(valMatch);
+            this.appendMatchOp(
+                marcQuery, matchOp, 'marc.value.text*', 'marc.value', value);
 
             if (tag) {
                 marcQuery.must(new TermQuery('marc.tag', tag));
@@ -257,12 +273,20 @@ export class ElasticService {
         } else if (fieldClass === 'keyword' &&
             matchOp === 'contains' && value.match(/:/)) {
 
+            // Map ti: to title\*: so the shortcut searches search
+            // across all sub-indexes.
+            let valueMod = value;
+            Object.keys(INDEX_SHORTCUTS_MAP).forEach(sc => {
+                const reg = new RegExp(sc, 'gi');
+                valueMod = valueMod.replace(reg, INDEX_SHORTCUTS_MAP[sc]);
+            });
+
             // A search where 'keyword' 'contains' a value with a ':'
             // character is assumed to be a complex / query string search.
             // NOTE: could handle this differently, e.g. provide an escape
             // character (e.g. !title:potter), a dedicated matchOp, etc.
             boolNode.must(
-                new QueryStringQuery(value)
+                new QueryStringQuery(valueMod)
                     .defaultOperator('AND')
                     .defaultField('keyword.text')
             );
@@ -270,7 +294,22 @@ export class ElasticService {
             return;
         }
 
-        const textIndex = `${fieldClass}.text*`;
+        // KCLS ident searches: Identifier indices don't have text variations
+        let textIndex = fieldClass.match('identifier') ?
+            fieldClass : `${fieldClass}.text*`;
+
+        // Bib call numbers use different indexes depending on the matchOp
+        if (fieldClass === 'identifier|bibcn' &&
+            matchOp.match(/contains|nocontains|phrase/)) {
+            textIndex = 'keyword|bibcn.text*';
+        }
+
+        this.appendMatchOp(boolNode, matchOp, textIndex, fieldClass, value);
+    }
+
+    appendMatchOp(boolNode: BoolQuery, matchOp: string,
+        textIndex: string, termIndex: string, value: string) {
+
         let query;
 
         switch (matchOp) {
@@ -297,15 +336,20 @@ export class ElasticService {
                 boolNode.mustNot(query);
                 return;
 
-            // "exact" and "starts" searches use term searches instead
-            // of full-text searches.
+            // "containsexact", "exact", "starts" searches use term
+            // searches instead of full-text searches.
+            case 'containsexact':
+                query = new WildcardQuery(termIndex, `*${value}*`);
+                boolNode.must(query);
+                return;
+
             case 'exact':
-                query = new TermQuery(fieldClass, value);
+                query = new TermQuery(termIndex, value);
                 boolNode.must(query);
                 return;
 
             case 'starts':
-                query = new PrefixQuery(fieldClass, value);
+                query = new PrefixQuery(termIndex, value);
                 boolNode.must(query);
                 return;
         }