fix(fts): fix multi-word field-restricted search query building

Fixes a critical bug in FTS query building where multi-word searches
with field restrictions incorrectly used OR between all word+field
combinations instead of requiring ALL words to match within at least
one field.

Example: searching "cute cat" in {title, tags} previously produced:
  title:cute* OR title:cat* OR tags:cute* OR tags:cat*
Which matched recipes with ANY word in ANY field.

Now produces:
  (title:cute* title:cat*) OR (tags:cute* tags:cat*)
Which requires ALL words to match within at least one field.

Also adds fallback to fuzzy search when FTS returns empty results,
improving search reliability.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Will Miao
2026-01-22 10:25:03 +08:00
parent 9150718edb
commit 17c5583297
3 changed files with 86 additions and 7 deletions

View File

@@ -509,21 +509,20 @@ class RecipeFTSIndex:
if not fields:
return term_expr
# Build field-restricted query with OR between fields
# Build field-restricted query where ALL words must match within at least one field
field_clauses = []
for field in fields:
if field in self.FIELD_MAP:
cols = self.FIELD_MAP[field]
for col in cols:
# FTS5 column filter syntax: column:term
# Need to handle multiple terms properly
for term in prefix_terms:
field_clauses.append(f'{col}:{term}')
# Create clause where ALL terms must match in this column (implicit AND)
col_terms = [f'{col}:{term}' for term in prefix_terms]
field_clauses.append('(' + ' '.join(col_terms) + ')')
if not field_clauses:
return term_expr
# Combine field clauses with OR
# Any field matching all terms is acceptable (OR between field clauses)
return ' OR '.join(field_clauses)
def _escape_fts_query(self, text: str) -> str:

View File

@@ -632,7 +632,12 @@ class RecipeScanner:
fields = None
try:
return self._fts_index.search(search, fields)
result = self._fts_index.search(search, fields)
# Return None if empty to trigger fuzzy fallback
# Empty FTS results may indicate query syntax issues or need for fuzzy matching
if not result:
return None
return result
except Exception as exc:
logger.debug("FTS search failed, falling back to fuzzy search: %s", exc)
return None