search_do 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +1 -0
- data/MIT-LICENSE +20 -0
- data/README.rdoc +63 -0
- data/Rakefile +47 -0
- data/TESTING +6 -0
- data/VERSION +1 -0
- data/examples/he_search.rb +13 -0
- data/examples/person.rb +20 -0
- data/init.rb +1 -0
- data/lib/estraier_admin.rb +47 -0
- data/lib/search_do/backends/hyper_estraier/estraier_pure_extention.rb +61 -0
- data/lib/search_do/backends/hyper_estraier.rb +213 -0
- data/lib/search_do/backends.rb +17 -0
- data/lib/search_do/dirty_tracking/bridge.rb +22 -0
- data/lib/search_do/dirty_tracking/self_made.rb +36 -0
- data/lib/search_do/dirty_tracking.rb +15 -0
- data/lib/search_do/indexer.rb +65 -0
- data/lib/search_do/utils.rb +11 -0
- data/lib/search_do.rb +330 -0
- data/lib/vendor/estraierpure.rb +1025 -0
- data/lib/vendor/overview +100 -0
- data/recipes/mode_maintenance.rb +52 -0
- data/spec/backends/hyper_estraier_spec.rb +220 -0
- data/spec/backends/result_document_spec.rb +26 -0
- data/spec/dirty_tracking/bridge_spec.rb +33 -0
- data/spec/estraier_admin_spec.rb +26 -0
- data/spec/fixtures/stories.yml +27 -0
- data/spec/indexer_spec.rb +59 -0
- data/spec/search_do_spec.rb +335 -0
- data/spec/setup_test_model.rb +38 -0
- data/spec/spec_helper.rb +52 -0
- data/tasks/acts_as_searchable_tasks.rake +70 -0
- metadata +95 -0
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
|
data/examples/person.rb
ADDED
@@ -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
|