xapit 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|