diff --git a/lib/stretchy/attributes/transformers/keyword_transformer.rb b/lib/stretchy/attributes/transformers/keyword_transformer.rb index caef01d..b9c755e 100644 --- a/lib/stretchy/attributes/transformers/keyword_transformer.rb +++ b/lib/stretchy/attributes/transformers/keyword_transformer.rb @@ -59,7 +59,10 @@ def protected?(arg) # this is for text fields that have a keyword subfield # `:text` and `:string` fields add a `:keyword` subfield to the attribute mapping automatically def transform(item, *ignore) - return unless Stretchy.configuration.auto_target_keywords + return item unless Stretchy.configuration.auto_target_keywords + if item.is_a?(String) + return (!protected?(item) && keyword_available?(item)) ? "#{item}.#{keyword_field_for(item)}" : item + end item.each_with_object({}) do |(key, value), new_item| if ignore && ignore.include?(key) new_item[key] = value diff --git a/lib/stretchy/relations/query_builder.rb b/lib/stretchy/relations/query_builder.rb index 95d2f52..ae41fa3 100644 --- a/lib/stretchy/relations/query_builder.rb +++ b/lib/stretchy/relations/query_builder.rb @@ -64,7 +64,7 @@ def regexes end def fields - values[:field] + values[:fields] end def source @@ -150,24 +150,21 @@ def build_query structure.hybrid do structure.queries do - hybrid[:neural].each do |n| - structure.child! do - params = n.dup - field_name, query_text = params.shift - structure.neural do - structure.set! field_name do - structure.query_text query_text - structure.extract! params, *params.keys - end - end + structure.child! do + params = hybrid[:neural].dup + field_name, query_text = params.shift + structure.neural do + structure.set! field_name do + structure.query_text query_text + structure.extract! params, *params.keys end end + end - hybrid[:query].each do |query| - structure.child! do - structure.extract! query, *query.keys - end - end + structure.child! do + hybrid_query = hybrid[:query].dup + structure.extract! hybrid_query, *hybrid_query.keys + end end end unless hybrid.nil? @@ -222,7 +219,7 @@ def build_query def build_regexp regexes.each do |args| - target_field = args.first.keys.first + target_field = keyword_transformer.transform(args.first.keys.first.to_s) value_field = args.first.values.first structure.set! target_field, args.last.merge(value: value_field) end @@ -300,7 +297,7 @@ def build_search_options def extra_search_options unless self.count? - values[:size] = size.present? ? size : values[:default_size] + values[:size] = size.present? ? size : default_size else values[:size] = nil end @@ -310,7 +307,7 @@ def extra_search_options def compact_where(q, opts = {bool:true}) return if q.nil? if opts.delete(:bool) - as_must(q) + as_must([merge_and_append(q)]) else as_query_string(q.flatten) end @@ -343,26 +340,29 @@ def as_query_string(q) q.each do |arg| arg.each_pair { |k,v| _and << "(#{k}:#{v})" } if arg.class == Hash - _and << "(#{arg})" if arg.class == String + if q.length == 1 + _and << "#{arg}" if arg.class == String + else + _and << "(#{arg})" if arg.class == String + end end _and.join(" AND ") end def merge_and_append(queries) - builder = {} - - queries.each do |q| - q.each do |k, v| - if builder.key?(k) - builder[k] = builder[k].class == Array ? builder[k] : [builder[k]] - builder[k] << v + result = {} + queries.each do |hash| + hash.each do |key, value| + if result[key].is_a?(Array) + result[key] << value + elsif result.key?(key) + result[key] = [result[key], value] else - builder[k] = v + result[key] = value end end end - - builder + result end def extract_highlighter(highlighter) diff --git a/lib/stretchy/relations/query_methods.rb b/lib/stretchy/relations/query_methods.rb index 628ca87..5ed5ea0 100644 --- a/lib/stretchy/relations/query_methods.rb +++ b/lib/stretchy/relations/query_methods.rb @@ -27,7 +27,7 @@ def registry MULTI_VALUE_METHODS = [ :where, :order, - :field, + :fields, :highlight, :source, :must_not, diff --git a/lib/stretchy/relations/query_methods/field.rb b/lib/stretchy/relations/query_methods/fields.rb similarity index 94% rename from lib/stretchy/relations/query_methods/field.rb rename to lib/stretchy/relations/query_methods/fields.rb index 0ab279a..123a74a 100644 --- a/lib/stretchy/relations/query_methods/field.rb +++ b/lib/stretchy/relations/query_methods/fields.rb @@ -1,7 +1,7 @@ module Stretchy module Relations module QueryMethods - module Field + module Fields extend ActiveSupport::Concern # Specify the fields to be returned by the Elasticsearch query. @@ -49,16 +49,16 @@ module Field # Model.fields('books.*') # ``` # - def field(*args) + def fields(*args) spawn.field!(*args) end # Alias for {#field} # @see #field - alias :fields :field + alias :field :fields def field!(*args) # :nodoc: - self.field_values += args + self.fields_values += args self end diff --git a/lib/stretchy/relations/query_methods/hybrid.rb b/lib/stretchy/relations/query_methods/hybrid.rb index 09dd716..21cd21a 100644 --- a/lib/stretchy/relations/query_methods/hybrid.rb +++ b/lib/stretchy/relations/query_methods/hybrid.rb @@ -49,7 +49,7 @@ def hybrid(opts) end def hybrid!(opts) # :nodoc: - self.hybrid_values += [opts] + self.hybrid_values = opts self end diff --git a/lib/stretchy/relations/query_methods/query_string.rb b/lib/stretchy/relations/query_methods/query_string.rb index 69cf279..ecc771f 100644 --- a/lib/stretchy/relations/query_methods/query_string.rb +++ b/lib/stretchy/relations/query_methods/query_string.rb @@ -28,22 +28,12 @@ module QueryString # ``` # def query_string(opts = :chain, *rest) - if opts == :chain - WhereChain.new(spawn) - elsif opts.blank? - self - else - spawn.query_string!(opts, *rest) - end + spawn.query_string!(opts, *rest) end def query_string!(opts, *rest) # :nodoc: - if opts == :chain - WhereChain.new(self) - else - self.query_string_values += build_where(opts, rest) - self - end + self.query_string_values += build_where(opts, rest) + self end QueryMethods.register!(:query_string) diff --git a/lib/stretchy/relations/query_methods/regexp.rb b/lib/stretchy/relations/query_methods/regexp.rb index e83c144..a1e72cf 100644 --- a/lib/stretchy/relations/query_methods/regexp.rb +++ b/lib/stretchy/relations/query_methods/regexp.rb @@ -48,8 +48,6 @@ def regexp!(args) # :nodoc: args = args.to_a target_field, regex = args.shift opts = args.to_h - opts.reverse_merge!(use_keyword: true) - target_field = "#{target_field}.keyword" if opts.delete(:use_keyword) opts.merge!(case_insensitive: true) if regex.casefold? self.regexp_values += [[Hash[target_field, regex.source], opts]] self diff --git a/lib/stretchy/relations/query_methods/where.rb b/lib/stretchy/relations/query_methods/where.rb index 891a438..64a65bb 100644 --- a/lib/stretchy/relations/query_methods/where.rb +++ b/lib/stretchy/relations/query_methods/where.rb @@ -63,8 +63,11 @@ def where(opts = :chain, *rest) opts.each do |key, value| case value when Range - opts.delete(key) - between(value, key) + range = opts.delete(key) + range_options = {gte: range.begin} + upper_bound = range.exclude_end? ? :lt : :lte + range_options[upper_bound] = range.end + filter_query(:range, key => range_options) when Hash opts.delete(key) filter_query(:range, key => value) if value.keys.any? { |k| [:gte, :lte, :gt, :lt].include?(k) } diff --git a/spec/models/test_model.rb b/spec/models/test_model.rb index a94feb6..4cbd73a 100644 --- a/spec/models/test_model.rb +++ b/spec/models/test_model.rb @@ -1,5 +1,6 @@ class TestModel < StretchyModel attribute :name, :text + attribute :title, :keyword attribute :age, :integer attribute :tags, :array attribute :data, :hash diff --git a/spec/stretchy/attributes/transformers/keyword_transformer_spec.rb b/spec/stretchy/attributes/transformers/keyword_transformer_spec.rb index a59ee27..dc37526 100644 --- a/spec/stretchy/attributes/transformers/keyword_transformer_spec.rb +++ b/spec/stretchy/attributes/transformers/keyword_transformer_spec.rb @@ -74,7 +74,7 @@ class MyModel < Stretchy::Record it 'does not transform' do transformed_keywords = values[:where].map do |arg| - described_class.new(model.attribute_types).transform(arg) + expect(described_class.new(model.attribute_types).transform(arg)).to eq(arg) end end end diff --git a/spec/stretchy/neural_spec.rb b/spec/stretchy/neural_spec.rb index 1c11223..b4e9303 100644 --- a/spec/stretchy/neural_spec.rb +++ b/spec/stretchy/neural_spec.rb @@ -123,11 +123,6 @@ expect(described_class).to respond_to(:neural) end - it 'adds values' do - values = described_class.neural(passage_embedding: { query_text: 'hello world', model_id: '1234'}).values[:neural] - expect(values.first).to eq({passage_embedding: { query_text: 'hello world', model_id: '1234'}}) - end - it 'returns results' do allow_any_instance_of(Elasticsearch::Persistence::Repository).to receive(:search).and_return(results) expect(described_class.neural(passage_embedding: 'hello world', model_id: '1234', k:2).total).to eq(2) @@ -152,10 +147,6 @@ expect(described_class).to respond_to(:hybrid) end - it 'adds values' do - values = described_class.hybrid(neural: [{passage_embedding: 'hello world', model_id: '1234', k: 2}], query: [{term: {status: :active}}]).values[:hybrid] - expect(values).to eq([{neural: [{passage_embedding: 'hello world', model_id: '1234', k: 2}], query: [{term: {status: :active}}]}]) - end end diff --git a/spec/stretchy/querying_spec.rb b/spec/stretchy/querying_spec.rb index d4557a8..af4c7e4 100644 --- a/spec/stretchy/querying_spec.rb +++ b/spec/stretchy/querying_spec.rb @@ -66,89 +66,12 @@ expect(described_class.order(age: :asc).count).to eq(19) end end - context '.regexp' do - it 'adds a regexp query' do - expect(described_class.regexp(name: /br.*n/).to_elastic).to eq({query: {regexp: {'name.keyword': { value: 'br.*n'}}}}.with_indifferent_access) - end - - it 'adds a regexp query with flags' do - expect(described_class.regexp(name: /br.*n/, flags: "ALL").to_elastic).to eq({query: {regexp: {'name.keyword': { value: 'br.*n', flags: "ALL"}}}}.with_indifferent_access) - end - - it 'respects .where' do - expect(described_class.where(name: "David Brown").regexp('position.name': /br.*n/).to_elastic).to eq({query: {bool: {must: {term: {'name.keyword': "David Brown"}}},regexp: {'position.name.keyword': { value: 'br.*n'}}}}.with_indifferent_access) - end - - it 'handles case insensitive regex' do - expect(described_class.regexp(name: /br.*n/i).to_elastic).to eq({query: {regexp: {'name.keyword': { value: 'br.*n', case_insensitive: true}}}}.with_indifferent_access) - end - it 'uses keyword when supplied' do - expect(described_class.regexp(name: /br.*n/, use_keyword: true).to_elastic).to eq({query: {regexp: {'name.keyword': { value: 'br.*n'}}}}.with_indifferent_access) - end - end - - context '.where' do - let(:subject) { - described_class.create({"name": "David Brown", "email": "david@example.com", "phone": "555-456-7890", "position": {"name": "Software Engineer", "level": "Junior"}, "gender": "male", "age": 25, "income": 80000, "income_after_raise": 90000}) - } - it 'returns resources with matching attributes' do - r = subject - described_class.refresh_index! - expect(described_class.where(age: 25).map(&:id)).to include(r.id) - r.delete - end - - context 'when using ranges' do - it 'gte and lte with .. ranges' do - begin_date = 2.days.ago.beginning_of_day.utc - end_date = 1.day.ago.end_of_day.utc - expect(described_class.where(date: begin_date..end_date).to_elastic[:query][:bool]).to eq({filter: [{range: {date: {gte: begin_date, lte: end_date}}}]}.with_indifferent_access) - end - - it 'gte and lt with ... ranges' do - begin_date = 2.days.ago.beginning_of_day.utc - end_date = 1.day.ago.end_of_day.utc - expect(described_class.where(date: begin_date...end_date).to_elastic[:query][:bool]).to eq({filter: [{range: {date: {gte: begin_date, lt: end_date}}}]}.with_indifferent_access) - end - - it 'handles integer ranges' do - expect(described_class.where(age: 18..30).to_elastic[:query][:bool]).to eq({filter: [{range: {age: {gte: 18, lte: 30}}}]}.with_indifferent_access) - end - - it 'handles explicit range values' do - expect(described_class.where(price: {gte: 100}).to_elastic).to eq({query: {bool: {filter:[ {range: {price: {gte: 100}}}]}}}.with_indifferent_access) - end - end - - context 'when using regex' do - it 'handles regex' do - expect(described_class.where(color: /gr(a|e)y/).to_elastic).to eq({query: {regexp: {'color.keyword': { value: 'gr(a|e)y' }}}}.with_indifferent_access) - end - - it 'handles regex with flags' do - expect(described_class.where(color: /gr(a|e)y/i).to_elastic).to eq({query: {regexp: {'color.keyword': { value: 'gr(a|e)y', case_insensitive: true }}}}.with_indifferent_access) - end - - it 'handles multiple conditions' do - expect(described_class.where(color: /gr(a|e)y/, age: 30).to_elastic).to eq({query: {bool: {must: {term: {age: 30}}},regexp: {'color.keyword': { value: 'gr(a|e)y' }}}}.with_indifferent_access) - end - end - - context 'when using terms' do - it 'handles terms' do - expect(described_class.where(name: ['Candy', 'Lilly']).to_elastic).to eq({query: {bool:{must: {terms: {'name.keyword': ['Candy', 'Lilly']}}}}}.with_indifferent_access) - end - end - - context 'when using ids' do - it 'handles ids' do - expect(described_class.where(id: [12, 80, 32]).to_elastic).to eq({query: {ids: {values: [12, 80, 32]}}}.with_indifferent_access) - end - end + context '.regexp' do end + context '.must_not' do it 'returns resources without the specified attribute' do expect(described_class.must_not(gender: 'male').map(&:id)).not_to include(subject.id) @@ -235,95 +158,27 @@ context 'sorting' do - it 'accepts fields as keyword arguments' do - result = described_class.order(age: :desc, name: :asc, created_at: {order: :desc, mode: :avg}) - expected = {:sort=>[{:age=>:desc}, {'name.keyword'=>:asc}, {:created_at=>{:order=>:desc, :mode=>:avg}}]} - expect(result.to_elastic).to eq(expected.with_indifferent_access) - end - - it 'is aliased as sort' do - result = described_class.sort(age: :desc) - expected = {:sort=>[{:age=>:desc}]} - expect(result.to_elastic).to eq(expected.with_indifferent_access) - end - - it 'overrides default sort with last' do - subject = described_class.sort(name: :asc) - query = subject.last!.to_elastic - expect(query).to eq({sort: [{'name.keyword': :desc}]}.with_indifferent_access) - end - - it 'overrides default sort with first' do - subject = described_class.sort(name: :asc) - query = subject.first!.to_elastic - expect(query).to eq({sort: [{'name.keyword': :asc}]}.with_indifferent_access) - end - - it 'changes first sort key to desc' do - subject = described_class.sort(name: :asc, age: :desc) - query = subject.last!.to_elastic - expect(query[:sort]).to eq([{'name.keyword' => :desc}, {'age' => :desc}]) - end - - it 'changes first sort key to asc' do - subject = described_class.sort(name: :desc, age: :desc) - query = subject.first!.to_elastic - expect(query[:sort]).to eq([{'name.keyword' => :asc}, {'age' => :desc}]) - end end - context 'fields' do - it 'returns only the specified fields' do - result = described_class.fields(:id, :name, :email) - expected = {:fields=>[:id, :name, :email]} - expect(result.to_elastic).to eq(expected.with_indifferent_access) - end - end - context 'source' do - it 'returns only the specified fields' do - result = described_class.source(includes: [:name, :email]) - expected = {:_source=>{:includes=>[:name, :email]}} - expect(result.to_elastic).to eq(expected.with_indifferent_access) - end + end context 'highlight' do - it 'returns highlighted fields' do - result = described_class.highlight(body: {}) - expected = {:highlight=>{:fields=>{body: {}}}} - expect(result.to_elastic).to eq(expected.with_indifferent_access) - end it 'stores highlights' do result = described_class.query_string("name: Soph*").highlight(name: {pre_tags: "__", post_tags: "__"}).first expect(result.highlights).to eq({"name"=>["__Sophia__ Anderson"]}) end - it 'allows single symbol argument' do - result = described_class.highlight(:body) - expected = {:highlight=>{:fields=>{body: {}}}} - expect(result.to_elastic).to eq(expected.with_indifferent_access) - end - it 'highlights_for ' do result = described_class.query_string("name: Soph*").highlight(name: {pre_tags: "__", post_tags: "__"}).first expect(result.highlights_for(:name)).to eq(["__Sophia__ Anderson"]) end end - it 'adds exists with has_field' do - expect(described_class.has_field(:name).to_elastic[:query][:bool]).to eq({:filter=>[{:exists=>{:field=>:name}}]}.with_indifferent_access) - end - - it 'returns a null relation' do - expect(described_class.none.class).to eq("#{described_class.name}::Stretchy_Relation".constantize) - end - - - end end end \ No newline at end of file diff --git a/spec/stretchy/relations/query_builder_spec.rb b/spec/stretchy/relations/query_builder_spec.rb index ca726ea..69fa5c3 100644 --- a/spec/stretchy/relations/query_builder_spec.rb +++ b/spec/stretchy/relations/query_builder_spec.rb @@ -76,73 +76,10 @@ expect(subject.send(:build_query)[:bool][:should]).to eq({term: {status: :active}}.with_indifferent_access) end end - - - end - - context 'neural search' do - context 'neural_sparse' do - it 'builds' do - subject = described_class.new(neural_sparse: [{embedding: 'hello world', model_id: '1234', max_token_score: 2}]) - expect(subject.to_elastic.dig(:query)).to have_key(:neural_sparse) - expect(subject.to_elastic.dig(:query, :neural_sparse)).to eq({embedding: { query_text: 'hello world', model_id: '1234', max_token_score: 2}}.with_indifferent_access) - end - end - - context 'neural' do - it 'builds' do - subject = described_class.new(neural: [{body_embedding: 'hello world', model_id: '1234', k: 2}]) - expect(subject.to_elastic.dig(:query)).to have_key(:neural) - expect(subject.to_elastic.dig(:query, :neural)).to eq({body_embedding: { query_text: 'hello world', model_id: '1234', k: 2}}.with_indifferent_access) - end - - context 'multimodal' do - it 'builds' do - subject = described_class.new(neural: [{body_embedding: {query_text: 'hello world', query_image: 'base64encodedimage'}, model_id: '1234', k: 2}]) - expect(subject.to_elastic.dig(:query, :neural)).to eq({body_embedding: { query_text: 'hello world', query_image: 'base64encodedimage', model_id: '1234', k: 2}}.with_indifferent_access) - end - - end - end - - context 'hybrid' do - it 'builds' do - subject = described_class.new(hybrid: {neural: [{body_embedding: 'hello world', model_id: '1234', k: 2}], query: [{term: {status: :active}}]}) - elastic_hash = subject.to_elastic - expect(elastic_hash.dig(:query)).to have_key(:hybrid) - expect(elastic_hash.dig(:query, :hybrid)).to have_key(:queries) - expect(elastic_hash.dig(:query, :hybrid, :queries)).to eq([{"neural" => {"body_embedding"=>{"k"=>2, "model_id"=>"1234", "query_text"=>"hello world"}}}, {"term"=>{"status"=>:active}}.with_indifferent_access]) - end - end - end - - context 'when using filters' do - let(:subject) { described_class.new(filters) } - 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) - end - end - context 'sorting' do - it 'accepts array of hashes' do - sorts = [{created_at: :desc}, {title: :asc}] - subject = described_class.new({order: sorts}, attribute_types) - query = subject.to_elastic - expect(query).to eq({sort: sorts}.with_indifferent_access) - end - - it 'accepts options' do - sorts = [{price: { order: :desc, mode: :avg}}] - subject = described_class.new({order: sorts}, attribute_types) - query = subject.to_elastic - expect(query).to eq({sort: sorts}.with_indifferent_access) - end - end context 'search options' do it 'accepts routing' do diff --git a/spec/stretchy/relations/query_methods/field_spec.rb b/spec/stretchy/relations/query_methods/field_spec.rb new file mode 100644 index 0000000..80a2f86 --- /dev/null +++ b/spec/stretchy/relations/query_methods/field_spec.rb @@ -0,0 +1,51 @@ +require 'spec_helper' +require 'models/test_model' + +describe Stretchy::Relations::QueryMethods::Fields do + let(:model) {TestModel} + let!(:relation) { Stretchy::Relation.new(model, {}) } + let(:value_key) { described_class.name.demodulize.underscore.to_sym } + let(:relation_values) { relation.values[value_key] } + + context 'api' do + context 'when not present' do + it 'should have empty values' do + expect(relation_values).to be_nil + end + end + + it 'with multiple fields' do + relation.fields(:id, :name, :email) + expect(relation_values).to eq([:id, :name, :email]) + end + + it 'with a single field' do + relation.fields(:id) + expect(relation_values).to eq([:id]) + end + + end + + + describe Stretchy::Relations::QueryBuilder do + let(:model) { TestModel} + let(:attribute_types) { model.attribute_types } + + let(:values) { {} } + let(:clause) { subject.to_elastic.deep_symbolize_keys.dig(:fields) } + + subject { described_class.new(values, attribute_types) } + + context 'when built' do + + context 'the structure has' do + it 'fields' do + values[:fields] = [:title] + expect(clause).not_to be_nil + expect(clause).to eq([:title]) + end + end + + end + end +end diff --git a/spec/stretchy/relations/query_methods/filter_query_spec.rb b/spec/stretchy/relations/query_methods/filter_query_spec.rb new file mode 100644 index 0000000..6a9cc40 --- /dev/null +++ b/spec/stretchy/relations/query_methods/filter_query_spec.rb @@ -0,0 +1,85 @@ +require 'spec_helper' +require 'models/test_model' + +describe Stretchy::Relations::QueryMethods::FilterQuery do + let(:model) {TestModel} + let!(:relation) { Stretchy::Relation.new(model, {}) } + let(:value_key) { described_class.name.demodulize.underscore.to_sym } + let(:relation_values) { relation.values[value_key] } + + context 'api' do + context 'when not present' do + it 'should have empty values' do + expect(relation_values).to be_nil + end + end + + it 'has name of filter and args' do + relation.filter_query(:range, { age: { gte: 18 } }) + expect(relation_values).to eq([args: {age: {:gte => 18}}, name: :range]) + end + + end + + + describe Stretchy::Relations::QueryBuilder do + let(:model) { TestModel} + let(:attribute_types) { model.attribute_types } + + let(:values) { {} } + let(:clause) { subject.to_elastic.deep_symbolize_keys.dig(:query, :bool, :filter) } + + subject { described_class.new(values, attribute_types) } + + context 'when built' do + context 'with no values' do + it 'is nil' do + expect(clause).to be_nil + end + end + + context 'the structure has' do + it 'filters' do + values[:filter_query] = [args: {age: {:gte => 18}}, name: :range] + expect(clause).not_to be_nil + expect(clause).to eq( + [ + { + range: { + age: { + gte: 18 + } + } + } + ] + ) + end + + it 'multiple filters' do + values[:filter_query] = [ + { args: { age: { gte: 18 } }, name: :range }, + { args: { name: 'Lilly' }, name: :term } + ] + expect(clause).not_to be_nil + expect(clause).to eq( + [ + { + range: { + age: { + gte: 18 + } + } + }, + { + term: { + name: 'Lilly' + } + } + ] + ) + end + end + + end + end +end diff --git a/spec/stretchy/relations/query_methods/has_field_spec.rb b/spec/stretchy/relations/query_methods/has_field_spec.rb new file mode 100644 index 0000000..3c9212d --- /dev/null +++ b/spec/stretchy/relations/query_methods/has_field_spec.rb @@ -0,0 +1,56 @@ +require 'spec_helper' +require 'models/test_model' + +describe Stretchy::Relations::QueryMethods::HasField do + let(:model) { TestModel } + let!(:relation) { Stretchy::Relation.new(model, {}) } + let(:value_key) { described_class.name.demodulize.underscore.to_sym } + let(:relation_values) { relation.values[:filter_query] } + + context 'api' do + context 'when not present' do + it 'should have empty values' do + expect(relation_values).to be_nil + end + end + + it 'builds a filter query' do + relation.has_field(:title) + expect(relation_values).to eq([args: {field: :title}, name: :exists]) + end + end + + describe Stretchy::Relations::QueryBuilder do + let(:model) { TestModel } + let(:attribute_types) { model.attribute_types } + + let(:values) { {} } + let(:clause) { subject.to_elastic.deep_symbolize_keys.dig(:query, :bool, :filter) } + + subject { described_class.new(values, attribute_types) } + + context 'when built' do + context 'with no values' do + it 'is nil' do + expect(clause).to be_nil + end + end + + context 'the structure has' do + it 'the correct query' do + values[:filter_query] = [args: {field: :title}, name: :exists] + expect(clause).not_to be_nil + expect(clause).to eq( + [ + { + exists: { + field: :title + } + } + ] + ) + end + end + end + end +end \ No newline at end of file diff --git a/spec/stretchy/relations/query_methods/highlight_spec.rb b/spec/stretchy/relations/query_methods/highlight_spec.rb new file mode 100644 index 0000000..0c624d2 --- /dev/null +++ b/spec/stretchy/relations/query_methods/highlight_spec.rb @@ -0,0 +1,78 @@ +require 'spec_helper' +require 'models/test_model' + +describe Stretchy::Relations::QueryMethods::Highlight do + let(:model) { TestModel } + let!(:relation) { Stretchy::Relation.new(model, {}) } + let(:value_key) { described_class.name.demodulize.underscore.to_sym } + let(:relation_values) { relation.values[value_key] } + + context 'api' do + context 'when not present' do + it 'should have empty values' do + expect(relation_values).to be_nil + end + end + + it 'stores values' do + relation.highlight(:title) + expect(relation_values).to eq([:title]) + end + + context 'with options' do + it 'stores values' do + relation.highlight(name: {pre_tags: "__", post_tags: "__"}) + expect(relation_values).to eq([{name: {pre_tags: "__", post_tags: "__"}}]) + end + end + end + + describe Stretchy::Relations::QueryBuilder do + let(:model) { TestModel } + let(:attribute_types) { model.attribute_types } + + let(:values) { {} } + let(:clause) { subject.to_elastic.deep_symbolize_keys.dig(:highlight) } + + subject { described_class.new(values, attribute_types) } + + context 'when built' do + context 'with no values' do + it 'is nil' do + expect(clause).to be_nil + end + end + + context 'the structure has' do + it 'the correct query' do + values[:highlight] = [:title] + expect(clause).not_to be_nil + expect(clause).to eq( + { + fields: { + title: {} + } + } + ) + end + + context 'with options' do + it 'the correct query' do + values[:highlight] = [{name: {pre_tags: "__", post_tags: "__"}}] + expect(clause).not_to be_nil + expect(clause).to eq( + { + fields: { + name: { + pre_tags: "__", + post_tags: "__" + } + } + } + ) + end + end + end + end + end +end \ No newline at end of file diff --git a/spec/stretchy/relations/query_methods/hybrid_spec.rb b/spec/stretchy/relations/query_methods/hybrid_spec.rb new file mode 100644 index 0000000..c7c2b5f --- /dev/null +++ b/spec/stretchy/relations/query_methods/hybrid_spec.rb @@ -0,0 +1,106 @@ +require 'spec_helper' +require 'models/test_model' + +describe Stretchy::Relations::QueryMethods::Hybrid, opensearch_only: true do + let(:model) { TestModel } + let!(:relation) { Stretchy::Relation.new(model, {}) } + let(:value_key) { described_class.name.demodulize.underscore.to_sym } + let(:relation_values) { relation.values[value_key] } + + context 'api' do + context 'when not present' do + it 'should have empty values' do + expect(relation_values).to be_nil + end + end + + it 'stores values' do + relation.hybrid( + neural: { + passage_embedding: 'hello world', + model_id: '1234', + k: 2 + }, + query: { + term: { + status: :active + } + } + ) + expect(relation_values).to eq( + + { + neural: { + passage_embedding: 'hello world', + model_id: '1234', + k: 2 + }, + query: { + term: { + status: :active + } + } + } + + ) + end + end + + describe Stretchy::Relations::QueryBuilder do + let(:model) { TestModel } + let(:attribute_types) { model.attribute_types } + + let(:values) { {} } + let(:clause) { subject.to_elastic.deep_symbolize_keys.dig(:query, :hybrid) } + + subject { described_class.new(values, attribute_types) } + + context 'when built' do + context 'with no values' do + it 'is nil' do + expect(clause).to be_nil + end + end + + context 'the structure has' do + it 'the correct query' do + values[:hybrid] = + { + neural: { + passage_embedding: 'hello world', + model_id: '1234', + k: 2 + }, + query: { + term: { + status: :active + } + } + } + + expect(clause).not_to be_nil + expect(clause).to eq( + { + queries: [ + { + neural: { + passage_embedding: { + query_text: 'hello world', + model_id: '1234', + k: 2 + } + } + }, + { + term: { + status: :active + } + } + ] + } + ) + end + end + end + end +end \ No newline at end of file diff --git a/spec/stretchy/relations/query_methods/ids_spec.rb b/spec/stretchy/relations/query_methods/ids_spec.rb new file mode 100644 index 0000000..ec7b594 --- /dev/null +++ b/spec/stretchy/relations/query_methods/ids_spec.rb @@ -0,0 +1,56 @@ +require 'spec_helper' +require 'models/test_model' + +describe Stretchy::Relations::QueryMethods::Ids do + let(:model) { TestModel } + let!(:relation) { Stretchy::Relation.new(model, {}) } + let(:value_key) { described_class.name.demodulize.underscore.to_sym } + let(:relation_values) { relation.values[value_key] } + + context 'api' do + context 'when not present' do + it 'should have empty values' do + expect(relation_values).to be_nil + end + end + + it 'stores values' do + relation.ids([1, 2, 3]) + expect(relation_values).to eq([[1, 2, 3]]) + end + end + + describe Stretchy::Relations::QueryBuilder do + let(:model) { TestModel } + let(:attribute_types) { model.attribute_types } + + let(:values) { {} } + let(:clause) { subject.to_elastic.deep_symbolize_keys.dig(:query, :ids) } + + subject { described_class.new(values, attribute_types) } + + context 'when built' do + context 'with no values' do + it 'is nil' do + expect(clause).to be_nil + end + end + + context 'the structure has' do + it 'the correct query' do + values[:ids] = [1, 2, 3] + expect(clause).not_to be_nil + expect(clause).to eq({ values: [1, 2, 3] }) + end + + context 'multiple chained .ids' do + it 'the correct query' do + values[:ids] = [[1, 2, 3], [2,6,9]] + expect(clause).not_to be_nil + expect(clause).to eq({ values: [1, 2, 3, 6, 9] }) + end + end + end + end + end +end \ No newline at end of file diff --git a/spec/stretchy/relations/query_methods/must_not_spec.rb b/spec/stretchy/relations/query_methods/must_not_spec.rb new file mode 100644 index 0000000..98e653e --- /dev/null +++ b/spec/stretchy/relations/query_methods/must_not_spec.rb @@ -0,0 +1,56 @@ +require 'spec_helper' +require 'models/test_model' + +describe Stretchy::Relations::QueryMethods::MustNot do + let(:model) { TestModel } + let!(:relation) { Stretchy::Relation.new(model, {}) } + let(:value_key) { described_class.name.demodulize.underscore.to_sym } + let(:relation_values) { relation.values[value_key] } + + context 'api' do + context 'when not present' do + it 'should have empty values' do + expect(relation_values).to be_nil + end + end + + it 'stores values' do + relation.must_not(title: 'test') + expect(relation_values).to eq([{title: 'test'}]) + end + end + + describe Stretchy::Relations::QueryBuilder do + let(:model) { TestModel } + let(:attribute_types) { model.attribute_types } + + let(:values) { {} } + let(:clause) { subject.to_elastic.deep_symbolize_keys.dig(:query, :bool, :must_not) } + + subject { described_class.new(values, attribute_types) } + + context 'when built' do + context 'with no values' do + it 'is nil' do + expect(clause).to be_nil + end + end + + context 'the structure has' do + it 'is a term query' do + values[:must_not] = [{title: 'test'}] + expect(clause).not_to be_nil + expect(clause).to eq({ term: { title: 'test' } }) + end + + context 'multiple chained .must_not' do + it 'is an array of terms' do + values[:must_not] = [{title: 'test'}, {name: 'test'}] + expect(clause).not_to be_nil + expect(clause).to eq([{ term: { title: 'test' } }, { term: { 'name.keyword': 'test' } }]) + end + end + end + end + end +end \ No newline at end of file diff --git a/spec/stretchy/relations/query_methods/neural_sparse_spec.rb b/spec/stretchy/relations/query_methods/neural_sparse_spec.rb new file mode 100644 index 0000000..6478764 --- /dev/null +++ b/spec/stretchy/relations/query_methods/neural_sparse_spec.rb @@ -0,0 +1,71 @@ +require 'spec_helper' +require 'models/test_model' + +describe Stretchy::Relations::QueryMethods::NeuralSparse, opensearch_only: true do + let(:model) { TestModel } + let!(:relation) { Stretchy::Relation.new(model, {}) } + let(:value_key) { described_class.name.demodulize.underscore.to_sym } + let(:relation_values) { relation.values[value_key] } + + context 'api' do + context 'when not present' do + it 'should have empty values' do + expect(relation_values).to be_nil + end + end + + it 'stores values' do + relation.neural_sparse(passage_embedding: 'hello world', model_id: '1234', max_token_score: 2) + expect(relation_values).to eq([{passage_embedding: 'hello world', model_id: '1234', max_token_score: 2}]) + end + end + + describe Stretchy::Relations::QueryBuilder do + let(:model) { TestModel } + let(:attribute_types) { model.attribute_types } + + let(:values) { {} } + let(:clause) { subject.to_elastic.deep_symbolize_keys.dig(:query, :neural_sparse) } + + subject { described_class.new(values, attribute_types) } + + context 'when built' do + context 'with no values' do + it 'is nil' do + expect(clause).to be_nil + end + end + + context 'the structure has' do + it 'the correct query' do + values[:neural_sparse] = [{passage_embedding: 'hello world', model_id: '1234', max_token_score: 2}] + expect(clause).not_to be_nil + expect(clause).to eq( + passage_embedding: { + model_id: '1234', + max_token_score: 2, + query_text: 'hello world' + } + ) + end + + context 'multiple chained .neural_sparse' do + #TODO: Need to test if multiple neural_sparse queries are allowed in OpenSearch + it 'overwrites the last value' do + values[:neural_sparse] = [{passage_embedding: 'hello world', model_id: '1234', max_token_score: 2}, {passage_embedding: 'goodbye world', model_id: '4321', max_token_score: 3}] + expect(clause).not_to be_nil + expect(clause).to eq( + { + passage_embedding: { + model_id: '4321', + max_token_score: 3, + query_text: 'goodbye world' + } + } + ) + end + end + end + end + end +end \ No newline at end of file diff --git a/spec/stretchy/relations/query_methods/neural_spec.rb b/spec/stretchy/relations/query_methods/neural_spec.rb new file mode 100644 index 0000000..479365b --- /dev/null +++ b/spec/stretchy/relations/query_methods/neural_spec.rb @@ -0,0 +1,78 @@ +require 'spec_helper' +require 'models/test_model' + +describe Stretchy::Relations::QueryMethods::Neural, opensearch_only: true do + let(:model) { TestModel } + let!(:relation) { Stretchy::Relation.new(model, {}) } + let(:value_key) { described_class.name.demodulize.underscore.to_sym } + let(:relation_values) { relation.values[value_key] } + + context 'api' do + context 'when not present' do + it 'should have empty values' do + expect(relation_values).to be_nil + end + end + + context 'unimodal' do + it 'stores values' do + relation.neural(body_embeddings: 'hello world', model_id: '1234') + expect(relation_values).to eq([{body_embeddings: 'hello world', model_id: '1234'}]) + end + end + + context 'multimodal' do + it 'stores values' do + relation.neural(body_embeddings: {query_text: 'hello world', query_image: 'base64encodedimage'}, model_id: '1234') + expect(relation_values).to eq([{body_embeddings: {query_text: 'hello world', query_image: 'base64encodedimage'}, model_id: '1234'}]) + end + end + end + + describe Stretchy::Relations::QueryBuilder do + let(:model) { TestModel } + let(:attribute_types) { model.attribute_types } + + let(:values) { {} } + let(:clause) { subject.to_elastic.deep_symbolize_keys.dig(:query, :neural) } + + subject { described_class.new(values, attribute_types) } + + context 'when built' do + context 'with no values' do + it 'is nil' do + expect(clause).to be_nil + end + end + + context 'the structure has' do + context 'multimodal' do + it 'the correct query' do + values[:neural] = [{body_embeddings: {query_text: 'hello world', query_image: 'base64encodedimage'}, model_id: '1234'}] + expect(clause).not_to be_nil + expect(clause).to eq( + body_embeddings: { + model_id: '1234', + query_text: 'hello world', + query_image: 'base64encodedimage' + } + ) + end + end + + context 'unimodal' do + it 'the correct query' do + values[:neural] = [{body_embeddings: 'hello world', model_id: '1234'}] + expect(clause).not_to be_nil + expect(clause).to eq( + body_embeddings: { + model_id: '1234', + query_text: 'hello world' + } + ) + end + end + end + end + end +end \ No newline at end of file diff --git a/spec/stretchy/relations/query_methods/none_spec.rb b/spec/stretchy/relations/query_methods/none_spec.rb new file mode 100644 index 0000000..5d7a92f --- /dev/null +++ b/spec/stretchy/relations/query_methods/none_spec.rb @@ -0,0 +1,10 @@ +require 'spec_helper' +require 'models/test_model' + +describe Stretchy::Relations::QueryMethods::None do + let(:model) { TestModel } + + it 'returns a null relation' do + expect(model.none.class).to eq("#{model.name}::Stretchy_Relation".constantize) + end +end \ No newline at end of file diff --git a/spec/stretchy/relations/query_methods/order_spec.rb b/spec/stretchy/relations/query_methods/order_spec.rb new file mode 100644 index 0000000..a402fae --- /dev/null +++ b/spec/stretchy/relations/query_methods/order_spec.rb @@ -0,0 +1,91 @@ +require 'spec_helper' +require 'models/test_model' + +describe Stretchy::Relations::QueryMethods::Order do + let(:model) { TestModel } + let!(:relation) { Stretchy::Relation.new(model, {}) } + let(:value_key) { described_class.name.demodulize.underscore.to_sym } + let(:relation_values) { relation.values[value_key] } + + context 'api' do + context 'when not present' do + it 'should have empty values' do + expect(relation_values).to be_nil + end + end + + it 'stores values' do + relation.order(age: :desc, name: :asc) + expect(relation_values).to eq([{age: :desc},{name: :asc}]) + end + + context 'multiple fields' do + it 'stores values' do + relation.order(age: :desc, name: :asc, price: {order: :desc, mode: :avg}) + expect(relation_values).to eq([{age: :desc},{name: :asc},{price: {order: :desc, mode: :avg}}]) + end + end + + context 'with first and last' do + it 'overrides default sort with last' do + subject = relation.sort(name: :asc) + query = relation.last!.to_elastic + expect(query).to eq({sort: [{'name.keyword': :desc}]}.with_indifferent_access) + end + + it 'overrides default sort with first' do + subject = relation.sort(name: :asc) + query = relation.first!.to_elastic + expect(query).to eq({sort: [{'name.keyword': :asc}]}.with_indifferent_access) + end + + it 'changes first sort key to desc' do + subject = relation.sort(name: :asc, age: :desc) + query = relation.last!.to_elastic + expect(query[:sort]).to eq([{'name.keyword' => :desc}, {'age' => :desc}]) + end + + it 'changes first sort key to asc' do + subject = relation.sort(name: :desc, age: :desc) + query = relation.first!.to_elastic + expect(query[:sort]).to eq([{'name.keyword' => :asc}, {'age' => :desc}]) + end + end + end + + describe Stretchy::Relations::QueryBuilder do + let(:model) { TestModel } + let(:attribute_types) { model.attribute_types } + + let(:values) { {} } + let(:clause) { subject.to_elastic.deep_symbolize_keys.dig(:sort) } + + subject { described_class.new(values, attribute_types) } + + context 'when built' do + context 'with no values' do + it 'is nil' do + expect(clause).to be_nil + end + end + + context 'the structure has' do + it 'the correct query' do + values[:order] = [{age: :desc, name: :asc}] + expect(clause).not_to be_nil + expect(clause).to eq([{ age: :desc, 'name.keyword': :asc }]) + end + + context 'multiple fields' do + it 'the correct query' do + values[:order] = [{age: :desc, name: :asc, price: {order: :desc, mode: :avg}}] + expect(clause).not_to be_nil + expect(clause).to eq([{ age: :desc, 'name.keyword': :asc, price: {order: :desc, mode: :avg} }]) + end + end + end + + + end + end +end \ No newline at end of file diff --git a/spec/stretchy/relations/query_methods/query_string_spec.rb b/spec/stretchy/relations/query_methods/query_string_spec.rb new file mode 100644 index 0000000..d8e38e5 --- /dev/null +++ b/spec/stretchy/relations/query_methods/query_string_spec.rb @@ -0,0 +1,48 @@ +require 'spec_helper' +require 'models/test_model' + +describe Stretchy::Relations::QueryMethods::QueryString do + let(:model) { TestModel } + let!(:relation) { Stretchy::Relation.new(model, {}) } + let(:value_key) { described_class.name.demodulize.underscore.to_sym } + let(:relation_values) { relation.values[value_key] } + + context 'api' do + context 'when not present' do + it 'should have empty values' do + expect(relation_values).to be_nil + end + end + + it 'stores values' do + relation.query_string("((big cat) OR (domestic cat)) AND NOT panther eye_color: green") + expect(relation_values).to eq(["((big cat) OR (domestic cat)) AND NOT panther eye_color: green"]) + end + end + + describe Stretchy::Relations::QueryBuilder do + let(:model) { TestModel } + let(:attribute_types) { model.attribute_types } + + let(:values) { {} } + let(:clause) { subject.to_elastic.deep_symbolize_keys.dig(:query, :query_string) } + + subject { described_class.new(values, attribute_types) } + + context 'when built' do + context 'with no values' do + it 'is nil' do + expect(clause).to be_nil + end + end + + context 'the structure has' do + it 'the correct query' do + values[:query_string] = ["((big cat) OR (domestic cat)) AND NOT panther eye_color: green"] + expect(clause).not_to be_nil + expect(clause).to eq({ query: "((big cat) OR (domestic cat)) AND NOT panther eye_color: green" }) + end + end + end + end +end \ No newline at end of file diff --git a/spec/stretchy/relations/query_methods/regexp_spec.rb b/spec/stretchy/relations/query_methods/regexp_spec.rb new file mode 100644 index 0000000..710d1e2 --- /dev/null +++ b/spec/stretchy/relations/query_methods/regexp_spec.rb @@ -0,0 +1,72 @@ +require 'spec_helper' +require 'models/test_model' + +describe Stretchy::Relations::QueryMethods::Regexp do + let(:model) { TestModel } + let!(:relation) { Stretchy::Relation.new(model, {}) } + let(:value_key) { described_class.name.demodulize.underscore.to_sym } + let(:relation_values) { relation.values[value_key] } + + context 'api' do + context 'when not present' do + it 'should have empty values' do + expect(relation_values).to be_nil + end + end + + it 'stores values' do + relation.regexp(title: /john|jane/) + expect(relation_values).to eq([[{'title': "john|jane"},{}]]) + end + + it 'detects case insensitive matching' do + relation.regexp(name: /john|jane/i) + expect(relation_values).to eq([[{'name': "john|jane"}, {case_insensitive: true}]]) + end + + it 'accepts options' do + relation.regexp(name: /john|jane/i, flags: 'ALL') + expect(relation_values).to eq([[{'name': "john|jane"}, {case_insensitive: true, flags: 'ALL'}]]) + end + + end + + describe Stretchy::Relations::QueryBuilder do + let(:model) { TestModel } + let(:attribute_types) { model.attribute_types } + + let(:values) { {} } + let(:clause) { subject.to_elastic.deep_symbolize_keys.dig(:query, :regexp) } + + subject { described_class.new(values, attribute_types) } + + context 'when built' do + context 'with no values' do + it 'is nil' do + expect(clause).to be_nil + end + end + + context 'the structure has' do + it 'the correct query' do + values[:regexp] = [[{name: 'john|jane'}, {case_insensitive: true, flags: 'ALL'}]] + expect(clause).not_to be_nil + expect(clause).to eq({ 'name.keyword': { value: 'john|jane', flags: 'ALL', case_insensitive: true } }) + end + + it 'appears when must clause is present' do + values[:where] = [{title: 'Fun times'}] + values[:regexp] = [[{name: 'john|jane'}, {case_insensitive: true, flags: 'ALL'}]] + expect(subject.to_elastic.deep_symbolize_keys.dig(:query, :bool, :must)).to eq( + { + term: { + title: 'Fun times' + } + } + ) + expect(clause).to eq({ 'name.keyword': { value: 'john|jane', flags: 'ALL', case_insensitive: true } }) + end + end + end + end +end \ No newline at end of file diff --git a/spec/stretchy/relations/query_methods/should_spec.rb b/spec/stretchy/relations/query_methods/should_spec.rb new file mode 100644 index 0000000..b250690 --- /dev/null +++ b/spec/stretchy/relations/query_methods/should_spec.rb @@ -0,0 +1,61 @@ +require 'spec_helper' +require 'models/test_model' + +describe Stretchy::Relations::QueryMethods::Should do + let(:model) { TestModel } + let!(:relation) { Stretchy::Relation.new(model, {}) } + let(:value_key) { described_class.name.demodulize.underscore.to_sym } + let(:relation_values) { relation.values[value_key] } + + context 'api' do + context 'when not present' do + it 'should have empty values' do + expect(relation_values).to be_nil + end + end + + it 'stores values' do + relation.should(color: 'pink', size: 'medium') + expect(relation_values).to eq([{color: 'pink', size: 'medium'}]) + end + end + + describe Stretchy::Relations::QueryBuilder do + let(:model) { TestModel } + let(:attribute_types) { model.attribute_types } + + let(:values) { {} } + let(:clause) { subject.to_elastic.deep_symbolize_keys.dig(:query, :bool, :should) } + + subject { described_class.new(values, attribute_types) } + + context 'when built' do + context 'with no values' do + it 'is nil' do + expect(clause).to be_nil + end + end + + context 'the structure has' do + it 'the correct query' do + values[:should] = [{color: 'pink', size: 'medium'}] + expect(clause).not_to be_nil + expect(clause).to eq( + [ + { + term: { + color: 'pink' + } + }, + { + term: { + size: 'medium' + } + } + ] + ) + end + end + end + end +end \ No newline at end of file diff --git a/spec/stretchy/relations/query_methods/size_spec.rb b/spec/stretchy/relations/query_methods/size_spec.rb new file mode 100644 index 0000000..3c17872 --- /dev/null +++ b/spec/stretchy/relations/query_methods/size_spec.rb @@ -0,0 +1,56 @@ +require 'spec_helper' +require 'models/test_model' + +describe Stretchy::Relations::QueryMethods::Size do + let(:model) { TestModel } + let!(:relation) { Stretchy::Relation.new(model, {}) } + let(:value_key) { described_class.name.demodulize.underscore.to_sym } + let(:relation_values) { relation.values[value_key] } + + context 'api' do + context 'when not present' do + it 'should have empty values' do + expect(relation_values).to be_nil + end + end + + it 'stores values' do + relation.size(10) + expect(relation_values).to eq(10) + end + end + + describe Stretchy::Relations::QueryBuilder do + let(:model) { TestModel } + let(:attribute_types) { model.attribute_types } + + let(:values) { {default_size: default_size } } + let(:default_size) { 10000 } + let(:clause) { subject.search_options.dig(:size) } + + subject { described_class.new(values, attribute_types) } + + context 'when built' do + context 'with no values' do + it 'has default size' do + expect(clause).to eq(default_size) + end + end + + context 'when count' do + it 'has no size' do + values[:count] = true + expect(clause).to be_nil + end + end + + context 'the structure has' do + it 'the correct query' do + values[:size] = 10 + expect(clause).not_to be_nil + expect(clause).to eq(values[:size]) + end + end + end + end +end \ No newline at end of file diff --git a/spec/stretchy/relations/query_methods/source_spec.rb b/spec/stretchy/relations/query_methods/source_spec.rb new file mode 100644 index 0000000..e293000 --- /dev/null +++ b/spec/stretchy/relations/query_methods/source_spec.rb @@ -0,0 +1,78 @@ +require 'spec_helper' +require 'models/test_model' + +describe Stretchy::Relations::QueryMethods::Source do + let(:model) { TestModel } + let!(:relation) { Stretchy::Relation.new(model, {}) } + let(:value_key) { described_class.name.demodulize.underscore.to_sym } + let(:relation_values) { relation.values[value_key] } + + context 'api' do + context 'when not present' do + it 'should have empty values' do + expect(relation_values).to be_nil + end + end + + it 'stores values for includes' do + relation.source(includes: [:name, :email]) + expect(relation_values).to eq([{ includes: [:name, :email] }]) + end + + it 'stores values for excludes' do + relation.source(excludes: [:name, :email]) + expect(relation_values).to eq([{ excludes: [:name, :email] }]) + end + + it 'stores values for boolean' do + relation.source(false) + expect(relation_values).to eq([false]) + end + end + + describe Stretchy::Relations::QueryBuilder do + let(:model) { TestModel } + let(:attribute_types) { model.attribute_types } + + let(:values) { {} } + let(:clause) { subject.to_elastic.deep_symbolize_keys.dig(:_source) } + + subject { described_class.new(values, attribute_types) } + + context 'when built' do + context 'with no values' do + it 'is nil' do + expect(clause).to be_nil + end + end + + context 'the structure has' do + it 'the correct query for includes' do + values[:source] = [{ includes: [:name, :email] }] + expect(clause).not_to be_nil + expect(clause).to eq( + { + includes: [:name, :email] + } + ) + end + + it 'the correct query for excludes' do + values[:source] = [{ excludes: [:name, :email] }] + expect(clause).not_to be_nil + expect(clause).to eq( + { + excludes: [:name, :email] + } + ) + end + + it 'the correct query for boolean' do + values[:source] = [false] + expect(clause).not_to be_nil + expect(clause).to eq(false) + end + end + end + end +end \ No newline at end of file diff --git a/spec/stretchy/relations/query_methods/where_spec.rb b/spec/stretchy/relations/query_methods/where_spec.rb new file mode 100644 index 0000000..5eed5f0 --- /dev/null +++ b/spec/stretchy/relations/query_methods/where_spec.rb @@ -0,0 +1,222 @@ +require 'spec_helper' +require 'models/test_model' + +describe Stretchy::Relations::QueryMethods::Where do + let(:model) {TestModel} + let!(:relation) { Stretchy::Relation.new(model, {}) } + let(:value_key) { described_class.name.demodulize.underscore.to_sym } + let(:relation_values) { relation.values[value_key] } + + context 'api' + # examples of usage on a StretchyModel + + context 'when not present' do + it 'should have empty values' do + expect(relation_values).to be_nil + end + end + + it 'stores values' do + relation.where(title: 'Fun times') + expect(relation_values).to eq([{title: 'Fun times'}]) + end + + context 'when chained' do + context 'with duplicate conditions' do + it 'stacks values' do + relation.where(title: 'Fun times').where(title: 'Sad times') + expect(relation_values).to eq([{title: 'Fun times'}, {title: 'Sad times'}]) + end + end + end + + + context 'when using ranges' do + let(:relation_filter_values) { relation.values[:filter_query] } + + it 'gte and lte with .. ranges' do + begin_date = 2.days.ago.beginning_of_day.utc + end_date = 1.day.ago.end_of_day.utc + relation.where(date: begin_date..end_date) + expect(relation_filter_values).to eq([args: {date: {:gte => begin_date, :lte => end_date}}, name: :range]) + end + + it 'gte and lt with ... ranges' do + begin_date = 2.days.ago.beginning_of_day.utc + end_date = 1.day.ago.end_of_day.utc + relation.where(date: begin_date...end_date) + expect(relation_filter_values).to eq([args: {date: {:gte => begin_date, :lt => end_date}}, name: :range]) + end + + it 'handles integer ranges' do + relation.where(age: 18..30) + expect(relation_filter_values).to eq([args: {age: {:gte => 18, :lte => 30}}, name: :range]) + end + + it 'handles explicit range values' do + relation.where(price: {gte: 100}) + expect(relation_filter_values).to eq([args: {price: {:gte => 100}}, name: :range]) + end + end + + context 'when using regex' do + let(:relation_regexp_values) { relation.values[:regexp] } + + it 'handles regex' do + relation.where(color: /gr(a|e)y/) + expect(relation_regexp_values).to eq([[{color: "gr(a|e)y"}, {}]]) + expect(relation_values).to be_nil + end + + it 'handles regex with flags' do + relation.where(color: /gr(a|e)y/i) + expect(relation_regexp_values).to eq([[{color: "gr(a|e)y"}, {:case_insensitive=>true}]]) + expect(relation_values).to be_nil + end + + it 'handles multiple conditions' do + relation.where(color: /gr(a|e)y/, age: 30) + expect(relation_regexp_values).to eq([[{color: "gr(a|e)y"}, {}]]) + expect(relation_values).to eq([{age: 30}]) + end + end + + context 'when using terms' do + it 'handles terms' do + relation.where(name: ['Candy', 'Lilly']) + expect(relation_values).to eq([{name: ['Candy', 'Lilly']}]) + end + end + + context 'when using ids' do + it 'handles ids' do + relation.where(id: [12, 80, 32]) + expect(relation.values[:ids]).to eq([[12, 80, 32]]) + expect(relation_values).to be_nil + end + end + + end + + describe Stretchy::Relations::QueryBuilder do + let(:model) { TestModel} + let(:attribute_types) { model.attribute_types } + + let(:values) { {} } + let(:clause) { subject.to_elastic.deep_symbolize_keys.dig(:query, :bool, :must) } + + subject { described_class.new(values, attribute_types) } + + context 'when built' do + context 'with no values' do + it 'is nil' do + expect(clause).to be_nil + end + end + + context 'the structure has' do + it 'query.bool.must' do + values[:where] = [{title: 'Fun times'}] + expect(clause).not_to be_nil + end + end + + context 'with a text attribute' do + context 'when configuration.auto_target_keywords' do + it 'adds .keyword' do + values[:where] = [{name: 'Zeenor'}] + expect(clause).to eq( + { + term: { + 'name.keyword': 'Zeenor' + } + } + ) + end + end + + context 'when configuration.auto_target_keywords is false' do + it 'does not add .keyword' do + Stretchy.configuration.auto_target_keywords = false + values[:where] = [{name: 'Zeenor'}] + expect(clause).to eq( + { + term: { + name: 'Zeenor' + } + } + ) + end + end + end + + context 'with single term' do + it 'is a term query' do + values[:where] = [{title: 'Fun times'}] + expect(clause).to eq( + { + term: { + title: 'Fun times' + } + } + ) + end + + it 'creates a term query for each distinct field' do + values[:where] = [{title: 'Fun times'}, {color: 'blue'}] + expect(clause).to eq( + [ + { + term: { + title: 'Fun times' + } + }, + { + term: { + color: 'blue' + } + } + ] + ) + end + end + + context 'when array of terms' do + it 'is a terms query' do + values[:where] = [{color: ['blue', 'green']}] + expect(clause).to eq( + { + terms: { + color: ['blue', 'green'] + } + } + ) + end + end + + context 'when key appears multiple times' do + it 'is a terms query' do + values[:where] = [ + {title: 'Fun times'}, + {title: 'Sad times'}, + {color: 'blue'} + ] + expect(clause).to eq( + [ + { + terms: { + title: ["Fun times", "Sad times"] + } + }, + { + term: { + color: 'blue' + } + } + ] + ) + end + end + + end + end