Filtering

Narrow results by metadata, dates, and identifiers.

Operators

OperatorDescriptionExample value
eqEquals"research"
neNot equals"deleted"
inMatches any in list["tech", "science"]
not_inExcludes values in list["spam", "junk"]
gtGreater than2020
gteGreater than or equal2020
ltLess than2025
lteLess than or equal2025
betweenRange (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.

FieldLower bound alone: lowest gteUpper bound alone: highest lte
journal_metric.2yr_mean_citedness1.10
journal_metric.h_index353
journal_metric.i10_index160none: add a lower end
journal_metric.impact_factor0.90.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 gte or gt below the minimum: raise it to the minimum or above, or add an upper end to make a band.
  • An lt or lte on its own above the maximum: to filter below a value X, wrap the opposite range in not. X must be at or above the metric's lowest gte in the table. "lt": 2 on 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. A not also keeps documents with no ISSN or no metric. Below the lowest gte (citedness "lt": 0.5, say) no one-sided filter is that narrow: the error suggests the narrowest not allowed 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: true matches everything in that collection, so its results are returned alongside per-article matches from other collections
  • open_access: false, or open_access under or / not, excludes that collection and the response says so with a filterWarnings entry, code open_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 on doi to 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 8 journal_metric conditions (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_type outside research-article, review, case-report, editorial, letter, correction, retraction, brief-report, protocol, commentary, book, book-chapter, preprint, monograph, dataset, trial-registration and other. Common synonyms such as review-article are 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.

Was this page helpful?

On this page