lexster 0.0.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.
@@ -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