stretchy 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
[![Build Status](https://travis-ci.org/hired/stretchy.svg?branch=master)](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: []
|