Filter operators
This page defines the metadata filter language: the structure of a filter, every operator with the value types that the operator accepts, taxonomy filters, the treatment of missing values and the errors that a filter returns. Integrators use this page as the reference while writing filters. Metadata, filters and taxonomies explains metadata schemas and taxonomies.
Where filters apply
| Operation | Field | Form |
|---|---|---|
POST /pipelines/{id}/search | filters in the body | One filter |
POST /agents/{id}/search and POST /agents/{id}/execute | filters in the body | An object that maps each retrieval placeholder name to one filter. A placeholder without an entry is not filtered, and names that match no placeholder are ignored. |
GET /pipelines/{id}/documents | metadata query parameter | One filter as URL-encoded JSON |
Every filter is checked against the metadata schema of the pipeline. The MCP search tool accepts no filter.
Filter structure
A filter is a JSON object in one of two forms:
- Field conditions. An object that maps each metadata field name to one comparison. All conditions in the object must be satisfied. An empty object,
{}, applies no restriction. - Logical operator. An object with exactly one key,
$and,$oror$not, that combines other filters.
The following grammar summarizes the structure:
filter = field-map | logical
field-map = { field: comparison, ... }
logical = { "$and": [filter, ...] } | { "$or": [filter, ...] } | { "$not": filter }
comparison = { operator: value }
The following rules apply to every filter:
- One operator per field. A comparison object holds exactly one operator. A range on one field combines two conditions with
$and, as shown in Logical operators. - Explicit operators. A field always takes a comparison object. The shorthand
{"room_id": "GENERAL"}is invalid; the valid form is{"room_id": {"$eq": "GENERAL"}}. - Separate logical objects. A logical operator and field conditions cannot share one object. A filter that needs both places the field conditions inside the
$andarray. - Top-level fields. Filters compare top-level metadata fields only. Field names consist of letters, digits and underscores and do not start with a digit.
- Nesting.
$and,$orand$notnest to any depth. The code sets no limit on depth or on the number of conditions, and the request body limit bounds the size of a filter.
Comparison operators
| Operator | Meaning | Field types | Value |
|---|---|---|---|
$eq | Equal to | String, integer, number, boolean | One value |
$neq | Not equal to | String, integer, number, boolean | One value |
$gt | Greater than | String, integer, number, boolean | One value |
$ge | Greater than or equal to | String, integer, number, boolean | One value |
$lt | Less than | String, integer, number, boolean | One value |
$le | Less than or equal to | String, integer, number, boolean | One value |
$like | Matches a pattern, case-sensitive | String | A pattern string |
$ilike | Matches a pattern, case-insensitive | String | A pattern string |
$in | Equal to one of the values | String, integer, number, boolean | An array of values of one type |
$not-in | Equal to none of the values | String, integer, number, boolean | An array of values of one type |
$teq | Equal to a value or to any value beneath that value in a taxonomy | String | An array of two strings: the value and the taxonomy identifier |
The operator names are exact. $ne, $gte, $lte, $nin, $exists, $contains and $regex are not operators of the filter language.
The following filters show each comparison operator:
| Operator | Example |
|---|---|
$eq | {"room_id": {"$eq": "GENERAL"}} |
$neq | {"username": {"$neq": "system"}} |
$gt | {"priority": {"$gt": 2}} |
$ge | {"timestamp": {"$ge": "2026-09-01T00:00:00.000Z"}} |
$lt | {"rating": {"$lt": 0.5}} |
$le | {"timestamp": {"$le": "2026-09-15T00:00:00.000Z"}} |
$like | {"username": {"$like": "ali%"}} |
$ilike | {"title": {"$ilike": "%vpn%"}} |
$in | {"room_id": {"$in": ["GENERAL", "6f1c2a"]}} |
$not-in | {"username": {"$not-in": ["system", "rocket.cat"]}} |
$teq | {"region": {"$teq": ["Europe", "0b8d6f3e-5c1a-4e2b-9a7d-3f6e1c2b4a90"]}} |
Value types
The metadata schema of the pipeline declares the type of each field, and the value in a comparison must have that type. PostgreSQL converts the stored metadata value to the declared type and compares the converted values.
| Schema type | Accepted JSON values | Comparison |
|---|---|---|
string | Strings | Text comparison under the collation of the database, as described in Strings and dates |
integer | Integers | 64-bit integer comparison |
number | Integers and decimals | Double-precision comparison |
boolean | true and false | Boolean comparison, where false is less than true |
object | None | Fields of type object cannot be compared |
null, arrays and objects are not valid comparison values. Metadata fields of type array cannot be declared in a schema, so no operator tests whether an array contains a value.
The conversion applies to the stored values as well. An integer field accepts a whole number written with a decimal point, such as 5.0, when the document is submitted, but while a document stores such a value, every filter on that field returns HTTP 500 with the database error invalid input syntax for type bigint, for every document of the pipeline. A client application therefore sends whole numbers without a decimal point.
Missing values
A comparison is never satisfied by a document whose metadata lacks the field or holds null in the field. The rule applies to every comparison operator, including $neq and $not-in, and to $not around a single comparison. The filter {"$not": {"username": {"$eq": "system"}}} therefore excludes the documents of the system user and every document without a username field. The filter language has no operator that tests whether a field is present.
Two cases include documents that lack the field:
$notaround$and. When another condition of the$andfails, the$andfails, and$notis satisfied whatever the missing field. The filter{"$not": {"$and": [{"username": {"$eq": "system"}}, {"room_id": {"$eq": "GENERAL"}}]}}therefore includes a document without ausernamefield whoseroom_idis notGENERAL.- Empty list on a boolean field. An empty
$not-inlist on a boolean field matches every document, as described in Lists.
A pipeline that filters on a field therefore sets the field in the metadata of every document, with a default value where the source has none.
Strings and dates
The collation of the database decides the order of strings that differ in case or in punctuation. Under a linguistic collation, such as en_US.UTF-8, letters compare without regard to case first, so Beta is greater than b. Under the C collation, every uppercase letter sorts before every lowercase letter, so Beta is less than b. A client application that compares strings with $gt, $ge, $lt or $le stores the values in one case.
The filter language has no date type. Dates are stored as strings and compared as text, so date comparisons are chronological only when every stored value and every filter value use the same format. The format that Rocket.Chat uses serves as an example: Coordinated Universal Time (UTC) with milliseconds and the suffix Z, such as 2026-09-12T10:00:00.000Z. The metadata schema does not enforce a date format, so the client application that writes metadata applies the format.
Patterns
$like and $ilike use the pattern syntax of the SQL LIKE operator:
%. Matches any sequence of characters, including none._. Matches exactly one character.
A pattern without % or _ matches the whole value only. A backslash before % or _ matches the character itself, so the pattern 100\% matches the value 100% and not 100x. In JSON the backslash is doubled: {"label": {"$like": "100\\%"}}.
Lists
$in and $not-in take an array whose values all have one JSON type, matching the field type. An array that mixes types, such as ["a", 1], is invalid.
A client application that builds a list at query time leaves the condition out when the list is empty, because an empty array returns HTTP 400 on string, integer and number fields. On a boolean field, an empty $in list matches no document, and an empty $not-in list matches every document, including documents that lack the field.
Taxonomy filters
$teq matches a value and every value beneath that value in a taxonomy. The operator takes a two-element array: the value, as a string, and the identifier of the taxonomy.
{"region": {"$teq": ["Europe", "0b8d6f3e-5c1a-4e2b-9a7d-3f6e1c2b4a90"]}}
The following rules apply to taxonomy filters:
- Expansion. Foundation4 follows the taxonomy entries from the starting field and value to every descendant, at any depth and across fields, and matches documents that carry any of the values reached. Ancestors and siblings of the starting value do not match. Field names and values match exactly, including case.
- Schema. The starting field and every field reached by the expansion must be declared in the pipeline schema as strings.
- Current entries. The entries are read at search time, so changes to taxonomy entries apply to the next search without reprocessing any document.
- Shared links. Each link from a parent field and value to a child field and value is stored once across all taxonomies. A taxonomy that adds a link that another taxonomy already holds does not receive the link, and
$teqwith that taxonomy does not expand through the link. - Unknown taxonomy. An identifier that names no taxonomy, or a value without entries, returns no error. The filter then matches the starting value only, as
$eqdoes.
Logical operators
| Operator | Operand | Satisfied when |
|---|---|---|
$and | An array of filters | Every filter in the array is satisfied. An empty array is always satisfied. |
$or | An array of filters | At least one filter in the array is satisfied. An empty array is never satisfied. |
$not | One filter object | The filter is not satisfied, subject to Missing values |
Inside $and, an empty object, {}, applies no restriction. Inside $or, the result of an empty object depends on its position: in the first position the $or matches every fragment, and in any later position the empty object matches nothing. A client application removes empty objects from $and and $or arrays.
The following filter restricts results to two rooms and to messages sent from 1 September to 15 September 2026:
{
"$and": [
{"room_id": {"$in": ["GENERAL", "6f1c2a"]}},
{"timestamp": {"$ge": "2026-09-01T00:00:00.000Z"}},
{"timestamp": {"$le": "2026-09-15T23:59:59.999Z"}}
]
}
The following filter matches messages in the room GENERAL, or messages from the user alice in any room:
{
"$or": [
{"room_id": {"$eq": "GENERAL"}},
{"username": {"$eq": "alice"}}
]
}
Where the filter applies in a search
PostgreSQL applies the filter in the same query that selects the fragments, together with the classifications and the current-version condition, before the k limit. Full-text search therefore returns up to k fragments that satisfy the filter. Similarity and MMR search use an approximate vector index, and a search with a selective filter can return fewer than k fragments. Custom metadata indexes do not accelerate filters.
Errors
| Status | Body | Causes |
|---|---|---|
| 400 | Schema error | The filter names a field that the schema does not declare, compares a field of type object, compares a value of another type than the field, uses $like or $ilike on a field that is not a string, passes an empty array to $in or $not-in on a string, integer or number field, or reaches a field through $teq that is not declared as a string |
| 422 | Plain text that ends with data did not match any variant of untagged enum ExprOrValueExpr at line <line> column <column> | The filter does not follow the grammar: an unknown operator, two operators on one field, the shorthand form, null as a value, a mixed-type array, an invalid field name or a logical operator combined with field conditions |
| 400 | Plain text: Failed to deserialize query string: data did not match any variant of untagged enum ExprOrValueExpr | The metadata query parameter of the document list does not follow the grammar |
| 500 | Query Error followed by a database error message | The filter names a nested field, such as author.name, compares a value outside the 64-bit integer range, or reaches a stored value that cannot be converted to the type that the schema declares, such as 5.0 in an integer field |
Schema error carries no details, so the client application checks each cause in the list. In agent requests, schema errors and database errors return HTTP 500 with the message Internal error, and details.error names the cause. Errors describes the error format.
Query parameter filters
List operations such as GET /pipelines accept a separate filter form in the query string, field$op=value, which compares the columns of an object rather than metadata. That form has a separate set of operators, $contains, $startswith, $gt, $ge, $lt and $le, described in API conventions. The one exception is the metadata query parameter of the document list, which takes a filter in the language on this page:
curl -G "$FOUNDATION4_URL/pipelines/$PIPELINE_ID/documents" \
--data-urlencode 'metadata={"room_id": {"$eq": "GENERAL"}}' \
-H "x-api-key: $FOUNDATION4_API_KEY" \
-H "x-api-key-secret: $FOUNDATION4_API_SECRET"
Expected result: status 200 and a list of the documents whose metadata matches the filter. The pipeline's schema declares room_id as a string, as in the schema of Metadata, filters and taxonomies; on a pipeline without that field, the request returns HTTP 400 with Schema error.