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 +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
|
|