load_balanced_tire 0.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/.gitignore +14 -0
- data/.travis.yml +29 -0
- data/Gemfile +4 -0
- data/MIT-LICENSE +20 -0
- data/README.markdown +760 -0
- data/Rakefile +78 -0
- data/examples/rails-application-template.rb +249 -0
- data/examples/tire-dsl.rb +876 -0
- data/lib/tire.rb +55 -0
- data/lib/tire/alias.rb +296 -0
- data/lib/tire/configuration.rb +30 -0
- data/lib/tire/dsl.rb +43 -0
- data/lib/tire/http/client.rb +62 -0
- data/lib/tire/http/clients/curb.rb +61 -0
- data/lib/tire/http/clients/faraday.rb +71 -0
- data/lib/tire/http/response.rb +27 -0
- data/lib/tire/index.rb +361 -0
- data/lib/tire/logger.rb +60 -0
- data/lib/tire/model/callbacks.rb +40 -0
- data/lib/tire/model/import.rb +26 -0
- data/lib/tire/model/indexing.rb +128 -0
- data/lib/tire/model/naming.rb +100 -0
- data/lib/tire/model/percolate.rb +99 -0
- data/lib/tire/model/persistence.rb +71 -0
- data/lib/tire/model/persistence/attributes.rb +143 -0
- data/lib/tire/model/persistence/finders.rb +66 -0
- data/lib/tire/model/persistence/storage.rb +69 -0
- data/lib/tire/model/search.rb +307 -0
- data/lib/tire/results/collection.rb +114 -0
- data/lib/tire/results/item.rb +86 -0
- data/lib/tire/results/pagination.rb +54 -0
- data/lib/tire/rubyext/hash.rb +8 -0
- data/lib/tire/rubyext/ruby_1_8.rb +7 -0
- data/lib/tire/rubyext/symbol.rb +11 -0
- data/lib/tire/search.rb +188 -0
- data/lib/tire/search/facet.rb +74 -0
- data/lib/tire/search/filter.rb +28 -0
- data/lib/tire/search/highlight.rb +37 -0
- data/lib/tire/search/query.rb +186 -0
- data/lib/tire/search/scan.rb +114 -0
- data/lib/tire/search/script_field.rb +23 -0
- data/lib/tire/search/sort.rb +25 -0
- data/lib/tire/tasks.rb +135 -0
- data/lib/tire/utils.rb +17 -0
- data/lib/tire/version.rb +22 -0
- data/test/fixtures/articles/1.json +1 -0
- data/test/fixtures/articles/2.json +1 -0
- data/test/fixtures/articles/3.json +1 -0
- data/test/fixtures/articles/4.json +1 -0
- data/test/fixtures/articles/5.json +1 -0
- data/test/integration/active_model_indexing_test.rb +51 -0
- data/test/integration/active_model_searchable_test.rb +114 -0
- data/test/integration/active_record_searchable_test.rb +446 -0
- data/test/integration/boolean_queries_test.rb +43 -0
- data/test/integration/count_test.rb +34 -0
- data/test/integration/custom_score_queries_test.rb +88 -0
- data/test/integration/dis_max_queries_test.rb +68 -0
- data/test/integration/dsl_search_test.rb +22 -0
- data/test/integration/explanation_test.rb +44 -0
- data/test/integration/facets_test.rb +259 -0
- data/test/integration/filtered_queries_test.rb +66 -0
- data/test/integration/filters_test.rb +63 -0
- data/test/integration/fuzzy_queries_test.rb +20 -0
- data/test/integration/highlight_test.rb +64 -0
- data/test/integration/index_aliases_test.rb +122 -0
- data/test/integration/index_mapping_test.rb +43 -0
- data/test/integration/index_store_test.rb +96 -0
- data/test/integration/index_update_document_test.rb +111 -0
- data/test/integration/mongoid_searchable_test.rb +309 -0
- data/test/integration/percolator_test.rb +111 -0
- data/test/integration/persistent_model_test.rb +130 -0
- data/test/integration/prefix_query_test.rb +43 -0
- data/test/integration/query_return_version_test.rb +70 -0
- data/test/integration/query_string_test.rb +52 -0
- data/test/integration/range_queries_test.rb +36 -0
- data/test/integration/reindex_test.rb +46 -0
- data/test/integration/results_test.rb +39 -0
- data/test/integration/scan_test.rb +56 -0
- data/test/integration/script_fields_test.rb +38 -0
- data/test/integration/sort_test.rb +36 -0
- data/test/integration/text_query_test.rb +39 -0
- data/test/models/active_model_article.rb +31 -0
- data/test/models/active_model_article_with_callbacks.rb +49 -0
- data/test/models/active_model_article_with_custom_document_type.rb +7 -0
- data/test/models/active_model_article_with_custom_index_name.rb +7 -0
- data/test/models/active_record_models.rb +122 -0
- data/test/models/article.rb +15 -0
- data/test/models/mongoid_models.rb +97 -0
- data/test/models/persistent_article.rb +11 -0
- data/test/models/persistent_article_in_namespace.rb +12 -0
- data/test/models/persistent_article_with_casting.rb +28 -0
- data/test/models/persistent_article_with_defaults.rb +11 -0
- data/test/models/persistent_articles_with_custom_index_name.rb +10 -0
- data/test/models/supermodel_article.rb +17 -0
- data/test/models/validated_model.rb +11 -0
- data/test/test_helper.rb +93 -0
- data/test/unit/active_model_lint_test.rb +17 -0
- data/test/unit/configuration_test.rb +74 -0
- data/test/unit/http_client_test.rb +76 -0
- data/test/unit/http_response_test.rb +49 -0
- data/test/unit/index_alias_test.rb +275 -0
- data/test/unit/index_test.rb +894 -0
- data/test/unit/logger_test.rb +125 -0
- data/test/unit/model_callbacks_test.rb +116 -0
- data/test/unit/model_import_test.rb +71 -0
- data/test/unit/model_persistence_test.rb +528 -0
- data/test/unit/model_search_test.rb +913 -0
- data/test/unit/results_collection_test.rb +281 -0
- data/test/unit/results_item_test.rb +162 -0
- data/test/unit/rubyext_test.rb +66 -0
- data/test/unit/search_facet_test.rb +153 -0
- data/test/unit/search_filter_test.rb +42 -0
- data/test/unit/search_highlight_test.rb +46 -0
- data/test/unit/search_query_test.rb +301 -0
- data/test/unit/search_scan_test.rb +113 -0
- data/test/unit/search_script_field_test.rb +26 -0
- data/test/unit/search_sort_test.rb +50 -0
- data/test/unit/search_test.rb +499 -0
- data/test/unit/tire_test.rb +126 -0
- data/tire.gemspec +90 -0
- metadata +549 -0
@@ -0,0 +1,114 @@
|
|
1
|
+
module Tire
|
2
|
+
module Results
|
3
|
+
|
4
|
+
class Collection
|
5
|
+
include Enumerable
|
6
|
+
include Pagination
|
7
|
+
|
8
|
+
attr_reader :time, :total, :options, :facets
|
9
|
+
|
10
|
+
def initialize(response, options={})
|
11
|
+
@response = response
|
12
|
+
@options = options
|
13
|
+
@time = response['took'].to_i
|
14
|
+
@total = response['hits']['total'].to_i
|
15
|
+
@facets = response['facets']
|
16
|
+
@wrapper = options[:wrapper] || Configuration.wrapper
|
17
|
+
end
|
18
|
+
|
19
|
+
def results
|
20
|
+
@results ||= begin
|
21
|
+
hits = @response['hits']['hits'].map { |d| d.update '_type' => Utils.unescape(d['_type']) }
|
22
|
+
|
23
|
+
unless @options[:load]
|
24
|
+
if @wrapper == Hash
|
25
|
+
hits
|
26
|
+
else
|
27
|
+
hits.map do |h|
|
28
|
+
document = {}
|
29
|
+
|
30
|
+
# Update the document with content and ID
|
31
|
+
document = h['_source'] ? document.update( h['_source'] || {} ) : document.update( __parse_fields__(h['fields']) )
|
32
|
+
document.update( {'id' => h['_id']} )
|
33
|
+
|
34
|
+
# Update the document with meta information
|
35
|
+
['_score', '_type', '_index', '_version', 'sort', 'highlight', '_explanation'].each { |key| document.update( {key => h[key]} || {} ) }
|
36
|
+
|
37
|
+
# Return an instance of the "wrapper" class
|
38
|
+
@wrapper.new(document)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
else
|
43
|
+
return [] if hits.empty?
|
44
|
+
|
45
|
+
records = {}
|
46
|
+
@response['hits']['hits'].group_by { |item| item['_type'] }.each do |type, items|
|
47
|
+
raise NoMethodError, "You have tried to eager load the model instances, " +
|
48
|
+
"but Tire cannot find the model class because " +
|
49
|
+
"document has no _type property." unless type
|
50
|
+
|
51
|
+
begin
|
52
|
+
klass = type.camelize.constantize
|
53
|
+
rescue NameError => e
|
54
|
+
raise NameError, "You have tried to eager load the model instances, but " +
|
55
|
+
"Tire cannot find the model class '#{type.camelize}' " +
|
56
|
+
"based on _type '#{type}'.", e.backtrace
|
57
|
+
end
|
58
|
+
ids = items.map { |h| h['_id'] }
|
59
|
+
records[type] = @options[:load] === true ? klass.find(ids) : klass.find(ids, @options[:load])
|
60
|
+
end
|
61
|
+
|
62
|
+
# Reorder records to preserve order from search results
|
63
|
+
@response['hits']['hits'].map { |item| records[item['_type']].detect { |record| record.id.to_s == item['_id'].to_s } }
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def each(&block)
|
69
|
+
results.each(&block)
|
70
|
+
end
|
71
|
+
|
72
|
+
def empty?
|
73
|
+
results.empty?
|
74
|
+
end
|
75
|
+
|
76
|
+
def size
|
77
|
+
results.size
|
78
|
+
end
|
79
|
+
alias :length :size
|
80
|
+
|
81
|
+
def [](index)
|
82
|
+
results[index]
|
83
|
+
end
|
84
|
+
|
85
|
+
def to_ary
|
86
|
+
self
|
87
|
+
end
|
88
|
+
|
89
|
+
# Handles _source prefixed fields properly: strips the prefix and converts fields to nested Hashes
|
90
|
+
#
|
91
|
+
def __parse_fields__(fields={})
|
92
|
+
( fields ||= {} ).clone.each_pair do |key,value|
|
93
|
+
next unless key.to_s =~ /_source/ # Skip regular JSON immediately
|
94
|
+
|
95
|
+
keys = key.to_s.split('.').reject { |n| n == '_source' }
|
96
|
+
fields.delete(key)
|
97
|
+
|
98
|
+
result = {}
|
99
|
+
path = []
|
100
|
+
|
101
|
+
keys.each do |name|
|
102
|
+
path << name
|
103
|
+
eval "result[:#{path.join('][:')}] ||= {}"
|
104
|
+
eval "result[:#{path.join('][:')}] = #{value.inspect}" if keys.last == name
|
105
|
+
end
|
106
|
+
fields.update result
|
107
|
+
end
|
108
|
+
fields
|
109
|
+
end
|
110
|
+
|
111
|
+
end
|
112
|
+
|
113
|
+
end
|
114
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
module Tire
|
2
|
+
module Results
|
3
|
+
|
4
|
+
class Item
|
5
|
+
extend ActiveModel::Naming
|
6
|
+
include ActiveModel::Conversion
|
7
|
+
|
8
|
+
# Create new instance, recursively converting all Hashes to Item
|
9
|
+
# and leaving everything else alone.
|
10
|
+
#
|
11
|
+
def initialize(args={})
|
12
|
+
raise ArgumentError, "Please pass a Hash-like object" unless args.respond_to?(:each_pair)
|
13
|
+
@attributes = {}
|
14
|
+
args.each_pair do |key, value|
|
15
|
+
if value.is_a?(Array)
|
16
|
+
@attributes[key.to_sym] = value.map { |item| @attributes[key.to_sym] = item.is_a?(Hash) ? Item.new(item.to_hash) : item }
|
17
|
+
else
|
18
|
+
@attributes[key.to_sym] = value.is_a?(Hash) ? Item.new(value.to_hash) : value
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
# Delegate method to a key in underlying hash, if present,
|
24
|
+
# otherwise return +nil+.
|
25
|
+
#
|
26
|
+
def method_missing(method_name, *arguments)
|
27
|
+
@attributes.has_key?(method_name.to_sym) ? @attributes[method_name.to_sym] : nil
|
28
|
+
end
|
29
|
+
|
30
|
+
def [](key)
|
31
|
+
@attributes[key.to_sym]
|
32
|
+
end
|
33
|
+
|
34
|
+
def id
|
35
|
+
@attributes[:_id] || @attributes[:id]
|
36
|
+
end
|
37
|
+
|
38
|
+
def type
|
39
|
+
@attributes[:_type] || @attributes[:type]
|
40
|
+
end
|
41
|
+
|
42
|
+
def persisted?
|
43
|
+
!!id
|
44
|
+
end
|
45
|
+
|
46
|
+
def errors
|
47
|
+
ActiveModel::Errors.new(self)
|
48
|
+
end
|
49
|
+
|
50
|
+
def valid?
|
51
|
+
true
|
52
|
+
end
|
53
|
+
|
54
|
+
def to_key
|
55
|
+
persisted? ? [id] : nil
|
56
|
+
end
|
57
|
+
|
58
|
+
def to_hash
|
59
|
+
@attributes.reduce({}) do |sum, item|
|
60
|
+
sum[ item.first ] = item.last.respond_to?(:to_hash) ? item.last.to_hash : item.last
|
61
|
+
sum
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
# Let's pretend we're someone else in Rails
|
66
|
+
#
|
67
|
+
def class
|
68
|
+
defined?(::Rails) && @attributes[:_type] ? @attributes[:_type].camelize.constantize : super
|
69
|
+
rescue NameError
|
70
|
+
super
|
71
|
+
end
|
72
|
+
|
73
|
+
def inspect
|
74
|
+
s = []; @attributes.each { |k,v| s << "#{k}: #{v.inspect}" }
|
75
|
+
%Q|<Item#{self.class.to_s == 'Tire::Results::Item' ? '' : " (#{self.class})"} #{s.join(', ')}>|
|
76
|
+
end
|
77
|
+
|
78
|
+
def to_json(options=nil)
|
79
|
+
@attributes.to_json(options)
|
80
|
+
end
|
81
|
+
alias_method :to_indexed_json, :to_json
|
82
|
+
|
83
|
+
end
|
84
|
+
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module Tire
|
2
|
+
module Results
|
3
|
+
|
4
|
+
# Adds support for WillPaginate and Kaminari
|
5
|
+
#
|
6
|
+
module Pagination
|
7
|
+
|
8
|
+
def total_entries
|
9
|
+
@total
|
10
|
+
end
|
11
|
+
|
12
|
+
def per_page
|
13
|
+
(@options[:per_page] || @options[:size] || 10 ).to_i
|
14
|
+
end
|
15
|
+
|
16
|
+
def total_pages
|
17
|
+
( @total.to_f / per_page ).ceil
|
18
|
+
end
|
19
|
+
|
20
|
+
def current_page
|
21
|
+
if @options[:page]
|
22
|
+
@options[:page].to_i
|
23
|
+
else
|
24
|
+
(per_page + @options[:from].to_i) / per_page
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def previous_page
|
29
|
+
current_page > 1 ? (current_page - 1) : nil
|
30
|
+
end
|
31
|
+
|
32
|
+
def next_page
|
33
|
+
current_page < total_pages ? (current_page + 1) : nil
|
34
|
+
end
|
35
|
+
|
36
|
+
def offset
|
37
|
+
per_page * (current_page - 1)
|
38
|
+
end
|
39
|
+
|
40
|
+
def out_of_bounds?
|
41
|
+
current_page > total_pages
|
42
|
+
end
|
43
|
+
|
44
|
+
# Kaminari support
|
45
|
+
#
|
46
|
+
alias :limit_value :per_page
|
47
|
+
alias :total_count :total_entries
|
48
|
+
alias :num_pages :total_pages
|
49
|
+
alias :offset_value :offset
|
50
|
+
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
54
|
+
end
|
data/lib/tire/search.rb
ADDED
@@ -0,0 +1,188 @@
|
|
1
|
+
module Tire
|
2
|
+
module Search
|
3
|
+
class SearchRequestFailed < StandardError; end
|
4
|
+
|
5
|
+
class Search
|
6
|
+
|
7
|
+
attr_reader :indices, :query, :facets, :filters, :options, :explain, :script_fields
|
8
|
+
|
9
|
+
def initialize(indices=nil, options={}, &block)
|
10
|
+
if indices.is_a?(Hash)
|
11
|
+
set_indices_options(indices)
|
12
|
+
@indices = indices.keys
|
13
|
+
else
|
14
|
+
@indices = Array(indices)
|
15
|
+
end
|
16
|
+
@types = Array(options.delete(:type)).map { |type| Utils.escape(type) }
|
17
|
+
@options = options
|
18
|
+
|
19
|
+
@path = ['/', @indices.join(','), @types.join(','), '_search'].compact.join('/').squeeze('/')
|
20
|
+
|
21
|
+
block.arity < 1 ? instance_eval(&block) : block.call(self) if block_given?
|
22
|
+
end
|
23
|
+
|
24
|
+
|
25
|
+
def set_indices_options(indices)
|
26
|
+
indices.each do |index, index_options|
|
27
|
+
if index_options[:boost]
|
28
|
+
@indices_boost ||= {}
|
29
|
+
@indices_boost[index] = index_options[:boost]
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def results
|
35
|
+
@results || (perform; @results)
|
36
|
+
end
|
37
|
+
|
38
|
+
def response
|
39
|
+
@response || (perform; @response)
|
40
|
+
end
|
41
|
+
|
42
|
+
def json
|
43
|
+
@json || (perform; @json)
|
44
|
+
end
|
45
|
+
|
46
|
+
def url
|
47
|
+
Configuration.url + @path
|
48
|
+
end
|
49
|
+
|
50
|
+
def params
|
51
|
+
@options.empty? ? '' : '?' + @options.to_param
|
52
|
+
end
|
53
|
+
|
54
|
+
def query(&block)
|
55
|
+
@query = Query.new
|
56
|
+
block.arity < 1 ? @query.instance_eval(&block) : block.call(@query)
|
57
|
+
self
|
58
|
+
end
|
59
|
+
|
60
|
+
def sort(&block)
|
61
|
+
@sort = Sort.new(&block).to_ary
|
62
|
+
self
|
63
|
+
end
|
64
|
+
|
65
|
+
def facet(name, options={}, &block)
|
66
|
+
@facets ||= {}
|
67
|
+
@facets.update Facet.new(name, options, &block).to_hash
|
68
|
+
self
|
69
|
+
end
|
70
|
+
|
71
|
+
def filter(type, *options)
|
72
|
+
@filters ||= []
|
73
|
+
@filters << Filter.new(type, *options).to_hash
|
74
|
+
self
|
75
|
+
end
|
76
|
+
|
77
|
+
def script_field(name, options={})
|
78
|
+
@script_fields ||= {}
|
79
|
+
@script_fields.merge! ScriptField.new(name, options).to_hash
|
80
|
+
self
|
81
|
+
end
|
82
|
+
|
83
|
+
def highlight(*args)
|
84
|
+
unless args.empty?
|
85
|
+
@highlight = Highlight.new(*args)
|
86
|
+
self
|
87
|
+
else
|
88
|
+
@highlight
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def from(value)
|
93
|
+
@from = value
|
94
|
+
@options[:from] = value
|
95
|
+
self
|
96
|
+
end
|
97
|
+
|
98
|
+
def size(value)
|
99
|
+
@size = value
|
100
|
+
@options[:size] = value
|
101
|
+
self
|
102
|
+
end
|
103
|
+
|
104
|
+
def fields(*fields)
|
105
|
+
@fields = Array(fields.flatten)
|
106
|
+
self
|
107
|
+
end
|
108
|
+
|
109
|
+
def explain(value)
|
110
|
+
@explain = value
|
111
|
+
self
|
112
|
+
end
|
113
|
+
|
114
|
+
def version(value)
|
115
|
+
@version = value
|
116
|
+
self
|
117
|
+
end
|
118
|
+
|
119
|
+
def perform
|
120
|
+
@response = Configuration.client.get(self.url + self.params, self.to_json)
|
121
|
+
if @response.failure?
|
122
|
+
STDERR.puts "[REQUEST FAILED] #{self.to_curl}\n"
|
123
|
+
raise SearchRequestFailed, @response.to_s
|
124
|
+
end
|
125
|
+
@json = MultiJson.decode(@response.body)
|
126
|
+
@results = Results::Collection.new(@json, @options)
|
127
|
+
return self
|
128
|
+
ensure
|
129
|
+
logged
|
130
|
+
end
|
131
|
+
|
132
|
+
def to_curl
|
133
|
+
%Q|curl -X GET "#{url}#{params.empty? ? '?' : params.to_s + '&'}pretty=true" -d '#{to_json}'|
|
134
|
+
end
|
135
|
+
|
136
|
+
def to_hash
|
137
|
+
@options.delete(:payload) || begin
|
138
|
+
request = {}
|
139
|
+
request.update( { :indices_boost => @indices_boost } ) if @indices_boost
|
140
|
+
request.update( { :query => @query.to_hash } ) if @query
|
141
|
+
request.update( { :sort => @sort.to_ary } ) if @sort
|
142
|
+
request.update( { :facets => @facets.to_hash } ) if @facets
|
143
|
+
request.update( { :filter => @filters.first.to_hash } ) if @filters && @filters.size == 1
|
144
|
+
request.update( { :filter => { :and => @filters.map {|filter| filter.to_hash} } } ) if @filters && @filters.size > 1
|
145
|
+
request.update( { :highlight => @highlight.to_hash } ) if @highlight
|
146
|
+
request.update( { :size => @size } ) if @size
|
147
|
+
request.update( { :from => @from } ) if @from
|
148
|
+
request.update( { :fields => @fields } ) if @fields
|
149
|
+
request.update( { :script_fields => @script_fields } ) if @script_fields
|
150
|
+
request.update( { :version => @version } ) if @version
|
151
|
+
request.update( { :explain => @explain } ) if @explain
|
152
|
+
request
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
def to_json
|
157
|
+
payload = to_hash
|
158
|
+
# TODO: Remove when deprecated interface is removed
|
159
|
+
payload.is_a?(String) ? payload : payload.to_json
|
160
|
+
end
|
161
|
+
|
162
|
+
def logged(error=nil)
|
163
|
+
if Configuration.logger
|
164
|
+
|
165
|
+
Configuration.logger.log_request '_search', indices, to_curl
|
166
|
+
|
167
|
+
took = @json['took'] rescue nil
|
168
|
+
code = @response.code rescue nil
|
169
|
+
|
170
|
+
if Configuration.logger.level.to_s == 'debug'
|
171
|
+
# FIXME: Depends on RestClient implementation
|
172
|
+
body = if @json
|
173
|
+
defined?(Yajl) ? Yajl::Encoder.encode(@json, :pretty => true) : MultiJson.encode(@json)
|
174
|
+
else
|
175
|
+
@response.body rescue nil
|
176
|
+
end
|
177
|
+
else
|
178
|
+
body = ''
|
179
|
+
end
|
180
|
+
|
181
|
+
Configuration.logger.log_response code || 'N/A', took || 'N/A', body || 'N/A'
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
end
|
186
|
+
|
187
|
+
end
|
188
|
+
end
|