Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implementing (approximately) fulltext search #27

Merged
merged 1 commit into from
Jan 25, 2015
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 57 additions & 12 deletions routes/foods.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,36 +9,81 @@ router.get('/', function(req, res) {
if ( ! query) {
res.json([]);
}
// Save a lowercase version of the query.
var lcQuery = query.toLowerCase();
// Read load foods JSON from file.
fs.readFile('sample_data/foods.json', null, function (err, data) {
if (err) {
res.status(500).send("Error loading locations.");
} else {
var foods = JSON.parse(data);
// Filter by search term.
var filteredFoods = foods.filter(filterSearchResult);
var filteredFoods = filterSearchResult(query, foods);
res.json(filteredFoods);
}
});

/**
* Super simple search: "Does food object contain query string in any of its attributes?"
* TODO: Break query up by word.
* @param food
* @returns {boolean}
* Two-pass, multi-field search. Combines all fields
* into one searchable string, then checks to see if:
* (1) The query appears in order within a single field
* (2) The fields (combined) contain all words of the query.
* Results satisfying (1) are given first, then (2).
*
* @param {string} query
* @param {array} foods
* @returns {array} filteredFoods
*/
function filterSearchResult(food)
function filterSearchResult(query, foods)
{
for(value in food) {
if (food[value].toLowerCase().match(lcQuery)) {
return true;
query = query.trim();

// Prefer matches in order
var bestRegex = new RegExp(RegExp.quote(query), 'i');

// Also accept arbitrarily ordered keywords
var words = query.split(/\W/);
var regex = '';
for (var i = 0; i < words.length; i++) {
if (words[i]) {
regex += '(?=.*' + RegExp.quote(words[i]) + ')';
}
}
return false;
regex = new RegExp(regex, 'i');

var results = [];
var bestResults = [];

// Iterate over each food, concatenate the fields,
// and search against the result.
for (var i = 0; i < foods.length; i++) {
var text = [];
for (var value in foods[i]) {
text.push(foods[i][value]);
}
text = text.join(' | ');

// Results where the query matches a single field,
// in order, are shown first.
if (text.match(bestRegex)) {
bestResults.push(foods[i]);
} else if (text.match(regex)) {
results.push(foods[i]);
}
}

// Return best results first, then the rest.
return bestResults.concat(results);
}

/**
* Escape characters for inclusion in a regular expression.
* @param {string} unescaped
* @return {string} escaped
*/
RegExp.quote = function (string)
{
return string.replace(/[-\\^$*+?.()|[\]{}]/g, "\\$&");
};

});

module.exports = router;