slingshot-rb 0.0.8 → 0.0.9
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
|