プロジェクト

全般

プロフィール

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

リビジョン 6 (Masanori Machii, 2012/01/19 16:33) → リビジョン 7/9 (Masanori Machii, 2012/01/19 19:19)

h1. プラグインXapian search pluginの検索エンジンをHyper Estraierへ替える 

 Xapian search pluginは,チケットの添付ファイル内のテキストを検索するプラグインです.これもまたその名が示すように,Xapianを使っており,そのままでは日本語に対応できません. 

 加えて,改造はDMSFよりも厄介です.プラグインの名称に,大胆にも Xapian という文字列を使っていることから想像されるように,モジュール名,変数名,メソッド名に現れる xapian を 
 すべて estraier に書き換えると,動きません!それを一部ずつ修正しても,かえってバグを混入させてしまいます. 

 なので,修正のポリシーは“[[プラグインDMSFの検索エンジンをHyper Estraierへ替える]]”とは同様にならず,少し複雑です. 

 まず,同様な方から示すと, 

 # Web画面に "Xapian" 由来の設定などがあれば,それはそのままにし,削除しない.(stem,languageらは機能しないまま残る) 
 # Web画面に Estraier に対応するものがあれば,それは "Estraier" へと修正する.(現バージョンではない) 
 # エラーメッセージなどに "Xapian" という文字列があれば,それは "Estraier" へと上書きする. 
 # ソースコード中 "Xapian" を呼び出す部分は削除する.(ロジックの変更) 
 # ソースコード中 "Xapian" という変数名を "Estraier" へと置き換える部分を極力少なくする. 
 # ソースコード中 "xapian" というラベル名称や,"redmine_xapian" のようにプラグインの由来の変数名があれば,そのまま利用する. 


 次に複雑な方を示します. 
 # 何らの理由により "Estraier" 検索環境が整備できない場合でも,添付ファイルの「タイトルのみ」検索はできるようにする. 
 # "Xapian search plugin" に,その仕掛けから生じたと思われる,添付ファイル検索の権限に関する外部仕様がある.「文書」と関連するこれを独立させた. 
 プラグインDMSFの場合は,アップロードしたファイルは,$REDMINE/files よりも一つ下へ下げたディレクトリ dmsf へファイルを置きましたが,添付ファイルはこの files 下へ置かれます.そのインデックスファイルはどこに置かれるべきか?と考えた場合,もちろん files 下へは置けません.なので,一つ上のディレクトリ $REDMINE へ置くことになります.そういうわけで,インデックス・ファイルの名称は files_index になります. 
 # 検索用インデックスのディレクトリは言語毎に分けない. 
 # バージョンは,日本語版であることを示すため,1.yy.zz-JP とする.  


 さて,変更するファイルは次の6つです. 

 # config/locales/ja.yml 
 # init.rb 
 # app/controllers/search_controller.rb 
 # app/views/search/index.rhtml 
 # lib/acts_as_searchable.rb 
 # lib/xapian_search.rb 

 ところで,本サイトからコピーする場合は "¥"(バックスラッシュ)に注意して下さい.円記号ではありません. 

 


 h2. config/locales/ja.yml 

 奇妙ですが,DMSFの方は最初から ja.yml がありました.ですが,さすがに当該プラグインにはそれはありません.なので,ja.yml は新たに(en.ymlを基にして)作ります. 

 <pre> 
 @@ -0,0 +1,29 @@ 
 +ja: 
 + 
 +      label_enable_redmine_xapian: "hyper estraier による添付ファイル検索を可能にする" 
 +      label_index_database: "hyper estraier 検索インデックスディレクトリ" 
 +      label_stemming_text: "set the stemming language, the default is 'english'. 
 +                             Possible values: danish dutch english finnish french 
 +                             german german2 hungarian italian kraaij_pohlmann 
 +                             lovins norwegian porter portuguese romanian russian 
 +                             spanish swedish turkish (pass 'none' to disable 
 +                             stemming)" 
 +      label_search2: "Extended Search" 
 +      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" 
 +      label_document: "ドキュメント" 
 +      label_issue: "チケット" 
 +      label_wiki: "Wiki" 
 +      label_message: "メッセージ" 
 +      label_article: "Article" 
 +      label_article_plural: "Articles" 
 +      label_stemming_lang: "Stemming Language(注:estraier版では未使用)" 
 +      label_enable_xapian_on_search: "検索画面でhyper estraier使用可" 
 +      label_search_languages: "Stemming languages on search screen(注:estraier版では未使用)" 
 +      label_stemming_strategy: "Stemming strategy(注:estraier版では未使用)" 
 +      label_stem_none: "Stem none" 
 +      label_stem_some: "Stem some" 
 +      label_stem_all:    "Stem all" 
 +      label_default_stemming_lang: "Set default stemming lang(注:estraier版では未使用)" 
 +      label_default_stemming_strategy: "Set default stemming strategy(注:estraier版では未使用)" 
 +      label_database_error: "検索インデックスエラー。Hyper Estraier利用環境を構築してください。" 
 + 
 </pre> 


 h2. init.rb 

 ライブラリ名称を xapian から estraier に変更します.加えて,先の ya.yml に対応する警告のラベルと一致させます. 

 <pre> 
 @ -7,10 +7,10 @@ 


 begin 
 -      require 'xapian' 
 +      require 'estraier' 
     $xapian_bindings_available = true 
 rescue LoadError 
 -      Rails.logger.info "REDMAIN_XAPIAN ERROR: No Ruby bindings for Xapian installed !!. PLEASE install Xapian search engine interface for Ruby." 
 +      Rails.logger.info "REDMAIN_XAPIAN ERROR: No Ruby bindings for Hyper Estraier installed !!. PLEASE install Hyper Estraier search engine interface for Ruby." 
     $xapian_bindings_available = false 
 else 
     require 'redmine' 
 @@ -28,7 +28,7 @@ 
        author_url 'http://undefinederror.org' 

        description 'With this plugin you will be able to do searches by file name and by strings inside your documents' 
 -         version '1.2.1' 
 +         version '1.2.1-JP' 
        requires_redmine :version_or_higher => '1.0.0' 

        settings :partial => 'settings/redmine_xapian_settings', 
 </pre> 

 h2. app/controllers/search_controller.rb 

 検索エンジンが利用できない場合は,警告メッセージを出力する.なお,「タイトルのみ」検索の場合はこの限りではない. 

 <pre> 
 @@ -96,6 +96,7 @@ 
	 end 
	
      end 
 +        flash[:warning] = "warning: #{l(:label_database_error)}" unless @titles_only || $xapian_bindings_available 
      @results = @results.sort {|a,b| b.event_datetime <=> a.event_datetime} 
      if params[:previous].nil? 
        @pagination_previous_date = @results[0].event_datetime if offset && @results[0] 
 </pre> 


 h2. app/views/search/index.rhtml 

 オリジナルでは,検索結果画面に表示される Stemらの設定部分画面を表示されないようにする. 

 <pre> 
 @@ -16,6 +16,7 @@ 
 <% end %> 
 </p> 
 <% logger.debug "DEBUG: object_types from search: " + Redmine::Search.available_search_types.inspect %> 
 +<% if false then %> 
 <% 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']) %> 

 <p> 
 @@ -31,6 +32,7 @@ 


 <%end%> 
 +<%end%> 
 <p><%= submit_tag l(:button_submit), :name => 'submit' %></p> 
 <% end %> 
 </div> 
 </pre> 


 h2. lib/acts_as_searchable.rb 


 サーバー側に検索環境が整備されていない場合でも,少なくともタイトルの検索を可能にする修正. 
 また,すべての添付ファイルの検索の権限が「文書の閲覧」で集約されていたものを分割する.すなわち,ファイルが添付されているチケットやwiki,それぞれの閲覧権限に従うようにする. 

 <pre> 
 @@ -15,7 +15,7 @@ 
 # along with this program; if not, write to the Free Software 
 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA    02110-1301, USA. 

 -require 'xapian_search' 
 +require 'xapian_search' if $xapian_bindings_available 

 module Redmine 
  module Acts 
 @@ -116,6 +116,10 @@ 
		 #Attahcment on documents 
		 results_doc= [] 
		 results_count_doc=0 
 + 		 project_conditions = [] 
 + 		 project_conditions << (searchable_options[:permission].nil? ? Project.visible_by(User.current) : 
 +                                                   Project.allowed_to_condition(User.current, :view_documents)) 
 + 		 project_conditions << "#{searchable_options[:project_key]} IN (#{projects.collect(&:id).join(',')})" unless projects.nil? 
		 find_options_tmp=Hash.new 
		 find_options_tmp=find_options_tmp.merge(find_options) 
		 find_options_tmp[:conditions] = merge_conditions (find_options_tmp[:conditions], :container_type=>"Document" ) 
 @@ -129,6 +133,10 @@ 
		 results +=results_doc 
                results_count += results_count_doc 
		 #Attachemnts on WikiPage 
 + 		 project_conditions = [] 
 + 		 project_conditions << (searchable_options[:permission].nil? ? Project.visible_by(User.current) : 
 +                                                   Project.allowed_to_condition(User.current, :view_wiki_pages)) 
 + 		 project_conditions << "#{searchable_options[:project_key]} IN (#{projects.collect(&:id).join(',')})" unless projects.nil? 
		 find_options_tmp=Hash.new 
		 find_options_tmp=find_options_tmp.merge(find_options) 
                results_wiki= [] 
 @@ -146,6 +154,10 @@ 
		 results+=results_wiki 
		 results_count+= results_count_wiki 
		 #Attachemnts on Message 
 + 		 project_conditions = [] 
 + 		 project_conditions << (searchable_options[:permission].nil? ? Project.visible_by(User.current) : 
 +                                                   Project.allowed_to_condition(User.current, :view_messages)) 
 + 		 project_conditions << "#{searchable_options[:project_key]} IN (#{projects.collect(&:id).join(',')})" unless projects.nil? 
		 find_options_tmp =Hash.new 
		 find_options_tmp=find_options_tmp.merge(find_options) 
                results_message= [] 
 @@ -163,6 +175,10 @@ 
		 results+=results_message 
		 results_count+= results_count_message 
		 #Attachemnts on Issues 
 + 		 project_conditions = [] 
 + 		 project_conditions << (searchable_options[:permission].nil? ? Project.visible_by(User.current) : 
 +                                                   Project.allowed_to_condition(User.current, :view_issues)) 
 + 		 project_conditions << "#{searchable_options[:project_key]} IN (#{projects.collect(&:id).join(',')})" unless projects.nil? 
		 find_options_tmp =Hash.new 
		 find_options_tmp=find_options_tmp.merge(find_options) 
                results_issue= [] 
 @@ -180,6 +196,9 @@ 
		 #Attachments on Articles 
		 if Redmine::Search.available_search_types.include?("articles") 
		   logger.debug "DEBUG: knowledgebase plugin installed" 
 + 		   project_conditions = [] 
 + 		   project_conditions << Project.visible_by(User.current) 
 + 		   project_conditions << "#{searchable_options[:project_key]} IN (#{projects.collect(&:id).join(',')})" unless projects.nil? 
		   find_options_tmp=Hash.new 
       	   find_options_tmp=find_options_tmp.merge(find_options) 
       	   results_article = [] 
 @@ -194,7 +213,7 @@ 
       	   results_count += results_count_article 
		 end 
		 # Search attachments over xapian 
 - 		 if !options[:titles_only] 
 + 		 if !options[:titles_only] && $xapian_bindings_available 
		   begin  
		     xapianresults, xapianresults_count =    XapianSearch.search_attachments( tokens, limit_options,  
								  options[:offset], projects, options[:all_words], options[:user_stem_lang], 
 </pre> 

 h2. lib/xapian_search.rb 

 【注意】このファイルについては,後のコードレビューにてさらに変更する可能性があります.特に権限の部分がそうです. 

 検索インデックスは言語毎に分けてはいない. 

 <pre> 
 @@ -11,69 +11,56 @@ 
                Rails.logger.debug "DEBUG: user_stem_lang: " + user_stem_lang.inspect 
                Rails.logger.debug "DEBUG: user_stem_strategy: " + user_stem_strategy.inspect 
		 Rails.logger.debug "DEBUG: databasepath: " + getDatabasePath(user_stem_lang) 
 - 		 databasepath = getDatabasePath(user_stem_lang) 
 + 		 databasepath = getDatabasePath('') 

		 begin 
 - 		   database = Xapian::Database.new(databasepath) 
 + 		   database = Estraier::Database::new 
 + 		   unless database.open(databasepath, Estraier::Database::DBREADER) 
 + 		     return [xpattachments,0] 
 + 		   end 
		 rescue => error 
		   raise databasepath 
 - 		   return [xpattachments,0] 
                end 

		 # Start an enquire session. 
		
 - 		 enquire = Xapian::Enquire.new(database) 
 + 		 enquire = Estraier::Condition::new 
 + 
 + 		 queryString = tokens.join(all_words ? ' AND ': ' OR ') 
 + 		 enquire.set_phrase(queryString) 
 + 		 enquire.set_max(100) 

 - 		 # Combine the rest of the command line arguments with spaces between 
 - 		 # them, so that simple queries don't have to be quoted at the shell 
 - 		 # level. 
 - 		 #queryString = ARGV[1..-1].join(' ') 
 - 		 queryString = tokens.join(' ') 
 - 		 # Parse the query string to produce a Xapian::Query object. 
 - 		 qp = Xapian::QueryParser.new() 
 - 		 stemmer = Xapian::Stem.new($user_stem_lang) 
 - 		 qp.stemmer = stemmer 
 - 		 qp.database = database 
 - 		 case @user_stem_strategy 
 - 		   when "STEM_NONE" then qp.stemming_strategy = Xapian::QueryParser::STEM_NONE 
 - 		   when "STEM_SOME" then qp.stemming_strategy = Xapian::QueryParser::STEM_SOME 
 - 		   when "STEM_ALL" then qp.stemming_strategy = Xapian::QueryParser::STEM_ALL 
 - 		 end 
 - 		 if all_words 
 - 		   qp.default_op = Xapian::Query::OP_AND 
 - 		 else 	
 - 		   qp.default_op = Xapian::Query::OP_OR 
 - 		 end 
 - 		 query = qp.parse_query(queryString) 
		 Rails.logger.debug "DEBUG queryString is: #{queryString}" 
 - 		 Rails.logger.debug "DEBUG: Parsed query is: #{query.description()} " 

 - 		 # Find the top 100 results for the query. 
 - 		 enquire.query = query 
 - 		 matchset = enquire.mset(0, 1000) 
 + 		 matchset = database.search(enquire) 
	
		 return [xpattachments,0] if matchset.nil? 

		 # Display the results. 
 - 		 #logger.debug "#{@matchset.matches_estimated()} results found." 
 - 		 Rails.logger.debug "DEBUG: Matches 1-#{matchset.size}:¥n" 
 + 		 Rails.logger.debug "DEBUG: Matches 1-#{matchset.doc_num}:¥n" 

 - 		 matchset.matches.each {|m| 
 + 		 dnum = matchset.doc_num 
 + 		 for i in 0...dnum 
 + 		   doc = database.get_doc(matchset.get_doc_id(i), 0) 
 + 		   next unless doc 
		   #Rails.logger.debug "#{m.rank + 1}: #{m.percent}% docid=#{m.docid} [#{m.document.data}]¥n" 
		   #logger.debug "DEBUG: m: " + m.document.data.inspect 
 - 		   docdata=m.document.data{url} 
 - 		   dochash=Hash[*docdata.scan(/(url|sample|modtime|type|size)=¥/?([^¥n¥]]+)/).flatten] 
 - 		   if not dochash.nil? then 
 - 		     find_conditions =    Attachment.merge_conditions (limit_options[:conditions],    :disk_filename => dochash.fetch('url') ) 
 + 
 + 		   uri = doc.attr("@uri") 
 + 		   if uri then 
 + 		     filename = uri.sub(/.*¥//, '') 
 + 
 + 		     find_conditions =    Attachment.merge_conditions (limit_options[:conditions],    :disk_filename => filename) 
		     docattach=Attachment.find (:first, :conditions =>    find_conditions ) 
		     if not docattach.nil? then 
		       if docattach["container_type"] == "Article" and not Redmine::Search.available_search_types.include?("articles") 
                        Rails.logger.debug "DEBUG: Knowledgebase plugin in not installed.." 
                      elsif not docattach.container.nil? then 
			 Rails.logger.debug "DEBUG: adding attach.. "  
 - 		         allowed =    User.current.allowed_to?("view_documents".to_sym, docattach.container.project)    || docattach.container_type == "Article" 
 + 		         # allowed =    User.current.allowed_to?("view_documents".to_sym, docattach.container.project)    || docattach.container_type == "Article" 
 + 		         allowed = true 
		         if ( allowed and project_included(docattach.container.project.id, projects_to_search ) ) 
 - 			   docattach[:description]=dochash["sample"] 
 + 			   docattach[:description] = doc.make_snippet(tokens,50,0,10).to_s 
			   xpattachments.push ( docattach ) 
		         else 
		 	   Rails.logger.debug "DEBUG: user without permissions" 
 @@ -81,7 +68,10 @@ 
		       end 
		     end 
		   end 
 - 		 } 	
 + 		 end 
 + 
 + 		 database.close 
 + 
		 @@numattach=xpattachments.size if offset.nil? 
		 xpattachments=xpattachments.sort_by{|x| x[:created_on] } 
		 [xpattachments, @@numattach] 
 </pre> 


 修正は以上です.Redmine-1.3へ対応したバージョン 1.2.3 の場合の差分ファイルを添付しておきます.