XapianQuery.php 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274
  1. <?php
  2. /* For licensing terms, see /license.txt */
  3. /**
  4. * @package chamilo.include.search
  5. */
  6. require_once 'xapian.php';
  7. //TODO: think another way without including specific fields here
  8. require_once api_get_path(LIBRARY_PATH).'specific_fields_manager.lib.php';
  9. define('XAPIAN_DB', api_get_path(SYS_UPLOAD_PATH).'plugins/xapian/searchdb/');
  10. /**
  11. * Queries the database.
  12. * The xapian_query function queries the database using both a query string
  13. * and application-defined terms. Based on drupal-xapian.
  14. *
  15. * @param string $query_string The search string. This string will
  16. * be parsed and stemmed automatically.
  17. * @param XapianDatabase $db Xapian database to connect
  18. * @param int $start An integer defining the first
  19. * document to return
  20. * @param int $length the number of results to return
  21. * @param array $extra an array containing arrays of
  22. * extra terms to search for
  23. * @param int $count_type Number of items to retrieve
  24. *
  25. * @return array an array of nids corresponding to the results
  26. */
  27. function xapian_query($query_string, $db = null, $start = 0, $length = 10, $extra = [], $count_type = 0)
  28. {
  29. try {
  30. if (!is_object($db)) {
  31. $db = new XapianDatabase(XAPIAN_DB);
  32. }
  33. // Build subqueries from $extra array. Now only used by tags search filter on search widget
  34. $subqueries = [];
  35. foreach ($extra as $subquery) {
  36. if (!empty($subquery)) {
  37. $subqueries[] = new XapianQuery($subquery);
  38. }
  39. }
  40. $query = null;
  41. $enquire = new XapianEnquire($db);
  42. if (!empty($query_string)) {
  43. $query_parser = new XapianQueryParser();
  44. //TODO: choose stemmer
  45. $stemmer = new XapianStem("english");
  46. $query_parser->set_stemmer($stemmer);
  47. $query_parser->set_database($db);
  48. $query_parser->set_stemming_strategy(XapianQueryParser::STEM_SOME);
  49. $query_parser->add_boolean_prefix('courseid', XAPIAN_PREFIX_COURSEID);
  50. $query_parser->add_boolean_prefix('toolid', XAPIAN_PREFIX_TOOLID);
  51. $query = $query_parser->parse_query($query_string);
  52. $final_array = array_merge($subqueries, [$query]);
  53. $query = new XapianQuery(XapianQuery::OP_AND, $final_array);
  54. } else {
  55. $query = new XapianQuery(XapianQuery::OP_OR, $subqueries);
  56. }
  57. $enquire->set_query($query);
  58. $matches = $enquire->get_mset((int) $start, (int) $length);
  59. $specific_fields = get_specific_field_list();
  60. $results = [];
  61. $i = $matches->begin();
  62. // Display the results.
  63. //echo $matches->get_matches_estimated().'results found';
  64. $count = 0;
  65. while (!$i->equals($matches->end())) {
  66. $count++;
  67. $document = $i->get_document();
  68. if (is_object($document)) {
  69. // process one item terms
  70. $courseid_terms = xapian_get_doc_terms($document, XAPIAN_PREFIX_COURSEID);
  71. $results[$count]['courseid'] = substr($courseid_terms[0]['name'], 1);
  72. $toolid_terms = xapian_get_doc_terms($document, XAPIAN_PREFIX_TOOLID);
  73. $results[$count]['toolid'] = substr($toolid_terms[0]['name'], 1);
  74. // process each specific field prefix
  75. foreach ($specific_fields as $specific_field) {
  76. $results[$count]['sf-'.$specific_field['code']] = xapian_get_doc_terms($document, $specific_field['code']);
  77. }
  78. // rest of data
  79. $results[$count]['xapian_data'] = unserialize($document->get_data());
  80. $results[$count]['score'] = ($i->get_percent());
  81. }
  82. $i->next();
  83. }
  84. switch ($count_type) {
  85. case 1: // Lower bound
  86. $count = $matches->get_matches_lower_bound();
  87. break;
  88. case 2: // Upper bound
  89. $count = $matches->get_matches_upper_bound();
  90. break;
  91. case 0: // Best estimate
  92. default:
  93. $count = $matches->get_matches_estimated();
  94. break;
  95. }
  96. return [$count, $results];
  97. } catch (Exception $e) {
  98. display_xapian_error($e->getMessage());
  99. return null;
  100. }
  101. }
  102. /**
  103. * build a boolean query.
  104. */
  105. function xapian_get_boolean_query($term)
  106. {
  107. return new XapianQuery($term);
  108. }
  109. /**
  110. * Retrieve a list db terms.
  111. *
  112. * @param int $count Number of terms to retrieve
  113. * @param char $prefix The prefix of the term to retrieve
  114. * @param XapianDatabase $db Xapian database to connect
  115. *
  116. * @return array
  117. */
  118. function xapian_get_all_terms($count = 0, $prefix, $db = null)
  119. {
  120. try {
  121. if (!is_object($db)) {
  122. $db = new XapianDatabase(XAPIAN_DB);
  123. }
  124. if (!empty($prefix)) {
  125. $termi = $db->allterms_begin($prefix);
  126. } else {
  127. $termi = $db->allterms_begin();
  128. }
  129. $terms = [];
  130. $i = 0;
  131. for (; !$termi->equals($db->allterms_end()) && (++$i <= $count || $count == 0); $termi->next()) {
  132. $terms[] = [
  133. 'frequency' => $termi->get_termfreq(),
  134. 'name' => $termi->get_term(),
  135. ];
  136. }
  137. return $terms;
  138. } catch (Exception $e) {
  139. display_xapian_error($e->getMessage());
  140. return null;
  141. }
  142. }
  143. /**
  144. * Retrieve all terms of a document.
  145. *
  146. * @param XapianDocument document searched
  147. *
  148. * @return array
  149. */
  150. function xapian_get_doc_terms($doc = null, $prefix)
  151. {
  152. try {
  153. if (!is_a($doc, 'XapianDocument')) {
  154. return;
  155. }
  156. //TODO: make the filter by prefix on xapian if possible
  157. //ojwb marvil07: use Document::termlist_begin() and then skip_to(prefix) on the TermIterator
  158. //ojwb you'll need to check the end condition by hand though
  159. $terms = [];
  160. for ($termi = $doc->termlist_begin(); !$termi->equals($doc->termlist_end()); $termi->next()) {
  161. $term = [
  162. 'frequency' => $termi->get_termfreq(),
  163. 'name' => $termi->get_term(),
  164. ];
  165. if ($term['name'][0] === $prefix) {
  166. $terms[] = $term;
  167. }
  168. }
  169. return $terms;
  170. } catch (Exception $e) {
  171. display_xapian_error($e->getMessage());
  172. return null;
  173. }
  174. }
  175. /**
  176. * Join xapian queries.
  177. *
  178. * @param XapianQuery|array $query1
  179. * @param XapianQuery|array $query2
  180. * @param string $op
  181. *
  182. * @return XapianQuery query joined
  183. */
  184. function xapian_join_queries($query1, $query2 = null, $op = 'or')
  185. {
  186. // let decide how to join, avoiding include xapian.php outside
  187. switch ($op) {
  188. case 'or':
  189. $op = XapianQuery::OP_OR;
  190. break;
  191. case 'and':
  192. $op = XapianQuery::OP_AND;
  193. break;
  194. default:
  195. $op = XapianQuery::OP_OR;
  196. break;
  197. }
  198. // review parameters to decide how to join
  199. if (!is_array($query1)) {
  200. $query1 = [$query1];
  201. }
  202. if (is_null($query2)) {
  203. // join an array of queries with $op
  204. return new XapianQuery($op, $query1);
  205. }
  206. if (!is_array($query2)) {
  207. $query2 = [$query2];
  208. }
  209. return new XapianQuery($op, array_merge($query1, $query2));
  210. }
  211. /**
  212. * @author Isaac flores paz <florespaz@bidsoftperu.com>
  213. *
  214. * @param string The xapian error message
  215. *
  216. * @return string The chamilo error message
  217. */
  218. function display_xapian_error($xapian_error_message)
  219. {
  220. $message = explode(':', $xapian_error_message);
  221. $type_error_message = $message[0];
  222. if ($type_error_message == 'DatabaseOpeningError') {
  223. $message_error = get_lang('Failed to open the search database');
  224. } elseif ($type_error_message == 'DatabaseVersionError') {
  225. $message_error = get_lang('The search database uses an unsupported format');
  226. } elseif ($type_error_message == 'DatabaseModifiedError') {
  227. $message_error = get_lang('The search database has been modified/broken');
  228. } elseif ($type_error_message == 'DatabaseLockError') {
  229. $message_error = get_lang('Failed to lock the search database');
  230. } elseif ($type_error_message == 'DatabaseCreateError') {
  231. $message_error = get_lang('Failed to create the search database');
  232. } elseif ($type_error_message == 'DatabaseCorruptError') {
  233. $message_error = get_lang('The search database has suffered corruption');
  234. } elseif ($type_error_message == 'NetworkTimeoutError') {
  235. $message_error = get_lang('Connection timed out while communicating with the remote search database');
  236. } else {
  237. $message_error = get_lang('Error in search engine');
  238. }
  239. $display_message = get_lang('Error').' : '.$message_error;
  240. echo Display::return_message($display_message, 'error');
  241. }