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

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