query.inc 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643
  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. * Whether to abort the search instead of executing it.
  47. *
  48. * @var bool
  49. */
  50. protected $abort = FALSE;
  51. /**
  52. * The names of all fields whose value is required by a handler.
  53. *
  54. * The format follows the same as Search API field identifiers (parent:child).
  55. *
  56. * @var array
  57. */
  58. protected $fields;
  59. /**
  60. * The query's sub-filters representing the different Views filter groups.
  61. *
  62. * @var array
  63. */
  64. protected $filters = array();
  65. /**
  66. * The conjunction with which multiple filter groups are combined.
  67. *
  68. * @var string
  69. */
  70. public $group_operator = 'AND';
  71. /**
  72. * Create the basic query object and fill with default values.
  73. */
  74. public function init($base_table, $base_field, $options) {
  75. try {
  76. $this->errors = array();
  77. parent::init($base_table, $base_field, $options);
  78. $this->fields = array();
  79. if (substr($base_table, 0, 17) == 'search_api_index_') {
  80. $id = substr($base_table, 17);
  81. $this->index = search_api_index_load($id);
  82. $this->query = $this->index->query(array(
  83. 'parse mode' => $this->options['parse_mode'],
  84. ));
  85. }
  86. }
  87. catch (Exception $e) {
  88. $this->errors[] = $e->getMessage();
  89. }
  90. }
  91. /**
  92. * Add a field that should be retrieved from the results by this view.
  93. *
  94. * @param $field
  95. * The field's identifier, as used by the Search API. E.g., "title" for a
  96. * node's title, "author:name" for a node's author's name.
  97. *
  98. * @return SearchApiViewsQuery
  99. * The called object.
  100. */
  101. public function addField($field) {
  102. $this->fields[$field] = TRUE;
  103. return $field;
  104. }
  105. /**
  106. * Add a sort to the query.
  107. *
  108. * @param $selector
  109. * The field to sort on. All indexed fields of the index are valid values.
  110. * In addition, the special fields 'search_api_relevance' (sort by
  111. * relevance) and 'search_api_id' (sort by item id) may be used.
  112. * @param $order
  113. * The order to sort items in - either 'ASC' or 'DESC'. Defaults to 'ASC'.
  114. */
  115. public function add_selector_orderby($selector, $order = 'ASC') {
  116. $this->query->sort($selector, $order);
  117. }
  118. /**
  119. * Defines the options used by this query plugin.
  120. *
  121. * Adds some access options.
  122. */
  123. public function option_definition() {
  124. return parent::option_definition() + array(
  125. 'search_api_bypass_access' => array(
  126. 'default' => FALSE,
  127. ),
  128. 'entity_access' => array(
  129. 'default' => FALSE,
  130. ),
  131. 'parse_mode' => array(
  132. 'default' => 'terms',
  133. ),
  134. );
  135. }
  136. /**
  137. * Add settings for the UI.
  138. *
  139. * Adds an option for bypassing access checks.
  140. */
  141. public function options_form(&$form, &$form_state) {
  142. parent::options_form($form, $form_state);
  143. $form['search_api_bypass_access'] = array(
  144. '#type' => 'checkbox',
  145. '#title' => t('Bypass access checks'),
  146. '#description' => t('If the underlying search index has access checks enabled, this option allows to disable them for this view.'),
  147. '#default_value' => $this->options['search_api_bypass_access'],
  148. );
  149. if (entity_get_info($this->index->item_type)) {
  150. $form['entity_access'] = array(
  151. '#type' => 'checkbox',
  152. '#title' => t('Additional access checks on result entities'),
  153. '#description' => t("Execute an access check for all result entities. This prevents users from seeing inappropriate content when the index contains stale data, or doesn't provide access checks. However, result counts, paging and other things won't work correctly if results are eliminated in this way, so only use this as a last ressort (and in addition to other checks, if possible)."),
  154. '#default_value' => $this->options['entity_access'],
  155. );
  156. }
  157. $form['parse_mode'] = array(
  158. '#type' => 'select',
  159. '#title' => t('Parse mode'),
  160. '#description' => t('Choose how the search keys will be parsed.'),
  161. '#options' => array(),
  162. '#default_value' => $this->options['parse_mode'],
  163. );
  164. $modes = array();
  165. foreach ($this->query->parseModes() as $key => $mode) {
  166. $form['parse_mode']['#options'][$key] = $mode['name'];
  167. if (!empty($mode['description'])) {
  168. $states['visible'][':input[name="query[options][parse_mode]"]']['value'] = $key;
  169. $form["parse_mode_{$key}_description"] = array(
  170. '#type' => 'item',
  171. '#title' => $mode['name'],
  172. '#description' => $mode['description'],
  173. '#states' => $states,
  174. );
  175. }
  176. }
  177. }
  178. /**
  179. * Builds the necessary info to execute the query.
  180. */
  181. public function build(&$view) {
  182. $this->view = $view;
  183. // Setup the nested filter structure for this query.
  184. if (!empty($this->where)) {
  185. // If the different groups are combined with the OR operator, we have to
  186. // add a new OR filter to the query to which the filters for the groups
  187. // will be added.
  188. if ($this->group_operator === 'OR') {
  189. $base = $this->query->createFilter('OR');
  190. $this->query->filter($base);
  191. }
  192. else {
  193. $base = $this->query;
  194. }
  195. // Add a nested filter for each filter group, with its set conjunction.
  196. foreach ($this->where as $group_id => $group) {
  197. if (!empty($group['conditions']) || !empty($group['filters'])) {
  198. $group += array('type' => 'AND');
  199. // For filters without a group, we want to always add them directly to
  200. // the query.
  201. $filter = ($group_id === '') ? $this->query : $this->query->createFilter($group['type']);
  202. if (!empty($group['conditions'])) {
  203. foreach ($group['conditions'] as $condition) {
  204. list($field, $value, $operator) = $condition;
  205. $filter->condition($field, $value, $operator);
  206. }
  207. }
  208. if (!empty($group['filters'])) {
  209. foreach ($group['filters'] as $nested_filter) {
  210. $filter->filter($nested_filter);
  211. }
  212. }
  213. // If no group was given, the filters were already set on the query.
  214. if ($group_id !== '') {
  215. $base->filter($filter);
  216. }
  217. }
  218. }
  219. }
  220. // Initialize the pager and let it modify the query to add limits.
  221. $view->init_pager();
  222. $this->pager->query();
  223. // Views passes sometimes NULL and sometimes the integer 0 for "All" in a
  224. // pager. If set to 0 items, a string "0" is passed. Therefore, we unset
  225. // the limit if an empty value OTHER than a string "0" was passed.
  226. if (!$this->limit && $this->limit !== '0') {
  227. $this->limit = NULL;
  228. }
  229. // Set the range. (We always set this, as there might even be an offset if
  230. // all items are shown.)
  231. $this->query->range($this->offset, $this->limit);
  232. // Set the search ID, if it was not already set.
  233. if ($this->query->getOption('search id') == get_class($this->query)) {
  234. $this->query->setOption('search id', 'search_api_views:' . $view->name . ':' . $view->current_display);
  235. }
  236. // Add the "search_api_bypass_access" option to the query, if desired.
  237. if (!empty($this->options['search_api_bypass_access'])) {
  238. $this->query->setOption('search_api_bypass_access', TRUE);
  239. }
  240. }
  241. /**
  242. * Executes the query and fills the associated view object with according
  243. * values.
  244. *
  245. * Values to set: $view->result, $view->total_rows, $view->execute_time,
  246. * $view->pager['current_page'].
  247. */
  248. public function execute(&$view) {
  249. if ($this->errors || $this->abort) {
  250. if (error_displayable()) {
  251. foreach ($this->errors as $msg) {
  252. drupal_set_message(check_plain($msg), 'error');
  253. }
  254. }
  255. $view->result = array();
  256. $view->total_rows = 0;
  257. $view->execute_time = 0;
  258. return;
  259. }
  260. try {
  261. $start = microtime(TRUE);
  262. // Execute the search.
  263. $results = $this->query->execute();
  264. $this->search_api_results = $results;
  265. // Store the results.
  266. $this->pager->total_items = $view->total_rows = $results['result count'];
  267. if (!empty($this->pager->options['offset'])) {
  268. $this->pager->total_items -= $this->pager->options['offset'];
  269. }
  270. $this->pager->update_page_info();
  271. $view->result = array();
  272. if (!empty($results['results'])) {
  273. $this->addResults($results['results'], $view);
  274. }
  275. // We shouldn't use $results['performance']['complete'] here, since
  276. // extracting the results probably takes considerable time as well.
  277. $view->execute_time = microtime(TRUE) - $start;
  278. }
  279. catch (Exception $e) {
  280. $this->errors[] = $e->getMessage();
  281. // Recursion to get the same error behaviour as above.
  282. return $this->execute($view);
  283. }
  284. }
  285. /**
  286. * Aborts this search query.
  287. *
  288. * Used by handlers to flag a fatal error which shouldn't be displayed but
  289. * still lead to the view returning empty and the search not being executed.
  290. */
  291. public function abort() {
  292. $this->abort = TRUE;
  293. }
  294. /**
  295. * Helper function for adding results to a view in the format expected by the
  296. * view.
  297. */
  298. protected function addResults(array $results, $view) {
  299. $rows = array();
  300. $missing = array();
  301. $items = array();
  302. // First off, we try to gather as much field values as possible without
  303. // loading any items.
  304. foreach ($results as $id => $result) {
  305. if (!empty($this->options['entity_access'])) {
  306. $entity = entity_load($this->index->item_type, array($id));
  307. if (!entity_access('view', $this->index->item_type, $entity[$id])) {
  308. continue;
  309. }
  310. }
  311. $row = array();
  312. // Include the loaded item for this result row, if present, or the item
  313. // ID.
  314. if (!empty($result['entity'])) {
  315. $row['entity'] = $result['entity'];
  316. }
  317. else {
  318. $row['entity'] = $id;
  319. }
  320. $row['_entity_properties']['search_api_relevance'] = $result['score'];
  321. $row['_entity_properties']['search_api_excerpt'] = empty($result['excerpt']) ? '' : $result['excerpt'];
  322. // Gather any fields from the search results.
  323. if (!empty($result['fields'])) {
  324. $row['_entity_properties'] += $result['fields'];
  325. }
  326. // Check whether we need to extract any properties from the result item.
  327. $missing_fields = array_diff_key($this->fields, $row);
  328. if ($missing_fields) {
  329. $missing[$id] = $missing_fields;
  330. if (is_object($row['entity'])) {
  331. $items[$id] = $row['entity'];
  332. }
  333. else {
  334. $ids[] = $id;
  335. }
  336. }
  337. // Save the row values for adding them to the Views result afterwards.
  338. $rows[$id] = (object) $row;
  339. }
  340. // Load items of those rows which haven't got all field values, yet.
  341. if (!empty($ids)) {
  342. $items += $this->index->loadItems($ids);
  343. // $items now includes loaded items, and those already passed in the
  344. // search results.
  345. foreach ($items as $id => $item) {
  346. // Extract item properties.
  347. $wrapper = $this->index->entityWrapper($item, FALSE);
  348. $rows[$id]->_entity_properties += $this->extractFields($wrapper, $missing[$id]);
  349. $rows[$id]->entity = $item;
  350. }
  351. }
  352. // Finally, add all rows to the Views result set.
  353. $view->result = array_values($rows);
  354. }
  355. /**
  356. * Helper function for extracting all necessary fields from a result item.
  357. *
  358. * Usually, this method isn't needed anymore as the properties are now
  359. * extracted by the field handlers themselves.
  360. */
  361. protected function extractFields(EntityMetadataWrapper $wrapper, array $all_fields) {
  362. $fields = array();
  363. foreach ($all_fields as $key => $true) {
  364. $fields[$key]['type'] = 'string';
  365. }
  366. $fields = search_api_extract_fields($wrapper, $fields, array('sanitized' => TRUE));
  367. $ret = array();
  368. foreach ($all_fields as $key => $true) {
  369. $ret[$key] = isset($fields[$key]['value']) ? $fields[$key]['value'] : '';
  370. }
  371. return $ret;
  372. }
  373. /**
  374. * Returns the according entity objects for the given query results.
  375. *
  376. * This is necessary to support generic entity handlers and plugins with this
  377. * query backend.
  378. *
  379. * If the current query isn't based on an entity type, the method will return
  380. * an empty array.
  381. */
  382. public function get_result_entities($results, $relationship = NULL, $field = NULL) {
  383. list($type, $wrappers) = $this->get_result_wrappers($results, $relationship, $field);
  384. $return = array();
  385. foreach ($wrappers as $i => $wrapper) {
  386. try {
  387. // Get the entity ID beforehand for possible watchdog messages.
  388. $id = $wrapper->value(array('identifier' => TRUE));
  389. // Only add results that exist.
  390. if ($entity = $wrapper->value()) {
  391. $return[$i] = $entity;
  392. }
  393. else {
  394. watchdog('search_api_views', 'The search index returned a reference to an entity with ID @id, which does not exist in the database. Your index may be out of sync and should be rebuilt.', array('@id' => $id), WATCHDOG_ERROR);
  395. }
  396. }
  397. catch (EntityMetadataWrapperException $e) {
  398. watchdog_exception('search_api_views', $e, "%type while trying to load search result entity with ID @id: !message in %function (line %line of %file).", array('@id' => $id), WATCHDOG_ERROR);
  399. }
  400. }
  401. return array($type, $return);
  402. }
  403. /**
  404. * Returns the according metadata wrappers for the given query results.
  405. *
  406. * This is necessary to support generic entity handlers and plugins with this
  407. * query backend.
  408. */
  409. public function get_result_wrappers($results, $relationship = NULL, $field = NULL) {
  410. $entity_type = $this->index->getEntityType();
  411. $wrappers = array();
  412. $load_entities = array();
  413. foreach ($results as $row_index => $row) {
  414. if ($entity_type && isset($row->entity)) {
  415. // If this entity isn't load, register it for pre-loading.
  416. if (!is_object($row->entity)) {
  417. $load_entities[$row->entity] = $row_index;
  418. }
  419. $wrappers[$row_index] = $this->index->entityWrapper($row->entity);
  420. }
  421. }
  422. // If the results are entities, we pre-load them to make use of a multiple
  423. // load. (Otherwise, each result would be loaded individually.)
  424. if (!empty($load_entities)) {
  425. $entities = entity_load($entity_type, array_keys($load_entities));
  426. foreach ($entities as $entity_id => $entity) {
  427. $wrappers[$load_entities[$entity_id]] = $this->index->entityWrapper($entity);
  428. }
  429. }
  430. // Apply the relationship, if necessary.
  431. $type = $entity_type ? $entity_type : $this->index->item_type;
  432. $selector_suffix = '';
  433. if ($field && ($pos = strrpos($field, ':'))) {
  434. $selector_suffix = substr($field, 0, $pos);
  435. }
  436. if ($selector_suffix || ($relationship && !empty($this->view->relationship[$relationship]))) {
  437. // Use EntityFieldHandlerHelper to compute the correct data selector for
  438. // the relationship.
  439. $handler = (object) array(
  440. 'view' => $this->view,
  441. 'relationship' => $relationship,
  442. 'real_field' => '',
  443. );
  444. $selector = EntityFieldHandlerHelper::construct_property_selector($handler);
  445. $selector .= ($selector ? ':' : '') . $selector_suffix;
  446. list($type, $wrappers) = EntityFieldHandlerHelper::extract_property_multiple($wrappers, $selector);
  447. }
  448. return array($type, $wrappers);
  449. }
  450. /**
  451. * API function for accessing the raw Search API query object.
  452. *
  453. * @return SearchApiQueryInterface
  454. * The search query object used internally by this handler.
  455. */
  456. public function getSearchApiQuery() {
  457. return $this->query;
  458. }
  459. /**
  460. * API function for accessing the raw Search API results.
  461. *
  462. * @return array
  463. * An associative array containing the search results, as specified by
  464. * SearchApiQueryInterface::execute().
  465. */
  466. public function getSearchApiResults() {
  467. return $this->search_api_results;
  468. }
  469. //
  470. // Query interface methods (proxy to $this->query)
  471. //
  472. public function createFilter($conjunction = 'AND') {
  473. if (!$this->errors) {
  474. return $this->query->createFilter($conjunction);
  475. }
  476. }
  477. public function keys($keys = NULL) {
  478. if (!$this->errors) {
  479. $this->query->keys($keys);
  480. }
  481. return $this;
  482. }
  483. public function fields(array $fields) {
  484. if (!$this->errors) {
  485. $this->query->fields($fields);
  486. }
  487. return $this;
  488. }
  489. /**
  490. * Adds a nested filter to the search query object.
  491. *
  492. * If $group is given, the filter is added to the relevant filter group
  493. * instead.
  494. */
  495. public function filter(SearchApiQueryFilterInterface $filter, $group = NULL) {
  496. if (!$this->errors) {
  497. $this->where[$group]['filters'][] = $filter;
  498. }
  499. return $this;
  500. }
  501. /**
  502. * Set a condition on the search query object.
  503. *
  504. * If $group is given, the condition is added to the relevant filter group
  505. * instead.
  506. */
  507. public function condition($field, $value, $operator = '=', $group = NULL) {
  508. if (!$this->errors) {
  509. $this->where[$group]['conditions'][] = array($field, $value, $operator);
  510. }
  511. return $this;
  512. }
  513. public function sort($field, $order = 'ASC') {
  514. if (!$this->errors) {
  515. $this->query->sort($field, $order);
  516. }
  517. return $this;
  518. }
  519. public function range($offset = NULL, $limit = NULL) {
  520. if (!$this->errors) {
  521. $this->query->range($offset, $limit);
  522. }
  523. return $this;
  524. }
  525. public function getIndex() {
  526. return $this->index;
  527. }
  528. public function &getKeys() {
  529. if (!$this->errors) {
  530. return $this->query->getKeys();
  531. }
  532. $ret = NULL;
  533. return $ret;
  534. }
  535. public function getOriginalKeys() {
  536. if (!$this->errors) {
  537. return $this->query->getOriginalKeys();
  538. }
  539. }
  540. public function &getFields() {
  541. if (!$this->errors) {
  542. return $this->query->getFields();
  543. }
  544. $ret = NULL;
  545. return $ret;
  546. }
  547. public function getFilter() {
  548. if (!$this->errors) {
  549. return $this->query->getFilter();
  550. }
  551. }
  552. public function &getSort() {
  553. if (!$this->errors) {
  554. return $this->query->getSort();
  555. }
  556. $ret = NULL;
  557. return $ret;
  558. }
  559. public function getOption($name) {
  560. if (!$this->errors) {
  561. return $this->query->getOption($name);
  562. }
  563. }
  564. public function setOption($name, $value) {
  565. if (!$this->errors) {
  566. return $this->query->setOption($name, $value);
  567. }
  568. }
  569. public function &getOptions() {
  570. if (!$this->errors) {
  571. return $this->query->getOptions();
  572. }
  573. $ret = NULL;
  574. return $ret;
  575. }
  576. }