flex-scopes 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
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,25 @@
1
+ # flex-scopes
2
+
3
+ Provides an easy to use ruby API to search elasticsearch with ActiveRecord-like chainable and mergeables scopes.
4
+
5
+
6
+ ## Links
7
+
8
+ * [Flex Repository](https://github.com/ddnexus/flex)
9
+ * [Flex Project (Global Documentation)](http://ddnexus.github.io/flex/doc/)
10
+ * [flex-scopes Gem (Specific Documentation)](http://ddnexus.github.io/flex/doc/3-flex-scopes)
11
+ * [Issues](https://github.com/ddnexus/flex-scopes/issues)
12
+ * [Pull Requests](https://github.com/ddnexus/flex-scopes/pulls)
13
+
14
+ ## Branches
15
+
16
+ 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.
17
+
18
+ ## Credits
19
+
20
+ Special thanks for their sponsorship to [Escalate Media](http://www.escalatemedia.com) and [Barquin International](http://www.barquin.com).
21
+
22
+ ## Copyright
23
+
24
+ Copyright (c) 2012-2013 by [Domizio Demichelis](mailto://dd.nexus@gmail.com)<br>
25
+ See [LICENSE](https://github.com/ddnexus/flex-scopes/blob/master/LICENSE) for details.
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 1.0.1
@@ -0,0 +1,19 @@
1
+ require 'date'
2
+ version = File.read(File.expand_path('../VERSION', __FILE__)).strip
3
+
4
+ Gem::Specification.new do |s|
5
+ s.name = 'flex-scopes'
6
+ s.summary = 'ActiveRecord-like chainable scopes and finders for Flex.'
7
+ s.description = 'Provides an easy to use ruby API to search elasticsearch with ActiveRecord-like chainable and mergeables scopes.'
8
+ s.homepage = 'http://github.com/ddnexus/flex-scopes'
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
+ end
@@ -0,0 +1,12 @@
1
+ require 'flex'
2
+ require 'flex/scope/utils'
3
+ require 'flex/scope/filter_methods'
4
+ require 'flex/scope/vars_methods'
5
+ require 'flex/scope/query_methods'
6
+ require 'flex/scope'
7
+ require 'flex/scopes'
8
+ require 'flex/result/scope'
9
+
10
+ Flex::LIB_PATHS << File.dirname(__FILE__)
11
+
12
+ Flex::Conf.result_extenders |= [Flex::Result::Scope]
@@ -0,0 +1,12 @@
1
+ module Flex
2
+ class Result
3
+ module Scope
4
+
5
+ def get_docs
6
+ return self if variables[:raw_result]
7
+ respond_to?(:collection) ? collection : self
8
+ end
9
+
10
+ end
11
+ end
12
+ end
data/lib/flex/scope.rb ADDED
@@ -0,0 +1,42 @@
1
+ module Flex
2
+ # never instantiate this class directly: it is automatically done by the scoped method
3
+ class Scope < Vars
4
+
5
+ class Error < StandardError; end
6
+
7
+ include FilterMethods
8
+ include VarsMethods
9
+ include QueryMethods
10
+
11
+ SCOPED_METHODS = FilterMethods.instance_methods + VarsMethods.instance_methods + QueryMethods.instance_methods
12
+
13
+ def inspect
14
+ "#<#{self.class.name} #{self}>"
15
+ end
16
+
17
+ def respond_to?(meth, private=false)
18
+ super || is_template?(meth) || is_scope?(meth)
19
+ end
20
+
21
+ def method_missing(meth, *args, &block)
22
+ super unless respond_to?(meth)
23
+ case
24
+ when is_scope?(meth)
25
+ deep_merge self[:context].send(meth, *args, &block)
26
+ when is_template?(meth)
27
+ self[:context].send(meth, deep_merge(*args), &block)
28
+ end
29
+ end
30
+
31
+ private
32
+
33
+ def is_template?(name)
34
+ self[:context].respond_to?(:template_methods) && self[:context].template_methods.include?(name.to_sym)
35
+ end
36
+
37
+ def is_scope?(name)
38
+ self[:context].scope_methods.include?(name.to_sym)
39
+ end
40
+
41
+ end
42
+ end
@@ -0,0 +1,81 @@
1
+ module Flex
2
+ class Scope
3
+ module FilterMethods
4
+
5
+ include Scope::Utils
6
+
7
+ # accepts also :any_term => nil for missing values
8
+ def terms(value)
9
+ terms, missing_list = {}, []
10
+ value.each { |f, v| v.nil? ? missing_list.push({ :missing => f }) : (terms[f] = v) }
11
+ terms, term = terms.partition{|k,v| v.is_a?(Array)}
12
+ term_list = []
13
+ term.each do |term, value|
14
+ term_list.push(:term => {term => value})
15
+ end
16
+ deep_merge boolean_wrapper( :terms_list => Hash[terms],
17
+ :term_list => term_list,
18
+ :_missing_list => missing_list )
19
+ end
20
+
21
+ # accepts one or an array or a list of filter structures
22
+ def filters(*value)
23
+ deep_merge boolean_wrapper( :filters => array_value(value) )
24
+ end
25
+
26
+ def missing(*fields)
27
+ missing_list = []
28
+ for field in fields
29
+ missing_list.push(:missing => field)
30
+ end
31
+ deep_merge :_missing_list => missing_list
32
+ end
33
+
34
+ # accepts a single key hash or a multiple keys hash, that will be translated in a array of single key hashes
35
+ def term(term_or_terms_hash)
36
+ term_list = []
37
+ term_or_terms_hash.each do |term, value|
38
+ term_list.push(:term => {term => value})
39
+ end
40
+ deep_merge boolean_wrapper(:term_list => term_list)
41
+ end
42
+
43
+ # accepts one hash of ranges documented at
44
+ # http://www.elasticsearch.org/guide/reference/query-dsl/range-filter/
45
+ def range(value)
46
+ deep_merge boolean_wrapper(:range => value)
47
+ end
48
+
49
+
50
+ %w[and or].each do |m|
51
+ class_eval <<-ruby, __FILE__, __LINE__
52
+ def #{m}(&block)
53
+ vars = {:_#{m} => Hash[Flex::Scope.new.instance_eval(&block).to_a]}
54
+ vars.merge!(:_boolean_wrapper => :_#{m}) if context_scope?
55
+ deep_merge vars
56
+ end
57
+ ruby
58
+ end
59
+
60
+ private
61
+
62
+ def context_scope?
63
+ has_key?(:context)
64
+ end
65
+
66
+ def boolean_wrapper(value)
67
+ if context_scope?
68
+ if has_key?(:_boolean_wrapper) && self[:_boolean_wrapper] != :_and
69
+ current_wrapper = {self[:_boolean_wrapper] => delete(self[:_boolean_wrapper])}
70
+ self.and{ current_wrapper }.and{ value }
71
+ else
72
+ self.and{value}
73
+ end
74
+ else
75
+ value
76
+ end
77
+ end
78
+
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,52 @@
1
+ ANCHORS:
2
+ - &filters
3
+ - <<filters= ~ >>
4
+ - <<term_list= ~ >>
5
+ - terms: <<terms_list= ~ >>
6
+ - <<_missing_list= ~ >>
7
+ - <<_and= ~ >>
8
+ - <<_or= ~ >>
9
+ - range: <<range= ~ >>
10
+
11
+ - &scope
12
+ query:
13
+ query_string:
14
+ "<<cleanable_query= {query: '*'} >>"
15
+ script_fields:
16
+ <<script_fields= ~ >>
17
+ sort: <<sort= ~ >>
18
+ facets: <<facets= ~ >>
19
+ highlight: <<highlight= ~ >>
20
+ filter: <<_boolean_wrapper= :_and >>
21
+
22
+ _and:
23
+ and: *filters
24
+
25
+ _or:
26
+ or: *filters
27
+
28
+ _missing_fields:
29
+ missing:
30
+ field: <<missing>>
31
+
32
+ get:
33
+ - GET
34
+ - /<<index>>/<<type>>/_search
35
+ - *scope
36
+
37
+
38
+
39
+ delete:
40
+ - DELETE
41
+ - /<<index>>/<<type>>/_query
42
+ - filtered:
43
+ <<: *scope
44
+
45
+
46
+
47
+ ids:
48
+ - GET
49
+ - /<<index>>/<<type>>/_search
50
+ - query:
51
+ terms:
52
+ _id: <<ids>>
@@ -0,0 +1,89 @@
1
+ module Flex
2
+ class Scope
3
+
4
+ module Query
5
+
6
+ include Templates
7
+ flex.load_source File.expand_path('../queries.yml', __FILE__)
8
+
9
+ end
10
+
11
+ module QueryMethods
12
+
13
+ # MyModel.find(ids, *vars)
14
+ # - ids can be a single id or an array of ids
15
+ #
16
+ # MyModel.find '1Momf4s0QViv-yc7wjaDCA'
17
+ # #=> #<MyModel ... color: "red", size: "small">
18
+ #
19
+ # MyModel.find ['1Momf4s0QViv-yc7wjaDCA', 'BFdIETdNQv-CuCxG_y2r8g']
20
+ # #=> [#<MyModel ... color: "red", size: "small">, #<MyModel ... color: "bue", size: "small">]
21
+ #
22
+ def find(ids, *vars)
23
+ raise ArgumentError, "Empty argument passed (got #{ids.inspect})" \
24
+ if ids.nil? || ids.respond_to?(:empty?) && ids.empty?
25
+ wrapped = ids.is_a?(::Array) ? ids : [ids]
26
+ result = Query.ids self, *vars, :ids => wrapped
27
+ docs = result.get_docs
28
+ ids.is_a?(::Array) ? docs : docs.first
29
+ end
30
+
31
+ # it limits the size of the query to the first document and returns it as a single document object
32
+ def first(*vars)
33
+ result = Query.get params(:size => 1), *vars
34
+ docs = result.get_docs
35
+ docs.is_a?(Array) ? docs.first : docs
36
+ end
37
+
38
+ # it limits the size of the query to the last document and returns it as a single document object
39
+ def last(*vars)
40
+ result = Query.get params(:from => count-1, :size => 1), *vars
41
+ docs = result.get_docs
42
+ docs.is_a?(Array) ? docs.first : docs
43
+ end
44
+
45
+ # will retrieve all documents, the results will be limited by the default :size param
46
+ # use #scan_all if you want to really retrieve all documents (in batches)
47
+ def all(*vars)
48
+ result = Query.get self, *vars
49
+ result.get_docs
50
+ end
51
+
52
+ def each(*vars, &block)
53
+ all(*vars).each &block
54
+ end
55
+
56
+ # scan_search: the block will be yielded many times with an array of batched results.
57
+ # You can pass :scroll and :size as params in order to control the action.
58
+ # See http://www.elasticsearch.org/guide/reference/api/search/scroll.html
59
+ def scan_all(*vars, &block)
60
+ Query.flex.scan_search(:get, self, *vars) do |result|
61
+ block.call result.get_docs
62
+ end
63
+ end
64
+ alias_method :each_batch, :scan_all
65
+ alias_method :find_in_batches, :scan_all
66
+
67
+ def delete(*vars)
68
+ Query.delete self, *vars
69
+ end
70
+
71
+ # performs a count search on the scope
72
+ # you can pass a template name as the first arg and
73
+ # it will be used to compute the count. For example:
74
+ # SearchClass.scoped.count(:search_template, vars)
75
+ #
76
+ def count(*vars)
77
+ result = if vars.first.is_a?(Symbol)
78
+ template = vars.shift
79
+ # preserves an eventual wrapper by calling the template method
80
+ self[:context].send(template, params(:search_type => 'count'), *vars)
81
+ else
82
+ Query.flex.count_search(:get, self, *vars)
83
+ end
84
+ result['hits']['total']
85
+ end
86
+
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,13 @@
1
+ module Flex
2
+ class Scope < Vars
3
+ module Utils
4
+
5
+ private
6
+
7
+ def array_value(value)
8
+ (value.first.is_a?(::Array) && value.size == 1) ? value.first : value
9
+ end
10
+
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,81 @@
1
+ module Flex
2
+ class Scope
3
+ module VarsMethods
4
+
5
+ include Scope::Utils
6
+
7
+ def query_string(q)
8
+ hash = q.is_a?(Hash) ? q : {:query => q}
9
+ deep_merge :cleanable_query => hash
10
+ end
11
+ alias_method :query, :query_string
12
+
13
+ # accepts one or an array or a list of sort structures documented at
14
+ # http://www.elasticsearch.org/guide/reference/api/search/sort.html
15
+ # doesn't probably support the multiple hash form, but you can pass an hash as single argument
16
+ # or an array or list of hashes
17
+ def sort(*value)
18
+ deep_merge :sort => array_value(value)
19
+ end
20
+
21
+ # the fields that you want to retrieve (limiting the size of the response)
22
+ # the returned records will be frozen (for Flex::ActiveModel objects), and the missing fields will be nil
23
+ # pass an array eg fields.([:field_one, :field_two]) or a list of fields e.g. fields(:field_one, :field_two)
24
+ def fields(*value)
25
+ deep_merge :params => {:fields => array_value(value)}
26
+ end
27
+
28
+ # limits the size of the retrieved hits
29
+ def size(value)
30
+ deep_merge :params => {:size => value}
31
+ end
32
+
33
+ # sets the :from param so it will return the nth page of size :size
34
+ def page(value)
35
+ deep_merge :page => value || 1
36
+ end
37
+
38
+ # the standard :params variable
39
+ def params(value)
40
+ deep_merge :params => value
41
+ end
42
+
43
+ # meaningful alias of deep_merge
44
+ def variables(*variables)
45
+ deep_merge *variables
46
+ end
47
+
48
+ def index(val)
49
+ deep_merge :index => val
50
+ end
51
+
52
+ def type(val)
53
+ deep_merge :type => val
54
+ end
55
+
56
+ # script_fields(:my_field => 'script ...', # simpler form
57
+ # :my_other_field => {:script => 'script ...', ...}) # ES API
58
+ def script_fields(hash)
59
+ hash.keys.each do |k|
60
+ v = hash[k]
61
+ hash[k] = {:script => v} unless v.is_a?(Hash)
62
+ hash[k][:script].gsub!(/\n+\s*/,' ')
63
+ end
64
+ deep_merge :script_fields => hash
65
+ end
66
+
67
+ def facets(hash)
68
+ deep_merge :facets => hash
69
+ end
70
+
71
+ def highlight(hash)
72
+ deep_merge :highlight => hash
73
+ end
74
+
75
+ def metrics
76
+ deep_merge :params => {:search_type => 'count'}
77
+ end
78
+
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,89 @@
1
+ module Flex
2
+ module Scopes
3
+
4
+ def self.included(context)
5
+ context.class_eval do
6
+ @flex ||= ClassProxy::Base.new(context)
7
+ def self.flex; @flex end
8
+
9
+ extend ClassMethods
10
+
11
+ @scope_methods = []
12
+ def self.scope_methods; @scope_methods end
13
+ end
14
+ end
15
+
16
+ module ClassMethods
17
+
18
+ # Scope methods. They returns a Scope object similar to AR.
19
+ # You can chain scopes, then you can call :count, :first, :all and :scan_all to get your result
20
+ # See Flex::Scope
21
+ #
22
+ # scoped = MyModel.terms(:field_one => 'something', :field_two => nil)
23
+ # .sort(:field_three => :desc)
24
+ # .filters(:range => {:created_at => {:from => 2.days.ago, :to => Time.now})
25
+ # .fields('field_one,field_two,field_three') # or [:field_one, :field_two, ...]
26
+ # .params(:any => 'param')
27
+ #
28
+ # # add another filter or other terms at any time
29
+ # scoped2 = scoped.terms(...).filters(...)
30
+ #
31
+ # scoped2.count
32
+ # scoped2.first
33
+ # scoped2.all
34
+ # scoped2.scan_all {|batch| do_something_with_results batch}
35
+ #
36
+ Utils.define_delegation :to => :scoped,
37
+ :in => self,
38
+ :by => :module_eval,
39
+ :for => Scope::SCOPED_METHODS
40
+
41
+
42
+ # You can start with a non restricted Flex::Scope object
43
+ def scoped
44
+ @scoped ||= Scope[:context => flex.context]
45
+ end
46
+
47
+
48
+ # define scopes as class methods
49
+ #
50
+ # class MyModel
51
+ # include Flex::StoredModel
52
+ # ...
53
+ # scope :red, terms(:color => 'red').sort(:supplier => :asc)
54
+ # scope :size do |size|
55
+ # terms(:size => size)
56
+ # end
57
+ #
58
+ # MyModel.size('large').first
59
+ # MyModel.red.all
60
+ # MyModel.size('small').red.all
61
+ #
62
+ def scope(name, scope=nil, &block)
63
+ raise ArgumentError, "Dangerous scope name: a :#{name} method is already defined. Please, use another one." \
64
+ if respond_to?(name)
65
+ proc = case
66
+ when block_given?
67
+ block
68
+ when scope.is_a?(Flex::Scope)
69
+ lambda {scope}
70
+ when scope.is_a?(Proc)
71
+ scope
72
+ else
73
+ raise ArgumentError, "Scope object or Proc expected (got #{scope.inspect})"
74
+ end
75
+ metaclass = class << self; self end
76
+ metaclass.send(:define_method, name) do |*args|
77
+ scope = proc.call(*args)
78
+ raise Scope::Error, "The scope :#{name} does not return a Flex::Scope object (got #{scope.inspect})" \
79
+ unless scope.is_a?(Flex::Scope)
80
+ scope
81
+ end
82
+ scope_methods << name
83
+ end
84
+
85
+ end
86
+
87
+ end
88
+ end
89
+
metadata ADDED
@@ -0,0 +1,76 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: flex-scopes
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
+ description: Provides an easy to use ruby API to search elasticsearch with ActiveRecord-like
31
+ chainable and mergeables scopes.
32
+ email: dd.nexus@gmail.com
33
+ executables: []
34
+ extensions: []
35
+ extra_rdoc_files:
36
+ - README.md
37
+ files:
38
+ - LICENSE
39
+ - README.md
40
+ - VERSION
41
+ - flex-scopes.gemspec
42
+ - lib/flex-scopes.rb
43
+ - lib/flex/result/scope.rb
44
+ - lib/flex/scope.rb
45
+ - lib/flex/scope/filter_methods.rb
46
+ - lib/flex/scope/queries.yml
47
+ - lib/flex/scope/query_methods.rb
48
+ - lib/flex/scope/utils.rb
49
+ - lib/flex/scope/vars_methods.rb
50
+ - lib/flex/scopes.rb
51
+ homepage: http://github.com/ddnexus/flex-scopes
52
+ licenses: []
53
+ post_install_message:
54
+ rdoc_options:
55
+ - --charset=UTF-8
56
+ require_paths:
57
+ - lib
58
+ required_ruby_version: !ruby/object:Gem::Requirement
59
+ none: false
60
+ requirements:
61
+ - - ! '>='
62
+ - !ruby/object:Gem::Version
63
+ version: '0'
64
+ required_rubygems_version: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ! '>='
68
+ - !ruby/object:Gem::Version
69
+ version: 1.3.6
70
+ requirements: []
71
+ rubyforge_project:
72
+ rubygems_version: 1.8.25
73
+ signing_key:
74
+ specification_version: 3
75
+ summary: ActiveRecord-like chainable scopes and finders for Flex.
76
+ test_files: []