nixme-thinking-sphinx 0.9.7

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. data/LICENCE +20 -0
  2. data/README +52 -0
  3. data/lib/riddle.rb +22 -0
  4. data/lib/riddle/client.rb +593 -0
  5. data/lib/riddle/client/filter.rb +44 -0
  6. data/lib/riddle/client/message.rb +65 -0
  7. data/lib/riddle/client/response.rb +84 -0
  8. data/lib/test.rb +46 -0
  9. data/lib/thinking_sphinx.rb +82 -0
  10. data/lib/thinking_sphinx/active_record.rb +138 -0
  11. data/lib/thinking_sphinx/active_record/delta.rb +90 -0
  12. data/lib/thinking_sphinx/active_record/has_many_association.rb +29 -0
  13. data/lib/thinking_sphinx/active_record/search.rb +43 -0
  14. data/lib/thinking_sphinx/association.rb +140 -0
  15. data/lib/thinking_sphinx/attribute.rb +282 -0
  16. data/lib/thinking_sphinx/configuration.rb +277 -0
  17. data/lib/thinking_sphinx/field.rb +198 -0
  18. data/lib/thinking_sphinx/index.rb +334 -0
  19. data/lib/thinking_sphinx/index/builder.rb +212 -0
  20. data/lib/thinking_sphinx/index/faux_column.rb +97 -0
  21. data/lib/thinking_sphinx/rails_additions.rb +56 -0
  22. data/lib/thinking_sphinx/search.rb +455 -0
  23. data/spec/unit/thinking_sphinx/active_record/delta_spec.rb +185 -0
  24. data/spec/unit/thinking_sphinx/active_record/has_many_association_spec.rb +53 -0
  25. data/spec/unit/thinking_sphinx/active_record/search_spec.rb +81 -0
  26. data/spec/unit/thinking_sphinx/active_record_spec.rb +201 -0
  27. data/spec/unit/thinking_sphinx/association_spec.rb +247 -0
  28. data/spec/unit/thinking_sphinx/attribute_spec.rb +356 -0
  29. data/spec/unit/thinking_sphinx/configuration_spec.rb +476 -0
  30. data/spec/unit/thinking_sphinx/field_spec.rb +215 -0
  31. data/spec/unit/thinking_sphinx/index/builder_spec.rb +33 -0
  32. data/spec/unit/thinking_sphinx/index/faux_column_spec.rb +41 -0
  33. data/spec/unit/thinking_sphinx/index_spec.rb +230 -0
  34. data/spec/unit/thinking_sphinx/search_spec.rb +163 -0
  35. data/spec/unit/thinking_sphinx_spec.rb +107 -0
  36. data/tasks/thinking_sphinx_tasks.rake +1 -0
  37. data/tasks/thinking_sphinx_tasks.rb +86 -0
  38. metadata +90 -0
data/LICENCE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2008 Pat Allan
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README ADDED
@@ -0,0 +1,52 @@
1
+ = Thinking Sphinx
2
+
3
+ == Usage
4
+
5
+ First, if you haven't done so already, check out the main usage[http://ts.freelancing-gods.com/usage.html] page. Once you've done that, the next place to look for information is the specific method docs - ThinkingSphinx::Search and ThinkingSphinx::Index::Builder in particular.
6
+
7
+ == Contributing
8
+
9
+ Fork on GitHub and after you've committed tested patches, send a pull request.
10
+
11
+ To get the spec suite running, you will need to install the not-a-mock gem if you don't already have it:
12
+
13
+ git clone git://github.com/freelancing-god/not-a-mock.git
14
+ cd not-a-mock
15
+ rake gem
16
+ gem install pkg/not_a_mock-1.1.0.gem
17
+
18
+ Then set up your database
19
+
20
+ cp spec/fixtures/database.yml.default spec/fixtures/database.yml
21
+ mysqladmin -u root create thinking_sphinx
22
+
23
+ You should now have a passing test suite from which to build your patch on.
24
+
25
+ rake spec
26
+
27
+ == Contributors
28
+
29
+ Since I first released this library, there's been quite a few people who have submitted patches, to my immense gratitude. Others have suggested syntax changes and general improvements. So my thanks to the following people:
30
+
31
+ - Joost Hietbrink
32
+ - Jonathon Conway
33
+ - Gregory Mirzayantz
34
+ - Tung Nguyen
35
+ - Sean Cribbs
36
+ - Benoit Caccinolo
37
+ - John Barton
38
+ - Oliver Beddows
39
+ - Arthur Zapparoli
40
+ - Dusty Doris
41
+ - Marcus Crafter
42
+ - Patrick Lenz
43
+ - Björn Andreasson
44
+ - James Healy
45
+ - Jae-Jun Hwang
46
+ - Xavier Shay
47
+ - Jason Rust
48
+ - Gopal Patel
49
+ - Chris Heald
50
+ - Peter Vandenberk
51
+ - Josh French
52
+ - Andrew Bennett
@@ -0,0 +1,22 @@
1
+ require 'socket'
2
+ require 'riddle/client'
3
+ require 'riddle/client/filter'
4
+ require 'riddle/client/message'
5
+ require 'riddle/client/response'
6
+
7
+ module Riddle #:nodoc:
8
+ class ConnectionError < StandardError #:nodoc:
9
+ end
10
+
11
+ module Version #:nodoc:
12
+ Major = 0
13
+ Minor = 9
14
+ Tiny = 8
15
+ # Revision number for RubyForge's sake, taken from what Sphinx
16
+ # outputs to the command line.
17
+ Rev = 1198
18
+
19
+ String = [Major, Minor, Tiny].join('.') + "rc1"
20
+ GemVersion = [Major, Minor, Tiny, Rev].join('.')
21
+ end
22
+ end
@@ -0,0 +1,593 @@
1
+ module Riddle
2
+ class VersionError < StandardError; end
3
+ class ResponseError < StandardError; end
4
+
5
+ # This class was heavily based on the existing Client API by Dmytro Shteflyuk
6
+ # and Alexy Kovyrin. Their code worked fine, I just wanted something a bit
7
+ # more Ruby-ish (ie. lowercase and underscored method names). I also have
8
+ # used a few helper classes, just to neaten things up.
9
+ #
10
+ # Feel free to use it wherever. Send bug reports, patches, comments and
11
+ # suggestions to pat at freelancing-gods dot com.
12
+ #
13
+ # Most properties of the client are accessible through attribute accessors,
14
+ # and where relevant use symboles instead of the long constants common in
15
+ # other clients.
16
+ # Some examples:
17
+ #
18
+ # client.sort_mode = :extended
19
+ # client.sort_by = "birthday DESC"
20
+ # client.match_mode = :extended
21
+ #
22
+ # To add a filter, you will need to create a Filter object:
23
+ #
24
+ # client.filters << Riddle::Client::Filter.new("birthday",
25
+ # Time.at(1975, 1, 1).to_i..Time.at(1985, 1, 1).to_i, false)
26
+ #
27
+ class Client
28
+ Commands = {
29
+ :search => 0, # SEARCHD_COMMAND_SEARCH
30
+ :excerpt => 1, # SEARCHD_COMMAND_EXCERPT
31
+ :update => 2, # SEARCHD_COMMAND_UPDATE
32
+ :keywords => 3 # SEARCHD_COMMAND_KEYWORDS
33
+ }
34
+
35
+ Versions = {
36
+ :search => 0x113, # VER_COMMAND_SEARCH
37
+ :excerpt => 0x100, # VER_COMMAND_EXCERPT
38
+ :update => 0x101, # VER_COMMAND_UPDATE
39
+ :keywords => 0x100 # VER_COMMAND_KEYWORDS
40
+ }
41
+
42
+ Statuses = {
43
+ :ok => 0, # SEARCHD_OK
44
+ :error => 1, # SEARCHD_ERROR
45
+ :retry => 2, # SEARCHD_RETRY
46
+ :warning => 3 # SEARCHD_WARNING
47
+ }
48
+
49
+ MatchModes = {
50
+ :all => 0, # SPH_MATCH_ALL
51
+ :any => 1, # SPH_MATCH_ANY
52
+ :phrase => 2, # SPH_MATCH_PHRASE
53
+ :boolean => 3, # SPH_MATCH_BOOLEAN
54
+ :extended => 4, # SPH_MATCH_EXTENDED
55
+ :fullscan => 5, # SPH_MATCH_FULLSCAN
56
+ :extended2 => 6 # SPH_MATCH_EXTENDED2
57
+ }
58
+
59
+ RankModes = {
60
+ :proximity_bm25 => 0, # SPH_RANK_PROXIMITY_BM25
61
+ :bm25 => 1, # SPH_RANK_BM25
62
+ :none => 2, # SPH_RANK_NONE
63
+ :wordcount => 3 # SPH_RANK_WORDCOUNT
64
+ }
65
+
66
+ SortModes = {
67
+ :relevance => 0, # SPH_SORT_RELEVANCE
68
+ :attr_desc => 1, # SPH_SORT_ATTR_DESC
69
+ :attr_asc => 2, # SPH_SORT_ATTR_ASC
70
+ :time_segments => 3, # SPH_SORT_TIME_SEGMENTS
71
+ :extended => 4, # SPH_SORT_EXTENDED
72
+ :expr => 5 # SPH_SORT_EXPR
73
+ }
74
+
75
+ AttributeTypes = {
76
+ :integer => 1, # SPH_ATTR_INTEGER
77
+ :timestamp => 2, # SPH_ATTR_TIMESTAMP
78
+ :ordinal => 3, # SPH_ATTR_ORDINAL
79
+ :bool => 4, # SPH_ATTR_BOOL
80
+ :float => 5, # SPH_ATTR_FLOAT
81
+ :multi => 0x40000000 # SPH_ATTR_MULTI
82
+ }
83
+
84
+ GroupFunctions = {
85
+ :day => 0, # SPH_GROUPBY_DAY
86
+ :week => 1, # SPH_GROUPBY_WEEK
87
+ :month => 2, # SPH_GROUPBY_MONTH
88
+ :year => 3, # SPH_GROUPBY_YEAR
89
+ :attr => 4, # SPH_GROUPBY_ATTR
90
+ :attrpair => 5 # SPH_GROUPBY_ATTRPAIR
91
+ }
92
+
93
+ FilterTypes = {
94
+ :values => 0, # SPH_FILTER_VALUES
95
+ :range => 1, # SPH_FILTER_RANGE
96
+ :float_range => 2 # SPH_FILTER_FLOATRANGE
97
+ }
98
+
99
+ attr_accessor :server, :port, :offset, :limit, :max_matches,
100
+ :match_mode, :sort_mode, :sort_by, :weights, :id_range, :filters,
101
+ :group_by, :group_function, :group_clause, :group_distinct, :cut_off,
102
+ :retry_count, :retry_delay, :anchor, :index_weights, :rank_mode,
103
+ :max_query_time, :field_weights
104
+ attr_reader :queue
105
+
106
+ # Can instantiate with a specific server and port - otherwise it assumes
107
+ # defaults of localhost and 3312 respectively. All other settings can be
108
+ # accessed and changed via the attribute accessors.
109
+ def initialize(server=nil, port=nil)
110
+ @server = server || "localhost"
111
+ @port = port || 3312
112
+
113
+ # defaults
114
+ @offset = 0
115
+ @limit = 20
116
+ @max_matches = 1000
117
+ @match_mode = :all
118
+ @sort_mode = :relevance
119
+ @sort_by = ''
120
+ @weights = []
121
+ @id_range = 0..0
122
+ @filters = []
123
+ @group_by = ''
124
+ @group_function = :day
125
+ @group_clause = '@group desc'
126
+ @group_distinct = ''
127
+ @cut_off = 0
128
+ @retry_count = 0
129
+ @retry_delay = 0
130
+ @anchor = {}
131
+ # string keys are index names, integer values are weightings
132
+ @index_weights = {}
133
+ @rank_mode = :proximity_bm25
134
+ @max_query_time = 0
135
+ # string keys are field names, integer values are weightings
136
+ @field_weights = {}
137
+
138
+ @queue = []
139
+ end
140
+
141
+ # Set the geo-anchor point - with the names of the attributes that contain
142
+ # the latitude and longitude (in radians), and the reference position.
143
+ # Note that for geocoding to work properly, you must also set
144
+ # match_mode to :extended. To sort results by distance, you will
145
+ # need to set sort_mode to '@geodist asc' for example. Sphinx
146
+ # expects latitude and longitude to be returned from you SQL source
147
+ # in radians.
148
+ #
149
+ # Example:
150
+ # client.set_anchor('lat', -0.6591741, 'long', 2.530770)
151
+ #
152
+ def set_anchor(lat_attr, lat, long_attr, long)
153
+ @anchor = {
154
+ :latitude_attribute => lat_attr,
155
+ :latitude => lat,
156
+ :longitude_attribute => long_attr,
157
+ :longitude => long
158
+ }
159
+ end
160
+
161
+ # Append a query to the queue. This uses the same parameters as the query
162
+ # method.
163
+ def append_query(search, index = '*', comments = '')
164
+ @queue << query_message(search, index, comments)
165
+ end
166
+
167
+ # Run all the queries currently in the queue. This will return an array of
168
+ # results hashes.
169
+ def run
170
+ response = Response.new request(:search, @queue)
171
+
172
+ results = @queue.collect do
173
+ result = {
174
+ :matches => [],
175
+ :fields => [],
176
+ :attributes => {},
177
+ :attribute_names => [],
178
+ :words => {}
179
+ }
180
+
181
+ result[:status] = response.next_int
182
+ case result[:status]
183
+ when Statuses[:warning]
184
+ result[:warning] = response.next
185
+ when Statuses[:error]
186
+ result[:error] = response.next
187
+ next result
188
+ end
189
+
190
+ result[:fields] = response.next_array
191
+
192
+ attributes = response.next_int
193
+ for i in 0...attributes
194
+ attribute_name = response.next
195
+ type = response.next_int
196
+
197
+ result[:attributes][attribute_name] = type
198
+ result[:attribute_names] << attribute_name
199
+ end
200
+
201
+ matches = response.next_int
202
+ is_64_bit = response.next_int
203
+ for i in 0...matches
204
+ doc = is_64_bit > 0 ? response.next_64bit_int : response.next_int
205
+ weight = response.next_int
206
+
207
+ result[:matches] << {:doc => doc, :weight => weight, :index => i, :attributes => {}}
208
+ result[:attribute_names].each do |attr|
209
+ result[:matches].last[:attributes][attr] = attribute_from_type(
210
+ result[:attributes][attr], response
211
+ )
212
+ end
213
+ end
214
+
215
+ result[:total] = response.next_int.to_i || 0
216
+ result[:total_found] = response.next_int.to_i || 0
217
+ result[:time] = ('%.3f' % (response.next_int / 1000.0)).to_f || 0.0
218
+
219
+ words = response.next_int
220
+ for i in 0...words
221
+ word = response.next
222
+ docs = response.next_int
223
+ hits = response.next_int
224
+ result[:words][word] = {:docs => docs, :hits => hits}
225
+ end
226
+
227
+ result
228
+ end
229
+
230
+ @queue.clear
231
+ results
232
+ end
233
+
234
+ # Query the Sphinx daemon - defaulting to all indexes, but you can specify
235
+ # a specific one if you wish. The search parameter should be a string
236
+ # following Sphinx's expectations.
237
+ #
238
+ # The object returned from this method is a hash with the following keys:
239
+ #
240
+ # * :matches
241
+ # * :fields
242
+ # * :attributes
243
+ # * :attribute_names
244
+ # * :words
245
+ # * :total
246
+ # * :total_found
247
+ # * :time
248
+ # * :status
249
+ # * :warning (if appropriate)
250
+ # * :error (if appropriate)
251
+ #
252
+ # The key <tt>:matches</tt> returns an array of hashes - the actual search
253
+ # results. Each hash has the document id (<tt>:doc</tt>), the result
254
+ # weighting (<tt>:weight</tt>), and a hash of the attributes for the
255
+ # document (<tt>:attributes</tt>).
256
+ #
257
+ # The <tt>:fields</tt> and <tt>:attribute_names</tt> keys return list of
258
+ # fields and attributes for the documents. The key <tt>:attributes</tt>
259
+ # will return a hash of attribute name and type pairs, and <tt>:words</tt>
260
+ # returns a hash of hashes representing the words from the search, with the
261
+ # number of documents and hits for each, along the lines of:
262
+ #
263
+ # results[:words]["Pat"] #=> {:docs => 12, :hits => 15}
264
+ #
265
+ # <tt>:total</tt>, <tt>:total_found</tt> and <tt>:time</tt> return the
266
+ # number of matches available, the total number of matches (which may be
267
+ # greater than the maximum available, depending on the number of matches
268
+ # and your sphinx configuration), and the time in milliseconds that the
269
+ # query took to run.
270
+ #
271
+ # <tt>:status</tt> is the error code for the query - and if there was a
272
+ # related warning, it will be under the <tt>:warning</tt> key. Fatal errors
273
+ # will be described under <tt>:error</tt>.
274
+ #
275
+ def query(search, index = '*', comments = '')
276
+ @queue << query_message(search, index, comments)
277
+ self.run.first
278
+ end
279
+
280
+ # Build excerpts from search terms (the +words+) and the text of documents. Excerpts are bodies of text that have the +words+ highlighted.
281
+ # They may also be abbreviated to fit within a word limit.
282
+ #
283
+ # As part of the options hash, you will need to
284
+ # define:
285
+ # * :docs
286
+ # * :words
287
+ # * :index
288
+ #
289
+ # Optional settings include:
290
+ # * :before_match (defaults to <span class="match">)
291
+ # * :after_match (defaults to </span>)
292
+ # * :chunk_separator (defaults to ' &#8230; ' - which is an HTML ellipsis)
293
+ # * :limit (defaults to 256)
294
+ # * :around (defaults to 5)
295
+ # * :exact_phrase (defaults to false)
296
+ # * :single_passage (defaults to false)
297
+ #
298
+ # The defaults differ from the official PHP client, as I've opted for
299
+ # semantic HTML markup.
300
+ #
301
+ # Example:
302
+ #
303
+ # client.excerpts(:docs => ["Pat Allan, Pat Cash"], :words => 'Pat', :index => 'pats')
304
+ # #=> ["<span class=\"match\">Pat</span> Allan, <span class=\"match\">Pat</span> Cash"]
305
+ #
306
+ # lorem_lipsum = "Lorem ipsum dolor..."
307
+ #
308
+ # client.excerpts(:docs => ["Pat Allan, #{lorem_lipsum} Pat Cash"], :words => 'Pat', :index => 'pats')
309
+ # #=> ["<span class=\"match\">Pat</span> Allan, Lorem ipsum dolor sit amet, consectetur adipisicing
310
+ # elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua &#8230; . Excepteur
311
+ # sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est
312
+ # laborum. <span class=\"match\">Pat</span> Cash"]
313
+ #
314
+ # Workflow:
315
+ #
316
+ # Excerpt creation is completely isolated from searching the index. The nominated index is only used to
317
+ # discover encoding and charset information.
318
+ #
319
+ # Therefore, the workflow goes:
320
+ #
321
+ # 1. Do the sphinx query.
322
+ # 2. Fetch the documents found by sphinx from their repositories.
323
+ # 3. Pass the documents' text to +excerpts+ for marking up of matched terms.
324
+ #
325
+ def excerpts(options = {})
326
+ options[:index] ||= '*'
327
+ options[:before_match] ||= '<span class="match">'
328
+ options[:after_match] ||= '</span>'
329
+ options[:chunk_separator] ||= ' &#8230; ' # ellipsis
330
+ options[:limit] ||= 256
331
+ options[:around] ||= 5
332
+ options[:exact_phrase] ||= false
333
+ options[:single_passage] ||= false
334
+
335
+ response = Response.new request(:excerpt, excerpts_message(options))
336
+
337
+ options[:docs].collect { response.next }
338
+ end
339
+
340
+ # Update attributes - first parameter is the relevant index, second is an
341
+ # array of attributes to be updated, and the third is a hash, where the
342
+ # keys are the document ids, and the values are arrays with the attribute
343
+ # values - in the same order as the second parameter.
344
+ #
345
+ # Example:
346
+ #
347
+ # client.update('people', ['birthday'], {1 => [Time.at(1982, 20, 8).to_i]})
348
+ #
349
+ def update(index, attributes, values_by_doc)
350
+ response = Response.new request(
351
+ :update,
352
+ update_message(index, attributes, values_by_doc)
353
+ )
354
+
355
+ response.next_int
356
+ end
357
+
358
+ # Generates a keyword list for a given query. Each keyword is represented
359
+ # by a hash, with keys :tokenised and :normalised. If return_hits is set to
360
+ # true it will also report on the number of hits and documents for each
361
+ # keyword (see :hits and :docs keys respectively).
362
+ def keywords(query, index, return_hits = false)
363
+ response = Response.new request(
364
+ :keywords,
365
+ keywords_message(query, index, return_hits)
366
+ )
367
+
368
+ (0...response.next_int).collect do
369
+ hash = {}
370
+ hash[:tokenised] = response.next
371
+ hash[:normalised] = response.next
372
+
373
+ if return_hits
374
+ hash[:docs] = response.next_int
375
+ hash[:hits] = response.next_int
376
+ end
377
+
378
+ hash
379
+ end
380
+ end
381
+
382
+ private
383
+
384
+ # Connects to the Sphinx daemon, and yields a socket to use. The socket is
385
+ # closed at the end of the block.
386
+ def connect(&block)
387
+ socket = TCPSocket.new @server, @port
388
+
389
+ # Checking version
390
+ version = socket.recv(4).unpack('N*').first
391
+ if version < 1
392
+ socket.close
393
+ raise VersionError, "Can only connect to searchd version 1.0 or better, not version #{version}"
394
+ end
395
+
396
+ # Send version
397
+ socket.send [1].pack('N'), 0
398
+
399
+ begin
400
+ yield socket
401
+ ensure
402
+ socket.close
403
+ end
404
+ end
405
+
406
+ # Send a collection of messages, for a command type (eg, search, excerpts,
407
+ # update), to the Sphinx daemon.
408
+ def request(command, messages)
409
+ response = ""
410
+ status = -1
411
+ version = 0
412
+ length = 0
413
+ message = Array(messages).join("")
414
+
415
+ connect do |socket|
416
+ case command
417
+ when :search
418
+ # Message length is +4 to account for the following count value for
419
+ # the number of messages (well, that's what I'm assuming).
420
+ socket.send [
421
+ Commands[command], Versions[command],
422
+ 4+message.length, messages.length
423
+ ].pack("nnNN") + message, 0
424
+ else
425
+ socket.send [
426
+ Commands[command], Versions[command], message.length
427
+ ].pack("nnN") + message, 0
428
+ end
429
+
430
+ header = socket.recv(8)
431
+ status, version, length = header.unpack('n2N')
432
+
433
+ while response.length < length
434
+ part = socket.recv(length - response.length)
435
+ response << part if part
436
+ end
437
+ end
438
+
439
+ if response.empty? || response.length != length
440
+ raise ResponseError, "No response from searchd (status: #{status}, version: #{version})"
441
+ end
442
+
443
+ case status
444
+ when Statuses[:ok]
445
+ if version < Versions[command]
446
+ puts format("searchd command v.%d.%d older than client (v.%d.%d)",
447
+ version >> 8, version & 0xff,
448
+ Versions[command] >> 8, Versions[command] & 0xff)
449
+ end
450
+ response
451
+ when Statuses[:warning]
452
+ length = response[0, 4].unpack('N*').first
453
+ puts response[4, length]
454
+ response[4 + length, response.length - 4 - length]
455
+ when Statuses[:error], Statuses[:retry]
456
+ raise ResponseError, "searchd error (status: #{status}): #{response[4, response.length - 4]}"
457
+ else
458
+ raise ResponseError, "Unknown searchd error (status: #{status})"
459
+ end
460
+ end
461
+
462
+ # Generation of the message to send to Sphinx for a search.
463
+ def query_message(search, index, comments = '')
464
+ message = Message.new
465
+
466
+ # Mode, Limits, Sort Mode
467
+ message.append_ints @offset, @limit, MatchModes[@match_mode],
468
+ RankModes[@rank_mode], SortModes[@sort_mode]
469
+ message.append_string @sort_by
470
+
471
+ # Query
472
+ message.append_string search
473
+
474
+ # Weights
475
+ message.append_int @weights.length
476
+ message.append_ints *@weights
477
+
478
+ # Index
479
+ message.append_string index
480
+
481
+ # ID Range
482
+ message.append_int 1
483
+ message.append_64bit_ints @id_range.first, @id_range.last
484
+
485
+ # Filters
486
+ message.append_int @filters.length
487
+ @filters.each { |filter| message.append filter.query_message }
488
+
489
+ # Grouping
490
+ message.append_int GroupFunctions[@group_function]
491
+ message.append_string @group_by
492
+ message.append_int @max_matches
493
+ message.append_string @group_clause
494
+ message.append_ints @cut_off, @retry_count, @retry_delay
495
+ message.append_string @group_distinct
496
+
497
+ # Anchor Point
498
+ if @anchor.empty?
499
+ message.append_int 0
500
+ else
501
+ message.append_int 1
502
+ message.append_string @anchor[:latitude_attribute]
503
+ message.append_string @anchor[:longitude_attribute]
504
+ message.append_floats @anchor[:latitude], @anchor[:longitude]
505
+ end
506
+
507
+ # Per Index Weights
508
+ message.append_int @index_weights.length
509
+ @index_weights.each do |key,val|
510
+ message.append_string key
511
+ message.append_int val
512
+ end
513
+
514
+ # Max Query Time
515
+ message.append_int @max_query_time
516
+
517
+ # Per Field Weights
518
+ message.append_int @field_weights.length
519
+ @field_weights.each do |key,val|
520
+ message.append_string key
521
+ message.append_int val
522
+ end
523
+
524
+ message.append_string comments
525
+
526
+ message.to_s
527
+ end
528
+
529
+ # Generation of the message to send to Sphinx for an excerpts request.
530
+ def excerpts_message(options)
531
+ message = Message.new
532
+
533
+ flags = 1
534
+ flags |= 2 if options[:exact_phrase]
535
+ flags |= 4 if options[:single_passage]
536
+ flags |= 8 if options[:use_boundaries]
537
+ flags |= 16 if options[:weight_order]
538
+
539
+ message.append [0, flags].pack('N2') # 0 = mode
540
+ message.append_string options[:index]
541
+ message.append_string options[:words]
542
+
543
+ # options
544
+ message.append_string options[:before_match]
545
+ message.append_string options[:after_match]
546
+ message.append_string options[:chunk_separator]
547
+ message.append_ints options[:limit], options[:around]
548
+
549
+ message.append_array options[:docs]
550
+
551
+ message.to_s
552
+ end
553
+
554
+ # Generation of the message to send to Sphinx to update attributes of a
555
+ # document.
556
+ def update_message(index, attributes, values_by_doc)
557
+ message = Message.new
558
+
559
+ message.append_string index
560
+ message.append_array attributes
561
+
562
+ message.append_int values_by_doc.length
563
+ values_by_doc.each do |key,values|
564
+ message.append_64bit_int key # document ID
565
+ message.append_ints *values # array of new values (integers)
566
+ end
567
+
568
+ message.to_s
569
+ end
570
+
571
+ # Generates the simple message to send to the daemon for a keywords request.
572
+ def keywords_message(query, index, return_hits)
573
+ message = Message.new
574
+
575
+ message.append_string query
576
+ message.append_string index
577
+ message.append_int return_hits ? 1 : 0
578
+
579
+ message.to_s
580
+ end
581
+
582
+ def attribute_from_type(type, response)
583
+ type -= AttributeTypes[:multi] if is_multi = type > AttributeTypes[:multi]
584
+
585
+ case type
586
+ when AttributeTypes[:float]
587
+ is_multi ? response.next_float_array : response.next_float
588
+ else
589
+ is_multi ? response.next_int_array : response.next_int
590
+ end
591
+ end
592
+ end
593
+ end