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 +2 -0
- data/lib/tire/dsl.rb +14 -0
- data/lib/tire/index.rb +77 -23
- data/lib/tire/results/item.rb +5 -1
- data/lib/tire/rubyext/hash.rb +1 -1
- data/lib/tire/search/scan.rb +114 -0
- data/lib/tire/version.rb +7 -20
- data/test/integration/highlight_test.rb +13 -0
- data/test/integration/index_aliases_test.rb +68 -0
- data/test/integration/reindex_test.rb +46 -0
- data/test/integration/scan_test.rb +56 -0
- data/test/test_helper.rb +12 -1
- data/test/unit/index_test.rb +149 -36
- data/test/unit/results_item_test.rb +14 -0
- data/test/unit/rubyext_test.rb +8 -1
- data/test/unit/search_scan_test.rb +113 -0
- data/test/unit/tire_test.rb +35 -0
- data/tire.gemspec +2 -2
- metadata +55 -56
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("#{
|
16
|
+
@response = Configuration.client.head("#{url}")
|
13
17
|
@response.success?
|
14
18
|
|
15
19
|
ensure
|
16
|
-
curl = %Q|curl -I "#{
|
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
|
25
|
+
@response = Configuration.client.delete url
|
22
26
|
@response.success?
|
23
27
|
|
24
28
|
ensure
|
25
|
-
curl = %Q|curl -X DELETE
|
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
|
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
|
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("#{
|
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 ? "#{
|
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("#{
|
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 "#{
|
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 = "#{
|
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 = "#{
|
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 "#{
|
223
|
+
@response = Configuration.client.post "#{url}/_refresh", ''
|
171
224
|
|
172
225
|
ensure
|
173
|
-
curl = %Q|curl -X POST "#{
|
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 "#{
|
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 "#{
|
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 "#{
|
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 "#{
|
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 "#{
|
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 "#{
|
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 "#{
|
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 "#{
|
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
|
|
data/lib/tire/results/item.rb
CHANGED
data/lib/tire/rubyext/hash.rb
CHANGED
@@ -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.
|
2
|
+
VERSION = "0.4.1"
|
3
3
|
|
4
4
|
CHANGELOG =<<-END
|
5
5
|
IMPORTANT CHANGES LATELY:
|
6
6
|
|
7
|
-
*
|
8
|
-
* Added
|
9
|
-
*
|
10
|
-
*
|
11
|
-
*
|
12
|
-
*
|
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
|
|