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.
- data/README.rdoc +200 -0
- data/Rakefile +49 -0
- data/VERSION.yml +4 -0
- data/lib/dm-sphinx-adapter.rb +23 -0
- data/lib/dm-sphinx-adapter/adapter.rb +200 -0
- data/lib/dm-sphinx-adapter/attribute.rb +83 -0
- data/lib/dm-sphinx-adapter/collection.rb +19 -0
- data/lib/dm-sphinx-adapter/index.rb +38 -0
- data/lib/dm-sphinx-adapter/query.rb +68 -0
- data/lib/dm-sphinx-adapter/resource.rb +120 -0
- data/lib/dm-sphinx-adapter/xmlpipe2.rb +94 -0
- data/lib/riddle.rb +28 -0
- data/lib/riddle/client.rb +619 -0
- data/lib/riddle/client/filter.rb +53 -0
- data/lib/riddle/client/message.rb +65 -0
- data/lib/riddle/client/response.rb +84 -0
- data/test/files/model.rb +24 -0
- data/test/files/source.xml +39 -0
- data/test/files/sphinx.conf +36 -0
- data/test/files/test_xmlpipe2.xml +1 -0
- data/test/helper.rb +46 -0
- data/test/test_adapter.rb +68 -0
- data/test/test_attribute.rb +36 -0
- data/test/test_collection.rb +26 -0
- data/test/test_index.rb +30 -0
- data/test/test_query.rb +61 -0
- data/test/test_resource.rb +17 -0
- data/test/test_xmlpipe2.rb +77 -0
- metadata +97 -0
@@ -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
|
data/test/files/model.rb
ADDED
@@ -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>
|
data/test/helper.rb
ADDED
@@ -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
|