stretchy 0.1.1
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.
- checksums.yaml +7 -0
- data/.gitignore +11 -0
- data/.travis.yml +6 -0
- data/Gemfile +4 -0
- data/README.md +184 -0
- data/Rakefile +7 -0
- data/bin/console +14 -0
- data/bin/setup +7 -0
- data/lib/stretchy/boosts/filter_boost.rb +20 -0
- data/lib/stretchy/boosts/geo_boost.rb +38 -0
- data/lib/stretchy/boosts/random_boost.rb +26 -0
- data/lib/stretchy/client_actions.rb +61 -0
- data/lib/stretchy/configuration.rb +34 -0
- data/lib/stretchy/filters/and_filter.rb +17 -0
- data/lib/stretchy/filters/bool_filter.rb +19 -0
- data/lib/stretchy/filters/exists_filter.rb +20 -0
- data/lib/stretchy/filters/geo_filter.rb +24 -0
- data/lib/stretchy/filters/not_filter.rb +20 -0
- data/lib/stretchy/filters/query_filter.rb +16 -0
- data/lib/stretchy/filters/range_filter.rb +22 -0
- data/lib/stretchy/filters/terms_filter.rb +19 -0
- data/lib/stretchy/null_query.rb +30 -0
- data/lib/stretchy/queries/filtered_query.rb +18 -0
- data/lib/stretchy/queries/function_score_query.rb +70 -0
- data/lib/stretchy/queries/match_all_query.rb +9 -0
- data/lib/stretchy/queries/match_query.rb +24 -0
- data/lib/stretchy/query.rb +241 -0
- data/lib/stretchy/request_body.rb +67 -0
- data/lib/stretchy/version.rb +3 -0
- data/lib/stretchy.rb +14 -0
- data/stretchy.gemspec +29 -0
- metadata +158 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 213d84388de53cfe8d9b815d34ef544349cb2bd0
|
4
|
+
data.tar.gz: 5cdfb873ac77da2e9815c7ac3f36da1dc27889e8
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 590ee2ca8abb6251effe21b6561a29459f80a6e4361158c9d0103c80dc0594a97b00c5c8b77b51beea296ce21350274b5a4ab128e40e010b5fb00b8cb289449b
|
7
|
+
data.tar.gz: e60c32da5e5cf45553f9f2163c0764d6ba0653b22d1e1942a28d777e61f81dca8585dd43c24ac9390a89a13ffe9be954bf598f11b0bf0e817555b43d535d399a
|
data/.gitignore
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,184 @@
|
|
1
|
+
# Stretchy
|
2
|
+
[](https://travis-ci.org/hired/stretchy)
|
3
|
+
|
4
|
+
Stretchy is a query builder for [Elasticsearch](https://www.elastic.co/products/elasticsearch). It helps you quickly construct the JSON to send to Elastic, which can get [rather complicated](http://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl.html).
|
5
|
+
|
6
|
+
Stretchy is modeled after ActiveRecord's interface and architecture - query objects are immutable and chainable, which makes quickly building the right query and caching the results easy.
|
7
|
+
|
8
|
+
Stretchy is *not*:
|
9
|
+
|
10
|
+
1. an integration with ActiveModel to help you index your data
|
11
|
+
2. a way to manage Elasticsearch configuration
|
12
|
+
3. a general-purpose Elasticsearch API client
|
13
|
+
|
14
|
+
The first two are very application-specific. For any non-trivial app, the level of customization necessary will have you writing almost everything yourself. The last one is better handled by the [elasticsearch gem](http://www.rubydoc.info/gems/elasticsearch-api/).
|
15
|
+
|
16
|
+
## Installation
|
17
|
+
|
18
|
+
Add this line to your application's Gemfile:
|
19
|
+
|
20
|
+
```ruby
|
21
|
+
gem 'stretchy'
|
22
|
+
```
|
23
|
+
|
24
|
+
And then execute:
|
25
|
+
|
26
|
+
$ bundle
|
27
|
+
|
28
|
+
Or install it yourself as:
|
29
|
+
|
30
|
+
$ gem install stretchy
|
31
|
+
|
32
|
+
## Usage
|
33
|
+
|
34
|
+
Stretchy is still in early development, so it does not yet support the full feature set of the [Elasticsearch API](http://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl.html). It does support fairly basic queries in an ActiveRecord-ish style.
|
35
|
+
|
36
|
+
### Base
|
37
|
+
|
38
|
+
```ruby
|
39
|
+
query = Stretchy::Query.new(index: 'app_production', type: 'model_name')
|
40
|
+
```
|
41
|
+
|
42
|
+
From here, you can chain the following query methods:
|
43
|
+
|
44
|
+
### Match
|
45
|
+
|
46
|
+
```ruby
|
47
|
+
query = query.match('welcome to my web site')
|
48
|
+
query = query.match('welcome to my web site', field: 'title', operator: 'or')
|
49
|
+
```
|
50
|
+
|
51
|
+
Performs a full-text search for the given string. `field` and `operator` are optional, and default to `_all` and `and` respectively.
|
52
|
+
|
53
|
+
#### Variants
|
54
|
+
|
55
|
+
* `not_match` - filters for documents not matching a full-text search
|
56
|
+
|
57
|
+
|
58
|
+
### Where
|
59
|
+
|
60
|
+
```ruby
|
61
|
+
query = query.where(
|
62
|
+
name: 'Exact Name',
|
63
|
+
email: [
|
64
|
+
'exact@email.com',
|
65
|
+
'another.user.with.same.name@email.com'
|
66
|
+
]
|
67
|
+
)
|
68
|
+
```
|
69
|
+
|
70
|
+
Allows passing a hash of matchable options in `field: [values]` format. If any one of the values matches, that field will be considered matched. All fields must match for a document to be returned. See the [Terms filter](http://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-terms-filter.html) for more details.
|
71
|
+
|
72
|
+
If you pass `field: nil`, Stretchy will construct the relevant `not { exists: field }` filter and apply it as expected.
|
73
|
+
|
74
|
+
#### Gotcha
|
75
|
+
|
76
|
+
Matches _must_ be exact; the values you pass in here are not analyzed by Elasticsearch, while the values stored in the index are (unless you turned analysis off for that field).
|
77
|
+
|
78
|
+
#### Variants
|
79
|
+
|
80
|
+
* `not_where` - filters for documents *not* matching the criteria
|
81
|
+
* `boost_where` - boosts the relevance score for matching documents
|
82
|
+
* `boost_not_where` - boosts the relevance score for documents not matching the criteria
|
83
|
+
|
84
|
+
### Range
|
85
|
+
|
86
|
+
```ruby
|
87
|
+
query = query.range(field: 'rating', min: 3, max: 5)
|
88
|
+
```
|
89
|
+
|
90
|
+
Only documents with the specified field, and within the specified range (inclusive) match. You can also pass in dates and times as ranges. Currently, you must pass both a min and a max value.
|
91
|
+
|
92
|
+
#### Variants
|
93
|
+
|
94
|
+
* `not_range` - filters for only documents where the field is *outside* the given range
|
95
|
+
* `boost_range` - boosts the relevance score for matching documents
|
96
|
+
|
97
|
+
### Geo
|
98
|
+
|
99
|
+
```ruby
|
100
|
+
query = query.geo(field: 'coords', distance: '20mi', lat: 35.0117, lng: 135.7683)
|
101
|
+
```
|
102
|
+
|
103
|
+
Filters for documents where the specified `geo_point` field is within the given range.
|
104
|
+
|
105
|
+
#### Gotcha
|
106
|
+
|
107
|
+
The field must be mapped as a `geo_point` field. See [Elasticsearch types](http://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-geo-point-type.html) for more info.
|
108
|
+
|
109
|
+
#### Variants
|
110
|
+
|
111
|
+
* `not_geo` - filters for documents outside the specified range
|
112
|
+
* `boost_geo` - boosts the relevance score for documents based on how far from the given point they are
|
113
|
+
|
114
|
+
### Limit and Offset
|
115
|
+
|
116
|
+
```ruby
|
117
|
+
query = query.limit(20).offset(1000)
|
118
|
+
```
|
119
|
+
|
120
|
+
Works the same way as ActiveRecord's limit and offset methods.
|
121
|
+
|
122
|
+
### Boost Random
|
123
|
+
|
124
|
+
```ruby
|
125
|
+
query = query.boost_random(user.id, 1.4)
|
126
|
+
```
|
127
|
+
|
128
|
+
Provides a random-but-deterministic boost to relevance scores. The first parameter is required, and represents the random seed. The second parameter is optional, and represents the weight for the random factor. See [Random Scoring](http://www.elastic.co/guide/en/elasticsearch/guide/master/random-scoring.html) for more details.
|
129
|
+
|
130
|
+
### Explain
|
131
|
+
|
132
|
+
```ruby
|
133
|
+
query = query.explain.results
|
134
|
+
```
|
135
|
+
|
136
|
+
Provides Elasticsearch explanation results in `query.response` . See [the explain documentation](http://www.elastic.co/guide/en/elasticsearch/reference/1.x/search-explain.html) for more info.
|
137
|
+
|
138
|
+
### Response
|
139
|
+
|
140
|
+
```ruby
|
141
|
+
query.response
|
142
|
+
```
|
143
|
+
|
144
|
+
Executes the query, returns the raw JSON response from Elasticsearch and caches it. Use this to get at search API data not in the source documents.
|
145
|
+
|
146
|
+
### Results
|
147
|
+
|
148
|
+
```ruby
|
149
|
+
query.results
|
150
|
+
```
|
151
|
+
|
152
|
+
Executes the query and provides the parsed json for each hit returned by Elasticsearch.
|
153
|
+
|
154
|
+
### Ids
|
155
|
+
|
156
|
+
```ruby
|
157
|
+
query.ids
|
158
|
+
```
|
159
|
+
|
160
|
+
Provides only the ids for each hit. If your document ids are numeric (as is the case for most ActiveRecord-integrated documents), they will be converted to integers.
|
161
|
+
|
162
|
+
This is somewhat intelligent - if you have already called `results` the ids will be fetched from there, otherwise it will run the search and skip the data-fetch phase in Elasticsearch.
|
163
|
+
|
164
|
+
### Total
|
165
|
+
|
166
|
+
```ruby
|
167
|
+
query.total
|
168
|
+
```
|
169
|
+
|
170
|
+
Returns the total number of matches returned by the query - not just the current page. Makes plugging into [Kaminari](https://github.com/amatsuda/kaminari) a snap.
|
171
|
+
|
172
|
+
## Development
|
173
|
+
|
174
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `bin/console` for an interactive prompt that will allow you to experiment.
|
175
|
+
|
176
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release` to create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
177
|
+
|
178
|
+
## Contributing
|
179
|
+
|
180
|
+
1. Fork it ( https://github.com/[my-github-username]/stretchy/fork )
|
181
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
182
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
183
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
184
|
+
5. Create a new Pull Request
|
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "stretchy"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start
|
data/bin/setup
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
module Stretchy
|
2
|
+
module Boosts
|
3
|
+
class FilterBoost
|
4
|
+
|
5
|
+
DEFAULT_WEIGHT = 1.2
|
6
|
+
|
7
|
+
def initialize(filter:, weight: DEFAULT_WEIGHT)
|
8
|
+
@filter = filter
|
9
|
+
@weight = weight
|
10
|
+
end
|
11
|
+
|
12
|
+
def to_search
|
13
|
+
{
|
14
|
+
filter: @filter.to_search,
|
15
|
+
weight: @weight
|
16
|
+
}
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module Stretchy
|
2
|
+
module Boosts
|
3
|
+
class GeoBoost
|
4
|
+
|
5
|
+
DEFAULTS = {
|
6
|
+
field: 'coords',
|
7
|
+
offset: '10km',
|
8
|
+
scale: '50km',
|
9
|
+
decay: 0.75,
|
10
|
+
weight: 1.2
|
11
|
+
}.freeze
|
12
|
+
|
13
|
+
def initialize(options = {})
|
14
|
+
@field = options[:field] || DEFAULTS[:field]
|
15
|
+
@offset = options[:offset] || DEFAULTS[:offset]
|
16
|
+
@scale = options[:scale] || DEFAULTS[:scale]
|
17
|
+
@decay = options[:decay] || DEFAULTS[:decay]
|
18
|
+
@weight = options[:weight] || DEFAULTS[:weight]
|
19
|
+
@lat = options[:lat]
|
20
|
+
@lng = options[:lng]
|
21
|
+
end
|
22
|
+
|
23
|
+
def to_search
|
24
|
+
{
|
25
|
+
gauss: {
|
26
|
+
@field => {
|
27
|
+
origin: { lat: @lat, lon: @lng },
|
28
|
+
offset: @offset,
|
29
|
+
scale: @scale,
|
30
|
+
decay: @decay
|
31
|
+
}
|
32
|
+
},
|
33
|
+
weight: @weight
|
34
|
+
}
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Stretchy
|
2
|
+
module Boosts
|
3
|
+
class RandomBoost
|
4
|
+
|
5
|
+
DEFAULT_WEIGHT = 1.2
|
6
|
+
|
7
|
+
# randomizes order (somewhat) consistently per-user
|
8
|
+
# http://www.elastic.co/guide/en/elasticsearch/guide/current/random-scoring.html
|
9
|
+
|
10
|
+
def initialize(seed, weight = DEFAULT_WEIGHT)
|
11
|
+
@seed = seed
|
12
|
+
@weight = weight
|
13
|
+
end
|
14
|
+
|
15
|
+
def to_search
|
16
|
+
{
|
17
|
+
random_score: {
|
18
|
+
seed: @seed
|
19
|
+
},
|
20
|
+
weight: @weight
|
21
|
+
}
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
require 'json'
|
2
|
+
module Stretchy
|
3
|
+
module ClientActions
|
4
|
+
|
5
|
+
def self.extended(base)
|
6
|
+
unless base.respond_to?(:client) && base.respond_to?(:index_name)
|
7
|
+
raise "ClientActions requires methods 'client' and 'index_name'"
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
# used for ensuring a concistent index in specs
|
12
|
+
def refresh
|
13
|
+
client.indices.refresh index: index_name
|
14
|
+
end
|
15
|
+
|
16
|
+
def count
|
17
|
+
client.cat.count(index: index_name).split(' ')[2].to_i
|
18
|
+
end
|
19
|
+
|
20
|
+
def search(type:, body:, fields: nil)
|
21
|
+
options = { index: index_name, type: type, body: body }
|
22
|
+
options[:fields] = fields if fields.is_a?(Array)
|
23
|
+
|
24
|
+
client.search(options)
|
25
|
+
end
|
26
|
+
|
27
|
+
def index(type:, body:, id: nil)
|
28
|
+
id ||= body['id'] || body['_id'] || body[:id] || body[:_id]
|
29
|
+
client.index(index: index_name, type: type, id: id, body: body)
|
30
|
+
end
|
31
|
+
|
32
|
+
def bulk(type:, documents:)
|
33
|
+
requests = documents.flat_map do |document|
|
34
|
+
id = document['id'] || document['_id'] || document[:id] || document[:_id]
|
35
|
+
[
|
36
|
+
{ index: { '_index' => index_name, '_type' => type, '_id' => id } },
|
37
|
+
document
|
38
|
+
]
|
39
|
+
end
|
40
|
+
client.bulk body: requests
|
41
|
+
end
|
42
|
+
|
43
|
+
def exists(_index_name = index_name)
|
44
|
+
client.indices.exists(index: _index_name)
|
45
|
+
end
|
46
|
+
alias :exists? :exists
|
47
|
+
|
48
|
+
def delete(_index_name = index_name)
|
49
|
+
client.indices.delete(index: _index_name) if exists?(_index_name)
|
50
|
+
end
|
51
|
+
|
52
|
+
def create(_index_name = index_name)
|
53
|
+
client.indices.create(index: _index_name) unless exists?(_index_name)
|
54
|
+
end
|
55
|
+
|
56
|
+
def mapping(_index_name, _type, _body)
|
57
|
+
client.indices.put_mapping(index: _index_name, type: _type, body: _body)
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module Stretchy
|
2
|
+
module Configuration
|
3
|
+
|
4
|
+
attr_accessor :index_name, :logger, :url, :adapter, :client
|
5
|
+
|
6
|
+
def self.extended(base)
|
7
|
+
base.set_default_configuration
|
8
|
+
end
|
9
|
+
|
10
|
+
def configure
|
11
|
+
yield self
|
12
|
+
end
|
13
|
+
|
14
|
+
def set_default_configuration
|
15
|
+
self.index_name = 'myapp'
|
16
|
+
self.adapter = :excon
|
17
|
+
self.url = ENV['ELASTICSEARCH_URL']
|
18
|
+
end
|
19
|
+
|
20
|
+
def client_options
|
21
|
+
Hash[
|
22
|
+
index_name: index_name,
|
23
|
+
log: !!logger,
|
24
|
+
logger: logger,
|
25
|
+
adapter: adapter,
|
26
|
+
url: url
|
27
|
+
]
|
28
|
+
end
|
29
|
+
|
30
|
+
def client(options = {})
|
31
|
+
@client ||= Elasticsearch::Client.new(client_options.merge(options))
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Stretchy
|
2
|
+
module Filters
|
3
|
+
class BoolFilter
|
4
|
+
def initialize(must:, must_not:, should: nil)
|
5
|
+
@must = Array(must)
|
6
|
+
@must_not = Array(must_not)
|
7
|
+
@should = Array(should)
|
8
|
+
end
|
9
|
+
|
10
|
+
def to_search
|
11
|
+
json = {}
|
12
|
+
json[:must] = @must.map(&:to_search) if @must
|
13
|
+
json[:must_not] = @must_not.map(&:to_search) if @must_not
|
14
|
+
json[:should] = @should.map(&:to_search) if @should
|
15
|
+
{ bool: json }
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Stretchy
|
2
|
+
module Filters
|
3
|
+
class ExistsFilter
|
4
|
+
|
5
|
+
# CAUTION: this will match empty strings
|
6
|
+
# see http://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-exists-filter.html
|
7
|
+
def initialize(field)
|
8
|
+
@field = field
|
9
|
+
end
|
10
|
+
|
11
|
+
def to_search
|
12
|
+
{
|
13
|
+
exists: {
|
14
|
+
field: @field
|
15
|
+
}
|
16
|
+
}
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Stretchy
|
2
|
+
module Filters
|
3
|
+
class GeoFilter
|
4
|
+
def initialize(field: 'coords', distance: '50km', lat:, lng:)
|
5
|
+
@field = field
|
6
|
+
@distance = distance
|
7
|
+
@lat = lat
|
8
|
+
@lng = lng
|
9
|
+
end
|
10
|
+
|
11
|
+
def to_search
|
12
|
+
{
|
13
|
+
geo_distance: {
|
14
|
+
distance: @distance,
|
15
|
+
@field => {
|
16
|
+
lat: @lat,
|
17
|
+
lon: @lng
|
18
|
+
}
|
19
|
+
}
|
20
|
+
}
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Stretchy
|
2
|
+
module Filters
|
3
|
+
class NotFilter
|
4
|
+
|
5
|
+
def initialize(filters)
|
6
|
+
filters = Array(filters)
|
7
|
+
|
8
|
+
if filters.count == 1
|
9
|
+
@filter = filters.first
|
10
|
+
else
|
11
|
+
@filter = AndFilter.new(filters)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def to_search
|
16
|
+
{ not: @filter.to_search }
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Stretchy
|
2
|
+
module Filters
|
3
|
+
class RangeFilter
|
4
|
+
def initialize(field:, min:, max:)
|
5
|
+
@field = field
|
6
|
+
@min = min
|
7
|
+
@max = max
|
8
|
+
end
|
9
|
+
|
10
|
+
def to_search
|
11
|
+
range = {}
|
12
|
+
range[:gte] = @min if @min
|
13
|
+
range[:lte] = @max if @max
|
14
|
+
{
|
15
|
+
range: {
|
16
|
+
@field => range
|
17
|
+
}
|
18
|
+
}
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Stretchy
|
2
|
+
class NullQuery
|
3
|
+
def initialize(options = {})
|
4
|
+
end
|
5
|
+
|
6
|
+
def response
|
7
|
+
{}
|
8
|
+
end
|
9
|
+
|
10
|
+
def id_response
|
11
|
+
{}
|
12
|
+
end
|
13
|
+
|
14
|
+
def results
|
15
|
+
[]
|
16
|
+
end
|
17
|
+
|
18
|
+
def ids
|
19
|
+
[]
|
20
|
+
end
|
21
|
+
|
22
|
+
def shards
|
23
|
+
[]
|
24
|
+
end
|
25
|
+
|
26
|
+
def total
|
27
|
+
0
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Stretchy
|
2
|
+
module Queries
|
3
|
+
class FilteredQuery
|
4
|
+
|
5
|
+
def initialize(query: nil, filter:)
|
6
|
+
@query = query
|
7
|
+
@filter = filter
|
8
|
+
end
|
9
|
+
|
10
|
+
def to_search
|
11
|
+
json = {}
|
12
|
+
json[:query] = @query.to_search if @query
|
13
|
+
json[:filter] = @filter.to_search if @filter
|
14
|
+
{ filtered: json }
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
module Stretchy
|
2
|
+
module Queries
|
3
|
+
class FunctionScoreQuery
|
4
|
+
|
5
|
+
SCORE_MODES = %w(multiply sum avg first max min)
|
6
|
+
BOOST_MODES = %w(multiply replace sum avg max min)
|
7
|
+
|
8
|
+
def initialize(options = {})
|
9
|
+
@functions = Array(options[:functions])
|
10
|
+
@query = options[:query]
|
11
|
+
@filter = options[:filter]
|
12
|
+
|
13
|
+
self.class.attributes.map do |field|
|
14
|
+
instance_variable_set("@#{field}", options[field])
|
15
|
+
end
|
16
|
+
validate
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.attributes
|
20
|
+
[:boost, :max_boost, :score_mode, :boost_mode, :min_score]
|
21
|
+
end
|
22
|
+
|
23
|
+
def validate
|
24
|
+
if @query && @filter
|
25
|
+
raise ArgumentError.new("Cannot have both query and filter -- combine using a FilteredQuery")
|
26
|
+
end
|
27
|
+
|
28
|
+
if @boost && !@boost.is_a?(Numeric)
|
29
|
+
raise ArgumentError.new("Boost must be a number - it is the global boost for the whole query")
|
30
|
+
end
|
31
|
+
|
32
|
+
if @max_boost && !@max_boost.is_a?(Numeric)
|
33
|
+
raise ArgumentError.new("Max boost must be a number")
|
34
|
+
end
|
35
|
+
|
36
|
+
if @min_score && !@min_score.is_a?(Numeric)
|
37
|
+
raise ArgumentError.new("min_score must be a number - it is the global boost for the whole query")
|
38
|
+
end
|
39
|
+
|
40
|
+
if @score_mode && !SCORE_MODES.include?(@score_mode)
|
41
|
+
raise ArgumentError.new("Score mode must be one of #{SCORE_MODES.join(', ')}")
|
42
|
+
end
|
43
|
+
|
44
|
+
if @boost_mode && !BOOST_MODES.include?(@boost_mode)
|
45
|
+
raise ArgumentError.new("Score mode must be one of #{BOOST_MODES.join(', ')}")
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def to_search
|
50
|
+
json = {}
|
51
|
+
json[:functions] = @functions.map(&:to_search)
|
52
|
+
if @query
|
53
|
+
json[:query] = @query.to_search
|
54
|
+
elsif @filter
|
55
|
+
json[:filter] = @filter.to_search
|
56
|
+
else
|
57
|
+
json[:query] = Stretchy::Queries::MatchAllQuery.new.to_search
|
58
|
+
end
|
59
|
+
|
60
|
+
self.class.attributes.reduce(json) do |body, field|
|
61
|
+
ivar = instance_variable_get("@#{field}")
|
62
|
+
body[field] = ivar if ivar
|
63
|
+
body
|
64
|
+
end
|
65
|
+
|
66
|
+
{ function_score: json }
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Stretchy
|
2
|
+
module Queries
|
3
|
+
class MatchQuery
|
4
|
+
|
5
|
+
def initialize(string, field: '_all', operator: 'and')
|
6
|
+
@field = field
|
7
|
+
@operator = operator
|
8
|
+
@string = string
|
9
|
+
end
|
10
|
+
|
11
|
+
def to_search
|
12
|
+
{
|
13
|
+
match: {
|
14
|
+
@field => {
|
15
|
+
query: @string,
|
16
|
+
operator: @operator
|
17
|
+
}
|
18
|
+
}
|
19
|
+
}
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,241 @@
|
|
1
|
+
module Stretchy
|
2
|
+
class Query
|
3
|
+
|
4
|
+
DEFAULT_LIMIT = 40
|
5
|
+
|
6
|
+
def initialize(options = {})
|
7
|
+
@offset = options[:offset] || 0
|
8
|
+
@limit = options[:limit] || DEFAULT_LIMIT
|
9
|
+
@match = options[:match]
|
10
|
+
@filters = options[:filters]
|
11
|
+
@not_filters = options[:not_filters]
|
12
|
+
@boosts = options[:boosts]
|
13
|
+
@type = options[:type] || 'documents'
|
14
|
+
@index = options[:index] || Stretchy.index_name
|
15
|
+
@explain = options[:explain]
|
16
|
+
end
|
17
|
+
|
18
|
+
def clone_with(options)
|
19
|
+
filters = [@filters, options[:filters], options[:filter]].flatten.compact
|
20
|
+
not_filters = [@not_filters, options[:not_filters], options[:not_filter]].flatten.compact
|
21
|
+
boosts = [@boosts, options[:boosts], options[:boost]].flatten.compact
|
22
|
+
|
23
|
+
self.class.new(
|
24
|
+
type: @type,
|
25
|
+
index: @index,
|
26
|
+
explain: options[:explain] || @explain,
|
27
|
+
offset: options[:offset] || @offset,
|
28
|
+
limit: options[:limit] || @limit,
|
29
|
+
match: options[:match] || @match,
|
30
|
+
filters: filters,
|
31
|
+
not_filters: not_filters,
|
32
|
+
boosts: boosts
|
33
|
+
)
|
34
|
+
end
|
35
|
+
|
36
|
+
def offset(num)
|
37
|
+
clone_with(offset: num)
|
38
|
+
end
|
39
|
+
|
40
|
+
def limit(num)
|
41
|
+
clone_with(limit: num)
|
42
|
+
end
|
43
|
+
|
44
|
+
def explain
|
45
|
+
clone_with(explain: true)
|
46
|
+
end
|
47
|
+
|
48
|
+
def where(options = {})
|
49
|
+
return self if options.empty?
|
50
|
+
filters, not_filters = where_clause(options)
|
51
|
+
clone_with(filters: filters, not_filters: not_filters)
|
52
|
+
end
|
53
|
+
|
54
|
+
def not_where(options = {})
|
55
|
+
return self if options.empty?
|
56
|
+
# reverse the order returned here, since we're doing not_where
|
57
|
+
not_filters, filters = where_clause(options)
|
58
|
+
clone_with(filters: filters, not_filters: not_filters)
|
59
|
+
end
|
60
|
+
|
61
|
+
def boost_where(options = {})
|
62
|
+
return self if options.empty?
|
63
|
+
weight = options.delete(:weight) || 1.2
|
64
|
+
|
65
|
+
boosts = options.map do |field, values|
|
66
|
+
filter = nil
|
67
|
+
if values.nil?
|
68
|
+
filter = Filters::NotFilter.new(Filters::ExistsFilter.new(field))
|
69
|
+
else
|
70
|
+
filter = Filters::TermsFilter.new(field: field, values: Array(values))
|
71
|
+
end
|
72
|
+
Boosts::FilterBoost.new(filter: filter, weight: weight)
|
73
|
+
end
|
74
|
+
clone_with(boosts: boosts)
|
75
|
+
end
|
76
|
+
|
77
|
+
def boost_not_where(options = {})
|
78
|
+
return self if options.empty?
|
79
|
+
weight = options.delete(:weight) || 1.2
|
80
|
+
|
81
|
+
boosts = []
|
82
|
+
options.each do |field, values|
|
83
|
+
filter = nil
|
84
|
+
if values.nil?
|
85
|
+
filter = Filters::ExistsFilter.new(field)
|
86
|
+
elsif values.is_a?(Array)
|
87
|
+
filter = NotFilter.new(Filters::TermsFilter.new(field: field, values: Array(values)))
|
88
|
+
end
|
89
|
+
Boosts::FilterBoost.new(filter: filter)
|
90
|
+
end
|
91
|
+
clone_with(boosts: boosts)
|
92
|
+
end
|
93
|
+
|
94
|
+
def range(field:, min: nil, max: nil)
|
95
|
+
return self unless min || max
|
96
|
+
clone_with(filter: Filters::RangeFilter.new(
|
97
|
+
field: field,
|
98
|
+
min: min,
|
99
|
+
max: max
|
100
|
+
))
|
101
|
+
end
|
102
|
+
|
103
|
+
def not_range(field:, min: nil, max: nil)
|
104
|
+
return self unless min || max
|
105
|
+
clone_with(not_filter: Filters::RangeFilter.new(
|
106
|
+
field: field,
|
107
|
+
min: min,
|
108
|
+
max: max
|
109
|
+
))
|
110
|
+
end
|
111
|
+
|
112
|
+
def boost_range(field:, min: nil, max: nil, weight: 1.2)
|
113
|
+
return self unless min || max
|
114
|
+
clone_with(boost: Boosts::Boost.new(
|
115
|
+
filter: Filters::RangeFilter.new(
|
116
|
+
field: field,
|
117
|
+
min: min,
|
118
|
+
max: max
|
119
|
+
),
|
120
|
+
weight: weight
|
121
|
+
))
|
122
|
+
end
|
123
|
+
|
124
|
+
def geo(field: 'coords', distance: '50km', lat:, lng:)
|
125
|
+
clone_with(filter: Filters::GeoFilter.new(
|
126
|
+
field: field,
|
127
|
+
distance: distance,
|
128
|
+
lat: lat,
|
129
|
+
lng: lng
|
130
|
+
))
|
131
|
+
end
|
132
|
+
|
133
|
+
def not_geo(field: 'coords', distance: '50km', lat:, lng:)
|
134
|
+
clone_with(not_filter: Filters::GeoFilter.new(
|
135
|
+
field: field,
|
136
|
+
distance: distance,
|
137
|
+
lat: lat,
|
138
|
+
lng: lng
|
139
|
+
))
|
140
|
+
end
|
141
|
+
|
142
|
+
def boost_geo(options = {})
|
143
|
+
return self if options.empty?
|
144
|
+
clone_with(boost: Boosts::GeoBoost.new(options))
|
145
|
+
end
|
146
|
+
|
147
|
+
def match(string, options = {})
|
148
|
+
return self if string.empty?
|
149
|
+
field = options[:field] || '_all'
|
150
|
+
operator = options[:operator] || 'and'
|
151
|
+
clone_with(match: Queries::MatchQuery.new(string, field: field, operator: operator))
|
152
|
+
end
|
153
|
+
|
154
|
+
def not_match(string, options = {})
|
155
|
+
field = options[:field] || '_all'
|
156
|
+
operator = options[:operator] || 'and'
|
157
|
+
clone_with(not_filter: Filters::QueryFilter.new(
|
158
|
+
Queries::MatchQuery.new(string, field: field, operator: operator)
|
159
|
+
))
|
160
|
+
end
|
161
|
+
|
162
|
+
def boost_random(seed)
|
163
|
+
clone_with(boost: Boosts::RandomBoost.new(seed))
|
164
|
+
end
|
165
|
+
|
166
|
+
def request
|
167
|
+
@request ||= RequestBody.new(
|
168
|
+
match: @match,
|
169
|
+
filters: @filters,
|
170
|
+
not_filters: @not_filters,
|
171
|
+
boosts: @boosts,
|
172
|
+
limit: @limit,
|
173
|
+
offset: @offset,
|
174
|
+
explain: @explain
|
175
|
+
)
|
176
|
+
end
|
177
|
+
|
178
|
+
def response
|
179
|
+
@response = Stretchy.search(type: @type, body: request.to_search)
|
180
|
+
# caution: huuuuge logs, but pretty helpful
|
181
|
+
# Rails.logger.debug(Colorize.purple(JSON.pretty_generate(@response)))
|
182
|
+
@response
|
183
|
+
end
|
184
|
+
|
185
|
+
def id_response
|
186
|
+
@id_response ||= Stretchy.search(type: @type, body: request.to_search, fields: [])
|
187
|
+
# caution: huuuuge logs, but pretty helpful
|
188
|
+
# Rails.logger.debug(Colorize.purple(JSON.pretty_generate(@id_response)))
|
189
|
+
@id_response
|
190
|
+
end
|
191
|
+
|
192
|
+
def results
|
193
|
+
@results ||= response['hits']['hits'].map do |hit|
|
194
|
+
hit['_source'].merge(
|
195
|
+
'_id' => hit['_id'],
|
196
|
+
'_type' => hit['_type'],
|
197
|
+
'_index' => hit['_index']
|
198
|
+
)
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
def ids
|
203
|
+
@ids ||= result_metadata('hits', 'hits').map do |hit|
|
204
|
+
hit['_id'].to_i == 0 ? hit['_id'] : hit['_id'].to_i
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
208
|
+
def shards
|
209
|
+
@shards ||= result_metadata('shards')
|
210
|
+
end
|
211
|
+
|
212
|
+
def total
|
213
|
+
@total ||= result_metadata('hits', 'total')
|
214
|
+
end
|
215
|
+
|
216
|
+
private
|
217
|
+
def result_metadata(*args)
|
218
|
+
if @response
|
219
|
+
args.reduce(@response){|json, field| json.nil? ? nil : json[field] }
|
220
|
+
else
|
221
|
+
args.reduce(id_response){|json, field| json.nil? ? nil : json[field] }
|
222
|
+
end
|
223
|
+
end
|
224
|
+
|
225
|
+
def where_clause(options = {})
|
226
|
+
return [[], []] if options.empty?
|
227
|
+
filters = []
|
228
|
+
not_filters = []
|
229
|
+
|
230
|
+
options.each do |field, values|
|
231
|
+
if values.nil?
|
232
|
+
not_filters << Filters::ExistsFilter.new(field)
|
233
|
+
else
|
234
|
+
filters << Filters::TermsFilter.new(field: field, values: Array(values))
|
235
|
+
end
|
236
|
+
end
|
237
|
+
[filters, not_filters]
|
238
|
+
end
|
239
|
+
|
240
|
+
end
|
241
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
module Stretchy
|
2
|
+
class RequestBody
|
3
|
+
|
4
|
+
def initialize(options = {})
|
5
|
+
@json = {}
|
6
|
+
@match = options[:match]
|
7
|
+
@filters = Array(options[:filters])
|
8
|
+
@not_filters = Array(options[:not_filters])
|
9
|
+
@boosts = Array(options[:boosts])
|
10
|
+
@offset = options[:offset] || 0
|
11
|
+
@limit = options[:limit] || Query::DEFAULT_LIMIT
|
12
|
+
@explain = options[:explain]
|
13
|
+
end
|
14
|
+
|
15
|
+
def to_search
|
16
|
+
return @json unless @json.empty?
|
17
|
+
|
18
|
+
query = @match || Queries::MatchAllQuery.new
|
19
|
+
|
20
|
+
if @filters.any? && @not_filters.any?
|
21
|
+
query = Queries::FilteredQuery.new(
|
22
|
+
query: query,
|
23
|
+
filter: Filters::BoolFilter.new(
|
24
|
+
must: @filters,
|
25
|
+
must_not: @not_filters
|
26
|
+
)
|
27
|
+
)
|
28
|
+
elsif @filters.any?
|
29
|
+
if @filters.count == 1
|
30
|
+
query = Queries::FilteredQuery.new(
|
31
|
+
query: query,
|
32
|
+
filter: @filters.first
|
33
|
+
)
|
34
|
+
else
|
35
|
+
query = Queries::FilteredQuery.new(
|
36
|
+
query: query,
|
37
|
+
filter: Filters::AndFilter.new(@filters)
|
38
|
+
)
|
39
|
+
end
|
40
|
+
elsif @not_filters.any?
|
41
|
+
query = Queries::FilteredQuery.new(
|
42
|
+
query: query,
|
43
|
+
filter: Filters::NotFilter.new(@not_filters)
|
44
|
+
)
|
45
|
+
end
|
46
|
+
|
47
|
+
if @boosts.any?
|
48
|
+
query = Queries::FunctionScoreQuery.new(
|
49
|
+
query: query,
|
50
|
+
functions: @boosts,
|
51
|
+
score_mode: 'sum',
|
52
|
+
boost_mode: 'max'
|
53
|
+
)
|
54
|
+
end
|
55
|
+
|
56
|
+
@json = {}
|
57
|
+
@json[:query] = query.to_search
|
58
|
+
@json[:from] = @offset
|
59
|
+
@json[:size] = @limit
|
60
|
+
@json[:explain] = @explain if @explain
|
61
|
+
|
62
|
+
# not a ton of output, usually worth having
|
63
|
+
# puts "Generated elastic query: #{JSON.pretty_generate(@json)}"
|
64
|
+
@json
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
data/lib/stretchy.rb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'logger'
|
3
|
+
require 'excon'
|
4
|
+
require 'elasticsearch'
|
5
|
+
|
6
|
+
Dir[File.join(File.dirname(__FILE__), 'stretchy', '**', '*.rb')].each do |path|
|
7
|
+
require path
|
8
|
+
end
|
9
|
+
|
10
|
+
module Stretchy
|
11
|
+
extend Configuration
|
12
|
+
extend ClientActions
|
13
|
+
|
14
|
+
end
|
data/stretchy.gemspec
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'stretchy/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "stretchy"
|
8
|
+
spec.version = Stretchy::VERSION
|
9
|
+
spec.authors = ["agius"]
|
10
|
+
spec.email = ["andrew@atevans.com"]
|
11
|
+
spec.licenses = ['MIT']
|
12
|
+
|
13
|
+
spec.summary = %q{Query builder for Elasticsearch}
|
14
|
+
spec.description = %q{Build queries for Elasticsearch with a chainable interface like ActiveRecord's.}
|
15
|
+
spec.homepage = "https://github.com/hired/stretchy"
|
16
|
+
|
17
|
+
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
18
|
+
spec.bindir = "exe"
|
19
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
20
|
+
spec.require_paths = ["lib"]
|
21
|
+
|
22
|
+
spec.add_dependency "elasticsearch", "~> 1.0"
|
23
|
+
spec.add_dependency "excon", "~> 0.45"
|
24
|
+
|
25
|
+
spec.add_development_dependency "bundler", "~> 1.8"
|
26
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
27
|
+
spec.add_development_dependency "rspec", "~> 3.2"
|
28
|
+
spec.add_development_dependency "fuubar", "~> 2.0"
|
29
|
+
end
|
metadata
ADDED
@@ -0,0 +1,158 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: stretchy
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- agius
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-04-17 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: elasticsearch
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: excon
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0.45'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0.45'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: bundler
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '1.8'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '1.8'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rake
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '10.0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '10.0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rspec
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '3.2'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '3.2'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: fuubar
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '2.0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '2.0'
|
97
|
+
description: Build queries for Elasticsearch with a chainable interface like ActiveRecord's.
|
98
|
+
email:
|
99
|
+
- andrew@atevans.com
|
100
|
+
executables: []
|
101
|
+
extensions: []
|
102
|
+
extra_rdoc_files: []
|
103
|
+
files:
|
104
|
+
- ".gitignore"
|
105
|
+
- ".travis.yml"
|
106
|
+
- Gemfile
|
107
|
+
- README.md
|
108
|
+
- Rakefile
|
109
|
+
- bin/console
|
110
|
+
- bin/setup
|
111
|
+
- lib/stretchy.rb
|
112
|
+
- lib/stretchy/boosts/filter_boost.rb
|
113
|
+
- lib/stretchy/boosts/geo_boost.rb
|
114
|
+
- lib/stretchy/boosts/random_boost.rb
|
115
|
+
- lib/stretchy/client_actions.rb
|
116
|
+
- lib/stretchy/configuration.rb
|
117
|
+
- lib/stretchy/filters/and_filter.rb
|
118
|
+
- lib/stretchy/filters/bool_filter.rb
|
119
|
+
- lib/stretchy/filters/exists_filter.rb
|
120
|
+
- lib/stretchy/filters/geo_filter.rb
|
121
|
+
- lib/stretchy/filters/not_filter.rb
|
122
|
+
- lib/stretchy/filters/query_filter.rb
|
123
|
+
- lib/stretchy/filters/range_filter.rb
|
124
|
+
- lib/stretchy/filters/terms_filter.rb
|
125
|
+
- lib/stretchy/null_query.rb
|
126
|
+
- lib/stretchy/queries/filtered_query.rb
|
127
|
+
- lib/stretchy/queries/function_score_query.rb
|
128
|
+
- lib/stretchy/queries/match_all_query.rb
|
129
|
+
- lib/stretchy/queries/match_query.rb
|
130
|
+
- lib/stretchy/query.rb
|
131
|
+
- lib/stretchy/request_body.rb
|
132
|
+
- lib/stretchy/version.rb
|
133
|
+
- stretchy.gemspec
|
134
|
+
homepage: https://github.com/hired/stretchy
|
135
|
+
licenses:
|
136
|
+
- MIT
|
137
|
+
metadata: {}
|
138
|
+
post_install_message:
|
139
|
+
rdoc_options: []
|
140
|
+
require_paths:
|
141
|
+
- lib
|
142
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
143
|
+
requirements:
|
144
|
+
- - ">="
|
145
|
+
- !ruby/object:Gem::Version
|
146
|
+
version: '0'
|
147
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
148
|
+
requirements:
|
149
|
+
- - ">="
|
150
|
+
- !ruby/object:Gem::Version
|
151
|
+
version: '0'
|
152
|
+
requirements: []
|
153
|
+
rubyforge_project:
|
154
|
+
rubygems_version: 2.2.2
|
155
|
+
signing_key:
|
156
|
+
specification_version: 4
|
157
|
+
summary: Query builder for Elasticsearch
|
158
|
+
test_files: []
|