xapit 0.1.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 (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