elasticsearch-persistence 5.1.0 → 6.0.0.pre

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 (58) hide show
  1. checksums.yaml +4 -4
  2. data/.rspec +2 -0
  3. data/Gemfile +9 -0
  4. data/README.md +164 -323
  5. data/Rakefile +8 -8
  6. data/elasticsearch-persistence.gemspec +4 -5
  7. data/lib/elasticsearch/persistence.rb +2 -110
  8. data/lib/elasticsearch/persistence/repository.rb +212 -53
  9. data/lib/elasticsearch/persistence/repository/dsl.rb +94 -0
  10. data/lib/elasticsearch/persistence/repository/find.rb +27 -10
  11. data/lib/elasticsearch/persistence/repository/response/results.rb +17 -5
  12. data/lib/elasticsearch/persistence/repository/search.rb +15 -4
  13. data/lib/elasticsearch/persistence/repository/serialize.rb +65 -7
  14. data/lib/elasticsearch/persistence/repository/store.rb +38 -44
  15. data/lib/elasticsearch/persistence/version.rb +1 -1
  16. data/spec/repository/find_spec.rb +179 -0
  17. data/spec/repository/response/results_spec.rb +105 -0
  18. data/spec/repository/search_spec.rb +181 -0
  19. data/spec/repository/serialize_spec.rb +53 -0
  20. data/spec/repository/store_spec.rb +327 -0
  21. data/spec/repository_spec.rb +716 -0
  22. data/spec/spec_helper.rb +28 -0
  23. metadata +25 -80
  24. data/lib/elasticsearch/persistence/client.rb +0 -51
  25. data/lib/elasticsearch/persistence/model.rb +0 -153
  26. data/lib/elasticsearch/persistence/model/base.rb +0 -87
  27. data/lib/elasticsearch/persistence/model/errors.rb +0 -8
  28. data/lib/elasticsearch/persistence/model/find.rb +0 -180
  29. data/lib/elasticsearch/persistence/model/rails.rb +0 -47
  30. data/lib/elasticsearch/persistence/model/store.rb +0 -254
  31. data/lib/elasticsearch/persistence/model/utils.rb +0 -0
  32. data/lib/elasticsearch/persistence/repository/class.rb +0 -71
  33. data/lib/elasticsearch/persistence/repository/naming.rb +0 -115
  34. data/lib/rails/generators/elasticsearch/model/model_generator.rb +0 -21
  35. data/lib/rails/generators/elasticsearch/model/templates/model.rb.tt +0 -9
  36. data/lib/rails/generators/elasticsearch_generator.rb +0 -2
  37. data/test/integration/model/model_basic_test.rb +0 -238
  38. data/test/integration/repository/custom_class_test.rb +0 -85
  39. data/test/integration/repository/customized_class_test.rb +0 -82
  40. data/test/integration/repository/default_class_test.rb +0 -116
  41. data/test/integration/repository/virtus_model_test.rb +0 -118
  42. data/test/test_helper.rb +0 -55
  43. data/test/unit/model_base_test.rb +0 -72
  44. data/test/unit/model_find_test.rb +0 -153
  45. data/test/unit/model_gateway_test.rb +0 -101
  46. data/test/unit/model_rails_test.rb +0 -112
  47. data/test/unit/model_store_test.rb +0 -576
  48. data/test/unit/persistence_test.rb +0 -32
  49. data/test/unit/repository_class_test.rb +0 -51
  50. data/test/unit/repository_client_test.rb +0 -32
  51. data/test/unit/repository_find_test.rb +0 -388
  52. data/test/unit/repository_indexing_test.rb +0 -37
  53. data/test/unit/repository_module_test.rb +0 -146
  54. data/test/unit/repository_naming_test.rb +0 -146
  55. data/test/unit/repository_response_results_test.rb +0 -98
  56. data/test/unit/repository_search_test.rb +0 -117
  57. data/test/unit/repository_serialize_test.rb +0 -57
  58. data/test/unit/repository_store_test.rb +0 -303
@@ -0,0 +1,94 @@
1
+ module Elasticsearch
2
+ module Persistence
3
+ module Repository
4
+
5
+ # Include this module to get class-level methods for repository configuration.
6
+ #
7
+ # @since 6.0.0
8
+ module DSL
9
+
10
+ def self.included(base)
11
+ base.send(:extend, Elasticsearch::Model::Indexing::ClassMethods)
12
+ base.send(:extend, ClassMethods)
13
+ end
14
+
15
+ # These methods are necessary to define at the class-level so that the methods available
16
+ # via Elasticsearch::Model::Indexing::ClassMethods have the references they depend on.
17
+ #
18
+ # @since 6.0.0
19
+ module ClassMethods
20
+
21
+ # Get or set the class-level document type setting.
22
+ #
23
+ # @example
24
+ # MyRepository.document_type
25
+ #
26
+ # @return [ String, Symbol ] _type The repository's document type.
27
+ #
28
+ # @since 6.0.0
29
+ def document_type(_type = nil)
30
+ @document_type ||= (_type || DEFAULT_DOC_TYPE)
31
+ end
32
+
33
+ # Get or set the class-level index name setting.
34
+ #
35
+ # @example
36
+ # MyRepository.index_name
37
+ #
38
+ # @return [ String, Symbol ] _name The repository's index name.
39
+ #
40
+ # @since 6.0.0
41
+ def index_name(_name = nil)
42
+ @index_name ||= (_name || DEFAULT_INDEX_NAME)
43
+ end
44
+
45
+ # Get or set the class-level setting for the class used by the repository when deserializing.
46
+ #
47
+ # @example
48
+ # MyRepository.klass
49
+ #
50
+ # @return [ Class ] _class The repository's klass for deserializing.
51
+ #
52
+ # @since 6.0.0
53
+ def klass(_class = nil)
54
+ instance_variables.include?(:@klass) ? @klass : @klass = _class
55
+ end
56
+
57
+ # Get or set the class-level setting for the client used by the repository.
58
+ #
59
+ # @example
60
+ # MyRepository.client
61
+ #
62
+ # @return [ Class ] _client The repository's client.
63
+ #
64
+ # @since 6.0.0
65
+ def client(_client = nil)
66
+ @client ||= (_client || Elasticsearch::Client.new)
67
+ end
68
+
69
+ def create_index!(*args)
70
+ __raise_not_implemented_error(__method__)
71
+ end
72
+
73
+ def delete_index!(*args)
74
+ __raise_not_implemented_error(__method__)
75
+ end
76
+
77
+ def refresh_index!(*args)
78
+ __raise_not_implemented_error(__method__)
79
+ end
80
+
81
+ def index_exists?(*args)
82
+ __raise_not_implemented_error(__method__)
83
+ end
84
+
85
+ private
86
+
87
+ def __raise_not_implemented_error(_method_)
88
+ raise NotImplementedError, "The '#{_method_}' method is not implemented on the Repository class."
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
@@ -40,19 +40,35 @@ module Elasticsearch
40
40
  # repository.exists?(1)
41
41
  # => true
42
42
  #
43
+ # @param [ String, Integer ] id The id to search.
44
+ # @param [ Hash ] options The options.
45
+ #
43
46
  # @return [true, false]
44
47
  #
45
48
  def exists?(id, options={})
46
- type = document_type || (klass ? __get_type_from_class(klass) : '_all')
47
- client.exists( { index: index_name, type: type, id: id }.merge(options) )
49
+ request = { index: index_name, id: id }
50
+ request[:type] = document_type if document_type
51
+ client.exists(request.merge(options))
48
52
  end
49
53
 
54
+ private
55
+
56
+ # The key for accessing the document found and returned from an
57
+ # Elasticsearch _mget query.
58
+ #
59
+ DOCS = 'docs'.freeze
60
+
61
+ # The key for the boolean value indicating whether a particular id
62
+ # has been successfully found in an Elasticsearch _mget query.
63
+ #
64
+ FOUND = 'found'.freeze
65
+
50
66
  # @api private
51
67
  #
52
68
  def __find_one(id, options={})
53
- type = document_type || (klass ? __get_type_from_class(klass) : '_all')
54
- document = client.get( { index: index_name, type: type, id: id }.merge(options) )
55
-
69
+ request = { index: index_name, id: id }
70
+ request[:type] = document_type if document_type
71
+ document = client.get(request.merge(options))
56
72
  deserialize(document)
57
73
  rescue Elasticsearch::Transport::Transport::Errors::NotFound => e
58
74
  raise DocumentNotFound, e.message, caller
@@ -61,13 +77,14 @@ module Elasticsearch
61
77
  # @api private
62
78
  #
63
79
  def __find_many(ids, options={})
64
- type = document_type || (klass ? __get_type_from_class(klass) : '_all')
65
- documents = client.mget( { index: index_name, type: type, body: { ids: ids } }.merge(options) )
66
-
67
- documents['docs'].map { |document| document['found'] ? deserialize(document) : nil }
80
+ request = { index: index_name, body: { ids: ids } }
81
+ request[:type] = document_type if document_type
82
+ documents = client.mget(request.merge(options))
83
+ documents[DOCS].map do |document|
84
+ deserialize(document) if document[FOUND]
85
+ end
68
86
  end
69
87
  end
70
-
71
88
  end
72
89
  end
73
90
  end
@@ -12,6 +12,18 @@ module Elasticsearch
12
12
 
13
13
  attr_reader :repository
14
14
 
15
+ # The key for accessing the results in an Elasticsearch query response.
16
+ #
17
+ HITS = 'hits'.freeze
18
+
19
+ # The key for accessing the total number of hits in an Elasticsearch query response.
20
+ #
21
+ TOTAL = 'total'.freeze
22
+
23
+ # The key for accessing the maximum score in an Elasticsearch query response.
24
+ #
25
+ MAX_SCORE = 'max_score'.freeze
26
+
15
27
  # @param repository [Elasticsearch::Persistence::Repository::Class] The repository instance
16
28
  # @param response [Hash] The full response returned from the Elasticsearch client
17
29
  # @param options [Hash] Optional parameters
@@ -33,25 +45,25 @@ module Elasticsearch
33
45
  # The number of total hits for a query
34
46
  #
35
47
  def total
36
- response['hits']['total']
48
+ response[HITS][TOTAL]
37
49
  end
38
50
 
39
51
  # The maximum score for a query
40
52
  #
41
53
  def max_score
42
- response['hits']['max_score']
54
+ response[HITS][MAX_SCORE]
43
55
  end
44
56
 
45
57
  # Yields [object, hit] pairs to the block
46
58
  #
47
59
  def each_with_hit(&block)
48
- results.zip(response['hits']['hits']).each(&block)
60
+ results.zip(response[HITS][HITS]).each(&block)
49
61
  end
50
62
 
51
63
  # Yields [object, hit] pairs and returns the result
52
64
  #
53
65
  def map_with_hit(&block)
54
- results.zip(response['hits']['hits']).map(&block)
66
+ results.zip(response[HITS][HITS]).map(&block)
55
67
  end
56
68
 
57
69
  # Return the collection of domain objects
@@ -64,7 +76,7 @@ module Elasticsearch
64
76
  # @return [Array]
65
77
  #
66
78
  def results
67
- @results ||= response['hits']['hits'].map do |document|
79
+ @results ||= response[HITS][HITS].map do |document|
68
80
  repository.deserialize(document.to_hash)
69
81
  end
70
82
  end
@@ -37,10 +37,13 @@ module Elasticsearch
37
37
  # # GET http://localhost:9200/notes/note/_search
38
38
  # # > {"query":{"match":{"title":"fox dog"}},"size":25}
39
39
  #
40
+ # @param [ Hash, String ] query_or_definition The query or search definition.
41
+ # @param [ Hash ] options The search options.
42
+ #
40
43
  # @return [Elasticsearch::Persistence::Repository::Response::Results]
41
44
  #
42
45
  def search(query_or_definition, options={})
43
- type = document_type || (klass ? __get_type_from_class(klass) : nil )
46
+ type = document_type
44
47
 
45
48
  case
46
49
  when query_or_definition.respond_to?(:to_hash)
@@ -71,11 +74,14 @@ module Elasticsearch
71
74
  # repository.search(query: { match: { title: 'fox dog' } })
72
75
  # # => 1
73
76
  #
77
+ # @param [ Hash, String ] query_or_definition The query or search definition.
78
+ # @param [ Hash ] options The search options.
79
+ #
74
80
  # @return [Integer]
75
81
  #
76
82
  def count(query_or_definition=nil, options={})
77
83
  query_or_definition ||= { query: { match_all: {} } }
78
- type = document_type || (klass ? __get_type_from_class(klass) : nil )
84
+ type = document_type
79
85
 
80
86
  case
81
87
  when query_or_definition.respond_to?(:to_hash)
@@ -86,10 +92,15 @@ module Elasticsearch
86
92
  raise ArgumentError, "[!] Pass the search definition as a Hash-like object or pass the query as a String, not as [#{query_or_definition.class}]"
87
93
  end
88
94
 
89
- response['count']
95
+ response[COUNT]
90
96
  end
91
- end
92
97
 
98
+ private
99
+
100
+ # The key for accessing the count in a Elasticsearch query response.
101
+ #
102
+ COUNT = 'count'.freeze
103
+ end
93
104
  end
94
105
  end
95
106
  end
@@ -2,30 +2,88 @@ module Elasticsearch
2
2
  module Persistence
3
3
  module Repository
4
4
 
5
- # Provide serialization and deserialization between Ruby objects and Elasticsearch documents
5
+ # Provide serialization and deserialization between Ruby objects and Elasticsearch documents.
6
6
  #
7
7
  # Override these methods in your repository class to customize the logic.
8
8
  #
9
9
  module Serialize
10
10
 
11
- # Serialize the object for storing it in Elasticsearch
11
+ # Serialize the object for storing it in Elasticsearch.
12
12
  #
13
13
  # In the default implementation, call the `to_hash` method on the passed object.
14
14
  #
15
+ # @param [ Object ] document The Ruby object to serialize.
16
+ #
17
+ # @return [ Hash ] The serialized document.
18
+ #
15
19
  def serialize(document)
16
20
  document.to_hash
17
21
  end
18
22
 
19
- # Deserialize the document retrieved from Elasticsearch into a Ruby object
23
+ # Deserialize the document retrieved from Elasticsearch into a Ruby object.
24
+ # If no klass is set for the Repository then the raw document '_source' field will be returned.
25
+ #
26
+ # def deserialize(document)
27
+ # Note.new document[SOURCE]
28
+ # end
20
29
  #
21
- # Use the `klass` property, if defined, otherwise try to get the class from the document's `_type`.
30
+ # @param [ Hash ] document The raw document.
31
+ #
32
+ # @return [ Object ] The deserialized object.
22
33
  #
23
34
  def deserialize(document)
24
- _klass = klass || __get_klass_from_type(document['_type'])
25
- _klass.new document['_source']
35
+ klass ? klass.new(document[SOURCE]) : document[SOURCE]
26
36
  end
27
- end
28
37
 
38
+ private
39
+
40
+ # The key for document fields in an Elasticsearch query response.
41
+ #
42
+ SOURCE = '_source'.freeze
43
+
44
+ # The key for the document type in an Elasticsearch query response.
45
+ # Note that it will be removed eventually, as multiple types in a single
46
+ # index are deprecated as of Elasticsearch 6.0.
47
+ #
48
+ TYPE = '_type'.freeze
49
+
50
+ IDS = [:id, 'id', :_id, '_id'].freeze
51
+
52
+ # Get a document ID from the document (assuming Hash or Hash-like object)
53
+ #
54
+ # @example
55
+ # repository.__get_id_from_document title: 'Test', id: 'abc123'
56
+ # => "abc123"
57
+ #
58
+ # @api private
59
+ #
60
+ def __get_id_from_document(document)
61
+ document[IDS.find { |id| document[id] }]
62
+ end
63
+
64
+ # Extract a document ID from the document (assuming Hash or Hash-like object)
65
+ #
66
+ # @note Calling this method will *remove* the `id` or `_id` key from the passed object.
67
+ #
68
+ # @example
69
+ # options = { title: 'Test', id: 'abc123' }
70
+ # repository.__extract_id_from_document options
71
+ # # => "abc123"
72
+ # options
73
+ # # => { title: 'Test' }
74
+ #
75
+ # @api private
76
+ #
77
+ def __extract_id_from_document(document)
78
+ IDS.inject(nil) do |deleted, id|
79
+ if document[id]
80
+ document.delete(id)
81
+ else
82
+ deleted
83
+ end
84
+ end
85
+ end
86
+ end
29
87
  end
30
88
  end
31
89
  end
@@ -12,13 +12,19 @@ module Elasticsearch
12
12
  # repository.save(myobject)
13
13
  # => {"_index"=>"...", "_type"=>"...", "_id"=>"...", "_version"=>1, "created"=>true}
14
14
  #
15
- # @return {Hash} The response from Elasticsearch
15
+ # @param [ Object ] document The document to save into Elasticsearch.
16
+ # @param [ Hash ] options The save request options.
17
+ #
18
+ # @return [ Hash ] The response from Elasticsearch
16
19
  #
17
20
  def save(document, options={})
18
21
  serialized = serialize(document)
19
- id = __get_id_from_document(serialized)
20
- type = document_type || __get_type_from_class(klass || document.class)
21
- client.index( { index: index_name, type: type, id: id, body: serialized }.merge(options) )
22
+ id = __get_id_from_document(serialized)
23
+ request = { index: index_name,
24
+ id: id,
25
+ body: serialized }
26
+ request[:type] = document_type if document_type
27
+ client.index(request.merge(options))
22
28
  end
23
29
 
24
30
  # Update the serialized object in Elasticsearch with partial data or script
@@ -33,39 +39,27 @@ module Elasticsearch
33
39
  # repository.update 1, script: 'ctx._source.views += 1'
34
40
  # # => {"_index"=>"...", "_type"=>"...", "_id"=>"1", "_version"=>3}
35
41
  #
36
- # @return {Hash} The response from Elasticsearch
42
+ # @param [ Object ] document_or_id The document to update or the id of the document to update.
43
+ # @param [ Hash ] options The update request options.
37
44
  #
38
- def update(document, options={})
39
- case
40
- when document.is_a?(String) || document.is_a?(Integer)
41
- id = document
42
- when document.respond_to?(:to_hash)
43
- serialized = document.to_hash
44
- id = __extract_id_from_document(serialized)
45
- else
46
- raise ArgumentError, "Expected a document ID or a Hash-like object, #{document.class} given"
47
- end
48
-
49
- type = options.delete(:type) || \
50
- (defined?(serialized) && serialized && serialized.delete(:type)) || \
51
- document_type || \
52
- __get_type_from_class(klass)
53
-
54
- if defined?(serialized) && serialized
55
- body = if serialized[:script]
56
- serialized.select { |k, v| [:script, :params, :upsert].include? k }
57
- else
58
- { doc: serialized }
59
- end
45
+ # @return [ Hash ] The response from Elasticsearch
46
+ #
47
+ def update(document_or_id, options = {})
48
+ if document_or_id.is_a?(String) || document_or_id.is_a?(Integer)
49
+ id = document_or_id
50
+ body = options
51
+ type = document_type
60
52
  else
61
- body = {}
62
- body.update( doc: options.delete(:doc)) if options[:doc]
63
- body.update( script: options.delete(:script)) if options[:script]
64
- body.update( params: options.delete(:params)) if options[:params]
65
- body.update( upsert: options.delete(:upsert)) if options[:upsert]
53
+ document = serialize(document_or_id)
54
+ id = __extract_id_from_document(document)
55
+ if options[:script]
56
+ body = options
57
+ else
58
+ body = { doc: document }.merge(options)
59
+ end
60
+ type = document.delete(:type) || document_type
66
61
  end
67
-
68
- client.update( { index: index_name, type: type, id: id, body: body }.merge(options) )
62
+ client.update(index: index_name, id: id, type: type, body: body)
69
63
  end
70
64
 
71
65
  # Remove the serialized object or document with specified ID from Elasticsearch
@@ -75,21 +69,21 @@ module Elasticsearch
75
69
  # repository.delete(1)
76
70
  # # => {"_index"=>"...", "_type"=>"...", "_id"=>"1", "_version"=>4}
77
71
  #
78
- # @return {Hash} The response from Elasticsearch
72
+ # @param [ Object ] document_or_id The document to delete or the id of the document to delete.
73
+ # @param [ Hash ] options The delete request options.
79
74
  #
80
- def delete(document, options={})
81
- if document.is_a?(String) || document.is_a?(Integer)
82
- id = document
83
- type = document_type || __get_type_from_class(klass)
75
+ # @return [ Hash ] The response from Elasticsearch
76
+ #
77
+ def delete(document_or_id, options = {})
78
+ if document_or_id.is_a?(String) || document_or_id.is_a?(Integer)
79
+ id = document_or_id
84
80
  else
85
- serialized = serialize(document)
86
- id = __get_id_from_document(serialized)
87
- type = document_type || __get_type_from_class(klass || document.class)
81
+ serialized = serialize(document_or_id)
82
+ id = __get_id_from_document(serialized)
88
83
  end
89
- client.delete( { index: index_name, type: type, id: id }.merge(options) )
84
+ client.delete({ index: index_name, type: document_type, id: id }.merge(options))
90
85
  end
91
86
  end
92
-
93
87
  end
94
88
  end
95
89
  end