chewie 0.2.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/.rspec +3 -0
- data/.rubocop.yml +650 -0
- data/.ruby-version +1 -0
- data/.travis.yml +7 -0
- data/.vscode/settings.json +1 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +59 -0
- data/LICENSE.txt +21 -0
- data/README.md +394 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/chewie.gemspec +38 -0
- data/lib/chewie.rb +47 -0
- data/lib/chewie/handler/reduced.rb +60 -0
- data/lib/chewie/interface/bool.rb +65 -0
- data/lib/chewie/interface/full_text.rb +47 -0
- data/lib/chewie/interface/term_level.rb +80 -0
- data/lib/chewie/query/bool.rb +51 -0
- data/lib/chewie/query/full_text.rb +60 -0
- data/lib/chewie/query/term_level.rb +49 -0
- data/lib/chewie/utils.rb +56 -0
- data/lib/chewie/version.rb +3 -0
- metadata +167 -0
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "chewie"
|
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(__FILE__)
|
data/bin/setup
ADDED
data/chewie.gemspec
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
lib = File.expand_path("lib", __dir__)
|
2
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
3
|
+
require "chewie/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = "chewie"
|
7
|
+
spec.version = Chewie::VERSION
|
8
|
+
spec.authors = ["mrjonesbot"]
|
9
|
+
spec.email = ["nate@mrjones.io"]
|
10
|
+
|
11
|
+
spec.summary = "A declarative interface for building Elasticsearch queries"
|
12
|
+
spec.homepage = "https://github.com/mrjonesbot/chewie"
|
13
|
+
spec.license = "MIT"
|
14
|
+
|
15
|
+
spec.metadata["yard.run"] = "yri"
|
16
|
+
|
17
|
+
# spec.metadata["homepage_uri"] = spec.homepage
|
18
|
+
# spec.metadata["source_code_uri"] = "TODO: Put your gem's public repo URL here."
|
19
|
+
# spec.metadata["changelog_uri"] = "TODO: Put your gem's CHANGELOG.md URL here."
|
20
|
+
|
21
|
+
# Specify which files should be added to the gem when it is released.
|
22
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
23
|
+
spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
|
24
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
25
|
+
end
|
26
|
+
spec.bindir = "exe"
|
27
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
28
|
+
spec.require_paths = ["lib"]
|
29
|
+
|
30
|
+
spec.add_development_dependency "bundler", "~> 2.0"
|
31
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
32
|
+
spec.add_development_dependency "rspec", "~> 3.0"
|
33
|
+
spec.add_development_dependency "pry"
|
34
|
+
spec.add_development_dependency "yard"
|
35
|
+
|
36
|
+
spec.add_runtime_dependency(%q<activesupport>, [">= 5.1.6"])
|
37
|
+
spec.add_runtime_dependency(%q<pry>)
|
38
|
+
end
|
data/lib/chewie.rb
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
require './lib/chewie/version'
|
2
|
+
require 'chewie/utils'
|
3
|
+
require 'chewie/interface/bool'
|
4
|
+
require 'chewie/interface/term_level'
|
5
|
+
require 'chewie/interface/full_text'
|
6
|
+
require 'chewie/handler/reduced'
|
7
|
+
require 'active_support/all'
|
8
|
+
require 'pry'
|
9
|
+
|
10
|
+
module Chewie
|
11
|
+
include Chewie::Interface::Bool
|
12
|
+
include Chewie::Interface::TermLevel
|
13
|
+
include Chewie::Interface::FullText
|
14
|
+
|
15
|
+
def handlers
|
16
|
+
@handlers ||= {
|
17
|
+
query: [],
|
18
|
+
bool: []
|
19
|
+
}
|
20
|
+
end
|
21
|
+
|
22
|
+
def build(query: '', filters: {}, options: {})
|
23
|
+
query_data = set_query_data(query, filters)
|
24
|
+
bool_options = options[:bool] || {}
|
25
|
+
|
26
|
+
match_all_query(options) if query_data[:filters].empty?
|
27
|
+
|
28
|
+
context(:query, options) do
|
29
|
+
query_context = reduce_handlers(data: query_data)
|
30
|
+
bool_context = context(:bool, bool_options) do
|
31
|
+
reduce_handlers(data: query_data, context: :bool)
|
32
|
+
end
|
33
|
+
|
34
|
+
query_context.merge(bool_context)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
include Utils
|
41
|
+
|
42
|
+
def match_all_query(options)
|
43
|
+
context(:query, options) do
|
44
|
+
{ match_all: {} }
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
require 'chewie/query/term_level'
|
2
|
+
require 'chewie/query/bool'
|
3
|
+
require 'chewie/query/full_text'
|
4
|
+
|
5
|
+
module Chewie
|
6
|
+
module Handler
|
7
|
+
class Reduced
|
8
|
+
attr_reader :context, :query, :clause, :built_query, :clause_or_query
|
9
|
+
|
10
|
+
def initialize(context:, handler: {}, filters: {})
|
11
|
+
@context = context
|
12
|
+
@query = handler[:query]
|
13
|
+
@clause = handler[:clause]
|
14
|
+
@built_query = build_query(handler, filters)
|
15
|
+
@clause_or_query = (@clause || @query)
|
16
|
+
end
|
17
|
+
|
18
|
+
def reduce_with(handlers, hash)
|
19
|
+
has_one = handlers[query].one?
|
20
|
+
|
21
|
+
if has_one && is_top_level_query
|
22
|
+
set_in_hash(hash)
|
23
|
+
else
|
24
|
+
push_to_array(hash)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def set_in_hash(hash)
|
31
|
+
hash[clause_or_query] =
|
32
|
+
(hash[clause_or_query] || {}).merge(built_query)
|
33
|
+
end
|
34
|
+
|
35
|
+
def push_to_array(hash)
|
36
|
+
hash[clause_or_query] =
|
37
|
+
(hash[clause_or_query] || []).push(built_query)
|
38
|
+
end
|
39
|
+
|
40
|
+
def is_top_level_query
|
41
|
+
!(context == :bool && clause.present?)
|
42
|
+
end
|
43
|
+
|
44
|
+
def build_query(handler, filters)
|
45
|
+
query_type = handler[:query_type]
|
46
|
+
|
47
|
+
case query_type
|
48
|
+
when :term_level
|
49
|
+
::Chewie::Query::TermLevel.new(handler, filters).build
|
50
|
+
when :bool
|
51
|
+
::Chewie::Query::Bool.new(handler, filters).build
|
52
|
+
when :full_text
|
53
|
+
::Chewie::Query::FullText.new(handler, filters).build
|
54
|
+
else
|
55
|
+
raise "Could not build a query for type: #{query_type}"
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
module Chewie
|
2
|
+
module Interface
|
3
|
+
module Bool
|
4
|
+
# https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-bool-query.html
|
5
|
+
#
|
6
|
+
# @param attribute [Symbol] Field you wish to search
|
7
|
+
# @param with [Symbol] Specify the term-level query [term, terms, range]
|
8
|
+
# @param combine [Array] Target additional filter values to be combined in the formatted output (optional)
|
9
|
+
# @param format [Lambda] Define custom output with :combine values at runtime (optional)
|
10
|
+
# @return [Hash] A valid bool query
|
11
|
+
# @note See README#filtering-by-associations use case for :combine and :filter options
|
12
|
+
def filter_by(attribute, with:, combine: [], format: nil)
|
13
|
+
handler = {
|
14
|
+
query: :filter,
|
15
|
+
attribute: attribute,
|
16
|
+
with: with,
|
17
|
+
combine: combine,
|
18
|
+
format: format,
|
19
|
+
query_type: :bool,
|
20
|
+
}
|
21
|
+
set_handler(context: :bool, handler: handler)
|
22
|
+
end
|
23
|
+
|
24
|
+
# (see #filter_by)
|
25
|
+
def should_include(attribute, with:, combine: [], format: nil)
|
26
|
+
handler = {
|
27
|
+
query: :should,
|
28
|
+
attribute: attribute,
|
29
|
+
with: with,
|
30
|
+
combine: combine,
|
31
|
+
format: format,
|
32
|
+
query_type: :bool,
|
33
|
+
}
|
34
|
+
set_handler(context: :bool, handler: handler)
|
35
|
+
end
|
36
|
+
|
37
|
+
# (see #filter_by)
|
38
|
+
def must_not_include(attribute, with:, combine: [], format: nil)
|
39
|
+
handler = {
|
40
|
+
query: :must_not,
|
41
|
+
attribute: attribute,
|
42
|
+
with: with,
|
43
|
+
combine: combine,
|
44
|
+
format: format,
|
45
|
+
query_type: :bool,
|
46
|
+
}
|
47
|
+
set_handler(context: :bool, handler: handler)
|
48
|
+
end
|
49
|
+
|
50
|
+
# (see #filter_by)
|
51
|
+
def must_include(attribute, with:, combine: [], format: nil)
|
52
|
+
handler = {
|
53
|
+
query: :must,
|
54
|
+
attribute: attribute,
|
55
|
+
with: with,
|
56
|
+
combine: combine,
|
57
|
+
format: format,
|
58
|
+
query_type: :bool,
|
59
|
+
}
|
60
|
+
|
61
|
+
set_handler(context: :bool, handler: handler)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module Chewie
|
2
|
+
module Interface
|
3
|
+
module FullText
|
4
|
+
# https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-match-query.html
|
5
|
+
#
|
6
|
+
# @param attribute [Symbol] Field you wish to search
|
7
|
+
# @param context [Symbol] Desired context the query should appear (see https://www.elastic.co/guide/en/elasticsearch/reference/current/compound-queries.html)
|
8
|
+
# @param clause [Symbol] Specify a nested clause, usually context dependent (optional)
|
9
|
+
# @param options [Hash] Options to augment search behavior: https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-match-query.html#match-field-params
|
10
|
+
# @return [Hash] A valid "must" query
|
11
|
+
def match(attribute, context: :query, clause: nil, options: {})
|
12
|
+
handler = {
|
13
|
+
query: :match,
|
14
|
+
clause: clause,
|
15
|
+
attribute: attribute,
|
16
|
+
query_type: :full_text,
|
17
|
+
options: options,
|
18
|
+
}
|
19
|
+
|
20
|
+
set_handler(context: context, handler: handler)
|
21
|
+
end
|
22
|
+
|
23
|
+
# https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-multi-match-query.html
|
24
|
+
#
|
25
|
+
# @param with [Array] A collection of field symbols to match against
|
26
|
+
# @param context [Symbol] Desired context the query should appear (see https://www.elastic.co/guide/en/elasticsearch/reference/current/compound-queries.html)
|
27
|
+
# @param clause [Symbol] Specify a nested clause, usually context dependent (optional)
|
28
|
+
# @param options [Hash] Options to augment search behavior
|
29
|
+
# @return [Hash] A valid "multi-match" query
|
30
|
+
def multimatch(with: [], context: :query, clause: nil, options: {})
|
31
|
+
if context == :compound
|
32
|
+
raise 'Please include a :clause value for compound queries.'
|
33
|
+
end
|
34
|
+
|
35
|
+
handler = {
|
36
|
+
query: :multimatch,
|
37
|
+
clause: clause,
|
38
|
+
with: with,
|
39
|
+
query_type: :full_text,
|
40
|
+
options: options,
|
41
|
+
}
|
42
|
+
|
43
|
+
set_handler(context: context, handler: handler)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
module Chewie
|
2
|
+
module Interface
|
3
|
+
module TermLevel
|
4
|
+
# https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-fuzzy-query.html
|
5
|
+
#
|
6
|
+
# @param attribute [Symbol] Field you wish to search
|
7
|
+
# @param context [Symbol] Desired context the query should appear (see https://www.elastic.co/guide/en/elasticsearch/reference/current/compound-queries.html)
|
8
|
+
# @param clause [Symbol] Specify a nested clause, usually context dependent (optional)
|
9
|
+
# @param options [Lambda] Options to augment search behavior: https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-fuzzy-query.html#fuzzy-query-field-params
|
10
|
+
# @return [Hash] A valid "fuzzy" query
|
11
|
+
def fuzzy(attribute, context: :query, clause: nil, options: {})
|
12
|
+
handler = {
|
13
|
+
query: :fuzzy,
|
14
|
+
clause: clause,
|
15
|
+
attribute: attribute,
|
16
|
+
query_type: :term_level,
|
17
|
+
options: options,
|
18
|
+
}
|
19
|
+
set_handler(context: context, handler: handler)
|
20
|
+
end
|
21
|
+
|
22
|
+
# https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-range-query.html
|
23
|
+
#
|
24
|
+
# @param attribute [Symbol] Field you wish to search
|
25
|
+
# @param context [Symbol] Desired context the query should appear (see https://www.elastic.co/guide/en/elasticsearch/reference/current/compound-queries.html)
|
26
|
+
# @param clause [Symbol] Specify a nested clause, usually context dependent (optional)
|
27
|
+
# @param options [Lambda] Options to augment search behavior: https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-range-query.html#range-query-field-params
|
28
|
+
# @return [Hash] A valid "range" query
|
29
|
+
def range(attribute, context: :query, clause: nil, options: {})
|
30
|
+
handler = {
|
31
|
+
query: :range,
|
32
|
+
clause: clause,
|
33
|
+
attribute: attribute,
|
34
|
+
query_type: :term_level,
|
35
|
+
options: options
|
36
|
+
}
|
37
|
+
|
38
|
+
set_handler(context: context, handler: handler)
|
39
|
+
end
|
40
|
+
|
41
|
+
# https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-term-query.html
|
42
|
+
#
|
43
|
+
# @param attribute [Symbol] Field you wish to search
|
44
|
+
# @param context [Symbol] Desired context the query should appear (see https://www.elastic.co/guide/en/elasticsearch/reference/current/compound-queries.html)
|
45
|
+
# @param clause [Symbol] Specify a nested clause, usually context dependent (optional)
|
46
|
+
# @param options [Lambda] Options to augment search behavior: https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-term-query.html#term-field-params
|
47
|
+
# @return [Hash] A valid "term" query
|
48
|
+
def term(attribute, context: :query, clause: nil, options: {})
|
49
|
+
handler = {
|
50
|
+
query: :term,
|
51
|
+
clause: clause,
|
52
|
+
attribute: attribute,
|
53
|
+
query_type: :term_level,
|
54
|
+
options: options
|
55
|
+
}
|
56
|
+
|
57
|
+
set_handler(context: context, handler: handler)
|
58
|
+
end
|
59
|
+
|
60
|
+
# https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-terms-query.html
|
61
|
+
#
|
62
|
+
# @param attribute [Symbol] Field you wish to search
|
63
|
+
# @param context [Symbol] Desired context the query should appear (see https://www.elastic.co/guide/en/elasticsearch/reference/current/compound-queries.html)
|
64
|
+
# @param clause [Symbol] Specify a nested clause, usually context dependent (optional)
|
65
|
+
# @param options [Lambda] Options to augment search behavior: https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-terms-query.html#terms-top-level-params
|
66
|
+
# @return [Hash] A valid "terms" query
|
67
|
+
def terms(attribute, context: :query, clause: nil, options: {})
|
68
|
+
handler = {
|
69
|
+
query: :terms,
|
70
|
+
clause: clause,
|
71
|
+
attribute: attribute,
|
72
|
+
query_type: :term_level,
|
73
|
+
options: options
|
74
|
+
}
|
75
|
+
|
76
|
+
set_handler(context: context, handler: handler)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
require 'chewie/utils'
|
2
|
+
|
3
|
+
module Chewie
|
4
|
+
module Query
|
5
|
+
class Bool
|
6
|
+
attr_reader :attribute, :query_format, :with, :value, :combine, :filters
|
7
|
+
|
8
|
+
def initialize(handler, filters)
|
9
|
+
@attribute = handler[:attribute]
|
10
|
+
@query_format = handler[:format]
|
11
|
+
@with = handler[:with]
|
12
|
+
@value = filters[attribute]
|
13
|
+
@combine = handler[:combine]
|
14
|
+
@filters = filters
|
15
|
+
end
|
16
|
+
|
17
|
+
def build
|
18
|
+
return {} if value.nil?
|
19
|
+
return [] if [value].flatten.empty?
|
20
|
+
|
21
|
+
context(with) do
|
22
|
+
{ attribute => exposed_value }
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
include Utils
|
29
|
+
|
30
|
+
def should_format
|
31
|
+
query_format.present?
|
32
|
+
end
|
33
|
+
|
34
|
+
def exposed_value
|
35
|
+
expose_or_return_value(formatted_value, with, should_format)
|
36
|
+
end
|
37
|
+
|
38
|
+
def formatted_value
|
39
|
+
if should_format
|
40
|
+
format_values(value, combined, query_format)
|
41
|
+
else
|
42
|
+
value
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def combined
|
47
|
+
combine_values(combine, filters)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
require 'chewie/utils'
|
2
|
+
|
3
|
+
module Chewie
|
4
|
+
module Query
|
5
|
+
class FullText
|
6
|
+
attr_reader :attribute, :with, :query, :clause, :value, :options, :strategy
|
7
|
+
|
8
|
+
def initialize(handler, filters)
|
9
|
+
@attribute = handler[:attribute]
|
10
|
+
@with = handler[:with]
|
11
|
+
@query = handler[:query]
|
12
|
+
@clause = handler[:clause]
|
13
|
+
@value = filters[attribute] || filters[:query]
|
14
|
+
@options = handler.fetch(:options, {})
|
15
|
+
@strategy = clause.present? ? 'clause' : 'attribute'
|
16
|
+
end
|
17
|
+
|
18
|
+
def build
|
19
|
+
return {} if value.nil?
|
20
|
+
return [] if [value].flatten.empty?
|
21
|
+
|
22
|
+
send("create_with_#{strategy}")
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
include Utils
|
28
|
+
|
29
|
+
def create_with_clause
|
30
|
+
context(query) do
|
31
|
+
multimatch_query? ? multimatch_query : attribute_query
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def create_with_attribute
|
36
|
+
return multimatch_query if multimatch_query?
|
37
|
+
|
38
|
+
context(attribute) do
|
39
|
+
if !value.is_a? Hash
|
40
|
+
{ query: value }.merge(options)
|
41
|
+
else
|
42
|
+
value.merge(options)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def multimatch_query?
|
48
|
+
with.present?
|
49
|
+
end
|
50
|
+
|
51
|
+
def attribute_query
|
52
|
+
{ attribute => { query: value }.merge(options) }
|
53
|
+
end
|
54
|
+
|
55
|
+
def multimatch_query
|
56
|
+
{ fields: with, query: value }.merge(options)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|