chewie 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.
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
@@ -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__)
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -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
@@ -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