PowerShellからLucene.netを使ってみる

| 2009年11月17日火曜日
@ITのこの記事を見て使ってみたくなったのでさわってみた。
お試しなので単純にできるように、テキストファイル限定で中身を全文検索してファイル名を返すようにしようと思う。

まずは、DLLをダウンロードしてくる。場所は記事に書かれているところから飛べるのでそこから落としてくる。
落としてくるのは、Lucene.Netと記事の筆者が作成された日本語用のDLLがあるのでそちらも落としてる。

記事ではLucene.Net.Analysys.Ja.JapaneseAnalyereを使用しているけど、TokenStreamメソッドを使おうとすると下の例外が出て使えなかった。

"2" 個の引数を指定して "TokenStream" を呼び出し中に例外が発生しました: "保護されているメモリに読み取りまたは書き込み操作を行おうとしました。他のメモリが壊れていることが考えられます。"
発生場所 行:1 文字:37
+ $tokenStream = $analyzer.TokenStream <<<< ("",$stringReader)
+ CategoryInfo : NotSpecified: (:) []、MethodInvocationException
+ FullyQualifiedErrorId : DotNetMethodTargetInvocation

できればJapaneseAnalyereを使って見たかったがしかたがない。今回はLuceneで全文検索をすることが目的なので、代わりにLucene.Net.Analysis.CJK.CJKAnalyzerの方を使う。
今度時間のある時に調べてみよう。N-gramより形態素解析の方が面白そうだからね。

Lucene.netを試してる時に、PowerShellからDLLのインナークラスの使い方がわからなかった。
「Lucene.Net.Documents.Field.Store.YES」と「Lucene.Net.Documents.Field.Index.TOKENIZED」は、ソースをみる限りでは、Lucene.Net.Documents.Fieldクラスのインナークラスみたいだけど、.(ドット)でつないだのではアクセスできなかった。
例外を出してその内容からわかった使い方は以下

[Lucene.Net.Documents.Field+Store]::YES
[Lucene.Net.Documents.Field+Index]::TOKENIZED

+(プラス)ってなんだ??
よくわからんが、インナークラスを使う時は+(プラス)でつなげるようだ。
一応、小さいインナークラスのDLLを作って試したけど、どうやらそうらしい。
しかし、これはildasmでDLLをみてもぱっと見わかんないな。
まぁDLLを読み込んだあとならPowerShellのTab補完が効くか効かないかで判断できるけど…in Actionに載ってたかなぁ…?

自分の試したサンプル下に貼っておく。
サンプルを実行するのに必要なDLLは下の二つなので、適当なフォルダにコピーしておく。

・Lucene.Net.dll
・Lucene.Net.Analysis.CJK.dll

そのフォルダに移動してから、サンプルをコピー&ペーストしてPowerShellに読み込ませる。
(たぶん、単純にコピーすると行番号までついてくるのでメモ帳にでも貼って番号を削除してからコピーして貼り付ければいい)

使い方は、

インデックスフォルダとインデックスファイルのベースの作成する。
> Create-LuceneIndex -IndexPath C:\LuceneIndex

インデックスファイルに指定したファイルを追加する。インデックスに追加されるのは、ファイル名(フルパス)とテキストファイルの中身。
追加するファイルを渡す引数の型は「System.IO.FileInfo」なので、「ls」の結果を変数に詰めて渡すか。フルパスを書く必要がある。
> Add-LuceneDocument -IndexPath C:\LuceneIndex -file c:\hoge\foo.txt

検索する。ファイルのフルパスが返ってくる。
> Find-LuceneDocument -IndexPath C:\LuceneIndex -word foo

インデックスを削除する。
> Remove-LuceneDocument -IndexPath C:\LuceneIndex -file c:\hoge\foo.txt

他にも追加と削除を組み合わせて更新用の関数なんかもあれば便利だと思う。
このサンプルのままで日本語も検索できるから、あと実際の業務で使おうと思ったら、クローラとOffice系のファイルの読み込みが必要かな。
Office系のファイルの読み込みならAdd-LuceneDocument関数内の

$contents = Get-Content $file

の部分を変更して、xDoc2txtなどを使う必要があると思う。
まぁPowerShellならクローラは簡単だと思うし、拡張子を見て$contentに入れる中身を変えるのも難しくないだろう。

ちょっと長いけど、サンプル↓
  1. [void][System.Reflection.Assembly]::LoadFrom((Join-Path $pwd Lucene.Net.dll))
  2. [void][System.Reflection.Assembly]::LoadFrom((Join-Path $pwd Lucene.Net.Analysis.CJK.dll))
  3. # 元となるインデックスを作成する
  4. function Create-LuceneIndex([string]$indexPath) {
  5. # インデックスが存在した場合はフォルダを削除して作り直す
  6. if (Test-Path $indexPath) {
  7. if ([Lucene.Net.Index.IndexReader]::IndexExists($indexPath)) {
  8. [void][System.IO.Directory]::Delete($indexPath, $true)
  9. [void][System.IO.Directory]::CreateDirectory($indexPath)
  10. }
  11. }
  12. $indexWriter = New-Object Lucene.Net.Index.IndexWriter($indexPath, ( New-Object Lucene.Net.Analysis.CJK.CJKAnalyzer ), $true)
  13. $indexWriter.Close()
  14. }
  15. # インデックスにファイルを追加する
  16. function Add-LuceneDocument([string]$indexPath, [System.IO.FileInfo]$file) {
  17. # Documentオブジェクトの作成
  18. $document = New-Object Lucene.Net.Documents.Document
  19. # Fieldオブジェクトの作成
  20. $fileName = $file.FullName
  21. $contents = Get-Content $file
  22. $fieldFileName = New-Object Lucene.Net.Documents.Field("filename", $fileName, [Lucene.Net.Documents.Field+Store]::YES, [Lucene.Net.Documents.Field+Index]::TOKENIZED)
  23. $fieldContents = New-Object Lucene.Net.Documents.Field("contents", $contents, [Lucene.Net.Documents.Field+Store]::YES, [Lucene.Net.Documents.Field+Index]::TOKENIZED)
  24. # Documentオブジェクトにフィールドを追加
  25. $document.Add($fieldFileName)
  26. $document.Add($fieldContents)
  27. $indexWriter = New-Object Lucene.Net.Index.IndexWriter($indexPath, ( New-Object Lucene.Net.Analysis.CJK.CJKAnalyzer ), $false)
  28. # インデックスにDocumentオブジェクトを追加
  29. $indexWriter.AddDocument($document)
  30. # インデックスを最適化する
  31. $indexWriter.Optimize()
  32. $indexWriter.Close()
  33. }
  34. # インデックスからファイルを削除する
  35. function Remove-LuceneDocument([string]$indexPath, [System.IO.FileInfo]$file) {
  36. # アナライザの準備
  37. $analyzer = New-Object Lucene.Net.Analysis.CJK.CJKAnalyzer
  38. # ファイル名を対象にクエリを作成
  39. $queryPsr = New-Object Lucene.Net.QueryParsers.QueryParser("filename", $analyzer)
  40. $word = $file.Name
  41. $query = $queryPsr.Parse($word)
  42. # インデックスからヒットするものを探す
  43. $searcher = New-Object Lucene.Net.Search.IndexSearcher($indexPath)
  44. $hits = $searcher.Search($query)
  45. $target = $file.FullName
  46. # ヒットしたものをインデックスから削除する
  47. $indexReader = [Lucene.Net.Index.indexReader]::Open($indexPath)
  48. for ($i = 0; $i -lt $hits.Length(); $i++) {
  49. $doc = $hits.Doc($i)
  50. # ファイルのフルパスと一致するものだけを削除する
  51. if ($doc.Get("filename") -eq $target) {
  52. $indexReader.DeleteDocument($hits.Id($i))
  53. }
  54. }
  55. $indexReader.Close()
  56. }
  57. # インデックスから検索する
  58. function Find-LuceneDocument([string]$indexPath, [string]$word) {
  59. # アナライザの準備
  60. $analyzer = New-Object Lucene.Net.Analysis.CJK.CJKAnalyzer
  61. # ファイルの中身を対象にクエリを作成
  62. $queryPsr = New-Object Lucene.Net.QueryParsers.QueryParser("contents", $analyzer)
  63. $query = $queryPsr.Parse($word)
  64. # インデックスからヒットするものを探す
  65. $searcher = New-Object Lucene.Net.Search.IndexSearcher($indexPath)
  66. $hits = $searcher.Search($query)
  67. # Lucene.Net.Documents.Documentの配列を作成してヒットしたものを詰める
  68. [Lucene.Net.Documents.Document[]]$docments = New-Object Lucene.Net.Documents.Document[]($hits.Length())
  69. for ($i = 0; $i -lt $hits.Length(); $i++) {
  70. $docments[$i] = $hits.Doc($i)
  71. }
  72. return ($docments | % { $_.Get("filename") })
  73. }

0 コメント: