[pgvector] Boolean values in operator filters (eq/ne/in/nin, list shorthand) never match stored booleans
Component
Vector Store
Description
In the Python pgvector store, _build_filter_conditions converts filter values with str(). For booleans that gives "True" / "False", but payload->>'key' returns the JSON text "true" / "false", so the comparison never matches.
#5264 fixed this for the plain scalar form ({"archived": False}), but the other forms still go through str():
| filter | expected | actual |
|---|---|---|
{"archived": False} |
only the non-archived memory | correct |
{"archived": {"eq": False}} |
only the non-archived memory | nothing |
{"archived": {"in": [False]}} |
only the non-archived memory | nothing |
{"archived": [False]} |
only the non-archived memory | nothing |
{"archived": {"ne": True}} |
only the non-archived memory | both memories, archived one included |
{"archived": {"nin": [True]}} |
only the non-archived memory | both memories, archived one included |
The ne / nin cases are the worst: they don't come back empty, they quietly return the records the caller is trying to exclude.
Memory.search() / get_all() pass filters through _process_metadata_filters unchanged, so ordinary SDK calls reach this code.
The TS pgvector store is not affected, because String(false) in JS is already "false".
Reproduction
Python 3.12, mem0 main @ 47a69e1e, pgvector/pgvector:pg16, psycopg 3, Ollama bge-m3. infer=False, so no LLM calls are made.
docker run -d --name mem0-pg -e POSTGRES_PASSWORD=postgres -p 55432:5432 pgvector/pgvector:pg16from mem0 import Memory
m = Memory.from_config({
"embedder": {"provider": "ollama", "config": {"model": "bge-m3", "embedding_dims": 1024}},
"llm": {"provider": "ollama", "config": {"model": "qwen2.5:1.5b"}},
"vector_store": {"provider": "pgvector", "config": {
"dbname": "postgres", "user": "postgres", "password": "postgres",
"host": "localhost", "port": 55432,
"collection_name": "repro_bool_e2e", "embedding_model_dims": 1024}},
})
m.reset()
m.add("I prefer window seats on flights", user_id="alice", metadata={"archived": False}, infer=False)
m.add("I used to live in Boston", user_id="alice", metadata={"archived": True}, infer=False)
for f in [{"archived": False}, {"archived": {"eq": False}}, {"archived": {"ne": True}}, {"archived": {"in": [False]}}]:
res = m.search("seat preference", filters={"user_id": "alice", **f}, top_k=5)
print(f, [r["memory"] for r in res["results"]])Output:
{'archived': False} ['I prefer window seats on flights']
{'archived': {'eq': False}} []
{'archived': {'ne': True}} ['I prefer window seats on flights', 'I used to live in Boston']
{'archived': {'in': [False]}} []Proposed fix
Extract the existing scalar-path logic (json.dumps for bool, str() otherwise) into a small helper and use it for the eq/ne, in/nin and list-shorthand paths as well. Numeric (gt/lt…) and contains paths are unchanged. It's about a 20-line change plus unit tests next to the existing test_boolean_*_uses_json_casing tests. I have it working locally: the new tests fail on main and pass with the change, and the repro above returns the expected rows for every form.
Happy to open a PR if this approach sounds right.
How You Verified This
Ran the script above against a local pgvector container and got the output shown. I also called PGVector.list() / search() directly with the same filters and got the same results.
AI Assistance
AI helped me find it, and I reproduced it myself afterwards
AI disclosure: an AI coding agent (Claude Code) found this while exercising the pgvector filters, and it drafted the reproduction and the proposed fix. I re-ran the reproduction myself on main and on the fix branch, reviewed the diff against the contributing guidelines, and confirmed that the new tests fail on main and pass with the fix.
Source: mem0ai/mem0