flex-models 1.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.
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2012-2013 by Domizio Demichelis
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,24 @@
1
+ # Flex-model
2
+
3
+ Transparently integrates your models with one or more elasticsearch indices
4
+
5
+ ## Links
6
+
7
+ * [Flex Repository](https://github.com/ddnexus/flex)
8
+ * [Flex Project (Global Documentation)](http://ddnexus.github.io/flex/doc/)
9
+ * [flex-models Gem (Specific Documentation)](http://ddnexus.github.io/flex/doc/4-flex-models)
10
+ * [Issues](https://github.com/ddnexus/flex-models/issues)
11
+ * [Pull Requests](https://github.com/ddnexus/flex-models/pulls)
12
+
13
+ ## Branches
14
+
15
+ The master branch reflects the last published gem. Then you may find a next-version branch (named after the version string), with the commits that will be merged in master just before publishing the next gem version. The next-version branch may get rebased or force pushed.
16
+
17
+ ## Credits
18
+
19
+ Special thanks for their sponsorship to [Escalate Media](http://www.escalatemedia.com) and [Barquin International](http://www.barquin.com).
20
+
21
+ ## Copyright
22
+
23
+ Copyright (c) 2012-2013 by [Domizio Demichelis](mailto://dd.nexus@gmail.com)<br>
24
+ See [LICENSE](https://github.com/ddnexus/flex-models/blob/master/LICENSE) for details.
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 1.0.1
@@ -0,0 +1,22 @@
1
+ require 'date'
2
+ version = File.read(File.expand_path('../VERSION', __FILE__)).strip
3
+
4
+ Gem::Specification.new do |s|
5
+ s.name = 'flex-models'
6
+ s.summary = 'Transparently integrates your models with one or more elasticsearch indices.'
7
+ s.description = 'Provides ActiveRecord, Mongoid, ActiveModel and elasticsearch-mapper-attachment integrations, cross syncing, parent/child relationships, bulk-import, live-reindex of models, ...'
8
+ s.homepage = 'http://github.com/ddnexus/flex-models'
9
+ s.authors = ["Domizio Demichelis"]
10
+ s.email = 'dd.nexus@gmail.com'
11
+ s.extra_rdoc_files = %w[README.md]
12
+ s.files = `git ls-files -z`.split("\0")
13
+ s.version = version
14
+ s.date = Date.today.to_s
15
+ s.required_rubygems_version = ">= 1.3.6"
16
+ s.rdoc_options = %w[--charset=UTF-8]
17
+
18
+ s.add_runtime_dependency 'flex', version
19
+ s.add_runtime_dependency 'flex-scopes', version
20
+
21
+ s.add_runtime_dependency 'active_attr', '>= 0.6.0'
22
+ end
@@ -0,0 +1,42 @@
1
+ require 'base64'
2
+ module Flex
3
+ module ActiveModel
4
+ module Attachment
5
+
6
+ # defines accessors for <attachment_field_name>
7
+ # if you omit the arguments it uses :attachment as the <attachment_field_name>
8
+ # you can also pass other properties that will be merged with the default property for attachment
9
+ # this will automatically add a :<attachment_field_name>_scope scope which will add
10
+ # all the meta fields (title, author, ...) to the returned fields, exluding the <attachment_field_name> field itself
11
+ # and including all the other attributes declared before it. For that reason you may want to declare it as
12
+ # the latest attribute.
13
+
14
+ def attribute_attachment(*args)
15
+ name = args.first.is_a?(Symbol) ? args.shift : :attachment
16
+ props = {:properties => { 'type' => 'attachment',
17
+ 'fields' => { name.to_s => { 'store' => 'yes', 'term_vector' => 'with_positions_offsets' },
18
+ 'title' => { 'store' => 'yes' },
19
+ 'author' => { 'store' => 'yes' },
20
+ 'name' => { 'store' => 'yes' },
21
+ 'content_type' => { 'store' => 'yes' },
22
+ 'date' => { 'store' => 'yes' },
23
+ 'keywords' => { 'store' => 'yes' }
24
+ }
25
+ }
26
+ }
27
+ props.extend(Struct::Mergeable).deep_merge! args.first if args.first.is_a?(Hash)
28
+
29
+ scope :"#{name}_scope", fields("#{name}.title",
30
+ "#{name}.author",
31
+ "#{name}.name",
32
+ "#{name}.content_type",
33
+ "#{name}.date",
34
+ "#{name}.keywords",
35
+ *attributes.keys)
36
+ attribute name, props
37
+
38
+ end
39
+
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,21 @@
1
+ module Flex
2
+ module ActiveModel
3
+ module Inspection
4
+
5
+ def inspect
6
+ descriptions = [%(_id: #{@_id.inspect}), %(_version: #{@_version})]
7
+ all_attributes = if respond_to?(:raw_document)
8
+ reader_keys = raw_document.send(:readers).keys.map(&:to_s)
9
+ # we send() the readers, so they will reflect an eventual overriding
10
+ Hash[ reader_keys.map{ |k| [k, send(k)] } ].merge(attributes)
11
+ else
12
+ attributes
13
+ end
14
+ descriptions << all_attributes.sort.map { |key, value| "#{key}: #{value.inspect}" }
15
+ separator = " " unless descriptions.empty?
16
+ "#<#{self.class.name}#{separator}#{descriptions.join(", ")}>"
17
+ end
18
+
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,127 @@
1
+ module Flex
2
+ module ActiveModel
3
+
4
+ class DocumentInvalidError < StandardError; end
5
+
6
+ module Storage
7
+
8
+ module ClassMethods
9
+
10
+ def create(args={})
11
+ document = new(args)
12
+ return false unless document.valid?
13
+ document.save
14
+ end
15
+
16
+ end
17
+
18
+
19
+ module InstanceMethods
20
+
21
+ def reload
22
+ document = flex.get
23
+ self.attributes = document['_source']
24
+ @_id = document['_id']
25
+ @_version = document['_version']
26
+ end
27
+
28
+ def save(options={})
29
+ perform_validations(options) ? do_save : false
30
+ end
31
+
32
+ def save!(options={})
33
+ perform_validations(options) ? do_save : raise(DocumentInvalidError, errors.full_messages.join(", "))
34
+ end
35
+
36
+ # Optimistic Lock Update
37
+ #
38
+ # doc.safe_update do |d|
39
+ # d.amount += 100
40
+ # end
41
+ #
42
+ # if you are trying to update a stale object, the block is yielded again with a fresh reloaded document and the
43
+ # document is saved only when it is not stale anymore (i.e. the _version has not changed since it has been loaded)
44
+ # read: http://www.elasticsearch.org/blog/2011/02/08/versioning.html
45
+ #
46
+ def safe_update(options={}, &block)
47
+ perform_validations(options) ? lock_update(&block) : false
48
+ end
49
+
50
+ def safe_update!(options={}, &block)
51
+ perform_validations(options) ? lock_update(&block) : raise(DocumentInvalidError, errors.full_messages.join(", "))
52
+ end
53
+
54
+ def valid?(context = nil)
55
+ context ||= (new_record? ? :create : :update)
56
+ output = super(context)
57
+ errors.empty? && output
58
+ end
59
+
60
+ def destroy
61
+ @destroyed = true
62
+ flex.sync
63
+ self.freeze
64
+ end
65
+
66
+ def delete
67
+ @skip_destroy_callbacks = true
68
+ destroy
69
+ end
70
+
71
+ def merge_attributes(attributes)
72
+ attributes.each {|name, value| send "#{name}=", value }
73
+ end
74
+
75
+ def update_attributes(attributes)
76
+ merge_attributes(attributes)
77
+ save
78
+ end
79
+
80
+ def destroyed?
81
+ !!@destroyed
82
+ end
83
+
84
+ def persisted?
85
+ !(new_record? || destroyed?)
86
+ end
87
+
88
+ def new_record?
89
+ !@_id || !@_version
90
+ end
91
+
92
+ private
93
+
94
+ def do_save
95
+ flex.sync
96
+ self
97
+ end
98
+
99
+ def lock_update
100
+ begin
101
+ yield self
102
+ flex.sync
103
+ rescue Flex::HttpError => e
104
+ if e.status == 409
105
+ reload
106
+ retry
107
+ else
108
+ raise
109
+ end
110
+ end
111
+ self
112
+ end
113
+
114
+ protected
115
+
116
+ def perform_validations(options={})
117
+ perform_validation = options[:validate] != false
118
+ perform_validation ? valid?(options[:context]) : true
119
+ end
120
+
121
+ end
122
+
123
+ end
124
+
125
+
126
+ end
127
+ end
@@ -0,0 +1,22 @@
1
+ module Flex
2
+ module ActiveModel
3
+ module Timestamps
4
+
5
+ def attribute_timestamps(props={})
6
+ attribute_created_at props
7
+ attribute_updated_at props
8
+ end
9
+
10
+ def attribute_created_at(props={})
11
+ attribute :created_at, {:type => DateTime}.merge(props)
12
+ before_create { self.created_at = Time.now.utc }
13
+ end
14
+
15
+ def attribute_updated_at(props={})
16
+ attribute :updated_at, {:type => DateTime}.merge(props)
17
+ before_save { self.updated_at = Time.now.utc }
18
+ end
19
+
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,47 @@
1
+ module Flex
2
+ module ActiveModel
3
+
4
+ attr_reader :_version, :_id, :highlight
5
+ alias_method :id, :_id
6
+
7
+ def self.included(base)
8
+ base.class_eval do
9
+ @flex ||= ClassProxy::Base.new(base)
10
+ @flex.extend(ClassProxy::ModelSyncer)
11
+ @flex.extend(ClassProxy::ModelIndexer).init
12
+ @flex.extend(ClassProxy::ActiveModel).init :params => {:version => true}
13
+ def self.flex; @flex end
14
+ flex.synced = [self]
15
+
16
+ include Scopes
17
+ include ActiveAttr::Model
18
+
19
+ extend ::ActiveModel::Callbacks
20
+ define_model_callbacks :create, :update, :save, :destroy
21
+
22
+ include Storage::InstanceMethods
23
+ extend Storage::ClassMethods
24
+ include Inspection
25
+ extend Timestamps
26
+ extend Attachment
27
+ end
28
+ end
29
+
30
+ def flex
31
+ @flex ||= InstanceProxy::ActiveModel.new(self)
32
+ end
33
+
34
+ def flex_source
35
+ attributes
36
+ end
37
+
38
+ def flex_indexable?
39
+ true
40
+ end
41
+
42
+ def method_missing(meth, *args, &block)
43
+ raw_document.respond_to?(meth) ? raw_document.send(meth) : super
44
+ end
45
+
46
+ end
47
+ end
@@ -0,0 +1,35 @@
1
+ module Flex
2
+ module ClassProxy
3
+ module ActiveModel
4
+
5
+ def init(*vars)
6
+ variables.deep_merge! *vars
7
+ end
8
+
9
+ def default_mapping
10
+ props = { }
11
+ context.attributes.each do |name, attr|
12
+ options = attr.send(:options)
13
+ props[name] = case
14
+ when options.has_key?(:properties)
15
+ Utils.keyfy(:to_s, attr.send(:options)[:properties])
16
+ when options.has_key?(:not_analyzed) && options[:not_analyzed] ||
17
+ options.has_key?(:analyzed) && !options[:analyzed]
18
+ { 'type' => 'string', 'index' => 'not_analyzed' }
19
+ when options[:type] == DateTime
20
+ { 'type' => 'date', 'format' => 'dateOptionalTime' }
21
+ else
22
+ next
23
+ end
24
+ end
25
+ props.empty? ? super : super.deep_merge(index => {'mappings' => {type => {'properties' => props}}})
26
+ end
27
+
28
+ # overrides the ModelSyncer#add_callbacks
29
+ def add_callbacks
30
+ # no callbacks to add, since it calls flex.sync on save and destroy
31
+ end
32
+
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,46 @@
1
+ module Flex
2
+ module ClassProxy
3
+ module ModelIndexer
4
+
5
+ module Types
6
+ extend self
7
+
8
+ attr_accessor :parents
9
+ @parents = []
10
+ end
11
+
12
+ attr_reader :parent_association, :parent_child_map
13
+
14
+ def init
15
+ variables.deep_merge! :type => Utils.class_name_to_type(context.name)
16
+ end
17
+
18
+ def parent(parent_association, map)
19
+ @parent_association = parent_association
20
+ Types.parents |= map.keys.map(&:to_s)
21
+ self.type = map.values.map(&:to_s)
22
+ @parent_child_map = map
23
+ @is_child = true
24
+ end
25
+
26
+ def is_child?
27
+ !!@is_child
28
+ end
29
+
30
+ def is_parent?
31
+ @is_parent ||= Types.parents.include?(type)
32
+ end
33
+
34
+ def default_mapping
35
+ default = {}.extend Struct::Mergeable
36
+ if is_child?
37
+ parent_child_map.each do |parent, child|
38
+ default.deep_merge! index => {'mappings' => {child => {'_parent' => {'type' => parent}}}}
39
+ end
40
+ end
41
+ default
42
+ end
43
+
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,30 @@
1
+ module Flex
2
+ module ClassProxy
3
+ module ModelSyncer
4
+
5
+ attr_accessor :synced
6
+
7
+ def sync(*synced)
8
+ # Flex::ActiveModel has its own way of syncing, and a Flex::ModelSyncer cannot be synced by itself
9
+ raise ArgumentError, %(You cannot flex.sync(self) #{context}.) \
10
+ if synced.any?{|s| s == context} && !context.include?(Flex::ModelIndexer)
11
+ synced.each do |s|
12
+ s == context || s.is_a?(Symbol) || s.is_a?(String) || raise(ArgumentError, "self, string or symbol expected, got #{s.inspect}")
13
+ end
14
+ @synced ||= []
15
+ @synced |= synced
16
+ add_callbacks
17
+ end
18
+
19
+ def add_callbacks
20
+ context.class_eval do
21
+ raise NotImplementedError, "the class #{self} must implement :after_save and :after_destroy callbacks" \
22
+ unless respond_to?(:after_save) && respond_to?(:after_destroy)
23
+ after_save { flex.sync }
24
+ after_destroy { flex.sync }
25
+ end
26
+ end
27
+
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,36 @@
1
+ module Flex
2
+ module InstanceProxy
3
+ class ActiveModel < ModelIndexer
4
+
5
+ def store(*vars)
6
+ return super unless instance.flex_indexable? # this should never happen since flex_indexable? returns true
7
+ meth = (id.nil? || id.empty?) ? :post_store : :put_store
8
+ Flex.send(meth, metainfo, {:data => instance.flex_source}, *vars)
9
+ end
10
+
11
+ def sync_self
12
+ instance.instance_eval do
13
+ if destroyed?
14
+ if @skip_destroy_callbacks
15
+ flex.remove
16
+ else
17
+ run_callbacks :destroy do
18
+ flex.remove
19
+ end
20
+ end
21
+ else
22
+ run_callbacks :save do
23
+ context = new_record? ? :create : :update
24
+ run_callbacks(context) do
25
+ result = context == :create ? flex.store : flex.store(:params => { :version => _version })
26
+ @_id = result['_id']
27
+ @_version = result['_version']
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,125 @@
1
+ module Flex
2
+ module InstanceProxy
3
+ class ModelIndexer < ModelSyncer
4
+
5
+ # delegates :index, :is_child?, :is_parent? to class_flex
6
+ Utils.define_delegation :to => :class_flex,
7
+ :in => self,
8
+ :by => :module_eval,
9
+ :for => [:is_child?, :is_parent?]
10
+
11
+ # indexes the document
12
+ # usually called from after_save, you can eventually call it explicitly for example from another callback
13
+ # or whenever the DB doesn't get updated by the model
14
+ # you can also pass the :data=>flex_source explicitly (useful for example to override the flex_source in the model)
15
+ def store(*vars)
16
+ if instance.flex_indexable?
17
+ Flex.store(metainfo, {:data => instance.flex_source}, *vars)
18
+ else
19
+ Flex.remove(metainfo, *vars) if Flex.get(metainfo, *vars, :raise => false)
20
+ end
21
+ end
22
+
23
+ # removes the document from the index (called from after_destroy)
24
+ def remove(*vars)
25
+ return unless instance.flex_indexable?
26
+ Flex.remove(metainfo, *vars)
27
+ end
28
+
29
+ # gets the document from ES
30
+ def get(*vars)
31
+ return unless instance.flex_indexable?
32
+ Flex.get(metainfo, *vars)
33
+ end
34
+
35
+ # like get, but it returns all the fields after a refresh
36
+ def full_get(*vars)
37
+ return unless instance.flex_indexable?
38
+ Flex.search_by_id(metainfo, {:refresh => true, :params => {:fields => '*,_source'}}, *vars)
39
+ end
40
+
41
+ def parent_instance
42
+ return unless is_child?
43
+ @parent_instance ||= instance.send(class_flex.parent_association) ||
44
+ raise(MissingParentError, "missing parent instance for document #{instance.inspect}.")
45
+ end
46
+
47
+ # helper that iterates through the parent record chain
48
+ # record.flex.each_parent{|p| p.do_something }
49
+ def each_parent
50
+ pi = parent_instance
51
+ while pi do
52
+ yield pi
53
+ pi = pi.flex.parent_instance
54
+ end
55
+ end
56
+
57
+ def index
58
+ @index ||= instance.respond_to?(:flex_index) ? instance.flex_index : class_flex.index
59
+ end
60
+ attr_writer :index
61
+
62
+ def type
63
+ @type ||= case
64
+ when instance.respond_to?(:flex_type) then instance.flex_type
65
+ when is_child? then class_flex.parent_child_map[parent_instance.flex.type]
66
+ else class_flex.type
67
+ end
68
+ end
69
+ attr_writer :type
70
+
71
+ def id
72
+ @id ||= instance.respond_to?(:flex_id) ? instance.flex_id : instance.id.to_s
73
+ end
74
+
75
+ def routing
76
+ @routing ||= case
77
+ when instance.respond_to?(:flex_routing) then instance.flex_routing
78
+ when is_child? then parent_instance.flex.routing
79
+ when is_parent? then create_routing
80
+ end
81
+ end
82
+ attr_writer :routing
83
+
84
+ def parent
85
+ @parent ||= case
86
+ when instance.respond_to?(:flex_parent) then instance.flex_parent
87
+ when is_child? then parent_instance.id.to_s
88
+ else nil
89
+ end
90
+ end
91
+ attr_writer :parent
92
+
93
+ def metainfo
94
+ meta = Vars.new( :index => index, :type => type, :id => id )
95
+ params = {}
96
+ params[:routing] = routing if routing
97
+ params[:parent] = parent if parent
98
+ meta.merge!(:params => params) unless params.empty?
99
+ meta
100
+ end
101
+
102
+ def sync_self
103
+ instance.destroyed? ? remove : store
104
+ end
105
+
106
+ private
107
+
108
+ BASE62_DIGITS = ('0'..'9').to_a + ('A'..'Z').to_a + ('a'..'z').to_a
109
+
110
+ def create_routing
111
+ string = [index, type, id].join
112
+ remainder = Digest::MD5.hexdigest(string).to_i(16)
113
+ result = []
114
+ max_power = ( Math.log(remainder) / Math.log(62) ).floor
115
+ max_power.downto(0) do |power|
116
+ digit, remainder = remainder.divmod(62**power)
117
+ result << digit
118
+ end
119
+ result << remainder if remainder > 0
120
+ result.map{|digit| BASE62_DIGITS[digit]}.join
121
+ end
122
+
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,54 @@
1
+ module Flex
2
+ module InstanceProxy
3
+ class ModelSyncer
4
+
5
+ attr_reader :instance, :class_flex
6
+
7
+ def initialize(instance)
8
+ @instance = instance
9
+ @class_flex = instance.class.flex
10
+ end
11
+
12
+ def sync(*trail)
13
+ return if trail.include?(uid) || class_flex.synced.nil?
14
+ trail << uid
15
+ class_flex.synced.each do |synced|
16
+ case
17
+ # sync self
18
+ when synced == instance.class
19
+ sync_self
20
+ # sync :author, :comments
21
+ # works for all association types, if the instances have a #flex proxy
22
+ when synced.is_a?(Symbol)
23
+ to_sync = instance.send(synced)
24
+ if to_sync.respond_to?(:each)
25
+ to_sync.each { |s| s.flex.sync(*trail) }
26
+ else
27
+ to_sync.flex.sync(*trail)
28
+ end
29
+ # sync 'blog'
30
+ # polymorphic: use this form only if you want to sync any specific parent type but not all
31
+ when synced.is_a?(String)
32
+ next unless synced == parent_instance.flex.type
33
+ parent_instance.flex.sync(*trail)
34
+ else
35
+ raise ArgumentError, "self, string or symbol expected, got #{synced.inspect}"
36
+ end
37
+ end
38
+ end
39
+
40
+ def uid
41
+ @uid ||= [instance.class, instance.id].join('-')
42
+ end
43
+
44
+ def refresh_index
45
+ class_flex.refresh_index
46
+ end
47
+
48
+ def sync_self
49
+ # nothing to sync, since a ModelSyncer cannot sync itselfs
50
+ end
51
+
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,67 @@
1
+ module Flex
2
+ # private module
3
+ module LiveReindex
4
+
5
+ def reindex_models(opts={})
6
+
7
+ raise NotImplementedError, 'Flex::LiveReindex.reindex_models requires the "flex-admin" gem. Please, install it.' \
8
+ unless defined?(Flex::Admin)
9
+
10
+ on_each_change do |action, document|
11
+ if action == 'index'
12
+ begin
13
+ { action => document.load! }
14
+ rescue Mongoid::Errors::DocumentNotFound, ActiveRecord::RecordNotFound
15
+ nil # record already deleted
16
+ end
17
+ else
18
+ { action => document }
19
+ end
20
+ end
21
+
22
+ yield self if block_given?
23
+
24
+ # we override the on_reindex eventually set
25
+ on_reindex do
26
+ opts = opts.merge(:force => false)
27
+ ModelTasks.new(opts).import_models
28
+ end
29
+
30
+ perform(opts)
31
+ end
32
+
33
+ def reindex_active_models(opts={})
34
+
35
+ raise NotImplementedError, 'Flex::LiveReindex.reindex_models requires the "flex-admin" gem. PLease, install it.' \
36
+ unless defined?(Flex::Admin)
37
+
38
+ yield self if block_given?
39
+
40
+ opts[:verbose] = true unless opts.has_key?(:verbose)
41
+ opts[:models] ||= Conf.flex_active_models
42
+
43
+ # we override the on_reindex eventually set
44
+ on_reindex do
45
+ opts[:models].each do |model|
46
+ model = eval("::#{model}") if model.is_a?(String)
47
+ raise ArgumentError, "The model #{model.name} is not a standard Flex::ActiveModel model" \
48
+ unless model.include?(Flex::ActiveModel)
49
+
50
+ pbar = ProgBar.new(model.count, nil, "Model #{model}: ") if opts[:verbose]
51
+
52
+ model.find_in_batches({:raw_result => true, :params => {:fields => '*,_source'}}, opts) do |result|
53
+ batch = result['hits']['hits']
54
+ result = process_and_post_batch(batch)
55
+ pbar.process_result(result, batch.size) if opts[:verbose]
56
+ end
57
+
58
+ pbar.finish if opts[:verbose]
59
+
60
+ end
61
+ end
62
+
63
+ perform(opts)
64
+ end
65
+
66
+ end
67
+ end
@@ -0,0 +1,27 @@
1
+ module Flex
2
+ module ModelIndexer
3
+
4
+ def self.included(base)
5
+ base.class_eval do
6
+ @flex ||= ClassProxy::Base.new(base)
7
+ @flex.extend(ClassProxy::ModelSyncer)
8
+ @flex.extend(ClassProxy::ModelIndexer).init
9
+ def self.flex; @flex end
10
+ end
11
+ end
12
+
13
+ def flex
14
+ @flex ||= InstanceProxy::ModelIndexer.new(self)
15
+ end
16
+
17
+ def flex_source
18
+ attributes.reject {|k| k.to_s =~ /^_*id$/}
19
+ end
20
+
21
+ def flex_indexable?
22
+ true
23
+ end
24
+
25
+ end
26
+
27
+ end
@@ -0,0 +1,17 @@
1
+ module Flex
2
+ module ModelSyncer
3
+
4
+ def self.included(base)
5
+ base.class_eval do
6
+ @flex ||= ClassProxy::Base.new(base)
7
+ @flex.extend(ClassProxy::ModelSyncer)
8
+ def self.flex; @flex end
9
+ end
10
+ end
11
+
12
+ def flex
13
+ @flex ||= InstanceProxy::ModelSyncer.new(self)
14
+ end
15
+
16
+ end
17
+ end
@@ -0,0 +1,116 @@
1
+ require 'flex/tasks'
2
+
3
+ module Flex
4
+
5
+ class Tasks
6
+ # patches the Flex::Tasks#config_hash so it evaluates also the default mapping for models
7
+ # it modifies also the index:create task
8
+ alias_method :original_config_hash, :config_hash
9
+ def config_hash
10
+ @config_hash ||= begin
11
+ default = {}.extend Struct::Mergeable
12
+ (Conf.flex_models + Conf.flex_active_models).each do |m|
13
+ m = eval"::#{m}" if m.is_a?(String)
14
+ default.deep_merge! m.flex.default_mapping
15
+ end
16
+ default.deep_merge(original_config_hash)
17
+ end
18
+ end
19
+ end
20
+
21
+ class ModelTasks < Flex::Tasks
22
+
23
+ attr_reader :options
24
+
25
+ def initialize(overrides={})
26
+ options = Flex::Utils.env2options *default_options.keys
27
+
28
+ options[:timeout] = options[:timeout].to_i if options[:timeout]
29
+ options[:batch_size] = options[:batch_size].to_i if options[:batch_size]
30
+ options[:models] = options[:models].split(',') if options[:models]
31
+
32
+ if options[:import_options]
33
+ import_options = {}
34
+ options[:import_options].split('&').each do |pair|
35
+ k, v = pair.split('=')
36
+ import_options[k.to_sym] = v
37
+ end
38
+ options[:import_options] = import_options
39
+ end
40
+
41
+ @options = default_options.merge(options).merge(overrides)
42
+ end
43
+
44
+ def default_options
45
+ @default_options ||= { :force => false,
46
+ :timeout => 20,
47
+ :batch_size => 1000,
48
+ :import_options => { },
49
+ :models => Conf.flex_models,
50
+ :config_file => Conf.config_file,
51
+ :verbose => true }
52
+ end
53
+
54
+ def import_models
55
+ Conf.http_client.options[:timeout] = options[:timeout]
56
+ deleted = []
57
+ models.each do |model|
58
+ raise ArgumentError, "The model #{model.name} is not a standard Flex::ModelIndexer model" \
59
+ unless model.include?(Flex::ModelIndexer)
60
+ index = model.flex.index
61
+ index = LiveReindex.prefix_index(index) if LiveReindex.should_prefix_index?
62
+
63
+ # block never called during live-reindex, since it doesn't exist
64
+ if options[:force]
65
+ unless deleted.include?(index)
66
+ delete_index(index)
67
+ deleted << index
68
+ puts "#{index} index deleted" if options[:verbose]
69
+ end
70
+ end
71
+
72
+ # block never called during live-reindex, since prefix_index creates it
73
+ unless exist?(index)
74
+ create(index)
75
+ puts "#{index} index created" if options[:verbose]
76
+ end
77
+
78
+ if defined?(Mongoid::Document) && model.include?(Mongoid::Document)
79
+ def model.find_in_batches(options={})
80
+ 0.step(count, options[:batch_size]) do |offset|
81
+ yield limit(options[:batch_size]).skip(offset).to_a
82
+ end
83
+ end
84
+ end
85
+
86
+ unless model.respond_to?(:find_in_batches)
87
+ Conf.logger.error "Model #{model} does not respond to :find_in_batches. Skipped."
88
+ next
89
+ end
90
+
91
+ pbar = ProgBar.new(model.count, options[:batch_size], "Model #{model}: ") if options[:verbose]
92
+
93
+ model.find_in_batches(:batch_size => options[:batch_size]) do |batch|
94
+ result = Flex.post_bulk_collection(batch, options[:import_options]) || next
95
+ pbar.process_result(result, batch.size) if options[:verbose]
96
+ end
97
+
98
+ pbar.finish if options[:verbose]
99
+ end
100
+ end
101
+
102
+ private
103
+
104
+ def models
105
+ @models ||= begin
106
+ models = options[:models] || Conf.flex_models
107
+ raise ArgumentError, 'no class defined. Please use MODELS=ClassA,ClassB ' +
108
+ 'or set the Flex::Configuration.flex_models properly' \
109
+ if models.nil? || models.empty?
110
+ models.map{|c| c.is_a?(String) ? eval("::#{c}") : c}
111
+ end
112
+ end
113
+
114
+ end
115
+
116
+ end
@@ -0,0 +1,15 @@
1
+ module Flex
2
+ module RefreshCallbacks
3
+
4
+ def self.included(base)
5
+ base.class_eval do
6
+ raise NotImplementedError, "the class #{self} must implement :after_create, :after_update and :after_destroy callbacks" \
7
+ unless respond_to?(:after_save) && respond_to?(:after_destroy)
8
+ refresh = proc{ Flex.refresh_index :index => self.class.flex.index }
9
+ after_save &refresh
10
+ after_destroy &refresh
11
+ end
12
+ end
13
+
14
+ end
15
+ end
@@ -0,0 +1,61 @@
1
+ module Flex
2
+ class Result
3
+ module ActiveModel
4
+
5
+ include Flex::Result::Scope
6
+
7
+ # extend if the context include a Flex::ActiveModel
8
+ def self.should_extend?(result)
9
+ result.variables[:context] && result.variables[:context].include?(Flex::ActiveModel)
10
+ end
11
+
12
+ def get_docs
13
+ # super is from flex-scopes
14
+ docs = super
15
+ return docs if variables[:raw_result]
16
+ raw_result = self
17
+ if docs.is_a?(Array)
18
+ res = docs.map {|doc| build_object(doc)}
19
+ res.extend(Struct::Paginable).setup(raw_result['hits']['total'], variables)
20
+ class << res; self end.class_eval do
21
+ define_method(:raw_result){ raw_result }
22
+ define_method(:method_missing) do |meth, *args, &block|
23
+ raw_result.respond_to?(meth) ? raw_result.send(meth, *args, &block) : super(meth, *args, &block)
24
+ end
25
+ end
26
+ res
27
+ else
28
+ build_object docs
29
+ end
30
+ end
31
+
32
+ private
33
+
34
+ def build_object(doc)
35
+ attrs = (doc['_source']||{}).merge(doc['fields']||{})
36
+ object = variables[:context].new attrs
37
+ raw_result = self
38
+ object.instance_eval do
39
+ class << self; self end.class_eval do
40
+ define_method(:raw_result){ raw_result }
41
+ define_method(:raw_document){ doc }
42
+ define_method(:respond_to?) do |*args|
43
+ doc.respond_to?(*args) || super(*args)
44
+ end
45
+ define_method(:method_missing) do |meth, *args, &block|
46
+ doc.respond_to?(meth) ? doc.send(meth, *args, &block) : super(meth, *args, &block)
47
+ end
48
+ end
49
+ @_id = doc['_id']
50
+ @_version = doc['_version']
51
+ @highlight = doc['highlight']
52
+ # load the flex proxy before freezing
53
+ flex
54
+ self.freeze if raw_result.variables[:params][:fields] || doc['fields']
55
+ end
56
+ object
57
+ end
58
+
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,56 @@
1
+ module Flex
2
+ class Result
3
+
4
+ # adds sugar to documents with the following structure:
5
+ #
6
+ # {
7
+ # "_index" : "twitter",
8
+ # "_type" : "tweet",
9
+ # "_id" : "1",
10
+ # }
11
+
12
+ module DocumentLoader
13
+
14
+ module ModelClasses
15
+ extend self
16
+ # maps all the index/types to the ruby class
17
+ def map
18
+ @map ||= begin
19
+ map = {}
20
+ (Conf.flex_models + Conf.flex_active_models).each do |m|
21
+ m = eval("::#{m}") if m.is_a?(String)
22
+ indices = m.flex.index.is_a?(Array) ? m.flex.index : [m.flex.index]
23
+ types = m.flex.type.is_a?(Array) ? m.flex.type : [m.flex.type]
24
+ indices.each do |i|
25
+ types.each { |t| map["#{i}/#{t}"] = m }
26
+ end
27
+ end
28
+ map
29
+ end
30
+ end
31
+ end
32
+
33
+ # extend if result has a structure like a document
34
+ def self.should_extend?(result)
35
+ result.is_a? Document
36
+ end
37
+
38
+ def model_class
39
+ @model_class ||= ModelClasses.map["#{index_basename}/#{self['_type']}"]
40
+ end
41
+
42
+ def load
43
+ model_class.find(self['_id']) if model_class
44
+ end
45
+
46
+ def load!
47
+ raise DocumentMappingError, "the '#{index_basename}/#{self['_type']}' document cannot be mapped to any class." \
48
+ unless model_class
49
+ model_class.find self['_id']
50
+ end
51
+
52
+ end
53
+
54
+ end
55
+ end
56
+
@@ -0,0 +1,40 @@
1
+ module Flex
2
+ class Result
3
+ module SearchLoader
4
+
5
+ # extend if result is a Search or MultiGet
6
+ def self.should_extend?(result)
7
+ result.is_a?(Search) || result.is_a?(MultiGet)
8
+ end
9
+
10
+ # extend the collection on extend
11
+ def self.extended(result)
12
+ result.collection.each { |h| h.extend(DocumentLoader) }
13
+ end
14
+
15
+ def loaded_collection
16
+ @loaded_collection ||= begin
17
+ records = []
18
+ # returns a structure like {Comment=>[{"_id"=>"123", ...}, {...}], BlogPost=>[...]}
19
+ h = Utils.group_array_by(collection) do |d|
20
+ d.model_class
21
+ end
22
+ h.each do |klass, docs|
23
+ records |= klass.find(docs.map(&:_id))
24
+ end
25
+ class_ids = collection.map { |d| [d.model_class.to_s, d._id] }
26
+ # Reorder records to preserve order from search results
27
+ records = class_ids.map do |class_str, id|
28
+ records.detect do |record|
29
+ record.class.to_s == class_str && record.id.to_s == id.to_s
30
+ end
31
+ end
32
+ records.extend Struct::Paginable
33
+ records.setup(collection.total_entries, variables)
34
+ records
35
+ end
36
+ end
37
+
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,38 @@
1
+ module Flex
2
+ module Struct
3
+ # allows deep merge between Hashes
4
+ module Mergeable
5
+
6
+ def deep_merge(*hashes)
7
+ merged = deep_dup
8
+ hashes.each {|h2| merged.replace(deep_merge_hash(merged,h2))}
9
+ merged
10
+ end
11
+
12
+ def deep_merge!(*hashes)
13
+ replace deep_merge(*hashes)
14
+ end
15
+
16
+ def deep_dup
17
+ Marshal.load(Marshal.dump(self))
18
+ end
19
+
20
+ private
21
+
22
+ def deep_merge_hash(h1, h2)
23
+ h2 ||= {}
24
+ h1.merge(h2) do |key, oldval, newval|
25
+ case
26
+ when oldval.is_a?(::Hash) && newval.is_a?(::Hash)
27
+ deep_merge_hash(oldval, newval)
28
+ when oldval.is_a?(::Array) && newval.is_a?(::Array)
29
+ oldval + newval
30
+ else
31
+ newval
32
+ end
33
+ end
34
+ end
35
+
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,40 @@
1
+ require 'flex'
2
+ require 'flex-scopes'
3
+ require 'active_attr'
4
+
5
+ require 'flex/struct/mergeable'
6
+
7
+ require 'flex/class_proxy/model_syncer'
8
+ require 'flex/instance_proxy/model_syncer'
9
+ require 'flex/model_syncer'
10
+
11
+ require 'flex/class_proxy/model_indexer'
12
+ require 'flex/instance_proxy/model_indexer'
13
+ require 'flex/model_indexer'
14
+
15
+ require 'flex/active_model/timestamps'
16
+ require 'flex/active_model/attachment'
17
+ require 'flex/active_model/inspection'
18
+ require 'flex/active_model/storage'
19
+ require 'flex/class_proxy/active_model'
20
+ require 'flex/instance_proxy/active_model'
21
+ require 'flex/active_model'
22
+
23
+ require 'flex/refresh_callbacks'
24
+
25
+ require 'flex/live_reindex_model'
26
+
27
+ require 'flex/result/document_loader'
28
+ require 'flex/result/search_loader'
29
+ require 'flex/result/active_model'
30
+
31
+ require 'flex/model_tasks'
32
+
33
+ Flex::LIB_PATHS << File.dirname(__FILE__)
34
+
35
+ # get_docs calls super so we make sure the result is extended by Scope first
36
+ Flex::Conf.result_extenders |= [ Flex::Result::DocumentLoader,
37
+ Flex::Result::SearchLoader,
38
+ Flex::Result::ActiveModel ]
39
+ Flex::Conf.flex_models = []
40
+ Flex::Conf.flex_active_models = []
data/lib/tasks.rake ADDED
@@ -0,0 +1,12 @@
1
+ require 'flex-models'
2
+
3
+ env = defined?(Rails) ? :environment : []
4
+
5
+ namespace :flex do
6
+
7
+ desc 'imports from an ActiveRecord or Mongoid models'
8
+ task(:import => env) { Flex::ModelTasks.new.import_models }
9
+
10
+ task(:reset_redis_keys) { Flex::Redis.reset_keys }
11
+
12
+ end
metadata ADDED
@@ -0,0 +1,122 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: flex-models
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Domizio Demichelis
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-08-12 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: flex
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - '='
20
+ - !ruby/object:Gem::Version
21
+ version: 1.0.1
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - '='
28
+ - !ruby/object:Gem::Version
29
+ version: 1.0.1
30
+ - !ruby/object:Gem::Dependency
31
+ name: flex-scopes
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - '='
36
+ - !ruby/object:Gem::Version
37
+ version: 1.0.1
38
+ type: :runtime
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - '='
44
+ - !ruby/object:Gem::Version
45
+ version: 1.0.1
46
+ - !ruby/object:Gem::Dependency
47
+ name: active_attr
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ! '>='
52
+ - !ruby/object:Gem::Version
53
+ version: 0.6.0
54
+ type: :runtime
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ! '>='
60
+ - !ruby/object:Gem::Version
61
+ version: 0.6.0
62
+ description: Provides ActiveRecord, Mongoid, ActiveModel and elasticsearch-mapper-attachment
63
+ integrations, cross syncing, parent/child relationships, bulk-import, live-reindex
64
+ of models, ...
65
+ email: dd.nexus@gmail.com
66
+ executables: []
67
+ extensions: []
68
+ extra_rdoc_files:
69
+ - README.md
70
+ files:
71
+ - LICENSE
72
+ - README.md
73
+ - VERSION
74
+ - flex-models.gemspec
75
+ - lib/flex-models.rb
76
+ - lib/flex/active_model.rb
77
+ - lib/flex/active_model/attachment.rb
78
+ - lib/flex/active_model/inspection.rb
79
+ - lib/flex/active_model/storage.rb
80
+ - lib/flex/active_model/timestamps.rb
81
+ - lib/flex/class_proxy/active_model.rb
82
+ - lib/flex/class_proxy/model_indexer.rb
83
+ - lib/flex/class_proxy/model_syncer.rb
84
+ - lib/flex/instance_proxy/active_model.rb
85
+ - lib/flex/instance_proxy/model_indexer.rb
86
+ - lib/flex/instance_proxy/model_syncer.rb
87
+ - lib/flex/live_reindex_model.rb
88
+ - lib/flex/model_indexer.rb
89
+ - lib/flex/model_syncer.rb
90
+ - lib/flex/model_tasks.rb
91
+ - lib/flex/refresh_callbacks.rb
92
+ - lib/flex/result/active_model.rb
93
+ - lib/flex/result/document_loader.rb
94
+ - lib/flex/result/search_loader.rb
95
+ - lib/flex/struct/mergeable.rb
96
+ - lib/tasks.rake
97
+ homepage: http://github.com/ddnexus/flex-models
98
+ licenses: []
99
+ post_install_message:
100
+ rdoc_options:
101
+ - --charset=UTF-8
102
+ require_paths:
103
+ - lib
104
+ required_ruby_version: !ruby/object:Gem::Requirement
105
+ none: false
106
+ requirements:
107
+ - - ! '>='
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
110
+ required_rubygems_version: !ruby/object:Gem::Requirement
111
+ none: false
112
+ requirements:
113
+ - - ! '>='
114
+ - !ruby/object:Gem::Version
115
+ version: 1.3.6
116
+ requirements: []
117
+ rubyforge_project:
118
+ rubygems_version: 1.8.25
119
+ signing_key:
120
+ specification_version: 3
121
+ summary: Transparently integrates your models with one or more elasticsearch indices.
122
+ test_files: []