ssickles-tire 0.4.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (114) hide show
  1. data/.gitignore +14 -0
  2. data/.travis.yml +13 -0
  3. data/Gemfile +4 -0
  4. data/MIT-LICENSE +20 -0
  5. data/README.markdown +760 -0
  6. data/Rakefile +78 -0
  7. data/examples/rails-application-template.rb +249 -0
  8. data/examples/tire-dsl.rb +876 -0
  9. data/lib/tire.rb +52 -0
  10. data/lib/tire/alias.rb +296 -0
  11. data/lib/tire/configuration.rb +30 -0
  12. data/lib/tire/dsl.rb +43 -0
  13. data/lib/tire/http/client.rb +62 -0
  14. data/lib/tire/http/clients/curb.rb +61 -0
  15. data/lib/tire/http/response.rb +27 -0
  16. data/lib/tire/index.rb +345 -0
  17. data/lib/tire/logger.rb +60 -0
  18. data/lib/tire/model/callbacks.rb +40 -0
  19. data/lib/tire/model/import.rb +26 -0
  20. data/lib/tire/model/indexing.rb +128 -0
  21. data/lib/tire/model/naming.rb +100 -0
  22. data/lib/tire/model/percolate.rb +99 -0
  23. data/lib/tire/model/persistence.rb +72 -0
  24. data/lib/tire/model/persistence/attributes.rb +143 -0
  25. data/lib/tire/model/persistence/finders.rb +66 -0
  26. data/lib/tire/model/persistence/storage.rb +71 -0
  27. data/lib/tire/model/search.rb +305 -0
  28. data/lib/tire/results/collection.rb +114 -0
  29. data/lib/tire/results/item.rb +83 -0
  30. data/lib/tire/results/pagination.rb +54 -0
  31. data/lib/tire/rubyext/hash.rb +8 -0
  32. data/lib/tire/rubyext/ruby_1_8.rb +54 -0
  33. data/lib/tire/rubyext/symbol.rb +11 -0
  34. data/lib/tire/search.rb +160 -0
  35. data/lib/tire/search/facet.rb +70 -0
  36. data/lib/tire/search/filter.rb +28 -0
  37. data/lib/tire/search/highlight.rb +37 -0
  38. data/lib/tire/search/query.rb +151 -0
  39. data/lib/tire/search/scan.rb +114 -0
  40. data/lib/tire/search/sort.rb +25 -0
  41. data/lib/tire/tasks.rb +135 -0
  42. data/lib/tire/utils.rb +17 -0
  43. data/lib/tire/version.rb +22 -0
  44. data/test/fixtures/articles/1.json +1 -0
  45. data/test/fixtures/articles/2.json +1 -0
  46. data/test/fixtures/articles/3.json +1 -0
  47. data/test/fixtures/articles/4.json +1 -0
  48. data/test/fixtures/articles/5.json +1 -0
  49. data/test/integration/active_model_indexing_test.rb +51 -0
  50. data/test/integration/active_model_searchable_test.rb +114 -0
  51. data/test/integration/active_record_searchable_test.rb +446 -0
  52. data/test/integration/boolean_queries_test.rb +43 -0
  53. data/test/integration/count_test.rb +34 -0
  54. data/test/integration/custom_score_queries_test.rb +88 -0
  55. data/test/integration/dsl_search_test.rb +22 -0
  56. data/test/integration/explanation_test.rb +44 -0
  57. data/test/integration/facets_test.rb +232 -0
  58. data/test/integration/filtered_queries_test.rb +66 -0
  59. data/test/integration/filters_test.rb +63 -0
  60. data/test/integration/fuzzy_queries_test.rb +20 -0
  61. data/test/integration/highlight_test.rb +64 -0
  62. data/test/integration/index_aliases_test.rb +122 -0
  63. data/test/integration/index_mapping_test.rb +43 -0
  64. data/test/integration/index_store_test.rb +96 -0
  65. data/test/integration/mongoid_searchable_test.rb +309 -0
  66. data/test/integration/percolator_test.rb +111 -0
  67. data/test/integration/persistent_model_test.rb +117 -0
  68. data/test/integration/query_return_version_test.rb +70 -0
  69. data/test/integration/query_string_test.rb +52 -0
  70. data/test/integration/range_queries_test.rb +36 -0
  71. data/test/integration/reindex_test.rb +46 -0
  72. data/test/integration/results_test.rb +39 -0
  73. data/test/integration/scan_test.rb +56 -0
  74. data/test/integration/sort_test.rb +36 -0
  75. data/test/integration/text_query_test.rb +39 -0
  76. data/test/models/active_model_article.rb +31 -0
  77. data/test/models/active_model_article_with_callbacks.rb +49 -0
  78. data/test/models/active_model_article_with_custom_document_type.rb +7 -0
  79. data/test/models/active_model_article_with_custom_index_name.rb +7 -0
  80. data/test/models/active_record_models.rb +122 -0
  81. data/test/models/article.rb +15 -0
  82. data/test/models/mongoid_models.rb +97 -0
  83. data/test/models/persistent_article.rb +11 -0
  84. data/test/models/persistent_article_in_namespace.rb +12 -0
  85. data/test/models/persistent_article_with_casting.rb +28 -0
  86. data/test/models/persistent_article_with_defaults.rb +11 -0
  87. data/test/models/persistent_articles_with_custom_index_name.rb +10 -0
  88. data/test/models/supermodel_article.rb +17 -0
  89. data/test/models/validated_model.rb +11 -0
  90. data/test/test_helper.rb +88 -0
  91. data/test/unit/active_model_lint_test.rb +17 -0
  92. data/test/unit/configuration_test.rb +74 -0
  93. data/test/unit/http_client_test.rb +76 -0
  94. data/test/unit/http_response_test.rb +49 -0
  95. data/test/unit/index_alias_test.rb +275 -0
  96. data/test/unit/index_test.rb +841 -0
  97. data/test/unit/logger_test.rb +125 -0
  98. data/test/unit/model_callbacks_test.rb +116 -0
  99. data/test/unit/model_import_test.rb +71 -0
  100. data/test/unit/model_persistence_test.rb +516 -0
  101. data/test/unit/model_search_test.rb +899 -0
  102. data/test/unit/results_collection_test.rb +281 -0
  103. data/test/unit/results_item_test.rb +155 -0
  104. data/test/unit/rubyext_test.rb +60 -0
  105. data/test/unit/search_facet_test.rb +153 -0
  106. data/test/unit/search_filter_test.rb +42 -0
  107. data/test/unit/search_highlight_test.rb +46 -0
  108. data/test/unit/search_query_test.rb +242 -0
  109. data/test/unit/search_scan_test.rb +113 -0
  110. data/test/unit/search_sort_test.rb +50 -0
  111. data/test/unit/search_test.rb +455 -0
  112. data/test/unit/tire_test.rb +126 -0
  113. data/tire.gemspec +85 -0
  114. metadata +506 -0
@@ -0,0 +1,70 @@
1
+ module Tire
2
+ module Search
3
+
4
+ #--
5
+ # TODO: Implement all elastic search facets (geo, histogram, range, etc)
6
+ # http://elasticsearch.org/guide/reference/api/search/facets/
7
+ #++
8
+
9
+ class Facet
10
+
11
+ def initialize(name, options={}, &block)
12
+ @name = name
13
+ @options = options
14
+ block.arity < 1 ? self.instance_eval(&block) : block.call(self) if block_given?
15
+ end
16
+
17
+ def terms(field, options={})
18
+ size = options.delete(:size) || 10
19
+ all_terms = options.delete(:all_terms) || false
20
+ @value = { :terms => { :field => field, :size => size, :all_terms => all_terms }.update(options) }
21
+ self
22
+ end
23
+
24
+ def date(field, options={})
25
+ interval = options.delete(:interval) || 'day'
26
+ @value = { :date_histogram => { :field => field, :interval => interval }.update(options) }
27
+ self
28
+ end
29
+
30
+ def range(field, ranges=[], options={})
31
+ @value = { :range => { :field => field, :ranges => ranges }.update(options) }
32
+ self
33
+ end
34
+
35
+ def histogram(field, options={})
36
+ @value = { :histogram => (options.delete(:histogram) || {:field => field}.update(options)) }
37
+ self
38
+ end
39
+
40
+ def statistical(field, options={})
41
+ @value = { :statistical => (options.delete(:statistical) || {:field => field}.update(options)) }
42
+ self
43
+ end
44
+
45
+ def terms_stats(key_field, value_field, options={})
46
+ @value = { :terms_stats => {:key_field => key_field, :value_field => value_field}.update(options) }
47
+ self
48
+ end
49
+
50
+ def query(&block)
51
+ @value = { :query => Query.new(&block).to_hash }
52
+ end
53
+
54
+ def filter(field, value, options={})
55
+ @value = { :filter => { :term => { field => value }}.update(options) }
56
+ self
57
+ end
58
+
59
+ def to_json
60
+ to_hash.to_json
61
+ end
62
+
63
+ def to_hash
64
+ @value.update @options
65
+ { @name => @value }
66
+ end
67
+ end
68
+
69
+ end
70
+ end
@@ -0,0 +1,28 @@
1
+ module Tire
2
+ module Search
3
+
4
+ # http://www.elasticsearch.org/guide/reference/api/search/filter.html
5
+ # http://www.elasticsearch.org/guide/reference/query-dsl/
6
+ #
7
+ class Filter
8
+
9
+ def initialize(type, *options)
10
+ value = if options.size < 2
11
+ options.first || {}
12
+ else
13
+ options # An +or+ filter encodes multiple filters as an array
14
+ end
15
+ @hash = { type => value }
16
+ end
17
+
18
+ def to_json
19
+ to_hash.to_json
20
+ end
21
+
22
+ def to_hash
23
+ @hash
24
+ end
25
+ end
26
+
27
+ end
28
+ end
@@ -0,0 +1,37 @@
1
+ module Tire
2
+ module Search
3
+
4
+ # http://www.elasticsearch.org/guide/reference/api/search/highlighting.html
5
+ #
6
+ class Highlight
7
+
8
+ def initialize(*args)
9
+ @options = (args.last.is_a?(Hash) && args.last.delete(:options)) || {}
10
+ extract_highlight_tags
11
+ @fields = args.inject({}) do |result, field|
12
+ field.is_a?(Hash) ? result.update(field) : result[field.to_sym] = {}; result
13
+ end
14
+ end
15
+
16
+ def to_json
17
+ to_hash.to_json
18
+ end
19
+
20
+ def to_hash
21
+ { :fields => @fields }.update @options
22
+ end
23
+
24
+ private
25
+
26
+ def extract_highlight_tags
27
+ if tag = @options.delete(:tag)
28
+ @options.update \
29
+ :pre_tags => [tag],
30
+ :post_tags => [tag.to_s.gsub(/^<([a-z]+).*/, '</\1>')]
31
+ end
32
+ end
33
+
34
+ end
35
+
36
+ end
37
+ end
@@ -0,0 +1,151 @@
1
+ module Tire
2
+ module Search
3
+
4
+ class Query
5
+ def initialize(&block)
6
+ @value = {}
7
+ block.arity < 1 ? self.instance_eval(&block) : block.call(self) if block_given?
8
+ end
9
+
10
+ def term(field, value, options={})
11
+ query = { field => { :term => value }.update(options) }
12
+ @value = { :term => query }
13
+ end
14
+
15
+ def terms(field, value, options={})
16
+ @value = { :terms => { field => value } }
17
+ @value[:terms].update( { :minimum_match => options[:minimum_match] } ) if options[:minimum_match]
18
+ @value
19
+ end
20
+
21
+ def range(field, value)
22
+ @value = { :range => { field => value } }
23
+ end
24
+
25
+ def text(field, value, options={})
26
+ query_options = { :query => value }.update(options)
27
+ @value = { :text => { field => query_options } }
28
+ @value
29
+ end
30
+
31
+ def string(value, options={})
32
+ @value = { :query_string => { :query => value } }
33
+ @value[:query_string].update(options)
34
+ @value
35
+ end
36
+
37
+ def custom_score(options={}, &block)
38
+ @custom_score ||= Query.new(&block)
39
+ @value[:custom_score] = options
40
+ @value[:custom_score].update({:query => @custom_score.to_hash})
41
+ @value
42
+ end
43
+
44
+ def fuzzy(field, value, options={})
45
+ query = { field => { :term => value }.update(options) }
46
+ @value = { :fuzzy => query }
47
+ end
48
+
49
+ def boolean(options={}, &block)
50
+ @boolean ||= BooleanQuery.new(options)
51
+ block.arity < 1 ? @boolean.instance_eval(&block) : block.call(@boolean) if block_given?
52
+ @value[:bool] = @boolean.to_hash
53
+ @value
54
+ end
55
+
56
+ def filtered(&block)
57
+ @filtered = FilteredQuery.new
58
+ block.arity < 1 ? @filtered.instance_eval(&block) : block.call(@filtered) if block_given?
59
+ @value[:filtered] = @filtered.to_hash
60
+ @value
61
+ end
62
+
63
+ def all
64
+ @value = { :match_all => {} }
65
+ @value
66
+ end
67
+
68
+ def ids(values, type)
69
+ @value = { :ids => { :values => values, :type => type } }
70
+ end
71
+
72
+ def to_hash
73
+ @value
74
+ end
75
+
76
+ def to_json
77
+ to_hash.to_json
78
+ end
79
+
80
+ end
81
+
82
+ class BooleanQuery
83
+
84
+ # TODO: Try to get rid of multiple `should`, `must`, etc invocations, and wrap queries directly:
85
+ #
86
+ # boolean do
87
+ # should do
88
+ # string 'foo'
89
+ # string 'bar'
90
+ # end
91
+ # end
92
+ #
93
+ # Inherit from Query, implement `encode` method there, and overload it here, so it puts
94
+ # queries in an Array instead of hash.
95
+
96
+ def initialize(options={}, &block)
97
+ @options = options
98
+ @value = {}
99
+ block.arity < 1 ? self.instance_eval(&block) : block.call(self) if block_given?
100
+ end
101
+
102
+ def must(&block)
103
+ (@value[:must] ||= []) << Query.new(&block).to_hash
104
+ @value
105
+ end
106
+
107
+ def must_not(&block)
108
+ (@value[:must_not] ||= []) << Query.new(&block).to_hash
109
+ @value
110
+ end
111
+
112
+ def should(&block)
113
+ (@value[:should] ||= []) << Query.new(&block).to_hash
114
+ @value
115
+ end
116
+
117
+ def to_hash
118
+ @value.update(@options)
119
+ end
120
+ end
121
+
122
+
123
+ class FilteredQuery
124
+ def initialize(&block)
125
+ @value = {}
126
+ block.arity < 1 ? self.instance_eval(&block) : block.call(self) if block_given?
127
+ end
128
+
129
+ def query(options={}, &block)
130
+ @value[:query] = Query.new(&block).to_hash
131
+ @value
132
+ end
133
+
134
+ def filter(type, *options)
135
+ @value[:filter] ||= {}
136
+ @value[:filter][:and] ||= []
137
+ @value[:filter][:and] << Filter.new(type, *options).to_hash
138
+ @value
139
+ end
140
+
141
+ def to_hash
142
+ @value
143
+ end
144
+
145
+ def to_json
146
+ to_hash.to_json
147
+ end
148
+ end
149
+
150
+ end
151
+ end
@@ -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
@@ -0,0 +1,25 @@
1
+ module Tire
2
+ module Search
3
+
4
+ class Sort
5
+ def initialize(&block)
6
+ @value = []
7
+ block.arity < 1 ? self.instance_eval(&block) : block.call(self) if block_given?
8
+ end
9
+
10
+ def by(name, direction=nil)
11
+ @value << ( direction ? { name => direction } : name )
12
+ self
13
+ end
14
+
15
+ def to_ary
16
+ @value
17
+ end
18
+
19
+ def to_json
20
+ @value.to_json
21
+ end
22
+ end
23
+
24
+ end
25
+ end
data/lib/tire/tasks.rb ADDED
@@ -0,0 +1,135 @@
1
+ require 'rake'
2
+ require 'benchmark'
3
+
4
+ namespace :tire do
5
+
6
+ full_comment = <<-DESC.gsub(/ /, '')
7
+ Import data from your model using paginate: rake environment tire:import CLASS='MyModel'.
8
+
9
+ Pass params for the `paginate` method:
10
+ $ rake environment tire:import CLASS='Article' PARAMS='{:page => 1}'
11
+
12
+ Force rebuilding the index (delete and create):
13
+ $ rake environment tire:import CLASS='Article' PARAMS='{:page => 1}' FORCE=1
14
+
15
+ Set target index name:
16
+ $ rake environment tire:import CLASS='Article' INDEX='articles-new'
17
+ DESC
18
+ desc full_comment
19
+ task :import do |t|
20
+
21
+ def elapsed_to_human(elapsed)
22
+ hour = 60*60
23
+ day = hour*24
24
+
25
+ case elapsed
26
+ when 0..59
27
+ "#{sprintf("%1.5f", elapsed)} seconds"
28
+ when 60..hour-1
29
+ "#{elapsed/60} minutes and #{elapsed % 60} seconds"
30
+ when hour..day
31
+ "#{elapsed/hour} hours and #{elapsed % hour} minutes"
32
+ else
33
+ "#{elapsed/hour} hours"
34
+ end
35
+ end
36
+
37
+ if ENV['CLASS'].to_s == ''
38
+ puts '='*90, 'USAGE', '='*90, full_comment, ""
39
+ exit(1)
40
+ end
41
+
42
+ klass = eval(ENV['CLASS'].to_s)
43
+ params = eval(ENV['PARAMS'].to_s) || {}
44
+
45
+ params.update :method => 'paginate'
46
+
47
+ index = Tire::Index.new( ENV['INDEX'] || klass.tire.index.name )
48
+
49
+ if ENV['FORCE']
50
+ puts "[IMPORT] Deleting index '#{index.name}'"
51
+ index.delete
52
+ end
53
+
54
+ unless index.exists?
55
+ mapping = defined?(Yajl) ? Yajl::Encoder.encode(klass.tire.mapping_to_hash, :pretty => true) :
56
+ MultiJson.encode(klass.tire.mapping_to_hash)
57
+ puts "[IMPORT] Creating index '#{index.name}' with mapping:", mapping
58
+ index.create :mappings => klass.tire.mapping_to_hash, :settings => klass.tire.settings
59
+ end
60
+
61
+ STDOUT.sync = true
62
+ puts "[IMPORT] Starting import for the '#{ENV['CLASS']}' class"
63
+ tty_cols = 80
64
+ total = klass.count rescue nil
65
+ offset = (total.to_s.size*2)+8
66
+ done = 0
67
+
68
+ STDOUT.puts '-'*tty_cols
69
+ elapsed = Benchmark.realtime do
70
+
71
+ # Add Kaminari-powered "paginate" method
72
+ #
73
+ if defined?(Kaminari) && klass.respond_to?(:page)
74
+ klass.instance_eval do
75
+ def paginate(options = {})
76
+ page(options[:page]).per(options[:per_page]).to_a
77
+ end
78
+ end
79
+ end unless klass.respond_to?(:paginate)
80
+
81
+ # Import the documents
82
+ #
83
+ index.import(klass, params) do |documents|
84
+
85
+ if total
86
+ done += documents.to_a.size
87
+ # I CAN HAZ PROGREZ BAR LIEK HOMEBRU!
88
+ percent = ( (done.to_f / total) * 100 ).to_i
89
+ glyphs = ( percent * ( (tty_cols-offset).to_f/100 ) ).to_i
90
+ STDOUT.print( "#" * glyphs )
91
+ STDOUT.print( "\r"*tty_cols+"#{done}/#{total} | \e[1m#{percent}%\e[0m " )
92
+ end
93
+
94
+ # Don't forget to return the documents collection back!
95
+ documents
96
+ end
97
+ end
98
+
99
+ puts "", '='*80, "Import finished in #{elapsed_to_human(elapsed)}"
100
+ end
101
+
102
+ namespace :index do
103
+
104
+ full_comment = <<-DESC.gsub(/ /, '')
105
+ Delete indices passed in the INDEX environment variable; separate multiple indices by comma.
106
+
107
+ Pass name of a single index to drop in the INDEX environmnet variable:
108
+ $ rake environment tire:index:drop INDEX=articles
109
+
110
+ Pass names of multiple indices to drop in the INDEX or INDICES environmnet variable:
111
+ $ rake environment tire:index:drop INDICES=articles-2011-01,articles-2011-02
112
+
113
+ DESC
114
+ desc full_comment
115
+ task :drop do
116
+ index_names = (ENV['INDEX'] || ENV['INDICES']).to_s.split(/,\s*/)
117
+
118
+ if index_names.empty?
119
+ puts '='*90, 'USAGE', '='*90, full_comment, ""
120
+ exit(1)
121
+ end
122
+
123
+ index_names.each do |name|
124
+ index = Tire::Index.new(name)
125
+ print "* Deleting index \e[1m#{index.name}\e[0m... "
126
+ puts index.delete ? "\e[32mOK\e[0m" : "\e[31mFAILED\e[0m | #{index.response.body}"
127
+ end
128
+
129
+ puts ""
130
+
131
+ end
132
+
133
+ end
134
+
135
+ end