elasticsearch-persistence-queryable 0.1.8

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