2015-03-24 03:45:18 +00:00
|
|
|
/*
|
|
|
|
* Copyright (c) 2015 Alex Yatskov <alex@foosoft.net>
|
|
|
|
* Author: Alex Yatskov <alex@foosoft.net>
|
|
|
|
*
|
|
|
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
|
|
* this software and associated documentation files (the "Software"), to deal in
|
|
|
|
* the Software without restriction, including without limitation the rights to
|
|
|
|
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
|
|
|
* the Software, and to permit persons to whom the Software is furnished to do so,
|
|
|
|
* subject to the following conditions:
|
|
|
|
*
|
|
|
|
* The above copyright notice and this permission notice shall be included in all
|
|
|
|
* copies or substantial portions of the Software.
|
|
|
|
*
|
|
|
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
|
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
|
|
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
|
|
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
|
|
|
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
|
|
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
|
|
*/
|
|
|
|
|
|
|
|
package main
|
|
|
|
|
2015-03-24 08:29:24 +00:00
|
|
|
import (
|
|
|
|
"log"
|
|
|
|
"math"
|
2015-03-25 03:33:41 +00:00
|
|
|
"strconv"
|
2015-06-24 10:11:43 +00:00
|
|
|
|
|
|
|
"github.com/kellydunn/golang-geo"
|
2015-03-24 08:29:24 +00:00
|
|
|
)
|
|
|
|
|
2015-03-25 04:12:22 +00:00
|
|
|
func fixFeatures(features featureMap) featureMap {
|
|
|
|
fixedFeatures := featureMap{
|
2015-04-27 04:53:16 +00:00
|
|
|
"nearby": 0.0,
|
|
|
|
"accessible": 0.0,
|
|
|
|
"delicious": 0.0,
|
|
|
|
"accommodating": 0.0,
|
|
|
|
"affordable": 0.0,
|
|
|
|
"atmospheric": 0.0}
|
2015-03-25 04:12:22 +00:00
|
|
|
|
|
|
|
for name, _ := range fixedFeatures {
|
|
|
|
value, _ := features[name]
|
|
|
|
fixedFeatures[name] = value
|
|
|
|
}
|
|
|
|
|
|
|
|
return fixedFeatures
|
|
|
|
}
|
|
|
|
|
2015-03-24 11:04:52 +00:00
|
|
|
func innerProduct(features1 featureMap, features2 featureMap) float64 {
|
2015-03-24 08:29:24 +00:00
|
|
|
var result float64
|
2015-03-24 03:45:18 +00:00
|
|
|
for key, value1 := range features1 {
|
|
|
|
value2, _ := features2[key]
|
|
|
|
result += value1 * value2
|
|
|
|
}
|
|
|
|
|
|
|
|
return result
|
|
|
|
}
|
|
|
|
|
2015-03-24 11:04:52 +00:00
|
|
|
func walkMatches(entries records, features featureMap, minScore float64, callback func(record, float64)) {
|
2015-03-26 03:51:49 +00:00
|
|
|
for _, entry := range entries {
|
|
|
|
if score := innerProduct(features, entry.features); score >= minScore {
|
|
|
|
callback(entry, score)
|
2015-03-24 03:45:18 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2015-03-24 04:17:39 +00:00
|
|
|
|
2015-03-25 11:17:12 +00:00
|
|
|
func statRecords(entries records, features featureMap, minScore float64) (float64, int) {
|
|
|
|
var compatibility float64
|
|
|
|
var count int
|
|
|
|
|
2015-03-26 03:51:49 +00:00
|
|
|
walkMatches(entries, features, minScore, func(entry record, score float64) {
|
|
|
|
compatibility += entry.compatibility
|
2015-03-25 11:17:12 +00:00
|
|
|
count++
|
2015-03-24 04:17:39 +00:00
|
|
|
})
|
|
|
|
|
2015-03-25 11:17:12 +00:00
|
|
|
return compatibility, count
|
2015-03-24 04:17:39 +00:00
|
|
|
}
|
|
|
|
|
2015-03-25 11:17:12 +00:00
|
|
|
func stepRange(min, max float64, steps int, callback func(float64)) {
|
|
|
|
stepSize := (max - min) / float64(steps)
|
2015-03-24 04:17:39 +00:00
|
|
|
|
|
|
|
for i := 0; i < steps; i++ {
|
2015-03-25 11:17:12 +00:00
|
|
|
stepMax := max - stepSize*float64(i)
|
2015-03-24 04:17:39 +00:00
|
|
|
stepMin := stepMax - stepSize
|
|
|
|
stepMid := (stepMin + stepMax) / 2
|
|
|
|
|
|
|
|
callback(stepMid)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-03-25 03:00:54 +00:00
|
|
|
func findRecords(entries records, features featureMap, minScore float64) records {
|
|
|
|
var foundEntries records
|
2015-03-24 06:16:58 +00:00
|
|
|
|
2015-03-26 03:51:49 +00:00
|
|
|
walkMatches(entries, features, minScore, func(entry record, score float64) {
|
|
|
|
entry.score = score
|
|
|
|
foundEntries = append(foundEntries, entry)
|
2015-03-24 06:16:58 +00:00
|
|
|
})
|
|
|
|
|
2015-03-25 03:00:54 +00:00
|
|
|
return foundEntries
|
2015-03-24 06:16:58 +00:00
|
|
|
}
|
|
|
|
|
2015-06-28 04:48:53 +00:00
|
|
|
func calibrateMinScore(entries records, features featureMap, bracket namedBracket) float64 {
|
2015-06-28 07:34:58 +00:00
|
|
|
bestScoreRank := -math.MaxFloat64
|
|
|
|
var bestMinScore float64
|
|
|
|
|
|
|
|
for minScore := float64(-len(features)); minScore <= float64(len(features)); minScore += 0.1 {
|
|
|
|
var scoreRank float64
|
|
|
|
|
|
|
|
for _, entry := range entries {
|
|
|
|
value, ok := entry.features[bracket.name]
|
|
|
|
if !ok {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
score := innerProduct(features, entry.features)
|
|
|
|
if score < minScore {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
if score > minScore {
|
|
|
|
if value >= bracket.min && value <= bracket.max {
|
|
|
|
dist := math.Abs(value - features[bracket.name])
|
|
|
|
scoreRank += 1 / (dist * dist)
|
|
|
|
} else {
|
|
|
|
dist := math.Min(math.Abs(value-bracket.min), math.Abs(value-bracket.max))
|
|
|
|
scoreRank -= 1 / (dist * dist)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if scoreRank > bestScoreRank {
|
|
|
|
bestScoreRank = scoreRank
|
|
|
|
bestMinScore = minScore
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
log.Printf("bestScoreRank: %f; bestMinScore: %f", bestScoreRank, bestMinScore)
|
|
|
|
return bestMinScore
|
2015-06-28 04:48:53 +00:00
|
|
|
}
|
|
|
|
|
2015-03-25 11:17:12 +00:00
|
|
|
func project(entries records, features featureMap, featureName string, minScore float64, steps int) []queryProjection {
|
2015-03-24 11:04:52 +00:00
|
|
|
sampleFeatures := make(featureMap)
|
2015-03-24 06:16:58 +00:00
|
|
|
for key, value := range features {
|
|
|
|
sampleFeatures[key] = value
|
|
|
|
}
|
|
|
|
|
2015-03-24 11:04:52 +00:00
|
|
|
var projection []queryProjection
|
2015-03-25 11:17:12 +00:00
|
|
|
stepRange(-1.0, 1.0, steps, func(sample float64) {
|
2015-04-18 09:24:45 +00:00
|
|
|
sample, sampleFeatures[featureName] = sampleFeatures[featureName], sample
|
2015-03-25 11:17:12 +00:00
|
|
|
compatibility, count := statRecords(entries, sampleFeatures, minScore)
|
2015-04-18 09:24:45 +00:00
|
|
|
sample, sampleFeatures[featureName] = sampleFeatures[featureName], sample
|
|
|
|
|
2015-03-25 11:17:12 +00:00
|
|
|
projection = append(projection, queryProjection{compatibility, count, sample})
|
2015-03-24 06:16:58 +00:00
|
|
|
})
|
|
|
|
|
|
|
|
return projection
|
|
|
|
}
|
2015-03-24 08:29:24 +00:00
|
|
|
|
2015-03-24 11:04:52 +00:00
|
|
|
func computeRecordGeo(entries records, context queryContext) {
|
2015-03-24 08:29:24 +00:00
|
|
|
distUserMin := math.MaxFloat64
|
|
|
|
distUserMax := 0.0
|
|
|
|
|
2015-03-25 09:22:57 +00:00
|
|
|
for index := range entries {
|
|
|
|
entry := &entries[index]
|
|
|
|
|
2015-03-24 13:55:25 +00:00
|
|
|
if context.geo != nil {
|
2015-03-24 08:58:35 +00:00
|
|
|
userPoint := geo.NewPoint(context.geo.latitude, context.geo.longitude)
|
2015-03-25 09:22:57 +00:00
|
|
|
entryPoint := geo.NewPoint(entry.geo.latitude, context.geo.longitude)
|
|
|
|
entry.distanceToUser = userPoint.GreatCircleDistance(entryPoint)
|
2015-03-24 08:29:24 +00:00
|
|
|
}
|
|
|
|
|
2015-03-25 09:22:57 +00:00
|
|
|
distUserMin = math.Min(entry.distanceToUser, distUserMin)
|
|
|
|
distUserMax = math.Max(entry.distanceToUser, distUserMax)
|
2015-03-24 08:29:24 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
distUserRange := distUserMax - distUserMin
|
|
|
|
|
2015-03-25 09:22:57 +00:00
|
|
|
for index := range entries {
|
|
|
|
entry := &entries[index]
|
|
|
|
|
|
|
|
var accessible, nearby float64
|
|
|
|
if distUserRange > 0 {
|
|
|
|
nearby = -((entry.distanceToUser-distUserMin)/distUserRange - 0.5) * 2.0
|
2015-03-24 08:29:24 +00:00
|
|
|
|
2015-03-25 09:22:57 +00:00
|
|
|
accessible = 1.0 - (entry.distanceToStn / context.walkingDist)
|
|
|
|
accessible = math.Max(accessible, -1.0)
|
|
|
|
accessible = math.Min(accessible, 1.0)
|
2015-03-24 08:29:24 +00:00
|
|
|
}
|
|
|
|
|
2015-03-25 09:22:57 +00:00
|
|
|
entry.features["nearby"] = nearby
|
|
|
|
entry.features["accessible"] = accessible
|
2015-03-24 08:29:24 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-03-24 11:04:52 +00:00
|
|
|
func computeRecordPopularity(entries records, context queryContext) {
|
2015-03-25 09:41:19 +00:00
|
|
|
for index := range entries {
|
|
|
|
entry := &entries[index]
|
|
|
|
|
|
|
|
historyRows, err := db.Query("SELECT id FROM history WHERE reviewId = (?)", entry.id)
|
2015-03-24 08:29:24 +00:00
|
|
|
if err != nil {
|
|
|
|
log.Fatal(err)
|
|
|
|
}
|
2015-03-25 10:25:14 +00:00
|
|
|
defer historyRows.Close()
|
2015-03-24 08:29:24 +00:00
|
|
|
|
|
|
|
var groupSum float64
|
|
|
|
var groupCount int
|
|
|
|
|
|
|
|
for historyRows.Next() {
|
|
|
|
var historyId int
|
|
|
|
if err := historyRows.Scan(&historyId); err != nil {
|
|
|
|
log.Fatal(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
groupRows, err := db.Query("SELECT categoryId, categoryValue FROM historyGroups WHERE historyId = (?)", historyId)
|
|
|
|
if err != nil {
|
|
|
|
log.Fatal(err)
|
|
|
|
}
|
2015-03-25 10:25:14 +00:00
|
|
|
defer groupRows.Close()
|
2015-03-24 08:29:24 +00:00
|
|
|
|
2015-03-24 11:04:52 +00:00
|
|
|
recordProfile := make(featureMap)
|
2015-03-24 08:29:24 +00:00
|
|
|
for groupRows.Next() {
|
|
|
|
var categoryId int
|
|
|
|
var categoryValue float64
|
|
|
|
|
|
|
|
if err := groupRows.Scan(&categoryId, &categoryValue); err != nil {
|
|
|
|
log.Fatal(err)
|
|
|
|
}
|
|
|
|
|
2015-03-25 03:33:41 +00:00
|
|
|
recordProfile[strconv.Itoa(categoryId)] = categoryValue
|
2015-03-24 08:29:24 +00:00
|
|
|
}
|
|
|
|
if err := groupRows.Err(); err != nil {
|
|
|
|
log.Fatal(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
groupSum += innerProduct(recordProfile, context.profile)
|
|
|
|
groupCount++
|
|
|
|
}
|
|
|
|
if err := historyRows.Err(); err != nil {
|
|
|
|
log.Fatal(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
if groupCount > 0 {
|
2015-03-25 09:41:19 +00:00
|
|
|
entry.compatibility = groupSum / float64(groupCount)
|
2015-03-24 08:29:24 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2015-03-25 03:00:54 +00:00
|
|
|
|
|
|
|
func getRecords(context queryContext) records {
|
2015-04-27 04:53:16 +00:00
|
|
|
recordRows, err := db.Query("SELECT name, url, delicious, accommodating, affordable, atmospheric, latitude, longitude, distanceToStn, closestStn, accessCount, id FROM reviews")
|
2015-03-25 03:00:54 +00:00
|
|
|
if err != nil {
|
|
|
|
log.Fatal(err)
|
|
|
|
}
|
2015-03-25 10:25:14 +00:00
|
|
|
defer recordRows.Close()
|
2015-03-25 03:00:54 +00:00
|
|
|
|
|
|
|
var entries []record
|
2015-03-25 10:25:14 +00:00
|
|
|
for recordRows.Next() {
|
2015-03-25 03:00:54 +00:00
|
|
|
var name, url, closestStn string
|
2015-04-27 04:53:16 +00:00
|
|
|
var delicious, accommodating, affordable, atmospheric, latitude, longitude, distanceToStn float64
|
2015-03-25 03:00:54 +00:00
|
|
|
var accessCount, id int
|
|
|
|
|
2015-03-25 10:25:14 +00:00
|
|
|
recordRows.Scan(
|
2015-03-25 03:33:41 +00:00
|
|
|
&name,
|
|
|
|
&url,
|
|
|
|
&delicious,
|
2015-04-27 04:53:16 +00:00
|
|
|
&accommodating,
|
2015-03-25 03:33:41 +00:00
|
|
|
&affordable,
|
|
|
|
&atmospheric,
|
|
|
|
&latitude,
|
|
|
|
&longitude,
|
|
|
|
&distanceToStn,
|
|
|
|
&closestStn,
|
|
|
|
&accessCount,
|
|
|
|
&id)
|
|
|
|
|
|
|
|
entry := record{
|
|
|
|
name: name,
|
2015-03-26 03:18:43 +00:00
|
|
|
url: "http://www.tripadvisor.com" + url,
|
2015-03-25 03:33:41 +00:00
|
|
|
distanceToStn: distanceToStn,
|
|
|
|
closestStn: closestStn,
|
|
|
|
accessCount: accessCount,
|
2015-03-25 11:17:12 +00:00
|
|
|
geo: geoData{latitude, longitude},
|
2015-03-25 03:33:41 +00:00
|
|
|
id: id}
|
2015-03-25 03:00:54 +00:00
|
|
|
|
2015-03-25 09:22:57 +00:00
|
|
|
entry.features = featureMap{
|
2015-04-27 04:53:16 +00:00
|
|
|
"delicious": delicious,
|
|
|
|
"accommodating": accommodating,
|
|
|
|
"affordable": affordable,
|
|
|
|
"atmospheric": atmospheric}
|
2015-03-25 03:00:54 +00:00
|
|
|
|
|
|
|
entries = append(entries, entry)
|
|
|
|
}
|
2015-03-25 10:25:14 +00:00
|
|
|
if err := recordRows.Err(); err != nil {
|
2015-03-25 03:00:54 +00:00
|
|
|
log.Fatal(err)
|
|
|
|
}
|
|
|
|
|
2015-03-25 09:22:57 +00:00
|
|
|
computeRecordPopularity(entries, context)
|
|
|
|
computeRecordGeo(entries, context)
|
|
|
|
|
2015-03-25 03:00:54 +00:00
|
|
|
return entries
|
|
|
|
}
|