elasticsearch-persistence-queryable 0.1.8

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 (99) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +17 -0
  3. data/CHANGELOG.md +16 -0
  4. data/Gemfile +4 -0
  5. data/LICENSE.txt +13 -0
  6. data/README.md +678 -0
  7. data/Rakefile +57 -0
  8. data/elasticsearch-persistence.gemspec +57 -0
  9. data/examples/music/album.rb +34 -0
  10. data/examples/music/artist.rb +50 -0
  11. data/examples/music/artists/_form.html.erb +8 -0
  12. data/examples/music/artists/artists_controller.rb +67 -0
  13. data/examples/music/artists/artists_controller_test.rb +53 -0
  14. data/examples/music/artists/index.html.erb +57 -0
  15. data/examples/music/artists/show.html.erb +51 -0
  16. data/examples/music/assets/application.css +226 -0
  17. data/examples/music/assets/autocomplete.css +48 -0
  18. data/examples/music/assets/blank_cover.png +0 -0
  19. data/examples/music/assets/form.css +113 -0
  20. data/examples/music/index_manager.rb +60 -0
  21. data/examples/music/search/index.html.erb +93 -0
  22. data/examples/music/search/search_controller.rb +41 -0
  23. data/examples/music/search/search_controller_test.rb +9 -0
  24. data/examples/music/search/search_helper.rb +15 -0
  25. data/examples/music/suggester.rb +45 -0
  26. data/examples/music/template.rb +392 -0
  27. data/examples/music/vendor/assets/jquery-ui-1.10.4.custom.min.css +7 -0
  28. data/examples/music/vendor/assets/jquery-ui-1.10.4.custom.min.js +6 -0
  29. data/examples/notes/.gitignore +7 -0
  30. data/examples/notes/Gemfile +28 -0
  31. data/examples/notes/README.markdown +36 -0
  32. data/examples/notes/application.rb +238 -0
  33. data/examples/notes/config.ru +7 -0
  34. data/examples/notes/test.rb +118 -0
  35. data/lib/elasticsearch/per_thread_registry.rb +53 -0
  36. data/lib/elasticsearch/persistence/client.rb +51 -0
  37. data/lib/elasticsearch/persistence/inheritence.rb +9 -0
  38. data/lib/elasticsearch/persistence/model/base.rb +95 -0
  39. data/lib/elasticsearch/persistence/model/callbacks.rb +37 -0
  40. data/lib/elasticsearch/persistence/model/errors.rb +9 -0
  41. data/lib/elasticsearch/persistence/model/find.rb +155 -0
  42. data/lib/elasticsearch/persistence/model/gateway_delegation.rb +23 -0
  43. data/lib/elasticsearch/persistence/model/hash_wrapper.rb +17 -0
  44. data/lib/elasticsearch/persistence/model/rails.rb +39 -0
  45. data/lib/elasticsearch/persistence/model/store.rb +271 -0
  46. data/lib/elasticsearch/persistence/model.rb +148 -0
  47. data/lib/elasticsearch/persistence/null_relation.rb +56 -0
  48. data/lib/elasticsearch/persistence/query_cache.rb +68 -0
  49. data/lib/elasticsearch/persistence/querying.rb +21 -0
  50. data/lib/elasticsearch/persistence/relation/delegation.rb +130 -0
  51. data/lib/elasticsearch/persistence/relation/finder_methods.rb +39 -0
  52. data/lib/elasticsearch/persistence/relation/merger.rb +179 -0
  53. data/lib/elasticsearch/persistence/relation/query_builder.rb +279 -0
  54. data/lib/elasticsearch/persistence/relation/query_methods.rb +362 -0
  55. data/lib/elasticsearch/persistence/relation/search_option_methods.rb +44 -0
  56. data/lib/elasticsearch/persistence/relation/spawn_methods.rb +61 -0
  57. data/lib/elasticsearch/persistence/relation.rb +110 -0
  58. data/lib/elasticsearch/persistence/repository/class.rb +71 -0
  59. data/lib/elasticsearch/persistence/repository/find.rb +73 -0
  60. data/lib/elasticsearch/persistence/repository/naming.rb +115 -0
  61. data/lib/elasticsearch/persistence/repository/response/results.rb +105 -0
  62. data/lib/elasticsearch/persistence/repository/search.rb +156 -0
  63. data/lib/elasticsearch/persistence/repository/serialize.rb +31 -0
  64. data/lib/elasticsearch/persistence/repository/store.rb +94 -0
  65. data/lib/elasticsearch/persistence/repository.rb +77 -0
  66. data/lib/elasticsearch/persistence/scoping/default.rb +137 -0
  67. data/lib/elasticsearch/persistence/scoping/named.rb +70 -0
  68. data/lib/elasticsearch/persistence/scoping.rb +52 -0
  69. data/lib/elasticsearch/persistence/version.rb +5 -0
  70. data/lib/elasticsearch/persistence.rb +157 -0
  71. data/lib/elasticsearch/rails_compatibility.rb +17 -0
  72. data/lib/rails/generators/elasticsearch/model/model_generator.rb +21 -0
  73. data/lib/rails/generators/elasticsearch/model/templates/model.rb.tt +9 -0
  74. data/lib/rails/generators/elasticsearch_generator.rb +2 -0
  75. data/lib/rails/instrumentation/railtie.rb +31 -0
  76. data/lib/rails/instrumentation.rb +10 -0
  77. data/test/integration/model/model_basic_test.rb +157 -0
  78. data/test/integration/repository/custom_class_test.rb +85 -0
  79. data/test/integration/repository/customized_class_test.rb +82 -0
  80. data/test/integration/repository/default_class_test.rb +114 -0
  81. data/test/integration/repository/virtus_model_test.rb +114 -0
  82. data/test/test_helper.rb +53 -0
  83. data/test/unit/model_base_test.rb +48 -0
  84. data/test/unit/model_find_test.rb +148 -0
  85. data/test/unit/model_gateway_test.rb +99 -0
  86. data/test/unit/model_rails_test.rb +88 -0
  87. data/test/unit/model_store_test.rb +514 -0
  88. data/test/unit/persistence_test.rb +32 -0
  89. data/test/unit/repository_class_test.rb +51 -0
  90. data/test/unit/repository_client_test.rb +32 -0
  91. data/test/unit/repository_find_test.rb +388 -0
  92. data/test/unit/repository_indexing_test.rb +37 -0
  93. data/test/unit/repository_module_test.rb +146 -0
  94. data/test/unit/repository_naming_test.rb +146 -0
  95. data/test/unit/repository_response_results_test.rb +98 -0
  96. data/test/unit/repository_search_test.rb +117 -0
  97. data/test/unit/repository_serialize_test.rb +57 -0
  98. data/test/unit/repository_store_test.rb +303 -0
  99. metadata +487 -0
@@ -0,0 +1,61 @@
1
+ require 'active_support/core_ext/hash/except'
2
+ require 'active_support/core_ext/hash/slice'
3
+ require 'elasticsearch/persistence/relation/merger'
4
+
5
+ module Elasticsearch
6
+ module Persistence
7
+
8
+ module SpawnMethods
9
+ def spawn
10
+ clone
11
+ end
12
+
13
+ def merge(other)
14
+ if other.is_a?(Array)
15
+ to_a & other
16
+ elsif other
17
+ spawn.merge!(other)
18
+ else
19
+ self
20
+ end
21
+ end
22
+
23
+ def merge!(other) # :nodoc:
24
+ if !other.is_a?(Relation) && other.respond_to?(:to_proc)
25
+ instance_exec(&other)
26
+ else
27
+ klass = other.is_a?(Hash) ? Relation::HashMerger : Relation::Merger
28
+ klass.new(self, other).merge
29
+ end
30
+ end
31
+
32
+ # Removes from the query the condition(s) specified in +skips+.
33
+ #
34
+ # Post.order('id asc').except(:order) # discards the order condition
35
+ # Post.where('id > 10').order('id asc').except(:where) # discards the where condition but keeps the order
36
+ def except(*skips)
37
+ relation_with values.except(*skips)
38
+ end
39
+
40
+ # Removes any condition from the query other than the one(s) specified in +onlies+.
41
+ #
42
+ # Post.order('id asc').only(:where) # discards the order condition
43
+ # Post.order('id asc').only(:where, :order) # uses the specified order
44
+ def only(*onlies)
45
+ if onlies.any? { |o| o == :where }
46
+ onlies << :bind
47
+ end
48
+ relation_with values.slice(*onlies)
49
+ end
50
+
51
+ private
52
+
53
+ def relation_with(values) # :nodoc:
54
+ result = Relation.create(klass, values)
55
+ result.extend(*extending_values) if extending_values.any?
56
+ result
57
+ end
58
+ end
59
+
60
+ end
61
+ end
@@ -0,0 +1,110 @@
1
+ module Elasticsearch
2
+ module Persistence
3
+ class Relation
4
+
5
+ MULTI_VALUE_METHODS = [:order, :where, :or_filter, :filter, :bind, :extending, :unscope, :skip_callbacks]
6
+ SINGLE_VALUE_METHODS = [:limit, :offset, :routing, :size]
7
+
8
+ INVALID_METHODS_FOR_DELETE_ALL = [:limit, :offset]
9
+
10
+ VALUE_METHODS = MULTI_VALUE_METHODS + SINGLE_VALUE_METHODS
11
+
12
+ include FinderMethods, SpawnMethods, QueryMethods, SearchOptionMethods, Delegation
13
+
14
+ attr_reader :klass, :loaded
15
+ alias :model :klass
16
+ alias :loaded? :loaded
17
+
18
+ delegate :blank?, :empty?, :any?, :many?, to: :results
19
+
20
+ def initialize(klass, values={})
21
+ @klass = klass
22
+ @values = values
23
+ @offsets = {}
24
+ @loaded = false
25
+ end
26
+
27
+ def build(*args)
28
+ @klass.new *args
29
+ end
30
+
31
+ def to_a
32
+ load
33
+ @records
34
+ end
35
+ alias :results :to_a
36
+
37
+ def as_json(options = nil)
38
+ to_a.as_json(options)
39
+ end
40
+
41
+ def to_elastic
42
+ query_builder.to_elastic
43
+ end
44
+
45
+ def create(*args, &block)
46
+ scoping { @klass.create!(*args, &block) }
47
+ end
48
+
49
+ def scoping
50
+ previous, klass.current_scope = klass.current_scope, self
51
+ yield
52
+ ensure
53
+ klass.current_scope = previous
54
+ end
55
+
56
+ def load
57
+ exec_queries unless loaded?
58
+
59
+ self
60
+ end
61
+ alias :fetch :load
62
+
63
+ def delete(opts=nil)
64
+ end
65
+
66
+ def exec_queries
67
+ # Run safety callback
68
+ klass.circuit_breaker_callbacks.each do |cb|
69
+ current_scope_values = self.send("#{cb[:options][:in]}_values")
70
+ next if skip_callbacks_values.include? cb[:name]
71
+ valid = if cb[:callback].nil?
72
+ current_scope_values.collect(&:keys).flatten.include? cb[:name]
73
+ else
74
+ cb[:callback].call(current_scope_values.collect(&:keys).flatten, current_scope_values)
75
+ end
76
+
77
+ raise Elasticsearch::Persistence::Model::QueryOptionMissing, "#{cb[:name]} #{cb[:options][:message]}" unless valid
78
+ end
79
+
80
+ @records = @klass.fetch_results(query_builder)
81
+
82
+ @loaded = true
83
+ @records
84
+ end
85
+
86
+ def values
87
+ Hash[@values]
88
+ end
89
+
90
+ def inspect
91
+ entries = to_a.results.take([size_value.to_i + 1, 11].compact.min).map!(&:inspect)
92
+ message = {}
93
+ message = {total: to_a.total, max: to_a.total}
94
+ message.merge!(aggregations: results.aggregations.keys) unless results.aggregations.nil?
95
+ message = message.each_pair.collect { |k,v| "#{k}: #{v}" }
96
+ message.unshift entries.join(', ') unless entries.size.zero?
97
+ "#<#{self.class.name} #{message.join(', ')}>"
98
+ end
99
+
100
+
101
+
102
+ private
103
+
104
+ def query_builder
105
+ QueryBuilder.new(values)
106
+ end
107
+
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,71 @@
1
+ module Elasticsearch
2
+ module Persistence
3
+ module Repository
4
+
5
+ # The default repository class, to be used either directly, or as a gateway in a custom repository class
6
+ #
7
+ # @example Standalone use
8
+ #
9
+ # repository = Elasticsearch::Persistence::Repository::Class.new
10
+ # # => #<Elasticsearch::Persistence::Repository::Class ...>
11
+ # repository.save(my_object)
12
+ # # => {"_index"=> ... }
13
+ #
14
+ # @example Shortcut use
15
+ #
16
+ # repository = Elasticsearch::Persistence::Repository.new
17
+ # # => #<Elasticsearch::Persistence::Repository::Class ...>
18
+ #
19
+ # @example Configuration via a block
20
+ #
21
+ # repository = Elasticsearch::Persistence::Repository.new do
22
+ # index 'my_notes'
23
+ # end
24
+ #
25
+ # # => #<Elasticsearch::Persistence::Repository::Class ...>
26
+ # # > repository.save(my_object)
27
+ # # => {"_index"=> ... }
28
+ #
29
+ # @example Accessing the gateway in a custom class
30
+ #
31
+ # class MyRepository
32
+ # include Elasticsearch::Persistence::Repository
33
+ # end
34
+ #
35
+ # repository = MyRepository.new
36
+ #
37
+ # repository.gateway.client.info
38
+ # # => {"status"=>200, "name"=>"Venom", ... }
39
+ #
40
+ class Class
41
+ include Elasticsearch::Persistence::Client::ClassMethods
42
+ include Elasticsearch::Persistence::Repository::Naming
43
+ include Elasticsearch::Persistence::Repository::Serialize
44
+ include Elasticsearch::Persistence::Repository::Store
45
+ include Elasticsearch::Persistence::Repository::Find
46
+ include Elasticsearch::Persistence::Repository::Search
47
+
48
+ include Elasticsearch::Model::Indexing::ClassMethods
49
+
50
+ attr_reader :options
51
+
52
+ def initialize(options={}, &block)
53
+ @options = options
54
+ index_name options.delete(:index)
55
+ block.arity < 1 ? instance_eval(&block) : block.call(self) if block_given?
56
+ end
57
+
58
+ # Return the "host" class, if this repository is a gateway hosted in another class
59
+ #
60
+ # @return [nil, Class]
61
+ #
62
+ # @api private
63
+ #
64
+ def host
65
+ options[:host]
66
+ end
67
+ end
68
+
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,73 @@
1
+ module Elasticsearch
2
+ module Persistence
3
+ module Repository
4
+ class DocumentNotFound < StandardError; end
5
+
6
+ # Retrieves one or more domain objects from the repository
7
+ #
8
+ module Find
9
+
10
+ # Retrieve a single object or multiple objects from Elasticsearch by ID or IDs
11
+ #
12
+ # @example Retrieve a single object by ID
13
+ #
14
+ # repository.find(1)
15
+ # # => <Note ...>
16
+ #
17
+ # @example Retrieve multiple objects by IDs
18
+ #
19
+ # repository.find(1, 2)
20
+ # # => [<Note ...>, <Note ...>
21
+ #
22
+ # @return [Object,Array]
23
+ #
24
+ def find(*args)
25
+ options = args.last.is_a?(Hash) ? args.pop : {}
26
+ ids = args
27
+
28
+ if args.size == 1
29
+ id = args.pop
30
+ id.is_a?(Array) ? __find_many(id, options) : __find_one(id, options)
31
+ else
32
+ __find_many args, options
33
+ end
34
+ end
35
+
36
+ # Return if object exists in the repository
37
+ #
38
+ # @example
39
+ #
40
+ # repository.exists?(1)
41
+ # => true
42
+ #
43
+ # @return [true, false]
44
+ #
45
+ def exists?(id, options={})
46
+ type = document_type || (klass ? __get_type_from_class(klass) : '_all')
47
+ client.exists( { index: index_name, id: id }.merge(options) )
48
+ end
49
+
50
+ # @api private
51
+ #
52
+ def __find_one(id, options={})
53
+ type = document_type || (klass ? __get_type_from_class(klass) : '_all')
54
+ document = client.get( { index: index_name, id: id }.merge(options) )
55
+
56
+ deserialize(document)
57
+ rescue Elasticsearch::Transport::Transport::Errors::NotFound => e
58
+ raise DocumentNotFound, e.message, caller
59
+ end
60
+
61
+ # @api private
62
+ #
63
+ def __find_many(ids, options={})
64
+ type = document_type || (klass ? __get_type_from_class(klass) : '_all')
65
+ documents = client.mget( { index: index_name, body: { ids: ids } }.merge(options) )
66
+
67
+ documents['docs'].map { |document| document['found'] ? deserialize(document) : nil }
68
+ end
69
+ end
70
+
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,115 @@
1
+ module Elasticsearch
2
+ module Persistence
3
+ module Repository
4
+
5
+ # Wraps all naming-related features of the repository (index name, the domain object class, etc)
6
+ #
7
+ module Naming
8
+
9
+ # Get or set the class used to initialize domain objects when deserializing them
10
+ #
11
+ def klass name=nil
12
+ @klass = name || @klass
13
+ end
14
+
15
+ # Set the class used to initialize domain objects when deserializing them
16
+ #
17
+ def klass=klass
18
+ @klass = klass
19
+ end
20
+
21
+ # Get or set the index name used when storing and retrieving documents
22
+ #
23
+ def index_name name=nil
24
+ @index_name = name || @index_name || begin
25
+ if respond_to?(:host) && host && host.is_a?(Module)
26
+ self.host.to_s.underscore.gsub(/\//, '-')
27
+ else
28
+ self.class.to_s.underscore.gsub(/\//, '-')
29
+ end
30
+ end
31
+ end; alias :index :index_name
32
+
33
+ # Set the index name used when storing and retrieving documents
34
+ #
35
+ def index_name=(name)
36
+ @index_name = name
37
+ end; alias :index= :index_name=
38
+
39
+ # Get or set the document type used when storing and retrieving documents
40
+ #
41
+ def document_type name=nil
42
+ @document_type = name || @document_type || (klass ? klass.to_s.underscore : nil)
43
+ end; alias :type :document_type
44
+
45
+ # Set the document type used when storing and retrieving documents
46
+ #
47
+ def document_type=(name)
48
+ @document_type = name
49
+ end; alias :type= :document_type=
50
+
51
+ # Get the Ruby class from the Elasticsearch `_type`
52
+ #
53
+ # @example
54
+ # repository.__get_klass_from_type 'note'
55
+ # => Note
56
+ #
57
+ # @return [Class] The class corresponding to the passed type
58
+ # @raise [NameError] if the class cannot be found
59
+ #
60
+ # @api private
61
+ #
62
+ def __get_klass_from_type(type)
63
+ klass = type.classify
64
+ klass.constantize
65
+ rescue NameError => e
66
+ raise NameError, "Attempted to get class '#{klass}' from the '#{type}' type, but no such class can be found."
67
+ end
68
+
69
+ # Get the Elasticsearch `_type` from the Ruby class
70
+ #
71
+ # @example
72
+ # repository.__get_type_from_class Note
73
+ # => "note"
74
+ #
75
+ # @return [String] The type corresponding to the passed class
76
+ #
77
+ # @api private
78
+ #
79
+ def __get_type_from_class(klass)
80
+ klass.to_s.underscore
81
+ end
82
+
83
+ # Get a document ID from the document (assuming Hash or Hash-like object)
84
+ #
85
+ # @example
86
+ # repository.__get_id_from_document title: 'Test', id: 'abc123'
87
+ # => "abc123"
88
+ #
89
+ # @api private
90
+ #
91
+ def __get_id_from_document(document)
92
+ document[:id] || document['id'] || document[:_id] || document['_id']
93
+ end
94
+
95
+ # Extract a document ID from the document (assuming Hash or Hash-like object)
96
+ #
97
+ # @note Calling this method will *remove* the `id` or `_id` key from the passed object.
98
+ #
99
+ # @example
100
+ # options = { title: 'Test', id: 'abc123' }
101
+ # repository.__extract_id_from_document options
102
+ # # => "abc123"
103
+ # options
104
+ # # => { title: 'Test' }
105
+ #
106
+ # @api private
107
+ #
108
+ def __extract_id_from_document(document)
109
+ document.delete(:id) || document.delete('id') || document.delete(:_id) || document.delete('_id')
110
+ end
111
+ end
112
+
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,105 @@
1
+ module Elasticsearch
2
+ module Persistence
3
+ module Repository
4
+ module Response # :nodoc:
5
+
6
+ # Encapsulates the domain objects and documents returned from Elasticsearch when searching
7
+ #
8
+ # Implements `Enumerable` and forwards its methods to the {#results} object.
9
+ #
10
+ class Results
11
+ include Enumerable
12
+
13
+ delegate :aggregations, to: :response
14
+
15
+ attr_reader :repository, :loaded
16
+ alias :loaded? :loaded
17
+
18
+ # @param repository [Elasticsearch::Persistence::Repository::Class] The repository instance
19
+ # @param response [Hash] The full response returned from the Elasticsearch client
20
+ # @param options [Hash] Optional parameters
21
+ #
22
+ def initialize(repository, response, options={})
23
+ @repository = repository
24
+ @response = Elasticsearch::Persistence::Model::HashWrapper.new(response)
25
+ @options = options
26
+ @loaded = false
27
+ end
28
+
29
+ def method_missing(method_name, *arguments, &block)
30
+ results.respond_to?(method_name) ? results.__send__(method_name, *arguments, &block) : super
31
+ end
32
+
33
+ def respond_to?(method_name, include_private = false)
34
+ results.respond_to?(method_name) || super
35
+ end
36
+
37
+ def inner_hits
38
+ response['hits']['hits'].collect { |d| d['inner_hits'] }
39
+ end
40
+
41
+
42
+ # The number of total hits for a query
43
+ #
44
+ def total
45
+ response['hits']['total']
46
+ end
47
+
48
+ # The maximum score for a query
49
+ #
50
+ def max_score
51
+ response['hits']['max_score']
52
+ end
53
+
54
+ # Yields [object, hit] pairs to the block
55
+ #
56
+ def each_with_hit(&block)
57
+ results.zip(response['hits']['hits']).each(&block)
58
+ end
59
+
60
+ # Yields [object, hit] pairs and returns the result
61
+ #
62
+ def map_with_hit(&block)
63
+ results.zip(response['hits']['hits']).map(&block)
64
+ end
65
+
66
+ # Return the collection of domain objects
67
+ #
68
+ # @example Iterate over the results
69
+ #
70
+ # results.map { |r| r.attributes[:title] }
71
+ # => ["Fox", "Dog"]
72
+ #
73
+ # @return [Array]
74
+ #
75
+ def results
76
+ @results ||= response['hits']['hits'].map do |document|
77
+ repository.deserialize(document.to_hash)
78
+ end
79
+ @loaded = true
80
+ @results
81
+ end
82
+
83
+ def delete(opts=nil)
84
+ end
85
+
86
+ # Access the response returned from Elasticsearch by the client
87
+ #
88
+ # @example Access the aggregations in the response
89
+ #
90
+ # results = repository.search query: { match: { title: 'fox dog' } },
91
+ # aggregations: { titles: { terms: { field: 'title' } } }
92
+ # results.response.aggregations.titles.buckets.map { |term| "#{term['key']}: #{term['doc_count']}" }
93
+ # # => ["brown: 1", "dog: 1", ...]
94
+ #
95
+ # @return [Hashie::Mash]
96
+ #
97
+ def response
98
+ @response
99
+ end
100
+
101
+ end
102
+ end
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,156 @@
1
+ module Elasticsearch
2
+ module Persistence
3
+ module Repository
4
+
5
+ # Returns a collection of domain objects by an Elasticsearch query
6
+ #
7
+ module Search
8
+ include Elasticsearch::Persistence::QueryCache
9
+ # Returns a collection of domain objects by an Elasticsearch query
10
+ #
11
+ # Pass the query either as a string or a Hash-like object
12
+ #
13
+ # @example Return objects matching a simple query
14
+ #
15
+ # repository.search('fox or dog')
16
+ #
17
+ # @example Return objects matching a query in the Elasticsearch DSL
18
+ #
19
+ # repository.search(query: { match: { title: 'fox dog' } })
20
+ #
21
+ # @example Define additional search parameters, such as highlighted excerpts
22
+ #
23
+ # results = repository.search(query: { match: { title: 'fox dog' } }, highlight: { fields: { title: {} } })
24
+ # results.map_with_hit { |d,h| h.highlight.title.join }
25
+ # # => ["quick brown <em>fox</em>", "fast white <em>dog</em>"]
26
+ #
27
+ # @example Perform aggregations as part of the request
28
+ #
29
+ # results = repository.search query: { match: { title: 'fox dog' } },
30
+ # aggregations: { titles: { terms: { field: 'title' } } }
31
+ # results.response.aggregations.titles.buckets.map { |term| "#{term['key']}: #{term['doc_count']}" }
32
+ # # => ["brown: 1", "dog: 1", ... ]
33
+ #
34
+ # @example Pass additional options to the search request, such as `size`
35
+ #
36
+ # repository.search query: { match: { title: 'fox dog' } }, size: 25
37
+ # # GET http://localhost:9200/notes/note/_search
38
+ # # > {"query":{"match":{"title":"fox dog"}},"size":25}
39
+ #
40
+ # @return [Elasticsearch::Persistence::Repository::Response::Results]
41
+ #
42
+ def search(query_or_definition, options = {})
43
+ request = { index: index_name, body: query_or_definition.to_hash }
44
+
45
+ case
46
+ when query_or_definition.respond_to?(:to_hash)
47
+ request.merge!(body: query_or_definition.to_hash)
48
+ when query_or_definition.is_a?(String)
49
+ request.merge!(q: query_or_definition)
50
+ else
51
+ raise ArgumentError, "[!] Pass the search definition as a Hash-like object or pass the query as a String" +
52
+ " -- #{query_or_definition.class} given."
53
+ end
54
+
55
+ response = cache_query(to_curl(request.merge(options)), klass) { client.search(request.merge(options)) }
56
+
57
+ Response::Results.new(self, response)
58
+ end
59
+
60
+ # Return the number of domain object in the index
61
+ #
62
+ # @example Return the number of all domain objects
63
+ #
64
+ # repository.count
65
+ # # => 2
66
+ #
67
+ # @example Return the count of domain object matching a simple query
68
+ #
69
+ # repository.count('fox or dog')
70
+ # # => 1
71
+ #
72
+ # @example Return the count of domain object matching a query in the Elasticsearch DSL
73
+ #
74
+ # repository.search(query: { match: { title: 'fox dog' } })
75
+ # # => 1
76
+ #
77
+ # @return [Integer]
78
+ #
79
+ def count(query_or_definition = nil, options = {})
80
+ query_or_definition ||= { query: { match_all: {} } }
81
+
82
+ request = { index: index_name, body: query_or_definition.to_hash }
83
+ response = cache_query(to_curl(request.merge(options), "_count"), klass) { client.count(request.merge(options)) }
84
+
85
+ response
86
+ end
87
+
88
+ private
89
+
90
+ ## TODO: Not happy with where this is living right now.
91
+ #
92
+ def to_curl(arguments = {}, end_point = "_search")
93
+ host = client.transport.options[:hosts]&.first || client.transport.options[:url]
94
+ arguments[:index] = "_all" if !arguments[:index] && arguments[:type]
95
+
96
+ valid_params = [
97
+ :analyzer,
98
+ :analyze_wildcard,
99
+ :default_operator,
100
+ :df,
101
+ :explain,
102
+ :fields,
103
+ :from,
104
+ :ignore_indices,
105
+ :ignore_unavailable,
106
+ :allow_no_indices,
107
+ :expand_wildcards,
108
+ :lenient,
109
+ :lowercase_expanded_terms,
110
+ :preference,
111
+ :q,
112
+ :routing,
113
+ :scroll,
114
+ :search_type,
115
+ :size,
116
+ :sort,
117
+ :source,
118
+ :_source,
119
+ :_source_include,
120
+ :_source_exclude,
121
+ :stats,
122
+ :suggest_field,
123
+ :suggest_mode,
124
+ :suggest_size,
125
+ :suggest_text,
126
+ :timeout,
127
+ :version,
128
+ ]
129
+
130
+ method = "GET"
131
+ path = Elasticsearch::API::Utils.__pathify(Elasticsearch::API::Utils.__listify(arguments[:index]), end_point)
132
+
133
+ params = Elasticsearch::API::Utils.__validate_and_extract_params arguments, valid_params
134
+ body = arguments[:body]
135
+
136
+ params[:fields] = Elasticsearch::API::Utils.__listify(params[:fields]) if params[:fields]
137
+
138
+ url = path
139
+
140
+ unless host.is_a? String
141
+ host_parts = "#{host[:protocol].to_s}://#{host[:host]}"
142
+ host_parts = "#{host_parts}:#{host[:port]}" if host[:port]
143
+ else
144
+ host_parts = host
145
+ end
146
+
147
+ trace_url = "#{host_parts}/#{url}"
148
+ trace_url += "?#{::Faraday::Utils::ParamsHash[params].to_query}" unless params.blank?
149
+ trace_body = body ? " -d '#{body.to_json}'" : ""
150
+
151
+ Rainbow("curl -X #{method.to_s.upcase} '#{CGI.unescape(trace_url)}'#{trace_body}\n").color :white
152
+ end
153
+ end
154
+ end
155
+ end
156
+ end