Filter Syntax (fq)
fq restricts which products an endpoint can consider. It runs before ranking,
so it changes what is eligible, never the order of what is left.
Miso passes fq to Elasticsearch as a query string, so the syntax is Lucene's:
field:value, ranges in brackets, and the boolean operators. The same rule
works on every endpoint that takes fq — search, recommendation, hybrid
search, summaries, and answers.
Every count on this page comes from the same catalog of 20,901 news articles, so you can compare what each construct does.
Match a field
tags:Internet
The baseline query matches 2,359 articles. With that filter, 403.
Use the field name exactly as you uploaded it. A custom attribute uses its dotted path:
custom_attributes.source:wikinews
Quote any value with a space
This is the mistake that costs the most time. An unquoted space ends the value, so the rest of it is read as more query:
| Filter | Matches |
|---|---|
custom_attributes.license:"CC BY 2.5" |
2,359 |
custom_attributes.license:CC BY 2.5 |
0 |
tags:"Science and technology" |
1,807 |
tags:Science and technology |
0 |
Neither form is an error. The unquoted one returns nothing, quietly.
Values are case sensitive
A tag or a category is matched exactly, as stored:
| Filter | Matches |
|---|---|
tags:Computing |
332 |
tags:computing |
4 |
tags:COMPUTING |
0 |
The 4 in the middle row is the point, not an exception. Those 4 articles carry
the tag in lower case, and they are the only ones tags:computing can find.
The other 332 are invisible to it.
Text fields behave differently. title is analyzed, so title:software
matches 25 articles whatever the case of the word in the headline. Keyword
fields — tags, categories, custom attributes — need the exact value.
Match anything in a field
custom_attributes.word_count:*
That matches all 2,359, because every record carries it. Use it to find records that have a field at all.
A wildcard also works inside a value:
tags:Comput*
339 articles, against 332 for the exact tag.
Match a range
The bracket decides whether the bound itself is included.
| Form | Bounds | Meaning |
|---|---|---|
[100 TO 200] |
both included | 100 and 200 match |
{100 TO 200} |
both excluded | 101 to 199 match |
[100 TO 200} |
low included, high excluded | 100 matches, 200 does not |
{100 TO 200] |
low excluded, high included | 100 does not match, 200 does |
The catalog holds 7 articles of exactly 150 words and 9 of exactly 250. That is enough to see each bracket work:
| Filter | Matches | |
|---|---|---|
custom_attributes.word_count:[150 TO 250] |
702 | |
custom_attributes.word_count:{150 TO 250] |
695 | 702 − 7 |
custom_attributes.word_count:[150 TO 250} |
693 | 702 − 9 |
custom_attributes.word_count:{150 TO 250} |
686 | 702 − 7 − 9 |
Use * for an open end, and mix the brackets as you need:
custom_attributes.word_count:[150 TO *] // 2,123, includes 150
custom_attributes.word_count:{150 TO *] // 2,116, excludes 150
Comparison operators do the same job, and follow the same rule:
| Operator | Same as | Matches |
|---|---|---|
custom_attributes.word_count:>=150 |
[150 TO *] |
2,123 |
custom_attributes.word_count:>150 |
{150 TO *] |
2,116 |
Dates take the same brackets, in ISO-8601:
published_at:[2005-01-01T00:00:00Z TO 2005-12-31T23:59:59Z] // 332
published_at:[2020-01-01T00:00:00Z TO *] // 78
Group with parentheses
CAUTION: Put parentheses around every mix of
ANDandOR. Without them, the filter runs, returns a result, and quietly ignores part of your condition.
This is Lucene's boolean rule, and it surprises most people:
-
AND, and a plain space, make a clause required. Miso sets the default operator toAND, sotags:Internet tags:Computingmeans both, and returns 164 articles rather than the 571 in either. ORmakes the clauses it joins optional.- When a query holds any required clause, the optional ones stop restricting the result. They only change the score, and a filter throws the score away.
- When a query holds no required clause, at least one optional clause must match.
Compare three filters. A is tags:Internet (403 articles), B is
tags:Computing (332), and C is custom_attributes.word_count:[300 TO *]
(1,147):
| Filter | Matches | Why |
|---|---|---|
A OR B AND C |
132 | The OR makes A and B optional, then the AND makes B and C required. A stays optional, so it no longer restricts. |
(A OR B) AND C |
253 | The brackets make the pair one required clause. |
A OR (B AND C) |
465 | Two optional clauses, so either side can match. |
B AND C on its own also returns 132, which is what the first row really ran.
It reports no error.
NOT follows the same rule:
| Filter | Matches | Why |
|---|---|---|
NOT A OR B |
168 | No required clause, so B must match, and A must not. |
NOT (A OR B) |
1,788 | Neither tag. |
(NOT A) OR B |
2,120 | Not A, plus all of B. |
The last figure checks out: 1,956 articles are not tagged Internet, 332 are tagged Computing, and 168 of those are in both sets. 1,956 + 332 − 168 = 2,120.
Test one field against several values
Brackets after a field name are shorter, and mean the same thing:
tags:(Internet OR Computing)
tags:Internet OR tags:Computing
Parentheses nest, so build the condition you mean and keep it explicit:
((tags:Internet OR tags:Computing) AND custom_attributes.word_count:[300 TO *])
AND NOT custom_attributes.premium:"true"
What a filter cannot do
-
It does not rank.
fqdecides what is eligible. To lift results instead of removing them, useboost_fq, which takes the same syntax. -
It does not create fields. A filter on a field you never uploaded matches
nothing.
custom_attributes.nope:1returns 0, not an error. - It does not search text. Use
qfor that, andfqto narrow it.
Errors
A filter that does not parse returns 422, and the message names fq:
{
"detail": [
{
"loc": ["body", "fq"],
"msg": "Syntax error in input : unexpected end of expression (maybe due to unmatched parenthesis) at the end!",
"type": "value_error.parsesyntax"
}
]
}
An empty fq is legal. It filters nothing.
Two failures are silent, and both return 0 instead of an error: an unquoted
value with a space, and a value with the wrong case. When a filter returns
nothing, test those two first.
Common filters
custom_attributes.premium:"false"
published_at:[2026-01-01T00:00:00Z TO *]
custom_attributes.section:markets AND custom_attributes.word_count:[800 TO *]
NOT tags:Sponsored
Where to use it
| Endpoint | What fq limits |
|---|---|
POST /v1/ask/questions |
The passages an answer can cite. |
POST /v1/ask/search |
The results, and the passages behind the answer. |
POST /v1/ask/summary |
The articles the summary reads. |
POST /v1/search/search |
The search results. |
POST /v1/recommendation/* |
The candidates for a unit. |
See Search & Recommendation and Product Schema.
