lexster 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,6 @@
1
+ module Lexster
2
+ class Config
3
+ attr_accessor :enable_subrefs
4
+ attr_accessor :enable_per_model_indexes
5
+ end
6
+ end
@@ -0,0 +1,12 @@
1
+ module Lexster
2
+ class NeoDatabaseCleaner
3
+ def self.clean_db(start_node = Lexster.db.get_root)
4
+ Lexster.db.execute_script <<-GREMLIN
5
+ g.V.toList().each { if (it.id != 0) g.removeVertex(it) }
6
+ g.indices.each { g.dropIndex(it.indexName); }
7
+ GREMLIN
8
+
9
+ true
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,16 @@
1
+ module Ndoid
2
+ class Middleware
3
+ def initialize(app)
4
+ @app = app
5
+ end
6
+
7
+ def call(env)
8
+ old_enabled, Thread.current[:lexster_enabled] = Thread.current[:lexster_enabled], true
9
+ old_batch, Thread.current[:lexster_current_batch] = Thread.current[:lexster_current_batch], nil
10
+ @app.call(env)
11
+ ensure
12
+ Thread.current[:lexster_enabled] = old_enabled
13
+ Thread.current[:lexster_current_batch] = old_batch
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,121 @@
1
+ module Lexster
2
+ module ModelAdditions
3
+ module ClassMethods
4
+ attr_reader :lexster_config
5
+
6
+ def lexster_config
7
+ @lexster_config ||= Lexster::ModelConfig.new(self)
8
+ end
9
+
10
+ def lexsterable(options = {})
11
+ # defaults
12
+ lexster_config.auto_index = true
13
+ lexster_config.enable_model_index = true # but the Lexster.enable_per_model_indexes is false by default. all models will be true only if the primary option is turned on.
14
+
15
+ yield(lexster_config) if block_given?
16
+
17
+ options.each do |key, value|
18
+ raise "Lexster #{self.name} model options: No such option #{key}" unless lexster_config.respond_to?("#{key}=")
19
+ lexster_config.send("#{key}=", value)
20
+ end
21
+ end
22
+
23
+ def neo_model_index_name
24
+ raise "Per Model index is not enabled. Nodes/Relationships are auto indexed with node_auto_index/relationship_auto_index" unless Lexster.config.enable_per_model_indexes || lexster_config.enable_model_index
25
+ @index_name ||= "#{self.name.tableize}_index"
26
+ end
27
+ end
28
+
29
+ module InstanceMethods
30
+ def to_neo
31
+ if self.class.lexster_config.stored_fields
32
+ hash = self.class.lexster_config.stored_fields.inject({}) do |all, (field, block)|
33
+ all[field] = if block
34
+ instance_eval(&block)
35
+ else
36
+ self.send(field) rescue (raise "No field #{field} for #{self.class.name}")
37
+ end
38
+
39
+ all
40
+ end
41
+
42
+ hash.reject { |k, v| v.nil? }
43
+ else
44
+ {}
45
+ end
46
+ end
47
+
48
+ def neo_save_after_model_save
49
+ return unless self.class.lexster_config.auto_index
50
+ neo_save
51
+ true
52
+ end
53
+
54
+ def neo_save
55
+ @_neo_destroyed = false
56
+ @_neo_representation = _neo_save
57
+ end
58
+
59
+ alias neo_create neo_save
60
+ alias neo_update neo_save
61
+
62
+ def neo_destroy
63
+ return if @_neo_destroyed
64
+ @_neo_destroyed = true
65
+
66
+ neo_representation = neo_find_by_id
67
+ return unless neo_representation
68
+
69
+ begin
70
+ neo_representation.del
71
+ rescue Neography::NodeNotFoundException => e
72
+ Lexster::logger.info "Lexster#neo_destroy entity not found #{self.class.name} #{self.id}"
73
+ end
74
+
75
+ # Not working yet because Neography can't delete a node and all of its realtionships in a batch, and deleting a node with relationships results an error
76
+ # if Lexster::Batch.current_batch
77
+ # Lexster::Batch.current_batch << [self.class.delete_command, neo_representation.neo_id]
78
+ # else
79
+ # begin
80
+ # neo_representation.del
81
+ # rescue Neography::NodeNotFoundException => e
82
+ # Lexster::logger.info "Lexster#neo_destroy entity not found #{self.class.name} #{self.id}"
83
+ # end
84
+ # end
85
+
86
+ _reset_neo_representation
87
+
88
+ true
89
+ end
90
+
91
+ def neo_unique_id
92
+ "#{self.class.name}:#{self.id}"
93
+ end
94
+
95
+ protected
96
+ def neo_properties_to_hash(*attribute_list)
97
+ attribute_list.flatten.inject({}) { |all, property|
98
+ all[property] = self.send(property)
99
+ all
100
+ }
101
+ end
102
+
103
+ private
104
+ def _neo_representation
105
+ @_neo_representation ||= neo_find_by_id || neo_save
106
+ end
107
+
108
+ def _reset_neo_representation
109
+ @_neo_representation = nil
110
+ end
111
+ end
112
+
113
+ def self.included(receiver)
114
+ receiver.extend ClassMethods
115
+ receiver.send :include, InstanceMethods
116
+
117
+ receiver.after_save :neo_save_after_model_save
118
+ receiver.after_destroy :neo_destroy
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,57 @@
1
+ module Lexster
2
+ class ModelConfig
3
+ attr_reader :properties
4
+ attr_reader :search_options
5
+ attr_reader :relationship_options
6
+ attr_accessor :enable_model_index
7
+ attr_accessor :auto_index
8
+
9
+ def initialize(klass)
10
+ @klass = klass
11
+ end
12
+
13
+ def stored_fields
14
+ @stored_fields ||= {}
15
+ end
16
+
17
+ def field(name, &block)
18
+ self.stored_fields[name] = block
19
+ end
20
+
21
+ def relationship(options)
22
+ @relationship_options = options
23
+ end
24
+
25
+ def search(&block)
26
+ raise "search needs a block" unless block_given?
27
+ @search_options = SearchConfig.new
28
+ block.(@search_options)
29
+ end
30
+
31
+ def inspect
32
+ "#<Lexster::ModelConfig @properties=#{properties.inspect} @search_options=#{@search_options.inspect}>"
33
+ end
34
+ end
35
+
36
+ class SearchConfig
37
+ def index_fields
38
+ @index_fields ||= {}
39
+ end
40
+
41
+ def fulltext_fields
42
+ @fulltext_fields ||= {}
43
+ end
44
+
45
+ def index(field, options = {}, &block)
46
+ index_fields[field] = options.merge(block: block)
47
+ end
48
+
49
+ def fulltext(field, options = {}, &block)
50
+ fulltext_fields[field] = options.merge(block: block)
51
+ end
52
+
53
+ def inspect
54
+ "#<Lexster::SearchConfig @index_fields=#{index_fields.inspect} @fulltext_fields=#{fulltext_fields.inspect}>"
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,210 @@
1
+ module Lexster
2
+ module Node
3
+ def self.from_hash(hash)
4
+ node = Neography::Node.new(hash)
5
+ node.neo_server = Lexster.db
6
+ node
7
+ end
8
+
9
+ module ClassMethods
10
+ attr_accessor :neo_subref_node
11
+
12
+ def neo_subref_rel_type
13
+ @_neo_subref_rel_type ||= "#{self.name.tableize}_subref"
14
+ end
15
+
16
+ def neo_subref_node_rel_type
17
+ @_neo_subref_node_rel_type ||= self.name.tableize
18
+ end
19
+
20
+ def delete_command
21
+ :delete_node
22
+ end
23
+
24
+ def neo_model_index
25
+ return nil unless Lexster.config.enable_per_model_indexes
26
+
27
+ Lexster::logger.info "Node#neo_model_index #{neo_subref_rel_type}"
28
+
29
+ gremlin_query = <<-GREMLIN
30
+ g.createManualIndex(neo_model_index_name, Vertex.class);
31
+ GREMLIN
32
+
33
+ # Lexster.logger.info "subref query:\n#{gremlin_query}"
34
+
35
+ script_vars = { neo_model_index_name: neo_model_index_name }
36
+
37
+ Lexster.execute_script_or_add_to_batch gremlin_query, script_vars
38
+ end
39
+
40
+ def neo_subref_node
41
+ return nil unless Lexster.config.enable_subrefs
42
+
43
+ @neo_subref_node ||= begin
44
+ Lexster::logger.info "Node#neo_subref_node #{neo_subref_rel_type}"
45
+
46
+ gremlin_query = <<-GREMLIN
47
+ q = g.v(0).out(neo_subref_rel_type);
48
+
49
+ subref = q.hasNext() ? q.next() : null;
50
+
51
+ if (!subref) {
52
+ subref = g.addVertex([name: neo_subref_rel_type, type: name]);
53
+ g.addEdge(g.v(0), subref, neo_subref_rel_type);
54
+ }
55
+
56
+ subref
57
+ GREMLIN
58
+
59
+ # Lexster.logger.info "subref query:\n#{gremlin_query}"
60
+
61
+ script_vars = {
62
+ neo_subref_rel_type: neo_subref_rel_type,
63
+ name: self.name
64
+ }
65
+
66
+ Lexster.execute_script_or_add_to_batch gremlin_query, script_vars do |value|
67
+ Lexster::Node.from_hash(value)
68
+ end
69
+ end
70
+ end
71
+
72
+ def reset_neo_subref_node
73
+ @neo_subref_node = nil
74
+ end
75
+
76
+ def neo_search(term, options = {})
77
+ Lexster.search(self, term, options)
78
+ end
79
+ end
80
+
81
+ module InstanceMethods
82
+ def neo_find_by_id
83
+ # Lexster::logger.info "Node#neo_find_by_id #{self.class.neo_index_name} #{self.id}"
84
+ node = Lexster.db.get_node_auto_index(Lexster::UNIQUE_ID_KEY, self.neo_unique_id)
85
+ node.present? ? Lexster::Node.from_hash(node[0]) : nil
86
+ end
87
+
88
+ def _neo_save
89
+ return unless Lexster.enabled?
90
+
91
+ data = self.to_neo.merge(ar_type: self.class.name, ar_id: self.id, Lexster::UNIQUE_ID_KEY => self.neo_unique_id)
92
+ data.reject! { |k, v| v.nil? }
93
+
94
+ gremlin_query = <<-GREMLIN
95
+ idx = g.idx('node_auto_index');
96
+ q = null;
97
+ if (idx) q = idx.get(unique_id_key, unique_id);
98
+
99
+ node = null;
100
+ if (q && q.hasNext()) {
101
+ node = q.next();
102
+ node_data.each {
103
+ if (node.getProperty(it.key) != it.value) {
104
+ node.setProperty(it.key, it.value);
105
+ }
106
+ }
107
+ } else {
108
+ node = g.addVertex(node_data);
109
+ if (enable_subrefs) g.addEdge(g.v(subref_id), node, neo_subref_node_rel_type);
110
+
111
+ if (enable_model_index) g.idx(neo_model_index_name).put('ar_id', node.ar_id, node);
112
+ }
113
+
114
+ node
115
+ GREMLIN
116
+
117
+ script_vars = {
118
+ unique_id_key: Lexster::UNIQUE_ID_KEY,
119
+ node_data: data,
120
+ unique_id: self.neo_unique_id,
121
+ enable_subrefs: Lexster.config.enable_subrefs,
122
+ enable_model_index: Lexster.config.enable_per_model_indexes && self.class.lexster_config.enable_model_index
123
+ }
124
+
125
+ if Lexster.config.enable_subrefs
126
+ script_vars.update(
127
+ subref_id: self.class.neo_subref_node.neo_id,
128
+ neo_subref_node_rel_type: self.class.neo_subref_node_rel_type
129
+ )
130
+ end
131
+
132
+ if Lexster.config.enable_per_model_indexes && self.class.lexster_config.enable_model_index
133
+ script_vars.update(
134
+ neo_model_index_name: self.class.neo_model_index_name
135
+ )
136
+ end
137
+
138
+ Lexster::logger.info "Node#neo_save #{self.class.name} #{self.id}"
139
+
140
+ node = Lexster.execute_script_or_add_to_batch(gremlin_query, script_vars) do |value|
141
+ @_neo_representation = Lexster::Node.from_hash(value)
142
+ end.then do |result|
143
+ neo_search_index
144
+ end
145
+
146
+ node
147
+ end
148
+
149
+ def neo_search_index
150
+ return if self.class.lexster_config.search_options.blank? || (
151
+ self.class.lexster_config.search_options.index_fields.blank? &&
152
+ self.class.lexster_config.search_options.fulltext_fields.blank?
153
+ )
154
+
155
+ Lexster.ensure_default_fulltext_search_index
156
+
157
+ Lexster.db.add_node_to_index(DEFAULT_FULLTEXT_SEARCH_INDEX_NAME, 'ar_type', self.class.name, neo_node.neo_id)
158
+
159
+ self.class.lexster_config.search_options.fulltext_fields.each do |field, options|
160
+ Lexster.db.add_node_to_index(DEFAULT_FULLTEXT_SEARCH_INDEX_NAME, "#{field}_fulltext", neo_helper_get_field_value(field, options), neo_node.neo_id)
161
+ end
162
+
163
+ self.class.lexster_config.search_options.index_fields.each do |field, options|
164
+ Lexster.db.add_node_to_index(DEFAULT_FULLTEXT_SEARCH_INDEX_NAME, field, neo_helper_get_field_value(field, options), neo_node.neo_id)
165
+ end
166
+
167
+ neo_node
168
+ end
169
+
170
+ def neo_helper_get_field_value(field, options = {})
171
+ if options[:block]
172
+ options[:block].call
173
+ else
174
+ self.send(field) rescue (raise "No field #{field} for #{self.class.name}")
175
+ end
176
+ end
177
+
178
+ def neo_load(hash)
179
+ Lexster::Node.from_hash(hash)
180
+ end
181
+
182
+ def neo_node
183
+ _neo_representation
184
+ end
185
+
186
+ def neo_after_relationship_remove(relationship)
187
+ relationship.neo_destroy
188
+ end
189
+
190
+ def neo_before_relationship_through_remove(record)
191
+ rel_model, foreign_key_of_owner, foreign_key_of_record = Lexster::Relationship.meta_data[self.class.name.to_s][record.class.name.to_s]
192
+ rel_model = rel_model.to_s.constantize
193
+ @__neo_temp_rels ||= {}
194
+ @__neo_temp_rels[record] = rel_model.where(foreign_key_of_owner => self.id, foreign_key_of_record => record.id).first
195
+ end
196
+
197
+ def neo_after_relationship_through_remove(record)
198
+ @__neo_temp_rels.each { |record, relationship| relationship.neo_destroy }
199
+ @__neo_temp_rels.delete(record)
200
+ end
201
+ end
202
+
203
+ def self.included(receiver)
204
+ receiver.send :include, Lexster::ModelAdditions
205
+ receiver.extend ClassMethods
206
+ receiver.send :include, InstanceMethods
207
+ Lexster.node_models << receiver
208
+ end
209
+ end
210
+ end
@@ -0,0 +1,15 @@
1
+ require 'lexster/middleware'
2
+
3
+ module Lexster
4
+ class Railtie < Rails::Railtie
5
+ initializer "lexster.configure_rails_initialization" do
6
+ config.after_initialize do
7
+ Lexster.initialize_all
8
+ end
9
+ end
10
+
11
+ initializer 'lexster.inject_middleware' do |app|
12
+ app.middleware.use Ndoid::Middleware
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,163 @@
1
+ module Lexster
2
+ module Relationship
3
+ # this is a proxy that delays loading of start_node and end_node from Neo4j until accessed.
4
+ # the original Neography Relatioship loaded them on initialization
5
+ class RelationshipLazyProxy < ::Neography::Relationship
6
+ def start_node
7
+ @start_node_from_db ||= @start_node = Neography::Node.load(@start_node, Lexster.db)
8
+ end
9
+
10
+ def end_node
11
+ @end_node_from_db ||= @end_node = Neography::Node.load(@end_node, Lexster.db)
12
+ end
13
+ end
14
+
15
+ def self.from_hash(hash)
16
+ relationship = RelationshipLazyProxy.new(hash)
17
+
18
+ relationship
19
+ end
20
+
21
+
22
+ module ClassMethods
23
+ def delete_command
24
+ :delete_relationship
25
+ end
26
+ end
27
+
28
+ module InstanceMethods
29
+ def neo_find_by_id
30
+ results = Lexster.db.get_relationship_auto_index(Lexster::UNIQUE_ID_KEY, self.neo_unique_id)
31
+ relationship = results.present? ? Lexster::Relationship.from_hash(results[0]) : nil
32
+ relationship
33
+ end
34
+
35
+ def _neo_save
36
+ return unless Lexster.enabled?
37
+
38
+ options = self.class.lexster_config.relationship_options
39
+
40
+ start_item = self.send(options[:start_node])
41
+ end_item = self.send(options[:end_node])
42
+
43
+ return unless start_item && end_item
44
+
45
+ # initialize nodes
46
+ start_item.neo_node
47
+ end_item.neo_node
48
+
49
+ data = self.to_neo.merge(ar_type: self.class.name, ar_id: self.id, Lexster::UNIQUE_ID_KEY => self.neo_unique_id)
50
+ data.reject! { |k, v| v.nil? }
51
+
52
+ rel_type = options[:type].is_a?(Proc) ? options[:type].call(self) : options[:type]
53
+
54
+ gremlin_query = <<-GREMLIN
55
+ idx = g.idx('relationship_auto_index');
56
+ q = null;
57
+ if (idx) q = idx.get(unique_id_key, unique_id);
58
+
59
+ relationship = null;
60
+ if (q && q.hasNext()) {
61
+ relationship = q.next();
62
+ relationship_data.each {
63
+ if (relationship.getProperty(it.key) != it.value) {
64
+ relationship.setProperty(it.key, it.value);
65
+ }
66
+ }
67
+ } else {
68
+ node_index = g.idx('node_auto_index');
69
+ start_node = node_index.get(unique_id_key, start_node_unique_id).next();
70
+ end_node = node_index.get(unique_id_key, end_node_unique_id).next();
71
+
72
+ relationship = g.addEdge(start_node, end_node, rel_type, relationship_data);
73
+ }
74
+
75
+ relationship
76
+ GREMLIN
77
+
78
+ script_vars = {
79
+ unique_id_key: Lexster::UNIQUE_ID_KEY,
80
+ relationship_data: data,
81
+ unique_id: self.neo_unique_id,
82
+ start_node_unique_id: start_item.neo_unique_id,
83
+ end_node_unique_id: end_item.neo_unique_id,
84
+ rel_type: rel_type
85
+ }
86
+
87
+ Lexster::logger.info "Relationship#neo_save #{self.class.name} #{self.id}"
88
+
89
+ relationship = Lexster.execute_script_or_add_to_batch gremlin_query, script_vars do |value|
90
+ Lexster::Relationship.from_hash(value)
91
+ end
92
+
93
+ relationship
94
+ end
95
+
96
+ def neo_load(hash)
97
+ Lexster::Relationship.from_hash(hash)
98
+ end
99
+
100
+ def neo_relationship
101
+ _neo_representation
102
+ end
103
+ end
104
+
105
+ def self.included(receiver)
106
+ receiver.send :include, Lexster::ModelAdditions
107
+ receiver.send :include, InstanceMethods
108
+ receiver.extend ClassMethods
109
+
110
+ initialize_relationship receiver if Lexster.env_loaded
111
+
112
+ Lexster.relationship_models << receiver
113
+ end
114
+
115
+ def self.meta_data
116
+ @meta_data ||= {}
117
+ end
118
+
119
+ def self.initialize_relationship(rel_model)
120
+ rel_model.reflect_on_all_associations(:belongs_to).each do |belongs_to|
121
+ return if belongs_to.options[:polymorphic]
122
+
123
+ # e.g. all has_many on User class
124
+ all_has_many = belongs_to.klass.reflect_on_all_associations(:has_many)
125
+
126
+ # has_many (without through) on the side of the relationship that removes a relationship. e.g. User has_many :likes
127
+ this_has_many = all_has_many.find { |o| o.klass == rel_model }
128
+ next unless this_has_many
129
+
130
+ # e.g. User has_many :likes, after_remove: ...
131
+ full_callback_name = "after_remove_for_#{this_has_many.name}"
132
+ belongs_to.klass.send(full_callback_name) << :neo_after_relationship_remove if belongs_to.klass.method_defined?(full_callback_name)
133
+
134
+ # has_many (with through) on the side of the relationship that removes a relationship. e.g. User has_many :movies, through :likes
135
+ many_to_many = all_has_many.find { |o| o.options[:through] == this_has_many.name }
136
+ next unless many_to_many
137
+
138
+ return if many_to_many.options[:as] # polymorphic are not supported here yet
139
+
140
+ # user_id
141
+ foreign_key_of_owner = many_to_many.through_reflection.foreign_key
142
+
143
+ # movie_id
144
+ foreign_key_of_record = many_to_many.source_reflection.foreign_key
145
+
146
+ (Lexster::Relationship.meta_data ||= {}).tap do |data|
147
+ (data[belongs_to.klass.name.to_s] ||= {}).tap do |model_data|
148
+ model_data[many_to_many.klass.name.to_s] = [rel_model.name.to_s, foreign_key_of_owner, foreign_key_of_record]
149
+ end
150
+ end
151
+
152
+ # e.g. User has_many :movies, through: :likes, before_remove: ...
153
+ full_callback_name = "before_remove_for_#{many_to_many.name}"
154
+ belongs_to.klass.send(full_callback_name) << :neo_before_relationship_through_remove if belongs_to.klass.method_defined?(full_callback_name)
155
+
156
+
157
+ # e.g. User has_many :movies, through: :likes, after_remove: ...
158
+ full_callback_name = "after_remove_for_#{many_to_many.name}"
159
+ belongs_to.klass.send(full_callback_name) << :neo_after_relationship_through_remove if belongs_to.klass.method_defined?(full_callback_name)
160
+ end
161
+ end
162
+ end
163
+ end