xapit 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (180) hide show
  1. data/LICENSE +20 -0
  2. data/Manifest +178 -0
  3. data/README.rdoc +183 -0
  4. data/Rakefile +15 -0
  5. data/TODO +23 -0
  6. data/features/facets.feature +51 -0
  7. data/features/finding.feature +119 -0
  8. data/features/indexing.feature +41 -0
  9. data/features/step_definitions/common_steps.rb +7 -0
  10. data/features/step_definitions/xapit_steps.rb +117 -0
  11. data/features/support/env.rb +7 -0
  12. data/features/support/xapit_helpers.rb +27 -0
  13. data/init.rb +3 -0
  14. data/install.rb +9 -0
  15. data/lib/xapit.rb +39 -0
  16. data/lib/xapit/collection.rb +165 -0
  17. data/lib/xapit/config.rb +83 -0
  18. data/lib/xapit/facet.rb +59 -0
  19. data/lib/xapit/facet_blueprint.rb +59 -0
  20. data/lib/xapit/facet_option.rb +56 -0
  21. data/lib/xapit/index_blueprint.rb +117 -0
  22. data/lib/xapit/indexers/abstract_indexer.rb +101 -0
  23. data/lib/xapit/indexers/classic_indexer.rb +27 -0
  24. data/lib/xapit/indexers/simple_indexer.rb +31 -0
  25. data/lib/xapit/membership.rb +103 -0
  26. data/lib/xapit/query.rb +62 -0
  27. data/lib/xapit/query_parsers/abstract_query_parser.rb +115 -0
  28. data/lib/xapit/query_parsers/classic_query_parser.rb +19 -0
  29. data/lib/xapit/query_parsers/simple_query_parser.rb +75 -0
  30. data/spec/spec_helper.rb +15 -0
  31. data/spec/tmp/xapdb/flintlock +0 -0
  32. data/spec/tmp/xapdb/iamflint +0 -0
  33. data/spec/tmp/xapdb/postlist.DB +0 -0
  34. data/spec/tmp/xapdb/postlist.baseA +0 -0
  35. data/spec/tmp/xapdb/postlist.baseB +0 -0
  36. data/spec/tmp/xapdb/record.DB +0 -0
  37. data/spec/tmp/xapdb/record.baseA +0 -0
  38. data/spec/tmp/xapdb/record.baseB +0 -0
  39. data/spec/tmp/xapdb/spelling.DB +0 -0
  40. data/spec/tmp/xapdb/spelling.baseA +0 -0
  41. data/spec/tmp/xapdb/spelling.baseB +0 -0
  42. data/spec/tmp/xapdb/termlist.DB +0 -0
  43. data/spec/tmp/xapdb/termlist.baseA +0 -0
  44. data/spec/tmp/xapdb/termlist.baseB +0 -0
  45. data/spec/tmp/xapian_db/flintlock +0 -0
  46. data/spec/tmp/xapian_db/iamflint +0 -0
  47. data/spec/tmp/xapian_db/postlist.DB +0 -0
  48. data/spec/tmp/xapian_db/postlist.baseA +0 -0
  49. data/spec/tmp/xapian_db/record.DB +0 -0
  50. data/spec/tmp/xapian_db/record.baseA +0 -0
  51. data/spec/tmp/xapian_db/termlist.DB +0 -0
  52. data/spec/tmp/xapian_db/termlist.baseA +0 -0
  53. data/spec/tmp/xapiandab/flintlock +0 -0
  54. data/spec/tmp/xapiandab/iamflint +0 -0
  55. data/spec/tmp/xapiandab/postlist.DB +0 -0
  56. data/spec/tmp/xapiandab/postlist.baseA +0 -0
  57. data/spec/tmp/xapiandab/postlist.baseB +0 -0
  58. data/spec/tmp/xapiandab/record.DB +0 -0
  59. data/spec/tmp/xapiandab/record.baseA +0 -0
  60. data/spec/tmp/xapiandab/record.baseB +0 -0
  61. data/spec/tmp/xapiandab/spelling.DB +0 -0
  62. data/spec/tmp/xapiandab/spelling.baseA +0 -0
  63. data/spec/tmp/xapiandab/spelling.baseB +0 -0
  64. data/spec/tmp/xapiandab/termlist.DB +0 -0
  65. data/spec/tmp/xapiandab/termlist.baseA +0 -0
  66. data/spec/tmp/xapiandab/termlist.baseB +0 -0
  67. data/spec/tmp/xapiandatab/flintlock +0 -0
  68. data/spec/tmp/xapiandatab/iamflint +0 -0
  69. data/spec/tmp/xapiandatab/postlist.DB +0 -0
  70. data/spec/tmp/xapiandatab/postlist.baseA +0 -0
  71. data/spec/tmp/xapiandatab/postlist.baseB +0 -0
  72. data/spec/tmp/xapiandatab/record.DB +0 -0
  73. data/spec/tmp/xapiandatab/record.baseA +0 -0
  74. data/spec/tmp/xapiandatab/record.baseB +0 -0
  75. data/spec/tmp/xapiandatab/spelling.DB +0 -0
  76. data/spec/tmp/xapiandatab/spelling.baseA +0 -0
  77. data/spec/tmp/xapiandatab/spelling.baseB +0 -0
  78. data/spec/tmp/xapiandatab/termlist.DB +0 -0
  79. data/spec/tmp/xapiandatab/termlist.baseA +0 -0
  80. data/spec/tmp/xapiandatab/termlist.baseB +0 -0
  81. data/spec/tmp/xapiandataba/flintlock +0 -0
  82. data/spec/tmp/xapiandataba/iamflint +0 -0
  83. data/spec/tmp/xapiandataba/postlist.DB +0 -0
  84. data/spec/tmp/xapiandataba/postlist.baseA +0 -0
  85. data/spec/tmp/xapiandataba/postlist.baseB +0 -0
  86. data/spec/tmp/xapiandataba/record.DB +0 -0
  87. data/spec/tmp/xapiandataba/record.baseA +0 -0
  88. data/spec/tmp/xapiandataba/record.baseB +0 -0
  89. data/spec/tmp/xapiandataba/spelling.DB +0 -0
  90. data/spec/tmp/xapiandataba/spelling.baseA +0 -0
  91. data/spec/tmp/xapiandataba/spelling.baseB +0 -0
  92. data/spec/tmp/xapiandataba/termlist.DB +0 -0
  93. data/spec/tmp/xapiandataba/termlist.baseA +0 -0
  94. data/spec/tmp/xapiandataba/termlist.baseB +0 -0
  95. data/spec/tmp/xapiandatabas/flintlock +0 -0
  96. data/spec/tmp/xapiandatabas/iamflint +0 -0
  97. data/spec/tmp/xapiandatabas/postlist.DB +0 -0
  98. data/spec/tmp/xapiandatabas/postlist.baseA +0 -0
  99. data/spec/tmp/xapiandatabas/record.DB +0 -0
  100. data/spec/tmp/xapiandatabas/record.baseA +0 -0
  101. data/spec/tmp/xapiandatabas/termlist.DB +0 -0
  102. data/spec/tmp/xapiandatabas/termlist.baseA +0 -0
  103. data/spec/tmp/xapiandatb/flintlock +0 -0
  104. data/spec/tmp/xapiandatb/iamflint +0 -0
  105. data/spec/tmp/xapiandatb/postlist.DB +0 -0
  106. data/spec/tmp/xapiandatb/postlist.baseA +0 -0
  107. data/spec/tmp/xapiandatb/postlist.baseB +0 -0
  108. data/spec/tmp/xapiandatb/record.DB +0 -0
  109. data/spec/tmp/xapiandatb/record.baseA +0 -0
  110. data/spec/tmp/xapiandatb/record.baseB +0 -0
  111. data/spec/tmp/xapiandatb/spelling.DB +0 -0
  112. data/spec/tmp/xapiandatb/spelling.baseA +0 -0
  113. data/spec/tmp/xapiandatb/spelling.baseB +0 -0
  114. data/spec/tmp/xapiandatb/termlist.DB +0 -0
  115. data/spec/tmp/xapiandatb/termlist.baseA +0 -0
  116. data/spec/tmp/xapiandatb/termlist.baseB +0 -0
  117. data/spec/tmp/xapiandbase/flintlock +0 -0
  118. data/spec/tmp/xapiandbase/iamflint +0 -0
  119. data/spec/tmp/xapiandbase/postlist.DB +0 -0
  120. data/spec/tmp/xapiandbase/postlist.baseA +0 -0
  121. data/spec/tmp/xapiandbase/postlist.baseB +0 -0
  122. data/spec/tmp/xapiandbase/record.DB +0 -0
  123. data/spec/tmp/xapiandbase/record.baseA +0 -0
  124. data/spec/tmp/xapiandbase/record.baseB +0 -0
  125. data/spec/tmp/xapiandbase/spelling.DB +0 -0
  126. data/spec/tmp/xapiandbase/spelling.baseA +0 -0
  127. data/spec/tmp/xapiandbase/spelling.baseB +0 -0
  128. data/spec/tmp/xapiandbase/termlist.DB +0 -0
  129. data/spec/tmp/xapiandbase/termlist.baseA +0 -0
  130. data/spec/tmp/xapiandbase/termlist.baseB +0 -0
  131. data/spec/xapit/collection_spec.rb +153 -0
  132. data/spec/xapit/config_spec.rb +48 -0
  133. data/spec/xapit/facet_blueprint_spec.rb +29 -0
  134. data/spec/xapit/facet_option_spec.rb +80 -0
  135. data/spec/xapit/facet_spec.rb +73 -0
  136. data/spec/xapit/index_blueprint_spec.rb +60 -0
  137. data/spec/xapit/indexers/abstract_indexer_spec.rb +74 -0
  138. data/spec/xapit/indexers/classic_indexer_spec.rb +26 -0
  139. data/spec/xapit/indexers/simple_indexer_spec.rb +53 -0
  140. data/spec/xapit/membership_spec.rb +39 -0
  141. data/spec/xapit/query_parsers/abstract_query_parser_spec.rb +23 -0
  142. data/spec/xapit/query_parsers/classic_query_parser_spec.rb +15 -0
  143. data/spec/xapit/query_parsers/simple_query_parser_spec.rb +86 -0
  144. data/spec/xapit/query_spec.rb +41 -0
  145. data/spec/xapit_member.rb +32 -0
  146. data/tasks/spec.rb +9 -0
  147. data/tasks/xapit.rake +9 -0
  148. data/tmp/xapiandatabase/flintlock +0 -0
  149. data/tmp/xapiandatabase/iamflint +0 -0
  150. data/tmp/xapiandatabase/postlist.DB +0 -0
  151. data/tmp/xapiandatabase/postlist.baseA +0 -0
  152. data/tmp/xapiandatabase/postlist.baseB +0 -0
  153. data/tmp/xapiandatabase/record.DB +0 -0
  154. data/tmp/xapiandatabase/record.baseA +0 -0
  155. data/tmp/xapiandatabase/record.baseB +0 -0
  156. data/tmp/xapiandatabase/spelling.DB +0 -0
  157. data/tmp/xapiandatabase/spelling.baseA +0 -0
  158. data/tmp/xapiandatabase/spelling.baseB +0 -0
  159. data/tmp/xapiandatabase/termlist.DB +0 -0
  160. data/tmp/xapiandatabase/termlist.baseA +0 -0
  161. data/tmp/xapiandatabase/termlist.baseB +0 -0
  162. data/tmp/xapiandatabase/value.baseB +0 -0
  163. data/tmp/xapiandb/flintlock +0 -0
  164. data/tmp/xapiandb/iamflint +0 -0
  165. data/tmp/xapiandb/postlist.DB +0 -0
  166. data/tmp/xapiandb/postlist.baseA +0 -0
  167. data/tmp/xapiandb/postlist.baseB +0 -0
  168. data/tmp/xapiandb/record.DB +0 -0
  169. data/tmp/xapiandb/record.baseA +0 -0
  170. data/tmp/xapiandb/record.baseB +0 -0
  171. data/tmp/xapiandb/spelling.DB +0 -0
  172. data/tmp/xapiandb/spelling.baseA +0 -0
  173. data/tmp/xapiandb/spelling.baseB +0 -0
  174. data/tmp/xapiandb/termlist.DB +0 -0
  175. data/tmp/xapiandb/termlist.baseA +0 -0
  176. data/tmp/xapiandb/termlist.baseB +0 -0
  177. data/tmp/xapiandb/value.baseB +0 -0
  178. data/uninstall.rb +5 -0
  179. data/xapit.gemspec +30 -0
  180. metadata +257 -0
@@ -0,0 +1,119 @@
1
+ Background:
2
+ Given an empty database at "tmp/xapiandatabase"
3
+
4
+ Scenario: Query Matching No Records
5
+ Given indexed records named "John, Jane"
6
+ When I query for "Sam"
7
+ Then I should find 0 records
8
+
9
+ Scenario: Query Matching One Record
10
+ Given indexed records named "John, Jane"
11
+ When I query for "John"
12
+ Then I should find record named "John"
13
+
14
+ Scenario: Query Matching Two Records
15
+ Given indexed records named "John Smith, Jane Smith, John Smithsonian"
16
+ When I query for "Smith"
17
+ Then I should find records named "John Smith, Jane Smith"
18
+
19
+ Scenario: Query Field Matching
20
+ Given the following indexed records
21
+ | name | age |
22
+ | John | 23 |
23
+ | Jane | 17 |
24
+ | Jack | 17 |
25
+ When I query "age" matching "17"
26
+ Then I should find records named "Jane, Jack"
27
+
28
+ Scenario: Query for Page 1
29
+ Given 3 indexed records
30
+ When I query page 1 at 2 per page
31
+ Then I should find 2 records
32
+
33
+ Scenario: Query for Page 2
34
+ Given 3 indexed records
35
+ When I query page 2 at 2 per page
36
+ Then I should find 1 record
37
+ And I should have 3 records total
38
+
39
+ Scenario: Query for One Facet
40
+ Given the following indexed records
41
+ | name | age |
42
+ | John | 23 |
43
+ | Jane | 17 |
44
+ | Jack | 17 |
45
+ When I query facets "0c93ee1"
46
+ Then I should find records named "Jane, Jack"
47
+
48
+
49
+ Scenario: Query for Two Facets
50
+ Given the following indexed records
51
+ | name | age |
52
+ | John | 23 |
53
+ | Jane | 17 |
54
+ | Jack | 17 |
55
+ When I query facets "0c93ee1-078661c"
56
+ Then I should find records named "Jane"
57
+
58
+ Scenario: Query for All Records Class Agnostic
59
+ Given indexed records named "John, Jane"
60
+ When I query for "John" on Xapit
61
+ Then I should find 1 record
62
+
63
+ Scenario: Query Matching Or Query
64
+ Given indexed records named "John, Jane, Jacob"
65
+ When I query for "Jane OR John"
66
+ Then I should find records named "John, Jane"
67
+
68
+ Scenario: Query Matching Not Query
69
+ Given indexed records named "John Smith, John Johnson"
70
+ When I query for "John NOT Smith"
71
+ Then I should find records named "John Johnson"
72
+
73
+ Scenario: Query for Facets with Keywords
74
+ Given the following indexed records
75
+ | name | age |
76
+ | John | 23 |
77
+ | Jane | 17 |
78
+ | Jack | 17 |
79
+ When I query "Jane" with facets "0c93ee1"
80
+ Then I should find record named "Jane"
81
+
82
+ Scenario: Query for All Records Sorted by Name
83
+ Given indexed records named "Zebra, Apple, Banana"
84
+ When I query "" sorted by name
85
+ Then I should find records named "Apple, Banana, Zebra"
86
+
87
+ Scenario: Query for All Records Sorted by Age then Name
88
+ Given the following indexed records
89
+ | name | age |
90
+ | Banana | 23 |
91
+ | Zebra | 17 |
92
+ | Apple | 17 |
93
+ When I query "" sorted by age, name
94
+ Then I should find records named "Apple, Zebra, Banana"
95
+
96
+ Scenario: Query for All Records Sorted by Name Descending
97
+ Given indexed records named "Zebra, Apple, Banana"
98
+ When I query "" sorted by name descending
99
+ Then I should find records named "Zebra, Banana, Apple"
100
+
101
+ Scenario: Spelling suggestion
102
+ Given indexed records named "Zebra, Apple, Bike"
103
+ When I query for "zerba bike aple"
104
+ Then I should have "zebra bike apple" as a spelling suggestion
105
+
106
+ Scenario: Match similar words with stemming
107
+ Given indexed records named "flies, fly, glider"
108
+ When I query for "flying"
109
+ Then I should find records named "flies, fly"
110
+
111
+ Scenario: Find similar records
112
+ Given indexed records named "Jason John Smith, John Doe, Jason Smith, Jacob Johnson"
113
+ When I query for similar records for "Jason John Smith"
114
+ Then I should find records named "Jason Smith, John Doe"
115
+
116
+ Scenario: Unicode characters in search
117
+ Given indexed records named "über cool, uber hot"
118
+ When I query for "über"
119
+ Then I should find records named "über cool"
@@ -0,0 +1,41 @@
1
+ Scenario: Save xapian database on index
2
+ Given no file exists at "tmp/xapiandb"
3
+ And I configured the database to be saved at "tmp/xapiandb"
4
+ And 3 records
5
+ When I index the database
6
+ Then I should find a directory at "tmp/xapiandb"
7
+
8
+ Scenario: Fetch all records which are indexed
9
+ Given an empty database at "tmp/xapiandb"
10
+ And records named "John, Jane, Joe"
11
+ When I index the database
12
+ And I query for ""
13
+ Then I should find records named "John, Jane, Joe"
14
+
15
+ Scenario: Split indexed text fields differently
16
+ Given an empty database at "tmp/xapiandb"
17
+ And records named "JohnXSmith, JaneXSmith, JoeXBlack"
18
+ When I index the database splitting name by "X"
19
+ And I query for "Smith"
20
+ Then I should find records named "JohnXSmith, JaneXSmith"
21
+
22
+ Scenario: Index Multiple Field Values Separately
23
+ Given an empty database at "tmp/xapiandatabase"
24
+ And the following indexed records
25
+ | name | age |
26
+ | John | 17, 16 |
27
+ | Jack | 17 |
28
+ | Jane | 16 |
29
+ When I query "age" matching "16"
30
+ Then I should find records named "Jane, John"
31
+
32
+ Scenario: Index Weighted Attributes
33
+ Given an empty database at "tmp/xapiandatabase"
34
+ And the following indexed records with "name" weighted by "10"
35
+ | name | description |
36
+ | foo | bar |
37
+ | bar | foo |
38
+ When I query for "bar"
39
+ Then I should find records named "bar, foo"
40
+ When I query for "foo"
41
+ Then I should find records named "foo, bar"
@@ -0,0 +1,7 @@
1
+ Given /^no file exists at "([^\"]*)"$/ do |path|
2
+ FileUtils.rm_rf(File.dirname(__FILE__) + "/../../#{path}") if File.exist? path
3
+ end
4
+
5
+ Then /^I should find a directory at "([^\"]*)"$/ do |path|
6
+ File.exist?(File.dirname(__FILE__) + "/../../#{path}").should be_true
7
+ end
@@ -0,0 +1,117 @@
1
+ Given /^I configured the database to be saved at "([^\"]*)"$/ do |path|
2
+ Xapit::Config.setup(:database_path => File.dirname(__FILE__) + "/../../#{path}")
3
+ end
4
+
5
+ Given /^an empty database at "([^\"]*)"$/ do |path|
6
+ Xapit::Config.setup(:database_path => File.dirname(__FILE__) + "/../../#{path}")
7
+ Xapit::Config.remove_database
8
+ XapitMember.delete_all
9
+ end
10
+
11
+ Given /^(indexed )?records? named "([^\"]*)"$/ do |indexed, joined_names|
12
+ records = joined_names.split(', ').map { |name| {:name => name} }
13
+ create_records(records, indexed)
14
+ end
15
+
16
+ Given /^([0-9]+) (indexed )?records?$/ do |num, indexed|
17
+ create_records([:name => "foo"]*num.to_i, indexed)
18
+ end
19
+
20
+ Given /^the following indexed records$/ do |records_table|
21
+ create_records(records_table.hashes)
22
+ end
23
+
24
+ Given /^the following indexed records with "([^\"]*)" weighted by "([^\"]*)"$/ do |weight_name, weight_value, records_table|
25
+ create_records(records_table.hashes) do |index, attribute|
26
+ if attribute.to_s == weight_name
27
+ index.text attribute, :weight => weight_value.to_i
28
+ else
29
+ index.text attribute
30
+ end
31
+ end
32
+ end
33
+
34
+ When /^I index the database$/ do
35
+ Xapit.index_all
36
+ end
37
+
38
+ When /^I index the database splitting name by "([^\"]*)"$/ do |divider|
39
+ XapitMember.xapit do |index|
40
+ index.text(:name) { |name| name.split(divider) }
41
+ end
42
+ Xapit.index_all
43
+ end
44
+
45
+ When /^I query for "([^\"]*)"$/ do |query|
46
+ @records = XapitMember.search(query)
47
+ end
48
+
49
+ When /^I query for "([^\"]*)" on Xapit$/ do |query|
50
+ @records = Xapit.search(query)
51
+ end
52
+
53
+ When /^I query "([^\"]*)" with facets "([^\"]*)"$/ do |keywords, facets|
54
+ @records = XapitMember.search(keywords, :facets => facets)
55
+ end
56
+
57
+ Then /^I should find records? named "([^\"]*)"$/ do |joined_names|
58
+ @records.map(&:name).join(", ").should == joined_names
59
+ end
60
+
61
+ Then /^I should find ([0-9]+) records?$/ do |num|
62
+ @records.should have(num.to_i).records
63
+ end
64
+
65
+ Then /^I should have ([0-9]+) records? total$/ do |num|
66
+ @records.total_entries.should == num.to_i
67
+ end
68
+
69
+ When /^I query "([^\"]*)" matching "([^\"]*)"$/ do |field, value|
70
+ @records = XapitMember.search("", :conditions => { field.to_sym => value })
71
+ end
72
+
73
+ When /^I query page ([0-9]+) at ([0-9]+) per page$/ do |page, per_page|
74
+ @records = XapitMember.search("", :page => page, :per_page => per_page.to_i)
75
+ end
76
+
77
+ When /^I query facets "([^\"]*)"$/ do |facets|
78
+ @records = XapitMember.search("", :facets => facets)
79
+ end
80
+
81
+ When /^I query "([^\"]*)" sorted by (.*?)( descending)?$/ do |keywords, sort, descending|
82
+ @records = XapitMember.search("", :order => sort.split(', '), :descending => descending)
83
+ end
84
+
85
+ When /^I query for similar records for "([^\"]*)"$/ do |keywords|
86
+ @records = XapitMember.search(keywords).first.search_similar
87
+ end
88
+
89
+ Then /^I should have the following facets$/ do |facets_table|
90
+ result = []
91
+ @records.facets.each do |facet|
92
+ facet.options.each do |option|
93
+ result << {
94
+ "facet" => facet.name,
95
+ "option" => option.name,
96
+ "count" => option.count.to_s
97
+ }
98
+ end
99
+ end
100
+ result.should == facets_table.hashes # this is somewhat fragile because it depends on order of hash result
101
+ end
102
+
103
+ Then /^I should have the following applied facets$/ do |facets_table|
104
+ result = []
105
+ @records.applied_facet_options.each do |option|
106
+ result << {
107
+ "facet" => option.facet.name,
108
+ "option" => option.name
109
+ }
110
+ end
111
+ result.should == facets_table.hashes # this is somewhat fragile because it depends on order of hash result
112
+ end
113
+
114
+ Then /^I should have "([^\"]*)" as a spelling suggestion$/ do |term|
115
+ @records.spelling_suggestion.should == term
116
+ end
117
+
@@ -0,0 +1,7 @@
1
+ require 'cucumber'
2
+ require 'spec'
3
+ require 'active_support'
4
+ require 'fileutils'
5
+
6
+ require File.dirname(__FILE__) + '/../../lib/xapit'
7
+ require File.dirname(__FILE__) + '/../../spec/xapit_member'
@@ -0,0 +1,27 @@
1
+ module XapitHelpers
2
+ def create_records(records, perform_index = true)
3
+ Xapit::Config.remove_database
4
+ XapitMember.delete_all
5
+ XapitMember.xapit do |index|
6
+ records.first.keys.each do |attribute|
7
+ if block_given?
8
+ yield(index, attribute)
9
+ else
10
+ index.text attribute
11
+ index.field attribute
12
+ index.facet attribute
13
+ index.sortable attribute
14
+ end
15
+ end
16
+ end
17
+ records.each do |attributes|
18
+ attributes.each do |key, value|
19
+ attributes[key] = value.split(', ') if value.include? ', '
20
+ end
21
+ XapitMember.new(attributes.symbolize_keys)
22
+ end
23
+ Xapit.index_all if perform_index
24
+ end
25
+ end
26
+
27
+ World(XapitHelpers)
data/init.rb ADDED
@@ -0,0 +1,3 @@
1
+ ActiveRecord::Base.class_eval do
2
+ include Xapit::Membership
3
+ end
@@ -0,0 +1,9 @@
1
+ path = "#{Rails.root}/config/initializers/setup_xapit.rb"
2
+ unless File.exist? path
3
+ puts "Adding setup_xapit.rb initializer."
4
+ File.open(path, "w") do |f|
5
+ f.write <<-EOS
6
+ Xapit::Config.setup(:database_path => "\#{Rails.root}/db/xapiandb")
7
+ EOS
8
+ end
9
+ end
@@ -0,0 +1,39 @@
1
+ require 'digest/sha1'
2
+ require 'rubygems'
3
+ require 'xapian'
4
+
5
+ # Looking for more documentation? A good place to start is Xapit::Membership
6
+ module Xapit
7
+
8
+ # Index all membership classes with xapit defined. Delegates to Xapit::IndexBlueprint.
9
+ # You will likely want to call Xapit::Config.remove_database before this.
10
+ def self.index_all(*args, &block)
11
+ IndexBlueprint.index_all(*args, &block)
12
+ end
13
+
14
+ # Used to perform a search on all indexed models. The returned collection can
15
+ # contain instances of different classes which were indexed.
16
+ #
17
+ # # perform a simple full text search
18
+ # @records = Xapit.search("phone")
19
+ #
20
+ # See Xapit::Membership for details on search options.
21
+ def self.search(*args)
22
+ Collection.new(nil, *args)
23
+ end
24
+ end
25
+
26
+ require File.dirname(__FILE__) + '/xapit/membership'
27
+ require File.dirname(__FILE__) + '/xapit/index_blueprint'
28
+ require File.dirname(__FILE__) + '/xapit/collection'
29
+ require File.dirname(__FILE__) + '/xapit/config'
30
+ require File.dirname(__FILE__) + '/xapit/facet_blueprint'
31
+ require File.dirname(__FILE__) + '/xapit/facet'
32
+ require File.dirname(__FILE__) + '/xapit/facet_option'
33
+ require File.dirname(__FILE__) + '/xapit/query'
34
+ require File.dirname(__FILE__) + '/xapit/query_parsers/abstract_query_parser'
35
+ require File.dirname(__FILE__) + '/xapit/query_parsers/simple_query_parser'
36
+ require File.dirname(__FILE__) + '/xapit/query_parsers/classic_query_parser'
37
+ require File.dirname(__FILE__) + '/xapit/indexers/abstract_indexer'
38
+ require File.dirname(__FILE__) + '/xapit/indexers/simple_indexer'
39
+ require File.dirname(__FILE__) + '/xapit/indexers/classic_indexer'
@@ -0,0 +1,165 @@
1
+ module Xapit
2
+ # This is the object which is returned when performing a search. It behaves like an array, so you do not need
3
+ # to worry about fetching the results separately. Just loop through this collection.
4
+ #
5
+ # The results are lazy loading, meaning it does not perform the query on the database until it has to.
6
+ # This allows you to string queries onto one another.
7
+ #
8
+ # Article.search("kite").search("sky") # only performs one query
9
+ #
10
+ # This class is compatible with will_paginate; you can pass it to the will_paginate helper in the view.
11
+ class Collection
12
+ NON_DELEGATE_METHODS = %w(nil? send object_id class extend size paginate first last empty? respond_to?).to_set
13
+ [].methods.each do |m|
14
+ delegate m, :to => :results unless m =~ /^__/ || NON_DELEGATE_METHODS.include?(m.to_s)
15
+ end
16
+
17
+ def self.search_similar(member, *args)
18
+ collection = new(member.class, *args)
19
+ indexer = SimpleIndexer.new(member.class.xapit_index_blueprint)
20
+ terms = indexer.text_terms(member) + indexer.field_terms(member)
21
+ collection.base_query.and_query(Xapian::Query.new(Xapian::Query::OP_OR, terms))
22
+ collection.base_query.not_query("Q#{member.class}-#{member.id}")
23
+ collection
24
+ end
25
+
26
+ def initialize(*args)
27
+ @query_parser = Config.query_parser.new(*args)
28
+ end
29
+
30
+ # Returns an array of results. You should not need to call this directly because most methods are
31
+ # automatically delegated to this array.
32
+ def results
33
+ @results ||= fetch_results
34
+ end
35
+
36
+ # The number of total records found despite any pagination settings.
37
+ def size
38
+ @query_parser.query.count
39
+ end
40
+ alias_method :total_entries, :size
41
+
42
+ # Returns true if no results are found.
43
+ def empty?
44
+ @results ? @results.empty? : size.zero?
45
+ end
46
+
47
+ # The first record in the result set.
48
+ def first
49
+ fetch_results(:offset => 0, :limit => 1).first
50
+ end
51
+
52
+ # The last record in the result set.
53
+ def last
54
+ fetch_results(:offset => size-1, :limit => 1).last
55
+ end
56
+
57
+ # Perform another search on this one, inheriting all options already passed.
58
+ # See Xapit::Membership for search options.
59
+ def search(keywords, options = {})
60
+ collection = Collection.new(@query_parser.member_class, keywords, options)
61
+ collection.base_query = @query_parser.query
62
+ collection
63
+ end
64
+
65
+ def base_query=(base_query)
66
+ @query_parser.base_query = base_query
67
+ end
68
+
69
+ def base_query
70
+ @query_parser.base_query
71
+ end
72
+
73
+ # The page number we are currently on.
74
+ def current_page
75
+ @query_parser.current_page
76
+ end
77
+
78
+ # How many records to display on each page, defaults to 20. Sets with :per_page option when performing search.
79
+ def per_page
80
+ @query_parser.per_page
81
+ end
82
+
83
+ # The offset for the current page
84
+ def offset
85
+ @query_parser.offset
86
+ end
87
+
88
+ # Total number of pages with found results.
89
+ def total_pages
90
+ (total_entries / per_page.to_f).ceil
91
+ end
92
+
93
+ # The previous page number. Returns nil if on first page.
94
+ def previous_page
95
+ current_page > 1 ? (current_page - 1) : nil
96
+ end
97
+
98
+ # The next page number. Returns nil if on last page.
99
+ def next_page
100
+ current_page < total_pages ? (current_page + 1): nil
101
+ end
102
+
103
+ # Xapit::Facet objects matching this search query. See class for details.
104
+ def facets
105
+ all_facets.select do |facet|
106
+ facet.options.size > 1
107
+ end
108
+ end
109
+
110
+ # Xapit::FacetOption objects which are currently applied to search (through :facets option). Use this to
111
+ # display the facets which are currently applied.
112
+ #
113
+ # <% for option in @articles.applied_facet_options %>
114
+ # <%=h option.name %>
115
+ # <%= link_to "remove", :overwrite_params => { :facets => option } %>
116
+ # <% end %>
117
+ #
118
+ # If you set :breadcrumb_facets option to true in Config#setup the link will drop leftover facets
119
+ # instead of removing the current one. This makes it easy to add a breadcrumb style interface.
120
+ #
121
+ # Config.setup(:breadcrumb_facets => true)
122
+ # <% for option in @articles.applied_facet_options %>
123
+ # <%= link_to h(option.name), :overwrite_params => { :facets => option } %> &gt;
124
+ # <% end %>
125
+ #
126
+ def applied_facet_options
127
+ @query_parser.facet_identifiers.map do |identifier|
128
+ option = FacetOption.find(identifier)
129
+ option.existing_facet_identifiers = @query_parser.facet_identifiers
130
+ option
131
+ end
132
+ end
133
+
134
+ # Includes a suggested variation of a term which will get many more results. Returns nil if no suggestion.
135
+ #
136
+ # <% if @articles.spelling_suggestion %>
137
+ # Did you mean <%= link_to h(@articles.spelling_suggestion), :overwrite_params => { :keywords => @articles.spelling_suggestion } %>?
138
+ # <% end %>
139
+ #
140
+ def spelling_suggestion
141
+ @query_parser.spelling_suggestion
142
+ end
143
+
144
+ private
145
+
146
+ def all_facets
147
+ @query_parser.member_class.xapit_index_blueprint.facets.map do |facet_blueprint|
148
+ Facet.new(facet_blueprint, @query_parser.query, @query_parser.facet_identifiers)
149
+ end
150
+ end
151
+
152
+ def matchset(options = {})
153
+ @query_parser.query.matchset(options)
154
+ end
155
+
156
+ def fetch_results(options = {})
157
+ matchset(options).matches.map do |match|
158
+ class_name, id = match.document.data.split('-')
159
+ member = class_name.constantize.find(id)
160
+ member.xapit_relevance = match.percent
161
+ member
162
+ end
163
+ end
164
+ end
165
+ end