[Search] Rewrite elasticsearch provider with prototype

Rewrite the elasticsearch provider to use prototypes and clean up the implementation.

Now returns a modelResults object to keep it in line with the general search
provider.
This commit is contained in:
Pete Richards
2015-10-16 16:05:31 -07:00
parent 78e5c0143b
commit a2fce8e56c

View File

@ -24,190 +24,127 @@
/** /**
* Module defining ElasticSearchProvider. Created by shale on 07/16/2015. * Module defining ElasticSearchProvider. Created by shale on 07/16/2015.
*/ */
define( define([
[],
function () {
"use strict";
// JSLint doesn't like underscore-prefixed properties, ], function (
// so hide them here.
var ID = "_id", ) {
SCORE = "_score", "use strict";
DEFAULT_MAX_RESULTS = 100;
var DEFAULT_MAX_RESULTS = 100;
/**
* A search service which searches through domain objects in /**
* the filetree using ElasticSearch. * A search service which searches through domain objects in
* * the filetree using ElasticSearch.
* @constructor *
* @param $http Angular's $http service, for working with urls. * @constructor
* @param {ObjectService} objectService the service from which * @param $http Angular's $http service, for working with urls.
* domain objects can be gotten. * @param {ObjectService} objectService the service from which
* @param ROOT the constant `ELASTIC_ROOT` which allows us to * domain objects can be gotten.
* interact with ElasticSearch. * @param ROOT the constant `ELASTIC_ROOT` which allows us to
*/ * interact with ElasticSearch.
function ElasticSearchProvider($http, objectService, ROOT) { */
this.$http = $http; function ElasticSearchProvider($http, objectService, ROOT) {
this.objectService = objectService; this.$http = $http;
this.root = ROOT; this.objectService = objectService;
this.root = ROOT;
}
/**
* Search for domain objects using elasticsearch as a search provider.
*
* @param {String} searchTerm the term to search by.
* @param {Number} [maxResults] the max numer of results to return.
* @returns {Promise} promise for a modelResults object.
*/
ElasticSearchProvider.prototype.query = function (searchTerm, maxResults) {
var searchUrl = this.root + '/_search/',
params = {},
provider = this;
if (!maxResults) {
maxResults = DEFAULT_MAX_RESULTS;
} }
/** searchTerm = this.cleanTerm(searchTerm);
* Searches through the filetree for domain objects using a search searchTerm = this.fuzzyMatchUnquotedTerms(searchTerm);
* 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.
*/
ElasticSearchProvider.prototype.query = function query(searchTerm, timestamp, maxResults, timeout) {
var $http = this.$http,
objectService = this.objectService,
root = this.root,
esQuery;
function addFuzziness(searchTerm, editDistance) {
if (!editDistance) {
editDistance = '';
}
return searchTerm.split(' ').map(function (s) { params.q = searchTerm;
// Don't add fuzziness for quoted strings params.size = maxResults;
if (s.indexOf('"') !== -1) {
return s;
} else {
return s + '~' + editDistance;
}
}).join(' ');
}
// Currently specific to elasticsearch return this
function processSearchTerm(searchTerm) { .$http({
var spaceIndex; method: "GET",
url: searchUrl,
// Cut out any extra spaces params: params
while (searchTerm.substr(0, 1) === ' ') { })
searchTerm = searchTerm.substring(1, searchTerm.length); .then(function success(succesResponse) {
} return provider.parseResponse(succesResponse);
while (searchTerm.substr(searchTerm.length - 1, 1) === ' ') { }, function error(errorResponse) {
searchTerm = searchTerm.substring(0, searchTerm.length - 1); // Gracefully fail.
} return {
spaceIndex = searchTerm.indexOf(' '); hits: [],
while (spaceIndex !== -1) { total: 0
searchTerm = searchTerm.substring(0, spaceIndex) + };
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
// (See documentation for query for object descriptions)
function processResults(rawResults, timestamp) {
var results = rawResults.data.hits.hits,
resultsLength = results.length,
ids = [],
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
searchResults.push({
id: id,
object: objects[id],
score: scores[id]
});
}
}
return {
hits: searchResults,
total: rawResults.data.hits.total,
timedOut: rawResults.data.timed_out
};
});
}
// Check to see if the user provided a maximum /**
// number of results to display * Clean excess whitespace from a search term and return the cleaned
if (!maxResults) { * version.
// Else, we provide a default value. *
maxResults = DEFAULT_MAX_RESULTS; * @private
} * @param {string} the search term to clean.
* @returns {string} search terms cleaned of excess whitespace.
*/
ElasticSearchProvider.prototype.cleanTerm = function (term) {
return term.trim().replace(/ +/, ' ');
};
// If the user input is empty, we want to have no search results. /**
if (searchTerm !== '' && searchTerm !== undefined) { * Add fuzzy matching markup to search terms that are not quoted.
// Process the search term *
searchTerm = processSearchTerm(searchTerm); * The following:
* hello welcome "to quoted village" have fun
* will become
* hello~ welcome~ "to quoted village" have~ fun~
*
* @private
*/
ElasticSearchProvider.prototype.fuzzyMatchUnquotedTerms = function (query) {
var matchUnquotedSpaces = '\\s+(?=([^"]*"[^"]*")*[^"]*$)',
matcher = new RegExp(matchUnquotedSpaces, 'g');
// Create the query to elasticsearch return query
esQuery = root + "/_search/?q=" + searchTerm + .replace(matcher, '~ ')
"&size=" + maxResults; .replace('"~', '"');
if (timeout) { };
esQuery += "&timeout=" + timeout;
}
// Get the data... /**
return this.$http({ * Parse the response from ElasticSearch and convert it to a
method: "GET", * modelResults object.
url: esQuery *
}).then(function (rawResults) { * @private
// ...then process the data * @param response a ES response object from $http
return processResults(rawResults, timestamp); * @returns modelResults
}, function (err) { */
// In case of error, return nothing. (To prevent ElasticSearchProvider.prototype.parseResponse = function (response) {
// infinite loading time.) var results = response.data.hits.hits,
return {hits: [], total: 0}; searchResults = results.map(function (result) {
}); return {
} else { id: result['_id'],
return {hits: [], total: 0}; model: result['_source'],
} score: result['_score']
};
});
return {
hits: searchResults,
total: response.data.hits.total,
timedOut: response.data.timed_out
}; };
};
return ElasticSearchProvider;
return ElasticSearchProvider; });
}
);