riddle 1.5.0 → 1.5.1

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 (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
+ //