rlibsphinxclient 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. data/.gitignore +3 -0
  2. data/CHANGELOG.rdoc +18 -0
  3. data/MIT-LICENSE +20 -0
  4. data/README.rdoc +151 -0
  5. data/Rakefile +39 -0
  6. data/VERSION +1 -0
  7. data/ext/extconf.rb +20 -0
  8. data/ext/rlibsphinxclient.i +314 -0
  9. data/ext/rlibsphinxclient_wrap.c +5931 -0
  10. data/init.rb +1 -0
  11. data/lib/sphinx.rb +22 -0
  12. data/lib/sphinx/client.rb +1070 -0
  13. data/lib/sphinx/fast_client.rb +184 -0
  14. data/lib/sphinx/request.rb +49 -0
  15. data/lib/sphinx/response.rb +69 -0
  16. data/lib/sphinx/safe_executor.rb +11 -0
  17. data/lib/sphinx/timeout.rb +9 -0
  18. data/rlibsphinxclient.gemspec +117 -0
  19. data/spec/client_response_spec.rb +135 -0
  20. data/spec/client_spec.rb +548 -0
  21. data/spec/fixtures/default_search.php +8 -0
  22. data/spec/fixtures/default_search_index.php +8 -0
  23. data/spec/fixtures/excerpt_custom.php +11 -0
  24. data/spec/fixtures/excerpt_default.php +8 -0
  25. data/spec/fixtures/excerpt_flags.php +11 -0
  26. data/spec/fixtures/field_weights.php +9 -0
  27. data/spec/fixtures/filter.php +9 -0
  28. data/spec/fixtures/filter_exclude.php +9 -0
  29. data/spec/fixtures/filter_float_range.php +9 -0
  30. data/spec/fixtures/filter_float_range_exclude.php +9 -0
  31. data/spec/fixtures/filter_range.php +9 -0
  32. data/spec/fixtures/filter_range_exclude.php +9 -0
  33. data/spec/fixtures/filter_ranges.php +10 -0
  34. data/spec/fixtures/filters.php +10 -0
  35. data/spec/fixtures/filters_different.php +13 -0
  36. data/spec/fixtures/geo_anchor.php +9 -0
  37. data/spec/fixtures/group_by_attr.php +9 -0
  38. data/spec/fixtures/group_by_attrpair.php +9 -0
  39. data/spec/fixtures/group_by_day.php +9 -0
  40. data/spec/fixtures/group_by_day_sort.php +9 -0
  41. data/spec/fixtures/group_by_month.php +9 -0
  42. data/spec/fixtures/group_by_week.php +9 -0
  43. data/spec/fixtures/group_by_year.php +9 -0
  44. data/spec/fixtures/group_distinct.php +10 -0
  45. data/spec/fixtures/id_range.php +9 -0
  46. data/spec/fixtures/id_range64.php +9 -0
  47. data/spec/fixtures/index_weights.php +9 -0
  48. data/spec/fixtures/keywords.php +8 -0
  49. data/spec/fixtures/limits.php +9 -0
  50. data/spec/fixtures/limits_cutoff.php +9 -0
  51. data/spec/fixtures/limits_max.php +9 -0
  52. data/spec/fixtures/limits_max_cutoff.php +9 -0
  53. data/spec/fixtures/match_all.php +9 -0
  54. data/spec/fixtures/match_any.php +9 -0
  55. data/spec/fixtures/match_boolean.php +9 -0
  56. data/spec/fixtures/match_extended.php +9 -0
  57. data/spec/fixtures/match_extended2.php +9 -0
  58. data/spec/fixtures/match_fullscan.php +9 -0
  59. data/spec/fixtures/match_phrase.php +9 -0
  60. data/spec/fixtures/max_query_time.php +9 -0
  61. data/spec/fixtures/miltiple_queries.php +12 -0
  62. data/spec/fixtures/ranking_bm25.php +9 -0
  63. data/spec/fixtures/ranking_none.php +9 -0
  64. data/spec/fixtures/ranking_proximity_bm25.php +9 -0
  65. data/spec/fixtures/ranking_wordcount.php +9 -0
  66. data/spec/fixtures/retries.php +9 -0
  67. data/spec/fixtures/retries_delay.php +9 -0
  68. data/spec/fixtures/sort_attr_asc.php +9 -0
  69. data/spec/fixtures/sort_attr_desc.php +9 -0
  70. data/spec/fixtures/sort_expr.php +9 -0
  71. data/spec/fixtures/sort_extended.php +9 -0
  72. data/spec/fixtures/sort_relevance.php +9 -0
  73. data/spec/fixtures/sort_time_segments.php +9 -0
  74. data/spec/fixtures/sphinxapi.php +1181 -0
  75. data/spec/fixtures/update_attributes.php +8 -0
  76. data/spec/fixtures/weights.php +9 -0
  77. data/spec/sphinx/sphinx.conf +67 -0
  78. data/spec/sphinx/sphinx_test.sql +86 -0
  79. metadata +133 -0
@@ -0,0 +1,12 @@
1
+ <?php
2
+
3
+ require ("sphinxapi.php");
4
+
5
+ $cl = new SphinxClient();
6
+ $cl->SetRetries(10, 20);
7
+ $cl->AddQuery('test1');
8
+ $cl->SetGroupBy('attr', SPH_GROUPBY_DAY);
9
+ $cl->AddQuery('test2');
10
+ $cl->RunQueries();
11
+
12
+ ?>
@@ -0,0 +1,9 @@
1
+ <?php
2
+
3
+ require ("sphinxapi.php");
4
+
5
+ $cl = new SphinxClient();
6
+ $cl->SetRankingMode(SPH_RANK_BM25);
7
+ $cl->Query('query');
8
+
9
+ ?>
@@ -0,0 +1,9 @@
1
+ <?php
2
+
3
+ require ("sphinxapi.php");
4
+
5
+ $cl = new SphinxClient();
6
+ $cl->SetRankingMode(SPH_RANK_NONE);
7
+ $cl->Query('query');
8
+
9
+ ?>
@@ -0,0 +1,9 @@
1
+ <?php
2
+
3
+ require ("sphinxapi.php");
4
+
5
+ $cl = new SphinxClient();
6
+ $cl->SetRankingMode(SPH_RANK_PROXIMITY_BM25);
7
+ $cl->Query('query');
8
+
9
+ ?>
@@ -0,0 +1,9 @@
1
+ <?php
2
+
3
+ require ("sphinxapi.php");
4
+
5
+ $cl = new SphinxClient();
6
+ $cl->SetRankingMode(SPH_RANK_WORDCOUNT);
7
+ $cl->Query('query');
8
+
9
+ ?>
@@ -0,0 +1,9 @@
1
+ <?php
2
+
3
+ require ("sphinxapi.php");
4
+
5
+ $cl = new SphinxClient();
6
+ $cl->SetRetries(10);
7
+ $cl->Query('query');
8
+
9
+ ?>
@@ -0,0 +1,9 @@
1
+ <?php
2
+
3
+ require ("sphinxapi.php");
4
+
5
+ $cl = new SphinxClient();
6
+ $cl->SetRetries(10, 20);
7
+ $cl->Query('query');
8
+
9
+ ?>
@@ -0,0 +1,9 @@
1
+ <?php
2
+
3
+ require ("sphinxapi.php");
4
+
5
+ $cl = new SphinxClient();
6
+ $cl->SetSortMode(SPH_SORT_ATTR_ASC, 'sortby');
7
+ $cl->Query('query');
8
+
9
+ ?>
@@ -0,0 +1,9 @@
1
+ <?php
2
+
3
+ require ("sphinxapi.php");
4
+
5
+ $cl = new SphinxClient();
6
+ $cl->SetSortMode(SPH_SORT_ATTR_DESC, 'sortby');
7
+ $cl->Query('query');
8
+
9
+ ?>
@@ -0,0 +1,9 @@
1
+ <?php
2
+
3
+ require ("sphinxapi.php");
4
+
5
+ $cl = new SphinxClient();
6
+ $cl->SetSortMode(SPH_SORT_EXPR, 'sortby');
7
+ $cl->Query('query');
8
+
9
+ ?>
@@ -0,0 +1,9 @@
1
+ <?php
2
+
3
+ require ("sphinxapi.php");
4
+
5
+ $cl = new SphinxClient();
6
+ $cl->SetSortMode(SPH_SORT_EXTENDED, 'sortby');
7
+ $cl->Query('query');
8
+
9
+ ?>
@@ -0,0 +1,9 @@
1
+ <?php
2
+
3
+ require ("sphinxapi.php");
4
+
5
+ $cl = new SphinxClient();
6
+ $cl->SetSortMode(SPH_SORT_RELEVANCE);
7
+ $cl->Query('query');
8
+
9
+ ?>
@@ -0,0 +1,9 @@
1
+ <?php
2
+
3
+ require ("sphinxapi.php");
4
+
5
+ $cl = new SphinxClient();
6
+ $cl->SetSortMode(SPH_SORT_TIME_SEGMENTS, 'sortby');
7
+ $cl->Query('query');
8
+
9
+ ?>
@@ -0,0 +1,1181 @@
1
+ <?php
2
+
3
+ //
4
+ // $Id$
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
+
191
+ /////////////////////////////////////////////////////////////////////////////
192
+ // common stuff
193
+ /////////////////////////////////////////////////////////////////////////////
194
+
195
+ /// create a new client object and fill defaults
196
+ function SphinxClient ()
197
+ {
198
+ // per-client-object settings
199
+ $this->_host = "localhost";
200
+ $this->_port = 3312;
201
+
202
+ // per-query settings
203
+ $this->_offset = 0;
204
+ $this->_limit = 20;
205
+ $this->_mode = SPH_MATCH_ALL;
206
+ $this->_weights = array ();
207
+ $this->_sort = SPH_SORT_RELEVANCE;
208
+ $this->_sortby = "";
209
+ $this->_min_id = 0;
210
+ $this->_max_id = 0;
211
+ $this->_filters = array ();
212
+ $this->_groupby = "";
213
+ $this->_groupfunc = SPH_GROUPBY_DAY;
214
+ $this->_groupsort = "@group desc";
215
+ $this->_groupdistinct= "";
216
+ $this->_maxmatches = 1000;
217
+ $this->_cutoff = 0;
218
+ $this->_retrycount = 0;
219
+ $this->_retrydelay = 0;
220
+ $this->_anchor = array ();
221
+ $this->_indexweights= array ();
222
+ $this->_ranker = SPH_RANK_PROXIMITY_BM25;
223
+ $this->_maxquerytime= 0;
224
+ $this->_fieldweights= array();
225
+
226
+ $this->_error = ""; // per-reply fields (for single-query case)
227
+ $this->_warning = "";
228
+ $this->_reqs = array (); // requests storage (for multi-query case)
229
+ $this->_mbenc = "";
230
+ $this->_arrayresult = false;
231
+ }
232
+
233
+ /// get last error message (string)
234
+ function GetLastError ()
235
+ {
236
+ return $this->_error;
237
+ }
238
+
239
+ /// get last warning message (string)
240
+ function GetLastWarning ()
241
+ {
242
+ return $this->_warning;
243
+ }
244
+
245
+ /// set searchd host name (string) and port (integer)
246
+ function SetServer ( $host, $port )
247
+ {
248
+ assert ( is_string($host) );
249
+ assert ( is_int($port) );
250
+ $this->_host = $host;
251
+ $this->_port = $port;
252
+ }
253
+
254
+ /////////////////////////////////////////////////////////////////////////////
255
+
256
+ /// enter mbstring workaround mode
257
+ function _MBPush ()
258
+ {
259
+ $this->_mbenc = "";
260
+ if ( ini_get ( "mbstring.func_overload" ) & 2 )
261
+ {
262
+ $this->_mbenc = mb_internal_encoding();
263
+ mb_internal_encoding ( "latin1" );
264
+ }
265
+ }
266
+
267
+ /// leave mbstring workaround mode
268
+ function _MBPop ()
269
+ {
270
+ if ( $this->_mbenc )
271
+ mb_internal_encoding ( $this->_mbenc );
272
+ }
273
+
274
+ /// connect to searchd server
275
+ function _Connect ()
276
+ {
277
+ return fopen('php://stdout', 'w');
278
+ }
279
+
280
+ function _OldConnect()
281
+ {
282
+ if (!( $fp = @fsockopen ( $this->_host, $this->_port ) ) )
283
+ {
284
+ $this->_error = "connection to {$this->_host}:{$this->_port} failed";
285
+ return false;
286
+ }
287
+
288
+ // check version
289
+ list(,$v) = unpack ( "N*", fread ( $fp, 4 ) );
290
+ $v = (int)$v;
291
+ if ( $v<1 )
292
+ {
293
+ fclose ( $fp );
294
+ $this->_error = "expected searchd protocol version 1+, got version '$v'";
295
+ return false;
296
+ }
297
+
298
+ // all ok, send my version
299
+ fwrite ( $fp, pack ( "N", 1 ) );
300
+ return $fp;
301
+ }
302
+
303
+ /// get and check response packet from searchd server
304
+ function _GetResponse ( $fp, $client_ver )
305
+ {
306
+ return false;
307
+ }
308
+
309
+ function _OldGetResponse ( $fp, $client_ver )
310
+ {
311
+ $response = "";
312
+ $len = 0;
313
+
314
+ $header = fread ( $fp, 8 );
315
+ if ( strlen($header)==8 )
316
+ {
317
+ list ( $status, $ver, $len ) = array_values ( unpack ( "n2a/Nb", $header ) );
318
+ $left = $len;
319
+ while ( $left>0 && !feof($fp) )
320
+ {
321
+ $chunk = fread ( $fp, $left );
322
+ if ( $chunk )
323
+ {
324
+ $response .= $chunk;
325
+ $left -= strlen($chunk);
326
+ }
327
+ }
328
+ }
329
+ fclose ( $fp );
330
+
331
+ // check response
332
+ $read = strlen ( $response );
333
+ if ( !$response || $read!=$len )
334
+ {
335
+ $this->_error = $len
336
+ ? "failed to read searchd response (status=$status, ver=$ver, len=$len, read=$read)"
337
+ : "received zero-sized searchd response";
338
+ return false;
339
+ }
340
+
341
+ // check status
342
+ if ( $status==SEARCHD_WARNING )
343
+ {
344
+ list(,$wlen) = unpack ( "N*", substr ( $response, 0, 4 ) );
345
+ $this->_warning = substr ( $response, 4, $wlen );
346
+ return substr ( $response, 4+$wlen );
347
+ }
348
+ if ( $status==SEARCHD_ERROR )
349
+ {
350
+ $this->_error = "searchd error: " . substr ( $response, 4 );
351
+ return false;
352
+ }
353
+ if ( $status==SEARCHD_RETRY )
354
+ {
355
+ $this->_error = "temporary searchd error: " . substr ( $response, 4 );
356
+ return false;
357
+ }
358
+ if ( $status!=SEARCHD_OK )
359
+ {
360
+ $this->_error = "unknown status code '$status'";
361
+ return false;
362
+ }
363
+
364
+ // check version
365
+ if ( $ver<$client_ver )
366
+ {
367
+ $this->_warning = sprintf ( "searchd command v.%d.%d older than client's v.%d.%d, some options might not work",
368
+ $ver>>8, $ver&0xff, $client_ver>>8, $client_ver&0xff );
369
+ }
370
+
371
+ return $response;
372
+ }
373
+
374
+ /////////////////////////////////////////////////////////////////////////////
375
+ // searching
376
+ /////////////////////////////////////////////////////////////////////////////
377
+
378
+ /// set offset and count into result set,
379
+ /// and optionally set max-matches and cutoff limits
380
+ function SetLimits ( $offset, $limit, $max=0, $cutoff=0 )
381
+ {
382
+ assert ( is_int($offset) );
383
+ assert ( is_int($limit) );
384
+ assert ( $offset>=0 );
385
+ assert ( $limit>0 );
386
+ assert ( $max>=0 );
387
+ $this->_offset = $offset;
388
+ $this->_limit = $limit;
389
+ if ( $max>0 )
390
+ $this->_maxmatches = $max;
391
+ if ( $cutoff>0 )
392
+ $this->_cutoff = $cutoff;
393
+ }
394
+
395
+ /// set maximum query time, in milliseconds, per-index
396
+ /// integer, 0 means "do not limit"
397
+ function SetMaxQueryTime ( $max )
398
+ {
399
+ assert ( is_int($max) );
400
+ assert ( $max>=0 );
401
+ $this->_maxquerytime = $max;
402
+ }
403
+
404
+ /// set matching mode
405
+ function SetMatchMode ( $mode )
406
+ {
407
+ assert ( $mode==SPH_MATCH_ALL
408
+ || $mode==SPH_MATCH_ANY
409
+ || $mode==SPH_MATCH_PHRASE
410
+ || $mode==SPH_MATCH_BOOLEAN
411
+ || $mode==SPH_MATCH_EXTENDED
412
+ || $mode==SPH_MATCH_FULLSCAN
413
+ || $mode==SPH_MATCH_EXTENDED2 );
414
+ $this->_mode = $mode;
415
+ }
416
+
417
+ /// set ranking mode
418
+ function SetRankingMode ( $ranker )
419
+ {
420
+ assert ( $ranker==SPH_RANK_PROXIMITY_BM25
421
+ || $ranker==SPH_RANK_BM25
422
+ || $ranker==SPH_RANK_NONE
423
+ || $ranker==SPH_RANK_WORDCOUNT );
424
+ $this->_ranker = $ranker;
425
+ }
426
+
427
+ /// set matches sorting mode
428
+ function SetSortMode ( $mode, $sortby="" )
429
+ {
430
+ assert (
431
+ $mode==SPH_SORT_RELEVANCE ||
432
+ $mode==SPH_SORT_ATTR_DESC ||
433
+ $mode==SPH_SORT_ATTR_ASC ||
434
+ $mode==SPH_SORT_TIME_SEGMENTS ||
435
+ $mode==SPH_SORT_EXTENDED ||
436
+ $mode==SPH_SORT_EXPR );
437
+ assert ( is_string($sortby) );
438
+ assert ( $mode==SPH_SORT_RELEVANCE || strlen($sortby)>0 );
439
+
440
+ $this->_sort = $mode;
441
+ $this->_sortby = $sortby;
442
+ }
443
+
444
+ /// bind per-field weights by order
445
+ /// DEPRECATED; use SetFieldWeights() instead
446
+ function SetWeights ( $weights )
447
+ {
448
+ assert ( is_array($weights) );
449
+ foreach ( $weights as $weight )
450
+ assert ( is_int($weight) );
451
+
452
+ $this->_weights = $weights;
453
+ }
454
+
455
+ /// bind per-field weights by name
456
+ function SetFieldWeights ( $weights )
457
+ {
458
+ assert ( is_array($weights) );
459
+ foreach ( $weights as $name=>$weight )
460
+ {
461
+ assert ( is_string($name) );
462
+ assert ( is_int($weight) );
463
+ }
464
+ $this->_fieldweights = $weights;
465
+ }
466
+
467
+ /// bind per-index weights by name
468
+ function SetIndexWeights ( $weights )
469
+ {
470
+ assert ( is_array($weights) );
471
+ foreach ( $weights as $index=>$weight )
472
+ {
473
+ assert ( is_string($index) );
474
+ assert ( is_int($weight) );
475
+ }
476
+ $this->_indexweights = $weights;
477
+ }
478
+
479
+ /// set IDs range to match
480
+ /// only match records if document ID is beetwen $min and $max (inclusive)
481
+ function SetIDRange ( $min, $max )
482
+ {
483
+ assert ( is_numeric($min) );
484
+ assert ( is_numeric($max) );
485
+ assert ( $min<=$max );
486
+ $this->_min_id = $min;
487
+ $this->_max_id = $max;
488
+ }
489
+
490
+ /// set values set filter
491
+ /// only match records where $attribute value is in given set
492
+ function SetFilter ( $attribute, $values, $exclude=false )
493
+ {
494
+ assert ( is_string($attribute) );
495
+ assert ( is_array($values) );
496
+ assert ( count($values) );
497
+
498
+ if ( is_array($values) && count($values) )
499
+ {
500
+ foreach ( $values as $value )
501
+ assert ( is_numeric($value) );
502
+
503
+ $this->_filters[] = array ( "type"=>SPH_FILTER_VALUES, "attr"=>$attribute, "exclude"=>$exclude, "values"=>$values );
504
+ }
505
+ }
506
+
507
+ /// set range filter
508
+ /// only match records if $attribute value is beetwen $min and $max (inclusive)
509
+ function SetFilterRange ( $attribute, $min, $max, $exclude=false )
510
+ {
511
+ assert ( is_string($attribute) );
512
+ assert ( is_int($min) );
513
+ assert ( is_int($max) );
514
+ assert ( $min<=$max );
515
+
516
+ $this->_filters[] = array ( "type"=>SPH_FILTER_RANGE, "attr"=>$attribute, "exclude"=>$exclude, "min"=>$min, "max"=>$max );
517
+ }
518
+
519
+ /// set float range filter
520
+ /// only match records if $attribute value is beetwen $min and $max (inclusive)
521
+ function SetFilterFloatRange ( $attribute, $min, $max, $exclude=false )
522
+ {
523
+ assert ( is_string($attribute) );
524
+ assert ( is_float($min) );
525
+ assert ( is_float($max) );
526
+ assert ( $min<=$max );
527
+
528
+ $this->_filters[] = array ( "type"=>SPH_FILTER_FLOATRANGE, "attr"=>$attribute, "exclude"=>$exclude, "min"=>$min, "max"=>$max );
529
+ }
530
+
531
+ /// setup anchor point for geosphere distance calculations
532
+ /// required to use @geodist in filters and sorting
533
+ /// latitude and longitude must be in radians
534
+ function SetGeoAnchor ( $attrlat, $attrlong, $lat, $long )
535
+ {
536
+ assert ( is_string($attrlat) );
537
+ assert ( is_string($attrlong) );
538
+ assert ( is_float($lat) );
539
+ assert ( is_float($long) );
540
+
541
+ $this->_anchor = array ( "attrlat"=>$attrlat, "attrlong"=>$attrlong, "lat"=>$lat, "long"=>$long );
542
+ }
543
+
544
+ /// set grouping attribute and function
545
+ function SetGroupBy ( $attribute, $func, $groupsort="@group desc" )
546
+ {
547
+ assert ( is_string($attribute) );
548
+ assert ( is_string($groupsort) );
549
+ assert ( $func==SPH_GROUPBY_DAY
550
+ || $func==SPH_GROUPBY_WEEK
551
+ || $func==SPH_GROUPBY_MONTH
552
+ || $func==SPH_GROUPBY_YEAR
553
+ || $func==SPH_GROUPBY_ATTR
554
+ || $func==SPH_GROUPBY_ATTRPAIR );
555
+
556
+ $this->_groupby = $attribute;
557
+ $this->_groupfunc = $func;
558
+ $this->_groupsort = $groupsort;
559
+ }
560
+
561
+ /// set count-distinct attribute for group-by queries
562
+ function SetGroupDistinct ( $attribute )
563
+ {
564
+ assert ( is_string($attribute) );
565
+ $this->_groupdistinct = $attribute;
566
+ }
567
+
568
+ /// set distributed retries count and delay
569
+ function SetRetries ( $count, $delay=0 )
570
+ {
571
+ assert ( is_int($count) && $count>=0 );
572
+ assert ( is_int($delay) && $delay>=0 );
573
+ $this->_retrycount = $count;
574
+ $this->_retrydelay = $delay;
575
+ }
576
+
577
+ /// set result set format (hash or array; hash by default)
578
+ /// PHP specific; needed for group-by-MVA result sets that may contain duplicate IDs
579
+ function SetArrayResult ( $arrayresult )
580
+ {
581
+ assert ( is_bool($arrayresult) );
582
+ $this->_arrayresult = $arrayresult;
583
+ }
584
+
585
+ //////////////////////////////////////////////////////////////////////////////
586
+
587
+ /// clear all filters (for multi-queries)
588
+ function ResetFilters ()
589
+ {
590
+ $this->_filters = array();
591
+ $this->_anchor = array();
592
+ }
593
+
594
+ /// clear groupby settings (for multi-queries)
595
+ function ResetGroupBy ()
596
+ {
597
+ $this->_groupby = "";
598
+ $this->_groupfunc = SPH_GROUPBY_DAY;
599
+ $this->_groupsort = "@group desc";
600
+ $this->_groupdistinct= "";
601
+ }
602
+
603
+ //////////////////////////////////////////////////////////////////////////////
604
+
605
+ /// connect to searchd server, run given search query through given indexes,
606
+ /// and return the search results
607
+ function Query ( $query, $index="*", $comment="" )
608
+ {
609
+ assert ( empty($this->_reqs) );
610
+
611
+ $this->AddQuery ( $query, $index, $comment );
612
+ $results = $this->RunQueries ();
613
+
614
+ if ( !is_array($results) )
615
+ return false; // probably network error; error message should be already filled
616
+
617
+ $this->_error = $results[0]["error"];
618
+ $this->_warning = $results[0]["warning"];
619
+ if ( $results[0]["status"]==SEARCHD_ERROR )
620
+ return false;
621
+ else
622
+ return $results[0];
623
+ }
624
+
625
+ /// helper to pack floats in network byte order
626
+ function _PackFloat ( $f )
627
+ {
628
+ $t1 = pack ( "f", $f ); // machine order
629
+ list(,$t2) = unpack ( "L*", $t1 ); // int in machine order
630
+ return pack ( "N", $t2 );
631
+ }
632
+
633
+ /// add query to multi-query batch
634
+ /// returns index into results array from RunQueries() call
635
+ function AddQuery ( $query, $index="*", $comment="" )
636
+ {
637
+ // mbstring workaround
638
+ $this->_MBPush ();
639
+
640
+ // build request
641
+ $req = pack ( "NNNNN", $this->_offset, $this->_limit, $this->_mode, $this->_ranker, $this->_sort ); // mode and limits
642
+ $req .= pack ( "N", strlen($this->_sortby) ) . $this->_sortby;
643
+ $req .= pack ( "N", strlen($query) ) . $query; // query itself
644
+ $req .= pack ( "N", count($this->_weights) ); // weights
645
+ foreach ( $this->_weights as $weight )
646
+ $req .= pack ( "N", (int)$weight );
647
+ $req .= pack ( "N", strlen($index) ) . $index; // indexes
648
+ $req .= pack ( "N", 1 ); // id64 range marker
649
+ $req .= sphPack64 ( $this->_min_id ) . sphPack64 ( $this->_max_id ); // id64 range
650
+
651
+ // filters
652
+ $req .= pack ( "N", count($this->_filters) );
653
+ foreach ( $this->_filters as $filter )
654
+ {
655
+ $req .= pack ( "N", strlen($filter["attr"]) ) . $filter["attr"];
656
+ $req .= pack ( "N", $filter["type"] );
657
+ switch ( $filter["type"] )
658
+ {
659
+ case SPH_FILTER_VALUES:
660
+ $req .= pack ( "N", count($filter["values"]) );
661
+ foreach ( $filter["values"] as $value )
662
+ $req .= pack ( "N", floatval($value) ); // this uberhack is to workaround 32bit signed int limit on x32 platforms
663
+ break;
664
+
665
+ case SPH_FILTER_RANGE:
666
+ $req .= pack ( "NN", $filter["min"], $filter["max"] );
667
+ break;
668
+
669
+ case SPH_FILTER_FLOATRANGE:
670
+ $req .= $this->_PackFloat ( $filter["min"] ) . $this->_PackFloat ( $filter["max"] );
671
+ break;
672
+
673
+ default:
674
+ assert ( 0 && "internal error: unhandled filter type" );
675
+ }
676
+ $req .= pack ( "N", $filter["exclude"] );
677
+ }
678
+
679
+ // group-by clause, max-matches count, group-sort clause, cutoff count
680
+ $req .= pack ( "NN", $this->_groupfunc, strlen($this->_groupby) ) . $this->_groupby;
681
+ $req .= pack ( "N", $this->_maxmatches );
682
+ $req .= pack ( "N", strlen($this->_groupsort) ) . $this->_groupsort;
683
+ $req .= pack ( "NNN", $this->_cutoff, $this->_retrycount, $this->_retrydelay );
684
+ $req .= pack ( "N", strlen($this->_groupdistinct) ) . $this->_groupdistinct;
685
+
686
+ // anchor point
687
+ if ( empty($this->_anchor) )
688
+ {
689
+ $req .= pack ( "N", 0 );
690
+ } else
691
+ {
692
+ $a =& $this->_anchor;
693
+ $req .= pack ( "N", 1 );
694
+ $req .= pack ( "N", strlen($a["attrlat"]) ) . $a["attrlat"];
695
+ $req .= pack ( "N", strlen($a["attrlong"]) ) . $a["attrlong"];
696
+ $req .= $this->_PackFloat ( $a["lat"] ) . $this->_PackFloat ( $a["long"] );
697
+ }
698
+
699
+ // per-index weights
700
+ $req .= pack ( "N", count($this->_indexweights) );
701
+ foreach ( $this->_indexweights as $idx=>$weight )
702
+ $req .= pack ( "N", strlen($idx) ) . $idx . pack ( "N", $weight );
703
+
704
+ // max query time
705
+ $req .= pack ( "N", $this->_maxquerytime );
706
+
707
+ // per-field weights
708
+ $req .= pack ( "N", count($this->_fieldweights) );
709
+ foreach ( $this->_fieldweights as $field=>$weight )
710
+ $req .= pack ( "N", strlen($field) ) . $field . pack ( "N", $weight );
711
+
712
+ // comment
713
+ $req .= pack ( "N", strlen($comment) ) . $comment;
714
+
715
+ // mbstring workaround
716
+ $this->_MBPop ();
717
+
718
+ // store request to requests array
719
+ $this->_reqs[] = $req;
720
+ return count($this->_reqs)-1;
721
+ }
722
+
723
+ /// connect to searchd, run queries batch, and return an array of result sets
724
+ function RunQueries ()
725
+ {
726
+ if ( empty($this->_reqs) )
727
+ {
728
+ $this->_error = "no queries defined, issue AddQuery() first";
729
+ return false;
730
+ }
731
+
732
+ // mbstring workaround
733
+ $this->_MBPush ();
734
+
735
+ if (!( $fp = $this->_Connect() ))
736
+ {
737
+ $this->_MBPop ();
738
+ return false;
739
+ }
740
+
741
+ ////////////////////////////
742
+ // send query, get response
743
+ ////////////////////////////
744
+
745
+ $nreqs = count($this->_reqs);
746
+ $req = join ( "", $this->_reqs );
747
+ $len = 4+strlen($req);
748
+ $req = pack ( "nnNN", SEARCHD_COMMAND_SEARCH, VER_COMMAND_SEARCH, $len, $nreqs ) . $req; // add header
749
+
750
+ fwrite ( $fp, $req, $len+8 );
751
+ if (!( $response = $this->_GetResponse ( $fp, VER_COMMAND_SEARCH ) ))
752
+ {
753
+ $this->_MBPop ();
754
+ return false;
755
+ }
756
+
757
+ $this->_reqs = array ();
758
+
759
+ //////////////////
760
+ // parse response
761
+ //////////////////
762
+
763
+ $p = 0; // current position
764
+ $max = strlen($response); // max position for checks, to protect against broken responses
765
+
766
+ $results = array ();
767
+ for ( $ires=0; $ires<$nreqs && $p<$max; $ires++ )
768
+ {
769
+ $results[] = array();
770
+ $result =& $results[$ires];
771
+
772
+ $result["error"] = "";
773
+ $result["warning"] = "";
774
+
775
+ // extract status
776
+ list(,$status) = unpack ( "N*", substr ( $response, $p, 4 ) ); $p += 4;
777
+ $result["status"] = $status;
778
+ if ( $status!=SEARCHD_OK )
779
+ {
780
+ list(,$len) = unpack ( "N*", substr ( $response, $p, 4 ) ); $p += 4;
781
+ $message = substr ( $response, $p, $len ); $p += $len;
782
+
783
+ if ( $status==SEARCHD_WARNING )
784
+ {
785
+ $result["warning"] = $message;
786
+ } else
787
+ {
788
+ $result["error"] = $message;
789
+ continue;
790
+ }
791
+ }
792
+
793
+ // read schema
794
+ $fields = array ();
795
+ $attrs = array ();
796
+
797
+ list(,$nfields) = unpack ( "N*", substr ( $response, $p, 4 ) ); $p += 4;
798
+ while ( $nfields-->0 && $p<$max )
799
+ {
800
+ list(,$len) = unpack ( "N*", substr ( $response, $p, 4 ) ); $p += 4;
801
+ $fields[] = substr ( $response, $p, $len ); $p += $len;
802
+ }
803
+ $result["fields"] = $fields;
804
+
805
+ list(,$nattrs) = unpack ( "N*", substr ( $response, $p, 4 ) ); $p += 4;
806
+ while ( $nattrs-->0 && $p<$max )
807
+ {
808
+ list(,$len) = unpack ( "N*", substr ( $response, $p, 4 ) ); $p += 4;
809
+ $attr = substr ( $response, $p, $len ); $p += $len;
810
+ list(,$type) = unpack ( "N*", substr ( $response, $p, 4 ) ); $p += 4;
811
+ $attrs[$attr] = $type;
812
+ }
813
+ $result["attrs"] = $attrs;
814
+
815
+ // read match count
816
+ list(,$count) = unpack ( "N*", substr ( $response, $p, 4 ) ); $p += 4;
817
+ list(,$id64) = unpack ( "N*", substr ( $response, $p, 4 ) ); $p += 4;
818
+
819
+ // read matches
820
+ $idx = -1;
821
+ while ( $count-->0 && $p<$max )
822
+ {
823
+ // index into result array
824
+ $idx++;
825
+
826
+ // parse document id and weight
827
+ if ( $id64 )
828
+ {
829
+ $doc = sphUnpack64 ( substr ( $response, $p, 8 ) ); $p += 8;
830
+ list(,$weight) = unpack ( "N*", substr ( $response, $p, 4 ) ); $p += 4;
831
+ } else
832
+ {
833
+ list ( $doc, $weight ) = array_values ( unpack ( "N*N*",
834
+ substr ( $response, $p, 8 ) ) );
835
+ $p += 8;
836
+
837
+ if ( PHP_INT_SIZE>=8 )
838
+ {
839
+ // x64 route, workaround broken unpack() in 5.2.2+
840
+ if ( $doc<0 ) $doc += (1<<32);
841
+ } else
842
+ {
843
+ // x32 route, workaround php signed/unsigned braindamage
844
+ $doc = sprintf ( "%u", $doc );
845
+ }
846
+ }
847
+ $weight = sprintf ( "%u", $weight );
848
+
849
+ // create match entry
850
+ if ( $this->_arrayresult )
851
+ $result["matches"][$idx] = array ( "id"=>$doc, "weight"=>$weight );
852
+ else
853
+ $result["matches"][$doc]["weight"] = $weight;
854
+
855
+ // parse and create attributes
856
+ $attrvals = array ();
857
+ foreach ( $attrs as $attr=>$type )
858
+ {
859
+ // handle floats
860
+ if ( $type==SPH_ATTR_FLOAT )
861
+ {
862
+ list(,$uval) = unpack ( "N*", substr ( $response, $p, 4 ) ); $p += 4;
863
+ list(,$fval) = unpack ( "f*", pack ( "L", $uval ) );
864
+ $attrvals[$attr] = $fval;
865
+ continue;
866
+ }
867
+
868
+ // handle everything else as unsigned ints
869
+ list(,$val) = unpack ( "N*", substr ( $response, $p, 4 ) ); $p += 4;
870
+ if ( $type & SPH_ATTR_MULTI )
871
+ {
872
+ $attrvals[$attr] = array ();
873
+ $nvalues = $val;
874
+ while ( $nvalues-->0 && $p<$max )
875
+ {
876
+ list(,$val) = unpack ( "N*", substr ( $response, $p, 4 ) ); $p += 4;
877
+ $attrvals[$attr][] = sprintf ( "%u", $val );
878
+ }
879
+ } else
880
+ {
881
+ $attrvals[$attr] = sprintf ( "%u", $val );
882
+ }
883
+ }
884
+
885
+ if ( $this->_arrayresult )
886
+ $result["matches"][$idx]["attrs"] = $attrvals;
887
+ else
888
+ $result["matches"][$doc]["attrs"] = $attrvals;
889
+ }
890
+
891
+ list ( $total, $total_found, $msecs, $words ) =
892
+ array_values ( unpack ( "N*N*N*N*", substr ( $response, $p, 16 ) ) );
893
+ $result["total"] = sprintf ( "%u", $total );
894
+ $result["total_found"] = sprintf ( "%u", $total_found );
895
+ $result["time"] = sprintf ( "%.3f", $msecs/1000 );
896
+ $p += 16;
897
+
898
+ while ( $words-->0 && $p<$max )
899
+ {
900
+ list(,$len) = unpack ( "N*", substr ( $response, $p, 4 ) ); $p += 4;
901
+ $word = substr ( $response, $p, $len ); $p += $len;
902
+ list ( $docs, $hits ) = array_values ( unpack ( "N*N*", substr ( $response, $p, 8 ) ) ); $p += 8;
903
+ $result["words"][$word] = array (
904
+ "docs"=>sprintf ( "%u", $docs ),
905
+ "hits"=>sprintf ( "%u", $hits ) );
906
+ }
907
+ }
908
+
909
+ $this->_MBPop ();
910
+ return $results;
911
+ }
912
+
913
+ /////////////////////////////////////////////////////////////////////////////
914
+ // excerpts generation
915
+ /////////////////////////////////////////////////////////////////////////////
916
+
917
+ /// connect to searchd server, and generate exceprts (snippets)
918
+ /// of given documents for given query. returns false on failure,
919
+ /// an array of snippets on success
920
+ function BuildExcerpts ( $docs, $index, $words, $opts=array() )
921
+ {
922
+ assert ( is_array($docs) );
923
+ assert ( is_string($index) );
924
+ assert ( is_string($words) );
925
+ assert ( is_array($opts) );
926
+
927
+ $this->_MBPush ();
928
+
929
+ if (!( $fp = $this->_Connect() ))
930
+ {
931
+ $this->_MBPop();
932
+ return false;
933
+ }
934
+
935
+ /////////////////
936
+ // fixup options
937
+ /////////////////
938
+
939
+ if ( !isset($opts["before_match"]) ) $opts["before_match"] = "<b>";
940
+ if ( !isset($opts["after_match"]) ) $opts["after_match"] = "</b>";
941
+ if ( !isset($opts["chunk_separator"]) ) $opts["chunk_separator"] = " ... ";
942
+ if ( !isset($opts["limit"]) ) $opts["limit"] = 256;
943
+ if ( !isset($opts["around"]) ) $opts["around"] = 5;
944
+ if ( !isset($opts["exact_phrase"]) ) $opts["exact_phrase"] = false;
945
+ if ( !isset($opts["single_passage"]) ) $opts["single_passage"] = false;
946
+ if ( !isset($opts["use_boundaries"]) ) $opts["use_boundaries"] = false;
947
+ if ( !isset($opts["weight_order"]) ) $opts["weight_order"] = false;
948
+
949
+ /////////////////
950
+ // build request
951
+ /////////////////
952
+
953
+ // v.1.0 req
954
+ $flags = 1; // remove spaces
955
+ if ( $opts["exact_phrase"] ) $flags |= 2;
956
+ if ( $opts["single_passage"] ) $flags |= 4;
957
+ if ( $opts["use_boundaries"] ) $flags |= 8;
958
+ if ( $opts["weight_order"] ) $flags |= 16;
959
+ $req = pack ( "NN", 0, $flags ); // mode=0, flags=$flags
960
+ $req .= pack ( "N", strlen($index) ) . $index; // req index
961
+ $req .= pack ( "N", strlen($words) ) . $words; // req words
962
+
963
+ // options
964
+ $req .= pack ( "N", strlen($opts["before_match"]) ) . $opts["before_match"];
965
+ $req .= pack ( "N", strlen($opts["after_match"]) ) . $opts["after_match"];
966
+ $req .= pack ( "N", strlen($opts["chunk_separator"]) ) . $opts["chunk_separator"];
967
+ $req .= pack ( "N", (int)$opts["limit"] );
968
+ $req .= pack ( "N", (int)$opts["around"] );
969
+
970
+ // documents
971
+ $req .= pack ( "N", count($docs) );
972
+ foreach ( $docs as $doc )
973
+ {
974
+ assert ( is_string($doc) );
975
+ $req .= pack ( "N", strlen($doc) ) . $doc;
976
+ }
977
+
978
+ ////////////////////////////
979
+ // send query, get response
980
+ ////////////////////////////
981
+
982
+ $len = strlen($req);
983
+ $req = pack ( "nnN", SEARCHD_COMMAND_EXCERPT, VER_COMMAND_EXCERPT, $len ) . $req; // add header
984
+ $wrote = fwrite ( $fp, $req, $len+8 );
985
+ if (!( $response = $this->_GetResponse ( $fp, VER_COMMAND_EXCERPT ) ))
986
+ {
987
+ $this->_MBPop ();
988
+ return false;
989
+ }
990
+
991
+ //////////////////
992
+ // parse response
993
+ //////////////////
994
+
995
+ $pos = 0;
996
+ $res = array ();
997
+ $rlen = strlen($response);
998
+ for ( $i=0; $i<count($docs); $i++ )
999
+ {
1000
+ list(,$len) = unpack ( "N*", substr ( $response, $pos, 4 ) );
1001
+ $pos += 4;
1002
+
1003
+ if ( $pos+$len > $rlen )
1004
+ {
1005
+ $this->_error = "incomplete reply";
1006
+ $this->_MBPop ();
1007
+ return false;
1008
+ }
1009
+ $res[] = $len ? substr ( $response, $pos, $len ) : "";
1010
+ $pos += $len;
1011
+ }
1012
+
1013
+ $this->_MBPop ();
1014
+ return $res;
1015
+ }
1016
+
1017
+
1018
+ /////////////////////////////////////////////////////////////////////////////
1019
+ // keyword generation
1020
+ /////////////////////////////////////////////////////////////////////////////
1021
+
1022
+ /// connect to searchd server, and generate keyword list for a given query
1023
+ /// returns false on failure,
1024
+ /// an array of words on success
1025
+ function BuildKeywords ( $query, $index, $hits )
1026
+ {
1027
+ assert ( is_string($query) );
1028
+ assert ( is_string($index) );
1029
+ assert ( is_bool($hits) );
1030
+
1031
+ $this->_MBPush ();
1032
+
1033
+ if (!( $fp = $this->_Connect() ))
1034
+ {
1035
+ $this->_MBPop();
1036
+ return false;
1037
+ }
1038
+
1039
+ /////////////////
1040
+ // build request
1041
+ /////////////////
1042
+
1043
+ // v.1.0 req
1044
+ $req = pack ( "N", strlen($query) ) . $query; // req query
1045
+ $req .= pack ( "N", strlen($index) ) . $index; // req index
1046
+ $req .= pack ( "N", (int)$hits );
1047
+
1048
+ ////////////////////////////
1049
+ // send query, get response
1050
+ ////////////////////////////
1051
+
1052
+ $len = strlen($req);
1053
+ $req = pack ( "nnN", SEARCHD_COMMAND_KEYWORDS, VER_COMMAND_KEYWORDS, $len ) . $req; // add header
1054
+ $wrote = fwrite ( $fp, $req, $len+8 );
1055
+ if (!( $response = $this->_GetResponse ( $fp, VER_COMMAND_KEYWORDS ) ))
1056
+ {
1057
+ $this->_MBPop ();
1058
+ return false;
1059
+ }
1060
+
1061
+ //////////////////
1062
+ // parse response
1063
+ //////////////////
1064
+
1065
+ $pos = 0;
1066
+ $res = array ();
1067
+ $rlen = strlen($response);
1068
+ list(,$nwords) = unpack ( "N*", substr ( $response, $pos, 4 ) );
1069
+ $pos += 4;
1070
+ for ( $i=0; $i<$nwords; $i++ )
1071
+ {
1072
+ list(,$len) = unpack ( "N*", substr ( $response, $pos, 4 ) ); $pos += 4;
1073
+ $tokenized = $len ? substr ( $response, $pos, $len ) : "";
1074
+ $pos += $len;
1075
+
1076
+ list(,$len) = unpack ( "N*", substr ( $response, $pos, 4 ) ); $pos += 4;
1077
+ $normalized = $len ? substr ( $response, $pos, $len ) : "";
1078
+ $pos += $len;
1079
+
1080
+ $res[] = array ( "tokenized"=>$tokenized, "normalized"=>$normalized );
1081
+
1082
+ if ( $hits )
1083
+ {
1084
+ list($ndocs,$nhits) = array_values ( unpack ( "N*N*", substr ( $response, $pos, 8 ) ) );
1085
+ $pos += 8;
1086
+ $res [$i]["docs"] = $ndocs;
1087
+ $res [$i]["hits"] = $nhits;
1088
+ }
1089
+
1090
+ if ( $pos > $rlen )
1091
+ {
1092
+ $this->_error = "incomplete reply";
1093
+ $this->_MBPop ();
1094
+ return false;
1095
+ }
1096
+ }
1097
+
1098
+ $this->_MBPop ();
1099
+ return $res;
1100
+ }
1101
+
1102
+ function EscapeString ( $string )
1103
+ {
1104
+ $from = array ( '(',')','|','-','!','@','~','\"','&' );
1105
+ $to = array ( '\\(','\\)','\\|','\\-','\\!','\\@','\\~','\\\"', '\\&' );
1106
+
1107
+ return str_replace ( $from, $to, $string );
1108
+ }
1109
+
1110
+ /////////////////////////////////////////////////////////////////////////////
1111
+ // attribute updates
1112
+ /////////////////////////////////////////////////////////////////////////////
1113
+
1114
+ /// update given attribute values on given documents in given indexes
1115
+ /// returns amount of updated documents (0 or more) on success, or -1 on failure
1116
+ function UpdateAttributes ( $index, $attrs, $values )
1117
+ {
1118
+ // verify everything
1119
+ assert ( is_string($index) );
1120
+
1121
+ assert ( is_array($attrs) );
1122
+ foreach ( $attrs as $attr )
1123
+ assert ( is_string($attr) );
1124
+
1125
+ assert ( is_array($values) );
1126
+ foreach ( $values as $id=>$entry )
1127
+ {
1128
+ assert ( is_numeric($id) );
1129
+ assert ( is_array($entry) );
1130
+ assert ( count($entry)==count($attrs) );
1131
+ foreach ( $entry as $v )
1132
+ assert ( is_int($v) );
1133
+ }
1134
+
1135
+ // build request
1136
+ $req = pack ( "N", strlen($index) ) . $index;
1137
+
1138
+ $req .= pack ( "N", count($attrs) );
1139
+ foreach ( $attrs as $attr )
1140
+ $req .= pack ( "N", strlen($attr) ) . $attr;
1141
+
1142
+ $req .= pack ( "N", count($values) );
1143
+ foreach ( $values as $id=>$entry )
1144
+ {
1145
+ $req .= sphPack64 ( $id );
1146
+ foreach ( $entry as $v )
1147
+ $req .= pack ( "N", $v );
1148
+ }
1149
+
1150
+ // mbstring workaround
1151
+ $this->_MBPush ();
1152
+
1153
+ // connect, send query, get response
1154
+ if (!( $fp = $this->_Connect() ))
1155
+ {
1156
+ $this->_MBPop ();
1157
+ return -1;
1158
+ }
1159
+
1160
+ $len = strlen($req);
1161
+ $req = pack ( "nnN", SEARCHD_COMMAND_UPDATE, VER_COMMAND_UPDATE, $len ) . $req; // add header
1162
+ fwrite ( $fp, $req, $len+8 );
1163
+
1164
+ if (!( $response = $this->_GetResponse ( $fp, VER_COMMAND_UPDATE ) ))
1165
+ {
1166
+ $this->_MBPop ();
1167
+ return -1;
1168
+ }
1169
+
1170
+ // parse response
1171
+ list(,$updated) = unpack ( "N*", substr ( $response, 0, 4 ) );
1172
+ $this->_MBPop ();
1173
+ return $updated;
1174
+ }
1175
+ }
1176
+
1177
+ //
1178
+ // $Id$
1179
+ //
1180
+
1181
+ ?>