1
restaurant-search/search.go

433 lines
11 KiB
Go
Raw Normal View History

/*
* 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.
*/
2015-09-01 07:52:19 +00:00
package search
import (
"database/sql"
"encoding/json"
2015-09-01 09:41:37 +00:00
"errors"
2015-03-26 04:20:34 +00:00
"fmt"
2015-06-27 03:48:44 +00:00
"math"
"net/http"
2015-09-01 09:41:37 +00:00
"path"
"runtime"
2015-03-24 03:45:18 +00:00
"strings"
2015-07-31 05:59:36 +00:00
"sync"
2015-07-31 04:13:31 +00:00
"time"
2015-06-24 10:11:43 +00:00
2015-06-28 07:54:22 +00:00
"github.com/GaryBoone/GoStats/stats"
2015-08-23 08:17:38 +00:00
_ "github.com/mattn/go-sqlite3"
)
2015-09-01 07:52:19 +00:00
var dataSrc string
2015-08-23 12:22:29 +00:00
func handleExecuteQuery(rw http.ResponseWriter, req *http.Request) {
2015-07-31 04:13:31 +00:00
startTime := time.Now()
2015-09-01 07:52:19 +00:00
db, err := sql.Open("sqlite3", dataSrc)
2015-08-24 07:02:04 +00:00
if err != nil {
http.Error(rw, err.Error(), http.StatusInternalServerError)
return
}
defer db.Close()
2015-08-23 10:34:45 +00:00
var (
request struct {
2015-08-23 11:03:26 +00:00
Features map[string]float64 `json:"features"`
Geo *geoData `json:"geo"`
MaxResults int `json:"maxResults"`
MinScore float64 `json:"minScore"`
Modes map[string]string `json:"modes"`
Profile map[string]float64 `json:"profile"`
Resolution int `json:"resolution"`
SortAsc bool `json:"sortAsc"`
SortKey string `json:"sortKey"`
WalkingDist float64 `json:"walkingDist"`
2015-08-23 10:34:45 +00:00
}
response struct {
2015-08-23 10:56:07 +00:00
Columns map[string]*column `json:"columns"`
Count int `json:"count"`
MinScore float64 `json:"minScore"`
Records []record `json:"records"`
ElapsedTime int64 `json:"elapsedTime"`
2015-08-23 10:34:45 +00:00
}
)
2015-03-24 11:52:40 +00:00
if err := json.NewDecoder(req.Body).Decode(&request); err != nil {
http.Error(rw, err.Error(), http.StatusInternalServerError)
return
}
2015-03-25 11:17:12 +00:00
var geo *geoData
2015-03-24 13:55:25 +00:00
if request.Geo != nil {
2015-03-25 11:17:12 +00:00
geo = &geoData{request.Geo.Latitude, request.Geo.Longitude}
2015-03-24 13:55:25 +00:00
}
2015-08-24 06:42:16 +00:00
allEntries, err := fetchRecords(db, queryContext{geo, request.Profile, request.WalkingDist})
if err != nil {
http.Error(rw, err.Error(), http.StatusInternalServerError)
return
}
2015-03-25 04:12:22 +00:00
features := fixFeatures(request.Features)
2015-06-29 04:06:26 +00:00
modes := fixModes(request.Modes)
2015-08-23 10:34:45 +00:00
matchedEntries := findRecords(allEntries, features, modes, request.MinScore)
sorter := recordSorter{entries: matchedEntries, key: request.SortKey, ascending: request.SortAsc}
sorter.sort()
2015-03-25 03:33:41 +00:00
2015-07-31 05:59:36 +00:00
var wg sync.WaitGroup
wg.Add(len(features))
2015-06-28 07:54:22 +00:00
2015-08-23 11:03:26 +00:00
response.Columns = make(map[string]*column)
2015-07-31 05:59:36 +00:00
for name := range features {
2015-08-23 10:56:07 +00:00
response.Columns[name] = new(column)
2015-08-24 07:21:18 +00:00
go func(name string) {
defer wg.Done()
col := response.Columns[name]
col.Bracket = bracket{Max: -1.0, Min: 1.0}
col.Hints = project(allEntries, features, modes, name, request.MinScore, request.Resolution)
col.Mode = modes[name].String()
col.Steps = request.Resolution
col.Value = features[name]
var d stats.Stats
for _, record := range matchedEntries {
if feature, ok := record.features[name]; ok {
d.Update(feature)
}
}
if d.Count() > 0 {
var dev float64
if d.Count() > 1 {
dev = d.SampleStandardDeviation() * 3
}
mean := d.Mean()
col.Bracket.Max = math.Min(mean+dev, d.Max())
col.Bracket.Min = math.Max(mean-dev, d.Min())
}
}(name)
2015-08-23 10:34:45 +00:00
}
wg.Wait()
2015-08-23 11:03:26 +00:00
response.Count = len(matchedEntries)
response.MinScore = request.MinScore
2015-07-31 04:13:31 +00:00
response.ElapsedTime = time.Since(startTime).Nanoseconds()
2015-08-23 11:03:26 +00:00
if len(matchedEntries) > request.MaxResults {
response.Records = matchedEntries[:request.MaxResults]
} else {
response.Records = matchedEntries
}
2015-03-25 03:33:41 +00:00
js, err := json.Marshal(response)
if err != nil {
2015-08-23 12:22:29 +00:00
http.Error(rw, err.Error(), http.StatusInternalServerError)
return
2015-03-25 03:33:41 +00:00
}
rw.Header().Set("Content-Type", "application/json")
rw.Write(js)
}
2015-08-23 12:22:29 +00:00
func handleGetCategories(rw http.ResponseWriter, req *http.Request) {
2015-09-01 07:52:19 +00:00
db, err := sql.Open("sqlite3", dataSrc)
2015-08-24 07:02:04 +00:00
if err != nil {
http.Error(rw, err.Error(), http.StatusInternalServerError)
return
}
defer db.Close()
2015-03-25 10:25:14 +00:00
categoryRows, err := db.Query("SELECT description, id FROM categories")
if err != nil {
2015-08-23 12:22:29 +00:00
http.Error(rw, err.Error(), http.StatusInternalServerError)
return
}
2015-03-25 10:25:14 +00:00
defer categoryRows.Close()
2015-08-23 09:56:18 +00:00
type category struct {
Description string `json:"description"`
Id int `json:"id"`
}
var response []category
2015-03-25 10:25:14 +00:00
for categoryRows.Next() {
var (
description string
id int
)
2015-03-25 10:25:14 +00:00
if err := categoryRows.Scan(&description, &id); err != nil {
2015-08-23 12:22:29 +00:00
http.Error(rw, err.Error(), http.StatusInternalServerError)
return
}
2015-08-23 09:56:18 +00:00
response = append(response, category{description, id})
}
2015-03-25 10:25:14 +00:00
if err := categoryRows.Err(); err != nil {
2015-08-23 12:22:29 +00:00
http.Error(rw, err.Error(), http.StatusInternalServerError)
return
}
2015-08-23 09:56:18 +00:00
js, err := json.Marshal(response)
if err != nil {
2015-08-23 12:22:29 +00:00
http.Error(rw, err.Error(), http.StatusInternalServerError)
return
}
rw.Header().Set("Content-Type", "application/json")
rw.Write(js)
}
2015-08-23 12:22:29 +00:00
func handleAddCategory(rw http.ResponseWriter, req *http.Request) {
2015-09-01 07:52:19 +00:00
db, err := sql.Open("sqlite3", dataSrc)
2015-08-24 07:02:04 +00:00
if err != nil {
http.Error(rw, err.Error(), http.StatusInternalServerError)
return
}
defer db.Close()
2015-08-23 10:34:45 +00:00
var (
request struct {
Description string `json:"description"`
}
2015-08-23 09:56:18 +00:00
2015-08-23 10:34:45 +00:00
response struct {
Description string `json:"description"`
Id int `json:"id"`
Success bool `json:"success"`
}
)
2015-08-23 09:56:18 +00:00
2015-03-24 03:22:21 +00:00
if err := json.NewDecoder(req.Body).Decode(&request); err != nil {
http.Error(rw, err.Error(), http.StatusInternalServerError)
return
}
2015-08-23 09:56:18 +00:00
response.Description = strings.TrimSpace(request.Description)
2015-03-24 03:22:21 +00:00
2015-08-23 09:56:18 +00:00
if len(response.Description) > 0 {
2015-03-24 03:22:21 +00:00
result, err := db.Exec("INSERT INTO categories(description) VALUES(?)", request.Description)
if err != nil {
2015-08-23 12:22:29 +00:00
http.Error(rw, err.Error(), http.StatusInternalServerError)
return
2015-03-24 03:22:21 +00:00
}
insertId, err := result.LastInsertId()
if err != nil {
2015-08-23 12:22:29 +00:00
http.Error(rw, err.Error(), http.StatusInternalServerError)
return
2015-03-24 03:22:21 +00:00
}
2015-08-24 06:42:16 +00:00
rows, err := result.RowsAffected()
2015-03-24 03:22:21 +00:00
if err != nil {
2015-08-23 12:22:29 +00:00
http.Error(rw, err.Error(), http.StatusInternalServerError)
return
2015-03-24 03:22:21 +00:00
}
2015-08-24 06:42:16 +00:00
response.Success = rows > 0
2015-03-24 03:22:21 +00:00
response.Id = int(insertId)
}
js, err := json.Marshal(response)
if err != nil {
2015-08-23 12:22:29 +00:00
http.Error(rw, err.Error(), http.StatusInternalServerError)
return
2015-03-24 03:22:21 +00:00
}
rw.Header().Set("Content-Type", "application/json")
rw.Write(js)
}
2015-08-23 12:22:29 +00:00
func handleRemoveCategory(rw http.ResponseWriter, req *http.Request) {
2015-09-01 07:52:19 +00:00
db, err := sql.Open("sqlite3", dataSrc)
2015-08-24 07:02:04 +00:00
if err != nil {
http.Error(rw, err.Error(), http.StatusInternalServerError)
return
}
defer db.Close()
2015-08-23 10:34:45 +00:00
var (
request struct {
Id int `json:"id"`
}
2015-08-23 09:56:18 +00:00
2015-08-23 10:34:45 +00:00
response struct {
Success bool `json:"success"`
}
)
2015-08-23 09:56:18 +00:00
2015-03-24 02:58:00 +00:00
if err := json.NewDecoder(req.Body).Decode(&request); err != nil {
http.Error(rw, err.Error(), http.StatusInternalServerError)
return
}
2015-08-23 09:56:18 +00:00
if _, err := db.Exec("DELETE FROM categories WHERE id = (?)", request.Id); err == nil {
response.Success = true
}
js, err := json.Marshal(response)
if err != nil {
2015-08-23 12:22:29 +00:00
http.Error(rw, err.Error(), http.StatusInternalServerError)
return
}
rw.Header().Set("Content-Type", "application/json")
rw.Write(js)
}
2015-08-23 12:22:29 +00:00
func handleAccessReview(rw http.ResponseWriter, req *http.Request) {
2015-09-01 07:52:19 +00:00
db, err := sql.Open("sqlite3", dataSrc)
2015-08-24 07:02:04 +00:00
if err != nil {
http.Error(rw, err.Error(), http.StatusInternalServerError)
return
}
defer db.Close()
2015-08-23 09:56:18 +00:00
var request struct {
2015-08-23 11:03:26 +00:00
Id int `json:"id"`
Profile map[string]float64 `json:"profile"`
2015-08-23 09:56:18 +00:00
}
2015-03-25 10:10:34 +00:00
if err := json.NewDecoder(req.Body).Decode(&request); err != nil {
http.Error(rw, err.Error(), http.StatusInternalServerError)
return
}
2015-03-26 03:18:43 +00:00
reviewsResult, err := db.Exec("UPDATE reviews SET accessCount = accessCount + 1 WHERE id = (?)", request.Id)
if err != nil {
2015-08-23 12:22:29 +00:00
http.Error(rw, err.Error(), http.StatusInternalServerError)
return
2015-03-26 03:18:43 +00:00
}
2015-03-25 10:10:34 +00:00
2015-03-26 03:18:43 +00:00
rowsAffected, err := reviewsResult.RowsAffected()
if err != nil {
2015-08-23 12:22:29 +00:00
http.Error(rw, err.Error(), http.StatusInternalServerError)
return
2015-03-25 10:10:34 +00:00
}
2015-03-26 03:18:43 +00:00
if rowsAffected == 0 || len(request.Profile) == 0 {
2015-09-18 10:07:55 +00:00
http.Error(rw, "invalid profile", http.StatusInternalServerError)
2015-03-26 03:18:43 +00:00
return
}
2015-03-25 10:10:34 +00:00
2015-09-23 01:42:26 +00:00
historyResult, err := db.Exec("INSERT INTO history(date, reviewId) VALUES(DATETIME('now'), ?)", request.Id)
2015-03-26 03:18:43 +00:00
if err != nil {
2015-08-23 12:22:29 +00:00
http.Error(rw, err.Error(), http.StatusInternalServerError)
return
2015-03-25 10:10:34 +00:00
}
2015-03-26 03:18:43 +00:00
insertId, err := historyResult.LastInsertId()
2015-03-25 10:10:34 +00:00
if err != nil {
2015-08-23 12:22:29 +00:00
http.Error(rw, err.Error(), http.StatusInternalServerError)
return
2015-03-25 10:10:34 +00:00
}
2015-03-26 03:18:43 +00:00
for id, value := range request.Profile {
2015-04-18 05:23:35 +00:00
catRow := db.QueryRow("SELECT EXISTS(SELECT NULL FROM categories WHERE id = ?)", id)
var catExists int
if err := catRow.Scan(&catExists); err != nil {
2015-08-23 12:22:29 +00:00
http.Error(rw, err.Error(), http.StatusInternalServerError)
return
2015-04-18 05:23:35 +00:00
}
if catExists == 0 {
continue
}
2015-03-26 03:18:43 +00:00
if _, err := db.Exec("INSERT INTO historyGroups(categoryId, categoryValue, historyId) VALUES(?, ?, ?)", id, value, insertId); err != nil {
2015-08-23 12:22:29 +00:00
http.Error(rw, err.Error(), http.StatusInternalServerError)
return
2015-03-26 03:18:43 +00:00
}
}
}
2015-08-23 12:22:29 +00:00
func handleClearHistory(rw http.ResponseWriter, req *http.Request) {
2015-09-01 07:52:19 +00:00
db, err := sql.Open("sqlite3", dataSrc)
2015-08-24 07:02:04 +00:00
if err != nil {
http.Error(rw, err.Error(), http.StatusInternalServerError)
return
}
defer db.Close()
2015-03-27 04:13:37 +00:00
if _, err := db.Exec("DELETE FROM historyGroups"); err != nil {
2015-08-23 12:22:29 +00:00
http.Error(rw, err.Error(), http.StatusInternalServerError)
return
2015-03-26 04:20:34 +00:00
}
2015-03-27 04:13:37 +00:00
if _, err := db.Exec("DELETE FROM history"); err != nil {
2015-08-23 12:22:29 +00:00
http.Error(rw, err.Error(), http.StatusInternalServerError)
return
2015-03-26 04:20:34 +00:00
}
rw.Header().Set("Content-Type", "text/plain")
fmt.Fprint(rw, "History tables cleared")
}
2015-09-01 09:41:37 +00:00
func queryPaths() (staticDat, dataSrc string, err error) {
_, filename, _, ok := runtime.Caller(1)
if !ok {
err = errors.New("unable to capture paths")
return
}
dir := path.Dir(filename)
dataSrc = path.Join(dir, "build/data/db.sqlite3")
staticDat = path.Join(dir, "static")
return
}
func NewSearchApp() (*http.ServeMux, error) {
var (
err error
staticDat string
)
staticDat, dataSrc, err = queryPaths()
if err != nil {
return nil, err
}
2015-07-31 06:33:09 +00:00
2015-09-01 07:52:19 +00:00
mux := http.NewServeMux()
mux.HandleFunc("/query", handleExecuteQuery)
mux.HandleFunc("/categories", handleGetCategories)
mux.HandleFunc("/learn", handleAddCategory)
mux.HandleFunc("/forget", handleRemoveCategory)
mux.HandleFunc("/access", handleAccessReview)
mux.HandleFunc("/clear", handleClearHistory)
2015-09-01 09:41:37 +00:00
mux.Handle("/", http.FileServer(http.Dir(staticDat)))
2015-09-01 09:41:37 +00:00
return mux, nil
}