slingshot-rb 0.0.5 → 0.0.6
Sign up to get free protection for your applications and to get access to all the features.
- data/README.markdown +49 -26
- data/examples/dsl.rb +8 -5
- data/lib/slingshot.rb +1 -0
- data/lib/slingshot/search.rb +17 -7
- data/lib/slingshot/search/filter.rb +29 -0
- data/lib/slingshot/version.rb +1 -1
- data/test/integration/filters_test.rb +46 -0
- data/test/unit/search_facet_test.rb +1 -1
- data/test/unit/search_filter_test.rb +33 -0
- data/test/unit/search_test.rb +22 -0
- metadata +9 -4
data/README.markdown
CHANGED
@@ -3,15 +3,15 @@ Slingshot
|
|
3
3
|
|
4
4
|
![Slingshot](https://github.com/karmi/slingshot/raw/master/slingshot.png)
|
5
5
|
|
6
|
-
_Slingshot_ aims to provide
|
6
|
+
_Slingshot_ aims to provide rich and comfortable Ruby API and DSL for the
|
7
7
|
[ElasticSearch](http://www.elasticsearch.org/) search engine/database.
|
8
8
|
|
9
9
|
_ElasticSearch_ is a scalable, distributed, highly-available,
|
10
10
|
RESTful database communicating by JSON over HTTP, based on [Lucene](http://lucene.apache.org/),
|
11
|
-
written in Java. It manages to very simple and very powerful at the same time.
|
11
|
+
written in Java. It manages to be very simple and very powerful at the same time.
|
12
12
|
You should seriously consider it to power search in your Ruby applications:
|
13
13
|
it will deliver all the features you want — and many more you may have not
|
14
|
-
imagined yet (native geo search? histogram facets
|
14
|
+
imagined yet (native geo search? date histogram facets? _percolator_?)
|
15
15
|
|
16
16
|
_Slingshot_ currently allows basic operation with the index and searching. More is planned.
|
17
17
|
|
@@ -44,7 +44,7 @@ Plans for full ActiveModel integration (and other convenience layers) are in pro
|
|
44
44
|
(see the [`activemodel`](https://github.com/karmi/slingshot/compare/activemodel) branch).
|
45
45
|
|
46
46
|
To kick the tires, require the gem in an IRB session or a Ruby script
|
47
|
-
(note that you can run the full example from [`examples/dsl.rb`](https://github.com/karmi/slingshot/blob/master/examples/dsl.rb)):
|
47
|
+
(note that you can just run the full example from [`examples/dsl.rb`](https://github.com/karmi/slingshot/blob/master/examples/dsl.rb)):
|
48
48
|
|
49
49
|
require 'rubygems'
|
50
50
|
require 'slingshot'
|
@@ -63,20 +63,36 @@ First, let's create an index named `articles` and store/index some documents:
|
|
63
63
|
refresh
|
64
64
|
end
|
65
65
|
|
66
|
-
|
66
|
+
You can also create the
|
67
|
+
index with specific [mappings](http://www.elasticsearch.org/guide/reference/api/admin-indices-create-index.html), such as:
|
67
68
|
|
68
|
-
|
69
|
-
|
69
|
+
Slingshot.index 'articles' do
|
70
|
+
create :mappings => {
|
71
|
+
:article => {
|
72
|
+
:properties => {
|
73
|
+
:id => { :type => 'string', :index => 'not_analyzed', :include_in_all => false },
|
74
|
+
:title => { :type => 'string', :boost => 2.0, :analyzer => 'snowball' },
|
75
|
+
:tags => { :type => 'string', :analyzer => 'keyword' },
|
76
|
+
:content => { :type => 'string', :analyzer => 'snowball' }
|
77
|
+
}
|
78
|
+
}
|
79
|
+
}
|
80
|
+
end
|
81
|
+
|
82
|
+
Now, let's query the database.
|
83
|
+
|
84
|
+
We are searching for articles whose `title` begins with letter “T”, sorted by `title` in `descending` order,
|
85
|
+
filtering them for ones tagged “ruby”, and also retrieving some [_facets_](http://www.lucidimagination.com/Community/Hear-from-the-Experts/Articles/Faceted-Search-Solr)
|
70
86
|
from the database:
|
71
87
|
|
72
88
|
s = Slingshot.search 'articles' do
|
73
89
|
query do
|
74
|
-
|
90
|
+
string 'title:T*'
|
75
91
|
end
|
76
92
|
|
77
|
-
|
78
|
-
|
79
|
-
|
93
|
+
filter :terms, :tags => ['ruby']
|
94
|
+
|
95
|
+
sort { title 'desc' }
|
80
96
|
|
81
97
|
facet 'global-tags' do
|
82
98
|
terms :tags, :global => true
|
@@ -90,14 +106,12 @@ from the database:
|
|
90
106
|
Let's display the results:
|
91
107
|
|
92
108
|
s.results.each do |document|
|
93
|
-
puts "* #{ document.title }"
|
109
|
+
puts "* #{ document.title } [tags: #{document.tags.join(', ')}]"
|
94
110
|
end
|
95
111
|
|
96
|
-
# * Two
|
97
|
-
# * One
|
98
|
-
# * Four
|
112
|
+
# * Two [tags: ruby, python]
|
99
113
|
|
100
|
-
Let's display the facets (distribution of tags across the whole database):
|
114
|
+
Let's display the global facets (distribution of tags across the whole database):
|
101
115
|
|
102
116
|
s.results.facets['global-tags']['terms'].each do |f|
|
103
117
|
puts "#{f['term'].ljust(10)} #{f['count']}"
|
@@ -107,18 +121,28 @@ Let's display the facets (distribution of tags across the whole database):
|
|
107
121
|
# python 1
|
108
122
|
# php 1
|
109
123
|
# java 1
|
110
|
-
|
124
|
+
|
125
|
+
Now, let's display the facets based on current query (notice that count for articles
|
126
|
+
tagged with 'java' is included, even though it's not returned by our query;
|
127
|
+
count for articles tagged 'php' is excluded, since they don't match the current query):
|
128
|
+
|
129
|
+
s.results.facets['current-tags']['terms'].each do |f|
|
130
|
+
puts "#{f['term'].ljust(10)} #{f['count']}"
|
131
|
+
end
|
132
|
+
|
133
|
+
# ruby 1
|
134
|
+
# python 1
|
135
|
+
# java 1
|
136
|
+
|
111
137
|
We can display the full query JSON:
|
112
138
|
|
113
139
|
puts s.to_json
|
114
|
-
# {"facets":{"current-tags":{"terms":{"field":"tags"}},"global-tags":{"global":true,"terms":{"field":"tags"}}},"
|
115
|
-
|
116
|
-
See, a Ruby DSL for this thing is kinda handy?
|
140
|
+
# {"facets":{"current-tags":{"terms":{"field":"tags"}},"global-tags":{"global":true,"terms":{"field":"tags"}}},"query":{"query_string":{"query":"title:T*"}},"filter":{"terms":{"tags":["ruby"]}},"sort":[{"title":"desc"}]}
|
117
141
|
|
118
|
-
|
142
|
+
Or, we can display the corresponding `curl` command for easy debugging:
|
119
143
|
|
120
144
|
puts s.to_curl
|
121
|
-
# curl -X POST "http://localhost:9200/articles/_search?pretty=true" -d '{"facets":{"current-tags":{"terms":{"field":"tags"}},"global-tags":{"global":true,"terms":{"field":"tags"}}},"
|
145
|
+
# curl -X POST "http://localhost:9200/articles/_search?pretty=true" -d '{"facets":{"current-tags":{"terms":{"field":"tags"}},"global-tags":{"global":true,"terms":{"field":"tags"}}},"query":{"query_string":{"query":"title:T*"}},"filter":{"terms":{"tags":["ruby"]}},"sort":[{"title":"desc"}]}'
|
122
146
|
|
123
147
|
|
124
148
|
Features
|
@@ -127,9 +151,11 @@ Features
|
|
127
151
|
Currently, _Slingshot_ supports only a limited subset of vast _ElasticSearch_ [Search API](http://www.elasticsearch.org/guide/reference/api/search/request-body.html) and it's [Query DSL](http://www.elasticsearch.org/guide/reference/query-dsl/):
|
128
152
|
|
129
153
|
* Creating, deleting and refreshing the index
|
154
|
+
* Creating the index with specific [mapping](http://www.elasticsearch.org/guide/reference/api/admin-indices-create-index.html)
|
130
155
|
* Storing a document in the index
|
131
156
|
* [Querying](https://github.com/karmi/slingshot/blob/master/examples/dsl.rb) the index with the `query_string`, `term` and `terms` types of queries
|
132
|
-
* Sorting the results by `fields`
|
157
|
+
* [Sorting](http://elasticsearch.org/guide/reference/api/search/sort.html) the results by `fields`
|
158
|
+
* [Filtering](http://elasticsearch.org/guide/reference/query-dsl/) the results
|
133
159
|
* Retrieving a _terms_ type of [facets](http://www.elasticsearch.org/guide/reference/api/search/facets/index.html) -- other types are high priority
|
134
160
|
* Returning just specific `fields` from documents
|
135
161
|
* Paging with `from` and `size` query options
|
@@ -155,9 +181,6 @@ In order of importance:
|
|
155
181
|
* Dual interface: allow to simply pass queries/options for _ElasticSearch_ as a Hash in any method
|
156
182
|
* [Histogram](http://www.elasticsearch.org/guide/reference/api/search/facets/histogram-facet.html) facets
|
157
183
|
* Seamless support for [auto-updating _river_ index](http://www.elasticsearch.org/guide/reference/river/couchdb.html) for _CouchDB_ `_changes` feed
|
158
|
-
* Infrastructure for query filters
|
159
|
-
* [Range](http://www.elasticsearch.org/guide/reference/query-dsl/range-filter.html) filters and queries
|
160
|
-
* [Geo Filters](http://www.elasticsearch.org/blog/2010/08/16/geo_location_and_search.html) for queries
|
161
184
|
* [Statistical](http://www.elasticsearch.org/guide/reference/api/search/facets/statistical-facet.html) facets
|
162
185
|
* [Geo Distance](http://www.elasticsearch.org/guide/reference/api/search/facets/geo-distance-facet.html) facets
|
163
186
|
* [Index aliases](http://www.elasticsearch.org/guide/reference/api/admin-indices-aliases.html) management
|
data/examples/dsl.rb
CHANGED
@@ -29,9 +29,11 @@ end
|
|
29
29
|
|
30
30
|
s = search 'articles' do
|
31
31
|
query do
|
32
|
-
|
32
|
+
string 'T*'
|
33
33
|
end
|
34
34
|
|
35
|
+
filter :terms, :tags => ['ruby']
|
36
|
+
|
35
37
|
sort do
|
36
38
|
title 'desc'
|
37
39
|
end
|
@@ -56,15 +58,16 @@ puts s.to_curl
|
|
56
58
|
|
57
59
|
puts "", "Results:", "-"*80
|
58
60
|
s.results.each_with_index do |document, i|
|
59
|
-
puts "#{i+1}. #{ document.title.ljust(
|
61
|
+
puts "#{i+1}. #{ document.title.ljust(10) } [id] #{document._id}"
|
60
62
|
end
|
61
63
|
|
62
64
|
puts "", "Facets: tags distribution across the whole database:", "-"*80
|
63
65
|
s.results.facets['global-tags']['terms'].each do |f|
|
64
|
-
puts "#{f['term'].ljust(
|
66
|
+
puts "#{f['term'].ljust(13)} #{f['count']}"
|
65
67
|
end
|
66
68
|
|
67
|
-
puts "", "Facets: tags distribution for the current query",
|
69
|
+
puts "", "Facets: tags distribution for the current query ",
|
70
|
+
"(Notice that 'java' is included, because of the filter)", "-"*80
|
68
71
|
s.results.facets['current-tags']['terms'].each do |f|
|
69
|
-
puts "#{f['term'].ljust(
|
72
|
+
puts "#{f['term'].ljust(13)} #{f['count']}"
|
70
73
|
end
|
data/lib/slingshot.rb
CHANGED
@@ -9,6 +9,7 @@ require 'slingshot/search'
|
|
9
9
|
require 'slingshot/search/query'
|
10
10
|
require 'slingshot/search/sort'
|
11
11
|
require 'slingshot/search/facet'
|
12
|
+
require 'slingshot/search/filter'
|
12
13
|
require 'slingshot/results/collection'
|
13
14
|
require 'slingshot/results/item'
|
14
15
|
require 'slingshot/index'
|
data/lib/slingshot/search.rb
CHANGED
@@ -3,7 +3,7 @@ module Slingshot
|
|
3
3
|
|
4
4
|
class Search
|
5
5
|
|
6
|
-
attr_reader :indices, :url, :results, :response, :query, :facets
|
6
|
+
attr_reader :indices, :url, :results, :response, :query, :facets, :filters
|
7
7
|
|
8
8
|
def initialize(*indices, &block)
|
9
9
|
@options = indices.pop if indices.last.is_a?(Hash)
|
@@ -31,6 +31,12 @@ module Slingshot
|
|
31
31
|
self
|
32
32
|
end
|
33
33
|
|
34
|
+
def filter(type, *options)
|
35
|
+
@filters ||= []
|
36
|
+
@filters << Filter.new(type, *options).to_hash
|
37
|
+
self
|
38
|
+
end
|
39
|
+
|
34
40
|
def from(value)
|
35
41
|
@from = value
|
36
42
|
self
|
@@ -51,6 +57,9 @@ module Slingshot
|
|
51
57
|
@response = JSON.parse( Configuration.client.post(@url, self.to_json) )
|
52
58
|
@results = Results::Collection.new(@response)
|
53
59
|
self
|
60
|
+
rescue Exception
|
61
|
+
STDERR.puts "Request failed: \n#{self.to_curl}"
|
62
|
+
raise
|
54
63
|
end
|
55
64
|
|
56
65
|
def to_curl
|
@@ -60,12 +69,13 @@ module Slingshot
|
|
60
69
|
def to_json
|
61
70
|
request = {}
|
62
71
|
request.update( { :query => @query } )
|
63
|
-
request.update( { :sort => @sort } )
|
64
|
-
request.update( { :facets => @facets } )
|
65
|
-
request.update( { :
|
66
|
-
request.update( { :
|
67
|
-
request.update( { :
|
68
|
-
request.
|
72
|
+
request.update( { :sort => @sort } ) if @sort
|
73
|
+
request.update( { :facets => @facets } ) if @facets
|
74
|
+
@filters.each { |filter| request.update( { :filter => filter } ) } if @filters
|
75
|
+
request.update( { :size => @size } ) if @size
|
76
|
+
request.update( { :from => @from } ) if @from
|
77
|
+
request.update( { :fields => @fields } ) if @fields
|
78
|
+
Yajl::Encoder.encode(request)
|
69
79
|
end
|
70
80
|
|
71
81
|
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Slingshot
|
2
|
+
module Search
|
3
|
+
|
4
|
+
# http://www.elasticsearch.org/guide/reference/api/search/filter.html
|
5
|
+
# http://www.elasticsearch.org/guide/reference/query-dsl/
|
6
|
+
#
|
7
|
+
class Filter
|
8
|
+
|
9
|
+
def initialize(type, *options)
|
10
|
+
@type = type
|
11
|
+
@options = options || []
|
12
|
+
end
|
13
|
+
|
14
|
+
def to_json
|
15
|
+
to_hash.to_json
|
16
|
+
end
|
17
|
+
|
18
|
+
def to_hash
|
19
|
+
initial = @options.size > 1 ? { @type => [] } : { @type => {} }
|
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
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
end
|
data/lib/slingshot/version.rb
CHANGED
@@ -0,0 +1,46 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
module Slingshot
|
4
|
+
|
5
|
+
class FiltersIntegrationTest < Test::Unit::TestCase
|
6
|
+
include Test::Integration
|
7
|
+
|
8
|
+
context "Filters" do
|
9
|
+
|
10
|
+
should "filter the results" do
|
11
|
+
s = Slingshot.search('articles-test') do
|
12
|
+
query { string 'title:T*' }
|
13
|
+
filter :terms, :tags => ['ruby']
|
14
|
+
end
|
15
|
+
|
16
|
+
assert_equal 1, s.results.count
|
17
|
+
assert_equal 'Two', s.results.first.title
|
18
|
+
end
|
19
|
+
|
20
|
+
should "filter the results with multiple filters" do
|
21
|
+
s = Slingshot.search('articles-test') do
|
22
|
+
query { string 'title:F*' }
|
23
|
+
filter :or, {:terms => {:tags => ['ruby']}},
|
24
|
+
{:terms => {:tags => ['erlang']}}
|
25
|
+
end
|
26
|
+
|
27
|
+
assert_equal 1, s.results.count
|
28
|
+
assert_equal 'Four', s.results.first.title
|
29
|
+
end
|
30
|
+
|
31
|
+
should "not influence facets" do
|
32
|
+
s = Slingshot.search('articles-test') do
|
33
|
+
query { string 'title:T*' }
|
34
|
+
filter :terms, :tags => ['ruby']
|
35
|
+
facet('tags') { terms :tags }
|
36
|
+
end
|
37
|
+
|
38
|
+
assert_equal 1, s.results.count
|
39
|
+
assert_equal 3, s.results.facets['tags'].size
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
module Slingshot::Search
|
4
|
+
|
5
|
+
class FilterTest < Test::Unit::TestCase
|
6
|
+
|
7
|
+
context "Filter" do
|
8
|
+
|
9
|
+
should "be serialized to JSON" do
|
10
|
+
assert_respond_to Filter.new(:terms, {}), :to_json
|
11
|
+
end
|
12
|
+
|
13
|
+
should "encode simple filter declarations as JSON" do
|
14
|
+
assert_equal( { :terms => { :tags => ['foo'] } }.to_json,
|
15
|
+
Filter.new('terms', :tags => ['foo']).to_json )
|
16
|
+
|
17
|
+
assert_equal( { :range => { :age => { :from => 10, :to => 20 } } }.to_json,
|
18
|
+
Filter.new('range', { :age => { :from => 10, :to => 20 } }).to_json )
|
19
|
+
|
20
|
+
assert_equal( { :geo_distance => { :distance => '12km', :location => [40, -70] } }.to_json,
|
21
|
+
Filter.new('geo_distance', { :distance => '12km', :location => [40, -70] }).to_json )
|
22
|
+
end
|
23
|
+
|
24
|
+
should "encode 'or' filter declarations as JSON" do
|
25
|
+
# See http://www.elasticsearch.org/guide/reference/query-dsl/or-filter.html
|
26
|
+
assert_equal( { :or => [ {:terms => {:tags => ['foo']}}, {:terms => {:tags => ['bar']}} ] }.to_json,
|
27
|
+
Filter.new('or', {:terms => {:tags => ['foo']}}, {:terms => {:tags => ['bar']}}).to_json )
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
end
|
data/test/unit/search_test.rb
CHANGED
@@ -46,6 +46,14 @@ module Slingshot
|
|
46
46
|
assert_not_nil s.response
|
47
47
|
end
|
48
48
|
|
49
|
+
should "print debugging information on exception and re-raise it" do
|
50
|
+
Configuration.client.expects(:post).raises(RestClient::InternalServerError)
|
51
|
+
STDERR.expects(:puts)
|
52
|
+
|
53
|
+
s = Search::Search.new('index')
|
54
|
+
assert_raise(RestClient::InternalServerError) { s.perform }
|
55
|
+
end
|
56
|
+
|
49
57
|
context "sort" do
|
50
58
|
|
51
59
|
should "allow sorting by multiple fields" do
|
@@ -77,6 +85,20 @@ module Slingshot
|
|
77
85
|
|
78
86
|
end
|
79
87
|
|
88
|
+
context "filter" do
|
89
|
+
|
90
|
+
should "allow to specify filter" do
|
91
|
+
s = Search::Search.new('index') do
|
92
|
+
filter :terms, :tags => ['foo']
|
93
|
+
end
|
94
|
+
|
95
|
+
assert_equal 1, s.filters.size
|
96
|
+
assert_not_nil s.filters.first
|
97
|
+
assert_not_nil s.filters.first[:terms]
|
98
|
+
end
|
99
|
+
|
100
|
+
end
|
101
|
+
|
80
102
|
context "with from/size" do
|
81
103
|
|
82
104
|
should "set the values in request" do
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: slingshot-rb
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
4
|
+
hash: 19
|
5
5
|
prerelease:
|
6
6
|
segments:
|
7
7
|
- 0
|
8
8
|
- 0
|
9
|
-
-
|
10
|
-
version: 0.0.
|
9
|
+
- 6
|
10
|
+
version: 0.0.6
|
11
11
|
platform: ruby
|
12
12
|
authors:
|
13
13
|
- Karel Minarik
|
@@ -15,7 +15,7 @@ autorequire:
|
|
15
15
|
bindir: bin
|
16
16
|
cert_chain: []
|
17
17
|
|
18
|
-
date: 2011-03-
|
18
|
+
date: 2011-03-13 00:00:00 +01:00
|
19
19
|
default_executable:
|
20
20
|
dependencies:
|
21
21
|
- !ruby/object:Gem::Dependency
|
@@ -163,6 +163,7 @@ files:
|
|
163
163
|
- lib/slingshot/rubyext/hash.rb
|
164
164
|
- lib/slingshot/search.rb
|
165
165
|
- lib/slingshot/search/facet.rb
|
166
|
+
- lib/slingshot/search/filter.rb
|
166
167
|
- lib/slingshot/search/query.rb
|
167
168
|
- lib/slingshot/search/sort.rb
|
168
169
|
- lib/slingshot/version.rb
|
@@ -174,6 +175,7 @@ files:
|
|
174
175
|
- test/fixtures/articles/4.json
|
175
176
|
- test/fixtures/articles/5.json
|
176
177
|
- test/integration/facets_test.rb
|
178
|
+
- test/integration/filters_test.rb
|
177
179
|
- test/integration/index_mapping_test.rb
|
178
180
|
- test/integration/index_store_test.rb
|
179
181
|
- test/integration/query_string_test.rb
|
@@ -187,6 +189,7 @@ files:
|
|
187
189
|
- test/unit/results_collection_test.rb
|
188
190
|
- test/unit/results_item_test.rb
|
189
191
|
- test/unit/search_facet_test.rb
|
192
|
+
- test/unit/search_filter_test.rb
|
190
193
|
- test/unit/search_query_test.rb
|
191
194
|
- test/unit/search_sort_test.rb
|
192
195
|
- test/unit/search_test.rb
|
@@ -234,6 +237,7 @@ test_files:
|
|
234
237
|
- test/fixtures/articles/4.json
|
235
238
|
- test/fixtures/articles/5.json
|
236
239
|
- test/integration/facets_test.rb
|
240
|
+
- test/integration/filters_test.rb
|
237
241
|
- test/integration/index_mapping_test.rb
|
238
242
|
- test/integration/index_store_test.rb
|
239
243
|
- test/integration/query_string_test.rb
|
@@ -247,6 +251,7 @@ test_files:
|
|
247
251
|
- test/unit/results_collection_test.rb
|
248
252
|
- test/unit/results_item_test.rb
|
249
253
|
- test/unit/search_facet_test.rb
|
254
|
+
- test/unit/search_filter_test.rb
|
250
255
|
- test/unit/search_query_test.rb
|
251
256
|
- test/unit/search_sort_test.rb
|
252
257
|
- test/unit/search_test.rb
|