related 0.1

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG ADDED
@@ -0,0 +1,4 @@
1
+
2
+ *0.1*
3
+
4
+ * First public release
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2010, 2011 Niklas Holmgren, Sutajio
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,92 @@
1
+ Related
2
+ =======
3
+
4
+ Related is a Redis-backed high performance graph database.
5
+
6
+ Setup
7
+ -----
8
+
9
+ Assuming you already have Redis installed:
10
+
11
+ $ gem install related
12
+
13
+ Or add the gem to your Gemfile.
14
+
15
+ require 'related'
16
+ Related.redis = 'redis://.../'
17
+
18
+ If you are using Rails, add the above to an initializer. If Redis is running
19
+ on localhost and on the default port the second line is not needed.
20
+
21
+ Example usage
22
+ -------------
23
+
24
+ node = Related::Node.create(:name => 'Example', :popularity => 2.3)
25
+ node.new_record?
26
+ node.popularity = 100
27
+ node.save
28
+ node = Related::Node.find(node.id)
29
+ node.destroy
30
+
31
+ node1 = Related::Node.create
32
+ node2 = Related::Node.create
33
+ rel = Related::Relationship.create(:friends, node1, node2, :have_met => true)
34
+
35
+ n = Related::Node.find(node1.id)
36
+ nn = Related::Node.find(node1.id, node2.id)
37
+
38
+ n = Related::Node.find(node1.id, :fields => [:name])
39
+ nn = Related::Node.find(node1.id, node2.id, :fields => [:name])
40
+
41
+ Nodes and relationships are both sub-classes of the same base class and both
42
+ behave similar to an ActiveRecord object and can store attributes etc.
43
+
44
+ To query the graph:
45
+
46
+ node.outgoing(:friends)
47
+ node.incoming(:friends)
48
+ node.outgoing(:friends).relationships
49
+ node.outgoing(:friends).nodes
50
+ node.outgoing(:friends).limit(5)
51
+ node1.path_to(node2).outgoing(:friends).depth(3)
52
+ node1.shortest_path_to(node2).outgoing(:friends).depth(3)
53
+
54
+ To get the results from a query:
55
+
56
+ node.outgoing(:friends).to_a
57
+ node.outgoing(:friends).count (or .size, which is memoized)
58
+
59
+ You can also do set operations, like union, diff and intersect (not implemented yet):
60
+
61
+ node1.outgoing(:friends).union(node2.outgoing(:friends))
62
+ node1.outgoing(:friends).diff(node2.outgoing(:friends))
63
+ node1.outgoing(:friends).intersect(node2.outgoing(:friends))
64
+
65
+ Development
66
+ -----------
67
+
68
+ If you want to make your own changes to Related, first clone the repo and
69
+ run the tests:
70
+
71
+ git clone git://github.com/sutajio/related.git
72
+ cd related
73
+ rake test
74
+
75
+ Remember to install the Redis server on your local machine.
76
+
77
+ Contributing
78
+ ------------
79
+
80
+ Once you've made your great commits:
81
+
82
+ 1. Fork Related
83
+ 2. Create a topic branch - git checkout -b my_branch
84
+ 3. Push to your branch - git push origin my_branch
85
+ 4. Create a Pull Request from your branch
86
+ 5. That's it!
87
+
88
+ Author
89
+ ------
90
+
91
+ Related was created by Niklas Holmgren (niklas@sutajio.se) and released under
92
+ the MIT license.
data/Rakefile ADDED
@@ -0,0 +1,11 @@
1
+ require 'rake/testtask'
2
+
3
+ $LOAD_PATH.unshift 'lib'
4
+
5
+ task :default => [:test]
6
+
7
+ task :test do
8
+ Dir.glob('test/**/*_test.rb').each do |file|
9
+ require File.expand_path(file)
10
+ end
11
+ end
@@ -0,0 +1,126 @@
1
+ module Related
2
+ class Entity
3
+
4
+ attr_reader :id
5
+ attr_reader :attributes
6
+ attr_reader :destroyed
7
+
8
+ def initialize(*attributes)
9
+ if attributes.first.is_a?(String)
10
+ @id = attributes.first
11
+ @attributes = attributes.last
12
+ else
13
+ @attributes = attributes.first
14
+ end
15
+ end
16
+
17
+ def to_s
18
+ self.id
19
+ end
20
+
21
+ def method_missing(sym, *args, &block)
22
+ @attributes[sym] || @attributes[sym.to_s]
23
+ end
24
+
25
+ def ==(other)
26
+ @id == other.id
27
+ end
28
+
29
+ def new_record?
30
+ @id.nil? ? true : false
31
+ end
32
+
33
+ def save
34
+ create_or_update
35
+ end
36
+
37
+ def destroy
38
+ delete
39
+ end
40
+
41
+ def self.create(attributes = {})
42
+ self.new(attributes).save
43
+ end
44
+
45
+ def self.find(*args)
46
+ options = args.size > 1 && args.last.is_a?(Hash) ? args.pop : {}
47
+ args.size == 1 && args.first.is_a?(String) ?
48
+ find_one(args.first, options) :
49
+ find_many(args.flatten, options)
50
+ end
51
+
52
+ private
53
+
54
+ def create_or_update
55
+ new_record? ? create : update
56
+ end
57
+
58
+ def create
59
+ @id = Related.generate_id
60
+ @attributes.merge!(:created_at => Time.now.utc)
61
+ Related.redis.hmset(@id, *@attributes.to_a.flatten)
62
+ self
63
+ end
64
+
65
+ def update
66
+ @attributes.merge!(:updated_at => Time.now.utc)
67
+ Related.redis.hmset(@id, *@attributes.to_a.flatten)
68
+ self
69
+ end
70
+
71
+ def delete
72
+ Related.redis.del(id)
73
+ @destroyed = true
74
+ self
75
+ end
76
+
77
+ def self.find_fields(id, fields)
78
+ res = Related.redis.hmget(id.to_s, *fields)
79
+ if res
80
+ attributes = {}
81
+ res.each_with_index do |value, i|
82
+ attributes[fields[i]] = value
83
+ end
84
+ attributes
85
+ end
86
+ end
87
+
88
+ def self.find_one(id, options = {})
89
+ attributes = options[:fields] ?
90
+ find_fields(id, options[:fields]) :
91
+ Related.redis.hgetall(id.to_s)
92
+ if attributes.empty?
93
+ if Related.redis.exists(id) == false
94
+ raise Related::NotFound, id
95
+ end
96
+ end
97
+ self.new(id, attributes)
98
+ end
99
+
100
+ def self.find_many(ids, options = {})
101
+ res = Related.redis.pipelined do
102
+ ids.each {|id|
103
+ if options[:fields]
104
+ Related.redis.hmget(id.to_s, *options[:fields])
105
+ else
106
+ Related.redis.hgetall(id.to_s)
107
+ end
108
+ }
109
+ end
110
+ objects = []
111
+ ids.each_with_index do |id,i|
112
+ if options[:fields]
113
+ attributes = {}
114
+ res[i].each_with_index do |value, i|
115
+ attributes[options[:fields][i]] = value
116
+ end
117
+ objects << self.new(id, attributes)
118
+ else
119
+ objects << self.new(id, Hash[*res[i]])
120
+ end
121
+ end
122
+ objects
123
+ end
124
+
125
+ end
126
+ end
@@ -0,0 +1,4 @@
1
+ module Related
2
+ class RelatedException < RuntimeError; end
3
+ class NotFound < RelatedException; end
4
+ end
@@ -0,0 +1,15 @@
1
+ require 'base64'
2
+ require 'digest/sha2'
3
+
4
+ module Related
5
+ module Helpers
6
+
7
+ # Generate a unique id
8
+ def generate_id
9
+ Base64.encode64(
10
+ Digest::SHA256.digest("#{Time.now}-#{rand}")
11
+ ).gsub('/','x').gsub('+','y').gsub('=','').strip[0..21]
12
+ end
13
+
14
+ end
15
+ end
@@ -0,0 +1,174 @@
1
+ module Related
2
+ class Node < Entity
3
+ module QueryMethods
4
+ def relationships
5
+ query = self.query
6
+ query.entity_type = :relationships
7
+ query
8
+ end
9
+
10
+ def nodes
11
+ query = self.query
12
+ query.entity_type = :nodes
13
+ query
14
+ end
15
+
16
+ def outgoing(type)
17
+ query = self.query
18
+ query.relationship_type = type
19
+ query.direction = :out
20
+ query
21
+ end
22
+
23
+ def incoming(type)
24
+ query = self.query
25
+ query.relationship_type = type
26
+ query.direction = :in
27
+ query
28
+ end
29
+
30
+ def limit(count)
31
+ query = self.query
32
+ query.limit = count
33
+ query
34
+ end
35
+
36
+ def depth(depth)
37
+ query = self.query
38
+ query.depth = depth
39
+ query
40
+ end
41
+
42
+ def include_start_node
43
+ query = self.query
44
+ query.include_start_node = true
45
+ query
46
+ end
47
+
48
+ def path_to(node)
49
+ query = self.query
50
+ query.destination = node
51
+ query.search_algorithm = :depth_first
52
+ query
53
+ end
54
+
55
+ def shortest_path_to(node)
56
+ query = self.query
57
+ query.destination = node
58
+ query.search_algorithm = :dijkstra
59
+ query
60
+ end
61
+ end
62
+
63
+ include QueryMethods
64
+
65
+ class Query
66
+ include QueryMethods
67
+
68
+ attr_writer :entity_type
69
+ attr_writer :relationship_type
70
+ attr_writer :direction
71
+ attr_writer :limit
72
+ attr_writer :depth
73
+ attr_writer :include_start_node
74
+ attr_writer :destination
75
+ attr_writer :search_algorithm
76
+
77
+ def initialize(node)
78
+ @node = node
79
+ @entity_type = :nodes
80
+ @depth = 4
81
+ end
82
+
83
+ def each(&block)
84
+ self.to_a.each(&block)
85
+ end
86
+
87
+ def map(&block)
88
+ self.to_a.map(&block)
89
+ end
90
+
91
+ def to_a
92
+ res = []
93
+ if @destination
94
+ res = self.send(@search_algorithm, [@node.id])
95
+ res.shift unless @include_start_node
96
+ return Related::Node.find(res)
97
+ else
98
+ if @limit
99
+ res = (1..@limit.to_i).map { Related.redis.srandmember(key) }
100
+ else
101
+ res = Related.redis.smembers(key)
102
+ end
103
+ end
104
+ res = Relationship.find(res)
105
+ if @entity_type == :nodes
106
+ res = Related::Node.find(res.map {|rel| @direction == :in ? rel.start_node_id : rel.end_node_id })
107
+ res.unshift(@node) if @include_start_node
108
+ end
109
+ res
110
+ end
111
+
112
+ def count
113
+ @count = Related.redis.scard(key)
114
+ @limit && @count > @limit ? @limit : @count
115
+ end
116
+
117
+ def size
118
+ @count || count
119
+ end
120
+
121
+ protected
122
+
123
+ def key
124
+ "#{@node.id}:rel:#{@relationship_type}:#{@direction}"
125
+ end
126
+
127
+ def query
128
+ self
129
+ end
130
+
131
+ def depth_first(nodes, depth = 0)
132
+ return [] if depth > @depth
133
+ nodes.each do |node|
134
+ key = "#{node}:nodes:#{@relationship_type}:#{@direction}"
135
+ if Related.redis.sismember(key, @destination.id)
136
+ return [node, @destination.id]
137
+ else
138
+ res = depth_first(Related.redis.smembers(key), depth+1)
139
+ return [node] + res unless res.empty?
140
+ end
141
+ end
142
+ return []
143
+ end
144
+
145
+ def dijkstra(nodes, depth = 0)
146
+ return [] if depth > @depth
147
+ shortest_path = []
148
+ nodes.each do |node|
149
+ key = "#{node}:nodes:#{@relationship_type}:#{@direction}"
150
+ if Related.redis.sismember(key, @destination.id)
151
+ return [node, @destination.id]
152
+ else
153
+ res = dijkstra(Related.redis.smembers(key), depth+1)
154
+ if res.size > 0
155
+ res = [node] + res
156
+ if res.size < shortest_path.size || shortest_path.size == 0
157
+ shortest_path = res
158
+ end
159
+ end
160
+ end
161
+ end
162
+ return shortest_path
163
+ end
164
+
165
+ end
166
+
167
+ protected
168
+
169
+ def query
170
+ Query.new(self)
171
+ end
172
+
173
+ end
174
+ end
@@ -0,0 +1,54 @@
1
+ module Related
2
+ class Relationship < Entity
3
+
4
+ def initialize(*attributes)
5
+ if attributes.first.is_a?(String)
6
+ @id = attributes.first
7
+ end
8
+ @attributes = attributes.last
9
+ end
10
+
11
+ def start_node
12
+ @start_node ||= Related::Node.find(start_node_id)
13
+ end
14
+
15
+ def end_node
16
+ @end_node ||= Related::Node.find(end_node_id)
17
+ end
18
+
19
+ def self.create(type, node1, node2, attributes = {})
20
+ self.new(attributes.merge(
21
+ :type => type,
22
+ :start_node_id => node1.to_s,
23
+ :end_node_id => node2.to_s
24
+ )).save
25
+ end
26
+
27
+ private
28
+
29
+ def create
30
+ Related.redis.multi do
31
+ super
32
+ Related.redis.sadd("#{self.start_node_id}:rel:#{type}:out", self.id)
33
+ Related.redis.sadd("#{self.end_node_id}:rel:#{type}:in", self.id)
34
+
35
+ Related.redis.sadd("#{self.start_node_id}:nodes:#{type}:out", self.end_node_id)
36
+ Related.redis.sadd("#{self.end_node_id}:nodes:#{type}:in", self.start_node_id)
37
+ end
38
+ self
39
+ end
40
+
41
+ def delete
42
+ Related.redis.multi do
43
+ Related.redis.srem("#{self.start_node_id}:rel:#{type}:out", self.id)
44
+ Related.redis.srem("#{self.end_node_id}:rel:#{type}:in", self.id)
45
+
46
+ Related.redis.srem("#{self.start_node_id}:nodes:#{type}:out", self.end_node_id)
47
+ Related.redis.srem("#{self.end_node_id}:nodes:#{type}:in", self.start_node_id)
48
+ super
49
+ end
50
+ self
51
+ end
52
+
53
+ end
54
+ end
@@ -0,0 +1,3 @@
1
+ module Related
2
+ Version = VERSION = '0.1'
3
+ end
data/lib/related.rb ADDED
@@ -0,0 +1,49 @@
1
+ require 'redis'
2
+ require 'redis/namespace'
3
+
4
+ require 'related/version'
5
+ require 'related/helpers'
6
+ require 'related/exceptions'
7
+ require 'related/entity'
8
+ require 'related/node'
9
+ require 'related/relationship'
10
+
11
+ module Related
12
+ include Helpers
13
+ extend self
14
+
15
+ # Accepts:
16
+ # 1. A 'hostname:port' string
17
+ # 2. A 'hostname:port:db' string (to select the Redis db)
18
+ # 3. A 'hostname:port/namespace' string (to set the Redis namespace)
19
+ # 4. A redis URL string 'redis://host:port'
20
+ # 5. An instance of `Redis`, `Redis::Client`, `Redis::DistRedis`,
21
+ # or `Redis::Namespace`.
22
+ def redis=(server)
23
+ if server.respond_to? :split
24
+ if server =~ /redis\:\/\//
25
+ redis = Redis.connect(:url => server)
26
+ else
27
+ server, namespace = server.split('/', 2)
28
+ host, port, db = server.split(':')
29
+ redis = Redis.new(:host => host, :port => port,
30
+ :thread_safe => true, :db => db)
31
+ end
32
+ namespace ||= :related
33
+ @redis = Redis::Namespace.new(namespace, :redis => redis)
34
+ elsif server.respond_to? :namespace=
35
+ @redis = server
36
+ else
37
+ @redis = Redis::Namespace.new(:related, :redis => server)
38
+ end
39
+ end
40
+
41
+ # Returns the current Redis connection. If none has been created, will
42
+ # create a new one.
43
+ def redis
44
+ return @redis if @redis
45
+ self.redis = 'localhost:6379'
46
+ self.redis
47
+ end
48
+
49
+ end
@@ -0,0 +1,75 @@
1
+ require File.expand_path('test/test_helper')
2
+ require 'benchmark'
3
+
4
+ class PerformanceTest < Test::Unit::TestCase
5
+
6
+ def setup
7
+ Related.redis.flushall
8
+ end
9
+
10
+ def test_simple
11
+ puts "Simple:"
12
+ node = Related::Node.create
13
+ time = Benchmark.measure do
14
+ 1000.times do
15
+ n = Related::Node.create
16
+ rel = Related::Relationship.create(:friends, node, n)
17
+ end
18
+ end
19
+ puts time
20
+ time = Benchmark.measure do
21
+ node.outgoing(:friends).to_a
22
+ end
23
+ puts time
24
+ end
25
+
26
+ def test_with_attributes
27
+ puts "With attributes:"
28
+ node = Related::Node.create
29
+ time = Benchmark.measure do
30
+ 1000.times do
31
+ n = Related::Node.create(
32
+ :title => 'Example title',
33
+ :description => 'Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.',
34
+ :status => 'archived',
35
+ :length => 42.3,
36
+ :enabled => true)
37
+ rel = Related::Relationship.create(:friends, node, n, :weight => 2.5, :list => 'Co-workers')
38
+ end
39
+ end
40
+ puts time
41
+ time = Benchmark.measure do
42
+ node.outgoing(:friends).to_a
43
+ end
44
+ puts time
45
+ end
46
+
47
+ def test_search
48
+ puts "Search:"
49
+ node = Related::Node.create
50
+ time = Benchmark.measure do
51
+ 10.times do
52
+ n = Related::Node.create
53
+ rel = Related::Relationship.create(:friends, node, n)
54
+ 10.times do
55
+ n2 = Related::Node.create
56
+ rel2 = Related::Relationship.create(:friends, n, n2)
57
+ 10.times do
58
+ n3 = Related::Node.create
59
+ rel3 = Related::Relationship.create(:friends, n2, n3)
60
+ 10.times do
61
+ n4 = Related::Node.create
62
+ rel4 = Related::Relationship.create(:friends, n3, n4)
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
68
+ puts time
69
+ time = Benchmark.measure do
70
+ node.outgoing(:friends).path_to(Related::Node.create)
71
+ end
72
+ puts time
73
+ end
74
+
75
+ end
@@ -0,0 +1,115 @@
1
+ # Redis configuration file example
2
+
3
+ # By default Redis does not run as a daemon. Use 'yes' if you need it.
4
+ # Note that Redis will write a pid file in /var/run/redis.pid when daemonized.
5
+ daemonize yes
6
+
7
+ # When run as a daemon, Redis write a pid file in /var/run/redis.pid by default.
8
+ # You can specify a custom pid file location here.
9
+ pidfile ./test/redis-test.pid
10
+
11
+ # Accept connections on the specified port, default is 6379
12
+ port 9736
13
+
14
+ # If you want you can bind a single interface, if the bind option is not
15
+ # specified all the interfaces will listen for connections.
16
+ #
17
+ # bind 127.0.0.1
18
+
19
+ # Close the connection after a client is idle for N seconds (0 to disable)
20
+ timeout 300
21
+
22
+ # Save the DB on disk:
23
+ #
24
+ # save <seconds> <changes>
25
+ #
26
+ # Will save the DB if both the given number of seconds and the given
27
+ # number of write operations against the DB occurred.
28
+ #
29
+ # In the example below the behaviour will be to save:
30
+ # after 900 sec (15 min) if at least 1 key changed
31
+ # after 300 sec (5 min) if at least 10 keys changed
32
+ # after 60 sec if at least 10000 keys changed
33
+ save 900 1
34
+ save 300 10
35
+ save 60 10000
36
+
37
+ # The filename where to dump the DB
38
+ dbfilename dump.rdb
39
+
40
+ # For default save/load DB in/from the working directory
41
+ # Note that you must specify a directory not a file name.
42
+ dir ./test/
43
+
44
+ # Set server verbosity to 'debug'
45
+ # it can be one of:
46
+ # debug (a lot of information, useful for development/testing)
47
+ # notice (moderately verbose, what you want in production probably)
48
+ # warning (only very important / critical messages are logged)
49
+ loglevel debug
50
+
51
+ # Specify the log file name. Also 'stdout' can be used to force
52
+ # the demon to log on the standard output. Note that if you use standard
53
+ # output for logging but daemonize, logs will be sent to /dev/null
54
+ logfile stdout
55
+
56
+ # Set the number of databases. The default database is DB 0, you can select
57
+ # a different one on a per-connection basis using SELECT <dbid> where
58
+ # dbid is a number between 0 and 'databases'-1
59
+ databases 16
60
+
61
+ ################################# REPLICATION #################################
62
+
63
+ # Master-Slave replication. Use slaveof to make a Redis instance a copy of
64
+ # another Redis server. Note that the configuration is local to the slave
65
+ # so for example it is possible to configure the slave to save the DB with a
66
+ # different interval, or to listen to another port, and so on.
67
+
68
+ # slaveof <masterip> <masterport>
69
+
70
+ ################################## SECURITY ###################################
71
+
72
+ # Require clients to issue AUTH <PASSWORD> before processing any other
73
+ # commands. This might be useful in environments in which you do not trust
74
+ # others with access to the host running redis-server.
75
+ #
76
+ # This should stay commented out for backward compatibility and because most
77
+ # people do not need auth (e.g. they run their own servers).
78
+
79
+ # requirepass foobared
80
+
81
+ ################################### LIMITS ####################################
82
+
83
+ # Set the max number of connected clients at the same time. By default there
84
+ # is no limit, and it's up to the number of file descriptors the Redis process
85
+ # is able to open. The special value '0' means no limts.
86
+ # Once the limit is reached Redis will close all the new connections sending
87
+ # an error 'max number of clients reached'.
88
+
89
+ # maxclients 128
90
+
91
+ # Don't use more memory than the specified amount of bytes.
92
+ # When the memory limit is reached Redis will try to remove keys with an
93
+ # EXPIRE set. It will try to start freeing keys that are going to expire
94
+ # in little time and preserve keys with a longer time to live.
95
+ # Redis will also try to remove objects from free lists if possible.
96
+ #
97
+ # If all this fails, Redis will start to reply with errors to commands
98
+ # that will use more memory, like SET, LPUSH, and so on, and will continue
99
+ # to reply to most read-only commands like GET.
100
+ #
101
+ # WARNING: maxmemory can be a good idea mainly if you want to use Redis as a
102
+ # 'state' server or cache, not as a real DB. When Redis is used as a real
103
+ # database the memory usage will grow over the weeks, it will be obvious if
104
+ # it is going to use too much memory in the long run, and you'll have the time
105
+ # to upgrade. With maxmemory after the limit is reached you'll start to get
106
+ # errors for write operations, and this may even lead to DB inconsistency.
107
+
108
+ # maxmemory <bytes>
109
+
110
+ ############################### ADVANCED CONFIG ###############################
111
+
112
+ # Glue small output buffers together in order to send small replies in a
113
+ # single TCP packet. Uses a bit more CPU but most of the times it is a win
114
+ # in terms of number of queries per second. Use 'yes' if unsure.
115
+ glueoutputbuf yes
@@ -0,0 +1,203 @@
1
+ require File.expand_path('test/test_helper')
2
+
3
+ class RelatedTest < Test::Unit::TestCase
4
+
5
+ def setup
6
+ Related.redis.flushall
7
+ end
8
+
9
+ def test_can_set_a_namespace_through_a_url_like_string
10
+ assert Related.redis
11
+ assert_equal :related, Related.redis.namespace
12
+ Related.redis = 'localhost:9736/namespace'
13
+ assert_equal 'namespace', Related.redis.namespace
14
+ end
15
+
16
+ def test_can_create_node
17
+ assert Related::Node.create
18
+ end
19
+
20
+ def test_can_create_node_with_attributes
21
+ node = Related::Node.create(:name => 'Example', :popularity => 12.3)
22
+ assert node
23
+ assert_equal 'Example', node.name
24
+ assert_equal 12.3, node.popularity
25
+ end
26
+
27
+ def test_can_create_node_and_then_find_it_again_using_its_id
28
+ node1 = Related::Node.create(:name => 'Example', :popularity => 12.3)
29
+ node2 = Related::Node.find(node1.id)
30
+ assert node2
31
+ assert_equal node1.id, node2.id
32
+ assert_equal 'Example', node2.name
33
+ assert_equal '12.3', node2.popularity
34
+ end
35
+
36
+ def test_can_find_many_nodes_at_the_same_time
37
+ node1 = Related::Node.create(:name => 'One')
38
+ node2 = Related::Node.create(:name => 'Two')
39
+ one, two = Related::Node.find(node1.id, node2.id)
40
+ assert one
41
+ assert two
42
+ end
43
+
44
+ def test_will_raise_exception_when_a_node_is_not_found
45
+ assert_raises Related::NotFound do
46
+ Related::Node.find('foo')
47
+ end
48
+ end
49
+
50
+ def test_can_update_node_with_new_attributes
51
+ node1 = Related::Node.create(:name => 'Example', :popularity => 12.3)
52
+ node1.description = 'Example description.'
53
+ node1.save
54
+ node2 = Related::Node.find(node1.id)
55
+ assert_equal node1.description, node2.description
56
+ end
57
+
58
+ def test_two_nodes_with_the_same_id_should_be_equal
59
+ assert_equal Related::Node.new('test', {}), Related::Node.new('test', {})
60
+ end
61
+
62
+ def test_can_find_a_node_and_only_load_specific_attributes
63
+ node1 = Related::Node.create(:name => 'Example', :popularity => 12.3)
64
+ node2 = Related::Node.find(node1.id, :fields => [:does_not_exist, :name])
65
+ assert_equal node1.name, node2.name
66
+ assert_nil node2.does_not_exist
67
+ assert_nil node2.popularity
68
+ end
69
+
70
+ def test_can_find_multiple_nodes_and_only_load_specific_attributes
71
+ node1 = Related::Node.create(:name => 'Example 1', :popularity => 12.3)
72
+ node2 = Related::Node.create(:name => 'Example 2', :popularity => 42.5)
73
+ n = Related::Node.find(node1.id, node2.id, :fields => [:does_not_exist, :name])
74
+ assert_equal 2, n.size
75
+ assert_equal 'Example 1', n.first.name
76
+ assert_nil n.first.does_not_exist
77
+ assert_nil n.first.popularity
78
+ assert_equal 'Example 2', n.last.name
79
+ assert_nil n.last.does_not_exist
80
+ assert_nil n.last.popularity
81
+ end
82
+
83
+ def test_can_destroy_node
84
+ node = Related::Node.create
85
+ node.destroy
86
+ assert_raises Related::NotFound do
87
+ Related::Node.find(node.id)
88
+ end
89
+ end
90
+
91
+ def test_can_create_a_relationship_between_two_nodes
92
+ node1 = Related::Node.create(:name => 'One')
93
+ node2 = Related::Node.create(:name => 'Two')
94
+ rel = Related::Relationship.create(:friends, node1, node2)
95
+ assert_equal [rel], node1.outgoing(:friends).relationships.to_a
96
+ assert_equal [node2], node1.outgoing(:friends).to_a
97
+ assert_equal [], node1.incoming(:friends).to_a
98
+ assert_equal [node1], node2.incoming(:friends).to_a
99
+ assert_equal [], node2.outgoing(:friends).to_a
100
+ end
101
+
102
+ def test_can_create_a_relationship_with_attributes
103
+ node1 = Related::Node.create(:name => 'One')
104
+ node2 = Related::Node.create(:name => 'Two')
105
+ rel = Related::Relationship.create(:friends, node1, node2, :weight => 2.5)
106
+ rel = Related::Relationship.find(rel.id)
107
+ assert_equal '2.5', rel.weight
108
+ end
109
+
110
+ def test_can_delete_a_relationship
111
+ node1 = Related::Node.create(:name => 'One')
112
+ node2 = Related::Node.create(:name => 'Two')
113
+ rel = Related::Relationship.create(:friends, node1, node2)
114
+ rel.destroy
115
+ assert_equal [], node1.outgoing(:friends).to_a
116
+ assert_equal [], node1.incoming(:friends).to_a
117
+ assert_equal [], node2.incoming(:friends).to_a
118
+ assert_equal [], node2.outgoing(:friends).to_a
119
+ assert_raises Related::NotFound do
120
+ Related::Relationship.find(rel.id)
121
+ end
122
+ end
123
+
124
+ def test_can_limit_the_number_of_nodes_returned_from_a_query
125
+ node1 = Related::Node.create
126
+ node2 = Related::Node.create
127
+ node3 = Related::Node.create
128
+ node4 = Related::Node.create
129
+ node5 = Related::Node.create
130
+ Related::Relationship.create(:friends, node1, node2)
131
+ Related::Relationship.create(:friends, node1, node3)
132
+ Related::Relationship.create(:friends, node1, node4)
133
+ Related::Relationship.create(:friends, node1, node5)
134
+ assert_equal 3, node1.outgoing(:friends).limit(3).to_a.size
135
+ end
136
+
137
+ def test_can_count_the_number_of_related_nodes
138
+ node1 = Related::Node.create
139
+ node2 = Related::Node.create
140
+ node3 = Related::Node.create
141
+ node4 = Related::Node.create
142
+ node5 = Related::Node.create
143
+ rel1 = Related::Relationship.create(:friends, node1, node2)
144
+ rel1 = Related::Relationship.create(:friends, node1, node3)
145
+ rel1 = Related::Relationship.create(:friends, node1, node4)
146
+ rel1 = Related::Relationship.create(:friends, node1, node5)
147
+ assert_equal 4, node1.outgoing(:friends).count
148
+ assert_equal 4, node1.outgoing(:friends).size
149
+ assert_equal 3, node1.outgoing(:friends).limit(3).count
150
+ assert_equal 4, node1.outgoing(:friends).limit(5).count
151
+ end
152
+
153
+ def test_can_find_path_between_two_nodes
154
+ node1 = Related::Node.create
155
+ node2 = Related::Node.create
156
+ node3 = Related::Node.create
157
+ node4 = Related::Node.create
158
+ node5 = Related::Node.create
159
+ node6 = Related::Node.create
160
+ node7 = Related::Node.create
161
+ node8 = Related::Node.create
162
+ rel1 = Related::Relationship.create(:friends, node1, node2)
163
+ rel1 = Related::Relationship.create(:friends, node2, node3)
164
+ rel1 = Related::Relationship.create(:friends, node3, node2)
165
+ rel1 = Related::Relationship.create(:friends, node3, node4)
166
+ rel1 = Related::Relationship.create(:friends, node4, node5)
167
+ rel1 = Related::Relationship.create(:friends, node5, node3)
168
+ rel1 = Related::Relationship.create(:friends, node5, node8)
169
+ rel1 = Related::Relationship.create(:friends, node2, node5)
170
+ rel1 = Related::Relationship.create(:friends, node2, node6)
171
+ rel1 = Related::Relationship.create(:friends, node6, node7)
172
+ rel1 = Related::Relationship.create(:friends, node7, node8)
173
+ assert_equal node8, node1.path_to(node8).outgoing(:friends).depth(5).to_a.last
174
+ assert_equal node1, node1.path_to(node8).outgoing(:friends).depth(5).include_start_node.to_a.first
175
+ assert_equal node1, node8.path_to(node1).incoming(:friends).depth(5).to_a.last
176
+ assert_equal node8, node8.path_to(node1).incoming(:friends).depth(5).include_start_node.to_a.first
177
+ end
178
+
179
+ def test_can_find_shortest_path_between_two_nodes
180
+ node1 = Related::Node.create
181
+ node2 = Related::Node.create
182
+ node3 = Related::Node.create
183
+ node4 = Related::Node.create
184
+ node5 = Related::Node.create
185
+ node6 = Related::Node.create
186
+ node7 = Related::Node.create
187
+ node8 = Related::Node.create
188
+ rel1 = Related::Relationship.create(:friends, node1, node2)
189
+ rel1 = Related::Relationship.create(:friends, node2, node3)
190
+ rel1 = Related::Relationship.create(:friends, node3, node2)
191
+ rel1 = Related::Relationship.create(:friends, node3, node4)
192
+ rel1 = Related::Relationship.create(:friends, node4, node5)
193
+ rel1 = Related::Relationship.create(:friends, node5, node3)
194
+ rel1 = Related::Relationship.create(:friends, node5, node8)
195
+ rel1 = Related::Relationship.create(:friends, node2, node5)
196
+ rel1 = Related::Relationship.create(:friends, node2, node6)
197
+ rel1 = Related::Relationship.create(:friends, node6, node7)
198
+ rel1 = Related::Relationship.create(:friends, node7, node8)
199
+ assert_equal [node2,node5,node8], node1.shortest_path_to(node8).outgoing(:friends).depth(5).to_a
200
+ assert_equal [node1,node2,node5,node8], node1.shortest_path_to(node8).outgoing(:friends).depth(5).include_start_node.to_a
201
+ end
202
+
203
+ end
@@ -0,0 +1,42 @@
1
+
2
+ dir = File.dirname(File.expand_path(__FILE__))
3
+ $LOAD_PATH.unshift dir + '/../lib'
4
+
5
+ require 'test/unit'
6
+ require 'related'
7
+
8
+ #
9
+ # make sure we can run redis
10
+ #
11
+
12
+ if !system("which redis-server")
13
+ puts '', "** can't find `redis-server` in your path"
14
+ puts "** try running `sudo rake install`"
15
+ abort ''
16
+ end
17
+
18
+
19
+ #
20
+ # start our own redis when the tests start,
21
+ # kill it when they end
22
+ #
23
+
24
+ at_exit do
25
+ next if $!
26
+
27
+ if defined?(MiniTest)
28
+ exit_code = MiniTest::Unit.new.run(ARGV)
29
+ else
30
+ exit_code = Test::Unit::AutoRunner.run
31
+ end
32
+
33
+ pid = `ps -A -o pid,command | grep [r]edis-test`.split(" ")[0]
34
+ puts "Killing test redis server..."
35
+ `rm -f #{dir}/dump.rdb`
36
+ Process.kill("KILL", pid.to_i)
37
+ exit exit_code
38
+ end
39
+
40
+ puts "Starting redis for testing at localhost:9736..."
41
+ `redis-server #{dir}/redis-test.conf`
42
+ Related.redis = 'localhost:9736'
metadata ADDED
@@ -0,0 +1,107 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: related
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 0
7
+ - 1
8
+ version: "0.1"
9
+ platform: ruby
10
+ authors:
11
+ - Niklas Holmgren
12
+ autorequire:
13
+ bindir: bin
14
+ cert_chain: []
15
+
16
+ date: 2011-09-09 00:00:00 +02:00
17
+ default_executable:
18
+ dependencies:
19
+ - !ruby/object:Gem::Dependency
20
+ name: redis
21
+ prerelease: false
22
+ requirement: &id001 !ruby/object:Gem::Requirement
23
+ none: false
24
+ requirements:
25
+ - - ">"
26
+ - !ruby/object:Gem::Version
27
+ segments:
28
+ - 2
29
+ - 0
30
+ - 0
31
+ version: 2.0.0
32
+ type: :runtime
33
+ version_requirements: *id001
34
+ - !ruby/object:Gem::Dependency
35
+ name: redis-namespace
36
+ prerelease: false
37
+ requirement: &id002 !ruby/object:Gem::Requirement
38
+ none: false
39
+ requirements:
40
+ - - ">"
41
+ - !ruby/object:Gem::Version
42
+ segments:
43
+ - 0
44
+ - 8
45
+ - 0
46
+ version: 0.8.0
47
+ type: :runtime
48
+ version_requirements: *id002
49
+ description: Related is a Redis-backed high performance graph database.
50
+ email: niklas@sutajio.se
51
+ executables: []
52
+
53
+ extensions: []
54
+
55
+ extra_rdoc_files:
56
+ - LICENSE
57
+ - README.md
58
+ files:
59
+ - README.md
60
+ - Rakefile
61
+ - LICENSE
62
+ - CHANGELOG
63
+ - lib/related/entity.rb
64
+ - lib/related/exceptions.rb
65
+ - lib/related/helpers.rb
66
+ - lib/related/node.rb
67
+ - lib/related/relationship.rb
68
+ - lib/related/version.rb
69
+ - lib/related.rb
70
+ - test/performance_test.rb
71
+ - test/redis-test.conf
72
+ - test/related_test.rb
73
+ - test/test_helper.rb
74
+ has_rdoc: true
75
+ homepage: http://github.com/sutajio/related/
76
+ licenses: []
77
+
78
+ post_install_message:
79
+ rdoc_options:
80
+ - --charset=UTF-8
81
+ require_paths:
82
+ - lib
83
+ required_ruby_version: !ruby/object:Gem::Requirement
84
+ none: false
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ segments:
89
+ - 0
90
+ version: "0"
91
+ required_rubygems_version: !ruby/object:Gem::Requirement
92
+ none: false
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ segments:
97
+ - 0
98
+ version: "0"
99
+ requirements: []
100
+
101
+ rubyforge_project:
102
+ rubygems_version: 1.3.7
103
+ signing_key:
104
+ specification_version: 3
105
+ summary: Related is a Redis-backed high performance graph database.
106
+ test_files: []
107
+