PageRenderTime 57ms CodeModel.GetById 20ms RepoModel.GetById 1ms app.codeStats 0ms

/app/lib/core/Search/SearchEngine.php

https://bitbucket.org/Sinfin/pawtucket
PHP | 1111 lines | 769 code | 115 blank | 227 comment | 155 complexity | b486beb27450a884449bb811cc00b453 MD5 | raw file
Possible License(s): LGPL-3.0, GPL-3.0
  1. <?php
  2. /* ----------------------------------------------------------------------
  3. * app/lib/core/Search/SearchEngine.php : Base class for searches
  4. * ----------------------------------------------------------------------
  5. * CollectiveAccess
  6. * Open-source collections management software
  7. * ----------------------------------------------------------------------
  8. *
  9. * Software by Whirl-i-Gig (http://www.whirl-i-gig.com)
  10. * Copyright 2007-2011 Whirl-i-Gig
  11. *
  12. * For more information visit http://www.CollectiveAccess.org
  13. *
  14. * This program is free software; you may redistribute it and/or modify it under
  15. * the terms of the provided license as published by Whirl-i-Gig
  16. *
  17. * CollectiveAccess is distributed in the hope that it will be useful, but
  18. * WITHOUT ANY WARRANTIES whatsoever, including any implied warranty of
  19. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
  20. *
  21. * This source code is free and modifiable under the terms of
  22. * GNU General Public License. (http://www.gnu.org/copyleft/gpl.html). See
  23. * the "license.txt" file for details, or visit the CollectiveAccess web site at
  24. * http://www.CollectiveAccess.org
  25. *
  26. * ----------------------------------------------------------------------
  27. */
  28. # ----------------------------------------------------------------------
  29. # --- Import classes
  30. # ----------------------------------------------------------------------
  31. require_once(__CA_LIB_DIR__."/core/Search/SearchBase.php");
  32. require_once(__CA_LIB_DIR__."/core/Zend/Search/Lucene.php");
  33. require_once(__CA_LIB_DIR__."/core/Plugins/SearchEngine/CachedResult.php");
  34. require_once(__CA_LIB_DIR__."/core/Search/SearchIndexer.php");
  35. require_once(__CA_LIB_DIR__."/core/Search/SearchResult.php");
  36. require_once(__CA_LIB_DIR__."/core/Search/SearchCache.php");
  37. require_once(__CA_LIB_DIR__."/core/Logging/Searchlog.php");
  38. require_once(__CA_LIB_DIR__."/core/Utils/Timer.php");
  39. require_once(__CA_MODELS_DIR__.'/ca_lists.php');
  40. require_once(__CA_LIB_DIR__."/core/Search/Common/Parsers/LuceneSyntaxParser.php");
  41. require_once(__CA_LIB_DIR__."/core/Zend/Search/Lucene/Search/Query.php");
  42. require_once(__CA_LIB_DIR__."/core/Zend/Search/Lucene/Search/Query/Boolean.php");
  43. require_once(__CA_LIB_DIR__."/core/Zend/Search/Lucene/Search/Query/Term.php");
  44. # ----------------------------------------------------------------------
  45. class SearchEngine extends SearchBase {
  46. private $opn_tablenum;
  47. private $opa_tables;
  48. // ----
  49. private $opa_options;
  50. private $opa_result_filters;
  51. /**
  52. * @var subject type_id to limit browsing to (eg. only search ca_objects with type_id = 10)
  53. */
  54. private $opa_search_type_ids = null;
  55. # ------------------------------------------------------------------
  56. public function __construct($opo_db=null, $ps_tablename=null) {
  57. parent::__construct($opo_db);
  58. if ($ps_tablename != null) { $this->ops_tablename = $ps_tablename; }
  59. $this->opa_options = array();
  60. $this->opa_result_filters = array();
  61. $this->opn_tablenum = $this->opo_datamodel->getTableNum($this->ops_tablename);
  62. $this->opa_tables = array();
  63. }
  64. # ------------------------------------------------------------------
  65. public function setOption($ps_option, $pm_value) {
  66. return $this->opo_engine->setOption($ps_option, $pm_value);
  67. }
  68. # ------------------------------------------------------------------
  69. public function getOption($ps_option) {
  70. return $this->opo_engine->getOption($ps_option);
  71. }
  72. # ------------------------------------------------------------------
  73. public function getAvailableOptions() {
  74. return $this->opo_engine->getAvailableOptions();
  75. }
  76. # ------------------------------------------------------------------
  77. public function isValidOption($ps_option) {
  78. return $this->opo_engine->isValidOption($ps_option);
  79. }
  80. # ------------------------------------------------------------------
  81. # Search
  82. # ------------------------------------------------------------------
  83. /**
  84. * Performs a search by calling the search() method on the underlying search engine plugin
  85. * Information about all searches is logged to ca_search_log
  86. *
  87. * @param string $ps_search The search to perform; engine takes Lucene syntax query
  88. * @param SearchResult $po_result A newly instantiated sub-class of SearchResult to place search results into and return. If this is not set, then a generic SearchResults object will be returned.
  89. * @param array $pa_options Optional array of options for the search. Options include
  90. *:
  91. * sort = field or attribute to sort on in <table name>.<field or attribute name> format (eg. ca_objects.idno); default is to sort on relevance (aka. sort='_natural')
  92. * sort_direction = direction to sort results by, either 'asc' for ascending order or 'desc' for descending order; default is 'asc'
  93. * no_cache = if true, search is performed regardless of whether results for the search are already cached; default is false
  94. * limit = if set then search results will be limited to the quantity specified. If not set then all results are returned.
  95. * form_id = optional form identifier string to record in log for search
  96. * log_details = optional form description to record in log for search
  97. * search_source = optional source indicator text to record in log for search
  98. * checkAccess = optional array of access values to filter results on
  99. * showDeleted = if set to true, related items that have been deleted are returned. Default is false.
  100. * limitToModifiedOn = if set returned results will be limited to rows modified within the specified date range. The value should be a date/time expression parse-able by TimeExpressionParser
  101. * sets = if value is a list of set_ids, only rows that are members of those sets will be returned
  102. *
  103. * @return SearchResult Results packages in a SearchResult object, or sub-class of SearchResult if an instance was passed in $po_result
  104. * @uses TimeExpressionParser::parse
  105. */
  106. public function &search($ps_search, $po_result=null, $pa_options=null) {
  107. $t = new Timer();
  108. if (!is_array($pa_options)) { $pa_options = array(); }
  109. $vn_limit = (isset($pa_options['limit']) && ($pa_options['limit'] > 0)) ? (int)$pa_options['limit'] : null;
  110. //print "QUERY=$ps_search<br>";
  111. //
  112. // Note that this is *not* misplaced code that should be in the Lucene plugin!
  113. //
  114. // We are using the Lucene syntax as our query syntax regardless the of back-end search engine.
  115. // The Lucene calls below just parse the query and then rewrite access points as-needed; the result
  116. // is a Lucene-compliant query ready-to-roll that is passed to the engine plugin. Of course, the Lucene
  117. // plugin just uses the string as-is... other plugins my choose to parse it however they wish to.
  118. //
  119. $vb_no_cache = isset($pa_options['no_cache']) ? $pa_options['no_cache'] : false;
  120. unset($pa_options['no_cache']);
  121. $t_table = $this->opo_datamodel->getInstanceByTableName($this->ops_tablename, true);
  122. $vs_pk = $t_table->primaryKey();
  123. $o_cache = new SearchCache();
  124. if (
  125. (!$vb_no_cache && ($o_cache->load($ps_search, $this->opn_tablenum, $pa_options)))
  126. ) {
  127. $va_hits = $o_cache->getResults();
  128. if (isset($pa_options['sort']) && $pa_options['sort'] && ($pa_options['sort'] != '_natural')) {
  129. $va_hits = $this->sortHits($va_hits, $pa_options['sort'], (isset($pa_options['sort_direction']) ? $pa_options['sort_direction'] : null));
  130. }
  131. $o_res = new WLPlugSearchEngineCachedResult(array_keys($va_hits), array(), $vs_pk);
  132. } else {
  133. $vs_char_set = $this->opo_app_config->get('character_set');
  134. $o_query_parser = new LuceneSyntaxParser();
  135. $o_query_parser->setEncoding($vs_char_set);
  136. $o_query_parser->setDefaultOperator(LuceneSyntaxParser::B_AND);
  137. $ps_search = preg_replace('![\']+!', '', $ps_search);
  138. try {
  139. $o_parsed_query = $o_query_parser->parse($ps_search, $vs_char_set);
  140. } catch (Exception $e) {
  141. $o_query_parser->parse('', $vs_char_set);
  142. }
  143. $va_rewrite_results = $this->_rewriteQuery($o_parsed_query);
  144. $o_rewritten_query = new Zend_Search_Lucene_Search_Query_Boolean($va_rewrite_results['terms'], $va_rewrite_results['signs']);
  145. $vs_search = $this->_queryToString($o_rewritten_query);
  146. //print "<div style='background:#FFFFFF; padding: 5px; border: 1px dotted #666666;'><strong>DEBUG: </strong>".$ps_search.'/'.$vs_search."</div>";
  147. $o_res = $this->opo_engine->search($this->opn_tablenum, $vs_search, $this->opa_result_filters, $o_rewritten_query);
  148. $va_query_terms = array();
  149. // cache the results
  150. $va_hits =array_flip($o_res->getPrimaryKeyValues($vn_limit));
  151. $o_res->seek(0);
  152. if (isset($pa_options['checkAccess']) && (is_array($pa_options['checkAccess']) && sizeof($pa_options['checkAccess']))) {
  153. $va_access_values = $pa_options['checkAccess'];
  154. $va_hits = $this->filterHitsByAccess($va_hits, $va_access_values, $pa_options);
  155. } else {
  156. $va_hits = $this->filterDeletedRecords($va_hits, $pa_options);
  157. }
  158. if (is_array($va_type_ids = $this->getTypeRestrictionList()) && sizeof($va_type_ids)) {
  159. $va_hits = $this->filterHitsByType($va_hits, $va_type_ids);
  160. }
  161. if (isset($pa_options['limitToModifiedOn']) && $pa_options['limitToModifiedOn']) {
  162. $va_hits = $this->filterByModificationDate($va_hits, $pa_options['limitToModifiedOn']);
  163. }
  164. if (isset($pa_options['sets']) && $pa_options['sets']) {
  165. $va_hits = $this->filterHitsBySets($va_hits, $pa_options['sets']);
  166. }
  167. if (isset($pa_options['sort']) && $pa_options['sort'] && ($pa_options['sort'] != '_natural')) {
  168. $va_hits = $this->sortHits($va_hits, $pa_options['sort'], (isset($pa_options['sort_direction']) ? $pa_options['sort_direction'] : null));
  169. }
  170. $va_hit_values = array_keys($va_hits);
  171. $o_res = new WLPlugSearchEngineCachedResult($va_hit_values, $va_query_terms, $vs_pk);
  172. // cache for later use
  173. $o_cache->save($ps_search, $this->opn_tablenum, array_flip($va_hit_values), null, null, array_merge($pa_options, array('filters' => $this->getResultFilters())));
  174. // log search
  175. $o_log = new Searchlog();
  176. global $AUTH_CURRENT_USER_ID;
  177. $vn_search_user_id = $AUTH_CURRENT_USER_ID ? $AUTH_CURRENT_USER_ID : null;
  178. $vn_search_form_id = isset($pa_options['form_id']) ? $pa_options['form_id'] : null;
  179. $vs_log_details = isset($pa_options['log_details']) ? $pa_options['log_details'] : '';
  180. $vs_search_source = isset($pa_options['search_source']) ? $pa_options['search_source'] : '';
  181. $vn_execution_time = $t->getTime(4);
  182. $o_log->log(array(
  183. 'user_id' => $vn_search_user_id,
  184. 'table_num' => $this->opn_tablenum,
  185. 'search_expression' => $ps_search,
  186. 'num_hits' => sizeof($va_hit_values),
  187. 'form_id' => $vn_search_form_id,
  188. 'ip_addr' => $_SERVER['REMOTE_ADDR'] ? $_SERVER['REMOTE_ADDR'] : null,
  189. 'details' => $vs_log_details,
  190. 'search_source' => $vs_search_source,
  191. 'execution_time' => $vn_execution_time
  192. ));
  193. }
  194. if ($po_result) {
  195. $po_result->init($this->opn_tablenum, $o_res, $this->opa_tables);
  196. return $po_result;
  197. } else {
  198. return new SearchResult($this->opn_tablenum, $o_res, $this->opa_tables);
  199. }
  200. }
  201. # ------------------------------------------------------------------
  202. /**
  203. * @param $pa_hits Array of row_ids to filter. *MUST HAVE row_ids AS KEYS, NOT VALUES*
  204. */
  205. public function filterDeletedRecords($pa_hits, $pa_options=null) {
  206. $o_db = new Db();
  207. if (!sizeof($pa_hits)) { return $pa_hits; }
  208. if (!($t_table = $this->opo_datamodel->getInstanceByTableNum($this->opn_tablenum, true))) { return $pa_hits; }
  209. if ((isset($pa_options['showDeleted']) && $pa_options['showDeleted']) || !$t_table->hasField('deleted')) { return $pa_hits; }
  210. $vs_table_name = $this->ops_tablename;
  211. $vs_table_pk = $t_table->primaryKey();
  212. $qr_sort = $o_db->query("
  213. SELECT {$vs_table_name}.{$vs_table_pk}
  214. FROM {$vs_table_name}
  215. WHERE
  216. ({$vs_table_name}.{$vs_table_pk} IN (".join(", ", array_keys($pa_hits))."))
  217. AND
  218. ({$vs_table_name}.deleted = 0)
  219. ");
  220. $va_hits = array();
  221. while($qr_sort->nextRow()) {
  222. $va_hits[$qr_sort->get($vs_table_pk, array('binary' => true))] = true;
  223. }
  224. return $va_hits;
  225. }
  226. # ------------------------------------------------------------------
  227. /**
  228. * @param $pa_hits Array of row_ids to sort. *MUST HAVE row_ids AS KEYS, NOT VALUES*
  229. */
  230. public function filterHitsByType($pa_hits, $pa_type_ids) {
  231. $o_db = new Db();
  232. if (!sizeof($pa_hits)) { return $pa_hits; }
  233. if (!($t_table = $this->opo_datamodel->getInstanceByTableNum($this->opn_tablenum, true))) { return $pa_hits; }
  234. $vs_table_pk = $t_table->primaryKey();
  235. $vs_table_name = $this->ops_tablename;
  236. if (!($vs_type_field_name = $t_table->getTypeFieldName())) { return $pa_hits; }
  237. $qr_sort = $o_db->query("
  238. SELECT {$vs_table_name}.{$vs_table_pk}
  239. FROM {$vs_table_name}
  240. WHERE
  241. {$vs_table_name}.{$vs_table_pk} IN (".join(", ", array_keys($pa_hits)).") AND
  242. {$vs_table_name}.{$vs_type_field_name} IN (".join(", ", $pa_type_ids).")
  243. ");
  244. $va_hits = array();
  245. while($qr_sort->nextRow()) {
  246. $va_hits[$qr_sort->get($vs_table_pk, array('binary' => true))] = true;
  247. }
  248. return $va_hits;
  249. }
  250. # ------------------------------------------------------------------
  251. /**
  252. * @param $pa_hits Array of row_ids to filter. *MUST HAVE row_ids AS KEYS, NOT VALUES*
  253. */
  254. public function filterHitsByAccess($pa_hits, $pa_access_values, $pa_options=null) {
  255. if (!sizeof($pa_hits)) { return $pa_hits; }
  256. if (!sizeof($pa_access_values)) { return $pa_hits; }
  257. if (!($t_table = $this->opo_datamodel->getInstanceByTableNum($this->opn_tablenum, true))) { return $pa_hits; }
  258. if (!$t_table->hasField('access')) {
  259. return $this->filterDeletedRecords($va_hits);
  260. }
  261. $vs_table_pk = $t_table->primaryKey();
  262. $vs_table_name = $this->ops_tablename;
  263. $vs_delete_filter_sql = ((!isset($pa_options['showDeleted']) || !$pa_options['showDeleted']) && $t_table->hasField('deleted')) ? " AND ({$vs_table_name}.deleted = 0)" : '';
  264. $o_db = new Db();
  265. $qr_sort = $o_db->query($x ="
  266. SELECT {$vs_table_name}.{$vs_table_pk}
  267. FROM {$vs_table_name}
  268. WHERE
  269. {$vs_table_name}.{$vs_table_pk} IN (".join(", ", array_keys($pa_hits)).") AND
  270. {$vs_table_name}.access IN (".join(", ", $pa_access_values).")
  271. {$vs_delete_filter_sql}
  272. ");
  273. $va_hits = array();
  274. while($qr_sort->nextRow()) {
  275. $va_hits[$qr_sort->get($vs_table_pk, array('binary' => true))] = true;
  276. }
  277. return $va_hits;
  278. }
  279. # ------------------------------------------------------------------
  280. /**
  281. * @param $pa_hits Array of row_ids to filter. *MUST HAVE row_ids AS KEYS, NOT VALUES*
  282. */
  283. public function filterByModificationDate($pa_hits, $ps_daterange, $pa_options=null) {
  284. $o_db = new Db();
  285. if (!sizeof($pa_hits)) { return $pa_hits; }
  286. if (!($t_table = $this->opo_datamodel->getInstanceByTableNum($this->opn_tablenum, true))) { return $pa_hits; }
  287. $vs_table_name = $this->ops_tablename;
  288. $vs_table_pk = $t_table->primaryKey();
  289. $o_tep = new TimeExpressionParser();
  290. if (!$o_tep->parse($ps_daterange)) { return $pa_hits; }
  291. $va_range = $o_tep->getUnixTimestamps();
  292. $qr_sort = $o_db->query("
  293. SELECT {$vs_table_name}.{$vs_table_pk}
  294. FROM {$vs_table_name}
  295. INNER JOIN ca_change_log_subjects ON ca_change_log_subjects.subject_row_id = {$vs_table_name}.{$vs_table_pk} AND ca_change_log_subjects.subject_table_num = ".$t_table->tableNum()."
  296. INNER JOIN ca_change_log ON ca_change_log.log_id = ca_change_log_subjects.log_id
  297. WHERE
  298. ({$vs_table_name}.{$vs_table_pk} IN (".join(", ", array_keys($pa_hits))."))
  299. AND
  300. (ca_change_log.log_datetime BETWEEN ".(int)$va_range['start']." AND ".(int)$va_range['end'].")
  301. AND
  302. (ca_change_log.changetype IN ('I', 'U', 'D'))
  303. ");
  304. $va_hits = array();
  305. while($qr_sort->nextRow()) {
  306. $va_hits[$qr_sort->get($vs_table_pk, array('binary' => true))] = true;
  307. }
  308. return $va_hits;
  309. }
  310. # ------------------------------------------------------------------
  311. /**
  312. * @param $pa_hits Array of row_ids to filter. *MUST HAVE row_ids AS KEYS, NOT VALUES*
  313. */
  314. public function filterHitsBySets($pa_hits, $pa_set_ids, $pa_options=null) {
  315. if (!sizeof($pa_hits)) { return $pa_hits; }
  316. if (!sizeof($pa_set_ids)) { return $pa_hits; }
  317. if (!($t_table = $this->opo_datamodel->getInstanceByTableNum($this->opn_tablenum, true))) { return $pa_hits; }
  318. $vs_table_name = $t_table->tableName();
  319. $vs_table_pk = $t_table->primaryKey();
  320. $o_db = new Db();
  321. $qr_sort = $o_db->query("
  322. SELECT {$vs_table_name}.{$vs_table_pk}
  323. FROM {$vs_table_name}
  324. INNER JOIN ca_set_items ON ca_set_items.row_id = {$vs_table_name}.{$vs_table_pk} AND ca_set_items.table_num = ?
  325. WHERE
  326. {$vs_table_name}.{$vs_table_pk} IN (".join(", ", array_keys($pa_hits)).") AND
  327. ca_set_items.set_id IN (".join(", ", $pa_set_ids).")
  328. ", (int)$this->opn_tablenum);
  329. $va_hits = array();
  330. while($qr_sort->nextRow()) {
  331. $va_hits[$qr_sort->get($vs_table_pk, array('binary' => true))] = true;
  332. }
  333. return $va_hits;
  334. }
  335. # ------------------------------------------------------------------
  336. /**
  337. *
  338. */
  339. public function getRandomResult($pn_num_hits=10, $po_result=null) {
  340. $o_db = new Db();
  341. if (!($t_table = $this->opo_datamodel->getInstanceByTableNum($this->opn_tablenum, true))) { return null; }
  342. $vs_table_pk = $t_table->primaryKey();
  343. $vs_table_name = $this->ops_tablename;
  344. $qr_res = $o_db->query("
  345. SELECT {$vs_table_name}.{$vs_table_pk}
  346. FROM {$vs_table_name}
  347. WHERE {$vs_table_name}.{$vs_table_pk} >=
  348. (SELECT FLOOR( MAX({$vs_table_name}.{$vs_table_pk}) * RAND()) FROM {$vs_table_name})
  349. LIMIT {$pn_num_hits}
  350. ");
  351. $va_hits = array();
  352. while($qr_res->nextRow()) {
  353. $va_hits[] = $qr_res->get($vs_table_pk, array('binary' => true));
  354. }
  355. $o_res = new WLPlugSearchEngineCachedResult($va_hits, array(), $vs_table_pk);
  356. if ($po_result) {
  357. $po_result->init($this->opn_tablenum, $o_res, array());
  358. return $po_result;
  359. } else {
  360. return new SearchResult($this->opn_tablenum, $o_res);
  361. }
  362. }
  363. # ------------------------------------------------------------------
  364. /**
  365. * @param $pa_hits Array of row_ids to sort. *MUST HAVE row_ids AS KEYS, NOT VALUES*
  366. */
  367. public function sortHits(&$pa_hits, $ps_field, $ps_direction='asc') {
  368. if (!in_array($ps_direction, array('asc', 'desc'))) { $ps_direction = 'asc'; }
  369. if (!is_array($pa_hits) || !sizeof($pa_hits)) { return $pa_hits; }
  370. $t_table = $this->opo_datamodel->getInstanceByTableNum($this->opn_tablenum, true);
  371. $vs_table_pk = $t_table->primaryKey();
  372. $vs_table_name = $this->ops_tablename;
  373. $va_fields = explode(';', $ps_field);
  374. $va_joins = array();
  375. $va_orderbys = array();
  376. $vs_locale_where = '';
  377. $vs_is_preferred_sql = '';
  378. foreach($va_fields as $vs_field) {
  379. $va_tmp = explode('.', $vs_field);
  380. if ($va_tmp[0] == $vs_table_name) {
  381. // sort field is in search table
  382. if (!$t_table->hasField($va_tmp[1])) {
  383. // is it an attribute?
  384. $t_element = new ca_metadata_elements();
  385. $vs_sort_element_code = array_pop($va_tmp);
  386. if ($t_element->load(array('element_code' => $vs_sort_element_code))) {
  387. $vn_element_id = $t_element->getPrimaryKey();
  388. if (!($vs_sort_field = Attribute::getSortFieldForDatatype($t_element->get('datatype')))) {
  389. return $pa_hits;
  390. }
  391. $vs_sql = "
  392. SELECT t.{$vs_table_pk}, attr.locale_id, {$vs_sort_field}
  393. FROM {$vs_table_name} t
  394. INNER JOIN ca_attributes AS attr ON attr.row_id = t.{$vs_table_pk}
  395. INNER JOIN ca_attribute_values AS attr_vals ON attr_vals.attribute_id = attr.attribute_id
  396. WHERE
  397. (t.{$vs_table_pk} IN (".join(", ", array_keys($pa_hits))."))
  398. AND
  399. (attr_vals.element_id = ?) AND (attr.table_num = ?) AND (attr_vals.{$vs_sort_field} IS NOT NULL)
  400. ";
  401. $qr_sort = $this->opo_db->query($vs_sql, (int)$vn_element_id, (int)$this->opn_tablenum);
  402. $va_sorted_hits = array();
  403. while($qr_sort->nextRow()) {
  404. $va_sorted_hits[$vn_id = $qr_sort->get($vs_table_pk, array('binary' => true))][$qr_sort->get('locale_id', array('binary' => true))] = $qr_sort->get($vs_sort_field);
  405. unset($pa_hits[$vn_id]);
  406. }
  407. foreach($pa_hits as $vn_id => $va_item) {
  408. $va_sorted_hits[$vn_id] = $va_item;
  409. }
  410. $va_sorted_hits = caExtractValuesByUserLocale($va_sorted_hits);
  411. asort($va_sorted_hits);
  412. if ($ps_direction == 'desc') { $va_sorted_hits = array_reverse($va_sorted_hits, true); }
  413. return $va_sorted_hits;
  414. }
  415. return $pa_hits; // return hits unsorted if field is not valid
  416. } else {
  417. $va_field_info = $t_table->getFieldInfo($va_tmp[1]);
  418. if ($va_field_info['START'] && $va_field_info['END']) {
  419. $va_orderbys[] = $va_field_info['START'].' '.$ps_direction;
  420. $va_orderbys[] = $va_field_info['END'].' '.$ps_direction;
  421. } else {
  422. $va_orderbys[] = $vs_field.' '.$ps_direction;
  423. }
  424. if ($t_table->hasField('locale_id')) {
  425. $vs_locale_where = ", ".$vs_table_name.".locale_id";
  426. }
  427. $vs_sortable_value = $vs_field;
  428. }
  429. } else {
  430. // sort field is in related table
  431. $va_path = $this->opo_datamodel->getPath($vs_table_name, $va_tmp[0]);
  432. if (sizeof($va_path) > 2) {
  433. // many-many
  434. $vs_last_table = null;
  435. // generate related joins
  436. foreach($va_path as $vs_table => $va_info) {
  437. $t_table = $this->opo_datamodel->getInstanceByTableName($vs_table, true);
  438. if (!$vs_last_table) {
  439. //$va_joins[$vs_table] = "INNER JOIN ".$vs_table." ON ".$vs_table.".".$t_table->primaryKey()." = ca_sql_search_search_final.row_id";
  440. } else {
  441. $va_rels = $this->opo_datamodel->getOneToManyRelations($vs_last_table, $vs_table);
  442. if (!sizeof($va_rels)) {
  443. $va_rels = $this->opo_datamodel->getOneToManyRelations($vs_table, $vs_last_table);
  444. }
  445. if ($vs_table == $va_rels['one_table']) {
  446. $va_joins[$vs_table] = "INNER JOIN ".$va_rels['one_table']." ON ".$va_rels['one_table'].".".$va_rels['one_table_field']." = ".$va_rels['many_table'].".".$va_rels['many_table_field'];
  447. } else {
  448. $va_joins[$vs_table] = "INNER JOIN ".$va_rels['many_table']." ON ".$va_rels['many_table'].".".$va_rels['many_table_field']." = ".$va_rels['one_table'].".".$va_rels['one_table_field'];
  449. }
  450. }
  451. $t_last_table = $t_table;
  452. $vs_last_table = $vs_table;
  453. }
  454. $va_orderbys[] = $vs_field.' '.$ps_direction;
  455. $vs_sortable_value = $vs_field;
  456. } else {
  457. $va_rels = $this->opo_datamodel->getRelationships($vs_table_name, $va_tmp[0]);
  458. if (!$va_rels) { return $pa_hits; } // return hits unsorted if field is not valid
  459. $t_rel = $this->opo_datamodel->getInstanceByTableName($va_tmp[0], true);
  460. if (!$t_rel->hasField($va_tmp[1])) { return $pa_hits; }
  461. $va_joins[$va_tmp[0]] = 'INNER JOIN '.$va_tmp[0].' ON '.$vs_table_name.'.'.$va_rels[$vs_table_name][$va_tmp[0]][0][0].' = '.$va_tmp[0].'.'.$va_rels[$vs_table_name][$va_tmp[0]][0][1]."\n";
  462. $va_orderbys[] = $vs_field.' '.$ps_direction;
  463. // if the related supports preferred values (eg. *_labels tables) then only consider those in the sort
  464. if ($t_rel->hasField('is_preferred')) {
  465. $vs_is_preferred_sql = " AND ".$va_tmp[0].".is_preferred = 1";
  466. }
  467. if ($t_rel->hasField('locale_id')) {
  468. $vs_locale_where = ", ".$va_tmp[0].".locale_id";
  469. }
  470. $vs_sortable_value = $vs_field;
  471. }
  472. }
  473. }
  474. $vs_join_sql = join("\n", $va_joins);
  475. $vs_sql = "
  476. SELECT {$vs_table_name}.{$vs_table_pk}{$vs_locale_where}, {$vs_sortable_value}
  477. FROM {$vs_table_name}
  478. {$vs_join_sql}
  479. WHERE
  480. {$vs_table_name}.{$vs_table_pk} IN (".join(", ", array_keys($pa_hits)).")
  481. {$vs_is_preferred_sql}
  482. ";
  483. //print $vs_sql;
  484. $qr_sort = $this->opo_db->query($vs_sql);
  485. $va_sorted_hits = array();
  486. while($qr_sort->nextRow()) {
  487. if ($vs_locale_where) {
  488. $va_sorted_hits[$qr_sort->get($vs_table_pk, array('binary' => true))][$qr_sort->get('locale_id', array('binary' => true))] = $qr_sort->get($vs_sortable_value);
  489. } else {
  490. $va_sorted_hits[$qr_sort->get($vs_table_pk, array('binary' => true))] = $qr_sort->get($vs_sortable_value);
  491. }
  492. }
  493. if ($vs_locale_where) {
  494. $va_sorted_hits = caExtractValuesByUserLocale($va_sorted_hits);
  495. }
  496. asort($va_sorted_hits);
  497. if ($ps_direction == 'desc') { $va_sorted_hits = array_reverse($va_sorted_hits, true); }
  498. return $va_sorted_hits;
  499. }
  500. # ------------------------------------------------------------------
  501. private function _rewriteQuery($po_query) {
  502. $va_terms = array();
  503. $va_signs = array();
  504. switch(get_class($po_query)) {
  505. case 'Zend_Search_Lucene_Search_Query_Boolean':
  506. $va_items = $po_query->getSubqueries();
  507. break;
  508. case 'Zend_Search_Lucene_Search_Query_MultiTerm':
  509. $va_items = $po_query->getTerms();
  510. break;
  511. default:
  512. $va_items = array();
  513. break;
  514. }
  515. if (method_exists($po_query, 'getSigns')) {
  516. $va_old_signs = $po_query->getSigns();
  517. } else {
  518. $va_old_signs = array();
  519. }
  520. $vn_i = 0;
  521. foreach($va_items as $o_term) {
  522. switch(get_class($o_term)) {
  523. case 'Zend_Search_Lucene_Search_Query_Preprocessing_Term':
  524. $va_terms[] = new Zend_Search_Lucene_Search_Query_Term(new Zend_Search_Lucene_Index_Term($o_term->__toString()));
  525. $va_signs[] = isset($va_old_signs[$vn_i]) ? $va_old_signs[$vn_i] : true;
  526. break;
  527. case 'Zend_Search_Lucene_Search_Query_Term':
  528. // if (preg_match('!\-!', $vs_term_text = $o_term->getTerm()->text)) { // hack to force hyphenated terms to quoted strings with a space instead of hyphen; addresses issue where PHP Lucene parser seems to do the wrong thing
  529. // $vs_term_text = str_replace("-", " ", $vs_term_text);
  530. // $va_terms[] = new Zend_Search_Lucene_Search_Query_Phrase(array($vs_term_text), null, $o_term->getTerm()->field);
  531. // $va_signs[] = isset($va_old_signs[$vn_i]) ? $va_old_signs[$vn_i] : true;
  532. // } else {
  533. $va_rewritten_terms = $this->_rewriteTerm($o_term, $va_old_signs[$vn_i]);
  534. if (sizeof($va_rewritten_terms['terms']) == 1) {
  535. $va_terms[] = new Zend_Search_Lucene_Search_Query_Term($va_rewritten_terms['terms'][0]);
  536. $va_signs[] = $va_rewritten_terms['signs'][0];
  537. } else {
  538. for($vn_i = 0; $vn_i < sizeof($va_rewritten_terms['terms']); $vn_i++) {
  539. $va_terms[] = new Zend_Search_Lucene_Search_Query_MultiTerm(array($va_rewritten_terms['terms'][$vn_i]), array($va_rewritten_terms['signs'][$vn_i]));
  540. $va_signs[] = $va_rewritten_terms['signs'][$vn_i] ? true : null;
  541. }
  542. //$o_mt = new Zend_Search_Lucene_Search_Query_MultiTerm($va_rewritten_terms['terms'], $va_rewritten_terms['signs']);
  543. }
  544. //$va_terms[] = $o_mt;
  545. //$va_signs[] = sizeof($va_old_signs) ? array_shift($va_old_signs): true;
  546. //}
  547. break;
  548. case 'Zend_Search_Lucene_Index_Term':
  549. $va_rewritten_terms = $this->_rewriteTerm(new Zend_Search_Lucene_Search_Query_Term($o_term), $va_old_signs[$vn_i]);
  550. if (sizeof($va_rewritten_terms['terms']) == 1) {
  551. $o_mt = new Zend_Search_Lucene_Search_Query_Term($va_rewritten_terms['terms'][0]);
  552. } else {
  553. $o_mt = new Zend_Search_Lucene_Search_Query_MultiTerm($va_rewritten_terms['terms'], $va_rewritten_terms['signs']);
  554. }
  555. $va_terms[] = $o_mt;
  556. $va_signs[] = sizeof($va_rewritten_terms['signs']) ? array_shift($va_rewritten_terms['signs']): true;
  557. break;
  558. case 'Zend_Search_Lucene_Search_Query_Wildcard':
  559. $va_rewritten_terms = $this->_rewriteTerm(new Zend_Search_Lucene_Search_Query_Term($o_term->getPattern()), $va_old_signs[$vn_i]);
  560. $o_mt = new Zend_Search_Lucene_Search_Query_MultiTerm($va_rewritten_terms['terms'], $va_rewritten_terms['signs']);
  561. $va_terms[] = $o_mt;
  562. $va_signs[] = sizeof($va_rewritten_terms['signs']) ? array_shift($va_rewritten_terms['signs']): true;
  563. break;
  564. case 'Zend_Search_Lucene_Search_Query_Phrase':
  565. $va_phrase_items = $o_term->getTerms();
  566. $va_rewritten_phrase = $this->_rewritePhrase($o_term, $va_old_signs[$vn_i]);
  567. foreach($va_rewritten_phrase['terms'] as $o_term) {
  568. $va_terms[] = $o_term;
  569. }
  570. foreach($va_rewritten_phrase['signs'] as $vb_sign) {
  571. $va_signs[] = $vb_sign;
  572. }
  573. break;
  574. case 'Zend_Search_Lucene_Search_Query_MultiTerm':
  575. $va_tmp = $this->_rewriteQuery($o_term);
  576. $va_terms[] = new Zend_Search_Lucene_Search_Query_MultiTerm($va_tmp['terms'], $va_tmp['signs']);
  577. $va_signs[] = $va_old_signs[$vn_i] ? $va_old_signs[$vn_i] : true;
  578. break;
  579. case 'Zend_Search_Lucene_Search_Query_Boolean':
  580. $va_tmp = $this->_rewriteQuery($o_term);
  581. $va_terms[] = new Zend_Search_Lucene_Search_Query_Boolean($va_tmp['terms'], $va_tmp['signs']);
  582. $va_signs[] = $va_old_signs[$vn_i] ? $va_old_signs[$vn_i] : true;
  583. break;
  584. case 'Zend_Search_Lucene_Search_Query_Range':
  585. $va_signs[] = $va_old_signs[$vn_i] ? $va_old_signs[$vn_i] : true;
  586. $va_terms = array_merge($va_terms, $this->_rewriteRange($o_term));
  587. break;
  588. default:
  589. // NOOP (TODO: do *something*)
  590. break;
  591. }
  592. $vn_i++;
  593. }
  594. return array(
  595. 'terms' => $va_terms,
  596. 'signs' => $va_signs
  597. );
  598. }
  599. # ------------------------------------------------------------------
  600. /**
  601. * @param $po_term - term to rewrite; must be Zend_Search_Lucene_Search_Query_Term object
  602. * @param $pb_sign - Zend boolean flag (true=and, null=or, false=not)
  603. * @return array - rewritten terms are *** Zend_Search_Lucene_Index_Term *** objects
  604. */
  605. private function _rewriteTerm($po_term, $pb_sign) {
  606. $vs_fld = $po_term->getTerm()->field;
  607. if (sizeof($va_access_points = $this->getAccessPoints($this->opn_tablenum))) {
  608. // if field is access point then do rewrite
  609. if (
  610. isset($va_access_points[$vs_fld])
  611. &&
  612. ($va_ap_info = $va_access_points[$vs_fld])
  613. ) {
  614. $va_fields = isset($va_ap_info['fields']) ? $va_ap_info['fields'] : null;
  615. if (!in_array($vs_bool = strtoupper($va_ap_info['boolean']), array('AND', 'OR'))) {
  616. $vs_bool = 'OR';
  617. }
  618. $va_terms = array();
  619. foreach($va_fields as $vs_field) {
  620. $va_terms['terms'][] = new Zend_Search_Lucene_Index_Term($po_term->getTerm()->text, $vs_field);
  621. $va_terms['signs'][] = ($vs_bool == 'AND') ? true : false;
  622. }
  623. if (is_array($va_additional_criteria = $va_ap_info['additional_criteria'])) {
  624. foreach($va_additional_criteria as $vs_criterion) {
  625. $va_terms['terms'][] = new Zend_Search_Lucene_Index_Term($vs_criterion);
  626. $va_terms['signs'][] = $vs_bool;
  627. }
  628. }
  629. return $va_terms;
  630. }
  631. }
  632. // is it preferred labels? Rewrite the field for that.
  633. $va_tmp = explode('.', $vs_fld);
  634. if ($va_tmp[1] == 'preferred_labels') {
  635. if ($t_instance = $this->opo_datamodel->getInstanceByTableName($va_tmp[0], true)) {
  636. if (method_exists($t_instance, "getLabelTableName")) {
  637. return array(
  638. 'terms' => array(new Zend_Search_Lucene_Index_Term($po_term->getTerm()->text, $t_instance->getLabelTableName().'.'.$t_instance->getLabelDisplayField())),
  639. 'signs' => array($pb_sign)
  640. );
  641. }
  642. }
  643. }
  644. return array('terms' => array($po_term->getTerm()), 'signs' => array($pb_sign));
  645. }
  646. # ------------------------------------------------------------------
  647. /**
  648. * @param $po_term - phrase expression to rewrite; must be Zend_Search_Lucene_Search_Query_Phrase object
  649. * @param $pb_sign - Zend boolean flag (true=and, null=or, false=not)
  650. * @return array - rewritten phrases are *** Zend_Search_Lucene_Search_Query_Phrase *** objects
  651. */
  652. private function _rewritePhrase($po_term, $pb_sign) {
  653. $va_index_term_strings = array();
  654. $va_phrase_terms = $po_term->getTerms();
  655. foreach($va_phrase_terms as $o_phrase_term) {
  656. $va_index_term_strings[] = $o_phrase_term->text;
  657. }
  658. $vs_fld = $va_phrase_terms[0]->field;
  659. if (sizeof($va_access_points = $this->getAccessPoints($this->opn_tablenum))) {
  660. // if field is access point then do rewrite
  661. if (
  662. isset($va_access_points[$vs_fld])
  663. &&
  664. ($va_ap_info = $va_access_points[$vs_fld])
  665. ) {
  666. $va_fields = isset($va_ap_info['fields']) ? $va_ap_info['fields'] : null;
  667. if (!in_array($vs_bool = strtoupper($va_ap_info['boolean']), array('AND', 'OR'))) {
  668. $vs_bool = 'OR';
  669. }
  670. foreach($va_fields as $vs_field) {
  671. $va_terms['terms'][] = new Zend_Search_Lucene_Search_Query_Phrase($va_index_term_strings, null, $vs_field);
  672. $va_terms['signs'][] = ($vs_bool == 'AND') ? true : false;
  673. }
  674. if (is_array($va_additional_criteria = $va_ap_info['additional_criteria'])) {
  675. foreach($va_additional_criteria as $vs_criterion) {
  676. $va_terms['terms'][] = new Zend_Search_Lucene_Index_Term($vs_criterion);
  677. $va_terms['signs'][] = $vs_bool;
  678. }
  679. }
  680. return $va_terms;
  681. }
  682. }
  683. // is it preferred labels? Rewrite the field for that.
  684. $va_tmp = explode('.', $vs_fld);
  685. if ($va_tmp[1] == 'preferred_labels') {
  686. if ($t_instance = $this->opo_datamodel->getInstanceByTableName($va_tmp[0], true)) {
  687. if (method_exists($t_instance, "getLabelTableName")) {
  688. return array(
  689. 'terms' => array(new Zend_Search_Lucene_Search_Query_Phrase($va_index_term_strings, null, $t_instance->getLabelTableName().'.'.$t_instance->getLabelDisplayField())),
  690. 'signs' => array($pb_sign)
  691. );
  692. }
  693. }
  694. }
  695. return array('terms' => array($po_term), 'signs' => array($pb_sign));
  696. }
  697. # ------------------------------------------------------------------
  698. /**
  699. * @param $po_range - range expression to rewrite; must be Zend_Search_Lucene_Search_Query_Range object
  700. * @return array - rewritten search terms
  701. */
  702. private function _rewriteRange($po_range) {
  703. if (sizeof($va_access_points = $this->getAccessPoints($this->opn_tablenum))) {
  704. // if field is access point then do rewrite
  705. if ($va_ap_info = $va_access_points[$po_range->getField()]) {
  706. $va_fields = $va_ap_info['fields'];
  707. if (!in_array($vs_bool = strtoupper($va_ap_info['boolean']), array('AND', 'OR'))) {
  708. $vs_bool = 'OR';
  709. }
  710. $va_tmp = array();
  711. foreach($va_fields as $vs_field) {
  712. $po_range->getLowerTerm()->field = $vs_field;
  713. $po_range->getUpperTerm()->field = $vs_field;
  714. $o_range = new Zend_Search_Lucene_Search_Query_Range($po_range->getLowerTerm(), $po_range->getUpperTerm(), (($vs_bool == 'OR') ? null : true));
  715. $o_range->field = $vs_field;
  716. $va_terms[] = $o_range;
  717. }
  718. if (is_array($va_additional_criteria = $va_ap_info['additional_criteria'])) {
  719. foreach($va_additional_criteria as $vs_criterion) {
  720. $va_terms[] = new Zend_Search_Lucene_Search_Query_Term(new Zend_Search_Lucene_Index_Term($vs_criterion));
  721. }
  722. }
  723. return $va_terms;
  724. }
  725. }
  726. return array($po_range);
  727. }
  728. # ------------------------------------------------------------------
  729. private function _queryToString($po_parsed_query) {
  730. switch(get_class($po_parsed_query)) {
  731. case 'Zend_Search_Lucene_Search_Query_Boolean':
  732. $va_items = $po_parsed_query->getSubqueries();
  733. $va_signs = $po_parsed_query->getSigns();
  734. break;
  735. case 'Zend_Search_Lucene_Search_Query_MultiTerm':
  736. $va_items = $po_parsed_query->getTerms();
  737. $va_signs = $po_parsed_query->getSigns();
  738. break;
  739. case 'Zend_Search_Lucene_Search_Query_Phrase':
  740. //$va_items = $po_parsed_query->getTerms();
  741. $va_items = $po_parsed_query;
  742. $va_signs = null;
  743. break;
  744. case 'Zend_Search_Lucene_Search_Query_Range':
  745. $va_items = $po_parsed_query;
  746. $va_signs = null;
  747. break;
  748. default:
  749. $va_items = array();
  750. $va_signs = null;
  751. break;
  752. }
  753. $vs_query = '';
  754. foreach ($va_items as $id => $subquery) {
  755. if ($id != 0) {
  756. $vs_query .= ' ';
  757. }
  758. if (($va_signs === null || $va_signs[$id] === true) && ($id)) {
  759. $vs_query .= ' AND ';
  760. } else if (($va_signs[$id] === false) && $id) {
  761. $vs_query .= ' NOT ';
  762. } else {
  763. if ($id) { $vs_query .= ' OR '; }
  764. }
  765. switch(get_class($subquery)) {
  766. case 'Zend_Search_Lucene_Search_Query_Phrase':
  767. $vs_query .= '(' . $subquery->__toString(). ')';
  768. break;
  769. case 'Zend_Search_Lucene_Index_Term':
  770. $subquery = new Zend_Search_Lucene_Search_Query_Term($subquery);
  771. // intentional fallthrough to next case here
  772. case 'Zend_Search_Lucene_Search_Query_Term':
  773. $vs_query .= '(' . $subquery->__toString() . ')';
  774. break;
  775. case 'Zend_Search_Lucene_Search_Query_Range':
  776. $vs_query = $subquery;
  777. break;
  778. default:
  779. $vs_query .= '(' . $this->_queryToString($subquery) . ')';
  780. break;
  781. }
  782. if ((method_exists($subquery, "getBoost")) && ($subquery->getBoost() != 1)) {
  783. $vs_query .= '^' . round($subquery->getBoost(), 4);
  784. }
  785. }
  786. return $vs_query;
  787. }
  788. # ------------------------------------------------------------------
  789. # Search parameter accessors
  790. # ------------------------------------------------------------------
  791. public function addTable($ps_tablename, $pa_fieldlist, $pa_join_tables=array(), $pa_criteria=array()) {
  792. $this->opa_tables[$ps_tablename] = array(
  793. 'fieldList' => $pa_fieldlist,
  794. 'joinTables' => $pa_join_tables,
  795. 'criteria' => $pa_criteria
  796. );
  797. }
  798. # ------------------------------------------------------------------
  799. public function removeTable($ps_tablename) {
  800. unset($this->opa_tables[$ps_tablename]);
  801. }
  802. # ------------------------------------------------------------------
  803. public function getTables() {
  804. return $this->opa_tables;
  805. }
  806. # ------------------------------------------------------------------
  807. /**
  808. * Result filters are criteria through which the results of a search are passed before being
  809. * returned to the caller. They are often used to restrict the domain over which searches operate
  810. * (for example, ensuring that a search only returns rows with a certain "status" field value)
  811. *
  812. * $ps_field is the name of an instrinsic field
  813. * $ps_operator is one of the following: =, <, >, <=, >=, - ("between"), in
  814. * $pm_value is the value to apply; this is usually text or a number; for the "in" operator this is a comma-separated list of string or numeric values
  815. *
  816. *
  817. */
  818. public function addResultFilter($ps_field, $ps_operator, $pm_value) {
  819. $ps_operator = strtolower($ps_operator);
  820. if (!in_array($ps_operator, array('=', '<', '>', '<=', '>=', '-', 'in', '<>', 'is', 'is not'))) { return false; }
  821. $this->opa_result_filters[] = array(
  822. 'field' => $ps_field,
  823. 'operator' => $ps_operator,
  824. 'value' => $pm_value
  825. );
  826. return true;
  827. }
  828. # ------------------------------------------------------------------
  829. public function clearResultFilters() {
  830. $this->opa_result_filters = array();
  831. }
  832. # ------------------------------------------------------------------
  833. public function getResultFilters() {
  834. return $this->opa_result_filters;
  835. }
  836. # ------------------------------------------------------
  837. # Type filtering
  838. # ------------------------------------------------------
  839. /**
  840. * When type restrictions are specified, the search will only consider items of the given types.
  841. * If you specify a type that has hierarchical children then the children will automatically be included
  842. * in the restriction. You may pass numeric type_id and alphanumeric type codes interchangeably.
  843. *
  844. * @param array $pa_type_codes_or_ids List of type_id or code values to filter search by. When set, the search will only consider items of the specified types. Using a hierarchical parent type will automatically include its children in the restriction.
  845. * @return boolean True on success, false on failure
  846. */
  847. public function setTypeRestrictions($pa_type_codes_or_ids) {
  848. $t_instance = $this->opo_datamodel->getInstanceByTableName($this->ops_tablename, true);
  849. if (!$pa_type_codes_or_ids) { return false; }
  850. if (is_array($pa_type_codes_or_ids) && !sizeof($pa_type_codes_or_ids)) { return false; }
  851. if (!is_array($pa_type_codes_or_ids)) { $pa_type_codes_or_ids = array($pa_type_codes_or_ids); }
  852. $t_list = new ca_lists();
  853. if (!method_exists($t_instance, 'getTypeListCode')) { return false; }
  854. if (!($vs_list_name = $t_instance->getTypeListCode())) { return false; }
  855. $va_type_list = $t_instance->getTypeList();
  856. $this->opa_search_type_ids = array();
  857. foreach($pa_type_codes_or_ids as $vs_code_or_id) {
  858. if (!(int)$vs_code_or_id) { continue; }
  859. if (!is_numeric($vs_code_or_id)) {
  860. $vn_type_id = $t_list->getItemIDFromList($vs_list_name, $vs_code_or_id);
  861. } else {
  862. $vn_type_id = (int)$vs_code_or_id;
  863. }
  864. if (!$vn_type_id) { return false; }
  865. if (isset($va_type_list[$vn_type_id]) && $va_type_list[$vn_type_id]) { // is valid type for this subject
  866. // See if there are any child types
  867. $t_item = new ca_list_items($vn_type_id);
  868. $va_ids = $t_item->getHierarchyChildren(null, array('idsOnly' => true));
  869. $va_ids[] = $vn_type_id;
  870. $this->opa_search_type_ids = array_merge($this->opa_search_type_ids, $va_ids);
  871. }
  872. }
  873. return true;
  874. }
  875. # ------------------------------------------------------
  876. /**
  877. * Returns list of type_id values to restrict search to. Return values are always numeric types,
  878. * never codes, and will include all type_ids to filter on, including children of hierarchical types.
  879. *
  880. * @return array List of type_id values to restrict search to.
  881. */
  882. public function getTypeRestrictionList() {
  883. return $this->opa_search_type_ids;
  884. }
  885. # ------------------------------------------------------
  886. /**
  887. * Removes any specified type restrictions on the search
  888. *
  889. * @return boolean Always returns true
  890. */
  891. public function clearTypeRestrictionList() {
  892. $this->opa_search_type_ids = null;
  893. return true;
  894. }
  895. # ------------------------------------------------------------------
  896. /**
  897. * Ask the search engine plugin if everything is configured properly.
  898. *
  899. * @return ASearchConfigurationSettings
  900. */
  901. public static function checkPluginConfiguration() {
  902. $o_config = Configuration::load();
  903. $ps_plugin_name = $o_config->get('search_engine_plugin');
  904. if (!file_exists(__CA_LIB_DIR__.'/core/Plugins/SearchEngine/'.$ps_plugin_name.'ConfigurationSettings.php')) {
  905. return null;
  906. }
  907. require_once(__CA_LIB_DIR__.'/core/Plugins/SearchEngine/'.$ps_plugin_name.'ConfigurationSettings.php');
  908. $ps_classname = $ps_plugin_name."ConfigurationSettings";
  909. return new $ps_classname;
  910. }
  911. # ------------------------------------------------------------------
  912. /**
  913. * Ask the search engine plugin for its display name
  914. *
  915. * @return String
  916. */
  917. public static function getPluginEngineName() {
  918. $o_config = Configuration::load();
  919. $ps_plugin_name = $o_config->get('search_engine_plugin');
  920. if (!file_exists(__CA_LIB_DIR__.'/core/Plugins/SearchEngine/'.$ps_plugin_name.'ConfigurationSettings.php')) {
  921. return null;
  922. }
  923. require_once(__CA_LIB_DIR__.'/core/Plugins/SearchEngine/'.$ps_plugin_name.'ConfigurationSettings.php');
  924. $ps_classname = $ps_plugin_name."ConfigurationSettings";
  925. $o_instance = new $ps_classname;
  926. return $o_instance->getEngineName();
  927. }
  928. # ------------------------------------------------------------------
  929. #
  930. # ------------------------------------------------------------------
  931. /**
  932. * Performs the quickest possible search on the index for the table specified by the superclass
  933. * using the text in $ps_search. Unlike the search() method, quickSearch doesn't support
  934. * any sort of search syntax. You give it some text and you get a collection of (hopefully) relevant results back quickly.
  935. * quickSearch() is intended for autocompleting search suggestion UI's and the like, where performance is critical
  936. * and the ability to control search parameters is not required.
  937. *
  938. * Quick searches are NOT logged to ca_search_log
  939. *
  940. * @param $ps_search - The text to search on
  941. * @param $ps_tablename - name of table to search on
  942. * @param $pn_table_num - number of table to search on (same table as $ps_tablename)
  943. * @param $pa_options - an optional associative array specifying search options. Supported options are: 'limit' (the maximum number of results to return), 'checkAccess' (only return results that have an access value = to the specified value)
  944. *
  945. * @return Array - an array of results is returned keys first by primary key id, then by locale_id. The array values are associative arrays with two keys: type_id (the type_id of the result; this points to a ca_list_items row defining the type of the result item) and label (the row item's label display field). You can push the returned results array from caExtractValuesByUserLocale() to get an array keyed by primary key id and returning for each id a displayable text label + the type of the found result item.
  946. *
  947. */
  948. static function quickSearch($ps_search, $ps_tablename, $pn_tablenum, $pa_options=null) {
  949. $o_config = Configuration::load();
  950. $o_dm = Datamodel::load();
  951. if (!($ps_plugin_name = $o_config->get('search_engine_plugin'))) { return null; }
  952. if (!@require_once(__CA_LIB_DIR__.'/core/Plugins/SearchEngine/'.$ps_plugin_name.'.php')) { return null; }
  953. $ps_classname = 'WLPlugSearchEngine'.$ps_plugin_name;
  954. if (!($o_engine = new $ps_classname)) { return null; }
  955. $va_ids = array_keys($o_engine->quickSearch($pn_tablenum, $ps_search, $pa_options));
  956. if (!is_array($va_ids) || !sizeof($va_ids)) { return array(); }
  957. $t_instance = $o_dm->getInstanceByTableNum($pn_tablenum, true);
  958. $t_label_instance = $t_instance->getLabelTableInstance();
  959. $vs_label_table_name = $t_instance->getLabelTableName();
  960. $vs_label_display_field = $t_instance->getLabelDisplayField();
  961. $vs_pk = $t_instance->primaryKey();
  962. $vs_is_preferred_sql = '';
  963. if ($t_label_instance->hasField('is_preferred')) {
  964. $vs_is_preferred_sql = ' AND (l.is_preferred = 1)';
  965. }
  966. $vs_check_access_sql = '';
  967. if (isset($pa_options['checkAccess']) && !is_null($pa_options['checkAccess']) && $t_instance->hasField('access')) {
  968. $vs_check_access_sql = ' AND (n.access = '.intval($pa_options['checkAccess']).')';
  969. }
  970. $vs_limit_sql = '';
  971. if (isset($pa_options['limit']) && !is_null($pa_options['limit']) && ($pa_options['limit'] > 0)) {
  972. $vs_limit_sql = ' LIMIT '.intval($pa_options['limit']);
  973. }
  974. $o_db = new Db();
  975. $qr_res = $o_db->query("
  976. SELECT n.{$vs_pk}, l.{$vs_label_display_field}, l.locale_id, n.type_id
  977. FROM {$vs_label_table_name} l
  978. INNER JOIN ".$ps_tablename." AS n ON n.{$vs_pk} = l.{$vs_pk}
  979. WHERE
  980. l.".$vs_pk." IN (".join(',', $va_ids).")
  981. {$vs_is_preferred_sql}
  982. {$vs_check_access_sql}
  983. {$vs_limit_sql}
  984. ");
  985. $va_hits = array();
  986. while($qr_res->nextRow()) {
  987. $va_hits[$qr_res->get($vs_pk)][$qr_res->get('locale_id')] = array(
  988. 'type_id' => $qr_res->get('type_id'),
  989. 'label' => $qr_res->get($vs_label_display_field)
  990. );
  991. }
  992. return $va_hits;
  993. }
  994. # ------------------------------------------------------------------
  995. }
  996. ?>