elasticsearch-persistence 5.1.0 → 6.0.0.pre

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