[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,16 +24,14 @@
/** /**
* Module defining ElasticSearchProvider. Created by shale on 07/16/2015. * Module defining ElasticSearchProvider. Created by shale on 07/16/2015.
*/ */
define( define([
[],
function () { ], function (
) {
"use strict"; "use strict";
// JSLint doesn't like underscore-prefixed properties, var DEFAULT_MAX_RESULTS = 100;
// so hide them here.
var ID = "_id",
SCORE = "_score",
DEFAULT_MAX_RESULTS = 100;
/** /**
* A search service which searches through domain objects in * A search service which searches through domain objects in
@ -53,161 +51,100 @@ define(
} }
/** /**
* Searches through the filetree for domain objects using a search * Search for domain objects using elasticsearch as a search provider.
* 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: * @param {String} searchTerm the term to search by.
* * The order of the results is from highest to lowest score, * @param {Number} [maxResults] the max numer of results to return.
* as elsaticsearch determines them to be. * @returns {Promise} promise for a modelResults object.
* * 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) { ElasticSearchProvider.prototype.query = function (searchTerm, maxResults) {
var $http = this.$http, var searchUrl = this.root + '/_search/',
objectService = this.objectService, params = {},
root = this.root, provider = this;
esQuery;
function addFuzziness(searchTerm, editDistance) {
if (!editDistance) {
editDistance = '';
}
return searchTerm.split(' ').map(function (s) {
// Don't add fuzziness for quoted strings
if (s.indexOf('"') !== -1) {
return s;
} else {
return s + '~' + editDistance;
}
}).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);
}
while (searchTerm.substr(searchTerm.length - 1, 1) === ' ') {
searchTerm = searchTerm.substring(0, searchTerm.length - 1);
}
spaceIndex = searchTerm.indexOf(' ');
while (spaceIndex !== -1) {
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
if (!maxResults) { if (!maxResults) {
// Else, we provide a default value.
maxResults = DEFAULT_MAX_RESULTS; maxResults = DEFAULT_MAX_RESULTS;
} }
// If the user input is empty, we want to have no search results. searchTerm = this.cleanTerm(searchTerm);
if (searchTerm !== '' && searchTerm !== undefined) { searchTerm = this.fuzzyMatchUnquotedTerms(searchTerm);
// Process the search term
searchTerm = processSearchTerm(searchTerm);
// Create the query to elasticsearch params.q = searchTerm;
esQuery = root + "/_search/?q=" + searchTerm + params.size = maxResults;
"&size=" + maxResults;
if (timeout) {
esQuery += "&timeout=" + timeout;
}
// Get the data... return this
return this.$http({ .$http({
method: "GET", method: "GET",
url: esQuery url: searchUrl,
}).then(function (rawResults) { params: params
// ...then process the data })
return processResults(rawResults, timestamp); .then(function success(succesResponse) {
}, function (err) { return provider.parseResponse(succesResponse);
// In case of error, return nothing. (To prevent }, function error(errorResponse) {
// infinite loading time.) // Gracefully fail.
return {hits: [], total: 0}; return {
hits: [],
total: 0
};
}); });
} else {
return {hits: [], total: 0};
}
}; };
/**
* Clean excess whitespace from a search term and return the cleaned
* version.
*
* @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(/ +/, ' ');
};
/**
* Add fuzzy matching markup to search terms that are not quoted.
*
* 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');
return query
.replace(matcher, '~ ')
.replace('"~', '"');
};
/**
* Parse the response from ElasticSearch and convert it to a
* modelResults object.
*
* @private
* @param response a ES response object from $http
* @returns modelResults
*/
ElasticSearchProvider.prototype.parseResponse = function (response) {
var results = response.data.hits.hits,
searchResults = results.map(function (result) {
return {
id: result['_id'],
model: result['_source'],
score: result['_score']
};
});
return {
hits: searchResults,
total: response.data.hits.total,
timedOut: response.data.timed_out
};
};
return ElasticSearchProvider; return ElasticSearchProvider;
} });
);