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
data/lib/slingshot.rb
CHANGED
@@ -1,7 +1,9 @@
|
|
1
1
|
require 'rest_client'
|
2
2
|
require 'yajl/json_gem'
|
3
|
+
require 'active_model'
|
3
4
|
|
4
5
|
require 'slingshot/rubyext/hash'
|
6
|
+
require 'slingshot/rubyext/symbol'
|
5
7
|
require 'slingshot/logger'
|
6
8
|
require 'slingshot/configuration'
|
7
9
|
require 'slingshot/client'
|
@@ -12,10 +14,21 @@ require 'slingshot/search/sort'
|
|
12
14
|
require 'slingshot/search/facet'
|
13
15
|
require 'slingshot/search/filter'
|
14
16
|
require 'slingshot/search/highlight'
|
17
|
+
require 'slingshot/results/pagination'
|
15
18
|
require 'slingshot/results/collection'
|
16
19
|
require 'slingshot/results/item'
|
17
20
|
require 'slingshot/index'
|
18
21
|
require 'slingshot/dsl'
|
22
|
+
require 'slingshot/model/naming'
|
23
|
+
require 'slingshot/model/callbacks'
|
24
|
+
require 'slingshot/model/search'
|
25
|
+
require 'slingshot/model/indexing'
|
26
|
+
require 'slingshot/model/import'
|
27
|
+
require 'slingshot/model/persistence/finders'
|
28
|
+
require 'slingshot/model/persistence/attributes'
|
29
|
+
require 'slingshot/model/persistence/storage'
|
30
|
+
require 'slingshot/model/persistence'
|
31
|
+
require 'slingshot/tasks'
|
19
32
|
|
20
33
|
module Slingshot
|
21
34
|
extend DSL
|
data/lib/slingshot/client.rb
CHANGED
@@ -4,12 +4,18 @@ module Slingshot
|
|
4
4
|
|
5
5
|
class Base
|
6
6
|
def get(url)
|
7
|
-
|
7
|
+
raise_no_method_error
|
8
8
|
end
|
9
9
|
def post(url, data)
|
10
|
+
raise_no_method_error
|
11
|
+
end
|
12
|
+
def put(url, data)
|
10
13
|
raise NoMethodError, "Implement this method in your client class"
|
11
14
|
end
|
12
15
|
def delete(url)
|
16
|
+
raise_no_method_error
|
17
|
+
end
|
18
|
+
def raise_no_method_error
|
13
19
|
raise NoMethodError, "Implement this method in your client class"
|
14
20
|
end
|
15
21
|
end
|
@@ -21,6 +27,9 @@ module Slingshot
|
|
21
27
|
def self.post(url, data)
|
22
28
|
::RestClient.post url, data
|
23
29
|
end
|
30
|
+
def self.put(url, data)
|
31
|
+
::RestClient.put url, data
|
32
|
+
end
|
24
33
|
def self.delete(url)
|
25
34
|
::RestClient.delete url rescue nil
|
26
35
|
end
|
data/lib/slingshot/dsl.rb
CHANGED
@@ -6,7 +6,23 @@ module Slingshot
|
|
6
6
|
end
|
7
7
|
|
8
8
|
def search(indices, options={}, &block)
|
9
|
-
|
9
|
+
if block_given?
|
10
|
+
Search::Search.new(indices, options, &block).perform
|
11
|
+
else
|
12
|
+
payload = case options
|
13
|
+
when Hash then options.to_json
|
14
|
+
when String then options
|
15
|
+
else raise ArgumentError, "Please pass a Ruby Hash or String with JSON"
|
16
|
+
end
|
17
|
+
|
18
|
+
response = Configuration.client.post( "#{Configuration.url}/#{indices}/_search", payload)
|
19
|
+
json = Yajl::Parser.parse(response.body)
|
20
|
+
results = Results::Collection.new(json, options)
|
21
|
+
end
|
22
|
+
rescue Exception => error
|
23
|
+
STDERR.puts "[REQUEST FAILED] #{error.class} #{error.http_body rescue nil}\n"
|
24
|
+
raise
|
25
|
+
ensure
|
10
26
|
end
|
11
27
|
|
12
28
|
def index(name, &block)
|
data/lib/slingshot/index.rb
CHANGED
@@ -1,15 +1,23 @@
|
|
1
1
|
module Slingshot
|
2
2
|
class Index
|
3
3
|
|
4
|
+
attr_reader :name
|
5
|
+
|
4
6
|
def initialize(name, &block)
|
5
7
|
@name = name
|
6
8
|
instance_eval(&block) if block_given?
|
7
9
|
end
|
8
10
|
|
11
|
+
def exists?
|
12
|
+
!!Configuration.client.get("#{Configuration.url}/#{@name}/_status")
|
13
|
+
rescue Exception => error
|
14
|
+
false
|
15
|
+
end
|
16
|
+
|
9
17
|
def delete
|
10
18
|
# FIXME: RestClient does not return response for DELETE requests?
|
11
19
|
@response = Configuration.client.delete "#{Configuration.url}/#{@name}"
|
12
|
-
return @response =~ /error/ ? false : true
|
20
|
+
return @response.body =~ /error/ ? false : true
|
13
21
|
rescue Exception => error
|
14
22
|
false
|
15
23
|
ensure
|
@@ -28,7 +36,8 @@ module Slingshot
|
|
28
36
|
end
|
29
37
|
|
30
38
|
def mapping
|
31
|
-
|
39
|
+
@response = Configuration.client.get("#{Configuration.url}/#{@name}/_mapping")
|
40
|
+
JSON.parse(@response.body)[@name]
|
32
41
|
end
|
33
42
|
|
34
43
|
def store(*args)
|
@@ -56,7 +65,7 @@ module Slingshot
|
|
56
65
|
url = id ? "#{Configuration.url}/#{@name}/#{type}/#{id}" : "#{Configuration.url}/#{@name}/#{type}/"
|
57
66
|
|
58
67
|
@response = Configuration.client.post url, document
|
59
|
-
JSON.parse(@response)
|
68
|
+
JSON.parse(@response.body)
|
60
69
|
|
61
70
|
rescue Exception => error
|
62
71
|
raise
|
@@ -65,14 +74,107 @@ module Slingshot
|
|
65
74
|
logged(error, "/#{@name}/#{type}/", curl)
|
66
75
|
end
|
67
76
|
|
77
|
+
def bulk_store documents
|
78
|
+
create unless exists?
|
79
|
+
|
80
|
+
payload = documents.map do |document|
|
81
|
+
old_verbose, $VERBOSE = $VERBOSE, nil # Silence Object#id deprecation warnings
|
82
|
+
id = case
|
83
|
+
when document.is_a?(Hash) then document[:id] || document['id']
|
84
|
+
when document.respond_to?(:id) && document.id != document.object_id then document.id
|
85
|
+
# TODO: Raise error when no id present
|
86
|
+
end
|
87
|
+
$VERBOSE = old_verbose
|
88
|
+
|
89
|
+
type = case
|
90
|
+
when document.is_a?(Hash) then document[:type] || document['type']
|
91
|
+
when document.respond_to?(:document_type) then document.document_type
|
92
|
+
end || 'document'
|
93
|
+
|
94
|
+
output = []
|
95
|
+
output << %Q|{"index":{"_index":"#{@name}","_type":"#{type}","_id":"#{id}"}}|
|
96
|
+
output << document.to_indexed_json
|
97
|
+
output.join("\n")
|
98
|
+
end
|
99
|
+
payload << ""
|
100
|
+
|
101
|
+
tries = 5
|
102
|
+
count = 0
|
103
|
+
|
104
|
+
begin
|
105
|
+
# STDERR.puts "Posting payload..."
|
106
|
+
# STDERR.puts payload.join("\n")
|
107
|
+
Configuration.client.post("#{Configuration.url}/_bulk", payload.join("\n"))
|
108
|
+
rescue Exception => error
|
109
|
+
if count < tries
|
110
|
+
count += 1
|
111
|
+
STDERR.puts "[ERROR] #{error.message}:#{error.http_body rescue nil}, retrying (#{count})..."
|
112
|
+
retry
|
113
|
+
else
|
114
|
+
STDERR.puts "[ERROR] Too many exceptions occured, giving up..."
|
115
|
+
STDERR.puts "Response: #{error.http_body rescue nil}"
|
116
|
+
raise
|
117
|
+
end
|
118
|
+
ensure
|
119
|
+
curl = %Q|curl -X POST "#{Configuration.url}/_bulk" -d '{... data omitted ...}'|
|
120
|
+
logged(error, 'BULK', curl)
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
def import(klass_or_collection, method=nil, options={})
|
125
|
+
# p [klass_or_collection, method, options]
|
126
|
+
|
127
|
+
case
|
128
|
+
|
129
|
+
when method
|
130
|
+
options = {:page => 1, :per_page => 1000}.merge options
|
131
|
+
while documents = klass_or_collection.send(method.to_sym, options.merge(:page => options[:page])) \
|
132
|
+
and not documents.empty?
|
133
|
+
documents = yield documents if block_given?
|
134
|
+
|
135
|
+
bulk_store documents
|
136
|
+
options[:page] += 1
|
137
|
+
end
|
138
|
+
|
139
|
+
when klass_or_collection.respond_to?(:map)
|
140
|
+
documents = block_given? ? yield(klass_or_collection) : klass_or_collection
|
141
|
+
bulk_store documents
|
142
|
+
else
|
143
|
+
raise ArgumentError, "Please pass either a collection of objects, "+
|
144
|
+
"or method for fetching records, or Enumerable compatible class"
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
def remove(*args)
|
149
|
+
# TODO: Infer type from the document (hash property, method)
|
150
|
+
|
151
|
+
if args.size > 1
|
152
|
+
(type, document = args)
|
153
|
+
else
|
154
|
+
(document = args.pop; type = :document)
|
155
|
+
end
|
156
|
+
|
157
|
+
old_verbose, $VERBOSE = $VERBOSE, nil # Silence Object#id deprecation warnings
|
158
|
+
id = case true
|
159
|
+
when document.is_a?(Hash) then document[:id] || document['id']
|
160
|
+
when document.respond_to?(:id) && document.id != document.object_id then document.id
|
161
|
+
else document
|
162
|
+
end
|
163
|
+
$VERBOSE = old_verbose
|
164
|
+
|
165
|
+
result = Configuration.client.delete "#{Configuration.url}/#{@name}/#{type}/#{id}"
|
166
|
+
JSON.parse(result) if result
|
167
|
+
end
|
168
|
+
|
68
169
|
def retrieve(type, id)
|
69
170
|
@response = Configuration.client.get "#{Configuration.url}/#{@name}/#{type}/#{id}"
|
70
|
-
h = JSON.parse(@response)
|
171
|
+
h = JSON.parse(@response.body)
|
71
172
|
if Configuration.wrapper == Hash then h
|
72
173
|
else
|
73
|
-
document =
|
74
|
-
h.update document
|
75
|
-
|
174
|
+
document = {}
|
175
|
+
document = h['_source'] ? document.update( h['_source'] ) : document.update( h['fields'] )
|
176
|
+
document.update('id' => h['_id'], '_version' => h['_version'])
|
177
|
+
Configuration.wrapper.new(document)
|
76
178
|
end
|
77
179
|
end
|
78
180
|
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Slingshot
|
2
|
+
module Model
|
3
|
+
|
4
|
+
module Callbacks
|
5
|
+
|
6
|
+
def self.included(base)
|
7
|
+
if base.respond_to?(:after_save) && base.respond_to?(:after_destroy)
|
8
|
+
base.send :after_save, :update_elastic_search_index
|
9
|
+
base.send :after_destroy, :update_elastic_search_index
|
10
|
+
end
|
11
|
+
|
12
|
+
if base.respond_to?(:before_destroy) && !base.respond_to?(:destroyed?)
|
13
|
+
base.class_eval do
|
14
|
+
before_destroy { @destroyed = true }
|
15
|
+
def destroyed?; !!@destroyed; end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Slingshot
|
2
|
+
module Model
|
3
|
+
|
4
|
+
module Import
|
5
|
+
|
6
|
+
module ClassMethods
|
7
|
+
|
8
|
+
def import options={}, &block
|
9
|
+
method = options.delete(:method) || 'paginate'
|
10
|
+
self.index.import self, method, options, &block
|
11
|
+
end
|
12
|
+
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
module Slingshot
|
2
|
+
module Model
|
3
|
+
|
4
|
+
module Indexing
|
5
|
+
|
6
|
+
module ClassMethods
|
7
|
+
|
8
|
+
def mapping
|
9
|
+
if block_given?
|
10
|
+
@store_mapping = true
|
11
|
+
yield
|
12
|
+
@store_mapping = false
|
13
|
+
create_index_or_update_mapping
|
14
|
+
else
|
15
|
+
@mapping ||= {}
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def indexes(name, options = {})
|
20
|
+
# p "#{self}, SEARCH PROPERTY, #{name}"
|
21
|
+
mapping[name] = options
|
22
|
+
end
|
23
|
+
|
24
|
+
def store_mapping?
|
25
|
+
@store_mapping || false
|
26
|
+
end
|
27
|
+
|
28
|
+
def create_index_or_update_mapping
|
29
|
+
# STDERR.puts "Creating index with mapping", mapping_to_hash.inspect
|
30
|
+
# STDERR.puts "Index exists?, #{index.exists?}"
|
31
|
+
unless index.exists?
|
32
|
+
index.create :mappings => mapping_to_hash
|
33
|
+
else
|
34
|
+
# TODO: Update mapping
|
35
|
+
end
|
36
|
+
rescue Exception => e
|
37
|
+
# TODO: STDERR + logger
|
38
|
+
raise
|
39
|
+
end
|
40
|
+
|
41
|
+
def mapping_to_hash
|
42
|
+
{ document_type.to_sym => { :properties => mapping } }
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Slingshot
|
2
|
+
module Model
|
3
|
+
|
4
|
+
module Naming
|
5
|
+
|
6
|
+
module ClassMethods
|
7
|
+
def index_name name=nil
|
8
|
+
@index_name = name if name
|
9
|
+
@index_name || model_name.plural
|
10
|
+
end
|
11
|
+
|
12
|
+
def document_type
|
13
|
+
model_name.singular
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
module InstanceMethods
|
18
|
+
def index_name
|
19
|
+
self.class.index_name
|
20
|
+
end
|
21
|
+
|
22
|
+
def document_type
|
23
|
+
self.class.document_type
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module Slingshot
|
2
|
+
module Model
|
3
|
+
|
4
|
+
module Persistence
|
5
|
+
|
6
|
+
def self.included(base)
|
7
|
+
|
8
|
+
base.class_eval do
|
9
|
+
include ActiveModel::AttributeMethods
|
10
|
+
include ActiveModel::Validations
|
11
|
+
include ActiveModel::Serialization
|
12
|
+
include ActiveModel::Serializers::JSON
|
13
|
+
include ActiveModel::Naming
|
14
|
+
include ActiveModel::Conversion
|
15
|
+
|
16
|
+
extend ActiveModel::Callbacks
|
17
|
+
define_model_callbacks :save, :destroy
|
18
|
+
|
19
|
+
include Slingshot::Model::Search
|
20
|
+
include Slingshot::Model::Callbacks
|
21
|
+
|
22
|
+
extend Persistence::Finders::ClassMethods
|
23
|
+
extend Persistence::Attributes::ClassMethods
|
24
|
+
include Persistence::Attributes::InstanceMethods
|
25
|
+
|
26
|
+
include Persistence::Storage
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
module Slingshot
|
2
|
+
module Model
|
3
|
+
|
4
|
+
module Persistence
|
5
|
+
|
6
|
+
module Attributes
|
7
|
+
|
8
|
+
module ClassMethods
|
9
|
+
|
10
|
+
def property(name, options = {})
|
11
|
+
# p "#{self}, PERSISTENCE PROPERTY, #{name}"
|
12
|
+
attr_accessor name.to_sym
|
13
|
+
properties << name.to_s unless properties.include?(name.to_s)
|
14
|
+
define_query_method name.to_sym
|
15
|
+
define_attribute_methods [name.to_sym]
|
16
|
+
mapping[name] = options if store_mapping?
|
17
|
+
self
|
18
|
+
end
|
19
|
+
|
20
|
+
def properties
|
21
|
+
@properties ||= []
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def define_query_method name
|
27
|
+
define_method("#{name}?") { !! send(name) }
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
|
32
|
+
module InstanceMethods
|
33
|
+
|
34
|
+
attr_accessor :id
|
35
|
+
|
36
|
+
def initialize(attributes={})
|
37
|
+
attributes.each { |name, value| send("#{name}=", value) }
|
38
|
+
end
|
39
|
+
|
40
|
+
def attributes
|
41
|
+
self.class.properties.
|
42
|
+
inject( self.id ? {'id' => self.id} : {} ) {|attributes, key| attributes[key] = send(key); attributes}
|
43
|
+
end
|
44
|
+
|
45
|
+
def attribute_names
|
46
|
+
self.class.properties.sort
|
47
|
+
end
|
48
|
+
|
49
|
+
def has_attribute?(name)
|
50
|
+
properties.include?(name.to_s)
|
51
|
+
end
|
52
|
+
alias :has_property? :has_attribute?
|
53
|
+
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|