#7443·mem0

[pgvector] Boolean values in operator filters (eq/ne/in/nin, list shorthand) never match stored booleans

Author: jclin0311Created Sep 24, 2026Updated Sep 29, 2026
Labelsvector-store

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.

bash
docker run -d --name mem0-pg -e POSTGRES_PASSWORD=postgres -p 55432:5432 pgvector/pgvector:pg16
python
from 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.