neoid 0.0.2 → 0.0.5.alpha

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/lib/neoid/node.rb CHANGED
@@ -11,50 +11,108 @@ module Neoid
11
11
  def neo_subref_node
12
12
  @_neo_subref_node ||= begin
13
13
  Neoid::logger.info "Node#neo_subref_node #{neo_subref_rel_type}"
14
-
15
- subref_node_query = Neoid.ref_node.outgoing(neo_subref_rel_type)
16
-
17
- if subref_node_query.to_a.blank?
18
- node = Neography::Node.create(type: self.name, name: neo_subref_rel_type)
19
- Neography::Relationship.create(
20
- neo_subref_rel_type,
21
- Neoid.ref_node,
22
- node
23
- )
24
- else
25
- node = subref_node_query.first
26
- end
27
-
14
+
15
+ gremlin_query = <<-GREMLIN
16
+ q = g.v(0).out(neo_subref_rel_type);
17
+
18
+ subref = q.hasNext() ? q.next() : null;
19
+
20
+ if (!subref) {
21
+ subref = g.addVertex([name: neo_subref_rel_type, type: name]);
22
+ g.addEdge(g.v(0), subref, neo_subref_rel_type);
23
+ }
24
+
25
+ g.createManualIndex(neo_index_name, Vertex.class);
26
+
27
+ subref
28
+ GREMLIN
29
+
30
+ Neoid.logger.info "subref query:\n#{gremlin_query}"
31
+
32
+ node = Neography::Node.load(Neoid.db.execute_script(gremlin_query, neo_subref_rel_type: neo_subref_rel_type, name: self.name, neo_index_name: self.neo_index_name))
33
+
28
34
  node
29
35
  end
30
36
  end
37
+
38
+ def neo_search(term, options = {})
39
+ Neoid.search(self, term, options)
40
+ end
31
41
  end
32
42
 
33
43
  module InstanceMethods
34
44
  def neo_find_by_id
35
45
  Neoid::logger.info "Node#neo_find_by_id #{self.class.neo_index_name} #{self.id}"
36
46
  Neoid.db.get_node_index(self.class.neo_index_name, :ar_id, self.id)
47
+ rescue Neography::NotFoundException
48
+ nil
37
49
  end
38
50
 
39
51
  def neo_create
52
+ return unless Neoid.enabled?
53
+
40
54
  data = self.to_neo.merge(ar_type: self.class.name, ar_id: self.id)
41
55
  data.reject! { |k, v| v.nil? }
42
56
 
43
57
  node = Neography::Node.create(data)
44
58
 
45
- Neography::Relationship.create(
46
- self.class.neo_subref_node_rel_type,
47
- self.class.neo_subref_node,
48
- node
49
- )
50
-
59
+ retires = 2
60
+ begin
61
+ Neography::Relationship.create(
62
+ self.class.neo_subref_node_rel_type,
63
+ self.class.neo_subref_node,
64
+ node
65
+ )
66
+ rescue
67
+ # something must've happened to the cached subref node, reset and retry
68
+ @_neo_subref_node = nil
69
+ retires -= 1
70
+ retry if retires > 0
71
+ end
72
+
51
73
  Neoid.db.add_node_to_index(self.class.neo_index_name, :ar_id, self.id, node)
52
74
 
53
75
  Neoid::logger.info "Node#neo_create #{self.class.name} #{self.id}, index = #{self.class.neo_index_name}"
76
+
77
+ neo_search_index
54
78
 
55
79
  node
56
80
  end
57
81
 
82
+ def neo_update
83
+ Neoid.db.set_node_properties(neo_node, self.to_neo)
84
+ neo_search_index
85
+ end
86
+
87
+ def neo_search_index
88
+ return if self.class.neoid_config.search_options.blank? || (
89
+ self.class.neoid_config.search_options.index_fields.blank? &&
90
+ self.class.neoid_config.search_options.fulltext_fields.blank?
91
+ )
92
+
93
+ Neoid.ensure_default_fulltext_search_index
94
+
95
+ Neoid.db.add_node_to_index(DEFAULT_FULLTEXT_SEARCH_INDEX_NAME, 'ar_type', self.class.name, neo_node.neo_id)
96
+
97
+ self.class.neoid_config.search_options.fulltext_fields.each do |field, options|
98
+ Neoid.db.add_node_to_index(DEFAULT_FULLTEXT_SEARCH_INDEX_NAME, "#{field}_fulltext", neo_helper_get_field_value(field, options), neo_node.neo_id)
99
+ end
100
+
101
+ self.class.neoid_config.search_options.index_fields.each do |field, options|
102
+ Neoid.db.add_node_to_index(DEFAULT_FULLTEXT_SEARCH_INDEX_NAME, field, neo_helper_get_field_value(field, options), neo_node.neo_id)
103
+ end
104
+
105
+ neo_node
106
+ end
107
+
108
+ def neo_helper_get_field_value(field, options = {})
109
+ if options[:block]
110
+ options[:block].call
111
+ else
112
+ self.send(field) rescue (raise "No field #{field} for #{self.class.name}")
113
+ end
114
+ end
115
+
58
116
  def neo_load(node)
59
117
  Neography::Node.load(node)
60
118
  end
@@ -65,21 +123,39 @@ module Neoid
65
123
 
66
124
  def neo_destroy
67
125
  return unless neo_node
126
+ Neoid.db.remove_node_from_index(DEFAULT_FULLTEXT_SEARCH_INDEX_NAME, neo_node)
127
+
68
128
  Neoid.db.remove_node_from_index(self.class.neo_index_name, neo_node)
69
129
  neo_node.del
130
+ _reset_neo_representation
131
+ end
132
+
133
+ def neo_after_relationship_remove(relationship)
134
+ relationship.neo_destroy
135
+ end
136
+
137
+ def neo_before_relationship_through_remove(record)
138
+ rel_model, foreign_key_of_owner, foreign_key_of_record = Neoid.config[:relationship_meta_data][self.class.name.to_s][record.class.name.to_s]
139
+ rel_model = rel_model.to_s.constantize
140
+ @__neo_temp_rels ||= {}
141
+ @__neo_temp_rels[record] = rel_model.where(foreign_key_of_owner => self.id, foreign_key_of_record => record.id).first
142
+ end
143
+
144
+ def neo_after_relationship_through_remove(record)
145
+ @__neo_temp_rels.each { |record, relationship| relationship.neo_destroy }
146
+ @__neo_temp_rels.delete(record)
70
147
  end
71
148
  end
72
149
 
73
150
  def self.included(receiver)
74
- receiver.extend Neoid::ModelAdditions::ClassMethods
75
- receiver.send :include, Neoid::ModelAdditions::InstanceMethods
151
+ receiver.send :include, Neoid::ModelAdditions
76
152
  receiver.extend ClassMethods
77
153
  receiver.send :include, InstanceMethods
78
-
79
- receiver.neo_subref_node # ensure
154
+ Neoid.node_models << receiver
80
155
 
81
156
  receiver.after_create :neo_create
157
+ receiver.after_update :neo_update
82
158
  receiver.after_destroy :neo_destroy
83
159
  end
84
160
  end
85
- end
161
+ end
@@ -0,0 +1,15 @@
1
+ require 'neoid/middleware'
2
+
3
+ module Neoid
4
+ class Railtie < Rails::Railtie
5
+ initializer "neoid.configure_rails_initialization" do
6
+ config.after_initialize do
7
+ Neoid.initialize_all
8
+ end
9
+ end
10
+
11
+ initializer 'neoid.inject_middleware' do |app|
12
+ app.middleware.use Ndoid::Middleware
13
+ end
14
+ end
15
+ end
@@ -3,20 +3,29 @@ module Neoid
3
3
  module InstanceMethods
4
4
  def neo_find_by_id
5
5
  Neoid.db.get_relationship_index(self.class.neo_index_name, :ar_id, self.id)
6
+ rescue Neography::NotFoundException
7
+ nil
6
8
  end
7
9
 
8
10
  def neo_create
9
- options = self.class.neoidable_options
11
+ return unless Neoid.enabled?
12
+ @_neo_destroyed = false
13
+
14
+ options = self.class.neoid_config.relationship_options
10
15
 
11
16
  start_node = self.send(options[:start_node])
12
17
  end_node = self.send(options[:end_node])
13
18
 
14
19
  return unless start_node && end_node
20
+
21
+ data = self.to_neo.merge(ar_type: self.class.name, ar_id: self.id)
22
+ data.reject! { |k, v| v.nil? }
15
23
 
16
24
  relationship = Neography::Relationship.create(
17
- options[:type],
25
+ options[:type].is_a?(Proc) ? options[:type].call(self) : options[:type],
18
26
  start_node.neo_node,
19
- end_node.neo_node
27
+ end_node.neo_node,
28
+ data
20
29
  )
21
30
 
22
31
  Neoid.db.add_relationship_to_index(self.class.neo_index_name, :ar_id, self.id, relationship)
@@ -31,23 +40,89 @@ module Neoid
31
40
  end
32
41
 
33
42
  def neo_destroy
43
+ return if @_neo_destroyed
44
+ @_neo_destroyed = true
34
45
  return unless neo_relationship
35
46
  Neoid.db.remove_relationship_from_index(self.class.neo_index_name, neo_relationship)
36
47
  neo_relationship.del
48
+ _reset_neo_representation
49
+
50
+ Neoid::logger.info "Relationship#neo_destroy #{self.class.name} #{self.id}, index = #{self.class.neo_index_name}"
51
+
52
+ true
37
53
  end
38
54
 
55
+ def neo_update
56
+ Neoid.db.set_relationship_properties(neo_relationship, self.to_neo) if neo_relationship
57
+ end
58
+
39
59
  def neo_relationship
40
60
  _neo_representation
41
61
  end
42
62
  end
43
63
 
44
64
  def self.included(receiver)
45
- receiver.extend Neoid::ModelAdditions::ClassMethods
46
- receiver.send :include, Neoid::ModelAdditions::InstanceMethods
65
+ receiver.send :include, Neoid::ModelAdditions
47
66
  receiver.send :include, InstanceMethods
48
67
 
49
68
  receiver.after_create :neo_create
50
69
  receiver.after_destroy :neo_destroy
70
+ receiver.after_update :neo_update
71
+
72
+ if Neoid.env_loaded
73
+ initialize_relationship receiver
74
+ else
75
+ Neoid.relationship_models << receiver
76
+ end
77
+ end
78
+
79
+ def self.initialize_relationship(rel_model)
80
+ rel_model.reflect_on_all_associations(:belongs_to).each do |belongs_to|
81
+ return if belongs_to.options[:polymorphic]
82
+
83
+ # e.g. all has_many on User class
84
+ all_has_many = belongs_to.klass.reflect_on_all_associations(:has_many)
85
+
86
+ # has_many (without through) on the side of the relationship that removes a relationship. e.g. User has_many :likes
87
+ this_has_many = all_has_many.find { |o| o.klass == rel_model }
88
+ next unless this_has_many
89
+
90
+ # e.g. User has_many :likes, after_remove: ...
91
+ full_callback_name = "after_remove_for_#{this_has_many.name}"
92
+ belongs_to.klass.send(full_callback_name) << :neo_after_relationship_remove if belongs_to.klass.method_defined?(full_callback_name)
93
+ # belongs_to.klass.send(:has_many, this_has_many.name, this_has_many.options.merge(after_remove: :neo_after_relationship_remove))
94
+
95
+ # has_many (with through) on the side of the relationship that removes a relationship. e.g. User has_many :movies, through :likes
96
+ many_to_many = all_has_many.find { |o| o.options[:through] == this_has_many.name }
97
+ next unless many_to_many
98
+
99
+ return if many_to_many.options[:as] # polymorphic are not supported here yet
100
+
101
+ # user_id
102
+ foreign_key_of_owner = many_to_many.through_reflection.foreign_key
103
+
104
+ # movie_id
105
+ foreign_key_of_record = many_to_many.source_reflection.foreign_key
106
+
107
+ (Neoid.config[:relationship_meta_data] ||= {}).tap do |data|
108
+ (data[belongs_to.klass.name.to_s] ||= {}).tap do |model_data|
109
+ model_data[many_to_many.klass.name.to_s] = [rel_model.name.to_s, foreign_key_of_owner, foreign_key_of_record]
110
+ end
111
+ end
112
+
113
+ # puts Neoid.config[:relationship_meta_data].inspect
114
+
115
+ # e.g. User has_many :movies, through: :likes, before_remove: ...
116
+ full_callback_name = "before_remove_for_#{many_to_many.name}"
117
+ belongs_to.klass.send(full_callback_name) << :neo_before_relationship_through_remove if belongs_to.klass.method_defined?(full_callback_name)
118
+ # belongs_to.klass.send(:has_many, many_to_many.name, many_to_many.options.merge(before_remove: :neo_after_relationship_remove))
119
+
120
+
121
+ # e.g. User has_many :movies, through: :likes, after_remove: ...
122
+ full_callback_name = "after_remove_for_#{many_to_many.name}"
123
+ belongs_to.klass.send(full_callback_name) << :neo_after_relationship_through_remove if belongs_to.klass.method_defined?(full_callback_name)
124
+ # belongs_to.klass.send(:has_many, many_to_many.name, many_to_many.options.merge(after_remove: :neo_after_relationship_remove))
125
+ end
51
126
  end
52
127
  end
53
- end
128
+ end
@@ -0,0 +1,28 @@
1
+ module Neoid
2
+ class SearchSession
3
+ def initialize(response, *models)
4
+ @response = response || []
5
+ @models = models
6
+ end
7
+
8
+ def hits
9
+ @response.map { |x| Neography::Node.new(x) }
10
+ end
11
+
12
+ def ids
13
+ @response.collect { |x| x['data']['ar_id'] }
14
+ end
15
+
16
+ def results
17
+ models_by_name = @models.inject({}) { |all, curr| all[curr.name] = curr; all }
18
+
19
+ ids_by_klass = @response.inject({}) do |all, curr|
20
+ klass_name = curr['data']['ar_type']
21
+ (all[models_by_name[klass_name]] ||= []) << curr['data']['ar_id']
22
+ all
23
+ end
24
+
25
+ ids_by_klass.map { |klass, ids| klass.where(id: ids) }.flatten
26
+ end
27
+ end
28
+ end
data/lib/neoid/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Neoid
2
- VERSION = "0.0.2"
2
+ VERSION = "0.0.5.alpha"
3
3
  end
data/lib/neoid.rb CHANGED
@@ -1,14 +1,43 @@
1
- require "neoid/version"
2
- require "neoid/model_config"
3
- require "neoid/model_additions"
4
- require "neoid/node"
5
- require "neoid/relationship"
1
+ require 'neoid/version'
2
+ require 'neoid/model_config'
3
+ require 'neoid/model_additions'
4
+ require 'neoid/search_session'
5
+ require 'neoid/node'
6
+ require 'neoid/relationship'
7
+ require 'neoid/database_cleaner'
8
+ require 'neoid/railtie' if defined?(Rails)
6
9
 
7
10
  module Neoid
11
+ DEFAULT_FULLTEXT_SEARCH_INDEX_NAME = 'neoid_default_search_index'
12
+
8
13
  class << self
9
14
  attr_accessor :db
10
15
  attr_accessor :logger
11
16
  attr_accessor :ref_node
17
+ attr_accessor :env_loaded
18
+
19
+ def models
20
+ @models ||= []
21
+ end
22
+
23
+ def node_models
24
+ @node_models ||= []
25
+ end
26
+
27
+ def relationship_models
28
+ @relationship_models ||= []
29
+ end
30
+
31
+ def config
32
+ @config ||= {}
33
+ end
34
+
35
+ def initialize_all
36
+ @env_loaded = true
37
+ relationship_models.each do |rel_model|
38
+ Relationship.initialize_relationship(rel_model)
39
+ end
40
+ end
12
41
 
13
42
  def db
14
43
  raise "Must set Neoid.db with a Neography::Rest instance" unless @db
@@ -16,11 +45,115 @@ module Neoid
16
45
  end
17
46
 
18
47
  def logger
19
- @logger ||= Logger.new(ENV['NEOID_LOG'] ? $stdout : '/dev/null')
48
+ @logger ||= Logger.new(ENV['NEOID_LOG'] ? ENV['NEOID_LOG_FILE'] || $stdout : '/dev/null')
20
49
  end
21
50
 
22
51
  def ref_node
23
52
  @ref_node ||= Neography::Node.load(Neoid.db.get_root['self'])
24
53
  end
54
+
55
+ def reset_cached_variables
56
+ Neoid.models.each do |klass|
57
+ klass.instance_variable_set(:@_neo_subref_node, nil)
58
+ end
59
+ $neo_ref_node = nil
60
+ end
61
+
62
+ def clean_db(confirm)
63
+ puts "must call with confirm: Neoid.clean_db(:yes_i_am_sure)" and return unless confirm == :yes_i_am_sure
64
+ Neoid::NeoDatabaseCleaner.clean_db
65
+ end
66
+
67
+
68
+ def enabled=(flag)
69
+ Thread.current[:neoid_enabled] = flag
70
+ end
71
+
72
+ def enabled
73
+ flag = Thread.current[:neoid_enabled]
74
+ # flag should be set by the middleware. in case it wasn't (non-rails app or console), default it to true
75
+ flag.nil? ? true : flag
76
+ end
77
+ alias enabled? enabled
78
+
79
+ def use(flag=true)
80
+ old, self.enabled = enabled?, flag
81
+ yield if block_given?
82
+ ensure
83
+ self.enabled = old
84
+ end
85
+
86
+ # create a fulltext index if not exists
87
+ def ensure_default_fulltext_search_index
88
+ Neoid.db.create_node_index(DEFAULT_FULLTEXT_SEARCH_INDEX_NAME, 'fulltext', 'lucene') unless (indexes = Neoid.db.list_node_indexes) && indexes[DEFAULT_FULLTEXT_SEARCH_INDEX_NAME]
89
+ end
90
+
91
+ def search(types, term, options = {})
92
+ options = options.reverse_merge(limit: 15)
93
+
94
+ types = [*types]
95
+
96
+ query = []
97
+
98
+ types.each do |type|
99
+ query_for_type = []
100
+
101
+ query_for_type << "ar_type:#{type.name}"
102
+
103
+ case term
104
+ when String
105
+ search_in_fields = type.neoid_config.search_options.fulltext_fields.keys
106
+ next if search_in_fields.empty?
107
+ query_for_type << search_in_fields.map{ |field| generate_field_query(field, term, true) }.join(" OR ")
108
+ when Hash
109
+ term.each do |field, value|
110
+ query_for_type << generate_field_query(field, value, false)
111
+ end
112
+ end
113
+
114
+ query << "(#{query_for_type.join(") AND (")})"
115
+ end
116
+
117
+ query = "(#{query.join(") OR (")})"
118
+
119
+ logger.info "Neoid query #{query}"
120
+
121
+ gremlin_query = <<-GREMLIN
122
+ #{options[:before_query]}
123
+
124
+ idx = g.getRawGraph().index().forNodes('#{DEFAULT_FULLTEXT_SEARCH_INDEX_NAME}')
125
+ hits = idx.query('#{sanitize_query_for_gremlin(query)}')
126
+
127
+ hits = #{options[:limit] ? "hits.take(#{options[:limit]})" : "hits"}
128
+
129
+ #{options[:after_query]}
130
+ GREMLIN
131
+
132
+ logger.info "[NEOID] search:\n#{gremlin_query}"
133
+
134
+ results = Neoid.db.execute_script(gremlin_query)
135
+
136
+ SearchSession.new(results, *types)
137
+ end
138
+
139
+ private
140
+ def sanitize_term(term)
141
+ # TODO - case sensitive?
142
+ term.downcase
143
+ end
144
+
145
+ def sanitize_query_for_gremlin(query)
146
+ # TODO - case sensitive?
147
+ query.gsub("'", "\\\\'")
148
+ end
149
+
150
+ def generate_field_query(field, term, fulltext = false)
151
+ term = term.to_s if term
152
+ return "" if term.nil? || term.empty?
153
+
154
+ fulltext = fulltext ? "_fulltext" : nil
155
+
156
+ "(" + term.split(/\s+/).reject(&:empty?).map{ |t| "#{field}#{fulltext}:#{sanitize_term(t)}" }.join(" AND ") + ")"
157
+ end
25
158
  end
26
159
  end
data/neoid.gemspec CHANGED
@@ -18,9 +18,11 @@ Gem::Specification.new do |s|
18
18
  s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
19
19
  s.require_paths = ["lib"]
20
20
 
21
- s.add_development_dependency "rake"
22
- s.add_development_dependency "rspec"
23
- s.add_development_dependency "rest-client"
24
- s.add_development_dependency "supermodel"
25
- s.add_runtime_dependency "neography"
21
+ s.add_development_dependency 'rake'
22
+ s.add_development_dependency 'rspec'
23
+ s.add_development_dependency 'rest-client'
24
+ s.add_development_dependency 'activerecord'
25
+ s.add_development_dependency 'sqlite3'
26
+
27
+ s.add_runtime_dependency 'neography'
26
28
  end