BioTorrents.de’s version of Gazelle
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

sphinxqlquery.class.php 13KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411
  1. <?php
  2. #declare(strict_types=1);
  3. class SphinxqlQuery
  4. {
  5. private $Sphinxql;
  6. private $Errors;
  7. private $Expressions;
  8. private $Filters;
  9. private $GroupBy;
  10. private $Indexes;
  11. private $Limits;
  12. private $Options;
  13. private $QueryString;
  14. private $Select;
  15. private $SortBy;
  16. private $SortGroupBy;
  17. /**
  18. * Initialize Sphinxql object
  19. *
  20. * @param string $Server server address or hostname
  21. * @param int $Port listening port
  22. * @param string $Socket Unix socket address, overrides $Server:$Port
  23. */
  24. public function __construct($Server = SPHINXQL_HOST, $Port = SPHINXQL_PORT, $Socket = SPHINXQL_SOCK)
  25. {
  26. $this->Sphinxql = Sphinxql::init_connection($Server, $Port, $Socket);
  27. $this->reset();
  28. }
  29. /**
  30. * Specify what data the Sphinx query is supposed to return
  31. *
  32. * @param string $Fields Attributes and expressions
  33. * @return current Sphinxql query object
  34. */
  35. public function select($Fields)
  36. {
  37. $this->Select = $Fields;
  38. return $this;
  39. }
  40. /**
  41. * Specify the indexes to use in the search
  42. *
  43. * @param string $Indexes comma-separated list of indexes
  44. * @return current Sphinxql query object
  45. */
  46. public function from($Indexes)
  47. {
  48. $this->Indexes = $Indexes;
  49. return $this;
  50. }
  51. /**
  52. * Add attribute filter. Calling multiple filter functions results in boolean AND between each condition.
  53. *
  54. * @param string $Attribute attribute which the filter will apply to
  55. * @param mixed $Values scalar or array of numerical values. Array uses boolean OR in query condition
  56. * @param bool $Exclude whether to exclude or include matching documents. Default mode is to include matches
  57. * @return current Sphinxql query object
  58. */
  59. public function where($Attribute, $Values, $Exclude = false)
  60. {
  61. if (empty($Attribute) || !isset($Values)) {
  62. $this->error("Attribute name and filter value are required.");
  63. return $this;
  64. }
  65. $Filters = [];
  66. if (is_array($Values)) {
  67. foreach ($Values as $Value) {
  68. if (!is_number($Value)) {
  69. $this->error("Filters only support numeric values.");
  70. return $this;
  71. }
  72. }
  73. if ($Exclude) {
  74. $Filters[] = "$Attribute NOT IN (".implode(",", $Values).")";
  75. } else {
  76. $Filters[] = "$Attribute IN (".implode(",", $Values).")";
  77. }
  78. } else {
  79. if (!is_number($Values)) {
  80. $this->error("Filters only support numeric values.");
  81. return $this;
  82. }
  83. if ($Exclude) {
  84. $Filters[] = "$Attribute != $Values";
  85. } else {
  86. $Filters[] = "$Attribute = $Values";
  87. }
  88. }
  89. $this->Filters[] = implode(" AND ", $Filters);
  90. return $this;
  91. }
  92. /**
  93. * Add attribute less-than filter. Calling multiple filter functions results in boolean AND between each condition.
  94. *
  95. * @param string $Attribute attribute which the filter will apply to
  96. * @param array $Value upper limit for matches
  97. * @param bool $Inclusive whether to use <= or <
  98. * @return current Sphinxql query object
  99. */
  100. public function where_lt($Attribute, $Value, $Inclusive = false)
  101. {
  102. if (empty($Attribute) || !isset($Value) || !is_number($Value)) {
  103. $this->error("Attribute name is required and only numeric filters are supported.");
  104. return $this;
  105. }
  106. $this->Filters[] = $Inclusive ? "$Attribute <= $Value" : "$Attribute < $Value";
  107. return $this;
  108. }
  109. /**
  110. * Add attribute greater-than filter. Calling multiple filter functions results in boolean AND between each condition.
  111. *
  112. * @param string $Attribute attribute which the filter will apply to
  113. * @param array $Value lower limit for matches
  114. * @param bool $Inclusive whether to use >= or >
  115. * @return current Sphinxql query object
  116. */
  117. public function where_gt($Attribute, $Value, $Inclusive = false)
  118. {
  119. if (empty($Attribute) || !isset($Value) || !is_number($Value)) {
  120. $this->error("Attribute name is required and only numeric filters are supported.");
  121. return $this;
  122. }
  123. $this->Filters[] = $Inclusive ? "$Attribute >= $Value" : "$Attribute > $Value";
  124. return $this;
  125. }
  126. /**
  127. * Add attribute range filter. Calling multiple filter functions results in boolean AND between each condition.
  128. *
  129. * @param string $Attribute attribute which the filter will apply to
  130. * @param array $Values pair of numerical values that defines the filter range
  131. * @return current Sphinxql query object
  132. */
  133. public function where_between($Attribute, $Values)
  134. {
  135. if (empty($Attribute) || empty($Values) || count($Values) != 2 || !is_number($Values[0]) || !is_number($Values[1])) {
  136. $this->error("Filter range requires array of two numerical boundaries as values.");
  137. return $this;
  138. }
  139. $this->Filters[] = "$Attribute BETWEEN $Values[0] AND $Values[1]";
  140. return $this;
  141. }
  142. /**
  143. * Add fulltext query expression. Calling multiple filter functions results in boolean AND between each condition.
  144. * Query expression is escaped automatically
  145. *
  146. * @param string $Expr query expression
  147. * @param string $Field field to match $Expr against. Default is *, which means all available fields
  148. * @return current Sphinxql query object
  149. */
  150. public function where_match($Expr, $Field = '*', $Escape = true)
  151. {
  152. if (empty($Expr)) {
  153. return $this;
  154. }
  155. if ($Field !== false) {
  156. $Field = "@$Field ";
  157. }
  158. if ($Escape === true) {
  159. $this->Expressions[] = "$Field".Sphinxql::sph_escape_string($Expr);
  160. } else {
  161. $this->Expressions[] = $Field.$Expr;
  162. }
  163. return $this;
  164. }
  165. /**
  166. * Specify the order of the matches. Calling this function multiple times sets secondary priorities
  167. *
  168. * @param string $Attribute attribute to use for sorting.
  169. * Passing an empty attribute value will clear the current sort settings
  170. * @param string $Mode sort method to apply to the selected attribute
  171. * @return current Sphinxql query object
  172. */
  173. public function order_by($Attribute = false, $Mode = false)
  174. {
  175. if (empty($Attribute)) {
  176. $this->SortBy = [];
  177. } else {
  178. $this->SortBy[] = "$Attribute $Mode";
  179. }
  180. return $this;
  181. }
  182. /**
  183. * Specify how the results are grouped
  184. *
  185. * @param string $Attribute group matches with the same $Attribute value.
  186. * Passing an empty attribute value will clear the current group settings
  187. * @return current Sphinxql query object
  188. */
  189. public function group_by($Attribute = false)
  190. {
  191. if (empty($Attribute)) {
  192. $this->GroupBy = '';
  193. } else {
  194. $this->GroupBy = $Attribute;
  195. }
  196. return $this;
  197. }
  198. /**
  199. * Specify the order of the results within groups
  200. *
  201. * @param string $Attribute attribute to use for sorting.
  202. * Passing an empty attribute will clear the current group sort settings
  203. * @param string $Mode sort method to apply to the selected attribute
  204. * @return current Sphinxql query object
  205. */
  206. public function order_group_by($Attribute = false, $Mode = false)
  207. {
  208. if (empty($Attribute)) {
  209. $this->SortGroupBy = '';
  210. } else {
  211. $this->SortGroupBy = "$Attribute $Mode";
  212. }
  213. return $this;
  214. }
  215. /**
  216. * Specify the offset and amount of matches to return
  217. *
  218. * @param int $Offset number of matches to discard
  219. * @param int $Limit number of matches to return
  220. * @param int $MaxMatches number of results to store in the Sphinx server's memory. Must be >= ($Offset+$Limit)
  221. * @return current Sphinxql query object
  222. */
  223. public function limit($Offset, $Limit, $MaxMatches = SPHINX_MAX_MATCHES)
  224. {
  225. $this->Limits = "$Offset, $Limit";
  226. $this->set('max_matches', $MaxMatches);
  227. return $this;
  228. }
  229. /**
  230. * Tweak the settings to use for the query. Sanity checking shouldn't be needed as Sphinx already does it
  231. *
  232. * @param string $Name setting name
  233. * @param mixed $Value value
  234. * @return current Sphinxql query object
  235. */
  236. public function set($Name, $Value)
  237. {
  238. $this->Options[$Name] = $Value;
  239. return $this;
  240. }
  241. /**
  242. * Combine the query options into a valid Sphinx query segment
  243. *
  244. * @return string of options
  245. */
  246. private function build_options()
  247. {
  248. $Options = [];
  249. foreach ($this->Options as $Option => $Value) {
  250. $Options[] = "$Option = $Value";
  251. }
  252. return implode(', ', $Options);
  253. }
  254. /**
  255. * Combine the query conditions into a valid Sphinx query segment
  256. */
  257. private function build_query()
  258. {
  259. if (!$this->Indexes) {
  260. $this->error('Index name is required.');
  261. return false;
  262. }
  263. $this->QueryString = "SELECT $this->Select\nFROM $this->Indexes";
  264. if (!empty($this->Expressions)) {
  265. $this->Filters['expr'] = "MATCH('".implode(' ', $this->Expressions)."')";
  266. }
  267. if (!empty($this->Filters)) {
  268. $this->QueryString .= "\nWHERE ".implode("\n\tAND ", $this->Filters);
  269. }
  270. if (!empty($this->GroupBy)) {
  271. $this->QueryString .= "\nGROUP BY $this->GroupBy";
  272. }
  273. if (!empty($this->SortGroupBy)) {
  274. $this->QueryString .= "\nWITHIN GROUP ORDER BY $this->SortGroupBy";
  275. }
  276. if (!empty($this->SortBy)) {
  277. $this->QueryString .= "\nORDER BY ".implode(", ", $this->SortBy);
  278. }
  279. if (!empty($this->Limits)) {
  280. $this->QueryString .= "\nLIMIT $this->Limits";
  281. }
  282. if (!empty($this->Options)) {
  283. $Options = $this->build_options();
  284. $this->QueryString .= "\nOPTION $Options";
  285. }
  286. }
  287. /**
  288. * Construct and send the query. Register the query in the global Sphinxql object
  289. *
  290. * @param bool GetMeta whether to fetch meta data for the executed query. Default is yes
  291. * @return Sphinxql result object
  292. */
  293. public function query($GetMeta = true)
  294. {
  295. $QueryStartTime = microtime(true);
  296. $this->build_query();
  297. if (count($this->Errors) > 0) {
  298. $ErrorMsg = implode("\n", $this->Errors);
  299. $this->Sphinxql->error("Query builder found errors:\n$ErrorMsg");
  300. return new SphinxqlResult(null, null, 1, $ErrorMsg);
  301. }
  302. $QueryString = $this->QueryString;
  303. $Result = $this->send_query($GetMeta);
  304. $QueryProcessTime = (microtime(true) - $QueryStartTime)*1000;
  305. Sphinxql::register_query($QueryString, $QueryProcessTime);
  306. return $Result;
  307. }
  308. /**
  309. * Run a manually constructed query
  310. *
  311. * @param string Query query expression
  312. * @param bool GetMeta whether to fetch meta data for the executed query. Default is yes
  313. * @return Sphinxql result object
  314. */
  315. public function raw_query($Query, $GetMeta = true)
  316. {
  317. $this->QueryString = $Query;
  318. return $this->send_query($GetMeta);
  319. }
  320. /**
  321. * Run a pre-processed query. Only used internally
  322. *
  323. * @param bool GetMeta whether to fetch meta data for the executed query
  324. * @return Sphinxql result object
  325. */
  326. private function send_query($GetMeta)
  327. {
  328. if (!$this->QueryString) {
  329. return false;
  330. }
  331. $this->Sphinxql->sph_connect();
  332. $Result = $this->Sphinxql->query($this->QueryString);
  333. if ($Result === false) {
  334. $Errno = $this->Sphinxql->errno;
  335. $Error = $this->Sphinxql->error;
  336. $this->Sphinxql->error("Query returned error $Errno ($Error).\n$this->QueryString");
  337. $Meta = null;
  338. } else {
  339. $Errno = 0;
  340. $Error = '';
  341. $Meta = $GetMeta ? $this->get_meta() : null;
  342. }
  343. return new SphinxqlResult($Result, $Meta, $Errno, $Error);
  344. }
  345. /**
  346. * Reset all query options and conditions
  347. */
  348. public function reset()
  349. {
  350. $this->Errors = [];
  351. $this->Expressions = [];
  352. $this->Filters = [];
  353. $this->GroupBy = '';
  354. $this->Indexes = '';
  355. $this->Limits = [];
  356. $this->Options = array('ranker' => 'none');
  357. $this->QueryString = '';
  358. $this->Select = '*';
  359. $this->SortBy = [];
  360. $this->SortGroupBy = '';
  361. }
  362. /**
  363. * Fetch and store meta data for the last executed query
  364. *
  365. * @return meta data
  366. */
  367. private function get_meta()
  368. {
  369. return $this->raw_query("SHOW META", false)->to_pair(0, 1);
  370. }
  371. /**
  372. * Copy attribute filters from another SphinxqlQuery object
  373. *
  374. * @param SphinxqlQuery $SphQLSource object to copy the filters from
  375. * @return current SphinxqlQuery object
  376. */
  377. public function copy_attributes_from($SphQLSource)
  378. {
  379. $this->Filters = $SphQLSource->Filters;
  380. }
  381. /**
  382. * Store error messages
  383. */
  384. private function error($Msg)
  385. {
  386. $this->Errors[] = $Msg;
  387. }
  388. }