slingshot-rb 0.0.5 → 0.0.6
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/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
|

|
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
|