1

Updating to use features instead of keywords

This commit is contained in:
Alex Yatskov 2014-11-08 11:23:42 +09:00
parent bfadd2038b
commit 8b7759a82a
7 changed files with 111 additions and 142 deletions

View File

@ -16,7 +16,6 @@
"bootstrap": "~3.2.0", "bootstrap": "~3.2.0",
"handlebars": "~1.3.0", "handlebars": "~1.3.0",
"underscore": "~1.6.0", "underscore": "~1.6.0",
"bootstrap-select": "~1.6.2",
"tinycolor": "~1.0.0", "tinycolor": "~1.0.0",
"seiyria-bootstrap-slider": "~4.0.1", "seiyria-bootstrap-slider": "~4.0.1",
"snap.svg": "~0.3.0" "snap.svg": "~0.3.0"

View File

@ -14,11 +14,11 @@
</div> </div>
<!-- query input --> <!-- query input -->
<div id="input" class="form-horizontal"> <div class="form-horizontal">
<div class="form-group"> <div class="form-group">
<label for="keywordsToSearch" class="col-md-2 control-label">Keywords</label> <label for="keywordsToSearch" class="col-md-2 control-label">Keywords</label>
<div class="col-md-10"> <div class="col-md-10">
<select id="keywordsToSearch" class="form-control" multiple="multiple" data-max-options="4" data-live-search="data-live-search"></select> <select id="keywordsToSearch" class="form-control"></select>
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
@ -39,13 +39,6 @@
<input class="form-control" type="number" value="100" id="maxResults"> <input class="form-control" type="number" value="100" id="maxResults">
</div> </div>
</div> </div>
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<button class="btn btn-primary" id="searchKeywords" type="button" disabled="disabled">
<span class="glyphicon glyphicon-search"></span> Search
</button>
</div>
</div>
</div> </div>
<!-- options dialog --> <!-- options dialog -->
@ -113,9 +106,9 @@
</div> </div>
<!-- query output --> <!-- query output -->
<div id="output" style="display: none;"> <div>
<!-- semantic tweaker --> <!-- semantic tweaker -->
<div class="panel panel-default unselectable"> <div class="panel panel-default">
<div class="panel-heading"> <div class="panel-heading">
<big>Semantic tweaks to: <span id="query"></span></big> <big>Semantic tweaks to: <span id="query"></span></big>
<div class="btn-group pull-right"> <div class="btn-group pull-right">

View File

@ -27,18 +27,19 @@
(function(hscd) { (function(hscd) {
'use strict'; 'use strict';
var ctx = {}; var ctx = {
var log = []; log: []
};
function onAdjust(name, value) { function onAdjust(name, value) {
ctx.searchParams[name] = value; ctx.features[name] = value;
var query = { var query = {
searchParams: ctx.searchParams, features: ctx.features,
searchRange: ctx.searchRange, range: ctx.range,
minScore: ctx.minScore, minScore: ctx.minScore,
hintSteps: ctx.hintSteps, hintSteps: ctx.hintSteps,
maxResults: ctx.maxResults maxResults: ctx.maxResults
}; };
ctx.grapher.enable(false); ctx.grapher.enable(false);
@ -49,33 +50,83 @@
}); });
} }
function onSearch() { function onReady() {
var keywords = $('#keywordsToSearch').val() || []; $('#history').on('slideStop', onSelectSnapshot);
var searchParams = {}; $('#history').slider({
formatter: function(value) {
var delta = ctx.log.length - (value + 1);
switch (delta) {
case 0:
return 'Most recent query';
case 1:
return 'Previous query';
default:
return String(delta) + ' queries back';
}
}
});
for (var i = 0, count = keywords.length; i < count; ++i) { $('#forgetKeyword').click(onForget);
searchParams[keywords[i]] = 1.0; $('#forgetDialog').on('show.bs.modal', function() {
$('#forgetError').hide();
$.getJSON('/get_keywords', function(keywords) {
$('#keywordToForget').empty();
for (var i = 0, count = keywords.length; i < count; ++i) {
$('#keywordToForget').append($('<option></option>', {
value: keywords[i],
text: keywords[i]
}));
}
});
});
$('#learnKeyword').click(onLearn);
$('#learnDialog').on('show.bs.modal', function() {
$('#learnError').hide();
$('#learnKeyword').prop('disabled', true);
$('#keywordToLearn').val('');
});
$('#keywordToLearn').bind('input', function() {
$('#learnKeyword').prop('disabled', !$(this).val());
});
$.getJSON('/get_keywords', function(keywords) {
ctx.keywords = keywords;
for (var keyword in keywords) {
$('#keywordsToSearch').append($('<option></option>', { value: keyword, text: keyword }));
}
onSearch();
});
}
function onSearch() {
var keyword = $('#keywordsToSearch').val();
var features = {};
for (var feature in ctx.keywords[keyword]) {
features[feature] = 1.0;
} }
var query = { var query = {
searchParams: searchParams, features: features,
searchRange: { min: -1.0, max: 1.0 }, range: { min: -1.0, max: 1.0 },
minScore: parseFloat($('#minScore').val()), minScore: parseFloat($('#minScore').val()),
hintSteps: parseInt($('#hintSteps').val()), hintSteps: parseInt($('#hintSteps').val()),
maxResults: parseInt($('#maxResults').val()) maxResults: parseInt($('#maxResults').val())
}; };
$.getJSON('/search', query, function(results) { $.getJSON('/search', query, function(results) {
ctx.searchParams = query.searchParams; ctx.features = query.features;
ctx.searchRange = query.searchRange; ctx.range = query.range;
ctx.minScore = query.minScore; ctx.minScore = query.minScore;
ctx.hintSteps = query.hintSteps; ctx.hintSteps = query.hintSteps;
ctx.maxResults = query.maxResults; ctx.maxResults = query.maxResults;
ctx.grapher = new grapher.Grapher({ ctx.grapher = new grapher.Grapher({
canvas: new Snap('#svg'), canvas: new Snap('#svg'),
steps: ctx.hintSteps, steps: ctx.hintSteps,
range: ctx.searchRange, range: ctx.range,
onValueChanged: onAdjust, onValueChanged: onAdjust,
useLocalScale: true, useLocalScale: true,
useRelativeScale: true useRelativeScale: true
@ -85,7 +136,7 @@
saveSnapshot(results); saveSnapshot(results);
outputMatches(results.items, results.count); outputMatches(results.items, results.count);
$('#query').text(keywords.join(', ')); $('#query').text(keyword);
$('#useLocalScale').click(function() { $('#useLocalScale').click(function() {
var useLocalScale = $('#useLocalScale').is(':checked'); var useLocalScale = $('#useLocalScale').is(':checked');
ctx.grapher.setUseLocalScale(useLocalScale); ctx.grapher.setUseLocalScale(useLocalScale);
@ -94,9 +145,6 @@
var useRelativeScale = $('#useRelativeScale').is(':checked'); var useRelativeScale = $('#useRelativeScale').is(':checked');
ctx.grapher.setUseRelativeScale(useRelativeScale); ctx.grapher.setUseRelativeScale(useRelativeScale);
}); });
$('#input').fadeOut(function() {
$('#output').fadeIn();
});
}); });
} }
@ -105,7 +153,7 @@
$('#learnError').slideUp(function() { $('#learnError').slideUp(function() {
var query = { var query = {
keyword: $('#keywordToLearn').val(), keyword: $('#keywordToLearn').val(),
params: ctx.searchParams params: ctx.features
}; };
$.getJSON('/add_keyword', query, function(results) { $.getJSON('/add_keyword', query, function(results) {
@ -143,13 +191,13 @@
function onSelectSnapshot() { function onSelectSnapshot() {
var index = $('#history').slider('getValue'); var index = $('#history').slider('getValue');
outputSnapshot(log[index]); outputSnapshot(ctx.log[index]);
} }
function saveSnapshot(results) { function saveSnapshot(results) {
log.push(results); ctx.log.push(results);
var count = log.length; var count = ctx.log.length;
var history = $('#history').slider(); var history = $('#history').slider();
history.slider('setAttribute', 'max', count - 1); history.slider('setAttribute', 'max', count - 1);
history.slider('setValue', count - 1); history.slider('setValue', count - 1);
@ -161,7 +209,7 @@
function outputSnapshot(results) { function outputSnapshot(results) {
for (var name in results.columns) { for (var name in results.columns) {
ctx.searchParams[name] = results.columns[name].value; ctx.features[name] = results.columns[name].value;
} }
ctx.grapher.setColumns(results.columns); ctx.grapher.setColumns(results.columns);
@ -181,70 +229,9 @@
} }
$(document).on({ $(document).on({
ajaxStart: function() { ajaxStart: function() { $('#spinner').show(); },
$('#spinner').show(); ajaxStop: function() { $('#spinner').hide(); },
}, ready: onReady()
ajaxStop: function() {
$('#spinner').hide();
},
ready: function() {
$('#keywordsToSearch').selectpicker();
$('#history').slider({
formatter: function(value) {
var delta = log.length - (value + 1);
switch (delta) {
case 0:
return 'Most recent query';
case 1:
return 'Previous query';
default:
return String(delta) + ' queries back';
}
}
});
$.getJSON('/get_keywords', function(keywords) {
$('#searchKeywords').click(onSearch);
for (var i = 0, count = keywords.length; i < count; ++i) {
$('#keywordsToSearch').append($('<option></option>', {
value: keywords[i],
text: keywords[i]
}));
}
$('#keywordsToSearch').selectpicker('refresh');
$('#keywordsToSearch').change(function() {
$('#searchKeywords').prop('disabled', !$(this).val());
});
$('#forgetKeyword').click(onForget);
$('#forgetDialog').on('show.bs.modal', function() {
$('#forgetError').hide();
$.getJSON('/get_keywords', function(keywords) {
$('#forgetKeyword').prop('disabled', keywords.length === 0);
$('#keywordToForget').empty();
for (var i = 0, count = keywords.length; i < count; ++i) {
$('#keywordToForget').append($('<option></option>', {
value: keywords[i],
text: keywords[i]
}));
}
});
});
$('#history').on('slideStop', onSelectSnapshot);
$('#learnKeyword').click(onLearn);
$('#keywordToLearn').bind('input', function() {
$('#learnKeyword').prop('disabled', !$(this).val());
});
$('#learnDialog').on('show.bs.modal', function() {
$('#learnKeyword').prop('disabled', true);
$('#keywordToLearn').val('');
$('#learnError').hide();
});
});
}
}); });
}(window.hscd = window.hscd || {})); }(window.hscd = window.hscd || {}));

View File

@ -281,7 +281,7 @@
var _canvas = params.canvas; var _canvas = params.canvas;
var _columns = {}; var _columns = {};
var _data = {}; var _data = {};
var _range = new Range(-1.0, 1.0); var _range = new Range(params.range.min || -1.0, params.range.max || 1.0);
var _steps = params.steps || 20; var _steps = params.steps || 20;
var _useLocalScale = params.useLocalScale || true; var _useLocalScale = params.useLocalScale || true;
var _useRelativeScale = params.useRelativeScale || true; var _useRelativeScale = params.useRelativeScale || true;

View File

@ -1,7 +0,0 @@
.unselectable {
-moz-user-select: none;
-khtml-user-select: none;
-webkit-user-select: none;
-ms-user-select: none;
user-select: none;
}

View File

@ -83,9 +83,7 @@ function combine(dict, params) {
return result; return result;
} }
function walkRecords(data, searchParams, minScore, callback) { function walkMatches(data, features, minScore, callback) {
var features = combine(data.keywords, searchParams);
for (var i = 0, count = data.records.length; i < count; ++i) { for (var i = 0, count = data.records.length; i < count; ++i) {
var record = data.records[i]; var record = data.records[i];
var score = innerProduct(features, record.rating); var score = innerProduct(features, record.rating);
@ -94,22 +92,20 @@ function walkRecords(data, searchParams, minScore, callback) {
callback(record, score); callback(record, score);
} }
} }
return features;
} }
function countRecords(data, searchParams, minScore) { function countRecords(data, features, minScore) {
var count = 0; var count = 0;
walkRecords(data, searchParams, minScore, function(record, score) { walkMatches(data, features, minScore, function(record, score) {
++count; ++count;
}); });
return count; return count;
} }
function findRecords(data, searchParams, minScore) { function findRecords(data, features, minScore) {
var results = []; var results = [];
walkRecords(data, searchParams, minScore, function(record, score) { walkMatches(data, features, minScore, function(record, score) {
results.push({ results.push({
name: record.name, name: record.name,
url: 'http://www.tripadvisor.com' + record.relativeUrl, url: 'http://www.tripadvisor.com' + record.relativeUrl,
@ -137,27 +133,27 @@ function step(range, steps, callback) {
} }
} }
function project(data, searchParams, minScore, keyword, range, steps) { function project(data, features, feature, minScore, range, steps) {
var testParams = _.clone(searchParams); var sample = _.clone(features);
var results = []; var results = [];
step(range, steps, function(position) { step(range, steps, function(position) {
testParams[keyword] = position; sample[feature] = position;
results.push({ results.push({
sample: position, sample: position,
count: countRecords(data, testParams, minScore) count: countRecords(data, sample, minScore)
}); });
}); });
return results; return results;
} }
function buildHints(data, searchParams, minScore, keyword, range, steps) { function buildHints(data, features, feature, minScore, range, steps) {
var projection = project( var projection = project(
data, data,
searchParams, features,
feature,
minScore, minScore,
keyword,
range, range,
steps steps
); );
@ -275,23 +271,24 @@ function execQuery(query, callback) {
getData(function(data) { getData(function(data) {
var searchResults = findRecords( var searchResults = findRecords(
data, data,
query.searchParams, query.features,
query.minScore query.minScore
); );
var graphColumns = {}; var graphColumns = {};
for (var keyword in query.searchParams) { console.log(query);
for (var feature in query.features) {
var searchHints = buildHints( var searchHints = buildHints(
data, data,
query.searchParams, query.features,
feature,
query.minScore, query.minScore,
keyword, query.range,
query.searchRange,
query.hintSteps query.hintSteps
); );
graphColumns[keyword] = { graphColumns[feature] = {
value: query.searchParams[keyword], value: query.features[feature],
hints: searchHints, hints: searchHints,
steps: query.hintSteps steps: query.hintSteps
}; };

View File

@ -45,7 +45,7 @@ function main(staticFiles, port) {
app.use('/get_keywords', function(req, res) { app.use('/get_keywords', function(req, res) {
search.getKeywords(function(keywords) { search.getKeywords(function(keywords) {
res.json(_.keys(keywords).sort()); res.json(keywords);
}); });
}); });