search_do 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 ADDED
@@ -0,0 +1 @@
1
+ - added html_snippet (attr_accessor :html_snippet)
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2005 Rick Olson
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,63 @@
1
+ = search_do
2
+ * Library for fulltext search integration with active record.
3
+ * Build to support multiple search Backends
4
+ * acts_as_searchable Successor
5
+
6
+
7
+ == Pre-requisites
8
+ A working Hyper Estraier instance, setup instructions:
9
+
10
+ * Setup Instructions: http://pragmatig.wordpress.com/2008/05/06/getting-started-with-acts_as_searchable-on-ubuntu/
11
+ * In-depth Documentation: http://hyperestraier.sourceforge.net/nguide-en.html
12
+ * rake search:node:create MODEL=User RAILS_ENV=production - for every model/environment you use
13
+
14
+
15
+ == Install
16
+ * script/plugin install git://github.com/grosser/search_do.git
17
+ * install will_paginate to use "paginate_by_fulltext_search" (Instruction: http://github.com/mislav/will_paginate/wikis/installation)
18
+
19
+
20
+ == Usage
21
+
22
+ #MODEL
23
+ class User < ActiveRecord::Base
24
+ acts_as_searchable(
25
+ #fields the will be found in fulltext search
26
+ :searchable_fields => [:name,:website,:city,:about],
27
+ #fields used for attribute search/ordering
28
+ :attributes => {:name=>nil,:city=>nil,:country=>nil,:age=>nil}
29
+ )
30
+ attr_accessor :html_snippet #add this to get html snippets on your results (see below)
31
+ end
32
+
33
+ #SEARCH
34
+ Users who:
35
+ - contain 'hello' in any of their searchable fields
36
+ - whose website attribute contains 'www' (contains search for strings)
37
+ - whose age is 1 (exact match for numbers/dates)
38
+ - sorted by age ASC
39
+ @results = User.paginate_by_fulltext_search('hello',:attributes=>{:website=>'www',:age=>1},:order=>'age ASC',:page=>1,:per_page=>20)
40
+
41
+ (Same can be done without pagination: User.fulltext_search)
42
+
43
+ #SNIPPETS
44
+ Each record found with a fulltext-search (not a attribute-only search) contains a snippet
45
+ of the surrounding where the phrase was found.
46
+
47
+ User.fulltext_search('hello') => user.html_snippet == "id like to say <b>hello</b> to my fellow students"
48
+
49
+ NOTE: html_snippet will not contain HTML except for the <b>, so there is no need to escape it.
50
+
51
+
52
+ == Hyperestraier Features
53
+ - Phrase search, regular expressions, attribute search, and similarity search
54
+ - Snippet retrival
55
+ - UTF8 support
56
+ - Web interface
57
+ - Built in P2P clustering of index servers
58
+
59
+
60
+ == Origin
61
+ Original is written by scoop see
62
+ * http://github.com/scoop/acts_as_searchable/tree/master
63
+ * http://poocs.net/2006/4/6/introducing-acts-as-searchable
data/Rakefile ADDED
@@ -0,0 +1,47 @@
1
+ require 'rake/rdoctask'
2
+ require 'spec'
3
+
4
+ desc 'Default: run specs_all.'
5
+ task :default => :spec_all
6
+
7
+ require 'spec/rake/spectask'
8
+ Spec::Rake::SpecTask.new {|t| t.spec_opts = ['--color']}
9
+
10
+ desc "Run specs both AR-latest and AR-2.0.x"
11
+ task :spec_all do
12
+ ar20xs = (::Gem.source_index.find_name("activerecord", "<2.1") & \
13
+ ::Gem.source_index.find_name("activerecord", ">=2.0"))
14
+ if ar20xs.empty?
15
+ Rake::Task[:spec].invoke
16
+ else
17
+ ar20 = ar20xs.sort_by(&:version).last
18
+ system("rake spec")
19
+ system("rake spec AR=#{ar20.version}")
20
+ end
21
+ end
22
+
23
+ desc 'Generate documentation for the acts_as_searchable plugin.'
24
+ Rake::RDocTask.new(:rdoc) do |doc|
25
+ doc.rdoc_dir = 'rdoc'
26
+ doc.title = 'SearchDo'
27
+ doc.options << '--line-numbers' << '--inline-source'
28
+ doc.rdoc_files.include('README.rdoc')
29
+ doc.rdoc_files.include('lib/**/*.rb')
30
+ end
31
+
32
+ begin
33
+ require 'jeweler'
34
+ project_name = 'search_do'
35
+
36
+ Jeweler::Tasks.new do |gem|
37
+ gem.name = project_name
38
+ gem.summary = "AR: Hyperestraier integration"
39
+ gem.email = "moronatural@gmail.com"
40
+ gem.homepage = "http://github.com/grosser/#{project_name}"
41
+ gem.authors = ["MOROHASHI Kyosuke"]
42
+ end
43
+
44
+ Jeweler::GemcutterTasks.new
45
+ rescue LoadError
46
+ puts "Jeweler, or one of its dependencies, is not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
47
+ end
data/TESTING ADDED
@@ -0,0 +1,6 @@
1
+ If you have test errors by HyperEstraier, change cache settings:
2
+
3
+ edit '${estraier_index_dir}/_conf' and set 'cachernum' parameter to 0.
4
+ (Ubuntu: /var/lib/hyperestraier/estmaster/_conf)
5
+ then restart hyperestraier
6
+ (Ubuntu: sudo /etc/init.d/hyperestraier restart)
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.2.0
@@ -0,0 +1,13 @@
1
+ # a module to include in your controllers
2
+ # params:
3
+ # q => search word
4
+ # search => {name=>something}
5
+ # order => name DESC
6
+ # define the constant PER_PAGE in your controller so it can be used here
7
+ module Modules
8
+ module HeSearch
9
+ def current_objects
10
+ @current_objects ||= current_model.fulltext_results(params[:q],:page=>params[:page],:per_page=>self.class.const_get('PER_PAGE'),:attributes=>params[:search],:order=>params[:order])
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,20 @@
1
+ # this is a person indexed by hyperestraier
2
+ # it should have name/city/country:string about:text online:boolean
3
+ class Person < ActiveRecord::Base
4
+ acts_as_searchable(
5
+ :ignore_timestamp=>true, #do not add timestamps to search_backend
6
+ :searchable_fields => [:name,:website,:city,:about],
7
+ :attributes => {"@title"=>:name,:name=>nil,:city=>nil,:country=>nil}#attribute results
8
+ #add @title for higher search weight on this attribute
9
+ )
10
+ attr_accessor :html_snippet #so we get nice html snippets for our search results...
11
+
12
+ #customize the length/widths of the fetched snippets
13
+ search_backend.connection.set_snippet_width(150,20,20)#total,beginning(>0!),around
14
+
15
+ #only index a person when it is online
16
+ def add_to_index_with_online_check
17
+ add_to_index_without_online_check if online?
18
+ end
19
+ alias_method_chain :add_to_index, :online_check
20
+ end
data/init.rb ADDED
@@ -0,0 +1 @@
1
+ require 'search_do'
@@ -0,0 +1,47 @@
1
+ require 'net/http'
2
+ require 'webrick/httputils'
3
+
4
+ class EstraierAdmin
5
+ RequestFailed = Class.new(StandardError)
6
+ CREATE_NODE_ACTION = 8
7
+ DELETE_NODE_ACTION = 9
8
+ DEFAULT_CONFIG = {
9
+ :host=> "localhost",
10
+ :port=>1978,
11
+ :user=>"admin",
12
+ :password=>"admin"
13
+ }.freeze
14
+
15
+ # requires host
16
+ def initialize(config={})
17
+ @config = DEFAULT_CONFIG.dup
18
+ config.each { |k, v| @config[k.to_sym] = v }
19
+ end
20
+
21
+ def create_node(name, label = nil)
22
+ label ||= name
23
+ request_or_raise(:name=>name, :action=>CREATE_NODE_ACTION, :label=>label)
24
+ return true
25
+ end
26
+
27
+ def delete_node(name)
28
+ request_or_raise(:name=>name, :action=>DELETE_NODE_ACTION, :sure=>1)
29
+ return true
30
+ end
31
+
32
+ private
33
+ def request_or_raise(params, path="/master_ui")
34
+ req = Net::HTTP::Post.new(path)
35
+ req.basic_auth(@config[:user], @config[:password])
36
+ res = Net::HTTP.start(@config[:host], @config[:port]) do |http|
37
+ http.request(req, build_body(params))
38
+ end
39
+ raise if res.code.to_i >= 400
40
+ return true
41
+ end
42
+
43
+ def build_body(params={})
44
+ params.map{|k,v| "#{k}=#{WEBrick::HTTPUtils.escape_form(v.to_s)}" }.join('&')
45
+ end
46
+ end
47
+
@@ -0,0 +1,61 @@
1
+ require 'search_do/backends/hyper_estraier'
2
+
3
+ module SearchDo::Backends
4
+ module HyperEstraier::EstraierPureExtention
5
+ def self.included(base)
6
+ [Node,Condition,NodeResult,ResultDocument].each do |klas|
7
+ basic_class_name = klas.to_s.split('::').last
8
+ base.const_get(basic_class_name).send(:include, klas)
9
+ end
10
+ end
11
+
12
+ module Node
13
+ def list
14
+ return false unless @url
15
+ turl = @url + "/list"
16
+ reqheads = [ "Content-Type: application/x-www-form-urlencoded" ]
17
+ reqheads.push("Authorization: Basic " + Utility::base_encode(@auth)) if @auth
18
+ reqbody = ""
19
+ resbody = StringIO::new
20
+ rv = Utility::shuttle_url(turl, @pxhost, @pxport, @timeout, reqheads, reqbody, nil, resbody)
21
+ @status = rv
22
+ return nil if rv != 200
23
+ lines = resbody.string.split(/\n/)
24
+ lines.collect { |l| val = l.split(/\t/) and { :id => val[0], :uri => val[1], :digest => val[2] } }
25
+ end
26
+ end
27
+
28
+ module Condition
29
+ def to_s
30
+ "phrase: %s, attrs: %s, max: %s, options: %s, order: %s, skip: %s" % [ phrase, attrs * ', ', max, options, order, skip ]
31
+ end
32
+ end
33
+
34
+ module NodeResult
35
+ include Enumerable
36
+ def each(&block)
37
+ (0...doc_num).each{|i| yield get_doc(i) }
38
+ end
39
+
40
+ def docs; map{|e| e } ; end
41
+
42
+ def first_doc; get_doc(0); end
43
+ end
44
+
45
+ module ResultDocument
46
+ #wacky snippet string parsed into a nice array of lines
47
+ #where the found word (here: a search for bob) is marked to be highlighted
48
+ #[["hallo my name is ",false],["bob",true],[" butcher",false]]
49
+ #output does not contain HTML
50
+ def snippet_a
51
+ snip = @snippet.sub(/^\.+/,"\n")#first ... can be understood as \n
52
+ snip.split("\n").reject{|x|x==''}.map do |part|
53
+ part.include?("\t") ? [part.split("\t")[0],true] : [part,false]
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
59
+
60
+ ::EstraierPure.send(:include, SearchDo::Backends::HyperEstraier::EstraierPureExtention)
61
+
@@ -0,0 +1,213 @@
1
+ #!/usr/bin/env ruby
2
+ require 'vendor/estraierpure'
3
+ require 'search_do/utils'
4
+
5
+ module SearchDo
6
+ module Backends
7
+ class HyperEstraier
8
+ SYSTEM_ATTRIBUTES = %w( uri digest cdate mdate adate title author type lang genre size weight misc )
9
+
10
+ attr_reader :connection
11
+ attr_accessor :node_name
12
+
13
+ DEFAULT_CONFIG = {
14
+ 'host' => 'localhost',
15
+ 'port' => 1978,
16
+ 'user' => 'admin',
17
+ 'password' => 'admin',
18
+ 'backend' => 'hyper_estraier',
19
+ }.freeze
20
+
21
+ # FIXME use URI
22
+ def initialize(ar_class, config = {})
23
+ @ar_class = ar_class
24
+ config = DEFAULT_CONFIG.merge(config)
25
+ self.node_name = calculate_node_name(config)
26
+
27
+ @connection = EstraierPure::Node.new
28
+ @connection.set_url("http://#{config['host']}:#{config['port']}/node/#{self.node_name}")
29
+ @connection.set_auth(config['user'], config['password'])
30
+ end
31
+
32
+ def index
33
+ cond = EstraierPure::Condition::new
34
+ cond.add_attr("db_id NUMGT 0")
35
+ result = raw_search(cond, 1)
36
+ result ? result.docs : []
37
+ end
38
+
39
+ def search_by_db_id(id)
40
+ cond = EstraierPure::Condition::new
41
+ cond.set_options(EstraierPure::Condition::SIMPLE | EstraierPure::Condition::USUAL)
42
+ cond.add_attr("db_id NUMEQ #{id}")
43
+
44
+ result = raw_search(cond, 1)
45
+ return nil if result.nil? || result.doc_num.zero?
46
+ result.first_doc
47
+ end
48
+
49
+ def count(query, options={})
50
+ cond = build_fulltext_condition(query, options.merge(:count=>true))
51
+ benchmark(" #{@ar_class.to_s} count fulltext, Cond: #{cond.to_s}") do
52
+ r = raw_search(cond, 1);
53
+ r.doc_num rescue 0
54
+ end
55
+ end
56
+
57
+ def search_all(query, options = {})
58
+ cond = build_fulltext_condition(query, options)
59
+
60
+ benchmark(" #{@ar_class.to_s} fulltext search, Cond: #{cond.to_s}") do
61
+ result = raw_search(cond, 1);
62
+ result ? result.docs : []
63
+ end
64
+ end
65
+
66
+ def search_all_ids(query, options ={})
67
+ search_all_ids_and_raw(query,options).map {|row|row[0]}
68
+ end
69
+
70
+ def search_all_ids_and_raw(query, options ={})
71
+ search_all(query, options).map{|doc| [doc.attr("db_id").to_i,doc] }
72
+ end
73
+
74
+ def add_to_index(texts, attrs)
75
+ doc = EstraierPure::Document::new
76
+ texts.reject(&:blank?).each{|t| doc.add_text(textise(t)) }
77
+ attrs.reject{|k,v| v.blank?}.each{|k,v| doc.add_attr(attribute_name(k), textise(v)) }
78
+
79
+ log = " #{@ar_class.name} [##{attrs["db_id"]}] Adding to index"
80
+ benchmark(log){ @connection.put_doc(doc) }
81
+ end
82
+
83
+ def remove_from_index(db_id)
84
+ return unless doc = search_by_db_id(db_id)
85
+ log = " #{@ar_class.name} [##{db_id}] Removing from index"
86
+ benchmark(log){ delete_from_index(doc) }
87
+ end
88
+
89
+ def clear_index!
90
+ benchmark(" Deleting all index"){ index.each { |d| delete_from_index(d) } }
91
+ end
92
+
93
+ def raw(id)
94
+ condition = build_fulltext_condition
95
+ add_attributes_to "db_id STREQ #{id}", condition
96
+ result = connection.search(condition, 1)
97
+ return unless result and result.doc_num > 0
98
+ result.docs[0]
99
+ end
100
+
101
+ private
102
+
103
+ def raw_search(cond, num)
104
+ @connection.search(cond, num)
105
+ end
106
+
107
+ def textise(obj) # :nodoc:
108
+ case obj
109
+ when Time then obj.iso8601
110
+ when Date, DateTime then obj.to_s # Date#to_s equals iso8601
111
+ else obj.to_s
112
+ end
113
+ end
114
+
115
+ def build_fulltext_condition(query='', options = {})
116
+ options = {:limit => 100, :offset => 0}.merge(options)
117
+ # options.assert_valid_keys(VALID_FULLTEXT_OPTIONS)
118
+
119
+ cond = EstraierPure::Condition::new
120
+ cond.set_options(EstraierPure::Condition::SIMPLE | EstraierPure::Condition::USUAL)
121
+
122
+ cond.set_phrase Utils.cleanup_query(query)
123
+
124
+ #add a always-true condition to trigger find all
125
+ add_attributes_to("db_id NUMGT 0",cond) if query.blank?
126
+ add_attributes_to(options[:attributes],cond)
127
+ cond.set_max options[:limit] unless options[:count]
128
+ cond.set_skip options[:offset]
129
+ cond.set_order translate_order_to_he(options[:order]) unless options[:order].blank?
130
+ return cond
131
+ end
132
+
133
+ def add_attributes_to(attributes,condition)
134
+ case attributes
135
+ when String,nil then condition.add_attr attributes unless attributes.blank?
136
+ when Array then
137
+ attributes.reject(&:blank?).each do |attr|
138
+ condition.add_attr attr
139
+ end
140
+ when Hash then
141
+ attributes.each do |attribute,value|
142
+ next if value.blank? or attribute.blank?
143
+ search_type = search_type_for_attribute(attribute)
144
+ attribute = translate_attribute_name_to_he(attribute)
145
+ condition.add_attr "#{attribute} #{search_type} #{value}"
146
+ end
147
+ else raise
148
+ end
149
+ end
150
+
151
+ def search_type_for_attribute(attribute)
152
+ column_type_of(attribute) == :numeric ? 'NUMEQ' : 'iSTRINC'
153
+ end
154
+
155
+ def translate_order_to_he(order)
156
+ order_parts = order.to_s.downcase.strip.split(' ')
157
+ return order if order_parts.size > 2
158
+ return order unless a_or_d(order_parts[1])
159
+
160
+ translated = translate_attribute_name_to_he(order_parts[0])
161
+ sort_word = (column_type_of(order_parts[0])==:numeric) ? 'NUM' : 'STR'
162
+ "#{translated} #{sort_word}#{a_or_d(order_parts[1])}"
163
+ end
164
+
165
+ #is the column numeric(numbers/dates) or string(else) ?
166
+ def column_type_of(attribute)
167
+ column = @ar_class.columns_hash[attribute.to_s]
168
+ return :string unless column
169
+ return :numeric if column.number? or [:datetime,:time,:date,:timestamp].include?(column.type)
170
+ return :string
171
+ end
172
+
173
+ def translate_attribute_name_to_he(name)
174
+ case name.to_s
175
+ when 'updated_at','updated_on' then "@mdate"
176
+ when 'created_at','created_on' then "@cdate"
177
+ when 'id' then "db_id"
178
+ else name
179
+ end
180
+ end
181
+
182
+ #pre: string is downcased & stripped
183
+ def a_or_d(order_end)
184
+ case order_end
185
+ when 'asc' then "A"
186
+ when 'desc','',nil then "D"
187
+ else nil
188
+ end
189
+ end
190
+
191
+ def delete_from_index(document)
192
+ @connection.out_doc(document.attr('@id'))
193
+ end
194
+
195
+ def benchmark(log, &block)
196
+ @ar_class.benchmark(log, &block)
197
+ end
198
+
199
+ def calculate_node_name(config)
200
+ node_prefix = config['node_prefix'] || config['node'] || RAILS_ENV
201
+ "#{node_prefix}_#{@ar_class.table_name}"
202
+ end
203
+
204
+ def attribute_name(attribute)
205
+ SYSTEM_ATTRIBUTES.include?(attribute.to_s) ? "@#{attribute}" : "#{attribute}"
206
+ end
207
+ end
208
+ end
209
+ end
210
+
211
+ # call after creating namespace SearchDo::Backends::HyperEstraier
212
+ require 'search_do/backends/hyper_estraier/estraier_pure_extention'
213
+
@@ -0,0 +1,17 @@
1
+ require 'search_do/backends/hyper_estraier'
2
+
3
+ module SearchDo
4
+ module Backends
5
+ def connect(model_klass, config)
6
+ backend = config['backend'] || "hyper_estraier"
7
+
8
+ case backend
9
+ when "hyper_estraier", nil # default
10
+ Backends::HyperEstraier.new(model_klass, config)
11
+ else
12
+ raise NotImplementedError.new("#{backend} backend is not supported")
13
+ end
14
+ end
15
+ module_function :connect
16
+ end
17
+ end
@@ -0,0 +1,22 @@
1
+ module SearchDo
2
+ module DirtyTracking
3
+ module Bridge
4
+
5
+ def need_update_index?(attr_name = nil)
6
+ return false unless changed?
7
+ cs = changed_attributes.keys
8
+ if attr_name
9
+ cs.include?(attr_name)
10
+ else
11
+ search_indexer.observing_fields.any?{|t| cs.include?(t) }
12
+ end
13
+ end
14
+
15
+ private
16
+ def clear_changed_attributes
17
+ changed_attributes.clear
18
+ end
19
+ end
20
+ end
21
+ end
22
+
@@ -0,0 +1,36 @@
1
+
2
+ module SearchDo
3
+ module DirtyTracking
4
+ module SelfMade
5
+ def self.included(base)
6
+ base.class_eval do
7
+ attr_accessor :changed_attributes
8
+
9
+ search_indexer.observing_fields.each do |attr_name|
10
+ define_method("#{attr_name}=") do |value|
11
+ write_changed_attribute attr_name, value
12
+ end
13
+ end
14
+ end
15
+ end
16
+
17
+ # If called with no parameters, gets whether the current model has changed and needs to updated in the index.
18
+ # If called with a single parameter, gets whether the parameter has changed.
19
+ def need_update_index?(attr_name = nil)
20
+ changed_attributes and (attr_name.nil? ?
21
+ (not changed_attributes.length.zero?) : (changed_attributes.include?(attr_name.to_s)) )
22
+ end
23
+
24
+ private
25
+ def clear_changed_attributes #:nodoc:
26
+ self.changed_attributes = []
27
+ end
28
+
29
+ def write_changed_attribute(attr_name, attr_value) #:nodoc:
30
+ (self.changed_attributes ||= []) << attr_name.to_s unless self.need_update_index?(attr_name) or self.send(attr_name) == attr_value
31
+ write_attribute(attr_name.to_s, attr_value)
32
+ end
33
+ end
34
+ end
35
+ end
36
+
@@ -0,0 +1,15 @@
1
+ require 'search_do/dirty_tracking/self_made'
2
+ require 'search_do/dirty_tracking/bridge'
3
+
4
+ module SearchDo
5
+ module DirtyTracking
6
+ def self.included(base)
7
+ mod = if defined?(ActiveRecord::Dirty) && base.included_modules.include?(ActiveRecord::Dirty)
8
+ DirtyTracking::Bridge
9
+ else
10
+ DirtyTracking::SelfMade
11
+ end
12
+ base.send(:include, mod)
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,65 @@
1
+ require 'set'
2
+
3
+ module SearchDo
4
+ class Indexer
5
+ attr_reader :searchable_fields, :if_changed, :attributes_to_store
6
+ def initialize(base, configuration)
7
+ @base = base
8
+ @configuration = configuration
9
+ end
10
+
11
+ def searchable_fields=(fields)
12
+ expire_observing_fields_cache!
13
+ @searchable_fields = fields
14
+ end
15
+
16
+ def if_changed=(fields)
17
+ expire_observing_fields_cache!
18
+ @if_changed = fields
19
+ end
20
+
21
+ def attributes_to_store=(attrs)
22
+ expire_observing_fields_cache!
23
+ @attributes_to_store = attrs.stringify_keys
24
+ end
25
+
26
+ def observing_fields(update = false)
27
+ expire_observing_fields_cache! if update
28
+ @observing_fields ||=
29
+ Set.new((if_changed + searchable_fields + attributes_to_store.values).map(&:to_s))
30
+ end
31
+
32
+ def record_timestamps!
33
+ begin
34
+ detect_col = lambda{|candidate_col| @base.column_names.include?(candidate_col) }
35
+ @attributes_to_store[backend_vocabulary :create_timestamp] =
36
+ %w(created_at created_on).detect(&detect_col)
37
+ @attributes_to_store[backend_vocabulary :update_timestamp] =
38
+ %w(updated_at updated_on).detect(&detect_col)
39
+ expire_observing_fields_cache!
40
+ rescue
41
+ #allow db-operations like schema loading etc to work without this crashing
42
+ puts "Your database is non-existent or in a very bad state -- From:#{__FILE__}:#{__LINE__}"
43
+ end
44
+ end
45
+
46
+ def add_callbacks!
47
+ @base.after_update(:update_index)
48
+ @base.after_create(:add_to_index)
49
+ @base.after_destroy(:remove_from_index)
50
+ @base.after_save(:clear_changed_attributes)
51
+ end
52
+
53
+ private
54
+ def expire_observing_fields_cache!
55
+ @observing_fields = nil
56
+ end
57
+
58
+ # TODO
59
+ def backend_vocabulary(type)
60
+ { :update_timestamp => 'mdate',
61
+ :create_timestamp => 'cdate',
62
+ }[type]
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,11 @@
1
+
2
+ module SearchDo
3
+ class Utils
4
+ MULTIBYTE_SPACE = [0x3000].pack("U")
5
+
6
+ def self.cleanup_query(query)
7
+ query.to_s.gsub(MULTIBYTE_SPACE,' ')
8
+ end
9
+ end
10
+ end
11
+