Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ Please mark backwards incompatible changes with an exclamation mark at the start
## [Unreleased]

### Added
- The `#bool` method to the `QueryBuilder::QueryClauses::Bool` class. This
allows boolean clauses to be nested.
- `QueryBuilder#sort` can now receive either the direction of the sorting (`asc`
or `desc`) or a `Hash` with advanced sorting options. These are relayed
directly to Elasticsearch.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,33 @@ turn:
query.wildcard(field: 'company', value: '*Inc.')
end

Boolean clauses can also be nested:

.. code-block:: ruby

query_builder = QueryBuilder.new
query_builder.query.bool.must do |must_clause|
must_clause.wildcard(field: 'user.id', value: 'ki*y')
must_clause.query_string(fields: 'city', query: '(new york city) OR (big apple)')
must_clause.bool.should do |should_clause|
should_clause.range(field: 'age', gt: 20)
should_clause.range(field: 'salary', gt: 60_000)
end
end

The behaviour of this clause is completely different to what would happen if
``must`` and ``should`` where both in the top boolean clause.

With the nested ``should`` clause, Elasticsearch will only return documents that
match both the ``wildcard`` and ``query_string`` clauses AND match at least one
of the ``range`` clauses.

When ``should`` isn't nested Elasticsearch will return documents that match both
the ``wildcard`` and ``query_string`` OR match any of the ``range`` clauses.

This difference in behaviour becomes more apparent when querying multiple
indexes where one of fields in the query isn't present in all the indexes.

match_phrase
++++++++++++

Expand Down
13 changes: 13 additions & 0 deletions lib/jay_api/elasticsearch/query_builder/query_clauses/bool.rb
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,19 @@ def must_not(&block)
add_boolean_clause(:must_not, &block)
end

# Adds a nested +Boolean+ clause to the receiver. For this to work the
# receiver must have an active sub-clause (for example +must+).
# @yield [JayAPI::Elasticsearch::QueryBuilder::QueryClauses::Bool]
# Yields the nested boolean clause to the given block.
# @return [JayAPI::Elasticsearch::QueryBuilder::QueryClauses::Bool]
# Returns the nested boolean clause.
def bool(&block)
nested_bool = self.class.new
self << nested_bool
block&.call(nested_bool)
nested_bool
end

# Adds a clause to the current sub-clause of the boolean clause.
# @param [JayAPI::Elasticsearch::QueryBuilder::QueryClauses::QueryClause]
# query_clause The query clause to add.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,90 @@ def to_h
end
end

shared_examples_for '#bool' do
context "when the receiver doesn't have an active sub-clause" do
it 'raises a JayAPI::Elasticsearch::QueryBuilder::Errors::QueryBuilderError' do
expect { method_call }.to raise_error(
JayAPI::Elasticsearch::QueryBuilder::Errors::QueryBuilderError,
'Please call #must, #filter, #should or #must_not in order to add query clauses inside a boolean clause'
)
end
end

context 'when the receiver has an active sub-clause' do
let(:modified_method_call) do
method_call.must << dummy_clause.new('clause 2')
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
# A match clause needs to be added to the nested boolean clause
# or #to_h will error out because the nested boolean clause is
# empty.
end

let(:expected_nested_boolean) do
{
bool: {
must: [
{ value: 'clause 2' }
]
}
}
end

let(:expected_boolean) do
{
bool: {
must_not: [
{ value: 'clause 1' },
expected_nested_boolean
]
}
}
end

before do
bool_clause.must_not << dummy_clause.new('clause 1')
end

it 'creates a new instance of the Bool clause class' do
expect(described_class).to receive(:new)
method_call
end

it 'adds the expected boolean clause' do
modified_method_call
expect(bool_clause.to_h).to eq(expected_boolean)
end

it 'returns the nested boolean clause' do
nested_boolean_clause = modified_method_call
expect(nested_boolean_clause).to be_a(described_class)
expect(nested_boolean_clause.to_h).to eq(expected_nested_boolean)
end
end
end

describe '#bool' do
context 'when no block has been given' do
subject(:method_call) { bool_clause.bool }

it_behaves_like '#bool'
end

context 'when a block has been given' do
subject(:method_call) { bool_clause.bool { nil } }

it_behaves_like '#bool'

context 'when the receiver has an active sub-clause' do
before { bool_clause.should }

it 'yields the nested boolean clause to the given block' do
expect { |b| bool_clause.bool(&b) }.to yield_with_args(be_a(described_class))
end
end
end
end

describe '#to_h' do
subject(:method_call) { bool_clause.to_h }

Expand Down