query.inc 32 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085
  1. <?php
  2. /**
  3. * @file
  4. * Contains SearchApiQueryInterface and SearchApiQuery.
  5. */
  6. /**
  7. * Interface representing a search query on an Search API index.
  8. *
  9. * Methods not returning something else will return the object itself, so calls
  10. * can be chained.
  11. */
  12. interface SearchApiQueryInterface {
  13. /**
  14. * Constructs a new search query.
  15. *
  16. * @param SearchApiIndex $index
  17. * The index the query should be executed on.
  18. * @param array $options
  19. * Associative array of options configuring this query. Recognized options
  20. * are:
  21. * - conjunction: The type of conjunction to use for this query - either
  22. * 'AND' or 'OR'. 'AND' by default. This only influences the search keys,
  23. * filters will always use AND by default.
  24. * - 'parse mode': The mode with which to parse the $keys variable, if it
  25. * is set and not already an array. See SearchApiQuery::parseModes() for
  26. * recognized parse modes.
  27. * - languages: The languages to search for, as an array of language IDs.
  28. * If not specified, all languages will be searched. Language-neutral
  29. * content (LANGUAGE_NONE) is always searched.
  30. * - offset: The position of the first returned search results relative to
  31. * the whole result in the index.
  32. * - limit: The maximum number of search results to return. -1 means no
  33. * limit.
  34. * - 'filter class': Can be used to change the SearchApiQueryFilterInterface
  35. * implementation to use.
  36. * - 'search id': A string that will be used as the identifier when storing
  37. * this search in the Search API's static cache.
  38. * - 'skip result count': If present and set to TRUE, the result's
  39. * "result count" key will not be needed. Service classes can check for
  40. * this option to possibly avoid executing expensive operations to compute
  41. * the result count in cases where it is not needed.
  42. * - search_api_access_account: The account which will be used for entity
  43. * access checks, if available and enabled for the index.
  44. * - search_api_bypass_access: If set to TRUE, entity access checks will be
  45. * skipped, even if enabled for the index.
  46. * All options are optional. Third-party modules might define and use other
  47. * options not listed here.
  48. *
  49. * @throws SearchApiException
  50. * If a search on that index (or with those options) won't be possible.
  51. */
  52. public function __construct(SearchApiIndex $index, array $options = array());
  53. /**
  54. * Retrieves the parse modes supported by this query class.
  55. *
  56. * @return array
  57. * An associative array of parse modes recognized by objects of this class.
  58. * The keys are the parse modes' ids, values are associative arrays
  59. * containing the following entries:
  60. * - name: The translated name of the parse mode.
  61. * - description: (optional) A translated text describing the parse mode.
  62. */
  63. public function parseModes();
  64. /**
  65. * Creates a new filter to use with this query object.
  66. *
  67. * @param string $conjunction
  68. * The conjunction to use for the filter - either 'AND' or 'OR'.
  69. * @param $tags
  70. * (Optional) An arbitrary set of tags. Can be used to identify this filter
  71. * down the line if necessary. This is primarily used by the facet system
  72. * to support OR facet queries.
  73. *
  74. * @return SearchApiQueryFilterInterface
  75. * A filter object that is set to use the specified conjunction.
  76. */
  77. public function createFilter($conjunction = 'AND', $tags = array());
  78. /**
  79. * Sets the keys to search for.
  80. *
  81. * If this method is not called on the query before execution, this will be a
  82. * filter-only query.
  83. *
  84. * @param array|string|null $keys
  85. * A string with the unparsed search keys, or NULL to use no search keys.
  86. *
  87. * @return SearchApiQueryInterface
  88. * The called object.
  89. */
  90. public function keys($keys = NULL);
  91. /**
  92. * Sets the fields that will be searched for the search keys.
  93. *
  94. * If this is not called, all fulltext fields will be searched.
  95. *
  96. * @param array $fields
  97. * An array containing fulltext fields that should be searched.
  98. *
  99. * @return SearchApiQueryInterface
  100. * The called object.
  101. *
  102. * @throws SearchApiException
  103. * If one of the fields isn't of type "text".
  104. */
  105. // @todo Allow calling with NULL.
  106. public function fields(array $fields);
  107. /**
  108. * Adds a subfilter to this query's filter.
  109. *
  110. * @param SearchApiQueryFilterInterface $filter
  111. * A SearchApiQueryFilter object that should be added as a subfilter.
  112. *
  113. * @return SearchApiQueryInterface
  114. * The called object.
  115. */
  116. public function filter(SearchApiQueryFilterInterface $filter);
  117. /**
  118. * Adds a new ($field $operator $value) condition filter.
  119. *
  120. * @param string $field
  121. * The field to filter on, e.g. 'title'.
  122. * @param mixed $value
  123. * The value the field should have (or be related to by the operator).
  124. * @param string $operator
  125. * The operator to use for checking the constraint. The following operators
  126. * are supported for primitive types: "=", "<>", "<", "<=", ">=", ">". They
  127. * have the same semantics as the corresponding SQL operators.
  128. * If $field is a fulltext field, $operator can only be "=" or "<>", which
  129. * are in this case interpreted as "contains" or "doesn't contain",
  130. * respectively.
  131. * If $value is NULL, $operator also can only be "=" or "<>", meaning the
  132. * field must have no or some value, respectively.
  133. *
  134. * @return SearchApiQueryInterface
  135. * The called object.
  136. */
  137. public function condition($field, $value, $operator = '=');
  138. /**
  139. * Adds a sort directive to this search query.
  140. *
  141. * If no sort is manually set, the results will be sorted descending by
  142. * relevance.
  143. *
  144. * @param string $field
  145. * The field to sort by. The special fields 'search_api_relevance' (sort by
  146. * relevance) and 'search_api_id' (sort by item id) may be used. Also, if
  147. * the search server supports the "search_api_random_sort" feature, the
  148. * "search_api_random" special field can be used to sort randomly.
  149. * @param string $order
  150. * The order to sort items in - either 'ASC' or 'DESC'.
  151. *
  152. * @return SearchApiQueryInterface
  153. * The called object.
  154. *
  155. * @throws SearchApiException
  156. * If the field is multi-valued or of a fulltext type.
  157. */
  158. public function sort($field, $order = 'ASC');
  159. /**
  160. * Adds a range of results to return.
  161. *
  162. * This will be saved in the query's options. If called without parameters,
  163. * this will remove all range restrictions previously set.
  164. *
  165. * @param int|null $offset
  166. * The zero-based offset of the first result returned.
  167. * @param int|null $limit
  168. * The number of results to return.
  169. *
  170. * @return SearchApiQueryInterface
  171. * The called object.
  172. */
  173. public function range($offset = NULL, $limit = NULL);
  174. /**
  175. * Executes this search query.
  176. *
  177. * @return array
  178. * An associative array containing the search results. The following keys
  179. * are standardized:
  180. * - 'result count': The overall number of results for this query, without
  181. * range restrictions. Might be approximated, for large numbers, or
  182. * skipped entirely if the "skip result count" option was set on this
  183. * query.
  184. * - results: An array of results, ordered as specified. The array keys are
  185. * the items' IDs, values are arrays containing the following keys:
  186. * - id: The item's ID.
  187. * - score: A float measuring how well the item fits the search.
  188. * - fields: (optional) If set, an array containing some field values
  189. * already ready-to-use. This allows search engines (or postprocessors)
  190. * to store extracted fields so other modules don't have to extract them
  191. * again. This fields should always be checked by modules that want to
  192. * use field contents of the result items. The format of the array is
  193. * field IDs (as used by the Search API internally) mapped to either the
  194. * raw value of the field (scalar or array value), or an associative
  195. * array with the following keys:
  196. * - #value: The raw field value.
  197. * - #sanitize_callback: The callback to use for sanitizing the field
  198. * value for HTML output, or FALSE to state that the field value is
  199. * already sanitized.
  200. * In the simple form, it's assumed the field value should be sanitized
  201. * with check_plain().
  202. * - entity: (optional) If set, the fully loaded result item. This field
  203. * should always be used by modules using search results, to avoid
  204. * duplicate item loads.
  205. * - excerpt: (optional) If set, an HTML text containing highlighted
  206. * portions of the fulltext that match the query.
  207. * - warnings: A numeric array of translated warning messages that may be
  208. * displayed to the user.
  209. * - ignored: A numeric array of search keys that were ignored for this
  210. * search (e.g., because of being too short or stop words).
  211. * - performance: An associative array with the time taken (as floats, in
  212. * seconds) for specific parts of the search execution:
  213. * - complete: The complete runtime of the query.
  214. * - hooks: Hook invocations and other client-side preprocessing.
  215. * - preprocessing: Preprocessing of the service class.
  216. * - execution: The actual query to the search server, in whatever form.
  217. * - postprocessing: Preparing the results for returning.
  218. * Additional metadata may be returned in other keys. Only 'result count'
  219. * and 'results' always have to be set, all other entries are optional.
  220. *
  221. * @throws SearchApiException
  222. * If an error prevented the search from completing.
  223. */
  224. public function execute();
  225. /**
  226. * Prepares the query object for the search.
  227. *
  228. * This method should always be called by execute() and contain all necessary
  229. * operations before the query is passed to the server's search() method.
  230. *
  231. * @throws SearchApiException
  232. * If any error occurred during the preparation of the query.
  233. */
  234. public function preExecute();
  235. /**
  236. * Postprocesses the search results before they are returned.
  237. *
  238. * This method should always be called by execute() and contain all necessary
  239. * operations after the results are returned from the server.
  240. *
  241. * @param array $results
  242. * The results returned by the server, which may be altered. The data
  243. * structure is the same as returned by execute().
  244. */
  245. public function postExecute(array &$results);
  246. /**
  247. * Retrieves the index associated with this search.
  248. *
  249. * @return SearchApiIndex
  250. * The search index this query should be executed on.
  251. */
  252. public function getIndex();
  253. /**
  254. * Retrieves the search keys for this query.
  255. *
  256. * @return array|string|null
  257. * This object's search keys - either a string or an array specifying a
  258. * complex search expression.
  259. * An array will contain a '#conjunction' key specifying the conjunction
  260. * type, and search strings or nested expression arrays at numeric keys.
  261. * Additionally, a '#negation' key might be present, which means – unless it
  262. * maps to a FALSE value – that the search keys contained in that array
  263. * should be negated, i.e. not be present in returned results. The negation
  264. * works on the whole array, not on each contained term individually – i.e.,
  265. * with the "AND" conjunction and negation, only results that contain all
  266. * the terms in the array should be excluded; with the "OR" conjunction and
  267. * negation, all results containing one or more of the terms in the array
  268. * should be excluded.
  269. *
  270. * @see keys()
  271. */
  272. public function &getKeys();
  273. /**
  274. * Retrieves the unparsed search keys for this query as originally entered.
  275. *
  276. * @return array|string|null
  277. * The unprocessed search keys, exactly as passed to this object. Has the
  278. * same format as the return value of getKeys().
  279. *
  280. * @see keys()
  281. */
  282. public function getOriginalKeys();
  283. /**
  284. * Retrieves the fulltext fields that will be searched for the search keys.
  285. *
  286. * @return array
  287. * An array containing the fields that should be searched for the search
  288. * keys.
  289. *
  290. * @see fields()
  291. */
  292. public function &getFields();
  293. /**
  294. * Retrieves the filter object associated with this search query.
  295. *
  296. * @return SearchApiQueryFilterInterface
  297. * This object's associated filter object.
  298. */
  299. public function getFilter();
  300. /**
  301. * Retrieves the sorts set for this query.
  302. *
  303. * @return array
  304. * An array specifying the sort order for this query. Array keys are the
  305. * field names in order of importance, the values are the respective order
  306. * in which to sort the results according to the field.
  307. *
  308. * @see sort()
  309. */
  310. public function &getSort();
  311. /**
  312. * Retrieves an option set on this search query.
  313. *
  314. * @param string $name
  315. * The name of an option.
  316. * @param mixed $default
  317. * The value to return if the specified option is not set.
  318. *
  319. * @return mixed
  320. * The value of the option with the specified name, if set. NULL otherwise.
  321. */
  322. public function getOption($name, $default = NULL);
  323. /**
  324. * Sets an option for this search query.
  325. *
  326. * @param string $name
  327. * The name of an option.
  328. * @param mixed $value
  329. * The new value of the option.
  330. *
  331. * @return mixed
  332. * The option's previous value.
  333. */
  334. public function setOption($name, $value);
  335. /**
  336. * Retrieves all options set for this search query.
  337. *
  338. * The return value is a reference to the options so they can also be altered
  339. * this way.
  340. *
  341. * @return array
  342. * An associative array of query options.
  343. */
  344. public function &getOptions();
  345. }
  346. /**
  347. * Provides a standard implementation of the SearchApiQueryInterface.
  348. */
  349. class SearchApiQuery implements SearchApiQueryInterface {
  350. /**
  351. * The index this query will use.
  352. *
  353. * @var SearchApiIndex
  354. */
  355. protected $index;
  356. /**
  357. * The index's machine name.
  358. *
  359. * Used during serialization to avoid serializing the whole index object.
  360. *
  361. * @var string
  362. */
  363. protected $index_id;
  364. /**
  365. * The search keys. If NULL, this will be a filter-only search.
  366. *
  367. * @var mixed
  368. */
  369. protected $keys;
  370. /**
  371. * The unprocessed search keys, as passed to the keys() method.
  372. *
  373. * @var mixed
  374. */
  375. protected $orig_keys;
  376. /**
  377. * The fields that will be searched for the keys.
  378. *
  379. * @var array
  380. */
  381. protected $fields;
  382. /**
  383. * The search filter associated with this query.
  384. *
  385. * @var SearchApiQueryFilterInterface
  386. */
  387. protected $filter;
  388. /**
  389. * The sort associated with this query.
  390. *
  391. * @var array
  392. */
  393. protected $sort;
  394. /**
  395. * Search options configuring this query.
  396. *
  397. * @var array
  398. */
  399. protected $options;
  400. /**
  401. * Flag for whether preExecute() was already called for this query.
  402. *
  403. * @var bool
  404. */
  405. protected $pre_execute = FALSE;
  406. /**
  407. * {@inheritdoc}
  408. */
  409. public function __construct(SearchApiIndex $index, array $options = array()) {
  410. if (empty($index->options['fields'])) {
  411. throw new SearchApiException(t("Can't search an index which hasn't got any fields defined."));
  412. }
  413. if (empty($index->enabled)) {
  414. throw new SearchApiException(t("Can't search a disabled index."));
  415. }
  416. if (isset($options['parse mode'])) {
  417. $modes = $this->parseModes();
  418. if (!isset($modes[$options['parse mode']])) {
  419. throw new SearchApiException(t('Unknown parse mode: @mode.', array('@mode' => $options['parse mode'])));
  420. }
  421. }
  422. $this->index = $index;
  423. $this->options = $options + array(
  424. 'conjunction' => 'AND',
  425. 'parse mode' => 'terms',
  426. 'filter class' => 'SearchApiQueryFilter',
  427. 'search id' => __CLASS__,
  428. );
  429. $this->filter = $this->createFilter('AND');
  430. $this->sort = array();
  431. }
  432. /**
  433. * {@inheritdoc}
  434. */
  435. public function parseModes() {
  436. $modes['direct'] = array(
  437. 'name' => t('Direct query'),
  438. 'description' => t("Don't parse the query, just hand it to the search server unaltered. " .
  439. "Might fail if the query contains syntax errors in regard to the specific server's query syntax."),
  440. );
  441. $modes['single'] = array(
  442. 'name' => t('Single term'),
  443. 'description' => t('The query is interpreted as a single keyword, maybe containing spaces or special characters.'),
  444. );
  445. $modes['terms'] = array(
  446. 'name' => t('Multiple terms'),
  447. 'description' => t('The query is interpreted as multiple keywords separated by spaces. ' .
  448. 'Keywords containing spaces may be "quoted". Quoted keywords must still be separated by spaces.'),
  449. );
  450. // @todo Add fourth mode for complicated expressions, e.g.: »"vanilla ice" OR (love NOT hate)«
  451. return $modes;
  452. }
  453. /**
  454. * {@inheritdoc}
  455. */
  456. protected function parseKeys($keys, $mode) {
  457. if ($keys === NULL || is_array($keys)) {
  458. return $keys;
  459. }
  460. $keys = '' . $keys;
  461. switch ($mode) {
  462. case 'direct':
  463. return $keys;
  464. case 'single':
  465. return array('#conjunction' => $this->options['conjunction'], $keys);
  466. case 'terms':
  467. $ret = preg_split('/\s+/u', $keys);
  468. $quoted = FALSE;
  469. $str = '';
  470. foreach ($ret as $k => $v) {
  471. if (!$v) {
  472. continue;
  473. }
  474. if ($quoted) {
  475. if (substr($v, -1) == '"') {
  476. $v = substr($v, 0, -1);
  477. $str .= ' ' . $v;
  478. $ret[$k] = $str;
  479. $quoted = FALSE;
  480. }
  481. else {
  482. $str .= ' ' . $v;
  483. unset($ret[$k]);
  484. }
  485. }
  486. elseif ($v[0] == '"') {
  487. $len = strlen($v);
  488. if ($len > 1 && $v[$len-1] == '"') {
  489. $ret[$k] = substr($v, 1, -1);
  490. }
  491. else {
  492. $str = substr($v, 1);
  493. $quoted = TRUE;
  494. unset($ret[$k]);
  495. }
  496. }
  497. }
  498. if ($quoted) {
  499. $ret[] = $str;
  500. }
  501. $ret['#conjunction'] = $this->options['conjunction'];
  502. return array_filter($ret);
  503. }
  504. }
  505. /**
  506. * {@inheritdoc}
  507. */
  508. public function createFilter($conjunction = 'AND', $tags = array()) {
  509. $filter_class = $this->options['filter class'];
  510. return new $filter_class($conjunction, $tags);
  511. }
  512. /**
  513. * {@inheritdoc}
  514. */
  515. public function keys($keys = NULL) {
  516. $this->orig_keys = $keys;
  517. if (isset($keys)) {
  518. $this->keys = $this->parseKeys($keys, $this->options['parse mode']);
  519. }
  520. else {
  521. $this->keys = NULL;
  522. }
  523. return $this;
  524. }
  525. /**
  526. * {@inheritdoc}
  527. */
  528. public function fields(array $fields) {
  529. $fulltext_fields = $this->index->getFulltextFields();
  530. foreach (array_diff($fields, $fulltext_fields) as $field) {
  531. throw new SearchApiException(t('Trying to search on field @field which is no indexed fulltext field.', array('@field' => $field)));
  532. }
  533. $this->fields = $fields;
  534. return $this;
  535. }
  536. /**
  537. * {@inheritdoc}
  538. */
  539. public function filter(SearchApiQueryFilterInterface $filter) {
  540. $this->filter->filter($filter);
  541. return $this;
  542. }
  543. /**
  544. * {@inheritdoc}
  545. */
  546. public function condition($field, $value, $operator = '=') {
  547. $this->filter->condition($field, $value, $operator);
  548. return $this;
  549. }
  550. /**
  551. * {@inheritdoc}
  552. */
  553. public function sort($field, $order = 'ASC') {
  554. $fields = $this->index->options['fields'];
  555. $fields += array(
  556. 'search_api_relevance' => array('type' => 'decimal'),
  557. 'search_api_id' => array('type' => 'integer'),
  558. );
  559. if ($this->getIndex()->server()->supportsFeature('search_api_random_sort')) {
  560. $fields['search_api_random'] = array('type' => 'integer');
  561. }
  562. if (empty($fields[$field])) {
  563. throw new SearchApiException(t('Trying to sort on unknown field @field.', array('@field' => $field)));
  564. }
  565. $type = $fields[$field]['type'];
  566. if (search_api_is_list_type($type) || search_api_is_text_type($type)) {
  567. throw new SearchApiException(t('Trying to sort on field @field of illegal type @type.', array('@field' => $field, '@type' => $type)));
  568. }
  569. $order = strtoupper(trim($order)) == 'DESC' ? 'DESC' : 'ASC';
  570. $this->sort[$field] = $order;
  571. return $this;
  572. }
  573. /**
  574. * {@inheritdoc}
  575. */
  576. public function range($offset = NULL, $limit = NULL) {
  577. $this->options['offset'] = $offset;
  578. $this->options['limit'] = $limit;
  579. return $this;
  580. }
  581. /**
  582. * {@inheritdoc}
  583. */
  584. public function execute() {
  585. $start = microtime(TRUE);
  586. // Prepare the query for execution by the server.
  587. $this->preExecute();
  588. $pre_search = microtime(TRUE);
  589. // Execute query.
  590. $response = $this->index->server()->search($this);
  591. $post_search = microtime(TRUE);
  592. // Postprocess the search results.
  593. $this->postExecute($response);
  594. $end = microtime(TRUE);
  595. $response['performance']['complete'] = $end - $start;
  596. $response['performance']['hooks'] = $response['performance']['complete'] - ($post_search - $pre_search);
  597. // Store search for later retrieval for facets, etc.
  598. search_api_current_search(NULL, $this, $response);
  599. return $response;
  600. }
  601. /**
  602. * Adds language filters for the query.
  603. *
  604. * Internal helper function.
  605. *
  606. * @param array $languages
  607. * The languages for which results should be returned.
  608. *
  609. * @throws SearchApiException
  610. * If there was a logical error in the combination of filters and languages.
  611. */
  612. protected function addLanguages(array $languages) {
  613. if (array_search(LANGUAGE_NONE, $languages) === FALSE) {
  614. $languages[] = LANGUAGE_NONE;
  615. }
  616. $languages = drupal_map_assoc($languages);
  617. $langs_to_add = $languages;
  618. $filters = $this->filter->getFilters();
  619. while ($filters && $langs_to_add) {
  620. $filter = array_shift($filters);
  621. if (is_array($filter)) {
  622. if ($filter[0] == 'search_api_language' && $filter[2] == '=') {
  623. $lang = $filter[1];
  624. if (isset($languages[$lang])) {
  625. unset($langs_to_add[$lang]);
  626. }
  627. else {
  628. throw new SearchApiException(t('Impossible combination of filters and languages. There is a filter for "@language", but allowed languages are: "@languages".', array('@language' => $lang, '@languages' => implode('", "', $languages))));
  629. }
  630. }
  631. }
  632. else {
  633. if ($filter->getConjunction() == 'AND') {
  634. $filters += $filter->getFilters();
  635. }
  636. }
  637. }
  638. if ($langs_to_add) {
  639. if (count($langs_to_add) == 1) {
  640. $this->condition('search_api_language', reset($langs_to_add));
  641. }
  642. else {
  643. $filter = $this->createFilter('OR');
  644. foreach ($langs_to_add as $lang) {
  645. $filter->condition('search_api_language', $lang);
  646. }
  647. $this->filter($filter);
  648. }
  649. }
  650. }
  651. /**
  652. * {@inheritdoc}
  653. */
  654. public function preExecute() {
  655. // Make sure to only execute this once per query.
  656. if (!$this->pre_execute) {
  657. $this->pre_execute = TRUE;
  658. // Add filter for languages.
  659. if (isset($this->options['languages'])) {
  660. $this->addLanguages($this->options['languages']);
  661. }
  662. // Add fulltext fields, unless set
  663. if ($this->fields === NULL) {
  664. $this->fields = $this->index->getFulltextFields();
  665. }
  666. // Preprocess query.
  667. $this->index->preprocessSearchQuery($this);
  668. // Let modules alter the query.
  669. drupal_alter('search_api_query', $this);
  670. }
  671. }
  672. /**
  673. * {@inheritdoc}
  674. */
  675. public function postExecute(array &$results) {
  676. // Postprocess results.
  677. $this->index->postprocessSearchResults($results, $this);
  678. // Let modules alter the results.
  679. drupal_alter('search_api_results', $results, $this);
  680. }
  681. /**
  682. * {@inheritdoc}
  683. */
  684. public function getIndex() {
  685. return $this->index;
  686. }
  687. /**
  688. * {@inheritdoc}
  689. */
  690. public function &getKeys() {
  691. return $this->keys;
  692. }
  693. /**
  694. * {@inheritdoc}
  695. */
  696. public function getOriginalKeys() {
  697. return $this->orig_keys;
  698. }
  699. /**
  700. * {@inheritdoc}
  701. */
  702. public function &getFields() {
  703. return $this->fields;
  704. }
  705. /**
  706. * {@inheritdoc}
  707. */
  708. public function getFilter() {
  709. return $this->filter;
  710. }
  711. /**
  712. * {@inheritdoc}
  713. */
  714. public function &getSort() {
  715. return $this->sort;
  716. }
  717. /**
  718. * {@inheritdoc}
  719. */
  720. public function getOption($name, $default = NULL) {
  721. return array_key_exists($name, $this->options) ? $this->options[$name] : $default;
  722. }
  723. /**
  724. * {@inheritdoc}
  725. */
  726. public function setOption($name, $value) {
  727. $old = $this->getOption($name);
  728. $this->options[$name] = $value;
  729. return $old;
  730. }
  731. /**
  732. * {@inheritdoc}
  733. */
  734. public function &getOptions() {
  735. return $this->options;
  736. }
  737. /**
  738. * Implements the magic __sleep() method to avoid serializing the index.
  739. */
  740. public function __sleep() {
  741. $this->index_id = $this->index->machine_name;
  742. $keys = get_object_vars($this);
  743. unset($keys['index']);
  744. return array_keys($keys);
  745. }
  746. /**
  747. * Implements the magic __wakeup() method to reload the query's index.
  748. */
  749. public function __wakeup() {
  750. if (!isset($this->index) && !empty($this->index_id)) {
  751. $this->index = search_api_index_load($this->index_id);
  752. unset($this->index_id);
  753. }
  754. }
  755. /**
  756. * Implements the magic __clone() method to clone the filter, too.
  757. */
  758. public function __clone() {
  759. $this->filter = clone $this->filter;
  760. }
  761. /**
  762. * Implements the magic __toString() method to simplify debugging.
  763. */
  764. public function __toString() {
  765. $ret = 'Index: ' . $this->index->machine_name . "\n";
  766. $ret .= 'Keys: ' . str_replace("\n", "\n ", var_export($this->orig_keys, TRUE)) . "\n";
  767. if (isset($this->keys)) {
  768. $ret .= 'Parsed keys: ' . str_replace("\n", "\n ", var_export($this->keys, TRUE)) . "\n";
  769. $ret .= 'Searched fields: ' . (isset($this->fields) ? implode(', ', $this->fields) : '[ALL]') . "\n";
  770. }
  771. if ($filter = (string) $this->filter) {
  772. $filter = str_replace("\n", "\n ", $filter);
  773. $ret .= "Filters:\n $filter\n";
  774. }
  775. if ($this->sort) {
  776. $sort = array();
  777. foreach ($this->sort as $field => $order) {
  778. $sort[] = "$field $order";
  779. }
  780. $ret .= 'Sorting: ' . implode(', ', $sort) . "\n";
  781. }
  782. $ret .= 'Options: ' . str_replace("\n", "\n ", var_export($this->options, TRUE)) . "\n";
  783. return $ret;
  784. }
  785. }
  786. /**
  787. * Represents a filter on a search query.
  788. *
  789. * Filters apply conditions on one or more fields with a specific conjunction
  790. * (AND or OR) and may contain nested filters.
  791. */
  792. interface SearchApiQueryFilterInterface {
  793. /**
  794. * Constructs a new filter that uses the specified conjunction.
  795. *
  796. * @param string $conjunction
  797. * (optional) The conjunction to use for this filter - either 'AND' or 'OR'.
  798. * @param array $tags
  799. * (optional) An arbitrary set of tags. Can be used to identify this filter
  800. * down the line if necessary. This is primarily used by the facet system
  801. * to support OR facet queries.
  802. */
  803. public function __construct($conjunction = 'AND', array $tags = array());
  804. /**
  805. * Sets this filter's conjunction.
  806. *
  807. * @param string $conjunction
  808. * The conjunction to use for this filter - either 'AND' or 'OR'.
  809. *
  810. * @return SearchApiQueryFilterInterface
  811. * The called object.
  812. */
  813. public function setConjunction($conjunction);
  814. /**
  815. * Adds a subfilter.
  816. *
  817. * @param SearchApiQueryFilterInterface $filter
  818. * A SearchApiQueryFilterInterface object that should be added as a
  819. * subfilter.
  820. *
  821. * @return SearchApiQueryFilterInterface
  822. * The called object.
  823. */
  824. public function filter(SearchApiQueryFilterInterface $filter);
  825. /**
  826. * Adds a new ($field $operator $value) condition.
  827. *
  828. * @param string $field
  829. * The field to filter on, e.g. 'title'.
  830. * @param mixed $value
  831. * The value the field should have (or be related to by the operator).
  832. * @param string $operator
  833. * The operator to use for checking the constraint. The following operators
  834. * are supported for primitive types: "=", "<>", "<", "<=", ">=", ">". They
  835. * have the same semantics as the corresponding SQL operators.
  836. * If $field is a fulltext field, $operator can only be "=" or "<>", which
  837. * are in this case interpreted as "contains" or "doesn't contain",
  838. * respectively.
  839. * If $value is NULL, $operator also can only be "=" or "<>", meaning the
  840. * field must have no or some value, respectively.
  841. *
  842. * @return SearchApiQueryFilterInterface
  843. * The called object.
  844. */
  845. public function condition($field, $value, $operator = '=');
  846. /**
  847. * Retrieves the conjunction used by this filter.
  848. *
  849. * @return string
  850. * The conjunction used by this filter - either 'AND' or 'OR'.
  851. */
  852. public function getConjunction();
  853. /**
  854. * Return all conditions and nested filters contained in this filter.
  855. *
  856. * @return array
  857. * An array containing this filter's subfilters. Each of these is either a
  858. * condition, represented as a numerically indexed array with the arguments
  859. * of a previous SearchApiQueryFilterInterface::condition() call (field,
  860. * value, operator); or a nested filter, represented by a
  861. * SearchApiQueryFilterInterface filter object.
  862. */
  863. public function &getFilters();
  864. /**
  865. * Checks whether a certain tag was set on this filter.
  866. *
  867. * @param string $tag
  868. * A tag to check for.
  869. *
  870. * @return bool
  871. * TRUE if the tag was set for this filter, FALSE otherwise.
  872. */
  873. public function hasTag($tag);
  874. /**
  875. * Retrieves the tags set on this filter.
  876. *
  877. * @return array
  878. * The tags associated with this filter, as both the array keys and values.
  879. */
  880. public function &getTags();
  881. }
  882. /**
  883. * Provides a standard implementation of SearchApiQueryFilterInterface.
  884. */
  885. class SearchApiQueryFilter implements SearchApiQueryFilterInterface {
  886. /**
  887. * Array containing subfilters.
  888. *
  889. * Each of these is either an array (field, value, operator), or another
  890. * SearchApiFilter object.
  891. *
  892. * @var array
  893. */
  894. protected $filters;
  895. /**
  896. * String specifying this filter's conjunction ('AND' or 'OR').
  897. *
  898. * @var string
  899. */
  900. protected $conjunction;
  901. /**
  902. * {@inheritdoc}
  903. */
  904. public function __construct($conjunction = 'AND', array $tags = array()) {
  905. $this->setConjunction($conjunction);
  906. $this->filters = array();
  907. $this->tags = drupal_map_assoc($tags);
  908. }
  909. /**
  910. * {@inheritdoc}
  911. */
  912. public function setConjunction($conjunction) {
  913. $this->conjunction = strtoupper(trim($conjunction)) == 'OR' ? 'OR' : 'AND';
  914. return $this;
  915. }
  916. /**
  917. * {@inheritdoc}
  918. */
  919. public function filter(SearchApiQueryFilterInterface $filter) {
  920. $this->filters[] = $filter;
  921. return $this;
  922. }
  923. /**
  924. * {@inheritdoc}
  925. */
  926. public function condition($field, $value, $operator = '=') {
  927. $this->filters[] = array($field, $value, $operator);
  928. return $this;
  929. }
  930. /**
  931. * {@inheritdoc}
  932. */
  933. public function getConjunction() {
  934. return $this->conjunction;
  935. }
  936. /**
  937. * {@inheritdoc}
  938. */
  939. public function &getFilters() {
  940. return $this->filters;
  941. }
  942. /**
  943. * {@inheritdoc}
  944. */
  945. public function hasTag($tag) {
  946. return isset($this->tags[$tag]);
  947. }
  948. /**
  949. * {@inheritdoc}
  950. */
  951. public function &getTags() {
  952. return $this->tags;
  953. }
  954. /**
  955. * Implements the magic __clone() method to clone nested filters, too.
  956. */
  957. public function __clone() {
  958. foreach ($this->filters as $i => $filter) {
  959. if (is_object($filter)) {
  960. $this->filters[$i] = clone $filter;
  961. }
  962. }
  963. }
  964. /**
  965. * Implements the magic __toString() method to simplify debugging.
  966. */
  967. public function __toString() {
  968. // Special case for a single, nested filter:
  969. if (count($this->filters) == 1 && is_object($this->filters[0])) {
  970. return (string) $this->filters[0];
  971. }
  972. $ret = array();
  973. foreach ($this->filters as $filter) {
  974. if (is_object($filter)) {
  975. $ret[] = "[\n " . str_replace("\n", "\n ", (string) $filter) . "\n ]";
  976. }
  977. else {
  978. $ret[] = "$filter[0] $filter[2] " . str_replace("\n", "\n ", var_export($filter[1], TRUE));
  979. }
  980. }
  981. return $ret ? ' ' . implode("\n{$this->conjunction}\n ", $ret) : '';
  982. }
  983. }