bdimcheff-dm-sphinx-adapter 0.8.0

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.
@@ -0,0 +1,53 @@
1
+ module Riddle
2
+ class Client
3
+ # Used for querying Sphinx.
4
+ class Filter
5
+ attr_accessor :attribute, :values, :exclude
6
+
7
+ # Attribute name, values (which can be an array or a range), and whether
8
+ # the filter should be exclusive.
9
+ def initialize(attribute, values, exclude=false)
10
+ @attribute, @values, @exclude = attribute, values, exclude
11
+ end
12
+
13
+ def exclude?
14
+ self.exclude
15
+ end
16
+
17
+ # Returns the message for this filter to send to the Sphinx service
18
+ def query_message
19
+ message = Message.new
20
+
21
+ message.append_string self.attribute.to_s
22
+ case self.values
23
+ when Range
24
+ if self.values.first.is_a?(Float) && self.values.last.is_a?(Float)
25
+ message.append_int FilterTypes[:float_range]
26
+ message.append_floats self.values.first, self.values.last
27
+ else
28
+ message.append_int FilterTypes[:range]
29
+ message.append_ints self.values.first, self.values.last
30
+ end
31
+ when Array
32
+ message.append_int FilterTypes[:values]
33
+ message.append_int self.values.length
34
+ # using to_f is a hack from the php client - to workaround 32bit
35
+ # signed ints on x32 platforms
36
+ message.append_ints *self.values.collect { |val|
37
+ case val
38
+ when TrueClass
39
+ 1.0
40
+ when FalseClass
41
+ 0.0
42
+ else
43
+ val.to_f
44
+ end
45
+ }
46
+ end
47
+ message.append_int self.exclude? ? 1 : 0
48
+
49
+ message.to_s
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,65 @@
1
+ module Riddle
2
+ class Client
3
+ # This class takes care of the translation of ints, strings and arrays to
4
+ # the format required by the Sphinx service.
5
+ class Message
6
+ def initialize
7
+ @message = ""
8
+ @size_method = @message.respond_to?(:bytesize) ? :bytesize : :length
9
+ end
10
+
11
+ # Append raw data (only use if you know what you're doing)
12
+ def append(*args)
13
+ return if args.length == 0
14
+
15
+ args.each { |arg| @message << arg }
16
+ end
17
+
18
+ # Append a string's length, then the string itself
19
+ def append_string(str)
20
+ @message << [str.send(@size_method)].pack('N') + str
21
+ end
22
+
23
+ # Append an integer
24
+ def append_int(int)
25
+ @message << [int].pack('N')
26
+ end
27
+
28
+ def append_64bit_int(int)
29
+ @message << [int >> 32, int & 0xFFFFFFFF].pack('NN')
30
+ end
31
+
32
+ # Append a float
33
+ def append_float(float)
34
+ @message << [float].pack('f').unpack('L*').pack("N")
35
+ end
36
+
37
+ # Append multiple integers
38
+ def append_ints(*ints)
39
+ ints.each { |int| append_int(int) }
40
+ end
41
+
42
+ def append_64bit_ints(*ints)
43
+ ints.each { |int| append_64bit_int(int) }
44
+ end
45
+
46
+ # Append multiple floats
47
+ def append_floats(*floats)
48
+ floats.each { |float| append_float(float) }
49
+ end
50
+
51
+ # Append an array of strings - first appends the length of the array,
52
+ # then each item's length and value.
53
+ def append_array(array)
54
+ append_int(array.length)
55
+
56
+ array.each { |item| append_string(item) }
57
+ end
58
+
59
+ # Returns the entire message
60
+ def to_s
61
+ @message
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,84 @@
1
+ module Riddle
2
+ class Client
3
+ # Used to interrogate responses from the Sphinx daemon. Keep in mind none
4
+ # of the methods here check whether the data they're grabbing are what the
5
+ # user expects - it just assumes the user knows what the data stream is
6
+ # made up of.
7
+ class Response
8
+ # Create with the data to interpret
9
+ def initialize(str)
10
+ @str = str
11
+ @marker = 0
12
+ end
13
+
14
+ # Return the next string value in the stream
15
+ def next
16
+ len = next_int
17
+ result = @str[@marker, len]
18
+ @marker += len
19
+
20
+ return result
21
+ end
22
+
23
+ # Return the next integer value from the stream
24
+ def next_int
25
+ int = @str[@marker, 4].unpack('N*').first
26
+ @marker += 4
27
+
28
+ return int
29
+ end
30
+
31
+ def next_64bit_int
32
+ high, low = @str[@marker, 8].unpack('N*N*')[0..1]
33
+ @marker += 8
34
+
35
+ return (high << 32) + low
36
+ end
37
+
38
+ # Return the next float value from the stream
39
+ def next_float
40
+ float = @str[@marker, 4].unpack('N*').pack('L').unpack('f*').first
41
+ @marker += 4
42
+
43
+ return float
44
+ end
45
+
46
+ # Returns an array of string items
47
+ def next_array
48
+ count = next_int
49
+ items = []
50
+ for i in 0...count
51
+ items << self.next
52
+ end
53
+
54
+ return items
55
+ end
56
+
57
+ # Returns an array of int items
58
+ def next_int_array
59
+ count = next_int
60
+ items = []
61
+ for i in 0...count
62
+ items << self.next_int
63
+ end
64
+
65
+ return items
66
+ end
67
+
68
+ def next_float_array
69
+ count = next_int
70
+ items = []
71
+ for i in 0...count
72
+ items << self.next_float
73
+ end
74
+
75
+ return items
76
+ end
77
+
78
+ # Returns the length of the streamed data
79
+ def length
80
+ @str.length
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,24 @@
1
+ class Item
2
+ include DataMapper::Resource
3
+ include DataMapper::SphinxResource
4
+ property :id, Serial
5
+ property :t_string, String
6
+ property :t_text, Text, :lazy => false
7
+ property :t_decimal, BigDecimal
8
+ property :t_float, Float
9
+ property :t_integer, Integer
10
+ property :t_datetime, DateTime
11
+
12
+ repository(:search) do
13
+ properties(:search).clear
14
+ property :id, Serial
15
+ property :t_string, String
16
+
17
+ attribute :t_text, Text, :lazy => false
18
+ attribute :t_decimal, BigDecimal
19
+ attribute :t_float, Float
20
+ attribute :t_integer, Integer
21
+ attribute :t_datetime, DateTime
22
+ end
23
+ end # Item
24
+
@@ -0,0 +1,39 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <sphinx:docset xmlns:sphinx="sphinx">
3
+ <sphinx:schema>
4
+ <sphinx:field name="t_string"/>
5
+ <sphinx:field name="t_text"/>
6
+ <sphinx:attr name="t_decimal" type="float"/>
7
+ <sphinx:attr name="t_float" type="float"/>
8
+ <sphinx:attr name="t_integer" type="int"/>
9
+ <sphinx:attr name="t_datetime" type="timestamp"/>
10
+ </sphinx:schema>
11
+
12
+ <sphinx:document id="1">
13
+ <t_string><![CDATA[one]]></t_string>
14
+ <t_text><![CDATA[text one!]]></t_text>
15
+ <t_decimal><![CDATA[10.5]]></t_decimal>
16
+ <t_float><![CDATA[100.5]]></t_float>
17
+ <t_integer><![CDATA[1000]]></t_integer>
18
+ <t_datetime><![CDATA[1235183682]]></t_datetime>
19
+ </sphinx:document>
20
+
21
+ <sphinx:document id="2">
22
+ <t_string><![CDATA[two]]></t_string>
23
+ <t_text><![CDATA[text two!]]></t_text>
24
+ <t_decimal><![CDATA[20.5]]></t_decimal>
25
+ <t_float><![CDATA[200.5]]></t_float>
26
+ <t_integer><![CDATA[2000]]></t_integer>
27
+ <t_datetime><![CDATA[1235183682]]></t_datetime>
28
+ </sphinx:document>
29
+
30
+ <sphinx:document id="3">
31
+ <t_string><![CDATA[three]]></t_string>
32
+ <t_text><![CDATA[text three!]]></t_text>
33
+ <t_decimal><![CDATA[30.5]]></t_decimal>
34
+ <t_float><![CDATA[300.5]]></t_float>
35
+ <t_integer><![CDATA[3000]]></t_integer>
36
+ <t_datetime><![CDATA[1235183682]]></t_datetime>
37
+ </sphinx:document>
38
+ </sphinx:docset>
39
+
@@ -0,0 +1,36 @@
1
+ # searchd and indexer must be run from the root directory of this lib.
2
+
3
+ indexer
4
+ {
5
+ mem_limit = 64M
6
+ }
7
+
8
+ searchd
9
+ {
10
+ address = localhost
11
+ port = 3312
12
+ log = test/files/tmp/sphinx.log
13
+ query_log = test/files/tmp/sphinx.query.log
14
+ read_timeout = 5
15
+ pid_file = test/files/tmp/sphinx.pid
16
+ max_matches = 1000
17
+ }
18
+
19
+ source items
20
+ {
21
+ type = xmlpipe2
22
+ xmlpipe_command = cat test/files/source.xml
23
+ }
24
+
25
+ index items_main
26
+ {
27
+ source = items
28
+ charset_type = utf-8
29
+ path = test/files/tmp/items
30
+ }
31
+
32
+ index items
33
+ {
34
+ type = distributed
35
+ local = items_main
36
+ }
@@ -0,0 +1 @@
1
+ <?xml version="1.0" encoding="UTF-8"?><sphinx:docset xmlns:sphinx="sphinx"><sphinx:schema><sphinx:field name="id"/><sphinx:field name="t_string"/><sphinx:attr type="str2ordinal" name="t_text"/><sphinx:attr type="float" name="t_decimal"/><sphinx:attr type="float" name="t_float"/><sphinx:attr type="int" name="t_integer"/><sphinx:attr type="timestamp" name="t_datetime"/></sphinx:schema><sphinx:document id="1"><id><![CDATA[1]]></id><t_string><![CDATA[one]]></t_string><t_text><![CDATA[text one!]]></t_text><t_decimal><![CDATA[0.01]]></t_decimal><t_float><![CDATA[0.0001]]></t_float><t_integer><![CDATA[1]]></t_integer><t_datetime><![CDATA[1235914716]]></t_datetime></sphinx:document></sphinx:docset>
@@ -0,0 +1,46 @@
1
+ $VERBOSE = false # Shitloads of warnings in dm :(
2
+ require 'rubygems'
3
+ require 'extlib'
4
+ require 'extlib/hook'
5
+ require 'pathname'
6
+ require 'test/unit'
7
+ require 'shoulda'
8
+
9
+ base = Pathname.new(__FILE__).dirname + '..'
10
+ files = base + 'test' + 'files'
11
+ %w{lib test}.each{|p| $:.unshift base + p}
12
+
13
+ require 'dm-sphinx-adapter'
14
+
15
+ # Sphinx runner.
16
+ Dir.chdir(base)
17
+ begin
18
+ TCPSocket.new('localhost', '3312')
19
+ rescue
20
+ puts 'Starting Sphinx...'
21
+ system("searchd --config #{files + 'sphinx.conf'}") || exit
22
+ system('ps aux | grep searchd')
23
+ end
24
+
25
+ indexer = `indexer --config #{files + 'sphinx.conf'} --all --rotate`
26
+ raise %{Re-create index failed:\n #{indexer}} if indexer =~ /error|fatal/i
27
+ sleep 1
28
+
29
+ # :default is unused at the moment.
30
+ DataMapper.setup(:default, :adapter => 'in_memory', :database => 'dm_sphinx_adapter_test')
31
+ DataMapper.setup(:search, :adapter => 'sphinx')
32
+
33
+ class Test::Unit::TestCase
34
+ include Extlib::Hook
35
+
36
+ # after :teardown do
37
+ def teardown
38
+ descendants = DataMapper::Resource.descendants.dup.to_a
39
+ while model = descendants.shift
40
+ descendants.concat(model.descendants) if model.respond_to?(:descendants)
41
+ Object.send(:remove_const, model.name.to_sym)
42
+ DataMapper::Resource.descendants.delete(model)
43
+ end
44
+ end
45
+ end
46
+
@@ -0,0 +1,68 @@
1
+ require File.join(File.dirname(__FILE__), 'helper')
2
+
3
+ class TestAdapter < Test::Unit::TestCase
4
+ context 'DM::A::Sphinx::Adapter' do
5
+ setup do
6
+ load File.join(File.dirname(__FILE__), 'files', 'model.rb')
7
+ @it = repository(:search)
8
+ @resource = Item
9
+ end
10
+
11
+ context 'class' do
12
+ should 'use default field naming convention' do
13
+ assert_equal(
14
+ DataMapper::NamingConventions::Field::Underscored,
15
+ @it.adapter.field_naming_convention
16
+ )
17
+ end
18
+
19
+ should 'use default resource naming convention' do
20
+ assert_equal(
21
+ DataMapper::NamingConventions::Resource::UnderscoredAndPluralized,
22
+ @it.adapter.resource_naming_convention
23
+ )
24
+ end
25
+ end
26
+
27
+ context '#read_many' do
28
+ context 'conditions' do
29
+ should 'return all objects when nil' do
30
+ assert_equal [1, 2, 3], @it.read_many(query).map{|d| d[:id]}
31
+ end
32
+
33
+ should 'return subset of objects for conditions' do
34
+ assert_equal [2], @it.read_many(query(:t_string => 'two')).map{|d| d[:id]}
35
+ end
36
+ end
37
+
38
+ context 'offsets' do
39
+ should 'be able to offset the objects' do
40
+ assert_equal [1, 2, 3], @it.read_many(query(:offset => 0)).map{|d| d[:id]}
41
+ assert_equal [2, 3], @it.read_many(query(:offset => 1)).map{|d| d[:id]}
42
+ assert_equal [], @it.read_many(query(:offset => 3))
43
+ end
44
+ end
45
+
46
+ context 'limits' do
47
+ should 'be able to limit the objects' do
48
+ assert_equal [1], @it.read_many(query(:limit => 1)).map{|d| d[:id]}
49
+ assert_equal [1, 2], @it.read_many(query(:limit => 2)).map{|d| d[:id]}
50
+ end
51
+ end
52
+ end
53
+
54
+ context '#read_one' do
55
+ should 'return the first object of a #read_many' do
56
+ assert_equal @it.read_many(query).first, @it.read_one(query)
57
+
58
+ query = query(:t_string => 'two')
59
+ assert_equal @it.read_many(query).first, @it.read_one(query)
60
+ end
61
+ end
62
+ end
63
+
64
+ protected
65
+ def query(conditions = {})
66
+ DataMapper::Query.new(repository(:search), @resource, conditions)
67
+ end
68
+ end
@@ -0,0 +1,36 @@
1
+ require File.join(File.dirname(__FILE__), 'helper')
2
+
3
+ class TestAttribute < Test::Unit::TestCase
4
+ context 'DM::A::Sphinx::Attribute instance' do
5
+ should 'typecast DateTime to Integer'
6
+ should 'typecast Date to Integer'
7
+ should 'typecast Time to Integer'
8
+ should 'typecast BigDecimal to Float'
9
+ end
10
+
11
+ context 'DM::A::Sphinx::Resource#attribute class method' do
12
+ setup do
13
+ class ::Resource
14
+ include DataMapper::SphinxResource
15
+ end
16
+ end
17
+
18
+ DataMapper::Adapters::Sphinx::Attribute::TYPES.each do |type|
19
+ should "accept a #{type} type" do
20
+ assert_nothing_raised do
21
+ Resource.class_eval do
22
+ attribute :name, type
23
+ end
24
+ end
25
+ end
26
+ end
27
+
28
+ should 'raise ArgumentError for unsupported type' do
29
+ assert_raise(ArgumentError) do
30
+ Resource.class_eval do
31
+ attribute :name, Test::Unit::TestCase
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end