The rejected title.
The parsed query tree.
Whether per-word fuzzy presence counts.
The phrases responsible for the drop.
const { ast } = parseSearchQuery("sodium AND NOT borohydride");
explainDrop("Potassium Chloride", ast, true); // => ["sodium"]
explainDrop("Sodium Borohydride 98%", ast, true); // => ["NOT borohydride"]
export function explainDrop(title: string, ast: SearchAst, fuzzyWords: boolean): string[] {
switch (ast.type) {
case 'term':
return isPhrasePresent(title, ast.value, fuzzyWords) ? [] : [ast.value];
case 'and':
return [
...explainDrop(title, ast.left, fuzzyWords),
...explainDrop(title, ast.right, fuzzyWords),
];
case 'or': {
const left = explainDrop(title, ast.left, fuzzyWords);
const right = explainDrop(title, ast.right, fuzzyWords);
// An OR only fails when both branches fail; if either matched, nothing is missing.
return left.length > 0 && right.length > 0 ? [...left, ...right] : [];
}
case 'not':
// The NOT held (operand absent), so it isn't the reason; otherwise it is.
return matchesAst(title, ast.operand, fuzzyWords) ? [`NOT ${renderAst(ast.operand)}`] : [];
default:
return [];
}
}
Explains why a title failed an AST predicate: missing phrases are listed verbatim, and a
NOTbranch that excluded the title is reported asNOT <phrase>. Every drop gets at least one reason, so the dropped table is never blank.