tire 0.4.0 → 0.4.1

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.
data/lib/tire.rb CHANGED
@@ -6,6 +6,7 @@ require 'cgi'
6
6
 
7
7
  require 'active_support/core_ext/object/to_param'
8
8
  require 'active_support/core_ext/object/to_query'
9
+ require 'active_support/core_ext/hash/except.rb'
9
10
 
10
11
  # Ruby 1.8 compatibility
11
12
  require 'tire/rubyext/ruby_1_8' if defined?(RUBY_VERSION) && RUBY_VERSION < '1.9'
@@ -23,6 +24,7 @@ require 'tire/search/sort'
23
24
  require 'tire/search/facet'
24
25
  require 'tire/search/filter'
25
26
  require 'tire/search/highlight'
27
+ require 'tire/search/scan'
26
28
  require 'tire/results/pagination'
27
29
  require 'tire/results/collection'
28
30
  require 'tire/results/item'
data/lib/tire/dsl.rb CHANGED
@@ -31,5 +31,19 @@ module Tire
31
31
  Index.new(name, &block)
32
32
  end
33
33
 
34
+ def scan(names, options={}, &block)
35
+ Search::Scan.new(names, options, &block)
36
+ end
37
+
38
+ def aliases
39
+ @response = Configuration.client.get "#{Configuration.url}/_aliases"
40
+ MultiJson.decode(@response.body).inject({}) do |acc, (index, value)|
41
+ next acc if value['aliases'].empty?
42
+
43
+ acc[index] = value['aliases'].keys
44
+ acc
45
+ end
46
+ end
47
+
34
48
  end
35
49
  end
data/lib/tire/index.rb CHANGED
@@ -8,39 +8,81 @@ module Tire
8
8
  instance_eval(&block) if block_given?
9
9
  end
10
10
 
11
+ def url
12
+ "#{Configuration.url}/#{@name}"
13
+ end
14
+
11
15
  def exists?
12
- @response = Configuration.client.head("#{Configuration.url}/#{@name}")
16
+ @response = Configuration.client.head("#{url}")
13
17
  @response.success?
14
18
 
15
19
  ensure
16
- curl = %Q|curl -I "#{Configuration.url}/#{@name}"|
20
+ curl = %Q|curl -I "#{url}"|
17
21
  logged('HEAD', curl)
18
22
  end
19
23
 
20
24
  def delete
21
- @response = Configuration.client.delete "#{Configuration.url}/#{@name}"
25
+ @response = Configuration.client.delete url
22
26
  @response.success?
23
27
 
24
28
  ensure
25
- curl = %Q|curl -X DELETE "#{Configuration.url}/#{@name}"|
29
+ curl = %Q|curl -X DELETE #{url}|
26
30
  logged('DELETE', curl)
27
31
  end
28
32
 
29
33
  def create(options={})
30
34
  @options = options
31
- @response = Configuration.client.post "#{Configuration.url}/#{@name}", MultiJson.encode(options)
35
+ @response = Configuration.client.post url, MultiJson.encode(options)
32
36
  @response.success? ? @response : false
33
37
 
34
38
  ensure
35
- curl = %Q|curl -X POST "#{Configuration.url}/#{@name}" -d '#{MultiJson.encode(options)}'|
39
+ curl = %Q|curl -X POST #{url} -d '#{MultiJson.encode(options)}'|
36
40
  logged('CREATE', curl)
37
41
  end
38
42
 
43
+ def add_alias(alias_name, configuration={})
44
+ payload = {'actions' => [ {'add' => {'index' => @name, 'alias' => alias_name}.merge(configuration) } ]}
45
+ @response = Configuration.client.post "#{Configuration.url}/_aliases", MultiJson.encode(payload)
46
+ @response.success?
47
+
48
+ ensure
49
+ curl = %Q|curl -X POST "#{Configuration.url}/_aliases" -d '#{MultiJson.encode(payload)}'|
50
+ logged('POST', curl)
51
+ end
52
+
53
+ def remove_alias(alias_name)
54
+ payload = {'actions' => [{'remove' => {'index' => @name, 'alias' => alias_name}}]}
55
+ @response = Configuration.client.post "#{Configuration.url}/_aliases", MultiJson.encode(payload)
56
+ @response.success?
57
+
58
+ ensure
59
+ curl = %Q|curl -X POST "#{Configuration.url}/_aliases" -d '#{MultiJson.encode(payload)}'|
60
+ logged('POST', curl)
61
+ end
62
+
63
+ def aliases(alias_name = nil)
64
+ @response = Configuration.client.get "#{url}/_aliases"
65
+ if alias_name
66
+ MultiJson.decode(@response.body)[@name]['aliases'][alias_name]
67
+ else
68
+ MultiJson.decode(@response.body)[@name]['aliases'].keys
69
+ end
70
+
71
+ ensure
72
+ curl = %Q|curl "#{url}/_aliases?pretty"|
73
+ logged('GET', curl)
74
+ end
75
+
39
76
  def mapping
40
- @response = Configuration.client.get("#{Configuration.url}/#{@name}/_mapping")
77
+ @response = Configuration.client.get("#{url}/_mapping")
41
78
  MultiJson.decode(@response.body)[@name]
42
79
  end
43
80
 
81
+ def settings
82
+ @response = Configuration.client.get("#{url}/_settings")
83
+ MultiJson.decode(@response.body)[@name]['settings']
84
+ end
85
+
44
86
  def store(*args)
45
87
  document, options = args
46
88
  type = get_type_from_document(document)
@@ -53,7 +95,7 @@ module Tire
53
95
  id = get_id_from_document(document)
54
96
  document = convert_document_to_json(document)
55
97
 
56
- url = id ? "#{Configuration.url}/#{@name}/#{type}/#{id}" : "#{Configuration.url}/#{@name}/#{type}/"
98
+ url = id ? "#{self.url}/#{type}/#{id}" : "#{self.url}/#{type}/"
57
99
  url += "?percolate=#{percolate}" if percolate
58
100
 
59
101
  @response = Configuration.client.post url, document
@@ -82,7 +124,7 @@ module Tire
82
124
  count = 0
83
125
 
84
126
  begin
85
- response = Configuration.client.post("#{Configuration.url}/_bulk", payload.join("\n"))
127
+ response = Configuration.client.post("#{url}/_bulk", payload.join("\n"))
86
128
  raise RuntimeError, "#{response.code} > #{response.body}" if response.failure?
87
129
  response
88
130
  rescue StandardError => error
@@ -96,7 +138,7 @@ module Tire
96
138
  end
97
139
 
98
140
  ensure
99
- curl = %Q|curl -X POST "#{Configuration.url}/_bulk" -d '{... data omitted ...}'|
141
+ curl = %Q|curl -X POST "#{url}/_bulk" -d '{... data omitted ...}'|
100
142
  logged('BULK', curl)
101
143
  end
102
144
  end
@@ -124,6 +166,17 @@ module Tire
124
166
  end
125
167
  end
126
168
 
169
+ def reindex(name, options={}, &block)
170
+ new_index = Index.new(name)
171
+ new_index.create(options) unless new_index.exists?
172
+
173
+ Search::Scan.new(self.name, &block).each do |results|
174
+ new_index.bulk_store results.map do |document|
175
+ document.to_hash.except(:type, :_index, :_explanation, :_score, :_version, :highlight, :sort)
176
+ end
177
+ end
178
+ end
179
+
127
180
  def remove(*args)
128
181
  if args.size > 1
129
182
  type, document = args
@@ -136,7 +189,7 @@ module Tire
136
189
  end
137
190
  raise ArgumentError, "Please pass a document ID" unless id
138
191
 
139
- url = "#{Configuration.url}/#{@name}/#{type}/#{id}"
192
+ url = "#{self.url}/#{type}/#{id}"
140
193
  result = Configuration.client.delete url
141
194
  MultiJson.decode(result.body) if result.success?
142
195
 
@@ -149,7 +202,7 @@ module Tire
149
202
  raise ArgumentError, "Please pass a document ID" unless id
150
203
 
151
204
  type = Utils.escape(type)
152
- url = "#{Configuration.url}/#{@name}/#{type}/#{id}"
205
+ url = "#{self.url}/#{type}/#{id}"
153
206
  @response = Configuration.client.get url
154
207
 
155
208
  h = MultiJson.decode(@response.body)
@@ -167,40 +220,40 @@ module Tire
167
220
  end
168
221
 
169
222
  def refresh
170
- @response = Configuration.client.post "#{Configuration.url}/#{@name}/_refresh", ''
223
+ @response = Configuration.client.post "#{url}/_refresh", ''
171
224
 
172
225
  ensure
173
- curl = %Q|curl -X POST "#{Configuration.url}/#{@name}/_refresh"|
226
+ curl = %Q|curl -X POST "#{url}/_refresh"|
174
227
  logged('_refresh', curl)
175
228
  end
176
229
 
177
230
  def open(options={})
178
231
  # TODO: Remove the duplication in the execute > rescue > ensure chain
179
- @response = Configuration.client.post "#{Configuration.url}/#{@name}/_open", MultiJson.encode(options)
232
+ @response = Configuration.client.post "#{url}/_open", MultiJson.encode(options)
180
233
  MultiJson.decode(@response.body)['ok']
181
234
 
182
235
  ensure
183
- curl = %Q|curl -X POST "#{Configuration.url}/#{@name}/_open"|
236
+ curl = %Q|curl -X POST "#{url}/_open"|
184
237
  logged('_open', curl)
185
238
  end
186
239
 
187
240
  def close(options={})
188
- @response = Configuration.client.post "#{Configuration.url}/#{@name}/_close", MultiJson.encode(options)
241
+ @response = Configuration.client.post "#{url}/_close", MultiJson.encode(options)
189
242
  MultiJson.decode(@response.body)['ok']
190
243
 
191
244
  ensure
192
- curl = %Q|curl -X POST "#{Configuration.url}/#{@name}/_close"|
245
+ curl = %Q|curl -X POST "#{url}/_close"|
193
246
  logged('_close', curl)
194
247
  end
195
248
 
196
249
  def analyze(text, options={})
197
250
  options = {:pretty => true}.update(options)
198
251
  params = options.to_param
199
- @response = Configuration.client.get "#{Configuration.url}/#{@name}/_analyze?#{params}", text
252
+ @response = Configuration.client.get "#{url}/_analyze?#{params}", text
200
253
  @response.success? ? MultiJson.decode(@response.body) : false
201
254
 
202
255
  ensure
203
- curl = %Q|curl -X GET "#{Configuration.url}/#{@name}/_analyze?#{params}" -d '#{text}'|
256
+ curl = %Q|curl -X GET "#{url}/_analyze?#{params}" -d '#{text}'|
204
257
  logged('_analyze', curl)
205
258
  end
206
259
 
@@ -235,11 +288,11 @@ module Tire
235
288
  payload = { :doc => document }
236
289
  payload.update( :query => query ) if query
237
290
 
238
- @response = Configuration.client.get "#{Configuration.url}/#{@name}/#{type}/_percolate", MultiJson.encode(payload)
291
+ @response = Configuration.client.get "#{url}/#{type}/_percolate", MultiJson.encode(payload)
239
292
  MultiJson.decode(@response.body)['matches']
240
293
 
241
294
  ensure
242
- curl = %Q|curl -X GET "#{Configuration.url}/#{@name}/#{type}/_percolate?pretty=1" -d '#{payload.to_json}'|
295
+ curl = %Q|curl -X GET "#{url}/#{type}/_percolate?pretty=1" -d '#{payload.to_json}'|
243
296
  logged('_percolate', curl)
244
297
  end
245
298
 
@@ -304,7 +357,8 @@ module Tire
304
357
  "please pass an object which responds to `to_indexed_json` or a plain Hash."
305
358
  document
306
359
  when document.respond_to?(:to_indexed_json) then document.to_indexed_json
307
- else raise ArgumentError, "Please pass a JSON string or object with a 'to_indexed_json' method"
360
+ else raise ArgumentError, "Please pass a JSON string or object with a 'to_indexed_json' method," +
361
+ "'#{document.class}' given."
308
362
  end
309
363
  end
310
364
 
@@ -32,7 +32,11 @@ module Tire
32
32
  end
33
33
 
34
34
  def id
35
- @attributes[:_id] || @attributes[:id]
35
+ @attributes[:_id] || @attributes[:id]
36
+ end
37
+
38
+ def type
39
+ @attributes[:_type] || @attributes[:type]
36
40
  end
37
41
 
38
42
  def persisted?
@@ -1,6 +1,6 @@
1
1
  class Hash
2
2
 
3
- def to_json
3
+ def to_json(options=nil)
4
4
  MultiJson.encode(self)
5
5
  end unless respond_to?(:to_json)
6
6
 
@@ -0,0 +1,114 @@
1
+ module Tire
2
+ module Search
3
+
4
+
5
+ # Performs a "scan/scroll" search request, which obtains a `scroll_id`
6
+ # and keeps returning documents matching the passed query (or all documents) in batches.
7
+ #
8
+ # You may want to iterate over the batches being returned:
9
+ #
10
+ # search = Tire::Search::Scan.new('articles')
11
+ # search.each do |results|
12
+ # puts results.map(&:title)
13
+ # end
14
+ #
15
+ # The scan object has a fully Enumerable-compatible interface, so you may
16
+ # call methods like `map` or `each_with_index` on it.
17
+ #
18
+ # To iterate over individual documents, use the `each_document` method:
19
+ #
20
+ # search.each_document do |document|
21
+ # puts document.title
22
+ # end
23
+ #
24
+ # You may limit the result set being returned by a regular Tire DSL query
25
+ # (or a hash, if you prefer), passed as a second argument:
26
+ #
27
+ # search = Tire::Search::Scan.new('articles') do
28
+ # query { term 'author.exact', 'John Smith' }
29
+ # end
30
+ #
31
+ # The feature is also exposed in the Tire top-level DSL:
32
+ #
33
+ # search = Tire.scan 'articles' do
34
+ # query { term 'author.exact', 'John Smith' }
35
+ # end
36
+ #
37
+ # See ElasticSearch documentation for further reference:
38
+ #
39
+ # * http://www.elasticsearch.org/guide/reference/api/search/search-type.html
40
+ # * http://www.elasticsearch.org/guide/reference/api/search/scroll.html
41
+ #
42
+ class Scan
43
+ include Enumerable
44
+
45
+ attr_reader :indices, :options, :search
46
+
47
+ def initialize(indices=nil, options={}, &block)
48
+ @indices = Array(indices)
49
+ @options = options.update(:search_type => 'scan', :scroll => '10m')
50
+ @seen = 0
51
+ @search = Search.new(@indices, @options, &block)
52
+ end
53
+
54
+ def url; Configuration.url + "/_search/scroll"; end
55
+ def params; @options.empty? ? '' : '?' + @options.to_param; end
56
+ def results; @results || (__perform; @results); end
57
+ def response; @response || (__perform; @response); end
58
+ def json; @json || (__perform; @json); end
59
+ def total; @total || (__perform; @total); end
60
+ def seen; @seen || (__perform; @seen); end
61
+
62
+ def scroll_id
63
+ @scroll_id ||= @search.perform.json['_scroll_id']
64
+ end
65
+
66
+ def each
67
+ until results.empty?
68
+ yield results.results
69
+ __perform
70
+ end
71
+ end
72
+
73
+ def each_document
74
+ until results.empty?
75
+ results.each { |item| yield item }
76
+ __perform
77
+ end
78
+ end
79
+
80
+ def size
81
+ results.size
82
+ end
83
+
84
+ def __perform
85
+ @response = Configuration.client.get [url, params].join, scroll_id
86
+ @json = MultiJson.decode @response.body
87
+ @results = Results::Collection.new @json, @options
88
+ @total = @json['hits']['total'].to_i
89
+ @seen += @results.size
90
+ @scroll_id = @json['_scroll_id']
91
+ return self
92
+ ensure
93
+ __logged
94
+ end
95
+
96
+ def to_a; results; end; alias :to_ary :to_a
97
+ def to_curl; %Q|curl -X GET "#{url}?pretty=true" -d '#{@scroll_id}'|; end
98
+
99
+ def __logged(error=nil)
100
+ if Configuration.logger
101
+ Configuration.logger.log_request 'scroll', nil, to_curl
102
+
103
+ took = @json['took'] rescue nil
104
+ code = @response.code rescue nil
105
+ body = "#{@seen}/#{@total} (#{@seen/@total.to_f*100}%)" rescue nil
106
+
107
+ Configuration.logger.log_response code || 'N/A', took || 'N/A', body
108
+ end
109
+ end
110
+
111
+ end
112
+
113
+ end
114
+ end
data/lib/tire/version.rb CHANGED
@@ -1,27 +1,14 @@
1
1
  module Tire
2
- VERSION = "0.4.0"
2
+ VERSION = "0.4.1"
3
3
 
4
4
  CHANGELOG =<<-END
5
5
  IMPORTANT CHANGES LATELY:
6
6
 
7
- * Persistence supports property defaults and casting model properties as Ruby objects
8
- * Added Hashr (http://rubygems.org/gems/hashr) as dependency
9
- * Search in persistence models returns model instances, not Items
10
- * Fixed errors in the Curb client
11
- * Re-raise the RestClient::RequestTimeout and RestClient::ServerBrokeConnection exceptions
12
- * Index#bulk_store and Index#import support the `:raise` option to re-raise exceptions
13
- * Prefer ELASTICSEARCH_URL environment variable as the default URL, if present
14
- * Added the "text" search query
15
- * Deprecated the support for passing JSON strings to `Index#store`
16
- * ActiveModel mapping has the `:as` option dynamically set property value for serialization
17
- * ActiveModel supports any level of mappings in `mapping`
18
- * ActiveModel search can eagerly load records of multiple types/classes
19
- * ActiveModel integration now properly supports namespaced models
20
- * Added support for passing search params (`search_type`, `timeout`, etc.) to search requests
21
- * Added the "tire:index:drop" Rake task
22
- * Added the "Filter" facet type
23
- * Added the "Fuzzy" search query type
24
- * Various test suite refactorings and changes
25
- * Relaxed gem dependencies
7
+ * Added a Index#settings method to retrieve index settings as a Hash
8
+ * Added support for the "scan" search in the Ruby API
9
+ * Added support for reindexing the index documents into new index
10
+ * Added basic support for index aliases
11
+ * Changed, that Index#bulk_store runs against an index endpoint, not against `/_bulk`
12
+ * Refactorings, fixes, Ruby 1.8 compatibility
26
13
  END
27
14
  end
@@ -21,6 +21,19 @@ module Tire
21
21
  assert doc.highlight.title.to_s.include?('<em>'), "Highlight does not include default highlight tag"
22
22
  end
23
23
 
24
+ should "highlight multiple fields with custom highlight tag" do
25
+ s = Tire.search('articles-test') do
26
+ query { string 'Two OR ruby' }
27
+ highlight :tags, :title, :options => { :tag => '<strong>' }
28
+ end
29
+
30
+ doc = s.results.first
31
+
32
+ assert_equal 1, doc.highlight.title.size
33
+ assert_equal "<strong>Two</strong>", doc.highlight.title.first, "Highlight does not include highlight tag"
34
+ assert_equal "<strong>ruby</strong>", doc.highlight.tags.first, "Highlight does not include highlight tag"
35
+ end
36
+
24
37
  should "return entire content with highlighted fragments" do
25
38
  # Tire::Configuration.logger STDERR, :level => 'debug'
26
39