elastic-builder / marc search
authorBill Erickson <berickxx@gmail.com>
Fri, 6 Sep 2019 22:10:45 +0000 (18:10 -0400)
committerBill Erickson <berickxx@gmail.com>
Mon, 13 Jun 2022 20:01:54 +0000 (16:01 -0400)
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.service.ts

index 71cc22d..4d4dc28 100644 (file)
@@ -117,6 +117,16 @@ 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'; }
 
index feb0690..26f33a0 100644 (file)
@@ -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<any> {
+
         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<any> {
@@ -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;
             }
         });
     }
-
-
 }