Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add Batch method to Rater #5

Merged
merged 1 commit into from
Jan 11, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,46 @@ for _, item := range items {
// The Shawshank Redemption
// The Matrix
```
### Batch Operations

``` go
te, err := too.New("redis://localhost", "movies")
if err != nil {
log.Fatal(err)
}

te.Likes.Batch([]too.BatchRaterOp{
{
User: "Sonic",
Items: []too.Item{
"The Shawshank Redemption",
"The Godfather",
"The Dark Knight",
"Pulp Fiction",
},
},
{
User: "Mario",
Items: []too.Item{
"The Godfather",
"The Dark Knight",
"The Shawshank Redemption",
"The Prestige",
"The Matrix",
},
},
{
User: "Peach",
Items: []too.Item{
"The Godfather",
"Inception",
"Fight Club",
"WALL·E",
"Princess Mononoke",
},
},
}, true) // The last command is about auto update the similars and Suggestions table
```

## Documentation

Expand Down
28 changes: 26 additions & 2 deletions engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ type Engine struct {
c redis.Conn
class string

autoUpdateSimilarsAndSuggestions bool

Likes Rater
Dislikes Rater

Expand All @@ -25,10 +27,32 @@ func New(url, class string) (*Engine, error) {
e := &Engine{
c: c,
class: class,
autoUpdateSimilarsAndSuggestions: true,
}
e.Likes = Rater{e, "likes"}
e.Dislikes = Rater{e, "dislikes"}
e.Likes = Rater{e, "likes", nil}
e.Dislikes = Rater{e, "dislikes", nil}
e.Similars = Similars{e}
e.Suggestions = Suggestions{e}
return e, nil
}

func (e *Engine) DisableAutoUpdateSimilarsAndSuggestions() {
e.autoUpdateSimilarsAndSuggestions = false
}

func (e *Engine) EnableAutoUpdateSimilarsAndSuggestions() {
e.autoUpdateSimilarsAndSuggestions = true
}

func (e Engine) Update(user User) error {
err := e.Similars.update(user)
if err != nil {
return err
}

err = e.Suggestions.update(user)
if err != nil {
return err
}
return nil
}
173 changes: 173 additions & 0 deletions example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package too_test
import (
"fmt"
"log"
"testing"

"github.com/hjr265/too"
)
Expand Down Expand Up @@ -44,3 +45,175 @@ func ExampleEngine() {
// The Shawshank Redemption
// The Matrix
}

func ExampleBatch() {
te, err := too.New("redis://localhost", "movies")
if err != nil {
log.Fatal(err)
}

err = te.Likes.Batch([]too.BatchRaterOp{
{
User: "Sonic",
Items: []too.Item{
"The Shawshank Redemption",
"The Godfather",
"The Dark Knight",
"Pulp Fiction",
},
},
{
User: "Mario",
Items: []too.Item{
"The Godfather",
"The Dark Knight",
"The Shawshank Redemption",
"The Prestige",
"The Matrix",
},
},
{
User: "Peach",
Items: []too.Item{
"The Godfather",
"Inception",
"Fight Club",
"WALL·E",
"Princess Mononoke",
},
}, {
User: "Luigi",
Items: []too.Item{
"The Prestige",
"The Dark Knight",
},
},
}, true)

if err != nil {
log.Fatal(err)
}

items, _ := te.Suggestions.For("Luigi", 2)
for _, item := range items {
fmt.Println(item)
}

// Output:
// The Shawshank Redemption
// The Matrix
}

func BenchmarkNoBatch(b *testing.B) {
te, err := too.New("redis://localhost", "movies")
if err != nil {
log.Fatal(err)
}

b.ResetTimer()

for i := 0; i < b.N; i++ {
te.Likes.Add("Sonic", "The Shawshank Redemption")
te.Likes.Add("Sonic", "The Godfather")
te.Likes.Add("Sonic", "The Dark Knight")
te.Likes.Add("Sonic", "Pulp Fiction")

te.Likes.Add("Mario", "The Godfather")
te.Likes.Add("Mario", "The Dark Knight")
te.Likes.Add("Mario", "The Shawshank Redemption")
te.Likes.Add("Mario", "The Prestige")
te.Likes.Add("Mario", "The Matrix")

te.Likes.Add("Peach", "The Godfather")
te.Likes.Add("Peach", "Inception")
te.Likes.Add("Peach", "Fight Club")
te.Likes.Add("Peach", "WALL·E")
te.Likes.Add("Peach", "Princess Mononoke")
}
}

func BenchmarkUsingBatchWithoutAutoUpdate(b *testing.B) {
te, err := too.New("redis://localhost", "movies")
if err != nil {
log.Fatal(err)
}

b.ResetTimer()

for i := 0; i < b.N; i++ {
te.Likes.Batch([]too.BatchRaterOp{
{
User: "Sonic",
Items: []too.Item{
"The Shawshank Redemption",
"The Godfather",
"The Dark Knight",
"Pulp Fiction",
},
},
{
User: "Mario",
Items: []too.Item{
"The Godfather",
"The Dark Knight",
"The Shawshank Redemption",
"The Prestige",
"The Matrix",
},
},
{
User: "Peach",
Items: []too.Item{
"The Godfather",
"Inception",
"Fight Club",
"WALL·E",
"Princess Mononoke",
},
},
}, false)
}
}

func BenchmarkUsingBatchWithAutoUpdate(b *testing.B) {
te, err := too.New("redis://localhost", "movies")
if err != nil {
log.Fatal(err)
}

b.ResetTimer()

for i := 0; i < b.N; i++ {
te.Likes.Batch([]too.BatchRaterOp{
{
User: "Sonic",
Items: []too.Item{
"The Shawshank Redemption",
"The Godfather",
"The Dark Knight",
"Pulp Fiction",
},
},
{
User: "Mario",
Items: []too.Item{
"The Godfather",
"The Dark Knight",
"The Shawshank Redemption",
"The Prestige",
"The Matrix",
},
},
{
User: "Peach",
Items: []too.Item{
"The Godfather",
"Inception",
"Fight Club",
"WALL·E",
"Princess Mononoke",
},
},
}, true)
}
}
86 changes: 79 additions & 7 deletions rater.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,87 @@ import (
)

type Rater struct {
e *Engine
kind string
e *Engine
kind string
memberships map[User][]Item
}

type BatchRaterOp struct {
User User
Items []Item
}

func (r Rater) Batch(ops []BatchRaterOp, updateSimilarsAndSuggestions bool) error {
// Disable Auto Update, Suggestions and Similiars will be updated later
r.e.DisableAutoUpdateSimilarsAndSuggestions()

// Cache memberships to be used into redis transaction
r.memberships = make(map[User][]Item, 0)
r.cacheMemberships(ops)

// Start a transaction
r.e.c.Send("MULTI")
for _, op := range ops {
for _, item := range op.Items {
err := r.Add(op.User, item)
if err != nil {
// Rollback if found error
r.e.c.Send("DISCARD")
return err
}
}
}
// Commit the transaction
r.e.c.Do("EXEC")

// After finished, update Suggestions and Similiars
if updateSimilarsAndSuggestions {
for _, op := range ops {
r.e.Update(op.User)
}
}

r.e.EnableAutoUpdateSimilarsAndSuggestions()

return nil
}

func (r Rater) cacheMemberships(ops []BatchRaterOp) {
for _, op := range ops {
for _, item := range op.Items {
r.e.c.Send("WATCH", r.memberKey(item))
yes, err := r.userIsMember(op.User, item, false)
if yes && err != nil {
r.memberships[op.User] = append(r.memberships[op.User], item)
}
}
}
}

func (r Rater) isCachedInMembership(user User, item Item) bool {
for _, _item := range r.memberships[user] {
if _item == item {
return true
}
}
return false
}

func (r Rater) userIsMember(user User, item Item, useCache bool) (bool, error) {
yes, err := redis.Bool(r.e.c.Do("SISMEMBER", r.memberKey(item), user))
if err != nil && useCache {
return r.isCachedInMembership(user, item), nil
}
return yes, err
}

func (r Rater) memberKey(item Item) string {
return fmt.Sprintf("%s:%s:%s", r.e.class, item, r.kind)
}

// Add adds a rating by user for item
func (r Rater) Add(user User, item Item) error {
yes, err := redis.Bool(r.e.c.Do("SISMEMBER", fmt.Sprintf("%s:%s:%s", r.e.class, item, r.kind), user))
yes, err := r.userIsMember(user, item, true)
if err != nil {
return err
}
Expand All @@ -37,12 +111,10 @@ func (r Rater) Add(user User, item Item) error {
return err
}

err = r.e.Similars.update(user)
if err != nil {
return err
if r.e.autoUpdateSimilarsAndSuggestions {
err = r.e.Update(user)
}

err = r.e.Suggestions.update(user)
if err != nil {
return err
}
Expand Down