Usage

Overview

god is a scalable, performant, persistent, in-memory data structure server. It allows massively distributed applications to update and fetch common data in a structured and sorted format.

Its main inspirations are Redis and Chord/DHash. Like Redis it focuses on performance, ease of use and a small, simple yet powerful feature set, while from the Chord/DHash projects it inherits scalability, redundancy, and transparent failover behaviour.

This is a usage instruction aiming to be somewhere between example collection and tutorial, targeting developers interested in trying out god as datastore. The example code snippets below are tested against somewhat current versions of Go and node, and should run as is if you provide them their dependencies and run a local god server.

To try it out right now, install Go, git, Mercurial and gcc, go get github.com/zond/god/god_server, run god_server, browse to http://localhost:9192/.

For API documentation, go to http://go.pkgdoc.org/github.com/zond/god.

For the source, go to https://github.com/zond/god.

Operations

god is built to be simple to use, and as such has few dependencies for building apart from the Go standard library. For running, all you need is one statically linked binary.

If you have a working Go environment installed (specifically a writable $GOPATH), you just run go get github.com/zond/god/god_server. This will download, compile and install god and all its dependencies. After this you will find god_server in $GOPATH/bin. To start a server, just run god_server -verbose=true. Other option can be listed if you run god_server -help.

Since there are a plethora of ways to daemonize such a program, and since Go doesn't currently contain its own way of doing it, daemonizing this binary is left to creative ops people out there.

To make your server join an existing cluster/server, just run god_server -joinIp HOST/IP. It will default to using port 9191, but using the switch -port can force it to try another port.

To make it easy to use, god always exports its interface on both Go net/rpc and HTTP/JSON. The HTTP/JSON interface is always exported on the next port after the main one where the net/rpc interface is exported. By default this is port 9192. If you started your server by running god_server -verbose=true as suggested above, you will find a small administrative/monitoring tool at http://localhost:9192.

The server only listens to the default network interface for localhost by default. To make it listen to another network interface, use the switch -listenIp HOST/IP and to make it broadcast another address to its peers use -broadcastIp HOST/IP.

Clients

As mentioned there are two APIs for god, net/rpc and HTTP/JSON.

Using the net/rpc API is as simple as go get github.com/zond/god/client, and then import "github.com/zond/god/client". See http://godoc.org/github.com/zond/god/client for godoc generated documentation.

To simplify trying it out from the command line, there is also a command line interface available. To install it, run go get github.com/zond/god/god_cli, and then run god_cli. Documentation for the command format for this tool can be found at https://github.com/zond/god/tree/master/god_cli.

Since the HTTP/JSON API is designed to be as simple as possible to interact with from any type of client system, there isn't really much to say about how to use it. Examples for the different commands found in the web monitor at http://localhost:9192 (if the server is started via god_server with default options) along with the generated documentation at http://godoc.org/github.com/zond/god/client are likely the best way to learn and explore.

The HTTP/JSON API is slightly less efficient than the net/rpc API, mainly due to the receiving node having to figure out who is responsible for the operation in the request, something that the native Go client does on its own.

Storing bytes

The simplest and most obvious way to use god is to store bytes in it. Like all content in god nodes, the bytes you put in the system will be stored in RAM in one or more (if you have a cluster, which is really recommended) servers. To store byte arrays in the database, use the put command, either via client.Conn#Put or POST /rpc/DHash.Put.

Users

A trivial example would be to store application users in god. Here is a bit of example Go code that would store and fetch a user object:

  package main

import (
  "fmt"
  "github.com/zond/god/client"
  "github.com/zond/god/common"
  "github.com/zond/god/murmur"
)

// a make believe user data structure
type User struct {
  Email    string
  Password string
  Name     string
}

func main() {
  // connect to the default local server
  conn := client.MustConn("localhost:9191")
  // create a user
  user := User{
    Email:    "mail@domain.tld",
    Password: "so secret",
    Name:     "john doe",
  }
  // serialize the user
  bytes := common.MustJSONEncode(user)
  // and put it in the database
  conn.Put(murmur.HashString(user.Email), bytes)
  // try to fetch the user again
  data, _ := conn.Get(murmur.HashString(user.Email))
  var found User
  // to unserialize it
  common.MustJSONDecode(data, &found)
  fmt.Printf("stored and found %+v\n", found)
}

// output: stored and found {Email:mail@domain.tld Password:so secret Name:john doe}

And here is a bit of node compatible JavaScript doing the same thing against the HTTP/JSON API:

  var http = require('http');

// call endpoint with params and run callback when finished
function rpc(endpoint, params, callback) {
  var data = '';
  var content = JSON.stringify(params);
  var req = http.request({
    hostname: 'localhost',
    port: 9192,
    headers: {
      'Content-Length': content.length,
      'Accept': 'application/json',
    },
    path: '/rpc/DHash.' + endpoint,
    method: 'POST',
  }, function(res) {
    res.setEncoding('utf8');
    res.on('data', function(d) {
      data += d;
    });
    res.on('end', function() {
      callback(JSON.parse(data));
    });
    res.on('close', function() {
      callback(JSON.parse(data));
    });
  });
  req.on('error', function(e) {
    console.log('problem with request: ' + e.message);
  });
  req.write(content);
  req.end();
};

// a fake user
var user = {
  email: 'mail@domain.tld',
  password: 'so secret',
  name: 'john doe',
};

// insert it into the database
rpc('Put', { 
  Key: new Buffer(user.email).toString('base64'),
  Value: new Buffer(JSON.stringify(user)).toString('base64'),
}, function() {
  // and fetch it again
  rpc('Get', { Key: new Buffer(user.email).toString('base64') }, function(data) {
    console.log('stored and found', JSON.parse(new Buffer(data.Value, 'base64').toString('utf-8')));
  });
});
// output: stored and found { email: 'mail@domain.tld',
// output: password: 'so secret',
// output: name: 'john doe' }

Storing structures

To experience the real power of god, one really has to use it to store data structures. The data structures in god are themselves key/value stores with unique keys and sorted on keys in byte order.

Since they are stored not as serialized values under their keys, but as individual tree structures they can be used to store large amounts of data in a structured fashion, by using the keys of byte values and structures (tree values) to relate them to each other.

To further make use of the efficient modification, sorting and lookup properties of these sub trees they can be both 'mirrored' to allow sorting on their values as well as keys and used in various set operations to analyze them in a more algorithmic fashion.

Followers

The simplest way to store structured data in god is to just dump it into a sub tree as is. Here is a bit of example Go code that would modify and query the followers of a user:

  package main

import (
  "fmt"
  "github.com/zond/god/client"
)

func main() {
  conn := client.MustConn("localhost:9191")
  // make up a key
  key := []byte("mail@domain.tld/followers")
  // dump sub keys into it
  conn.SubPut(key, []byte("follower1@domain.tld"), nil)
  conn.SubPut(key, []byte("follower2@domain.tld"), nil)
  conn.SubPut(key, []byte("follower3@domain.tld"), nil)
  // and fetch bits and pieces of it
  fmt.Printf("my first follower is %+v\n", string(conn.SliceLen(key, nil, true, 1)[0].Key))
  last2 := conn.ReverseSliceLen(key, nil, true, 2)
  fmt.Printf("my last two followers are %+v and %+v\n", string(last2[1].Key), string(last2[0].Key))
}

// output: my first follower is follower1@domain.tld
// output: my last two followers are user2@domain.tld and user3@domain.tld

And here is a bit of nody JavaScript doing the same thing against the HTTP/JSON API:

  var http = require('http');

function rpc(endpoint, params, callback) {
  var data = '';
  var content = JSON.stringify(params);
  var req = http.request({
    hostname: 'localhost',
    port: 9192,
    headers: {
      'Content-Length': content.length,
      'Accept': 'application/json',
    },
    path: '/rpc/DHash.' + endpoint,
    method: 'POST',
  }, function(res) {
    res.setEncoding('utf8');
    res.on('data', function(d) {
      data += d;
    });
    res.on('end', function() {
      callback(JSON.parse(data));
    });
    res.on('close', function() {
      callback(JSON.parse(data));
    });
  });
  req.on('error', function(e) {
    console.log('problem with request: ' + e.message);
  });
  req.write(content);
  req.end();
};

function after(n, callback) {
  var count = n;
  return function() {
    count--;
    if (count == 0) {
      callback();
    }
  }
}

// make up a key
var key = new Buffer("mail@domain.tld/followers").toString('base64')
// and some sub keys
var followers = ["follower1@domain.tld", "follower2@domain.tld", "follower3@domain.tld"];
// define a callback that
var cb = after(followers.length, function() {
  // fetches bits and pieces of the sub set
  rpc('SliceLen', {
    Key: key,
    Len: 1,
  }, function(data) {
    console.log('my first follower is', new Buffer(data[0].Key, 'base64').toString('utf-8'));
    rpc('ReverseSliceLen', {
      Key: key,
      Len: 2,
    }, function(data) {
      console.log('my last two followers are', new Buffer(data[1].Key, 'base64').toString('utf-8'), 'and', new Buffer(data[0].Key, 'base64').toString('utf-8'));
    });
  });
});
// dump the sub keys into the sub set
followers.map(function(follower) {
  rpc('SubPut', {
    Key: key,
    SubKey: new Buffer(follower).toString('base64'),
  }, cb);
});
// output: my first follower is follower1@domain.tld
// output: my last two followers are user2@domain.tld and user3@domain.tld

Friends

One simple way to use the set operation support in god would be to intersect followers and followees to produce the users who are both. Below follows a bit of Go code that would produce these users, let us call them friends:

  package main

import (
	"fmt"
	"github.com/zond/god/client"
	"github.com/zond/setop"
)

func main() {
	conn := client.MustConn("localhost:9191")
	// lets store followers here
	followersKey := []byte("mail@domain.tld/followers")
	// and followees here
	followeesKey := []byte("mail@domain.tld/followees")
	// create a few of each
	conn.SubPut(followersKey, []byte("user1@domain.tld"), nil)
	conn.SubPut(followersKey, []byte("user2@domain.tld"), nil)
	conn.SubPut(followersKey, []byte("user3@domain.tld"), nil)
	conn.SubPut(followeesKey, []byte("user3@domain.tld"), nil)
	conn.SubPut(followeesKey, []byte("user4@domain.tld"), nil)
	// and fetch the intersection!
	for _, friend := range conn.SetExpression(setop.SetExpression{
		Code: fmt.Sprintf("(I %v %v)", string(followersKey), string(followeesKey)),
	}) {
		fmt.Println(string(friend.Key))
	}
}

// output: user3@domain.tld

To do the same thing using node, you can run:

  var http = require("http");

function rpc(endpoint, params, callback) {
  var data = "";
  var content = JSON.stringify(params);
  var req = http.request({
    hostname: "localhost",
    port: 9192,
    headers: {
      "Content-Length": content.length,
      "Accept": "application/json",
    },
    path: "/rpc/DHash." + endpoint,
    method: "POST",
  }, function(res) {
    res.setEncoding("utf8");
    res.on("data", function(d) {
      data += d;
    });
    res.on("end", function() {
      callback(JSON.parse(data));
    });
    res.on("close", function() {
      callback(JSON.parse(data));
    });
  });
  req.on("error", function(e) {
    console.log("problem with request: " + e.message);
  });
  req.write(content);
  req.end();
};

// run the callback the n'th time the return value is executed
function after(n, callback) {
  var count = n;
  return function() {
    count--;
    if (count == 0) {
      callback();
    }
  }
}

// lets store followers here
var followers_key = new Buffer("mail@domain.tld/followers").toString("base64")
// and followees here
var followees_key = new Buffer("mail@domain.tld/followees").toString("base64")
// define a few of each
var followers = ["user1@domain.tld", "user2@domain.tld", "user3@domain.tld"];
var followees = ["user3@domain.tld", "user4@domain.tld"];
// define a callback that...
var cb = after(followers.length + followees.length, function() {
  // runs an intersection of the followers and followees
  rpc("SetExpression", {
    Code: "(I " + new Buffer(followers_key, "base64").toString("utf-8") + " " + new Buffer(followees_key, "base64").toString("utf-8") + ")",
  }, function(data) {
    // and just prints them
    data.map(function(friend) {
      console.log(new Buffer(friend.Key, "base64").toString("utf-8"));
    });
  });
});
// insert the followers
followers.map(function(follower) {
  rpc("SubPut", {
    Key: followers_key,
    SubKey: new Buffer(follower).toString("base64"),
  }, cb);
});
// insert the followees
followees.map(function(followee) {
  rpc("SubPut", {
    Key: followees_key,
    SubKey: new Buffer(followee).toString("base64"),
  }, cb);
});
// output: user3@domain.tld

Top lists

The tree mirroring capabilities of god lets you sort a mapping on both key and value, by keeping a mirrored version of the main tree, with key and value reversed. This is quite costly, as it has to update the mirrored tree for each update of the main tree, but it lets you do things like keeping a running top list of the highest scoring players of your fancy multi player game. Here is some Go code to do that:

  package main

import (
  "fmt"
  "github.com/zond/god/client"
  "github.com/zond/god/common"
)

func main() {
  conn := client.MustConn("localhost:9191")
  key := []byte("score_by_email")
  // make the sub set mirrored
  conn.SubAddConfiguration(key, "mirrored", "yes")
  conn.SubPut(key, []byte("mail1@domain.tld"), common.EncodeInt64(414))
  conn.SubPut(key, []byte("mail2@domain.tld"), common.EncodeInt64(12))
  conn.SubPut(key, []byte("mail3@domain.tld"), common.EncodeInt64(9912))
  conn.SubPut(key, []byte("mail4@domain.tld"), common.EncodeInt64(33))
  conn.SubPut(key, []byte("mail5@domain.tld"), common.EncodeInt64(511))
  conn.SubPut(key, []byte("mail6@domain.tld"), common.EncodeInt64(4512))
  conn.SubPut(key, []byte("mail7@domain.tld"), common.EncodeInt64(1023))
  conn.SubPut(key, []byte("mail8@domain.tld"), common.EncodeInt64(121))
  fmt.Println("top three scores:")
  // fetch a slice of len 3 in reverse from the mirror tree
  for index, user := range conn.MirrorReverseSliceLen(key, nil, true, 3) {
    fmt.Println(index, string(user.Value), common.MustDecodeInt64(user.Key))
  }
}

// output: top three scores:
// output: 0 mail3@domain.tld 9912
// output: 1 mail6@domain.tld 4512
// output: 2 mail7@domain.tld 1023

And the corresponding node code:

  var http = require('http');

function rpc(endpoint, params, callback) {
  var data = '';
  var content = JSON.stringify(params);
  var req = http.request({
    hostname: 'localhost',
    port: 9192,
    headers: {
      'Content-Length': content.length,
      'Accept': 'application/json',
    },
    path: '/rpc/DHash.' + endpoint,
    method: 'POST',
  }, function(res) {
    res.setEncoding('utf8');
    res.on('data', function(d) {
      data += d;
    });
    res.on('end', function() {
      callback(JSON.parse(data));
    });
    res.on('close', function() {
      callback(JSON.parse(data));
    });
  });
  req.on('error', function(e) {
    console.log('problem with request: ' + e.message);
  });
  req.write(content);
  req.end();
};

function after(n, callback) {
  var count = n;
  return function() {
    count--;
    if (count == 0) {
      callback();
    }
  }
};

// convert integer to base64 encoded bytes
function i2b(i) {
  var b = new Buffer(4);
  b.writeInt32BE(i, 0);
  return b.toString('base64');
};

// convert base64 encoded bytes to an integer
function b2i(b) {
  var b = new Buffer(b, 'base64');
  return b.readInt32BE(0);
};

// dump a bunch of scores
var key = new Buffer("score_by_email").toString('base64')
var scores = {
  "mail1@domain.tld": i2b(1234),
  "mail2@domain.tld": i2b(3),
  "mail3@domain.tld": i2b(61),
  "mail4@domain.tld": i2b(1121),
  "mail5@domain.tld": i2b(9192),
  "mail6@domain.tld": i2b(5123),
  "mail7@domain.tld": i2b(44),
  "mail8@domain.tld": i2b(6),
};
// define a callback that
var cb = after(9, function() {
  // fetches a slice of len 3 in reverse from the mirror tree
  rpc('MirrorReverseSliceLen', {
    Key: key,
    Len: 3,
  }, function(data) {
    // and prints it
    console.log("top three scores");
    data.map(function(score) {
      console.log(new Buffer(score.Value, 'base64').toString('utf-8'), b2i(score.Key));
    });
  });
});
// make the score tree mirrored
rpc('SubAddConfiguration', {
  Key: 'mirrored',
  Value: 'yes',
}, cb);
// and insert the scores
for (var email in scores) {
  rpc('SubPut', {
    Key: key,
    SubKey: new Buffer(email).toString('base64'),
    Value: scores[email],
  }, cb);
}
// output: top three scores
// output: mail5@domain.tld 9192
// output: mail6@domain.tld 5123
// output: mail1@domain.tld 1234

Similarity

Now, over to something slightly more advanced. Say that we have a site where our users 'like' ice cream flavours.

If we just want to help our users find potential friends, we could collect all the users that have liked any flavour that we have liked, and sort them so that we find the one having liked the most flavours in common with us. Here is some Go code for that (in the interest of sanity, I will avoid the callback soup required to do it in node):

  package main

import (
	"fmt"
	"github.com/zond/god/client"
	"github.com/zond/god/common"
	"github.com/zond/setop"
	"math/rand"
)

var conn = client.MustConn("localhost:9191")

type user string

func (self user) like(flavour string) {
	// remember that we have liked this flavour by putting it in our sub tree with nil value, and in the sub tree of the flavour with value 1
	conn.SubPut([]byte(self), []byte(flavour), nil)
	conn.SubPut([]byte(flavour), []byte(self), common.EncodeInt64(1))
}

func (self user) similar() user {
	// create a set operation that returns the union of the tasters of all flavors we have rated, summing the values
	op := &setop.SetOp{
		Merge: setop.IntegerSum,
		Type:  setop.Union,
	}
	// for each flavor we have tried, add the raters of that flavor as a source
	for _, flavour := range conn.Slice([]byte(self), nil, nil, true, true) {
		op.Sources = append(op.Sources, setop.SetOpSource{Key: flavour.Key})
	}
	// designate a dump subset
	dumpkey := []byte(fmt.Sprintf("%v_similar_%v", self, rand.Int63()))
	// make it mirrored
	conn.SubAddConfiguration(dumpkey, "mirrored", "yes")
	// make sure we clean up after ourselves
	defer conn.SubClear(dumpkey)
	// run the set operation dumping the values in the dump tree
	conn.SetExpression(setop.SetExpression{
		Op:   op,
		Dest: dumpkey,
	})
	for _, user := range conn.Slice(dumpkey, nil, nil, true, true) {
		fmt.Printf("%v has liked %v flavours in common with %v\n", string(user.Key), common.MustDecodeInt64(user.Value), self)
	}
	// fetch the second best match (the best is likely us...)
	best := conn.MirrorReverseSliceLen(dumpkey, nil, true, 2)[1]
	return user(best.Value)
}

func main() {
	conn.Clear()
	adam := user("adam")
	beatrice := user("beatrice")
	charlie := user("charlie")
	denise := user("denise")
	eddard := user("eddard")
	adam.like("vanilla")
	adam.like("licorice")
	adam.like("mulberry")
	adam.like("wood shavings")
	beatrice.like("licorice")
	beatrice.like("chocolate")
	charlie.like("strawberry")
	charlie.like("pumpkin")
	denise.like("vanilla")
	denise.like("mulberry")
	denise.like("strawberry")
	eddard.like("steel")
	eddard.like("snow")
	fmt.Println("adam should get to know", adam.similar())
}

// output: adam has liked 4 flavours in common with adam
// output: beatrice has liked 1 flavours in common with adam
// output: denise has liked 2 flavours in common with adam
// output: adam should get to know denise

Relevance

For even more data mining juicyness: If we let our users rate the flavours from 0 to 10 we can do some more nifty relevance searches!

We can again find all users that have rated a flavour that we have rated, and for each user sum how similar to us they have rated.

Then we go through their ratings, for each found flavour summing it up with a weight score calculated by our similarity to the found user and dumping it in a mirrored set. Then we just fetch the top hit from the mirror tree of this set. Go code! (no node code here either)

  package main

import (
	"fmt"
	"github.com/zond/god/client"
	"github.com/zond/god/common"
	"github.com/zond/setop"
	"math"
	"math/rand"
)

func abs(i int64) int64 {
	if i < 0 {
		return -i
	}
	return i
}

var conn = client.MustConn("localhost:9191")

type recommendation struct {
	name  string
	score float64
}

type user string

func (self user) rate(f string, score int) {
	// store our rating of this flavour under our key
	conn.SubPut([]byte(self), []byte(f), common.EncodeInt64(int64(score)))
	// store that we have rated this flavour under the flavour key
	conn.SubPut([]byte(f), []byte(self), nil)
}
func (self user) recommended() recommendation {
	// Create a set operation that returns the union of the tasters of all flavors we have rated, just returning the taster key
	op := &setop.SetOp{
		Merge: setop.First,
		Type:  setop.Union,
	}
	// for each flavor we have tried, add the raters of that flavor as a source
	for _, flavour := range conn.Slice([]byte(self), nil, nil, true, true) {
		op.Sources = append(op.Sources, setop.SetOpSource{Key: flavour.Key})
	}
	// designate a dump subset
	dumpkey := []byte(fmt.Sprintf("%v_recommended_%v", self, rand.Int63()))
	// make it mirrored
	conn.SubAddConfiguration(dumpkey, "mirrored", "yes")
	// make sure we clean up after ourselves
	defer conn.SubClear(dumpkey)
	// create a new set operation that sums all flavours rated by all tasters having rated a flavour we have rated
	recOp := &setop.SetOp{
		Merge: setop.FloatSum,
		Type:  setop.Union,
	}
	// execute the first set expression, and for each rater
	for _, u := range conn.SetExpression(setop.SetExpression{
		Op: op,
	}) {
		// if the rater is not us
		if user(u.Key) != self {
			var sum int64
			var count int
			// fetch the intersection of the flavours we and the other taster has tried, subtracting the ratings from each other, and for each match
			for _, f := range conn.SetExpression(setop.SetExpression{
				Code: fmt.Sprintf("(I:IntegerSum %v %v*-1)", self, string(u.Key)),
			}) {
				// sum the similarity between our ratings
				sum += (10 - abs(common.MustDecodeInt64(f.Values[0])))
				count++
			}
			fmt.Printf("%v has %v ratings in common with %v, and they are %v similar\n", string(u.Key), count, self, sum)
			avg_similarity := float64(sum) / float64(count)
			// let the relevance of this user be the average similarity times log(count of common ratings + 1)
			weight := avg_similarity * math.Log(float64(count+1))
			fmt.Printf("this gives them an average similarity of %v, and a weight of %v\n", avg_similarity, weight)
			// add all flavours rated by this rater as a source to the new set operation
			recOp.Sources = append(recOp.Sources, setop.SetOpSource{
				Key:    u.Key,
				Weight: &weight,
			})
		}
	}
	// dump the difference between the new set operation and us (ie the flavours the other rater has tried, but we havent),
	// just returning the first value
	conn.SetExpression(setop.SetExpression{
		Op: &setop.SetOp{
			Type:  setop.Difference,
			Merge: setop.First,
			Sources: []setop.SetOpSource{
				setop.SetOpSource{
					SetOp: recOp,
				},
				setop.SetOpSource{
					Key: []byte(self),
				},
			},
		},
		Dest: dumpkey,
	})
	// return the highest rated recommendation
	best := conn.MirrorReverseSliceLen(dumpkey, nil, true, 1)[0]
	return recommendation{name: string(best.Value), score: common.MustDecodeFloat64(best.Key)}
}

func main() {
	conn.Clear()
	adam := user("adam")
	beatrice := user("beatrice")
	charlie := user("charlie")
	denise := user("denise")
	eddard := user("eddard")
	adam.rate("vanilla", 4)
	adam.rate("strawberry", 1)
	adam.rate("licorice", 10)
	beatrice.rate("vanilla", 2)
	beatrice.rate("licorice", 7)
	beatrice.rate("chocolate", 4)
	charlie.rate("strawberry", 6)
	charlie.rate("chocolate", 3)
	charlie.rate("pumpkin", 10)
	denise.rate("vanilla", 10)
	denise.rate("strawberry", 10)
	denise.rate("licorice", 1)
	eddard.rate("blood", 0)
	eddard.rate("steel", 5)
	eddard.rate("snow", 8)
	fmt.Println("with the data we have, we recommend that adam tries", adam.recommended().name)
}

// output: beatrice has 2 ratings in common with adam, and they are 15 similar
// output: this gives them an average similarity of 7.5, and a weight of 8.239592165010821
// output: charlie has 1 ratings in common with adam, and they are 5 similar
// output: this gives them an average similarity of 5, and a weight of 3.4657359027997265
// output: denise has 3 ratings in common with adam, and they are 6 similar
// output: this gives them an average similarity of 2, and a weight of 2.772588722239781
// output: with the data we have, we recommend that adam tries chocolate