DesktopHEのインデックスを作る

はじめに(2007.10.21)

DesktopHEHyper Estraierを使ったデスクトップ検索ツール)を使ってハードディスク内のファイルの全文検索を行っている.全文検索ではGoogleデスクトップが有名でとても便利なのだが,常駐するのがなんとなく嫌なので今は使っていない.Googleデスクトップを使ってpcが遅くなったと言うわけではなく,常駐するのがただなんとなく嫌というだけである.

そこで,常駐しないDesktopHEを使ってみる事にした.常駐しないと言うことは,インデックスは手動で作ると言うことだ.DesktopHEを動かして,インデックスを作ってと言う作業をいちいち手で行うのは面倒なので,プログラムを作って定期的に動かすことにした.

「そんなことをするぐらいだったら,Googleデスクトップを使えば良いんじゃないか」と思わなくもないが,まあプログラムを作ることにしよう.

言語はrubyにした.流行っているからだ.cygwinで動かす.インデックス作成に使うコマンドやインデックスの格納先を記述する設定ファイルはYAMLで作ることにした.XMLよりマイナーだからだ.

環境は以下の通り.

  • DesktopHE 1.7.0
  • WindowsXP SP2+月次パッチたくさん
  • cygwin版ruby 1.8.5

起動方法を決める(2007.11.4)

スケジューラで動かすつもりなのでコマンドラインで動けば良い.ファイル名(コマンド名)はEst.rbとした.引数に実行したいサブコマンドを指定して起動する.サブコマンドはDesktopHEのインデックスの操作(「ファイル」メニューの下の4個のコマンド)と合わせ

  • gather:インデックス開始
  • purge:削除したファイルの情報を除去
  • extkeys:キーワード抽出
  • optimize:インデックス最適化

の4種類だ.例えば,

$ Est.rb gather

とすればインデックスを開始する.空白で区切って複数のサブコマンドを実行できるようにしよう.例えば,

$ Est.rb gather purge

とすればインデックスを作った後で削除したファイルの情報を除去する.

設定ファイルを作る(2007.11.18)

YAMLの書式で設定ファイルを作る.記述するのは以下の4項目.

  • インデックス作成に使うコマンド
  • インデックスを格納するディレクトリ
  • 検索対象のディレクトリ
  • 実行結果の入ったログ

“インデックス作成に使うコマンド”は以下の手順で確認できる.

  1. DesktopHEのメニューバーから“設定”を選ぶ.
  2. プルダウンメニューの“上級者向けインデックス設定”を選ぶ.

以下のダイアログが出る.

上級者向けインデックス設定

テキストボックスの中に書いてあるのがインデックス作成に使うコマンドだ.

続いて,“インデックスを格納するディレクトリ”と“検索対象のディレクトリ”は以下の手順で確認する.

  1. DesktopHEのメニューバーから“設定”を選ぶ.
  2. プルダウンメニューの“インデックス設定”を選ぶ.

以下のダイアログが出る.

インデックス設定

“インデックスデータ格納フォルダ”と“インデックス対象フォルダ”を確認しよう.

実行結果を入れるログの設定はDesktopHEにはない.どこか適当なディレクトリとファイル名を決めておこう.

これらが決まったら,YAMLファイルを作成する.ファイル名はconf.yamlとした.

command
インデックス作成に使うコマンド
index
インデックスデータ格納フォルダ
dir
検索対象のディレクトリ
log
実行結果を入れるログ

である.特にひねりもなく,そのまま書いた.

#conf.yaml
command:
  gather:
    text:   estcmd gather -il ja -sd -cm -pc CP932 -lf 10
    office: estcmd gather -fx .pdf,.rtf,.doc,.xls,.ppt T@estxfilt -fz -ic CP932 -pc CP932 -sd -cm
  purge:    estcmd purge -pc CP932
  extkeys:  estcmd extkeys -um
  optimize: estcmd optimize

index: C:/Documents and Settings/foo/Application Data/DesktopHE/index

dir:
  - C:/Documents and Settings/foo/デスクトップ
  - C:/Documents and Settings/foo/My Documents

log:
  path: C:/Documents and Settings/foo/デスクトップ
  gather:
    text:   est-gather.log
    office: est-gather.log
  purge:    est-purge.log
  extkeys:  est-extkeys.log
  optimize: est-optimize.log

ディレクトリの区切文字はUNIX風(“/”)でもWindows/DOS風(“\”)でもどちらでも良い.ただし,末尾が“\”で終る場合(例えば“f:\”)はエスケープ(“f:\\”)しないと正しくパスとして認識しない.

初版(2007.12.2)

とりあえず作ってみた初版.

gather以外のサブコマンド(purge, extkeys, optimize)の実行はexecuteメソッドで統一できたが,gatherだけは別個に実装している.その点が不完全.rubyのオプション(-Ks)で文字コードをシフトJISにしている.コマンドの実行にはなくても良いが,コマンドラインに結果を出力する際に文字化けを防ぐことができる.

#!/bin/ruby -Ks

require 'yaml'

class Est
  def initialize file
    @CONST=YAML.load File.open(file)
  end

  def gather
    @CONST['estcmd']['gather'].each_value do |cmd|
      @CONST['dirs'].each do |dir|
        log=%[>>"#{@CONST['log']['path']}/#{@CONST['log']['gather']}"] unless @CONST['log']['gather'].nil?
        `#{cmd} "#{@CONST['index']}" "#{dir}">>"#{@CONST['log']['path']}/#{@CONST['log']['gather']}"`
      end
    end
  end

  def purge
    execute 'purge'
  end

  def extkeys
    execute 'extkeys'
  end

  def optimize
    execute 'optimize'
  end

  def execute command
    `#{@CONST['estcmd'][command]} "#{@CONST['index']}">>"#{@CONST['log']['path']}/#{@CONST['log'][command]}"`
  end
end

METHODS={'gather'=>:gather,'purge'=>:purge,'extkeys'=>:extkeys,'optimize'=>:optimize} # 1. 引数(サブコマンド)と実行するメソッドの対応

est=Est.new 'conf.yaml' # 2. 設定ファイルの読込み
ARGV.each do |arg|
  est.send METHODS[arg] # 3. 引数に対応するメソッドの実行
end

1で引数(サブコマンド)とメソッドとの対応表を作っている.もし,サブコマンドだけではなくてオプション(例えば-g, -p, -e, -o)でもインデックスを操作する場合には

METHODS={'gather'=>:gather,'purge'=>:purge,'extkeys'=>:extkeys,'optimize'=>:optimize,
         '-g'=>:gahter,'-p'=>:purge,'-e'=>:extkeys,'-o'=>:optimize} 

とすればOK.

2で設定ファイルconf.yamlを引数としてEstクラスのインスタンスを作っている.その結果,インスタンスestのインスタンス変数@CONSTは以下のような値となる.

{"command"=>{"purge"=>"estcmd purge -pc CP932", 
             "optimize"=>"estcmd optimize", 
             "gather"=>{"text"=>"estcmd gather -il ja -sd -cm -pc CP932 -lf 10", 
                        "office"=>"estcmd gather -fx .pdf,.rtf,.doc,.xls,.ppt T@estxfilt -fz -ic CP932 -pc CP932 -sd -cm"}, 
             "extkeys"=>"estcmd extkeys -um"}, 
 "dir"=>["C:/Documents and Settings/foo/デスクトップ", 
         "C:/Documents and Settings/foo/My Documents"], 
 "log"=>{"optimize"=>"est-optimize.log", 
         "gather"=>{"text"=>"est-gather.log", 
                    "office"=>"est-gather.log"}, 
         "extkeys"=>"est-extkeys.log"}, 
 "index"=>"C:/Documents and Settings/foo/Application Data/DesktopHE/index"}

3で引数に指定したサブコマンドに対応するメソッドを呼ぶ.

purge/extkeys/optimizeは引数が同じ(HyperEstraierのコマンド,インデックスデータ格納フォルダ,ログ)なので,実行はexecuteメソッドに統合した.

gatherは他のコマンドより引数が多い(HyperEstraierのコマンド,インデックスデータ格納フォルダ,検索対象のディレクトリ,ログ).また,テキスト用とオフィス用でコマンドが別れているので,executeメソッドに統合できていない.

gatherもexecuteに統一版(2007.12.15)

purge/extkeys/optimizeメソッドと同様に,gatherメソッドでもexecuteメソッドを呼ぶことにした.検索対象ディレクトリをexecuteメソッドの引数に追加,この引数を使うのはgatherメソッドだけで,purge/extkeys/optimizeメソッドではnilを設定している.

#!/bin/ruby -Ks

require 'yaml'

class Est
  def initialize file
    @CONST=YAML.load File.open(file)
  end

  def gather
    @CONST['estcmd']['gather'].each do |operation,cmd|
      @CONST['dirs'].each do |dir|
        execute cmd,dir,@CONST['log'][operation]
      end
    end
  end

  def purge
    execute @CONST['estcmd']['purge'],nil,@CONST['log']['purge']
  end

  def extkeys
    execute @CONST['estcmd']['extkeys'],nil,@CONST['log']['extkeys']
  end

  def optimize
    execute @CONST['estcmd']['optimize'],nil,@CONST['log']['optimize']
  end

  def execute cmd,dir=nil,log=nil
    exec=%[#{cmd} "#{@CONST['index']}"]
    exec<<%[ "#{dir}"] unless dir.nil?
    exec<<%[>>"#{@CONST['log']['path']}/#{log}"] unless log.nil?
    `#{exec}`
  end
end

METHODS={'gather'=>:gather,'purge'=>:purge,'extkeys'=>:extkeys,'optimize'=>:optimize}

est=Est.new 'conf.yaml'
ARGV.each do |arg|
  est.send METHODS[arg]
end

古いログを消す(2007.12.30)

ログが残ったままなので,コマンド実行前に削除することにした.サブコマンドに対応したログを選び出すremoveLogメソッドとログを消すremoveメソッドを追加.

removeメソッドの中にあるハッシュproc(ソース中の1)を説明しよう.このハッシュはremoveメソッドの引数の型と実行する処理を対応付けたものである.

removeメソッドの引数がハッシュだった場合は,まだログファイル自体にたどり着いていない.したがって,引数がStringになるまでremoveメソッドを再帰的に呼ぶ.removeメソッドの引数が文字列だった場合は,その文字列のファイルを削除する.

gahterメソッドの場合を考えてみよう.まず@conf['log']['gather']を引数にremoveメソッドを呼ぶ.この時@conf['log']['gather']には以下のハッシュが入っている.gahterではtext用とoffice用の二つのログを出力するためだ.

@conf['log']['gather']={"text"=>"est-gather.log","office"=>"est-gather.log"}

削除対象のログはハッシュの中にあるので,ハッシュの値を引数にremoveを呼ぶ.したがってこのハッシュの場合,キー"text"の値"est-gather.log"と,キー"office"の値"est-gather.log"を引数にremoveメソッドを呼ぶ.

キーの型がStringの場合はその値をログファイルの名前としてファイルを削除する.ログファイルが存在しないとエラーが発生するが,無視する.

ログの削除をインデックス操作の前に実施する(ソースファイルの2).

#!/bin/ruby -Ks

require 'yaml'

class Est
  attr_accessor :conf,:log,:dir

  def initialize file
    self.conf=YAML.load File.open(file)
  end

  def gather
    @conf['dir'].each do |dir|
      execute @conf['command']['gather'],@conf['log']['gather'],dir
    end
  end

  def purge
    execute @conf['command']['purge'],@conf['log']['purge']
  end

  def extkeys
    execute @conf['command']['extkeys'],@conf['log']['extkeys']
  end

  def optimize
    execute @conf['command']['optimize'],@conf['log']['optimize']
  end

  def execute cmd,log=nil,dir=nil
    if cmd.class==String
      exec=%[#{cmd} "#{@conf['index']}"]
      exec<<%[ "#{dir}"] unless dir.nil?
      exec<<%[>>"#{(@conf['log']['path']||=".")}/#{log}"] unless log.nil?
      `#{exec}`
    else
      cmd.each do |key,cmd|
        execute cmd,dir,log[key]
      end
    end
  end

  def removeLog cmds
    cmds.each do |cmd|
      remove @conf['log'][cmd]
    end
  end

  def remove log
    proc={Hash=>Proc.new {|log| log.each_value{|log| remove log}},
          String=>Proc.new {|log| File.delete log rescue nil}} # 1. 引数logの型と実行する処理の対応付け
    proc[log.class].call log unless proc[log.class].nil?
  end
end

METHODS={'gather'=>:gather,'purge'=>:purge,'extkeys'=>:extkeys,'optimize'=>:optimize}

ARGV.uniq!
incorrectArg=ARGV-METHODS.keys
abort <<END unless incorrectArg.empty?
incorrect argument(s): #{incorrectArg.join ','}
usage: Est.rb [gather|purge|extkeys|optimize]...
END

est=Est.new 'conf.yaml'
est.removeLog ARGV # 2. コマンドの実行前にログを削除
ARGV.each do |arg|
  est.send METHODS[arg]
end

コマンドオブジェクト導入(2008.1.13)

conf.yamlを見てみると,HyperEstraierのコマンドもログと同様に階層構造を持っている.つまり,ハッシュの値がハッシュだったら複数のコマンドがあり,文字列だったらそれが実行するコマンドと言うことだ.そこで,コマンドもログと同様にデータ型によって実行するメソッドを決めることにしよう.そのために,コマンドオブジェクトを定義した.

コマンドオブジェクトのサブクラスとしてEstCommand(HyperEstraier用のコマンド)とRmCommand(ログファイル削除用のコマンド)を定義(ソースコード中の1と3).それぞれの中に引数のデータ型に対応した処理を記述してハッシュprocを用意した(ソースコード中の2と4).

例えばHyperEstraierのgatherコマンドを実行する場合,@conf['command']['gahter']を引数にEstCommandのインスタンスを生成する(ソースコード中の5).conf['command']['gahter']には以下のハッシュが入っている.

@conf['log']['gather']={"text"=>"est-gather.log","office"=>"est-gather.log"}

ハッシュなので,各要素の値を引数にもう一度EstCommandのインスタンスを生成する.そうすると引数が文字列となり,文字列に該当するHyperEstraierのコマンドを実行する.

RmCommandも同様で,ハッシュなら値を引数に再度RmCommandのインスタンスを生成する.引数が文字列ならログファイルを削除する.

#!/bin/ruby -Ks

require 'yaml'

class Command
  def execute
    proc[conf['handler'].class].call
  end
end

class EstCommand<Command # 1. HyperEstraier用のコマンド
  attr_accessor :conf,:proc,:dir

  def initialize conf
    self.conf=conf
    # 2. 引数confのキー"handler"の値の型によって実行する処理を記述.
    self.proc={
      Hash=>Proc.new {
        conf['handler'].each do |key,command|
          EstCommand.new('handler'=>command,
                         'index'=>conf['index'],
                         'dir'=>conf['dir'],
                         'log'=>{'file'=>conf['log']['file'][key],
                                 'path'=>conf['log']['path']}).execute
        end
      },
      String=>Proc.new {
        exec=%[#{conf['handler']} "#{conf['index']}"]
        exec<<%[ "#{conf['dir']}"] unless conf['dir'].nil?
        exec<<%[>>"#{(conf['log']['path']||=".")}/#{conf['log']['file']}"] unless conf['log']['file'].nil?
        `#{exec}`
      }
    }
  end

  def dir=dir
    self.conf['dir']=dir
  end
end

class RmCommand<Command # 1. ログ削除用のコマンド
  attr_accessor :conf,:proc

  def initialize conf
    self.conf=conf
    # 2. 引数confのキー"handler"の値の型によって実行する処理を記述.
    self.proc={
      Hash=>Proc.new {
        conf['handler'].each_value do |log|
          RmCommand.new('handler'=>log).execute
        end
      },
      String=>Proc.new {
        File.delete conf['handler'] rescue nil unless conf['handler'].nil?
      },
      NilClass=>Proc.new {nil}
    }
  end
end

class Est
  attr_accessor :conf

  def initialize file
    self.conf=YAML.load File.open(file)
  end

  def gather
    command=prepare 'gather'
    conf['dir'].each do |dir|
      command.dir=dir
      command.execute
    end
  end

  def purge
    prepare('purge').execute
  end

  def extkeys
    prepare('extkeys').execute
  end

  def optimize
    prepare('optimize').execute
  end

  def prepare command
    RmCommand.new('handler'=>conf['log'][command]).execute
    EstCommand.new 'handler'=>conf['command'][command],
                   'index'=>conf['index'],
                   'log'=>{'file'=>conf['log'][command],'path'=>conf['log']['path']}
    # 5. EstCommandのインスタンス生成
  end
end

METHODS={'gather'=>:gather,'purge'=>:purge,'extkeys'=>:extkeys,'optimize'=>:optimize}

ARGV.uniq!
incorrectArg=ARGV-METHODS.keys
abort <<END unless incorrectArg.empty?
incorrect argument(s): #{incorrectArg.join ','}
usage: Est.rb [gather|purge|extkeys|optimize]...
END

est=Est.new 'conf.yaml'
ARGV.each do |arg|
  est.send METHODS[arg]
end