dnif 0.0.1.beta.3 → 0.0.1.beta.4
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/.gitignore +4 -0
- data/.rspec +2 -0
- data/Gemfile +15 -0
- data/Gemfile.lock +52 -0
- data/Rakefile +24 -26
- data/dnif.gemspec +81 -55
- data/lib/dnif.rb +73 -24
- data/lib/dnif/configuration.rb +4 -3
- data/lib/dnif/document.rb +8 -8
- data/lib/dnif/indexer.rb +0 -2
- data/lib/dnif/schema.rb +1 -1
- data/lib/dnif/search.rb +1 -50
- data/lib/dnif/tasks.rb +11 -11
- data/lib/dnif/version.rb +10 -0
- data/spec/dnif/configuration_spec.rb +41 -0
- data/spec/dnif/dnif_spec.rb +157 -0
- data/spec/dnif/document_spec.rb +46 -0
- data/spec/dnif/index_spec.rb +20 -0
- data/spec/dnif/indexer_spec.rb +64 -0
- data/spec/dnif/multi_attribute_spec.rb +20 -0
- data/{test/unit/test_schema.rb → spec/dnif/schema_spec.rb} +5 -5
- data/spec/dnif/search_spec.rb +18 -0
- data/{test → spec}/fixtures/db/schema.rb +1 -0
- data/{test → spec}/fixtures/log/searchd.pid +0 -0
- data/{test → spec}/fixtures/models.rb +0 -0
- data/{test → spec}/fixtures/sphinx_1.xml +1 -0
- data/{test → spec}/fixtures/sphinx_2.xml +1 -0
- data/{test → spec}/fixtures/sphinx_3.xml +1 -0
- data/{test → spec}/fixtures/sphinx_4.xml +1 -0
- data/{test → spec}/fixtures/sphinx_5.xml +1 -0
- data/{test → spec}/fixtures/templates/config.erb +0 -0
- data/spec/spec_helper.rb +31 -0
- data/{test/test_helper.rb → spec/support/activerecord/models.rb} +2 -21
- data/templates/config.erb +38 -0
- metadata +158 -46
- data/lib/dnif/index_builder.rb +0 -30
- data/test/unit/test_configuration.rb +0 -37
- data/test/unit/test_dnif.rb +0 -21
- data/test/unit/test_document.rb +0 -41
- data/test/unit/test_index.rb +0 -21
- data/test/unit/test_index_builder.rb +0 -33
- data/test/unit/test_indexer.rb +0 -60
- data/test/unit/test_multi_attribute.rb +0 -17
- data/test/unit/test_search.rb +0 -116
data/lib/dnif/document.rb
CHANGED
@@ -13,7 +13,7 @@ module Dnif
|
|
13
13
|
(index.fields - fields).each do |name|
|
14
14
|
if @object.index.fields.include?(name)
|
15
15
|
xml.tag!(name) do
|
16
|
-
xml.cdata!(@object.send(name))
|
16
|
+
xml.cdata!(@object.send(name).to_s)
|
17
17
|
end
|
18
18
|
else
|
19
19
|
xml.tag!(name, "")
|
@@ -52,14 +52,14 @@ module Dnif
|
|
52
52
|
xml.target!
|
53
53
|
end
|
54
54
|
|
55
|
-
private
|
56
|
-
|
57
|
-
def encoded_class_name
|
58
|
-
@encoded_class_name ||= Dnif::MultiAttribute.encode(@object.class.name)
|
59
|
-
end
|
60
|
-
|
61
55
|
def document_id
|
62
56
|
@object.id + encoded_class_name.split(',').sum { |c| c.to_i }
|
63
57
|
end
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
def encoded_class_name
|
62
|
+
@encoded_class_name ||= Dnif::MultiAttribute.encode(@object.class.name)
|
63
|
+
end
|
64
64
|
end
|
65
|
-
end
|
65
|
+
end
|
data/lib/dnif/indexer.rb
CHANGED
data/lib/dnif/schema.rb
CHANGED
data/lib/dnif/search.rb
CHANGED
@@ -1,58 +1,9 @@
|
|
1
1
|
module Dnif
|
2
2
|
|
3
|
-
def self.search(query, options = {})
|
4
|
-
options.reverse_merge!(:index => '*')
|
5
|
-
|
6
|
-
if options[:classes]
|
7
|
-
ids = if options[:classes].is_a?(Array)
|
8
|
-
options[:classes].map { |name| ActiveRecord::Base.indexes.keys.index(name) }
|
9
|
-
else
|
10
|
-
[ActiveRecord::Base.indexes.keys.index(options[:classes])]
|
11
|
-
end
|
12
|
-
|
13
|
-
client.filters = [Riddle::Client::Filter.new("class_id", ids)]
|
14
|
-
end
|
15
|
-
|
16
|
-
results = client.query(query, options[:index])
|
17
|
-
raise results[:error] if results[:error]
|
18
|
-
|
19
|
-
models = results[:matches].inject({}) do |memo, match|
|
20
|
-
encoded_class_name = match[:attributes]["class_name"].split(',').flatten
|
21
|
-
class_name = Dnif::MultiAttribute.decode(encoded_class_name)
|
22
|
-
|
23
|
-
memo[class_name] ||= []
|
24
|
-
memo[class_name] << (match[:doc] - encoded_class_name.sum { |c| c.to_i })
|
25
|
-
memo
|
26
|
-
end
|
27
|
-
|
28
|
-
models.map do |class_name, ids|
|
29
|
-
class_name.constantize.find_all_by_id(ids)
|
30
|
-
end.flatten
|
31
|
-
end
|
32
|
-
|
33
|
-
def self.client
|
34
|
-
searchd = Dnif::Configuration.options_for("searchd", config_path)
|
35
|
-
if searchd["listen"]
|
36
|
-
address, port = searchd["listen"].split(":")
|
37
|
-
else
|
38
|
-
address = searchd["address"] || "127.0.0.1"
|
39
|
-
port = searchd["port"] || 3313
|
40
|
-
end
|
41
|
-
|
42
|
-
@client ||= Riddle::Client.new(address, port)
|
43
|
-
end
|
44
|
-
|
45
|
-
def self.config_path
|
46
|
-
Dnif.root_path ||= File.expand_path(File.dirname("."))
|
47
|
-
File.join(Dnif.root_path, "config/sphinx", Dnif.environment + ".erb")
|
48
|
-
end
|
49
|
-
|
50
3
|
module Search
|
51
4
|
|
52
5
|
def search(query)
|
53
6
|
Dnif.search(query, :classes => self.name)
|
54
7
|
end
|
55
8
|
end
|
56
|
-
end
|
57
|
-
|
58
|
-
ActiveRecord::Base.extend(Dnif::Search) if defined?(ActiveRecord::Base)
|
9
|
+
end
|
data/lib/dnif/tasks.rb
CHANGED
@@ -1,6 +1,11 @@
|
|
1
|
-
require
|
2
|
-
require
|
3
|
-
|
1
|
+
require "riddle"
|
2
|
+
require "fileutils"
|
3
|
+
|
4
|
+
if defined?(Rails)
|
5
|
+
Dnif.root_path = Rails.root
|
6
|
+
Dnif.environment = Rails.env
|
7
|
+
Dnif.models_path = File.join(Rails.root, "app", "models")
|
8
|
+
end
|
4
9
|
|
5
10
|
def controller
|
6
11
|
require 'riddle'
|
@@ -17,12 +22,6 @@ end
|
|
17
22
|
|
18
23
|
namespace :dnif do
|
19
24
|
|
20
|
-
task :environment do
|
21
|
-
if task = Rake::Task[:environment]
|
22
|
-
task.invoke
|
23
|
-
end
|
24
|
-
end
|
25
|
-
|
26
25
|
desc "Generates the configuration file needed for sphinx"
|
27
26
|
task :configure => :environment do
|
28
27
|
if Dnif.models_path.nil?
|
@@ -40,11 +39,12 @@ namespace :dnif do
|
|
40
39
|
|
41
40
|
base_path = File.join(config_path, Dnif.environment + ".erb")
|
42
41
|
if not File.exist?(base_path)
|
43
|
-
FileUtils.cp(File.dirname(__FILE__) + "/../../
|
42
|
+
FileUtils.cp(File.dirname(__FILE__) + "/../../templates/config.erb", base_path) # TODO change this path. find out how this kind of stuff is handle in others gems
|
44
43
|
end
|
45
44
|
|
46
45
|
Dnif.load_models
|
47
|
-
Dnif::Configuration.generate(base_path)
|
46
|
+
path = Dnif::Configuration.generate(base_path)
|
47
|
+
puts "Config file generated: #{path}"
|
48
48
|
end
|
49
49
|
|
50
50
|
desc "Generates the XML used by sphinx to create indexes"
|
data/lib/dnif/version.rb
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require 'spec_helper'
|
3
|
+
require 'erb'
|
4
|
+
|
5
|
+
describe Dnif::Configuration do
|
6
|
+
|
7
|
+
it "should generate configuration from default template" do
|
8
|
+
File.should_receive(:read).with("config/path.erb").and_return("configurations")
|
9
|
+
|
10
|
+
template = mock("Template")
|
11
|
+
template.should_receive(:result).and_return('output')
|
12
|
+
ERB.should_receive(:new).with("configurations").and_return(template)
|
13
|
+
|
14
|
+
file = mock
|
15
|
+
file.should_receive(:puts).with('output')
|
16
|
+
File.should_receive(:open).with(Dnif.root_path + "/config/sphinx/development.conf", "w").and_yield(file)
|
17
|
+
|
18
|
+
path = Dnif::Configuration.generate("config/path.erb")
|
19
|
+
path.should == File.join(Dnif.root_path, "config", "sphinx", "development.conf")
|
20
|
+
end
|
21
|
+
|
22
|
+
describe "helpers" do
|
23
|
+
|
24
|
+
it "should iterate over all indexed classes" do
|
25
|
+
names = []
|
26
|
+
classes = []
|
27
|
+
Dnif::Configuration.sources do |name, class_name|
|
28
|
+
names << name
|
29
|
+
classes << class_name
|
30
|
+
end
|
31
|
+
|
32
|
+
names.should == ["users_main", "people_main", "orders_main", "notes_main"]
|
33
|
+
classes.should == ["User", "Person", "Order", "Note"]
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
it ".options_for" do
|
38
|
+
client = Dnif::Configuration.options_for("searchd", File.join(Dnif.root_path, "templates", "config.erb"))
|
39
|
+
client.should be_kind_of(Hash)
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,157 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require 'spec_helper'
|
3
|
+
|
4
|
+
describe Dnif do
|
5
|
+
|
6
|
+
after(:each) do
|
7
|
+
Dnif.instance_variable_set(:"@root_path", nil)
|
8
|
+
Dnif.instance_variable_set(:"@environment", nil)
|
9
|
+
Dnif.instance_variable_set(:"@models_path", nil)
|
10
|
+
end
|
11
|
+
|
12
|
+
it ".root_path" do
|
13
|
+
Dnif.root_path = "/root/path"
|
14
|
+
Dnif.root_path.should == "/root/path"
|
15
|
+
end
|
16
|
+
|
17
|
+
it ".environment" do
|
18
|
+
Dnif.environment.should == "development"
|
19
|
+
|
20
|
+
Dnif.environment = "production"
|
21
|
+
Dnif.environment.should == "production"
|
22
|
+
end
|
23
|
+
|
24
|
+
it "should have .environment aliased by .env" do
|
25
|
+
Dnif.env.should == "development"
|
26
|
+
|
27
|
+
Dnif.env = "production"
|
28
|
+
Dnif.env.should == "production"
|
29
|
+
end
|
30
|
+
|
31
|
+
it ".models_path" do
|
32
|
+
Dnif.models_path = "/models/path"
|
33
|
+
Dnif.models_path.should == "/models/path"
|
34
|
+
end
|
35
|
+
|
36
|
+
describe ".client" do
|
37
|
+
|
38
|
+
it "should understand 'listen' sphinx setting" do
|
39
|
+
Dnif.should_receive(:config_path).and_return("config/path.erb")
|
40
|
+
Dnif::Configuration.should_receive(:options_for).with("searchd", "config/path.erb").and_return({ "listen" => "127.0.0.1:3313" })
|
41
|
+
Riddle::Client.should_receive(:new).with("127.0.0.1", "3313")
|
42
|
+
|
43
|
+
Dnif.client
|
44
|
+
end
|
45
|
+
|
46
|
+
it "should understand 'address' and 'port' sphinx setting" do
|
47
|
+
Dnif.should_receive(:config_path).and_return("config/path.erb")
|
48
|
+
Dnif::Configuration.should_receive(:options_for).with("searchd", "config/path.erb").and_return({ "address" => "127.0.0.1", "port" => "3313" })
|
49
|
+
Riddle::Client.should_receive(:new).with("127.0.0.1", "3313")
|
50
|
+
|
51
|
+
Dnif.client
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
describe ".search" do
|
56
|
+
|
57
|
+
it "should search for all models" do
|
58
|
+
results = {
|
59
|
+
:matches => [{
|
60
|
+
:doc => 2983,
|
61
|
+
:attributes => {
|
62
|
+
"class_name" => "336,623,883,1140"
|
63
|
+
}
|
64
|
+
}, {
|
65
|
+
:doc => 7893,
|
66
|
+
:attributes => {
|
67
|
+
"class_name" => "323,623,877,1133,1381,1646,1908"
|
68
|
+
}
|
69
|
+
}]
|
70
|
+
}
|
71
|
+
|
72
|
+
riddle = mock("Riddle")
|
73
|
+
riddle.should_receive(:query).with("my search", "*").and_return(results)
|
74
|
+
Dnif.should_receive(:client).and_return(riddle)
|
75
|
+
Post.should_receive(:find_all_by_id).once.with([1])
|
76
|
+
Comment.should_receive(:find_all_by_id).once.with([2])
|
77
|
+
|
78
|
+
Dnif.search("my search")
|
79
|
+
end
|
80
|
+
|
81
|
+
it "should search specific models" do
|
82
|
+
riddle = mock("Riddle")
|
83
|
+
riddle.should_receive(:query).with("post", "*").and_return(results_for_post)
|
84
|
+
riddle.should_receive(:query).with("comment", "*").and_return(results_for_comment)
|
85
|
+
riddle.should_receive(:filters=).twice.and_return([])
|
86
|
+
Dnif.should_receive(:client).exactly(4).times.and_return(riddle)
|
87
|
+
|
88
|
+
ActiveRecord::Base.should_receive(:indexes).twice.and_return({ "Post" => mock, "Comment" => mock })
|
89
|
+
|
90
|
+
Riddle::Client::Filter.should_receive(:new).with("class_id", [0])
|
91
|
+
Riddle::Client::Filter.should_receive(:new).with("class_id", [1])
|
92
|
+
|
93
|
+
Post.should_receive(:find_all_by_id).once.with([1])
|
94
|
+
Comment.should_receive(:find_all_by_id).once.with([2])
|
95
|
+
|
96
|
+
Post.search("post")
|
97
|
+
Comment.search("comment")
|
98
|
+
end
|
99
|
+
|
100
|
+
it "should search only specified models" do
|
101
|
+
riddle = mock("Riddle")
|
102
|
+
riddle.should_receive(:query).with("post", "*").and_return(results_for_post)
|
103
|
+
riddle.should_receive(:filters=).and_return([])
|
104
|
+
Dnif.should_receive(:client).exactly(2).times.and_return(riddle)
|
105
|
+
|
106
|
+
ActiveRecord::Base.should_receive(:indexes).and_return({ "Post" => mock })
|
107
|
+
|
108
|
+
Riddle::Client::Filter.should_receive(:new).with("class_id", [0])
|
109
|
+
Post.should_receive(:find_all_by_id).once.with([1])
|
110
|
+
|
111
|
+
Dnif.search("post", :classes => "Post")
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
describe ".config_path" do
|
116
|
+
|
117
|
+
it "should return '.' when not specified" do
|
118
|
+
File.should_receive(:expand_path).and_return("/expanded/path")
|
119
|
+
Dnif.should_receive(:root_path).twice.and_return(nil, ".")
|
120
|
+
Dnif.should_receive(:root_path=).with("/expanded/path")
|
121
|
+
Dnif.should_receive(:environment).and_return("development")
|
122
|
+
|
123
|
+
Dnif.config_path.should == "./config/sphinx/development.erb"
|
124
|
+
end
|
125
|
+
|
126
|
+
it "should return specified root_path" do
|
127
|
+
Dnif.should_receive(:root_path).twice.and_return("/root/path", "/root/path")
|
128
|
+
Dnif.should_receive(:environment).and_return("development")
|
129
|
+
|
130
|
+
Dnif.config_path.should == "/root/path/config/sphinx/development.erb"
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
private
|
135
|
+
|
136
|
+
def results_for_post
|
137
|
+
{
|
138
|
+
:matches => [{
|
139
|
+
:doc => 2983,
|
140
|
+
:attributes => {
|
141
|
+
"class_name" => "336,623,883,1140"
|
142
|
+
}
|
143
|
+
}]
|
144
|
+
}
|
145
|
+
end
|
146
|
+
|
147
|
+
def results_for_comment
|
148
|
+
{
|
149
|
+
:matches => [{
|
150
|
+
:doc => 7893,
|
151
|
+
:attributes => {
|
152
|
+
"class_name" => "323,623,877,1133,1381,1646,1908"
|
153
|
+
}
|
154
|
+
}]
|
155
|
+
}
|
156
|
+
end
|
157
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require 'spec_helper'
|
3
|
+
|
4
|
+
describe Dnif::Document do
|
5
|
+
|
6
|
+
describe "#generate" do
|
7
|
+
|
8
|
+
it "should generate a single document" do
|
9
|
+
Dnif::MultiAttribute.should_receive(:encode).with("User").and_return("1,2,3,4")
|
10
|
+
|
11
|
+
user = User.create!(:name => "Rafael Souza", :active => true, :weight => 34.5)
|
12
|
+
|
13
|
+
document = Dnif::Document.new(user)
|
14
|
+
expected = File.read(File.dirname(__FILE__) + "/../fixtures/sphinx_5.xml")
|
15
|
+
document.generate.should == expected
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
describe "#document_id" do
|
20
|
+
|
21
|
+
it "should calculate document id for sphinx" do
|
22
|
+
user = User.create!(:name => "Rafael Souza", :active => true)
|
23
|
+
|
24
|
+
document = Dnif::Document.new(user)
|
25
|
+
document.document_id.should == user.id + [ 341, 627, 869, 1138].sum
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
it "should convert date/datetime values to timestamp" do
|
30
|
+
now = DateTime.now
|
31
|
+
now.should_receive(:to_i)
|
32
|
+
expire = Date.today + 2.days
|
33
|
+
expire.should_receive(:to_i)
|
34
|
+
expire.should_receive(:to_datetime).and_return(expire)
|
35
|
+
|
36
|
+
note = Note.create!(
|
37
|
+
:title => "Note Title",
|
38
|
+
:clicked => 10,
|
39
|
+
:published_at => now,
|
40
|
+
:expire_at => expire,
|
41
|
+
:active => true,
|
42
|
+
:points => 1000
|
43
|
+
)
|
44
|
+
note.to_sphinx
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require 'spec_helper'
|
3
|
+
|
4
|
+
describe Dnif::Index do
|
5
|
+
|
6
|
+
it "should define a field" do
|
7
|
+
index = Dnif::Index.new(&proc { field :name })
|
8
|
+
index.fields.should == [ :name ]
|
9
|
+
end
|
10
|
+
|
11
|
+
it "should define an attribute" do
|
12
|
+
index = Dnif::Index.new(&proc { attribute :another, :type => :bool })
|
13
|
+
index.attributes.should == { :another => :bool }
|
14
|
+
end
|
15
|
+
|
16
|
+
it "should define where clause" do
|
17
|
+
index = Dnif::Index.new(&proc { where "field = 'value'" })
|
18
|
+
index.conditions.should == "field = 'value'"
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require 'spec_helper'
|
3
|
+
|
4
|
+
describe Dnif::Indexer do
|
5
|
+
|
6
|
+
describe ".define_index" do
|
7
|
+
|
8
|
+
it ".define_index" do
|
9
|
+
klass = Class.new
|
10
|
+
klass.extend(Dnif::Indexer)
|
11
|
+
klass.should_receive(:name).and_return("Klass")
|
12
|
+
klass.define_index do
|
13
|
+
field :a
|
14
|
+
attribute :b, :type => :integer
|
15
|
+
where "c"
|
16
|
+
end
|
17
|
+
|
18
|
+
klass.indexes["Klass"].fields.should == [ :a ]
|
19
|
+
klass.indexes["Klass"].attributes.should == { :b => :integer }
|
20
|
+
klass.indexes["Klass"].conditions.should == "c"
|
21
|
+
|
22
|
+
klass.indexes.delete("Klass")
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
it "should not include Index if there is no index definition inside the class" do
|
27
|
+
Post.new.respond_to?(:to_sphinx).should be_false
|
28
|
+
Post.respond_to?(:to_sphinx).should be_false
|
29
|
+
end
|
30
|
+
|
31
|
+
describe "#to_sphinx" do
|
32
|
+
|
33
|
+
it "should return a sphinx document including defined fields" do
|
34
|
+
comment = Person.create!(:first_name => "Rafael", :last_name => "Souza")
|
35
|
+
|
36
|
+
expected = File.read(File.dirname(__FILE__) + "/../fixtures/sphinx_3.xml")
|
37
|
+
comment.to_sphinx.should == expected
|
38
|
+
end
|
39
|
+
|
40
|
+
it "should return a sphinx document including defined attributes" do
|
41
|
+
note = Note.create!(:id => 1, :title => "Note Title", :clicked => 10, :published_at => (now = DateTime.now), :expire_at => (expire = Date.today + 2.days), :active => true, :points => 1000)
|
42
|
+
|
43
|
+
expected = File.read(File.dirname(__FILE__) + "/../fixtures/sphinx_2.xml")
|
44
|
+
note.to_sphinx.should == expected.gsub("{now}", now.to_i.to_s).gsub("{expire}", expire.to_datetime.to_i.to_s)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
describe ".to_sphinx" do
|
49
|
+
|
50
|
+
it "should generate a full sphinx xml" do
|
51
|
+
comment = Person.create!(:first_name => "Rafael", :last_name => "Souza")
|
52
|
+
|
53
|
+
expected = File.read(File.dirname(__FILE__) + "/../fixtures/sphinx_1.xml")
|
54
|
+
Person.to_sphinx.should == expected.gsub("{comment}", comment.to_sphinx)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
describe ".indexes" do
|
59
|
+
|
60
|
+
it "should return all indexed classes" do
|
61
|
+
ActiveRecord::Base.indexes.keys.should == ["User", "Person", "Order", "Note"]
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|