elasticquery 0.1.2 → 0.1.3

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.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +12 -0
  3. data/README.md +151 -70
  4. data/Rakefile +1 -9
  5. data/elasticquery.gemspec +2 -2
  6. data/lib/elasticquery.rb +1 -0
  7. data/lib/elasticquery/base.rb +6 -48
  8. data/lib/elasticquery/builder.rb +74 -17
  9. data/lib/elasticquery/es.rb +32 -0
  10. data/lib/elasticquery/filters/base.rb +4 -0
  11. data/lib/elasticquery/filters/exists.rb +24 -0
  12. data/lib/elasticquery/filters/not.rb +7 -11
  13. data/lib/elasticquery/filters/range.rb +3 -7
  14. data/lib/elasticquery/filters/term.rb +4 -28
  15. data/lib/elasticquery/filters/terms.rb +23 -0
  16. data/lib/elasticquery/queries/base.rb +16 -0
  17. data/lib/elasticquery/queries/multi_match.rb +34 -0
  18. data/lib/elasticquery/query.rb +25 -36
  19. data/lib/elasticquery/version.rb +1 -1
  20. data/test/base_test.rb +4 -4
  21. data/test/builder_test.rb +24 -8
  22. data/test/es_test.rb +23 -0
  23. data/test/filters/exists_test.rb +30 -0
  24. data/test/filters/not_test.rb +23 -10
  25. data/test/filters/range_test.rb +10 -11
  26. data/test/filters/term_test.rb +6 -7
  27. data/test/filters/terms_test.rb +50 -0
  28. data/test/integration/chainable_call_test.rb +6 -6
  29. data/test/integration/exists_case_test.rb +56 -0
  30. data/test/integration/not_case_test.rb +11 -5
  31. data/test/integration/queries_inheritence_test.rb +12 -4
  32. data/test/integration/range_case_test.rb +4 -2
  33. data/test/integration/search_case_test.rb +20 -11
  34. data/test/integration/term_case_test.rb +4 -2
  35. data/test/integration/terms_case_test.rb +49 -0
  36. data/test/queries/multi_match_test.rb +49 -0
  37. data/test/query_test.rb +40 -40
  38. metadata +26 -27
  39. data/lib/elasticquery/filters/search.rb +0 -54
  40. data/test/filters/search_test.rb +0 -62
@@ -1,7 +1,10 @@
1
1
  require "elasticquery/filters/term"
2
- require "elasticquery/filters/search"
3
2
  require "elasticquery/filters/not"
3
+ require "elasticquery/filters/terms"
4
4
  require "elasticquery/filters/range"
5
+ require "elasticquery/filters/exists"
6
+
7
+ require "elasticquery/queries/multi_match"
5
8
 
6
9
  require "active_support/concern"
7
10
  require "active_support/core_ext/string/inflections"
@@ -10,23 +13,77 @@ module Elasticquery
10
13
  module Builder
11
14
  extend ActiveSupport::Concern
12
15
 
13
- filters = [
14
- Filters::Term,
15
- Filters::Search,
16
- Filters::Not,
17
- Filters::Range
18
- ]
19
-
20
- included do
21
- filters.each do |filter_class|
22
- filter_name = filter_class.to_s.split("::").last.underscore
23
-
24
- define_method filter_name do |*args|
25
- filter = filter_class.new *args
26
- query << filter
27
- self
28
- end
16
+ def filters
17
+ @namespace = "Filters"
18
+ if block_given?
19
+ yield
20
+ @namespace = nil
21
+ end
22
+ self
23
+ end
24
+
25
+ def queries
26
+ @namespace = "Queries"
27
+ if block_given?
28
+ yield
29
+ @namespace = nil
30
+ end
31
+ self
32
+ end
33
+
34
+ def execute(method, *args)
35
+ filter_class = "Elasticquery::#{@namespace}::#{method.camelize}".constantize
36
+ instance = filter_class.new *args
37
+ case @namespace
38
+ when "Filters"
39
+ query.push_filter instance
40
+ when "Queries"
41
+ query.push_query instance
42
+ else
43
+ raise "Please specify block. Queries and Filters are supported"
29
44
  end
45
+ self
46
+ end
47
+ private :execute
48
+
49
+ def term(*args)
50
+ execute "term", *args
51
+ end
52
+
53
+ def terms(*args)
54
+ execute "terms", *args
55
+ end
56
+ def where(*args)
57
+ terms *args
58
+ end
59
+
60
+ def not(*args)
61
+ execute "not", *args
62
+ end
63
+
64
+ def range(*args)
65
+ execute "range", *args
66
+ end
67
+
68
+ def multi_match(*args)
69
+ execute "multi_match", *args
70
+ end
71
+ def search(*args)
72
+ multi_match *args
73
+ end
74
+
75
+ def exists(*args)
76
+ execute "exists", *args
77
+ end
78
+ def with(*args)
79
+ exists *args
80
+ end
81
+
82
+ def missing(*args)
83
+ exists.not *args
84
+ end
85
+ def without(*args)
86
+ missing *args
30
87
  end
31
88
  end
32
89
  end
@@ -0,0 +1,32 @@
1
+ module Elasticquery
2
+ EsNotSupported = Class.new(StandardError)
3
+
4
+ class BaseChain < Elasticquery::Base
5
+ def initialize(klass)
6
+ @klass = klass
7
+ super
8
+ end
9
+
10
+ delegate :each, :empty?, :size, :slice, :[], :to_ary, to: :results
11
+
12
+ def records
13
+ results.records
14
+ end
15
+
16
+ def results
17
+ @klass.__elasticsearch__.search(build)
18
+ end
19
+ end
20
+
21
+ module Es
22
+ def self.extended(base)
23
+ unless base.ancestors.include? Elasticsearch::Model
24
+ raise EsNotSupported.new("Can't extend. It's not Elasticsearch::Model")
25
+ end
26
+ end
27
+
28
+ def es
29
+ BaseChain.new(self)
30
+ end
31
+ end
32
+ end
@@ -12,6 +12,10 @@ module Elasticquery
12
12
  !valid?
13
13
  end
14
14
 
15
+ def to_not_hash
16
+ {not: {filter: to_hash}}
17
+ end
18
+
15
19
  def dup_with(*args)
16
20
  self.class.new *args
17
21
  end
@@ -0,0 +1,24 @@
1
+ require_relative "base"
2
+
3
+ module Elasticquery
4
+ module Filters
5
+ class Exists < Base
6
+
7
+ def initialize(field = nil)
8
+ @field = field
9
+ end
10
+
11
+ def valid?
12
+ @field.present?
13
+ end
14
+
15
+ def to_hash
16
+ {exists: {field: @field}}
17
+ end
18
+
19
+ def to_not_hash
20
+ {missing: {field: @field}}
21
+ end
22
+ end
23
+ end
24
+ end
@@ -5,25 +5,21 @@ module Elasticquery
5
5
  class Not < Base
6
6
 
7
7
  def initialize(*args)
8
- @args = args
8
+ @args = Marshal.load(Marshal.dump(args))
9
9
  end
10
10
 
11
11
  def dup_with(*args)
12
12
  raise StandardError, "Cannot use Filters::Not twice"
13
13
  end
14
14
 
15
+ def valid?
16
+ args = @args
17
+ -> { filters[-2].dup_with(*args).valid? }
18
+ end
19
+
15
20
  def to_hash
16
21
  args = @args
17
- -> do
18
- filter = filters.last.dup_with *args
19
- if filter.valid?
20
- subquery = filter.to_hash
21
- q = subquery[:query][:filtered][:filter][:and][0]
22
- {query: {filtered: {filter: {and: [{not: {filter: q}}]}}}}
23
- else
24
- {}
25
- end
26
- end
22
+ -> { filters[-2].dup_with(*args).to_not_hash }
27
23
  end
28
24
  end
29
25
  end
@@ -5,8 +5,8 @@ module Elasticquery
5
5
  class Range < Base
6
6
  OPTIONS = %i(_cache execution)
7
7
 
8
- def initialize(field=nil, gte: nil, lte: nil, **options)
9
- @options = options.extract! *OPTIONS
8
+ def initialize(field = nil, gte: nil, lte: nil, **options)
9
+ @options = options.slice *OPTIONS
10
10
  @field, @gte, @lte = field, gte, lte
11
11
  end
12
12
 
@@ -15,11 +15,7 @@ module Elasticquery
15
15
  end
16
16
 
17
17
  def to_hash
18
- if valid?
19
- {query: {filtered: {filter: {and: [{range: range_with_options}]}}}}
20
- else
21
- {}
22
- end
18
+ {range: range_with_options}
23
19
  end
24
20
 
25
21
  private
@@ -5,41 +5,17 @@ module Elasticquery
5
5
  class Term < Base
6
6
  OPTIONS = %i(_cache)
7
7
 
8
- # Create term filter for elasticsearch builder
9
- #
10
- # @param [Hash] condition passed to define
11
- #
12
- # @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/query-dsl-filtered-query.html#_filtering_without_a_query
13
- def initialize(data={})
14
- @options = data.extract! *OPTIONS
15
- @condition = data
8
+ def initialize(data = {})
9
+ @options = data.slice *OPTIONS
10
+ @condition = data.except *OPTIONS
16
11
  end
17
12
 
18
- # Is passed condition valid. Passed condition must have
19
- # one key and present value
20
- #
21
- # @return [Boolean] is condition have at least one key
22
- #
23
- # @example
24
- # rule = Elasticquery::Rules::Term.new
25
- # rule.valid? #=> false
26
13
  def valid?
27
14
  @condition.keys.size == 1 && @condition.values[0].present?
28
15
  end
29
16
 
30
- # Hash presentation of query.
31
- #
32
- # @return [Hash] presentation of filter.
33
- #
34
- # @example
35
- # valid = Elasticquery::Filters::Term.new {name: "John"}
36
- # valid.to_hash #=> {query: {filtered: {filter: {and: [{term: {name: 'John'}}]}}}
37
17
  def to_hash
38
- if valid?
39
- {query: {filtered: {filter: {and: [{term: @condition.merge(@options)}]}}}}
40
- else
41
- {}
42
- end
18
+ {term: @condition.merge(@options)}
43
19
  end
44
20
  end
45
21
  end
@@ -0,0 +1,23 @@
1
+ require "active_support/core_ext/array/wrap"
2
+ require_relative "base"
3
+
4
+ module Elasticquery
5
+ module Filters
6
+ class Terms < Base
7
+ OPTIONS = %i(_cache execution)
8
+
9
+ def initialize(data = {})
10
+ @options = data.slice *OPTIONS
11
+ @conditions = data.except(*OPTIONS).delete_if{ |_, value| value.blank? }
12
+ end
13
+
14
+ def valid?
15
+ @conditions.keys.any?
16
+ end
17
+
18
+ def to_hash
19
+ {terms: @conditions.merge(@options)}
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,16 @@
1
+ require 'active_support/core_ext/object/blank'
2
+
3
+ module Elasticquery
4
+ module Queries
5
+ class Base
6
+
7
+ def valid?
8
+ true
9
+ end
10
+
11
+ def invalid?
12
+ !valid?
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,34 @@
1
+ require_relative "base"
2
+
3
+ module Elasticquery
4
+ module Queries
5
+ class MultiMatch < Base
6
+ OPERATORS = %w(and or)
7
+ TYPES = %w(best_fields most_fields cross_fields phrase pharse_prefix)
8
+
9
+ def initialize(query, fields: "_all", operator: "and", type: "best_fields")
10
+ @fields = fields
11
+ @operator = operator
12
+ @type = type
13
+ @query = query
14
+ end
15
+
16
+ def valid?
17
+ OPERATORS.include?(@operator) &&
18
+ TYPES.include?(@type) &&
19
+ @query.present? &&
20
+ ( Array === @fields || @fields == "_all" )
21
+ end
22
+
23
+ def to_hash
24
+ {multi_match: subquery}
25
+ end
26
+
27
+ private
28
+
29
+ def subquery
30
+ {fields: @fields, operator: @operator, type: @type, query: @query}
31
+ end
32
+ end
33
+ end
34
+ end
@@ -1,64 +1,53 @@
1
- require "deep_merge/rails_compat"
2
-
3
1
  module Elasticquery
4
2
  class Query
5
3
 
6
- attr_reader :filters
4
+ attr_reader :filters, :queries
7
5
 
8
6
  DEFAULT = {query: {filtered: {query: {match_all:{}}}}}
9
7
 
10
- # Create new query from hash.
11
- #
12
- # @param [Hash] query passed as hash. When it not passed use default es 'match_all' query
13
- #
14
- # @example
15
- # Elasticsearch::Query.new.to_hash #=> {query: {filtered: {query: {match_all:{}}}}}
16
8
  def initialize(query = DEFAULT)
17
9
  @query = query
18
- @filters = []
10
+ @filters, @queries = [], []
19
11
  end
20
12
 
21
- # Convery query object ot hash
22
- #
23
- # @return [Hash] hash presentation of query
24
13
  def to_hash
25
- return DEFAULT if @query == {}
26
14
  @query
27
15
  end
28
16
 
29
- # Merge filter to query. If current query is `matche_all`ed
30
- # then clear it and use new value. Populate #filters array
31
- # with given classes
32
- #
33
- # @param [Elasticquery::filter::Base] filter passed
34
- #
35
- # @see #match_all?
36
- def <<(filter)
37
- @query = {} if match_all?
38
- merge filter
17
+ def push_filter(filter)
39
18
  @filters << filter
19
+ merge_filter filter
40
20
  end
41
21
 
42
- # Ckeck current object to default elasticsearch param
43
- #
44
- # @return [Boolean] is passed query undefined
45
- def match_all?
46
- @query == DEFAULT
22
+ def push_query(query)
23
+ @queries << query
24
+ merge_query query
47
25
  end
48
26
 
49
27
  private
50
28
 
51
- def merge(filter)
29
+ def merge_filter(filter)
30
+ return unless filter_valid?(filter)
31
+
52
32
  hash = filter.to_hash
53
33
  subquery = hash.respond_to?(:call) ? instance_exec(&hash) : hash
54
- @query.deeper_merge! subquery
34
+ @query = {query: {filtered: {}}} if @query == DEFAULT
35
+
36
+ @query[:query][:filtered][:filter] ||= {and: []}
37
+ @query[:query][:filtered][:filter][:and] << subquery
55
38
  end
56
39
 
57
- def _filters
58
- @query[:query] &&
59
- @query[:query][:filtered] &&
60
- @query[:query][:filtered][:filter] &&
61
- @query[:query][:filtered][:filter][:and]
40
+ def filter_valid?(filter)
41
+ validation = filter.valid?
42
+ validation.respond_to?(:call) ? instance_exec(&validation) : validation
43
+ end
44
+
45
+ def merge_query(query)
46
+ return unless query.valid?
47
+ @query = {query: {filtered: {}}} if @query == DEFAULT
48
+
49
+ @query[:query][:filtered][:query] ||= {bool: {must: []}}
50
+ @query[:query][:filtered][:query][:bool][:must] << query.to_hash
62
51
  end
63
52
  end
64
53
  end