omf_rete 0.5 → 0.6.1
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +82 -74
- data/lib/omf_rete/abstract_tuple_set.rb +5 -1
- data/lib/omf_rete/indexed_tuple_set.rb +84 -17
- data/lib/omf_rete/join_op.rb +78 -18
- data/lib/omf_rete/planner/join_plan.rb +16 -15
- data/lib/omf_rete/planner/plan_builder.rb +52 -48
- data/lib/omf_rete/planner/source_plan.rb +14 -13
- data/lib/omf_rete/store/alpha/alpha_element.rb +3 -74
- data/lib/omf_rete/store/alpha/alpha_inner_element.rb +8 -58
- data/lib/omf_rete/store/alpha/alpha_leaf_element.rb +18 -3
- data/lib/omf_rete/store/alpha_store.rb +118 -0
- data/lib/omf_rete/store/named_alpha_store.rb +38 -0
- data/lib/omf_rete/store/object_store.rb +119 -0
- data/lib/omf_rete/store/predicate_store.rb +96 -0
- data/lib/omf_rete/store.rb +58 -13
- data/lib/omf_rete/tuple.rb +53 -0
- data/lib/omf_rete/tuple_stream.rb +89 -60
- data/lib/omf_rete/version.rb +1 -1
- data/lib/omf_rete.rb +14 -4
- data/omf_rete.gemspec +3 -0
- data/tests/test_filter.rb +1 -1
- data/tests/test_join_op.rb +30 -0
- data/tests/test_named_store.rb +36 -0
- data/tests/test_object_store.rb +150 -0
- data/tests/test_planner.rb +72 -40
- data/tests/test_predicate_store.rb +95 -0
- data/tests/test_readme.rb +66 -0
- data/tests/test_store.rb +56 -0
- metadata +30 -5
- data/lib/omf_rete/store/alpha/alpha_store.rb +0 -197
data/README.md
CHANGED
@@ -1,5 +1,8 @@
|
|
1
1
|
|
2
|
-
|
2
|
+
# Introduction
|
3
|
+
|
4
|
+
__Warning: This is embarrassingly out of date. Check tests for a more accurate
|
5
|
+
reflection of what's implemented__
|
3
6
|
|
4
7
|
This library implements a tuple store with a query and subscribe mechanism.
|
5
8
|
A subscribe is effectively a standing query which executes a block whenever
|
@@ -10,65 +13,69 @@ The store holds same sized tuples with each value being assigned a name and
|
|
10
13
|
type at creation to support varous convenience functions to create and retrieve
|
11
14
|
tuples.
|
12
15
|
|
13
|
-
The following code snippet creates a simple RDF store and adds a
|
16
|
+
The following code snippet creates a simple RDF store (tuple_length: 3) and adds a triplet
|
14
17
|
to it.
|
15
18
|
|
16
|
-
|
17
|
-
|
19
|
+
eng = OMF::Rete.create_engine(tuple_length: 3)
|
20
|
+
eng.add_fact('myFridge', 'contains', 'milk')
|
18
21
|
|
19
|
-
A
|
22
|
+
A rule consists of an array of tuple +patterns+ and a +block+ to be called when the store
|
20
23
|
contains a set of tuples matching the +pattern+.
|
21
24
|
|
22
25
|
The following filter only looks for a single, specific tuple. The supplied block is called
|
23
26
|
immediately if the tuple already exists in the store, or when such a tuple would be added at a later
|
24
27
|
stage.
|
25
28
|
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
29
|
+
eng = OMF::Rete.create_engine(tuple_length: 3)
|
30
|
+
eng.add_rule(:report_problem, [
|
31
|
+
['myFridge', 'status', 'broken']
|
32
|
+
]) do |m|
|
33
|
+
puts "My fridge is broken"
|
34
|
+
end
|
35
|
+
eng.add_fact('myFridge', 'status', 'ok')
|
36
|
+
eng.add_fact('myFridge', 'status', 'broken')
|
31
37
|
|
32
38
|
The following filter contains two +patterns+ and therefore both need to be matched at the same
|
33
39
|
time in order for the block to fire. Note, that the order these tuples are added to the store
|
34
40
|
or the interval between is irrelevant.
|
35
41
|
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
42
|
+
eng.subscribe(:save_milk, [
|
43
|
+
[:fridge?, 'status', 'broken'],
|
44
|
+
[:fridge?, 'contains', 'milk'],
|
45
|
+
]) do |m|
|
46
|
+
puts "Save the milk from #{m.fridge?}"
|
47
|
+
end
|
48
|
+
eng.add_fact('myFridge', 'status', 'broken')
|
43
49
|
|
44
|
-
So far the filter pattern were fully specified. The <tt
|
50
|
+
So far the filter pattern were fully specified. The <tt>nil</tt> value can be used as a wildcard identifier.
|
45
51
|
The following code snippet reports anything which is broken.
|
46
52
|
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
53
|
+
eng.subscribe(:something_broken, [
|
54
|
+
[nil, 'status', 'broken']
|
55
|
+
]) do |m|
|
56
|
+
puts "Something is broken"
|
57
|
+
end
|
58
|
+
eng.add_fact('myFridge', 'status', 'broken')
|
52
59
|
|
53
60
|
_Not implemented yet_
|
54
61
|
Similar to OMF::Rete::Store#addNamed we can describe a pattern with a hash. Any value not named is automatically
|
55
62
|
wildcarded. Therefore, an alternative represenation of the previous filter is as follows:
|
56
63
|
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
64
|
+
store.subscribe(:something_broken, [
|
65
|
+
{:pred => 'status', :obj => 'broken'}
|
66
|
+
]) do |m|
|
67
|
+
puts "Something is broken"
|
68
|
+
end
|
62
69
|
|
63
70
|
The +match+ argument to the block holds the context of the match and specifically, the tuples involved
|
64
71
|
in the match.
|
65
72
|
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
73
|
+
store.subscribe(:something_broken, [
|
74
|
+
[:_, 'status', 'broken']
|
75
|
+
]) do |match|
|
76
|
+
what = match.tuples[0][:subject]
|
77
|
+
puts "#{what} is broken"
|
78
|
+
end
|
72
79
|
|
73
80
|
<tt>match.tuples</tt> returns an area of tuples one for each pattern. The matched tuple for the first pattern is at index 0,
|
74
81
|
the second one at index 1, and so on. Individual values of a tuple can be retrieved through the initially declared
|
@@ -78,18 +85,19 @@ Let us assume we are monitoring many fridges, so if we want to report broken one
|
|
78
85
|
that the +subject+ in both patterns in our second example are identical. Or in more technical terms, we need to +bind+ or +join+
|
79
86
|
values across patterns. A binding variable is identified by a symbol with a trailing <b>?</b>.
|
80
87
|
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
+
store.subscribe(:save_milk, [
|
89
|
+
[:fridge?, 'status', 'broken'],
|
90
|
+
[:fridge?, 'contains', 'milk'],
|
91
|
+
]) do |match|
|
92
|
+
fridge = match[:fridge]
|
93
|
+
puts "Save the milk from #{fridge}"
|
94
|
+
end
|
88
95
|
|
89
96
|
<tt>match[bindingName]</tt> (without the '?') returns the value bound to <tt>:fridge?</tt> for this match.
|
90
97
|
Obviously <tt>match.tuples[0][:subject]</tt> will return the same value.
|
91
98
|
|
92
|
-
|
99
|
+
## Functions
|
100
|
+
|
93
101
|
|
94
102
|
Pattern matches alone are not always sufficient. For instance, let us assume that we have also stored the age in years
|
95
103
|
of each monitored fridge and want to replace each broken one which is older than 10 years. To describe such a filter
|
@@ -99,38 +107,38 @@ Functions are identified by the <tt>:PROC</tt> symbol in the first position of a
|
|
99
107
|
name, and the list of parameters. Effectively, a function filters the values previosuly bound to a variable to those
|
100
108
|
for which the function returns true.
|
101
109
|
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
110
|
+
store.subscribe(:replace_old_ones, [
|
111
|
+
[:fridge?, 'status', 'broken'],
|
112
|
+
[:fridge?, 'age', :age?],
|
113
|
+
[:PROC, :greater, :age?, 10]
|
114
|
+
]) do |match|
|
115
|
+
puts "Replace #{match[:fridge]}"
|
116
|
+
end
|
109
117
|
|
110
118
|
<b>Design Note:</b> A more generic solution based on a 'lambda' is most likely cleaner. This is effectively
|
111
119
|
identical to the final block, except that the block should return +true+ for tuples passing the filter,
|
112
120
|
and +false+ for all others. To further simplify this and also reduce the search space, we can define a
|
113
121
|
+filter+ function which takes a list of bound variables and calls the associated block with specific bindings.
|
114
122
|
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
123
|
+
store.subscribe(:replace_old_ones, [
|
124
|
+
[:fridge?, 'status', 'broken'],
|
125
|
+
[:fridge?, 'age', :age?],
|
126
|
+
filter(:age?) { |age| age > 10 }
|
127
|
+
]) do |match|
|
128
|
+
puts "Replace #{match[:fridge]}"
|
129
|
+
end
|
122
130
|
|
123
|
-
|
131
|
+
### Set Operators
|
124
132
|
|
125
133
|
Let us assume we want the store to not only reflect the current facts but the entire history of a system. We
|
126
134
|
can achieve that by adding a timestamp to each fact and never retract facts.
|
127
135
|
|
128
|
-
|
136
|
+
store = OMF::Rete::Store.new(:subj => String, :pred => String, :obj => Object, :tstamp => Time)
|
129
137
|
|
130
138
|
This now allows us to capture that a fridge broke on a specific date and was fixed some times later.
|
131
139
|
|
132
|
-
|
133
|
-
|
140
|
+
store.add('myFridge', 'status', 'broken', '2008-12-20')
|
141
|
+
store.add('myFridge', 'status', 'ok', '2008-12-22')
|
134
142
|
|
135
143
|
However, how can we now determine that a specific fridge is CURRENTLY broken? The pattern
|
136
144
|
<tt>[:f?, 'status' 'broken']</tt> will identify all fridges which are currently broken, as well as those
|
@@ -141,21 +149,21 @@ the filter picks the one with the most recent timestamp.
|
|
141
149
|
The current syntax achieves this through special match values. For instance, <tt>:LATEST</tt> for <tt>Time</tt>
|
142
150
|
types picks the most recent fact.
|
143
151
|
|
144
|
-
|
152
|
+
[:fridge?, 'status', :_, :LATEST]
|
145
153
|
|
146
154
|
To find all currently broken fridges we need to bind this to all broken status facts.
|
147
155
|
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
156
|
+
store.subscribe(:broken_lately, [
|
157
|
+
[:fridge?, 'status', :_, :LATEST],
|
158
|
+
[:fridge?, 'status', 'broken']
|
159
|
+
]) do |match|
|
160
|
+
puts "#{match[:fridge]} is broken"
|
161
|
+
end
|
154
162
|
|
155
163
|
<b>Design Note:</b> This seems to be a fairly ad-hoc syntax. Is there a better one? This assumes that there is no join
|
156
164
|
on any of the bound variables, they are simply keys for the sets. But overloading functionality always adds complexity.
|
157
165
|
|
158
|
-
|
166
|
+
## Negated Conditions
|
159
167
|
|
160
168
|
Now let us consider we know that our fridge is broken and we want to monitor any future status updates.
|
161
169
|
There may be many different status types and we are interested in all of them as long as they are
|
@@ -163,12 +171,12 @@ different to 'broken'. In other words, we need a way to describe what is refered
|
|
163
171
|
condition' and is defined by a leading <tt>:NOT</tt>, followed by one or multiple patterns describing
|
164
172
|
what should NOT be in the store.
|
165
173
|
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
174
|
+
store.subscribe(:find_latest, [
|
175
|
+
['My Fridge', :status, :_, :LATEST],
|
176
|
+
[:NOT, ['My Fridge', 'status', 'broken']]
|
177
|
+
]) do |match|
|
178
|
+
puts "Status for my fridge changed to '#{match.tuples[0][:obj]}."
|
179
|
+
end
|
172
180
|
|
173
181
|
Please note that the above example fails to report when my fridge is reported as broken again.
|
174
182
|
|
@@ -27,7 +27,11 @@ module OMF::Rete
|
|
27
27
|
def addTuple(tuple)
|
28
28
|
raise 'Abstract class'
|
29
29
|
end
|
30
|
-
|
30
|
+
|
31
|
+
def removeTuple(tuple)
|
32
|
+
raise 'Abstract class'
|
33
|
+
end
|
34
|
+
|
31
35
|
# Call block for every tuple stored in this set currently and
|
32
36
|
# in the future. In other words, the block may be called even after this
|
33
37
|
# method returns.
|
@@ -8,25 +8,25 @@ module OMF::Rete
|
|
8
8
|
# removed.
|
9
9
|
#
|
10
10
|
# The IndexedTupleSet is defined by a +description+ and an
|
11
|
-
# +indexPattern+.
|
11
|
+
# +indexPattern+.
|
12
12
|
#
|
13
13
|
# The +description+ is an array of the
|
14
14
|
# same length as the tuples maintained. Each element,
|
15
15
|
# if not nil, names the binding variable associated with it.
|
16
|
-
# The position of a binding can be retrieved with
|
16
|
+
# The position of a binding can be retrieved with
|
17
17
|
# +index_for_binding+.
|
18
18
|
#
|
19
19
|
# The +indexPattern+ describes which elements of the inserted
|
20
|
-
# tuple are being combined in an array to form the index
|
21
|
-
# key for each internal tuple. The elements in the +indexPattern+
|
20
|
+
# tuple are being combined in an array to form the index
|
21
|
+
# key for each internal tuple. The elements in the +indexPattern+
|
22
22
|
# are described by the binding name.
|
23
23
|
#
|
24
24
|
#
|
25
25
|
class IndexedTupleSet < AbstractTupleSet
|
26
|
-
|
26
|
+
|
27
27
|
attr_reader :indexPattern
|
28
28
|
attr_writer :transient # if true only process tuple but don't store it
|
29
|
-
|
29
|
+
|
30
30
|
def initialize(description, indexPattern, source = nil, opts = {})
|
31
31
|
super description, source
|
32
32
|
if (indexPattern.length == 0)
|
@@ -39,7 +39,7 @@ module OMF::Rete
|
|
39
39
|
|
40
40
|
@index = {}
|
41
41
|
end
|
42
|
-
|
42
|
+
|
43
43
|
def addTuple(tuple)
|
44
44
|
key = @indexMap.collect do |ii|
|
45
45
|
tuple[ii]
|
@@ -58,12 +58,38 @@ module OMF::Rete
|
|
58
58
|
end
|
59
59
|
tuple # return added tuple
|
60
60
|
end
|
61
|
-
|
61
|
+
|
62
|
+
def removeTuple(tuple)
|
63
|
+
key = @indexMap.collect do |ii|
|
64
|
+
tuple[ii]
|
65
|
+
end
|
66
|
+
|
67
|
+
if @transient
|
68
|
+
@onRemoveBlockWithIndex.call(key, tuple) if @onRemoveBlockWithIndex
|
69
|
+
@onRemoveBlock.call(tuple) if @onRemoveBlock
|
70
|
+
else
|
71
|
+
vset = @index[key]
|
72
|
+
if vset
|
73
|
+
vset.delete(tuple)
|
74
|
+
@onRemoveBlockWithIndex.call(key, tuple) if @onRemoveBlockWithIndex
|
75
|
+
@onRemoveBlock.call(tuple) if @onRemoveBlock
|
76
|
+
end
|
77
|
+
end
|
78
|
+
tuple # return removed tuple
|
79
|
+
end
|
80
|
+
|
81
|
+
# Clear index
|
82
|
+
def clear()
|
83
|
+
@onRemoveBlockWithIndex.call(nil, nil) if @onRemoveBlockWithIndex
|
84
|
+
@onRemoveBlock.call(nil) if @onRemoveBlock
|
85
|
+
@index = {}
|
86
|
+
end
|
87
|
+
|
62
88
|
# Call block for every tuple stored in this set currently and
|
63
89
|
# in the future. In other words, the block may be called even after this
|
64
|
-
# method returns.
|
90
|
+
# method returns.
|
65
91
|
#
|
66
|
-
# The block will be called with one parameters, the
|
92
|
+
# The block will be called with one parameters, the
|
67
93
|
# tuple added.
|
68
94
|
#
|
69
95
|
# Note: Only one +block+ can be registered at a time
|
@@ -80,9 +106,9 @@ module OMF::Rete
|
|
80
106
|
|
81
107
|
# Call block for every tuple stored in this set currently and
|
82
108
|
# in the future. In other words, the block may be called even after this
|
83
|
-
# method returns.
|
109
|
+
# method returns.
|
84
110
|
#
|
85
|
-
# The block will be called with two parameters, the index of the tuple followed by the
|
111
|
+
# The block will be called with two parameters, the index of the tuple followed by the
|
86
112
|
# tuple itself.
|
87
113
|
#
|
88
114
|
# Note: Only one +block+ can be registered at a time
|
@@ -96,14 +122,44 @@ module OMF::Rete
|
|
96
122
|
@onAddBlockWithIndex = block
|
97
123
|
end
|
98
124
|
|
99
|
-
#
|
125
|
+
# Call block for every tuple removed from this set in the future.
|
126
|
+
# In other words, the block may be called after this
|
127
|
+
# method returns.
|
128
|
+
#
|
129
|
+
# The block will be called with one parameters, the
|
130
|
+
# tuple removed. If the parameter is nil, everything has
|
131
|
+
# been removed (cleared)
|
132
|
+
#
|
133
|
+
# Note: Only one +block+ can be registered at a time
|
134
|
+
#
|
135
|
+
def on_remove(&block)
|
136
|
+
@onRemoveBlock = block
|
137
|
+
end
|
138
|
+
|
139
|
+
|
140
|
+
# Call block for every tuple removed from this set
|
141
|
+
# in the future. In other words, the block may be called even after this
|
142
|
+
# method returns.
|
143
|
+
#
|
144
|
+
# The block will be called with two parameters, the index of the tuple followed by the
|
145
|
+
# tuple itself. If both parameters are nil, everything has
|
146
|
+
# been removed (cleared)
|
147
|
+
#
|
148
|
+
# Note: Only one +block+ can be registered at a time
|
149
|
+
#
|
150
|
+
def on_remove_with_index(&block)
|
151
|
+
@onRemoveBlockWithIndex = block
|
152
|
+
end
|
153
|
+
|
154
|
+
|
155
|
+
# Return the set of tuples index by +key+.
|
100
156
|
# Will return nil if nothing is stored for +key+
|
101
157
|
#
|
102
158
|
def [](key)
|
103
159
|
res = @index[key]
|
104
160
|
res
|
105
161
|
end
|
106
|
-
|
162
|
+
|
107
163
|
# Return all stored tuples in an array.
|
108
164
|
def to_a
|
109
165
|
a = []
|
@@ -114,16 +170,27 @@ module OMF::Rete
|
|
114
170
|
end
|
115
171
|
a
|
116
172
|
end
|
117
|
-
|
173
|
+
|
174
|
+
# Return all stored tuples in a set.
|
175
|
+
def to_set
|
176
|
+
a = Set.new
|
177
|
+
@index.each_value do |s|
|
178
|
+
s.each do |t|
|
179
|
+
a << t
|
180
|
+
end
|
181
|
+
end
|
182
|
+
a
|
183
|
+
end
|
184
|
+
|
118
185
|
def describe(out = STDOUT, offset = 0, incr = 2, sep = "\n")
|
119
186
|
out.write(" " * offset)
|
120
187
|
desc = @description.collect do |e| e || '*' end
|
121
188
|
out.write("ts: [#{desc.join(', ')}]")
|
122
189
|
ind = @indexMap.collect do |i| @description[i] end
|
123
190
|
out.write(" (index: [#{ind.sort.join(', ')}])#{sep}")
|
124
|
-
@source.describe(out, offset + incr, incr, sep) if @source
|
191
|
+
@source.describe(out, offset + incr, incr, sep) if @source
|
125
192
|
end
|
126
|
-
|
193
|
+
|
127
194
|
end # class IndexedTupleSet
|
128
195
|
end # module
|
129
196
|
|
data/lib/omf_rete/join_op.rb
CHANGED
@@ -2,27 +2,27 @@ require 'omf_rete/indexed_tuple_set'
|
|
2
2
|
|
3
3
|
module OMF::Rete
|
4
4
|
|
5
|
-
# This class implements the join operation between two
|
5
|
+
# This class implements the join operation between two
|
6
6
|
# +IndexedTupleSets+ feeding into a third, result tuple set.
|
7
7
|
# The size of both incoming tuple sets needs to be identical and they
|
8
8
|
# are supposed to be indexed on the same list of variables as this is
|
9
9
|
# what they wil be joined at.
|
10
|
-
#
|
11
|
-
# Implementation Note: We first calculate a +combinePattern+
|
10
|
+
#
|
11
|
+
# Implementation Note: We first calculate a +combinePattern+
|
12
12
|
# from the +description+ of the result set.
|
13
13
|
# The +combinePattern+ describes how to create a joined tuple to insert
|
14
|
-
# into the result tuple set. The +combinePattern+ is an array of
|
15
|
-
# the same size as the result tuple. Each element is a 2-array
|
14
|
+
# into the result tuple set. The +combinePattern+ is an array of
|
15
|
+
# the same size as the result tuple. Each element is a 2-array
|
16
16
|
# with the first element describing the input set (0 .. left, 1 .. right)
|
17
17
|
# and the second one the index from which to take the value.
|
18
|
-
#
|
18
|
+
#
|
19
19
|
#
|
20
20
|
class JoinOP
|
21
21
|
def initialize(leftSet, rightSet, resultSet)
|
22
22
|
@resultSet = resultSet
|
23
23
|
@left = leftSet
|
24
24
|
@right = rightSet
|
25
|
-
|
25
|
+
|
26
26
|
@combinePattern = resultSet.description.collect do |bname|
|
27
27
|
side = 0
|
28
28
|
unless (i = leftSet.index_for_binding(bname))
|
@@ -35,7 +35,8 @@ module OMF::Rete
|
|
35
35
|
[side, i]
|
36
36
|
end
|
37
37
|
@resultLength = @combinePattern.length
|
38
|
-
|
38
|
+
@results = {} # Keep track of how may input tuples create the same result - necessary for 'removeTuple'
|
39
|
+
|
39
40
|
leftSet.on_add_with_index do |index, ltuple|
|
40
41
|
if (rs = rightSet[index])
|
41
42
|
rs.each do |rtuple|
|
@@ -50,7 +51,30 @@ module OMF::Rete
|
|
50
51
|
end
|
51
52
|
end
|
52
53
|
end
|
53
|
-
|
54
|
+
|
55
|
+
leftSet.on_remove_with_index do |index, ltuple|
|
56
|
+
if (index.nil?)
|
57
|
+
clear_result()
|
58
|
+
else
|
59
|
+
if (rs = rightSet[index])
|
60
|
+
rs.each do |rtuple|
|
61
|
+
remove_result(ltuple, rtuple)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
rightSet.on_remove_with_index do |index, rtuple|
|
67
|
+
if (index.nil?)
|
68
|
+
clear_result()
|
69
|
+
else
|
70
|
+
if (ls = leftSet[index])
|
71
|
+
ls.each do |ltuple|
|
72
|
+
remove_result(ltuple, rtuple)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
54
78
|
# Supporting 'check_for_tuple'
|
55
79
|
@left_pattern = @left.description.map do |bname|
|
56
80
|
@resultSet.index_for_binding(bname)
|
@@ -60,7 +84,7 @@ module OMF::Rete
|
|
60
84
|
end
|
61
85
|
|
62
86
|
end
|
63
|
-
|
87
|
+
|
64
88
|
# Check if +tuple+ can be produced by this join op. We first
|
65
89
|
# check if we can find a match on one side and then request
|
66
90
|
# from the other side all the tuples which would lead to full
|
@@ -76,19 +100,19 @@ module OMF::Rete
|
|
76
100
|
end
|
77
101
|
return false
|
78
102
|
end
|
79
|
-
|
103
|
+
|
80
104
|
def description()
|
81
105
|
@resultSet.description
|
82
106
|
end
|
83
|
-
|
107
|
+
|
84
108
|
def describe(out = STDOUT, offset = 0, incr = 2, sep = "\n")
|
85
109
|
out.write(" " * offset)
|
86
110
|
result = @combinePattern.collect do |side, index|
|
87
111
|
(side == 0) ? @left.binding_at(index) : @right.binding_at(index)
|
88
112
|
end
|
89
113
|
out.write("join: [#{@left.indexPattern.sort.join(', ')}] => [#{result.sort.join(', ')}]#{sep}")
|
90
|
-
@left.describe(out, offset + incr, incr, sep)
|
91
|
-
@right.describe(out, offset + incr, incr, sep)
|
114
|
+
@left.describe(out, offset + incr, incr, sep)
|
115
|
+
@right.describe(out, offset + incr, incr, sep)
|
92
116
|
end
|
93
117
|
|
94
118
|
private
|
@@ -104,10 +128,46 @@ module OMF::Rete
|
|
104
128
|
result[i] = t[index]
|
105
129
|
i += 1
|
106
130
|
end
|
107
|
-
|
131
|
+
|
132
|
+
if @results.key? result
|
133
|
+
@results[result] = @results[result] + 1
|
134
|
+
else
|
135
|
+
@results[result] = 1
|
136
|
+
@resultSet.addTuple(result)
|
137
|
+
end
|
138
|
+
#puts "add: #{result.inspect} (#{@results.inspect})"
|
108
139
|
end
|
109
|
-
|
110
|
-
|
111
|
-
|
140
|
+
|
141
|
+
def remove_result(ltuple, rtuple)
|
142
|
+
unless @resultLength
|
143
|
+
i = 2
|
144
|
+
end
|
145
|
+
result = Array.new(@resultLength)
|
146
|
+
i = 0
|
147
|
+
@combinePattern.each do |setId, index|
|
148
|
+
t = setId == 0 ? ltuple : rtuple
|
149
|
+
result[i] = t[index]
|
150
|
+
i += 1
|
151
|
+
end
|
152
|
+
# Remove it from result set if it has only been added once
|
153
|
+
#puts "remove: #{result.inspect} (#{@results.inspect})"
|
154
|
+
if count = @results[result]
|
155
|
+
if count == 1
|
156
|
+
@results.delete(result)
|
157
|
+
@resultSet.removeTuple(result)
|
158
|
+
else
|
159
|
+
@results[result] = count - 1
|
160
|
+
end
|
161
|
+
else
|
162
|
+
raise "Should never happen"
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
def clear_result()
|
167
|
+
@results = {}
|
168
|
+
@resultSet.clear
|
169
|
+
end
|
170
|
+
|
171
|
+
|
112
172
|
end # class
|
113
173
|
end # module
|
@@ -1,15 +1,16 @@
|
|
1
1
|
|
2
2
|
require 'omf_rete/tuple_stream'
|
3
|
+
require 'omf_rete/join_op'
|
3
4
|
|
4
5
|
module OMF::Rete
|
5
6
|
module Planner
|
6
|
-
|
7
|
-
|
7
|
+
|
8
|
+
|
8
9
|
# This class represents a planned join op.
|
9
|
-
#
|
10
|
+
#
|
10
11
|
#
|
11
12
|
class JoinPlan < AbstractPlan
|
12
|
-
|
13
|
+
|
13
14
|
# stream1 - first stream to join
|
14
15
|
# stream2 - second stream to join
|
15
16
|
# joinSet - set of bindings to join on
|
@@ -17,8 +18,8 @@ module OMF::Rete
|
|
17
18
|
# coverSet - set of leaf nodes contributing to this result
|
18
19
|
#
|
19
20
|
def initialize(stream1, stream2, joinSet, resultSet, coverSet, planBuilder)
|
20
|
-
super coverSet, resultSet
|
21
|
-
|
21
|
+
super coverSet, resultSet
|
22
|
+
|
22
23
|
@planBuilder = planBuilder
|
23
24
|
@left = stream1
|
24
25
|
@right = stream2
|
@@ -34,7 +35,7 @@ module OMF::Rete
|
|
34
35
|
description = @result_set.to_a.sort
|
35
36
|
resultSet = IndexedTupleSet.new(description, indexPattern, nil, opts)
|
36
37
|
end
|
37
|
-
|
38
|
+
|
38
39
|
indexPattern = @join_set.to_a
|
39
40
|
leftSet = @left.materialize(indexPattern, nil, opts)
|
40
41
|
rightSet = @right.materialize(indexPattern, nil, opts)
|
@@ -46,7 +47,7 @@ module OMF::Rete
|
|
46
47
|
# Create a hash for this plan which allows us to
|
47
48
|
# to identify identical plans.
|
48
49
|
#
|
49
|
-
# Please note, that there is most likely a mroe efficient way to
|
50
|
+
# Please note, that there is most likely a mroe efficient way to
|
50
51
|
# calculate a hash with the above properties
|
51
52
|
#
|
52
53
|
def hash()
|
@@ -58,7 +59,7 @@ module OMF::Rete
|
|
58
59
|
end
|
59
60
|
@hash
|
60
61
|
end
|
61
|
-
|
62
|
+
|
62
63
|
# Return the cost of this plan.
|
63
64
|
#
|
64
65
|
# TODO: Some more meaningful heuristic will be nice
|
@@ -69,25 +70,25 @@ module OMF::Rete
|
|
69
70
|
rcost = @right.cost()
|
70
71
|
#@cost = 1 + 1.2 * (lcost > rcost ? lcost : rcost)
|
71
72
|
@cost = 1 + 1.2 * (lcost + rcost)
|
72
|
-
|
73
|
+
|
73
74
|
end
|
74
75
|
@cost
|
75
76
|
end
|
76
|
-
|
77
|
+
|
77
78
|
def describe(out = STDOUT, offset = 0, incr = 2, sep = "\n")
|
78
79
|
out.write(" " * offset)
|
79
80
|
result = @result_set.to_a.sort
|
80
81
|
join = @join_set.to_a.sort
|
81
82
|
out.write("join: [#{join.join(', ')}] => [#{result.join(', ')}] cost: #{cost}#{sep}")
|
82
|
-
@left.describe(out, offset + incr, incr, sep)
|
83
|
-
@right.describe(out, offset + incr, incr, sep)
|
83
|
+
@left.describe(out, offset + incr, incr, sep)
|
84
|
+
@right.describe(out, offset + incr, incr, sep)
|
84
85
|
end
|
85
|
-
|
86
|
+
|
86
87
|
def to_s
|
87
88
|
result = @result_set.to_a.sort
|
88
89
|
join = @join_set.to_a.sort
|
89
90
|
"JoinPlan [#{join.join(', ')}] out: [#{result.join(', ')}]"
|
90
|
-
end
|
91
|
+
end
|
91
92
|
end # PlanBuilder
|
92
93
|
|
93
94
|
end # Planner
|