estella 0.2.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 +1 -0
- data/.rspec +5 -0
- data/.rubocop.yml +5 -0
- data/.rubocop_todo.yml +78 -0
- data/.travis.yml +13 -0
- data/CHANGELOG.md +5 -0
- data/Gemfile +3 -0
- data/LICENSE +21 -0
- data/README.md +195 -0
- data/RELEASING.md +66 -0
- data/Rakefile +16 -0
- data/estella.gemspec +27 -0
- data/lib/estella/analysis.rb +61 -0
- data/lib/estella/helpers.rb +94 -0
- data/lib/estella/parser.rb +27 -0
- data/lib/estella/query.rb +125 -0
- data/lib/estella/searchable.rb +87 -0
- data/lib/estella/version.rb +3 -0
- data/lib/estella.rb +8 -0
- data/spec/searchable_spec.rb +108 -0
- data/spec/spec_helper.rb +22 -0
- metadata +194 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: bf61c93e5566773e32ae933239e245236cce24e3
|
4
|
+
data.tar.gz: c3d455dacea867bd1e53d325120991ff59cc0979
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 56bb154fc881a5670a089247dbb4d83134cbbb9303707835a3138ae987d13385e3598c2b7841ce912e635d5177885cda1441e321db7854cfcc0df87765ee90ea
|
7
|
+
data.tar.gz: 4b6ad124cba5f0a4bfd777de3afcf81c6d17af16796ff160825b190a26359942450e1b657ef9024206e3e5c4831c378100355f0a3cfe5e0c2425d8816d1442f0
|
data/.gitignore
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
Gemfile.lock
|
data/.rspec
ADDED
data/.rubocop.yml
ADDED
data/.rubocop_todo.yml
ADDED
@@ -0,0 +1,78 @@
|
|
1
|
+
# This configuration was generated by
|
2
|
+
# `rubocop --auto-gen-config`
|
3
|
+
# on 2017-01-24 13:49:04 -0500 using RuboCop version 0.47.1.
|
4
|
+
# The point is for the user to remove these configuration records
|
5
|
+
# one by one as the offenses are removed from the code base.
|
6
|
+
# Note that changes in the inspected code, or installation of new
|
7
|
+
# versions of RuboCop, may require this file to be generated again.
|
8
|
+
|
9
|
+
# Offense count: 3
|
10
|
+
Metrics/AbcSize:
|
11
|
+
Max: 24
|
12
|
+
|
13
|
+
# Offense count: 3
|
14
|
+
# Configuration parameters: CountComments, ExcludedMethods.
|
15
|
+
Metrics/BlockLength:
|
16
|
+
Max: 94
|
17
|
+
|
18
|
+
# Offense count: 26
|
19
|
+
# Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns.
|
20
|
+
# URISchemes: http, https
|
21
|
+
Metrics/LineLength:
|
22
|
+
Max: 131
|
23
|
+
|
24
|
+
# Offense count: 1
|
25
|
+
# Configuration parameters: CountComments.
|
26
|
+
Metrics/MethodLength:
|
27
|
+
Max: 14
|
28
|
+
|
29
|
+
# Offense count: 3
|
30
|
+
# Cop supports --auto-correct.
|
31
|
+
# Configuration parameters: MaxKeyValuePairs.
|
32
|
+
Performance/RedundantMerge:
|
33
|
+
Exclude:
|
34
|
+
- 'lib/estella/parser.rb'
|
35
|
+
- 'lib/estella/searchable.rb'
|
36
|
+
|
37
|
+
# Offense count: 1
|
38
|
+
Style/AccessorMethodName:
|
39
|
+
Exclude:
|
40
|
+
- 'lib/estella/helpers.rb'
|
41
|
+
|
42
|
+
# Offense count: 1
|
43
|
+
Style/ClassVars:
|
44
|
+
Exclude:
|
45
|
+
- 'lib/estella/helpers.rb'
|
46
|
+
|
47
|
+
# Offense count: 7
|
48
|
+
Style/Documentation:
|
49
|
+
Exclude:
|
50
|
+
- 'spec/**/*'
|
51
|
+
- 'test/**/*'
|
52
|
+
- 'lib/estella/analysis.rb'
|
53
|
+
- 'lib/estella/helpers.rb'
|
54
|
+
- 'lib/estella/parser.rb'
|
55
|
+
- 'lib/estella/query.rb'
|
56
|
+
- 'lib/estella/searchable.rb'
|
57
|
+
|
58
|
+
# Offense count: 3
|
59
|
+
# Configuration parameters: MinBodyLength.
|
60
|
+
Style/GuardClause:
|
61
|
+
Exclude:
|
62
|
+
- 'lib/estella/query.rb'
|
63
|
+
- 'lib/estella/searchable.rb'
|
64
|
+
|
65
|
+
# Offense count: 9
|
66
|
+
# Cop supports --auto-correct.
|
67
|
+
Style/MutableConstant:
|
68
|
+
Exclude:
|
69
|
+
- 'lib/estella/analysis.rb'
|
70
|
+
- 'lib/estella/version.rb'
|
71
|
+
|
72
|
+
# Offense count: 2
|
73
|
+
# Cop supports --auto-correct.
|
74
|
+
# Configuration parameters: EnforcedStyle, SupportedStyles.
|
75
|
+
# SupportedStyles: only_raise, only_fail, semantic
|
76
|
+
Style/SignalException:
|
77
|
+
Exclude:
|
78
|
+
- 'lib/estella/parser.rb'
|
data/.travis.yml
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
language: ruby
|
2
|
+
|
3
|
+
cache: bundler
|
4
|
+
|
5
|
+
rvm:
|
6
|
+
- 2.2.2
|
7
|
+
|
8
|
+
before_install:
|
9
|
+
- gem update bundler
|
10
|
+
- "curl -O https://download.elasticsearch.org/elasticsearch/release/org/elasticsearch/distribution/deb/elasticsearch/2.1.1/elasticsearch-2.1.1.deb && sudo dpkg -i --force-confnew elasticsearch-2.1.1.deb"
|
11
|
+
- "echo 'script.inline: on' | sudo tee -a /etc/elasticsearch/elasticsearch.yml"
|
12
|
+
- "sudo /etc/init.d/elasticsearch start"
|
13
|
+
- "sleep 5"
|
data/CHANGELOG.md
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2017 Artsy Inc.
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,195 @@
|
|
1
|
+
# estella
|
2
|
+
|
3
|
+
[](https://badge.fury.io/rb/estella)
|
4
|
+
[](https://travis-ci.org/artsy/estella)
|
5
|
+
[](https://git.legal/projects/3493)
|
6
|
+
|
7
|
+
Builds on [elasticsearch-model](https://github.com/elastic/elasticsearch-rails/tree/master/elasticsearch-model) to make your Ruby objects searchable with Elasticsearch. Provides fine-grained control of fields, analysis, filters, weightings and boosts.
|
8
|
+
|
9
|
+
## Installation
|
10
|
+
|
11
|
+
```
|
12
|
+
gem 'estella'
|
13
|
+
```
|
14
|
+
|
15
|
+
The module will try to use Elasticsearch on `localhost:9200` by default. You can configure your global ES client like so:
|
16
|
+
|
17
|
+
```ruby
|
18
|
+
Elasticsearch::Model.client = Elasticsearch::Client.new host: 'foo.com', log: true
|
19
|
+
```
|
20
|
+
|
21
|
+
It is also configurable on a per model basis, see the [doc](https://github.com/elastic/elasticsearch-rails/tree/master/elasticsearch-model#the-elasticsearch-client).
|
22
|
+
|
23
|
+
## Indexing
|
24
|
+
|
25
|
+
Just include the `Estella::Searchable` module and add a `searchable` block in your ActiveRecord or Mongoid model declaring the fields to be indexed like so:
|
26
|
+
|
27
|
+
```ruby
|
28
|
+
class Artist < ActiveRecord::Base
|
29
|
+
include Estella::Searchable
|
30
|
+
|
31
|
+
searchable do
|
32
|
+
field :name, type: :string, analysis: Estella::Analysis::FULLTEXT_ANALYSIS, factor: 1.0
|
33
|
+
field :keywords, type: :string, analysis: ['snowball', 'shingle'], factor: 0.5
|
34
|
+
field :bio, using: :biography, type: :string, index: :not_analyzed
|
35
|
+
field :birth_date, type: :date
|
36
|
+
field :follows, type: :integer
|
37
|
+
field :published, type: :boolean, filter: true
|
38
|
+
boost :follows, modifier: 'log1p', factor: 1E-3
|
39
|
+
end
|
40
|
+
...
|
41
|
+
end
|
42
|
+
```
|
43
|
+
|
44
|
+
For a full understanding of the options available for field mappings, see the Elastic [mapping documentation](https://www.elastic.co/guide/en/elasticsearch/reference/2.4/mapping.html).
|
45
|
+
|
46
|
+
The `filter` option allows the field to be used as a filter at search time.
|
47
|
+
|
48
|
+
You can optionally provide field weightings to be applied at search time using the `factor` option. These are multipliers.
|
49
|
+
|
50
|
+
Document-level boosts can be applied with the `boost` declaration, see the [field_value_factor](https://www.elastic.co/guide/en/elasticsearch/reference/2.4/query-dsl-function-score-query.html#function-field-value-factor) documentation for boost options.
|
51
|
+
|
52
|
+
While `filter`, `boost` and `factor` are query options, Estella allows for their static declaration in the `searchable` block for simplicity - they will be applied at query time by default when using `#estella_search`.
|
53
|
+
|
54
|
+
You can now create your index mappings with this migration:
|
55
|
+
|
56
|
+
```ruby
|
57
|
+
Artist.reload_index!
|
58
|
+
```
|
59
|
+
|
60
|
+
This uses a default index naming scheme based on your model name, which you can override simply by declaring the following in your model:
|
61
|
+
|
62
|
+
```ruby
|
63
|
+
index_name 'my_index_name'
|
64
|
+
```
|
65
|
+
|
66
|
+
Start indexing documents simply by creating or saving them:
|
67
|
+
|
68
|
+
```ruby
|
69
|
+
Artist.create(name: 'Frank Estella', keywords: ['art', 'minimalism'])
|
70
|
+
```
|
71
|
+
|
72
|
+
Estella adds `after_save` and `after_destroy` callbacks for inline indexing, override these callbacks if you'd like to do your indexing in a background process. For example:
|
73
|
+
|
74
|
+
```ruby
|
75
|
+
class Artist < ActiveRecord::Base
|
76
|
+
include Estella::Searchable
|
77
|
+
|
78
|
+
# disable estella inline callbacks
|
79
|
+
skip_callback(:save, :after, :es_index)
|
80
|
+
skip_callback(:destroy, :after, :es_delete)
|
81
|
+
|
82
|
+
# declare your own
|
83
|
+
after_save :delay_es_index
|
84
|
+
after_destroy :delay_es_delete
|
85
|
+
|
86
|
+
...
|
87
|
+
end
|
88
|
+
```
|
89
|
+
|
90
|
+
## Custom Analysis
|
91
|
+
|
92
|
+
Estella defines `standard`, `snowball`, `ngram` and `shingle` analysers by default. These cover most search contexts, including auto-suggest. In order to enable full-text search for a field, use:
|
93
|
+
|
94
|
+
```ruby
|
95
|
+
analysis: Estella::Analysis::FULLTEXT_ANALYSIS
|
96
|
+
```
|
97
|
+
|
98
|
+
Or alternatively select your analysis by listing the analysers you want enabled for a given field:
|
99
|
+
|
100
|
+
```ruby
|
101
|
+
es_field :keywords, type: :string, analysis: ['snowball', 'shingle']
|
102
|
+
```
|
103
|
+
|
104
|
+
The searchable block takes a `settings` hash in case you require custom analysers or sharding (see [doc](https://www.elastic.co/guide/en/elasticsearch/guide/current/configuring-analyzers.html)):
|
105
|
+
|
106
|
+
```ruby
|
107
|
+
my_analysis = {
|
108
|
+
tokenizer: {
|
109
|
+
...
|
110
|
+
},
|
111
|
+
filter: {
|
112
|
+
...
|
113
|
+
}
|
114
|
+
}
|
115
|
+
|
116
|
+
my_settings = {
|
117
|
+
analysis: my_analysis,
|
118
|
+
index: {
|
119
|
+
number_of_shards: 1,
|
120
|
+
number_of_replicas: 1
|
121
|
+
}
|
122
|
+
}
|
123
|
+
|
124
|
+
searchable my_settings do
|
125
|
+
...
|
126
|
+
end
|
127
|
+
```
|
128
|
+
|
129
|
+
It will otherwise use Estella defaults.
|
130
|
+
|
131
|
+
## Searching
|
132
|
+
|
133
|
+
Finally perform full-text search:
|
134
|
+
|
135
|
+
```ruby
|
136
|
+
Artist.estella_search(term: 'frank')
|
137
|
+
Artist.estella_search(term: 'minimalism')
|
138
|
+
```
|
139
|
+
|
140
|
+
Estella searches all analysed text fields by default, using a [multi_match](https://www.elastic.co/guide/en/elasticsearch/guide/current/multi-match-query.html) search. The search will return an array of database records in score order. If you'd like access to the raw Elasticsearch response data use the `raw` option:
|
141
|
+
|
142
|
+
```ruby
|
143
|
+
Artist.estella_search(term: 'frank', raw: true)
|
144
|
+
```
|
145
|
+
|
146
|
+
Estella supports filtering on `filter` fields and pagination:
|
147
|
+
|
148
|
+
```ruby
|
149
|
+
Artist.estella_search(term: 'frank', published: true)
|
150
|
+
Artist.estella_search(term: 'frank', size: 10, from: 5)
|
151
|
+
```
|
152
|
+
|
153
|
+
If you'd like to customize your query further, you can extend `Estella::Query` and override the `query_definition`:
|
154
|
+
|
155
|
+
```ruby
|
156
|
+
class MyQuery < Estella::Query
|
157
|
+
def query_definition
|
158
|
+
{
|
159
|
+
multi_match: {
|
160
|
+
...
|
161
|
+
}
|
162
|
+
}
|
163
|
+
end
|
164
|
+
end
|
165
|
+
```
|
166
|
+
|
167
|
+
And then override class method `estella_search_query` to direct Estella to use your query object:
|
168
|
+
|
169
|
+
```ruby
|
170
|
+
class Artist < ActiveRecord::Base
|
171
|
+
include Estella::Searchable
|
172
|
+
|
173
|
+
searchable do
|
174
|
+
...
|
175
|
+
end
|
176
|
+
|
177
|
+
def self.estella_search_query
|
178
|
+
MyQuery
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
Artist.estella_search (term: 'frank')
|
183
|
+
```
|
184
|
+
|
185
|
+
For further search customization, see the [elasticsearch dsl](https://github.com/elastic/elasticsearch-rails/tree/master/elasticsearch-model#the-elasticsearch-dsl).
|
186
|
+
|
187
|
+
Estella works with any ActiveRecord or Mongoid compatible data models.
|
188
|
+
|
189
|
+
## Contributing
|
190
|
+
|
191
|
+
Just fork the repo and submit a pull request.
|
192
|
+
|
193
|
+
## License
|
194
|
+
|
195
|
+
Copyright (c) 2017 Artsy Inc., [MIT License](LICENSE).
|
data/RELEASING.md
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
# Releasing Estella
|
2
|
+
|
3
|
+
There're no hard rules about when to release estella. Release bug fixes frequenty, features not so frequently and breaking API changes rarely.
|
4
|
+
|
5
|
+
### Release
|
6
|
+
|
7
|
+
Run tests, check that all tests succeed locally.
|
8
|
+
|
9
|
+
```
|
10
|
+
bundle install
|
11
|
+
rake
|
12
|
+
```
|
13
|
+
|
14
|
+
Check that the last build succeeded in [Travis CI](https://travis-ci.org/dblock/estella) for all supported platforms.
|
15
|
+
|
16
|
+
Increment the version, modify [lib/estella/version.rb](lib/estella/version.rb).
|
17
|
+
|
18
|
+
* Increment the third number if the release has bug fixes and/or very minor features, only (eg. change `0.2.1` to `0.2.2`).
|
19
|
+
* Increment the second number if the release contains major features or breaking API changes (eg. change `0.2.1` to `0.3.0`).
|
20
|
+
|
21
|
+
Change "Next Release" in [CHANGELOG.md](CHANGELOG.md) to the new version.
|
22
|
+
|
23
|
+
```
|
24
|
+
### 0.2.2 (1/17/2017)
|
25
|
+
```
|
26
|
+
|
27
|
+
Remove the line with "Your contribution here.", since there will be no more contributions to this release.
|
28
|
+
|
29
|
+
Commit your changes.
|
30
|
+
|
31
|
+
```
|
32
|
+
git add README.md CHANGELOG.md lib/estella/version.rb
|
33
|
+
git commit -m "Preparing for release, 0.2.2."
|
34
|
+
git push origin master
|
35
|
+
```
|
36
|
+
|
37
|
+
Release.
|
38
|
+
|
39
|
+
```
|
40
|
+
$ rake release
|
41
|
+
|
42
|
+
estella 0.2.2 built to pkg/estella-0.2.2.gem.
|
43
|
+
Tagged v0.2.2.
|
44
|
+
Pushed git commits and tags.
|
45
|
+
Pushed estella 0.2.2 to rubygems.org.
|
46
|
+
```
|
47
|
+
|
48
|
+
### Prepare for the Next Version
|
49
|
+
|
50
|
+
Increment the third version number in [lib/estella/version.rb](lib/estella/version.rb).
|
51
|
+
|
52
|
+
Add the next release to [CHANGELOG.md](CHANGELOG.md).
|
53
|
+
|
54
|
+
```
|
55
|
+
### 0.2.3 (Next)
|
56
|
+
|
57
|
+
* Your contribution here.
|
58
|
+
```
|
59
|
+
|
60
|
+
Comit your changes.
|
61
|
+
|
62
|
+
```
|
63
|
+
git add CHANGELOG.md lib/estella/version.rb
|
64
|
+
git commit -m "Preparing for next development iteration, 0.2.3."
|
65
|
+
git push origin master
|
66
|
+
```
|
data/Rakefile
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'bundler/gem_tasks'
|
3
|
+
|
4
|
+
Bundler.setup :default, :development
|
5
|
+
|
6
|
+
require 'rspec/core'
|
7
|
+
require 'rspec/core/rake_task'
|
8
|
+
|
9
|
+
RSpec::Core::RakeTask.new(:spec) do |spec|
|
10
|
+
spec.pattern = FileList['spec/**/*_spec.rb']
|
11
|
+
end
|
12
|
+
|
13
|
+
require 'rubocop/rake_task'
|
14
|
+
RuboCop::RakeTask.new(:rubocop)
|
15
|
+
|
16
|
+
task default: [:rubocop, :spec]
|
data/estella.gemspec
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
$LOAD_PATH.unshift File.expand_path('../lib', __FILE__)
|
2
|
+
require 'estella/version'
|
3
|
+
|
4
|
+
Gem::Specification.new do |gem|
|
5
|
+
gem.name = 'estella'
|
6
|
+
gem.homepage = 'https://github.com/artsy/estella'
|
7
|
+
gem.license = 'MIT'
|
8
|
+
gem.summary = %(Make your Ruby objects searchable with Elasticsearch.)
|
9
|
+
gem.version = Estella::VERSION
|
10
|
+
gem.description = 'Make your Ruby objects searchable with Elasticsearch.'
|
11
|
+
gem.email = ['anil@artsy.net']
|
12
|
+
gem.authors = ['Anil Bawa-Cavia', 'Matt Zikherman']
|
13
|
+
|
14
|
+
gem.files = `git ls-files`.split("\n")
|
15
|
+
gem.test_files = `git ls-files -- spec/*`.split("\n")
|
16
|
+
|
17
|
+
gem.add_runtime_dependency 'elasticsearch-model'
|
18
|
+
gem.add_runtime_dependency 'activesupport'
|
19
|
+
gem.add_runtime_dependency 'activemodel'
|
20
|
+
|
21
|
+
gem.add_development_dependency 'rake', '~> 11.0'
|
22
|
+
gem.add_development_dependency 'activerecord'
|
23
|
+
gem.add_development_dependency 'rspec', '~> 3.1.0'
|
24
|
+
gem.add_development_dependency 'rspec-expectations'
|
25
|
+
gem.add_development_dependency 'sqlite3'
|
26
|
+
gem.add_development_dependency 'rubocop', '0.47.1'
|
27
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
module Estella
|
2
|
+
module Analysis
|
3
|
+
# Default Elasticsearch analysers
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
FRONT_NGRAM_FILTER =
|
7
|
+
{ type: 'edgeNGram', min_gram: 2, max_gram: 15, side: 'front' }
|
8
|
+
|
9
|
+
DEFAULT_ANALYZER =
|
10
|
+
{ type: 'custom', tokenizer: 'standard_tokenizer', filter: %w(lowercase asciifolding) }
|
11
|
+
|
12
|
+
SNOWBALL_ANALYZER =
|
13
|
+
{ type: 'custom', tokenizer: 'standard_tokenizer', filter: %w(lowercase asciifolding snowball) }
|
14
|
+
|
15
|
+
SHINGLE_ANALYZER =
|
16
|
+
{ type: 'custom', tokenizer: 'standard_tokenizer', filter: %w(shingle lowercase asciifolding) }
|
17
|
+
|
18
|
+
NGRAM_ANALYZER =
|
19
|
+
{ type: 'custom', tokenizer: 'standard_tokenizer', filter: %w(lowercase asciifolding front_ngram_filter) }
|
20
|
+
|
21
|
+
DEFAULT_ANALYSIS = {
|
22
|
+
tokenizer: {
|
23
|
+
standard_tokenizer: { type: 'standard' }
|
24
|
+
},
|
25
|
+
filter: {
|
26
|
+
front_ngram_filter: FRONT_NGRAM_FILTER
|
27
|
+
},
|
28
|
+
analyzer: {
|
29
|
+
default_analyzer: DEFAULT_ANALYZER,
|
30
|
+
snowball_analyzer: SNOWBALL_ANALYZER,
|
31
|
+
shingle_analyzer: SHINGLE_ANALYZER,
|
32
|
+
ngram_analyzer: NGRAM_ANALYZER,
|
33
|
+
search_analyzer: DEFAULT_ANALYZER
|
34
|
+
}
|
35
|
+
}
|
36
|
+
|
37
|
+
DEFAULT_FIELDS = {
|
38
|
+
default: { type: 'string', analyzer: 'default_analyzer' },
|
39
|
+
snowball: { type: 'string', analyzer: 'snowball_analyzer' },
|
40
|
+
shingle: { type: 'string', analyzer: 'shingle_analyzer' },
|
41
|
+
ngram: { type: 'string', analyzer: 'ngram_analyzer', search_analyzer: 'search_analyzer' }
|
42
|
+
}
|
43
|
+
|
44
|
+
DEFAULT_FIELD_FACTORS = {
|
45
|
+
default: 10,
|
46
|
+
ngram: 10,
|
47
|
+
snowball: 3,
|
48
|
+
shingle: 2,
|
49
|
+
search: 2
|
50
|
+
}
|
51
|
+
|
52
|
+
FULLTEXT_ANALYSIS = DEFAULT_FIELDS.keys
|
53
|
+
|
54
|
+
DEFAULT_SETTINGS = if defined? Rails && Rails.env == 'test'
|
55
|
+
# Ensure no sharding in test env in order to enforce deterministic scores.
|
56
|
+
{ analysis: DEFAULT_ANALYSIS, index: { number_of_shards: 1, number_of_replicas: 1 } }
|
57
|
+
else
|
58
|
+
{ analysis: DEFAULT_ANALYSIS }
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
module Estella
|
2
|
+
module Helpers
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
@@types = []
|
6
|
+
|
7
|
+
included do
|
8
|
+
index_name search_index_name
|
9
|
+
|
10
|
+
after_save :es_index
|
11
|
+
after_destroy :es_delete
|
12
|
+
|
13
|
+
attr_accessor :es_indexing
|
14
|
+
|
15
|
+
@@types << self
|
16
|
+
end
|
17
|
+
|
18
|
+
# track dependent classes for spec support
|
19
|
+
def self.types
|
20
|
+
@@types
|
21
|
+
end
|
22
|
+
|
23
|
+
def es_index
|
24
|
+
self.es_indexing = true
|
25
|
+
__elasticsearch__.index_document
|
26
|
+
ensure
|
27
|
+
self.es_indexing = nil
|
28
|
+
end
|
29
|
+
|
30
|
+
def es_delete
|
31
|
+
es_delete_document id
|
32
|
+
end
|
33
|
+
|
34
|
+
def es_transform
|
35
|
+
{ index: { _id: id.to_s, data: as_indexed_json } }
|
36
|
+
end
|
37
|
+
|
38
|
+
module ClassMethods
|
39
|
+
## Searching
|
40
|
+
|
41
|
+
def stella_raw_search(params = {})
|
42
|
+
__elasticsearch__.search(estella_query(params))
|
43
|
+
end
|
44
|
+
|
45
|
+
# @return an array of database records mapped using an adapter
|
46
|
+
def estella_search(params = {})
|
47
|
+
rsp = stella_raw_search(params)
|
48
|
+
params[:raw] ? rsp.response : rsp.records.to_a
|
49
|
+
end
|
50
|
+
|
51
|
+
## Indexing
|
52
|
+
|
53
|
+
# default index naming scheme is pluralized model_name
|
54
|
+
def search_index_name
|
55
|
+
model_name.route_key
|
56
|
+
end
|
57
|
+
|
58
|
+
def batch_to_bulk(batch_of_ids)
|
59
|
+
find(batch_of_ids).map(&:es_transform)
|
60
|
+
end
|
61
|
+
|
62
|
+
def bulk_index(batch_of_ids)
|
63
|
+
__elasticsearch__.client.bulk index: index_name, type: model_name.element, body: batch_to_bulk(batch_of_ids)
|
64
|
+
end
|
65
|
+
|
66
|
+
def index_exists?
|
67
|
+
__elasticsearch__.client.indices.exists index: index_name
|
68
|
+
end
|
69
|
+
|
70
|
+
def reload_index!
|
71
|
+
__elasticsearch__.client.indices.delete index: index_name if index_exists?
|
72
|
+
__elasticsearch__.client.indices.create index: index_name, body: { settings: settings.to_hash, mappings: mappings.to_hash }
|
73
|
+
end
|
74
|
+
|
75
|
+
def recreate_index!
|
76
|
+
reload_index!
|
77
|
+
import
|
78
|
+
refresh_index!
|
79
|
+
end
|
80
|
+
|
81
|
+
def refresh_index!
|
82
|
+
__elasticsearch__.refresh_index!
|
83
|
+
end
|
84
|
+
|
85
|
+
def set_index_alias!(name)
|
86
|
+
__elasticsearch__.client.indices.put_alias index: index_name, name: name
|
87
|
+
end
|
88
|
+
|
89
|
+
def es_delete_document(id)
|
90
|
+
__elasticsearch__.client.delete type: document_type, id: id, index: index_name
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Estella
|
2
|
+
class Parser
|
3
|
+
def initialize(model)
|
4
|
+
@model = model
|
5
|
+
end
|
6
|
+
|
7
|
+
# document level boost
|
8
|
+
# @see https://www.elastic.co/guide/en/elasticsearch/guide/current/boosting-by-popularity.html
|
9
|
+
def boost(name, opts = {})
|
10
|
+
fail ArgumentError, 'Boost field is not indexed!' unless @model.indexed_fields.include? name
|
11
|
+
unless (opts.keys & [:modifier, :factor]).length == 2
|
12
|
+
fail ArgumentError, 'Please supply a modifier and a factor for your boost!'
|
13
|
+
end
|
14
|
+
@model.field_boost = { boost: { field: name }.merge(opts) }
|
15
|
+
end
|
16
|
+
|
17
|
+
# index a field
|
18
|
+
def field(name, opts = {})
|
19
|
+
using = opts[:using] || name
|
20
|
+
analysis = opts[:analysis] & @model.default_analysis_fields.keys
|
21
|
+
opts[:fields] ||= Hash[analysis.zip(@model.default_analysis_fields.values_at(*analysis))] if analysis
|
22
|
+
|
23
|
+
@model.indexed_json.merge!(name => using)
|
24
|
+
@model.indexed_fields.merge!(name => opts)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,125 @@
|
|
1
|
+
module Estella
|
2
|
+
class Query
|
3
|
+
# Constructs a search query for ES
|
4
|
+
attr_accessor :query
|
5
|
+
attr_reader :params
|
6
|
+
|
7
|
+
def initialize(params)
|
8
|
+
@params = params
|
9
|
+
@query = {
|
10
|
+
_source: false,
|
11
|
+
query: {},
|
12
|
+
filter: {
|
13
|
+
bool: { must: [], must_not: [] }
|
14
|
+
},
|
15
|
+
aggregations: {}
|
16
|
+
}
|
17
|
+
add_query
|
18
|
+
add_filters
|
19
|
+
add_pagination
|
20
|
+
add_aggregations if params[:aggregations]
|
21
|
+
add_sort
|
22
|
+
end
|
23
|
+
|
24
|
+
# override if needed
|
25
|
+
def add_aggregations; end
|
26
|
+
|
27
|
+
# override if needed
|
28
|
+
def add_sort; end
|
29
|
+
|
30
|
+
def must(filter)
|
31
|
+
query[:filter][:bool][:must] << filter
|
32
|
+
end
|
33
|
+
|
34
|
+
def exclude(filter)
|
35
|
+
query[:filter][:bool][:must_not] << filter
|
36
|
+
end
|
37
|
+
|
38
|
+
def add_pagination
|
39
|
+
query[:size] = params[:size] if params[:size]
|
40
|
+
query[:from] = params[:from] if params[:from]
|
41
|
+
end
|
42
|
+
|
43
|
+
def add_query
|
44
|
+
if params[:term] && params[:indexed_fields]
|
45
|
+
add_term_query
|
46
|
+
else
|
47
|
+
query[:query] = { match_all: {} }
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
# fulltext search across all string fields
|
52
|
+
def add_term_query
|
53
|
+
query[:query] = {
|
54
|
+
function_score: {
|
55
|
+
query: query_definition
|
56
|
+
}
|
57
|
+
}
|
58
|
+
|
59
|
+
add_field_boost
|
60
|
+
end
|
61
|
+
|
62
|
+
def query_definition
|
63
|
+
{
|
64
|
+
multi_match: {
|
65
|
+
type: 'most_fields',
|
66
|
+
fields: term_search_fields,
|
67
|
+
query: params[:term]
|
68
|
+
}
|
69
|
+
}
|
70
|
+
end
|
71
|
+
|
72
|
+
def add_field_boost
|
73
|
+
if params[:boost]
|
74
|
+
query[:query][:function_score][:field_value_factor] = {
|
75
|
+
field: params[:boost][:field],
|
76
|
+
modifier: params[:boost][:modifier],
|
77
|
+
factor: params[:boost][:factor]
|
78
|
+
}
|
79
|
+
|
80
|
+
if params[:boost][:max]
|
81
|
+
query[:query][:function_score][:max_boost] = params[:boost][:max]
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def field_factors
|
87
|
+
Estella::Analysis::DEFAULT_FIELD_FACTORS
|
88
|
+
end
|
89
|
+
|
90
|
+
# search all analysed string fields by default
|
91
|
+
# boost them by factor if provided
|
92
|
+
def term_search_fields
|
93
|
+
params[:indexed_fields]
|
94
|
+
.select { |_, opts| opts[:type].to_s == 'string' }
|
95
|
+
.reject { |_, opts| opts[:analysis].nil? }
|
96
|
+
.map do |field, opts|
|
97
|
+
opts[:analysis].map do |analyzer|
|
98
|
+
factor = field_factors[analyzer] * opts.fetch(:factor, 1.0)
|
99
|
+
"#{field}.#{analyzer}^#{factor}"
|
100
|
+
end
|
101
|
+
end
|
102
|
+
.flatten
|
103
|
+
end
|
104
|
+
|
105
|
+
def add_filters
|
106
|
+
if params[:indexed_fields]
|
107
|
+
params[:indexed_fields].each do |field, opts|
|
108
|
+
must term: { field => params[field] } if opts[:filter] && params[field]
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
def bool_filter(field, param)
|
114
|
+
if param
|
115
|
+
{ term: { field => true } }
|
116
|
+
elsif !param.nil?
|
117
|
+
{ term: { field => false } }
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
def add_bool_filter(field, param)
|
122
|
+
must bool_filter(field, param) if bool_filter(field, param)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
module Estella
|
2
|
+
module Searchable
|
3
|
+
# Makes your ActiveRecord model searchable via Elasticsearch
|
4
|
+
#
|
5
|
+
# Just include a block in your model like so:
|
6
|
+
#
|
7
|
+
# class Artist < ActiveRecord::Base
|
8
|
+
# searchable do
|
9
|
+
# field :name, type: :string, using: :my_attr, analysis: Estella::Analysis::FULLTEXT_ANALYSIS
|
10
|
+
# field :follows, type: :integer
|
11
|
+
# ...
|
12
|
+
# boost :follows, modifier: 'log1p', factor: 1E-3
|
13
|
+
# end
|
14
|
+
# end
|
15
|
+
#
|
16
|
+
# Document boosts are optional.
|
17
|
+
# You can now create your index with the following migration:
|
18
|
+
#
|
19
|
+
# Artist.reload_index!
|
20
|
+
# Artist.import
|
21
|
+
#
|
22
|
+
# And perform full-text search using:
|
23
|
+
#
|
24
|
+
# Artist.estella_search(term: x)
|
25
|
+
#
|
26
|
+
extend ActiveSupport::Concern
|
27
|
+
|
28
|
+
included do
|
29
|
+
include Elasticsearch::Model
|
30
|
+
include Estella::Helpers
|
31
|
+
include Estella::Analysis
|
32
|
+
|
33
|
+
@indexed_json = {}
|
34
|
+
@indexed_fields = {}
|
35
|
+
@field_boost = {}
|
36
|
+
|
37
|
+
class << self
|
38
|
+
attr_accessor :indexed_json, :indexed_fields, :field_boost
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.estella_query(params = {})
|
42
|
+
params.merge!(field_boost)
|
43
|
+
params.merge!(indexed_fields: indexed_fields)
|
44
|
+
estella_search_query.new(params).query
|
45
|
+
end
|
46
|
+
|
47
|
+
def self.estella_search_query
|
48
|
+
Estella::Query
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def as_indexed_json(_options = {})
|
53
|
+
schema = self.class.indexed_json
|
54
|
+
Hash[schema.keys.zip(schema.values.map { |v| v.respond_to?(:call) ? instance_exec(&v) : send(v) })]
|
55
|
+
end
|
56
|
+
|
57
|
+
module ClassMethods
|
58
|
+
# support for mongoid::slug
|
59
|
+
# indexes slug attribue by default
|
60
|
+
def index_slug
|
61
|
+
if defined? slug
|
62
|
+
indexed_fields.merge!(slug: { type: :string, index: :not_analyzed })
|
63
|
+
indexed_json.merge!(slug: :slug)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def default_analysis_fields
|
68
|
+
Estella::Analysis::DEFAULT_FIELDS
|
69
|
+
end
|
70
|
+
|
71
|
+
# sets up mappings and settings for index
|
72
|
+
def searchable(settings = Estella::Analysis::DEFAULT_SETTINGS, &block)
|
73
|
+
Estella::Parser.new(self).instance_eval(&block)
|
74
|
+
index_slug
|
75
|
+
indexed_fields = @indexed_fields
|
76
|
+
|
77
|
+
settings(settings) do
|
78
|
+
mapping do
|
79
|
+
indexed_fields.each do |name, opts|
|
80
|
+
indexes name, opts.except(:analysis, :using, :factor, :filter)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
data/lib/estella.rb
ADDED
@@ -0,0 +1,108 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'estella'
|
3
|
+
require 'active_record'
|
4
|
+
|
5
|
+
describe Estella::Searchable, type: :model do
|
6
|
+
before do
|
7
|
+
ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: ':memory:')
|
8
|
+
end
|
9
|
+
|
10
|
+
describe 'searchable model', elasticsearch: true do
|
11
|
+
before do
|
12
|
+
class SearchableModel < ActiveRecord::Base
|
13
|
+
include Estella::Searchable
|
14
|
+
|
15
|
+
def self.slug
|
16
|
+
# mongoid::slug support
|
17
|
+
'foo'
|
18
|
+
end
|
19
|
+
|
20
|
+
searchable do
|
21
|
+
field :title, type: :string, analysis: Estella::Analysis::FULLTEXT_ANALYSIS, factor: 1.0
|
22
|
+
field :keywords, type: :string, analysis: [:default, :snowball], factor: 0.5
|
23
|
+
field :follows_count, type: :integer
|
24
|
+
field :published, type: :boolean, filter: true
|
25
|
+
|
26
|
+
boost :follows_count, modifier: 'log2p', factor: 5E-4, max: 1.0
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
ActiveRecord::Schema.define(version: 1) do
|
31
|
+
create_table(:searchable_models) do |t|
|
32
|
+
t.string :title
|
33
|
+
t.string :keywords
|
34
|
+
t.string :slug
|
35
|
+
t.boolean :published
|
36
|
+
t.integer :follows_count, default: 0
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
SearchableModel.reload_index!
|
41
|
+
@jez = SearchableModel.create(title: 'jeremy corbyn', keywords: ['jez'])
|
42
|
+
@tez = SearchableModel.create(title: 'theresa may', keywords: ['tez'])
|
43
|
+
SearchableModel.refresh_index!
|
44
|
+
end
|
45
|
+
it 'returns relevant results' do
|
46
|
+
expect(SearchableModel.all.size).to eq(2)
|
47
|
+
expect(SearchableModel.estella_search(term: 'jeremy')).to eq([@jez])
|
48
|
+
expect(SearchableModel.estella_search(term: 'theresa')).to eq([@tez])
|
49
|
+
end
|
50
|
+
it 'uses ngram analysis by default' do
|
51
|
+
expect(SearchableModel.estella_search(term: 'jer')).to eq([@jez])
|
52
|
+
expect(SearchableModel.estella_search(term: 'there')).to eq([@tez])
|
53
|
+
end
|
54
|
+
it 'searches all text fields by default' do
|
55
|
+
expect(SearchableModel.estella_search(term: 'jez')).to eq([@jez])
|
56
|
+
end
|
57
|
+
it 'boosts on follows_count' do
|
58
|
+
popular_jeremy = SearchableModel.create(title: 'jeremy corban', follows_count: 20_000)
|
59
|
+
SearchableModel.refresh_index!
|
60
|
+
expect(SearchableModel.estella_search(term: 'jeremy')).to eq([popular_jeremy, @jez])
|
61
|
+
end
|
62
|
+
it 'uses factor option to weight fields' do
|
63
|
+
@dude = SearchableModel.create(keywords: ['dude'])
|
64
|
+
@dude2 = SearchableModel.create(title: 'dude')
|
65
|
+
SearchableModel.refresh_index!
|
66
|
+
expect(SearchableModel.estella_search(term: 'dude')).to eq([@dude2, @dude])
|
67
|
+
end
|
68
|
+
it 'returns raw response when raw option is set' do
|
69
|
+
expect(SearchableModel.estella_search(term: 'jeremy', raw: true).hits.hits.first['_id']).to eq(@jez.id.to_s)
|
70
|
+
end
|
71
|
+
it 'indexes slug field by default' do
|
72
|
+
SearchableModel.create(title: 'liapunov', slug: 'liapunov')
|
73
|
+
SearchableModel.refresh_index!
|
74
|
+
expect(SearchableModel.mappings.to_hash[:searchable_model][:properties].keys.include?(:slug)).to eq true
|
75
|
+
end
|
76
|
+
it 'supports boolean filters' do
|
77
|
+
@liapunov = SearchableModel.create(title: 'liapunov', published: true)
|
78
|
+
SearchableModel.create(title: 'liapunov unpublished')
|
79
|
+
SearchableModel.refresh_index!
|
80
|
+
expect(SearchableModel.estella_search(published: true)).to eq [@liapunov]
|
81
|
+
end
|
82
|
+
it 'does not override field method on class' do
|
83
|
+
expect(SearchableModel.methods.include?(:field)).to eq(false)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
describe 'configuration errors' do
|
88
|
+
it 'raises error when boost field is invalid' do
|
89
|
+
expect do
|
90
|
+
class BadSearchableModel < ActiveRecord::Base
|
91
|
+
include Estella::Searchable
|
92
|
+
searchable { boost :follows_count }
|
93
|
+
end
|
94
|
+
end.to raise_error(ArgumentError, 'Boost field is not indexed!')
|
95
|
+
end
|
96
|
+
it 'raises error when boost params are not set' do
|
97
|
+
expect do
|
98
|
+
class BadSearchableModel < ActiveRecord::Base
|
99
|
+
include Estella::Searchable
|
100
|
+
searchable do
|
101
|
+
field :follows_count, type: 'integer'
|
102
|
+
boost :follows_count
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end.to raise_error(ArgumentError, 'Please supply a modifier and a factor for your boost!')
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'active_support'
|
2
|
+
require 'active_model'
|
3
|
+
require 'rspec'
|
4
|
+
|
5
|
+
require File.expand_path('../../lib/estella.rb', __FILE__)
|
6
|
+
|
7
|
+
RSpec.configure do |config|
|
8
|
+
config.mock_with :rspec do |c|
|
9
|
+
c.syntax = :expect
|
10
|
+
end
|
11
|
+
|
12
|
+
config.expect_with :rspec do |c|
|
13
|
+
c.syntax = :expect
|
14
|
+
end
|
15
|
+
|
16
|
+
config.raise_errors_for_deprecations!
|
17
|
+
|
18
|
+
config.before(:context, elasticsearch: true) do
|
19
|
+
Elasticsearch::Model.client = Elasticsearch::Client.new
|
20
|
+
Estella::Helpers.types.each { |type| type.__elasticsearch__.client = nil } # clear memoized clients
|
21
|
+
end
|
22
|
+
end
|
metadata
ADDED
@@ -0,0 +1,194 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: estella
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.2.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Anil Bawa-Cavia
|
8
|
+
- Matt Zikherman
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2017-01-24 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: elasticsearch-model
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
requirements:
|
18
|
+
- - ">="
|
19
|
+
- !ruby/object:Gem::Version
|
20
|
+
version: '0'
|
21
|
+
type: :runtime
|
22
|
+
prerelease: false
|
23
|
+
version_requirements: !ruby/object:Gem::Requirement
|
24
|
+
requirements:
|
25
|
+
- - ">="
|
26
|
+
- !ruby/object:Gem::Version
|
27
|
+
version: '0'
|
28
|
+
- !ruby/object:Gem::Dependency
|
29
|
+
name: activesupport
|
30
|
+
requirement: !ruby/object:Gem::Requirement
|
31
|
+
requirements:
|
32
|
+
- - ">="
|
33
|
+
- !ruby/object:Gem::Version
|
34
|
+
version: '0'
|
35
|
+
type: :runtime
|
36
|
+
prerelease: false
|
37
|
+
version_requirements: !ruby/object:Gem::Requirement
|
38
|
+
requirements:
|
39
|
+
- - ">="
|
40
|
+
- !ruby/object:Gem::Version
|
41
|
+
version: '0'
|
42
|
+
- !ruby/object:Gem::Dependency
|
43
|
+
name: activemodel
|
44
|
+
requirement: !ruby/object:Gem::Requirement
|
45
|
+
requirements:
|
46
|
+
- - ">="
|
47
|
+
- !ruby/object:Gem::Version
|
48
|
+
version: '0'
|
49
|
+
type: :runtime
|
50
|
+
prerelease: false
|
51
|
+
version_requirements: !ruby/object:Gem::Requirement
|
52
|
+
requirements:
|
53
|
+
- - ">="
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
version: '0'
|
56
|
+
- !ruby/object:Gem::Dependency
|
57
|
+
name: rake
|
58
|
+
requirement: !ruby/object:Gem::Requirement
|
59
|
+
requirements:
|
60
|
+
- - "~>"
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
version: '11.0'
|
63
|
+
type: :development
|
64
|
+
prerelease: false
|
65
|
+
version_requirements: !ruby/object:Gem::Requirement
|
66
|
+
requirements:
|
67
|
+
- - "~>"
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '11.0'
|
70
|
+
- !ruby/object:Gem::Dependency
|
71
|
+
name: activerecord
|
72
|
+
requirement: !ruby/object:Gem::Requirement
|
73
|
+
requirements:
|
74
|
+
- - ">="
|
75
|
+
- !ruby/object:Gem::Version
|
76
|
+
version: '0'
|
77
|
+
type: :development
|
78
|
+
prerelease: false
|
79
|
+
version_requirements: !ruby/object:Gem::Requirement
|
80
|
+
requirements:
|
81
|
+
- - ">="
|
82
|
+
- !ruby/object:Gem::Version
|
83
|
+
version: '0'
|
84
|
+
- !ruby/object:Gem::Dependency
|
85
|
+
name: rspec
|
86
|
+
requirement: !ruby/object:Gem::Requirement
|
87
|
+
requirements:
|
88
|
+
- - "~>"
|
89
|
+
- !ruby/object:Gem::Version
|
90
|
+
version: 3.1.0
|
91
|
+
type: :development
|
92
|
+
prerelease: false
|
93
|
+
version_requirements: !ruby/object:Gem::Requirement
|
94
|
+
requirements:
|
95
|
+
- - "~>"
|
96
|
+
- !ruby/object:Gem::Version
|
97
|
+
version: 3.1.0
|
98
|
+
- !ruby/object:Gem::Dependency
|
99
|
+
name: rspec-expectations
|
100
|
+
requirement: !ruby/object:Gem::Requirement
|
101
|
+
requirements:
|
102
|
+
- - ">="
|
103
|
+
- !ruby/object:Gem::Version
|
104
|
+
version: '0'
|
105
|
+
type: :development
|
106
|
+
prerelease: false
|
107
|
+
version_requirements: !ruby/object:Gem::Requirement
|
108
|
+
requirements:
|
109
|
+
- - ">="
|
110
|
+
- !ruby/object:Gem::Version
|
111
|
+
version: '0'
|
112
|
+
- !ruby/object:Gem::Dependency
|
113
|
+
name: sqlite3
|
114
|
+
requirement: !ruby/object:Gem::Requirement
|
115
|
+
requirements:
|
116
|
+
- - ">="
|
117
|
+
- !ruby/object:Gem::Version
|
118
|
+
version: '0'
|
119
|
+
type: :development
|
120
|
+
prerelease: false
|
121
|
+
version_requirements: !ruby/object:Gem::Requirement
|
122
|
+
requirements:
|
123
|
+
- - ">="
|
124
|
+
- !ruby/object:Gem::Version
|
125
|
+
version: '0'
|
126
|
+
- !ruby/object:Gem::Dependency
|
127
|
+
name: rubocop
|
128
|
+
requirement: !ruby/object:Gem::Requirement
|
129
|
+
requirements:
|
130
|
+
- - '='
|
131
|
+
- !ruby/object:Gem::Version
|
132
|
+
version: 0.47.1
|
133
|
+
type: :development
|
134
|
+
prerelease: false
|
135
|
+
version_requirements: !ruby/object:Gem::Requirement
|
136
|
+
requirements:
|
137
|
+
- - '='
|
138
|
+
- !ruby/object:Gem::Version
|
139
|
+
version: 0.47.1
|
140
|
+
description: Make your Ruby objects searchable with Elasticsearch.
|
141
|
+
email:
|
142
|
+
- anil@artsy.net
|
143
|
+
executables: []
|
144
|
+
extensions: []
|
145
|
+
extra_rdoc_files: []
|
146
|
+
files:
|
147
|
+
- ".gitignore"
|
148
|
+
- ".rspec"
|
149
|
+
- ".rubocop.yml"
|
150
|
+
- ".rubocop_todo.yml"
|
151
|
+
- ".travis.yml"
|
152
|
+
- CHANGELOG.md
|
153
|
+
- Gemfile
|
154
|
+
- LICENSE
|
155
|
+
- README.md
|
156
|
+
- RELEASING.md
|
157
|
+
- Rakefile
|
158
|
+
- estella.gemspec
|
159
|
+
- lib/estella.rb
|
160
|
+
- lib/estella/analysis.rb
|
161
|
+
- lib/estella/helpers.rb
|
162
|
+
- lib/estella/parser.rb
|
163
|
+
- lib/estella/query.rb
|
164
|
+
- lib/estella/searchable.rb
|
165
|
+
- lib/estella/version.rb
|
166
|
+
- spec/searchable_spec.rb
|
167
|
+
- spec/spec_helper.rb
|
168
|
+
homepage: https://github.com/artsy/estella
|
169
|
+
licenses:
|
170
|
+
- MIT
|
171
|
+
metadata: {}
|
172
|
+
post_install_message:
|
173
|
+
rdoc_options: []
|
174
|
+
require_paths:
|
175
|
+
- lib
|
176
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
177
|
+
requirements:
|
178
|
+
- - ">="
|
179
|
+
- !ruby/object:Gem::Version
|
180
|
+
version: '0'
|
181
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
182
|
+
requirements:
|
183
|
+
- - ">="
|
184
|
+
- !ruby/object:Gem::Version
|
185
|
+
version: '0'
|
186
|
+
requirements: []
|
187
|
+
rubyforge_project:
|
188
|
+
rubygems_version: 2.4.8
|
189
|
+
signing_key:
|
190
|
+
specification_version: 4
|
191
|
+
summary: Make your Ruby objects searchable with Elasticsearch.
|
192
|
+
test_files:
|
193
|
+
- spec/searchable_spec.rb
|
194
|
+
- spec/spec_helper.rb
|