xapit 0.1.0 → 0.2.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.
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')