riddle 1.4.0 → 1.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (133) hide show
  1. data/.gitignore +6 -0
  2. data/.travis.yml +16 -0
  3. data/Gemfile +6 -0
  4. data/HISTORY +45 -0
  5. data/LICENCE +20 -0
  6. data/README.textile +5 -3
  7. data/Rakefile +23 -0
  8. data/lib/riddle.rb +1 -0
  9. data/lib/riddle/0.9.9/configuration/searchd.rb +10 -8
  10. data/lib/riddle/auto_version.rb +2 -2
  11. data/lib/riddle/client.rb +117 -118
  12. data/lib/riddle/configuration.rb +6 -6
  13. data/lib/riddle/configuration/distributed_index.rb +16 -16
  14. data/lib/riddle/configuration/sql_source.rb +5 -5
  15. data/lib/riddle/controller.rb +28 -25
  16. data/lib/riddle/query.rb +31 -20
  17. data/lib/riddle/query/select.rb +69 -8
  18. data/lib/riddle/version.rb +3 -0
  19. data/riddle.gemspec +25 -0
  20. data/spec/fixtures/.gitignore +2 -0
  21. data/spec/fixtures/data/0.9.9/anchor.bin +0 -0
  22. data/spec/fixtures/data/0.9.9/any.bin +0 -0
  23. data/spec/fixtures/data/0.9.9/boolean.bin +0 -0
  24. data/spec/fixtures/data/0.9.9/comment.bin +0 -0
  25. data/spec/fixtures/data/0.9.9/distinct.bin +0 -0
  26. data/spec/fixtures/data/0.9.9/field_weights.bin +0 -0
  27. data/spec/fixtures/data/0.9.9/filter.bin +0 -0
  28. data/spec/fixtures/data/0.9.9/filter_array.bin +0 -0
  29. data/spec/fixtures/data/0.9.9/filter_array_exclude.bin +0 -0
  30. data/spec/fixtures/data/0.9.9/filter_boolean.bin +0 -0
  31. data/spec/fixtures/data/0.9.9/filter_floats.bin +0 -0
  32. data/spec/fixtures/data/0.9.9/filter_floats_exclude.bin +0 -0
  33. data/spec/fixtures/data/0.9.9/filter_range.bin +0 -0
  34. data/spec/fixtures/data/0.9.9/filter_range_exclude.bin +0 -0
  35. data/spec/fixtures/data/0.9.9/group.bin +0 -0
  36. data/spec/fixtures/data/0.9.9/index.bin +0 -0
  37. data/spec/fixtures/data/0.9.9/index_weights.bin +0 -0
  38. data/spec/fixtures/data/0.9.9/keywords_with_hits.bin +0 -0
  39. data/spec/fixtures/data/0.9.9/keywords_without_hits.bin +0 -0
  40. data/spec/fixtures/data/0.9.9/overrides.bin +0 -0
  41. data/spec/fixtures/data/0.9.9/phrase.bin +0 -0
  42. data/spec/fixtures/data/0.9.9/rank_mode.bin +0 -0
  43. data/spec/fixtures/data/0.9.9/select.bin +0 -0
  44. data/spec/fixtures/data/0.9.9/simple.bin +0 -0
  45. data/spec/fixtures/data/0.9.9/sort.bin +0 -0
  46. data/spec/fixtures/data/0.9.9/update_simple.bin +0 -0
  47. data/spec/fixtures/data/0.9.9/weights.bin +0 -0
  48. data/spec/fixtures/data/1.10/anchor.bin +0 -0
  49. data/spec/fixtures/data/1.10/any.bin +0 -0
  50. data/spec/fixtures/data/1.10/boolean.bin +0 -0
  51. data/spec/fixtures/data/1.10/comment.bin +0 -0
  52. data/spec/fixtures/data/1.10/distinct.bin +0 -0
  53. data/spec/fixtures/data/1.10/field_weights.bin +0 -0
  54. data/spec/fixtures/data/1.10/filter.bin +0 -0
  55. data/spec/fixtures/data/1.10/filter_array.bin +0 -0
  56. data/spec/fixtures/data/1.10/filter_array_exclude.bin +0 -0
  57. data/spec/fixtures/data/1.10/filter_boolean.bin +0 -0
  58. data/spec/fixtures/data/1.10/filter_floats.bin +0 -0
  59. data/spec/fixtures/data/1.10/filter_floats_exclude.bin +0 -0
  60. data/spec/fixtures/data/1.10/filter_range.bin +0 -0
  61. data/spec/fixtures/data/1.10/filter_range_exclude.bin +0 -0
  62. data/spec/fixtures/data/1.10/group.bin +0 -0
  63. data/spec/fixtures/data/1.10/index.bin +0 -0
  64. data/spec/fixtures/data/1.10/index_weights.bin +0 -0
  65. data/spec/fixtures/data/1.10/keywords_with_hits.bin +0 -0
  66. data/spec/fixtures/data/1.10/keywords_without_hits.bin +0 -0
  67. data/spec/fixtures/data/1.10/overrides.bin +0 -0
  68. data/spec/fixtures/data/1.10/phrase.bin +0 -0
  69. data/spec/fixtures/data/1.10/rank_mode.bin +0 -0
  70. data/spec/fixtures/data/1.10/select.bin +0 -0
  71. data/spec/fixtures/data/1.10/simple.bin +0 -0
  72. data/spec/fixtures/data/1.10/sort.bin +0 -0
  73. data/spec/fixtures/data/1.10/update_simple.bin +0 -0
  74. data/spec/fixtures/data/1.10/weights.bin +0 -0
  75. data/spec/fixtures/data/2.0.1/anchor.bin +0 -0
  76. data/spec/fixtures/data/2.0.1/any.bin +0 -0
  77. data/spec/fixtures/data/2.0.1/boolean.bin +0 -0
  78. data/spec/fixtures/data/2.0.1/comment.bin +0 -0
  79. data/spec/fixtures/data/2.0.1/distinct.bin +0 -0
  80. data/spec/fixtures/data/2.0.1/field_weights.bin +0 -0
  81. data/spec/fixtures/data/2.0.1/filter.bin +0 -0
  82. data/spec/fixtures/data/2.0.1/filter_array.bin +0 -0
  83. data/spec/fixtures/data/2.0.1/filter_array_exclude.bin +0 -0
  84. data/spec/fixtures/data/2.0.1/filter_boolean.bin +0 -0
  85. data/spec/fixtures/data/2.0.1/filter_floats.bin +0 -0
  86. data/spec/fixtures/data/2.0.1/filter_floats_exclude.bin +0 -0
  87. data/spec/fixtures/data/2.0.1/filter_range.bin +0 -0
  88. data/spec/fixtures/data/2.0.1/filter_range_exclude.bin +0 -0
  89. data/spec/fixtures/data/2.0.1/group.bin +0 -0
  90. data/spec/fixtures/data/2.0.1/index.bin +0 -0
  91. data/spec/fixtures/data/2.0.1/index_weights.bin +0 -0
  92. data/spec/fixtures/data/2.0.1/keywords_with_hits.bin +0 -0
  93. data/spec/fixtures/data/2.0.1/keywords_without_hits.bin +0 -0
  94. data/spec/fixtures/data/2.0.1/overrides.bin +0 -0
  95. data/spec/fixtures/data/2.0.1/phrase.bin +0 -0
  96. data/spec/fixtures/data/2.0.1/rank_mode.bin +0 -0
  97. data/spec/fixtures/data/2.0.1/select.bin +0 -0
  98. data/spec/fixtures/data/2.0.1/simple.bin +0 -0
  99. data/spec/fixtures/data/2.0.1/sort.bin +0 -0
  100. data/spec/fixtures/data/2.0.1/update_simple.bin +0 -0
  101. data/spec/fixtures/data/2.0.1/weights.bin +0 -0
  102. data/spec/fixtures/data_generator.0.9.8.php +208 -0
  103. data/spec/fixtures/data_generator.0.9.9.php +5 -0
  104. data/spec/fixtures/data_generator.1.10.php +5 -0
  105. data/spec/fixtures/data_generator.2.0.1.php +5 -0
  106. data/spec/fixtures/data_generator.php +223 -0
  107. data/spec/fixtures/sphinxapi.0.9.8.php +1228 -0
  108. data/spec/fixtures/sphinxapi.0.9.9.php +1646 -0
  109. data/spec/fixtures/sphinxapi.1.10.php +1728 -0
  110. data/spec/fixtures/sphinxapi.2.0.1.php +1731 -0
  111. data/spec/fixtures/sql/conf.example.yml +3 -0
  112. data/spec/fixtures/sql/data.sql +25000 -0
  113. data/spec/fixtures/sql/data.tsv +25000 -0
  114. data/spec/fixtures/sql/structure.sql +16 -0
  115. data/spec/functional/connection_spec.rb +10 -12
  116. data/spec/functional/excerpt_spec.rb +1 -1
  117. data/spec/functional/keywords_spec.rb +1 -1
  118. data/spec/functional/persistance_spec.rb +1 -1
  119. data/spec/functional/search_spec.rb +1 -1
  120. data/spec/functional/status_spec.rb +1 -1
  121. data/spec/functional/update_spec.rb +1 -1
  122. data/spec/riddle/auto_version_spec.rb +18 -10
  123. data/spec/riddle/query/select_spec.rb +78 -14
  124. data/spec/riddle/query_spec.rb +5 -3
  125. data/spec/spec_helper.rb +13 -15
  126. data/spec/support/binary_fixtures.rb +18 -0
  127. data/spec/support/sphinx.rb +135 -0
  128. data/spec/unit/client_spec.rb +150 -142
  129. data/spec/unit/configuration/distributed_index_spec.rb +15 -15
  130. data/spec/unit/configuration/searchd_spec.rb +28 -3
  131. data/spec/unit/configuration_spec.rb +6 -6
  132. metadata +254 -68
  133. data/spec/sphinx_helper.rb +0 -96
@@ -0,0 +1,1228 @@
1
+ <?php
2
+
3
+ //
4
+ // $Id: sphinxapi.php 1418 2008-08-28 15:30:05Z shodan $
5
+ //
6
+
7
+ //
8
+ // Copyright (c) 2001-2008, Andrew Aksyonoff. All rights reserved.
9
+ //
10
+ // This program is free software; you can redistribute it and/or modify
11
+ // it under the terms of the GNU General Public License. You should have
12
+ // received a copy of the GPL license along with this program; if you
13
+ // did not, you can find it at http://www.gnu.org/
14
+ //
15
+
16
+ /////////////////////////////////////////////////////////////////////////////
17
+ // PHP version of Sphinx searchd client (PHP API)
18
+ /////////////////////////////////////////////////////////////////////////////
19
+
20
+ /// known searchd commands
21
+ define ( "SEARCHD_COMMAND_SEARCH", 0 );
22
+ define ( "SEARCHD_COMMAND_EXCERPT", 1 );
23
+ define ( "SEARCHD_COMMAND_UPDATE", 2 );
24
+ define ( "SEARCHD_COMMAND_KEYWORDS",3 );
25
+
26
+ /// current client-side command implementation versions
27
+ define ( "VER_COMMAND_SEARCH", 0x113 );
28
+ define ( "VER_COMMAND_EXCERPT", 0x100 );
29
+ define ( "VER_COMMAND_UPDATE", 0x101 );
30
+ define ( "VER_COMMAND_KEYWORDS", 0x100 );
31
+
32
+ /// known searchd status codes
33
+ define ( "SEARCHD_OK", 0 );
34
+ define ( "SEARCHD_ERROR", 1 );
35
+ define ( "SEARCHD_RETRY", 2 );
36
+ define ( "SEARCHD_WARNING", 3 );
37
+
38
+ /// known match modes
39
+ define ( "SPH_MATCH_ALL", 0 );
40
+ define ( "SPH_MATCH_ANY", 1 );
41
+ define ( "SPH_MATCH_PHRASE", 2 );
42
+ define ( "SPH_MATCH_BOOLEAN", 3 );
43
+ define ( "SPH_MATCH_EXTENDED", 4 );
44
+ define ( "SPH_MATCH_FULLSCAN", 5 );
45
+ define ( "SPH_MATCH_EXTENDED2", 6 ); // extended engine V2 (TEMPORARY, WILL BE REMOVED)
46
+
47
+ /// known ranking modes (ext2 only)
48
+ define ( "SPH_RANK_PROXIMITY_BM25", 0 ); ///< default mode, phrase proximity major factor and BM25 minor one
49
+ define ( "SPH_RANK_BM25", 1 ); ///< statistical mode, BM25 ranking only (faster but worse quality)
50
+ define ( "SPH_RANK_NONE", 2 ); ///< no ranking, all matches get a weight of 1
51
+ define ( "SPH_RANK_WORDCOUNT", 3 ); ///< simple word-count weighting, rank is a weighted sum of per-field keyword occurence counts
52
+
53
+ /// known sort modes
54
+ define ( "SPH_SORT_RELEVANCE", 0 );
55
+ define ( "SPH_SORT_ATTR_DESC", 1 );
56
+ define ( "SPH_SORT_ATTR_ASC", 2 );
57
+ define ( "SPH_SORT_TIME_SEGMENTS", 3 );
58
+ define ( "SPH_SORT_EXTENDED", 4 );
59
+ define ( "SPH_SORT_EXPR", 5 );
60
+
61
+ /// known filter types
62
+ define ( "SPH_FILTER_VALUES", 0 );
63
+ define ( "SPH_FILTER_RANGE", 1 );
64
+ define ( "SPH_FILTER_FLOATRANGE", 2 );
65
+
66
+ /// known attribute types
67
+ define ( "SPH_ATTR_INTEGER", 1 );
68
+ define ( "SPH_ATTR_TIMESTAMP", 2 );
69
+ define ( "SPH_ATTR_ORDINAL", 3 );
70
+ define ( "SPH_ATTR_BOOL", 4 );
71
+ define ( "SPH_ATTR_FLOAT", 5 );
72
+ define ( "SPH_ATTR_MULTI", 0x40000000 );
73
+
74
+ /// known grouping functions
75
+ define ( "SPH_GROUPBY_DAY", 0 );
76
+ define ( "SPH_GROUPBY_WEEK", 1 );
77
+ define ( "SPH_GROUPBY_MONTH", 2 );
78
+ define ( "SPH_GROUPBY_YEAR", 3 );
79
+ define ( "SPH_GROUPBY_ATTR", 4 );
80
+ define ( "SPH_GROUPBY_ATTRPAIR", 5 );
81
+
82
+
83
+ /// portably pack numeric to 64 unsigned bits, network order
84
+ function sphPack64 ( $v )
85
+ {
86
+ assert ( is_numeric($v) );
87
+
88
+ // x64 route
89
+ if ( PHP_INT_SIZE>=8 )
90
+ {
91
+ $i = (int)$v;
92
+ return pack ( "NN", $i>>32, $i&((1<<32)-1) );
93
+ }
94
+
95
+ // x32 route, bcmath
96
+ $x = "4294967296";
97
+ if ( function_exists("bcmul") )
98
+ {
99
+ $h = bcdiv ( $v, $x, 0 );
100
+ $l = bcmod ( $v, $x );
101
+ return pack ( "NN", (float)$h, (float)$l ); // conversion to float is intentional; int would lose 31st bit
102
+ }
103
+
104
+ // x32 route, 15 or less decimal digits
105
+ // we can use float, because its actually double and has 52 precision bits
106
+ if ( strlen($v)<=15 )
107
+ {
108
+ $f = (float)$v;
109
+ $h = (int)($f/$x);
110
+ $l = (int)($f-$x*$h);
111
+ return pack ( "NN", $h, $l );
112
+ }
113
+
114
+ // x32 route, 16 or more decimal digits
115
+ // well, let me know if you *really* need this
116
+ die ( "INTERNAL ERROR: packing more than 15-digit numeric on 32-bit PHP is not implemented yet (contact support)" );
117
+ }
118
+
119
+
120
+ /// portably unpack 64 unsigned bits, network order to numeric
121
+ function sphUnpack64 ( $v )
122
+ {
123
+ list($h,$l) = array_values ( unpack ( "N*N*", $v ) );
124
+
125
+ // x64 route
126
+ if ( PHP_INT_SIZE>=8 )
127
+ {
128
+ if ( $h<0 ) $h += (1<<32); // because php 5.2.2 to 5.2.5 is totally fucked up again
129
+ if ( $l<0 ) $l += (1<<32);
130
+ return ($h<<32) + $l;
131
+ }
132
+
133
+ // x32 route
134
+ $h = sprintf ( "%u", $h );
135
+ $l = sprintf ( "%u", $l );
136
+ $x = "4294967296";
137
+
138
+ // bcmath
139
+ if ( function_exists("bcmul") )
140
+ return bcadd ( $l, bcmul ( $x, $h ) );
141
+
142
+ // no bcmath, 15 or less decimal digits
143
+ // we can use float, because its actually double and has 52 precision bits
144
+ if ( $h<1048576 )
145
+ {
146
+ $f = ((float)$h)*$x + (float)$l;
147
+ return sprintf ( "%.0f", $f ); // builtin conversion is only about 39-40 bits precise!
148
+ }
149
+
150
+ // x32 route, 16 or more decimal digits
151
+ // well, let me know if you *really* need this
152
+ die ( "INTERNAL ERROR: unpacking more than 15-digit numeric on 32-bit PHP is not implemented yet (contact support)" );
153
+ }
154
+
155
+
156
+ /// sphinx searchd client class
157
+ class SphinxClient
158
+ {
159
+ var $_host; ///< searchd host (default is "localhost")
160
+ var $_port; ///< searchd port (default is 3312)
161
+ var $_offset; ///< how many records to seek from result-set start (default is 0)
162
+ var $_limit; ///< how many records to return from result-set starting at offset (default is 20)
163
+ var $_mode; ///< query matching mode (default is SPH_MATCH_ALL)
164
+ var $_weights; ///< per-field weights (default is 1 for all fields)
165
+ var $_sort; ///< match sorting mode (default is SPH_SORT_RELEVANCE)
166
+ var $_sortby; ///< attribute to sort by (defualt is "")
167
+ var $_min_id; ///< min ID to match (default is 0, which means no limit)
168
+ var $_max_id; ///< max ID to match (default is 0, which means no limit)
169
+ var $_filters; ///< search filters
170
+ var $_groupby; ///< group-by attribute name
171
+ var $_groupfunc; ///< group-by function (to pre-process group-by attribute value with)
172
+ var $_groupsort; ///< group-by sorting clause (to sort groups in result set with)
173
+ var $_groupdistinct;///< group-by count-distinct attribute
174
+ var $_maxmatches; ///< max matches to retrieve
175
+ var $_cutoff; ///< cutoff to stop searching at (default is 0)
176
+ var $_retrycount; ///< distributed retries count
177
+ var $_retrydelay; ///< distributed retries delay
178
+ var $_anchor; ///< geographical anchor point
179
+ var $_indexweights; ///< per-index weights
180
+ var $_ranker; ///< ranking mode (default is SPH_RANK_PROXIMITY_BM25)
181
+ var $_maxquerytime; ///< max query time, milliseconds (default is 0, do not limit)
182
+ var $_fieldweights; ///< per-field-name weights
183
+
184
+ var $_error; ///< last error message
185
+ var $_warning; ///< last warning message
186
+
187
+ var $_reqs; ///< requests array for multi-query
188
+ var $_mbenc; ///< stored mbstring encoding
189
+ var $_arrayresult; ///< whether $result["matches"] should be a hash or an array
190
+ var $_timeout; ///< connect timeout
191
+
192
+ /////////////////////////////////////////////////////////////////////////////
193
+ // common stuff
194
+ /////////////////////////////////////////////////////////////////////////////
195
+
196
+ /// create a new client object and fill defaults
197
+ function SphinxClient ()
198
+ {
199
+ // per-client-object settings
200
+ $this->_host = "localhost";
201
+ $this->_port = 3312;
202
+
203
+ // per-query settings
204
+ $this->_offset = 0;
205
+ $this->_limit = 20;
206
+ $this->_mode = SPH_MATCH_ALL;
207
+ $this->_weights = array ();
208
+ $this->_sort = SPH_SORT_RELEVANCE;
209
+ $this->_sortby = "";
210
+ $this->_min_id = 0;
211
+ $this->_max_id = 0;
212
+ $this->_filters = array ();
213
+ $this->_groupby = "";
214
+ $this->_groupfunc = SPH_GROUPBY_DAY;
215
+ $this->_groupsort = "@group desc";
216
+ $this->_groupdistinct= "";
217
+ $this->_maxmatches = 1000;
218
+ $this->_cutoff = 0;
219
+ $this->_retrycount = 0;
220
+ $this->_retrydelay = 0;
221
+ $this->_anchor = array ();
222
+ $this->_indexweights= array ();
223
+ $this->_ranker = SPH_RANK_PROXIMITY_BM25;
224
+ $this->_maxquerytime= 0;
225
+ $this->_fieldweights= array();
226
+
227
+ $this->_error = ""; // per-reply fields (for single-query case)
228
+ $this->_warning = "";
229
+ $this->_reqs = array (); // requests storage (for multi-query case)
230
+ $this->_mbenc = "";
231
+ $this->_arrayresult = false;
232
+ $this->_timeout = 0;
233
+ }
234
+
235
+ /// get last error message (string)
236
+ function GetLastError ()
237
+ {
238
+ return $this->_error;
239
+ }
240
+
241
+ /// get last warning message (string)
242
+ function GetLastWarning ()
243
+ {
244
+ return $this->_warning;
245
+ }
246
+
247
+ /// set searchd host name (string) and port (integer)
248
+ function SetServer ( $host, $port )
249
+ {
250
+ assert ( is_string($host) );
251
+ assert ( is_int($port) );
252
+ $this->_host = $host;
253
+ $this->_port = $port;
254
+ }
255
+
256
+ /// set server connection timeout (0 to remove)
257
+ function SetConnectTimeout ( $timeout )
258
+ {
259
+ assert ( is_numeric($timeout) );
260
+ $this->_timeout = $timeout;
261
+ }
262
+
263
+ /////////////////////////////////////////////////////////////////////////////
264
+
265
+ /// enter mbstring workaround mode
266
+ function _MBPush ()
267
+ {
268
+ $this->_mbenc = "";
269
+ if ( ini_get ( "mbstring.func_overload" ) & 2 )
270
+ {
271
+ $this->_mbenc = mb_internal_encoding();
272
+ mb_internal_encoding ( "latin1" );
273
+ }
274
+ }
275
+
276
+ /// leave mbstring workaround mode
277
+ function _MBPop ()
278
+ {
279
+ if ( $this->_mbenc )
280
+ mb_internal_encoding ( $this->_mbenc );
281
+ }
282
+
283
+ /// connect to searchd server
284
+ function _Connect ()
285
+ {
286
+ $errno = 0;
287
+ $errstr = "";
288
+ if ( $this->_timeout<=0 )
289
+ $fp = @fsockopen ( $this->_host, $this->_port, $errno, $errstr );
290
+ else
291
+ $fp = @fsockopen ( $this->_host, $this->_port, $errno, $errstr, $this->_timeout );
292
+
293
+ if ( !$fp )
294
+ {
295
+ $errstr = trim ( $errstr );
296
+ $this->_error = "connection to {$this->_host}:{$this->_port} failed (errno=$errno, msg=$errstr)";
297
+ return false;
298
+ }
299
+
300
+ // check version
301
+ list(,$v) = unpack ( "N*", fread ( $fp, 4 ) );
302
+ $v = (int)$v;
303
+ if ( $v<1 )
304
+ {
305
+ fclose ( $fp );
306
+ $this->_error = "expected searchd protocol version 1+, got version '$v'";
307
+ return false;
308
+ }
309
+
310
+ // all ok, send my version
311
+ fwrite ( $fp, pack ( "N", 1 ) );
312
+ return $fp;
313
+ }
314
+
315
+ /// get and check response packet from searchd server
316
+ function _GetResponse ( $fp, $client_ver )
317
+ {
318
+ $response = "";
319
+ $len = 0;
320
+
321
+ $header = fread ( $fp, 8 );
322
+ if ( strlen($header)==8 )
323
+ {
324
+ list ( $status, $ver, $len ) = array_values ( unpack ( "n2a/Nb", $header ) );
325
+ $left = $len;
326
+ while ( $left>0 && !feof($fp) )
327
+ {
328
+ $chunk = fread ( $fp, $left );
329
+ if ( $chunk )
330
+ {
331
+ $response .= $chunk;
332
+ $left -= strlen($chunk);
333
+ }
334
+ }
335
+ }
336
+ fclose ( $fp );
337
+
338
+ // check response
339
+ $read = strlen ( $response );
340
+ if ( !$response || $read!=$len )
341
+ {
342
+ $this->_error = $len
343
+ ? "failed to read searchd response (status=$status, ver=$ver, len=$len, read=$read)"
344
+ : "received zero-sized searchd response";
345
+ return false;
346
+ }
347
+
348
+ // check status
349
+ if ( $status==SEARCHD_WARNING )
350
+ {
351
+ list(,$wlen) = unpack ( "N*", substr ( $response, 0, 4 ) );
352
+ $this->_warning = substr ( $response, 4, $wlen );
353
+ return substr ( $response, 4+$wlen );
354
+ }
355
+ if ( $status==SEARCHD_ERROR )
356
+ {
357
+ $this->_error = "searchd error: " . substr ( $response, 4 );
358
+ return false;
359
+ }
360
+ if ( $status==SEARCHD_RETRY )
361
+ {
362
+ $this->_error = "temporary searchd error: " . substr ( $response, 4 );
363
+ return false;
364
+ }
365
+ if ( $status!=SEARCHD_OK )
366
+ {
367
+ $this->_error = "unknown status code '$status'";
368
+ return false;
369
+ }
370
+
371
+ // check version
372
+ if ( $ver<$client_ver )
373
+ {
374
+ $this->_warning = sprintf ( "searchd command v.%d.%d older than client's v.%d.%d, some options might not work",
375
+ $ver>>8, $ver&0xff, $client_ver>>8, $client_ver&0xff );
376
+ }
377
+
378
+ return $response;
379
+ }
380
+
381
+ /////////////////////////////////////////////////////////////////////////////
382
+ // searching
383
+ /////////////////////////////////////////////////////////////////////////////
384
+
385
+ /// set offset and count into result set,
386
+ /// and optionally set max-matches and cutoff limits
387
+ function SetLimits ( $offset, $limit, $max=0, $cutoff=0 )
388
+ {
389
+ assert ( is_int($offset) );
390
+ assert ( is_int($limit) );
391
+ assert ( $offset>=0 );
392
+ assert ( $limit>0 );
393
+ assert ( $max>=0 );
394
+ $this->_offset = $offset;
395
+ $this->_limit = $limit;
396
+ if ( $max>0 )
397
+ $this->_maxmatches = $max;
398
+ if ( $cutoff>0 )
399
+ $this->_cutoff = $cutoff;
400
+ }
401
+
402
+ /// set maximum query time, in milliseconds, per-index
403
+ /// integer, 0 means "do not limit"
404
+ function SetMaxQueryTime ( $max )
405
+ {
406
+ assert ( is_int($max) );
407
+ assert ( $max>=0 );
408
+ $this->_maxquerytime = $max;
409
+ }
410
+
411
+ /// set matching mode
412
+ function SetMatchMode ( $mode )
413
+ {
414
+ assert ( $mode==SPH_MATCH_ALL
415
+ || $mode==SPH_MATCH_ANY
416
+ || $mode==SPH_MATCH_PHRASE
417
+ || $mode==SPH_MATCH_BOOLEAN
418
+ || $mode==SPH_MATCH_EXTENDED
419
+ || $mode==SPH_MATCH_FULLSCAN
420
+ || $mode==SPH_MATCH_EXTENDED2 );
421
+ $this->_mode = $mode;
422
+ }
423
+
424
+ /// set ranking mode
425
+ function SetRankingMode ( $ranker )
426
+ {
427
+ assert ( $ranker==SPH_RANK_PROXIMITY_BM25
428
+ || $ranker==SPH_RANK_BM25
429
+ || $ranker==SPH_RANK_NONE
430
+ || $ranker==SPH_RANK_WORDCOUNT );
431
+ $this->_ranker = $ranker;
432
+ }
433
+
434
+ /// set matches sorting mode
435
+ function SetSortMode ( $mode, $sortby="" )
436
+ {
437
+ assert (
438
+ $mode==SPH_SORT_RELEVANCE ||
439
+ $mode==SPH_SORT_ATTR_DESC ||
440
+ $mode==SPH_SORT_ATTR_ASC ||
441
+ $mode==SPH_SORT_TIME_SEGMENTS ||
442
+ $mode==SPH_SORT_EXTENDED ||
443
+ $mode==SPH_SORT_EXPR );
444
+ assert ( is_string($sortby) );
445
+ assert ( $mode==SPH_SORT_RELEVANCE || strlen($sortby)>0 );
446
+
447
+ $this->_sort = $mode;
448
+ $this->_sortby = $sortby;
449
+ }
450
+
451
+ /// bind per-field weights by order
452
+ /// DEPRECATED; use SetFieldWeights() instead
453
+ function SetWeights ( $weights )
454
+ {
455
+ assert ( is_array($weights) );
456
+ foreach ( $weights as $weight )
457
+ assert ( is_int($weight) );
458
+
459
+ $this->_weights = $weights;
460
+ }
461
+
462
+ /// bind per-field weights by name
463
+ function SetFieldWeights ( $weights )
464
+ {
465
+ assert ( is_array($weights) );
466
+ foreach ( $weights as $name=>$weight )
467
+ {
468
+ assert ( is_string($name) );
469
+ assert ( is_int($weight) );
470
+ }
471
+ $this->_fieldweights = $weights;
472
+ }
473
+
474
+ /// bind per-index weights by name
475
+ function SetIndexWeights ( $weights )
476
+ {
477
+ assert ( is_array($weights) );
478
+ foreach ( $weights as $index=>$weight )
479
+ {
480
+ assert ( is_string($index) );
481
+ assert ( is_int($weight) );
482
+ }
483
+ $this->_indexweights = $weights;
484
+ }
485
+
486
+ /// set IDs range to match
487
+ /// only match records if document ID is beetwen $min and $max (inclusive)
488
+ function SetIDRange ( $min, $max )
489
+ {
490
+ assert ( is_numeric($min) );
491
+ assert ( is_numeric($max) );
492
+ assert ( $min<=$max );
493
+ $this->_min_id = $min;
494
+ $this->_max_id = $max;
495
+ }
496
+
497
+ /// set values set filter
498
+ /// only match records where $attribute value is in given set
499
+ function SetFilter ( $attribute, $values, $exclude=false )
500
+ {
501
+ assert ( is_string($attribute) );
502
+ assert ( is_array($values) );
503
+ assert ( count($values) );
504
+
505
+ if ( is_array($values) && count($values) )
506
+ {
507
+ foreach ( $values as $value )
508
+ assert ( is_numeric($value) );
509
+
510
+ $this->_filters[] = array ( "type"=>SPH_FILTER_VALUES, "attr"=>$attribute, "exclude"=>$exclude, "values"=>$values );
511
+ }
512
+ }
513
+
514
+ /// set range filter
515
+ /// only match records if $attribute value is beetwen $min and $max (inclusive)
516
+ function SetFilterRange ( $attribute, $min, $max, $exclude=false )
517
+ {
518
+ assert ( is_string($attribute) );
519
+ assert ( is_int($min) );
520
+ assert ( is_int($max) );
521
+ assert ( $min<=$max );
522
+
523
+ $this->_filters[] = array ( "type"=>SPH_FILTER_RANGE, "attr"=>$attribute, "exclude"=>$exclude, "min"=>$min, "max"=>$max );
524
+ }
525
+
526
+ /// set float range filter
527
+ /// only match records if $attribute value is beetwen $min and $max (inclusive)
528
+ function SetFilterFloatRange ( $attribute, $min, $max, $exclude=false )
529
+ {
530
+ assert ( is_string($attribute) );
531
+ assert ( is_float($min) );
532
+ assert ( is_float($max) );
533
+ assert ( $min<=$max );
534
+
535
+ $this->_filters[] = array ( "type"=>SPH_FILTER_FLOATRANGE, "attr"=>$attribute, "exclude"=>$exclude, "min"=>$min, "max"=>$max );
536
+ }
537
+
538
+ /// setup anchor point for geosphere distance calculations
539
+ /// required to use @geodist in filters and sorting
540
+ /// latitude and longitude must be in radians
541
+ function SetGeoAnchor ( $attrlat, $attrlong, $lat, $long )
542
+ {
543
+ assert ( is_string($attrlat) );
544
+ assert ( is_string($attrlong) );
545
+ assert ( is_float($lat) );
546
+ assert ( is_float($long) );
547
+
548
+ $this->_anchor = array ( "attrlat"=>$attrlat, "attrlong"=>$attrlong, "lat"=>$lat, "long"=>$long );
549
+ }
550
+
551
+ /// set grouping attribute and function
552
+ function SetGroupBy ( $attribute, $func, $groupsort="@group desc" )
553
+ {
554
+ assert ( is_string($attribute) );
555
+ assert ( is_string($groupsort) );
556
+ assert ( $func==SPH_GROUPBY_DAY
557
+ || $func==SPH_GROUPBY_WEEK
558
+ || $func==SPH_GROUPBY_MONTH
559
+ || $func==SPH_GROUPBY_YEAR
560
+ || $func==SPH_GROUPBY_ATTR
561
+ || $func==SPH_GROUPBY_ATTRPAIR );
562
+
563
+ $this->_groupby = $attribute;
564
+ $this->_groupfunc = $func;
565
+ $this->_groupsort = $groupsort;
566
+ }
567
+
568
+ /// set count-distinct attribute for group-by queries
569
+ function SetGroupDistinct ( $attribute )
570
+ {
571
+ assert ( is_string($attribute) );
572
+ $this->_groupdistinct = $attribute;
573
+ }
574
+
575
+ /// set distributed retries count and delay
576
+ function SetRetries ( $count, $delay=0 )
577
+ {
578
+ assert ( is_int($count) && $count>=0 );
579
+ assert ( is_int($delay) && $delay>=0 );
580
+ $this->_retrycount = $count;
581
+ $this->_retrydelay = $delay;
582
+ }
583
+
584
+ /// set result set format (hash or array; hash by default)
585
+ /// PHP specific; needed for group-by-MVA result sets that may contain duplicate IDs
586
+ function SetArrayResult ( $arrayresult )
587
+ {
588
+ assert ( is_bool($arrayresult) );
589
+ $this->_arrayresult = $arrayresult;
590
+ }
591
+
592
+ //////////////////////////////////////////////////////////////////////////////
593
+
594
+ /// clear all filters (for multi-queries)
595
+ function ResetFilters ()
596
+ {
597
+ $this->_filters = array();
598
+ $this->_anchor = array();
599
+ }
600
+
601
+ /// clear groupby settings (for multi-queries)
602
+ function ResetGroupBy ()
603
+ {
604
+ $this->_groupby = "";
605
+ $this->_groupfunc = SPH_GROUPBY_DAY;
606
+ $this->_groupsort = "@group desc";
607
+ $this->_groupdistinct= "";
608
+ }
609
+
610
+ //////////////////////////////////////////////////////////////////////////////
611
+
612
+ /// connect to searchd server, run given search query through given indexes,
613
+ /// and return the search results
614
+ function Query ( $query, $index="*", $comment="" )
615
+ {
616
+ assert ( empty($this->_reqs) );
617
+
618
+ $this->AddQuery ( $query, $index, $comment );
619
+ $results = $this->RunQueries ();
620
+ $this->_reqs = array (); // just in case it failed too early
621
+
622
+ if ( !is_array($results) )
623
+ return false; // probably network error; error message should be already filled
624
+
625
+ $this->_error = $results[0]["error"];
626
+ $this->_warning = $results[0]["warning"];
627
+ if ( $results[0]["status"]==SEARCHD_ERROR )
628
+ return false;
629
+ else
630
+ return $results[0];
631
+ }
632
+
633
+ /// helper to pack floats in network byte order
634
+ function _PackFloat ( $f )
635
+ {
636
+ $t1 = pack ( "f", $f ); // machine order
637
+ list(,$t2) = unpack ( "L*", $t1 ); // int in machine order
638
+ return pack ( "N", $t2 );
639
+ }
640
+
641
+ /// add query to multi-query batch
642
+ /// returns index into results array from RunQueries() call
643
+ function AddQuery ( $query, $index="*", $comment="" )
644
+ {
645
+ // mbstring workaround
646
+ $this->_MBPush ();
647
+
648
+ // build request
649
+ $req = pack ( "NNNNN", $this->_offset, $this->_limit, $this->_mode, $this->_ranker, $this->_sort ); // mode and limits
650
+ $req .= pack ( "N", strlen($this->_sortby) ) . $this->_sortby;
651
+ $req .= pack ( "N", strlen($query) ) . $query; // query itself
652
+ $req .= pack ( "N", count($this->_weights) ); // weights
653
+ foreach ( $this->_weights as $weight )
654
+ $req .= pack ( "N", (int)$weight );
655
+ $req .= pack ( "N", strlen($index) ) . $index; // indexes
656
+ $req .= pack ( "N", 1 ); // id64 range marker
657
+ $req .= sphPack64 ( $this->_min_id ) . sphPack64 ( $this->_max_id ); // id64 range
658
+
659
+ // filters
660
+ $req .= pack ( "N", count($this->_filters) );
661
+ foreach ( $this->_filters as $filter )
662
+ {
663
+ $req .= pack ( "N", strlen($filter["attr"]) ) . $filter["attr"];
664
+ $req .= pack ( "N", $filter["type"] );
665
+ switch ( $filter["type"] )
666
+ {
667
+ case SPH_FILTER_VALUES:
668
+ $req .= pack ( "N", count($filter["values"]) );
669
+ foreach ( $filter["values"] as $value )
670
+ $req .= pack ( "N", floatval($value) ); // this uberhack is to workaround 32bit signed int limit on x32 platforms
671
+ break;
672
+
673
+ case SPH_FILTER_RANGE:
674
+ $req .= pack ( "NN", $filter["min"], $filter["max"] );
675
+ break;
676
+
677
+ case SPH_FILTER_FLOATRANGE:
678
+ $req .= $this->_PackFloat ( $filter["min"] ) . $this->_PackFloat ( $filter["max"] );
679
+ break;
680
+
681
+ default:
682
+ assert ( 0 && "internal error: unhandled filter type" );
683
+ }
684
+ $req .= pack ( "N", $filter["exclude"] );
685
+ }
686
+
687
+ // group-by clause, max-matches count, group-sort clause, cutoff count
688
+ $req .= pack ( "NN", $this->_groupfunc, strlen($this->_groupby) ) . $this->_groupby;
689
+ $req .= pack ( "N", $this->_maxmatches );
690
+ $req .= pack ( "N", strlen($this->_groupsort) ) . $this->_groupsort;
691
+ $req .= pack ( "NNN", $this->_cutoff, $this->_retrycount, $this->_retrydelay );
692
+ $req .= pack ( "N", strlen($this->_groupdistinct) ) . $this->_groupdistinct;
693
+
694
+ // anchor point
695
+ if ( empty($this->_anchor) )
696
+ {
697
+ $req .= pack ( "N", 0 );
698
+ } else
699
+ {
700
+ $a =& $this->_anchor;
701
+ $req .= pack ( "N", 1 );
702
+ $req .= pack ( "N", strlen($a["attrlat"]) ) . $a["attrlat"];
703
+ $req .= pack ( "N", strlen($a["attrlong"]) ) . $a["attrlong"];
704
+ $req .= $this->_PackFloat ( $a["lat"] ) . $this->_PackFloat ( $a["long"] );
705
+ }
706
+
707
+ // per-index weights
708
+ $req .= pack ( "N", count($this->_indexweights) );
709
+ foreach ( $this->_indexweights as $idx=>$weight )
710
+ $req .= pack ( "N", strlen($idx) ) . $idx . pack ( "N", $weight );
711
+
712
+ // max query time
713
+ $req .= pack ( "N", $this->_maxquerytime );
714
+
715
+ // per-field weights
716
+ $req .= pack ( "N", count($this->_fieldweights) );
717
+ foreach ( $this->_fieldweights as $field=>$weight )
718
+ $req .= pack ( "N", strlen($field) ) . $field . pack ( "N", $weight );
719
+
720
+ // comment
721
+ $req .= pack ( "N", strlen($comment) ) . $comment;
722
+
723
+ // mbstring workaround
724
+ $this->_MBPop ();
725
+
726
+ // store request to requests array
727
+ $this->_reqs[] = $req;
728
+ return count($this->_reqs)-1;
729
+ }
730
+
731
+ /// connect to searchd, run queries batch, and return an array of result sets
732
+ function RunQueries ()
733
+ {
734
+ if ( empty($this->_reqs) )
735
+ {
736
+ $this->_error = "no queries defined, issue AddQuery() first";
737
+ return false;
738
+ }
739
+
740
+ // mbstring workaround
741
+ $this->_MBPush ();
742
+
743
+ if (!( $fp = $this->_Connect() ))
744
+ {
745
+ $this->_MBPop ();
746
+ return false;
747
+ }
748
+
749
+ ////////////////////////////
750
+ // send query, get response
751
+ ////////////////////////////
752
+
753
+ $nreqs = count($this->_reqs);
754
+ $req = join ( "", $this->_reqs );
755
+ $len = 4+strlen($req);
756
+ $req = pack ( "nnNN", SEARCHD_COMMAND_SEARCH, VER_COMMAND_SEARCH, $len, $nreqs ) . $req; // add header
757
+
758
+ fwrite ( $fp, $req, $len+8 );
759
+ if (!( $response = $this->_GetResponse ( $fp, VER_COMMAND_SEARCH ) ))
760
+ {
761
+ $this->_MBPop ();
762
+ return false;
763
+ }
764
+
765
+ $this->_reqs = array ();
766
+
767
+ //////////////////
768
+ // parse response
769
+ //////////////////
770
+
771
+ $p = 0; // current position
772
+ $max = strlen($response); // max position for checks, to protect against broken responses
773
+
774
+ $results = array ();
775
+ for ( $ires=0; $ires<$nreqs && $p<$max; $ires++ )
776
+ {
777
+ $results[] = array();
778
+ $result =& $results[$ires];
779
+
780
+ $result["error"] = "";
781
+ $result["warning"] = "";
782
+
783
+ // extract status
784
+ list(,$status) = unpack ( "N*", substr ( $response, $p, 4 ) ); $p += 4;
785
+ $result["status"] = $status;
786
+ if ( $status!=SEARCHD_OK )
787
+ {
788
+ list(,$len) = unpack ( "N*", substr ( $response, $p, 4 ) ); $p += 4;
789
+ $message = substr ( $response, $p, $len ); $p += $len;
790
+
791
+ if ( $status==SEARCHD_WARNING )
792
+ {
793
+ $result["warning"] = $message;
794
+ } else
795
+ {
796
+ $result["error"] = $message;
797
+ continue;
798
+ }
799
+ }
800
+
801
+ // read schema
802
+ $fields = array ();
803
+ $attrs = array ();
804
+
805
+ list(,$nfields) = unpack ( "N*", substr ( $response, $p, 4 ) ); $p += 4;
806
+ while ( $nfields-->0 && $p<$max )
807
+ {
808
+ list(,$len) = unpack ( "N*", substr ( $response, $p, 4 ) ); $p += 4;
809
+ $fields[] = substr ( $response, $p, $len ); $p += $len;
810
+ }
811
+ $result["fields"] = $fields;
812
+
813
+ list(,$nattrs) = unpack ( "N*", substr ( $response, $p, 4 ) ); $p += 4;
814
+ while ( $nattrs-->0 && $p<$max )
815
+ {
816
+ list(,$len) = unpack ( "N*", substr ( $response, $p, 4 ) ); $p += 4;
817
+ $attr = substr ( $response, $p, $len ); $p += $len;
818
+ list(,$type) = unpack ( "N*", substr ( $response, $p, 4 ) ); $p += 4;
819
+ $attrs[$attr] = $type;
820
+ }
821
+ $result["attrs"] = $attrs;
822
+
823
+ // read match count
824
+ list(,$count) = unpack ( "N*", substr ( $response, $p, 4 ) ); $p += 4;
825
+ list(,$id64) = unpack ( "N*", substr ( $response, $p, 4 ) ); $p += 4;
826
+
827
+ // read matches
828
+ $idx = -1;
829
+ while ( $count-->0 && $p<$max )
830
+ {
831
+ // index into result array
832
+ $idx++;
833
+
834
+ // parse document id and weight
835
+ if ( $id64 )
836
+ {
837
+ $doc = sphUnpack64 ( substr ( $response, $p, 8 ) ); $p += 8;
838
+ list(,$weight) = unpack ( "N*", substr ( $response, $p, 4 ) ); $p += 4;
839
+ } else
840
+ {
841
+ list ( $doc, $weight ) = array_values ( unpack ( "N*N*",
842
+ substr ( $response, $p, 8 ) ) );
843
+ $p += 8;
844
+
845
+ if ( PHP_INT_SIZE>=8 )
846
+ {
847
+ // x64 route, workaround broken unpack() in 5.2.2+
848
+ if ( $doc<0 ) $doc += (1<<32);
849
+ } else
850
+ {
851
+ // x32 route, workaround php signed/unsigned braindamage
852
+ $doc = sprintf ( "%u", $doc );
853
+ }
854
+ }
855
+ $weight = sprintf ( "%u", $weight );
856
+
857
+ // create match entry
858
+ if ( $this->_arrayresult )
859
+ $result["matches"][$idx] = array ( "id"=>$doc, "weight"=>$weight );
860
+ else
861
+ $result["matches"][$doc]["weight"] = $weight;
862
+
863
+ // parse and create attributes
864
+ $attrvals = array ();
865
+ foreach ( $attrs as $attr=>$type )
866
+ {
867
+ // handle floats
868
+ if ( $type==SPH_ATTR_FLOAT )
869
+ {
870
+ list(,$uval) = unpack ( "N*", substr ( $response, $p, 4 ) ); $p += 4;
871
+ list(,$fval) = unpack ( "f*", pack ( "L", $uval ) );
872
+ $attrvals[$attr] = $fval;
873
+ continue;
874
+ }
875
+
876
+ // handle everything else as unsigned ints
877
+ list(,$val) = unpack ( "N*", substr ( $response, $p, 4 ) ); $p += 4;
878
+ if ( $type & SPH_ATTR_MULTI )
879
+ {
880
+ $attrvals[$attr] = array ();
881
+ $nvalues = $val;
882
+ while ( $nvalues-->0 && $p<$max )
883
+ {
884
+ list(,$val) = unpack ( "N*", substr ( $response, $p, 4 ) ); $p += 4;
885
+ $attrvals[$attr][] = sprintf ( "%u", $val );
886
+ }
887
+ } else
888
+ {
889
+ $attrvals[$attr] = sprintf ( "%u", $val );
890
+ }
891
+ }
892
+
893
+ if ( $this->_arrayresult )
894
+ $result["matches"][$idx]["attrs"] = $attrvals;
895
+ else
896
+ $result["matches"][$doc]["attrs"] = $attrvals;
897
+ }
898
+
899
+ list ( $total, $total_found, $msecs, $words ) =
900
+ array_values ( unpack ( "N*N*N*N*", substr ( $response, $p, 16 ) ) );
901
+ $result["total"] = sprintf ( "%u", $total );
902
+ $result["total_found"] = sprintf ( "%u", $total_found );
903
+ $result["time"] = sprintf ( "%.3f", $msecs/1000 );
904
+ $p += 16;
905
+
906
+ while ( $words-->0 && $p<$max )
907
+ {
908
+ list(,$len) = unpack ( "N*", substr ( $response, $p, 4 ) ); $p += 4;
909
+ $word = substr ( $response, $p, $len ); $p += $len;
910
+ list ( $docs, $hits ) = array_values ( unpack ( "N*N*", substr ( $response, $p, 8 ) ) ); $p += 8;
911
+ $result["words"][$word] = array (
912
+ "docs"=>sprintf ( "%u", $docs ),
913
+ "hits"=>sprintf ( "%u", $hits ) );
914
+ }
915
+ }
916
+
917
+ $this->_MBPop ();
918
+ return $results;
919
+ }
920
+
921
+ /////////////////////////////////////////////////////////////////////////////
922
+ // excerpts generation
923
+ /////////////////////////////////////////////////////////////////////////////
924
+
925
+ /// connect to searchd server, and generate exceprts (snippets)
926
+ /// of given documents for given query. returns false on failure,
927
+ /// an array of snippets on success
928
+ function BuildExcerpts ( $docs, $index, $words, $opts=array() )
929
+ {
930
+ assert ( is_array($docs) );
931
+ assert ( is_string($index) );
932
+ assert ( is_string($words) );
933
+ assert ( is_array($opts) );
934
+
935
+ $this->_MBPush ();
936
+
937
+ if (!( $fp = $this->_Connect() ))
938
+ {
939
+ $this->_MBPop();
940
+ return false;
941
+ }
942
+
943
+ /////////////////
944
+ // fixup options
945
+ /////////////////
946
+
947
+ if ( !isset($opts["before_match"]) ) $opts["before_match"] = "<b>";
948
+ if ( !isset($opts["after_match"]) ) $opts["after_match"] = "</b>";
949
+ if ( !isset($opts["chunk_separator"]) ) $opts["chunk_separator"] = " ... ";
950
+ if ( !isset($opts["limit"]) ) $opts["limit"] = 256;
951
+ if ( !isset($opts["around"]) ) $opts["around"] = 5;
952
+ if ( !isset($opts["exact_phrase"]) ) $opts["exact_phrase"] = false;
953
+ if ( !isset($opts["single_passage"]) ) $opts["single_passage"] = false;
954
+ if ( !isset($opts["use_boundaries"]) ) $opts["use_boundaries"] = false;
955
+ if ( !isset($opts["weight_order"]) ) $opts["weight_order"] = false;
956
+
957
+ /////////////////
958
+ // build request
959
+ /////////////////
960
+
961
+ // v.1.0 req
962
+ $flags = 1; // remove spaces
963
+ if ( $opts["exact_phrase"] ) $flags |= 2;
964
+ if ( $opts["single_passage"] ) $flags |= 4;
965
+ if ( $opts["use_boundaries"] ) $flags |= 8;
966
+ if ( $opts["weight_order"] ) $flags |= 16;
967
+ $req = pack ( "NN", 0, $flags ); // mode=0, flags=$flags
968
+ $req .= pack ( "N", strlen($index) ) . $index; // req index
969
+ $req .= pack ( "N", strlen($words) ) . $words; // req words
970
+
971
+ // options
972
+ $req .= pack ( "N", strlen($opts["before_match"]) ) . $opts["before_match"];
973
+ $req .= pack ( "N", strlen($opts["after_match"]) ) . $opts["after_match"];
974
+ $req .= pack ( "N", strlen($opts["chunk_separator"]) ) . $opts["chunk_separator"];
975
+ $req .= pack ( "N", (int)$opts["limit"] );
976
+ $req .= pack ( "N", (int)$opts["around"] );
977
+
978
+ // documents
979
+ $req .= pack ( "N", count($docs) );
980
+ foreach ( $docs as $doc )
981
+ {
982
+ assert ( is_string($doc) );
983
+ $req .= pack ( "N", strlen($doc) ) . $doc;
984
+ }
985
+
986
+ ////////////////////////////
987
+ // send query, get response
988
+ ////////////////////////////
989
+
990
+ $len = strlen($req);
991
+ $req = pack ( "nnN", SEARCHD_COMMAND_EXCERPT, VER_COMMAND_EXCERPT, $len ) . $req; // add header
992
+ $wrote = fwrite ( $fp, $req, $len+8 );
993
+ if (!( $response = $this->_GetResponse ( $fp, VER_COMMAND_EXCERPT ) ))
994
+ {
995
+ $this->_MBPop ();
996
+ return false;
997
+ }
998
+
999
+ //////////////////
1000
+ // parse response
1001
+ //////////////////
1002
+
1003
+ $pos = 0;
1004
+ $res = array ();
1005
+ $rlen = strlen($response);
1006
+ for ( $i=0; $i<count($docs); $i++ )
1007
+ {
1008
+ list(,$len) = unpack ( "N*", substr ( $response, $pos, 4 ) );
1009
+ $pos += 4;
1010
+
1011
+ if ( $pos+$len > $rlen )
1012
+ {
1013
+ $this->_error = "incomplete reply";
1014
+ $this->_MBPop ();
1015
+ return false;
1016
+ }
1017
+ $res[] = $len ? substr ( $response, $pos, $len ) : "";
1018
+ $pos += $len;
1019
+ }
1020
+
1021
+ $this->_MBPop ();
1022
+ return $res;
1023
+ }
1024
+
1025
+
1026
+ /////////////////////////////////////////////////////////////////////////////
1027
+ // keyword generation
1028
+ /////////////////////////////////////////////////////////////////////////////
1029
+
1030
+ /// connect to searchd server, and generate keyword list for a given query
1031
+ /// returns false on failure,
1032
+ /// an array of words on success
1033
+ function BuildKeywords ( $query, $index, $hits )
1034
+ {
1035
+ assert ( is_string($query) );
1036
+ assert ( is_string($index) );
1037
+ assert ( is_bool($hits) );
1038
+
1039
+ // Commented out for testing Riddle
1040
+ // $this->_MBPush ();
1041
+ //
1042
+ // if (!( $fp = $this->_Connect() ))
1043
+ // {
1044
+ // $this->_MBPop();
1045
+ // return false;
1046
+ // }
1047
+
1048
+ /////////////////
1049
+ // build request
1050
+ /////////////////
1051
+
1052
+ // v.1.0 req
1053
+ $req = pack ( "N", strlen($query) ) . $query; // req query
1054
+ $req .= pack ( "N", strlen($index) ) . $index; // req index
1055
+ $req .= pack ( "N", (int)$hits );
1056
+
1057
+ // Line for testing Riddle:
1058
+ return $req;
1059
+
1060
+ ////////////////////////////
1061
+ // send query, get response
1062
+ ////////////////////////////
1063
+
1064
+ $len = strlen($req);
1065
+ $req = pack ( "nnN", SEARCHD_COMMAND_KEYWORDS, VER_COMMAND_KEYWORDS, $len ) . $req; // add header
1066
+ $wrote = fwrite ( $fp, $req, $len+8 );
1067
+ if (!( $response = $this->_GetResponse ( $fp, VER_COMMAND_KEYWORDS ) ))
1068
+ {
1069
+ $this->_MBPop ();
1070
+ return false;
1071
+ }
1072
+
1073
+ //////////////////
1074
+ // parse response
1075
+ //////////////////
1076
+
1077
+ $pos = 0;
1078
+ $res = array ();
1079
+ $rlen = strlen($response);
1080
+ list(,$nwords) = unpack ( "N*", substr ( $response, $pos, 4 ) );
1081
+ $pos += 4;
1082
+ for ( $i=0; $i<$nwords; $i++ )
1083
+ {
1084
+ list(,$len) = unpack ( "N*", substr ( $response, $pos, 4 ) ); $pos += 4;
1085
+ $tokenized = $len ? substr ( $response, $pos, $len ) : "";
1086
+ $pos += $len;
1087
+
1088
+ list(,$len) = unpack ( "N*", substr ( $response, $pos, 4 ) ); $pos += 4;
1089
+ $normalized = $len ? substr ( $response, $pos, $len ) : "";
1090
+ $pos += $len;
1091
+
1092
+ $res[] = array ( "tokenized"=>$tokenized, "normalized"=>$normalized );
1093
+
1094
+ if ( $hits )
1095
+ {
1096
+ list($ndocs,$nhits) = array_values ( unpack ( "N*N*", substr ( $response, $pos, 8 ) ) );
1097
+ $pos += 8;
1098
+ $res [$i]["docs"] = $ndocs;
1099
+ $res [$i]["hits"] = $nhits;
1100
+ }
1101
+
1102
+ if ( $pos > $rlen )
1103
+ {
1104
+ $this->_error = "incomplete reply";
1105
+ $this->_MBPop ();
1106
+ return false;
1107
+ }
1108
+ }
1109
+
1110
+ $this->_MBPop ();
1111
+ return $res;
1112
+ }
1113
+
1114
+ function EscapeString ( $string )
1115
+ {
1116
+ $from = array ( '(',')','|','-','!','@','~','"','&', '/' );
1117
+ $to = array ( '\(','\)','\|','\-','\!','\@','\~','\"', '\&', '\/' );
1118
+
1119
+ return str_replace ( $from, $to, $string );
1120
+ }
1121
+
1122
+ /////////////////////////////////////////////////////////////////////////////
1123
+ // attribute updates
1124
+ /////////////////////////////////////////////////////////////////////////////
1125
+
1126
+ /// update given attribute values on given documents in given indexes
1127
+ /// returns amount of updated documents (0 or more) on success, or -1 on failure
1128
+ function UpdateAttributes ( $index, $attrs, $values )
1129
+ {
1130
+ // verify everything
1131
+ assert ( is_string($index) );
1132
+
1133
+ assert ( is_array($attrs) );
1134
+ foreach ( $attrs as $attr )
1135
+ assert ( is_string($attr) );
1136
+
1137
+ assert ( is_array($values) );
1138
+ foreach ( $values as $id=>$entry )
1139
+ {
1140
+ assert ( is_numeric($id) );
1141
+ assert ( is_array($entry) );
1142
+ assert ( count($entry)==count($attrs) );
1143
+ foreach ( $entry as $v )
1144
+ assert ( is_int($v) );
1145
+ }
1146
+
1147
+ // build request
1148
+ $req = pack ( "N", strlen($index) ) . $index;
1149
+
1150
+ $req .= pack ( "N", count($attrs) );
1151
+ foreach ( $attrs as $attr )
1152
+ $req .= pack ( "N", strlen($attr) ) . $attr;
1153
+
1154
+ $req .= pack ( "N", count($values) );
1155
+ foreach ( $values as $id=>$entry )
1156
+ {
1157
+ $req .= sphPack64 ( $id );
1158
+ foreach ( $entry as $v )
1159
+ $req .= pack ( "N", $v );
1160
+ }
1161
+
1162
+ // Line for testing Riddle:
1163
+ return $req;
1164
+
1165
+ // mbstring workaround
1166
+ $this->_MBPush ();
1167
+
1168
+ // connect, send query, get response
1169
+ if (!( $fp = $this->_Connect() ))
1170
+ {
1171
+ $this->_MBPop ();
1172
+ return -1;
1173
+ }
1174
+
1175
+ $len = strlen($req);
1176
+ $req = pack ( "nnN", SEARCHD_COMMAND_UPDATE, VER_COMMAND_UPDATE, $len ) . $req; // add header
1177
+ fwrite ( $fp, $req, $len+8 );
1178
+
1179
+ if (!( $response = $this->_GetResponse ( $fp, VER_COMMAND_UPDATE ) ))
1180
+ {
1181
+ $this->_MBPop ();
1182
+ return -1;
1183
+ }
1184
+
1185
+ // parse response
1186
+ list(,$updated) = unpack ( "N*", substr ( $response, 0, 4 ) );
1187
+ $this->_MBPop ();
1188
+ return $updated;
1189
+ }
1190
+
1191
+ // Function for Riddle, copied from internal code.
1192
+ function FilterOutput()
1193
+ {
1194
+ foreach ( $this->_filters as $filter )
1195
+ {
1196
+ $req .= pack ( "N", strlen($filter["attr"]) ) . $filter["attr"];
1197
+ $req .= pack ( "N", $filter["type"] );
1198
+ switch ( $filter["type"] )
1199
+ {
1200
+ case SPH_FILTER_VALUES:
1201
+ $req .= pack ( "N", count($filter["values"]) );
1202
+ foreach ( $filter["values"] as $value )
1203
+ $req .= pack ( "N", floatval($value) ); // this uberhack is to workaround 32bit signed int limit on x32 platforms
1204
+ break;
1205
+
1206
+ case SPH_FILTER_RANGE:
1207
+ $req .= pack ( "NN", $filter["min"], $filter["max"] );
1208
+ break;
1209
+
1210
+ case SPH_FILTER_FLOATRANGE:
1211
+ $req .= $this->_PackFloat ( $filter["min"] ) . $this->_PackFloat ( $filter["max"] );
1212
+ break;
1213
+
1214
+ default:
1215
+ assert ( 0 && "internal error: unhandled filter type" );
1216
+ }
1217
+ $req .= pack ( "N", $filter["exclude"] );
1218
+ }
1219
+
1220
+ return $req;
1221
+ }
1222
+ }
1223
+
1224
+ //
1225
+ // $Id: sphinxapi.php 1418 2008-08-28 15:30:05Z shodan $
1226
+ //
1227
+
1228
+ ?>