xapit 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +27 -0
- data/Manifest +16 -1
- data/README.rdoc +29 -15
- data/Rakefile +1 -1
- data/features/facets.feature +40 -1
- data/features/finding.feature +15 -59
- data/features/sorting.feature +29 -0
- data/features/step_definitions/xapit_steps.rb +11 -3
- data/features/suggestions.feature +17 -0
- data/features/support/xapit_helpers.rb +1 -1
- data/install.rb +7 -8
- data/lib/xapit.rb +34 -2
- data/lib/xapit/adapters/abstract_adapter.rb +46 -0
- data/lib/xapit/adapters/active_record_adapter.rb +20 -0
- data/lib/xapit/adapters/data_mapper_adapter.rb +10 -0
- data/lib/xapit/collection.rb +17 -5
- data/lib/xapit/config.rb +1 -9
- data/lib/xapit/facet.rb +11 -8
- data/lib/xapit/index_blueprint.rb +9 -3
- data/lib/xapit/indexers/abstract_indexer.rb +13 -2
- data/lib/xapit/indexers/classic_indexer.rb +5 -3
- data/lib/xapit/indexers/simple_indexer.rb +15 -8
- data/lib/xapit/membership.rb +19 -1
- data/lib/xapit/query.rb +40 -15
- data/lib/xapit/query_parsers/abstract_query_parser.rb +46 -24
- data/lib/xapit/rake_tasks.rb +13 -0
- data/rails_generators/xapit/USAGE +13 -0
- data/rails_generators/xapit/templates/setup_xapit.rb +1 -0
- data/rails_generators/xapit/templates/xapit.rake +4 -0
- data/rails_generators/xapit/xapit_generator.rb +20 -0
- data/spec/spec_helper.rb +2 -2
- data/spec/xapit/adapters/active_record_adapter_spec.rb +31 -0
- data/spec/xapit/adapters/data_mapper_adapter_spec.rb +10 -0
- data/spec/xapit/facet_option_spec.rb +2 -2
- data/spec/xapit/index_blueprint_spec.rb +11 -3
- data/spec/xapit/indexers/abstract_indexer_spec.rb +37 -0
- data/spec/xapit/indexers/classic_indexer_spec.rb +9 -0
- data/spec/xapit/indexers/simple_indexer_spec.rb +22 -6
- data/spec/xapit/membership_spec.rb +16 -0
- data/spec/xapit/query_parsers/abstract_query_parser_spec.rb +21 -3
- data/spec/xapit/query_spec.rb +21 -0
- data/spec/xapit_member.rb +13 -2
- data/tasks/xapit.rake +1 -9
- data/tmp/xapiandatabase/postlist.DB +0 -0
- data/tmp/xapiandatabase/postlist.baseB +0 -0
- data/tmp/xapiandatabase/record.DB +0 -0
- data/tmp/xapiandatabase/record.baseB +0 -0
- data/tmp/xapiandatabase/spelling.DB +0 -0
- data/tmp/xapiandatabase/spelling.baseB +0 -0
- data/tmp/xapiandatabase/termlist.DB +0 -0
- data/tmp/xapiandatabase/termlist.baseB +0 -0
- data/tmp/xapiandatabase/value.DB +0 -0
- data/tmp/xapiandatabase/value.baseA +0 -0
- data/tmp/xapiandb/spelling.DB +0 -0
- data/tmp/xapiandb/spelling.baseB +0 -0
- data/xapit.gemspec +4 -4
- metadata +23 -3
@@ -0,0 +1,46 @@
|
|
1
|
+
module Xapit
|
2
|
+
# Adapters are used to support multiple ORMs (ActiveRecord, Datamapper, Sequel, etc.).
|
3
|
+
# It abstracts out all find calls so they can be handled differently for each ORM.
|
4
|
+
# To create your own adapter, subclass AbstractAdapter and override the placeholder methods.
|
5
|
+
# See ActiveRecordAdapter for an example.
|
6
|
+
class AbstractAdapter
|
7
|
+
def self.inherited(subclass)
|
8
|
+
@subclasses ||= []
|
9
|
+
@subclasses << subclass
|
10
|
+
end
|
11
|
+
|
12
|
+
# Returns all adapter classes.
|
13
|
+
def self.subclasses
|
14
|
+
@subclasses
|
15
|
+
end
|
16
|
+
|
17
|
+
# Sets the @target instance, this is the class the adapter needs to forward
|
18
|
+
# its messages to.
|
19
|
+
def initialize(target)
|
20
|
+
@target = target
|
21
|
+
end
|
22
|
+
|
23
|
+
# Used to determine if the given adapter should be used for the passed in class.
|
24
|
+
# Usually one will see if it inherits from another class (ActiveRecord::Base)
|
25
|
+
def self.for_class?(member_class)
|
26
|
+
raise "To be implemented in subclass"
|
27
|
+
end
|
28
|
+
|
29
|
+
# Fetch a single record by the given id.
|
30
|
+
def find_single(id)
|
31
|
+
raise "To be implemented in subclass"
|
32
|
+
end
|
33
|
+
|
34
|
+
# Fetch multiple records from the passed in array of ids.
|
35
|
+
def find_multiple(ids)
|
36
|
+
raise "To be implemented in subclass"
|
37
|
+
end
|
38
|
+
|
39
|
+
# Iiterate through all records using the given parameters.
|
40
|
+
# It should yield to the block and pass in each record individually.
|
41
|
+
# The args are the same as those passed from the XapitMember#xapit call.
|
42
|
+
def find_each(*args, &block)
|
43
|
+
raise "To be implemented in subclass"
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Xapit
|
2
|
+
# This adapter is used for all ActiveRecord models. See AbstractAdapter for details.
|
3
|
+
class ActiveRecordAdapter < AbstractAdapter
|
4
|
+
def self.for_class?(member_class)
|
5
|
+
member_class.ancestors.map(&:to_s).include? "ActiveRecord::Base"
|
6
|
+
end
|
7
|
+
|
8
|
+
def find_single(id)
|
9
|
+
@target.find(id)
|
10
|
+
end
|
11
|
+
|
12
|
+
def find_multiple(ids)
|
13
|
+
@target.find(ids)
|
14
|
+
end
|
15
|
+
|
16
|
+
def find_each(*args, &block)
|
17
|
+
@target.find_each(*args, &block)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
module Xapit
|
2
|
+
# This adapter is used for all DataMapper models. See AbstractAdapter for details.
|
3
|
+
class DataMapperAdapter < AbstractAdapter
|
4
|
+
def self.for_class?(member_class)
|
5
|
+
member_class.ancestors.map(&:to_s).include? "DataMapper::Resource"
|
6
|
+
end
|
7
|
+
|
8
|
+
# TODO override the rest of the methods here...
|
9
|
+
end
|
10
|
+
end
|
data/lib/xapit/collection.rb
CHANGED
@@ -18,7 +18,7 @@ module Xapit
|
|
18
18
|
collection = new(member.class, *args)
|
19
19
|
indexer = SimpleIndexer.new(member.class.xapit_index_blueprint)
|
20
20
|
terms = indexer.text_terms(member) + indexer.field_terms(member)
|
21
|
-
collection.base_query.and_query(
|
21
|
+
collection.base_query.and_query(terms, :or)
|
22
22
|
collection.base_query.not_query("Q#{member.class}-#{member.id}")
|
23
23
|
collection
|
24
24
|
end
|
@@ -103,7 +103,7 @@ module Xapit
|
|
103
103
|
# Xapit::Facet objects matching this search query. See class for details.
|
104
104
|
def facets
|
105
105
|
all_facets.select do |facet|
|
106
|
-
facet.options.size >
|
106
|
+
facet.options.size > 0
|
107
107
|
end
|
108
108
|
end
|
109
109
|
|
@@ -118,7 +118,7 @@ module Xapit
|
|
118
118
|
# If you set :breadcrumb_facets option to true in Config#setup the link will drop leftover facets
|
119
119
|
# instead of removing the current one. This makes it easy to add a breadcrumb style interface.
|
120
120
|
#
|
121
|
-
#
|
121
|
+
# Xapit.setup(:breadcrumb_facets => true)
|
122
122
|
# <% for option in @articles.applied_facet_options %>
|
123
123
|
# <%= link_to h(option.name), :overwrite_params => { :facets => option } %> >
|
124
124
|
# <% end %>
|
@@ -153,10 +153,22 @@ module Xapit
|
|
153
153
|
@query_parser.query.matchset(options)
|
154
154
|
end
|
155
155
|
|
156
|
+
# TODO this could use some refactoring
|
157
|
+
# See issue #11 for why this is so complex.
|
156
158
|
def fetch_results(options = {})
|
157
|
-
matchset(options).matches
|
159
|
+
matches = matchset(options).matches
|
160
|
+
records_by_class = {}
|
161
|
+
matches.each do |match|
|
158
162
|
class_name, id = match.document.data.split('-')
|
159
|
-
|
163
|
+
records_by_class[class_name] ||= []
|
164
|
+
records_by_class[class_name] << id
|
165
|
+
end
|
166
|
+
records_by_class.each do |class_name, ids|
|
167
|
+
records_by_class[class_name] = class_name.constantize.xapit_adapter.find_multiple(ids)
|
168
|
+
end
|
169
|
+
matches.map do |match|
|
170
|
+
class_name, id = match.document.data.split('-')
|
171
|
+
member = records_by_class[class_name].detect { |m| m.id == id.to_i }
|
160
172
|
member.xapit_relevance = match.percent
|
161
173
|
member
|
162
174
|
end
|
data/lib/xapit/config.rb
CHANGED
@@ -4,15 +4,7 @@ module Xapit
|
|
4
4
|
class << self
|
5
5
|
attr_reader :options
|
6
6
|
|
7
|
-
#
|
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
|
-
#
|
7
|
+
# See Xapit#setup
|
16
8
|
def setup(options = {})
|
17
9
|
if @options && options[:database_path] != @options[:database_path]
|
18
10
|
@database = nil
|
data/lib/xapit/facet.rb
CHANGED
@@ -23,19 +23,22 @@ module Xapit
|
|
23
23
|
# Xapit::FacetOption objects for this facet. This only lists the ones which match the current query.
|
24
24
|
def options
|
25
25
|
matching_identifiers.map do |identifier, count|
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
26
|
+
if count < @query.count
|
27
|
+
option = FacetOption.find(identifier)
|
28
|
+
if option.facet.attribute == @blueprint.attribute
|
29
|
+
option.count = count
|
30
|
+
option.existing_facet_identifiers = @existing_facet_identifiers
|
31
|
+
option
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end.compact.sort_by(&:name)
|
31
35
|
end
|
32
36
|
|
33
37
|
def matching_identifiers
|
34
38
|
result = {}
|
35
39
|
matches.each do |match|
|
36
|
-
|
37
|
-
|
38
|
-
@blueprint.identifiers_for(record).each do |identifier|
|
40
|
+
identifiers = match.document.terms.map(&:term).grep(/^F/).map { |t| t[1..-1] }
|
41
|
+
identifiers.each do |identifier|
|
39
42
|
unless existing_facet_identifiers.include? identifier
|
40
43
|
result[identifier] ||= 0
|
41
44
|
result[identifier] += (match.collapse_count + 1)
|
@@ -72,19 +72,25 @@ module Xapit
|
|
72
72
|
end
|
73
73
|
|
74
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
|
75
|
+
# You will likely want to call Xapit.remove_database before this.
|
76
76
|
def index_all
|
77
|
-
@member_class.find_each(*@args) do |member|
|
77
|
+
@member_class.xapit_adapter.find_each(*@args) do |member|
|
78
78
|
@indexer.add_member(member)
|
79
79
|
end
|
80
80
|
end
|
81
81
|
|
82
|
-
def
|
82
|
+
def position_of_sortable(sortable_attribute)
|
83
83
|
index = sortable_attributes.map(&:to_s).index(sortable_attribute.to_s)
|
84
84
|
raise "Unable to find indexed sortable attribute \"#{sortable_attribute}\" in #{@member_class} sortable attributes: #{sortable_attributes.inspect}" if index.nil?
|
85
85
|
index + facets.size
|
86
86
|
end
|
87
87
|
|
88
|
+
def position_of_field(field_attribute)
|
89
|
+
index = field_attributes.map(&:to_s).index(field_attribute.to_s)
|
90
|
+
raise "Unable to find indexed field attribute \"#{field_attribute}\" in #{@member_class} field attributes: #{field_attributes.inspect}" if index.nil?
|
91
|
+
index + facets.size + sortable_attributes.size
|
92
|
+
end
|
93
|
+
|
88
94
|
private
|
89
95
|
|
90
96
|
# Make sure all models are loaded - without reloading any that
|
@@ -71,12 +71,23 @@ module Xapit
|
|
71
71
|
end
|
72
72
|
|
73
73
|
def values(member)
|
74
|
-
facet_values(member) + sortable_values(member)
|
74
|
+
facet_values(member) + sortable_values(member) + field_values(member)
|
75
75
|
end
|
76
76
|
|
77
77
|
def sortable_values(member)
|
78
78
|
@blueprint.sortable_attributes.map do |sortable|
|
79
|
-
member.send(sortable)
|
79
|
+
value = member.send(sortable)
|
80
|
+
value = value.first if value.kind_of? Array
|
81
|
+
Xapit.serialize_value(value)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
# TODO remove duplication with sortable_values
|
86
|
+
def field_values(member)
|
87
|
+
@blueprint.field_attributes.map do |sortable|
|
88
|
+
value = member.send(sortable)
|
89
|
+
value = value.first if value.kind_of? Array
|
90
|
+
Xapit.serialize_value(value)
|
80
91
|
end
|
81
92
|
end
|
82
93
|
|
@@ -3,11 +3,13 @@ module Xapit
|
|
3
3
|
def index_text_attributes(member, document)
|
4
4
|
term_generator.document = document
|
5
5
|
@blueprint.text_attributes.each do |name, options|
|
6
|
-
content = member.send(name)
|
6
|
+
content = member.send(name)
|
7
7
|
if options[:proc]
|
8
|
-
index_terms(options[:proc].call(content).reject(&:blank?).map(&:to_s).map(&:downcase), document)
|
8
|
+
index_terms(options[:proc].call(content.to_s).reject(&:blank?).map(&:to_s).map(&:downcase), document)
|
9
|
+
elsif content.kind_of? Array
|
10
|
+
index_terms(content.reject(&:blank?).map(&:to_s).map(&:downcase), document)
|
9
11
|
else
|
10
|
-
term_generator.index_text(content)
|
12
|
+
term_generator.index_text(content.to_s)
|
11
13
|
end
|
12
14
|
end
|
13
15
|
end
|
@@ -6,21 +6,28 @@ module Xapit
|
|
6
6
|
document.add_term(term, options[:weight] || 1)
|
7
7
|
database.add_spelling(term) if Config.spelling?
|
8
8
|
end
|
9
|
+
if Config.stemming
|
10
|
+
stemmed_terms_for_attribute(member, name, options).each do |term|
|
11
|
+
document.add_term(term, options[:weight] || 1)
|
12
|
+
end
|
13
|
+
end
|
9
14
|
end
|
10
15
|
end
|
11
16
|
|
12
|
-
def
|
13
|
-
|
14
|
-
|
15
|
-
end
|
17
|
+
def stemmed_terms_for_attribute(member, name, options)
|
18
|
+
terms_for_attribute(member, name, options).map do |term|
|
19
|
+
"Z#{stemmer.call(term)}"
|
20
|
+
end
|
16
21
|
end
|
17
22
|
|
18
|
-
def
|
19
|
-
content = member.send(name)
|
23
|
+
def terms_for_attribute(member, name, options)
|
24
|
+
content = member.send(name)
|
20
25
|
if options[:proc]
|
21
|
-
options[:proc].call(content).reject(&:blank?).map(&:to_s).map(&:downcase)
|
26
|
+
options[:proc].call(content.to_s).reject(&:blank?).map(&:to_s).map(&:downcase)
|
27
|
+
elsif content.kind_of? Array
|
28
|
+
content.reject(&:blank?).map(&:to_s).map(&:downcase)
|
22
29
|
else
|
23
|
-
content.scan(/\w+/u).map(&:downcase)
|
30
|
+
content.to_s.scan(/\w+/u).map(&:downcase)
|
24
31
|
end
|
25
32
|
end
|
26
33
|
|
data/lib/xapit/membership.rb
CHANGED
@@ -73,6 +73,12 @@ module Xapit
|
|
73
73
|
# # search based on indexed fields
|
74
74
|
# @articles = Article.search("phone", :conditions => { :category_id => params[:category_id] })
|
75
75
|
#
|
76
|
+
# # search for multiple negative conditions (doesn't match 3, 5, or 8)
|
77
|
+
# @articles = Article.search(:not_conditions => { :category_id => [3, 5, 8] })
|
78
|
+
#
|
79
|
+
# # search for range of conditions by number
|
80
|
+
# @articles = Article.search(:conditions => { :released_at => 2.years.ago..Time.now })
|
81
|
+
#
|
76
82
|
# # manually sort based on any number of indexed fields, sort defaults to most relevant
|
77
83
|
# @articles = Article.search("phone", :order => [:category_id, :id], :descending => true)
|
78
84
|
#
|
@@ -90,7 +96,19 @@ module Xapit
|
|
90
96
|
def xapit_index_blueprint
|
91
97
|
@xapit_index_blueprint
|
92
98
|
end
|
93
|
-
|
99
|
+
|
100
|
+
# The Xapit::AbstractAdapter used to perform database queries on.
|
101
|
+
def xapit_adapter
|
102
|
+
@xapit_adapter ||= begin
|
103
|
+
adapter_class = AbstractAdapter.subclasses.detect { |a| a.for_class?(self) }
|
104
|
+
if adapter_class
|
105
|
+
adapter_class.new(self)
|
106
|
+
else
|
107
|
+
raise "Unable to find Xapit adapter for class #{self.name}"
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
94
112
|
# Finds a Xapit::FacetBlueprint for the given attribute.
|
95
113
|
def xapit_facet_blueprint(attribute)
|
96
114
|
result = xapit_index_blueprint.facets.detect { |f| f.attribute.to_s == attribute.to_s }
|
data/lib/xapit/query.rb
CHANGED
@@ -5,24 +5,21 @@ module Xapit
|
|
5
5
|
class Query
|
6
6
|
attr_reader :default_options, :xapian_query
|
7
7
|
|
8
|
-
def initialize(
|
9
|
-
@xapian_query = build_xapian_query(
|
8
|
+
def initialize(*args)
|
9
|
+
@xapian_query = build_xapian_query(*args)
|
10
10
|
@default_options = { :offset => 0, :sort_descending => false }
|
11
11
|
end
|
12
12
|
|
13
|
-
def and_query(
|
14
|
-
|
15
|
-
self
|
13
|
+
def and_query(*args)
|
14
|
+
merge_query(:and, *args)
|
16
15
|
end
|
17
16
|
|
18
|
-
def or_query(
|
19
|
-
|
20
|
-
self
|
17
|
+
def or_query(*args)
|
18
|
+
merge_query(:or, *args)
|
21
19
|
end
|
22
20
|
|
23
|
-
def not_query(
|
24
|
-
|
25
|
-
self
|
21
|
+
def not_query(*args)
|
22
|
+
merge_query(:not, *args)
|
26
23
|
end
|
27
24
|
|
28
25
|
def matchset(options = {})
|
@@ -51,11 +48,39 @@ module Xapit
|
|
51
48
|
|
52
49
|
private
|
53
50
|
|
54
|
-
def
|
55
|
-
|
56
|
-
|
51
|
+
def merge_query(operator, *args)
|
52
|
+
@xapian_query = Xapian::Query.new(xapian_operator(operator), @xapian_query, build_xapian_query(*args)) unless args.first.blank?
|
53
|
+
self
|
54
|
+
end
|
55
|
+
|
56
|
+
def build_xapian_query(query, operator = :and)
|
57
|
+
extract_queries(query, operator).inject(nil) do |query, extra_query|
|
58
|
+
if query
|
59
|
+
extra_query = extra_query.xapian_query if extra_query.respond_to? :xapian_query
|
60
|
+
Xapian::Query.new(xapian_operator(operator), query, extra_query)
|
61
|
+
else
|
62
|
+
extra_query = extra_query.xapian_query if extra_query.respond_to? :xapian_query
|
63
|
+
extra_query
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def extract_queries(query, operator)
|
69
|
+
queries = [query].flatten
|
70
|
+
terms = queries.select { |q| q.kind_of? String }
|
71
|
+
if terms.empty?
|
72
|
+
queries
|
57
73
|
else
|
58
|
-
Xapian::Query.new(
|
74
|
+
(queries - terms) + [Xapian::Query.new(xapian_operator(operator), terms)]
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def xapian_operator(operator)
|
79
|
+
case operator
|
80
|
+
when :and then Xapian::Query::OP_AND
|
81
|
+
when :or then Xapian::Query::OP_OR
|
82
|
+
when :not then Xapian::Query::OP_AND_NOT
|
83
|
+
else raise "Unknown Xapian operator #{operator}"
|
59
84
|
end
|
60
85
|
end
|
61
86
|
end
|
@@ -10,10 +10,10 @@ module Xapit
|
|
10
10
|
end
|
11
11
|
|
12
12
|
def query
|
13
|
-
if (@search_text.split + condition_terms + facet_terms).empty?
|
13
|
+
if (@search_text.split + condition_terms + not_condition_terms + facet_terms).empty?
|
14
14
|
base_query
|
15
15
|
else
|
16
|
-
@query ||= base_query.and_query(xapian_query_from_text(@search_text)).and_query(condition_terms + facet_terms)
|
16
|
+
@query ||= base_query.and_query(xapian_query_from_text(@search_text)).and_query(condition_terms + facet_terms).not_query(not_condition_terms)
|
17
17
|
end
|
18
18
|
end
|
19
19
|
|
@@ -34,10 +34,10 @@ module Xapit
|
|
34
34
|
index = @member_class.xapit_index_blueprint
|
35
35
|
if @options[:order].kind_of? Array
|
36
36
|
@options[:order].map do |attribute|
|
37
|
-
index.
|
37
|
+
index.position_of_sortable(attribute)
|
38
38
|
end
|
39
39
|
else
|
40
|
-
[index.
|
40
|
+
[index.position_of_sortable(@options[:order])]
|
41
41
|
end
|
42
42
|
end
|
43
43
|
end
|
@@ -47,7 +47,7 @@ module Xapit
|
|
47
47
|
end
|
48
48
|
|
49
49
|
def initial_query
|
50
|
-
query = Query.new(
|
50
|
+
query = Query.new(initial_query_strings, :or)
|
51
51
|
query.default_options[:offset] = offset
|
52
52
|
query.default_options[:limit] = per_page
|
53
53
|
query.default_options[:sort_by_values] = sort_by_values
|
@@ -68,18 +68,11 @@ module Xapit
|
|
68
68
|
end
|
69
69
|
|
70
70
|
def condition_terms
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
value = value.to_time.to_i
|
77
|
-
end
|
78
|
-
"X#{name}-#{value.to_s.downcase}"
|
79
|
-
end
|
80
|
-
else
|
81
|
-
[]
|
82
|
-
end
|
71
|
+
condition_terms_from_hash(@options[:conditions])
|
72
|
+
end
|
73
|
+
|
74
|
+
def not_condition_terms
|
75
|
+
condition_terms_from_hash(@options[:not_conditions])
|
83
76
|
end
|
84
77
|
|
85
78
|
def facet_terms
|
@@ -97,19 +90,48 @@ module Xapit
|
|
97
90
|
end
|
98
91
|
|
99
92
|
def spelling_suggestion
|
100
|
-
raise "Spelling has been disabled. Enable spelling in Xapit
|
101
|
-
if @search_text.
|
93
|
+
raise "Spelling has been disabled. Enable spelling in Xapit.setup." unless Config.spelling?
|
94
|
+
if [@search_text, *@search_text.scan(/\w+/)].all? { |term| term_suggestion(term).nil? }
|
102
95
|
nil
|
103
96
|
else
|
97
|
+
return term_suggestion(@search_text) unless term_suggestion(@search_text).blank?
|
104
98
|
@search_text.downcase.gsub(/\w+/) do |term|
|
105
|
-
|
106
|
-
|
107
|
-
|
99
|
+
term_suggestion(term) || term
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def term_suggestion(term)
|
105
|
+
suggestion = Config.database.get_spelling_suggestion(term.downcase)
|
106
|
+
suggestion.blank? ? nil : suggestion
|
107
|
+
end
|
108
|
+
|
109
|
+
private
|
110
|
+
|
111
|
+
def condition_terms_from_hash(conditions)
|
112
|
+
if conditions
|
113
|
+
conditions.map do |name, value|
|
114
|
+
if value.kind_of? Array
|
115
|
+
Query.new(value.map { |v| condition_term(name, v) }, :or)
|
116
|
+
elsif value.kind_of?(Range) && @member_class
|
117
|
+
position = @member_class.xapit_index_blueprint.position_of_field(name)
|
118
|
+
Xapian::Query.new(Xapian::Query::OP_VALUE_RANGE, position, Xapit.serialize_value(value.begin), Xapit.serialize_value(value.end))
|
108
119
|
else
|
109
|
-
|
120
|
+
condition_term(name, value)
|
110
121
|
end
|
111
|
-
end
|
122
|
+
end.flatten
|
123
|
+
else
|
124
|
+
[]
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
def condition_term(name, value)
|
129
|
+
if value.kind_of? Time
|
130
|
+
value = value.to_i
|
131
|
+
elsif value.kind_of? Date
|
132
|
+
value = value.to_time.to_i
|
112
133
|
end
|
134
|
+
"X#{name}-#{value.to_s.downcase}"
|
113
135
|
end
|
114
136
|
end
|
115
137
|
end
|