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.
- data/LICENSE +20 -0
- data/Manifest +178 -0
- data/README.rdoc +183 -0
- data/Rakefile +15 -0
- data/TODO +23 -0
- data/features/facets.feature +51 -0
- data/features/finding.feature +119 -0
- data/features/indexing.feature +41 -0
- data/features/step_definitions/common_steps.rb +7 -0
- data/features/step_definitions/xapit_steps.rb +117 -0
- data/features/support/env.rb +7 -0
- data/features/support/xapit_helpers.rb +27 -0
- data/init.rb +3 -0
- data/install.rb +9 -0
- data/lib/xapit.rb +39 -0
- data/lib/xapit/collection.rb +165 -0
- data/lib/xapit/config.rb +83 -0
- data/lib/xapit/facet.rb +59 -0
- data/lib/xapit/facet_blueprint.rb +59 -0
- data/lib/xapit/facet_option.rb +56 -0
- data/lib/xapit/index_blueprint.rb +117 -0
- data/lib/xapit/indexers/abstract_indexer.rb +101 -0
- data/lib/xapit/indexers/classic_indexer.rb +27 -0
- data/lib/xapit/indexers/simple_indexer.rb +31 -0
- data/lib/xapit/membership.rb +103 -0
- data/lib/xapit/query.rb +62 -0
- data/lib/xapit/query_parsers/abstract_query_parser.rb +115 -0
- data/lib/xapit/query_parsers/classic_query_parser.rb +19 -0
- data/lib/xapit/query_parsers/simple_query_parser.rb +75 -0
- data/spec/spec_helper.rb +15 -0
- data/spec/tmp/xapdb/flintlock +0 -0
- data/spec/tmp/xapdb/iamflint +0 -0
- data/spec/tmp/xapdb/postlist.DB +0 -0
- data/spec/tmp/xapdb/postlist.baseA +0 -0
- data/spec/tmp/xapdb/postlist.baseB +0 -0
- data/spec/tmp/xapdb/record.DB +0 -0
- data/spec/tmp/xapdb/record.baseA +0 -0
- data/spec/tmp/xapdb/record.baseB +0 -0
- data/spec/tmp/xapdb/spelling.DB +0 -0
- data/spec/tmp/xapdb/spelling.baseA +0 -0
- data/spec/tmp/xapdb/spelling.baseB +0 -0
- data/spec/tmp/xapdb/termlist.DB +0 -0
- data/spec/tmp/xapdb/termlist.baseA +0 -0
- data/spec/tmp/xapdb/termlist.baseB +0 -0
- data/spec/tmp/xapian_db/flintlock +0 -0
- data/spec/tmp/xapian_db/iamflint +0 -0
- data/spec/tmp/xapian_db/postlist.DB +0 -0
- data/spec/tmp/xapian_db/postlist.baseA +0 -0
- data/spec/tmp/xapian_db/record.DB +0 -0
- data/spec/tmp/xapian_db/record.baseA +0 -0
- data/spec/tmp/xapian_db/termlist.DB +0 -0
- data/spec/tmp/xapian_db/termlist.baseA +0 -0
- data/spec/tmp/xapiandab/flintlock +0 -0
- data/spec/tmp/xapiandab/iamflint +0 -0
- data/spec/tmp/xapiandab/postlist.DB +0 -0
- data/spec/tmp/xapiandab/postlist.baseA +0 -0
- data/spec/tmp/xapiandab/postlist.baseB +0 -0
- data/spec/tmp/xapiandab/record.DB +0 -0
- data/spec/tmp/xapiandab/record.baseA +0 -0
- data/spec/tmp/xapiandab/record.baseB +0 -0
- data/spec/tmp/xapiandab/spelling.DB +0 -0
- data/spec/tmp/xapiandab/spelling.baseA +0 -0
- data/spec/tmp/xapiandab/spelling.baseB +0 -0
- data/spec/tmp/xapiandab/termlist.DB +0 -0
- data/spec/tmp/xapiandab/termlist.baseA +0 -0
- data/spec/tmp/xapiandab/termlist.baseB +0 -0
- data/spec/tmp/xapiandatab/flintlock +0 -0
- data/spec/tmp/xapiandatab/iamflint +0 -0
- data/spec/tmp/xapiandatab/postlist.DB +0 -0
- data/spec/tmp/xapiandatab/postlist.baseA +0 -0
- data/spec/tmp/xapiandatab/postlist.baseB +0 -0
- data/spec/tmp/xapiandatab/record.DB +0 -0
- data/spec/tmp/xapiandatab/record.baseA +0 -0
- data/spec/tmp/xapiandatab/record.baseB +0 -0
- data/spec/tmp/xapiandatab/spelling.DB +0 -0
- data/spec/tmp/xapiandatab/spelling.baseA +0 -0
- data/spec/tmp/xapiandatab/spelling.baseB +0 -0
- data/spec/tmp/xapiandatab/termlist.DB +0 -0
- data/spec/tmp/xapiandatab/termlist.baseA +0 -0
- data/spec/tmp/xapiandatab/termlist.baseB +0 -0
- data/spec/tmp/xapiandataba/flintlock +0 -0
- data/spec/tmp/xapiandataba/iamflint +0 -0
- data/spec/tmp/xapiandataba/postlist.DB +0 -0
- data/spec/tmp/xapiandataba/postlist.baseA +0 -0
- data/spec/tmp/xapiandataba/postlist.baseB +0 -0
- data/spec/tmp/xapiandataba/record.DB +0 -0
- data/spec/tmp/xapiandataba/record.baseA +0 -0
- data/spec/tmp/xapiandataba/record.baseB +0 -0
- data/spec/tmp/xapiandataba/spelling.DB +0 -0
- data/spec/tmp/xapiandataba/spelling.baseA +0 -0
- data/spec/tmp/xapiandataba/spelling.baseB +0 -0
- data/spec/tmp/xapiandataba/termlist.DB +0 -0
- data/spec/tmp/xapiandataba/termlist.baseA +0 -0
- data/spec/tmp/xapiandataba/termlist.baseB +0 -0
- data/spec/tmp/xapiandatabas/flintlock +0 -0
- data/spec/tmp/xapiandatabas/iamflint +0 -0
- data/spec/tmp/xapiandatabas/postlist.DB +0 -0
- data/spec/tmp/xapiandatabas/postlist.baseA +0 -0
- data/spec/tmp/xapiandatabas/record.DB +0 -0
- data/spec/tmp/xapiandatabas/record.baseA +0 -0
- data/spec/tmp/xapiandatabas/termlist.DB +0 -0
- data/spec/tmp/xapiandatabas/termlist.baseA +0 -0
- data/spec/tmp/xapiandatb/flintlock +0 -0
- data/spec/tmp/xapiandatb/iamflint +0 -0
- data/spec/tmp/xapiandatb/postlist.DB +0 -0
- data/spec/tmp/xapiandatb/postlist.baseA +0 -0
- data/spec/tmp/xapiandatb/postlist.baseB +0 -0
- data/spec/tmp/xapiandatb/record.DB +0 -0
- data/spec/tmp/xapiandatb/record.baseA +0 -0
- data/spec/tmp/xapiandatb/record.baseB +0 -0
- data/spec/tmp/xapiandatb/spelling.DB +0 -0
- data/spec/tmp/xapiandatb/spelling.baseA +0 -0
- data/spec/tmp/xapiandatb/spelling.baseB +0 -0
- data/spec/tmp/xapiandatb/termlist.DB +0 -0
- data/spec/tmp/xapiandatb/termlist.baseA +0 -0
- data/spec/tmp/xapiandatb/termlist.baseB +0 -0
- data/spec/tmp/xapiandbase/flintlock +0 -0
- data/spec/tmp/xapiandbase/iamflint +0 -0
- data/spec/tmp/xapiandbase/postlist.DB +0 -0
- data/spec/tmp/xapiandbase/postlist.baseA +0 -0
- data/spec/tmp/xapiandbase/postlist.baseB +0 -0
- data/spec/tmp/xapiandbase/record.DB +0 -0
- data/spec/tmp/xapiandbase/record.baseA +0 -0
- data/spec/tmp/xapiandbase/record.baseB +0 -0
- data/spec/tmp/xapiandbase/spelling.DB +0 -0
- data/spec/tmp/xapiandbase/spelling.baseA +0 -0
- data/spec/tmp/xapiandbase/spelling.baseB +0 -0
- data/spec/tmp/xapiandbase/termlist.DB +0 -0
- data/spec/tmp/xapiandbase/termlist.baseA +0 -0
- data/spec/tmp/xapiandbase/termlist.baseB +0 -0
- data/spec/xapit/collection_spec.rb +153 -0
- data/spec/xapit/config_spec.rb +48 -0
- data/spec/xapit/facet_blueprint_spec.rb +29 -0
- data/spec/xapit/facet_option_spec.rb +80 -0
- data/spec/xapit/facet_spec.rb +73 -0
- data/spec/xapit/index_blueprint_spec.rb +60 -0
- data/spec/xapit/indexers/abstract_indexer_spec.rb +74 -0
- data/spec/xapit/indexers/classic_indexer_spec.rb +26 -0
- data/spec/xapit/indexers/simple_indexer_spec.rb +53 -0
- data/spec/xapit/membership_spec.rb +39 -0
- data/spec/xapit/query_parsers/abstract_query_parser_spec.rb +23 -0
- data/spec/xapit/query_parsers/classic_query_parser_spec.rb +15 -0
- data/spec/xapit/query_parsers/simple_query_parser_spec.rb +86 -0
- data/spec/xapit/query_spec.rb +41 -0
- data/spec/xapit_member.rb +32 -0
- data/tasks/spec.rb +9 -0
- data/tasks/xapit.rake +9 -0
- data/tmp/xapiandatabase/flintlock +0 -0
- data/tmp/xapiandatabase/iamflint +0 -0
- data/tmp/xapiandatabase/postlist.DB +0 -0
- data/tmp/xapiandatabase/postlist.baseA +0 -0
- data/tmp/xapiandatabase/postlist.baseB +0 -0
- data/tmp/xapiandatabase/record.DB +0 -0
- data/tmp/xapiandatabase/record.baseA +0 -0
- data/tmp/xapiandatabase/record.baseB +0 -0
- data/tmp/xapiandatabase/spelling.DB +0 -0
- data/tmp/xapiandatabase/spelling.baseA +0 -0
- data/tmp/xapiandatabase/spelling.baseB +0 -0
- data/tmp/xapiandatabase/termlist.DB +0 -0
- data/tmp/xapiandatabase/termlist.baseA +0 -0
- data/tmp/xapiandatabase/termlist.baseB +0 -0
- data/tmp/xapiandatabase/value.baseB +0 -0
- data/tmp/xapiandb/flintlock +0 -0
- data/tmp/xapiandb/iamflint +0 -0
- data/tmp/xapiandb/postlist.DB +0 -0
- data/tmp/xapiandb/postlist.baseA +0 -0
- data/tmp/xapiandb/postlist.baseB +0 -0
- data/tmp/xapiandb/record.DB +0 -0
- data/tmp/xapiandb/record.baseA +0 -0
- data/tmp/xapiandb/record.baseB +0 -0
- data/tmp/xapiandb/spelling.DB +0 -0
- data/tmp/xapiandb/spelling.baseA +0 -0
- data/tmp/xapiandb/spelling.baseB +0 -0
- data/tmp/xapiandb/termlist.DB +0 -0
- data/tmp/xapiandb/termlist.baseA +0 -0
- data/tmp/xapiandb/termlist.baseB +0 -0
- data/tmp/xapiandb/value.baseB +0 -0
- data/uninstall.rb +5 -0
- data/xapit.gemspec +30 -0
- 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,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
data/install.rb
ADDED
data/lib/xapit.rb
ADDED
|
@@ -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 } %> >
|
|
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
|