query.inc 35 KB

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