search_api_multi.query.inc 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863
  1. <?php
  2. /**
  3. * Interface representing a search query on multiple Search API indexes on the
  4. * same server. The server has to support the "search_api_multi" feature.
  5. *
  6. * For discerning from which index a certain field should be used (for filtering
  7. * or specifying the fulltext fields, fr instance), all field identifiers have
  8. * to be prefixed with their index' machine name, seperated by a colon.
  9. * For example, to filter on the "author:name" field from the index with the
  10. * machine name "default_node_index", use "default_node_index:author:name" as
  11. * the identifier.
  12. *
  13. * Methods not returning something else will return the object itself, so calls
  14. * can be chained.
  15. */
  16. interface SearchApiMultiQueryInterface {
  17. /**
  18. * Constructor used when creating SearchApiMultiQueryInterface objects.
  19. *
  20. * @param SearchApiServer $server
  21. * The server this search will be executed on.
  22. * @param array $options
  23. * Associative array of options configuring this query. Recognized options
  24. * are:
  25. * - conjunction: The type of conjunction to use for this query - either
  26. * 'AND' or 'OR'. 'AND' by default. This only influences the search keys,
  27. * filters will always use AND by default.
  28. * - 'parse mode': The mode with which to parse the $keys variable, if it
  29. * is set and not already an array. See SearchApiMultiQuery::parseModes() for
  30. * recognized parse modes.
  31. * - languages: The languages to search for, as an array of language IDs.
  32. * If not specified, all languages will be searched. Language-neutral
  33. * content (LANGUAGE_NONE) is always searched.
  34. * - offset: The position of the first returned search results relative to
  35. * the whole result on the server.
  36. * - limit: The maximum number of search results to return. -1 means no
  37. * limit.
  38. * - 'filter class': Can be used to change the SearchApiQueryFilterInterface
  39. * implementation to use.
  40. * - 'search id': A string that will be used as the identifier when storing
  41. * this search in the static cache.
  42. * All options are optional.
  43. *
  44. * @throws SearchApiException
  45. * If a search with these options won't be possible.
  46. */
  47. public function __construct(SearchApiServer $server, array $options = array());
  48. /**
  49. * @return array
  50. * An associative array of parse modes recognized by objects of this class.
  51. * The keys are the parse modes' ids, values are associative arrays
  52. * containing the following entries:
  53. * - name: The translated name of the parse mode.
  54. * - description: (optional) A translated text describing the parse mode.
  55. */
  56. public function parseModes();
  57. /**
  58. * Method for creating a filter to use with this query object.
  59. *
  60. * @param $conjunction
  61. * The conjunction to use for the filter - either 'AND' or 'OR'.
  62. *
  63. * @return SearchApiQueryFilterInterface
  64. * A filter object that is set to use the specified conjunction.
  65. */
  66. public function createFilter($conjunction = 'AND');
  67. /**
  68. * Sets the keys to search for. If this method is not called on the query
  69. * before execution, this will be a filter-only query.
  70. *
  71. * @param $keys
  72. * A string with the unparsed search keys, or NULL to use no search keys.
  73. *
  74. * @return SearchApiMultiQueryInterface
  75. * The called object.
  76. */
  77. public function keys($keys = NULL);
  78. /**
  79. * Sets the fields that will be searched for the search keys. If this is not
  80. * called, all fulltext fields should be searched.
  81. *
  82. * @param array $fields
  83. * An array containing fulltext fields that should be searched.
  84. *
  85. * @throws SearchApiException
  86. * If one of the fields isn't of type "text".
  87. *
  88. * @return SearchApiMultiQueryInterface
  89. * The called object.
  90. */
  91. public function fields(array $fields);
  92. /**
  93. * Adds a subfilter to this query's filter.
  94. *
  95. * @param SearchApiQueryFilterInterface $filter
  96. * A SearchApiQueryFilter object that should be added as a subfilter.
  97. *
  98. * @return SearchApiMultiQueryInterface
  99. * The called object.
  100. */
  101. public function filter(SearchApiQueryFilterInterface $filter);
  102. /**
  103. * Add a new ($field $operator $value) condition filter.
  104. *
  105. * @param $field
  106. * The field to filter on. Either a field specification as detailed in the
  107. * class comment, or the special field "search_api_multi_index" which means
  108. * a filter on the searched indexes.
  109. * @param $value
  110. * The value the field should have (or be related to by the operator).
  111. * @param $operator
  112. * The operator to use for checking the constraint. The following operators
  113. * are supported for primitive types: "=", "<>", "<", "<=", ">=", ">". They
  114. * have the same semantics as the corresponding SQL operators.
  115. * If $field is a fulltext field, $operator can only be "=" or "<>", which
  116. * are in this case interpreted as "contains" or "doesn't contain",
  117. * respectively.
  118. * If $value is NULL, $operator also can only be "=" or "<>", meaning the
  119. * field must have no or some value, respectively.
  120. *
  121. * @return SearchApiMultiQueryInterface
  122. * The called object.
  123. */
  124. public function condition($field, $value, $operator = '=');
  125. /**
  126. * Add a sort directive to this search query. If no sort is manually set, the
  127. * results will be sorted descending by relevance.
  128. *
  129. * How sorts on index-specific fields are handled may differ between service
  130. * backends.
  131. *
  132. * @param $field
  133. * The field to sort by. The special field "search_api_relevance" may be
  134. * used to sort by relevance.
  135. * @param $order
  136. * The order to sort items in - either 'ASC' or 'DESC'.
  137. *
  138. * @throws SearchApiException
  139. * If the field is multi-valued or of a fulltext type.
  140. *
  141. * @return SearchApiMultiQueryInterface
  142. * The called object.
  143. */
  144. public function sort($field, $order = 'ASC');
  145. /**
  146. * Adds a range of results to return. This will be saved in the query's
  147. * options. If called without parameters, this will remove all range
  148. * restrictions previously set.
  149. *
  150. * @param $offset
  151. * The zero-based offset of the first result returned.
  152. * @param $limit
  153. * The number of results to return.
  154. *
  155. * @return SearchApiMultiQueryInterface
  156. * The called object.
  157. */
  158. public function range($offset = NULL, $limit = NULL);
  159. /**
  160. * Executes this search query.
  161. *
  162. * @return array
  163. * An associative array containing the search results. The following keys
  164. * are standardized:
  165. * - 'result count': The overall number of results for this query, without
  166. * range restrictions. Might be approximated, for large numbers.
  167. * - results: An array of results, ordered as specified. The array keys are
  168. * the items' IDs, values are arrays containing the following keys:
  169. * - id: The item's ID.
  170. * - index_id: The machine name of the index this item was found on.
  171. * - score: A float measuring how well the item fits the search.
  172. * - entity (optional): If set, the fully loaded result item. This field
  173. * should always be used by modules using search results, to avoid
  174. * duplicate item loads.
  175. * - excerpt (optional): If set, an HTML text containing highlighted
  176. * portions of the fulltext that match the query.
  177. * - warnings: A numeric array of translated warning messages that may be
  178. * displayed to the user.
  179. * - ignored: A numeric array of search keys that were ignored for this
  180. * search (e.g., because of being too short or stop words).
  181. * - performance: An associative array with the time taken (as floats, in
  182. * seconds) for specific parts of the search execution:
  183. * - complete: The complete runtime of the query.
  184. * - hooks: Hook invocations and other client-side preprocessing.
  185. * - preprocessing: Preprocessing of the service class.
  186. * - execution: The actual query to the search server, in whatever form.
  187. * - postprocessing: Preparing the results for returning.
  188. * Additional metadata may be returned in other keys. Only 'result count'
  189. * and 'result' always have to be set, all other entries are optional.
  190. */
  191. public function execute();
  192. /**
  193. * @return SearchApiServer
  194. * The search server this query should be executed on.
  195. */
  196. public function getServer();
  197. /**
  198. * @return array
  199. * An array of SearchApiIndex objects representing all indexes that will be
  200. * searched.
  201. */
  202. public function getIndexes();
  203. /**
  204. * @return
  205. * This object's search keys - either a string or an array specifying a
  206. * complex search expression.
  207. * An array will contain a '#conjunction' key specifying the conjunction
  208. * type, and search strings or nested expression arrays at numeric keys.
  209. * Additionally, a '#negation' key might be present, which means – unless it
  210. * maps to a FALSE value – that the search keys contained in that array
  211. * should be negated, i.e. not be present in returned results.
  212. */
  213. public function &getKeys();
  214. /**
  215. * @return
  216. * The unprocessed search keys, exactly as passed to this object. Has the
  217. * same format as getKeys().
  218. */
  219. public function getOriginalKeys();
  220. /**
  221. * @return array
  222. * An array containing the fields that should be searched for the search
  223. * keys.
  224. */
  225. public function &getFields();
  226. /**
  227. * @return SearchApiQueryFilterInterface
  228. * This object's associated filter object.
  229. */
  230. public function getFilter();
  231. /**
  232. * @return array
  233. * An array specifying the sort order for this query. Array keys are the
  234. * field names in order of importance, the values are the respective order
  235. * in which to sort the results according to the field.
  236. */
  237. public function &getSort();
  238. /**
  239. * @param $name string
  240. * The name of an option.
  241. *
  242. * @return mixed
  243. * The value of the option with the specified name, if set. NULL otherwise.
  244. */
  245. public function getOption($name);
  246. /**
  247. * @param string $name
  248. * The name of an option.
  249. * @param mixed $value
  250. * The new value of the option.
  251. *
  252. * @return The option's previous value.
  253. */
  254. public function setOption($name, $value);
  255. /**
  256. * @return array
  257. * An associative array of query options.
  258. */
  259. public function &getOptions();
  260. }
  261. /**
  262. * Standard implementation of SearchApiMultiQueryInterface.
  263. */
  264. class SearchApiMultiQuery implements SearchApiMultiQueryInterface {
  265. /**
  266. * The server.
  267. *
  268. * @var SearchApiServer
  269. */
  270. protected $server;
  271. /**
  272. * The searched indexes.
  273. *
  274. * @var array
  275. */
  276. protected $indexes;
  277. /**
  278. * The search keys. If NULL, this will be a filter-only search.
  279. *
  280. * @var mixed
  281. */
  282. protected $keys;
  283. /**
  284. * The unprocessed search keys, as passed to the keys() method.
  285. *
  286. * @var mixed
  287. */
  288. protected $orig_keys;
  289. /**
  290. * The fields that will be searched for the keys.
  291. *
  292. * @var array
  293. */
  294. protected $fields;
  295. /**
  296. * The search filter associated with this query.
  297. *
  298. * @var SearchApiQueryFilterInterface
  299. */
  300. protected $filter;
  301. /**
  302. * The sort associated with this query.
  303. *
  304. * @var array
  305. */
  306. protected $sort;
  307. /**
  308. * Search options configuring this query.
  309. *
  310. * @var array
  311. */
  312. protected $options;
  313. /**
  314. * Count for providing a unique ID.
  315. */
  316. protected static $count = 0;
  317. /**
  318. * Constructor for SearchApiMultiQuery objects.
  319. *
  320. * @param SearchApiIndex $server
  321. * The server the query should be executed on.
  322. * @param array $options
  323. * Associative array of options configuring this query. Recognized options
  324. * are:
  325. * - conjunction: The type of conjunction to use for this query - either
  326. * 'AND' or 'OR'. 'AND' by default. This only influences the search keys,
  327. * filters will always use AND by default.
  328. * - 'parse mode': The mode with which to parse the $keys variable, if it
  329. * is set and not already an array. See SearchApiMultiQuery::parseModes() for
  330. * recognized parse modes.
  331. * - languages: The languages to search for, as an array of language IDs.
  332. * If not specified, all languages will be searched. Language-neutral
  333. * content (LANGUAGE_NONE) is always searched.
  334. * - offset: The position of the first returned search results relative to
  335. * the whole result on the server.
  336. * - limit: The maximum number of search results to return. -1 means no
  337. * limit.
  338. * - 'filter class': Can be used to change the SearchApiQueryFilterInterface
  339. * implementation to use.
  340. * - 'search id': A string that will be used as the identifier when storing
  341. * this search in the Search API's static cache.
  342. * All options are optional.
  343. *
  344. * @throws SearchApiException
  345. * If a search with these options won't be possible.
  346. */
  347. public function __construct(SearchApiServer $server, array $options = array()) {
  348. if (!$server->supportsFeature('search_api_multi')) {
  349. throw new SearchApiException(t("The search server @name doesn't support multi-index searches.", array('@name' => $server->name)));
  350. }
  351. $this->server = $server;
  352. $this->indexes = search_api_index_load_multiple(FALSE, array('server' => $server->machine_name, 'enabled' => 1));
  353. if (empty($this->indexes)) {
  354. throw new SearchApiException(t('There are no enabled indexes on the searched server @name.', array('@name' => $server->name)));
  355. }
  356. foreach ($this->indexes as $index) {
  357. if (empty($index->options['fields'])) {
  358. throw new SearchApiException(t("Can't search an index which hasn't got any fields defined."));
  359. }
  360. if (isset($options['parse mode'])) {
  361. $modes = $this->parseModes();
  362. if (!isset($modes[$options['parse mode']])) {
  363. throw new SearchApiException(t('Unknown parse mode: @mode.', array('@mode' => $options['parse mode'])));
  364. }
  365. }
  366. }
  367. $this->options = $options + array(
  368. 'conjunction' => 'AND',
  369. 'parse mode' => 'terms',
  370. 'filter class' => 'SearchApiQueryFilter',
  371. 'search id' => __CLASS__,
  372. );
  373. $this->filter = $this->createFilter('AND');
  374. $this->sort = array();
  375. }
  376. /**
  377. * @return array
  378. * An associative array of parse modes recognized by objects of this class.
  379. * The keys are the parse modes' ids, values are associative arrays
  380. * containing the following entries:
  381. * - name: The translated name of the parse mode.
  382. * - description: (optional) A translated text describing the parse mode.
  383. */
  384. public function parseModes() {
  385. $modes['direct'] = array(
  386. 'name' => t('Direct query'),
  387. 'description' => t("Don't parse the query, just hand it to the search server unaltered. " .
  388. "Might fail if the query contains syntax errors in regard to the specific server's query syntax."),
  389. );
  390. $modes['single'] = array(
  391. 'name' => t('Single term'),
  392. 'description' => t('The query is interpreted as a single keyword, maybe containing spaces or special characters.'),
  393. );
  394. $modes['terms'] = array(
  395. 'name' => t('Multiple terms'),
  396. 'description' => t('The query is interpreted as multiple keywords seperated by spaces. ' .
  397. 'Keywords containing spaces may be "quoted". Quoted keywords must still be seperated by spaces.'),
  398. );
  399. return $modes;
  400. }
  401. /**
  402. * Parses the keys string according to the $mode parameter.
  403. *
  404. * @return
  405. * The parsed keys. Either a string or an array.
  406. */
  407. protected function parseKeys($keys, $mode) {
  408. if ($keys == NULL || is_array($keys)) {
  409. return $keys;
  410. }
  411. $keys = '' . $keys;
  412. switch ($mode) {
  413. case 'direct':
  414. return $keys;
  415. case 'single':
  416. return array('#conjunction' => $this->options['conjunction'], $keys);
  417. case 'terms':
  418. $ret = explode(' ', $keys);
  419. $ret['#conjunction'] = $this->options['conjunction'];
  420. $quoted = FALSE;
  421. $str = '';
  422. foreach ($ret as $k => $v) {
  423. if (!$v) {
  424. continue;
  425. }
  426. if ($quoted) {
  427. if ($v[drupal_strlen($v)-1] == '"') {
  428. $v = substr($v, 0, -1);
  429. $str .= ' ' . $v;
  430. $ret[$k] = $str;
  431. $quoted = FALSE;
  432. }
  433. else {
  434. $str .= ' ' . $v;
  435. unset($ret[$k]);
  436. }
  437. }
  438. elseif ($v[0] == '"') {
  439. $len = drupal_strlen($v);
  440. if ($len > 1 && $v[$len-1] == '"') {
  441. $ret[$k] = substr($v, 1, -1);
  442. }
  443. else {
  444. $str = substr($v, 1);
  445. $quoted = TRUE;
  446. unset($ret[$k]);
  447. }
  448. }
  449. }
  450. if ($quoted) {
  451. $ret[] = $str;
  452. }
  453. return array_filter($ret);
  454. }
  455. }
  456. /**
  457. * Method for creating a filter to use with this query object.
  458. *
  459. * @param $conjunction
  460. * The conjunction to use for the filter - either 'AND' or 'OR'.
  461. *
  462. * @return SearchApiQueryFilterInterface
  463. * A filter object that is set to use the specified conjunction.
  464. */
  465. public function createFilter($conjunction = 'AND') {
  466. $filter_class = $this->options['filter class'];
  467. return new $filter_class($conjunction);
  468. }
  469. /**
  470. * Sets the keys to search for. If this method is not called on the query
  471. * before execution, this will be a filter-only query.
  472. *
  473. * @param $keys
  474. * A string with the unparsed search keys, or NULL to use no search keys.
  475. *
  476. * @return SearchApiMultiQuery
  477. * The called object.
  478. */
  479. public function keys($keys = NULL) {
  480. $this->orig_keys = $keys;
  481. if (isset($keys)) {
  482. $this->keys = $this->parseKeys($keys, $this->options['parse mode']);
  483. }
  484. else {
  485. $this->keys = NULL;
  486. }
  487. return $this;
  488. }
  489. /**
  490. * Sets the fields that will be searched for the search keys. If this is not
  491. * called, all fulltext fields should be searched.
  492. *
  493. * @param array $fields
  494. * An array containing fulltext fields that should be searched.
  495. *
  496. * @throws SearchApiException
  497. * If one of the fields isn't of type "text".
  498. *
  499. * @return SearchApiMultiQueryInterface
  500. * The called object.
  501. */
  502. public function fields(array $fields) {
  503. foreach ($fields as $spec) {
  504. list($index_id, $field) = explode(':', $spec, 2);
  505. if (!isset($this->indexes[$index_id])) {
  506. throw new SearchApiException(t("Trying to search on fields of index @index which doesn't lie on server @server.", array('@index' => $index_id, '@server' => $this->server->name)));
  507. }
  508. if (empty($this->indexes[$index_id]->options['fields'][$field]) || !search_api_is_text_type($this->indexes[$index_id]->options['fields'][$field]['type'])) {
  509. throw new SearchApiException(t('Trying to search on field @field which is no indexed fulltext field.', array('@field' => $field)));
  510. }
  511. }
  512. $this->fields = $fields;
  513. return $this;
  514. }
  515. /**
  516. * Adds a subfilter to this query's filter.
  517. *
  518. * @param SearchApiQueryFilterInterface $filter
  519. * A SearchApiQueryFilter object that should be added as a subfilter.
  520. *
  521. * @return SearchApiMultiQuery
  522. * The called object.
  523. */
  524. public function filter(SearchApiQueryFilterInterface $filter) {
  525. $this->filter->filter($filter);
  526. return $this;
  527. }
  528. /**
  529. * Add a new ($field $operator $value) condition filter.
  530. *
  531. * @param $field
  532. * The field to filter on, e.g. 'title'.
  533. * @param $value
  534. * The value the field should have (or be related to by the operator).
  535. * @param $operator
  536. * The operator to use for checking the constraint. The following operators
  537. * are supported for primitive types: "=", "<>", "<", "<=", ">=", ">". They
  538. * have the same semantics as the corresponding SQL operators.
  539. * If $field is a fulltext field, $operator can only be "=" or "<>", which
  540. * are in this case interpreted as "contains" or "doesn't contain",
  541. * respectively.
  542. * If $value is NULL, $operator also can only be "=" or "<>", meaning the
  543. * field must have no or some value, respectively.
  544. *
  545. * @return SearchApiMultiQuery
  546. * The called object.
  547. */
  548. public function condition($field, $value, $operator = '=') {
  549. $this->filter->condition($field, $value, $operator);
  550. return $this;
  551. }
  552. /**
  553. * Add a sort directive to this search query. If no sort is manually set, the
  554. * results will be sorted descending by relevance.
  555. *
  556. * @param $field
  557. * The field to sort by. The special fields 'search_api_relevance' (sort by
  558. * relevance) and 'search_api_id' (sort by item id) may be used.
  559. * @param $order
  560. * The order to sort items in - either 'ASC' or 'DESC'.
  561. *
  562. * @throws SearchApiException
  563. * If the field is multi-valued or of a fulltext type.
  564. *
  565. * @return SearchApiMultiQuery
  566. * The called object.
  567. */
  568. public function sort($field, $order = 'ASC') {
  569. if ($field != 'search_api_relevance' && $field != 'search_api_id') {
  570. list($index_id, $f) = explode(':', $field, 2);
  571. if (!isset($this->indexes[$index_id])) {
  572. throw new SearchApiException(t("Trying to search on fields of index @index which doesn't lie on server @server.", array('@index' => $index_id, '@server' => $this->server->name)));
  573. }
  574. $fields = $this->indexes[$index_id]->options['fields'];
  575. $fields += array(
  576. 'search_api_relevance' => array('type' => 'decimal'),
  577. 'search_api_id' => array('type' => 'integer'),
  578. );
  579. if (empty($fields[$f])) {
  580. throw new SearchApiException(t('Trying to sort on unknown field @field.', array('@field' => $f)));
  581. }
  582. $type = $fields[$f]['type'];
  583. if (search_api_is_list_type($type) || search_api_is_text_type($type)) {
  584. throw new SearchApiException(t('Trying to sort on field @field of illegal type @type.', array('@field' => $f, '@type' => $type)));
  585. }
  586. }
  587. $order = strtoupper(trim($order)) == 'DESC' ? 'DESC' : 'ASC';
  588. $this->sort[$field] = $order;
  589. return $this;
  590. }
  591. /**
  592. * Adds a range of results to return. This will be saved in the query's
  593. * options. If called without parameters, this will remove all range
  594. * restrictions previously set.
  595. *
  596. * @param $offset
  597. * The zero-based offset of the first result returned.
  598. * @param $limit
  599. * The number of results to return.
  600. *
  601. * @return SearchApiMultiQueryInterface
  602. * The called object.
  603. */
  604. public function range($offset = NULL, $limit = NULL) {
  605. $this->options['offset'] = $offset;
  606. $this->options['limit'] = $limit;
  607. return $this;
  608. }
  609. /**
  610. * Executes this search query.
  611. *
  612. * @return array
  613. * An associative array containing the search results. The following keys
  614. * are standardized:
  615. * - 'result count': The overall number of results for this query, without
  616. * range restrictions. Might be approximated, for large numbers.
  617. * - results: An array of results, ordered as specified. The array keys are
  618. * the items' IDs, values are arrays containing the following keys:
  619. * - id: The item's ID.
  620. * - index_id: The machine name of the index this item was found on.
  621. * - score: A float measuring how well the item fits the search.
  622. * - entity (optional): If set, the fully loaded result item. This field
  623. * should always be used by modules using search results, to avoid
  624. * duplicate item loads.
  625. * - excerpt (optional): If set, an HTML text containing highlighted
  626. * portions of the fulltext that match the query.
  627. * - warnings: A numeric array of translated warning messages that may be
  628. * displayed to the user.
  629. * - ignored: A numeric array of search keys that were ignored for this
  630. * search (e.g., because of being too short or stop words).
  631. * - performance: An associative array with the time taken (as floats, in
  632. * seconds) for specific parts of the search execution:
  633. * - complete: The complete runtime of the query.
  634. * - hooks: Hook invocations and other client-side preprocessing.
  635. * - preprocessing: Preprocessing of the service class.
  636. * - execution: The actual query to the search server, in whatever form.
  637. * - postprocessing: Preparing the results for returning.
  638. * Additional metadata may be returned in other keys. Only 'result count'
  639. * and 'result' always have to be set, all other entries are optional.
  640. */
  641. public final function execute() {
  642. $start = microtime(TRUE);
  643. // Add filter for languages.
  644. if (isset($this->options['languages'])) {
  645. $this->addLanguages($this->options['languages']);
  646. }
  647. // Add fulltext fields, unless set
  648. if ($this->fields === NULL) {
  649. $this->fields = array();
  650. foreach ($this->indexes as $index) {
  651. $prefix = $index->machine_name . ':';
  652. foreach ($index->getFulltextFields() as $f) {
  653. $this->fields[] = $prefix . $f;
  654. }
  655. }
  656. }
  657. // Call pre-execute hook.
  658. $this->preExecute();
  659. // Let modules alter the query.
  660. drupal_alter('search_api_multi_query', $this);
  661. $pre_search = microtime(TRUE);
  662. // Execute query.
  663. $response = $this->server->searchMultiple($this);
  664. $post_search = microtime(TRUE);
  665. // Call post-execute hook.
  666. $this->postExecute($response);
  667. $end = microtime(TRUE);
  668. $response['performance']['complete'] = $end - $start;
  669. $response['performance']['hooks'] = $response['performance']['complete'] - ($post_search - $pre_search);
  670. // Store search for later retrieval for facets, etc.
  671. search_api_multi_current_search(NULL, $this, $response);
  672. return $response;
  673. }
  674. /**
  675. * Helper method for adding a language filter.
  676. */
  677. protected function addLanguages(array $languages) {
  678. if (array_search(LANGUAGE_NONE, $languages) === FALSE) {
  679. $languages[] = LANGUAGE_NONE;
  680. }
  681. if (count($languages) == 1) {
  682. $this->condition('search_api_language', reset($languages));
  683. }
  684. else {
  685. $filter = $this->createFilter('OR');
  686. foreach ($languages as $lang) {
  687. $filter->condition('search_api_language', $lang);
  688. }
  689. $this->filter($filter);
  690. }
  691. }
  692. /**
  693. * Pre-execute hook for modifying search behaviour.
  694. */
  695. protected function preExecute() {}
  696. /**
  697. * Post-execute hook for modifying search behaviour.
  698. *
  699. * @param array $results
  700. * The results returned by the server, which may be altered.
  701. */
  702. protected function postExecute(array &$results) {}
  703. /**
  704. * @return SearchApiIndex
  705. * The search index this query will be executed on.
  706. */
  707. public function getServer() {
  708. return $this->server;
  709. }
  710. /**
  711. * @return array
  712. * An array of SearchApiIndex objects representing all indexes that will be
  713. * searched.
  714. */
  715. public function getIndexes() {
  716. return $this->indexes;
  717. }
  718. /**
  719. * @return
  720. * This object's search keys - either a string or an array specifying a
  721. * complex search expression.
  722. * An array will contain a '#conjunction' key specifying the conjunction
  723. * type, and search strings or nested expression arrays at numeric keys.
  724. * Additionally, a '#negation' key might be present, which means – unless it
  725. * maps to a FALSE value – that the search keys contained in that array
  726. * should be negated, i.e. not be present in returned results.
  727. */
  728. public function &getKeys() {
  729. return $this->keys;
  730. }
  731. /**
  732. * @return
  733. * The unprocessed search keys, exactly as passed to this object. Has the
  734. * same format as getKeys().
  735. */
  736. public function getOriginalKeys() {
  737. return $this->orig_keys;
  738. }
  739. /**
  740. * @return array
  741. * An array containing the fields that should be searched for the search
  742. * keys.
  743. */
  744. public function &getFields() {
  745. return $this->fields;
  746. }
  747. /**
  748. * @return SearchApiQueryFilterInterface
  749. * This object's associated filter object.
  750. */
  751. public function getFilter() {
  752. return $this->filter;
  753. }
  754. /**
  755. * @return array
  756. * An array specifying the sort order for this query. Array keys are the
  757. * field names in order of importance, the values are the respective order
  758. * in which to sort the results according to the field.
  759. */
  760. public function &getSort() {
  761. return $this->sort;
  762. }
  763. /**
  764. * @param $name string
  765. * The name of an option.
  766. *
  767. * @return mixed
  768. * The option with the specified name, if set, or NULL otherwise.
  769. */
  770. public function getOption($name) {
  771. return isset($this->options[$name]) ? $this->options[$name] : NULL;
  772. }
  773. /**
  774. * @param string $name
  775. * The name of an option.
  776. * @param mixed $value
  777. * The new value of the option.
  778. *
  779. * @return The option's previous value.
  780. */
  781. public function setOption($name, $value) {
  782. $old = $this->getOption($name);
  783. $this->options[$name] = $value;
  784. return $old;
  785. }
  786. /**
  787. * @return array
  788. * An associative array of query options.
  789. */
  790. public function &getOptions() {
  791. return $this->options;
  792. }
  793. }