riddle 1.5.0 → 1.5.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. data/HISTORY +4 -0
  2. data/README.textile +28 -38
  3. data/lib/riddle/2.1.0.rb +16 -0
  4. data/lib/riddle/auto_version.rb +2 -0
  5. data/lib/riddle/client.rb +53 -49
  6. data/lib/riddle/version.rb +1 -1
  7. data/spec/fixtures/data/2.1.0/anchor.bin +0 -0
  8. data/spec/fixtures/data/2.1.0/any.bin +0 -0
  9. data/spec/fixtures/data/2.1.0/boolean.bin +0 -0
  10. data/spec/fixtures/data/2.1.0/comment.bin +0 -0
  11. data/spec/fixtures/data/2.1.0/distinct.bin +0 -0
  12. data/spec/fixtures/data/2.1.0/field_weights.bin +0 -0
  13. data/spec/fixtures/data/2.1.0/filter.bin +0 -0
  14. data/spec/fixtures/data/2.1.0/filter_array.bin +0 -0
  15. data/spec/fixtures/data/2.1.0/filter_array_exclude.bin +0 -0
  16. data/spec/fixtures/data/2.1.0/filter_boolean.bin +0 -0
  17. data/spec/fixtures/data/2.1.0/filter_floats.bin +0 -0
  18. data/spec/fixtures/data/2.1.0/filter_floats_exclude.bin +0 -0
  19. data/spec/fixtures/data/2.1.0/filter_range.bin +0 -0
  20. data/spec/fixtures/data/2.1.0/filter_range_exclude.bin +0 -0
  21. data/spec/fixtures/data/2.1.0/group.bin +0 -0
  22. data/spec/fixtures/data/2.1.0/index.bin +0 -0
  23. data/spec/fixtures/data/2.1.0/index_weights.bin +0 -0
  24. data/spec/fixtures/data/2.1.0/keywords_with_hits.bin +0 -0
  25. data/spec/fixtures/data/2.1.0/keywords_without_hits.bin +0 -0
  26. data/spec/fixtures/data/2.1.0/overrides.bin +0 -0
  27. data/spec/fixtures/data/2.1.0/phrase.bin +0 -0
  28. data/spec/fixtures/data/2.1.0/rank_mode.bin +0 -0
  29. data/spec/fixtures/data/2.1.0/select.bin +0 -0
  30. data/spec/fixtures/data/2.1.0/simple.bin +0 -0
  31. data/spec/fixtures/data/2.1.0/sort.bin +0 -0
  32. data/spec/fixtures/data/2.1.0/update_simple.bin +0 -0
  33. data/spec/fixtures/data/2.1.0/weights.bin +0 -0
  34. data/spec/fixtures/data_generator.2.1.0.php +5 -0
  35. data/spec/fixtures/sphinxapi.2.1.0.php +1752 -0
  36. data/spec/functional/excerpt_spec.rb +12 -12
  37. data/spec/riddle/auto_version_spec.rb +7 -0
  38. data/spec/support/binary_fixtures.rb +3 -3
  39. metadata +70 -10
data/HISTORY CHANGED
@@ -1,3 +1,7 @@
1
+ 1.5.1 - January 2nd 2011
2
+ - If no known servers work, raise an appropriate error.
3
+ - Sphinx 2.1.0-dev support.
4
+
1
5
  1.5.0 - November 4th 2011
2
6
  - Handle exclusive filters in SphinxQL SELECT commands.
3
7
  - Allow for native Ruby objects in SphinxQL UPDATE commands.
@@ -1,65 +1,55 @@
1
1
  h1. Riddle
2
2
 
3
- This client has been written to interface with "Sphinx":http://sphinxsearch.com/. It is written by
4
- "Pat Allan":http://freelancing-gods.com, and has been influenced by both Dmytro Shteflyuk's Ruby
5
- client and the original PHP client - credit where credit's due, after all.
3
+ This client has been written to interface with "Sphinx":http://sphinxsearch.com/. It is written by "Pat Allan":http://freelancing-gods.com, and has been influenced by both Dmytro Shteflyuk's Ruby client and the original PHP client - credit where credit's due, after all.
6
4
 
7
- It does not follow the same syntax as those two, though (not much point writing this otherwise) -
8
- opting for a more Ruby-like structure.
5
+ It does not follow the same syntax as those two, though (not much point writing this otherwise) - opting for a more Ruby-like structure.
9
6
 
10
7
  h2. Installation
11
8
 
12
- sudo gem install riddle --source http://gemcutter.org
9
+ <pre><code>sudo gem install riddle</code></pre>
13
10
 
14
11
  h3. Usage
15
12
 
16
13
  As of version 1.0.0, Riddle now supports multiple versions of Sphinx in the one gem - you'll need to require your specific version after a normal require, though.
17
14
 
18
- require 'riddle'
19
- require 'riddle/0.9.9'
15
+ <pre><code>require 'riddle'
16
+ require 'riddle/0.9.9'</code></pre>
20
17
 
21
18
  To get started, just instantiate a Client object:
22
19
 
23
- client = Riddle::Client.new # defaults to localhost and port 3312
24
- client = Riddle::Client.new "sphinxserver.domain.tld", 3333 # custom settings
20
+ <pre><code>client = Riddle::Client.new # defaults to localhost and port 3312
21
+ client = Riddle::Client.new "sphinxserver.domain.tld", 3333 # custom settings</code></pre>
25
22
 
26
23
  And then set the parameters to what you want, before running a query:
27
24
 
28
- client.match_mode = :extended
29
- client.query "Pat Allan @state Victoria"
25
+ <pre><code>client.match_mode = :extended
26
+ client.query "Pat Allan @state Victoria"</code></pre>
30
27
 
31
28
  The results from a query are similar to the other clients - but here's the details. It's a hash with
32
29
  the following keys:
33
30
 
34
- * :matches
35
- * :fields
36
- * :attributes
37
- * :attribute_names
38
- * :words
39
- * :total
40
- * :total_found
41
- * :time
42
- * :status
43
- * :warning (if appropriate)
44
- * :error (if appropriate)
45
-
46
- The key @:matches@ returns an array of hashes - the actual search results. Each hash has the
47
- document id (@:doc@), the result weighting (@:weight@), and a hash of the attributes for
48
- the document (@:attributes@).
49
-
50
- The @:fields@ and @:attribute_names@ keys return list of fields and attributes for the
51
- documents. The key @:attributes@ will return a hash of attribute name and type pairs, and
52
- @:words@ returns a hash of hashes representing the words from the search, with the number of
53
- documents and hits for each, along the lines of:
54
-
55
- results[:words]["Pat"] #=> {:docs => 12, :hits => 15}
31
+ * @:matches@
32
+ * @:fields@
33
+ * @:attributes@
34
+ * @:attribute_names@
35
+ * @:words@
36
+ * @:total@
37
+ * @:total_found@
38
+ * @:time@
39
+ * @:status@
40
+ * @:warning@ (if appropriate)
41
+ * @:error@ (if appropriate)
42
+
43
+ The key @:matches@ returns an array of hashes - the actual search results. Each hash has the document id (@:doc@), the result weighting (@:weight@), and a hash of the attributes for the document (@:attributes@).
44
+
45
+ The @:fields@ and @:attribute_names@ keys return list of fields and attributes for the documents. The key @:attributes@ will return a hash of attribute name and type pairs, and @:words@ returns a hash of hashes representing the words from the search, with the number of documents and hits for each, along the lines of:
46
+
47
+ <pre><code>results[:words]["Pat"] #=> {:docs => 12, :hits => 15}</code></pre>
56
48
 
57
49
  @:total@, @:total_found@ and @:time@ return the number of matches available, the
58
- total number of matches (which may be greater than the maximum available), and the time in milliseconds
59
- that the query took to run.
50
+ total number of matches (which may be greater than the maximum available), and the time in milliseconds that the query took to run.
60
51
 
61
- @:status@ is the error code for the query - and if there was a related warning, it will be under
62
- the @:warning@ key. Fatal errors will be described under @:error@.
52
+ @:status@ is the error code for the query - and if there was a related warning, it will be under the @:warning@ key. Fatal errors will be described under @:error@.
63
53
 
64
54
  If you've installed the gem and wondering why there's no tests - check out the git version. I've kept the specs out of the gem as I have a decent amount of test data in there, which really isn't needed unless you want to submit patches.
65
55
 
@@ -0,0 +1,16 @@
1
+ require 'riddle/0.9.9'
2
+ require 'riddle/1.10'
3
+ require 'riddle/2.0.1'
4
+
5
+ Riddle.loaded_version = '2.1.0'
6
+
7
+ Riddle::Client::Versions[:search] = 0x119
8
+ Riddle::Client::Versions[:excerpt] = 0x104
9
+
10
+ Riddle::Client::RankModes[:expr] = 8
11
+ Riddle::Client::RankModes[:total] = 9
12
+
13
+ Riddle::Client::AttributeTypes[:multi] = 0x40000001
14
+ Riddle::Client::AttributeTypes[:multi_64] = 0x40000002
15
+
16
+ Riddle::Client::AttributeHandlers[Riddle::Client::AttributeTypes[:multi_64]] = :next_64bit_int_array
@@ -10,6 +10,8 @@ class Riddle::AutoVersion
10
10
  require 'riddle/1.10'
11
11
  when /2.0.\d/
12
12
  require 'riddle/2.0.1'
13
+ when /2.1.\d/
14
+ require 'riddle/2.1.0'
13
15
  end
14
16
  end
15
17
  end
@@ -119,8 +119,8 @@ module Riddle
119
119
  :match_mode, :sort_mode, :sort_by, :weights, :id_range, :filters,
120
120
  :group_by, :group_function, :group_clause, :group_distinct, :cut_off,
121
121
  :retry_count, :retry_delay, :anchor, :index_weights, :rank_mode,
122
- :max_query_time, :field_weights, :timeout, :overrides, :select,
123
- :connection, :key
122
+ :rank_expr, :max_query_time, :field_weights, :timeout, :overrides,
123
+ :select, :connection, :key
124
124
  attr_reader :queue
125
125
 
126
126
  @@connection = nil
@@ -174,6 +174,7 @@ module Riddle
174
174
  # string keys are index names, integer values are weightings
175
175
  @index_weights = {}
176
176
  @rank_mode = :proximity_bm25
177
+ @rank_expr = ''
177
178
  @max_query_time = 0
178
179
  # string keys are field names, integer values are weightings
179
180
  @field_weights = {}
@@ -383,24 +384,25 @@ module Riddle
383
384
  # 3. Pass the documents' text to +excerpts+ for marking up of matched terms.
384
385
  #
385
386
  def excerpts(options = {})
386
- options[:index] ||= '*'
387
- options[:before_match] ||= '<span class="match">'
388
- options[:after_match] ||= '</span>'
389
- options[:chunk_separator] ||= ' &#8230; ' # ellipsis
390
- options[:limit] ||= 256
391
- options[:limit_passages] ||= 0
392
- options[:limit_words] ||= 0
393
- options[:around] ||= 5
394
- options[:exact_phrase] ||= false
395
- options[:single_passage] ||= false
396
- options[:query_mode] ||= false
397
- options[:force_all_words] ||= false
398
- options[:start_passage_id] ||= 1
399
- options[:load_files] ||= false
400
- options[:html_strip_mode] ||= 'index'
401
- options[:allow_empty] ||= false
402
- options[:passage_boundary] ||= 'none'
403
- options[:emit_zones] ||= false
387
+ options[:index] ||= '*'
388
+ options[:before_match] ||= '<span class="match">'
389
+ options[:after_match] ||= '</span>'
390
+ options[:chunk_separator] ||= ' &#8230; ' # ellipsis
391
+ options[:limit] ||= 256
392
+ options[:limit_passages] ||= 0
393
+ options[:limit_words] ||= 0
394
+ options[:around] ||= 5
395
+ options[:exact_phrase] ||= false
396
+ options[:single_passage] ||= false
397
+ options[:query_mode] ||= false
398
+ options[:force_all_words] ||= false
399
+ options[:start_passage_id] ||= 1
400
+ options[:load_files] ||= false
401
+ options[:html_strip_mode] ||= 'index'
402
+ options[:allow_empty] ||= false
403
+ options[:passage_boundary] ||= 'none'
404
+ options[:emit_zones] ||= false
405
+ options[:load_files_scattered] ||= false
404
406
 
405
407
  response = Response.new request(:excerpt, excerpts_message(options))
406
408
 
@@ -565,10 +567,12 @@ module Riddle
565
567
  self.connection.call(self)
566
568
  elsif self.class.connection
567
569
  self.class.connection.call(self)
568
- elsif server.index('/') == 0
570
+ elsif server && server.index('/') == 0
569
571
  UNIXSocket.new server
570
- else
572
+ elsif server
571
573
  TCPSocket.new server, @port
574
+ else
575
+ raise "Server not set."
572
576
  end
573
577
  rescue Errno::ETIMEDOUT, Errno::ECONNRESET, Errno::ECONNREFUSED => e
574
578
  retry if (tries += 1) < 5
@@ -674,9 +678,15 @@ module Riddle
674
678
  def query_message(search, index, comments = '')
675
679
  message = Message.new
676
680
 
677
- # Mode, Limits, Sort Mode
678
- message.append_ints @offset, @limit, MatchModes[@match_mode],
679
- RankModes[@rank_mode], SortModes[@sort_mode]
681
+ # Mode, Limits
682
+ message.append_ints @offset, @limit, MatchModes[@match_mode]
683
+
684
+ # Ranking
685
+ message.append_int RankModes[@rank_mode]
686
+ message.append_string @rank_expr if @rank_mode == :expr
687
+
688
+ # Sort Mode
689
+ message.append_int SortModes[@sort_mode]
680
690
  message.append_string @sort_by
681
691
 
682
692
  # Query
@@ -809,21 +819,14 @@ module Riddle
809
819
  end
810
820
 
811
821
  AttributeHandlers = {
812
- AttributeTypes[:integer] => :next_int,
813
- AttributeTypes[:timestamp] => :next_int,
814
- AttributeTypes[:ordinal] => :next_int,
815
- AttributeTypes[:bool] => :next_int,
816
- AttributeTypes[:float] => :next_float,
817
- AttributeTypes[:bigint] => :next_64bit_int,
818
- AttributeTypes[:string] => :next,
819
-
820
- AttributeTypes[:multi] + AttributeTypes[:integer] => :next_int_array,
821
- AttributeTypes[:multi] + AttributeTypes[:timestamp] => :next_int_array,
822
- AttributeTypes[:multi] + AttributeTypes[:ordinal] => :next_int_array,
823
- AttributeTypes[:multi] + AttributeTypes[:bool] => :next_int_array,
824
- AttributeTypes[:multi] + AttributeTypes[:float] => :next_float_array,
825
- AttributeTypes[:multi] + AttributeTypes[:bigint] => :next_64bit_int_array,
826
- AttributeTypes[:multi] + AttributeTypes[:string] => :next_array
822
+ AttributeTypes[:integer] => :next_int,
823
+ AttributeTypes[:timestamp] => :next_int,
824
+ AttributeTypes[:ordinal] => :next_int,
825
+ AttributeTypes[:bool] => :next_int,
826
+ AttributeTypes[:float] => :next_float,
827
+ AttributeTypes[:bigint] => :next_64bit_int,
828
+ AttributeTypes[:string] => :next,
829
+ AttributeTypes[:multi] + AttributeTypes[:integer] => :next_int_array
827
830
  }
828
831
 
829
832
  def attribute_from_type(type, response)
@@ -833,15 +836,16 @@ module Riddle
833
836
 
834
837
  def excerpt_flags(options)
835
838
  flags = 1
836
- flags |= 2 if options[:exact_phrase]
837
- flags |= 4 if options[:single_passage]
838
- flags |= 8 if options[:use_boundaries]
839
- flags |= 16 if options[:weight_order]
840
- flags |= 32 if options[:query_mode]
841
- flags |= 64 if options[:force_all_words]
842
- flags |= 128 if options[:load_files]
843
- flags |= 256 if options[:allow_empty]
844
- flags |= 512 if options[:emit_zones]
839
+ flags |= 2 if options[:exact_phrase]
840
+ flags |= 4 if options[:single_passage]
841
+ flags |= 8 if options[:use_boundaries]
842
+ flags |= 16 if options[:weight_order]
843
+ flags |= 32 if options[:query_mode]
844
+ flags |= 64 if options[:force_all_words]
845
+ flags |= 128 if options[:load_files]
846
+ flags |= 256 if options[:allow_empty]
847
+ flags |= 512 if options[:emit_zones]
848
+ flags |= 1024 if options[:load_files_scattered]
845
849
  flags
846
850
  end
847
851
  end
@@ -1,3 +1,3 @@
1
1
  module Riddle
2
- Version = '1.5.0'
2
+ Version = '1.5.1'
3
3
  end
@@ -0,0 +1,5 @@
1
+ <?php
2
+ $version = '2.1.0';
3
+ include "sphinxapi.$version.php";
4
+ include "data_generator.php";
5
+ ?>
@@ -0,0 +1,1752 @@
1
+ <?php
2
+
3
+ //
4
+ // $Id: sphinxapi.php 2970 2011-09-23 16:50:22Z klirichek $
5
+ //
6
+
7
+ //
8
+ // Copyright (c) 2001-2011, Andrew Aksyonoff
9
+ // Copyright (c) 2008-2011, Sphinx Technologies Inc
10
+ // All rights reserved
11
+ //
12
+ // This program is free software; you can redistribute it and/or modify
13
+ // it under the terms of the GNU General Public License. You should have
14
+ // received a copy of the GPL license along with this program; if you
15
+ // did not, you can find it at http://www.gnu.org/
16
+ //
17
+
18
+ /////////////////////////////////////////////////////////////////////////////
19
+ // PHP version of Sphinx searchd client (PHP API)
20
+ /////////////////////////////////////////////////////////////////////////////
21
+
22
+ /// known searchd commands
23
+ define ( "SEARCHD_COMMAND_SEARCH", 0 );
24
+ define ( "SEARCHD_COMMAND_EXCERPT", 1 );
25
+ define ( "SEARCHD_COMMAND_UPDATE", 2 );
26
+ define ( "SEARCHD_COMMAND_KEYWORDS", 3 );
27
+ define ( "SEARCHD_COMMAND_PERSIST", 4 );
28
+ define ( "SEARCHD_COMMAND_STATUS", 5 );
29
+ define ( "SEARCHD_COMMAND_FLUSHATTRS", 7 );
30
+
31
+ /// current client-side command implementation versions
32
+ define ( "VER_COMMAND_SEARCH", 0x119 );
33
+ define ( "VER_COMMAND_EXCERPT", 0x104 );
34
+ define ( "VER_COMMAND_UPDATE", 0x102 );
35
+ define ( "VER_COMMAND_KEYWORDS", 0x100 );
36
+ define ( "VER_COMMAND_STATUS", 0x100 );
37
+ define ( "VER_COMMAND_QUERY", 0x100 );
38
+ define ( "VER_COMMAND_FLUSHATTRS", 0x100 );
39
+
40
+ /// known searchd status codes
41
+ define ( "SEARCHD_OK", 0 );
42
+ define ( "SEARCHD_ERROR", 1 );
43
+ define ( "SEARCHD_RETRY", 2 );
44
+ define ( "SEARCHD_WARNING", 3 );
45
+
46
+ /// known match modes
47
+ define ( "SPH_MATCH_ALL", 0 );
48
+ define ( "SPH_MATCH_ANY", 1 );
49
+ define ( "SPH_MATCH_PHRASE", 2 );
50
+ define ( "SPH_MATCH_BOOLEAN", 3 );
51
+ define ( "SPH_MATCH_EXTENDED", 4 );
52
+ define ( "SPH_MATCH_FULLSCAN", 5 );
53
+ define ( "SPH_MATCH_EXTENDED2", 6 ); // extended engine V2 (TEMPORARY, WILL BE REMOVED)
54
+
55
+ /// known ranking modes (ext2 only)
56
+ define ( "SPH_RANK_PROXIMITY_BM25", 0 ); ///< default mode, phrase proximity major factor and BM25 minor one
57
+ define ( "SPH_RANK_BM25", 1 ); ///< statistical mode, BM25 ranking only (faster but worse quality)
58
+ define ( "SPH_RANK_NONE", 2 ); ///< no ranking, all matches get a weight of 1
59
+ define ( "SPH_RANK_WORDCOUNT", 3 ); ///< simple word-count weighting, rank is a weighted sum of per-field keyword occurence counts
60
+ define ( "SPH_RANK_PROXIMITY", 4 );
61
+ define ( "SPH_RANK_MATCHANY", 5 );
62
+ define ( "SPH_RANK_FIELDMASK", 6 );
63
+ define ( "SPH_RANK_SPH04", 7 );
64
+ define ( "SPH_RANK_EXPR", 8 );
65
+ define ( "SPH_RANK_TOTAL", 9 );
66
+
67
+ /// known sort modes
68
+ define ( "SPH_SORT_RELEVANCE", 0 );
69
+ define ( "SPH_SORT_ATTR_DESC", 1 );
70
+ define ( "SPH_SORT_ATTR_ASC", 2 );
71
+ define ( "SPH_SORT_TIME_SEGMENTS", 3 );
72
+ define ( "SPH_SORT_EXTENDED", 4 );
73
+ define ( "SPH_SORT_EXPR", 5 );
74
+
75
+ /// known filter types
76
+ define ( "SPH_FILTER_VALUES", 0 );
77
+ define ( "SPH_FILTER_RANGE", 1 );
78
+ define ( "SPH_FILTER_FLOATRANGE", 2 );
79
+
80
+ /// known attribute types
81
+ define ( "SPH_ATTR_INTEGER", 1 );
82
+ define ( "SPH_ATTR_TIMESTAMP", 2 );
83
+ define ( "SPH_ATTR_ORDINAL", 3 );
84
+ define ( "SPH_ATTR_BOOL", 4 );
85
+ define ( "SPH_ATTR_FLOAT", 5 );
86
+ define ( "SPH_ATTR_BIGINT", 6 );
87
+ define ( "SPH_ATTR_STRING", 7 );
88
+ define ( "SPH_ATTR_MULTI", 0x40000001 );
89
+ define ( "SPH_ATTR_MULTI64", 0x40000002 );
90
+
91
+ /// known grouping functions
92
+ define ( "SPH_GROUPBY_DAY", 0 );
93
+ define ( "SPH_GROUPBY_WEEK", 1 );
94
+ define ( "SPH_GROUPBY_MONTH", 2 );
95
+ define ( "SPH_GROUPBY_YEAR", 3 );
96
+ define ( "SPH_GROUPBY_ATTR", 4 );
97
+ define ( "SPH_GROUPBY_ATTRPAIR", 5 );
98
+
99
+ // important properties of PHP's integers:
100
+ // - always signed (one bit short of PHP_INT_SIZE)
101
+ // - conversion from string to int is saturated
102
+ // - float is double
103
+ // - div converts arguments to floats
104
+ // - mod converts arguments to ints
105
+
106
+ // the packing code below works as follows:
107
+ // - when we got an int, just pack it
108
+ // if performance is a problem, this is the branch users should aim for
109
+ //
110
+ // - otherwise, we got a number in string form
111
+ // this might be due to different reasons, but we assume that this is
112
+ // because it didn't fit into PHP int
113
+ //
114
+ // - factor the string into high and low ints for packing
115
+ // - if we have bcmath, then it is used
116
+ // - if we don't, we have to do it manually (this is the fun part)
117
+ //
118
+ // - x64 branch does factoring using ints
119
+ // - x32 (ab)uses floats, since we can't fit unsigned 32-bit number into an int
120
+ //
121
+ // unpacking routines are pretty much the same.
122
+ // - return ints if we can
123
+ // - otherwise format number into a string
124
+
125
+ /// pack 64-bit signed
126
+ function sphPackI64 ( $v )
127
+ {
128
+ assert ( is_numeric($v) );
129
+
130
+ // x64
131
+ if ( PHP_INT_SIZE>=8 )
132
+ {
133
+ $v = (int)$v;
134
+ return pack ( "NN", $v>>32, $v&0xFFFFFFFF );
135
+ }
136
+
137
+ // x32, int
138
+ if ( is_int($v) )
139
+ return pack ( "NN", $v < 0 ? -1 : 0, $v );
140
+
141
+ // x32, bcmath
142
+ if ( function_exists("bcmul") )
143
+ {
144
+ if ( bccomp ( $v, 0 ) == -1 )
145
+ $v = bcadd ( "18446744073709551616", $v );
146
+ $h = bcdiv ( $v, "4294967296", 0 );
147
+ $l = bcmod ( $v, "4294967296" );
148
+ return pack ( "NN", (float)$h, (float)$l ); // conversion to float is intentional; int would lose 31st bit
149
+ }
150
+
151
+ // x32, no-bcmath
152
+ $p = max(0, strlen($v) - 13);
153
+ $lo = abs((float)substr($v, $p));
154
+ $hi = abs((float)substr($v, 0, $p));
155
+
156
+ $m = $lo + $hi*1316134912.0; // (10 ^ 13) % (1 << 32) = 1316134912
157
+ $q = floor($m/4294967296.0);
158
+ $l = $m - ($q*4294967296.0);
159
+ $h = $hi*2328.0 + $q; // (10 ^ 13) / (1 << 32) = 2328
160
+
161
+ if ( $v<0 )
162
+ {
163
+ if ( $l==0 )
164
+ $h = 4294967296.0 - $h;
165
+ else
166
+ {
167
+ $h = 4294967295.0 - $h;
168
+ $l = 4294967296.0 - $l;
169
+ }
170
+ }
171
+ return pack ( "NN", $h, $l );
172
+ }
173
+
174
+ /// pack 64-bit unsigned
175
+ function sphPackU64 ( $v )
176
+ {
177
+ assert ( is_numeric($v) );
178
+
179
+ // x64
180
+ if ( PHP_INT_SIZE>=8 )
181
+ {
182
+ assert ( $v>=0 );
183
+
184
+ // x64, int
185
+ if ( is_int($v) )
186
+ return pack ( "NN", $v>>32, $v&0xFFFFFFFF );
187
+
188
+ // x64, bcmath
189
+ if ( function_exists("bcmul") )
190
+ {
191
+ $h = bcdiv ( $v, 4294967296, 0 );
192
+ $l = bcmod ( $v, 4294967296 );
193
+ return pack ( "NN", $h, $l );
194
+ }
195
+
196
+ // x64, no-bcmath
197
+ $p = max ( 0, strlen($v) - 13 );
198
+ $lo = (int)substr ( $v, $p );
199
+ $hi = (int)substr ( $v, 0, $p );
200
+
201
+ $m = $lo + $hi*1316134912;
202
+ $l = $m % 4294967296;
203
+ $h = $hi*2328 + (int)($m/4294967296);
204
+
205
+ return pack ( "NN", $h, $l );
206
+ }
207
+
208
+ // x32, int
209
+ if ( is_int($v) )
210
+ return pack ( "NN", 0, $v );
211
+
212
+ // x32, bcmath
213
+ if ( function_exists("bcmul") )
214
+ {
215
+ $h = bcdiv ( $v, "4294967296", 0 );
216
+ $l = bcmod ( $v, "4294967296" );
217
+ return pack ( "NN", (float)$h, (float)$l ); // conversion to float is intentional; int would lose 31st bit
218
+ }
219
+
220
+ // x32, no-bcmath
221
+ $p = max(0, strlen($v) - 13);
222
+ $lo = (float)substr($v, $p);
223
+ $hi = (float)substr($v, 0, $p);
224
+
225
+ $m = $lo + $hi*1316134912.0;
226
+ $q = floor($m / 4294967296.0);
227
+ $l = $m - ($q * 4294967296.0);
228
+ $h = $hi*2328.0 + $q;
229
+
230
+ return pack ( "NN", $h, $l );
231
+ }
232
+
233
+ // unpack 64-bit unsigned
234
+ function sphUnpackU64 ( $v )
235
+ {
236
+ list ( $hi, $lo ) = array_values ( unpack ( "N*N*", $v ) );
237
+
238
+ if ( PHP_INT_SIZE>=8 )
239
+ {
240
+ if ( $hi<0 ) $hi += (1<<32); // because php 5.2.2 to 5.2.5 is totally fucked up again
241
+ if ( $lo<0 ) $lo += (1<<32);
242
+
243
+ // x64, int
244
+ if ( $hi<=2147483647 )
245
+ return ($hi<<32) + $lo;
246
+
247
+ // x64, bcmath
248
+ if ( function_exists("bcmul") )
249
+ return bcadd ( $lo, bcmul ( $hi, "4294967296" ) );
250
+
251
+ // x64, no-bcmath
252
+ $C = 100000;
253
+ $h = ((int)($hi / $C) << 32) + (int)($lo / $C);
254
+ $l = (($hi % $C) << 32) + ($lo % $C);
255
+ if ( $l>$C )
256
+ {
257
+ $h += (int)($l / $C);
258
+ $l = $l % $C;
259
+ }
260
+
261
+ if ( $h==0 )
262
+ return $l;
263
+ return sprintf ( "%d%05d", $h, $l );
264
+ }
265
+
266
+ // x32, int
267
+ if ( $hi==0 )
268
+ {
269
+ if ( $lo>0 )
270
+ return $lo;
271
+ return sprintf ( "%u", $lo );
272
+ }
273
+
274
+ $hi = sprintf ( "%u", $hi );
275
+ $lo = sprintf ( "%u", $lo );
276
+
277
+ // x32, bcmath
278
+ if ( function_exists("bcmul") )
279
+ return bcadd ( $lo, bcmul ( $hi, "4294967296" ) );
280
+
281
+ // x32, no-bcmath
282
+ $hi = (float)$hi;
283
+ $lo = (float)$lo;
284
+
285
+ $q = floor($hi/10000000.0);
286
+ $r = $hi - $q*10000000.0;
287
+ $m = $lo + $r*4967296.0;
288
+ $mq = floor($m/10000000.0);
289
+ $l = $m - $mq*10000000.0;
290
+ $h = $q*4294967296.0 + $r*429.0 + $mq;
291
+
292
+ $h = sprintf ( "%.0f", $h );
293
+ $l = sprintf ( "%07.0f", $l );
294
+ if ( $h=="0" )
295
+ return sprintf( "%.0f", (float)$l );
296
+ return $h . $l;
297
+ }
298
+
299
+ // unpack 64-bit signed
300
+ function sphUnpackI64 ( $v )
301
+ {
302
+ list ( $hi, $lo ) = array_values ( unpack ( "N*N*", $v ) );
303
+
304
+ // x64
305
+ if ( PHP_INT_SIZE>=8 )
306
+ {
307
+ if ( $hi<0 ) $hi += (1<<32); // because php 5.2.2 to 5.2.5 is totally fucked up again
308
+ if ( $lo<0 ) $lo += (1<<32);
309
+
310
+ return ($hi<<32) + $lo;
311
+ }
312
+
313
+ // x32, int
314
+ if ( $hi==0 )
315
+ {
316
+ if ( $lo>0 )
317
+ return $lo;
318
+ return sprintf ( "%u", $lo );
319
+ }
320
+ // x32, int
321
+ elseif ( $hi==-1 )
322
+ {
323
+ if ( $lo<0 )
324
+ return $lo;
325
+ return sprintf ( "%.0f", $lo - 4294967296.0 );
326
+ }
327
+
328
+ $neg = "";
329
+ $c = 0;
330
+ if ( $hi<0 )
331
+ {
332
+ $hi = ~$hi;
333
+ $lo = ~$lo;
334
+ $c = 1;
335
+ $neg = "-";
336
+ }
337
+
338
+ $hi = sprintf ( "%u", $hi );
339
+ $lo = sprintf ( "%u", $lo );
340
+
341
+ // x32, bcmath
342
+ if ( function_exists("bcmul") )
343
+ return $neg . bcadd ( bcadd ( $lo, bcmul ( $hi, "4294967296" ) ), $c );
344
+
345
+ // x32, no-bcmath
346
+ $hi = (float)$hi;
347
+ $lo = (float)$lo;
348
+
349
+ $q = floor($hi/10000000.0);
350
+ $r = $hi - $q*10000000.0;
351
+ $m = $lo + $r*4967296.0;
352
+ $mq = floor($m/10000000.0);
353
+ $l = $m - $mq*10000000.0 + $c;
354
+ $h = $q*4294967296.0 + $r*429.0 + $mq;
355
+ if ( $l==10000000 )
356
+ {
357
+ $l = 0;
358
+ $h += 1;
359
+ }
360
+
361
+ $h = sprintf ( "%.0f", $h );
362
+ $l = sprintf ( "%07.0f", $l );
363
+ if ( $h=="0" )
364
+ return $neg . sprintf( "%.0f", (float)$l );
365
+ return $neg . $h . $l;
366
+ }
367
+
368
+
369
+ function sphFixUint ( $value )
370
+ {
371
+ if ( PHP_INT_SIZE>=8 )
372
+ {
373
+ // x64 route, workaround broken unpack() in 5.2.2+
374
+ if ( $value<0 ) $value += (1<<32);
375
+ return $value;
376
+ }
377
+ else
378
+ {
379
+ // x32 route, workaround php signed/unsigned braindamage
380
+ return sprintf ( "%u", $value );
381
+ }
382
+ }
383
+
384
+
385
+ /// sphinx searchd client class
386
+ class SphinxClient
387
+ {
388
+ var $_host; ///< searchd host (default is "localhost")
389
+ var $_port; ///< searchd port (default is 9312)
390
+ var $_offset; ///< how many records to seek from result-set start (default is 0)
391
+ var $_limit; ///< how many records to return from result-set starting at offset (default is 20)
392
+ var $_mode; ///< query matching mode (default is SPH_MATCH_ALL)
393
+ var $_weights; ///< per-field weights (default is 1 for all fields)
394
+ var $_sort; ///< match sorting mode (default is SPH_SORT_RELEVANCE)
395
+ var $_sortby; ///< attribute to sort by (defualt is "")
396
+ var $_min_id; ///< min ID to match (default is 0, which means no limit)
397
+ var $_max_id; ///< max ID to match (default is 0, which means no limit)
398
+ var $_filters; ///< search filters
399
+ var $_groupby; ///< group-by attribute name
400
+ var $_groupfunc; ///< group-by function (to pre-process group-by attribute value with)
401
+ var $_groupsort; ///< group-by sorting clause (to sort groups in result set with)
402
+ var $_groupdistinct;///< group-by count-distinct attribute
403
+ var $_maxmatches; ///< max matches to retrieve
404
+ var $_cutoff; ///< cutoff to stop searching at (default is 0)
405
+ var $_retrycount; ///< distributed retries count
406
+ var $_retrydelay; ///< distributed retries delay
407
+ var $_anchor; ///< geographical anchor point
408
+ var $_indexweights; ///< per-index weights
409
+ var $_ranker; ///< ranking mode (default is SPH_RANK_PROXIMITY_BM25)
410
+ var $_rankexpr; ///< ranking mode expression (for SPH_RANK_EXPR)
411
+ var $_maxquerytime; ///< max query time, milliseconds (default is 0, do not limit)
412
+ var $_fieldweights; ///< per-field-name weights
413
+ var $_overrides; ///< per-query attribute values overrides
414
+ var $_select; ///< select-list (attributes or expressions, with optional aliases)
415
+
416
+ var $_error; ///< last error message
417
+ var $_warning; ///< last warning message
418
+ var $_connerror; ///< connection error vs remote error flag
419
+
420
+ var $_reqs; ///< requests array for multi-query
421
+ var $_mbenc; ///< stored mbstring encoding
422
+ var $_arrayresult; ///< whether $result["matches"] should be a hash or an array
423
+ var $_timeout; ///< connect timeout
424
+
425
+ /////////////////////////////////////////////////////////////////////////////
426
+ // common stuff
427
+ /////////////////////////////////////////////////////////////////////////////
428
+
429
+ /// create a new client object and fill defaults
430
+ function SphinxClient ()
431
+ {
432
+ // per-client-object settings
433
+ $this->_host = "localhost";
434
+ $this->_port = 9312;
435
+ $this->_path = false;
436
+ $this->_socket = false;
437
+
438
+ // per-query settings
439
+ $this->_offset = 0;
440
+ $this->_limit = 20;
441
+ $this->_mode = SPH_MATCH_ALL;
442
+ $this->_weights = array ();
443
+ $this->_sort = SPH_SORT_RELEVANCE;
444
+ $this->_sortby = "";
445
+ $this->_min_id = 0;
446
+ $this->_max_id = 0;
447
+ $this->_filters = array ();
448
+ $this->_groupby = "";
449
+ $this->_groupfunc = SPH_GROUPBY_DAY;
450
+ $this->_groupsort = "@group desc";
451
+ $this->_groupdistinct= "";
452
+ $this->_maxmatches = 1000;
453
+ $this->_cutoff = 0;
454
+ $this->_retrycount = 0;
455
+ $this->_retrydelay = 0;
456
+ $this->_anchor = array ();
457
+ $this->_indexweights= array ();
458
+ $this->_ranker = SPH_RANK_PROXIMITY_BM25;
459
+ $this->_rankexpr = "";
460
+ $this->_maxquerytime= 0;
461
+ $this->_fieldweights= array();
462
+ $this->_overrides = array();
463
+ $this->_select = "*";
464
+
465
+ $this->_error = ""; // per-reply fields (for single-query case)
466
+ $this->_warning = "";
467
+ $this->_connerror = false;
468
+
469
+ $this->_reqs = array (); // requests storage (for multi-query case)
470
+ $this->_mbenc = "";
471
+ $this->_arrayresult = false;
472
+ $this->_timeout = 0;
473
+ }
474
+
475
+ function __destruct()
476
+ {
477
+ if ( $this->_socket !== false )
478
+ fclose ( $this->_socket );
479
+ }
480
+
481
+ /// get last error message (string)
482
+ function GetLastError ()
483
+ {
484
+ return $this->_error;
485
+ }
486
+
487
+ /// get last warning message (string)
488
+ function GetLastWarning ()
489
+ {
490
+ return $this->_warning;
491
+ }
492
+
493
+ /// get last error flag (to tell network connection errors from searchd errors or broken responses)
494
+ function IsConnectError()
495
+ {
496
+ return $this->_connerror;
497
+ }
498
+
499
+ /// set searchd host name (string) and port (integer)
500
+ function SetServer ( $host, $port = 0 )
501
+ {
502
+ assert ( is_string($host) );
503
+ if ( $host[0] == '/')
504
+ {
505
+ $this->_path = 'unix://' . $host;
506
+ return;
507
+ }
508
+ if ( substr ( $host, 0, 7 )=="unix://" )
509
+ {
510
+ $this->_path = $host;
511
+ return;
512
+ }
513
+
514
+ assert ( is_int($port) );
515
+ $this->_host = $host;
516
+ $this->_port = $port;
517
+ $this->_path = '';
518
+
519
+ }
520
+
521
+ /// set server connection timeout (0 to remove)
522
+ function SetConnectTimeout ( $timeout )
523
+ {
524
+ assert ( is_numeric($timeout) );
525
+ $this->_timeout = $timeout;
526
+ }
527
+
528
+
529
+ function _Send ( $handle, $data, $length )
530
+ {
531
+ if ( feof($handle) || fwrite ( $handle, $data, $length ) !== $length )
532
+ {
533
+ $this->_error = 'connection unexpectedly closed (timed out?)';
534
+ $this->_connerror = true;
535
+ return false;
536
+ }
537
+ return true;
538
+ }
539
+
540
+ /////////////////////////////////////////////////////////////////////////////
541
+
542
+ /// enter mbstring workaround mode
543
+ function _MBPush ()
544
+ {
545
+ $this->_mbenc = "";
546
+ if ( ini_get ( "mbstring.func_overload" ) & 2 )
547
+ {
548
+ $this->_mbenc = mb_internal_encoding();
549
+ mb_internal_encoding ( "latin1" );
550
+ }
551
+ }
552
+
553
+ /// leave mbstring workaround mode
554
+ function _MBPop ()
555
+ {
556
+ if ( $this->_mbenc )
557
+ mb_internal_encoding ( $this->_mbenc );
558
+ }
559
+
560
+ /// connect to searchd server
561
+ function _Connect ()
562
+ {
563
+ if ( $this->_socket!==false )
564
+ {
565
+ // we are in persistent connection mode, so we have a socket
566
+ // however, need to check whether it's still alive
567
+ if ( !@feof ( $this->_socket ) )
568
+ return $this->_socket;
569
+
570
+ // force reopen
571
+ $this->_socket = false;
572
+ }
573
+
574
+ $errno = 0;
575
+ $errstr = "";
576
+ $this->_connerror = false;
577
+
578
+ if ( $this->_path )
579
+ {
580
+ $host = $this->_path;
581
+ $port = 0;
582
+ }
583
+ else
584
+ {
585
+ $host = $this->_host;
586
+ $port = $this->_port;
587
+ }
588
+
589
+ if ( $this->_timeout<=0 )
590
+ $fp = @fsockopen ( $host, $port, $errno, $errstr );
591
+ else
592
+ $fp = @fsockopen ( $host, $port, $errno, $errstr, $this->_timeout );
593
+
594
+ if ( !$fp )
595
+ {
596
+ if ( $this->_path )
597
+ $location = $this->_path;
598
+ else
599
+ $location = "{$this->_host}:{$this->_port}";
600
+
601
+ $errstr = trim ( $errstr );
602
+ $this->_error = "connection to $location failed (errno=$errno, msg=$errstr)";
603
+ $this->_connerror = true;
604
+ return false;
605
+ }
606
+
607
+ // send my version
608
+ // this is a subtle part. we must do it before (!) reading back from searchd.
609
+ // because otherwise under some conditions (reported on FreeBSD for instance)
610
+ // TCP stack could throttle write-write-read pattern because of Nagle.
611
+ if ( !$this->_Send ( $fp, pack ( "N", 1 ), 4 ) )
612
+ {
613
+ fclose ( $fp );
614
+ $this->_error = "failed to send client protocol version";
615
+ return false;
616
+ }
617
+
618
+ // check version
619
+ list(,$v) = unpack ( "N*", fread ( $fp, 4 ) );
620
+ $v = (int)$v;
621
+ if ( $v<1 )
622
+ {
623
+ fclose ( $fp );
624
+ $this->_error = "expected searchd protocol version 1+, got version '$v'";
625
+ return false;
626
+ }
627
+
628
+ return $fp;
629
+ }
630
+
631
+ /// get and check response packet from searchd server
632
+ function _GetResponse ( $fp, $client_ver )
633
+ {
634
+ $response = "";
635
+ $len = 0;
636
+
637
+ $header = fread ( $fp, 8 );
638
+ if ( strlen($header)==8 )
639
+ {
640
+ list ( $status, $ver, $len ) = array_values ( unpack ( "n2a/Nb", $header ) );
641
+ $left = $len;
642
+ while ( $left>0 && !feof($fp) )
643
+ {
644
+ $chunk = fread ( $fp, min ( 8192, $left ) );
645
+ if ( $chunk )
646
+ {
647
+ $response .= $chunk;
648
+ $left -= strlen($chunk);
649
+ }
650
+ }
651
+ }
652
+ if ( $this->_socket === false )
653
+ fclose ( $fp );
654
+
655
+ // check response
656
+ $read = strlen ( $response );
657
+ if ( !$response || $read!=$len )
658
+ {
659
+ $this->_error = $len
660
+ ? "failed to read searchd response (status=$status, ver=$ver, len=$len, read=$read)"
661
+ : "received zero-sized searchd response";
662
+ return false;
663
+ }
664
+
665
+ // check status
666
+ if ( $status==SEARCHD_WARNING )
667
+ {
668
+ list(,$wlen) = unpack ( "N*", substr ( $response, 0, 4 ) );
669
+ $this->_warning = substr ( $response, 4, $wlen );
670
+ return substr ( $response, 4+$wlen );
671
+ }
672
+ if ( $status==SEARCHD_ERROR )
673
+ {
674
+ $this->_error = "searchd error: " . substr ( $response, 4 );
675
+ return false;
676
+ }
677
+ if ( $status==SEARCHD_RETRY )
678
+ {
679
+ $this->_error = "temporary searchd error: " . substr ( $response, 4 );
680
+ return false;
681
+ }
682
+ if ( $status!=SEARCHD_OK )
683
+ {
684
+ $this->_error = "unknown status code '$status'";
685
+ return false;
686
+ }
687
+
688
+ // check version
689
+ if ( $ver<$client_ver )
690
+ {
691
+ $this->_warning = sprintf ( "searchd command v.%d.%d older than client's v.%d.%d, some options might not work",
692
+ $ver>>8, $ver&0xff, $client_ver>>8, $client_ver&0xff );
693
+ }
694
+
695
+ return $response;
696
+ }
697
+
698
+ /////////////////////////////////////////////////////////////////////////////
699
+ // searching
700
+ /////////////////////////////////////////////////////////////////////////////
701
+
702
+ /// set offset and count into result set,
703
+ /// and optionally set max-matches and cutoff limits
704
+ function SetLimits ( $offset, $limit, $max=0, $cutoff=0 )
705
+ {
706
+ assert ( is_int($offset) );
707
+ assert ( is_int($limit) );
708
+ assert ( $offset>=0 );
709
+ assert ( $limit>0 );
710
+ assert ( $max>=0 );
711
+ $this->_offset = $offset;
712
+ $this->_limit = $limit;
713
+ if ( $max>0 )
714
+ $this->_maxmatches = $max;
715
+ if ( $cutoff>0 )
716
+ $this->_cutoff = $cutoff;
717
+ }
718
+
719
+ /// set maximum query time, in milliseconds, per-index
720
+ /// integer, 0 means "do not limit"
721
+ function SetMaxQueryTime ( $max )
722
+ {
723
+ assert ( is_int($max) );
724
+ assert ( $max>=0 );
725
+ $this->_maxquerytime = $max;
726
+ }
727
+
728
+ /// set matching mode
729
+ function SetMatchMode ( $mode )
730
+ {
731
+ assert ( $mode==SPH_MATCH_ALL
732
+ || $mode==SPH_MATCH_ANY
733
+ || $mode==SPH_MATCH_PHRASE
734
+ || $mode==SPH_MATCH_BOOLEAN
735
+ || $mode==SPH_MATCH_EXTENDED
736
+ || $mode==SPH_MATCH_FULLSCAN
737
+ || $mode==SPH_MATCH_EXTENDED2 );
738
+ $this->_mode = $mode;
739
+ }
740
+
741
+ /// set ranking mode
742
+ function SetRankingMode ( $ranker, $rankexpr="" )
743
+ {
744
+ assert ( $ranker>=0 && $ranker<SPH_RANK_TOTAL );
745
+ assert ( is_string($rankexpr) );
746
+ $this->_ranker = $ranker;
747
+ $this->_rankexpr = $rankexpr;
748
+ }
749
+
750
+ /// set matches sorting mode
751
+ function SetSortMode ( $mode, $sortby="" )
752
+ {
753
+ assert (
754
+ $mode==SPH_SORT_RELEVANCE ||
755
+ $mode==SPH_SORT_ATTR_DESC ||
756
+ $mode==SPH_SORT_ATTR_ASC ||
757
+ $mode==SPH_SORT_TIME_SEGMENTS ||
758
+ $mode==SPH_SORT_EXTENDED ||
759
+ $mode==SPH_SORT_EXPR );
760
+ assert ( is_string($sortby) );
761
+ assert ( $mode==SPH_SORT_RELEVANCE || strlen($sortby)>0 );
762
+
763
+ $this->_sort = $mode;
764
+ $this->_sortby = $sortby;
765
+ }
766
+
767
+ /// bind per-field weights by order
768
+ /// DEPRECATED; use SetFieldWeights() instead
769
+ function SetWeights ( $weights )
770
+ {
771
+ assert ( is_array($weights) );
772
+ foreach ( $weights as $weight )
773
+ assert ( is_int($weight) );
774
+
775
+ $this->_weights = $weights;
776
+ }
777
+
778
+ /// bind per-field weights by name
779
+ function SetFieldWeights ( $weights )
780
+ {
781
+ assert ( is_array($weights) );
782
+ foreach ( $weights as $name=>$weight )
783
+ {
784
+ assert ( is_string($name) );
785
+ assert ( is_int($weight) );
786
+ }
787
+ $this->_fieldweights = $weights;
788
+ }
789
+
790
+ /// bind per-index weights by name
791
+ function SetIndexWeights ( $weights )
792
+ {
793
+ assert ( is_array($weights) );
794
+ foreach ( $weights as $index=>$weight )
795
+ {
796
+ assert ( is_string($index) );
797
+ assert ( is_int($weight) );
798
+ }
799
+ $this->_indexweights = $weights;
800
+ }
801
+
802
+ /// set IDs range to match
803
+ /// only match records if document ID is beetwen $min and $max (inclusive)
804
+ function SetIDRange ( $min, $max )
805
+ {
806
+ assert ( is_numeric($min) );
807
+ assert ( is_numeric($max) );
808
+ assert ( $min<=$max );
809
+ $this->_min_id = $min;
810
+ $this->_max_id = $max;
811
+ }
812
+
813
+ /// set values set filter
814
+ /// only match records where $attribute value is in given set
815
+ function SetFilter ( $attribute, $values, $exclude=false )
816
+ {
817
+ assert ( is_string($attribute) );
818
+ assert ( is_array($values) );
819
+ assert ( count($values) );
820
+
821
+ if ( is_array($values) && count($values) )
822
+ {
823
+ foreach ( $values as $value )
824
+ assert ( is_numeric($value) );
825
+
826
+ $this->_filters[] = array ( "type"=>SPH_FILTER_VALUES, "attr"=>$attribute, "exclude"=>$exclude, "values"=>$values );
827
+ }
828
+ }
829
+
830
+ /// set range filter
831
+ /// only match records if $attribute value is beetwen $min and $max (inclusive)
832
+ function SetFilterRange ( $attribute, $min, $max, $exclude=false )
833
+ {
834
+ assert ( is_string($attribute) );
835
+ assert ( is_numeric($min) );
836
+ assert ( is_numeric($max) );
837
+ assert ( $min<=$max );
838
+
839
+ $this->_filters[] = array ( "type"=>SPH_FILTER_RANGE, "attr"=>$attribute, "exclude"=>$exclude, "min"=>$min, "max"=>$max );
840
+ }
841
+
842
+ /// set float range filter
843
+ /// only match records if $attribute value is beetwen $min and $max (inclusive)
844
+ function SetFilterFloatRange ( $attribute, $min, $max, $exclude=false )
845
+ {
846
+ assert ( is_string($attribute) );
847
+ assert ( is_float($min) );
848
+ assert ( is_float($max) );
849
+ assert ( $min<=$max );
850
+
851
+ $this->_filters[] = array ( "type"=>SPH_FILTER_FLOATRANGE, "attr"=>$attribute, "exclude"=>$exclude, "min"=>$min, "max"=>$max );
852
+ }
853
+
854
+ /// setup anchor point for geosphere distance calculations
855
+ /// required to use @geodist in filters and sorting
856
+ /// latitude and longitude must be in radians
857
+ function SetGeoAnchor ( $attrlat, $attrlong, $lat, $long )
858
+ {
859
+ assert ( is_string($attrlat) );
860
+ assert ( is_string($attrlong) );
861
+ assert ( is_float($lat) );
862
+ assert ( is_float($long) );
863
+
864
+ $this->_anchor = array ( "attrlat"=>$attrlat, "attrlong"=>$attrlong, "lat"=>$lat, "long"=>$long );
865
+ }
866
+
867
+ /// set grouping attribute and function
868
+ function SetGroupBy ( $attribute, $func, $groupsort="@group desc" )
869
+ {
870
+ assert ( is_string($attribute) );
871
+ assert ( is_string($groupsort) );
872
+ assert ( $func==SPH_GROUPBY_DAY
873
+ || $func==SPH_GROUPBY_WEEK
874
+ || $func==SPH_GROUPBY_MONTH
875
+ || $func==SPH_GROUPBY_YEAR
876
+ || $func==SPH_GROUPBY_ATTR
877
+ || $func==SPH_GROUPBY_ATTRPAIR );
878
+
879
+ $this->_groupby = $attribute;
880
+ $this->_groupfunc = $func;
881
+ $this->_groupsort = $groupsort;
882
+ }
883
+
884
+ /// set count-distinct attribute for group-by queries
885
+ function SetGroupDistinct ( $attribute )
886
+ {
887
+ assert ( is_string($attribute) );
888
+ $this->_groupdistinct = $attribute;
889
+ }
890
+
891
+ /// set distributed retries count and delay
892
+ function SetRetries ( $count, $delay=0 )
893
+ {
894
+ assert ( is_int($count) && $count>=0 );
895
+ assert ( is_int($delay) && $delay>=0 );
896
+ $this->_retrycount = $count;
897
+ $this->_retrydelay = $delay;
898
+ }
899
+
900
+ /// set result set format (hash or array; hash by default)
901
+ /// PHP specific; needed for group-by-MVA result sets that may contain duplicate IDs
902
+ function SetArrayResult ( $arrayresult )
903
+ {
904
+ assert ( is_bool($arrayresult) );
905
+ $this->_arrayresult = $arrayresult;
906
+ }
907
+
908
+ /// set attribute values override
909
+ /// there can be only one override per attribute
910
+ /// $values must be a hash that maps document IDs to attribute values
911
+ function SetOverride ( $attrname, $attrtype, $values )
912
+ {
913
+ assert ( is_string ( $attrname ) );
914
+ assert ( in_array ( $attrtype, array ( SPH_ATTR_INTEGER, SPH_ATTR_TIMESTAMP, SPH_ATTR_BOOL, SPH_ATTR_FLOAT, SPH_ATTR_BIGINT ) ) );
915
+ assert ( is_array ( $values ) );
916
+
917
+ $this->_overrides[$attrname] = array ( "attr"=>$attrname, "type"=>$attrtype, "values"=>$values );
918
+ }
919
+
920
+ /// set select-list (attributes or expressions), SQL-like syntax
921
+ function SetSelect ( $select )
922
+ {
923
+ assert ( is_string ( $select ) );
924
+ $this->_select = $select;
925
+ }
926
+
927
+ //////////////////////////////////////////////////////////////////////////////
928
+
929
+ /// clear all filters (for multi-queries)
930
+ function ResetFilters ()
931
+ {
932
+ $this->_filters = array();
933
+ $this->_anchor = array();
934
+ }
935
+
936
+ /// clear groupby settings (for multi-queries)
937
+ function ResetGroupBy ()
938
+ {
939
+ $this->_groupby = "";
940
+ $this->_groupfunc = SPH_GROUPBY_DAY;
941
+ $this->_groupsort = "@group desc";
942
+ $this->_groupdistinct= "";
943
+ }
944
+
945
+ /// clear all attribute value overrides (for multi-queries)
946
+ function ResetOverrides ()
947
+ {
948
+ $this->_overrides = array ();
949
+ }
950
+
951
+ //////////////////////////////////////////////////////////////////////////////
952
+
953
+ /// connect to searchd server, run given search query through given indexes,
954
+ /// and return the search results
955
+ function Query ( $query, $index="*", $comment="" )
956
+ {
957
+ assert ( empty($this->_reqs) );
958
+
959
+ $this->AddQuery ( $query, $index, $comment );
960
+ $results = $this->RunQueries ();
961
+ $this->_reqs = array (); // just in case it failed too early
962
+
963
+ if ( !is_array($results) )
964
+ return false; // probably network error; error message should be already filled
965
+
966
+ $this->_error = $results[0]["error"];
967
+ $this->_warning = $results[0]["warning"];
968
+ if ( $results[0]["status"]==SEARCHD_ERROR )
969
+ return false;
970
+ else
971
+ return $results[0];
972
+ }
973
+
974
+ /// helper to pack floats in network byte order
975
+ function _PackFloat ( $f )
976
+ {
977
+ $t1 = pack ( "f", $f ); // machine order
978
+ list(,$t2) = unpack ( "L*", $t1 ); // int in machine order
979
+ return pack ( "N", $t2 );
980
+ }
981
+
982
+ /// add query to multi-query batch
983
+ /// returns index into results array from RunQueries() call
984
+ function AddQuery ( $query, $index="*", $comment="" )
985
+ {
986
+ // mbstring workaround
987
+ $this->_MBPush ();
988
+
989
+ // build request
990
+ $req = pack ( "NNNN", $this->_offset, $this->_limit, $this->_mode, $this->_ranker );
991
+ if ( $this->_ranker==SPH_RANK_EXPR )
992
+ $req .= pack ( "N", strlen($this->_rankexpr) ) . $this->_rankexpr;
993
+ $req .= pack ( "N", $this->_sort ); // (deprecated) sort mode
994
+ $req .= pack ( "N", strlen($this->_sortby) ) . $this->_sortby;
995
+ $req .= pack ( "N", strlen($query) ) . $query; // query itself
996
+ $req .= pack ( "N", count($this->_weights) ); // weights
997
+ foreach ( $this->_weights as $weight )
998
+ $req .= pack ( "N", (int)$weight );
999
+ $req .= pack ( "N", strlen($index) ) . $index; // indexes
1000
+ $req .= pack ( "N", 1 ); // id64 range marker
1001
+ $req .= sphPackU64 ( $this->_min_id ) . sphPackU64 ( $this->_max_id ); // id64 range
1002
+
1003
+ // filters
1004
+ $req .= pack ( "N", count($this->_filters) );
1005
+ foreach ( $this->_filters as $filter )
1006
+ {
1007
+ $req .= pack ( "N", strlen($filter["attr"]) ) . $filter["attr"];
1008
+ $req .= pack ( "N", $filter["type"] );
1009
+ switch ( $filter["type"] )
1010
+ {
1011
+ case SPH_FILTER_VALUES:
1012
+ $req .= pack ( "N", count($filter["values"]) );
1013
+ foreach ( $filter["values"] as $value )
1014
+ $req .= sphPackI64 ( $value );
1015
+ break;
1016
+
1017
+ case SPH_FILTER_RANGE:
1018
+ $req .= sphPackI64 ( $filter["min"] ) . sphPackI64 ( $filter["max"] );
1019
+ break;
1020
+
1021
+ case SPH_FILTER_FLOATRANGE:
1022
+ $req .= $this->_PackFloat ( $filter["min"] ) . $this->_PackFloat ( $filter["max"] );
1023
+ break;
1024
+
1025
+ default:
1026
+ assert ( 0 && "internal error: unhandled filter type" );
1027
+ }
1028
+ $req .= pack ( "N", $filter["exclude"] );
1029
+ }
1030
+
1031
+ // group-by clause, max-matches count, group-sort clause, cutoff count
1032
+ $req .= pack ( "NN", $this->_groupfunc, strlen($this->_groupby) ) . $this->_groupby;
1033
+ $req .= pack ( "N", $this->_maxmatches );
1034
+ $req .= pack ( "N", strlen($this->_groupsort) ) . $this->_groupsort;
1035
+ $req .= pack ( "NNN", $this->_cutoff, $this->_retrycount, $this->_retrydelay );
1036
+ $req .= pack ( "N", strlen($this->_groupdistinct) ) . $this->_groupdistinct;
1037
+
1038
+ // anchor point
1039
+ if ( empty($this->_anchor) )
1040
+ {
1041
+ $req .= pack ( "N", 0 );
1042
+ } else
1043
+ {
1044
+ $a =& $this->_anchor;
1045
+ $req .= pack ( "N", 1 );
1046
+ $req .= pack ( "N", strlen($a["attrlat"]) ) . $a["attrlat"];
1047
+ $req .= pack ( "N", strlen($a["attrlong"]) ) . $a["attrlong"];
1048
+ $req .= $this->_PackFloat ( $a["lat"] ) . $this->_PackFloat ( $a["long"] );
1049
+ }
1050
+
1051
+ // per-index weights
1052
+ $req .= pack ( "N", count($this->_indexweights) );
1053
+ foreach ( $this->_indexweights as $idx=>$weight )
1054
+ $req .= pack ( "N", strlen($idx) ) . $idx . pack ( "N", $weight );
1055
+
1056
+ // max query time
1057
+ $req .= pack ( "N", $this->_maxquerytime );
1058
+
1059
+ // per-field weights
1060
+ $req .= pack ( "N", count($this->_fieldweights) );
1061
+ foreach ( $this->_fieldweights as $field=>$weight )
1062
+ $req .= pack ( "N", strlen($field) ) . $field . pack ( "N", $weight );
1063
+
1064
+ // comment
1065
+ $req .= pack ( "N", strlen($comment) ) . $comment;
1066
+
1067
+ // attribute overrides
1068
+ $req .= pack ( "N", count($this->_overrides) );
1069
+ foreach ( $this->_overrides as $key => $entry )
1070
+ {
1071
+ $req .= pack ( "N", strlen($entry["attr"]) ) . $entry["attr"];
1072
+ $req .= pack ( "NN", $entry["type"], count($entry["values"]) );
1073
+ foreach ( $entry["values"] as $id=>$val )
1074
+ {
1075
+ assert ( is_numeric($id) );
1076
+ assert ( is_numeric($val) );
1077
+
1078
+ $req .= sphPackU64 ( $id );
1079
+ switch ( $entry["type"] )
1080
+ {
1081
+ case SPH_ATTR_FLOAT: $req .= $this->_PackFloat ( $val ); break;
1082
+ case SPH_ATTR_BIGINT: $req .= sphPackI64 ( $val ); break;
1083
+ default: $req .= pack ( "N", $val ); break;
1084
+ }
1085
+ }
1086
+ }
1087
+
1088
+ // select-list
1089
+ $req .= pack ( "N", strlen($this->_select) ) . $this->_select;
1090
+
1091
+ // mbstring workaround
1092
+ $this->_MBPop ();
1093
+
1094
+ // store request to requests array
1095
+ $this->_reqs[] = $req;
1096
+ return count($this->_reqs)-1;
1097
+ }
1098
+
1099
+ /// connect to searchd, run queries batch, and return an array of result sets
1100
+ function RunQueries ()
1101
+ {
1102
+ if ( empty($this->_reqs) )
1103
+ {
1104
+ $this->_error = "no queries defined, issue AddQuery() first";
1105
+ return false;
1106
+ }
1107
+
1108
+ // mbstring workaround
1109
+ $this->_MBPush ();
1110
+
1111
+ if (!( $fp = $this->_Connect() ))
1112
+ {
1113
+ $this->_MBPop ();
1114
+ return false;
1115
+ }
1116
+
1117
+ // send query, get response
1118
+ $nreqs = count($this->_reqs);
1119
+ $req = join ( "", $this->_reqs );
1120
+ $len = 8+strlen($req);
1121
+ $req = pack ( "nnNNN", SEARCHD_COMMAND_SEARCH, VER_COMMAND_SEARCH, $len, 0, $nreqs ) . $req; // add header
1122
+
1123
+ if ( !( $this->_Send ( $fp, $req, $len+8 ) ) ||
1124
+ !( $response = $this->_GetResponse ( $fp, VER_COMMAND_SEARCH ) ) )
1125
+ {
1126
+ $this->_MBPop ();
1127
+ return false;
1128
+ }
1129
+
1130
+ // query sent ok; we can reset reqs now
1131
+ $this->_reqs = array ();
1132
+
1133
+ // parse and return response
1134
+ return $this->_ParseSearchResponse ( $response, $nreqs );
1135
+ }
1136
+
1137
+ /// parse and return search query (or queries) response
1138
+ function _ParseSearchResponse ( $response, $nreqs )
1139
+ {
1140
+ $p = 0; // current position
1141
+ $max = strlen($response); // max position for checks, to protect against broken responses
1142
+
1143
+ $results = array ();
1144
+ for ( $ires=0; $ires<$nreqs && $p<$max; $ires++ )
1145
+ {
1146
+ $results[] = array();
1147
+ $result =& $results[$ires];
1148
+
1149
+ $result["error"] = "";
1150
+ $result["warning"] = "";
1151
+
1152
+ // extract status
1153
+ list(,$status) = unpack ( "N*", substr ( $response, $p, 4 ) ); $p += 4;
1154
+ $result["status"] = $status;
1155
+ if ( $status!=SEARCHD_OK )
1156
+ {
1157
+ list(,$len) = unpack ( "N*", substr ( $response, $p, 4 ) ); $p += 4;
1158
+ $message = substr ( $response, $p, $len ); $p += $len;
1159
+
1160
+ if ( $status==SEARCHD_WARNING )
1161
+ {
1162
+ $result["warning"] = $message;
1163
+ } else
1164
+ {
1165
+ $result["error"] = $message;
1166
+ continue;
1167
+ }
1168
+ }
1169
+
1170
+ // read schema
1171
+ $fields = array ();
1172
+ $attrs = array ();
1173
+
1174
+ list(,$nfields) = unpack ( "N*", substr ( $response, $p, 4 ) ); $p += 4;
1175
+ while ( $nfields-->0 && $p<$max )
1176
+ {
1177
+ list(,$len) = unpack ( "N*", substr ( $response, $p, 4 ) ); $p += 4;
1178
+ $fields[] = substr ( $response, $p, $len ); $p += $len;
1179
+ }
1180
+ $result["fields"] = $fields;
1181
+
1182
+ list(,$nattrs) = unpack ( "N*", substr ( $response, $p, 4 ) ); $p += 4;
1183
+ while ( $nattrs-->0 && $p<$max )
1184
+ {
1185
+ list(,$len) = unpack ( "N*", substr ( $response, $p, 4 ) ); $p += 4;
1186
+ $attr = substr ( $response, $p, $len ); $p += $len;
1187
+ list(,$type) = unpack ( "N*", substr ( $response, $p, 4 ) ); $p += 4;
1188
+ $attrs[$attr] = $type;
1189
+ }
1190
+ $result["attrs"] = $attrs;
1191
+
1192
+ // read match count
1193
+ list(,$count) = unpack ( "N*", substr ( $response, $p, 4 ) ); $p += 4;
1194
+ list(,$id64) = unpack ( "N*", substr ( $response, $p, 4 ) ); $p += 4;
1195
+
1196
+ // read matches
1197
+ $idx = -1;
1198
+ while ( $count-->0 && $p<$max )
1199
+ {
1200
+ // index into result array
1201
+ $idx++;
1202
+
1203
+ // parse document id and weight
1204
+ if ( $id64 )
1205
+ {
1206
+ $doc = sphUnpackU64 ( substr ( $response, $p, 8 ) ); $p += 8;
1207
+ list(,$weight) = unpack ( "N*", substr ( $response, $p, 4 ) ); $p += 4;
1208
+ }
1209
+ else
1210
+ {
1211
+ list ( $doc, $weight ) = array_values ( unpack ( "N*N*",
1212
+ substr ( $response, $p, 8 ) ) );
1213
+ $p += 8;
1214
+ $doc = sphFixUint($doc);
1215
+ }
1216
+ $weight = sprintf ( "%u", $weight );
1217
+
1218
+ // create match entry
1219
+ if ( $this->_arrayresult )
1220
+ $result["matches"][$idx] = array ( "id"=>$doc, "weight"=>$weight );
1221
+ else
1222
+ $result["matches"][$doc]["weight"] = $weight;
1223
+
1224
+ // parse and create attributes
1225
+ $attrvals = array ();
1226
+ foreach ( $attrs as $attr=>$type )
1227
+ {
1228
+ // handle 64bit ints
1229
+ if ( $type==SPH_ATTR_BIGINT )
1230
+ {
1231
+ $attrvals[$attr] = sphUnpackI64 ( substr ( $response, $p, 8 ) ); $p += 8;
1232
+ continue;
1233
+ }
1234
+
1235
+ // handle floats
1236
+ if ( $type==SPH_ATTR_FLOAT )
1237
+ {
1238
+ list(,$uval) = unpack ( "N*", substr ( $response, $p, 4 ) ); $p += 4;
1239
+ list(,$fval) = unpack ( "f*", pack ( "L", $uval ) );
1240
+ $attrvals[$attr] = $fval;
1241
+ continue;
1242
+ }
1243
+
1244
+ // handle everything else as unsigned ints
1245
+ list(,$val) = unpack ( "N*", substr ( $response, $p, 4 ) ); $p += 4;
1246
+ if ( $type==SPH_ATTR_MULTI )
1247
+ {
1248
+ $attrvals[$attr] = array ();
1249
+ $nvalues = $val;
1250
+ while ( $nvalues-->0 && $p<$max )
1251
+ {
1252
+ list(,$val) = unpack ( "N*", substr ( $response, $p, 4 ) ); $p += 4;
1253
+ $attrvals[$attr][] = sphFixUint($val);
1254
+ }
1255
+ } else if ( $type==SPH_ATTR_MULTI64 )
1256
+ {
1257
+ $attrvals[$attr] = array ();
1258
+ $nvalues = $val;
1259
+ while ( $nvalues>0 && $p<$max )
1260
+ {
1261
+ $attrvals[$attr][] = sphUnpackU64 ( substr ( $response, $p, 8 ) ); $p += 8;
1262
+ $nvalues -= 2;
1263
+ }
1264
+ } else if ( $type==SPH_ATTR_STRING )
1265
+ {
1266
+ $attrvals[$attr] = substr ( $response, $p, $val );
1267
+ $p += $val;
1268
+ } else
1269
+ {
1270
+ $attrvals[$attr] = sphFixUint($val);
1271
+ }
1272
+ }
1273
+
1274
+ if ( $this->_arrayresult )
1275
+ $result["matches"][$idx]["attrs"] = $attrvals;
1276
+ else
1277
+ $result["matches"][$doc]["attrs"] = $attrvals;
1278
+ }
1279
+
1280
+ list ( $total, $total_found, $msecs, $words ) =
1281
+ array_values ( unpack ( "N*N*N*N*", substr ( $response, $p, 16 ) ) );
1282
+ $result["total"] = sprintf ( "%u", $total );
1283
+ $result["total_found"] = sprintf ( "%u", $total_found );
1284
+ $result["time"] = sprintf ( "%.3f", $msecs/1000 );
1285
+ $p += 16;
1286
+
1287
+ while ( $words-->0 && $p<$max )
1288
+ {
1289
+ list(,$len) = unpack ( "N*", substr ( $response, $p, 4 ) ); $p += 4;
1290
+ $word = substr ( $response, $p, $len ); $p += $len;
1291
+ list ( $docs, $hits ) = array_values ( unpack ( "N*N*", substr ( $response, $p, 8 ) ) ); $p += 8;
1292
+ $result["words"][$word] = array (
1293
+ "docs"=>sprintf ( "%u", $docs ),
1294
+ "hits"=>sprintf ( "%u", $hits ) );
1295
+ }
1296
+ }
1297
+
1298
+ $this->_MBPop ();
1299
+ return $results;
1300
+ }
1301
+
1302
+ /////////////////////////////////////////////////////////////////////////////
1303
+ // excerpts generation
1304
+ /////////////////////////////////////////////////////////////////////////////
1305
+
1306
+ /// connect to searchd server, and generate exceprts (snippets)
1307
+ /// of given documents for given query. returns false on failure,
1308
+ /// an array of snippets on success
1309
+ function BuildExcerpts ( $docs, $index, $words, $opts=array() )
1310
+ {
1311
+ assert ( is_array($docs) );
1312
+ assert ( is_string($index) );
1313
+ assert ( is_string($words) );
1314
+ assert ( is_array($opts) );
1315
+
1316
+ $this->_MBPush ();
1317
+
1318
+ if (!( $fp = $this->_Connect() ))
1319
+ {
1320
+ $this->_MBPop();
1321
+ return false;
1322
+ }
1323
+
1324
+ /////////////////
1325
+ // fixup options
1326
+ /////////////////
1327
+
1328
+ if ( !isset($opts["before_match"]) ) $opts["before_match"] = "<b>";
1329
+ if ( !isset($opts["after_match"]) ) $opts["after_match"] = "</b>";
1330
+ if ( !isset($opts["chunk_separator"]) ) $opts["chunk_separator"] = " ... ";
1331
+ if ( !isset($opts["limit"]) ) $opts["limit"] = 256;
1332
+ if ( !isset($opts["limit_passages"]) ) $opts["limit_passages"] = 0;
1333
+ if ( !isset($opts["limit_words"]) ) $opts["limit_words"] = 0;
1334
+ if ( !isset($opts["around"]) ) $opts["around"] = 5;
1335
+ if ( !isset($opts["exact_phrase"]) ) $opts["exact_phrase"] = false;
1336
+ if ( !isset($opts["single_passage"]) ) $opts["single_passage"] = false;
1337
+ if ( !isset($opts["use_boundaries"]) ) $opts["use_boundaries"] = false;
1338
+ if ( !isset($opts["weight_order"]) ) $opts["weight_order"] = false;
1339
+ if ( !isset($opts["query_mode"]) ) $opts["query_mode"] = false;
1340
+ if ( !isset($opts["force_all_words"]) ) $opts["force_all_words"] = false;
1341
+ if ( !isset($opts["start_passage_id"]) ) $opts["start_passage_id"] = 1;
1342
+ if ( !isset($opts["load_files"]) ) $opts["load_files"] = false;
1343
+ if ( !isset($opts["html_strip_mode"]) ) $opts["html_strip_mode"] = "index";
1344
+ if ( !isset($opts["allow_empty"]) ) $opts["allow_empty"] = false;
1345
+ if ( !isset($opts["passage_boundary"]) ) $opts["passage_boundary"] = "none";
1346
+ if ( !isset($opts["emit_zones"]) ) $opts["emit_zones"] = false;
1347
+ if ( !isset($opts["load_files_scattered"]) ) $opts["load_files_scattered"] = false;
1348
+
1349
+
1350
+ /////////////////
1351
+ // build request
1352
+ /////////////////
1353
+
1354
+ // v.1.2 req
1355
+ $flags = 1; // remove spaces
1356
+ if ( $opts["exact_phrase"] ) $flags |= 2;
1357
+ if ( $opts["single_passage"] ) $flags |= 4;
1358
+ if ( $opts["use_boundaries"] ) $flags |= 8;
1359
+ if ( $opts["weight_order"] ) $flags |= 16;
1360
+ if ( $opts["query_mode"] ) $flags |= 32;
1361
+ if ( $opts["force_all_words"] ) $flags |= 64;
1362
+ if ( $opts["load_files"] ) $flags |= 128;
1363
+ if ( $opts["allow_empty"] ) $flags |= 256;
1364
+ if ( $opts["emit_zones"] ) $flags |= 512;
1365
+ if ( $opts["load_files_scattered"] ) $flags |= 1024;
1366
+ $req = pack ( "NN", 0, $flags ); // mode=0, flags=$flags
1367
+ $req .= pack ( "N", strlen($index) ) . $index; // req index
1368
+ $req .= pack ( "N", strlen($words) ) . $words; // req words
1369
+
1370
+ // options
1371
+ $req .= pack ( "N", strlen($opts["before_match"]) ) . $opts["before_match"];
1372
+ $req .= pack ( "N", strlen($opts["after_match"]) ) . $opts["after_match"];
1373
+ $req .= pack ( "N", strlen($opts["chunk_separator"]) ) . $opts["chunk_separator"];
1374
+ $req .= pack ( "NN", (int)$opts["limit"], (int)$opts["around"] );
1375
+ $req .= pack ( "NNN", (int)$opts["limit_passages"], (int)$opts["limit_words"], (int)$opts["start_passage_id"] ); // v.1.2
1376
+ $req .= pack ( "N", strlen($opts["html_strip_mode"]) ) . $opts["html_strip_mode"];
1377
+ $req .= pack ( "N", strlen($opts["passage_boundary"]) ) . $opts["passage_boundary"];
1378
+
1379
+ // documents
1380
+ $req .= pack ( "N", count($docs) );
1381
+ foreach ( $docs as $doc )
1382
+ {
1383
+ assert ( is_string($doc) );
1384
+ $req .= pack ( "N", strlen($doc) ) . $doc;
1385
+ }
1386
+
1387
+ ////////////////////////////
1388
+ // send query, get response
1389
+ ////////////////////////////
1390
+
1391
+ $len = strlen($req);
1392
+ $req = pack ( "nnN", SEARCHD_COMMAND_EXCERPT, VER_COMMAND_EXCERPT, $len ) . $req; // add header
1393
+ if ( !( $this->_Send ( $fp, $req, $len+8 ) ) ||
1394
+ !( $response = $this->_GetResponse ( $fp, VER_COMMAND_EXCERPT ) ) )
1395
+ {
1396
+ $this->_MBPop ();
1397
+ return false;
1398
+ }
1399
+
1400
+ //////////////////
1401
+ // parse response
1402
+ //////////////////
1403
+
1404
+ $pos = 0;
1405
+ $res = array ();
1406
+ $rlen = strlen($response);
1407
+ for ( $i=0; $i<count($docs); $i++ )
1408
+ {
1409
+ list(,$len) = unpack ( "N*", substr ( $response, $pos, 4 ) );
1410
+ $pos += 4;
1411
+
1412
+ if ( $pos+$len > $rlen )
1413
+ {
1414
+ $this->_error = "incomplete reply";
1415
+ $this->_MBPop ();
1416
+ return false;
1417
+ }
1418
+ $res[] = $len ? substr ( $response, $pos, $len ) : "";
1419
+ $pos += $len;
1420
+ }
1421
+
1422
+ $this->_MBPop ();
1423
+ return $res;
1424
+ }
1425
+
1426
+
1427
+ /////////////////////////////////////////////////////////////////////////////
1428
+ // keyword generation
1429
+ /////////////////////////////////////////////////////////////////////////////
1430
+
1431
+ /// connect to searchd server, and generate keyword list for a given query
1432
+ /// returns false on failure,
1433
+ /// an array of words on success
1434
+ function BuildKeywords ( $query, $index, $hits )
1435
+ {
1436
+ assert ( is_string($query) );
1437
+ assert ( is_string($index) );
1438
+ assert ( is_bool($hits) );
1439
+
1440
+ // Commented out for testing Riddle
1441
+ // $this->_MBPush ();
1442
+ //
1443
+ // if (!( $fp = $this->_Connect() ))
1444
+ // {
1445
+ // $this->_MBPop();
1446
+ // return false;
1447
+ // }
1448
+
1449
+ /////////////////
1450
+ // build request
1451
+ /////////////////
1452
+
1453
+ // v.1.0 req
1454
+ $req = pack ( "N", strlen($query) ) . $query; // req query
1455
+ $req .= pack ( "N", strlen($index) ) . $index; // req index
1456
+ $req .= pack ( "N", (int)$hits );
1457
+
1458
+ // Line for testing Riddle:
1459
+ return $req;
1460
+
1461
+ ////////////////////////////
1462
+ // send query, get response
1463
+ ////////////////////////////
1464
+
1465
+ $len = strlen($req);
1466
+ $req = pack ( "nnN", SEARCHD_COMMAND_KEYWORDS, VER_COMMAND_KEYWORDS, $len ) . $req; // add header
1467
+ if ( !( $this->_Send ( $fp, $req, $len+8 ) ) ||
1468
+ !( $response = $this->_GetResponse ( $fp, VER_COMMAND_KEYWORDS ) ) )
1469
+ {
1470
+ $this->_MBPop ();
1471
+ return false;
1472
+ }
1473
+
1474
+ //////////////////
1475
+ // parse response
1476
+ //////////////////
1477
+
1478
+ $pos = 0;
1479
+ $res = array ();
1480
+ $rlen = strlen($response);
1481
+ list(,$nwords) = unpack ( "N*", substr ( $response, $pos, 4 ) );
1482
+ $pos += 4;
1483
+ for ( $i=0; $i<$nwords; $i++ )
1484
+ {
1485
+ list(,$len) = unpack ( "N*", substr ( $response, $pos, 4 ) ); $pos += 4;
1486
+ $tokenized = $len ? substr ( $response, $pos, $len ) : "";
1487
+ $pos += $len;
1488
+
1489
+ list(,$len) = unpack ( "N*", substr ( $response, $pos, 4 ) ); $pos += 4;
1490
+ $normalized = $len ? substr ( $response, $pos, $len ) : "";
1491
+ $pos += $len;
1492
+
1493
+ $res[] = array ( "tokenized"=>$tokenized, "normalized"=>$normalized );
1494
+
1495
+ if ( $hits )
1496
+ {
1497
+ list($ndocs,$nhits) = array_values ( unpack ( "N*N*", substr ( $response, $pos, 8 ) ) );
1498
+ $pos += 8;
1499
+ $res [$i]["docs"] = $ndocs;
1500
+ $res [$i]["hits"] = $nhits;
1501
+ }
1502
+
1503
+ if ( $pos > $rlen )
1504
+ {
1505
+ $this->_error = "incomplete reply";
1506
+ $this->_MBPop ();
1507
+ return false;
1508
+ }
1509
+ }
1510
+
1511
+ $this->_MBPop ();
1512
+ return $res;
1513
+ }
1514
+
1515
+ function EscapeString ( $string )
1516
+ {
1517
+ $from = array ( '\\', '(',')','|','-','!','@','~','"','&', '/', '^', '$', '=' );
1518
+ $to = array ( '\\\\', '\(','\)','\|','\-','\!','\@','\~','\"', '\&', '\/', '\^', '\$', '\=' );
1519
+
1520
+ return str_replace ( $from, $to, $string );
1521
+ }
1522
+
1523
+ /////////////////////////////////////////////////////////////////////////////
1524
+ // attribute updates
1525
+ /////////////////////////////////////////////////////////////////////////////
1526
+
1527
+ /// batch update given attributes in given rows in given indexes
1528
+ /// returns amount of updated documents (0 or more) on success, or -1 on failure
1529
+ function UpdateAttributes ( $index, $attrs, $values, $mva=false )
1530
+ {
1531
+ // verify everything
1532
+ assert ( is_string($index) );
1533
+ assert ( is_bool($mva) );
1534
+
1535
+ assert ( is_array($attrs) );
1536
+ foreach ( $attrs as $attr )
1537
+ assert ( is_string($attr) );
1538
+
1539
+ assert ( is_array($values) );
1540
+ foreach ( $values as $id=>$entry )
1541
+ {
1542
+ assert ( is_numeric($id) );
1543
+ assert ( is_array($entry) );
1544
+ assert ( count($entry)==count($attrs) );
1545
+ foreach ( $entry as $v )
1546
+ {
1547
+ if ( $mva )
1548
+ {
1549
+ assert ( is_array($v) );
1550
+ foreach ( $v as $vv )
1551
+ assert ( is_int($vv) );
1552
+ } else
1553
+ assert ( is_int($v) );
1554
+ }
1555
+ }
1556
+
1557
+ // build request
1558
+ $this->_MBPush ();
1559
+ $req = pack ( "N", strlen($index) ) . $index;
1560
+
1561
+ $req .= pack ( "N", count($attrs) );
1562
+ foreach ( $attrs as $attr )
1563
+ {
1564
+ $req .= pack ( "N", strlen($attr) ) . $attr;
1565
+ $req .= pack ( "N", $mva ? 1 : 0 );
1566
+ }
1567
+
1568
+ $req .= pack ( "N", count($values) );
1569
+ foreach ( $values as $id=>$entry )
1570
+ {
1571
+ $req .= sphPackU64 ( $id );
1572
+ foreach ( $entry as $v )
1573
+ {
1574
+ $req .= pack ( "N", $mva ? count($v) : $v );
1575
+ if ( $mva )
1576
+ foreach ( $v as $vv )
1577
+ $req .= pack ( "N", $vv );
1578
+ }
1579
+ }
1580
+
1581
+ // Line for testing Riddle:
1582
+ return $req;
1583
+
1584
+ // connect, send query, get response
1585
+ if (!( $fp = $this->_Connect() ))
1586
+ {
1587
+ $this->_MBPop ();
1588
+ return -1;
1589
+ }
1590
+
1591
+ $len = strlen($req);
1592
+ $req = pack ( "nnN", SEARCHD_COMMAND_UPDATE, VER_COMMAND_UPDATE, $len ) . $req; // add header
1593
+ if ( !$this->_Send ( $fp, $req, $len+8 ) )
1594
+ {
1595
+ $this->_MBPop ();
1596
+ return -1;
1597
+ }
1598
+
1599
+ if (!( $response = $this->_GetResponse ( $fp, VER_COMMAND_UPDATE ) ))
1600
+ {
1601
+ $this->_MBPop ();
1602
+ return -1;
1603
+ }
1604
+
1605
+ // parse response
1606
+ list(,$updated) = unpack ( "N*", substr ( $response, 0, 4 ) );
1607
+ $this->_MBPop ();
1608
+ return $updated;
1609
+ }
1610
+
1611
+ /////////////////////////////////////////////////////////////////////////////
1612
+ // persistent connections
1613
+ /////////////////////////////////////////////////////////////////////////////
1614
+
1615
+ function Open()
1616
+ {
1617
+ if ( $this->_socket !== false )
1618
+ {
1619
+ $this->_error = 'already connected';
1620
+ return false;
1621
+ }
1622
+ if ( !$fp = $this->_Connect() )
1623
+ return false;
1624
+
1625
+ // command, command version = 0, body length = 4, body = 1
1626
+ $req = pack ( "nnNN", SEARCHD_COMMAND_PERSIST, 0, 4, 1 );
1627
+ if ( !$this->_Send ( $fp, $req, 12 ) )
1628
+ return false;
1629
+
1630
+ $this->_socket = $fp;
1631
+ return true;
1632
+ }
1633
+
1634
+ function Close()
1635
+ {
1636
+ if ( $this->_socket === false )
1637
+ {
1638
+ $this->_error = 'not connected';
1639
+ return false;
1640
+ }
1641
+
1642
+ fclose ( $this->_socket );
1643
+ $this->_socket = false;
1644
+
1645
+ return true;
1646
+ }
1647
+
1648
+ //////////////////////////////////////////////////////////////////////////
1649
+ // status
1650
+ //////////////////////////////////////////////////////////////////////////
1651
+
1652
+ function Status ()
1653
+ {
1654
+ $this->_MBPush ();
1655
+ if (!( $fp = $this->_Connect() ))
1656
+ {
1657
+ $this->_MBPop();
1658
+ return false;
1659
+ }
1660
+
1661
+ $req = pack ( "nnNN", SEARCHD_COMMAND_STATUS, VER_COMMAND_STATUS, 4, 1 ); // len=4, body=1
1662
+ if ( !( $this->_Send ( $fp, $req, 12 ) ) ||
1663
+ !( $response = $this->_GetResponse ( $fp, VER_COMMAND_STATUS ) ) )
1664
+ {
1665
+ $this->_MBPop ();
1666
+ return false;
1667
+ }
1668
+
1669
+ $res = substr ( $response, 4 ); // just ignore length, error handling, etc
1670
+ $p = 0;
1671
+ list ( $rows, $cols ) = array_values ( unpack ( "N*N*", substr ( $response, $p, 8 ) ) ); $p += 8;
1672
+
1673
+ $res = array();
1674
+ for ( $i=0; $i<$rows; $i++ )
1675
+ for ( $j=0; $j<$cols; $j++ )
1676
+ {
1677
+ list(,$len) = unpack ( "N*", substr ( $response, $p, 4 ) ); $p += 4;
1678
+ $res[$i][] = substr ( $response, $p, $len ); $p += $len;
1679
+ }
1680
+
1681
+ $this->_MBPop ();
1682
+ return $res;
1683
+ }
1684
+
1685
+ //////////////////////////////////////////////////////////////////////////
1686
+ // flush
1687
+ //////////////////////////////////////////////////////////////////////////
1688
+
1689
+ function FlushAttributes ()
1690
+ {
1691
+ $this->_MBPush ();
1692
+ if (!( $fp = $this->_Connect() ))
1693
+ {
1694
+ $this->_MBPop();
1695
+ return -1;
1696
+ }
1697
+
1698
+ $req = pack ( "nnN", SEARCHD_COMMAND_FLUSHATTRS, VER_COMMAND_FLUSHATTRS, 0 ); // len=0
1699
+ if ( !( $this->_Send ( $fp, $req, 8 ) ) ||
1700
+ !( $response = $this->_GetResponse ( $fp, VER_COMMAND_FLUSHATTRS ) ) )
1701
+ {
1702
+ $this->_MBPop ();
1703
+ return -1;
1704
+ }
1705
+
1706
+ $tag = -1;
1707
+ if ( strlen($response)==4 )
1708
+ list(,$tag) = unpack ( "N*", $response );
1709
+ else
1710
+ $this->_error = "unexpected response length";
1711
+
1712
+ $this->_MBPop ();
1713
+ return $tag;
1714
+ }
1715
+
1716
+ // Added for Riddle - code is taken from AddQuery
1717
+ function FilterOutput()
1718
+ {
1719
+ $req = "";
1720
+ foreach ( $this->_filters as $filter )
1721
+ {
1722
+ $req .= pack ( "N", strlen($filter["attr"]) ) . $filter["attr"];
1723
+ $req .= pack ( "N", $filter["type"] );
1724
+ switch ( $filter["type"] )
1725
+ {
1726
+ case SPH_FILTER_VALUES:
1727
+ $req .= pack ( "N", count($filter["values"]) );
1728
+ foreach ( $filter["values"] as $value )
1729
+ $req .= sphPackI64 ( $value );
1730
+ break;
1731
+
1732
+ case SPH_FILTER_RANGE:
1733
+ $req .= sphPackI64 ( $filter["min"] ) . sphPackI64 ( $filter["max"] );
1734
+ break;
1735
+
1736
+ case SPH_FILTER_FLOATRANGE:
1737
+ $req .= $this->_PackFloat ( $filter["min"] ) . $this->_PackFloat ( $filter["max"] );
1738
+ break;
1739
+
1740
+ default:
1741
+ assert ( 0 && "internal error: unhandled filter type" );
1742
+ }
1743
+ $req .= pack ( "N", $filter["exclude"] );
1744
+ }
1745
+
1746
+ return $req;
1747
+ }
1748
+ }
1749
+
1750
+ //
1751
+ // $Id: sphinxapi.php 2970 2011-09-23 16:50:22Z klirichek $
1752
+ //