tire 0.6.1 → 0.6.2
Sign up to get free protection for your applications and to get access to all the features.
- data/README.markdown +2 -0
- data/examples/tire-dsl.rb +150 -0
- data/lib/tire.rb +4 -0
- data/lib/tire/dsl.rb +4 -0
- data/lib/tire/model/search.rb +5 -3
- data/lib/tire/model/suggest.rb +34 -0
- data/lib/tire/results/collection.rb +9 -8
- data/lib/tire/results/suggestions.rb +68 -0
- data/lib/tire/search.rb +7 -0
- data/lib/tire/suggest.rb +108 -0
- data/lib/tire/suggest/suggestion.rb +105 -0
- data/lib/tire/version.rb +4 -8
- data/test/integration/bulk_test.rb +1 -1
- data/test/integration/search_response_test.rb +70 -0
- data/test/integration/suggest_test.rb +129 -0
- data/test/models/active_record_models.rb +13 -1
- data/test/unit/search_test.rb +27 -0
- data/test/unit/suggest_test.rb +82 -0
- metadata +16 -8
data/README.markdown
CHANGED
@@ -820,3 +820,5 @@ You can send feedback via [e-mail](mailto:karmi@karmi.cz) or via [Github Issues]
|
|
820
820
|
-----
|
821
821
|
|
822
822
|
[Karel Minarik](http://karmi.cz) and [contributors](http://github.com/karmi/tire/contributors)
|
823
|
+
|
824
|
+
![](https://ga-beacon.appspot.com/UA-46901128-1/karmi/retire?pixel)
|
data/examples/tire-dsl.rb
CHANGED
@@ -816,6 +816,156 @@ s = Tire.search 'articles' do
|
|
816
816
|
highlight :title, :body, :options => { :tag => '<strong class="highlight">' }
|
817
817
|
end
|
818
818
|
|
819
|
+
#### Suggest
|
820
|
+
|
821
|
+
#
|
822
|
+
# _Elasticsearch_
|
823
|
+
# [suggest](http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-suggesters.html)
|
824
|
+
# feature suggests similar terms based on user input.
|
825
|
+
# You can specify either the `term` or `phrase` suggester in the Tire DSL, or
|
826
|
+
# use the `completion` suggester to get fast completions of user inputs, suitable
|
827
|
+
# for auto-complete and instant search features.
|
828
|
+
|
829
|
+
# Suggestion API is available either as standalone method or part of the search request.
|
830
|
+
|
831
|
+
# To get search suggestions while doing a search, call the suggest API
|
832
|
+
#
|
833
|
+
s = Tire.search 'articles' do
|
834
|
+
|
835
|
+
# To define a suggest using the term suggester, first provide a custom name for the suggest.
|
836
|
+
#
|
837
|
+
suggest :suggest_title do
|
838
|
+
# Specify the input text.
|
839
|
+
#
|
840
|
+
text 'thrree blind mice'
|
841
|
+
# Then, define the field you want to use for suggestions and any options.
|
842
|
+
#
|
843
|
+
term :title, size: 3, sort: 'frequency'
|
844
|
+
end
|
845
|
+
|
846
|
+
# To define a suggest using the `phrase` suggest, use a different name.
|
847
|
+
suggest :phrase_suggest_title do
|
848
|
+
# Specify the text input text.
|
849
|
+
#
|
850
|
+
text 'thrree blind mice'
|
851
|
+
# Again, define the field you want to use for suggestions and any options.
|
852
|
+
#
|
853
|
+
phrase :title, size: 3 do
|
854
|
+
# Optinally, configure the `smoothing` option...
|
855
|
+
#
|
856
|
+
smoothing :stupid_backoff, discount: 0.5
|
857
|
+
|
858
|
+
# ...or the `generator` option.
|
859
|
+
generator :title, min_word_len: 1
|
860
|
+
end
|
861
|
+
end
|
862
|
+
end
|
863
|
+
|
864
|
+
# The results will be available in the `suggestions` property (which is iterable)
|
865
|
+
#
|
866
|
+
s.results.suggestions.each do |name, options|
|
867
|
+
puts "Suggestion returned for #{name}:\n"
|
868
|
+
options.each do |option|
|
869
|
+
puts "* Raw result: #{option}"
|
870
|
+
end
|
871
|
+
end
|
872
|
+
|
873
|
+
# You can also use helper methods available in suggestions results to get only
|
874
|
+
# the suggested terms or phrases.
|
875
|
+
#
|
876
|
+
puts "Available corrections for suggest_title: #{s.results.suggestions.texts(:suggest_title).join(', ')}"
|
877
|
+
|
878
|
+
# You can use the standalone API to achieve the same result:
|
879
|
+
#
|
880
|
+
s = Tire.suggest('articles') do
|
881
|
+
|
882
|
+
# Notice that for standalone API, the block method is `suggestion` rather than `suggest`:
|
883
|
+
#
|
884
|
+
suggestion :term_suggest do
|
885
|
+
text 'thrree'
|
886
|
+
term :title, size: 3, sort: 'frequency'
|
887
|
+
end
|
888
|
+
|
889
|
+
end
|
890
|
+
|
891
|
+
# You'll get the same object as above but as top level object
|
892
|
+
#
|
893
|
+
puts "Available corrections: #{s.results.texts.join(', ')}"
|
894
|
+
|
895
|
+
#### Completion
|
896
|
+
|
897
|
+
# In order to use _Elasticsearch_ completion you'll need to update your mappings to provide a field
|
898
|
+
# with completion type. The example is adapted from this
|
899
|
+
# [blog post](http://www.elasticsearch.org/blog/you-complete-me/).
|
900
|
+
#
|
901
|
+
index = Tire.index('hotels') do
|
902
|
+
delete
|
903
|
+
|
904
|
+
# Notice the type completion for the field _name_suggest_:
|
905
|
+
#
|
906
|
+
create :mappings => {
|
907
|
+
:hotel => {
|
908
|
+
:properties => {
|
909
|
+
:name => {:type => 'string'},
|
910
|
+
:city => {:type => 'string'},
|
911
|
+
:name_suggest => {:type => 'completion'}
|
912
|
+
}
|
913
|
+
}
|
914
|
+
}
|
915
|
+
|
916
|
+
# Let's add some documents into this index:
|
917
|
+
#
|
918
|
+
import([
|
919
|
+
{:id => '1', :type => 'hotel', :name => 'Mercure Hotel Munich', :city => 'Munich', :name_suggest => 'Mercure Hotel Munich'},
|
920
|
+
{:id => '2', :type => 'hotel', :name => 'Hotel Monaco', :city => 'Munich', :name_suggest => 'Hotel Monaco'},
|
921
|
+
])
|
922
|
+
refresh
|
923
|
+
|
924
|
+
end
|
925
|
+
|
926
|
+
# We can ask for all hotels starting with a given prefix (such as "m") with this query:
|
927
|
+
#
|
928
|
+
s = Tire.suggest('hotels') do
|
929
|
+
suggestion 'complete' do
|
930
|
+
text 'm'
|
931
|
+
completion 'name_suggest'
|
932
|
+
end
|
933
|
+
end
|
934
|
+
|
935
|
+
# And retrieve results as above with the same object:
|
936
|
+
#
|
937
|
+
puts "There are #{s.results.texts.size} hotels starting with m:"
|
938
|
+
s.results.texts.each do |hotel|
|
939
|
+
puts "* #{hotel}"
|
940
|
+
end
|
941
|
+
|
942
|
+
# You can use some advanced features of completion such as multiple inputs and unified output for
|
943
|
+
# the same document.
|
944
|
+
|
945
|
+
# If you add a document which has inputs and output values for the suggest field:
|
946
|
+
#
|
947
|
+
index.store({:id => '1', :type => 'hotel', :name => 'Mercure Hotel Munich', :city => 'Munich',
|
948
|
+
:name_suggest => {:input => ['Mercure Hotel Munich', 'Mercure Munich'], :output => 'Hotel Mercure'}})
|
949
|
+
index.store({:id => '2', :type => 'hotel', :name => 'Hotel Monaco', :city => 'Munich',
|
950
|
+
:name_suggest => {:input => ['Monaco Munich', 'Hotel Monaco'], :output => 'Hotel Monaco'}})
|
951
|
+
index.refresh
|
952
|
+
|
953
|
+
# ... a completion request with the same input as above ...
|
954
|
+
#
|
955
|
+
s = Tire.suggest('hotels') do
|
956
|
+
suggestion 'complete' do
|
957
|
+
text 'm'
|
958
|
+
completion 'name_suggest'
|
959
|
+
end
|
960
|
+
end
|
961
|
+
|
962
|
+
# ... will match multiple inputs for the same document and return unified output in results:
|
963
|
+
#
|
964
|
+
puts "There are #{s.results.texts.size} hotels starting with m:"
|
965
|
+
s.results.texts.each do |hotel|
|
966
|
+
puts "* #{hotel}"
|
967
|
+
end
|
968
|
+
|
819
969
|
#### Percolation
|
820
970
|
|
821
971
|
# _Elasticsearch_ comes with one very interesting, and rather unique feature:
|
data/lib/tire.rb
CHANGED
@@ -30,12 +30,15 @@ require 'tire/search/filter'
|
|
30
30
|
require 'tire/search/highlight'
|
31
31
|
require 'tire/search/scan'
|
32
32
|
require 'tire/search/script_field'
|
33
|
+
require 'tire/suggest'
|
34
|
+
require 'tire/suggest/suggestion'
|
33
35
|
require 'tire/delete_by_query'
|
34
36
|
require 'tire/multi_search'
|
35
37
|
require 'tire/count'
|
36
38
|
require 'tire/results/pagination'
|
37
39
|
require 'tire/results/collection'
|
38
40
|
require 'tire/results/item'
|
41
|
+
require 'tire/results/suggestions'
|
39
42
|
require 'tire/index'
|
40
43
|
require 'tire/alias'
|
41
44
|
require 'tire/dsl'
|
@@ -44,6 +47,7 @@ require 'tire/model/callbacks'
|
|
44
47
|
require 'tire/model/percolate'
|
45
48
|
require 'tire/model/indexing'
|
46
49
|
require 'tire/model/import'
|
50
|
+
require 'tire/model/suggest'
|
47
51
|
require 'tire/model/search'
|
48
52
|
require 'tire/model/persistence/finders'
|
49
53
|
require 'tire/model/persistence/attributes'
|
data/lib/tire/dsl.rb
CHANGED
data/lib/tire/model/search.rb
CHANGED
@@ -145,8 +145,8 @@ module Tire
|
|
145
145
|
if instance.destroyed?
|
146
146
|
index.remove instance
|
147
147
|
else
|
148
|
-
response = index.store( instance, {:percolate => percolator} )
|
149
|
-
instance.tire.matches = response['matches'] if instance.tire.respond_to?(:matches=)
|
148
|
+
@response = index.store( instance, {:percolate => percolator} )
|
149
|
+
instance.tire.matches = @response['matches'] if instance.tire.respond_to?(:matches=)
|
150
150
|
self
|
151
151
|
end
|
152
152
|
end
|
@@ -225,6 +225,7 @@ module Tire
|
|
225
225
|
include Tire::Model::Import::ClassMethods
|
226
226
|
include Tire::Model::Indexing::ClassMethods
|
227
227
|
include Tire::Model::Percolate::ClassMethods
|
228
|
+
include Tire::Model::Suggest::ClassMethods
|
228
229
|
include ClassMethods
|
229
230
|
|
230
231
|
INTERFACE = public_instance_methods.map(&:to_sym) - Object.public_instance_methods.map(&:to_sym)
|
@@ -245,9 +246,10 @@ module Tire
|
|
245
246
|
|
246
247
|
INTERFACE = public_instance_methods.map(&:to_sym) - Object.public_instance_methods.map(&:to_sym)
|
247
248
|
|
248
|
-
attr_reader :instance
|
249
|
+
attr_reader :instance, :response
|
249
250
|
def initialize(instance)
|
250
251
|
@instance = instance
|
252
|
+
@response = {}
|
251
253
|
end
|
252
254
|
end
|
253
255
|
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module Tire
|
2
|
+
module Model
|
3
|
+
module Suggest
|
4
|
+
module ClassMethods
|
5
|
+
def suggest(*args, &block)
|
6
|
+
default_options = {:type => document_type, :index => index.name}
|
7
|
+
|
8
|
+
if block_given?
|
9
|
+
options = args.shift || {}
|
10
|
+
else
|
11
|
+
query, options = args
|
12
|
+
options ||= {}
|
13
|
+
end
|
14
|
+
|
15
|
+
options = default_options.update(options)
|
16
|
+
|
17
|
+
s = Tire::Suggest::Suggest.new(options.delete(:index), options)
|
18
|
+
|
19
|
+
if block_given?
|
20
|
+
block.arity < 1 ? s.instance_eval(&block) : block.call(s)
|
21
|
+
else
|
22
|
+
s.suggestion 'default_suggestion' do
|
23
|
+
text query
|
24
|
+
completion 'suggest'
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
s.results
|
29
|
+
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -5,16 +5,17 @@ module Tire
|
|
5
5
|
include Enumerable
|
6
6
|
include Pagination
|
7
7
|
|
8
|
-
attr_reader :time, :total, :options, :facets, :max_score
|
8
|
+
attr_reader :time, :total, :options, :facets, :max_score, :suggestions
|
9
9
|
|
10
10
|
def initialize(response, options={})
|
11
|
-
@response
|
12
|
-
@options
|
13
|
-
@time
|
14
|
-
@total
|
15
|
-
@facets
|
16
|
-
@
|
17
|
-
@
|
11
|
+
@response = response
|
12
|
+
@options = options
|
13
|
+
@time = response['took'].to_i
|
14
|
+
@total = response['hits']['total'].to_i rescue nil
|
15
|
+
@facets = response['facets']
|
16
|
+
@suggestions = Suggestions.new(response['suggest']) if response['suggest']
|
17
|
+
@max_score = response['hits']['max_score'].to_f rescue nil
|
18
|
+
@wrapper = options[:wrapper] || Configuration.wrapper
|
18
19
|
end
|
19
20
|
|
20
21
|
def results
|
@@ -0,0 +1,68 @@
|
|
1
|
+
module Tire
|
2
|
+
module Results
|
3
|
+
|
4
|
+
class Suggestions
|
5
|
+
include Enumerable
|
6
|
+
|
7
|
+
def initialize(response, options={})
|
8
|
+
@response = response
|
9
|
+
@options = options
|
10
|
+
@shards_info ||= @response.delete '_shards'
|
11
|
+
@keys ||= @response.keys
|
12
|
+
end
|
13
|
+
|
14
|
+
def results
|
15
|
+
return [] if failure?
|
16
|
+
@results ||= @response
|
17
|
+
end
|
18
|
+
|
19
|
+
def keys
|
20
|
+
@keys
|
21
|
+
end
|
22
|
+
|
23
|
+
def each(&block)
|
24
|
+
results.each(&block)
|
25
|
+
end
|
26
|
+
|
27
|
+
def size
|
28
|
+
results.size
|
29
|
+
end
|
30
|
+
|
31
|
+
def options(suggestion=:all)
|
32
|
+
if suggestion == :all
|
33
|
+
results.map {|k,v| v.map{|s| s['options']}}.flatten
|
34
|
+
else
|
35
|
+
results[suggestion.to_s].map{|s| s['options']}.flatten
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def texts(suggestion=:all)
|
40
|
+
if suggestion == :all
|
41
|
+
results.map {|k,v| v.map{|s| s['options'].map {|o| o['text']}}}.flatten
|
42
|
+
else
|
43
|
+
results[suggestion.to_s].map{|s| s['options'].map {|o| o['text']}}.flatten
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def payloads(suggestion=:all)
|
48
|
+
if suggestion == :all
|
49
|
+
results.map {|k,v| v.map{|s| s['options'].map {|o| o['payload']}}}.flatten
|
50
|
+
else
|
51
|
+
results[suggestion.to_s].map{|s| s['options'].map {|o| o['payload']}}.flatten
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def error
|
56
|
+
@response['error']
|
57
|
+
end
|
58
|
+
|
59
|
+
def success?
|
60
|
+
error.to_s.empty?
|
61
|
+
end
|
62
|
+
|
63
|
+
def failure?
|
64
|
+
! success?
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
data/lib/tire/search.rb
CHANGED
@@ -81,6 +81,12 @@ module Tire
|
|
81
81
|
self
|
82
82
|
end
|
83
83
|
|
84
|
+
def suggest(name, &block)
|
85
|
+
@suggest ||= {}
|
86
|
+
@suggest.update Tire::Suggest::Suggestion.new(name, &block).to_hash
|
87
|
+
self
|
88
|
+
end
|
89
|
+
|
84
90
|
def highlight(*args)
|
85
91
|
unless args.empty?
|
86
92
|
@highlight = Highlight.new(*args)
|
@@ -160,6 +166,7 @@ module Tire
|
|
160
166
|
request.update( { :filter => @filters.first.to_hash } ) if @filters && @filters.size == 1
|
161
167
|
request.update( { :filter => { :and => @filters.map {|filter| filter.to_hash} } } ) if @filters && @filters.size > 1
|
162
168
|
request.update( { :highlight => @highlight.to_hash } ) if @highlight
|
169
|
+
request.update( { :suggest => @suggest.to_hash } ) if @suggest
|
163
170
|
request.update( { :size => @size } ) if @size
|
164
171
|
request.update( { :from => @from } ) if @from
|
165
172
|
request.update( { :fields => @fields } ) if @fields
|
data/lib/tire/suggest.rb
ADDED
@@ -0,0 +1,108 @@
|
|
1
|
+
module Tire
|
2
|
+
module Suggest
|
3
|
+
class SuggestRequestFailed < StandardError; end
|
4
|
+
|
5
|
+
class Suggest
|
6
|
+
|
7
|
+
attr_reader :indices, :suggestion, :options
|
8
|
+
|
9
|
+
def initialize(indices=nil, options={}, &block)
|
10
|
+
if indices.is_a?(Hash)
|
11
|
+
@indices = indices.keys
|
12
|
+
else
|
13
|
+
@indices = Array(indices)
|
14
|
+
end
|
15
|
+
|
16
|
+
#TODO no options for now
|
17
|
+
@options = options
|
18
|
+
|
19
|
+
@path = ['/', @indices.join(','), '_suggest'].compact.join('/').squeeze('/')
|
20
|
+
|
21
|
+
block.arity < 1 ? instance_eval(&block) : block.call(self) if block_given?
|
22
|
+
end
|
23
|
+
|
24
|
+
def suggestion(name, &block)
|
25
|
+
@suggestion = Suggestion.new(name, &block)
|
26
|
+
self
|
27
|
+
end
|
28
|
+
|
29
|
+
def multi(&block)
|
30
|
+
@suggestion = MultiSuggestion.new(&block)
|
31
|
+
self
|
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 = @options.except(:wrapper, :payload, :load)
|
52
|
+
options.empty? ? '' : '?' + options.to_param
|
53
|
+
end
|
54
|
+
|
55
|
+
def perform
|
56
|
+
@response = Configuration.client.get(self.url + self.params, self.to_json)
|
57
|
+
if @response.failure?
|
58
|
+
STDERR.puts "[REQUEST FAILED] #{self.to_curl}\n"
|
59
|
+
raise Tire::Search::SearchRequestFailed, @response.to_s
|
60
|
+
end
|
61
|
+
@json = MultiJson.decode(@response.body)
|
62
|
+
@results = Results::Suggestions.new(@json, @options)
|
63
|
+
return self
|
64
|
+
ensure
|
65
|
+
logged
|
66
|
+
end
|
67
|
+
|
68
|
+
def to_curl
|
69
|
+
to_json_escaped = to_json.gsub("'",'\u0027')
|
70
|
+
%Q|curl -X GET '#{url}#{params.empty? ? '?' : params.to_s + '&'}pretty' -d '#{to_json_escaped}'|
|
71
|
+
end
|
72
|
+
|
73
|
+
def to_hash
|
74
|
+
request = {}
|
75
|
+
request.update( @suggestion.to_hash )
|
76
|
+
request
|
77
|
+
end
|
78
|
+
|
79
|
+
def to_json(options={})
|
80
|
+
payload = to_hash
|
81
|
+
MultiJson.encode(payload, :pretty => Configuration.pretty)
|
82
|
+
end
|
83
|
+
|
84
|
+
def logged(endpoint='_search')
|
85
|
+
if Configuration.logger
|
86
|
+
|
87
|
+
Configuration.logger.log_request endpoint, indices, to_curl
|
88
|
+
|
89
|
+
took = @json['took'] rescue nil
|
90
|
+
code = @response.code rescue nil
|
91
|
+
|
92
|
+
if Configuration.logger.level.to_s == 'debug'
|
93
|
+
body = if @json
|
94
|
+
MultiJson.encode( @json, :pretty => Configuration.pretty)
|
95
|
+
else
|
96
|
+
MultiJson.encode( MultiJson.load(@response.body), :pretty => Configuration.pretty) rescue ''
|
97
|
+
end
|
98
|
+
else
|
99
|
+
body = ''
|
100
|
+
end
|
101
|
+
|
102
|
+
Configuration.logger.log_response code || 'N/A', took || 'N/A', body || 'N/A'
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
@@ -0,0 +1,105 @@
|
|
1
|
+
module Tire
|
2
|
+
module Suggest
|
3
|
+
|
4
|
+
class Suggestion
|
5
|
+
attr_accessor :value, :name
|
6
|
+
|
7
|
+
def initialize(name, &block)
|
8
|
+
@name = name
|
9
|
+
@value = {}
|
10
|
+
block.arity < 1 ? self.instance_eval(&block) : block.call(self) if block_given?
|
11
|
+
end
|
12
|
+
|
13
|
+
def text(value)
|
14
|
+
@value[:text] = value
|
15
|
+
self
|
16
|
+
end
|
17
|
+
|
18
|
+
def completion(value, options={})
|
19
|
+
@value[:completion] = {:field => value}.update(options)
|
20
|
+
self
|
21
|
+
end
|
22
|
+
|
23
|
+
def term(value, options={})
|
24
|
+
@value[:term] = { :field => value }.update(options)
|
25
|
+
self
|
26
|
+
end
|
27
|
+
|
28
|
+
def phrase(field, options={}, &block)
|
29
|
+
@value[:phrase] = PhraseSuggester.new(field, options, &block).to_hash
|
30
|
+
self
|
31
|
+
end
|
32
|
+
|
33
|
+
def to_hash
|
34
|
+
{@name.to_sym => @value}
|
35
|
+
end
|
36
|
+
|
37
|
+
def to_json(options={})
|
38
|
+
to_hash.to_json
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
|
43
|
+
# Used to generate phrase suggestions
|
44
|
+
class PhraseSuggester
|
45
|
+
|
46
|
+
def initialize(field, options={}, &block)
|
47
|
+
@options = options
|
48
|
+
@value = { :field => field }
|
49
|
+
block.arity < 1 ? self.instance_eval(&block) : block.call(self) if block_given?
|
50
|
+
end
|
51
|
+
|
52
|
+
def generator(field, options={})
|
53
|
+
@generators ||= []
|
54
|
+
@generators << { :field => field }.update(options).to_hash
|
55
|
+
self
|
56
|
+
end
|
57
|
+
|
58
|
+
def smoothing(type, options={})
|
59
|
+
@value[:smoothing] = { type => options }
|
60
|
+
end
|
61
|
+
|
62
|
+
def to_json(options={})
|
63
|
+
to_hash.to_json
|
64
|
+
end
|
65
|
+
|
66
|
+
def to_hash
|
67
|
+
@value.update(@options)
|
68
|
+
@value.update( { :direct_generator => @generators } ) if @generators && @generators.size > 0
|
69
|
+
|
70
|
+
@value
|
71
|
+
end
|
72
|
+
|
73
|
+
end
|
74
|
+
|
75
|
+
class MultiSuggestion
|
76
|
+
attr_accessor :suggestions
|
77
|
+
|
78
|
+
def initialize(&block)
|
79
|
+
@value = {}
|
80
|
+
block.arity < 1 ? self.instance_eval(&block) : block.call(self) if block_given?
|
81
|
+
end
|
82
|
+
|
83
|
+
def text(value)
|
84
|
+
@global_text = value
|
85
|
+
self
|
86
|
+
end
|
87
|
+
|
88
|
+
def suggestion(name, &block)
|
89
|
+
@suggestions ||= {}
|
90
|
+
@suggestions.update Suggestion.new(name, &block).to_hash
|
91
|
+
self
|
92
|
+
end
|
93
|
+
|
94
|
+
def to_hash
|
95
|
+
@value.update @suggestions
|
96
|
+
@value[:text] = @global_text if @global_text
|
97
|
+
@value
|
98
|
+
end
|
99
|
+
|
100
|
+
def to_json(options={})
|
101
|
+
to_hash.to_json
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
data/lib/tire/version.rb
CHANGED
@@ -1,15 +1,11 @@
|
|
1
1
|
module Tire
|
2
|
-
VERSION = "0.6.
|
2
|
+
VERSION = "0.6.2"
|
3
3
|
|
4
4
|
CHANGELOG =<<-END
|
5
5
|
IMPORTANT CHANGES LATELY:
|
6
6
|
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
* Added the information about the gem "retirement" and other documentation improvements
|
11
|
-
* Fixed errors due to NewRelic's patching of Curl
|
12
|
-
* [ACTIVEMODEL] Use Object#id#to_s in `get_id_from_document`
|
13
|
-
* Added support for "Delete By Query" API
|
7
|
+
19e524c [ACTIVEMODEL] Exposed the response from `MyModel#.update_index` as the `response` method on return value
|
8
|
+
bfcde21 [#916] Added support for the Suggest API (@marc-villanueva)
|
9
|
+
710451d [#857] Added support for the Suggest API (@fbatista)
|
14
10
|
END
|
15
11
|
end
|
@@ -61,7 +61,7 @@ module Tire
|
|
61
61
|
end
|
62
62
|
@index.bulk_update documents, refresh: true
|
63
63
|
|
64
|
-
documents = Tire.search('bulk-test') { query {all} }.results.to_a
|
64
|
+
documents = Tire.search('bulk-test') { query {all} }.results.to_a.sort { |a,b| a.id <=> b.id }
|
65
65
|
assert_equal 'one-updated', documents[0][:title]
|
66
66
|
assert_equal 'two-updated', documents[1][:title]
|
67
67
|
assert_equal 'three-updated', documents[2][:title]
|
@@ -0,0 +1,70 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
#require File.expand_path('../../models/supermodel_article', __FILE__)
|
3
|
+
|
4
|
+
module Tire
|
5
|
+
|
6
|
+
class SearchResponseIntegrationTest < Test::Unit::TestCase
|
7
|
+
include Test::Integration
|
8
|
+
|
9
|
+
class ::ActiveModelArticleWithTitle < ActiveModelArticleWithCallbacks
|
10
|
+
mapping do
|
11
|
+
indexes :title, type: :string
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
class ::ActiveModelArticleWithMalformedTitle < ActiveModelArticleWithCallbacks
|
16
|
+
mapping do
|
17
|
+
indexes :title, type: :string
|
18
|
+
end
|
19
|
+
|
20
|
+
def to_indexed_json
|
21
|
+
json = JSON.parse(super)
|
22
|
+
json["title"] = { key: "value" }
|
23
|
+
json.to_json
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def setup
|
28
|
+
super
|
29
|
+
ActiveModelArticleWithTitle.index.delete
|
30
|
+
ActiveModelArticleWithMalformedTitle.index.delete
|
31
|
+
end
|
32
|
+
|
33
|
+
def teardown
|
34
|
+
super
|
35
|
+
ActiveModelArticleWithTitle.index.delete
|
36
|
+
ActiveModelArticleWithMalformedTitle.index.delete
|
37
|
+
end
|
38
|
+
|
39
|
+
context "Successful index update" do
|
40
|
+
|
41
|
+
setup do
|
42
|
+
@model = ActiveModelArticleWithTitle.new \
|
43
|
+
:id => 1,
|
44
|
+
:title => 'Test article',
|
45
|
+
:content => 'Lorem Ipsum. Dolor Sit Amet.'
|
46
|
+
@response = @model.update_index
|
47
|
+
end
|
48
|
+
|
49
|
+
should "expose the index response on successful update" do
|
50
|
+
assert_equal @response.response["ok"], true
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
54
|
+
|
55
|
+
context "Unsuccessful index update" do
|
56
|
+
setup do
|
57
|
+
ActiveModelArticleWithMalformedTitle.create_elasticsearch_index
|
58
|
+
@model = ActiveModelArticleWithMalformedTitle.new \
|
59
|
+
:id => 1,
|
60
|
+
:title => 'Test article',
|
61
|
+
:content => 'Lorem Ipsum. Dolor Sit Amet.'
|
62
|
+
@response = @model.update_index
|
63
|
+
end
|
64
|
+
|
65
|
+
should "expose the index response on update error" do
|
66
|
+
assert_equal @response.response["status"], 400
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,129 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
module Tire
|
4
|
+
|
5
|
+
class SuggestIntegrationTest < Test::Unit::TestCase
|
6
|
+
include Test::Integration
|
7
|
+
|
8
|
+
context 'Search Suggest' do
|
9
|
+
|
10
|
+
should 'add suggestions field to the results using the term suggester' do
|
11
|
+
# Tire::Configuration.logger STDERR, :level => 'debug'
|
12
|
+
s = Tire.search('articles-test') do
|
13
|
+
suggest :term_suggest1 do
|
14
|
+
text 'thrree'
|
15
|
+
term :title
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
assert_equal 1, s.results.suggestions.size
|
20
|
+
assert_equal 'three', s.results.suggestions.texts.first
|
21
|
+
end
|
22
|
+
|
23
|
+
should 'add suggestions field to the results using the phrase suggester' do
|
24
|
+
# Tire::Configuration.logger STDERR, :level => 'debug'
|
25
|
+
s = Tire.search('articles-test') do
|
26
|
+
suggest :phrase_suggest1 do
|
27
|
+
text 'thrree'
|
28
|
+
phrase :title
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
assert_equal 1, s.results.suggestions.size
|
33
|
+
assert_equal 'three', s.results.suggestions.texts.first
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
37
|
+
|
38
|
+
context 'Standalone term and phrase suggest' do
|
39
|
+
|
40
|
+
should 'return term suggestions when used with standalone api' do
|
41
|
+
# Tire::Configuration.logger STDERR, :level => 'debug'
|
42
|
+
s = Tire.suggest('articles-test') do
|
43
|
+
suggestion :term_suggest do
|
44
|
+
text 'thrree'
|
45
|
+
term :title
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
assert_equal 1, s.results.texts.size
|
50
|
+
assert_equal 'three', s.results.texts.first
|
51
|
+
end
|
52
|
+
|
53
|
+
should 'return phrase suggestions when used with standalone api' do
|
54
|
+
# Tire::Configuration.logger STDERR, :level => 'debug'
|
55
|
+
s = Tire.suggest('articles-test') do
|
56
|
+
suggestion :prhase_suggest do
|
57
|
+
text 'thrree'
|
58
|
+
phrase :title
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
assert_equal 1, s.results.texts.size
|
63
|
+
assert_equal 'three', s.results.texts.first
|
64
|
+
end
|
65
|
+
|
66
|
+
end
|
67
|
+
|
68
|
+
context 'Standalone suggest' do
|
69
|
+
setup do
|
70
|
+
Tire.index('suggest-test') do
|
71
|
+
delete
|
72
|
+
create :mappings => {
|
73
|
+
:article => {
|
74
|
+
:properties => {
|
75
|
+
:title => {:type => 'string', :analyzer => 'simple'},
|
76
|
+
:title_suggest => {:type => 'completion', :analyzer => 'simple'},
|
77
|
+
}
|
78
|
+
}
|
79
|
+
}
|
80
|
+
import([
|
81
|
+
{:id => '1', :type => 'article', :title => 'one', :title_suggest => 'one'},
|
82
|
+
# this document has multiple inputs for completion field and a specified output
|
83
|
+
{:id => '2', :type => 'article', :title => 'two', :title_suggest => {:input => %w(two dos due), :output => 'Two[2]'}},
|
84
|
+
{:id => '3', :type => 'article', :title => 'three', :title_suggest => 'three'}
|
85
|
+
])
|
86
|
+
refresh
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
teardown do
|
91
|
+
Tire.index('suggest-test') { delete }
|
92
|
+
end
|
93
|
+
|
94
|
+
should 'return completion suggestions when used with standalone api' do
|
95
|
+
# Tire::Configuration.logger STDERR, :level => 'debug'
|
96
|
+
s = Tire.suggest('suggest-test') do
|
97
|
+
suggestion 'complete' do
|
98
|
+
text 't'
|
99
|
+
completion 'title_suggest'
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
assert_equal 2, s.results.texts.size
|
104
|
+
assert_equal %w(Two[2] three), s.results.texts
|
105
|
+
end
|
106
|
+
|
107
|
+
should 'allow multiple completion requests in the same request' do
|
108
|
+
# Tire::Configuration.logger STDERR, :level => 'debug'
|
109
|
+
s = Tire.suggest('suggest-test') do
|
110
|
+
multi do
|
111
|
+
suggestion 'foo' do
|
112
|
+
text 'o'
|
113
|
+
completion 'title_suggest'
|
114
|
+
end
|
115
|
+
suggestion 'bar' do
|
116
|
+
text 'd'
|
117
|
+
completion 'title_suggest'
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
assert_equal 2, s.results.size
|
123
|
+
assert_equal %w(one), s.results.texts(:foo)
|
124
|
+
assert_equal %w(Two[2]), s.results.texts(:bar)
|
125
|
+
end
|
126
|
+
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
@@ -11,7 +11,10 @@ class ActiveRecordArticle < ActiveRecord::Base
|
|
11
11
|
mapping do
|
12
12
|
indexes :title, :type => 'string', :boost => 10, :analyzer => 'snowball'
|
13
13
|
indexes :created_at, :type => 'date'
|
14
|
-
|
14
|
+
indexes :suggest, :type => :completion,
|
15
|
+
:index_analyzer => :simple,
|
16
|
+
:search_analyzer => :simple,
|
17
|
+
:payloads => true
|
15
18
|
indexes :comments do
|
16
19
|
indexes :author
|
17
20
|
indexes :body
|
@@ -23,6 +26,7 @@ class ActiveRecordArticle < ActiveRecord::Base
|
|
23
26
|
{
|
24
27
|
:title => title,
|
25
28
|
:length => length,
|
29
|
+
:suggest => suggest,
|
26
30
|
|
27
31
|
:comments => comments.map { |c| { :_type => 'active_record_comment',
|
28
32
|
:_id => c.id,
|
@@ -32,6 +36,14 @@ class ActiveRecordArticle < ActiveRecord::Base
|
|
32
36
|
}.to_json
|
33
37
|
end
|
34
38
|
|
39
|
+
def suggest
|
40
|
+
{
|
41
|
+
input: self.title.split(/\W/).reject(&:empty?),
|
42
|
+
output: self.title,
|
43
|
+
payload: { length: length, comment_authors: comment_authors}
|
44
|
+
}
|
45
|
+
end
|
46
|
+
|
35
47
|
def length
|
36
48
|
title.length
|
37
49
|
end
|
data/test/unit/search_test.rb
CHANGED
@@ -339,6 +339,33 @@ module Tire
|
|
339
339
|
|
340
340
|
end
|
341
341
|
|
342
|
+
context "suggest" do
|
343
|
+
should "allow to specify term suggest" do
|
344
|
+
s = Search::Search.new('index') do
|
345
|
+
suggest :suggest_name do
|
346
|
+
text 'text'
|
347
|
+
term :candidate_field
|
348
|
+
end
|
349
|
+
end
|
350
|
+
|
351
|
+
hash = MultiJson.decode( s.to_json )
|
352
|
+
assert_not_nil hash['suggest']
|
353
|
+
end
|
354
|
+
|
355
|
+
should "allow to specify phrase suggest" do
|
356
|
+
s = Search::Search.new('index') do
|
357
|
+
suggest :suggest_name do
|
358
|
+
text 'text'
|
359
|
+
phrase :candidate_field
|
360
|
+
end
|
361
|
+
end
|
362
|
+
|
363
|
+
hash = MultiJson.decode( s.to_json )
|
364
|
+
assert_not_nil hash['suggest']
|
365
|
+
end
|
366
|
+
|
367
|
+
end
|
368
|
+
|
342
369
|
context "with version" do
|
343
370
|
|
344
371
|
should "set the version" do
|
@@ -0,0 +1,82 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
module Tire
|
4
|
+
|
5
|
+
class SuggestTest < Test::Unit::TestCase
|
6
|
+
|
7
|
+
context "Suggest" do
|
8
|
+
setup { Configuration.reset }
|
9
|
+
|
10
|
+
should "be initialized with single index" do
|
11
|
+
s = Suggest::Suggest.new('index') do
|
12
|
+
suggestion 'default-suggestion' do
|
13
|
+
text 'foo'
|
14
|
+
completion 'bar'
|
15
|
+
end
|
16
|
+
end
|
17
|
+
assert_match %r|/index/_suggest|, s.url
|
18
|
+
end
|
19
|
+
|
20
|
+
should "allow to suggest all indices by leaving index empty" do
|
21
|
+
s = Suggest::Suggest.new do
|
22
|
+
suggestion 'default-suggestion' do
|
23
|
+
text 'foo'
|
24
|
+
completion 'bar'
|
25
|
+
end
|
26
|
+
end
|
27
|
+
assert_match %r|localhost:9200/_suggest|, s.url
|
28
|
+
end
|
29
|
+
|
30
|
+
should "return curl snippet for debugging" do
|
31
|
+
s = Suggest::Suggest.new('index') do
|
32
|
+
suggestion 'default-suggestion' do
|
33
|
+
text 'foo'
|
34
|
+
completion 'bar'
|
35
|
+
end
|
36
|
+
end
|
37
|
+
assert_match %r|curl \-X GET 'http://localhost:9200/index/_suggest\?pretty' -d |, s.to_curl
|
38
|
+
assert_match %r|\s*{\s*"default-suggestion"\s*:\s*{\s*"text"\s*:\s*"foo"\s*,\s*"completion"\s*:\s*{\s*"field"\s*:\s*"bar"\s*}\s*}\s*}\s*|, s.to_curl
|
39
|
+
end
|
40
|
+
|
41
|
+
should "return itself as a Hash" do
|
42
|
+
s = Suggest::Suggest.new('index') do
|
43
|
+
suggestion 'default_suggestion' do
|
44
|
+
text 'foo'
|
45
|
+
completion 'bar'
|
46
|
+
end
|
47
|
+
end
|
48
|
+
assert_nothing_raised do
|
49
|
+
assert_instance_of Hash, s.to_hash
|
50
|
+
assert_equal "foo", s.to_hash[:default_suggestion][:text]
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
should "allow to pass options for completion queries" do
|
55
|
+
s = Suggest::Suggest.new do
|
56
|
+
suggestion 'default_suggestion' do
|
57
|
+
text 'foo'
|
58
|
+
completion 'bar', :fuzzy => true
|
59
|
+
end
|
60
|
+
end
|
61
|
+
assert_equal true, s.to_hash[:default_suggestion][:completion][:fuzzy]
|
62
|
+
end
|
63
|
+
|
64
|
+
should "perform the suggest lazily" do
|
65
|
+
response = mock_response '{"_shards": {"total": 5, "successful": 5, "failed": 0}, "default-suggestion": [{"text": "ssd", "offset": 0, "length": 10, "options": [] } ] }', 200
|
66
|
+
Configuration.client.expects(:get).returns(response)
|
67
|
+
Results::Suggestions.expects(:new).returns([])
|
68
|
+
|
69
|
+
s = Suggest::Suggest.new('index') do
|
70
|
+
suggestion 'default-suggestion' do
|
71
|
+
text 'foo'
|
72
|
+
completion 'bar'
|
73
|
+
end
|
74
|
+
end
|
75
|
+
assert_not_nil s.results
|
76
|
+
assert_not_nil s.response
|
77
|
+
assert_not_nil s.json
|
78
|
+
end
|
79
|
+
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: tire
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.6.
|
4
|
+
version: 0.6.2
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date:
|
12
|
+
date: 2014-01-15 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: rake
|
@@ -409,10 +409,12 @@ files:
|
|
409
409
|
- lib/tire/model/persistence/finders.rb
|
410
410
|
- lib/tire/model/persistence/storage.rb
|
411
411
|
- lib/tire/model/search.rb
|
412
|
+
- lib/tire/model/suggest.rb
|
412
413
|
- lib/tire/multi_search.rb
|
413
414
|
- lib/tire/results/collection.rb
|
414
415
|
- lib/tire/results/item.rb
|
415
416
|
- lib/tire/results/pagination.rb
|
417
|
+
- lib/tire/results/suggestions.rb
|
416
418
|
- lib/tire/rubyext/hash.rb
|
417
419
|
- lib/tire/rubyext/ruby_1_8.rb
|
418
420
|
- lib/tire/rubyext/symbol.rb
|
@@ -427,6 +429,8 @@ files:
|
|
427
429
|
- lib/tire/search/scan.rb
|
428
430
|
- lib/tire/search/script_field.rb
|
429
431
|
- lib/tire/search/sort.rb
|
432
|
+
- lib/tire/suggest.rb
|
433
|
+
- lib/tire/suggest/suggestion.rb
|
430
434
|
- lib/tire/tasks.rb
|
431
435
|
- lib/tire/utils.rb
|
432
436
|
- lib/tire/version.rb
|
@@ -472,7 +476,9 @@ files:
|
|
472
476
|
- test/integration/results_test.rb
|
473
477
|
- test/integration/scan_test.rb
|
474
478
|
- test/integration/script_fields_test.rb
|
479
|
+
- test/integration/search_response_test.rb
|
475
480
|
- test/integration/sort_test.rb
|
481
|
+
- test/integration/suggest_test.rb
|
476
482
|
- test/models/active_model_article.rb
|
477
483
|
- test/models/active_model_article_with_callbacks.rb
|
478
484
|
- test/models/active_model_article_with_custom_document_type.rb
|
@@ -516,18 +522,17 @@ files:
|
|
516
522
|
- test/unit/search_script_field_test.rb
|
517
523
|
- test/unit/search_sort_test.rb
|
518
524
|
- test/unit/search_test.rb
|
525
|
+
- test/unit/suggest_test.rb
|
519
526
|
- test/unit/tire_test.rb
|
520
527
|
- tire.gemspec
|
521
528
|
homepage: http://github.com/karmi/tire
|
522
529
|
licenses: []
|
523
530
|
post_install_message: ! "================================================================================\n\n
|
524
531
|
\ Please check the documentation at <http://karmi.github.com/retire/>.\n\n--------------------------------------------------------------------------------\n\n
|
525
|
-
\ IMPORTANT CHANGES LATELY:\n\n
|
526
|
-
|
527
|
-
|
528
|
-
|
529
|
-
in `get_id_from_document`\n * Added support for \"Delete By Query\" API\n\n See
|
530
|
-
the full changelog at <http://github.com/karmi/tire/commits/v0.6.1>.\n\n--------------------------------------------------------------------------------\n"
|
532
|
+
\ IMPORTANT CHANGES LATELY:\n\n 19e524c [ACTIVEMODEL] Exposed the response from
|
533
|
+
`MyModel#.update_index` as the `response` method on return value\n bfcde21 [#916]
|
534
|
+
Added support for the Suggest API (@marc-villanueva)\n 710451d [#857] Added support
|
535
|
+
for the Suggest API (@fbatista)\n\n See the full changelog at <http://github.com/karmi/tire/commits/v0.6.2>.\n\n--------------------------------------------------------------------------------\n"
|
531
536
|
rdoc_options:
|
532
537
|
- --charset=UTF-8
|
533
538
|
require_paths:
|
@@ -593,7 +598,9 @@ test_files:
|
|
593
598
|
- test/integration/results_test.rb
|
594
599
|
- test/integration/scan_test.rb
|
595
600
|
- test/integration/script_fields_test.rb
|
601
|
+
- test/integration/search_response_test.rb
|
596
602
|
- test/integration/sort_test.rb
|
603
|
+
- test/integration/suggest_test.rb
|
597
604
|
- test/models/active_model_article.rb
|
598
605
|
- test/models/active_model_article_with_callbacks.rb
|
599
606
|
- test/models/active_model_article_with_custom_document_type.rb
|
@@ -637,5 +644,6 @@ test_files:
|
|
637
644
|
- test/unit/search_script_field_test.rb
|
638
645
|
- test/unit/search_sort_test.rb
|
639
646
|
- test/unit/search_test.rb
|
647
|
+
- test/unit/suggest_test.rb
|
640
648
|
- test/unit/tire_test.rb
|
641
649
|
has_rdoc:
|