example_service.php 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209
  1. <?php
  2. /**
  3. * @file
  4. * Example implementation for a service class which supports facets.
  5. */
  6. /**
  7. * Example class explaining how facets can be supported by a service class.
  8. *
  9. * This class defines the "search_api_facets" and
  10. * "search_api_facets_operator_or" features. Read the method documentation and
  11. * inline comments in search() to learn how they can be supported by a service
  12. * class.
  13. */
  14. abstract class SearchApiFacetapiExampleService extends SearchApiAbstractService {
  15. /**
  16. * Determines whether this service class implementation supports a given
  17. * feature. Features are optional extensions to Search API functionality and
  18. * usually defined and used by third-party modules.
  19. *
  20. * If the service class supports facets, it should return TRUE if called with
  21. * the feature name "search_api_facets". If it also supports "OR" facets, it
  22. * should also return TRUE if called with "search_api_facets_operator_or".
  23. *
  24. * @param string $feature
  25. * The name of the optional feature.
  26. *
  27. * @return boolean
  28. * TRUE if this service knows and supports the specified feature. FALSE
  29. * otherwise.
  30. */
  31. public function supportsFeature($feature) {
  32. $supported = array(
  33. 'search_api_facets' => TRUE,
  34. 'search_api_facets_operator_or' => TRUE,
  35. );
  36. return isset($supported[$feature]);
  37. }
  38. /**
  39. * Executes a search on the server represented by this object.
  40. *
  41. * If the service class supports facets, it should check for an additional
  42. * option on the query object:
  43. * - search_api_facets: An array of facets to return along with the results
  44. * for this query. The array is keyed by an arbitrary string which should
  45. * serve as the facet's unique identifier for this search. The values are
  46. * arrays with the following keys:
  47. * - field: The field to construct facets for.
  48. * - limit: The maximum number of facet terms to return. 0 or an empty
  49. * value means no limit.
  50. * - min_count: The minimum number of results a facet value has to have in
  51. * order to be returned.
  52. * - missing: If TRUE, a facet for all items with no value for this field
  53. * should be returned (if it conforms to limit and min_count).
  54. * - operator: (optional) If the service supports "OR" facets and this key
  55. * contains the string "or", the returned facets should be "OR" facets. If
  56. * the server doesn't support "OR" facets, this key can be ignored.
  57. *
  58. * The basic principle of facets is explained quite well in the
  59. * @link http://en.wikipedia.org/wiki/Faceted_search Wikipedia article on
  60. * "Faceted search" @endlink. Basically, you should return for each field
  61. * filter values which would yield some results when used with the search.
  62. * E.g., if you return for a field $field the term $term with $count results,
  63. * the given $query along with
  64. * $query->condition($field, $term)
  65. * should yield exactly (or about) $count results.
  66. *
  67. * For "OR" facets, all existing filters on the facetted field should be
  68. * ignored for computing the facets.
  69. *
  70. * @param $query
  71. * The SearchApiQueryInterface object to execute.
  72. *
  73. * @return array
  74. * An associative array containing the search results, as required by
  75. * SearchApiQueryInterface::execute().
  76. * In addition, if the "search_api_facets" option is present on the query,
  77. * the results should contain an array of facets in the "search_api_facets"
  78. * key, as specified by the option. The facets array should be keyed by the
  79. * facets' unique identifiers, and contain a numeric array of facet terms,
  80. * sorted descending by result count. A term is represented by an array with
  81. * the following keys:
  82. * - count: Number of results for this term.
  83. * - filter: The filter to apply when selecting this facet term. A filter is
  84. * a string of one of the following forms:
  85. * - "VALUE": Filter by the literal value VALUE (always include the
  86. * quotes, not only for strings).
  87. * - [VALUE1 VALUE2]: Filter for a value between VALUE1 and VALUE2. Use
  88. * parantheses for excluding the border values and square brackets for
  89. * including them. An asterisk (*) can be used as a wildcard. E.g.,
  90. * (* 0) or [* 0) would be a filter for all negative values.
  91. * - !: Filter for items without a value for this field (i.e., the
  92. * "missing" facet).
  93. *
  94. * @throws SearchApiException
  95. * If an error prevented the search from completing.
  96. */
  97. public function search(SearchApiQueryInterface $query) {
  98. // We assume here that we have an AI search which understands English
  99. // commands.
  100. // First, create the normal search query, without facets.
  101. $search = new SuperCoolAiSearch($query->getIndex());
  102. $search->cmd('create basic search for the following query', $query);
  103. $ret = $search->cmd('return search results in Search API format');
  104. // Then, let's see if we should return any facets.
  105. if ($facets = $query->getOption('search_api_facets')) {
  106. // For the facets, we need all results, not only those in the specified
  107. // range.
  108. $results = $search->cmd('return unlimited search results as a set');
  109. foreach ($facets as $id => $facet) {
  110. $field = $facet['field'];
  111. $limit = empty($facet['limit']) ? 'all' : $facet['limit'];
  112. $min_count = $facet['min_count'];
  113. $missing = $facet['missing'];
  114. $or = isset($facet['operator']) && $facet['operator'] == 'or';
  115. // If this is an "OR" facet, existing filters on the field should be
  116. // ignored for computing the facets.
  117. // You can ignore this if your service class doesn't support the
  118. // "search_api_facets_operator_or" feature.
  119. if ($or) {
  120. // We have to execute another query (in the case of this hypothetical
  121. // search backend, at least) to get the right result set to facet.
  122. $tmp_search = new SuperCoolAiSearch($query->getIndex());
  123. $tmp_search->cmd('create basic search for the following query', $query);
  124. $tmp_search->cmd("remove all conditions for field $field");
  125. $tmp_results = $tmp_search->cmd('return unlimited search results as a set');
  126. }
  127. else {
  128. // Otherwise, we can just use the normal results.
  129. $tmp_results = $results;
  130. }
  131. $filters = array();
  132. if ($search->cmd("$field is a date or numeric field")) {
  133. // For date, integer or float fields, range facets are more useful.
  134. $ranges = $search->cmd("list $limit ranges of field $field in the following set", $tmp_results);
  135. foreach ($ranges as $range) {
  136. if ($range->getCount() >= $min_count) {
  137. // Get the lower and upper bound of the range. * means unlimited.
  138. $lower = $range->getLowerBound();
  139. $lower = ($lower == SuperCoolAiSearch::RANGE_UNLIMITED) ? '*' : $lower;
  140. $upper = $range->getUpperBound();
  141. $upper = ($upper == SuperCoolAiSearch::RANGE_UNLIMITED) ? '*' : $upper;
  142. // Then, see whether the bounds are included in the range. These
  143. // can be specified independently for the lower and upper bound.
  144. // Parentheses are used for exclusive bounds, square brackets are
  145. // used for inclusive bounds.
  146. $lowChar = $range->isLowerBoundInclusive() ? '[' : '(';
  147. $upChar = $range->isUpperBoundInclusive() ? ']' : ')';
  148. // Create the filter, which separates the bounds with a single
  149. // space.
  150. $filter = "$lowChar$lower $upper$upChar";
  151. $filters[$filter] = $range->getCount();
  152. }
  153. }
  154. }
  155. else {
  156. // Otherwise, we use normal single-valued facets.
  157. $terms = $search->cmd("list $limit values of field $field in the following set", $tmp_results);
  158. foreach ($terms as $term) {
  159. if ($term->getCount() >= $min_count) {
  160. // For single-valued terms, we just need to wrap them in quotes.
  161. $filter = '"' . $term->getValue() . '"';
  162. $filters[$filter] = $term->getCount();
  163. }
  164. }
  165. }
  166. // If we should also return a "missing" facet, compute that as the
  167. // number of results without a value for the facet field.
  168. if ($missing) {
  169. $count = $search->cmd("return number of results without field $field in the following set", $tmp_results);
  170. if ($count >= $min_count) {
  171. $filters['!'] = $count;
  172. }
  173. }
  174. // Sort the facets descending by result count.
  175. arsort($filters);
  176. // With the "missing" facet, we might have too many facet terms (unless
  177. // $limit was empty and is therefore now set to "all"). If this is the
  178. // case, remove those with the lowest number of results.
  179. while (is_numeric($limit) && count($filters) > $limit) {
  180. array_pop($filters);
  181. }
  182. // Now add the facet terms to the return value, as specified in the doc
  183. // comment for this method.
  184. foreach ($filters as $filter => $count) {
  185. $ret['search_api_facets'][$id][] = array(
  186. 'count' => $count,
  187. 'filter' => $filter,
  188. );
  189. }
  190. }
  191. }
  192. // Return the results, which now also includes the facet information.
  193. return $ret;
  194. }
  195. }