search_api_facetapi.module 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631
  1. <?php
  2. /**
  3. * @file
  4. * Integrates the Search API with the Facet API.
  5. */
  6. /**
  7. * Implements hook_menu().
  8. */
  9. function search_api_facetapi_menu() {
  10. // We need to handle our own menu paths for facets because we need a facet
  11. // configuration page per index.
  12. $first = TRUE;
  13. foreach (facetapi_get_realm_info() as $realm_name => $realm) {
  14. if ($first) {
  15. $first = FALSE;
  16. $items['admin/config/search/search_api/index/%search_api_index/facets'] = array(
  17. 'title' => 'Facets',
  18. 'page callback' => 'search_api_facetapi_settings',
  19. 'page arguments' => array($realm_name, 5),
  20. 'weight' => -1,
  21. 'access arguments' => array('administer search_api'),
  22. 'type' => MENU_LOCAL_TASK,
  23. 'context' => MENU_CONTEXT_PAGE | MENU_CONTEXT_INLINE,
  24. );
  25. $items['admin/config/search/search_api/index/%search_api_index/facets/' . $realm_name] = array(
  26. 'title' => $realm['label'],
  27. 'type' => MENU_DEFAULT_LOCAL_TASK,
  28. 'weight' => $realm['weight'],
  29. );
  30. }
  31. else {
  32. $items['admin/config/search/search_api/index/%search_api_index/facets/' . $realm_name] = array(
  33. 'title' => $realm['label'],
  34. 'page callback' => 'search_api_facetapi_settings',
  35. 'page arguments' => array($realm_name, 5),
  36. 'access arguments' => array('administer search_api'),
  37. 'type' => MENU_LOCAL_TASK,
  38. 'context' => MENU_CONTEXT_PAGE | MENU_CONTEXT_INLINE,
  39. 'weight' => $realm['weight'],
  40. );
  41. }
  42. }
  43. return $items;
  44. }
  45. /**
  46. * Implements hook_facetapi_searcher_info().
  47. */
  48. function search_api_facetapi_facetapi_searcher_info() {
  49. $info = array();
  50. $indexes = search_api_index_load_multiple(FALSE);
  51. foreach ($indexes as $index) {
  52. if (_search_api_facetapi_index_support_feature($index)) {
  53. $searcher_name = 'search_api@' . $index->machine_name;
  54. $info[$searcher_name] = array(
  55. 'label' => t('Search service: @name', array('@name' => $index->name)),
  56. 'adapter' => 'search_api',
  57. 'instance' => $index->machine_name,
  58. 'types' => array($index->item_type),
  59. 'path' => '',
  60. 'supports facet missing' => TRUE,
  61. 'supports facet mincount' => TRUE,
  62. 'include default facets' => FALSE,
  63. );
  64. if (($entity_type = $index->getEntityType()) && $entity_type !== $index->item_type) {
  65. $info[$searcher_name]['types'][] = $entity_type;
  66. }
  67. }
  68. }
  69. return $info;
  70. }
  71. /**
  72. * Implements hook_facetapi_facet_info().
  73. */
  74. function search_api_facetapi_facetapi_facet_info(array $searcher_info) {
  75. $facet_info = array();
  76. if ('search_api' == $searcher_info['adapter']) {
  77. $index = search_api_index_load($searcher_info['instance']);
  78. if (!empty($index->options['fields'])) {
  79. $wrapper = $index->entityWrapper();
  80. $bundle_key = NULL;
  81. if ($index->getEntityType() && ($entity_info = entity_get_info($index->getEntityType())) && !empty($entity_info['bundle keys']['bundle'])) {
  82. $bundle_key = $entity_info['bundle keys']['bundle'];
  83. }
  84. // Some type-specific settings. Allowing to set some additional callbacks
  85. // (and other settings) in the map options allows for easier overriding by
  86. // other modules.
  87. $type_settings = array(
  88. 'taxonomy_term' => array(
  89. 'hierarchy callback' => 'search_api_facetapi_get_taxonomy_hierarchy',
  90. ),
  91. 'date' => array(
  92. 'query type' => 'date',
  93. 'map options' => array(
  94. 'map callback' => 'search_api_facetapi_map_date',
  95. ),
  96. ),
  97. );
  98. // Iterate through the indexed fields to set the facetapi settings for
  99. // each one.
  100. foreach ($index->getFields() as $key => $field) {
  101. $field['key'] = $key;
  102. // Determine which, if any, of the field type-specific options will be
  103. // used for this field.
  104. $type = isset($field['entity_type']) ? $field['entity_type'] : $field['type'];
  105. $type_settings += array($type => array());
  106. $facet_info[$key] = $type_settings[$type] + array(
  107. 'label' => $field['name'],
  108. 'description' => t('Filter by @type.', array('@type' => $field['name'])),
  109. 'allowed operators' => array(
  110. FACETAPI_OPERATOR_AND => TRUE,
  111. FACETAPI_OPERATOR_OR => _search_api_facetapi_index_support_feature($index, 'search_api_facets_operator_or'),
  112. ),
  113. 'dependency plugins' => array('role'),
  114. 'facet missing allowed' => TRUE,
  115. 'facet mincount allowed' => TRUE,
  116. 'map callback' => 'search_api_facetapi_facet_map_callback',
  117. 'map options' => array(),
  118. 'field type' => $type,
  119. );
  120. if ($type === 'date') {
  121. $facet_info[$key]['description'] .= ' ' . t('(Caution: This may perform very poorly for large result sets.)');
  122. }
  123. $facet_info[$key]['map options'] += array(
  124. 'field' => $field,
  125. 'index id' => $index->machine_name,
  126. 'value callback' => '_search_api_facetapi_facet_create_label',
  127. );
  128. // Find out whether this property is a Field API field.
  129. if (strpos($key, ':') === FALSE) {
  130. if (isset($wrapper->$key)) {
  131. $property_info = $wrapper->$key->info();
  132. if (!empty($property_info['field'])) {
  133. $facet_info[$key]['field api name'] = $key;
  134. }
  135. }
  136. }
  137. // Add bundle information, if applicable.
  138. if ($bundle_key) {
  139. if ($key === $bundle_key) {
  140. // Set entity type this field contains bundle information for.
  141. $facet_info[$key]['field api bundles'][] = $index->getEntityType();
  142. }
  143. else {
  144. // Add "bundle" as possible dependency plugin.
  145. $facet_info[$key]['dependency plugins'][] = 'bundle';
  146. }
  147. }
  148. }
  149. }
  150. }
  151. return $facet_info;
  152. }
  153. /**
  154. * Implements hook_facetapi_adapters().
  155. */
  156. function search_api_facetapi_facetapi_adapters() {
  157. return array(
  158. 'search_api' => array(
  159. 'handler' => array(
  160. 'class' => 'SearchApiFacetapiAdapter',
  161. ),
  162. ),
  163. );
  164. }
  165. /**
  166. * Implements hook_facetapi_query_types().
  167. */
  168. function search_api_facetapi_facetapi_query_types() {
  169. return array(
  170. 'search_api_term' => array(
  171. 'handler' => array(
  172. 'class' => 'SearchApiFacetapiTerm',
  173. 'adapter' => 'search_api',
  174. ),
  175. ),
  176. 'search_api_date' => array(
  177. 'handler' => array(
  178. 'class' => 'SearchApiFacetapiDate',
  179. 'adapter' => 'search_api',
  180. ),
  181. ),
  182. );
  183. }
  184. /**
  185. * Implements hook_search_api_query_alter().
  186. *
  187. * Adds Facet API support to the query.
  188. */
  189. function search_api_facetapi_search_api_query_alter($query) {
  190. $index = $query->getIndex();
  191. if ($index->server()->supportsFeature('search_api_facets')) {
  192. // This is the main point of communication between the facet system and the
  193. // search back-end - it makes the query respond to active facets.
  194. $searcher = 'search_api@' . $index->machine_name;
  195. $adapter = facetapi_adapter_load($searcher);
  196. if ($adapter) {
  197. $adapter->addActiveFilters($query);
  198. }
  199. }
  200. }
  201. /**
  202. * Implements hook_date_formats().
  203. */
  204. function search_api_facetapi_date_formats() {
  205. return array(
  206. array(
  207. 'type' => 'search_api_facetapi_' . FACETAPI_DATE_YEAR,
  208. 'format' => 'Y',
  209. 'locales' => array(),
  210. ),
  211. array(
  212. 'type' => 'search_api_facetapi_' . FACETAPI_DATE_MONTH,
  213. 'format' => 'F Y',
  214. 'locales' => array(),
  215. ),
  216. array(
  217. 'type' => 'search_api_facetapi_' . FACETAPI_DATE_DAY,
  218. 'format' => 'F j, Y',
  219. 'locales' => array(),
  220. ),
  221. array(
  222. 'type' => 'search_api_facetapi_' . FACETAPI_DATE_HOUR,
  223. 'format' => 'H:__',
  224. 'locales' => array(),
  225. ),
  226. array(
  227. 'type' => 'search_api_facetapi_' . FACETAPI_DATE_MINUTE,
  228. 'format' => 'H:i',
  229. 'locales' => array(),
  230. ),
  231. array(
  232. 'type' => 'search_api_facetapi_' . FACETAPI_DATE_SECOND,
  233. 'format' => 'H:i:s',
  234. 'locales' => array(),
  235. ),
  236. );
  237. }
  238. /**
  239. * Implements hook_date_format_types().
  240. */
  241. function search_api_facetapi_date_format_types() {
  242. return array(
  243. 'search_api_facetapi_' . FACETAPI_DATE_YEAR => t('Search facets - Years'),
  244. 'search_api_facetapi_' . FACETAPI_DATE_MONTH => t('Search facets - Months'),
  245. 'search_api_facetapi_' . FACETAPI_DATE_DAY => t('Search facets - Days'),
  246. 'search_api_facetapi_' . FACETAPI_DATE_HOUR => t('Search facets - Hours'),
  247. 'search_api_facetapi_' . FACETAPI_DATE_MINUTE => t('Search facets - Minutes'),
  248. 'search_api_facetapi_' . FACETAPI_DATE_SECOND => t('Search facets - Seconds'),
  249. );
  250. }
  251. /**
  252. * Menu callback for the facet settings page.
  253. */
  254. function search_api_facetapi_settings($realm_name, SearchApiIndex $index) {
  255. if (!$index->enabled) {
  256. return array('#markup' => t('Since this index is at the moment disabled, no facets can be activated.'));
  257. }
  258. if (!_search_api_facetapi_index_support_feature($index)) {
  259. return array('#markup' => t('This index uses a server that does not support facet functionality.'));
  260. }
  261. $searcher_name = 'search_api@' . $index->machine_name;
  262. module_load_include('inc', 'facetapi', 'facetapi.admin');
  263. return drupal_get_form('facetapi_realm_settings_form', $searcher_name, $realm_name);
  264. }
  265. /**
  266. * Checks whether a certain feature is supported for an index.
  267. *
  268. * @param SearchApiIndex $index
  269. * The search index which should be checked.
  270. * @param string $feature
  271. * (optional) The feature to check for. Defaults to "search_api_facets".
  272. *
  273. * @return bool
  274. * TRUE if the feature is supported by the index's server (and the index is
  275. * currently enabled), FALSE otherwise.
  276. */
  277. function _search_api_facetapi_index_support_feature(SearchApiIndex $index, $feature = 'search_api_facets') {
  278. try {
  279. $server = $index->server();
  280. return $server && $server->supportsFeature($feature);
  281. }
  282. catch (SearchApiException $e) {
  283. return FALSE;
  284. }
  285. }
  286. /**
  287. * Gets hierarchy information for taxonomy terms.
  288. *
  289. * Used as a hierarchy callback in search_api_facetapi_facetapi_facet_info().
  290. *
  291. * Internally just uses facetapi_get_taxonomy_hierarchy(), but makes sure that
  292. * our special "!" value is not passed.
  293. *
  294. * @param array $values
  295. * An array containing the term IDs.
  296. *
  297. * @return array
  298. * An associative array mapping term IDs to parent IDs (where parents could be
  299. * found).
  300. */
  301. function search_api_facetapi_get_taxonomy_hierarchy(array $values) {
  302. $values = array_filter($values, 'is_numeric');
  303. return $values ? facetapi_get_taxonomy_hierarchy($values) : array();
  304. }
  305. /**
  306. * Map callback for all search_api facet fields.
  307. *
  308. * @param array $values
  309. * The values to map.
  310. * @param array $options
  311. * An associative array containing:
  312. * - field: Field information, as stored in the index, but with an additional
  313. * "key" property set to the field's internal name.
  314. * - index id: The machine name of the index for this facet.
  315. * - map callback: (optional) A callback that will be called at the beginning,
  316. * which allows initial mapping of filters. Only values not mapped by that
  317. * callback will be processed by this method.
  318. * - value callback: A callback used to map single values and the limits of
  319. * ranges. The signature is the same as for this function, but all values
  320. * will be single values.
  321. * - missing label: (optional) The label used for the "missing" facet.
  322. *
  323. * @return array
  324. * An array mapping raw filter values to their labels.
  325. */
  326. function search_api_facetapi_facet_map_callback(array $values, array $options = array()) {
  327. $map = array();
  328. // See if we have an additional map callback.
  329. if (isset($options['map callback']) && is_callable($options['map callback'])) {
  330. $map = call_user_func($options['map callback'], $values, $options);
  331. }
  332. // Then look at all unmapped values and save information for them.
  333. $mappable_values = array();
  334. $ranges = array();
  335. foreach ($values as $value) {
  336. $value = (string) $value;
  337. if (isset($map[$value])) {
  338. continue;
  339. }
  340. if ($value == '!') {
  341. // The "missing" filter is usually always the same, but we allow an easy
  342. // override via the "missing label" map option.
  343. $map['!'] = isset($options['missing label']) ? $options['missing label'] : '(' . t('none') . ')';
  344. continue;
  345. }
  346. $length = strlen($value);
  347. if ($length > 5 && $value[0] == '[' && $value[$length - 1] == ']' && ($pos = strpos($value, ' TO '))) {
  348. // This is a range filter.
  349. $lower = trim(substr($value, 1, $pos));
  350. $upper = trim(substr($value, $pos + 4, -1));
  351. if ($lower != '*') {
  352. $mappable_values[$lower] = TRUE;
  353. }
  354. if ($upper != '*') {
  355. $mappable_values[$upper] = TRUE;
  356. }
  357. $ranges[$value] = array(
  358. 'lower' => $lower,
  359. 'upper' => $upper,
  360. );
  361. }
  362. else {
  363. // A normal, single-value filter.
  364. $mappable_values[$value] = TRUE;
  365. }
  366. }
  367. if ($mappable_values) {
  368. $map += call_user_func($options['value callback'], array_keys($mappable_values), $options);
  369. }
  370. foreach ($ranges as $value => $range) {
  371. $lower = isset($map[$range['lower']]) ? $map[$range['lower']] : $range['lower'];
  372. $upper = isset($map[$range['upper']]) ? $map[$range['upper']] : $range['upper'];
  373. if ($lower == '*' && $upper == '*') {
  374. $map[$value] = t('any');
  375. }
  376. elseif ($lower == '*') {
  377. $map[$value] = "≤ $upper";
  378. }
  379. elseif ($upper == '*') {
  380. $map[$value] = "≥ $lower";
  381. }
  382. else {
  383. $map[$value] = "$lower – $upper";
  384. }
  385. }
  386. return $map;
  387. }
  388. /**
  389. * Creates a human-readable label for single facet filter values.
  390. *
  391. * @param array $values
  392. * The values for which labels should be returned.
  393. * @param array $options
  394. * An associative array containing the following information about the facet:
  395. * - field: Field information, as stored in the index, but with an additional
  396. * "key" property set to the field's internal name.
  397. * - index id: The machine name of the index for this facet.
  398. * - map callback: (optional) A callback that will be called at the beginning,
  399. * which allows initial mapping of filters. Only values not mapped by that
  400. * callback will be processed by this method.
  401. * - value callback: A callback used to map single values and the limits of
  402. * ranges. The signature is the same as for this function, but all values
  403. * will be single values.
  404. * - missing label: (optional) The label used for the "missing" facet.
  405. *
  406. * @return array
  407. * An array mapping raw facet values to their labels.
  408. */
  409. function _search_api_facetapi_facet_create_label(array $values, array $options) {
  410. $field = $options['field'];
  411. $map = array();
  412. $n = count($values);
  413. // For entities, we can simply use the entity labels.
  414. if (isset($field['entity_type'])) {
  415. $type = $field['entity_type'];
  416. $entities = entity_load($type, $values);
  417. foreach ($entities as $id => $entity) {
  418. $label = entity_label($type, $entity);
  419. if ($label !== FALSE) {
  420. $map[$id] = $label;
  421. }
  422. }
  423. if (count($map) == $n) {
  424. return $map;
  425. }
  426. }
  427. // Then, we check whether there is an options list for the field.
  428. $index = search_api_index_load($options['index id']);
  429. $wrapper = $index->entityWrapper();
  430. $values = drupal_map_assoc($values);
  431. foreach (explode(':', $field['key']) as $part) {
  432. if (!isset($wrapper->$part)) {
  433. $wrapper = NULL;
  434. break;
  435. }
  436. $wrapper = $wrapper->$part;
  437. while (($info = $wrapper->info()) && search_api_is_list_type($info['type'])) {
  438. $wrapper = $wrapper[0];
  439. }
  440. }
  441. if ($wrapper && ($options_list = $wrapper->optionsList('view'))) {
  442. // We have no use for empty strings, as then the facet links would be
  443. // invisible.
  444. $map += array_intersect_key(array_filter($options_list, 'strlen'), $values);
  445. if (count($map) == $n) {
  446. return $map;
  447. }
  448. }
  449. // As a "last resort" we try to create a label based on the field type, for
  450. // all values that haven't got a mapping yet.
  451. foreach (array_diff_key($values, $map) as $value) {
  452. switch ($field['type']) {
  453. case 'boolean':
  454. $map[$value] = $value ? t('true') : t('false');
  455. break;
  456. case 'date':
  457. $v = is_numeric($value) ? $value : strtotime($value);
  458. $map[$value] = format_date($v, 'short');
  459. break;
  460. case 'duration':
  461. $map[$value] = format_interval($value);
  462. break;
  463. }
  464. }
  465. return $map;
  466. }
  467. /**
  468. * Implements hook_form_FORM_ID_alter().
  469. */
  470. function search_api_facetapi_form_search_api_admin_index_fields_alter(&$form, &$form_state) {
  471. $form['#submit'][] = 'search_api_facetapi_search_api_admin_index_fields_submit';
  472. }
  473. /**
  474. * Form submission handler for search_api_admin_index_fields().
  475. */
  476. function search_api_facetapi_search_api_admin_index_fields_submit($form, &$form_state) {
  477. // Clears this searcher's cached facet definitions.
  478. $cid = 'facetapi:facet_info:search_api@' . $form_state['index']->machine_name . ':';
  479. cache_clear_all($cid, 'cache', TRUE);
  480. }
  481. /**
  482. * Computes the granularity of a date facet filter.
  483. *
  484. * @param $filter
  485. * The filter value to examine.
  486. *
  487. * @return string|null
  488. * Either one of the FACETAPI_DATE_* constants corresponding to the
  489. * granularity of the filter, or NULL if it couldn't be computed.
  490. */
  491. function search_api_facetapi_date_get_granularity($filter) {
  492. // Granularity corresponds to number of dashes in filter value.
  493. $units = array(
  494. FACETAPI_DATE_YEAR,
  495. FACETAPI_DATE_MONTH,
  496. FACETAPI_DATE_DAY,
  497. FACETAPI_DATE_HOUR,
  498. FACETAPI_DATE_MINUTE,
  499. FACETAPI_DATE_SECOND,
  500. );
  501. $count = substr_count($filter, '-');
  502. return isset($units[$count]) ? $units[$count] : NULL;
  503. }
  504. /**
  505. * Returns the date format used for a given granularity.
  506. *
  507. * @param $granularity
  508. * One of the FACETAPI_DATE_* constants.
  509. *
  510. * @return string
  511. * The date format used for the given granularity.
  512. */
  513. function search_api_facetapi_date_get_granularity_format($granularity) {
  514. $formats = array(
  515. FACETAPI_DATE_YEAR => 'Y',
  516. FACETAPI_DATE_MONTH => 'Y-m',
  517. FACETAPI_DATE_DAY => 'Y-m-d',
  518. FACETAPI_DATE_HOUR => 'Y-m-d-H',
  519. FACETAPI_DATE_MINUTE => 'Y-m-d-H-i',
  520. FACETAPI_DATE_SECOND => 'Y-m-d-H-i-s',
  521. );
  522. return $formats[$granularity];
  523. }
  524. /**
  525. * Constructs labels for date facet filter values.
  526. *
  527. * @param array $values
  528. * The date facet filter values, as used in URL parameters.
  529. * @param array $options
  530. * (optional) Options for creating the mapping. The following options are
  531. * recognized:
  532. * - format callback: A callback for creating a label for a timestamp. The
  533. * function signature is like search_api_facetapi_format_timestamp(),
  534. * receiving a timestamp and one of the FACETAPI_DATE_* constants as the
  535. * parameters and returning a human-readable label.
  536. *
  537. * @return array
  538. * An array of labels for the given facet filters.
  539. */
  540. function search_api_facetapi_map_date(array $values, array $options = array()) {
  541. $map = array();
  542. foreach ($values as $value) {
  543. // Ignore any filters passed directly from the server (range or missing). We
  544. // always create filters starting with a year.
  545. $value = "$value";
  546. if (!$value || !ctype_digit($value[0])) {
  547. continue;
  548. }
  549. // Get the granularity of the filter.
  550. $granularity = search_api_facetapi_date_get_granularity($value);
  551. if (!$granularity) {
  552. continue;
  553. }
  554. // Otherwise, parse the timestamp from the known format and format it as a
  555. // label.
  556. $format = search_api_facetapi_date_get_granularity_format($granularity);
  557. // Use the "!" modifier to make the date parsing independent of the current
  558. // date/time. (See #2678856.)
  559. $date = DateTime::createFromFormat('!' . $format, $value);
  560. if (!$date) {
  561. continue;
  562. }
  563. $format_callback = 'search_api_facetapi_format_timestamp';
  564. if (!empty($options['format callback']) && is_callable($options['format callback'])) {
  565. $format_callback = $options['format callback'];
  566. }
  567. $map[$value] = call_user_func($format_callback, $date->format('U'), $granularity);
  568. }
  569. return $map;
  570. }
  571. /**
  572. * Format a date according to the default timezone and the given precision.
  573. *
  574. * @param int $timestamp
  575. * An integer containing the Unix timestamp being formated.
  576. * @param string $precision
  577. * A string containing the formatting precision. See the FACETAPI_DATE_*
  578. * constants for valid values.
  579. *
  580. * @return string
  581. * A human-readable representation of the timestamp.
  582. */
  583. function search_api_facetapi_format_timestamp($timestamp, $precision = FACETAPI_DATE_YEAR) {
  584. $formats = array(
  585. FACETAPI_DATE_YEAR,
  586. FACETAPI_DATE_MONTH,
  587. FACETAPI_DATE_DAY,
  588. FACETAPI_DATE_HOUR,
  589. FACETAPI_DATE_MINUTE,
  590. FACETAPI_DATE_SECOND,
  591. );
  592. if (!in_array($precision, $formats)) {
  593. $precision = FACETAPI_DATE_YEAR;
  594. }
  595. return format_date($timestamp, 'search_api_facetapi_' . $precision);
  596. }