xapit 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (57) hide show
  1. data/CHANGELOG +27 -0
  2. data/Manifest +16 -1
  3. data/README.rdoc +29 -15
  4. data/Rakefile +1 -1
  5. data/features/facets.feature +40 -1
  6. data/features/finding.feature +15 -59
  7. data/features/sorting.feature +29 -0
  8. data/features/step_definitions/xapit_steps.rb +11 -3
  9. data/features/suggestions.feature +17 -0
  10. data/features/support/xapit_helpers.rb +1 -1
  11. data/install.rb +7 -8
  12. data/lib/xapit.rb +34 -2
  13. data/lib/xapit/adapters/abstract_adapter.rb +46 -0
  14. data/lib/xapit/adapters/active_record_adapter.rb +20 -0
  15. data/lib/xapit/adapters/data_mapper_adapter.rb +10 -0
  16. data/lib/xapit/collection.rb +17 -5
  17. data/lib/xapit/config.rb +1 -9
  18. data/lib/xapit/facet.rb +11 -8
  19. data/lib/xapit/index_blueprint.rb +9 -3
  20. data/lib/xapit/indexers/abstract_indexer.rb +13 -2
  21. data/lib/xapit/indexers/classic_indexer.rb +5 -3
  22. data/lib/xapit/indexers/simple_indexer.rb +15 -8
  23. data/lib/xapit/membership.rb +19 -1
  24. data/lib/xapit/query.rb +40 -15
  25. data/lib/xapit/query_parsers/abstract_query_parser.rb +46 -24
  26. data/lib/xapit/rake_tasks.rb +13 -0
  27. data/rails_generators/xapit/USAGE +13 -0
  28. data/rails_generators/xapit/templates/setup_xapit.rb +1 -0
  29. data/rails_generators/xapit/templates/xapit.rake +4 -0
  30. data/rails_generators/xapit/xapit_generator.rb +20 -0
  31. data/spec/spec_helper.rb +2 -2
  32. data/spec/xapit/adapters/active_record_adapter_spec.rb +31 -0
  33. data/spec/xapit/adapters/data_mapper_adapter_spec.rb +10 -0
  34. data/spec/xapit/facet_option_spec.rb +2 -2
  35. data/spec/xapit/index_blueprint_spec.rb +11 -3
  36. data/spec/xapit/indexers/abstract_indexer_spec.rb +37 -0
  37. data/spec/xapit/indexers/classic_indexer_spec.rb +9 -0
  38. data/spec/xapit/indexers/simple_indexer_spec.rb +22 -6
  39. data/spec/xapit/membership_spec.rb +16 -0
  40. data/spec/xapit/query_parsers/abstract_query_parser_spec.rb +21 -3
  41. data/spec/xapit/query_spec.rb +21 -0
  42. data/spec/xapit_member.rb +13 -2
  43. data/tasks/xapit.rake +1 -9
  44. data/tmp/xapiandatabase/postlist.DB +0 -0
  45. data/tmp/xapiandatabase/postlist.baseB +0 -0
  46. data/tmp/xapiandatabase/record.DB +0 -0
  47. data/tmp/xapiandatabase/record.baseB +0 -0
  48. data/tmp/xapiandatabase/spelling.DB +0 -0
  49. data/tmp/xapiandatabase/spelling.baseB +0 -0
  50. data/tmp/xapiandatabase/termlist.DB +0 -0
  51. data/tmp/xapiandatabase/termlist.baseB +0 -0
  52. data/tmp/xapiandatabase/value.DB +0 -0
  53. data/tmp/xapiandatabase/value.baseA +0 -0
  54. data/tmp/xapiandb/spelling.DB +0 -0
  55. data/tmp/xapiandb/spelling.baseB +0 -0
  56. data/xapit.gemspec +4 -4
  57. metadata +23 -3
@@ -0,0 +1,13 @@
1
+ # TODO investigate why this is needed to ensure it doesn't load twice
2
+ unless @xapit_rake_loaded
3
+ @xapit_rake_loaded = true
4
+ namespace :xapit do
5
+ desc "Index all xapit models."
6
+ task :index => :environment do
7
+ Xapit.remove_database
8
+ Xapit.index_all do |member_class|
9
+ puts "Indexing #{member_class.name}"
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ Description:
2
+ Generates files necessary to setup Xapit. This includes an initializer
3
+ to specify the configuration (setup_xapit.rb) and a rake file to load
4
+ the rake tasks (xapit.rake).
5
+
6
+ IMPORTANT: Only use this generator if you are using the gem version of
7
+ Xapit, it is not needed if you are installing via Rails plugin.
8
+
9
+ Examples:
10
+ script/generate xapit
11
+
12
+ Initializer: config/initializers/setup_xapit.rb
13
+ Rake Tasks: lib/tasks/xapit.rake
@@ -0,0 +1 @@
1
+ Xapit.setup(:database_path => "#{Rails.root}/db/xapiandb")
@@ -0,0 +1,4 @@
1
+ begin
2
+ require 'xapit/rake_tasks'
3
+ rescue MissingSourceFile
4
+ end
@@ -0,0 +1,20 @@
1
+ class XapitGenerator < Rails::Generator::Base
2
+ def manifest
3
+ record do |m|
4
+ m.directory "config/initializers"
5
+ m.file "setup_xapit.rb", "config/initializers/setup_xapit.rb"
6
+
7
+ m.directory "lib/tasks"
8
+ m.file "xapit.rake", "lib/tasks/xapit.rake"
9
+ end
10
+ end
11
+
12
+ protected
13
+ def banner
14
+ <<-EOS
15
+ Generates files necessary to setup Xapit.
16
+
17
+ USAGE: #{$0} #{spec.name}
18
+ EOS
19
+ end
20
+ end
data/spec/spec_helper.rb CHANGED
@@ -8,8 +8,8 @@ require File.dirname(__FILE__) + '/xapit_member'
8
8
  Spec::Runner.configure do |config|
9
9
  config.mock_with :rr
10
10
  config.before(:each) do
11
- Xapit::Config.setup(:database_path => File.dirname(__FILE__) + '/tmp/xapiandb')
12
- Xapit::Config.remove_database
11
+ Xapit.setup(:database_path => File.dirname(__FILE__) + '/tmp/xapiandb')
12
+ Xapit.remove_database
13
13
  XapitMember.delete_all
14
14
  end
15
15
  end
@@ -0,0 +1,31 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper')
2
+
3
+ describe Xapit::ActiveRecordAdapter do
4
+ it "should be used for ActiveRecord::Base subclasses" do
5
+ Xapit::ActiveRecordAdapter.should_not be_for_class(Object)
6
+ klass = Object.new
7
+ stub(klass).ancestors { ["ActiveRecord::Base"] }
8
+ Xapit::ActiveRecordAdapter.should be_for_class(klass)
9
+ end
10
+
11
+ it "should pass find_single to find method to target" do
12
+ target = Object.new
13
+ mock(target).find(1) { :record }
14
+ adapter = Xapit::ActiveRecordAdapter.new(target)
15
+ adapter.find_single(1).should == :record
16
+ end
17
+
18
+ it "should pass find_multiple to find method to target" do
19
+ target = Object.new
20
+ mock(target).find([1, 2]) { :record }
21
+ adapter = Xapit::ActiveRecordAdapter.new(target)
22
+ adapter.find_multiple([1, 2]).should == :record
23
+ end
24
+
25
+ it "should pass find_each to target" do
26
+ target = Object.new
27
+ mock(target).find_each(:args) { 5 }
28
+ adapter = Xapit::ActiveRecordAdapter.new(target)
29
+ adapter.find_each(:args).should == 5
30
+ end
31
+ end
@@ -0,0 +1,10 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper')
2
+
3
+ describe Xapit::DataMapperAdapter do
4
+ it "should be used for DataMapper::Resource model" do
5
+ Xapit::DataMapperAdapter.should_not be_for_class(Object)
6
+ klass = Object.new
7
+ stub(klass).ancestors { ["DataMapper::Resource"] }
8
+ Xapit::DataMapperAdapter.should be_for_class(klass)
9
+ end
10
+ end
@@ -9,7 +9,7 @@ describe Xapit::FacetOption do
9
9
  end
10
10
 
11
11
  it "should remove current identifier from previous identifiers if it exists" do
12
- Xapit::Config.setup(:breadcrumb_facets => false)
12
+ Xapit.setup(:breadcrumb_facets => false)
13
13
  option = Xapit::FacetOption.new(nil, nil, nil)
14
14
  option.existing_facet_identifiers = ["abc", "123", "foo"]
15
15
  stub(option).identifier { "foo" }
@@ -17,7 +17,7 @@ describe Xapit::FacetOption do
17
17
  end
18
18
 
19
19
  it "should support breadcrumb style facets" do
20
- Xapit::Config.setup(:breadcrumb_facets => true)
20
+ Xapit.setup(:breadcrumb_facets => true)
21
21
  option = Xapit::FacetOption.new(nil, nil, nil)
22
22
  option.existing_facet_identifiers = ["abc", "123", "foo"]
23
23
  stub(option).identifier { "123" }
@@ -2,6 +2,7 @@ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2
2
 
3
3
  describe Xapit::IndexBlueprint do
4
4
  before(:each) do
5
+ XapitMember.xapit { } # call so methods are generated
5
6
  @index = Xapit::IndexBlueprint.new(XapitMember)
6
7
  end
7
8
 
@@ -36,7 +37,14 @@ describe Xapit::IndexBlueprint do
36
37
  @index.facet(:foo)
37
38
  @index.facet(:test)
38
39
  @index.sortable(:bar, :blah)
39
- @index.sortable_position_for(:blah).should == 3
40
+ @index.position_of_sortable(:blah).should == 3
41
+ end
42
+
43
+ it "should have a field position offset by facets + sortable" do
44
+ @index.facet(:foo)
45
+ @index.sortable(:bar, :blah)
46
+ @index.field(:bar, :blah)
47
+ @index.position_of_field(:blah).should == 4
40
48
  end
41
49
 
42
50
  it "should index member document into database" do
@@ -53,8 +61,8 @@ describe Xapit::IndexBlueprint do
53
61
  end
54
62
 
55
63
  it "should pass in extra arguments to each method" do
56
- index = Xapit::IndexBlueprint.new(Object, :foo, :bar => :blah)
57
- mock(Object).find_each(:foo, :bar => :blah)
64
+ index = Xapit::IndexBlueprint.new(XapitMember, :foo, :bar => :blah)
65
+ mock(XapitMember).find_each(:foo, :bar => :blah)
58
66
  index.index_all
59
67
  end
60
68
  end
@@ -71,4 +71,41 @@ describe Xapit::AbstractIndexer do
71
71
  @index.field(:created_on)
72
72
  @indexer.field_terms(member).should == ["Xcreated_on-#{member.created_on.to_time.to_i}"]
73
73
  end
74
+
75
+ it "should use sortable_serialze for numeric sortable" do
76
+ member = Object.new
77
+ stub(member).age { 7.89 }
78
+ @index.sortable(:age)
79
+ @indexer.values(member).should == [Xapian.sortable_serialise(7.89)]
80
+ end
81
+
82
+ it "should only use first value if sortable attribute is an array" do
83
+ member = Object.new
84
+ stub(member).age { [1, 2] }
85
+ @index.sortable(:age)
86
+ @indexer.values(member).should == [Xapian.sortable_serialise(1)]
87
+ end
88
+
89
+ it "should add sortable_serialze value for numeric field" do
90
+ member = Object.new
91
+ stub(member).age { [1, 2] }
92
+ @index.field(:age)
93
+ @indexer.values(member).should == [Xapian.sortable_serialise(1)]
94
+ end
95
+
96
+ it "should use sortable_serialze for date field" do
97
+ date = Date.today
98
+ member = Object.new
99
+ stub(member).age { date }
100
+ @index.field(:age)
101
+ @indexer.values(member).should == [Xapian.sortable_serialise(date.to_time.to_i)]
102
+ end
103
+
104
+ it "should use sortable_serialze for time field" do
105
+ time = Time.now
106
+ member = Object.new
107
+ stub(member).age { time }
108
+ @index.field(:age)
109
+ @indexer.values(member).should == [Xapian.sortable_serialise(time.to_i)]
110
+ end
74
111
  end
@@ -23,4 +23,13 @@ describe Xapit::ClassicIndexer do
23
23
  @indexer.index_text_attributes(member, document)
24
24
  document.terms.map(&:term).sort.should == %w[6].sort
25
25
  end
26
+
27
+ it "should return terms separated by array" do
28
+ member = Object.new
29
+ stub(member).description { ["foo bar", 6, "", nil] }
30
+ @index.text(:description)
31
+ document = Xapian::Document.new
32
+ @indexer.index_text_attributes(member, document)
33
+ document.terms.map(&:term).sort.should == ["foo bar", "6"].sort
34
+ end
26
35
  end
@@ -10,21 +10,21 @@ describe Xapit::SimpleIndexer do
10
10
  member = Object.new
11
11
  stub(member).description { "This is a test" }
12
12
  @index.text(:description)
13
- @indexer.terms_for_attribute_without_stemming(member, :description, {}).should == %w[this is a test]
13
+ @indexer.terms_for_attribute(member, :description, {}).should == %w[this is a test]
14
14
  end
15
15
 
16
- it "should return text term with stemming added" do
16
+ it "should return text terms with stemming added" do
17
17
  member = Object.new
18
18
  stub(member).description { "jumping high" }
19
19
  @index.text(:description)
20
- @indexer.terms_for_attribute(member, :description, {}).should == %w[jumping Zjump high Zhigh]
20
+ @indexer.stemmed_terms_for_attribute(member, :description, {}).should == %w[Zjump Zhigh]
21
21
  end
22
22
 
23
23
  it "should convert attribute to string when converting text to terms" do
24
24
  member = Object.new
25
25
  stub(member).num { 123 }
26
26
  @index.text(:num)
27
- @indexer.terms_for_attribute_without_stemming(member, :num, {}).should == %w[123]
27
+ @indexer.terms_for_attribute(member, :num, {}).should == %w[123]
28
28
  end
29
29
 
30
30
  it "should add text terms to document when indexing attributes" do
@@ -32,14 +32,14 @@ describe Xapit::SimpleIndexer do
32
32
  stub(@indexer).terms_for_attribute { %w[term list] }
33
33
  document = Xapian::Document.new
34
34
  @indexer.index_text_attributes(nil, document)
35
- document.terms.map(&:term).sort.should == %w[term list].sort
35
+ document.terms.map(&:term).sort.should == %w[Zlist Zterm list term].sort
36
36
  end
37
37
 
38
38
  it "should use given block to generate text terms" do
39
39
  member = Object.new
40
40
  stub(member).name { "foobar" }
41
41
  proc = lambda { |t| [t.length] }
42
- @indexer.terms_for_attribute_without_stemming(member, :name, { :proc => proc }).should == ["6"]
42
+ @indexer.terms_for_attribute(member, :name, { :proc => proc }).should == ["6"]
43
43
  end
44
44
 
45
45
  it "should increment term frequency by weight option" do
@@ -50,4 +50,20 @@ describe Xapit::SimpleIndexer do
50
50
  @indexer.index_text_attributes(member, document)
51
51
  document.terms.first.wdf.should == 10
52
52
  end
53
+
54
+ it "should increment term frequency by weight option" do
55
+ member = Object.new
56
+ stub(member).description { "This is a test" }
57
+ @index.text(:description, :weight => 10)
58
+ document = Xapian::Document.new
59
+ @indexer.index_text_attributes(member, document)
60
+ document.terms.first.wdf.should == 10
61
+ end
62
+
63
+ it "should return terms separated by array" do
64
+ member = Object.new
65
+ stub(member).description { ["foo bar", 6, "", nil] }
66
+ @index.text(:description)
67
+ @indexer.terms_for_attribute(member, :description, {}).should == ["foo bar", "6"]
68
+ end
53
69
  end
@@ -21,11 +21,13 @@ describe XapitMember do
21
21
  XapitMember.xapit do |index|
22
22
  index.text :description
23
23
  end
24
+ XapitMember.instance_variable_set("@xapit_adapter", nil)
24
25
  end
25
26
 
26
27
  it "should have xapit index blueprint" do
27
28
  XapitMember.xapit_index_blueprint.should be_kind_of(Xapit::IndexBlueprint)
28
29
  end
30
+
29
31
  it "should return collection from search" do
30
32
  XapitMember.search("foo").class.should == Xapit::Collection
31
33
  end
@@ -35,5 +37,19 @@ describe XapitMember do
35
37
  member.xapit_relevance = 123
36
38
  member.xapit_relevance.should == 123
37
39
  end
40
+
41
+ it "should have an adapter" do
42
+ XapitMember.xapit_adapter.class.should == Xapit::ActiveRecordAdapter
43
+ end
44
+
45
+ it "should use DataMapper adapter if that is ancestor" do
46
+ stub(XapitMember).ancestors { ["DataMapper::Resource"] }
47
+ XapitMember.xapit_adapter.class.should == Xapit::DataMapperAdapter
48
+ end
49
+
50
+ it "should raise an exception when no adapter is found" do
51
+ stub(XapitMember).ancestors { [] }
52
+ lambda { XapitMember.xapit_adapter }.should raise_error
53
+ end
38
54
  end
39
55
  end
@@ -4,20 +4,38 @@ describe Xapit::AbstractQueryParser do
4
4
  before(:each) do
5
5
  end
6
6
 
7
- it "parse conditions hash into terms" do
7
+ it "should parse conditions hash into terms" do
8
8
  parser = Xapit::AbstractQueryParser.new(:conditions => { :foo => 'bar', 'hello' => :world })
9
9
  parser.condition_terms.sort.should == ["Xfoo-bar", "Xhello-world"].sort
10
10
  end
11
11
 
12
- it "convert time into integer before placing in condition term" do
12
+ it "should convert time into integer before placing in condition term" do
13
13
  time = Time.now
14
14
  parser = Xapit::AbstractQueryParser.new(:conditions => { :time => time })
15
15
  parser.condition_terms.should == ["Xtime-#{time.to_i}"]
16
16
  end
17
17
 
18
- it "convert date into time then integer before placing in condition term" do
18
+ it "should convert date into time then integer before placing in condition term" do
19
19
  date = Date.today
20
20
  parser = Xapit::AbstractQueryParser.new(:conditions => { :date => date })
21
21
  parser.condition_terms.should == ["Xdate-#{date.to_time.to_i}"]
22
22
  end
23
+
24
+ it "should give spelling suggestion on full term" do
25
+ Xapit::Config.writable_database.add_spelling("foo bar")
26
+ parser = Xapit::AbstractQueryParser.new(nil, "foo barr")
27
+ parser.spelling_suggestion.should == "foo bar"
28
+ end
29
+
30
+ it "should allow an array of conditions to be specified and use OR xapian query." do
31
+ parser = Xapit::AbstractQueryParser.new(:not_conditions => { :foo => %w[hello world]})
32
+ parser.not_condition_terms.first.xapian_query.description.should == Xapit::Query.new(%w[Xfoo-hello Xfoo-world], :or).xapian_query.description
33
+ end
34
+
35
+ it "should allow range condition to be specified and use VALUE_RANGE xapian query." do
36
+ XapitMember.xapit { |i| i.field :foo }
37
+ expected = Xapian::Query.new(Xapian::Query::OP_VALUE_RANGE, 0, Xapian.sortable_serialise(2), Xapian.sortable_serialise(5))
38
+ parser = Xapit::AbstractQueryParser.new(XapitMember, :conditions => { :foo => 2..5 })
39
+ parser.condition_terms.first.description.should == expected.description
40
+ end
23
41
  end
@@ -19,6 +19,12 @@ describe Xapit::Query do
19
19
  query.xapian_query.description.should == expected.description
20
20
  end
21
21
 
22
+ it "should build a query from an array of strings with :or operator" do
23
+ expected = Xapian::Query.new(Xapian::Query::OP_OR, %w[foo bar])
24
+ query = Xapit::Query.new(%w[foo bar], :or)
25
+ query.xapian_query.description.should == expected.description
26
+ end
27
+
22
28
  it "should AND two queries together" do
23
29
  expected = Xapian::Query.new(Xapian::Query::OP_AND,
24
30
  Xapian::Query.new(Xapian::Query::OP_AND, ["foo"]),
@@ -38,4 +44,19 @@ describe Xapit::Query do
38
44
  query.or_query("bar")
39
45
  query.xapian_query.description.should == expected.description
40
46
  end
47
+
48
+ it "should build a query from an array of mixed strings and queries" do
49
+ expected = Xapian::Query.new(Xapian::Query::OP_AND,
50
+ Xapian::Query.new(Xapian::Query::OP_AND, ["foo"]),
51
+ Xapian::Query.new(Xapian::Query::OP_AND, ["bar"])
52
+ )
53
+ query = Xapit::Query.new([Xapit::Query.new("foo"), Xapian::Query.new(Xapian::Query::OP_AND, ["bar"])])
54
+ query.xapian_query.description.should == expected.description
55
+ end
56
+
57
+ it "should use xapit query passed in" do
58
+ expected = Xapian::Query.new(Xapian::Query::OP_AND, ["foo bar"])
59
+ query = Xapit::Query.new(Xapit::Query.new("foo bar"))
60
+ query.xapian_query.description.should == expected.description
61
+ end
41
62
  end
data/spec/xapit_member.rb CHANGED
@@ -3,6 +3,12 @@ class XapitMember
3
3
 
4
4
  attr_reader :id
5
5
 
6
+ # Make it look like this inherits from ActiveRecord::Base
7
+ # so it will use the ActiveRecord adapter.
8
+ def self.ancestors
9
+ ["ActiveRecord::Base"]
10
+ end
11
+
6
12
  def self.find_each(&block)
7
13
  @@records.each(&block) if @@records
8
14
  end
@@ -11,8 +17,13 @@ class XapitMember
11
17
  @@records = []
12
18
  end
13
19
 
14
- def self.find(id)
15
- @@records.detect { |r| r.id == id.to_i }
20
+ def self.find(ids)
21
+ if ids.kind_of? Array
22
+ # change the order to mimic database where we can't predict the order
23
+ ids.sort.map { |id| @@records.detect { |r| r.id == id.to_i } }
24
+ else
25
+ @@records.detect { |r| r.id == ids.to_i }
26
+ end
16
27
  end
17
28
 
18
29
  def initialize(attributes = {})
data/tasks/xapit.rake CHANGED
@@ -1,9 +1 @@
1
- namespace :xapit do
2
- desc "Index all xapit models."
3
- task :index => :environment do
4
- Xapit::Config.remove_database
5
- Xapit.index_all do |member_class|
6
- puts "Indexing #{member_class.name}"
7
- end
8
- end
9
- end
1
+ require File.join(File.dirname(__FILE__), '/../lib/xapit/rake_tasks')