Skip to content
Open
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
12 changes: 11 additions & 1 deletion app/models/hybrid_query_builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,19 @@ def combine_queries(semantic_query, lexical_query)
lexical_query
end

# Remove filters from semantic branch since they'll be applied at top level
# to avoid redundant filter clauses
semantic_without_filters = if semantic_query.is_a?(Hash) && semantic_query[:bool]
{
bool: semantic_query[:bool].reject { |k, _| k == :filter }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use except(:filter) instead. [rubocop:Style/HashExcept]

}
else
semantic_query
end

hybrid_bool = {
should: [
semantic_query,
semantic_without_filters,
lexical_search
]
}
Expand Down
23 changes: 20 additions & 3 deletions app/models/semantic_query_builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,28 @@ class LambdaError < StandardError; end
def build(params, fulltext: false)
query_text = params[:q].to_s.strip

# If no query text provided, return a match_all query (consistent with keyword search behavior)
return { match_all: {} } if query_text.blank?
# If no query text provided, return a match_all query with filters applied
# (consistent with keyword search behavior and enabling tab filtering with empty search)
if query_text.blank?
filters = FilterBuilder.new.build(params)
return { bool: { filter: filters } } if filters.present?
Comment thread
qltysh[bot] marked this conversation as resolved.

return { match_all: {} }
end

lambda_response = invoke_semantic_builder(query_text)
parse_lambda_response(lambda_response)
semantic_query = parse_lambda_response(lambda_response)

# Validate the query structure has a bool clause before applying filters
unless semantic_query.is_a?(Hash) && semantic_query[:bool].is_a?(Hash)
raise "Invalid semantic query structure: expected bool clause, got #{semantic_query.inspect}"
end

# Apply filters to the semantic query (matching LexicalQueryBuilder pattern)
filters = FilterBuilder.new.build(params)
semantic_query[:bool][:filter] = filters

Comment thread
jazairi marked this conversation as resolved.
Comment thread
jazairi marked this conversation as resolved.
semantic_query
Comment thread
qltysh[bot] marked this conversation as resolved.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Found 3 issues:

1. Function with high complexity (count = 5): build [qlty:function-complexity]


2. Assignment Branch Condition size for build is too high. [<6, 18, 4> 19.39/17] [rubocop:Metrics/AbcSize]


3. Method has too many lines. [14/10] [rubocop:Metrics/MethodLength]

end

private
Expand Down
86 changes: 85 additions & 1 deletion test/models/semantic_query_builder_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,16 @@ def setup_mock_lambda(response_data)
assert_equal({ match_all: {} }, result)
end

test 'applies filters to blank query' do
params = { q: '', source_filter: ['aspace'] }
result = @builder.build(params)

# When query is blank but filters are specified, should return bool query with filter clause
assert result.key?(:bool)
assert result[:bool].key?(:filter)
assert result[:bool][:filter].present?
end

test 'builds semantic query from lambda response' do
query_text = 'hello world'
mock_response = {
Expand All @@ -57,7 +67,8 @@ def setup_mock_lambda(response_data)
should: [
{ rank_feature: { field: 'embedding_full_record.hello', boost: 6.94 } },
{ rank_feature: { field: 'embedding_full_record.world', boost: 3.42 } }
]
],
filter: []
}
}

Expand Down Expand Up @@ -114,4 +125,77 @@ def setup_mock_lambda(response_data)
@builder.build(params)
end
end

test 'preserves source_filter in semantic queries' do
query_text = 'test search'
mock_response = {
'query' => {
'bool' => {
'should' => [
{ 'rank_feature' => { 'field' => 'embedding_full_record.test', 'boost' => 5.0 } }
]
}
}
}

setup_mock_lambda(mock_response)

params = { q: query_text, source_filter: ['aspace'] }
result = @builder.build(params)

# Verify filter clause was added to the semantic query
assert_includes result[:bool].keys, :filter
assert result[:bool][:filter].present?

# Verify the filter contains the source filter
filter_terms = result[:bool][:filter].map { |f| f[:bool][:should].first[:term][:source] }.flatten
assert_includes filter_terms, 'aspace'
end

test 'preserves content_type_filter in semantic queries' do
query_text = 'test search'
mock_response = {
'query' => {
'bool' => {
'should' => [
{ 'rank_feature' => { 'field' => 'embedding_full_record.test', 'boost' => 5.0 } }
]
}
}
}

setup_mock_lambda(mock_response)

params = { q: query_text, content_type_filter: %w[article book] }
result = @builder.build(params)

# Verify filter clause was added to the semantic query
assert_includes result[:bool].keys, :filter
assert result[:bool][:filter].present?

# Verify the filter contains multiple content type filters
assert_equal 2, result[:bool][:filter].length
end

test 'applies empty filters array when no filters specified' do
query_text = 'test search'
mock_response = {
'query' => {
'bool' => {
'should' => [
{ 'rank_feature' => { 'field' => 'embedding_full_record.test', 'boost' => 5.0 } }
]
}
}
}

setup_mock_lambda(mock_response)

params = { q: query_text }
result = @builder.build(params)

# Verify filter clause exists but is empty array
assert_includes result[:bool].keys, :filter
assert_equal [], result[:bool][:filter]
end
end