xapit 0.2.7 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- data/{CHANGELOG → CHANGELOG.rdoc} +7 -2
- data/Gemfile +19 -0
- data/LICENSE +4 -4
- data/README.rdoc +61 -108
- data/Rakefile +11 -10
- data/features/facets.feature +93 -82
- data/features/finding.feature +196 -138
- data/features/indexing.feature +35 -37
- data/features/remote_server.feature +10 -0
- data/features/step_definitions/xapit_steps.rb +53 -25
- data/features/suggestions.feature +20 -14
- data/features/support/env.rb +13 -6
- data/features/support/xapit_helpers.rb +8 -9
- data/lib/generators/xapit/install_generator.rb +14 -0
- data/lib/generators/xapit/templates/xapit.ru +6 -0
- data/lib/generators/xapit/templates/xapit.yml +11 -0
- data/lib/xapit.rb +106 -64
- data/lib/xapit/client/collection.rb +150 -0
- data/lib/xapit/client/facet.rb +11 -0
- data/lib/xapit/client/facet_option.rb +29 -0
- data/lib/xapit/client/index_builder.rb +67 -0
- data/lib/xapit/client/membership.rb +46 -0
- data/lib/xapit/client/model_adapters/abstract_model_adapter.rb +30 -0
- data/lib/xapit/client/model_adapters/active_record_adapter.rb +27 -0
- data/lib/xapit/client/model_adapters/default_model_adapter.rb +7 -0
- data/lib/xapit/client/railtie.rb +18 -0
- data/lib/xapit/client/remote_database.rb +21 -0
- data/lib/xapit/client/tasks.rb +18 -0
- data/lib/xapit/server/app.rb +27 -0
- data/lib/xapit/server/database.rb +47 -0
- data/lib/xapit/server/indexer.rb +138 -0
- data/lib/xapit/server/query.rb +240 -0
- data/spec/fixtures/blankdb/flintlock +0 -0
- data/spec/fixtures/blankdb/iamchert +1 -0
- data/spec/fixtures/blankdb/postlist.DB +0 -0
- data/spec/fixtures/blankdb/postlist.baseA +0 -0
- data/spec/fixtures/blankdb/record.DB +0 -0
- data/spec/fixtures/blankdb/record.baseA +0 -0
- data/spec/fixtures/blankdb/termlist.DB +0 -0
- data/spec/fixtures/blankdb/termlist.baseA +0 -0
- data/spec/fixtures/xapit.ru +13 -0
- data/spec/fixtures/xapit.yml +4 -0
- data/spec/spec_helper.rb +8 -9
- data/spec/support/spec_macros.rb +6 -0
- data/spec/{xapit_member.rb → support/xapit_member.rb} +14 -16
- data/spec/xapit/client/collection_spec.rb +63 -0
- data/spec/xapit/client/facet_option_spec.rb +26 -0
- data/spec/xapit/client/facet_spec.rb +13 -0
- data/spec/xapit/client/index_builder_spec.rb +66 -0
- data/spec/xapit/client/membership_spec.rb +43 -0
- data/spec/xapit/client/model_adapters/active_record_adapter_spec.rb +62 -0
- data/spec/xapit/client/model_adapters/default_model_adapter_spec.rb +7 -0
- data/spec/xapit/client/remote_database_spec.rb +19 -0
- data/spec/xapit/server/app_spec.rb +22 -0
- data/spec/xapit/server/database_spec.rb +37 -0
- data/spec/xapit/server/indexer_spec.rb +82 -0
- data/spec/xapit/server/query_spec.rb +43 -0
- data/spec/xapit/xapit_spec.rb +28 -0
- metadata +124 -93
- data/Manifest +0 -60
- data/features/sorting.feature +0 -29
- data/init.rb +0 -1
- data/install.rb +0 -8
- data/lib/xapit/adapters/abstract_adapter.rb +0 -47
- data/lib/xapit/adapters/active_record_adapter.rb +0 -20
- data/lib/xapit/adapters/data_mapper_adapter.rb +0 -10
- data/lib/xapit/collection.rb +0 -187
- data/lib/xapit/config.rb +0 -84
- data/lib/xapit/facet.rb +0 -67
- data/lib/xapit/facet_blueprint.rb +0 -59
- data/lib/xapit/facet_option.rb +0 -56
- data/lib/xapit/index_blueprint.rb +0 -147
- data/lib/xapit/indexers/abstract_indexer.rb +0 -116
- data/lib/xapit/indexers/classic_indexer.rb +0 -29
- data/lib/xapit/indexers/simple_indexer.rb +0 -38
- data/lib/xapit/membership.rb +0 -137
- data/lib/xapit/query.rb +0 -89
- data/lib/xapit/query_parsers/abstract_query_parser.rb +0 -174
- data/lib/xapit/query_parsers/classic_query_parser.rb +0 -29
- data/lib/xapit/query_parsers/simple_query_parser.rb +0 -75
- data/lib/xapit/rake_tasks.rb +0 -13
- data/rails_generators/xapit/USAGE +0 -13
- data/rails_generators/xapit/templates/setup_xapit.rb +0 -1
- data/rails_generators/xapit/templates/xapit.rake +0 -4
- data/rails_generators/xapit/xapit_generator.rb +0 -20
- data/spec/xapit/adapters/active_record_adapter_spec.rb +0 -31
- data/spec/xapit/adapters/data_mapper_adapter_spec.rb +0 -10
- data/spec/xapit/collection_spec.rb +0 -176
- data/spec/xapit/config_spec.rb +0 -62
- data/spec/xapit/facet_blueprint_spec.rb +0 -29
- data/spec/xapit/facet_option_spec.rb +0 -80
- data/spec/xapit/facet_spec.rb +0 -73
- data/spec/xapit/index_blueprint_spec.rb +0 -112
- data/spec/xapit/indexers/abstract_indexer_spec.rb +0 -111
- data/spec/xapit/indexers/classic_indexer_spec.rb +0 -35
- data/spec/xapit/indexers/simple_indexer_spec.rb +0 -69
- data/spec/xapit/membership_spec.rb +0 -55
- data/spec/xapit/query_parsers/abstract_query_parser_spec.rb +0 -60
- data/spec/xapit/query_parsers/classic_query_parser_spec.rb +0 -20
- data/spec/xapit/query_parsers/simple_query_parser_spec.rb +0 -86
- data/spec/xapit/query_spec.rb +0 -60
- data/tasks/spec.rb +0 -9
- data/tasks/xapit.rake +0 -1
- data/uninstall.rb +0 -5
- data/xapit.gemspec +0 -30
@@ -1,17 +1,23 @@
|
|
1
|
-
|
2
|
-
Given an empty database at "tmp/xapiandatabase"
|
1
|
+
Feature: Suggestions
|
3
2
|
|
4
|
-
Scenario: Spelling suggestion
|
5
|
-
|
6
|
-
|
7
|
-
|
3
|
+
Scenario: Spelling suggestion
|
4
|
+
Given an empty database at "tmp/spellingdb"
|
5
|
+
And spelling is enabled
|
6
|
+
And indexed records named "Zebra, Apple, Bike"
|
7
|
+
When I query for "zerba bike aple"
|
8
|
+
Then I should have "zebra bike apple" as a spelling suggestion
|
8
9
|
|
9
|
-
Scenario:
|
10
|
-
|
11
|
-
|
12
|
-
|
10
|
+
Scenario: No spelling suggestion
|
11
|
+
Given indexed records named "Zebra, Apple, Bike"
|
12
|
+
When I query for "zerba bike aple"
|
13
|
+
Then I should have "" as a spelling suggestion
|
13
14
|
|
14
|
-
Scenario: Find similar records
|
15
|
-
|
16
|
-
|
17
|
-
|
15
|
+
Scenario: Find similar records
|
16
|
+
Given indexed records named "Jason John Smith, John Doe, Jason Smith, Jacob Johnson"
|
17
|
+
When I query for similar records for "Jason John Smith"
|
18
|
+
Then I should find records named "Jason Smith, John Doe"
|
19
|
+
|
20
|
+
Scenario: Match similar words with stemming
|
21
|
+
Given indexed records named "flies, fly, glider"
|
22
|
+
When I query for "flying"
|
23
|
+
Then I should find records named "flies, fly"
|
data/features/support/env.rb
CHANGED
@@ -1,7 +1,14 @@
|
|
1
|
-
require '
|
2
|
-
require 'spec'
|
3
|
-
require 'active_support'
|
4
|
-
require 'fileutils'
|
1
|
+
require 'bundler/setup'
|
5
2
|
|
6
|
-
require
|
7
|
-
|
3
|
+
Bundler.require(:default)
|
4
|
+
|
5
|
+
require File.expand_path('../../../spec/support/xapit_member', __FILE__)
|
6
|
+
|
7
|
+
Before do
|
8
|
+
Xapit.reset_config
|
9
|
+
Xapit.config[:spelling] = false
|
10
|
+
end
|
11
|
+
|
12
|
+
at_exit do
|
13
|
+
$server.close if $server
|
14
|
+
end
|
@@ -1,16 +1,15 @@
|
|
1
1
|
module XapitHelpers
|
2
2
|
def create_records(records, perform_index = true)
|
3
|
-
Xapit.remove_database
|
4
3
|
XapitMember.delete_all
|
5
|
-
XapitMember.xapit do
|
4
|
+
XapitMember.xapit do
|
6
5
|
records.first.keys.each do |attribute|
|
7
6
|
if block_given?
|
8
|
-
yield(
|
7
|
+
yield(self, attribute)
|
9
8
|
else
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
9
|
+
text attribute
|
10
|
+
field attribute
|
11
|
+
facet attribute
|
12
|
+
sortable attribute
|
14
13
|
end
|
15
14
|
end
|
16
15
|
end
|
@@ -18,9 +17,9 @@ module XapitHelpers
|
|
18
17
|
attributes.each do |key, value|
|
19
18
|
attributes[key] = value.split(', ') if value.include? ', '
|
20
19
|
end
|
21
|
-
XapitMember.new(attributes
|
20
|
+
member = XapitMember.new(attributes)
|
21
|
+
member.class.xapit_index_builder.add_document(member) if perform_index
|
22
22
|
end
|
23
|
-
Xapit.index_all if perform_index
|
24
23
|
end
|
25
24
|
end
|
26
25
|
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module Xapit
|
2
|
+
module Generators
|
3
|
+
class InstallGenerator < Rails::Generators::Base
|
4
|
+
def self.source_root
|
5
|
+
File.dirname(__FILE__) + "/templates"
|
6
|
+
end
|
7
|
+
|
8
|
+
def copy_files
|
9
|
+
copy_file "xapit.yml", "config/xapit.yml"
|
10
|
+
copy_file "xapit.ru", "xapit.ru"
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
data/lib/xapit.rb
CHANGED
@@ -1,71 +1,113 @@
|
|
1
|
-
require 'digest/sha1'
|
2
|
-
require 'rubygems'
|
3
1
|
require 'xapian'
|
2
|
+
require 'digest/sha1'
|
3
|
+
require 'rack'
|
4
|
+
require 'json'
|
5
|
+
require 'net/http'
|
4
6
|
|
5
|
-
# Looking for more documentation? A good place to start is Xapit::Membership
|
6
7
|
module Xapit
|
7
|
-
#
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
end
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
value.
|
8
|
+
# A general Xapit exception
|
9
|
+
class Error < StandardError; end
|
10
|
+
|
11
|
+
# Raised when accessing the database when Xapit is disabled
|
12
|
+
class Disabled < Error; end
|
13
|
+
|
14
|
+
class << self
|
15
|
+
attr_reader :config
|
16
|
+
|
17
|
+
def reset_config
|
18
|
+
@database = nil
|
19
|
+
@config = {
|
20
|
+
:enabled => true,
|
21
|
+
:spelling => true,
|
22
|
+
:stemming => "english"
|
23
|
+
}
|
24
|
+
end
|
25
|
+
|
26
|
+
def reload
|
27
|
+
reset_config
|
28
|
+
@config.merge!(@loaded_config) if @loaded_config
|
29
|
+
end
|
30
|
+
|
31
|
+
def database
|
32
|
+
raise Disabled, "Unable to access Xapit database because it is disabled in configuration." unless Xapit.config[:enabled]
|
33
|
+
if config[:server]
|
34
|
+
@database ||= Xapit::Client::RemoteDatabase.new(config[:server])
|
35
|
+
else
|
36
|
+
@database ||= Xapit::Server::Database.new(config[:database_path])
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def load_config(filename, environment)
|
41
|
+
@loaded_config = symbolize_keys(YAML.load_file(filename)[environment.to_s])
|
42
|
+
raise ArgumentError, "The #{environment} environment does not exist in #{filename}" if @loaded_config.nil?
|
43
|
+
@config.merge!(@loaded_config)
|
44
|
+
end
|
45
|
+
|
46
|
+
def value_index(type, attribute)
|
47
|
+
Zlib.crc32(["xapit", type, attribute].join) % 99999999 # TODO: Figure out the true max of a xapian value index
|
48
|
+
end
|
49
|
+
|
50
|
+
def facet_identifier(attribute, value)
|
51
|
+
Digest::SHA1.hexdigest(["xapit", attribute, value].join)[0..6]
|
52
|
+
end
|
53
|
+
|
54
|
+
def search(*args)
|
55
|
+
Xapit::Client::Collection.new.not_in_classes("FacetOption").search(*args)
|
56
|
+
end
|
57
|
+
|
58
|
+
def serialize_value(value)
|
59
|
+
if value.kind_of?(Time)
|
60
|
+
Xapian.sortable_serialise(value.to_i)
|
61
|
+
elsif value.kind_of?(Numeric) || value.to_s =~ /^[0-9]+$/
|
62
|
+
Xapian.sortable_serialise(value.to_f)
|
63
|
+
else
|
64
|
+
value.to_s.downcase
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def enable
|
69
|
+
config[:enabled] = true
|
70
|
+
end
|
71
|
+
|
72
|
+
def index(*models)
|
73
|
+
models.each do |model|
|
74
|
+
model.xapit_model_adapter.index_all
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
# from http://snippets.dzone.com/posts/show/11121
|
79
|
+
# could use some refactoring
|
80
|
+
def symbolize_keys(arg)
|
81
|
+
case arg
|
82
|
+
when Array
|
83
|
+
arg.map { |elem| symbolize_keys(elem) }
|
84
|
+
when Hash
|
85
|
+
Hash[
|
86
|
+
arg.map { |key, value|
|
87
|
+
k = key.is_a?(String) ? key.to_sym : key
|
88
|
+
v = symbolize_keys(value)
|
89
|
+
[k,v]
|
90
|
+
}]
|
91
|
+
else
|
92
|
+
arg
|
93
|
+
end
|
51
94
|
end
|
52
95
|
end
|
96
|
+
|
97
|
+
reset_config
|
53
98
|
end
|
54
99
|
|
55
|
-
require
|
56
|
-
require
|
57
|
-
require
|
58
|
-
require
|
59
|
-
require
|
60
|
-
require
|
61
|
-
require
|
62
|
-
require
|
63
|
-
require
|
64
|
-
require
|
65
|
-
require
|
66
|
-
require
|
67
|
-
require
|
68
|
-
require
|
69
|
-
require File.dirname(__FILE__) + '/xapit/adapters/abstract_adapter'
|
70
|
-
require File.dirname(__FILE__) + '/xapit/adapters/active_record_adapter'
|
71
|
-
require File.dirname(__FILE__) + '/xapit/adapters/data_mapper_adapter'
|
100
|
+
require 'xapit/server/database'
|
101
|
+
require 'xapit/server/query'
|
102
|
+
require 'xapit/server/indexer'
|
103
|
+
require 'xapit/server/app'
|
104
|
+
require 'xapit/client/membership'
|
105
|
+
require 'xapit/client/index_builder'
|
106
|
+
require 'xapit/client/collection'
|
107
|
+
require 'xapit/client/facet'
|
108
|
+
require 'xapit/client/facet_option'
|
109
|
+
require 'xapit/client/remote_database'
|
110
|
+
require 'xapit/client/railtie' if defined? Rails
|
111
|
+
require 'xapit/client/model_adapters/abstract_model_adapter'
|
112
|
+
require 'xapit/client/model_adapters/default_model_adapter'
|
113
|
+
require 'xapit/client/model_adapters/active_record_adapter'
|
@@ -0,0 +1,150 @@
|
|
1
|
+
module Xapit
|
2
|
+
module Client
|
3
|
+
class Collection
|
4
|
+
DEFAULT_PER_PAGE = 20
|
5
|
+
|
6
|
+
attr_reader :clauses
|
7
|
+
|
8
|
+
def initialize(clauses = [])
|
9
|
+
@clauses = clauses
|
10
|
+
end
|
11
|
+
|
12
|
+
def in_classes(*classes)
|
13
|
+
scope(:in_classes, classes)
|
14
|
+
end
|
15
|
+
|
16
|
+
def not_in_classes(*classes)
|
17
|
+
scope(:not_in_classes, classes)
|
18
|
+
end
|
19
|
+
|
20
|
+
def search(phrase = nil)
|
21
|
+
if phrase && !phrase.empty?
|
22
|
+
scope(:search, phrase)
|
23
|
+
else
|
24
|
+
self
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def where(conditions)
|
29
|
+
scope(:where, where_conditions(conditions))
|
30
|
+
end
|
31
|
+
|
32
|
+
def not_where(conditions)
|
33
|
+
scope(:not_where, where_conditions(conditions))
|
34
|
+
end
|
35
|
+
|
36
|
+
def or_where(conditions)
|
37
|
+
scope(:or_where, where_conditions(conditions))
|
38
|
+
end
|
39
|
+
|
40
|
+
def order(column, direction = :asc)
|
41
|
+
scope(:order, [column, direction])
|
42
|
+
end
|
43
|
+
|
44
|
+
def page(page_num)
|
45
|
+
scope(:page, page_num)
|
46
|
+
end
|
47
|
+
|
48
|
+
def per(per_page)
|
49
|
+
scope(:per_page, per_page)
|
50
|
+
end
|
51
|
+
|
52
|
+
def similar_to(member)
|
53
|
+
scope(:similar_to, member.class.xapit_index_builder.document_data(member))
|
54
|
+
end
|
55
|
+
|
56
|
+
def with_facets(facets)
|
57
|
+
facets.to_s.length.zero? ? self : scope(:with_facets, facets.split("-"))
|
58
|
+
end
|
59
|
+
|
60
|
+
def include_facets(*facets)
|
61
|
+
facets.empty? ? self : scope(:include_facets, facets)
|
62
|
+
end
|
63
|
+
|
64
|
+
def records
|
65
|
+
@records ||= query[:records].map { |record| Kernel.const_get(record[:class]).find(record[:id]) }
|
66
|
+
end
|
67
|
+
|
68
|
+
def total_entries
|
69
|
+
query[:total].to_i
|
70
|
+
end
|
71
|
+
|
72
|
+
def current_page
|
73
|
+
(clause_value(:page) || 1).to_i
|
74
|
+
end
|
75
|
+
|
76
|
+
def limit_value
|
77
|
+
(clause_value(:per_page) || DEFAULT_PER_PAGE).to_i
|
78
|
+
end
|
79
|
+
|
80
|
+
def num_pages
|
81
|
+
(total_entries.to_f / limit_value).ceil
|
82
|
+
end
|
83
|
+
alias_method :total_pages, :num_pages
|
84
|
+
|
85
|
+
def applied_facet_options
|
86
|
+
query[:applied_facet_options].map do |option|
|
87
|
+
FacetOption.new(option[:name], {:value => option[:value]}, applied_facet_identifiers)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def applied_facet_identifiers
|
92
|
+
query[:applied_facet_options].map { |option| option[:id] }
|
93
|
+
end
|
94
|
+
|
95
|
+
def facets
|
96
|
+
@facets ||= fetch_facets.select { |f| f.options.size > 0 }
|
97
|
+
end
|
98
|
+
|
99
|
+
def spelling_suggestion
|
100
|
+
@spelling_suggestion ||= Xapit.database.spelling_suggestion(@clauses)
|
101
|
+
end
|
102
|
+
|
103
|
+
def respond_to?(method, include_private = false)
|
104
|
+
Array.method_defined?(method) || super
|
105
|
+
end
|
106
|
+
|
107
|
+
private
|
108
|
+
|
109
|
+
def where_conditions(conditions)
|
110
|
+
conditions.keys.each do |key|
|
111
|
+
if conditions[key].kind_of? Range
|
112
|
+
conditions[key] = {:from => conditions[key].begin, :to => conditions[key].end}
|
113
|
+
end
|
114
|
+
end
|
115
|
+
conditions
|
116
|
+
end
|
117
|
+
|
118
|
+
def fetch_facets
|
119
|
+
applied_facets = applied_facet_options.map(&:identifier)
|
120
|
+
query[:facets].map { |attribute, options| Facet.new(attribute, filter_facet_options(options), applied_facets) }
|
121
|
+
end
|
122
|
+
|
123
|
+
def filter_facet_options(options)
|
124
|
+
options.select do |option|
|
125
|
+
option[:count].to_i < total_entries
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
def clause_value(key)
|
130
|
+
clauses.map { |clause| clause[key] }.compact.first
|
131
|
+
end
|
132
|
+
|
133
|
+
def query
|
134
|
+
@query ||= Xapit.database.query(@clauses)
|
135
|
+
end
|
136
|
+
|
137
|
+
def scope(type, args)
|
138
|
+
Collection.new(@clauses + [{type => args}])
|
139
|
+
end
|
140
|
+
|
141
|
+
def method_missing(method, *args, &block)
|
142
|
+
if Array.method_defined?(method)
|
143
|
+
records.send(method, *args, &block)
|
144
|
+
else
|
145
|
+
super
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|