Custom search_api query for categories block
The following code does what we need.

  /**
   * @return int[]
   */
  function getCountsByTid($indexName, $facetFieldName) {

    $options = [];
    $options['search_api_facets'][$facetFieldName] = [
      'field' =&gt; $facetFieldName,
      // No limit.
      'limit' =&gt; -1,
      'operator' =&gt; 'and',
      'min_count' =&gt; '1',
      'missing' =&gt; '0',
    ];

    $q = search_api_query($indexName, $options);
    $q-&gt;range(0, 0);
    $q-&gt;fields([]);

    // Skip hook_search_api_query_alter().
    $result = $q-&gt;getIndex()-&gt;server()-&gt;search($q);

    if (!isset($result['search_api_facets'][$facetFieldName])) {
      return [];
    }

    $countsByTid = [];
    foreach ($result['search_api_facets'][$facetFieldName] as $item) {
      // The tid comes in a weird format.
      $tid = (int)substr($item['filter'], 1, -1);
      $count = $item['count'];
      $countsByTid[$tid] = $count;
    }

    return $countsByTid;
  }


Normally, the $options['search_api_facets'] would be set in search_api_facetapi_search_api_query_alter(). However, this adds ALL facets, instead of just a specific facet. Also it limits the number of facet counts to 50. See #12132878: search_api_facetapi pollutes and slows all queries

Instead we set this option manually, and then skip the $q-&gt;execute() that would trigger hook_search_api_query_alter().

SearchApiService classes always add quotes around the search values, which seems quite pointless to me.

We could get the raw tids from $result['search_api_solr_response']-&gt;facet_counts-&gt;facet_fields-&gt;$propkey, with $propkey = 'im_' . str_replace(':', '$', $facetFieldName), but this only applies to search_api_solr.