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 +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
|