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
data/lib/xapit/config.rb
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
module Xapit
|
|
2
|
+
# Singleton class for storing Xapit configuration settings. Currently this only includes the database path.
|
|
3
|
+
class Config
|
|
4
|
+
class << self
|
|
5
|
+
attr_reader :options
|
|
6
|
+
|
|
7
|
+
# Setup configuration options. The following options are supported.
|
|
8
|
+
#
|
|
9
|
+
# <tt>:database_path</tt>: Where the database is stored.
|
|
10
|
+
# <tt>:stemming</tt>: The language to use for stemming, defaults to "english".
|
|
11
|
+
# <tt>:spelling</tt>: True or false to enable/disable spelling, defaults to true.
|
|
12
|
+
# <tt>:indexer</tt>: Class to handle the indexing, defaults to SimpleIndexer.
|
|
13
|
+
# <tt>:query_parser</tt>: Class to handle the parsing, defaults to ClassicQueryParser.
|
|
14
|
+
# <tt>:breadcrumb_facets</tt>: Use breadcrumb mode for applied facets. See Collection#applied_facet_options for details.
|
|
15
|
+
#
|
|
16
|
+
def setup(options = {})
|
|
17
|
+
if @options && options[:database_path] != @options[:database_path]
|
|
18
|
+
@database = nil
|
|
19
|
+
@writable_database = nil
|
|
20
|
+
end
|
|
21
|
+
@options = options.reverse_merge(default_options)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def default_options
|
|
25
|
+
{
|
|
26
|
+
:indexer => SimpleIndexer,
|
|
27
|
+
:query_parser => ClassicQueryParser,
|
|
28
|
+
:spelling => true,
|
|
29
|
+
:stemming => "english"
|
|
30
|
+
}
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# See if setup options are already set.
|
|
34
|
+
def setup?
|
|
35
|
+
@options
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# The configured path to the database.
|
|
39
|
+
def path
|
|
40
|
+
@options[:database_path]
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def query_parser
|
|
44
|
+
@options[:query_parser]
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def indexer
|
|
48
|
+
@options[:indexer]
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def spelling?
|
|
52
|
+
@options[:spelling]
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def stemming
|
|
56
|
+
@options[:stemming]
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def breadcrumb_facets?
|
|
60
|
+
@options[:breadcrumb_facets]
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Fetch Xapian::Database object at configured path. Database is stored in memory.
|
|
64
|
+
def database
|
|
65
|
+
@writable_database || (@database ||= Xapian::Database.new(path))
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Fetch Xapian::WritableDatabase object at configured path. Database is stored in memory.
|
|
69
|
+
# Creates the database directory if needed.
|
|
70
|
+
def writable_database
|
|
71
|
+
FileUtils.mkdir_p(File.dirname(path)) unless File.exist?(File.dirname(path))
|
|
72
|
+
@writable_database ||= Xapian::WritableDatabase.new(path, Xapian::DB_CREATE_OR_OPEN)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Removes the configured database file and clears the stored one in memory.
|
|
76
|
+
def remove_database # this can be a bit dangers, maybe do some checking here first?
|
|
77
|
+
FileUtils.rm_rf(path) if File.exist? path
|
|
78
|
+
@database = nil
|
|
79
|
+
@writable_database = nil
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
data/lib/xapit/facet.rb
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
module Xapit
|
|
2
|
+
# Facets allow users to further filter the result set based on certain attributes.
|
|
3
|
+
# You should fetch facets by calling "facets" on a Xapit::Collection search result.
|
|
4
|
+
#
|
|
5
|
+
# <% for facet in @articles.facets %>
|
|
6
|
+
# <%= facet.name %>
|
|
7
|
+
# <% for option in facet.options %>
|
|
8
|
+
# <%= link_to option.name, :overwrite_params => { :facets => option } %>
|
|
9
|
+
# (<%= option.count %>)
|
|
10
|
+
# <% end %>
|
|
11
|
+
# <% end %>
|
|
12
|
+
#
|
|
13
|
+
# See Xapit::FacetBlueprint for details on how to index a facet.
|
|
14
|
+
class Facet
|
|
15
|
+
attr_accessor :existing_facet_identifiers
|
|
16
|
+
|
|
17
|
+
def initialize(blueprint, query, existing_facet_identifiers)
|
|
18
|
+
@blueprint = blueprint
|
|
19
|
+
@query = query.dup
|
|
20
|
+
@existing_facet_identifiers = existing_facet_identifiers
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Xapit::FacetOption objects for this facet. This only lists the ones which match the current query.
|
|
24
|
+
def options
|
|
25
|
+
matching_identifiers.map do |identifier, count|
|
|
26
|
+
option = FacetOption.find(identifier)
|
|
27
|
+
option.count = count
|
|
28
|
+
option.existing_facet_identifiers = @existing_facet_identifiers
|
|
29
|
+
option
|
|
30
|
+
end.sort_by(&:name)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def matching_identifiers
|
|
34
|
+
result = {}
|
|
35
|
+
matches.each do |match|
|
|
36
|
+
class_name, id = match.document.data.split('-')
|
|
37
|
+
record = class_name.constantize.find(id)
|
|
38
|
+
@blueprint.identifiers_for(record).each do |identifier|
|
|
39
|
+
unless existing_facet_identifiers.include? identifier
|
|
40
|
+
result[identifier] ||= 0
|
|
41
|
+
result[identifier] += (match.collapse_count + 1)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
result
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# The name of the facet. See Xapit::FacetBlueprint for details.
|
|
49
|
+
def name
|
|
50
|
+
@blueprint.name
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
def matches
|
|
56
|
+
@query.matches(:offset => 0, :limit => 1000, :collapse_key => @blueprint.position)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
module Xapit
|
|
2
|
+
# A facet blueprint keeps track of the settings for indexing a given facet. You can specify a custom
|
|
3
|
+
# name for a given facet by providing a second argument when defining.
|
|
4
|
+
#
|
|
5
|
+
# xapit do |index|
|
|
6
|
+
# index.facet :category_name, "Category"
|
|
7
|
+
# end
|
|
8
|
+
#
|
|
9
|
+
# Multiple facet values are supported for a single record. All you need to do is return an array of
|
|
10
|
+
# values instead of a single string.
|
|
11
|
+
#
|
|
12
|
+
# def category_names
|
|
13
|
+
# categories.map(&:name) # => ["Toys", "Clothing"]
|
|
14
|
+
# end
|
|
15
|
+
#
|
|
16
|
+
class FacetBlueprint
|
|
17
|
+
attr_reader :member_class
|
|
18
|
+
attr_reader :position
|
|
19
|
+
attr_reader :attribute
|
|
20
|
+
|
|
21
|
+
def initialize(member_class, position, attribute, custom_name = nil)
|
|
22
|
+
@member_class = member_class
|
|
23
|
+
@position = position
|
|
24
|
+
@attribute = attribute
|
|
25
|
+
@custom_name = custom_name
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def identifiers_for(member)
|
|
29
|
+
values_for(member).map do |value|
|
|
30
|
+
Digest::SHA1.hexdigest(@attribute.to_s + value)[0..6]
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# The name of the facet. This will return the custom name if given while setting up the index,
|
|
35
|
+
# or default to humanizing the attribute name.
|
|
36
|
+
def name
|
|
37
|
+
@custom_name || @attribute.to_s.humanize
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def save_facet_options_for(member)
|
|
41
|
+
values_for(member).map do |value|
|
|
42
|
+
option = FacetOption.new(member.class.name, @attribute.to_s, value)
|
|
43
|
+
option.save
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def values_for(member)
|
|
50
|
+
value = member.send(@attribute)
|
|
51
|
+
if value.kind_of? Array
|
|
52
|
+
value.map(&:to_s).reject(&:empty?)
|
|
53
|
+
else
|
|
54
|
+
[value].map(&:to_s).reject(&:empty?)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
module Xapit
|
|
2
|
+
# A facet option is a specific value or choice for a facet. See Xapit::Facet for details on how to use it.
|
|
3
|
+
class FacetOption
|
|
4
|
+
attr_accessor :facet, :name, :existing_facet_identifiers, :count
|
|
5
|
+
|
|
6
|
+
# Fetch a facet option given an id.
|
|
7
|
+
def self.find(id)
|
|
8
|
+
match = Query.new("Q#{name}-#{id}").matches(:offset => 0, :limit => 1).first
|
|
9
|
+
if match.nil?
|
|
10
|
+
raise "Unable to find facet option for #{id}."
|
|
11
|
+
else
|
|
12
|
+
class_name, facet_attribute, name = match.document.data.split('|||')
|
|
13
|
+
new(class_name.to_s, facet_attribute.to_s, name.to_s)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# See if the given facet option exists with this id.
|
|
18
|
+
def self.exist?(id)
|
|
19
|
+
Query.new("Q#{name}-#{id}").count >= 1
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def initialize(class_name, facet_attribute, name)
|
|
23
|
+
@facet = class_name.constantize.xapit_facet_blueprint(facet_attribute) if class_name && facet_attribute
|
|
24
|
+
@name = name
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def identifier
|
|
28
|
+
Digest::SHA1.hexdigest(facet.attribute.to_s + name)[0..6]
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Saves the given facet option to the database if it hasn't been already.
|
|
32
|
+
def save
|
|
33
|
+
unless self.class.exist?(identifier)
|
|
34
|
+
doc = Xapian::Document.new
|
|
35
|
+
doc.data = [facet.member_class.name, facet.attribute, name].join("|||")
|
|
36
|
+
doc.add_term("Q#{self.class.name}-#{identifier}")
|
|
37
|
+
Xapit::Config.writable_database.add_document(doc)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Converts the facet to be used in a URL. It adds to the existing ones for convenience.
|
|
42
|
+
# If this facet option is currently selected, then this will return all selected facets except
|
|
43
|
+
# this one. This conveniently allows you to use this as both an "add this facet" and "remove this facet" link.
|
|
44
|
+
def to_param
|
|
45
|
+
if existing_facet_identifiers.include? identifier
|
|
46
|
+
if Xapit::Config.breadcrumb_facets?
|
|
47
|
+
existing_facet_identifiers[0..existing_facet_identifiers.index(identifier)].join('-')
|
|
48
|
+
else
|
|
49
|
+
(existing_facet_identifiers - [identifier]).join('-')
|
|
50
|
+
end
|
|
51
|
+
else
|
|
52
|
+
(existing_facet_identifiers + [identifier]).join('-')
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
module Xapit
|
|
2
|
+
# This is the object used in the block of the xapit method in Xapit::Membership. It keeps track of the
|
|
3
|
+
# index settings for a given class. It also provides some indexing functionality.
|
|
4
|
+
class IndexBlueprint
|
|
5
|
+
attr_reader :text_attributes
|
|
6
|
+
attr_reader :field_attributes
|
|
7
|
+
attr_reader :sortable_attributes
|
|
8
|
+
attr_reader :facets
|
|
9
|
+
|
|
10
|
+
# Indexes all classes known to have an index blueprint defined.
|
|
11
|
+
def self.index_all
|
|
12
|
+
load_models
|
|
13
|
+
@@instances.each do |member_class, blueprint|
|
|
14
|
+
yield(member_class) if block_given?
|
|
15
|
+
blueprint.index_all
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def initialize(member_class, *args)
|
|
20
|
+
@member_class = member_class
|
|
21
|
+
@args = args
|
|
22
|
+
@text_attributes = {}
|
|
23
|
+
@field_attributes = []
|
|
24
|
+
@sortable_attributes = []
|
|
25
|
+
@facets = []
|
|
26
|
+
@@instances ||= {}
|
|
27
|
+
@@instances[member_class] = self # TODO make this thread safe
|
|
28
|
+
@indexer = SimpleIndexer.new(self)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Adds a text attribute. Each word in the text will be indexed as a separate term allowing full text searching.
|
|
32
|
+
# Text terms are what is searched by the primary string in a search query.
|
|
33
|
+
#
|
|
34
|
+
# Article.search("kite")
|
|
35
|
+
#
|
|
36
|
+
# You can specify a :weight option to give a text attribute more importance. This will cause search terms matching
|
|
37
|
+
# that attribute to have a higher rank. The default weight is 1. Decimal (0.5) weight values are not supported.
|
|
38
|
+
#
|
|
39
|
+
# index.text :name, :weight => 10
|
|
40
|
+
#
|
|
41
|
+
def text(*attributes, &proc)
|
|
42
|
+
options = attributes.extract_options!
|
|
43
|
+
options[:proc] ||= proc
|
|
44
|
+
attributes.each do |attribute|
|
|
45
|
+
@text_attributes[attribute] = options
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Adds a field attribute. Field terms are not split by word so it is not designed for full text search.
|
|
50
|
+
# Instead you can filter through a field using the :conditions hash in a search query.
|
|
51
|
+
#
|
|
52
|
+
# Article.search("", :conditions => { :priority => 5 })
|
|
53
|
+
#
|
|
54
|
+
# Multiple field values are supported if the given attribute is an array.
|
|
55
|
+
#
|
|
56
|
+
# def priority
|
|
57
|
+
# [3, 5] # will match priority search for 3 or 5
|
|
58
|
+
# end
|
|
59
|
+
#
|
|
60
|
+
def field(*attributes)
|
|
61
|
+
@field_attributes += attributes
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Adds a facet attribute. See Xapit::FacetBlueprint and Xapit::Facet for details.
|
|
65
|
+
def facet(*args, &block)
|
|
66
|
+
@facets << FacetBlueprint.new(@member_class, @facets.size, *args, &block)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Adds a sortable attribute for use with the :order option in a search call.
|
|
70
|
+
def sortable(*attributes)
|
|
71
|
+
@sortable_attributes += attributes
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Indexes all records of this blueprint class. It does this using the ".find_each" method on the member class.
|
|
75
|
+
# You will likely want to call Xapit::Config.remove_database before this.
|
|
76
|
+
def index_all
|
|
77
|
+
@member_class.find_each(*@args) do |member|
|
|
78
|
+
@indexer.add_member(member)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def sortable_position_for(sortable_attribute)
|
|
83
|
+
index = sortable_attributes.map(&:to_s).index(sortable_attribute.to_s)
|
|
84
|
+
raise "Unable to find indexed sortable attribute \"#{sortable_attribute}\" in #{@member_class} sortable attributes: #{sortable_attributes.inspect}" if index.nil?
|
|
85
|
+
index + facets.size
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
private
|
|
89
|
+
|
|
90
|
+
# Make sure all models are loaded - without reloading any that
|
|
91
|
+
# ActiveRecord::Base is already aware of (otherwise we start to hit some
|
|
92
|
+
# messy dependencies issues).
|
|
93
|
+
#
|
|
94
|
+
# Taken from thinking-sphinx
|
|
95
|
+
def self.load_models
|
|
96
|
+
if defined? Rails
|
|
97
|
+
base = "#{Rails.root}/app/models/"
|
|
98
|
+
Dir["#{base}**/*.rb"].each do |file|
|
|
99
|
+
model_name = file.gsub(/^#{base}([\w_\/\\]+)\.rb/, '\1')
|
|
100
|
+
|
|
101
|
+
next if model_name.nil?
|
|
102
|
+
next if ::ActiveRecord::Base.send(:subclasses).detect { |model|
|
|
103
|
+
model.name == model_name
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
begin
|
|
107
|
+
model_name.camelize.constantize
|
|
108
|
+
rescue LoadError
|
|
109
|
+
model_name.gsub!(/.*[\/\\]/, '').nil? ? next : retry
|
|
110
|
+
rescue NameError
|
|
111
|
+
next
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
module Xapit
|
|
2
|
+
class AbstractIndexer
|
|
3
|
+
def initialize(blueprint)
|
|
4
|
+
@blueprint = blueprint
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
def add_member(member)
|
|
8
|
+
database.add_document(document_for(member))
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def document_for(member)
|
|
12
|
+
document = Xapian::Document.new
|
|
13
|
+
document.data = "#{member.class}-#{member.id}"
|
|
14
|
+
index_text_attributes(member, document)
|
|
15
|
+
index_terms(other_terms(member), document)
|
|
16
|
+
values(member).each_with_index do |value, index|
|
|
17
|
+
document.add_value(index, value)
|
|
18
|
+
end
|
|
19
|
+
save_facet_options_for(member)
|
|
20
|
+
document
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def index_terms(terms, document)
|
|
24
|
+
terms.each do |term|
|
|
25
|
+
document.add_term(term)
|
|
26
|
+
database.add_spelling(term) if Config.spelling?
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def index_text_attributes(member, document)
|
|
31
|
+
# to be overridden by subclass
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def other_terms(member)
|
|
35
|
+
base_terms(member) + field_terms(member) + facet_terms(member)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def base_terms(member)
|
|
39
|
+
["C#{member.class}", "Q#{member.class}-#{member.id}"]
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def field_terms(member)
|
|
43
|
+
@blueprint.field_attributes.map do |name|
|
|
44
|
+
[member.send(name)].flatten.map do |value|
|
|
45
|
+
if value.kind_of? Time
|
|
46
|
+
value = value.to_i
|
|
47
|
+
elsif value.kind_of? Date
|
|
48
|
+
value = value.to_time.to_i
|
|
49
|
+
end
|
|
50
|
+
"X#{name}-#{value.to_s.downcase}"
|
|
51
|
+
end
|
|
52
|
+
end.flatten
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def facet_terms(member)
|
|
56
|
+
@blueprint.facets.map do |facet|
|
|
57
|
+
facet.identifiers_for(member).map { |id| "F#{id}" }
|
|
58
|
+
end.flatten
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# used primarily by search similar functionality
|
|
62
|
+
def text_terms(member) # REFACTORME some duplicaiton with simple indexer
|
|
63
|
+
@blueprint.text_attributes.map do |name, options|
|
|
64
|
+
content = member.send(name).to_s
|
|
65
|
+
if options[:proc]
|
|
66
|
+
options[:proc].call(content).reject(&:blank?).map(&:to_s).map(&:downcase)
|
|
67
|
+
else
|
|
68
|
+
content.scan(/\w+/u).map(&:downcase)
|
|
69
|
+
end
|
|
70
|
+
end.flatten
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def values(member)
|
|
74
|
+
facet_values(member) + sortable_values(member)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def sortable_values(member)
|
|
78
|
+
@blueprint.sortable_attributes.map do |sortable|
|
|
79
|
+
member.send(sortable).to_s.downcase
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def facet_values(member)
|
|
84
|
+
@blueprint.facets.map do |facet|
|
|
85
|
+
facet.identifiers_for(member).join("-")
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def save_facet_options_for(member)
|
|
90
|
+
@blueprint.facets.each do |facet|
|
|
91
|
+
facet.save_facet_options_for(member)
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
private
|
|
96
|
+
|
|
97
|
+
def database
|
|
98
|
+
Config.writable_database
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|