From 10f9d1bb96bdf487052fd040d4bdf62dedf45214 Mon Sep 17 00:00:00 2001 From: Aditya Kanekar Date: Tue, 27 Sep 2022 18:20:14 -0400 Subject: [PATCH 1/6] Added support for ElasticCloud - Added support for Elastic Cloud with esCloudSearcherFactory class - Added support for API Key - Added Elastic Cloud option in searcherFactory - Added esCloudUrlSvc for parsing URL for Elastic Cloud with API Key - Added esCloudSearcherPreprocessor to prepare get and post request --- factories/esCloudSearcherFactory.js | 382 +++++++++++++++++++++ factories/searcherFactory.js | 1 + factories/settingsValidatorFactory.js | 6 +- services/esCloudSearcherPreprocessorSvc.js | 129 +++++++ services/esCloudUrlSvc.js | 162 +++++++++ services/searchSvc.js | 9 +- 6 files changed, 687 insertions(+), 2 deletions(-) create mode 100644 factories/esCloudSearcherFactory.js create mode 100644 services/esCloudSearcherPreprocessorSvc.js create mode 100644 services/esCloudUrlSvc.js diff --git a/factories/esCloudSearcherFactory.js b/factories/esCloudSearcherFactory.js new file mode 100644 index 0000000..d13fd8d --- /dev/null +++ b/factories/esCloudSearcherFactory.js @@ -0,0 +1,382 @@ +'use strict'; + +/*jslint latedef:false*/ + +(function() { + angular.module('o19s.splainer-search') + .factory('EsCloudSearcherFactory', [ + '$http', + '$q', + '$log', + 'EsDocFactory', + 'activeQueries', + 'esCloudSearcherPreprocessorSvc', + 'esCloudUrlSvc', + 'SearcherFactory', + 'transportSvc', + EsCloudSearcherFactory + ]); + + function EsCloudSearcherFactory( + $http, $q, $log, + EsDocFactory, + activeQueries, + esCloudSearcherPreprocessorSvc, esCloudUrlSvc, + SearcherFactory, + transportSvc + ) { + + var Searcher = function(options) { + SearcherFactory.call(this, options, esCloudSearcherPreprocessorSvc); + }; + + Searcher.prototype = Object.create(SearcherFactory.prototype); + Searcher.prototype.constructor = Searcher; // Reset the constructor + + + Searcher.prototype.addDocToGroup = addDocToGroup; + Searcher.prototype.pager = pager; + Searcher.prototype.search = search; + Searcher.prototype.explainOther = explainOther; + Searcher.prototype.explain = explain; + Searcher.prototype.majorVersion = majorVersion; + Searcher.prototype.isTemplateCall = isTemplateCall; + + + function addDocToGroup (groupedBy, group, solrDoc) { + /*jslint validthis:true*/ + var self = this; + + if (!self.grouped.hasOwnProperty(groupedBy)) { + self.grouped[groupedBy] = []; + } + + var found = null; + angular.forEach(self.grouped[groupedBy], function(groupedDocs) { + if (groupedDocs.value === group && !found) { + found = groupedDocs; + } + }); + + if (!found) { + found = {docs:[], value:group}; + self.grouped[groupedBy].push(found); + } + + found.docs.push(solrDoc); + } + + // return a new searcher that will give you + // the next page upon search(). To get the subsequent + // page, call pager on that searcher ad infinidum + function pager () { + /*jslint validthis:true*/ + var self = this; + var pagerArgs = { from: 0, size: self.config.numberOfRows }; + var nextArgs = angular.copy(self.args); + + if (nextArgs.hasOwnProperty('pager') && nextArgs.pager !== undefined) { + pagerArgs = nextArgs.pager; + } else if (self.hasOwnProperty('pagerArgs') && self.pagerArgs !== undefined) { + pagerArgs = self.pagerArgs; + } + + if (pagerArgs.hasOwnProperty('from')) { + pagerArgs.from = parseInt(pagerArgs.from) + pagerArgs.size; + + if (pagerArgs.from >= self.numFound) { + return null; // no more results + } + } else { + pagerArgs.from = pagerArgs.size; + } + + nextArgs.pager = pagerArgs; + var options = { + args: nextArgs, + config: self.config, + fieldList: self.fieldList, + queryText: self.queryText, + type: self.type, + url: self.url, + apiKey: self.apiKey + }; + + var nextSearcher = new Searcher(options); + + return nextSearcher; + } + + // search (execute the query) and produce results + // to the returned future + function search () { + /*jslint validthis:true*/ + var self = this; + var uri = esCloudUrlSvc.parseUrl(self.url); + var apiKey = self.apiKey; + var apiMethod = self.config.apiMethod; + + if ( esCloudUrlSvc.isBulkCall(uri) ) { + apiMethod = 'BULK'; + } + + // Using templates assumes that the _source field is defined + // in the template, not passed in + if (apiMethod === 'GET' && !esCloudUrlSvc.isTemplateCall(uri)) { + var fieldList = (self.fieldList === '*') ? '*' : self.fieldList.join(','); + + if ( 5 <= self.majorVersion() ) { + /*jshint camelcase: false */ + esCloudUrlSvc.setParams(uri, { + _source: fieldList, + }); + } else { + esCloudUrlSvc.setParams(uri, { + _source: fieldList, + }); + } + } + + var url = esCloudUrlSvc.buildUrl(uri); + var transport = transportSvc.getTransport({apiMethod: apiMethod}); + + var queryDslWithPagerArgs = angular.copy(self.queryDsl); + if (self.pagerArgs) { + if (esCloudUrlSvc.isTemplateCall(uri)) { + queryDslWithPagerArgs.params.from = self.pagerArgs.from; + queryDslWithPagerArgs.params.size = self.pagerArgs.size; + } + else { + queryDslWithPagerArgs.from = self.pagerArgs.from; + queryDslWithPagerArgs.size = self.pagerArgs.size; + } + } + + if (esCloudUrlSvc.isTemplateCall(uri)) { + delete queryDslWithPagerArgs._source; + delete queryDslWithPagerArgs.highlight; + } else if (self.config.highlight===false) { + delete queryDslWithPagerArgs.highlight; + } + + self.inError = false; + + var getExplData = function(doc) { + if (doc.hasOwnProperty('_explanation')) { + return doc._explanation; + } + else { + return null; + } + }; + + var getHlData = function(doc) { + if (doc.hasOwnProperty('highlight')) { + return doc.highlight; + } else { + return null; + } + }; + + var getQueryParsingData = function(data) { + if (data.hasOwnProperty('profile')) { + return data.profile; + } + else { + return {}; + } + }; + + var formatError = function(msg) { + var errorMsg = ''; + if (msg) { + if (msg.status >= 400) { + errorMsg = 'HTTP Error: ' + msg.status + ' ' + msg.statusText; + } + if (msg.status > 0) { + if (msg.hasOwnProperty('data') && msg.data) { + + if (msg.data.hasOwnProperty('error')) { + errorMsg += '\n' + JSON.stringify(msg.data.error, null, 2); + } + if (msg.data.hasOwnProperty('_shards')) { + angular.forEach(msg.data._shards.failures, function(failure) { + errorMsg += '\n' + JSON.stringify(failure, null, 2); + }); + } + + } + } + else if (msg.status === -1 || msg.status === 0) { + errorMsg += 'Network Error! (host not found)\n'; + errorMsg += '\n'; + errorMsg += 'or CORS needs to be configured for your Elasticsearch\n'; + errorMsg += '\n'; + errorMsg += 'Enable CORS in elasticsearch.yml:\n'; + errorMsg += '\n'; + errorMsg += 'http.cors.allow-origin: "/https?:\\\\/\\\\/(.*?\\\\.)?(quepid\\\\.com|splainer\\\\.io)/"\n'; + errorMsg += 'http.cors.enabled: true\n'; + } + msg.searchError = errorMsg; + } + return msg; + }; + + // Build URL with params if any + // Eg. without params: /_search + // Eg. with params: /_search?size=5&from=5 + //esCloudUrlSvc.setParams(uri, self.pagerArgs); + + var headers = esCloudUrlSvc.getHeaders(uri, apiKey); + + activeQueries.count++; + return transport.query(url, queryDslWithPagerArgs, headers) + .then(function success(httpConfig) { + var data = httpConfig.data; + activeQueries.count--; + if (data.hits.hasOwnProperty('total') && data.hits.total.hasOwnProperty('value')) { + self.numFound = data.hits.total.value; + } + else { + self.numFound = data.hits.total; + } + self.parsedQueryDetails = getQueryParsingData(data); + + var parseDoc = function(doc, groupedBy, group) { + var explDict = getExplData(doc); + var hlDict = getHlData(doc); + + var options = { + groupedBy: groupedBy, + group: group, + fieldList: self.fieldList, + url: self.url, + explDict: explDict, + hlDict: hlDict, + version: self.majorVersion(), + }; + + return new EsDocFactory(doc, options); + }; + + angular.forEach(data.hits.hits, function(hit) { + var doc = parseDoc(hit); + self.docs.push(doc); + }); + + if ( angular.isDefined(data._shards) && data._shards.failed > 0 ) { + return $q.reject(formatError(httpConfig)); + } + }, function error(msg) { + activeQueries.count--; + self.inError = true; + return $q.reject(formatError(msg)); + }) + .catch(function(response) { + $log.debug('Failed to execute search'); + return $q.reject(response); + }); + } // end of search() + + function explainOther (otherQuery) { + /*jslint validthis:true*/ + var self = this; + + var otherSearcherOptions = { + fieldList: self.fieldList, + url: self.url, + args: self.args, + queryText: otherQuery, + config: { + apiMethod: 'POST', + numberOfRows: self.config.numberOfRows, + version: self.config.version, + }, + type: self.type, + }; + + if ( angular.isDefined(self.pagerArgs) && self.pagerArgs !== null ) { + otherSearcherOptions.args.pager = self.pagerArgs; + } + + var otherSearcher = new Searcher(otherSearcherOptions); + + return otherSearcher.search() + .then(function() { + self.numFound = otherSearcher.numFound; + + var defer = $q.defer(); + var promises = []; + var docs = []; + + angular.forEach(otherSearcher.docs, function(doc) { + var promise = self.explain(doc) + .then(function(parsedDoc) { + docs.push(parsedDoc); + }); + + promises.push(promise); + }); + + $q.all(promises) + .then(function () { + self.docs = docs; + defer.resolve(); + }); + + return defer.promise; + }).catch(function(response) { + $log.debug('Failed to run explainOther'); + return response; + }); + } // end of explainOther() + + function explain(doc) { + /*jslint validthis:true*/ + var self = this; + var uri = esCloudUrlSvc.parseUrl(self.url); + var url = esCloudUrlSvc.buildExplainUrl(uri, doc); + var apiKey = self.apiKey; + var headers = esCloudUrlSvc.getHeaders(uri, apiKey); + + return $http.post(url, { query: self.queryDsl.query }, {headers: headers}) + .then(function(response) { + var explDict = response.data.explanation || null; + + var options = { + fieldList: self.fieldList, + url: self.url, + explDict: explDict, + }; + + return new EsDocFactory(doc, options); + }).catch(function(response) { + $log.debug('Failed to run explain'); + return response; + }); + } // end of explain() + + function majorVersion() { + var self = this; + + if ( angular.isDefined(self.config) && + angular.isDefined(self.config.version) && + self.config.version !== null && + self.config.version !== '' + ) { + return parseInt(self.config.version.split('.')[0]); + } else { + return null; + } + } + + function isTemplateCall() { + var self = this; + + return esCloudUrlSvc.isTemplateCall(esCloudUrlSvc.parseUrl(self.url)); + } + + // Return factory object + return Searcher; + } +})(); diff --git a/factories/searcherFactory.js b/factories/searcherFactory.js index 39b3d10..88b226d 100644 --- a/factories/searcherFactory.js +++ b/factories/searcherFactory.js @@ -18,6 +18,7 @@ self.queryText = options.queryText; self.config = options.config; self.type = options.type; + self.apiKey = options.apiKey; self.docs = []; self.grouped = {}; diff --git a/factories/settingsValidatorFactory.js b/factories/settingsValidatorFactory.js index 087457b..eba2a4d 100644 --- a/factories/settingsValidatorFactory.js +++ b/factories/settingsValidatorFactory.js @@ -18,6 +18,7 @@ self.searchEngine = settings.searchEngine; self.apiMethod = settings.apiMethod; self.version = settings.version; + self.apiKey = settings.apiKey; self.searcher = null; self.fields = []; @@ -47,7 +48,8 @@ version: self.version, apiMethod: self.apiMethod }, - self.searchEngine + self.searchEngine, + self.apiKey ); } @@ -56,6 +58,8 @@ return doc.doc; } else if (self.searchEngine === 'es') { return doc.doc._source; + } else if (self.searchEngine === 'ec') { + return doc.doc._source; } } diff --git a/services/esCloudSearcherPreprocessorSvc.js b/services/esCloudSearcherPreprocessorSvc.js new file mode 100644 index 0000000..f87fc75 --- /dev/null +++ b/services/esCloudSearcherPreprocessorSvc.js @@ -0,0 +1,129 @@ +'use strict'; + +angular.module('o19s.splainer-search') + .service('esCloudSearcherPreprocessorSvc', [ + 'queryTemplateSvc', + 'defaultESConfig', + function esCloudSearcherPreprocessorSvc(queryTemplateSvc, defaultESConfig) { + var self = this; + + // Attributes + // field name since ES 5.0 + self.fieldsParamNames = [ '_source']; + + // Functions + self.prepare = prepare; + + var replaceQuery = function(args, queryText) { + // Allows full override of query if a JSON friendly format is sent in + if (queryText instanceof Object) { + return queryText; + } else { + if (queryText) { + queryText = queryText.replace(/\\/g, '\\\\'); + queryText = queryText.replace(/"/g, '\\\"'); + } + + var replaced = angular.toJson(args, true); + + replaced = queryTemplateSvc.hydrate(replaced, queryText, {encodeURI: false, defaultKw: '\\"\\"'}); + replaced = angular.fromJson(replaced); + + return replaced; + } + }; + + var prepareHighlighting = function (args, fields) { + if ( angular.isDefined(fields) && fields !== null ) { + if ( fields.hasOwnProperty('fields') ) { + fields = fields.fields; + } + + if ( fields.length > 0 ) { + var hl = { fields: {} }; + + angular.forEach(fields, function(fieldName) { + /* + * ES doesn't like highlighting on _id if the query has been filtered on _id using a terms query. + */ + if (fieldName === '_id') { + return; + } + + hl.fields[fieldName] = { }; + }); + + return hl; + } + } + + return { + fields: { + _all: {} + } + }; + }; + + var preparePostRequest = function (searcher) { + var pagerArgs = angular.copy(searcher.args.pager); + if ( angular.isUndefined(pagerArgs) || pagerArgs === null ) { + pagerArgs = {}; + } + + var defaultPagerArgs = { + from: 0, + size: searcher.config.numberOfRows, + }; + + searcher.pagerArgs = angular.merge({}, defaultPagerArgs, pagerArgs); + delete searcher.args.pager; + + var queryDsl = replaceQuery(searcher.args, searcher.queryText); + queryDsl.explain = true; + queryDsl.profile = true; + + if ( angular.isDefined(searcher.fieldList) && searcher.fieldList !== null ) { + angular.forEach(self.fieldsParamNames, function(name) { + queryDsl[name] = searcher.fieldList; + }); + } + + if ( !queryDsl.hasOwnProperty('highlight') ) { + queryDsl.highlight = prepareHighlighting(searcher.args, queryDsl[self.fieldsParamNames[0]]); + } + + searcher.queryDsl = queryDsl; + searcher.apiKey = searcher.apiKey; + }; + + var prepareGetRequest = function (searcher) { + searcher.url = searcher.url + '?q=' + searcher.queryText; + searcher.apiKey = searcher.apiKey; + var pagerArgs = angular.copy(searcher.args.pager); + delete searcher.args.pager; + + if ( angular.isDefined(pagerArgs) && pagerArgs !== null ) { + searcher.url += '&from=' + pagerArgs.from; + searcher.url += '&size=' + pagerArgs.size; + } else { + searcher.url += '&size=' + searcher.config.numberOfRows; + } + }; + + function prepare (searcher) { + if (searcher.config === undefined) { + searcher.config = defaultESConfig; + } else { + // make sure config params that weren't passed through are set from + // the default config object. + searcher.config = angular.merge({}, defaultESConfig, searcher.config); + } + + if ( searcher.config.apiMethod === 'POST') { + preparePostRequest(searcher); + } else if ( searcher.config.apiMethod === 'GET') { + prepareGetRequest(searcher); + } + } + } + ]); diff --git a/services/esCloudUrlSvc.js b/services/esCloudUrlSvc.js new file mode 100644 index 0000000..28d79b4 --- /dev/null +++ b/services/esCloudUrlSvc.js @@ -0,0 +1,162 @@ +'use strict'; + +/*global URI*/ +angular.module('o19s.splainer-search') + .service('esCloudUrlSvc', [ + function esCloudUrlSvc() { + + var self = this; + + self.parseUrl = parseUrl; + self.buildDocUrl = buildDocUrl; + self.buildExplainUrl = buildExplainUrl; + self.buildUrl = buildUrl; + self.buildBaseUrl = buildBaseUrl; + self.setParams = setParams; + self.getHeaders = getHeaders; + self.isBulkCall = isBulkCall; + self.isTemplateCall = isTemplateCall; + + /** + * + * private method fixURLProtocol + * Adds 'http://' to the beginning of the URL if no protocol was specified. + * + */ + var protocolRegex = /^https{0,1}\:/; + function fixURLProtocol(url) { + if (!protocolRegex.test(url)) { + url = 'http://' + url; + } + return url; + } + + /** + * + * Parses an ES URL of the form [http|https]://[host][:port]/[collectionName]/_search + * Splits up the different parts of the URL. + * + */ + function parseUrl (url) { + url = fixURLProtocol(url); + var a = new URI(url); + + var esUri = { + protocol: a.protocol(), + host: a.host(), + pathname: a.pathname(), + query: a.query(), + }; + + if (esUri.pathname.endsWith('/')) { + esUri.pathname = esUri.pathname.substring(0, esUri.pathname.length - 1); + } + + return esUri; + } + + /** + * + * Builds ES URL of the form [protocol]://[host][:port]/[index]/[type]/[id] + * for an ES document. + * + */ + function buildDocUrl (uri, doc) { + var index = doc._index; + var type = doc._type; + var id = doc._id; + + var url = self.buildBaseUrl(uri); + url = url + '/' + index + '/' + type + '/' + id; + + return url; + } + + + /** + * + * Builds ES URL of the form [protocol]://[host][:port]/[index]/[type]/[id]/_explain + * for an ES document. + * + */ + function buildExplainUrl (uri, doc) { + var docUrl = self.buildDocUrl(uri, doc); + + var url = docUrl + '/_explain'; + + return url; + } + + /** + * + * Builds ES URL for a search query. + * Adds any query params if present: /_search?from=10&size=10 + */ + function buildUrl (uri) { + var self = this; + + var url = self.buildBaseUrl(uri); + url = url + uri.pathname; + + // Return original URL if no params to append. + if ( angular.isUndefined(uri.params) && angular.isUndefined(uri.query) ) { + return url; + } + + var paramsAsStrings = []; + + angular.forEach(uri.params, function(value, key) { + paramsAsStrings.push(key + '=' + value); + }); + + if ( angular.isDefined(uri.query) && uri.query !== '' ) { + paramsAsStrings.push(uri.query); + } + + // Return original URL if no params to append. + if ( paramsAsStrings.length === 0 ) { + return url; + } + + var finalUrl = url; + + if (finalUrl.substring(finalUrl.length - 1) === '?') { + finalUrl += paramsAsStrings.join('&'); + } else { + finalUrl += '?' + paramsAsStrings.join('&'); + } + + return finalUrl; + } + + function buildBaseUrl (uri) { + var url = uri.protocol + '://'; + url += (uri.host); + + return url; + } + + function setParams (uri, params) { + uri.params = params; + } + + function getHeaders (uri, apiKey) { + var headers = {}; + + if ( angular.isDefined(apiKey) && uri.apiKey !== '') { + var authorization = apiKey; + headers = { 'Authorization': 'ApiKey ' + authorization }; + } + + return headers; + } + + function isBulkCall (uri) { + return uri.pathname.endsWith('_msearch'); + } + + function isTemplateCall (uri) { + return uri.pathname.endsWith('_search/template'); + } + } + ]); diff --git a/services/searchSvc.js b/services/searchSvc.js index 188ebac..803f1e0 100644 --- a/services/searchSvc.js +++ b/services/searchSvc.js @@ -1,16 +1,20 @@ 'use strict'; +// const { option } = require('grunt'); + // Executes a generic search and returns // a set of generic documents angular.module('o19s.splainer-search') .service('searchSvc', [ 'SolrSearcherFactory', 'EsSearcherFactory', + 'EsCloudSearcherFactory', 'activeQueries', 'defaultSolrConfig', function searchSvc( SolrSearcherFactory, EsSearcherFactory, + EsCloudSearcherFactory, activeQueries, defaultSolrConfig ) { @@ -26,7 +30,7 @@ angular.module('o19s.splainer-search') return angular.copy(defaultSolrConfig); }; - this.createSearcher = function (fieldSpec, url, args, queryText, config, searchEngine) { + this.createSearcher = function (fieldSpec, url, args, queryText, config, searchEngine, apiKey) { if ( searchEngine === undefined ) { searchEngine = 'solr'; } @@ -36,6 +40,7 @@ angular.module('o19s.splainer-search') hlFieldList: fieldSpec.highlightFieldList(), url: url, args: args, + apiKey: apiKey, queryText: queryText, config: config, type: searchEngine @@ -50,6 +55,8 @@ angular.module('o19s.splainer-search') searcher = new SolrSearcherFactory(options); } else if ( searchEngine === 'es') { searcher = new EsSearcherFactory(options); + } else if (searchEngine === 'ec') { + searcher = new EsCloudSearcherFactory(options); } return searcher; From f8a40fd8885c581c501e97302ea1cc00e3ff7ba7 Mon Sep 17 00:00:00 2001 From: Daniel Worley Date: Thu, 6 Oct 2022 15:10:52 -0400 Subject: [PATCH 2/6] Refactor apikey changes to be more generic custom header support --- factories/esCloudSearcherFactory.js | 382 --------------------- factories/esSearcherFactory.js | 4 +- factories/searcherFactory.js | 2 +- factories/settingsValidatorFactory.js | 18 +- services/esCloudSearcherPreprocessorSvc.js | 129 ------- services/esCloudUrlSvc.js | 162 --------- services/esUrlSvc.js | 10 +- services/searchSvc.js | 7 +- 8 files changed, 19 insertions(+), 695 deletions(-) delete mode 100644 factories/esCloudSearcherFactory.js delete mode 100644 services/esCloudSearcherPreprocessorSvc.js delete mode 100644 services/esCloudUrlSvc.js diff --git a/factories/esCloudSearcherFactory.js b/factories/esCloudSearcherFactory.js deleted file mode 100644 index d13fd8d..0000000 --- a/factories/esCloudSearcherFactory.js +++ /dev/null @@ -1,382 +0,0 @@ -'use strict'; - -/*jslint latedef:false*/ - -(function() { - angular.module('o19s.splainer-search') - .factory('EsCloudSearcherFactory', [ - '$http', - '$q', - '$log', - 'EsDocFactory', - 'activeQueries', - 'esCloudSearcherPreprocessorSvc', - 'esCloudUrlSvc', - 'SearcherFactory', - 'transportSvc', - EsCloudSearcherFactory - ]); - - function EsCloudSearcherFactory( - $http, $q, $log, - EsDocFactory, - activeQueries, - esCloudSearcherPreprocessorSvc, esCloudUrlSvc, - SearcherFactory, - transportSvc - ) { - - var Searcher = function(options) { - SearcherFactory.call(this, options, esCloudSearcherPreprocessorSvc); - }; - - Searcher.prototype = Object.create(SearcherFactory.prototype); - Searcher.prototype.constructor = Searcher; // Reset the constructor - - - Searcher.prototype.addDocToGroup = addDocToGroup; - Searcher.prototype.pager = pager; - Searcher.prototype.search = search; - Searcher.prototype.explainOther = explainOther; - Searcher.prototype.explain = explain; - Searcher.prototype.majorVersion = majorVersion; - Searcher.prototype.isTemplateCall = isTemplateCall; - - - function addDocToGroup (groupedBy, group, solrDoc) { - /*jslint validthis:true*/ - var self = this; - - if (!self.grouped.hasOwnProperty(groupedBy)) { - self.grouped[groupedBy] = []; - } - - var found = null; - angular.forEach(self.grouped[groupedBy], function(groupedDocs) { - if (groupedDocs.value === group && !found) { - found = groupedDocs; - } - }); - - if (!found) { - found = {docs:[], value:group}; - self.grouped[groupedBy].push(found); - } - - found.docs.push(solrDoc); - } - - // return a new searcher that will give you - // the next page upon search(). To get the subsequent - // page, call pager on that searcher ad infinidum - function pager () { - /*jslint validthis:true*/ - var self = this; - var pagerArgs = { from: 0, size: self.config.numberOfRows }; - var nextArgs = angular.copy(self.args); - - if (nextArgs.hasOwnProperty('pager') && nextArgs.pager !== undefined) { - pagerArgs = nextArgs.pager; - } else if (self.hasOwnProperty('pagerArgs') && self.pagerArgs !== undefined) { - pagerArgs = self.pagerArgs; - } - - if (pagerArgs.hasOwnProperty('from')) { - pagerArgs.from = parseInt(pagerArgs.from) + pagerArgs.size; - - if (pagerArgs.from >= self.numFound) { - return null; // no more results - } - } else { - pagerArgs.from = pagerArgs.size; - } - - nextArgs.pager = pagerArgs; - var options = { - args: nextArgs, - config: self.config, - fieldList: self.fieldList, - queryText: self.queryText, - type: self.type, - url: self.url, - apiKey: self.apiKey - }; - - var nextSearcher = new Searcher(options); - - return nextSearcher; - } - - // search (execute the query) and produce results - // to the returned future - function search () { - /*jslint validthis:true*/ - var self = this; - var uri = esCloudUrlSvc.parseUrl(self.url); - var apiKey = self.apiKey; - var apiMethod = self.config.apiMethod; - - if ( esCloudUrlSvc.isBulkCall(uri) ) { - apiMethod = 'BULK'; - } - - // Using templates assumes that the _source field is defined - // in the template, not passed in - if (apiMethod === 'GET' && !esCloudUrlSvc.isTemplateCall(uri)) { - var fieldList = (self.fieldList === '*') ? '*' : self.fieldList.join(','); - - if ( 5 <= self.majorVersion() ) { - /*jshint camelcase: false */ - esCloudUrlSvc.setParams(uri, { - _source: fieldList, - }); - } else { - esCloudUrlSvc.setParams(uri, { - _source: fieldList, - }); - } - } - - var url = esCloudUrlSvc.buildUrl(uri); - var transport = transportSvc.getTransport({apiMethod: apiMethod}); - - var queryDslWithPagerArgs = angular.copy(self.queryDsl); - if (self.pagerArgs) { - if (esCloudUrlSvc.isTemplateCall(uri)) { - queryDslWithPagerArgs.params.from = self.pagerArgs.from; - queryDslWithPagerArgs.params.size = self.pagerArgs.size; - } - else { - queryDslWithPagerArgs.from = self.pagerArgs.from; - queryDslWithPagerArgs.size = self.pagerArgs.size; - } - } - - if (esCloudUrlSvc.isTemplateCall(uri)) { - delete queryDslWithPagerArgs._source; - delete queryDslWithPagerArgs.highlight; - } else if (self.config.highlight===false) { - delete queryDslWithPagerArgs.highlight; - } - - self.inError = false; - - var getExplData = function(doc) { - if (doc.hasOwnProperty('_explanation')) { - return doc._explanation; - } - else { - return null; - } - }; - - var getHlData = function(doc) { - if (doc.hasOwnProperty('highlight')) { - return doc.highlight; - } else { - return null; - } - }; - - var getQueryParsingData = function(data) { - if (data.hasOwnProperty('profile')) { - return data.profile; - } - else { - return {}; - } - }; - - var formatError = function(msg) { - var errorMsg = ''; - if (msg) { - if (msg.status >= 400) { - errorMsg = 'HTTP Error: ' + msg.status + ' ' + msg.statusText; - } - if (msg.status > 0) { - if (msg.hasOwnProperty('data') && msg.data) { - - if (msg.data.hasOwnProperty('error')) { - errorMsg += '\n' + JSON.stringify(msg.data.error, null, 2); - } - if (msg.data.hasOwnProperty('_shards')) { - angular.forEach(msg.data._shards.failures, function(failure) { - errorMsg += '\n' + JSON.stringify(failure, null, 2); - }); - } - - } - } - else if (msg.status === -1 || msg.status === 0) { - errorMsg += 'Network Error! (host not found)\n'; - errorMsg += '\n'; - errorMsg += 'or CORS needs to be configured for your Elasticsearch\n'; - errorMsg += '\n'; - errorMsg += 'Enable CORS in elasticsearch.yml:\n'; - errorMsg += '\n'; - errorMsg += 'http.cors.allow-origin: "/https?:\\\\/\\\\/(.*?\\\\.)?(quepid\\\\.com|splainer\\\\.io)/"\n'; - errorMsg += 'http.cors.enabled: true\n'; - } - msg.searchError = errorMsg; - } - return msg; - }; - - // Build URL with params if any - // Eg. without params: /_search - // Eg. with params: /_search?size=5&from=5 - //esCloudUrlSvc.setParams(uri, self.pagerArgs); - - var headers = esCloudUrlSvc.getHeaders(uri, apiKey); - - activeQueries.count++; - return transport.query(url, queryDslWithPagerArgs, headers) - .then(function success(httpConfig) { - var data = httpConfig.data; - activeQueries.count--; - if (data.hits.hasOwnProperty('total') && data.hits.total.hasOwnProperty('value')) { - self.numFound = data.hits.total.value; - } - else { - self.numFound = data.hits.total; - } - self.parsedQueryDetails = getQueryParsingData(data); - - var parseDoc = function(doc, groupedBy, group) { - var explDict = getExplData(doc); - var hlDict = getHlData(doc); - - var options = { - groupedBy: groupedBy, - group: group, - fieldList: self.fieldList, - url: self.url, - explDict: explDict, - hlDict: hlDict, - version: self.majorVersion(), - }; - - return new EsDocFactory(doc, options); - }; - - angular.forEach(data.hits.hits, function(hit) { - var doc = parseDoc(hit); - self.docs.push(doc); - }); - - if ( angular.isDefined(data._shards) && data._shards.failed > 0 ) { - return $q.reject(formatError(httpConfig)); - } - }, function error(msg) { - activeQueries.count--; - self.inError = true; - return $q.reject(formatError(msg)); - }) - .catch(function(response) { - $log.debug('Failed to execute search'); - return $q.reject(response); - }); - } // end of search() - - function explainOther (otherQuery) { - /*jslint validthis:true*/ - var self = this; - - var otherSearcherOptions = { - fieldList: self.fieldList, - url: self.url, - args: self.args, - queryText: otherQuery, - config: { - apiMethod: 'POST', - numberOfRows: self.config.numberOfRows, - version: self.config.version, - }, - type: self.type, - }; - - if ( angular.isDefined(self.pagerArgs) && self.pagerArgs !== null ) { - otherSearcherOptions.args.pager = self.pagerArgs; - } - - var otherSearcher = new Searcher(otherSearcherOptions); - - return otherSearcher.search() - .then(function() { - self.numFound = otherSearcher.numFound; - - var defer = $q.defer(); - var promises = []; - var docs = []; - - angular.forEach(otherSearcher.docs, function(doc) { - var promise = self.explain(doc) - .then(function(parsedDoc) { - docs.push(parsedDoc); - }); - - promises.push(promise); - }); - - $q.all(promises) - .then(function () { - self.docs = docs; - defer.resolve(); - }); - - return defer.promise; - }).catch(function(response) { - $log.debug('Failed to run explainOther'); - return response; - }); - } // end of explainOther() - - function explain(doc) { - /*jslint validthis:true*/ - var self = this; - var uri = esCloudUrlSvc.parseUrl(self.url); - var url = esCloudUrlSvc.buildExplainUrl(uri, doc); - var apiKey = self.apiKey; - var headers = esCloudUrlSvc.getHeaders(uri, apiKey); - - return $http.post(url, { query: self.queryDsl.query }, {headers: headers}) - .then(function(response) { - var explDict = response.data.explanation || null; - - var options = { - fieldList: self.fieldList, - url: self.url, - explDict: explDict, - }; - - return new EsDocFactory(doc, options); - }).catch(function(response) { - $log.debug('Failed to run explain'); - return response; - }); - } // end of explain() - - function majorVersion() { - var self = this; - - if ( angular.isDefined(self.config) && - angular.isDefined(self.config.version) && - self.config.version !== null && - self.config.version !== '' - ) { - return parseInt(self.config.version.split('.')[0]); - } else { - return null; - } - } - - function isTemplateCall() { - var self = this; - - return esCloudUrlSvc.isTemplateCall(esCloudUrlSvc.parseUrl(self.url)); - } - - // Return factory object - return Searcher; - } -})(); diff --git a/factories/esSearcherFactory.js b/factories/esSearcherFactory.js index fa32d34..9c0aec4 100644 --- a/factories/esSearcherFactory.js +++ b/factories/esSearcherFactory.js @@ -225,7 +225,7 @@ // Eg. with params: /_search?size=5&from=5 //esUrlSvc.setParams(uri, self.pagerArgs); - var headers = esUrlSvc.getHeaders(uri); + var headers = esUrlSvc.getHeaders(uri, self.config.customHeaders); activeQueries.count++; return transport.query(url, queryDslWithPagerArgs, headers) @@ -334,7 +334,7 @@ var self = this; var uri = esUrlSvc.parseUrl(self.url); var url = esUrlSvc.buildExplainUrl(uri, doc); - var headers = esUrlSvc.getHeaders(uri); + var headers = esUrlSvc.getHeaders(uri, self.config.customHeaders); return $http.post(url, { query: self.queryDsl.query }, {headers: headers}) .then(function(response) { diff --git a/factories/searcherFactory.js b/factories/searcherFactory.js index 88b226d..722e823 100644 --- a/factories/searcherFactory.js +++ b/factories/searcherFactory.js @@ -18,7 +18,7 @@ self.queryText = options.queryText; self.config = options.config; self.type = options.type; - self.apiKey = options.apiKey; + self.customHeaders = options.customHeaders; self.docs = []; self.grouped = {}; diff --git a/factories/settingsValidatorFactory.js b/factories/settingsValidatorFactory.js index 5185bf4..9279c43 100644 --- a/factories/settingsValidatorFactory.js +++ b/factories/settingsValidatorFactory.js @@ -14,11 +14,11 @@ var Validator = function(settings) { var self = this; - self.searchUrl = settings.searchUrl; - self.searchEngine = settings.searchEngine; - self.apiMethod = settings.apiMethod; - self.version = settings.version; - self.apiKey = settings.apiKey; + self.searchUrl = settings.searchUrl; + self.searchEngine = settings.searchEngine; + self.apiMethod = settings.apiMethod; + self.version = settings.version; + self.customHeaders = settings.customHeaders; self.searcher = null; self.fields = []; @@ -46,10 +46,10 @@ '', { version: self.version, - apiMethod: self.apiMethod + apiMethod: self.apiMethod, + customHeaders: self.customHeaders }, - self.searchEngine, - self.apiKey + self.searchEngine ); } @@ -58,8 +58,6 @@ return doc.doc; } else if (self.searchEngine === 'es' || self.searchEngine === 'os') { return doc.doc._source; - } else if (self.searchEngine === 'ec') { - return doc.doc._source; } } diff --git a/services/esCloudSearcherPreprocessorSvc.js b/services/esCloudSearcherPreprocessorSvc.js deleted file mode 100644 index f87fc75..0000000 --- a/services/esCloudSearcherPreprocessorSvc.js +++ /dev/null @@ -1,129 +0,0 @@ -'use strict'; - -angular.module('o19s.splainer-search') - .service('esCloudSearcherPreprocessorSvc', [ - 'queryTemplateSvc', - 'defaultESConfig', - function esCloudSearcherPreprocessorSvc(queryTemplateSvc, defaultESConfig) { - var self = this; - - // Attributes - // field name since ES 5.0 - self.fieldsParamNames = [ '_source']; - - // Functions - self.prepare = prepare; - - var replaceQuery = function(args, queryText) { - // Allows full override of query if a JSON friendly format is sent in - if (queryText instanceof Object) { - return queryText; - } else { - if (queryText) { - queryText = queryText.replace(/\\/g, '\\\\'); - queryText = queryText.replace(/"/g, '\\\"'); - } - - var replaced = angular.toJson(args, true); - - replaced = queryTemplateSvc.hydrate(replaced, queryText, {encodeURI: false, defaultKw: '\\"\\"'}); - replaced = angular.fromJson(replaced); - - return replaced; - } - }; - - var prepareHighlighting = function (args, fields) { - if ( angular.isDefined(fields) && fields !== null ) { - if ( fields.hasOwnProperty('fields') ) { - fields = fields.fields; - } - - if ( fields.length > 0 ) { - var hl = { fields: {} }; - - angular.forEach(fields, function(fieldName) { - /* - * ES doesn't like highlighting on _id if the query has been filtered on _id using a terms query. - */ - if (fieldName === '_id') { - return; - } - - hl.fields[fieldName] = { }; - }); - - return hl; - } - } - - return { - fields: { - _all: {} - } - }; - }; - - var preparePostRequest = function (searcher) { - var pagerArgs = angular.copy(searcher.args.pager); - if ( angular.isUndefined(pagerArgs) || pagerArgs === null ) { - pagerArgs = {}; - } - - var defaultPagerArgs = { - from: 0, - size: searcher.config.numberOfRows, - }; - - searcher.pagerArgs = angular.merge({}, defaultPagerArgs, pagerArgs); - delete searcher.args.pager; - - var queryDsl = replaceQuery(searcher.args, searcher.queryText); - queryDsl.explain = true; - queryDsl.profile = true; - - if ( angular.isDefined(searcher.fieldList) && searcher.fieldList !== null ) { - angular.forEach(self.fieldsParamNames, function(name) { - queryDsl[name] = searcher.fieldList; - }); - } - - if ( !queryDsl.hasOwnProperty('highlight') ) { - queryDsl.highlight = prepareHighlighting(searcher.args, queryDsl[self.fieldsParamNames[0]]); - } - - searcher.queryDsl = queryDsl; - searcher.apiKey = searcher.apiKey; - }; - - var prepareGetRequest = function (searcher) { - searcher.url = searcher.url + '?q=' + searcher.queryText; - searcher.apiKey = searcher.apiKey; - var pagerArgs = angular.copy(searcher.args.pager); - delete searcher.args.pager; - - if ( angular.isDefined(pagerArgs) && pagerArgs !== null ) { - searcher.url += '&from=' + pagerArgs.from; - searcher.url += '&size=' + pagerArgs.size; - } else { - searcher.url += '&size=' + searcher.config.numberOfRows; - } - }; - - function prepare (searcher) { - if (searcher.config === undefined) { - searcher.config = defaultESConfig; - } else { - // make sure config params that weren't passed through are set from - // the default config object. - searcher.config = angular.merge({}, defaultESConfig, searcher.config); - } - - if ( searcher.config.apiMethod === 'POST') { - preparePostRequest(searcher); - } else if ( searcher.config.apiMethod === 'GET') { - prepareGetRequest(searcher); - } - } - } - ]); diff --git a/services/esCloudUrlSvc.js b/services/esCloudUrlSvc.js deleted file mode 100644 index 28d79b4..0000000 --- a/services/esCloudUrlSvc.js +++ /dev/null @@ -1,162 +0,0 @@ -'use strict'; - -/*global URI*/ -angular.module('o19s.splainer-search') - .service('esCloudUrlSvc', [ - function esCloudUrlSvc() { - - var self = this; - - self.parseUrl = parseUrl; - self.buildDocUrl = buildDocUrl; - self.buildExplainUrl = buildExplainUrl; - self.buildUrl = buildUrl; - self.buildBaseUrl = buildBaseUrl; - self.setParams = setParams; - self.getHeaders = getHeaders; - self.isBulkCall = isBulkCall; - self.isTemplateCall = isTemplateCall; - - /** - * - * private method fixURLProtocol - * Adds 'http://' to the beginning of the URL if no protocol was specified. - * - */ - var protocolRegex = /^https{0,1}\:/; - function fixURLProtocol(url) { - if (!protocolRegex.test(url)) { - url = 'http://' + url; - } - return url; - } - - /** - * - * Parses an ES URL of the form [http|https]://[host][:port]/[collectionName]/_search - * Splits up the different parts of the URL. - * - */ - function parseUrl (url) { - url = fixURLProtocol(url); - var a = new URI(url); - - var esUri = { - protocol: a.protocol(), - host: a.host(), - pathname: a.pathname(), - query: a.query(), - }; - - if (esUri.pathname.endsWith('/')) { - esUri.pathname = esUri.pathname.substring(0, esUri.pathname.length - 1); - } - - return esUri; - } - - /** - * - * Builds ES URL of the form [protocol]://[host][:port]/[index]/[type]/[id] - * for an ES document. - * - */ - function buildDocUrl (uri, doc) { - var index = doc._index; - var type = doc._type; - var id = doc._id; - - var url = self.buildBaseUrl(uri); - url = url + '/' + index + '/' + type + '/' + id; - - return url; - } - - - /** - * - * Builds ES URL of the form [protocol]://[host][:port]/[index]/[type]/[id]/_explain - * for an ES document. - * - */ - function buildExplainUrl (uri, doc) { - var docUrl = self.buildDocUrl(uri, doc); - - var url = docUrl + '/_explain'; - - return url; - } - - /** - * - * Builds ES URL for a search query. - * Adds any query params if present: /_search?from=10&size=10 - */ - function buildUrl (uri) { - var self = this; - - var url = self.buildBaseUrl(uri); - url = url + uri.pathname; - - // Return original URL if no params to append. - if ( angular.isUndefined(uri.params) && angular.isUndefined(uri.query) ) { - return url; - } - - var paramsAsStrings = []; - - angular.forEach(uri.params, function(value, key) { - paramsAsStrings.push(key + '=' + value); - }); - - if ( angular.isDefined(uri.query) && uri.query !== '' ) { - paramsAsStrings.push(uri.query); - } - - // Return original URL if no params to append. - if ( paramsAsStrings.length === 0 ) { - return url; - } - - var finalUrl = url; - - if (finalUrl.substring(finalUrl.length - 1) === '?') { - finalUrl += paramsAsStrings.join('&'); - } else { - finalUrl += '?' + paramsAsStrings.join('&'); - } - - return finalUrl; - } - - function buildBaseUrl (uri) { - var url = uri.protocol + '://'; - url += (uri.host); - - return url; - } - - function setParams (uri, params) { - uri.params = params; - } - - function getHeaders (uri, apiKey) { - var headers = {}; - - if ( angular.isDefined(apiKey) && uri.apiKey !== '') { - var authorization = apiKey; - headers = { 'Authorization': 'ApiKey ' + authorization }; - } - - return headers; - } - - function isBulkCall (uri) { - return uri.pathname.endsWith('_msearch'); - } - - function isTemplateCall (uri) { - return uri.pathname.endsWith('_search/template'); - } - } - ]); diff --git a/services/esUrlSvc.js b/services/esUrlSvc.js index 163c8ba..24b47ae 100644 --- a/services/esUrlSvc.js +++ b/services/esUrlSvc.js @@ -142,10 +142,14 @@ angular.module('o19s.splainer-search') uri.params = params; } - function getHeaders (uri) { + function getHeaders (uri, customHeaders) { var headers = {}; + customHeaders = customHeaders || ''; - if ( angular.isDefined(uri.username) && uri.username !== '' && + if (customHeaders.length > 0) { + // TODO: Validate before saving? Or throw exception when this is called + headers = JSON.parse(customHeaders); + } else if ( angular.isDefined(uri.username) && uri.username !== '' && angular.isDefined(uri.password) && uri.password !== '') { var authorization = 'Basic ' + btoa(uri.username + ':' + uri.password); headers = { 'Authorization': authorization }; @@ -157,7 +161,7 @@ angular.module('o19s.splainer-search') function isBulkCall (uri) { return uri.pathname.endsWith('_msearch'); } - + function isTemplateCall (uri) { return uri.pathname.endsWith('_search/template'); } diff --git a/services/searchSvc.js b/services/searchSvc.js index dc37330..403ff92 100644 --- a/services/searchSvc.js +++ b/services/searchSvc.js @@ -8,13 +8,11 @@ angular.module('o19s.splainer-search') .service('searchSvc', [ 'SolrSearcherFactory', 'EsSearcherFactory', - 'EsCloudSearcherFactory', 'activeQueries', 'defaultSolrConfig', function searchSvc( SolrSearcherFactory, EsSearcherFactory, - EsCloudSearcherFactory, activeQueries, defaultSolrConfig ) { @@ -30,7 +28,7 @@ angular.module('o19s.splainer-search') return angular.copy(defaultSolrConfig); }; - this.createSearcher = function (fieldSpec, url, args, queryText, config, searchEngine, apiKey) { + this.createSearcher = function (fieldSpec, url, args, queryText, config, searchEngine) { if ( searchEngine === undefined ) { searchEngine = 'solr'; } @@ -40,7 +38,6 @@ angular.module('o19s.splainer-search') hlFieldList: fieldSpec.highlightFieldList(), url: url, args: args, - apiKey: apiKey, queryText: queryText, config: config, type: searchEngine @@ -55,8 +52,6 @@ angular.module('o19s.splainer-search') searcher = new SolrSearcherFactory(options); } else if ( searchEngine === 'es') { searcher = new EsSearcherFactory(options); - } else if (searchEngine === 'ec') { - searcher = new EsCloudSearcherFactory(options); } else if ( searchEngine === 'os') { searcher = new EsSearcherFactory(options); } From a6c1a4041934d0c8afcec6a24760246903c249db Mon Sep 17 00:00:00 2001 From: Daniel Worley Date: Fri, 7 Oct 2022 16:47:28 -0400 Subject: [PATCH 3/6] Part 1 of fix for explain other --- factories/esSearcherFactory.js | 1 + 1 file changed, 1 insertion(+) diff --git a/factories/esSearcherFactory.js b/factories/esSearcherFactory.js index 9c0aec4..ea9ad31 100644 --- a/factories/esSearcherFactory.js +++ b/factories/esSearcherFactory.js @@ -287,6 +287,7 @@ queryText: otherQuery, config: { apiMethod: 'POST', + customHeaders: self.config.customHeaders, numberOfRows: self.config.numberOfRows, version: self.config.version, }, From 0ada699c7dce3604d67bfb80c74c6e81e38f217b Mon Sep 17 00:00:00 2001 From: Daniel Worley Date: Fri, 7 Oct 2022 17:06:09 -0400 Subject: [PATCH 4/6] Fix explain issues on newer versions of ES --- services/esUrlSvc.js | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/services/esUrlSvc.js b/services/esUrlSvc.js index 24b47ae..b521657 100644 --- a/services/esUrlSvc.js +++ b/services/esUrlSvc.js @@ -63,13 +63,18 @@ angular.module('o19s.splainer-search') * for an ES document. * */ - function buildDocUrl (uri, doc) { + function buildDocUrl (uri, doc, addExplain) { var index = doc._index; var type = doc._type; var id = doc._id; var url = self.buildBaseUrl(uri); - url = url + '/' + index + '/' + type + '/' + id; + + if (type) { + url = url + '/' + index + '/' + type + '/' + id + (addExplain ? '/_explain' : ''); + } else { + url = url + '/' + index + '/' + (addExplain ? '_explain/' : '') + id; + } return url; } @@ -80,13 +85,12 @@ angular.module('o19s.splainer-search') * Builds ES URL of the form [protocol]://[host][:port]/[index]/[type]/[id]/_explain * for an ES document. * + * For newer versions of ES the format is as doc types are deprecated: + * [protocol]://[host][:port]/[index]/_explain/[id] + * */ function buildExplainUrl (uri, doc) { - var docUrl = self.buildDocUrl(uri, doc); - - var url = docUrl + '/_explain'; - - return url; + return buildDocUrl(uri, doc, true); } /** From cca1421d155450afd1c02f16d8ac2b441948c018 Mon Sep 17 00:00:00 2001 From: Daniel Worley Date: Thu, 13 Oct 2022 09:41:11 -0400 Subject: [PATCH 5/6] Cleanup --- services/esUrlSvc.js | 2 +- services/searchSvc.js | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/services/esUrlSvc.js b/services/esUrlSvc.js index b521657..2067155 100644 --- a/services/esUrlSvc.js +++ b/services/esUrlSvc.js @@ -85,7 +85,7 @@ angular.module('o19s.splainer-search') * Builds ES URL of the form [protocol]://[host][:port]/[index]/[type]/[id]/_explain * for an ES document. * - * For newer versions of ES the format is as doc types are deprecated: + * For newer versions of ES the format has changed as doc types are deprecated: * [protocol]://[host][:port]/[index]/_explain/[id] * */ diff --git a/services/searchSvc.js b/services/searchSvc.js index 403ff92..cb9d21e 100644 --- a/services/searchSvc.js +++ b/services/searchSvc.js @@ -1,7 +1,5 @@ 'use strict'; -// const { option } = require('grunt'); - // Executes a generic search and returns // a set of generic documents angular.module('o19s.splainer-search') From e827721ea5053e2e2db60a3a26c61a614468ffac Mon Sep 17 00:00:00 2001 From: Daniel Worley Date: Thu, 13 Oct 2022 09:42:35 -0400 Subject: [PATCH 6/6] 2.20.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d549930..d7ee61a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "splainer-search", - "version": "2.19.0", + "version": "2.20.0", "main": "splainer-search.js", "authors": [ "Doug Turnbull ",