elasticsearch-model-queryable 0.1.5

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.
Files changed (70) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +20 -0
  3. data/CHANGELOG.md +26 -0
  4. data/Gemfile +4 -0
  5. data/LICENSE.txt +13 -0
  6. data/README.md +695 -0
  7. data/Rakefile +59 -0
  8. data/elasticsearch-model.gemspec +57 -0
  9. data/examples/activerecord_article.rb +77 -0
  10. data/examples/activerecord_associations.rb +162 -0
  11. data/examples/couchbase_article.rb +66 -0
  12. data/examples/datamapper_article.rb +71 -0
  13. data/examples/mongoid_article.rb +68 -0
  14. data/examples/ohm_article.rb +70 -0
  15. data/examples/riak_article.rb +52 -0
  16. data/gemfiles/3.0.gemfile +12 -0
  17. data/gemfiles/4.0.gemfile +11 -0
  18. data/lib/elasticsearch/model/adapter.rb +145 -0
  19. data/lib/elasticsearch/model/adapters/active_record.rb +104 -0
  20. data/lib/elasticsearch/model/adapters/default.rb +50 -0
  21. data/lib/elasticsearch/model/adapters/mongoid.rb +92 -0
  22. data/lib/elasticsearch/model/callbacks.rb +35 -0
  23. data/lib/elasticsearch/model/client.rb +61 -0
  24. data/lib/elasticsearch/model/ext/active_record.rb +14 -0
  25. data/lib/elasticsearch/model/hash_wrapper.rb +15 -0
  26. data/lib/elasticsearch/model/importing.rb +144 -0
  27. data/lib/elasticsearch/model/indexing.rb +472 -0
  28. data/lib/elasticsearch/model/naming.rb +101 -0
  29. data/lib/elasticsearch/model/proxy.rb +127 -0
  30. data/lib/elasticsearch/model/response/base.rb +44 -0
  31. data/lib/elasticsearch/model/response/pagination.rb +173 -0
  32. data/lib/elasticsearch/model/response/records.rb +69 -0
  33. data/lib/elasticsearch/model/response/result.rb +63 -0
  34. data/lib/elasticsearch/model/response/results.rb +31 -0
  35. data/lib/elasticsearch/model/response.rb +71 -0
  36. data/lib/elasticsearch/model/searching.rb +107 -0
  37. data/lib/elasticsearch/model/serializing.rb +35 -0
  38. data/lib/elasticsearch/model/version.rb +5 -0
  39. data/lib/elasticsearch/model.rb +157 -0
  40. data/test/integration/active_record_associations_parent_child.rb +139 -0
  41. data/test/integration/active_record_associations_test.rb +307 -0
  42. data/test/integration/active_record_basic_test.rb +179 -0
  43. data/test/integration/active_record_custom_serialization_test.rb +62 -0
  44. data/test/integration/active_record_import_test.rb +100 -0
  45. data/test/integration/active_record_namespaced_model_test.rb +49 -0
  46. data/test/integration/active_record_pagination_test.rb +132 -0
  47. data/test/integration/mongoid_basic_test.rb +193 -0
  48. data/test/test_helper.rb +63 -0
  49. data/test/unit/adapter_active_record_test.rb +140 -0
  50. data/test/unit/adapter_default_test.rb +41 -0
  51. data/test/unit/adapter_mongoid_test.rb +102 -0
  52. data/test/unit/adapter_test.rb +69 -0
  53. data/test/unit/callbacks_test.rb +31 -0
  54. data/test/unit/client_test.rb +27 -0
  55. data/test/unit/importing_test.rb +176 -0
  56. data/test/unit/indexing_test.rb +478 -0
  57. data/test/unit/module_test.rb +57 -0
  58. data/test/unit/naming_test.rb +76 -0
  59. data/test/unit/proxy_test.rb +89 -0
  60. data/test/unit/response_base_test.rb +40 -0
  61. data/test/unit/response_pagination_kaminari_test.rb +189 -0
  62. data/test/unit/response_pagination_will_paginate_test.rb +208 -0
  63. data/test/unit/response_records_test.rb +91 -0
  64. data/test/unit/response_result_test.rb +90 -0
  65. data/test/unit/response_results_test.rb +31 -0
  66. data/test/unit/response_test.rb +67 -0
  67. data/test/unit/searching_search_request_test.rb +78 -0
  68. data/test/unit/searching_test.rb +41 -0
  69. data/test/unit/serializing_test.rb +17 -0
  70. metadata +466 -0
@@ -0,0 +1,44 @@
1
+ module Elasticsearch
2
+ module Model
3
+ module Response
4
+ # Common funtionality for classes in the {Elasticsearch::Model::Response} module
5
+ #
6
+ module Base
7
+ attr_reader :klass, :response
8
+
9
+ # @param klass [Class] The name of the model class
10
+ # @param response [Hash] The full response returned from Elasticsearch client
11
+ # @param options [Hash] Optional parameters
12
+ #
13
+ def initialize(klass, response, options={})
14
+ @klass = klass
15
+ @response = response
16
+ end
17
+
18
+ # @abstract Implement this method in specific class
19
+ #
20
+ def results
21
+ raise NotImplemented, "Implement this method in #{klass}"
22
+ end
23
+
24
+ # @abstract Implement this method in specific class
25
+ #
26
+ def records
27
+ raise NotImplemented, "Implement this method in #{klass}"
28
+ end
29
+
30
+ # Returns the total number of hits
31
+ #
32
+ def total
33
+ response.response['hits']['total']
34
+ end
35
+
36
+ # Returns the max_score
37
+ #
38
+ def max_score
39
+ response.response['hits']['max_score']
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,173 @@
1
+ module Elasticsearch
2
+ module Model
3
+ module Response
4
+
5
+ # Pagination for search results/records
6
+ #
7
+ module Pagination
8
+ # Allow models to be paginated with the "kaminari" gem [https://github.com/amatsuda/kaminari]
9
+ #
10
+ module Kaminari
11
+ def self.included(base)
12
+ # Include the Kaminari configuration and paging method in response
13
+ #
14
+ base.__send__ :include, ::Kaminari::ConfigurationMethods::ClassMethods
15
+ base.__send__ :include, ::Kaminari::PageScopeMethods
16
+
17
+ # Include the Kaminari paging methods in results and records
18
+ #
19
+ Elasticsearch::Model::Response::Results.__send__ :include, ::Kaminari::ConfigurationMethods::ClassMethods
20
+ Elasticsearch::Model::Response::Results.__send__ :include, ::Kaminari::PageScopeMethods
21
+ Elasticsearch::Model::Response::Records.__send__ :include, ::Kaminari::PageScopeMethods
22
+
23
+ Elasticsearch::Model::Response::Results.__send__ :delegate, :limit_value, :offset_value, :total_count, to: :response
24
+ Elasticsearch::Model::Response::Records.__send__ :delegate, :limit_value, :offset_value, :total_count, to: :response
25
+
26
+ base.class_eval <<-RUBY, __FILE__, __LINE__ + 1
27
+ # Define the `page` Kaminari method
28
+ #
29
+ def #{::Kaminari.config.page_method_name}(num=nil)
30
+ @results = nil
31
+ @records = nil
32
+ @response = nil
33
+ @page = [num.to_i, 1].max
34
+ @per_page ||= klass.default_per_page
35
+
36
+ self.search.definition.update size: @per_page,
37
+ from: @per_page * (@page - 1)
38
+
39
+ self
40
+ end
41
+ RUBY
42
+ end
43
+
44
+ # Returns the current "limit" (`size`) value
45
+ #
46
+ def limit_value
47
+ case
48
+ when search.definition[:size]
49
+ search.definition[:size]
50
+ else
51
+ search.klass.default_per_page
52
+ end
53
+ end
54
+
55
+ # Returns the current "offset" (`from`) value
56
+ #
57
+ def offset_value
58
+ case
59
+ when search.definition[:from]
60
+ search.definition[:from]
61
+ else
62
+ 0
63
+ end
64
+ end
65
+
66
+ # Set the "limit" (`size`) value
67
+ #
68
+ def limit(value)
69
+ @results = nil
70
+ @records = nil
71
+ @response = nil
72
+ @per_page = value
73
+
74
+ search.definition.update :size => @per_page
75
+ search.definition.update :from => @per_page * (@page - 1) if @page
76
+ self
77
+ end
78
+
79
+ # Set the "offset" (`from`) value
80
+ #
81
+ def offset(value)
82
+ @results = nil
83
+ @records = nil
84
+ @response = nil
85
+ @page = nil
86
+ search.definition.update :from => value
87
+ self
88
+ end
89
+
90
+ # Returns the total number of results
91
+ #
92
+ def total_count
93
+ results.total
94
+ end
95
+ end
96
+
97
+ # Allow models to be paginated with the "will_paginate" gem [https://github.com/mislav/will_paginate]
98
+ #
99
+ module WillPaginate
100
+ def self.included(base)
101
+ base.__send__ :include, ::WillPaginate::CollectionMethods
102
+
103
+ # Include the paging methods in results and records
104
+ #
105
+ methods = [:current_page, :offset, :length, :per_page, :total_entries, :total_pages, :previous_page, :next_page, :out_of_bounds?]
106
+ Elasticsearch::Model::Response::Results.__send__ :delegate, *methods, to: :response
107
+ Elasticsearch::Model::Response::Records.__send__ :delegate, *methods, to: :response
108
+ end
109
+
110
+ def offset
111
+ (current_page - 1) * per_page
112
+ end
113
+
114
+ def length
115
+ search.definition[:size]
116
+ end
117
+
118
+ # Main pagination method
119
+ #
120
+ # @example
121
+ #
122
+ # Article.search('foo').paginate(page: 1, per_page: 30)
123
+ #
124
+ def paginate(options)
125
+ page = [options[:page].to_i, 1].max
126
+ per_page = (options[:per_page] || klass.per_page).to_i
127
+
128
+ search.definition.update size: per_page,
129
+ from: (page - 1) * per_page
130
+ self
131
+ end
132
+
133
+ # Return the current page
134
+ #
135
+ def current_page
136
+ search.definition[:from] / per_page + 1 if search.definition[:from] && per_page
137
+ end
138
+
139
+ # Pagination method
140
+ #
141
+ # @example
142
+ #
143
+ # Article.search('foo').page(2)
144
+ #
145
+ def page(num)
146
+ paginate(page: num, per_page: per_page) # shorthand
147
+ end
148
+
149
+ # Return or set the "size" value
150
+ #
151
+ # @example
152
+ #
153
+ # Article.search('foo').per_page(15).page(2)
154
+ #
155
+ def per_page(num = nil)
156
+ if num.nil?
157
+ search.definition[:size]
158
+ else
159
+ paginate(page: current_page, per_page: num) # shorthand
160
+ end
161
+ end
162
+
163
+ # Returns the total number of results
164
+ #
165
+ def total_entries
166
+ results.total
167
+ end
168
+ end
169
+ end
170
+
171
+ end
172
+ end
173
+ end
@@ -0,0 +1,69 @@
1
+ module Elasticsearch
2
+ module Model
3
+ module Response
4
+
5
+ # Encapsulates the collection of records returned from the database
6
+ #
7
+ # Implements Enumerable and forwards its methods to the {#records} object,
8
+ # which is provided by an {Elasticsearch::Model::Adapter::Adapter} implementation.
9
+ #
10
+ class Records
11
+ include Enumerable
12
+
13
+ delegate :each, :empty?, :size, :slice, :[], :to_a, :to_ary, to: :records
14
+
15
+ include Base
16
+
17
+ # @see Base#initialize
18
+ #
19
+ def initialize(klass, response, options = {})
20
+ super
21
+
22
+ # Include module provided by the adapter in the singleton class ("metaclass")
23
+ #
24
+ adapter = Adapter.from_class(klass)
25
+ metaclass = class << self; self; end
26
+ metaclass.__send__ :include, adapter.records_mixin
27
+
28
+ self
29
+ end
30
+
31
+ # Returns the hit IDs
32
+ #
33
+ def ids
34
+ response.response["hits"]["hits"].map { |hit| hit["_id"] }
35
+ end
36
+
37
+ # Returns the {Results} collection
38
+ #
39
+ def results
40
+ response.results
41
+ end
42
+
43
+ # Yields [record, hit] pairs to the block
44
+ #
45
+ def each_with_hit(&block)
46
+ records.to_a.zip(results).each(&block)
47
+ end
48
+
49
+ # Yields [record, hit] pairs and returns the result
50
+ #
51
+ def map_with_hit(&block)
52
+ records.to_a.zip(results).map(&block)
53
+ end
54
+
55
+ # Delegate methods to `@records`
56
+ #
57
+ def method_missing(method_name, *arguments)
58
+ records.respond_to?(method_name) ? records.__send__(method_name, *arguments) : super
59
+ end
60
+
61
+ # Respond to methods from `@records`
62
+ #
63
+ def respond_to?(method_name, include_private = false)
64
+ records.respond_to?(method_name) || super
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,63 @@
1
+ module Elasticsearch
2
+ module Model
3
+ module Response
4
+
5
+ # Encapsulates the "hit" returned from the Elasticsearch client
6
+ #
7
+ # Wraps the raw Hash with in a `Hashie::Mash` instance, providing
8
+ # access to the Hash properties by calling Ruby methods.
9
+ #
10
+ # @see https://github.com/intridea/hashie
11
+ #
12
+ class Result
13
+
14
+ # @param attributes [Hash] A Hash with document properties
15
+ #
16
+ def initialize(attributes={})
17
+ @result = HashWrapper.new(attributes)
18
+ end
19
+
20
+ # Return document `_id` as `id`
21
+ #
22
+ def id
23
+ @result['_id']
24
+ end
25
+
26
+ # Return document `_type` as `_type`
27
+ #
28
+ def type
29
+ @result['_type']
30
+ end
31
+
32
+ # Delegate methods to `@result` or `@result._source`
33
+ #
34
+ def method_missing(name, *arguments)
35
+ case
36
+ when name.to_s.end_with?('?')
37
+ @result.__send__(name, *arguments) || ( @result._source && @result._source.__send__(name, *arguments) )
38
+ when @result.respond_to?(name)
39
+ @result.__send__ name, *arguments
40
+ when @result._source && @result._source.respond_to?(name)
41
+ @result._source.__send__ name, *arguments
42
+ else
43
+ super
44
+ end
45
+ end
46
+
47
+ # Respond to methods from `@result` or `@result._source`
48
+ #
49
+ def respond_to?(method_name, include_private = false)
50
+ @result.respond_to?(method_name.to_sym) || \
51
+ @result._source && @result._source.respond_to?(method_name.to_sym) || \
52
+ super
53
+ end
54
+
55
+ def as_json(options={})
56
+ @result.as_json(options)
57
+ end
58
+
59
+ # TODO: #to_s, #inspect, with support for Pry
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,31 @@
1
+ module Elasticsearch
2
+ module Model
3
+ module Response
4
+
5
+ # Encapsulates the collection of documents returned from Elasticsearch
6
+ #
7
+ # Implements Enumerable and forwards its methods to the {#results} object.
8
+ #
9
+ class Results
10
+ include Base
11
+ include Enumerable
12
+
13
+ delegate :each, :empty?, :size, :slice, :[], :to_a, :to_ary, to: :results
14
+
15
+ # @see Base#initialize
16
+ #
17
+ def initialize(klass, response, options={})
18
+ super
19
+ end
20
+
21
+ # Returns the {Results} collection
22
+ #
23
+ def results
24
+ # TODO: Configurable custom wrapper
25
+ @results = response.response['hits']['hits'].map { |hit| Result.new(hit) }
26
+ end
27
+
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,71 @@
1
+ module Elasticsearch
2
+ module Model
3
+
4
+ # Contains modules and classes for wrapping the response from Elasticsearch
5
+ #
6
+ module Response
7
+
8
+ # Encapsulate the response returned from the Elasticsearch client
9
+ #
10
+ # Implements Enumerable and forwards its methods to the {#results} object.
11
+ #
12
+ class Response
13
+ attr_reader :klass, :search, :response,
14
+ :took, :timed_out, :shards
15
+
16
+ include Enumerable
17
+
18
+ delegate :each, :empty?, :size, :slice, :[], :to_ary, to: :results
19
+
20
+ def initialize(klass, search, options={})
21
+ @klass = klass
22
+ @search = search
23
+ end
24
+
25
+ # Returns the Elasticsearch response
26
+ #
27
+ # @return [Hash]
28
+ #
29
+ def response
30
+ @response ||= begin
31
+ HashWrapper.new(search.execute!)
32
+ end
33
+ end
34
+
35
+ # Returns the collection of "hits" from Elasticsearch
36
+ #
37
+ # @return [Results]
38
+ #
39
+ def results
40
+ @results ||= Results.new(klass, self)
41
+ end
42
+
43
+ # Returns the collection of records from the database
44
+ #
45
+ # @return [Records]
46
+ #
47
+ def records
48
+ @records ||= Records.new(klass, self)
49
+ end
50
+
51
+ # Returns the "took" time
52
+ #
53
+ def took
54
+ response['took']
55
+ end
56
+
57
+ # Returns whether the response timed out
58
+ #
59
+ def timed_out
60
+ response['timed_out']
61
+ end
62
+
63
+ # Returns the statistics on shards
64
+ #
65
+ def shards
66
+ HashWrapper.new(response['_shards'])
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,107 @@
1
+ module Elasticsearch
2
+ module Model
3
+
4
+ # Contains functionality related to searching.
5
+ #
6
+ module Searching
7
+
8
+ # Wraps a search request definition
9
+ #
10
+ class SearchRequest
11
+ attr_reader :klass, :definition
12
+
13
+ # @param klass [Class] The class of the model
14
+ # @param query_or_payload [String,Hash,Object] The search request definition
15
+ # (string, JSON, Hash, or object responding to `to_hash`)
16
+ # @param options [Hash] Optional parameters to be passed to the Elasticsearch client
17
+ #
18
+ def initialize(klass, query_or_payload, options={})
19
+ @klass = klass
20
+
21
+ __index_name = options[:index] || klass.index_name
22
+ __document_type = options[:type] || klass.document_type
23
+
24
+ case
25
+ # search query: ...
26
+ when query_or_payload.respond_to?(:to_hash)
27
+ body = query_or_payload.to_hash
28
+
29
+ # search '{ "query" : ... }'
30
+ when query_or_payload.is_a?(String) && query_or_payload =~ /^\s*{/
31
+ body = query_or_payload
32
+
33
+ # search '...'
34
+ else
35
+ q = query_or_payload
36
+ end
37
+
38
+ if body
39
+ @definition = { index: __index_name, type: __document_type, body: body }.update options
40
+ else
41
+ @definition = { index: __index_name, type: __document_type, q: q }.update options
42
+ end
43
+ end
44
+
45
+ # Performs the request and returns the response from client
46
+ #
47
+ # @return [Hash] The response from Elasticsearch
48
+ #
49
+ def execute!
50
+ klass.client.search(@definition)
51
+ end
52
+ end
53
+
54
+ module ClassMethods
55
+
56
+ # Provides a `search` method for the model to easily search within an index/type
57
+ # corresponding to the model settings.
58
+ #
59
+ # @param query_or_payload [String,Hash,Object] The search request definition
60
+ # (string, JSON, Hash, or object responding to `to_hash`)
61
+ # @param options [Hash] Optional parameters to be passed to the Elasticsearch client
62
+ #
63
+ # @return [Elasticsearch::Model::Response::Response]
64
+ #
65
+ # @example Simple search in `Article`
66
+ #
67
+ # Article.search 'foo'
68
+ #
69
+ # @example Search using a search definition as a Hash
70
+ #
71
+ # response = Article.search \
72
+ # query: {
73
+ # match: {
74
+ # title: 'foo'
75
+ # }
76
+ # },
77
+ # highlight: {
78
+ # fields: {
79
+ # title: {}
80
+ # }
81
+ # }
82
+ #
83
+ # response.results.first.title
84
+ # # => "Foo"
85
+ #
86
+ # response.results.first.highlight.title
87
+ # # => ["<em>Foo</em>"]
88
+ #
89
+ # response.records.first.title
90
+ # # Article Load (0.2ms) SELECT "articles".* FROM "articles" WHERE "articles"."id" IN (1, 3)
91
+ # # => "Foo"
92
+ #
93
+ # @example Search using a search definition as a JSON string
94
+ #
95
+ # Article.search '{"query" : { "match_all" : {} }}'
96
+ #
97
+ def search(query_or_payload, options={})
98
+ search = SearchRequest.new(self, query_or_payload, options)
99
+
100
+ Response::Response.new(self, search)
101
+ end
102
+
103
+ end
104
+
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,35 @@
1
+ module Elasticsearch
2
+ module Model
3
+
4
+ # Contains functionality for serializing model instances for the client
5
+ #
6
+ module Serializing
7
+
8
+ module ClassMethods
9
+ end
10
+
11
+ module InstanceMethods
12
+
13
+ # Serialize the record as a Hash, to be passed to the client.
14
+ #
15
+ # Re-define this method to customize the serialization.
16
+ #
17
+ # @return [Hash]
18
+ #
19
+ # @example Return the model instance as a Hash
20
+ #
21
+ # Article.first.__elasticsearch__.as_indexed_json
22
+ # => {"title"=>"Foo"}
23
+ #
24
+ # @see Elasticsearch::Model::Indexing
25
+ #
26
+ def as_indexed_json(options={})
27
+ # TODO: Play with the `MyModel.indexes` method -- reject non-mapped attributes, `:as` options, etc
28
+ self.as_json(options.merge root: false)
29
+ end
30
+
31
+ end
32
+
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,5 @@
1
+ module Elasticsearch
2
+ module Model
3
+ VERSION = "0.1.5"
4
+ end
5
+ end