Filtering
Narrow results by metadata, dates, and identifiers.
Operators
| Operator | Description | Example value |
|---|---|---|
eq | Equals | "research" |
ne | Not equals | "deleted" |
in | Matches any in list | ["tech", "science"] |
not_in | Excludes values in list | ["spam", "junk"] |
gt | Greater than | 2020 |
gte | Greater than or equal | 2020 |
lt | Less than | 2025 |
lte | Less than or equal | 2025 |
between | Range (inclusive) | [2020, 2025] |
Building filters in the SDKs
The SDKs ship a builder, F, that produces the structured DSL below without hand-writing JSON. Pick a field, apply one operator, combine with and, or, not. The result goes straight into the filters argument of search and assisted_search. A raw object in either format is accepted in the same place if you already have one.
from redpine import F
recent = F("publication_date").between("2020-01-01", "2025-12-31")
by_journal = F("issn").in_(["1234-5679", "2345-6787"]) | F("publisher").eq("Example Publishing")
not_one = ~F("issn").eq("2345-6787")
high_impact = F("journal_metric.2yr_mean_citedness").gte(5)
r = client.search("crispr delivery", collection="my-collection", filters=recent & by_journal & not_one)Operators map one to one onto the table above: eq, ne, in, not_in, gt, gte, lt, lte, between. Python spells them in_ and not_in, TypeScript in and notIn, Go In and NotIn. Combining two and filters flattens into one and list rather than nesting, so the JSON sent is the compact form shown in the sections below.
Simple format
Key-value pairs where each key is a metadata field name. Supports exact match, list membership, range operators, and negation. An object value takes only range keys (gt, gte, lt, lte) or not.
Don't mix the two formats: a filter with and, or or not beside plain field keys is rejected, because the field keys would be ignored. Put them inside the list as {"field": ..., "eq": ...} conditions.
Exact match
{
"filters": {
"journal": "Nature"
}
}Range
{
"filters": {
"publication_date": { "gte": "2020-01-01", "lte": "2025-12-31" }
}
}Any-of list
Pass an array to match any value in the list (equivalent to in).
{
"filters": {
"issn": ["1234-5679", "2345-6787"]
}
}Negation
{
"filters": {
"publisher": { "not": "Example Publishing" }
}
}Structured DSL
Use boolean combinators (and, or, not) with explicit field conditions for complex filtering logic. Supports arbitrary nesting.
Each condition takes one operator. The range bounds gt, gte, lt and lte are the exception and combine in one condition ({"field": "publication_date", "gte": "2020-01-01", "lte": "2025-12-31"}); between does not combine with gte or lte. Any other pair, such as gte with ne, is rejected: put each in its own condition under and.
Basic AND condition
{
"filters": {
"and": [
{ "field": "publication_date", "gte": "2020-01-01" },
{ "field": "publisher", "eq": "Example Publishing" }
]
}
}OR condition
{
"filters": {
"or": [
{ "field": "publisher", "eq": "tech" },
{ "field": "publisher", "eq": "science" }
]
}
}Nested combinators
Combinators can be nested to build complex expressions. This example matches documents published between 2020 and 2025 that are either from one of two specific ISSNs or from Example Publishing.
{
"filters": {
"and": [
{ "field": "publication_date", "gte": "2020-01-01" },
{ "field": "publication_date", "lte": "2025-12-31" },
{
"or": [
{ "field": "issn", "in": ["1234-5679", "2345-6787"] },
{ "field": "publisher", "eq": "Example Publishing" }
]
}
]
}
}NOT combinator
Use not to exclude results matching a condition. Like and and or, it takes an array of conditions: passing a bare object is a parse error.
{
"filters": {
"and": [
{ "field": "publisher", "eq": "Example Publishing" },
{
"not": [
{ "field": "issn", "eq": "2345-6787" }
]
}
]
}
}Filtering by journal and article identity
issn and doi are both indexed. Prefer issn over journal when you mean a specific journal: the same journal appears in the corpus under several title spellings ("Example Medical Journal", "Example Med J", "EXAMPLE MEDICAL JOURNAL"), while its ISSN does not change.
ISSN accepts hyphenated or bare, upper- or lower-case check character. All four forms select the same journal:
{ "filters": { "issn": "1234-561X" } }"1234-561X", "1234-561x", "1234561X" and "1234561x" are equivalent.
DOIs are matched case-insensitively, and a resolver prefix is optional:
{ "filters": { "doi": "10.1234/example.2020.001" } }"10.1234/EXAMPLE.2020.001", "https://doi.org/10.1234/example.2020.001" and "doi:10.1234/example.2020.001" are equivalent.
When a search that pins one or more DOIs returns nothing, filterWarnings can carry a doi_not_in_collection entry for each pinned DOI your key cannot reach in the collection, so you can tell a missing paper from a filter that is too narrow. A retracted paper counts as not available. The check is best effort: it covers up to ten DOIs and is left out if it cannot finish in time, so a DOI without a warning is not proof the collection holds it. A paper that is available but excluded by another condition, such as section, is not reported missing.
Exclusion uses the operators you already have: ne, not_in and not. There is no separate exclusion syntax. Documents carrying no ISSN at all are not removed by an issn exclusion.
{
"filters": {
"and": [
{ "field": "issn", "not_in": ["1234-5679", "2345-6787"] }
]
}
}Journal metrics
Filter by a journal-level citation metric. These fields accept range operators only (gt, gte, lt, lte, between) and are resolved server-side into the matching ISSNs, so they cost nothing extra at search time.
Available: journal_metric.2yr_mean_citedness, journal_metric.h_index, journal_metric.i10_index, journal_metric.impact_factor.
2yr_mean_citedness is not an impact factor. The value is OpenAlex's 2-year mean citedness, published under CC0. The Journal Impact Factor is Clarivate's proprietary metric, computed over the Web of Science corpus with a different citation window; the two are not interchangeable and the numbers will not agree.
OpenAlex scores a journal with no citations in the window 0, so lt and lte thresholds include such journals. A 0 can also mean OpenAlex indexes few or no works for that journal, so it is not evidence of coverage. To leave out high-impact journals, use not with gte (below). A not condition also keeps documents with no ISSN or no metric.
impact_factor is an impact factor computed by Redpine from OpenAlex citation data with the JIF formula. It is not Clarivate's Journal Impact Factor. The 2025 value is the citations received in 2025 by everything a journal published in 2023 and 2024, divided by the articles and reviews it published in those two years. Journals that publish many news items, letters and editorials (for example BMJ, JAMA, The Lancet) score far below their JCR figure, because OpenAlex classifies those items as articles.
A journal with fewer than 20 articles and reviews in those two years, or where they are under half of what it published, has no impact_factor rather than 0: no threshold matches it, and a not keeps it.
When a condition matches no journal, the response carries a filterWarnings entry with code journal_metric_matched_nothing.
{ "filters": { "journal_metric.2yr_mean_citedness": { "gte": 5.0 } } }{ "filters": { "journal_metric.impact_factor": { "gte": 3 } } }Exclude by metric:
{
"filters": {
"not": [
{ "field": "journal_metric.2yr_mean_citedness", "gte": 20 }
]
}
}The response reports what each threshold expanded to, so the selection is inspectable. The expansion is capped at 35,000 ISSNs per condition and 70,000 values per request, counting every metric expansion plus your own in and not_in lists. A filter over either limit returns 422 INVALID_FILTER naming the limit, rather than a truncated list, because a partial list would return confidently wrong results.
{
"results": [ ... ],
"journalMetricExpansions": [
{
"field": "journal_metric.2yr_mean_citedness",
"condition": "gte 5.0",
"matchedJournals": 2883,
"issnCount": 4949,
"sampleIssns": ["0028-0836", "1476-4687", "0027-8424"]
}
]
}Metric bounds
A threshold with only one end has to stay inside these bounds. They are checked before anything is searched, and they leave room for the data to grow, so a threshold that works today keeps working when the metrics are refreshed.
| Field | Lower bound alone: lowest gte | Upper bound alone: highest lte |
|---|---|---|
journal_metric.2yr_mean_citedness | 1.1 | 0 |
journal_metric.h_index | 35 | 3 |
journal_metric.i10_index | 160 | none: add a lower end |
journal_metric.impact_factor | 0.9 | 0.2 |
gt and lt are held to the same bounds by what they select. h_index and i10_index are whole numbers, so "gt": 34 on h_index selects what "gte": 35 does and is accepted, and "lt": 4 selects what "lte": 3 does. On citedness, "gt": 1.1 is accepted and "gt": 1.0 is not; on impact factor, "gt": 0.9 is accepted and "gt": 0.8 is not.
A range with both ends (between, or gte with lte) is accepted whenever it matches at most 35,000 ISSNs, so a narrow band such as "between": [0.5, 0.9] works. The bounds come from the current OpenAlex snapshot (2026-09-23) and can move when it is refreshed; this table moves with them.
Two metric conditions near their bounds use most of a request's 70,000 values, so a third, or a long in or not_in list beside them, can be refused. That 422 names the counts; raise a threshold to make room. A filter can also hold at most 8 journal_metric conditions; put both bounds for one metric in a single condition.
Outside the bounds, the 422 INVALID_FILTER names the field, the value you sent and the bound, and carries the bound in details.min_gte and details.max_lte. To fix it:
- A
gteorgtbelow the minimum: raise it to the minimum or above, or add an upper end to make a band. - An
ltorlteon its own above the maximum: to filter below a value X, wrap the opposite range innot. X must be at or above the metric's lowestgtein the table."lt": 2on citedness would match about 126,000 ISSNs;{"not": [{"field": "journal_metric.2yr_mean_citedness", "gte": 2}]}sends about 19,000 and keeps the same journals. Anotalso keeps documents with no ISSN or no metric. Below the lowestgte(citedness"lt": 0.5, say) no one-sided filter is that narrow: the error suggests the narrowestnotallowed and says it is broader than what you sent. - A band over 35,000 ISSNs: narrow it.
Filtering on other fields
Indexed on every collection: journal, publisher, keywords, publication_date, doi, issn, article_type, section, isbn, open_access, chapter_number, chapter_title and chapter_authors.
Additionally indexed on editorial collections only: topic, url, medical_board_approved, last_updated_date, and the clinical-trial fields nct_id, overall_status, phases and has_results. A meta-collection skips a member that has no index for one of these fields instead of failing; a collection you name directly that lacks the index still rejects the filter.
Open access
open_access is a boolean on each result. Where a collection holds only open-access content, its articles do not carry the field; the collection answers the filter instead:
open_access: truematches everything in that collection, so its results are returned alongside per-article matches from other collectionsopen_access: false, oropen_accessunderor/not, excludes that collection and the response says so with afilterWarningsentry, codeopen_access_collection_excluded
Results from such a collection carry open_access: true in their metadata.
license (the Creative Commons licence id, where the publisher declares one) is returned in result metadata but is not a filter: sending it is rejected with INVALID_FILTER. Filter on open_access instead.
Other fields returned in result metadata, such as pmid, title or authors, have no index. Collections that support scan matching still apply them, but slowly, and large collections can time out; the response carries a warning. Collections that do not support scan matching reject them with 422 INVALID_FILTER. The warning looks like this:
{
"results": [ ... ],
"filterWarnings": [
{
"code": "unindexed_filter_field",
"field": "authors",
"message": "Filter field 'authors' has no payload index, so it is matched by scanning and may be slow or time out on large collections. Indexed fields: article_type, chapter_authors, chapter_number, chapter_title, doi, isbn, issn, journal, keywords, last_updated_date, medical_board_approved, open_access, publication_date, publisher, section, topic, url."
}
]
}Rejected filters
A filter that cannot be applied exactly is rejected before the search runs, with 422 INVALID_FILTER naming the field. Nothing is charged, and retrying the same filter fails the same way. Rejected:
- A field that is not a result metadata field at all, such as a typo
doc_id, which is never returned on results: filter ondoito select one document- A
journal_metric.*condition naming an unknown metric, using a non-range operator, outside the metric bounds or matching more than 35,000 ISSNs, a request whose filter values exceed 70,000 in total, or a filter with more than 8journal_metricconditions (see Journal metrics) - A field the collection has no index for, where that collection cannot match it by scanning. The message names the field and the collection; on a multi-collection search each collection is checked
- An unknown operator, a condition mixing operator families, or the two formats mixed
- An
article_typeoutsideresearch-article,review,case-report,editorial,letter,correction,retraction,brief-report,protocol,commentary,book,book-chapter,preprint,monograph,dataset,trial-registrationandother. Common synonyms such asreview-articleare accepted - A date that is not a real calendar date, or not in an accepted form (see Date filtering)
{
"error": {
"code": "INVALID_FILTER",
"message": "'journall' is not a filterable field. Indexed fields: article_type, chapter_authors, chapter_number, chapter_title, doi, isbn, issn, journal, keywords, last_updated_date, medical_board_approved, open_access, publication_date, publisher, section, topic, url.",
"details": {
"filter": { "journall": "Nature" },
"field": "journall"
},
"requestId": "req_a1b2c3d4"
}
}A search that runs echoes the filters it applied as filtersApplied, on POST /api/v1/search/query and POST /api/v1/search/preview, and on the MCP search and preview tools. It is omitted when no filters were sent.
Date filtering
ISO date strings (YYYY-MM-DD or YYYY-MM-DDTHH:MM:SS) are automatically detected and used for datetime range queries. No special syntax is needed: just pass the date string as a value.
publication_date also accepts a year (YYYY) or a year and month (YYYY-MM). last_updated_date takes a full date.
Date range with gte/lte
{
"filters": {
"and": [
{ "field": "publication_date", "gte": "2024-01-01" },
{ "field": "publication_date", "lte": "2024-12-31" }
]
}
}Date range with between
{
"filters": {
"field": "publication_date",
"between": ["2024-01-01", "2024-12-31"]
}
}The filters parameter is part of the Search API request body. See Search for the full endpoint reference.