プロジェクト

全般

プロフィール

プラグインXapian search pluginの検索エンジンをHyper Estraierへ替える » 履歴 » バージョン 9

Masanori Machii, 2012/01/20 17:45

1 1 Masanori Machii
h1. プラグインXapian search pluginの検索エンジンをHyper Estraierへ替える
2
3
Xapian search pluginは,チケットの添付ファイル内のテキストを検索するプラグインです.これもまたその名が示すように,Xapianを使っており,そのままでは日本語に対応できません.
4
5 3 Masanori Machii
加えて,改造はDMSFよりも厄介です.プラグインの名称に,大胆にも Xapian という文字列を使っていることから想像されるように,モジュール名,変数名,メソッド名に現れる xapian を
6
すべて estraier に書き換えると,動きません!それを一部ずつ修正しても,かえってバグを混入させてしまいます.
7 1 Masanori Machii
8 3 Masanori Machii
なので,修正のポリシーは“[[プラグインDMSFの検索エンジンをHyper Estraierへ替える]]”とは同様にならず,少し複雑です.
9 1 Masanori Machii
10 3 Masanori Machii
まず,同様な方から示すと,
11
12
# Web画面に "Xapian" 由来の設定などがあれば,それはそのままにし,削除しない.(stem,languageらは機能しないまま残る)
13 1 Masanori Machii
# Web画面に Estraier に対応するものがあれば,それは "Estraier" へと修正する.(現バージョンではない)
14
# エラーメッセージなどに "Xapian" という文字列があれば,それは "Estraier" へと上書きする.
15
# ソースコード中 "Xapian" を呼び出す部分は削除する.(ロジックの変更)
16 3 Masanori Machii
# ソースコード中 "Xapian" という変数名を "Estraier" へと置き換える部分を極力少なくする.
17
# ソースコード中 "xapian" というラベル名称や,"redmine_xapian" のようにプラグインの由来の変数名があれば,そのまま利用する.
18 1 Masanori Machii
19
20 3 Masanori Machii
次に複雑な方を示します.
21
# 何らの理由により "Estraier" 検索環境が整備できない場合でも,添付ファイルの「タイトルのみ」検索はできるようにする.
22
# "Xapian search plugin" に,その仕掛けから生じたと思われる,添付ファイル検索の権限に関する外部仕様がある.「文書」と関連するこれを独立させた.
23
プラグインDMSFの場合は,アップロードしたファイルは,$REDMINE/files よりも一つ下へ下げたディレクトリ dmsf へファイルを置きましたが,添付ファイルはこの files 下へ置かれます.そのインデックスファイルはどこに置かれるべきか?と考えた場合,もちろん files 下へは置けません.なので,一つ上のディレクトリ $REDMINE へ置くことになります.そういうわけで,インデックス・ファイルの名称は files_index になります.
24
# 検索用インデックスのディレクトリは言語毎に分けない.
25
# バージョンは,日本語版であることを示すため,1.yy.zz-JP とする. 
26 1 Masanori Machii
27 3 Masanori Machii
28
さて,変更するファイルは次の6つです.
29
30
# config/locales/ja.yml
31
# init.rb
32
# app/controllers/search_controller.rb
33
# app/views/search/index.rhtml
34
# lib/acts_as_searchable.rb
35
# lib/xapian_search.rb
36
37 7 Masanori Machii
ところで,本サイトからコピーする場合は "¥"(バックスラッシュ)に注意して下さい.円記号ではありません.
38 3 Masanori Machii
39
h2. config/locales/ja.yml
40
41
奇妙ですが,DMSFの方は最初から ja.yml がありました.ですが,さすがに当該プラグインにはそれはありません.なので,ja.yml は新たに(en.ymlを基にして)作ります.
42
43
<pre>
44
@@ -0,0 +1,29 @@
45
+ja:
46
+
47
+    label_enable_redmine_xapian: "hyper estraier による添付ファイル検索を可能にする"
48
+    label_index_database: "hyper estraier 検索インデックスディレクトリ"
49
+    label_stemming_text: "set the stemming language, the default is 'english'.
50
+                           Possible values: danish dutch english finnish french
51
+                           german german2 hungarian italian kraaij_pohlmann
52
+                           lovins norwegian porter portuguese romanian russian
53
+                           spanish swedish turkish (pass 'none' to disable
54
+                           stemming)"
55
+    label_search2: "Extended Search"
56
+    text_search1: "This plugin does same redmine search plus searches into attachment. To enable xapian you have to build database with omindex utility and configure index database on plugin settings page"
57
+    label_document: "ドキュメント"
58
+    label_issue: "チケット"
59
+    label_wiki: "Wiki"
60
+    label_message: "メッセージ"
61
+    label_article: "Article"
62
+    label_article_plural: "Articles"
63
+    label_stemming_lang: "Stemming Language(注:estraier版では未使用)"
64
+    label_enable_xapian_on_search: "検索画面でhyper estraier使用可"
65
+    label_search_languages: "Stemming languages on search screen(注:estraier版では未使用)"
66
+    label_stemming_strategy: "Stemming strategy(注:estraier版では未使用)"
67
+    label_stem_none: "Stem none"
68
+    label_stem_some: "Stem some"
69
+    label_stem_all:  "Stem all"
70
+    label_default_stemming_lang: "Set default stemming lang(注:estraier版では未使用)"
71
+    label_default_stemming_strategy: "Set default stemming strategy(注:estraier版では未使用)"
72
+    label_database_error: "検索インデックスエラー。Hyper Estraier利用環境を構築してください。"
73
+
74
</pre>
75
76
77
h2. init.rb
78
79
ライブラリ名称を xapian から estraier に変更します.加えて,先の ya.yml に対応する警告のラベルと一致させます.
80
81
<pre>
82
@ -7,10 +7,10 @@
83
84
85
begin
86
-    require 'xapian'
87
+    require 'estraier'
88
    $xapian_bindings_available = true
89
rescue LoadError
90
-    Rails.logger.info "REDMAIN_XAPIAN ERROR: No Ruby bindings for Xapian installed !!. PLEASE install Xapian search engine interface for Ruby."
91
+    Rails.logger.info "REDMAIN_XAPIAN ERROR: No Ruby bindings for Hyper Estraier installed !!. PLEASE install Hyper Estraier search engine interface for Ruby."
92
    $xapian_bindings_available = false
93
else
94
    require 'redmine'
95
@@ -28,7 +28,7 @@
96
       author_url 'http://undefinederror.org'
97
98
       description 'With this plugin you will be able to do searches by file name and by strings inside your documents'
99
-       version '1.2.1'
100
+       version '1.2.1-JP'
101
       requires_redmine :version_or_higher => '1.0.0'
102
103
       settings :partial => 'settings/redmine_xapian_settings',
104
</pre>
105
106
h2. app/controllers/search_controller.rb
107
108
検索エンジンが利用できない場合は,警告メッセージを出力する.なお,「タイトルのみ」検索の場合はこの限りではない.
109
110
<pre>
111
@@ -96,6 +96,7 @@
112
	end
113
	
114
     end
115
+      flash[:warning] = "warning: #{l(:label_database_error)}" unless @titles_only || $xapian_bindings_available
116
     @results = @results.sort {|a,b| b.event_datetime <=> a.event_datetime}
117
     if params[:previous].nil?
118
       @pagination_previous_date = @results[0].event_datetime if offset && @results[0]
119
</pre>
120
121
122
h2. app/views/search/index.rhtml
123
124 4 Masanori Machii
オリジナルでは,検索結果画面に表示される Stemらの設定部分画面を表示されないようにする.
125
126
<pre>
127
@@ -16,6 +16,7 @@
128
<% end %>
129
</p>
130
<% logger.debug "DEBUG: object_types from search: " + Redmine::Search.available_search_types.inspect %>
131
+<% if false then %>
132
<% Setting.plugin_redmine_xapian['stem_langs'].push(Setting.plugin_redmine_xapian['stemming_lang']) unless Setting.plugin_redmine_xapian['stem_langs'].include?(Setting.plugin_redmine_xapian['stemming_lang']) %>
133
134
<p>
135
@@ -31,6 +32,7 @@
136
137
138
<%end%>
139
+<%end%>
140
<p><%= submit_tag l(:button_submit), :name => 'submit' %></p>
141
<% end %>
142
</div>
143
</pre>
144
145
146 3 Masanori Machii
h2. lib/acts_as_searchable.rb
147
148 4 Masanori Machii
149
サーバー側に検索環境が整備されていない場合でも,少なくともタイトルの検索を可能にする修正.
150
また,すべての添付ファイルの検索の権限が「文書の閲覧」で集約されていたものを分割する.すなわち,ファイルが添付されているチケットやwiki,それぞれの閲覧権限に従うようにする.
151
152
<pre>
153
@@ -15,7 +15,7 @@
154
# along with this program; if not, write to the Free Software
155
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
156
157
-require 'xapian_search'
158
+require 'xapian_search' if $xapian_bindings_available
159
160
module Redmine
161
 module Acts
162
@@ -116,6 +116,10 @@
163
		#Attahcment on documents
164
		results_doc= []
165
		results_count_doc=0
166
+		project_conditions = []
167
+		project_conditions << (searchable_options[:permission].nil? ? Project.visible_by(User.current) :
168
+                                                 Project.allowed_to_condition(User.current, :view_documents))
169
+		project_conditions << "#{searchable_options[:project_key]} IN (#{projects.collect(&:id).join(',')})" unless projects.nil?
170
		find_options_tmp=Hash.new
171
		find_options_tmp=find_options_tmp.merge(find_options)
172
		find_options_tmp[:conditions] = merge_conditions (find_options_tmp[:conditions], :container_type=>"Document" )
173
@@ -129,6 +133,10 @@
174
		results +=results_doc
175
               results_count += results_count_doc
176
		#Attachemnts on WikiPage
177
+		project_conditions = []
178
+		project_conditions << (searchable_options[:permission].nil? ? Project.visible_by(User.current) :
179
+                                                 Project.allowed_to_condition(User.current, :view_wiki_pages))
180
+		project_conditions << "#{searchable_options[:project_key]} IN (#{projects.collect(&:id).join(',')})" unless projects.nil?
181
		find_options_tmp=Hash.new
182
		find_options_tmp=find_options_tmp.merge(find_options)
183
               results_wiki= []
184
@@ -146,6 +154,10 @@
185
		results+=results_wiki
186
		results_count+= results_count_wiki
187
		#Attachemnts on Message
188
+		project_conditions = []
189
+		project_conditions << (searchable_options[:permission].nil? ? Project.visible_by(User.current) :
190
+                                                 Project.allowed_to_condition(User.current, :view_messages))
191
+		project_conditions << "#{searchable_options[:project_key]} IN (#{projects.collect(&:id).join(',')})" unless projects.nil?
192
		find_options_tmp =Hash.new
193
		find_options_tmp=find_options_tmp.merge(find_options)
194
               results_message= []
195
@@ -163,6 +175,10 @@
196
		results+=results_message
197
		results_count+= results_count_message
198
		#Attachemnts on Issues
199
+		project_conditions = []
200
+		project_conditions << (searchable_options[:permission].nil? ? Project.visible_by(User.current) :
201
+                                                 Project.allowed_to_condition(User.current, :view_issues))
202
+		project_conditions << "#{searchable_options[:project_key]} IN (#{projects.collect(&:id).join(',')})" unless projects.nil?
203
		find_options_tmp =Hash.new
204
		find_options_tmp=find_options_tmp.merge(find_options)
205
               results_issue= []
206
@@ -180,6 +196,9 @@
207
		#Attachments on Articles
208
		if Redmine::Search.available_search_types.include?("articles")
209
		  logger.debug "DEBUG: knowledgebase plugin installed"
210
+		  project_conditions = []
211
+		  project_conditions << Project.visible_by(User.current)
212
+		  project_conditions << "#{searchable_options[:project_key]} IN (#{projects.collect(&:id).join(',')})" unless projects.nil?
213
		  find_options_tmp=Hash.new
214
       	  find_options_tmp=find_options_tmp.merge(find_options)
215
       	  results_article = []
216
@@ -194,7 +213,7 @@
217
       	  results_count += results_count_article
218
		end
219
		# Search attachments over xapian
220
-		if !options[:titles_only]
221
+		if !options[:titles_only] && $xapian_bindings_available
222
		  begin 
223
		    xapianresults, xapianresults_count =  XapianSearch.search_attachments( tokens, limit_options, 
224
								 options[:offset], projects, options[:all_words], options[:user_stem_lang],
225
</pre>
226
227 3 Masanori Machii
h2. lib/xapian_search.rb
228 4 Masanori Machii
229
【注意】このファイルについては,後のコードレビューにてさらに変更する可能性があります.特に権限の部分がそうです.
230
231
検索インデックスは言語毎に分けてはいない.
232
233
<pre>
234
@@ -11,69 +11,56 @@
235
               Rails.logger.debug "DEBUG: user_stem_lang: " + user_stem_lang.inspect
236
               Rails.logger.debug "DEBUG: user_stem_strategy: " + user_stem_strategy.inspect
237
		Rails.logger.debug "DEBUG: databasepath: " + getDatabasePath(user_stem_lang)
238
-		databasepath = getDatabasePath(user_stem_lang)
239
+		databasepath = getDatabasePath('')
240
241
		begin
242
-		  database = Xapian::Database.new(databasepath)
243
+		  database = Estraier::Database::new
244
+		  unless database.open(databasepath, Estraier::Database::DBREADER)
245
+		    return [xpattachments,0]
246
+		  end
247
		rescue => error
248
		  raise databasepath
249
-		  return [xpattachments,0]
250
               end
251
252
		# Start an enquire session.
253
		
254
-		enquire = Xapian::Enquire.new(database)
255
+		enquire = Estraier::Condition::new
256
+
257
+		queryString = tokens.join(all_words ? ' AND ': ' OR ')
258
+		enquire.set_phrase(queryString)
259
+		enquire.set_max(100)
260
261
-		# Combine the rest of the command line arguments with spaces between
262
-		# them, so that simple queries don't have to be quoted at the shell
263
-		# level.
264
-		#queryString = ARGV[1..-1].join(' ')
265
-		queryString = tokens.join(' ')
266
-		# Parse the query string to produce a Xapian::Query object.
267
-		qp = Xapian::QueryParser.new()
268
-		stemmer = Xapian::Stem.new($user_stem_lang)
269
-		qp.stemmer = stemmer
270
-		qp.database = database
271
-		case @user_stem_strategy
272
-		  when "STEM_NONE" then qp.stemming_strategy = Xapian::QueryParser::STEM_NONE
273
-		  when "STEM_SOME" then qp.stemming_strategy = Xapian::QueryParser::STEM_SOME
274
-		  when "STEM_ALL" then qp.stemming_strategy = Xapian::QueryParser::STEM_ALL
275
-		end
276
-		if all_words
277
-		  qp.default_op = Xapian::Query::OP_AND
278
-		else	
279
-		  qp.default_op = Xapian::Query::OP_OR
280
-		end
281
-		query = qp.parse_query(queryString)
282
		Rails.logger.debug "DEBUG queryString is: #{queryString}"
283
-		Rails.logger.debug "DEBUG: Parsed query is: #{query.description()} "
284
285
-		# Find the top 100 results for the query.
286
-		enquire.query = query
287
-		matchset = enquire.mset(0, 1000)
288
+		matchset = database.search(enquire)
289
	
290
		return [xpattachments,0] if matchset.nil?
291
292
		# Display the results.
293
-		#logger.debug "#{@matchset.matches_estimated()} results found."
294
-		Rails.logger.debug "DEBUG: Matches 1-#{matchset.size}:¥n"
295
+		Rails.logger.debug "DEBUG: Matches 1-#{matchset.doc_num}:¥n"
296
297
-		matchset.matches.each {|m|
298
+		dnum = matchset.doc_num
299
+		for i in 0...dnum
300
+		  doc = database.get_doc(matchset.get_doc_id(i), 0)
301
+		  next unless doc
302
		  #Rails.logger.debug "#{m.rank + 1}: #{m.percent}% docid=#{m.docid} [#{m.document.data}]¥n"
303
		  #logger.debug "DEBUG: m: " + m.document.data.inspect
304
-		  docdata=m.document.data{url}
305
-		  dochash=Hash[*docdata.scan(/(url|sample|modtime|type|size)=¥/?([^¥n¥]]+)/).flatten]
306
-		  if not dochash.nil? then
307
-		    find_conditions =  Attachment.merge_conditions (limit_options[:conditions],  :disk_filename => dochash.fetch('url') )
308
+
309
+		  uri = doc.attr("@uri")
310
+		  if uri then
311
+		    filename = uri.sub(/.*¥//, '')
312
+
313
+		    find_conditions =  Attachment.merge_conditions (limit_options[:conditions],  :disk_filename => filename)
314
		    docattach=Attachment.find (:first, :conditions =>  find_conditions )
315
		    if not docattach.nil? then
316
		      if docattach["container_type"] == "Article" and not Redmine::Search.available_search_types.include?("articles")
317
                       Rails.logger.debug "DEBUG: Knowledgebase plugin in not installed.."
318
                     elsif not docattach.container.nil? then
319
			Rails.logger.debug "DEBUG: adding attach.. " 
320
-		        allowed =  User.current.allowed_to?("view_documents".to_sym, docattach.container.project)  || docattach.container_type == "Article"
321
+		        # allowed =  User.current.allowed_to?("view_documents".to_sym, docattach.container.project)  || docattach.container_type == "Article"
322
+		        allowed = true
323
		        if ( allowed and project_included(docattach.container.project.id, projects_to_search ) )
324
-			  docattach[:description]=dochash["sample"]
325
+			  docattach[:description] = doc.make_snippet(tokens,50,0,10).to_s
326
			  xpattachments.push ( docattach )
327
		        else
328
		 	  Rails.logger.debug "DEBUG: user without permissions"
329
@@ -81,7 +68,10 @@
330
		      end
331
		    end
332
		  end
333
-		}	
334
+		end
335
+
336
+		database.close
337
+
338
		@@numattach=xpattachments.size if offset.nil?
339
		xpattachments=xpattachments.sort_by{|x| x[:created_on] }
340
		[xpattachments, @@numattach]
341
</pre>
342 5 Masanori Machii
343
344 9 Masanori Machii
修正は以上です.
345
346
-Redmine-1.3へ対応したバージョン 1.2.3 の場合の差分ファイルを添付しておきます.-