query.inc 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564
  1. <?php
  2. /**
  3. * Views query class using a Search API index as the data source.
  4. */
  5. class SearchApiViewsQuery extends views_plugin_query {
  6. /**
  7. * Number of results to display.
  8. *
  9. * @var int
  10. */
  11. protected $limit;
  12. /**
  13. * Offset of first displayed result.
  14. *
  15. * @var int
  16. */
  17. protected $offset;
  18. /**
  19. * The index this view accesses.
  20. *
  21. * @var SearchApiIndex
  22. */
  23. protected $index;
  24. /**
  25. * The query that will be executed.
  26. *
  27. * @var SearchApiQueryInterface
  28. */
  29. protected $query;
  30. /**
  31. * The results returned by the query, after it was executed.
  32. *
  33. * @var array
  34. */
  35. protected $search_api_results = array();
  36. /**
  37. * Array of all encountered errors.
  38. *
  39. * Each of these is fatal, meaning that a non-empty $errors property will
  40. * result in an empty result being returned.
  41. *
  42. * @var array
  43. */
  44. protected $errors;
  45. /**
  46. * The names of all fields whose value is required by a handler.
  47. *
  48. * The format follows the same as Search API field identifiers (parent:child).
  49. *
  50. * @var array
  51. */
  52. protected $fields;
  53. /**
  54. * The query's sub-filters representing the different Views filter groups.
  55. *
  56. * @var array
  57. */
  58. protected $filters = array();
  59. /**
  60. * The conjunction with which multiple filter groups are combined.
  61. *
  62. * @var string
  63. */
  64. public $group_operator = 'AND';
  65. /**
  66. * Create the basic query object and fill with default values.
  67. */
  68. public function init($base_table, $base_field, $options) {
  69. try {
  70. $this->errors = array();
  71. parent::init($base_table, $base_field, $options);
  72. $this->fields = array();
  73. if (substr($base_table, 0, 17) == 'search_api_index_') {
  74. $id = substr($base_table, 17);
  75. $this->index = search_api_index_load($id);
  76. $this->query = $this->index->query(array(
  77. 'parse mode' => 'terms',
  78. ));
  79. }
  80. }
  81. catch (Exception $e) {
  82. $this->errors[] = $e->getMessage();
  83. }
  84. }
  85. /**
  86. * Add a field that should be retrieved from the results by this view.
  87. *
  88. * @param $field
  89. * The field's identifier, as used by the Search API. E.g., "title" for a
  90. * node's title, "author:name" for a node's author's name.
  91. *
  92. * @return SearchApiViewsQuery
  93. * The called object.
  94. */
  95. public function addField($field) {
  96. $this->fields[$field] = TRUE;
  97. return $field;
  98. }
  99. /**
  100. * Add a sort to the query.
  101. *
  102. * @param $selector
  103. * The field to sort on. All indexed fields of the index are valid values.
  104. * In addition, the special fields 'search_api_relevance' (sort by
  105. * relevance) and 'search_api_id' (sort by item id) may be used.
  106. * @param $order
  107. * The order to sort items in - either 'ASC' or 'DESC'. Defaults to 'ASC'.
  108. */
  109. public function add_selector_orderby($selector, $order = 'ASC') {
  110. $this->query->sort($selector, $order);
  111. }
  112. /**
  113. * Defines the options used by this query plugin.
  114. *
  115. * Adds an option to bypass access checks.
  116. */
  117. public function option_definition() {
  118. return parent::option_definition() + array(
  119. 'search_api_bypass_access' => array(
  120. 'default' => FALSE,
  121. ),
  122. );
  123. }
  124. /**
  125. * Add settings for the UI.
  126. *
  127. * Adds an option for bypassing access checks.
  128. */
  129. public function options_form(&$form, &$form_state) {
  130. parent::options_form($form, $form_state);
  131. $form['search_api_bypass_access'] = array(
  132. '#type' => 'checkbox',
  133. '#title' => t('Bypass access checks'),
  134. '#description' => t('If the underlying search index has access checks enabled, this option allows to disable them for this view.'),
  135. '#default_value' => $this->options['search_api_bypass_access'],
  136. );
  137. }
  138. /**
  139. * Builds the necessary info to execute the query.
  140. */
  141. public function build(&$view) {
  142. $this->view = $view;
  143. // Setup the nested filter structure for this query.
  144. if (!empty($this->where)) {
  145. // If the different groups are combined with the OR operator, we have to
  146. // add a new OR filter to the query to which the filters for the groups
  147. // will be added.
  148. if ($this->group_operator === 'OR') {
  149. $base = $this->query->createFilter('OR');
  150. $this->query->filter($base);
  151. }
  152. else {
  153. $base = $this->query;
  154. }
  155. // Add a nested filter for each filter group, with its set conjunction.
  156. foreach ($this->where as $group_id => $group) {
  157. if (!empty($group['conditions']) || !empty($group['filters'])) {
  158. // For filters without a group, we want to always add them directly to
  159. // the query.
  160. $filter = ($group_id === '') ? $this->query : $this->query->createFilter($group['type']);
  161. if (!empty($group['conditions'])) {
  162. foreach ($group['conditions'] as $condition) {
  163. list($field, $value, $operator) = $condition;
  164. $filter->condition($field, $value, $operator);
  165. }
  166. }
  167. if (!empty($group['filters'])) {
  168. foreach ($group['filters'] as $nested_filter) {
  169. $filter->filter($nested_filter);
  170. }
  171. }
  172. // If no group was given, the filters were already set on the query.
  173. if ($group_id !== '') {
  174. $base->filter($filter);
  175. }
  176. }
  177. }
  178. }
  179. // Initialize the pager and let it modify the query to add limits.
  180. $view->init_pager();
  181. $this->pager->query();
  182. // Add the "search_api_bypass_access" option to the query, if desired.
  183. if (!empty($this->options['search_api_bypass_access'])) {
  184. $this->query->setOption('search_api_bypass_access', TRUE);
  185. }
  186. }
  187. /**
  188. * Executes the query and fills the associated view object with according
  189. * values.
  190. *
  191. * Values to set: $view->result, $view->total_rows, $view->execute_time,
  192. * $view->pager['current_page'].
  193. */
  194. public function execute(&$view) {
  195. if ($this->errors) {
  196. if (error_displayable()) {
  197. foreach ($this->errors as $msg) {
  198. drupal_set_message(check_plain($msg), 'error');
  199. }
  200. }
  201. $view->result = array();
  202. $view->total_rows = 0;
  203. $view->execute_time = 0;
  204. return;
  205. }
  206. try {
  207. $start = microtime(TRUE);
  208. // Add range and search ID (if it wasn't already set).
  209. $this->query->range($this->offset, $this->limit);
  210. if ($this->query->getOption('search id') == get_class($this->query)) {
  211. $this->query->setOption('search id', 'search_api_views:' . $view->name . ':' . $view->current_display);
  212. }
  213. // Execute the search.
  214. $results = $this->query->execute();
  215. $this->search_api_results = $results;
  216. // Store the results.
  217. $this->pager->total_items = $view->total_rows = $results['result count'];
  218. if (!empty($this->pager->options['offset'])) {
  219. $this->pager->total_items -= $this->pager->options['offset'];
  220. }
  221. $this->pager->update_page_info();
  222. $view->result = array();
  223. if (!empty($results['results'])) {
  224. $this->addResults($results['results'], $view);
  225. }
  226. // We shouldn't use $results['performance']['complete'] here, since
  227. // extracting the results probably takes considerable time as well.
  228. $view->execute_time = microtime(TRUE) - $start;
  229. }
  230. catch (Exception $e) {
  231. $this->errors[] = $e->getMessage();
  232. // Recursion to get the same error behaviour as above.
  233. return $this->execute($view);
  234. }
  235. }
  236. /**
  237. * Helper function for adding results to a view in the format expected by the
  238. * view.
  239. */
  240. protected function addResults(array $results, $view) {
  241. $rows = array();
  242. $missing = array();
  243. $items = array();
  244. // First off, we try to gather as much field values as possible without
  245. // loading any items.
  246. foreach ($results as $id => $result) {
  247. $row = array();
  248. // Include the loaded item for this result row, if present, or the item
  249. // ID.
  250. if (!empty($result['entity'])) {
  251. $row['entity'] = $result['entity'];
  252. }
  253. else {
  254. $row['entity'] = $id;
  255. }
  256. $row['_entity_properties']['search_api_relevance'] = $result['score'];
  257. $row['_entity_properties']['search_api_excerpt'] = empty($result['excerpt']) ? '' : $result['excerpt'];
  258. // Gather any fields from the search results.
  259. if (!empty($result['fields'])) {
  260. $row['_entity_properties'] += $result['fields'];
  261. }
  262. // Check whether we need to extract any properties from the result item.
  263. $missing_fields = array_diff_key($this->fields, $row);
  264. if ($missing_fields) {
  265. $missing[$id] = $missing_fields;
  266. if (is_object($row['entity'])) {
  267. $items[$id] = $row['entity'];
  268. }
  269. else {
  270. $ids[] = $id;
  271. }
  272. }
  273. // Save the row values for adding them to the Views result afterwards.
  274. $rows[$id] = (object) $row;
  275. }
  276. // Load items of those rows which haven't got all field values, yet.
  277. if (!empty($ids)) {
  278. $items += $this->index->loadItems($ids);
  279. // $items now includes loaded items, and those already passed in the
  280. // search results.
  281. foreach ($items as $id => $item) {
  282. // Extract item properties.
  283. $wrapper = $this->index->entityWrapper($item, FALSE);
  284. $rows[$id]->_entity_properties += $this->extractFields($wrapper, $missing[$id]);
  285. $rows[$id]->entity = $item;
  286. }
  287. }
  288. // Finally, add all rows to the Views result set.
  289. $view->result = array_values($rows);
  290. }
  291. /**
  292. * Helper function for extracting all necessary fields from a result item.
  293. *
  294. * Usually, this method isn't needed anymore as the properties are now
  295. * extracted by the field handlers themselves.
  296. */
  297. protected function extractFields(EntityMetadataWrapper $wrapper, array $all_fields) {
  298. $fields = array();
  299. foreach ($all_fields as $key => $true) {
  300. $fields[$key]['type'] = 'string';
  301. }
  302. $fields = search_api_extract_fields($wrapper, $fields, array('sanitized' => TRUE));
  303. $ret = array();
  304. foreach ($all_fields as $key => $true) {
  305. $ret[$key] = isset($fields[$key]['value']) ? $fields[$key]['value'] : '';
  306. }
  307. return $ret;
  308. }
  309. /**
  310. * Returns the according entity objects for the given query results.
  311. *
  312. * This is necessary to support generic entity handlers and plugins with this
  313. * query backend.
  314. *
  315. * If the current query isn't based on an entity type, the method will return
  316. * an empty array.
  317. */
  318. public function get_result_entities($results, $relationship = NULL, $field = NULL) {
  319. list($type, $wrappers) = $this->get_result_wrappers($results, $relationship, $field);
  320. $return = array();
  321. foreach ($wrappers as $id => $wrapper) {
  322. try {
  323. $return[$id] = $wrapper->value();
  324. }
  325. catch (EntityMetadataWrapperException $e) {
  326. // Ignore.
  327. }
  328. }
  329. return array($type, $return);
  330. }
  331. /**
  332. * Returns the according metadata wrappers for the given query results.
  333. *
  334. * This is necessary to support generic entity handlers and plugins with this
  335. * query backend.
  336. */
  337. public function get_result_wrappers($results, $relationship = NULL, $field = NULL) {
  338. $is_entity = (boolean) entity_get_info($this->index->item_type);
  339. $wrappers = array();
  340. $load_entities = array();
  341. foreach ($results as $row_index => $row) {
  342. if ($is_entity && isset($row->entity)) {
  343. // If this entity isn't load, register it for pre-loading.
  344. if (!is_object($row->entity)) {
  345. $load_entities[$row->entity] = $row_index;
  346. }
  347. $wrappers[$row_index] = $this->index->entityWrapper($row->entity);
  348. }
  349. }
  350. // If the results are entities, we pre-load them to make use of a multiple
  351. // load. (Otherwise, each result would be loaded individually.)
  352. if (!empty($load_entities)) {
  353. $entities = entity_load($this->index->item_type, array_keys($load_entities));
  354. foreach ($entities as $entity_id => $entity) {
  355. $wrappers[$load_entities[$entity_id]] = $this->index->entityWrapper($entity);
  356. }
  357. }
  358. // Apply the relationship, if necessary.
  359. $type = $this->index->item_type;
  360. $selector_suffix = '';
  361. if ($field && ($pos = strrpos($field, ':'))) {
  362. $selector_suffix = substr($field, 0, $pos);
  363. }
  364. if ($selector_suffix || ($relationship && !empty($this->view->relationship[$relationship]))) {
  365. // Use EntityFieldHandlerHelper to compute the correct data selector for
  366. // the relationship.
  367. $handler = (object) array(
  368. 'view' => $this->view,
  369. 'relationship' => $relationship,
  370. 'real_field' => '',
  371. );
  372. $selector = EntityFieldHandlerHelper::construct_property_selector($handler);
  373. $selector .= ($selector ? ':' : '') . $selector_suffix;
  374. list($type, $wrappers) = EntityFieldHandlerHelper::extract_property_multiple($wrappers, $selector);
  375. }
  376. return array($type, $wrappers);
  377. }
  378. /**
  379. * API function for accessing the raw Search API query object.
  380. *
  381. * @return SearchApiQueryInterface
  382. * The search query object used internally by this handler.
  383. */
  384. public function getSearchApiQuery() {
  385. return $this->query;
  386. }
  387. /**
  388. * API function for accessing the raw Search API results.
  389. *
  390. * @return array
  391. * An associative array containing the search results, as specified by
  392. * SearchApiQueryInterface::execute().
  393. */
  394. public function getSearchApiResults() {
  395. return $this->search_api_results;
  396. }
  397. //
  398. // Query interface methods (proxy to $this->query)
  399. //
  400. public function createFilter($conjunction = 'AND') {
  401. if (!$this->errors) {
  402. return $this->query->createFilter($conjunction);
  403. }
  404. }
  405. public function keys($keys = NULL) {
  406. if (!$this->errors) {
  407. $this->query->keys($keys);
  408. }
  409. return $this;
  410. }
  411. public function fields(array $fields) {
  412. if (!$this->errors) {
  413. $this->query->fields($fields);
  414. }
  415. return $this;
  416. }
  417. /**
  418. * Adds a nested filter to the search query object.
  419. *
  420. * If $group is given, the filter is added to the relevant filter group
  421. * instead.
  422. */
  423. public function filter(SearchApiQueryFilterInterface $filter, $group = NULL) {
  424. if (!$this->errors) {
  425. $this->where[$group]['filters'][] = $filter;
  426. }
  427. return $this;
  428. }
  429. /**
  430. * Set a condition on the search query object.
  431. *
  432. * If $group is given, the condition is added to the relevant filter group
  433. * instead.
  434. */
  435. public function condition($field, $value, $operator = '=', $group = NULL) {
  436. if (!$this->errors) {
  437. $this->where[$group]['conditions'][] = array($field, $value, $operator);
  438. }
  439. return $this;
  440. }
  441. public function sort($field, $order = 'ASC') {
  442. if (!$this->errors) {
  443. $this->query->sort($field, $order);
  444. }
  445. return $this;
  446. }
  447. public function range($offset = NULL, $limit = NULL) {
  448. if (!$this->errors) {
  449. $this->query->range($offset, $limit);
  450. }
  451. return $this;
  452. }
  453. public function getIndex() {
  454. return $this->index;
  455. }
  456. public function &getKeys() {
  457. if (!$this->errors) {
  458. return $this->query->getKeys();
  459. }
  460. $ret = NULL;
  461. return $ret;
  462. }
  463. public function getOriginalKeys() {
  464. if (!$this->errors) {
  465. return $this->query->getOriginalKeys();
  466. }
  467. }
  468. public function &getFields() {
  469. if (!$this->errors) {
  470. return $this->query->getFields();
  471. }
  472. $ret = NULL;
  473. return $ret;
  474. }
  475. public function getFilter() {
  476. if (!$this->errors) {
  477. return $this->query->getFilter();
  478. }
  479. }
  480. public function &getSort() {
  481. if (!$this->errors) {
  482. return $this->query->getSort();
  483. }
  484. $ret = NULL;
  485. return $ret;
  486. }
  487. public function getOption($name) {
  488. if (!$this->errors) {
  489. return $this->query->getOption($name);
  490. }
  491. }
  492. public function setOption($name, $value) {
  493. if (!$this->errors) {
  494. return $this->query->setOption($name, $value);
  495. }
  496. }
  497. public function &getOptions() {
  498. if (!$this->errors) {
  499. return $this->query->getOptions();
  500. }
  501. $ret = NULL;
  502. return $ret;
  503. }
  504. }