acts_as_oqgraph 0.1.2 → 0.1.3
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +64 -14
- data/VERSION +1 -1
- data/acts_as_oqgraph.gemspec +1 -1
- data/lib/graph_edge.rb +1 -2
- data/test/test_acts_as_oqgraph.rb +16 -1
- metadata +2 -2
data/README.rdoc
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
|
1
|
+
= ActsAsOQGraph
|
2
2
|
|
3
3
|
This gem can be used with ActiveRecord to access the features of the OQGraph
|
4
4
|
MySQL plugin.
|
@@ -11,7 +11,7 @@ pros and cons of both approaches, It really depends on your needs. Both librarie
|
|
11
11
|
use the C++ Boost graph library at the bottom layers. OQGraph has expensive
|
12
12
|
insert operations but is very fast at delivering the graph data and finding paths in a single SQL query.
|
13
13
|
|
14
|
-
|
14
|
+
== Concepts
|
15
15
|
|
16
16
|
The term graph we are using here is a mathematical one not a pretty picture (sorry designers).
|
17
17
|
For more see: http://en.wikipedia.org/wiki/Graph_(mathematics)
|
@@ -24,7 +24,7 @@ one each way between each node.
|
|
24
24
|
Edges can be assigned positive floating point values called weights. Weights default to 1.0
|
25
25
|
The weights are used in shortest path calculations, a path is shorter if the sum of weights over each edge is smaller.
|
26
26
|
|
27
|
-
|
27
|
+
== What you can do with OQGraph?
|
28
28
|
|
29
29
|
Imagine your shiny new social networking app, FarceBook.
|
30
30
|
You have lots and lots of users each with several friends. How do you find the friends of friends?
|
@@ -43,9 +43,9 @@ and I get the whole tree of friends of friends etc...
|
|
43
43
|
|
44
44
|
Imagine you have a maze to solve. With OQGraph the solution is as simple as: start_cell.shortest_path_to(finish_cell).
|
45
45
|
|
46
|
-
It's good for representing tree structures, networks, routes between cities
|
46
|
+
It's good for representing tree structures, networks, routes between cities etc.
|
47
47
|
|
48
|
-
|
48
|
+
== Usage
|
49
49
|
|
50
50
|
In your node model file:
|
51
51
|
|
@@ -53,16 +53,24 @@ In your node model file:
|
|
53
53
|
acts_as_oqgraph
|
54
54
|
end
|
55
55
|
|
56
|
-
|
56
|
+
=== Options
|
57
|
+
Given in a hash appended to the acts_as_oqgrpah call.
|
57
58
|
|
59
|
+
acts_as_oqgraph :class_name => 'FooLinks',
|
60
|
+
:table_name => "my_foo_links",
|
61
|
+
:oqgraph_table_name => 'foo_oqgraph',
|
62
|
+
:from_key => 'origin',
|
63
|
+
:to_key => 'dest'
|
64
|
+
:weight_column => 'weight'
|
65
|
+
|
58
66
|
* class_name - The name of the edge class, defaults to current class name appended with Edge. eg FooEdge
|
59
67
|
* table_name - The name of the edge table, defaults to table name of the specified class, eg foo_edges
|
60
68
|
* oqgraph_table_name - the name of the volatile oqgraph table. Default foo_edge_oqgraph
|
61
69
|
* from_key - The from key field in the edge table. Default 'from_id'
|
62
70
|
* to_key - The to key field in the edge table. Default: 'to_id'
|
63
|
-
* weight_column - The weight field in the edge table.
|
71
|
+
* weight_column - The weight field in the edge table. Default: weight
|
64
72
|
|
65
|
-
|
73
|
+
== Setup
|
66
74
|
|
67
75
|
This gem requires the use of MySQL or MariaDB with the OQGraph engine plugin.
|
68
76
|
For details of this see: http://openquery.com/products/graph-engine
|
@@ -85,9 +93,9 @@ The associations are:
|
|
85
93
|
edge_model.from
|
86
94
|
|
87
95
|
|
88
|
-
|
96
|
+
= Examples of Use
|
89
97
|
|
90
|
-
Creating edges:
|
98
|
+
=== Creating edges:
|
91
99
|
foo.create_edge_to(bar)
|
92
100
|
foo.create_edge_to_and_from(bar)
|
93
101
|
|
@@ -105,7 +113,7 @@ Removing a edge:
|
|
105
113
|
foo.remove_edge_from(bar)
|
106
114
|
Note that these calls remove ALL edges to bar from foo
|
107
115
|
|
108
|
-
Examining edges:
|
116
|
+
=== Examining edges:
|
109
117
|
|
110
118
|
What nodes point to this one?
|
111
119
|
Gives us an array of nodes that are connected to this one in the inward (from) direction.
|
@@ -119,14 +127,20 @@ Includes the node calling the method.
|
|
119
127
|
bar.reachable
|
120
128
|
foo.reachable?(baz)
|
121
129
|
|
122
|
-
Path Finding:
|
130
|
+
=== Path Finding:
|
123
131
|
foo.shortest_path_to(baz)
|
124
132
|
returns [foo, bar,baz]
|
133
|
+
|
134
|
+
The shortest path to can take a :method which can either be :dijikstra (the default)
|
135
|
+
or :breadth_first. The breadth first method does not take weights into account.
|
136
|
+
It is faster in some cases.
|
137
|
+
|
138
|
+
foo.shortest_path_to(baz, :method => :breadth_first)
|
125
139
|
|
126
140
|
All these methods return the node object with an additional weight field.
|
127
141
|
This enables you to query the weights associated with the edges found.
|
128
142
|
|
129
|
-
|
143
|
+
== Behind the Scenes
|
130
144
|
|
131
145
|
When you declare acts_as_oqgraph then the edge class gets created. You can add extra functionality
|
132
146
|
if you wish to the edge class by the usual Ruby monkey patching methods.
|
@@ -137,6 +151,42 @@ When your application starts up it will put all the edges into the graph table a
|
|
137
151
|
they are created, deleted or modified. This could slow things down at startup but caching classes in production
|
138
152
|
means it does not happen on every request. I will add functionality to only update the oqgraph table if the
|
139
153
|
db server has been stopped, once I figure out the best way to do it.
|
140
|
-
|
154
|
+
|
155
|
+
== How fast is it?
|
156
|
+
I've tested with an application with 10000 nodes and 0 to 9 links from each.
|
157
|
+
|
158
|
+
For a node connected to most of the network finding all reachable nodes averages at about 300ms.
|
159
|
+
This is strongly dependent on how well connected the graph is.
|
160
|
+
|
161
|
+
To find shortest paths between nodes takes about 5 to 10ms per request.
|
162
|
+
Here's an example request:
|
163
|
+
Processing OqgraphUsersController#show_path (for 127.0.0.1 at 2010-07-21 17:09:59) [GET]
|
164
|
+
Parameters: {"id"=>"223", "other_id"=>"2333"}
|
165
|
+
OqgraphUser Load (0.3ms) SELECT * FROM `oqgraph_users` WHERE (`oqgraph_users`.`id` = 223)
|
166
|
+
OqgraphUser Load (0.1ms) SELECT * FROM `oqgraph_users` WHERE (`oqgraph_users`.`id` = 2333)
|
167
|
+
OqgraphUser Load (2.2ms) SELECT oqgraph_users.id,oqgraph_users.first_name,oqgraph_users.last_name, oqgraph_user_oqgraph.weight FROM oqgraph_user_oqgraph JOIN oqgraph_users ON (linkid=id) WHERE latch = 1 AND origid = 223 AND destid = 2333
|
168
|
+
ORDER BY seq;
|
169
|
+
|
170
|
+
Rendering oqgraph_users/show_path
|
171
|
+
Completed in 6ms (View: 2, DB: 3) | 200 OK [http://localhost/oqgraph_users/223/path_to/2333]
|
172
|
+
|
173
|
+
Of course YMMV.
|
174
|
+
|
175
|
+
== Hairy bits, bugs and gotchas
|
176
|
+
|
177
|
+
To keep the oqgraph table up to date the edge model copies all of it records in when first instantiated.
|
178
|
+
This means that if you do not have
|
179
|
+
config.cache_classes = true
|
180
|
+
in your environment and you have a lot of nodes your app will be dog slow. Also the app will be slow to start at first if
|
181
|
+
there are many nodes even when caches are cached.
|
182
|
+
|
183
|
+
I've encountered a bug where the oqgraph table occasionally needs to be dropped and rebuilt. It's being tracked down.
|
184
|
+
If you are not getting any results from the oqgrah table try dropping it and restarting the app.
|
185
|
+
|
186
|
+
I'm working on a way to tell if the oqgraph table is stale. Suggestions would be welcome.
|
187
|
+
|
188
|
+
There is a method 'in_edges' in the edge model class. This does not currently work. 'out_edges' does.
|
189
|
+
|
190
|
+
This gem is very new, the API is liable to change until it stabilises.
|
141
191
|
|
142
192
|
Copyright (c) 2010 Stuart Coyle, released under the MIT license
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.1.
|
1
|
+
0.1.3
|
data/acts_as_oqgraph.gemspec
CHANGED
data/lib/graph_edge.rb
CHANGED
@@ -87,9 +87,8 @@ class GraphEdge < ActiveRecord::Base
|
|
87
87
|
|
88
88
|
def add_to_graph
|
89
89
|
connection.execute <<-EOS
|
90
|
-
|
90
|
+
REPLACE INTO #{oqgraph_table_name} (origid, destid, weight)
|
91
91
|
VALUES (#{self.send(self.class.from_key)}, #{self.send(self.class.to_key)}, #{weight || 1.0})
|
92
|
-
ON DUPLICATE KEY UPDATE origid=#{self.send(self.class.from_key)};
|
93
92
|
EOS
|
94
93
|
end
|
95
94
|
|
@@ -195,8 +195,23 @@ class TestActsAsOqgraph < ActiveSupport::TestCase
|
|
195
195
|
def test_duplicate_links_ignored
|
196
196
|
@test_1.create_edge_to @test_2
|
197
197
|
assert_nothing_raised do
|
198
|
-
@test_1.create_edge_to @test_2
|
198
|
+
@test_1.create_edge_to @test_2
|
199
199
|
end
|
200
200
|
end
|
201
|
+
|
202
|
+
def test_duplicate_link_error
|
203
|
+
ActiveRecord::Base.connection.execute("INSERT INTO test_model_oqgraph (destid, origid, weight) VALUES (99,99,1.0);")
|
204
|
+
assert_raises ActiveRecord::StatementInvalid do
|
205
|
+
ActiveRecord::Base.connection.execute("INSERT INTO test_model_oqgraph (destid, origid, weight) VALUES (99,99,1.0);")
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
def test_duplicate_link_error_fix
|
210
|
+
ActiveRecord::Base.connection.execute("REPLACE INTO test_model_oqgraph (destid, origid, weight) VALUES (99,99,1.0);")
|
211
|
+
assert_nothing_raised do
|
212
|
+
ActiveRecord::Base.connection.execute("REPLACE INTO test_model_oqgraph (destid, origid, weight) VALUES (99,99,1.0);")
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
201
216
|
|
202
217
|
end
|