Selector Syntax¶
IFClite reads the IfcOpenShell selector (filter) syntax
— the one-line form used by Bonsai and by ifcopenshell.util.selector — and turns it
into filter rules.
Type it into the Selector field at the top of the viewer's Filter tab, or parse it
yourself with parseSelector from @ifc-lite/query.
Not every construct has a rule behind it yet. The ones that do not are listed back to you by name; nothing is dropped in silence. What is missing is tracked in issue #4094.
The grammar¶
selector := group ("+" group)* groups are unioned
group := filter ("," filter)* filters narrow left to right
| Filter | Example | Means |
|---|---|---|
| Class | IfcWall |
the class and all its subclasses |
| Class, subtracted | ! IfcWall |
remove that class and its subclasses |
| GlobalId | 325Q7Fhnf67OZC$$r43uzK |
one element, by GlobalId |
| Attribute | Name=D01 |
an IFC attribute of the element |
| Property | Pset_WallCommon.FireRating=2HR |
a property in a property set |
| Type | type=WT01 |
the relating type's Name |
| Material | material=concrete |
a material Name or Category |
| Classification | classification=Pr_25 |
a classification reference |
| Location | location="Level 3" |
the spatial element containing it |
| Parent | parent=Foo |
a descendant of the element named Foo |
| Value query | query:types.count=0 |
a value-query key path |
Operators: =, !=, >, >=, <, <=, *= (contains), !*= (does not contain).
Values, and property-set and property names, come in three spellings:
| Spelling | Example | Notes |
|---|---|---|
| bare | concrete |
no spaces, no , + = ! < > * " / |
| quoted | "Level 3" |
\" and \\ escape inside |
| regular expression | /D[0-9]{2}/ |
unanchored, case-sensitive |
Quoting is what forces a literal: "/Wall/".FireRating looks for a property
set actually named /Wall/, not a pattern. It works in either position, so
Pset_BeamCommon."IsExternal" and /Pset_.*Common/."IsExternal" are both valid.
NULL (any case, unquoted) is the null literal, so FireRating != NULL means "has a
FireRating". TRUE and FALSE are compared as the strings IFC property sets render
them (True / False), case-insensitively.
There is no * wildcard. IfcWall* is a syntax error, not a match-nothing: *
exists only as part of *=. Use a regular expression for wildcards.
What works in the viewer today¶
| Construct | Viewer Filter tab | Notes |
|---|---|---|
IfcWall, several classes, ! subtraction |
✅ | subclasses included, per the model's schema |
Name=, !=, *=, !*= |
✅ | case-insensitive |
Name=/regex/ |
✅ | case-sensitive |
PredefinedType=, != |
✅ | no regex |
Pset.Prop with all eight operators |
✅ | |
Pset.Prop = NULL / != NULL |
✅ | becomes "is not set" / "is set" |
/Pset_.*Common/.Prop regex set or property name |
✅ | one rule reaches several sets |
Qto_….Quantity > 10 |
⚠️ | only a Qto_ set, and only a numeric comparison — see below |
material= |
⚠️ | matches material names; Category is not read yet |
classification=, = NULL, != NULL |
✅ | matches the code or the name |
location="Level 3" |
⚠️ | see below |
GlobalId terms, ! <GlobalId> |
✅ | several terms union (add) or subtract, mirroring class terms |
Description=, ObjectType=, Tag=, any other schema attribute |
✅ | all eight operators, = NULL / != NULL as presence — see below |
type=WT01 |
✅ | matches the relating type's Name; =, !=, *=, !*= and /regex/, like Name= — no >, >=, <, <= |
parent=, query: |
❌ | reported, not applied |
+ unions of groups |
❌ | the first group is applied, the rest reported |
Which quantities a selector can reach¶
Quantities are a separate table from property sets, and the two rules do not read each other's rows. A term reaches the quantity table only when both halves hold:
- the set name starts with
Qto_(case-sensitive), or is a pattern whoseQto_opens it or opens one of its alternatives —/^Qto_.*/,/(Qto_Wall|Qto_Slab)BaseQuantities/; - the comparison is
=,!=,>,>=,<or<=against a number.
A Qto_ term failing the second half is reported, not applied:
Qto_WallBaseQuantities.NetVolume = NULL, …NetVolume *= 1 and
…Note = draft all come back named rather than silently run against property
sets, where they would find nothing (= NULL was worse still — "is not set"
against a set no property row carries matched every element).
Quantities written under a set with no Qto_ prefix are not reachable from a
selector today: Revit's IFC2x3 export writes BaseQuantities and ArchiCAD writes
ArchiCADQuantities, so BaseQuantities.NetVolume > 1 becomes a property rule and
finds nothing. Reading quantity rows from a property term is part of #4094.
Generic attribute terms, and the one that stays reported¶
Description=, ObjectType=, Tag=, LongName=, or any other name the IFC schema
declares as an attribute becomes an attribute rule, read from the same on-demand
per-entity extraction the IDS attribute facet uses. All eight operators work, and
= NULL / != NULL read as "is not set" / "is set", the same as a property term.
GlobalId= (the comparison spelling, not the bare-GlobalId term) is the one
exception: the underlying extraction skips GlobalId as a structural/display
attribute, so routing it through the generic attribute rule would silently match
nothing. It stays reported; use a bare GlobalId term (325Q7Fhnf67OZC$$r43uzK)
instead, which IS supported — see the grammar table above.
How far location= reaches¶
location="Level 3" becomes a storey-name rule, and that rule matches an element the
storey contains directly, plus the parts aggregated under such an element.
It does not reach an element one level further down, inside an IfcSpace on that
storey. So IfcOpenShell's example IfcPump, location="Level 3" — a pump in a room on
Level 3 — finds the pump in IfcOpenShell and not in IFClite. This is measured, not
assumed: see storey rule reach in apps/viewer/src/lib/search/filter-evaluate.test.ts.
A spatial-ancestor rule is part of #4094.
Where else can I filter?¶
Only the viewer's Filter tab accepts selector text today. The other surfaces have their own structured filters, and this is how the same intent is spelled in each:
| Selector | CLI | MCP query_entities |
SDK / sandbox |
|---|---|---|---|
IfcWall, IfcSlab |
--type IfcWall,IfcSlab |
types: ['IfcWall','IfcSlab'] |
bim.query().byType('IfcWall') |
Pset_WallCommon.FireRating=2HR |
--where "Pset_WallCommon.FireRating=2HR" |
property: { pset, name, op: '=', value } |
.where('Pset_WallCommon','FireRating','=','2HR') |
…FireRating*=REI |
--where "Pset_WallCommon.FireRating~REI" |
op: 'contains' |
.where(…, 'contains', 'REI') |
…FireRating != NULL |
--where "Pset_WallCommon.FireRating" |
op: 'exists' |
— |
/Pset_.*Common/.FireRating |
— | — | bim.query.property(entity, '/Pset_.*Common/', 'FireRating') |
IfcWall* (a wildcard) |
--a "IfcWall*" (clash selectors only) |
— | — |
Note the last row: the clash rule selectors (ifc-lite clash --a "IfcDuct*|IfcPipe*") are
a different, older mini-language with a suffix * glob. That grammar is unchanged and
is not the one on this page.
Accepting selector text in the CLI, MCP and the scripting API is tracked in #4094; the
parser already lives in @ifc-lite/query, so those surfaces adapt the same AST rather
than growing a second grammar.
Parsing it yourself¶
import { parseSelector } from '@ifc-lite/query';
const result = parseSelector('IfcWall, Pset_WallCommon.FireRating=/REI.*/');
if (result.ok === false) {
console.error(`${result.error.message} at character ${result.error.offset + 1}`);
} else {
for (const group of result.query.groups) {
for (const filter of group.filters) {
console.log(filter.kind, filter.text);
}
}
}
parseSelector reads the whole grammar, including the constructs no surface can
evaluate yet. That is deliberate: a caller adapts the AST onto its own filter model and
reports what it could not carry, instead of matching nothing and saying nothing.
See also¶
- Querying Data — the fluent API, SQL, and the property lookups behind these rules
- CLI Toolkit —
--typeand--where - Clash Detection — the separate
IfcDuct*|IfcPipe*selector language