Skip to content

Commit ffc5c66

Browse files
phernandezclaude
andcommitted
fix: handle parentheses properly in Boolean search queries
- Add _prepare_parenthetical_term() method to preserve parentheses for grouping - Add _needs_quoting() helper method for cleaner quoting logic - Fix parentheses being quoted incorrectly in Boolean expressions - Ensure queries like "(hello AND world) OR test" work correctly - Maintain proper quoting for hyphenated terms within parentheses Fixes failing tests: - test_boolean_operators_preserved - test_hyphenated_terms_with_boolean_operators 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 5ad0774 commit ffc5c66

1 file changed

Lines changed: 64 additions & 3 deletions

File tree

src/basic_memory/repository/search_repository.py

Lines changed: 64 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -146,12 +146,73 @@ def _prepare_boolean_query(self, query: str) -> str:
146146
if part in ['AND', 'OR', 'NOT']:
147147
processed_parts.append(part)
148148
else:
149-
# This is a search term - for Boolean queries, don't add prefix wildcards
150-
prepared_term = self._prepare_single_term(part, is_prefix=False)
151-
processed_parts.append(prepared_term)
149+
# Handle parentheses specially - they should be preserved for grouping
150+
if '(' in part or ')' in part:
151+
# Parse parenthetical expressions carefully
152+
processed_part = self._prepare_parenthetical_term(part)
153+
processed_parts.append(processed_part)
154+
else:
155+
# This is a search term - for Boolean queries, don't add prefix wildcards
156+
prepared_term = self._prepare_single_term(part, is_prefix=False)
157+
processed_parts.append(prepared_term)
152158

153159
return " ".join(processed_parts)
154160

161+
def _prepare_parenthetical_term(self, term: str) -> str:
162+
"""Prepare a term that contains parentheses, preserving the parentheses for grouping.
163+
164+
Args:
165+
term: A term that may contain parentheses like "(hello" or "world)" or "(hello OR world)"
166+
167+
Returns:
168+
A properly formatted term with parentheses preserved
169+
"""
170+
# Handle terms that start/end with parentheses but may contain quotable content
171+
result = ""
172+
i = 0
173+
while i < len(term):
174+
if term[i] in '()':
175+
# Preserve parentheses as-is
176+
result += term[i]
177+
i += 1
178+
else:
179+
# Find the next parenthesis or end of string
180+
start = i
181+
while i < len(term) and term[i] not in '()':
182+
i += 1
183+
184+
# Extract the content between parentheses
185+
content = term[start:i].strip()
186+
if content:
187+
# Only quote if it actually needs quoting (has hyphens, special chars, etc)
188+
# but don't quote if it's just simple words
189+
if self._needs_quoting(content):
190+
escaped_content = content.replace('"', '""')
191+
result += f'"{escaped_content}"'
192+
else:
193+
result += content
194+
195+
return result
196+
197+
def _needs_quoting(self, term: str) -> bool:
198+
"""Check if a term needs to be quoted for FTS5 safety.
199+
200+
Args:
201+
term: The term to check
202+
203+
Returns:
204+
True if the term should be quoted
205+
"""
206+
if not term or not term.strip():
207+
return False
208+
209+
# Characters that indicate we should quote (excluding parentheses which are valid syntax)
210+
needs_quoting_chars = [" ", ".", ":", ";", ",", "<", ">", "?", "/", "-", "'", '"',
211+
"[", "]", "{", "}", "+", "!", "@", "#", "$", "%", "^", "&",
212+
"=", "|", "\\", "~", "`"]
213+
214+
return any(c in term for c in needs_quoting_chars)
215+
155216
def _prepare_single_term(self, term: str, is_prefix: bool = True) -> str:
156217
"""Prepare a single search term (no Boolean operators).
157218

0 commit comments

Comments
 (0)