Filter Expressions

The hs_py.filter module parses and evaluates Haystack filter strings — the query language used to search for records by their tags.

See also

Filter for the full filter API reference (AST nodes, lexer, parser, evaluator).

Filter Syntax

Haystack filters support tag presence checks, comparisons, boolean logic, and dotted path traversal:

Filter Syntax

Expression

Meaning

site

Records that have the site marker tag

not site

Records that do not have the site marker tag

curVal == 72

Records where curVal equals 72

curVal > 72

Records where curVal is greater than 72

curVal >= 72

Greater than or equal

curVal < 72

Less than

curVal <= 72

Less than or equal

curVal != 72

Not equal

point and sensor

Logical AND

site or equip

Logical OR

point and not hidden

Combined logic

equipRef->dis

Path traversal through a Ref

equipRef->siteRef->dis == "HQ"

Multi-hop path traversal

Operator precedence: not > and > or. Use parentheses to override.

Parsing

parse() converts a filter string into an AST. Results are cached (LRU, 256 entries) for repeated filter expressions. Invalid filter strings raise ParseError:

from hs_py import parse
from hs_py.filter.parser import ParseError

ast = parse("point and sensor and curVal > 72")

# Invalid filters raise ParseError
try:
    parse("and or not ===")
except ParseError as e:
    print(f"Bad filter: {e}")

# Successful parse:
print(ast)
# And(And(Has(Path('point')), Has(Path('sensor'))),
#     Cmp(Path('curVal'), CmpOp.GT, 72))

AST Node Types

The hs_py.filter.ast module defines the AST:

Node

Description

Has

Tag exists (marker check)

Missing

Tag does not exist

Cmp

Comparison (==, !=, <, <=, >, >=)

And

Logical AND

Or

Logical OR

Path

Dotted path (e.g. equipRef->siteRef->dis)

Evaluating Against Dicts

evaluate() tests a single dict against a parsed filter:

from hs_py import parse, evaluate, MARKER, Number

f = parse("point and sensor and curVal > 72")

rec1 = {"point": MARKER, "sensor": MARKER, "curVal": Number(75)}
rec2 = {"point": MARKER, "sensor": MARKER, "curVal": Number(68)}
rec3 = {"point": MARKER, "equip": MARKER}

assert evaluate(f, rec1) is True
assert evaluate(f, rec2) is False
assert evaluate(f, rec3) is False

Filtering Grids

evaluate_grid() returns a new grid containing only the matching rows:

from hs_py import parse, evaluate_grid

filtered = evaluate_grid(parse("point and curVal > 70"), grid)
print(f"Matched {len(filtered)} of {len(grid)} rows")

Path Traversal

Haystack filters support -> path traversal for following Ref-valued tags to related records. For example, equipRef->dis means “follow the equipRef tag to the referenced record, then read its dis tag.”

When evaluating against a grid, the evaluator automatically builds a resolver from the grid’s id column:

from hs_py import Grid, Col, Ref, MARKER, parse, evaluate_grid

grid = Grid(
    cols=(Col("id", {}), Col("dis", {}), Col("site", {}),
          Col("equipRef", {}), Col("point", {})),
    rows=(
        {"id": Ref("s1"), "dis": "HQ", "site": MARKER},
        {"id": Ref("e1"), "dis": "AHU-1", "equipRef": Ref("s1")},
        {"id": Ref("p1"), "dis": "Temp", "point": MARKER, "equipRef": Ref("e1")},
    ),
)

# Find points whose equip's dis is "AHU-1"
result = evaluate_grid(parse('equipRef->dis == "AHU-1"'), grid)

When evaluating against a plain dict, provide a custom resolver function:

from hs_py import parse, evaluate, Ref

db = {
    "s1": {"id": Ref("s1"), "dis": "HQ"},
    "e1": {"id": Ref("e1"), "dis": "AHU-1", "siteRef": Ref("s1")},
}

def resolver(ref_val: str) -> dict | None:
    return db.get(ref_val)

f = parse('siteRef->dis == "HQ"')
assert evaluate(f, db["e1"], resolver=resolver)

Supported Literals

Filters support these literal types in comparisons:

Strings:     "hello"
Numbers:     72.5  or  72.5°F  or  -3.14
Booleans:    true  false
Refs:        @p:demo:r:1
URIs:        `http://example.com`
Dates:       2026-02-16
Times:       14:30:00
DateTimes:   2026-02-16T14:30:00-05:00 New_York
Symbols:     ^site

Performance

The parser caches up to 256 parsed ASTs. If your application uses a fixed set of filters, they will be parsed only once:

# These two calls return the same cached AST:
f1 = parse("point and sensor")
f2 = parse("point and sensor")
assert f1 is f2