sensei-rb 0.1.0
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 +53 -0
- data/.travis.yml +17 -0
- data/Gemfile +2 -0
- data/MIT-LICENSE +20 -0
- data/README.md +11 -0
- data/Rakefile +10 -0
- data/lib/sensei/client.rb +243 -0
- data/lib/sensei/query.rb +230 -0
- data/lib/sensei/version.rb +3 -0
- data/lib/sensei-rb.rb +2 -0
- data/sensei-rb.gemspec +23 -0
- data/spec/fixtures/sensei-rails.yml +10 -0
- data/spec/fixtures/sensei-rails.yml.erb +10 -0
- data/spec/fixtures/sensei.yml +6 -0
- data/spec/fixtures/sensei.yml.erb +6 -0
- data/spec/sensei/client_spec.rb +50 -0
- data/spec/spec_helper.rb +18 -0
- metadata +132 -0
data/.gitignore
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
*.rbc
|
|
2
|
+
*.sassc
|
|
3
|
+
.sass-cache
|
|
4
|
+
capybara-*.html
|
|
5
|
+
.idea
|
|
6
|
+
.rspec
|
|
7
|
+
/.bundle
|
|
8
|
+
/vendor/bundle
|
|
9
|
+
/log/*
|
|
10
|
+
/tmp
|
|
11
|
+
.DS_Store
|
|
12
|
+
.vagrant
|
|
13
|
+
.tm_properties
|
|
14
|
+
/vendor/qbes
|
|
15
|
+
.vagrant
|
|
16
|
+
.tm_properties
|
|
17
|
+
|
|
18
|
+
/db/*.sqlite3
|
|
19
|
+
/public/system/*
|
|
20
|
+
/coverage/
|
|
21
|
+
/spec/tmp/*
|
|
22
|
+
**.orig
|
|
23
|
+
rerun.txt
|
|
24
|
+
pickle-email-*.html
|
|
25
|
+
*.js.js
|
|
26
|
+
app/assets/stylesheets/application.css
|
|
27
|
+
*.swp
|
|
28
|
+
*.iml
|
|
29
|
+
.pt
|
|
30
|
+
*.rdb
|
|
31
|
+
Vagrantfile
|
|
32
|
+
db/structure.sql
|
|
33
|
+
|
|
34
|
+
# JetBrains IDE files
|
|
35
|
+
.idea
|
|
36
|
+
*.iml
|
|
37
|
+
*.iws
|
|
38
|
+
piston.ipr
|
|
39
|
+
|
|
40
|
+
# Zeus
|
|
41
|
+
custom_plan.rb
|
|
42
|
+
zeus.json
|
|
43
|
+
|
|
44
|
+
# Ignore simplecov output
|
|
45
|
+
coverage
|
|
46
|
+
|
|
47
|
+
# Ignore ci_reporter output
|
|
48
|
+
spec/reports
|
|
49
|
+
|
|
50
|
+
# Ignore local config
|
|
51
|
+
.ruby-version
|
|
52
|
+
|
|
53
|
+
Gemfile.lock
|
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/MIT-LICENSE
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Copyright (c) 2012 Identified
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
4
|
+
a copy of this software and associated documentation files (the
|
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
|
9
|
+
the following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice shall be
|
|
12
|
+
included in all copies or substantial portions of the Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
sensei-rb
|
|
2
|
+
=========
|
|
3
|
+
|
|
4
|
+
[](http://travis-ci.org/Identified/sensei-rb?branch=master)
|
|
5
|
+
|
|
6
|
+
Install
|
|
7
|
+
-------
|
|
8
|
+
Include the gem in your Gemfile:
|
|
9
|
+
``` ruby
|
|
10
|
+
gem 'sensei-rb', '~> 0.1.0'
|
|
11
|
+
```
|
data/Rakefile
ADDED
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
# require 'erb'
|
|
2
|
+
require 'yaml'
|
|
3
|
+
|
|
4
|
+
module Sensei
|
|
5
|
+
class HTTPBadResponse < StandardError
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
class Client
|
|
9
|
+
cattr_accessor :sensei_hosts, :sensei_port, :http_kafka_port, :uid_key, :http_kafka_hosts, :fake_update
|
|
10
|
+
attr_accessor :search_timeout
|
|
11
|
+
|
|
12
|
+
DATA_TRANSACTION_KEY = "sensei_client_data_transaction"
|
|
13
|
+
TEST_TRANSACTION_KEY = "sensei_client_test_transaction"
|
|
14
|
+
|
|
15
|
+
def self.configure(path = "config/sensei.yml")
|
|
16
|
+
if File.exists? path
|
|
17
|
+
config = YAML.load(ERB.new(File.read(path)).result)
|
|
18
|
+
|
|
19
|
+
# Limit config to specific environment if Rails is defined
|
|
20
|
+
defined? ::Rails and
|
|
21
|
+
config = config[::Rails.env]
|
|
22
|
+
|
|
23
|
+
self.sensei_hosts = config['sensei_hosts']
|
|
24
|
+
self.sensei_port = config['sensei_port']
|
|
25
|
+
self.http_kafka_port = config['http_kafka_port']
|
|
26
|
+
self.uid_key = config['uid_key']
|
|
27
|
+
self.http_kafka_hosts = config['http_kafka_hosts']
|
|
28
|
+
self.fake_update = config['fake_update'] || false
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
yield self if block_given?
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def self.current_data_transaction
|
|
36
|
+
Thread.current[DATA_TRANSACTION_KEY].last
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def self.current_test_transaction
|
|
40
|
+
Thread.current[TEST_TRANSACTION_KEY].last
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def self.begin_transaction key
|
|
44
|
+
Thread.current[key] ||= []
|
|
45
|
+
Thread.current[key] << []
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def self.in_sensei_transaction? key
|
|
49
|
+
Thread.current[key] ||= []
|
|
50
|
+
Thread.current[key].count > 0
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# This does a "data transaction," in which any update events will get
|
|
54
|
+
# buffered until the block is finished, after which everything gets sent.
|
|
55
|
+
def self.transaction &block
|
|
56
|
+
begin
|
|
57
|
+
begin_transaction DATA_TRANSACTION_KEY
|
|
58
|
+
block.call
|
|
59
|
+
kafka_commit(current_data_transaction)
|
|
60
|
+
ensure
|
|
61
|
+
Thread.current[DATA_TRANSACTION_KEY].pop
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def self.test_transaction &block
|
|
66
|
+
begin
|
|
67
|
+
begin_transaction TEST_TRANSACTION_KEY
|
|
68
|
+
block.call
|
|
69
|
+
ensure
|
|
70
|
+
kafka_rollback(current_test_transaction)
|
|
71
|
+
Thread.current[TEST_TRANSACTION_KEY].pop
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Undo all of the data events that just occurred.
|
|
76
|
+
# This is only really useful during tests. Also,
|
|
77
|
+
# it's only capable of rolling back insertions.
|
|
78
|
+
def self.kafka_rollback(data_events)
|
|
79
|
+
to_delete = data_events.select{|x| x[uid_key]}.map{|x| {:_type => '_delete', :_uid => x[uid_key]}}
|
|
80
|
+
kafka_commit to_delete
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def self.in_data_transaction?
|
|
84
|
+
self.in_sensei_transaction? DATA_TRANSACTION_KEY
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def self.in_test_transaction?
|
|
88
|
+
self.in_sensei_transaction? TEST_TRANSACTION_KEY
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def self.sensei_url
|
|
92
|
+
raise unless sensei_hosts
|
|
93
|
+
"http://#{sensei_hosts.sample}:#{sensei_port || 8080}/sensei"
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def initialize optargs={}
|
|
97
|
+
@query = optargs[:query].try(:to_sensei)
|
|
98
|
+
@facets = (optargs[:facets] || {})
|
|
99
|
+
@selections = (optargs[:selections] || {})
|
|
100
|
+
@other_options = optargs.dup.keep_if {|k,v| ![:query, :facets, :selections].member?(k)}
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def self.kafka_send items
|
|
104
|
+
if in_data_transaction?
|
|
105
|
+
current_data_transaction << items
|
|
106
|
+
else
|
|
107
|
+
kafka_commit items
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
if in_test_transaction?
|
|
111
|
+
Thread.current[TEST_TRANSACTION_KEY].last << items
|
|
112
|
+
end
|
|
113
|
+
true
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def self.kafka_commit items
|
|
117
|
+
if !fake_update
|
|
118
|
+
req = Curl::Easy.new("http://#{http_kafka_hosts.sample}:#{http_kafka_port}/")
|
|
119
|
+
req.http_post(items.map(&:to_json).join("\n"))
|
|
120
|
+
raise Sensei::HTTPBadResponse, "Kafka url=#{req.url}, response_code=#{req.response_code}, response_body=#{req.body_str}" if req.response_code != 200
|
|
121
|
+
req.body_str
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def self.delete uids
|
|
126
|
+
kafka_send(uids.map do |uid|
|
|
127
|
+
{:type => 'delete', :uid => uid.to_s}
|
|
128
|
+
end)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def self.update(documents)
|
|
132
|
+
kafka_send documents
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
DEFAULT_FACET_OPTIONS = {:max => 6, :minCount => 1}
|
|
136
|
+
|
|
137
|
+
# Add a desired facet to the results
|
|
138
|
+
def facet(field, options={})
|
|
139
|
+
@facets[field] = DEFAULT_FACET_OPTIONS.merge(options)
|
|
140
|
+
self
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def relevance(r)
|
|
144
|
+
@relevance = r
|
|
145
|
+
self
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def query(q)
|
|
149
|
+
@query=q.to_sensei
|
|
150
|
+
self
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def all(q)
|
|
154
|
+
@query ? (@query &= q.to_sensei) : (@query = q.to_sensei)
|
|
155
|
+
self
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def any(q)
|
|
159
|
+
@query ? (@query |= q.to_sensei) : (@query = q.to_sensei)
|
|
160
|
+
self
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def not(q)
|
|
164
|
+
@query ? (@query &= q.to_sensei.must_not) : (@query = q.to_sensei.must_not)
|
|
165
|
+
self
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# Do facet selection
|
|
169
|
+
def selection(fields = {})
|
|
170
|
+
@selections.merge!(fields)
|
|
171
|
+
self
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def options(opts = {})
|
|
175
|
+
@other_options.merge!(opts)
|
|
176
|
+
self
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def to_h
|
|
180
|
+
out = {}
|
|
181
|
+
if @query
|
|
182
|
+
out[:query] = @query.to_h
|
|
183
|
+
if @relevance
|
|
184
|
+
out[:query] = Sensei::BoolQuery.new(:operands => [@query], :operation => :must).to_h
|
|
185
|
+
out[:query][:bool][:relevance] = @relevance
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
(out[:facets] = @facets) if @facets.count > 0
|
|
190
|
+
selections = @selections.map { |field, terms| {:terms => {field => {values: terms, :operator => "or"}}} }
|
|
191
|
+
(out[:selections] = selections) if selections.count > 0
|
|
192
|
+
out.merge!(@other_options)
|
|
193
|
+
out
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def self.q h
|
|
197
|
+
h.to_sensei
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def self.construct options={}, &block
|
|
201
|
+
out = self.new(options)
|
|
202
|
+
search_query = class_eval(&block)
|
|
203
|
+
out.query(search_query)
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def search
|
|
207
|
+
req = Curl::Easy.new(self.class.sensei_url)
|
|
208
|
+
req.timeout = self.search_timeout
|
|
209
|
+
req.http_post(self.to_h.to_json)
|
|
210
|
+
raise Sensei::HTTPBadResponse, "url=#{req.url}, response_code=#{req.response_code}, response_body=#{req.body_str}" if req.response_code != 200
|
|
211
|
+
JSON.parse(req.body_str)
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# This method performs several separate queries with different
|
|
215
|
+
# selection settings as necessary. This is needed to perform
|
|
216
|
+
# the common interaction pattern for faceted search, in which
|
|
217
|
+
# it is desired that selections from other facets affect a
|
|
218
|
+
# particular facet's counts, but a facet's own selections do
|
|
219
|
+
# facet do not affect its own counts.
|
|
220
|
+
def select_search
|
|
221
|
+
all_selection_results = search
|
|
222
|
+
facet_requests.map(&:search).each do |result|
|
|
223
|
+
field, counts = result['facets'].first
|
|
224
|
+
all_selection_results['facets'][field] += counts
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
all_selection_results['facets'] = Hash[*all_selection_results['facets'].map do |k,v|
|
|
228
|
+
[k, v.uniq_by{|x| x['value']}]
|
|
229
|
+
end.flatten(1)]
|
|
230
|
+
all_selection_results
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
# This method builds the requests necessary to perform the `select_search' method.
|
|
234
|
+
def facet_requests
|
|
235
|
+
@selections.map do |field, values|
|
|
236
|
+
Sensei::Client.new(:query => @query,
|
|
237
|
+
:facets => @facets.dup.keep_if {|name, opts| name==field},
|
|
238
|
+
:selections => @selections.dup.keep_if {|name, opts| name != field},
|
|
239
|
+
:size => 0)
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
end
|
data/lib/sensei/query.rb
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
# Query DSL for SenseiDB
|
|
2
|
+
# The basic grammar is as follows:
|
|
3
|
+
|
|
4
|
+
# query := q(field => value) (produces a term query)
|
|
5
|
+
# / q(field => [values ...]) (produces a boolean query composed of
|
|
6
|
+
# the OR of {field => value} queries for each value)
|
|
7
|
+
# / q(field => (start..end)) (produces a range query on field between start and end)
|
|
8
|
+
# / query & query (ANDs two subqueries together)
|
|
9
|
+
# / query | query (ORs two subqueries together)
|
|
10
|
+
#
|
|
11
|
+
# value := something that should probably be a string, but might work if it isn't
|
|
12
|
+
#
|
|
13
|
+
# Note: use of the `q' operator must be performed within the context of
|
|
14
|
+
# a Sensei::Query.construct block, i.e.
|
|
15
|
+
|
|
16
|
+
# Sensei::Query.construct do
|
|
17
|
+
# (q(:foo => (15..30)) & q(:bar => '1')).boost!(10) | q(:baz => 'wiz')
|
|
18
|
+
# end
|
|
19
|
+
|
|
20
|
+
# If you're not in a construct block, you can still do Sensei::Query.q(...).
|
|
21
|
+
|
|
22
|
+
module Sensei
|
|
23
|
+
module Operators
|
|
24
|
+
def &(x)
|
|
25
|
+
return self if self == x
|
|
26
|
+
return self if x.is_a? EmptyQuery
|
|
27
|
+
BoolQuery.new(:operands => [self.to_sensei, x.to_sensei], :operation => :must)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def |(x)
|
|
31
|
+
return self if self == x
|
|
32
|
+
return self if x.is_a? EmptyQuery
|
|
33
|
+
BoolQuery.new(:operands => [self.to_sensei, x.to_sensei], :operation => :should)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def ~
|
|
37
|
+
self.must_not
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def *(x)
|
|
41
|
+
self.boost!(x)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def must_not
|
|
45
|
+
BoolQuery.new(:operands => [self.to_sensei], :operation => :must_not)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def boost! amt
|
|
49
|
+
self.to_sensei.tap do |x| x.options[:boost] = amt end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
class Query
|
|
54
|
+
attr_accessor :options
|
|
55
|
+
cattr_accessor :result_klass
|
|
56
|
+
|
|
57
|
+
include Operators
|
|
58
|
+
|
|
59
|
+
def initialize(opts={})
|
|
60
|
+
@options = opts
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def get_boost
|
|
64
|
+
options[:boost] ? {:boost => options[:boost]} : {}
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def to_sensei
|
|
68
|
+
self
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def self.construct &block
|
|
72
|
+
class_eval(&block)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def self.q(h)
|
|
76
|
+
h.to_sensei
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def not_query?
|
|
80
|
+
self.is_a?(Sensei::BoolQuery) && options[:operation] == :must_not
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def run(options = {})
|
|
84
|
+
results = Sensei::Client.new(options.merge(:query => self)).search
|
|
85
|
+
if @@result_klass
|
|
86
|
+
@@result_klass.new(results)
|
|
87
|
+
else
|
|
88
|
+
results
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
class BoolQuery < Query
|
|
94
|
+
def operands
|
|
95
|
+
options[:operands]
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def to_h
|
|
99
|
+
if self.not_query?
|
|
100
|
+
raise Exception, "Error: independent boolean NOT query not allowed."
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
not_queries, non_not_queries = operands.partition(&:not_query?)
|
|
104
|
+
not_queries = not_queries.map{|x| x.operands.map(&:to_h)}.flatten
|
|
105
|
+
|
|
106
|
+
non_not_queries = non_not_queries.reject{|x| x.is_a? AllQuery} if options[:operation] == :must
|
|
107
|
+
|
|
108
|
+
subqueries = non_not_queries.map(&:to_h)
|
|
109
|
+
mergeable, nonmergeable = subqueries.partition do |x|
|
|
110
|
+
isbool = x[:bool]
|
|
111
|
+
sameop = isbool && isbool[options[:operation]]
|
|
112
|
+
boosted = isbool && isbool[:boost]
|
|
113
|
+
isbool && sameop && (boosted.nil? || boosted == options[:boost])
|
|
114
|
+
end
|
|
115
|
+
merged_queries = mergeable.map{|x| x[:bool][options[:operation]]}.flatten(1)
|
|
116
|
+
merged_nots = mergeable.map{|x| x[:bool][:must_not] || []}.flatten(1)
|
|
117
|
+
|
|
118
|
+
all_nots = merged_nots + not_queries
|
|
119
|
+
not_clause = (all_nots.count > 0 ? {:must_not => all_nots} : {})
|
|
120
|
+
|
|
121
|
+
{:bool => {
|
|
122
|
+
options[:operation] => nonmergeable + merged_queries
|
|
123
|
+
}.merge(get_boost).merge(not_clause)
|
|
124
|
+
}
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
class TermQuery < Query
|
|
129
|
+
def to_h
|
|
130
|
+
{:term => {options[:field] => {:value => options[:value].to_s}.merge(get_boost)}}
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
class TermsQuery < Query
|
|
135
|
+
def to_h
|
|
136
|
+
{:terms => {options[:field] => {:values => options[:values].map(&:to_s)}.merge(get_boost)}}
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
class RangeQuery < Query
|
|
141
|
+
def to_h
|
|
142
|
+
{:range => {
|
|
143
|
+
options[:field] => {
|
|
144
|
+
:from => options[:from],
|
|
145
|
+
:to => options[:to],
|
|
146
|
+
:_type => options[:type] || ((options[:from].is_a?(Float) || options[:to].is_a?(Float)) ? "double" : "float")
|
|
147
|
+
}.merge(get_boost).merge(options[:type] == :date ? {:_date_format => options[:date_format] || 'YYYY-MM-DD'} : {})
|
|
148
|
+
},
|
|
149
|
+
}
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
class EmptyQuery < Query
|
|
154
|
+
|
|
155
|
+
def &(x)
|
|
156
|
+
x
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def |(x)
|
|
160
|
+
x
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def ~
|
|
164
|
+
raise 'Should not call on an empty query'
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def *(x)
|
|
168
|
+
raise 'Should not call on an empty query'
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def must_not
|
|
172
|
+
raise 'Should not call on an empty query'
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def boost! amt
|
|
176
|
+
raise 'Should not call on an empty query'
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def to_h
|
|
180
|
+
{}
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
class AllQuery < Query
|
|
185
|
+
def to_h
|
|
186
|
+
{:match_all => {}.merge(get_boost)}
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
class UIDQuery < Query
|
|
191
|
+
def initialize(uids)
|
|
192
|
+
uids = [uids] unless uids.is_a?(Array)
|
|
193
|
+
@uids = uids
|
|
194
|
+
end
|
|
195
|
+
def to_h
|
|
196
|
+
{:ids => {:values => @uids}}
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
class Hash
|
|
202
|
+
def to_sensei
|
|
203
|
+
field, value = self.first
|
|
204
|
+
if [String, Fixnum, Float, Bignum].member?(value.class)
|
|
205
|
+
Sensei::TermQuery.new(:field => field, :value => value)
|
|
206
|
+
else
|
|
207
|
+
value.to_sensei(field)
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
class Range
|
|
213
|
+
def to_sensei(field)
|
|
214
|
+
Sensei::RangeQuery.new(:from => self.begin, :to => self.end, :field => field)
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
class Array
|
|
219
|
+
def to_sensei(field, op=:should)
|
|
220
|
+
if op == :should
|
|
221
|
+
if self.length == 1
|
|
222
|
+
Sensei::TermQuery.new(:field => field, :value => self.first)
|
|
223
|
+
else
|
|
224
|
+
Sensei::TermsQuery.new(:field => field, :values => self)
|
|
225
|
+
end
|
|
226
|
+
else
|
|
227
|
+
Sensei::BoolQuery.new(:operation => op, :operands => self.map{|value| {field => value}.to_sensei})
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
end
|
data/lib/sensei-rb.rb
ADDED
data/sensei-rb.gemspec
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
$:.unshift File.expand_path("../lib", __FILE__)
|
|
2
|
+
require "sensei/version"
|
|
3
|
+
|
|
4
|
+
Gem::Specification.new do |s|
|
|
5
|
+
s.name = 'sensei-rb'
|
|
6
|
+
s.version = '0.1.0'
|
|
7
|
+
s.date = '2012-04-20'
|
|
8
|
+
s.summary = "Ruby client for SenseiDB"
|
|
9
|
+
s.description = "A ruby client for SenseiDB."
|
|
10
|
+
s.authors = ["Identified"]
|
|
11
|
+
s.email = 'engineers@identified.com'
|
|
12
|
+
s.homepage = 'https://github.com/Identified/sensei-rb'
|
|
13
|
+
s.license = 'MIT'
|
|
14
|
+
|
|
15
|
+
s.add_dependency('activesupport')
|
|
16
|
+
s.add_development_dependency('bundler')
|
|
17
|
+
s.add_development_dependency('rake')
|
|
18
|
+
s.add_development_dependency('rspec', '~> 2.13.0')
|
|
19
|
+
|
|
20
|
+
s.files = `git ls-files`.split("\n")
|
|
21
|
+
s.test_files = `git ls-files -- {spec,features}/*`.split("\n")
|
|
22
|
+
s.require_paths = ["lib"]
|
|
23
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
require "spec_helper"
|
|
2
|
+
|
|
3
|
+
describe Sensei::Client do
|
|
4
|
+
shared_examples "a configured client" do
|
|
5
|
+
subject { Sensei::Client }
|
|
6
|
+
|
|
7
|
+
describe "::configure" do
|
|
8
|
+
it "sets the configs from the yaml file into its class variables" do
|
|
9
|
+
subject.configure(file_path)
|
|
10
|
+
expect(Sensei::Client.sensei_hosts).to match_array(["localhost"])
|
|
11
|
+
expect(Sensei::Client.sensei_port).to eq(8080)
|
|
12
|
+
expect(Sensei::Client.http_kafka_port).to eq(9876)
|
|
13
|
+
expect(Sensei::Client.http_kafka_hosts).to match_array(["localhost"])
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
context "without RAILS_ENV" do
|
|
19
|
+
context "without ERB" do
|
|
20
|
+
it_behaves_like "a configured client" do
|
|
21
|
+
let(:file_path) { File.dirname(__FILE__) + '/../fixtures/sensei.yml' }
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
context "with ERB" do
|
|
26
|
+
it_behaves_like "a configured client" do
|
|
27
|
+
let(:file_path) { File.dirname(__FILE__) + '/../fixtures/sensei.yml.erb' }
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
context "with RAILS_ENV" do
|
|
34
|
+
before { Rails = Struct.new(:env).new("some_rails_env") }
|
|
35
|
+
after { Object.send(:remove_const, :Rails) }
|
|
36
|
+
|
|
37
|
+
context "without ERB" do
|
|
38
|
+
it_behaves_like "a configured client" do
|
|
39
|
+
let(:file_path) { File.dirname(__FILE__) + '/../fixtures/sensei-rails.yml' }
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
context "with ERB" do
|
|
44
|
+
it_behaves_like "a configured client" do
|
|
45
|
+
let(:file_path) { File.dirname(__FILE__) + '/../fixtures/sensei-rails.yml.erb' }
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
end
|
data/spec/spec_helper.rb
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
$:.unshift File.expand_path("../lib", __FILE__)
|
|
2
|
+
require 'active_support/all'
|
|
3
|
+
require 'sensei/client'
|
|
4
|
+
|
|
5
|
+
Dir["./spec/support/**/*.rb"].sort.each { |f| require f }
|
|
6
|
+
|
|
7
|
+
RSpec.configure do |config|
|
|
8
|
+
config.treat_symbols_as_metadata_keys_with_true_values = true
|
|
9
|
+
config.run_all_when_everything_filtered = true
|
|
10
|
+
config.filter_run :focus
|
|
11
|
+
|
|
12
|
+
# Run specs in random order to surface order dependencies. If you find an
|
|
13
|
+
# order dependency and want to debug it, you can fix the order by providing
|
|
14
|
+
# the seed, which is printed after each run.
|
|
15
|
+
# --seed 1234
|
|
16
|
+
config.order = 'random'
|
|
17
|
+
|
|
18
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: sensei-rb
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
prerelease:
|
|
6
|
+
platform: ruby
|
|
7
|
+
authors:
|
|
8
|
+
- Identified
|
|
9
|
+
autorequire:
|
|
10
|
+
bindir: bin
|
|
11
|
+
cert_chain: []
|
|
12
|
+
date: 2012-04-20 00:00:00.000000000 Z
|
|
13
|
+
dependencies:
|
|
14
|
+
- !ruby/object:Gem::Dependency
|
|
15
|
+
name: activesupport
|
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
|
17
|
+
none: false
|
|
18
|
+
requirements:
|
|
19
|
+
- - ! '>='
|
|
20
|
+
- !ruby/object:Gem::Version
|
|
21
|
+
version: '0'
|
|
22
|
+
type: :runtime
|
|
23
|
+
prerelease: false
|
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
25
|
+
none: false
|
|
26
|
+
requirements:
|
|
27
|
+
- - ! '>='
|
|
28
|
+
- !ruby/object:Gem::Version
|
|
29
|
+
version: '0'
|
|
30
|
+
- !ruby/object:Gem::Dependency
|
|
31
|
+
name: bundler
|
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
|
33
|
+
none: false
|
|
34
|
+
requirements:
|
|
35
|
+
- - ! '>='
|
|
36
|
+
- !ruby/object:Gem::Version
|
|
37
|
+
version: '0'
|
|
38
|
+
type: :development
|
|
39
|
+
prerelease: false
|
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
41
|
+
none: false
|
|
42
|
+
requirements:
|
|
43
|
+
- - ! '>='
|
|
44
|
+
- !ruby/object:Gem::Version
|
|
45
|
+
version: '0'
|
|
46
|
+
- !ruby/object:Gem::Dependency
|
|
47
|
+
name: rake
|
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
|
49
|
+
none: false
|
|
50
|
+
requirements:
|
|
51
|
+
- - ! '>='
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '0'
|
|
54
|
+
type: :development
|
|
55
|
+
prerelease: false
|
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
57
|
+
none: false
|
|
58
|
+
requirements:
|
|
59
|
+
- - ! '>='
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: '0'
|
|
62
|
+
- !ruby/object:Gem::Dependency
|
|
63
|
+
name: rspec
|
|
64
|
+
requirement: !ruby/object:Gem::Requirement
|
|
65
|
+
none: false
|
|
66
|
+
requirements:
|
|
67
|
+
- - ~>
|
|
68
|
+
- !ruby/object:Gem::Version
|
|
69
|
+
version: 2.13.0
|
|
70
|
+
type: :development
|
|
71
|
+
prerelease: false
|
|
72
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
73
|
+
none: false
|
|
74
|
+
requirements:
|
|
75
|
+
- - ~>
|
|
76
|
+
- !ruby/object:Gem::Version
|
|
77
|
+
version: 2.13.0
|
|
78
|
+
description: A ruby client for SenseiDB.
|
|
79
|
+
email: engineers@identified.com
|
|
80
|
+
executables: []
|
|
81
|
+
extensions: []
|
|
82
|
+
extra_rdoc_files: []
|
|
83
|
+
files:
|
|
84
|
+
- .gitignore
|
|
85
|
+
- .travis.yml
|
|
86
|
+
- Gemfile
|
|
87
|
+
- MIT-LICENSE
|
|
88
|
+
- README.md
|
|
89
|
+
- Rakefile
|
|
90
|
+
- lib/sensei-rb.rb
|
|
91
|
+
- lib/sensei/client.rb
|
|
92
|
+
- lib/sensei/query.rb
|
|
93
|
+
- lib/sensei/version.rb
|
|
94
|
+
- sensei-rb.gemspec
|
|
95
|
+
- spec/fixtures/sensei-rails.yml
|
|
96
|
+
- spec/fixtures/sensei-rails.yml.erb
|
|
97
|
+
- spec/fixtures/sensei.yml
|
|
98
|
+
- spec/fixtures/sensei.yml.erb
|
|
99
|
+
- spec/sensei/client_spec.rb
|
|
100
|
+
- spec/spec_helper.rb
|
|
101
|
+
homepage: https://github.com/Identified/sensei-rb
|
|
102
|
+
licenses:
|
|
103
|
+
- MIT
|
|
104
|
+
post_install_message:
|
|
105
|
+
rdoc_options: []
|
|
106
|
+
require_paths:
|
|
107
|
+
- lib
|
|
108
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
109
|
+
none: false
|
|
110
|
+
requirements:
|
|
111
|
+
- - ! '>='
|
|
112
|
+
- !ruby/object:Gem::Version
|
|
113
|
+
version: '0'
|
|
114
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
115
|
+
none: false
|
|
116
|
+
requirements:
|
|
117
|
+
- - ! '>='
|
|
118
|
+
- !ruby/object:Gem::Version
|
|
119
|
+
version: '0'
|
|
120
|
+
requirements: []
|
|
121
|
+
rubyforge_project:
|
|
122
|
+
rubygems_version: 1.8.23
|
|
123
|
+
signing_key:
|
|
124
|
+
specification_version: 3
|
|
125
|
+
summary: Ruby client for SenseiDB
|
|
126
|
+
test_files:
|
|
127
|
+
- spec/fixtures/sensei-rails.yml
|
|
128
|
+
- spec/fixtures/sensei-rails.yml.erb
|
|
129
|
+
- spec/fixtures/sensei.yml
|
|
130
|
+
- spec/fixtures/sensei.yml.erb
|
|
131
|
+
- spec/sensei/client_spec.rb
|
|
132
|
+
- spec/spec_helper.rb
|