diff --git a/src/memos/graph_dbs/neo4j.py b/src/memos/graph_dbs/neo4j.py index 5ba1f116c..c8a1f5144 100644 --- a/src/memos/graph_dbs/neo4j.py +++ b/src/memos/graph_dbs/neo4j.py @@ -1441,17 +1441,24 @@ def build_filter_condition(condition_dict: dict, param_counter: list) -> tuple[s f"{node_alias}.{key} {cypher_op} ${param_name}" ) elif op == "contains": - # Handle contains operator (for array fields like tags, sources) - param_name = f"filter_{key}_{op}_{param_counter[0]}" - param_counter[0] += 1 - params[param_name] = op_value - - # For array fields, check if element is in array - if key in ("tags", "sources"): - condition_parts.append(f"${param_name} IN {node_alias}.{key}") - else: - # For non-array fields, contains might not be applicable, but we'll treat it as IN for consistency - condition_parts.append(f"${param_name} IN {node_alias}.{key}") + # Handle contains operator (for array fields) + # Only supports array format: {"field": {"contains": ["value1", "value2"]}} + # Single string values are not supported, use array format instead: {"field": {"contains": ["value"]}} + if not isinstance(op_value, list): + raise ValueError( + f"contains operator only supports array format. " + f"Use {{'{key}': {{'contains': ['{op_value}']}}}} instead of {{'{key}': {{'contains': '{op_value}'}}}}" + ) + # Handle array of values: generate AND conditions for each value (all must be present) + and_conditions = [] + for item in op_value: + param_name = f"filter_{key}_{op}_{param_counter[0]}" + param_counter[0] += 1 + params[param_name] = item + # For array fields, check if element is in array + and_conditions.append(f"${param_name} IN {node_alias}.{key}") + if and_conditions: + condition_parts.append(f"({' AND '.join(and_conditions)})") elif op == "like": # Handle like operator (for fuzzy matching, similar to SQL LIKE '%value%') # Neo4j uses CONTAINS for string matching diff --git a/src/memos/graph_dbs/polardb.py b/src/memos/graph_dbs/polardb.py index bfde8c80c..27cd936db 100644 --- a/src/memos/graph_dbs/polardb.py +++ b/src/memos/graph_dbs/polardb.py @@ -3443,23 +3443,40 @@ def build_cypher_filter_condition(condition_dict: dict) -> str: condition_parts.append(f"n.{key} = {op_value}") elif op == "contains": # Handle contains operator (for array fields) + # Only supports array format: {"field": {"contains": ["value1", "value2"]}} + # Single string values are not supported, use array format instead: {"field": {"contains": ["value"]}} + if not isinstance(op_value, list): + raise ValueError( + f"contains operator only supports array format. " + f"Use {{'{key}': {{'contains': ['{op_value}']}}}} instead of {{'{key}': {{'contains': '{op_value}'}}}}" + ) # Check if key starts with "info." prefix if key.startswith("info."): info_field = key[5:] # Remove "info." prefix - if isinstance(op_value, str): - escaped_value = escape_cypher_string(op_value) - condition_parts.append( - f"'{escaped_value}' IN n.info.{info_field}" - ) - else: - condition_parts.append(f"{op_value} IN n.info.{info_field}") + # Handle array of values: generate AND conditions for each value (all must be present) + and_conditions = [] + for item in op_value: + if isinstance(item, str): + escaped_value = escape_cypher_string(item) + and_conditions.append( + f"'{escaped_value}' IN n.info.{info_field}" + ) + else: + and_conditions.append(f"{item} IN n.info.{info_field}") + if and_conditions: + condition_parts.append(f"({' AND '.join(and_conditions)})") else: # Direct property access - if isinstance(op_value, str): - escaped_value = escape_cypher_string(op_value) - condition_parts.append(f"'{escaped_value}' IN n.{key}") - else: - condition_parts.append(f"{op_value} IN n.{key}") + # Handle array of values: generate AND conditions for each value (all must be present) + and_conditions = [] + for item in op_value: + if isinstance(item, str): + escaped_value = escape_cypher_string(item) + and_conditions.append(f"'{escaped_value}' IN n.{key}") + else: + and_conditions.append(f"{item} IN n.{key}") + if and_conditions: + condition_parts.append(f"({' AND '.join(and_conditions)})") elif op == "like": # Handle like operator (for fuzzy matching, similar to SQL LIKE '%value%') # Check if key starts with "info." prefix @@ -3668,29 +3685,46 @@ def build_filter_condition(condition_dict: dict) -> str: ) elif op == "contains": # Handle contains operator (for array fields) - use @> operator + # Only supports array format: {"field": {"contains": ["value1", "value2"]}} + # Single string values are not supported, use array format instead: {"field": {"contains": ["value"]}} + if not isinstance(op_value, list): + raise ValueError( + f"contains operator only supports array format. " + f"Use {{'{key}': {{'contains': ['{op_value}']}}}} instead of {{'{key}': {{'contains': '{op_value}'}}}}" + ) # Check if key starts with "info." prefix if key.startswith("info."): info_field = key[5:] # Remove "info." prefix - if isinstance(op_value, str): - escaped_value = escape_sql_string(op_value) - condition_parts.append( - f"ag_catalog.agtype_access_operator(VARIADIC ARRAY[properties, '\"info\"'::ag_catalog.agtype, '\"{info_field}\"'::ag_catalog.agtype]) @> '\"{escaped_value}\"'::agtype" - ) - else: - condition_parts.append( - f"ag_catalog.agtype_access_operator(VARIADIC ARRAY[properties, '\"info\"'::ag_catalog.agtype, '\"{info_field}\"'::ag_catalog.agtype]) @> {op_value}::agtype" - ) + # Handle array of values: generate AND conditions for each value (all must be present) + and_conditions = [] + for item in op_value: + if isinstance(item, str): + escaped_value = escape_sql_string(item) + and_conditions.append( + f"ag_catalog.agtype_access_operator(VARIADIC ARRAY[properties, '\"info\"'::ag_catalog.agtype, '\"{info_field}\"'::ag_catalog.agtype]) @> '\"{escaped_value}\"'::agtype" + ) + else: + and_conditions.append( + f"ag_catalog.agtype_access_operator(VARIADIC ARRAY[properties, '\"info\"'::ag_catalog.agtype, '\"{info_field}\"'::ag_catalog.agtype]) @> {item}::agtype" + ) + if and_conditions: + condition_parts.append(f"({' AND '.join(and_conditions)})") else: # Direct property access - if isinstance(op_value, str): - escaped_value = escape_sql_string(op_value) - condition_parts.append( - f"ag_catalog.agtype_access_operator(properties, '\"{key}\"'::agtype) @> '\"{escaped_value}\"'::agtype" - ) - else: - condition_parts.append( - f"ag_catalog.agtype_access_operator(properties, '\"{key}\"'::agtype) @> {op_value}::agtype" - ) + # Handle array of values: generate AND conditions for each value (all must be present) + and_conditions = [] + for item in op_value: + if isinstance(item, str): + escaped_value = escape_sql_string(item) + and_conditions.append( + f"ag_catalog.agtype_access_operator(properties, '\"{key}\"'::agtype) @> '\"{escaped_value}\"'::agtype" + ) + else: + and_conditions.append( + f"ag_catalog.agtype_access_operator(properties, '\"{key}\"'::agtype) @> {item}::agtype" + ) + if and_conditions: + condition_parts.append(f"({' AND '.join(and_conditions)})") elif op == "like": # Handle like operator (for fuzzy matching, similar to SQL LIKE '%value%') # Check if key starts with "info." prefix