PageRenderTime 36ms CodeModel.GetById 14ms RepoModel.GetById 0ms app.codeStats 1ms

/lib/core/Search/MySql/Index.php

https://gitlab.com/ElvisAns/tiki
PHP | 260 lines | 207 code | 41 blank | 12 comment | 28 complexity | 87fe9d0f6623957e29eba5a35d025751 MD5 | raw file
  1. <?php
  2. // (c) Copyright by authors of the Tiki Wiki CMS Groupware Project
  3. //
  4. // All Rights Reserved. See copyright.txt for details and a complete list of authors.
  5. // Licensed under the GNU LESSER GENERAL PUBLIC LICENSE. See license.txt for details.
  6. // $Id$
  7. class Search_MySql_Index implements Search_Index_Interface
  8. {
  9. private $db;
  10. private $table;
  11. private $builder;
  12. private $tfTranslator;
  13. private $index_name;
  14. public function __construct(TikiDb $db, $index)
  15. {
  16. $this->db = $db;
  17. $this->index_name = $index;
  18. $this->table = new Search_MySql_Table($db, $index);
  19. $this->builder = new Search_MySql_QueryBuilder($db);
  20. $this->tfTranslator = new Search_MySql_TrackerFieldTranslator();
  21. }
  22. public function destroy()
  23. {
  24. $this->table->drop();
  25. return true;
  26. }
  27. public function exists()
  28. {
  29. return $this->table->exists();
  30. }
  31. public function addDocument(array $data)
  32. {
  33. foreach ($data as $key => $value) {
  34. $this->handleField($key, $value);
  35. }
  36. $data = array_map(
  37. function ($entry) {
  38. return $this->getValue($entry);
  39. },
  40. $data
  41. );
  42. $this->table->insert($data);
  43. }
  44. private function getValue(Search_Type_Interface $data)
  45. {
  46. $value = $data->getValue();
  47. if (
  48. ($data instanceof Search_Type_Whole
  49. || $data instanceof Search_Type_PlainShortText
  50. || $data instanceof Search_Type_PlainText
  51. || $data instanceof Search_Type_MultivalueText)
  52. && strlen($value) >= 65535
  53. ) {
  54. $value = function_exists('mb_strcut') ?
  55. mb_strcut($value, 0, 65535) : substr($value, 0, 65535);
  56. }
  57. return $value;
  58. }
  59. private function handleField($name, $value)
  60. {
  61. if ($value instanceof Search_Type_Whole) {
  62. $this->table->ensureHasField($name, 'TEXT');
  63. } elseif ($value instanceof Search_Type_Numeric) {
  64. $this->table->ensureHasField($name, 'FLOAT');
  65. } elseif ($value instanceof Search_Type_PlainShortText) {
  66. $this->table->ensureHasField($name, 'TEXT');
  67. } elseif ($value instanceof Search_Type_PlainText) {
  68. $this->table->ensureHasField($name, 'TEXT');
  69. } elseif ($value instanceof Search_Type_PlainMediumText) {
  70. $this->table->ensureHasField($name, 'MEDIUMTEXT');
  71. } elseif ($value instanceof Search_Type_WikiText) {
  72. $this->table->ensureHasField($name, 'MEDIUMTEXT');
  73. } elseif ($value instanceof Search_Type_MultivalueText) {
  74. $this->table->ensureHasField($name, 'TEXT');
  75. } elseif ($value instanceof Search_Type_Timestamp) {
  76. $this->table->ensureHasField($name, $value->isDateOnly() ? 'DATE' : 'DATETIME');
  77. } else {
  78. throw new Exception('Unsupported type: ' . get_class($value));
  79. }
  80. }
  81. public function endUpdate()
  82. {
  83. $this->table->flush();
  84. }
  85. public function optimize()
  86. {
  87. $this->table->flush();
  88. }
  89. public function invalidateMultiple(array $objectList)
  90. {
  91. foreach ($objectList as $object) {
  92. $this->table->deleteMultiple($object);
  93. }
  94. }
  95. public function find(Search_Query_Interface $query, $resultStart, $resultCount)
  96. {
  97. try {
  98. $words = $this->getWords($query->getExpr());
  99. $condition = $this->builder->build($query->getExpr());
  100. $conditions = empty($condition) ? [] : [
  101. $this->table->expr($condition),
  102. ];
  103. $scoreFields = [];
  104. $indexes = $this->builder->getRequiredIndexes();
  105. foreach ($indexes as $index) {
  106. $this->table->ensureHasIndex($index['field'], $index['type']);
  107. if (! in_array($index, $scoreFields) && $index['type'] == 'fulltext') {
  108. $scoreFields[] = $index;
  109. }
  110. }
  111. $this->table->flush();
  112. $order = $this->getOrderClause($query, (bool) $scoreFields);
  113. $selectFields = $this->table->all();
  114. if ($scoreFields) {
  115. $str = $this->db->qstr(implode(' ', $words));
  116. $scoreCalc = '';
  117. foreach ($scoreFields as $field) {
  118. $scoreCalc .= $scoreCalc ? ' + ' : '';
  119. $scoreCalc .= "ROUND(MATCH(`{$this->tfTranslator->shortenize($field['field'])}`) AGAINST ($str),2) * {$field['weight']}";
  120. }
  121. $selectFields['score'] = $this->table->expr($scoreCalc);
  122. }
  123. $count = $this->table->fetchCount($conditions);
  124. $entries = $this->table->fetchAll($selectFields, $conditions, $resultCount, $resultStart, $order);
  125. foreach ($entries as &$entry) {
  126. foreach ($entry as $key => $val) {
  127. $normalized = $this->tfTranslator->normalize($key);
  128. if ($normalized != $key) {
  129. $entry[$normalized] = $val;
  130. unset($entry[$key]);
  131. }
  132. }
  133. }
  134. $resultSet = new Search_ResultSet($entries, $count, $resultStart, $resultCount);
  135. $resultSet->setHighlightHelper(new Search_MySql_HighlightHelper($words));
  136. return $resultSet;
  137. } catch (Search_MySql_QueryException $e) {
  138. if (empty($e->suppress_feedback)) {
  139. Feedback::error($e->getMessage());
  140. }
  141. $resultSet = new Search_ResultSet([], 0, $resultStart, $resultCount);
  142. return $resultSet;
  143. }
  144. }
  145. public function scroll(Search_Query_Interface $query)
  146. {
  147. $perPage = 100;
  148. $hasMore = true;
  149. for ($from = 0; $hasMore; $from += $perPage) {
  150. $result = $this->find($query, $from, $perPage);
  151. foreach ($result as $row) {
  152. yield $row;
  153. }
  154. $hasMore = $result->hasMore();
  155. }
  156. }
  157. private function getOrderClause($query, $useScore)
  158. {
  159. $order = $query->getSortOrder();
  160. if ($order->getField() == Search_Query_Order::FIELD_SCORE) {
  161. if ($useScore) {
  162. return ['score' => 'DESC'];
  163. } else {
  164. return; // No specific order
  165. }
  166. }
  167. $this->table->ensureHasIndex($order->getField(), 'sort');
  168. if ($order->getMode() == Search_Query_Order::MODE_NUMERIC) {
  169. return $this->table->expr("CAST(`{$this->tfTranslator->shortenize($order->getField())}` as SIGNED) {$order->getOrder()}");
  170. } else {
  171. return $this->table->expr("`{$this->tfTranslator->shortenize($order->getField())}` {$order->getOrder()}");
  172. }
  173. }
  174. private function getWords($expr)
  175. {
  176. $words = [];
  177. $factory = new Search_Type_Factory_Direct();
  178. $expr->walk(
  179. function ($node) use (&$words, $factory) {
  180. if ($node instanceof Search_Expr_Token && $node->getField() !== 'searchable') {
  181. $word = $node->getValue($factory)->getValue();
  182. if (is_string($word) && ! in_array($word, $words)) {
  183. $words[] = $word;
  184. }
  185. }
  186. }
  187. );
  188. return $words;
  189. }
  190. public function getTypeFactory()
  191. {
  192. return new Search_MySql_TypeFactory();
  193. }
  194. public function getFieldsCount()
  195. {
  196. return count($this->db->fetchAll("show columns from `{$this->index_name}`"));
  197. }
  198. /**
  199. * Function responsible for restoring old indexes
  200. * @param $indexesToRestore
  201. * @param $currentIndexTableName
  202. * @throws Exception
  203. */
  204. public function restoreOldIndexes($indexesToRestore, $currentIndexTableName)
  205. {
  206. $columns = array_column(TikiDb::get()->fetchAll("SHOW COLUMNS FROM $currentIndexTableName"), 'Field');
  207. foreach ($indexesToRestore as $indexToRestore) {
  208. if (! in_array($indexToRestore['Column_name'], $columns)) {
  209. continue;
  210. }
  211. $indexType = strtolower($indexToRestore['Index_type']) == 'fulltext' ? 'fulltext' : 'index';
  212. try {
  213. $this->table->ensureHasIndex($indexToRestore['Column_name'], $indexType);
  214. } catch (Search_MySql_QueryException $exception) {
  215. // Left blank on purpose
  216. }
  217. }
  218. }
  219. }