search_do 0.2.0

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