neoid 0.0.2 → 0.0.5.alpha

Sign up to get free protection for your applications and to get access to all the features.
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