Skip to content

Commit c878621

Browse files
authored
Merge pull request #968 from MITLibraries/use-593
Add filter support to semantic search
2 parents fb23979 + f3d1cea commit c878621

3 files changed

Lines changed: 116 additions & 5 deletions

File tree

app/models/hybrid_query_builder.rb

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,19 @@ def combine_queries(semantic_query, lexical_query)
4646
lexical_query
4747
end
4848

49+
# Remove filters from semantic branch since they'll be applied at top level
50+
# to avoid redundant filter clauses
51+
semantic_without_filters = if semantic_query.is_a?(Hash) && semantic_query[:bool]
52+
{
53+
bool: semantic_query[:bool].except(:filter)
54+
}
55+
else
56+
semantic_query
57+
end
58+
4959
hybrid_bool = {
5060
should: [
51-
semantic_query,
61+
semantic_without_filters,
5262
lexical_search
5363
]
5464
}

app/models/semantic_query_builder.rb

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,28 @@ class LambdaError < StandardError; end
55
def build(params, fulltext: false)
66
query_text = params[:q].to_s.strip
77

8-
# If no query text provided, return a match_all query (consistent with keyword search behavior)
9-
return { match_all: {} } if query_text.blank?
8+
# If no query text provided, return a match_all query with filters applied
9+
# (consistent with keyword search behavior and enabling tab filtering with empty search)
10+
if query_text.blank?
11+
filters = FilterBuilder.new.build(params)
12+
return { bool: { filter: filters } } if filters.present?
13+
14+
return { match_all: {} }
15+
end
1016

1117
lambda_response = invoke_semantic_builder(query_text)
12-
parse_lambda_response(lambda_response)
18+
semantic_query = parse_lambda_response(lambda_response)
19+
20+
# Validate the query structure has a bool clause before applying filters
21+
unless semantic_query.is_a?(Hash) && semantic_query[:bool].is_a?(Hash)
22+
raise "Invalid semantic query structure: expected bool clause, got #{semantic_query.inspect}"
23+
end
24+
25+
# Apply filters to the semantic query (matching LexicalQueryBuilder pattern)
26+
filters = FilterBuilder.new.build(params)
27+
semantic_query[:bool][:filter] = filters
28+
29+
semantic_query
1330
end
1431

1532
private

test/models/semantic_query_builder_test.rb

Lines changed: 85 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,16 @@ def setup_mock_lambda(response_data)
3434
assert_equal({ match_all: {} }, result)
3535
end
3636

37+
test 'applies filters to blank query' do
38+
params = { q: '', source_filter: ['aspace'] }
39+
result = @builder.build(params)
40+
41+
# When query is blank but filters are specified, should return bool query with filter clause
42+
assert result.key?(:bool)
43+
assert result[:bool].key?(:filter)
44+
assert result[:bool][:filter].present?
45+
end
46+
3747
test 'builds semantic query from lambda response' do
3848
query_text = 'hello world'
3949
mock_response = {
@@ -57,7 +67,8 @@ def setup_mock_lambda(response_data)
5767
should: [
5868
{ rank_feature: { field: 'embedding_full_record.hello', boost: 6.94 } },
5969
{ rank_feature: { field: 'embedding_full_record.world', boost: 3.42 } }
60-
]
70+
],
71+
filter: []
6172
}
6273
}
6374

@@ -114,4 +125,77 @@ def setup_mock_lambda(response_data)
114125
@builder.build(params)
115126
end
116127
end
128+
129+
test 'preserves source_filter in semantic queries' do
130+
query_text = 'test search'
131+
mock_response = {
132+
'query' => {
133+
'bool' => {
134+
'should' => [
135+
{ 'rank_feature' => { 'field' => 'embedding_full_record.test', 'boost' => 5.0 } }
136+
]
137+
}
138+
}
139+
}
140+
141+
setup_mock_lambda(mock_response)
142+
143+
params = { q: query_text, source_filter: ['aspace'] }
144+
result = @builder.build(params)
145+
146+
# Verify filter clause was added to the semantic query
147+
assert_includes result[:bool].keys, :filter
148+
assert result[:bool][:filter].present?
149+
150+
# Verify the filter contains the source filter
151+
filter_terms = result[:bool][:filter].map { |f| f[:bool][:should].first[:term][:source] }.flatten
152+
assert_includes filter_terms, 'aspace'
153+
end
154+
155+
test 'preserves content_type_filter in semantic queries' do
156+
query_text = 'test search'
157+
mock_response = {
158+
'query' => {
159+
'bool' => {
160+
'should' => [
161+
{ 'rank_feature' => { 'field' => 'embedding_full_record.test', 'boost' => 5.0 } }
162+
]
163+
}
164+
}
165+
}
166+
167+
setup_mock_lambda(mock_response)
168+
169+
params = { q: query_text, content_type_filter: %w[article book] }
170+
result = @builder.build(params)
171+
172+
# Verify filter clause was added to the semantic query
173+
assert_includes result[:bool].keys, :filter
174+
assert result[:bool][:filter].present?
175+
176+
# Verify the filter contains multiple content type filters
177+
assert_equal 2, result[:bool][:filter].length
178+
end
179+
180+
test 'applies empty filters array when no filters specified' do
181+
query_text = 'test search'
182+
mock_response = {
183+
'query' => {
184+
'bool' => {
185+
'should' => [
186+
{ 'rank_feature' => { 'field' => 'embedding_full_record.test', 'boost' => 5.0 } }
187+
]
188+
}
189+
}
190+
}
191+
192+
setup_mock_lambda(mock_response)
193+
194+
params = { q: query_text }
195+
result = @builder.build(params)
196+
197+
# Verify filter clause exists but is empty array
198+
assert_includes result[:bool].keys, :filter
199+
assert_equal [], result[:bool][:filter]
200+
end
117201
end

0 commit comments

Comments
 (0)