PageRenderTime 74ms CodeModel.GetById 30ms RepoModel.GetById 1ms app.codeStats 0ms

/phpmyfaq/src/phpMyFAQ/Search.php

http://github.com/thorsten/phpMyFAQ
PHP | 353 lines | 203 code | 42 blank | 108 comment | 24 complexity | a5c3c58c502c0e2e912eaef7ea710ebe MD5 | raw file
Possible License(s): MPL-2.0-no-copyleft-exception, LGPL-2.1, LGPL-3.0
  1. <?php
  2. /**
  3. * The phpMyFAQ Search class.
  4. *
  5. * This Source Code Form is subject to the terms of the Mozilla Public License,
  6. * v. 2.0. If a copy of the MPL was not distributed with this file, You can
  7. * obtain one at http://mozilla.org/MPL/2.0/.
  8. *
  9. * @package phpMyFAQ
  10. * @author Thorsten Rinne <thorsten@phpmyfaq.de>
  11. * @author Matteo Scaramuccia <matteo@scaramuccia.com>
  12. * @author Adrianna Musiol <musiol@imageaccess.de>
  13. * @copyright 2008-2021 phpMyFAQ Team
  14. * @license http://www.mozilla.org/MPL/2.0/ Mozilla Public License Version 2.0
  15. * @link https://www.phpmyfaq.de
  16. * @since 2008-01-26
  17. */
  18. namespace phpMyFAQ;
  19. use DateTime;
  20. use Exception;
  21. use phpMyFAQ\Search\Elasticsearch;
  22. use phpMyFAQ\Search\SearchFactory;
  23. use stdClass;
  24. /**
  25. * Class Search
  26. *
  27. * @package phpMyFAQ
  28. */
  29. class Search
  30. {
  31. /** @var Configuration */
  32. private $config;
  33. /** @var int */
  34. private $categoryId = null;
  35. /** @var Category */
  36. private $category = null;
  37. /** @var string */
  38. private $table;
  39. /**
  40. * Constructor.
  41. *
  42. * @param Configuration $config
  43. */
  44. public function __construct(Configuration $config)
  45. {
  46. $this->config = $config;
  47. $this->table = Database::getTablePrefix() . 'faqsearches';
  48. }
  49. /**
  50. * Setter for category.
  51. *
  52. * @param int $categoryId Entity ID
  53. */
  54. public function setCategoryId(int $categoryId): void
  55. {
  56. $this->categoryId = $categoryId;
  57. }
  58. /**
  59. * Getter for category.
  60. *
  61. * @return int
  62. */
  63. public function getCategoryId(): ?int
  64. {
  65. return $this->categoryId;
  66. }
  67. /**
  68. * The search function to handle the different search engines.
  69. *
  70. * @param string $searchTerm Text/Number (solution id)
  71. * @param bool $allLanguages true to search over all languages
  72. * @throws Exception
  73. * @return mixed[]
  74. */
  75. public function search(string $searchTerm, $allLanguages = true): array
  76. {
  77. if (is_numeric($searchTerm)) {
  78. return $this->searchDatabase($searchTerm, $allLanguages);
  79. }
  80. if ($this->config->get('search.enableElasticsearch')) {
  81. return $this->searchElasticsearch($searchTerm, $allLanguages);
  82. } else {
  83. return $this->searchDatabase($searchTerm, $allLanguages);
  84. }
  85. }
  86. /**
  87. * The auto complete function to handle the different search engines.
  88. *
  89. * @param string $searchTerm Text to auto complete
  90. * @throws Exception
  91. * @return mixed[]
  92. */
  93. public function autoComplete(string $searchTerm): array
  94. {
  95. if ($this->config->get('search.enableElasticsearch')) {
  96. $esSearch = new Elasticsearch($this->config);
  97. $allCategories = $this->getCategory()->getAllCategoryIds();
  98. $esSearch->setCategoryIds($allCategories);
  99. $esSearch->setLanguage($this->config->getLanguage()->getLanguage());
  100. return $esSearch->autoComplete($searchTerm);
  101. } else {
  102. return $this->searchDatabase($searchTerm, false);
  103. }
  104. }
  105. /**
  106. * The search function for the database powered full text search.
  107. *
  108. * @param string $searchTerm Text/Number (solution id)
  109. * @param bool $allLanguages true to search over all languages
  110. * @throws Exception
  111. * @return mixed[]
  112. */
  113. public function searchDatabase(string $searchTerm, $allLanguages = true): array
  114. {
  115. $fdTable = Database::getTablePrefix() . 'faqdata AS fd';
  116. $fcrTable = Database::getTablePrefix() . 'faqcategoryrelations';
  117. $condition = ['fd.active' => "'yes'"];
  118. $search = SearchFactory::create($this->config, ['database' => Database::getType()]);
  119. if (!is_null($this->getCategoryId()) && 0 < $this->getCategoryId()) {
  120. if ($this->getCategory() instanceof Category) {
  121. $children = $this->getCategory()->getChildNodes($this->getCategoryId());
  122. $selectedCategory = [
  123. $fcrTable . '.category_id' => array_merge((array)$this->getCategoryId(), $children),
  124. ];
  125. } else { // @phpstan-ignore-line
  126. $selectedCategory = [
  127. $fcrTable . '.category_id' => $this->getCategoryId(),
  128. ];
  129. }
  130. $condition = array_merge($selectedCategory, $condition);
  131. }
  132. if ((!$allLanguages) && (!is_numeric($searchTerm))) {
  133. $selectedLanguage = ['fd.lang' => "'" . $this->config->getLanguage()->getLanguage() . "'"];
  134. $condition = array_merge($selectedLanguage, $condition);
  135. }
  136. $search->setTable($fdTable)
  137. ->setResultColumns(
  138. [
  139. 'fd.id AS id',
  140. 'fd.lang AS lang',
  141. 'fd.solution_id AS solution_id',
  142. $fcrTable . '.category_id AS category_id',
  143. 'fd.thema AS question',
  144. 'fd.content AS answer'
  145. ]
  146. )
  147. ->setJoinedTable($fcrTable)
  148. ->setJoinedColumns(
  149. [
  150. 'fd.id = ' . $fcrTable . '.record_id',
  151. 'fd.lang = ' . $fcrTable . '.record_lang'
  152. ]
  153. )
  154. ->setConditions($condition);
  155. if (is_numeric($searchTerm)) {
  156. $search->setMatchingColumns(['fd.solution_id']);
  157. } else {
  158. $search->setMatchingColumns(['fd.thema', 'fd.content', 'fd.keywords']);
  159. }
  160. $result = $search->search($searchTerm);
  161. if (!$this->config->getDb()->numRows($result)) {
  162. return [];
  163. } else {
  164. return $this->config->getDb()->fetchAll($result);
  165. }
  166. }
  167. /**
  168. * The search function for the Elasticsearch powered full text search.
  169. *
  170. * @param string $searchTerm Text/Number (solution id)
  171. * @param bool $allLanguages true to search over all languages
  172. * @return stdClass[]
  173. */
  174. public function searchElasticsearch(string $searchTerm, $allLanguages = true): array
  175. {
  176. $esSearch = new Elasticsearch($this->config);
  177. if (!is_null($this->getCategoryId()) && 0 < $this->getCategoryId()) {
  178. if ($this->getCategory() instanceof Category) {
  179. $children = $this->getCategory()->getChildNodes($this->getCategoryId());
  180. $esSearch->setCategoryIds(array_merge([$this->getCategoryId()], $children));
  181. }
  182. } else {
  183. $allCategories = $this->getCategory()->getAllCategoryIds();
  184. $esSearch->setCategoryIds($allCategories);
  185. }
  186. if (!$allLanguages) {
  187. $esSearch->setLanguage($this->config->getLanguage()->getLanguage());
  188. }
  189. return $esSearch->search($searchTerm);
  190. }
  191. /**
  192. * Logging of search terms for improvements.
  193. *
  194. * @param string $searchTerm Search term
  195. * @throws Exception
  196. */
  197. public function logSearchTerm(string $searchTerm): void
  198. {
  199. if (Strings::strlen($searchTerm) === 0) {
  200. return;
  201. }
  202. $date = new DateTime();
  203. $query = sprintf(
  204. "INSERT INTO %s (id, lang, searchterm, searchdate) VALUES (%d, '%s', '%s', '%s')",
  205. $this->table,
  206. $this->config->getDb()->nextId($this->table, 'id'),
  207. $this->config->getLanguage()->getLanguage(),
  208. $this->config->getDb()->escape($searchTerm),
  209. $date->format('Y-m-d H:i:s')
  210. );
  211. $this->config->getDb()->query($query);
  212. }
  213. /**
  214. * Deletes a search term.
  215. *
  216. * @param string $searchTerm
  217. * @return bool
  218. */
  219. public function deleteSearchTerm(string $searchTerm): bool
  220. {
  221. $query = sprintf(
  222. "
  223. DELETE FROM
  224. %s
  225. WHERE
  226. searchterm = '%s'",
  227. $this->table,
  228. $searchTerm
  229. );
  230. return $this->config->getDb()->query($query);
  231. }
  232. /**
  233. * Deletes all search terms.
  234. *
  235. * @return bool
  236. */
  237. public function deleteAllSearchTerms(): bool
  238. {
  239. $query = sprintf('DELETE FROM %s', $this->table);
  240. return $this->config->getDb()->query($query);
  241. }
  242. /**
  243. * Returns the most popular searches.
  244. *
  245. * @param int $numResults Number of Results, default: 7
  246. * @param bool $withLang Should the language be included in the result?
  247. *
  248. * @return array<string[]>
  249. */
  250. public function getMostPopularSearches(int $numResults = 7, bool $withLang = false): array
  251. {
  252. $searchResult = [];
  253. $byLang = $withLang ? ', lang' : '';
  254. $query = sprintf(
  255. '
  256. SELECT
  257. MIN(id) as id, searchterm, COUNT(searchterm) AS number %s
  258. FROM
  259. %s
  260. GROUP BY
  261. searchterm %s
  262. ORDER BY
  263. number
  264. DESC',
  265. $byLang,
  266. $this->table,
  267. $byLang
  268. );
  269. $result = $this->config->getDb()->query($query);
  270. if (false !== $result) {
  271. $i = 0;
  272. while ($row = $this->config->getDb()->fetchObject($result)) {
  273. if ($i < $numResults) {
  274. $searchResult[] = (array)$row;
  275. }
  276. ++$i;
  277. }
  278. }
  279. return $searchResult;
  280. }
  281. /**
  282. * Returns row count from the "faqsearches" table.
  283. *
  284. * @return int
  285. */
  286. public function getSearchesCount(): int
  287. {
  288. $sql = sprintf(
  289. 'SELECT COUNT(1) AS count FROM %s',
  290. $this->table
  291. );
  292. $result = $this->config->getDb()->query($sql);
  293. return (int)$this->config->getDb()->fetchObject($result)->count;
  294. }
  295. /**
  296. * Sets the Entity object.
  297. *
  298. * @param Category $category
  299. */
  300. public function setCategory(Category $category): void
  301. {
  302. $this->category = $category;
  303. }
  304. /**
  305. * @return Category
  306. */
  307. public function getCategory(): Category
  308. {
  309. return $this->category;
  310. }
  311. }