Skip to content

Commit

Permalink
add Batch method to Rater
Browse files Browse the repository at this point in the history
Happy new year!!!

This commits introduces follow features:

* `EnableAutoUpdate` and `DisableAutoUpdate` method to engine, with this
methods the user can manager the engine auto update similars and
suggestions status.

* `Update` method to engine, now users can update the similars and
suggestions to a user manually.

* `Batch` method to rater, now users can rater things in batch,
maybe this commit resolve part of the FurqanSoftware#1

In addition to the new API the batch operations is faster than normal
operations, follow a Benchmark

``` bash
$ go test -bench .
PASS
BenchmarkNoBatch              20          53301558 ns/op
BenchmarkUsingBatch        10000            187824 ns/op
```
  • Loading branch information
Duke committed Jan 1, 2016
1 parent ebaf1e9 commit a5838e2
Show file tree
Hide file tree
Showing 4 changed files with 318 additions and 9 deletions.
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

0 comments on commit a5838e2

Please sign in to comment.