tire 0.6.1 → 0.6.2
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/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
|
+

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