Skip to content

Commit

Permalink
Merge pull request #25 from zhenghaoz/dev
Browse files Browse the repository at this point in the history
feat: support timestamps, popularity and interactive recommendation
  • Loading branch information
zhenghaoz committed Dec 20, 2019
2 parents 4c422e2 + c1f2568 commit fe4d3e2
Show file tree
Hide file tree
Showing 20 changed files with 1,202 additions and 383 deletions.
14 changes: 9 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ It's easy to setup a recomendation service with `gorse`.
- **Step 1**: Import feedback and items.

```bash
$ gorse import-feedback ~/.gorse/gorse.db u.data --sep $'\t'
$ gorse import-feedback ~/.gorse/gorse.db u.data --sep $'\t' --timestamp 2
$ gorse import-items ~/.gorse/gorse.db u.item --sep '|'
```

Expand All @@ -121,12 +121,16 @@ It requests 5 recommended items for the 1-th user. The response might be:
```
[
{
"ItemId": "202",
"Score": 2.901297852545712
"ItemId": "919",
"Popularity": 96,
"Timestamp": "1995-01-01T00:00:00Z",
"Score": 1
},
{
"ItemId": "151",
"Score": 2.8871064286482864
"ItemId": "474",
"Popularity": 194,
"Timestamp": "1963-01-01T00:00:00Z",
"Score": 0.9486470268850127
},
...
]
Expand Down
14 changes: 9 additions & 5 deletions README.zh-cn.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ $ gorse test bpr --load-csv u.data --csv-sep $'\t' --eval-precision --eval-recal
- **第一步**: 导入反馈和物品

```bash
$ gorse import-feedback ~/.gorse/gorse.db u.data --sep $'\t'
$ gorse import-feedback ~/.gorse/gorse.db u.data --sep $'\t' --timestamp 2
$ gorse import-items ~/.gorse/gorse.db u.item --sep '|'
```

Expand All @@ -118,12 +118,16 @@ $ curl 127.0.0.1:8080/recommends/1?number=5
```
[
{
"ItemId": "202",
"Score": 2.901297852545712
"ItemId": "919",
"Popularity": 96,
"Timestamp": "1995-01-01T00:00:00Z",
"Score": 1
},
{
"ItemId": "151",
"Score": 2.8871064286482864
"ItemId": "474",
"Popularity": 194,
"Timestamp": "1963-01-01T00:00:00Z",
"Score": 0.9486470268850127
},
...
]
Expand Down
17 changes: 13 additions & 4 deletions cmd/dump.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"github.com/spf13/cobra"
"github.com/zhenghaoz/gorse/engine"
"log"
"time"
)

var commandImportFeedback = &cobra.Command{
Expand All @@ -23,11 +24,13 @@ var commandImportFeedback = &cobra.Command{
// Import feedback
printCount(db)
log.Printf("import feedback from %s\n", csvFile)
start := time.Now()
if err = db.LoadFeedbackFromCSV(csvFile, sep, header); err != nil {
log.Fatal(err)
}
elapsed := time.Since(start)
printCount(db)
log.Printf("feedback are imported successfully!")
log.Printf("feedback are imported successfully! (%v)\n", elapsed)
},
}

Expand All @@ -40,6 +43,7 @@ var commandImportItems = &cobra.Command{
csvFile := args[1]
sep, _ := cmd.PersistentFlags().GetString("sep")
header, _ := cmd.PersistentFlags().GetBool("header")
timestampColumn, _ := cmd.PersistentFlags().GetInt("timestamp")
// Connect database
db, err := engine.Open(databaseFile)
if err != nil {
Expand All @@ -48,11 +52,13 @@ var commandImportItems = &cobra.Command{
// Import feedback
printCount(db)
log.Printf("import items from %s\n", csvFile)
if err = db.LoadItemsFromCSV(csvFile, sep, header); err != nil {
start := time.Now()
if err = db.LoadItemsFromCSV(csvFile, sep, header, timestampColumn); err != nil {
log.Fatal(err)
}
elapsed := time.Since(start)
printCount(db)
log.Println("items are imported successfully!")
log.Printf("items are imported successfully! (%v)\n", elapsed)
},
}

Expand Down Expand Up @@ -88,14 +94,15 @@ var commandExportItems = &cobra.Command{
csvFile := args[1]
sep, _ := cmd.PersistentFlags().GetString("sep")
header, _ := cmd.PersistentFlags().GetBool("header")
timestamp, _ := cmd.PersistentFlags().GetBool("timestamp")
// Connect database
db, err := engine.Open(databaseFile)
if err != nil {
log.Fatal(err)
}
// Import feedback
log.Printf("export items to %s\n", csvFile)
if err = db.SaveItemsToCSV(csvFile, sep, header); err != nil {
if err = db.SaveItemsToCSV(csvFile, sep, header, timestamp); err != nil {
log.Fatal(err)
}
log.Println("items are exported successfully!")
Expand All @@ -104,6 +111,8 @@ var commandExportItems = &cobra.Command{

func init() {
commands := []*cobra.Command{commandImportFeedback, commandImportItems, commandExportFeedback, commandExportItems}
commandImportItems.PersistentFlags().IntP("timestamp", "t", 0, "specify the timestamp column")
commandExportItems.PersistentFlags().BoolP("timestamp", "t", false, "export with timestamp")
for _, command := range commands {
command.PersistentFlags().String("sep", ",", "set the separator for CSV files")
command.PersistentFlags().Bool("header", false, "set the header for CSV files")
Expand Down
146 changes: 116 additions & 30 deletions cmd/rest.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package cmd

import (
"fmt"
"github.com/araddon/dateparse"
"github.com/emicklei/go-restful"
"github.com/zhenghaoz/gorse/engine"
"log"
Expand All @@ -13,40 +14,57 @@ func serve(config engine.ServerConfig) {
// Create a web service
ws := new(restful.WebService)
ws.Consumes(restful.MIME_JSON).Produces(restful.MIME_JSON)

// Get the recommendation list
ws.Route(ws.GET("/recommends/{user-id}").
To(getRecommends).
Doc("get the top list for a user").
Param(ws.PathParameter("user-id", "identifier of the user").DataType("int")).
Param(ws.FormParameter("number", "the number of recommendations").DataType("int")))
// Get user list
ws.Route(ws.GET("/users").
To(getUsers).
Doc("get the list of users"))
// Get user's feedback
ws.Route(ws.GET("/user/{user-id}").
To(getUser).
Doc("get a user's feedback").
Param(ws.PathParameter("user-id", "identifier of the user").DataType("int")))
// Popular items
Param(ws.FormParameter("number", "the number of recommendations").DataType("int")).
Param(ws.FormParameter("p", "weight of popularity").DataType("float")).
Param(ws.FormParameter("t", "weight of time").DataType("float")).
Param(ws.PathParameter("c", "weight of collaborative filtering").DataType("float")))
// Get popular items
ws.Route(ws.GET("/popular").To(getPopular).
Doc("get popular items").
Param(ws.FormParameter("number", "the number of popular items").DataType("int")))
// Random items
// Get latest items
ws.Route(ws.GET("/latest").To(getLatest).
Doc("get latest items").
Param(ws.FormParameter("number", "the number of latest items").DataType("int")))
// Get random items
ws.Route(ws.GET("/random").To(getRandom).
Doc("get random items").
Param(ws.FormParameter("number", "the number of random items").DataType("int")))
// Neighbors
// Get neighbors
ws.Route(ws.GET("/neighbors/{item-id}").To(getNeighbors).
Doc("get neighbors of a item").
Param(ws.PathParameter("item-id", "identifier of the item").DataType("int")).
Param(ws.FormParameter("number", "the number of neighbors").DataType("int")))
// Add items

// Put items
ws.Route(ws.PUT("/items").To(putItems)).
Doc("put items")
// Add ratings
// Get items
ws.Route(ws.GET("/items").To(getItems)).
Doc("get items")
// Get item
ws.Route(ws.GET("/item/{item-id}").To(getItem).
Doc("get a item").
Param(ws.PathParameter("item-id", "identifier of the item").DataType("int")))

// Put feedback
ws.Route(ws.PUT("/feedback").To(putFeedback).
Doc("put feedback"))
// Get users
ws.Route(ws.GET("/users").
To(getUsers).
Doc("get the list of users"))
// Get user feedback
ws.Route(ws.GET("/user/{user-id}/feedback").To(getUserFeedback).
Doc("get a user's feedback").
Param(ws.PathParameter("user-id", "identifier of the user").DataType("int")))

ws.Route(ws.GET("/status").To(getStatus))
// Start web service
restful.DefaultContainer.Add(ws)
Expand Down Expand Up @@ -111,13 +129,9 @@ func getUsers(request *restful.Request, response *restful.Response) {
json(response, users)
}

func getUser(request *restful.Request, response *restful.Response) {
func getUserFeedback(request *restful.Request, response *restful.Response) {
// Get user id
paramUserId := request.PathParameter("user-id")
userId, err := strconv.Atoi(paramUserId)
if err != nil {
badRequest(response, err)
}
userId := request.PathParameter("user-id")
// Get the user's feedback
items, err := db.GetUserFeedback(userId)
if err != nil {
Expand All @@ -142,7 +156,29 @@ func getPopular(request *restful.Request, response *restful.Response) {
}
}
// Get the popular list
items, err := db.GetPopular(number)
items, err := db.GetList(engine.ListPop, number)
if err != nil {
internalServerError(response, err)
return
}
// Send result
json(response, items)
}

func getLatest(request *restful.Request, response *restful.Response) {
// Get the number
paramNumber := request.QueryParameter("number")
number, err := strconv.Atoi(paramNumber)
if err != nil {
if len(paramNumber) == 0 {
number = 10
} else {
badRequest(response, err)
return
}
}
// Get the popular list
items, err := db.GetList(engine.ListLatest, number)
if err != nil {
internalServerError(response, err)
return
Expand Down Expand Up @@ -190,7 +226,7 @@ func getNeighbors(request *restful.Request, response *restful.Response) {
}
}
// Get recommended items
items, err := db.GetNeighbors(itemId, number)
items, err := db.GetIdentList(engine.BucketNeighbors, itemId, number)
if err != nil {
internalServerError(response, err)
return
Expand All @@ -214,13 +250,30 @@ func getRecommends(request *restful.Request, response *restful.Response) {
return
}
}
// Get weights
weights := []float64{0.0, 0.0, 1.0}
params := []string{
request.QueryParameter("p"),
request.QueryParameter("t"),
request.QueryParameter("c"),
}
for i := range params {
if len(params[i]) > 0 {
weights[i], err = strconv.ParseFloat(params[i], 64)
if err != nil {
badRequest(response, err)
}
}
}
p, t, c := weights[0], weights[1], weights[2]
// Get recommended items
items, err := db.GetRecommends(userId, number)
items, err := db.GetIdentList(engine.BucketRecommends, userId, 0)
if err != nil {
internalServerError(response, err)
return
}
// Send result
items = engine.Ranking(items, number, p, t, c)
json(response, items)
}

Expand All @@ -234,15 +287,29 @@ type Change struct {
FeedbackAfter int // number of feedback after change
}

type ItemStringTime struct {
ItemId string
Timestamp string
}

// putItems puts items into the database.
func putItems(request *restful.Request, response *restful.Response) {
// Add ratings
items := new([]string)
if err := request.ReadEntity(items); err != nil {
temp := new([]ItemStringTime)
if err := request.ReadEntity(temp); err != nil {
badRequest(response, err)
return
}
// Parse timestamp
var err error
items := make([]engine.Item, len(*temp))
for i, v := range *temp {
items[i].ItemId = v.ItemId
items[i].Timestamp, err = dateparse.ParseAny(v.Timestamp)
if err != nil {
badRequest(response, err)
}
}
change := Change{}
// Get status before change
stat, err := status()
Expand All @@ -254,8 +321,8 @@ func putItems(request *restful.Request, response *restful.Response) {
change.ItemsBefore = stat.ItemCount
change.UsersBefore = stat.UserCount
// Insert items
for _, itemId := range *items {
err = db.InsertItem(itemId)
for _, item := range items {
err = db.InsertItem(item.ItemId, &item.Timestamp)
if err != nil {
internalServerError(response, err)
return
Expand All @@ -273,10 +340,29 @@ func putItems(request *restful.Request, response *restful.Response) {
json(response, change)
}

func getItems(request *restful.Request, response *restful.Response) {
items, err := db.GetItems()
if err != nil {
internalServerError(response, err)
}
json(response, items)
}

func getItem(request *restful.Request, response *restful.Response) {
// Get item id
itemId := request.PathParameter("item-id")
// Get item
item, err := db.GetItem(itemId)
if err != nil {
internalServerError(response, err)
}
json(response, item)
}

// Feedback is the feedback from a user to an item.
type Feedback struct {
UserId string // identifier of the user
ItemId string // identifier of the item
UserId string // identifier of the user
ItemId string // identifier of the item
Feedback float64 // rating, confidence or indicator
}

Expand Down

0 comments on commit fe4d3e2

Please sign in to comment.