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.
Files changed (44) hide show
  1. data/.gitignore +4 -0
  2. data/.rspec +2 -0
  3. data/Gemfile +15 -0
  4. data/Gemfile.lock +52 -0
  5. data/Rakefile +24 -26
  6. data/dnif.gemspec +81 -55
  7. data/lib/dnif.rb +73 -24
  8. data/lib/dnif/configuration.rb +4 -3
  9. data/lib/dnif/document.rb +8 -8
  10. data/lib/dnif/indexer.rb +0 -2
  11. data/lib/dnif/schema.rb +1 -1
  12. data/lib/dnif/search.rb +1 -50
  13. data/lib/dnif/tasks.rb +11 -11
  14. data/lib/dnif/version.rb +10 -0
  15. data/spec/dnif/configuration_spec.rb +41 -0
  16. data/spec/dnif/dnif_spec.rb +157 -0
  17. data/spec/dnif/document_spec.rb +46 -0
  18. data/spec/dnif/index_spec.rb +20 -0
  19. data/spec/dnif/indexer_spec.rb +64 -0
  20. data/spec/dnif/multi_attribute_spec.rb +20 -0
  21. data/{test/unit/test_schema.rb → spec/dnif/schema_spec.rb} +5 -5
  22. data/spec/dnif/search_spec.rb +18 -0
  23. data/{test → spec}/fixtures/db/schema.rb +1 -0
  24. data/{test → spec}/fixtures/log/searchd.pid +0 -0
  25. data/{test → spec}/fixtures/models.rb +0 -0
  26. data/{test → spec}/fixtures/sphinx_1.xml +1 -0
  27. data/{test → spec}/fixtures/sphinx_2.xml +1 -0
  28. data/{test → spec}/fixtures/sphinx_3.xml +1 -0
  29. data/{test → spec}/fixtures/sphinx_4.xml +1 -0
  30. data/{test → spec}/fixtures/sphinx_5.xml +1 -0
  31. data/{test → spec}/fixtures/templates/config.erb +0 -0
  32. data/spec/spec_helper.rb +31 -0
  33. data/{test/test_helper.rb → spec/support/activerecord/models.rb} +2 -21
  34. data/templates/config.erb +38 -0
  35. metadata +158 -46
  36. data/lib/dnif/index_builder.rb +0 -30
  37. data/test/unit/test_configuration.rb +0 -37
  38. data/test/unit/test_dnif.rb +0 -21
  39. data/test/unit/test_document.rb +0 -41
  40. data/test/unit/test_index.rb +0 -21
  41. data/test/unit/test_index_builder.rb +0 -33
  42. data/test/unit/test_indexer.rb +0 -60
  43. data/test/unit/test_multi_attribute.rb +0 -17
  44. data/test/unit/test_search.rb +0 -116
@@ -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
@@ -48,5 +48,3 @@ module Dnif
48
48
  end
49
49
  end
50
50
  end
51
-
52
- ActiveRecord::Base.extend(Dnif::Indexer) if defined?(ActiveRecord::Base)
@@ -42,7 +42,7 @@ module Dnif
42
42
  "timestamp"
43
43
  when :boolean
44
44
  "bool"
45
- when :float
45
+ when :float, :decimal
46
46
  "float"
47
47
  end
48
48
  end
@@ -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
@@ -1,6 +1,11 @@
1
- require 'active_support'
2
- require 'fileutils'
3
- require 'dnif'
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__) + "/../../test/fixtures/templates/config.erb", base_path) # TODO change this path. find out how this kind of stuff is handle in others gems
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"
@@ -0,0 +1,10 @@
1
+ module Dnif
2
+ module Version
3
+ MAJOR = 0
4
+ MINOR = 0
5
+ PATCH = 1
6
+ BUILD = "beta.4"
7
+
8
+ STRING = [MAJOR, MINOR, PATCH, BUILD].compact.join('.')
9
+ end
10
+ end
@@ -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