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 AND and OR. 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 to AND, so tags:Internet tags:Computing means both, and returns 164 articles rather than the 571 in either.
  • OR makes 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. fq decides what is eligible. To lift results instead of removing them, use boost_fq, which takes the same syntax.
  • It does not create fields. A filter on a field you never uploaded matches nothing. custom_attributes.nope:1 returns 0, not an error.
  • It does not search text. Use q for that, and fq to 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.