Skip to content

Commit

Permalink
Merge pull request #47 from theablefew/feature/aggregation_methods
Browse files Browse the repository at this point in the history
Rename filter to filter_query
  • Loading branch information
esmarkowski authored Mar 9, 2024
2 parents a5f9988 + 59a4b14 commit 61af64d
Show file tree
Hide file tree
Showing 9 changed files with 865 additions and 64 deletions.
11 changes: 6 additions & 5 deletions lib/stretchy/querying.rb
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
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 :skip_callbacks, :routing, to: :all
delegate :search_options, :routing, to: :all
delegate :must, :must_not, :should, :where_not, :query_string, 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, :search_options, to: :all
delegate :must, :must_not, :should, :where_not, :where, :filter_query, :query_string, to: :all

def fetch_results(es)
unless es.count?
Expand Down
4 changes: 2 additions & 2 deletions lib/stretchy/relation.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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
Expand Down
758 changes: 758 additions & 0 deletions lib/stretchy/relations/aggregation_methods.rb

Large diffs are not rendered by default.

12 changes: 6 additions & 6 deletions lib/stretchy/relations/merger.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -114,19 +114,19 @@ def merge_multi_values
lhs_wheres = relation.where_values
rhs_wheres = values[:where] || []

lhs_filters = relation.filter_values
rhs_filters = values[:filter] || []
lhs_filters = relation.filter_query_values
rhs_filters = values[:filter_query] || []

removed, kept = partition_overwrites(lhs_wheres, rhs_wheres)

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
Expand Down
4 changes: 2 additions & 2 deletions lib/stretchy/relations/query_builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ def aggregations
end

def filters
values[:filter]
values[:filter_query]
end

def or_filters
Expand Down Expand Up @@ -58,7 +58,7 @@ def sort
end

def query_filters
values[:filter]
values[:filter_query]
end

def search_options
Expand Down
46 changes: 10 additions & 36 deletions lib/stretchy/relations/query_methods.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ module QueryMethods
:query_string,
:aggregation,
:search_option,
:filter,
:filter_query,
:or_filter,
:extending,
:skip_callbacks
Expand Down Expand Up @@ -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
Expand All @@ -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:
self.filter_values += [{name: name, args: options}]
def filter_query!(name, options = {}, &block) # :nodoc:
self.filter_query_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



Expand Down Expand Up @@ -430,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


Expand Down
4 changes: 2 additions & 2 deletions spec/stretchy/query_builder_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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)
Expand Down
22 changes: 11 additions & 11 deletions spec/stretchy/querying_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand Down
68 changes: 68 additions & 0 deletions spec/stretchy/relations/aggregation_methods_spec.rb
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit 61af64d

Please sign in to comment.