2014-09-17 07:39:46 +00:00
|
|
|
'use strict';
|
|
|
|
|
|
|
|
var _ = require('underscore');
|
|
|
|
var mysql = require('mysql');
|
|
|
|
var connection = null;
|
|
|
|
|
|
|
|
|
|
|
|
function innerProduct(values1, values2) {
|
|
|
|
var result = 0.0;
|
|
|
|
|
|
|
|
for (var feature in values1) {
|
|
|
|
result += values1[feature] * (values2[feature] || 0.0);
|
|
|
|
}
|
|
|
|
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
function scale(values, factor) {
|
|
|
|
var result = {};
|
|
|
|
|
|
|
|
for (var feature in values) {
|
|
|
|
result[feature] = values[feature] * factor;
|
|
|
|
}
|
|
|
|
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
2014-09-18 14:35:55 +00:00
|
|
|
function add(values1, values2) {
|
2014-09-18 08:22:57 +00:00
|
|
|
var result = {};
|
|
|
|
|
|
|
|
for (var feature in values1) {
|
|
|
|
result[feature] = values1[feature] + (values2[feature] || 0.0);
|
|
|
|
}
|
|
|
|
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
2014-09-19 00:38:58 +00:00
|
|
|
function combine(dict, params) {
|
|
|
|
var result = {};
|
2014-09-17 07:39:46 +00:00
|
|
|
|
2014-09-19 00:38:58 +00:00
|
|
|
for (var key in params) {
|
|
|
|
var values = scale(dict[key], params[key]);
|
|
|
|
result = add(values, result);
|
2014-09-17 07:39:46 +00:00
|
|
|
}
|
|
|
|
|
2014-09-19 00:58:30 +00:00
|
|
|
for (var key in result) {
|
|
|
|
var value = result[key];
|
|
|
|
value = Math.min(1.0, Math.max(-1.0, value));
|
|
|
|
result[key] = value;
|
|
|
|
}
|
|
|
|
|
2014-09-19 00:38:58 +00:00
|
|
|
return result;
|
2014-09-17 07:39:46 +00:00
|
|
|
}
|
|
|
|
|
2014-09-19 00:38:58 +00:00
|
|
|
function walkRecords(data, searchParams, minScore, callback) {
|
|
|
|
var features = combine(data.keywords, searchParams);
|
2014-09-17 07:39:46 +00:00
|
|
|
|
|
|
|
for (var i = 0, count = data.records.length; i < count; ++i) {
|
|
|
|
var record = data.records[i];
|
2014-09-19 00:38:58 +00:00
|
|
|
var score = innerProduct(features, record.rating);
|
2014-09-17 07:39:46 +00:00
|
|
|
|
|
|
|
if (score >= minScore) {
|
2014-09-19 00:38:58 +00:00
|
|
|
callback(record, score);
|
2014-09-17 07:39:46 +00:00
|
|
|
}
|
|
|
|
}
|
2014-09-19 00:58:30 +00:00
|
|
|
|
|
|
|
return features;
|
2014-09-19 00:38:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
function countRecords(data, searchParams, minScore) {
|
|
|
|
var count = 0;
|
|
|
|
walkRecords(data, searchParams, minScore, function(record, score) {
|
|
|
|
++count;
|
|
|
|
});
|
|
|
|
|
|
|
|
return count;
|
|
|
|
}
|
|
|
|
|
|
|
|
function findRecords(data, searchParams, minScore) {
|
|
|
|
var results = [];
|
|
|
|
walkRecords(data, searchParams, minScore, function(record, score) {
|
|
|
|
results.push({
|
|
|
|
name: record.name,
|
|
|
|
url: 'http://www.tripadvisor.com' + record.relativeUrl,
|
|
|
|
score: score
|
|
|
|
});
|
|
|
|
});
|
2014-09-17 07:39:46 +00:00
|
|
|
|
|
|
|
results.sort(function(a, b) {
|
|
|
|
return b.score - a.score;
|
|
|
|
});
|
|
|
|
|
|
|
|
return results;
|
|
|
|
}
|
|
|
|
|
|
|
|
function step(range, steps, callback) {
|
|
|
|
var stepSize = (range.max - range.min) / steps;
|
|
|
|
|
|
|
|
for (var i = 0; i < steps; ++i) {
|
|
|
|
var stepMax = range.max - stepSize * i;
|
|
|
|
var stepMin = stepMax - stepSize;
|
|
|
|
var stepMid = (stepMin + stepMax) / 2;
|
|
|
|
|
|
|
|
callback(stepMid);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function project(data, searchParams, minScore, keyword, range, steps) {
|
|
|
|
var testParams = _.clone(searchParams);
|
|
|
|
var results = [];
|
|
|
|
|
|
|
|
step(range, steps, function(position) {
|
|
|
|
testParams[keyword] = position;
|
|
|
|
results.push({
|
|
|
|
sample: position,
|
|
|
|
count: countRecords(data, testParams, minScore)
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
return results;
|
|
|
|
}
|
|
|
|
|
|
|
|
function buildHints(data, searchParams, minScore, keyword, range, steps) {
|
|
|
|
var projection = project(
|
|
|
|
data,
|
|
|
|
searchParams,
|
|
|
|
minScore,
|
|
|
|
keyword,
|
|
|
|
range,
|
|
|
|
steps
|
|
|
|
);
|
|
|
|
|
|
|
|
var hints = [];
|
|
|
|
_.each(projection, function(result) {
|
|
|
|
hints.push({
|
|
|
|
sample: result.sample,
|
|
|
|
count: result.count
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
return hints;
|
|
|
|
}
|
|
|
|
|
|
|
|
function loadDb(params) {
|
|
|
|
connection = mysql.createConnection(params);
|
|
|
|
}
|
|
|
|
|
2014-09-18 08:22:57 +00:00
|
|
|
function addKeyword(query, callback) {
|
|
|
|
getKeywords(function(keywords) {
|
2014-09-19 06:08:09 +00:00
|
|
|
var keyword = query.keyword.toLowerCase();
|
2014-09-19 00:38:58 +00:00
|
|
|
var features = combine(keywords, query.params);
|
2014-09-19 06:08:09 +00:00
|
|
|
|
2014-09-18 12:00:46 +00:00
|
|
|
var values = [
|
2014-09-19 06:08:09 +00:00
|
|
|
keyword,
|
2014-09-19 00:38:58 +00:00
|
|
|
features.food || 0.0,
|
|
|
|
features.service || 0.0,
|
|
|
|
features.value || 0.0,
|
|
|
|
features.atmosphere || 0.0
|
2014-09-18 12:00:46 +00:00
|
|
|
];
|
|
|
|
|
2014-09-18 08:22:57 +00:00
|
|
|
connection.query('INSERT INTO keywords VALUES(?, ?, ?, ?, ?)', values, function(err) {
|
|
|
|
callback({
|
2014-09-19 06:08:09 +00:00
|
|
|
keyword: keyword,
|
2014-09-18 08:22:57 +00:00
|
|
|
success: err === null
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
function removeKeyword(query, callback) {
|
2014-09-20 09:31:39 +00:00
|
|
|
connection.query('DELETE FROM keywords WHERE name=? AND name NOT IN (SELECT name FROM presets)', [query.keyword], function(err, fields) {
|
2014-09-18 08:22:57 +00:00
|
|
|
callback({
|
|
|
|
keyword: query.keyword,
|
2014-09-20 09:31:39 +00:00
|
|
|
success: err === null && fields.affectedRows > 0
|
2014-09-18 08:22:57 +00:00
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2014-09-17 07:39:46 +00:00
|
|
|
function getKeywords(callback) {
|
|
|
|
connection.query('SELECT * FROM keywords', function(err, rows) {
|
|
|
|
if (err) {
|
|
|
|
throw err;
|
|
|
|
}
|
|
|
|
|
|
|
|
var keywords = {};
|
|
|
|
for (var i = 0, count = rows.length; i < count; ++i) {
|
|
|
|
var row = rows[i];
|
|
|
|
keywords[row.name] = {
|
|
|
|
food: row.food,
|
|
|
|
service: row.service,
|
|
|
|
value: row.value,
|
|
|
|
atmosphere: row.atmosphere
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
callback(keywords);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
function getRecords(callback) {
|
|
|
|
connection.query('SELECT * FROM reviews', function(err, rows) {
|
|
|
|
if (err) {
|
|
|
|
throw err;
|
|
|
|
}
|
|
|
|
|
|
|
|
var records = _.map(rows, function(row) {
|
|
|
|
return {
|
|
|
|
name: row.name,
|
|
|
|
relativeUrl: row.url,
|
|
|
|
rating: {
|
|
|
|
food: row.food,
|
|
|
|
service: row.service,
|
|
|
|
value: row.value,
|
|
|
|
atmosphere: row.atmosphere
|
|
|
|
}
|
|
|
|
};
|
|
|
|
});
|
|
|
|
|
|
|
|
callback(records);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
function getData(callback) {
|
|
|
|
getKeywords(function(keywords) {
|
|
|
|
getRecords(function(records) {
|
|
|
|
callback({
|
|
|
|
keywords: keywords,
|
|
|
|
records: records
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
function execQuery(query, callback) {
|
|
|
|
getData(function(data) {
|
|
|
|
var searchResults = findRecords(
|
|
|
|
data,
|
|
|
|
query.searchParams,
|
|
|
|
query.minScore * _.keys(query.searchParams).length
|
|
|
|
);
|
|
|
|
|
|
|
|
var graphColumns = {};
|
|
|
|
for (var keyword in query.searchParams) {
|
|
|
|
var searchHints = buildHints(
|
|
|
|
data,
|
|
|
|
query.searchParams,
|
|
|
|
query.minScore * _.keys(query.searchParams).length,
|
|
|
|
keyword,
|
|
|
|
query.searchRange,
|
|
|
|
query.hintSteps
|
|
|
|
);
|
|
|
|
|
|
|
|
graphColumns[keyword] = {
|
|
|
|
value: query.searchParams[keyword],
|
|
|
|
hints: searchHints,
|
|
|
|
steps: query.hintSteps
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
callback({
|
|
|
|
columns: graphColumns,
|
|
|
|
items: searchResults.slice(0, query.maxResults),
|
|
|
|
count: searchResults.length
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
module.exports = {
|
2014-09-18 08:22:57 +00:00
|
|
|
'loadDb': loadDb,
|
|
|
|
'addKeyword': addKeyword,
|
|
|
|
'removeKeyword': removeKeyword,
|
|
|
|
'getKeywords': getKeywords,
|
|
|
|
'execQuery': execQuery
|
2014-09-17 07:39:46 +00:00
|
|
|
};
|