From f9660cd890e9e2801042a4443eb4a7fbb93fd02a Mon Sep 17 00:00:00 2001 From: Spencer Markowski Date: Sat, 9 Mar 2024 11:53:30 -0500 Subject: [PATCH 1/3] Rename filter to filter_query #45 --- lib/stretchy/querying.rb | 8 +++-- lib/stretchy/relation.rb | 4 +-- lib/stretchy/relations/merger.rb | 6 ++-- lib/stretchy/relations/query_methods.rb | 40 +++++-------------------- 4 files changed, 17 insertions(+), 41 deletions(-) diff --git a/lib/stretchy/querying.rb b/lib/stretchy/querying.rb index 84c74cc..9789768 100644 --- a/lib/stretchy/querying.rb +++ b/lib/stretchy/querying.rb @@ -1,11 +1,13 @@ module Stretchy module Querying delegate :first, :first!, :last, :last!, :exists?, :has_field, :any?, :many?, to: :all - delegate :order, :limit, :size, :sort, :where, :rewhere, :eager_load, :includes, :create_with, :none, :unscope, to: :all - delegate :or_filter, :filter, :fields, :source, :highlight, :aggregation, to: :all + delegate :order, :limit, :size, :sort, :rewhere, :eager_load, :includes, :create_with, :none, :unscope, to: :all + delegate :or_filter, :fields, :source, :highlight, to: :all + delegate *Stretchy::Relations::AggregationMethods::AGGREGATION_METHODS, to: :all + delegate :skip_callbacks, :routing, to: :all delegate :search_options, :routing, to: :all - delegate :must, :must_not, :should, :where_not, :query_string, to: :all + delegate :must, :must_not, :should, :where_not, :where, :filter_query, :query_string, to: :all def fetch_results(es) unless es.count? diff --git a/lib/stretchy/relation.rb b/lib/stretchy/relation.rb index 9dd9011..c90adab 100644 --- a/lib/stretchy/relation.rb +++ b/lib/stretchy/relation.rb @@ -4,7 +4,7 @@ module Stretchy class Relation # These methods can accept multiple values. - MULTI_VALUE_METHODS = [:order, :where, :or_filter, :filter, :bind, :extending, :unscope, :skip_callbacks] + MULTI_VALUE_METHODS = [:order, :where, :or_filter, :filter_query, :bind, :extending, :unscope, :skip_callbacks] # These methods can accept a single value. SINGLE_VALUE_METHODS = [:limit, :offset, :routing, :size] @@ -16,7 +16,7 @@ class Relation VALUE_METHODS = MULTI_VALUE_METHODS + SINGLE_VALUE_METHODS # Include modules. - include Relations::FinderMethods, Relations::SpawnMethods, Relations::QueryMethods, Relations::SearchOptionMethods, Delegation + include Relations::FinderMethods, Relations::SpawnMethods, Relations::QueryMethods, Relations::AggregationMethods, Relations::SearchOptionMethods, Delegation # Getters. attr_reader :klass, :loaded diff --git a/lib/stretchy/relations/merger.rb b/lib/stretchy/relations/merger.rb index c534f65..2c53ce8 100644 --- a/lib/stretchy/relations/merger.rb +++ b/lib/stretchy/relations/merger.rb @@ -50,7 +50,7 @@ def initialize(relation, other) @other = other end - NORMAL_VALUES = [:where, :first, :last, :filter] + NORMAL_VALUES = [:where, :first, :last, :filter_query] def normal_values NORMAL_VALUES @@ -67,7 +67,7 @@ def merge unless value.nil? || (value.blank? && false != value) if name == :select relation._select!(*value) - elsif name == :filter + elsif name == :filter_query values.each do |v| relation.send("#{name}!", v.first, v.last) end @@ -115,7 +115,7 @@ def merge_multi_values rhs_wheres = values[:where] || [] lhs_filters = relation.filter_values - rhs_filters = values[:filter] || [] + rhs_filters = values[:filter_query] || [] removed, kept = partition_overwrites(lhs_wheres, rhs_wheres) diff --git a/lib/stretchy/relations/query_methods.rb b/lib/stretchy/relations/query_methods.rb index 64db82d..3217268 100644 --- a/lib/stretchy/relations/query_methods.rb +++ b/lib/stretchy/relations/query_methods.rb @@ -15,7 +15,7 @@ module QueryMethods :query_string, :aggregation, :search_option, - :filter, + :query_filter, :or_filter, :extending, :skip_callbacks @@ -322,53 +322,27 @@ def or_filter!(name, options = {}, &block) # :nodoc: # # This method supports all filters supported by Elasticsearch. # - # @overload filter(type, opts) + # @overload filter_query(type, opts) # @param type [Symbol] the type of filter to add (:range, :term, etc.) # @param opts [Hash] a hash containing the attribute and value to filter by # # @example - # Model.filter(:range, age: {gte: 30}) - # Model.filter(:term, color: :blue) + # Model.filter_query(:range, age: {gte: 30}) + # Model.filter_query(:term, color: :blue) # # @return [Stretchy::Relation] a new relation, which reflects the filter - def filter(name, options = {}, &block) - spawn.filter!(name, options, &block) + def filter_query(name, options = {}, &block) + spawn.filter_query!(name, options, &block) end - def filter!(name, options = {}, &block) # :nodoc: + def filter_query!(name, options = {}, &block) # :nodoc: self.filter_values += [{name: name, args: options}] self end - # Adds an aggregation to the query. - # - # @param name [Symbol, String] the name of the aggregation - # @param options [Hash] a hash of options for the aggregation - # @param block [Proc] an optional block to further configure the aggregation - # - # @example - # Model.aggregation(:avg_price, field: :price) - # Model.aggregation(:price_ranges) do - # range field: :price, ranges: [{to: 100}, {from: 100, to: 200}, {from: 200}] - # end - # - # Aggregation results are available in the `aggregations` method of the results under name provided in the aggregation. - # - # @example - # results = Model.where(color: :blue).aggregation(:avg_price, field: :price) - # results.aggregations.avg_price - # - # @return [Stretchy::Relation] a new relation - def aggregation(name, options = {}, &block) - spawn.aggregation!(name, options, &block) - end - def aggregation!(name, options = {}, &block) # :nodoc: - self.aggregation_values += [{name: name, args: assume_keyword_field(options)}] - self - end From 866b346922a2cce46bb55ee78e1d3b76e4ecfcc3 Mon Sep 17 00:00:00 2001 From: Spencer Markowski Date: Sat, 9 Mar 2024 12:21:02 -0500 Subject: [PATCH 2/3] Add aggregation type scopes for ease #46 --- lib/stretchy/relations/aggregation_methods.rb | 758 ++++++++++++++++++ .../relations/aggregation_methods_spec.rb | 68 ++ 2 files changed, 826 insertions(+) create mode 100644 lib/stretchy/relations/aggregation_methods.rb create mode 100644 spec/stretchy/relations/aggregation_methods_spec.rb diff --git a/lib/stretchy/relations/aggregation_methods.rb b/lib/stretchy/relations/aggregation_methods.rb new file mode 100644 index 0000000..5aeefde --- /dev/null +++ b/lib/stretchy/relations/aggregation_methods.rb @@ -0,0 +1,758 @@ +module Stretchy + module Relations + module AggregationMethods + + AGGREGATION_METHODS = [ + :aggregation, + :avg, + :bucket_script, + :bucket_selector, + :bucket_sort, + :cardinality, + :children, + :composite, + :date_histogram, + :date_range, + :extended_stats, + :filter, # filter is a query method + :filters, + :geo_bounds, + :geo_centroid, + :global, + :histogram, + :ip_range, + :max, + :min, + :missing, + :nested, + :percentile_ranks, + :percentiles, + :range, + :reverse_nested, + :sampler, + :scripted_metric, + :significant_terms, + :stats, + :sum, + :terms, + :top_hits, + :top_metrics, + :value_count, + :weighted_avg + ].freeze + + # Adds an aggregation to the query. + # + # @param name [Symbol, String] the name of the aggregation + # @param options [Hash] a hash of options for the aggregation + # @param block [Proc] an optional block to further configure the aggregation + # + # @example + # Model.aggregation(:avg_price, field: :price) + # Model.aggregation(:price_ranges) do + # range field: :price, ranges: [{to: 100}, {from: 100, to: 200}, {from: 200}] + # end + # + # Aggregation results are available in the `aggregations` method of the results under name provided in the aggregation. + # + # @example + # results = Model.where(color: :blue).aggregation(:avg_price, field: :price) + # results.aggregations.avg_price + # + # @return [Stretchy::Relation] a new relation + def aggregation(name, options = {}, &block) + spawn.aggregation!(name, options, &block) + end + + def aggregation!(name, options = {}, &block) # :nodoc: + self.aggregation_values += [{name: name, args: assume_keyword_field(options)}] + self + end + + + + # Public: Perform an avg aggregation. + # + # name - The Symbol or String name of the aggregation. + # options - The Hash options used to refine the aggregation (default: {}): + # :field - The field to calculate the average on. + # aggs - The Hash of nested aggregations. + # + # Examples + # + # Model.aggregation(:average_price, field: :price) + # + # Returns a new Stretchy::Relation. + def avg(name, options = {}, *aggs) + options = {avg: options}.merge(*aggs) + aggregation(name, options) + end + + # Public: Perform a bucket_script aggregation. + # + # name - The Symbol or String name of the aggregation. + # options - The Hash options used to refine the aggregation (default: {}): + # :buckets_path - The paths to the buckets. + # :script - The script to execute. + # aggs - The Hash of nested aggregations. + # + # Examples + # + # Model.aggregation(:total_sales, script: "params.tShirtsSold * params.price", buckets_path: {tShirtsSold: "tShirtsSold", price: "price"}) + # + # Returns a new Stretchy::Relation. + def bucket_script(name, options = {}, *aggs) + options = {bucket_script: options}.merge(*aggs) + aggregation(name, options) + end + + # Public: Perform a bucket_selector aggregation. + # + # name - The Symbol or String name of the aggregation. + # options - The Hash options used to refine the aggregation (default: {}): + # :script - The script to determine whether the current bucket will be retained. + # aggs - The Hash of nested aggregations. + # + # Examples + # + # Model.aggregation(:sales_bucket_filter, script: "params.totalSales > 200", buckets_path: {totalSales: "totalSales"}) + # + # Returns a new Stretchy::Relation. + def bucket_selector(name, options = {}, *aggs) + options = {bucket_selector: options}.merge(*aggs) + aggregation(name, options) + end + + # Public: Perform a bucket_sort aggregation. + # + # name - The Symbol or String name of the aggregation. + # options - The Hash options used to refine the aggregation (default: {}): + # :field - The field to sort on. + # aggs - The Hash of nested aggregations. + # + # Examples + # + # Model.bucket_sort(:my_agg, {field: 'my_field'}) + # Model.bucket_sort(:my_agg, {field: 'my_field'}, aggs: {...}) + # + # Returns a new Stretchy::Relation. + def bucket_sort(name, options = {}, *aggs) + options = {bucket_sort: options}.merge(*aggs) + aggregation(name, options) + end + + # Public: Perform a cardinality aggregation. + # + # name - The Symbol or String name of the aggregation. + # options - The Hash options used to refine the aggregation (default: {}): + # :field - The field to perform the aggregation on. + # aggs - The Hash of nested aggregations. + # + # Examples + # + # Model.cardinality(:unique_names, {field: 'names'}) + # Model.cardinality(:unique_names, {field: 'names'}, aggs: {...}) + # + # Returns a new Stretchy::Relation. + def cardinality(name, options = {}, *aggs) + options = {cardinality: options}.merge(*aggs) + aggregation(name, options) + end + + # Public: Perform a children aggregation. + # + # name - The Symbol or String name of the aggregation. + # options - The Hash options used to refine the aggregation (default: {}): + # :type - The type of children to aggregate. + # aggs - The Hash of nested aggregations. + # + # Examples + # + # Model.children(:my_agg, {type: 'my_type'}) + # Model.children(:my_agg, {type: 'my_type'}, aggs: {...}) + # + # Returns a new Stretchy::Relation. + def children(name, options = {}, *aggs) + options = {children: options}.merge(*aggs) + aggregation(name, options) + end + + # Public: Perform a composite aggregation. + # + # name - The Symbol or String name of the aggregation. + # options - The Hash options used to refine the aggregation (default: {}): + # :sources - The sources to use for the composite aggregation. + # :size - The size of the composite aggregation. + # aggs - The Hash of nested aggregations. + # + # Examples + # + # Model.composite(:my_agg, {sources: [...], size: 100}) + # Model.composite(:my_agg, {sources: [...], size: 100}, aggs: {...}) + # + # Returns a new Stretchy::Relation. + def composite(name, options = {}, *aggs) + options = {composite: options}.merge(*aggs) + aggregation(name, options) + end + + # Public: Perform a date_histogram aggregation. + # + # name - The Symbol or String name of the aggregation. + # options - The Hash options used to refine the aggregation (default: {}): + # :field - The field to use for the date_histogram aggregation. + # :interval - The interval for the date_histogram aggregation. + # :calendar_interval - The calendar interval for the date_histogram aggregation. + # :format - The format for the date_histogram aggregation. + # :time_zone - The time zone for the date_histogram aggregation. + # :min_doc_count - The minimum document count for the date_histogram aggregation. + # :extended_bounds - The extended bounds for the date_histogram aggregation. + # aggs - The Hash of nested aggregations. + # + # Examples + # + # Model.date_histogram(:my_agg, {field: 'date', interval: 'month', format: 'MM-yyyy', time_zone: 'UTC'}) + # Model.date_histogram(:my_agg, {field: 'date', calendar_interval: :month, format: 'MM-yyyy', time_zone: 'UTC'}, aggs: {...}) + # + # Returns a new Stretchy::Relation. + def date_histogram(name, options = {}, *aggs) + options = {date_histogram: options}.merge(*aggs) + aggregation(name, options) + end + + # Public: Perform a date_range aggregation. + # + # name - The Symbol or String name of the aggregation. + # options - The Hash options used to refine the aggregation (default: {}): + # :field - The field to use for the date_range aggregation. + # :format - The format for the date_range aggregation. + # :time_zone - The time zone for the date_range aggregation. + # :ranges - The ranges for the date_range aggregation. + # :keyed - The keyed option for the date_range aggregation. + # aggs - The Hash of nested aggregations. + # + # Examples + # + # Model.date_range(:my_agg, {field: 'date', format: 'MM-yyyy', time_zone: 'UTC', ranges: [{to: 'now', from: 'now-1M'}]}) + # Model.date_range(:my_agg, {field: 'date', format: 'MM-yyyy', time_zone: 'UTC', ranges: [{to: 'now', from: 'now-1M'}]}, aggs: {...}) + # + # Returns a new Stretchy::Relation. + def date_range(name, options = {}, *aggs) + options = {date_range: options}.merge(*aggs) + aggregation(name, options) + end + # Public: Perform an extended_stats aggregation. + # + # name - The Symbol or String name of the aggregation. + # options - The Hash options used to refine the aggregation (default: {}): + # :field - The field to use for the extended_stats aggregation. + # :sigma - The sigma for the extended_stats aggregation. + # aggs - The Array of additional nested aggregations (optional). + # + # Examples + # + # Model.extended_stats(:my_agg, {field: 'field_name', sigma: 1.0}) + # Model.extended_stats(:my_agg, {field: 'field_name', sigma: 1.0}, aggs: {...}) + # + # Returns a new Stretchy::Relation. + def extended_stats(name, options = {}, *aggs) + options = {extended_stats: options}.merge(*aggs) + aggregation(name, options) + end + + # Public: Perform a filter_agg aggregation. + # + # name - The Symbol or String name of the aggregation. + # options - The Hash options used to refine the aggregation (default: {}): + # :filter - The filter to use for the filter_agg aggregation. + # aggs - The Array of additional nested aggregations (optional). + # + # Examples + # + # Model.filter_agg(:my_agg, {filter: {...}}) + # Model.filter_agg(:my_agg, {filter: {...}}, aggs: {...}) + # + # Returns a new Stretchy::Relation. + def filter(name, options = {}, *aggs) + options = {filter: options}.merge(*aggs) + aggregation(name, options) + end + + # Public: Perform a filters aggregation. + # + # name - The Symbol or String name of the aggregation. + # options - The Hash options used to refine the aggregation (default: {}): + # :filters - The filters to use for the filters aggregation. + # aggs - The Array of additional nested aggregations (optional). + # + # Examples + # + # Model.filters(:my_agg, {filters: {...}}) + # Model.filters(:my_agg, {filters: {...}}, aggs: {...}) + # + # Returns a new Stretchy::Relation. + def filters(name, options = {}, *aggs) + options = {filters: options}.merge(*aggs) + aggregation(name, options) + end + + # Public: Perform a geo_bounds aggregation. + # + # name - The Symbol or String name of the aggregation. + # options - The Hash options used to refine the aggregation (default: {}): + # :field - The field to use for the geo_bounds aggregation. + # aggs - The Array of additional nested aggregations (optional). + # + # Examples + # + # Model.geo_bounds(:my_agg, {field: 'location_field'}) + # Model.geo_bounds(:my_agg, {field: 'location_field'}, aggs: {...}) + # + # Returns a new Stretchy::Relation. + def geo_bounds(name, options = {}, *aggs) + options = {geo_bounds: options}.merge(*aggs) + aggregation(name, options) + end + + # Public: Perform a geo_centroid aggregation. + # + # name - The Symbol or String name of the aggregation. + # options - The Hash options used to refine the aggregation (default: {}): + # :field - The field to use for the geo_centroid aggregation. + # aggs - The Array of additional nested aggregations (optional). + # + # Examples + # + # Model.geo_centroid(:my_agg, {field: 'location_field'}) + # Model.geo_centroid(:my_agg, {field: 'location_field'}, aggs: {...}) + # + # Returns a new Stretchy::Relation. + def geo_centroid(name, options = {}, *aggs) + options = {geo_centroid: options}.merge(*aggs) + aggregation(name, options) + end + + # Public: Perform a global aggregation. + # + # name - The Symbol or String name of the aggregation. + # options - The Hash options used to refine the aggregation (default: {}). + # aggs - The Array of additional nested aggregations (optional). + # + # Examples + # + # Model.global(:my_agg) + # Model.global(:my_agg, {}, aggs: {...}) + # + # Returns a new Stretchy::Relation. + def global(name, options = {}, *aggs) + options = {global: options}.merge(*aggs) + aggregation(name, options) + end + # Public: Perform a histogram aggregation. + # + # name - The Symbol or String name of the aggregation. + # options - The Hash options used to refine the aggregation (default: {}): + # :field - The field to use for the histogram aggregation. + # :interval - The interval for the histogram aggregation. + # :min_doc_count - The minimum document count for the histogram aggregation. + # :extended_bounds - The extended bounds for the histogram aggregation. + # aggs - The Array of additional nested aggregations (optional). + # + # Examples + # + # Model.histogram(:my_agg, {field: 'field_name', interval: 5}) + # Model.histogram(:my_agg, {field: 'field_name', interval: 5}, aggs: {...}) + # + # Returns a new Stretchy::Relation. + def histogram(name, options = {}, *aggs) + options = {histogram: options}.merge(*aggs) + aggregation(name, options) + end + + # Public: Perform an ip_range aggregation. + # + # name - The Symbol or String name of the aggregation. + # options - The Hash options used to refine the aggregation (default: {}): + # :field - The field to use for the ip_range aggregation. + # :ranges - The ranges to use for the ip_range aggregation. ranges: [{to: '10.0.0.5'}, {from: '10.0.0.5'}] + # aggs - The Array of additional nested aggregations (optional). + # + # Examples + # + # Model.ip_range(:my_agg, {field: 'ip_field'}) + # Model.ip_range(:my_agg, {field: 'ip_field'}, aggs: {...}) + # + # Returns a new Stretchy::Relation. + def ip_range(name, options = {}, *aggs) + options = {ip_range: options}.merge(*aggs) + aggregation(name, options) + end + + # Public: Perform a max aggregation. + # + # name - The Symbol or String name of the aggregation. + # options - The Hash options used to refine the aggregation (default: {}): + # :field - The field to use for the max aggregation. + # aggs - The Array of additional nested aggregations (optional). + # + # Examples + # + # Model.max(:my_agg, {field: 'field_name'}) + # Model.max(:my_agg, {field: 'field_name'}, aggs: {...}) + # + # Returns a new Stretchy::Relation. + def max(name, options = {}, *aggs) + options = {max: options}.merge(*aggs) + aggregation(name, options) + end + + # Public: Perform a min aggregation. + # + # name - The Symbol or String name of the aggregation. + # options - The Hash options used to refine the aggregation (default: {}): + # :field - The field to use for the min aggregation. + # aggs - The Array of additional nested aggregations (optional). + # + # Examples + # + # Model.min(:my_agg, {field: 'field_name'}) + # Model.min(:my_agg, {field: 'field_name'}, aggs: {...}) + # + # Returns a new Stretchy::Relation. + def min(name, options = {}, *aggs) + options = {min: options}.merge(*aggs) + aggregation(name, options) + end + + # Public: Perform a missing aggregation. + # + # name - The Symbol or String name of the aggregation. + # options - The Hash options used to refine the aggregation (default: {}): + # :field - The field to use for the missing aggregation. + # aggs - The Array of additional nested aggregations (optional). + # + # Examples + # + # Model.missing(:my_agg, {field: 'field_name'}) + # Model.missing(:my_agg, {field: 'field_name'}, aggs: {...}) + # + # Returns a new Stretchy::Relation. + def missing(name, options = {}, *aggs) + options = {missing: options}.merge(*aggs) + aggregation(name, options) + end + + # Public: Perform a nested aggregation. + # + # name - The Symbol or String name of the aggregation. + # options - The Hash options used to refine the aggregation (default: {}): + # :path - The path to use for the nested aggregation. + # aggs - The Array of additional nested aggregations (optional). + # + # Examples + # + # Model.nested(:my_agg, {path: 'path_to_field'}) + # Model.nested(:my_agg, {path: 'path_to_field'}, aggs: {...}) + # + # Returns a new Stretchy::Relation. + def nested(name, options = {}, *aggs) + options = {nested: options}.merge(*aggs) + aggregation(name, options) + end + + # Public: Perform a percentile_ranks aggregation. + # + # name - The Symbol or String name of the aggregation. + # options - The Hash options used to refine the aggregation (default: {}): + # :field - The field to use for the percentile_ranks aggregation. + # :values - The values to use for the percentile_ranks aggregation. + # :keyed - associates a unique string key with each bucket and returns the ranges as a hash rather than an array. default: true + # :script - The script to use for the percentile_ranks aggregation. (optional) script: {source: "doc['field_name'].value", lang: "painless"} + # :hdr - The hdr to use for the percentile_ranks aggregation. (optional) hdr: {number_of_significant_value_digits: 3} + # :missing - The missing to use for the percentile_ranks aggregation. (optional) missing: 10 + # aggs - The Array of additional nested aggregations (optional). + # + # Examples + # + # Model.percentile_ranks(:my_agg, {field: 'field_name', values: [1, 2, 3]}) + # Model.percentile_ranks(:my_agg, {field: 'field_name', values: [1, 2, 3]}, aggs: {...}) + # + # Returns a new Stretchy::Relation. + def percentile_ranks(name, options = {}, *aggs) + options = {percentile_ranks: options}.merge(*aggs) + aggregation(name, options) + end + + # Public: Perform a percentiles aggregation. + # + # name - The Symbol or String name of the aggregation. + # options - The Hash options used to refine the aggregation (default: {}): + # :field - The field to use for the percentiles aggregation. + # :percents - The percents to use for the percentiles aggregation. percents: [95, 99, 99.9] + # :keyed - associates a unique string key with each bucket and returns the ranges as a hash rather than an array. default: true + # :tdigest - The tdigest to use for the percentiles aggregation. (optional) tdigest: {compression: 100, execution_hint: "high_accuracy"} + # :compression - The compression factor to use for the t-digest algorithm. A higher compression factor will yield more accurate percentiles, but will require more memory. The default value is 100. + # :execution_hint - The execution_hint to use for the t-digest algorithm. (optional) execution_hint: "auto" + # aggs - The Array of additional nested aggregations (optional). + # + # Examples + # + # Model.percentiles(:my_agg, {field: 'field_name', percents: [1, 2, 3]}) + # Model.percentiles(:my_agg, {field: 'field_name', percents: [1, 2, 3]}, aggs: {...}) + # + # Returns a new Stretchy::Relation. + def percentiles(name, options = {}, *aggs) + options = {percentiles: options}.merge(*aggs) + aggregation(name, options) + end + + # Public: Perform a range aggregation. + # + # name - The Symbol or String name of the aggregation. + # options - The Hash options used to refine the aggregation (default: {}): + # :field - The field to use for the range aggregation. + # :ranges - The ranges to use for the range aggregation. + # :keyed - associates a unique string key with each bucket and returns the ranges as a hash rather than an array. default: true + # aggs - The Array of additional nested aggregations (optional). + # + # Examples + # + # Model.range(:my_agg, {field: 'field_name', ranges: [{from: 1, to: 2}, {from: 2, to: 3}]}) + # Model.range(:my_agg, {field: 'field_name', ranges: [{from: 1, to: 2}, {from: 2, to: 3}]}, aggs: {...}) + # + # Returns a new Stretchy::Relation. + def range(name, options = {}, *aggs) + options = {range: options}.merge(*aggs) + aggregation(name, options) + end + + # Public: Perform a reverse_nested aggregation. + # + # name - The Symbol or String name of the aggregation. + # options - The Hash options used to refine the aggregation (default: {}). + # aggs - The Array of additional nested aggregations (optional). + # + # Examples + # + # Model.reverse_nested(:my_agg) + # Model.reverse_nested(:my_agg, {}, aggs: {...}) + # + # Returns a new Stretchy::Relation. + def reverse_nested(name, options = {}, *aggs) + options = {reverse_nested: options}.merge(*aggs) + aggregation(name, options) + end + + # Public: Perform a sampler aggregation. + # + # name - The Symbol or String name of the aggregation. + # options - The Hash options used to refine the aggregation (default: {}): + # :shard_size - The shard size to use for the sampler aggregation. + # aggs - The Array of additional nested aggregations (optional). + # + # Examples + # + # Model.sampler(:my_agg, {shard_size: 100}) + # Model.sampler(:my_agg, {shard_size: 100}, aggs: {...}) + # + # Returns a new Stretchy::Relation. + def sampler(name, options = {}, *aggs) + options = {sampler: options}.merge(*aggs) + aggregation(name, options) + end + + # Public: Perform a scripted_metric aggregation. + # + # name - The Symbol or String name of the aggregation. + # options - The Hash options used to refine the aggregation (default: {}): + # :init_script - The initialization script for the scripted_metric aggregation. + # :map_script - The map script for the scripted_metric aggregation. + # :combine_script - The combine script for the scripted_metric aggregation. + # :reduce_script - The reduce script for the scripted_metric aggregation. + # aggs - The Array of additional nested aggregations (optional). + # + # Examples + # + # Model.scripted_metric(:my_agg, {init_script: '...', map_script: '...', combine_script: '...', reduce_script: '...'}) + # Model.scripted_metric(:my_agg, {init_script: '...', map_script: '...', combine_script: '...', reduce_script: '...'}, aggs: {...}) + # + # Returns a new Stretchy::Relation. + def scripted_metric(name, options = {}, *aggs) + options = {scripted_metric: options}.merge(*aggs) + aggregation(name, options) + end + + # Public: Perform a significant_terms aggregation. + # + # name - The Symbol or String name of the aggregation. + # options - The Hash options used to refine the aggregation (default: {}): + # :field - The field to use for the significant_terms aggregation. + # :background_filter - The background filter to use for the significant_terms aggregation. + # :mutual_information - The mutual information to use for the significant_terms aggregation. + # :chi_square - The chi square to use for the significant_terms aggregation. + # :gnd - The gnd to use for the significant_terms aggregation. + # :jlh - The jlh to use for the significant_terms aggregation. + # aggs - The Array of additional nested aggregations (optional). + # + # Examples + # + # Model.significant_terms(:my_agg, {field: 'field_name'}) + # Model.significant_terms(:my_agg, {field: 'field_name'}, aggs: {...}) + # + # Returns a new Stretchy::Relation. + def significant_terms(name, options = {}, *aggs) + options = {significant_terms: options}.merge(*aggs) + aggregation(name, options) + end + + # Public: Perform a stats aggregation. + # + # name - The Symbol or String name of the aggregation. + # options - The Hash options used to refine the aggregation (default: {}): + # :field - The field to use for the stats aggregation. + # :missing - The missing to use for the stats aggregation. + # :script - The script to use for the stats aggregation. + # aggs - The Array of additional nested aggregations (optional). + # + # Examples + # + # Model.stats(:my_agg, {field: 'field_name'}) + # Model.stats(:my_agg, {field: 'field_name'}, aggs: {...}) + # + # Returns a new Stretchy::Relation. + def stats(name, options = {}, *aggs) + options = {stats: options}.merge(*aggs) + aggregation(name, options) + end + + # Public: Perform a sum aggregation. + # + # name - The Symbol or String name of the aggregation. + # options - The Hash options used to refine the aggregation (default: {}): + # :field - The field to use for the sum aggregation. + # :missing - The missing to use for the sum aggregation. + # :script - The script to use for the sum aggregation. + # aggs - The Array of additional nested aggregations (optional). + # + # Examples + # + # Model.sum(:my_agg, {field: 'field_name'}) + # Model.sum(:my_agg, {field: 'field_name'}, aggs: {...}) + # + # Returns a new Stretchy::Relation. + def sum(name, options = {}, *aggs) + options = {sum: options}.merge(*aggs) + aggregation(name, options) + end + + # Public: Perform a terms aggregation. + # + # name - The Symbol or String name of the aggregation. + # options - The Hash options used to refine the aggregation (default: {}): + # :field - The field to use for the terms aggregation. + # :size - The size for the terms aggregation. (optional) + # :min_doc_count - The minimum document count for the terms aggregation. (optional) + # :shard_min_doc_count - The shard minimum document count for the terms aggregation. (optional) + # :show_term_doc_count_error - The show_term_doc_count_error for the terms aggregation. (optional) default: false + # :shard_size - The shard size for the terms aggregation. (optional) + # :order - The order for the terms aggregation. (optional) + # + # aggs - The Array of additional nested aggregations (optional). + # + # Examples + # + # Model.terms(:my_agg, {field: 'field_name', size: 10, min_doc_count: 1, shard_size: 100}) + # Model.terms(:my_agg, {field: 'field_name', size: 10, min_doc_count: 1, shard_size: 100}, aggs: {...}) + # + # Returns a new Stretchy::Relation. + def terms(name, options = {}, *aggs) + options = {terms: options}.merge(*aggs) + aggregation(name, options) + end + + + # Public: Perform a top_hits aggregation. + # + # name - The Symbol or String name of the aggregation. + # options - The Hash options used to refine the aggregation (default: {}): + # :size - The size for the top_hits aggregation. + # :from - The from for the top_hits aggregation. + # :sort - The sort for the top_hits aggregation. + # aggs - The Array of additional nested aggregations (optional). + # + # Examples + # + # Model.top_hits(:my_agg, {size: 10, sort: {...}}) + # Model.top_hits(:my_agg, {size: 10, sort: {...}}, aggs: {...}) + # + # Returns a new Stretchy::Relation. + def top_hits(name, options = {}, *aggs) + options = {top_hits: options}.merge(*aggs) + aggregation(name, options) + end + + # Public: Perform a top_metrics aggregation. + # + # name - The Symbol or String name of the aggregation. + # options - The Hash options used to refine the aggregation (default: {}): + # :metrics - The metrics to use for the top_metrics aggregation. metrics: [{field: 'field_name', type: 'max'}, {field: 'field_name', type: 'min'] + # :field - The field to use for the top_metrics aggregation. (optional) + # :size - The size for the top_metrics aggregation. (optional) + # :sort - The sort for the top_metrics aggregation. (optional) + # :missing - The missing for the top_metrics aggregation. (optional) + # aggs - The Array of additional nested aggregations (optional). + # + # Examples + # + # Model.top_metrics(:my_agg, {metrics: ['metric1', 'metric2']}) + # Model.top_metrics(:my_agg, {metrics: ['metric1', 'metric2']}, aggs: {...}) + # + # Returns a new Stretchy::Relation. + def top_metrics(name, options = {}, *aggs) + options = {top_metrics: options}.merge(*aggs) + aggregation(name, options) + end + + # Public: Perform a value_count aggregation. + # + # name - The Symbol or String name of the aggregation. + # options - The Hash options used to refine the aggregation (default: {}): + # :field - The field to use for the value_count aggregation. + # :script - The script to use for the value_count aggregation. (optional) script: {source: "doc['field_name'].value", lang: "painless"} + # aggs - The Array of additional nested aggregations (optional). + # + # Examples + # + # Model.value_count(:my_agg, {field: 'field_name'}) + # Model.value_count(:my_agg, {field: 'field_name'}, aggs: {...}) + # + # Returns a new Stretchy::Relation. + def value_count(name, options = {}, *aggs) + options = {value_count: options}.merge(*aggs) + aggregation(name, options) + end + + # Public: Perform a weighted_avg aggregation. + # + # name - The Symbol or String name of the aggregation. + # options - The Hash options used to refine the aggregation (default: {}): + # :value - The value field to use for the weighted_avg aggregation. {value: { field: 'price', missing: 0}} + # :weight - The weight field to use for the weighted_avg aggregation. {weight: { field: 'weight', missing: 0}} + # :format - The format for the weighted_avg aggregation. (optional) + # aggs - The Array of additional nested aggregations (optional). + # + # Examples + # + # Model.weighted_avg(:my_agg, {value_field: 'value_field_name', weight_field: 'weight_field_name'}) + # Model.weighted_avg(:my_agg, {value_field: 'value_field_name', weight_field: 'weight_field_name'}, aggs: {...}) + # + # Returns a new Stretchy::Relation. + def weighted_avg(name, options = {}, *aggs) + options = {weighted_avg: options}.merge(*aggs) + aggregation(name, options) + end + + + end + end +end \ No newline at end of file diff --git a/spec/stretchy/relations/aggregation_methods_spec.rb b/spec/stretchy/relations/aggregation_methods_spec.rb new file mode 100644 index 0000000..feccc79 --- /dev/null +++ b/spec/stretchy/relations/aggregation_methods_spec.rb @@ -0,0 +1,68 @@ +describe Stretchy::Relations::AggregationMethods do + let(:model) do + class TestModel < Stretchy::Record + end + TestModel + end + + + let(:relation) { Stretchy::Relation.new(model, {}) } # create a real instance + + before do + allow(model).to receive(:all).and_return(relation) + end + + shared_examples 'an aggregation method' do |method, args, expected_args| + describe "##{method}" do + before do + allow(relation).to receive(:aggregation) + end + + it "performs a #{method} aggregation" do + if args.is_a?(Array) + model.send(method, :my_agg, *args) + else + model.send(method, :my_agg, args) + end + expect(relation).to have_received(:aggregation).with(:my_agg, expected_args) + end + end + end + + it_behaves_like 'an aggregation method', :avg, {field: :price}, {avg: {field: :price}} + it_behaves_like 'an aggregation method', :bucket_script, {script: "params.tShirtsSold * params.price", buckets_path: {tShirtsSold: "tShirtsSold", price: "price"}}, {bucket_script: {script: "params.tShirtsSold * params.price", buckets_path: {tShirtsSold: "tShirtsSold", price: "price"}}} + it_behaves_like 'an aggregation method', :bucket_selector, {script: "params.totalSales > 200", buckets_path: {totalSales: "totalSales"}}, {bucket_selector: {script: "params.totalSales > 200", buckets_path: {totalSales: "totalSales"}}} + it_behaves_like 'an aggregation method', :cardinality, {field: :category}, {cardinality: {field: :category}} + it_behaves_like 'an aggregation method', :date_histogram, {field: :created_at, interval: 'month'}, {date_histogram: {field: :created_at, interval: 'month'}} + it_behaves_like 'an aggregation method', :extended_stats, {field: :price}, {extended_stats: {field: :price}} + it_behaves_like 'an aggregation method', :filter, {term: {category: 'electronics'}}, {filter: {term: {category: 'electronics'}}} + it_behaves_like 'an aggregation method', :filters, {filters: {electronics: {term: {category: 'electronics'}}, books: {term: {category: 'books'}}}}, {filters: {filters: {electronics: {term: {category: 'electronics'}}, books: {term: {category: 'books'}}}}} + it_behaves_like 'an aggregation method', :geo_bounds, {field: :location}, {geo_bounds: {field: :location}} + it_behaves_like 'an aggregation method', :geo_centroid, {field: :location}, {geo_centroid: {field: :location}} + it_behaves_like 'an aggregation method', :global, {}, {global: {}} + it_behaves_like 'an aggregation method', :histogram, {field: :price, interval: 10}, {histogram: {field: :price, interval: 10}} + it_behaves_like 'an aggregation method', :ip_range, {field: :ip, ranges: [{to: '10.0.0.5'}, {from: '10.0.0.5'}]}, {ip_range: {field: :ip, ranges: [{to: '10.0.0.5'}, {from: '10.0.0.5'}]}} + it_behaves_like 'an aggregation method', :max, {field: :price}, {max: {field: :price}} + it_behaves_like 'an aggregation method', :min, {field: :price}, {min: {field: :price}} + it_behaves_like 'an aggregation method', :missing, {field: :price}, {missing: {field: :price}} + it_behaves_like 'an aggregation method', :nested, {path: 'comments'}, {nested: {path: 'comments'}} + it_behaves_like 'an aggregation method', :percentile_ranks, {field: :price, values: [100, 200]}, {percentile_ranks: {field: :price, values: [100, 200]}} + it_behaves_like 'an aggregation method', :percentiles, {field: :price, percents: [25, 50, 75]}, {percentiles: {field: :price, percents: [25, 50, 75]}} + it_behaves_like 'an aggregation method', :range, {field: :price, ranges: [{to: 100}, {from: 100, to: 200}, {from: 200}]}, {range: {field: :price, ranges: [{to: 100}, {from: 100, to: 200}, {from: 200}]}} + it_behaves_like 'an aggregation method', :reverse_nested, {}, {reverse_nested: {}} + it_behaves_like 'an aggregation method', :sampler, {shard_size: 200}, {sampler: {shard_size: 200}} + it_behaves_like 'an aggregation method', :scripted_metric, {init_script: "_agg['sales'] = 0", map_script: "_agg['sales'] += params.sales", combine_script: "return _agg['sales']", reduce_script: "return _agg['sales']"}, {scripted_metric: {init_script: "_agg['sales'] = 0", map_script: "_agg['sales'] += params.sales", combine_script: "return _agg['sales']", reduce_script: "return _agg['sales']"}} + it_behaves_like 'an aggregation method', :significant_terms, {field: :category}, {significant_terms: {field: :category}} + it_behaves_like 'an aggregation method', :stats, {field: :price}, {stats: {field: :price}} + it_behaves_like 'an aggregation method', :sum, {field: :price}, {sum: {field: :price}} + it_behaves_like 'an aggregation method', :terms, {field: :category}, {terms: {field: :category}} + it_behaves_like 'an aggregation method', :top_hits, {size: 1, sort: [{price: 'desc'}]}, {top_hits: {size: 1, sort: [{price: 'desc'}]}} + it_behaves_like 'an aggregation method', :top_metrics, {metrics: [{field: :price}]}, {top_metrics: {metrics: [{field: :price}]}} + it_behaves_like 'an aggregation method', :value_count, {field: :price}, {value_count: {field: :price}} + it_behaves_like 'an aggregation method', :weighted_avg, {value: {field: :price}, weight: {field: :sales}}, {weighted_avg: {value: {field: :price}, weight: {field: :sales}}} + + context 'when passing nested aggregations' do + it_behaves_like 'an aggregation method', :terms, [{field: :category}, {aggs: {avg_price: {avg: {field: :price}}}}], {terms: {field: :category}, aggs: {avg_price: {avg: {field: :price}}}} + end + +end \ No newline at end of file From 59a4b14a83416e660a1986fd46b07196a8065155 Mon Sep 17 00:00:00 2001 From: Spencer Markowski Date: Sat, 9 Mar 2024 12:22:25 -0500 Subject: [PATCH 3/3] [BREAKING CHANGES] Query filter is renamed to filter_query #45 --- lib/stretchy/querying.rb | 5 ++--- lib/stretchy/relations/merger.rb | 6 +++--- lib/stretchy/relations/query_builder.rb | 4 ++-- lib/stretchy/relations/query_methods.rb | 8 ++++---- spec/stretchy/query_builder_spec.rb | 4 ++-- spec/stretchy/querying_spec.rb | 22 +++++++++++----------- 6 files changed, 24 insertions(+), 25 deletions(-) diff --git a/lib/stretchy/querying.rb b/lib/stretchy/querying.rb index 9789768..84baa63 100644 --- a/lib/stretchy/querying.rb +++ b/lib/stretchy/querying.rb @@ -1,12 +1,11 @@ module Stretchy module Querying delegate :first, :first!, :last, :last!, :exists?, :has_field, :any?, :many?, to: :all - delegate :order, :limit, :size, :sort, :rewhere, :eager_load, :includes, :create_with, :none, :unscope, to: :all + delegate :order, :limit, :size, :sort, :rewhere, :eager_load, :includes, :create_with, :none, :unscope, to: :all delegate :or_filter, :fields, :source, :highlight, to: :all delegate *Stretchy::Relations::AggregationMethods::AGGREGATION_METHODS, to: :all - delegate :skip_callbacks, :routing, to: :all - delegate :search_options, :routing, to: :all + delegate :skip_callbacks, :routing, :search_options, to: :all delegate :must, :must_not, :should, :where_not, :where, :filter_query, :query_string, to: :all def fetch_results(es) diff --git a/lib/stretchy/relations/merger.rb b/lib/stretchy/relations/merger.rb index 2c53ce8..f8e0ede 100644 --- a/lib/stretchy/relations/merger.rb +++ b/lib/stretchy/relations/merger.rb @@ -114,7 +114,7 @@ def merge_multi_values lhs_wheres = relation.where_values rhs_wheres = values[:where] || [] - lhs_filters = relation.filter_values + lhs_filters = relation.filter_query_values rhs_filters = values[:filter_query] || [] removed, kept = partition_overwrites(lhs_wheres, rhs_wheres) @@ -122,11 +122,11 @@ def merge_multi_values where_values = kept + rhs_wheres filters_removed, filters_kept = partition_overwrites(lhs_wheres, rhs_wheres) - filter_values = rhs_filters + filter_query_values = rhs_filters relation.where_values = where_values.empty? ? nil : where_values - relation.filter_values = filter_values.empty? ? nil : filter_values + relation.filter_query_values = filter_query_values.empty? ? nil : filter_query_values if values[:reordering] # override any order specified in the original relation diff --git a/lib/stretchy/relations/query_builder.rb b/lib/stretchy/relations/query_builder.rb index 6408bba..d415c12 100644 --- a/lib/stretchy/relations/query_builder.rb +++ b/lib/stretchy/relations/query_builder.rb @@ -14,7 +14,7 @@ def aggregations end def filters - values[:filter] + values[:filter_query] end def or_filters @@ -58,7 +58,7 @@ def sort end def query_filters - values[:filter] + values[:filter_query] end def search_options diff --git a/lib/stretchy/relations/query_methods.rb b/lib/stretchy/relations/query_methods.rb index 3217268..030cd36 100644 --- a/lib/stretchy/relations/query_methods.rb +++ b/lib/stretchy/relations/query_methods.rb @@ -15,7 +15,7 @@ module QueryMethods :query_string, :aggregation, :search_option, - :query_filter, + :filter_query, :or_filter, :extending, :skip_callbacks @@ -308,7 +308,7 @@ def should!(opts, *rest) # :nodoc: - # @deprecated in elasticsearch 7.x+ use {#filter} instead + # @deprecated in elasticsearch 7.x+ use {#filter_query} instead def or_filter(name, options = {}, &block) spawn.or_filter!(name, options, &block) end @@ -336,7 +336,7 @@ def filter_query(name, options = {}, &block) end def filter_query!(name, options = {}, &block) # :nodoc: - self.filter_values += [{name: name, args: options}] + self.filter_query_values += [{name: name, args: options}] self end @@ -404,7 +404,7 @@ def source!(*args) # :nodoc: # # @return [ActiveRecord::Relation] a new relation, which reflects the exists filter def has_field(field) - spawn.filter(:exists, {field: field}) + spawn.filter_query(:exists, {field: field}) end diff --git a/spec/stretchy/query_builder_spec.rb b/spec/stretchy/query_builder_spec.rb index bffeecb..c6e3b24 100644 --- a/spec/stretchy/query_builder_spec.rb +++ b/spec/stretchy/query_builder_spec.rb @@ -12,7 +12,7 @@ describe '#filters' do it 'returns the filters value' do - expect(subject.filters).to eq(values[:filter]) + expect(subject.filters).to eq(values[:filter_query]) end end @@ -69,7 +69,7 @@ context 'when using filters' do let(:subject) { described_class.new(filters) } - let(:filters) { {filter: [name: :active, args: {term: {status: :active}}]} } + let(:filters) { {filter_query: [name: :active, args: {term: {status: :active}}]} } it 'builds the query structure' do expect(subject.send(:build_query)[:bool][:filter]).to include({active: {term: {status: :active}}}.with_indifferent_access) diff --git a/spec/stretchy/querying_spec.rb b/spec/stretchy/querying_spec.rb index 4f31bcd..b8ce114 100644 --- a/spec/stretchy/querying_spec.rb +++ b/spec/stretchy/querying_spec.rb @@ -52,7 +52,7 @@ end it 'with a filter' do - count = described_class.filter(:terms, gender: [:female]).count + count = described_class.filter_query(:terms, gender: [:female]).count expect(count).to be_a(Integer) expect(count).to eq(10) end @@ -91,33 +91,33 @@ end end - context '.filter' do + context '.filter_query' do it 'filters by term' do - expect(described_class.filter(:term, gender: 'male').map(&:gender)).to all(eq('male')) - expect(described_class.filter(:term, gender: 'female').map(&:gender)).to all(eq('female')) + expect(described_class.filter_query(:term, gender: 'male').map(&:gender)).to all(eq('male')) + expect(described_class.filter_query(:term, gender: 'female').map(&:gender)).to all(eq('female')) end it 'filters by range' do - expect(described_class.filter(:range, age: {gte: 30}).map(&:age)).to all(be >= 30) - expect(described_class.filter(:range, age: {lte: 30}).map(&:age)).to all(be <= 30) + expect(described_class.filter_query(:range, age: {gte: 30}).map(&:age)).to all(be >= 30) + expect(described_class.filter_query(:range, age: {lte: 30}).map(&:age)).to all(be <= 30) end it 'filters by terms' do - expect(described_class.filter(:terms, 'position.name.keyword': ['Software Engineer', 'Product Manager']).map{|r| r.position['name']}).to all(be_in(['Software Engineer', 'Product Manager'])) + expect(described_class.filter_query(:terms, 'position.name.keyword': ['Software Engineer', 'Product Manager']).map{|r| r.position['name']}).to all(be_in(['Software Engineer', 'Product Manager'])) end it 'filters by exists' do - expect(described_class.filter(:exists, field: 'position.level').map{|r| r.position['level']}).to all(be_truthy) + expect(described_class.filter_query(:exists, field: 'position.level').map{|r| r.position['level']}).to all(be_truthy) end # Doesn't seem to be supported in 7.x+ xit 'filters by or' do - expect(described_class.filter(:or, [{term: {age: 25}}, {term: {age: 30}}]).map(&:age)).to all(include(25,30)) + expect(described_class.filter_query(:or, [{term: {age: 25}}, {term: {age: 30}}]).map(&:age)).to all(include(25,30)) end # Doesn't seem to be supported in 7.x+ xit 'filters by not' do - expect(described_class.filter(:not, {term: {age: 25}}).map(&:age)).not_to include(25) + expect(described_class.filter_query(:not, {term: {age: 25}}).map(&:age)).not_to include(25) end end @@ -149,7 +149,7 @@ context 'query string' do it 'filter with query string' do - result = described_class.filter(:query_string, {query: "Mia OR Isabella", default_field: "name"} ) + result = described_class.filter_query(:query_string, {query: "Mia OR Isabella", default_field: "name"} ) expect(result.map(&:name)).to include("Mia Rodriguez", "Isabella Lewis") end