omf_rete 0.5 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|