tire 0.4.0 → 0.4.1

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