slingshot-rb 0.0.8 → 0.0.9
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 +1 -0
- data/README.markdown +276 -50
- data/examples/rails-application-template.rb +144 -0
- data/examples/slingshot-dsl.rb +272 -102
- data/lib/slingshot.rb +13 -0
- data/lib/slingshot/client.rb +10 -1
- data/lib/slingshot/dsl.rb +17 -1
- data/lib/slingshot/index.rb +109 -7
- data/lib/slingshot/model/callbacks.rb +23 -0
- data/lib/slingshot/model/import.rb +18 -0
- data/lib/slingshot/model/indexing.rb +50 -0
- data/lib/slingshot/model/naming.rb +30 -0
- data/lib/slingshot/model/persistence.rb +34 -0
- data/lib/slingshot/model/persistence/attributes.rb +60 -0
- data/lib/slingshot/model/persistence/finders.rb +61 -0
- data/lib/slingshot/model/persistence/storage.rb +75 -0
- data/lib/slingshot/model/search.rb +97 -0
- data/lib/slingshot/results/collection.rb +35 -10
- data/lib/slingshot/results/item.rb +10 -7
- data/lib/slingshot/results/pagination.rb +30 -0
- data/lib/slingshot/rubyext/symbol.rb +11 -0
- data/lib/slingshot/search.rb +3 -2
- data/lib/slingshot/search/facet.rb +8 -6
- data/lib/slingshot/search/filter.rb +7 -8
- data/lib/slingshot/search/highlight.rb +1 -3
- data/lib/slingshot/search/query.rb +4 -0
- data/lib/slingshot/search/sort.rb +5 -0
- data/lib/slingshot/tasks.rb +88 -0
- data/lib/slingshot/version.rb +1 -1
- data/slingshot.gemspec +17 -4
- data/test/integration/active_model_searchable_test.rb +80 -0
- data/test/integration/active_record_searchable_test.rb +193 -0
- data/test/integration/highlight_test.rb +1 -1
- data/test/integration/index_mapping_test.rb +1 -1
- data/test/integration/index_store_test.rb +27 -0
- data/test/integration/persistent_model_test.rb +35 -0
- data/test/integration/query_string_test.rb +3 -3
- data/test/integration/sort_test.rb +2 -2
- 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_index_name.rb +5 -0
- data/test/models/active_record_article.rb +12 -0
- data/test/models/persistent_article.rb +11 -0
- data/test/models/persistent_articles_with_custom_index_name.rb +10 -0
- data/test/models/supermodel_article.rb +22 -0
- data/test/models/validated_model.rb +11 -0
- data/test/test_helper.rb +4 -0
- data/test/unit/active_model_lint_test.rb +17 -0
- data/test/unit/client_test.rb +4 -0
- data/test/unit/configuration_test.rb +4 -0
- data/test/unit/index_test.rb +240 -17
- data/test/unit/model_callbacks_test.rb +90 -0
- data/test/unit/model_import_test.rb +71 -0
- data/test/unit/model_persistence_test.rb +400 -0
- data/test/unit/model_search_test.rb +289 -0
- data/test/unit/results_collection_test.rb +69 -7
- data/test/unit/results_item_test.rb +8 -14
- data/test/unit/rubyext_hash_test.rb +19 -0
- data/test/unit/search_facet_test.rb +25 -7
- data/test/unit/search_filter_test.rb +3 -0
- data/test/unit/search_query_test.rb +11 -0
- data/test/unit/search_sort_test.rb +8 -0
- data/test/unit/search_test.rb +14 -0
- data/test/unit/slingshot_test.rb +38 -0
- metadata +133 -26
@@ -0,0 +1,61 @@
|
|
1
|
+
module Slingshot
|
2
|
+
module Model
|
3
|
+
|
4
|
+
module Persistence
|
5
|
+
|
6
|
+
module Finders
|
7
|
+
|
8
|
+
module ClassMethods
|
9
|
+
|
10
|
+
def find *args
|
11
|
+
# TODO: Options like `sort`
|
12
|
+
old_wrapper = Slingshot::Configuration.wrapper
|
13
|
+
Slingshot::Configuration.wrapper self
|
14
|
+
options = args.pop if args.last.is_a?(Hash)
|
15
|
+
args.flatten!
|
16
|
+
if args.size > 1
|
17
|
+
Slingshot::Search::Search.new(index.name).query do |query|
|
18
|
+
query.ids(args, document_type)
|
19
|
+
end.perform.results
|
20
|
+
else
|
21
|
+
case args = args.pop
|
22
|
+
when Fixnum, String
|
23
|
+
Index.new(index_name).retrieve document_type, args
|
24
|
+
when :all, :first
|
25
|
+
send(args)
|
26
|
+
else
|
27
|
+
raise ArgumentError, "Please pass either ID as Fixnum or String, or :all, :first as an argument"
|
28
|
+
end
|
29
|
+
end
|
30
|
+
ensure
|
31
|
+
Slingshot::Configuration.wrapper old_wrapper
|
32
|
+
end
|
33
|
+
|
34
|
+
def all
|
35
|
+
# TODO: Options like `sort`; Possibly `filters`
|
36
|
+
old_wrapper = Slingshot::Configuration.wrapper
|
37
|
+
Slingshot::Configuration.wrapper self
|
38
|
+
s = Slingshot::Search::Search.new(index_name).query { all }
|
39
|
+
s.perform.results
|
40
|
+
ensure
|
41
|
+
Slingshot::Configuration.wrapper old_wrapper
|
42
|
+
end
|
43
|
+
|
44
|
+
def first
|
45
|
+
# TODO: Options like `sort`; Possibly `filters`
|
46
|
+
old_wrapper = Slingshot::Configuration.wrapper
|
47
|
+
Slingshot::Configuration.wrapper self
|
48
|
+
s = Slingshot::Search::Search.new(index_name).query { all }.size(1)
|
49
|
+
s.perform.results.first
|
50
|
+
ensure
|
51
|
+
Slingshot::Configuration.wrapper old_wrapper
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
module Slingshot
|
2
|
+
module Model
|
3
|
+
|
4
|
+
module Persistence
|
5
|
+
|
6
|
+
module Storage
|
7
|
+
|
8
|
+
def self.included(base)
|
9
|
+
|
10
|
+
base.class_eval do
|
11
|
+
extend ClassMethods
|
12
|
+
include InstanceMethods
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
16
|
+
|
17
|
+
module ClassMethods
|
18
|
+
|
19
|
+
def create(args={})
|
20
|
+
document = new(args)
|
21
|
+
return false unless document.valid?
|
22
|
+
document.save
|
23
|
+
document
|
24
|
+
end
|
25
|
+
|
26
|
+
def index
|
27
|
+
@index = Index.new(index_name)
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
|
32
|
+
module InstanceMethods
|
33
|
+
|
34
|
+
def update_attribute(name, value)
|
35
|
+
send("#{name}=", value)
|
36
|
+
save
|
37
|
+
end
|
38
|
+
|
39
|
+
def update_attributes(attributes={})
|
40
|
+
attributes.each do |name, value|
|
41
|
+
send("#{name}=", value)
|
42
|
+
end
|
43
|
+
save
|
44
|
+
end
|
45
|
+
|
46
|
+
def save
|
47
|
+
return false unless valid?
|
48
|
+
run_callbacks :save do
|
49
|
+
# Document#id is set in the +update_elastic_search_index+ method,
|
50
|
+
# where we have access to the JSON response
|
51
|
+
end
|
52
|
+
self
|
53
|
+
end
|
54
|
+
|
55
|
+
def destroy
|
56
|
+
run_callbacks :destroy do
|
57
|
+
@destroyed = true
|
58
|
+
end
|
59
|
+
self.freeze
|
60
|
+
end
|
61
|
+
|
62
|
+
# TODO: Implement `new_record?` and clean up
|
63
|
+
|
64
|
+
def destroyed?; !!@destroyed; end
|
65
|
+
|
66
|
+
def persisted?; !!id; end
|
67
|
+
|
68
|
+
end
|
69
|
+
|
70
|
+
end
|
71
|
+
|
72
|
+
end
|
73
|
+
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
module Slingshot
|
2
|
+
module Model
|
3
|
+
|
4
|
+
module Search
|
5
|
+
|
6
|
+
def self.included(base)
|
7
|
+
base.class_eval do
|
8
|
+
extend Slingshot::Model::Naming::ClassMethods
|
9
|
+
include Slingshot::Model::Naming::InstanceMethods
|
10
|
+
|
11
|
+
extend Slingshot::Model::Indexing::ClassMethods
|
12
|
+
extend Slingshot::Model::Import::ClassMethods
|
13
|
+
|
14
|
+
extend ClassMethods
|
15
|
+
include InstanceMethods
|
16
|
+
|
17
|
+
['_score', '_type', '_index', '_version', 'sort', 'highlight'].each do |attr|
|
18
|
+
# TODO: Find a sane way to add attributes like _score for ActiveRecord -
|
19
|
+
# `define_attribute_methods [attr]` does not work in AR.
|
20
|
+
define_method("#{attr}=") { |value| @attributes ||= {}; @attributes[attr] = value }
|
21
|
+
define_method("#{attr}") { @attributes[attr] }
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
module ClassMethods
|
27
|
+
|
28
|
+
def search(query=nil, options={}, &block)
|
29
|
+
old_wrapper = Slingshot::Configuration.wrapper
|
30
|
+
Slingshot::Configuration.wrapper self
|
31
|
+
sort = options[:order] || options[:sort]
|
32
|
+
sort = Array(sort)
|
33
|
+
unless block_given?
|
34
|
+
s = Slingshot::Search::Search.new(index.name, options)
|
35
|
+
s.query { string query }
|
36
|
+
s.sort do
|
37
|
+
sort.each do |t|
|
38
|
+
field_name, direction = t.split(' ')
|
39
|
+
field_name.include?('.') ? field(field_name, direction) : send(field_name, direction)
|
40
|
+
end
|
41
|
+
end unless sort.empty?
|
42
|
+
s.size( options[:per_page].to_i ) if options[:per_page]
|
43
|
+
s.from( options[:page].to_i <= 1 ? 0 : (options[:per_page].to_i * (options[:page].to_i-1)) ) if options[:page] && options[:per_page]
|
44
|
+
s.perform.results
|
45
|
+
else
|
46
|
+
s = Slingshot::Search::Search.new(index.name, options)
|
47
|
+
block.arity < 1 ? s.instance_eval(&block) : block.call(s)
|
48
|
+
s.perform.results
|
49
|
+
end
|
50
|
+
ensure
|
51
|
+
Slingshot::Configuration.wrapper old_wrapper
|
52
|
+
end
|
53
|
+
|
54
|
+
def index
|
55
|
+
@index = Index.new(index_name)
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
59
|
+
|
60
|
+
module InstanceMethods
|
61
|
+
|
62
|
+
def score
|
63
|
+
attributes['_score']
|
64
|
+
end
|
65
|
+
|
66
|
+
def index
|
67
|
+
self.class.index
|
68
|
+
end
|
69
|
+
|
70
|
+
def update_elastic_search_index
|
71
|
+
if destroyed?
|
72
|
+
self.class.index.remove document_type, self
|
73
|
+
else
|
74
|
+
response = self.class.index.store document_type, self
|
75
|
+
self.id ||= response['_id'] if self.respond_to?(:id=)
|
76
|
+
self
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def to_indexed_json
|
81
|
+
if self.class.mapping.empty?
|
82
|
+
self.serializable_hash.
|
83
|
+
to_json
|
84
|
+
else
|
85
|
+
self.serializable_hash.
|
86
|
+
reject { |key, value| ! self.class.mapping.keys.map(&:to_s).include?(key.to_s) }.
|
87
|
+
to_json
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
end
|
92
|
+
|
93
|
+
extend ClassMethods
|
94
|
+
end
|
95
|
+
|
96
|
+
end
|
97
|
+
end
|
@@ -3,19 +3,32 @@ module Slingshot
|
|
3
3
|
|
4
4
|
class Collection
|
5
5
|
include Enumerable
|
6
|
-
|
6
|
+
include Pagination
|
7
7
|
|
8
|
-
|
9
|
-
|
10
|
-
|
8
|
+
attr_reader :time, :total, :options, :results, :facets
|
9
|
+
|
10
|
+
def initialize(response, options={})
|
11
|
+
@options = options
|
12
|
+
@time = response['took'].to_i
|
13
|
+
@total = response['hits']['total'].to_i
|
11
14
|
@results = response['hits']['hits'].map do |h|
|
12
|
-
if Configuration.wrapper == Hash
|
13
|
-
h
|
15
|
+
if Configuration.wrapper == Hash then h
|
14
16
|
else
|
15
|
-
document =
|
16
|
-
|
17
|
-
|
18
|
-
|
17
|
+
document = {}
|
18
|
+
|
19
|
+
# Update the document with content and ID
|
20
|
+
document = h['_source'] ? document.update( h['_source'] || {} ) : document.update( h['fields'] || {} )
|
21
|
+
document.update( {'id' => h['_id']} )
|
22
|
+
|
23
|
+
# Update the document with meta information
|
24
|
+
['_score', '_version', 'sort', 'highlight'].each { |key| document.update( {key => h[key]} || {} ) }
|
25
|
+
|
26
|
+
object = Configuration.wrapper.new(document)
|
27
|
+
# TODO: Figure out how to circumvent mass assignment protection for id in ActiveRecord
|
28
|
+
object.id = h['_id'] if object.respond_to?(:id=)
|
29
|
+
# TODO: Figure out how mark record as "not new record" in ActiveRecord
|
30
|
+
object.instance_variable_set(:@new_record, false) if object.respond_to?(:new_record?)
|
31
|
+
object
|
19
32
|
end
|
20
33
|
end
|
21
34
|
@facets = response['facets']
|
@@ -25,6 +38,18 @@ module Slingshot
|
|
25
38
|
@results.each(&block)
|
26
39
|
end
|
27
40
|
|
41
|
+
def empty?
|
42
|
+
@results.empty?
|
43
|
+
end
|
44
|
+
|
45
|
+
def size
|
46
|
+
@results.size
|
47
|
+
end
|
48
|
+
|
49
|
+
def to_ary
|
50
|
+
self
|
51
|
+
end
|
52
|
+
|
28
53
|
end
|
29
54
|
|
30
55
|
end
|
@@ -7,13 +7,9 @@ module Slingshot
|
|
7
7
|
# and leaving everything else alone.
|
8
8
|
#
|
9
9
|
def initialize(args={})
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
end
|
14
|
-
super.replace self
|
15
|
-
else
|
16
|
-
super
|
10
|
+
raise ArgumentError, "Please pass a Hash-like object" unless args.respond_to?(:each_pair)
|
11
|
+
args.each_pair do |key, value|
|
12
|
+
self[key.to_sym] = value.respond_to?(:to_hash) ? self.class.new(value) : value
|
17
13
|
end
|
18
14
|
end
|
19
15
|
|
@@ -24,12 +20,19 @@ module Slingshot
|
|
24
20
|
self.has_key?(method_name.to_sym) ? self[method_name.to_sym] : nil
|
25
21
|
end
|
26
22
|
|
23
|
+
# Get ID
|
24
|
+
#
|
25
|
+
def id
|
26
|
+
self[:id]
|
27
|
+
end
|
28
|
+
|
27
29
|
def inspect
|
28
30
|
s = []; self.each { |k,v| s << "#{k}: #{v.inspect}" }
|
29
31
|
%Q|<Item #{s.join(', ')}>|
|
30
32
|
end
|
31
33
|
|
32
34
|
alias_method :to_indexed_json, :to_json
|
35
|
+
|
33
36
|
end
|
34
37
|
|
35
38
|
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Slingshot
|
2
|
+
module Results
|
3
|
+
|
4
|
+
module Pagination
|
5
|
+
|
6
|
+
def total_entries
|
7
|
+
@total
|
8
|
+
end
|
9
|
+
|
10
|
+
def total_pages
|
11
|
+
result = @total.to_f / (@options[:per_page] ? @options[:per_page].to_i : 10 )
|
12
|
+
result < 1 ? 1 : result.round
|
13
|
+
end
|
14
|
+
|
15
|
+
def current_page
|
16
|
+
@options[:page].to_i
|
17
|
+
end
|
18
|
+
|
19
|
+
def previous_page
|
20
|
+
@options[:page].to_i - 1
|
21
|
+
end
|
22
|
+
|
23
|
+
def next_page
|
24
|
+
@options[:page].to_i + 1
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
end
|
data/lib/slingshot/search.rb
CHANGED
@@ -16,7 +16,8 @@ module Slingshot
|
|
16
16
|
end
|
17
17
|
|
18
18
|
def query(&block)
|
19
|
-
@query = Query.new
|
19
|
+
@query = Query.new
|
20
|
+
block.arity < 1 ? @query.instance_eval(&block) : block.call(@query)
|
20
21
|
self
|
21
22
|
end
|
22
23
|
|
@@ -65,7 +66,7 @@ module Slingshot
|
|
65
66
|
@url = "#{Configuration.url}/#{indices.join(',')}/_search"
|
66
67
|
@response = Configuration.client.post(@url, self.to_json)
|
67
68
|
@json = Yajl::Parser.parse(@response.body)
|
68
|
-
@results = Results::Collection.new(@json)
|
69
|
+
@results = Results::Collection.new(@json, @options)
|
69
70
|
self
|
70
71
|
rescue Exception => error
|
71
72
|
STDERR.puts "[REQUEST FAILED] #{self.to_curl}\n"
|
@@ -14,12 +14,15 @@ module Slingshot
|
|
14
14
|
self.instance_eval(&block) if block_given?
|
15
15
|
end
|
16
16
|
|
17
|
-
def terms(field,
|
18
|
-
|
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)
|
19
21
|
self
|
20
22
|
end
|
21
23
|
|
22
|
-
def date(field,
|
24
|
+
def date(field, options={})
|
25
|
+
interval = options.delete(:interval) || 'day'
|
23
26
|
@value = { :date_histogram => { :field => field, :interval => interval } }.update(options)
|
24
27
|
self
|
25
28
|
end
|
@@ -29,9 +32,8 @@ module Slingshot
|
|
29
32
|
end
|
30
33
|
|
31
34
|
def to_hash
|
32
|
-
|
33
|
-
|
34
|
-
return h
|
35
|
+
@value.update @options
|
36
|
+
{ @name => @value }
|
35
37
|
end
|
36
38
|
end
|
37
39
|
|
@@ -7,8 +7,12 @@ module Slingshot
|
|
7
7
|
class Filter
|
8
8
|
|
9
9
|
def initialize(type, *options)
|
10
|
-
|
11
|
-
|
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 }
|
12
16
|
end
|
13
17
|
|
14
18
|
def to_json
|
@@ -16,12 +20,7 @@ module Slingshot
|
|
16
20
|
end
|
17
21
|
|
18
22
|
def to_hash
|
19
|
-
|
20
|
-
method = initial[@type].is_a?(Hash) ? :update : :push
|
21
|
-
@options.inject(initial) do |hash, option|
|
22
|
-
hash[@type].send(method, option)
|
23
|
-
hash
|
24
|
-
end
|
23
|
+
@hash
|
25
24
|
end
|
26
25
|
end
|
27
26
|
|