From 4481c44c4bda753a3fba029f3405fee8361c24c2 Mon Sep 17 00:00:00 2001 From: Victor Woeltjen Date: Mon, 17 Aug 2015 11:20:23 -0700 Subject: [PATCH] [Code Style] Refactor search to use prototypes WTD-1482. --- .../src/ElasticsearchSearchProvider.js | 174 +++++++++--------- platform/search/src/GenericSearchProvider.js | 167 ++++++++--------- platform/search/src/SearchAggregator.js | 135 +++++++------- 3 files changed, 239 insertions(+), 237 deletions(-) diff --git a/platform/persistence/elastic/src/ElasticsearchSearchProvider.js b/platform/persistence/elastic/src/ElasticsearchSearchProvider.js index af13628af9..d7dea9b1f0 100644 --- a/platform/persistence/elastic/src/ElasticsearchSearchProvider.js +++ b/platform/persistence/elastic/src/ElasticsearchSearchProvider.js @@ -44,17 +44,52 @@ define( * @param $http Angular's $http service, for working with urls. * @param {ObjectService} objectService the service from which * domain objects can be gotten. - * @param ROOT the constant ELASTIC_ROOT which allows us to + * @param root the constant `ELASTIC_ROOT` which allows us to * interact with ElasticSearch. */ - function ElasticsearchSearchProvider($http, objectService, ROOT) { - - // Add the fuzziness operator to the search term + function ElasticsearchSearchProvider($http, objectService, root) { + this.$http = $http; + this.objectService = objectService; + this.root = root; + } + + /** + * Searches through the filetree for domain objects using a search + * term. This is done through querying elasticsearch. Returns a + * promise for a result object that has the format + * {hits: searchResult[], total: number, timedOut: boolean} + * where a searchResult has the format + * {id: string, object: domainObject, score: number} + * + * Notes: + * * The order of the results is from highest to lowest score, + * as elsaticsearch determines them to be. + * * Uses the fuzziness operator to get more results. + * * More about this search's behavior at + * https://www.elastic.co/guide/en/elasticsearch/reference/current/search-uri-request.html + * + * @param searchTerm The text input that is the query. + * @param timestamp The time at which this function was called. + * This timestamp is used as a unique identifier for this + * query and the corresponding results. + * @param maxResults (optional) The maximum number of results + * that this function should return. + * @param timeout (optional) The time after which the search should + * stop calculations and return partial results. Elasticsearch + * does not guarentee that this timeout will be strictly followed. + */ + ElasticsearchSearchProvider.prototype.query = function query(searchTerm, timestamp, maxResults, timeout) { + var $http = this.$http, + objectService = this.objectService, + root = this.root, + esQuery; + + // Add the fuzziness operator to the search term function addFuzziness(searchTerm, editDistance) { if (!editDistance) { editDistance = ''; } - + return searchTerm.split(' ').map(function (s) { // Don't add fuzziness for quoted strings if (s.indexOf('"') !== -1) { @@ -64,11 +99,11 @@ define( } }).join(' '); } - + // Currently specific to elasticsearch function processSearchTerm(searchTerm) { var spaceIndex; - + // Cut out any extra spaces while (searchTerm.substr(0, 1) === ' ') { searchTerm = searchTerm.substring(1, searchTerm.length); @@ -79,18 +114,18 @@ define( spaceIndex = searchTerm.indexOf(' '); while (spaceIndex !== -1) { searchTerm = searchTerm.substring(0, spaceIndex) + - searchTerm.substring(spaceIndex + 1, searchTerm.length); + searchTerm.substring(spaceIndex + 1, searchTerm.length); spaceIndex = searchTerm.indexOf(' '); } - + // Add fuzziness for completeness searchTerm = addFuzziness(searchTerm); - + return searchTerm; } - - // Processes results from the format that elasticsearch returns to - // a list of searchResult objects, then returns a result object + + // Processes results from the format that elasticsearch returns to + // a list of searchResult objects, then returns a result object // (See documentation for query for object descriptions) function processResults(rawResults, timestamp) { var results = rawResults.data.hits.hits, @@ -99,25 +134,25 @@ define( scores = {}, searchResults = [], i; - + // Get the result objects' IDs for (i = 0; i < resultsLength; i += 1) { ids.push(results[i][ID]); } - + // Get the result objects' scores for (i = 0; i < resultsLength; i += 1) { scores[ids[i]] = results[i][SCORE]; } - + // Get the domain objects from their IDs return objectService.getObjects(ids).then(function (objects) { var j, id; - + for (j = 0; j < resultsLength; j += 1) { id = ids[j]; - + // Include items we can get models for if (objects[id].getModel) { // Format the results as searchResult objects @@ -128,7 +163,7 @@ define( }); } } - + return { hits: searchResults, total: rawResults.data.hits.total, @@ -136,76 +171,43 @@ define( }; }); } - - // For documentation, see query below. - function query(searchTerm, timestamp, maxResults, timeout) { - var esQuery; - - // Check to see if the user provided a maximum - // number of results to display - if (!maxResults) { - // Else, we provide a default value. - maxResults = DEFAULT_MAX_RESULTS; - } - - // If the user input is empty, we want to have no search results. - if (searchTerm !== '' && searchTerm !== undefined) { - // Process the search term - searchTerm = processSearchTerm(searchTerm); - // Create the query to elasticsearch - esQuery = ROOT + "/_search/?q=" + searchTerm + - "&size=" + maxResults; - if (timeout) { - esQuery += "&timeout=" + timeout; - } - // Get the data... - return $http({ - method: "GET", - url: esQuery - }).then(function (rawResults) { - // ...then process the data - return processResults(rawResults, timestamp); - }, function (err) { - // In case of error, return nothing. (To prevent - // infinite loading time.) - return {hits: [], total: 0}; - }); - } else { - return {hits: [], total: 0}; - } + // Check to see if the user provided a maximum + // number of results to display + if (!maxResults) { + // Else, we provide a default value. + maxResults = DEFAULT_MAX_RESULTS; } - - return { - /** - * Searches through the filetree for domain objects using a search - * term. This is done through querying elasticsearch. Returns a - * promise for a result object that has the format - * {hits: searchResult[], total: number, timedOut: boolean} - * where a searchResult has the format - * {id: string, object: domainObject, score: number} - * - * Notes: - * * The order of the results is from highest to lowest score, - * as elsaticsearch determines them to be. - * * Uses the fuzziness operator to get more results. - * * More about this search's behavior at - * https://www.elastic.co/guide/en/elasticsearch/reference/current/search-uri-request.html - * - * @param searchTerm The text input that is the query. - * @param timestamp The time at which this function was called. - * This timestamp is used as a unique identifier for this - * query and the corresponding results. - * @param maxResults (optional) The maximum number of results - * that this function should return. - * @param timeout (optional) The time after which the search should - * stop calculations and return partial results. Elasticsearch - * does not guarentee that this timeout will be strictly followed. - */ - query: query - }; - } + + // If the user input is empty, we want to have no search results. + if (searchTerm !== '' && searchTerm !== undefined) { + // Process the search term + searchTerm = processSearchTerm(searchTerm); + + // Create the query to elasticsearch + esQuery = root + "/_search/?q=" + searchTerm + + "&size=" + maxResults; + if (timeout) { + esQuery += "&timeout=" + timeout; + } + + // Get the data... + return this.$http({ + method: "GET", + url: esQuery + }).then(function (rawResults) { + // ...then process the data + return processResults(rawResults, timestamp); + }, function (err) { + // In case of error, return nothing. (To prevent + // infinite loading time.) + return {hits: [], total: 0}; + }); + } else { + return {hits: [], total: 0}; + } + }; return ElasticsearchSearchProvider; diff --git a/platform/search/src/GenericSearchProvider.js b/platform/search/src/GenericSearchProvider.js index dae2cab9a9..014d8d7fda 100644 --- a/platform/search/src/GenericSearchProvider.js +++ b/platform/search/src/GenericSearchProvider.js @@ -48,10 +48,14 @@ define( * domain objects' IDs. */ function GenericSearchProvider($q, $timeout, objectService, workerService, ROOTS) { - var worker = workerService.run('genericSearchWorker'), - indexed = {}, - pendingQueries = {}; - // pendingQueries is a dictionary with the key value pairs st + var indexed = {}, + pendingQueries = {}, + worker = workerService.run('genericSearchWorker'); + + this.worker = worker; + this.pendingQueries = pendingQueries; + this.$q = $q; + // pendingQueries is a dictionary with the key value pairs st // the key is the timestamp and the value is the promise // Tell the web worker to add a domain object's model to its list of items. @@ -71,20 +75,7 @@ define( } } - // Tell the worker to search for items it has that match this searchInput. - // Takes the searchInput, as well as a max number of results (will return - // less than that if there are fewer matches). - function workerSearch(searchInput, maxResults, timestamp, timeout) { - var message = { - request: 'search', - input: searchInput, - maxNumber: maxResults, - timestamp: timestamp, - timeout: timeout - }; - worker.postMessage(message); - } - + // Handles responses from the web worker. Namely, the results of // a search request. function handleResponse(event) { @@ -120,8 +111,6 @@ define( } } - worker.onmessage = handleResponse; - // Helper function for getItems(). Indexes the tree. function indexItems(nodes) { nodes.forEach(function (node) { @@ -193,75 +182,87 @@ define( indexItems(objects); }); } - - // For documentation, see query below - function query(input, timestamp, maxResults, timeout) { - var terms = [], - searchResults = [], - defer = $q.defer(); - - // If the input is nonempty, do a search - if (input !== '' && input !== undefined) { - - // Allow us to access this promise later to resolve it later - pendingQueries[timestamp] = defer; - - // Check to see if the user provided a maximum - // number of results to display - if (!maxResults) { - // Else, we provide a default value - maxResults = DEFAULT_MAX_RESULTS; - } - // Similarly, check if timeout was provided - if (!timeout) { - timeout = DEFAULT_TIMEOUT; - } - // Send the query to the worker - workerSearch(input, maxResults, timestamp, timeout); + worker.onmessage = handleResponse; - return defer.promise; - } else { - // Otherwise return an empty result - return {hits: [], total: 0}; - } - } - // Index the tree's contents once at the beginning getItems(); - - return { - /** - * Searches through the filetree for domain objects which match - * the search term. This function is to be used as a fallback - * in the case where other search services are not avaliable. - * Returns a promise for a result object that has the format - * {hits: searchResult[], total: number, timedOut: boolean} - * where a searchResult has the format - * {id: string, object: domainObject, score: number} - * - * Notes: - * * The order of the results is not guarenteed. - * * A domain object qualifies as a match for a search input if - * the object's name property contains any of the search terms - * (which are generated by splitting the input at spaces). - * * Scores are higher for matches that have more of the terms - * as substrings. - * - * @param input The text input that is the query. - * @param timestamp The time at which this function was called. - * This timestamp is used as a unique identifier for this - * query and the corresponding results. - * @param maxResults (optional) The maximum number of results - * that this function should return. - * @param timeout (optional) The time after which the search should - * stop calculations and return partial results. - */ - query: query - - }; } + /** + * Searches through the filetree for domain objects which match + * the search term. This function is to be used as a fallback + * in the case where other search services are not avaliable. + * Returns a promise for a result object that has the format + * {hits: searchResult[], total: number, timedOut: boolean} + * where a searchResult has the format + * {id: string, object: domainObject, score: number} + * + * Notes: + * * The order of the results is not guarenteed. + * * A domain object qualifies as a match for a search input if + * the object's name property contains any of the search terms + * (which are generated by splitting the input at spaces). + * * Scores are higher for matches that have more of the terms + * as substrings. + * + * @param input The text input that is the query. + * @param timestamp The time at which this function was called. + * This timestamp is used as a unique identifier for this + * query and the corresponding results. + * @param maxResults (optional) The maximum number of results + * that this function should return. + * @param timeout (optional) The time after which the search should + * stop calculations and return partial results. + */ + GenericSearchProvider.prototype.query = function query(input, timestamp, maxResults, timeout) { + var terms = [], + searchResults = [], + pendingQueries = this.pendingQueries, + worker = this.worker, + defer = this.$q.defer(); + + // Tell the worker to search for items it has that match this searchInput. + // Takes the searchInput, as well as a max number of results (will return + // less than that if there are fewer matches). + function workerSearch(searchInput, maxResults, timestamp, timeout) { + var message = { + request: 'search', + input: searchInput, + maxNumber: maxResults, + timestamp: timestamp, + timeout: timeout + }; + worker.postMessage(message); + } + + // If the input is nonempty, do a search + if (input !== '' && input !== undefined) { + + // Allow us to access this promise later to resolve it later + pendingQueries[timestamp] = defer; + + // Check to see if the user provided a maximum + // number of results to display + if (!maxResults) { + // Else, we provide a default value + maxResults = DEFAULT_MAX_RESULTS; + } + // Similarly, check if timeout was provided + if (!timeout) { + timeout = DEFAULT_TIMEOUT; + } + + // Send the query to the worker + workerSearch(input, maxResults, timestamp, timeout); + + return defer.promise; + } else { + // Otherwise return an empty result + return { hits: [], total: 0 }; + } + }; + return GenericSearchProvider; } diff --git a/platform/search/src/SearchAggregator.js b/platform/search/src/SearchAggregator.js index da267214bf..2324090595 100644 --- a/platform/search/src/SearchAggregator.js +++ b/platform/search/src/SearchAggregator.js @@ -42,33 +42,55 @@ define( * aggregated. */ function SearchAggregator($q, providers) { - - // Remove duplicate objects that have the same ID. Modifies the passed - // array, and returns the number that were removed. + this.$q = $q; + this.providers = providers; + } + + /** + * Sends a query to each of the providers. Returns a promise for + * a result object that has the format + * {hits: searchResult[], total: number, timedOut: boolean} + * where a searchResult has the format + * {id: string, object: domainObject, score: number} + * + * @param inputText The text input that is the query. + * @param maxResults (optional) The maximum number of results + * that this function should return. If not provided, a + * default of 100 will be used. + */ + SearchAggregator.prototype.query = function queryAll(inputText, maxResults) { + var $q = this.$q, + providers = this.providers, + i, + timestamp = Date.now(), + resultPromises = []; + + // Remove duplicate objects that have the same ID. Modifies the passed + // array, and returns the number that were removed. function filterDuplicates(results, total) { var ids = {}, numRemoved = 0, i; - + for (i = 0; i < results.length; i += 1) { if (ids[results[i].id]) { // If this result's ID is already there, remove the object results.splice(i, 1); numRemoved += 1; - - // Reduce loop index because we shortened the array + + // Reduce loop index because we shortened the array i -= 1; } else { - // Otherwise add the ID to the list of the ones we have seen + // Otherwise add the ID to the list of the ones we have seen ids[results[i].id] = true; } } - + return numRemoved; } - + // Order the objects from highest to lowest score in the array. - // Modifies the passed array, as well as returns the modified array. + // Modifies the passed array, as well as returns the modified array. function orderByScore(results) { results.sort(function (a, b) { if (a.score > b.score) { @@ -81,65 +103,42 @@ define( }); return results; } - - // For documentation, see query below. - function queryAll(inputText, maxResults) { - var i, - timestamp = Date.now(), - resultPromises = []; - - if (!maxResults) { - maxResults = DEFAULT_MAX_RESULTS; - } - - // Send the query to all the providers - for (i = 0; i < providers.length; i += 1) { - resultPromises.push( - providers[i].query(inputText, timestamp, maxResults, DEFUALT_TIMEOUT) - ); - } - - // Get promises for results arrays - return $q.all(resultPromises).then(function (resultObjects) { - var results = [], - totalSum = 0, - i; - - // Merge results - for (i = 0; i < resultObjects.length; i += 1) { - results = results.concat(resultObjects[i].hits); - totalSum += resultObjects[i].total; - } - // Order by score first, so that when removing repeats we keep the higher scored ones - orderByScore(results); - totalSum -= filterDuplicates(results, totalSum); - - return { - hits: results, - total: totalSum, - timedOut: resultObjects.some(function (obj) { - return obj.timedOut; - }) - }; - }); + + if (!maxResults) { + maxResults = DEFAULT_MAX_RESULTS; } - - return { - /** - * Sends a query to each of the providers. Returns a promise for - * a result object that has the format - * {hits: searchResult[], total: number, timedOut: boolean} - * where a searchResult has the format - * {id: string, object: domainObject, score: number} - * - * @param inputText The text input that is the query. - * @param maxResults (optional) The maximum number of results - * that this function should return. If not provided, a - * default of 100 will be used. - */ - query: queryAll - }; - } + + // Send the query to all the providers + for (i = 0; i < providers.length; i += 1) { + resultPromises.push( + providers[i].query(inputText, timestamp, maxResults, DEFUALT_TIMEOUT) + ); + } + + // Get promises for results arrays + return $q.all(resultPromises).then(function (resultObjects) { + var results = [], + totalSum = 0, + i; + + // Merge results + for (i = 0; i < resultObjects.length; i += 1) { + results = results.concat(resultObjects[i].hits); + totalSum += resultObjects[i].total; + } + // Order by score first, so that when removing repeats we keep the higher scored ones + orderByScore(results); + totalSum -= filterDuplicates(results, totalSum); + + return { + hits: results, + total: totalSum, + timedOut: resultObjects.some(function (obj) { + return obj.timedOut; + }) + }; + }); + }; return SearchAggregator; }