xapit 0.1.0 → 0.2.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/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
|