Hyper Estraier で検索

カテゴリ
ブックマーク数
このエントリーを含むはてなブックマーク はてなブックマーク - Hyper Estraier で検索
このエントリーをはてなブックマークに追加
はじめまして。ライブドアの山本です。

弊社は最近 CGMコンテンツに注力しています。
データがたまってくると、ユーザーが必要な情報をすばやく得られるように検索機能を実装する必要がでてきます。

各コンテンツそれぞれで検索機能を実装しているのですが、mysql から直接引いたり、NamazuSUFARY などの検索エンジンもかなり使ったりしています。

今回は某コンテンツの検索エンジンとして使用している HyperEstraier のちょっとした導入方法をご紹介します。

HyperEstraier は平林幹雄さんが開発された検索エンジンで、次のような特徴があります。

* インデックスを使った高速な検索ができます。
* 大量の文書のインデックスを短時間で作成できます。
* N-gram方式による漏れのない検索ができます。
* 形態素解析とN-gramのハイブリッド機構で検索精度を向上させます。
* フレーズ検索や正規表現検索や属性検索や類似検索をサポートします。
* 世界各国の言語が扱えます。
* 対象文書の所在や形式に依存しません。
* 賢いWebクローラが付属しています。
* ライブラリとして各種製品に組み込めます。
* P2P連携機能をサポートします。

※本家サイトから抜粋

インストール

まずは、本家サイトから最新版をダウンロードします。
※現在の最新版は、1.4.10 です。(2007/8/6現在)

HyperEstraier は以下のライブラリを必要としますので、こちらも併せてダウンロード・インストール
しておきます。
libiconv : 文字コード変換。バージョン1.9.1以降(glibcにも同梱)。
zlib : 可逆データ圧縮。バージョン1.2.1以降。
QDBM : 組み込み用データベース。バージョン1.8.75以降。

インストールは通常通り、configure、make、make install です。

> wget http://hyperestraier.sourceforge.net/hyperestraier-1.4.10.tar.gz
> tar zxvf hyperestraier-1.4.10.tar.gz
> cd ./hyperestraier-1.4.10
> ./configure
> make
> make check
> make install

必要なライブラリさえあれば問題なくインストールできると思います。


インデックスの作成

Hyper Estraier はインデックス型の検索エンジンです。
検索対象の情報を登録したインデックスを準備して、そこから対象を検索する為、非常に高速に検索することができます。

今回はDB に登録された情報から、文書ドラフト形式に変換してインデックスを作成してみます。また文字コードはすべて utf-8 です。
文書ドラフトとは、「@key=value」の組み合わせからなる Hyper Estraier 独自のデータ形式です。
システム属性として定義されているもの以外にも、ユーザーが独自に定義して記述することができます。
今回使用するのは

@uri=http://localhost/casket/detail.cgi?id=ID
@title=タイトル
@cdate=yyyy-mm-ddTHH:MM:SS+09:00
[改行]
本文

という形式です。

情報を登録するテーブルは下記のように定義します。

create table entry (
id int(10) unsigned auto_increment not null,
title varchar(255) not null,
body text not null,
created_on datetime not null DEFAULT '0000-00-00 00:00:00',
updated_on timestamp(14) not null,
PRIMARY KEY (id),
KEY title (title),
KEY created_on (created_on)
) Type=InnoDB;

+------------+------------------+------+-----+---------------------+----------------+
| Field | Type | Null | Key | Default | Extra |
+------------+------------------+------+-----+---------------------+----------------+
| id | int(10) unsigned | NO | PRI | NULL | auto_increment |
| title | varchar(255) | NO | MUL | | |
| body | text | NO | | | |
| created_on | datetime | NO | MUL | 0000-00-00 00:00:00 | |
| updated_on | timestamp | NO | | CURRENT_TIMESTAMP | |
+------------+------------------+------+-----+---------------------+----------------+

テーブルにデータを insert します。

INSERT INTO entry (title, body, created_on) values ('greeting1', 'おはようございます。livedoor 開発 blog です。', now());
INSERT INTO entry (title, body, created_on) values ('greeting2', 'こんにちは。livedoor 開発 blog です。', now());
INSERT INTO entry (title, body, created_on) values ('greeting3', 'こんばんは。livedoor 開発 blog です。', now());

インデックスを管理するのは、「estcmd」というコマンドを使用します。
estcmd はサブコマンドの集合体で、第1引数にサブコマンドを指定することでいろいろな機能を使うことが出来ます。

主なものとしては、これらがあります。
estcmd create  - インデックスを作成します。
estcmd put - 文書ドラフト形式のファイルを登録します。
estcmd search - インデックスに登録された文書を検索します。
estcmd extkeys - インデックス内の各文書のキーワードを抽出したデータベースを作成します。

その他のサブコマンドについては付属のドキュメントに詳しい説明があります。

それでは実際にインデックスに登録してみましょう。
以下のようなスクリプトで登録していきます。

indexer.pl

#!/usr/local/bin/perl
use strict;
use DBI;
use Readonly;
use Encode;
use DateTime::Format::MySQL;

Readonly my $datasources => [ 'dbi:mysql:hyper_estraier_test', 'root', '' ];
Readonly my $base_dir => "/tmp";
Readonly my $node_path => "$base_dir/casket/";
Readonly my $ESTCMD => "/usr/local/bin/estcmd";
Readonly my $DRAFT_FORMAT => <<'END_DRAFT'
@uri=http://localhost/casket/detail.cgi?id=%d
@title=%s
@cdata=%s+09:00

%s
END_DRAFT
;

## create node
system( ESTCMD, "create", $node_path )
unless -d $node_path;

## indexing
my $entries = get_entries();
for my $entry (@$entries) {
do_estcmd("put", {
entry => $entry,
opts => [qw(-cl)],
}, \&_mk_draft);
}
do_estcmd("extkeys", { opts => [qw(-um)] });

sub build_estcmd {
my ( $subcmd, $opts ) = @_;
my $cmd = join( ' ', $ESTCMD, $subcmd, @$opts, $node_path );
return $cmd;
}

sub do_estcmd {
my ($subcmd, $params, $callback) = @_;
my $command = build_estcmd($subcmd, $params->{opts});
open( CMD, "| $command" ) || die $!;
print CMD $callback->($params->{entry}) if $callback;
close( CMD );
}

sub _mk_draft {
my $entry = shift;
my $dt = DateTime::Format::MySQL->parse_datetime( $entry->{created_on} );
my $input = sprintf( $DRAFT_FORMAT,
$entry->{id}, $entry->{title}, $dt, $entry->{body} );
return $input;
}

sub get_entries {
my $dbh = DBI->connect( @{$datasources},
{ RootClass => "DBIx::ContextualFetch" } )
or die $DBI::errstr;
my $sql = qq{SELECT * FROM entry};
my $sth = $dbh->prepare($sql);
$sth->execute;
my $entries = $sth->fetchall_hash;
$sth->finish;
$dbh->disconnect;
return $entries;
}

1;

__END__


このスクリプトをindexer.pl という名前で保存して実行します。

> ./indexer.pl
ESTCMD: INFO: status: name=/tmp/casket/ dnum=0 wnum=0 fsiz=6899176 crnum=0 csiz=0 dknum=0
ESTCMD: INFO: closing: name=/tmp/casket/ dnum=0 wnum=0 fsiz=6899280 crnum=0 csiz=0 dknum=0
/usr/local/bin/estcmd: INFO: status: name=/tmp/casket/ dnum=0 wnum=0 fsiz=6899280 crnum=0 csiz=0 dknum=0
/usr/local/bin/estcmd: INFO: 1 (http://localhost/casket/detail.cgi?id=1): registered
/usr/local/bin/estcmd: INFO: flushing index words: name=/tmp/casket/ dnum=1 wnum=1 fsiz=6899705 crnum=15 csiz=678 dknum=0
/usr/local/bin/estcmd: INFO: closing: name=/tmp/casket/ dnum=1 wnum=16 fsiz=6899954 crnum=0 csiz=0 dknum=0
/usr/local/bin/estcmd: INFO: status: name=/tmp/casket/ dnum=1 wnum=16 fsiz=6900125 crnum=0 csiz=0 dknum=0
/usr/local/bin/estcmd: INFO: 2 (http://localhost/casket/detail.cgi?id=2): registered
/usr/local/bin/estcmd: INFO: flushing index words: name=/tmp/casket/ dnum=2 wnum=16 fsiz=6900429 crnum=12 csiz=541 dknum=0
/usr/local/bin/estcmd: INFO: closing: name=/tmp/casket/ dnum=2 wnum=21 fsiz=6900450 crnum=0 csiz=0 dknum=0
/usr/local/bin/estcmd: INFO: status: name=/tmp/casket/ dnum=2 wnum=21 fsiz=6900450 crnum=0 csiz=0 dknum=0
/usr/local/bin/estcmd: INFO: 3 (http://localhost/casket/detail.cgi?id=3): registered
/usr/local/bin/estcmd: INFO: flushing index words: name=/tmp/casket/ dnum=3 wnum=21 fsiz=6900749 crnum=12 csiz=541 dknum=0
/usr/local/bin/estcmd: INFO: closing: name=/tmp/casket/ dnum=3 wnum=24 fsiz=6900785 crnum=0 csiz=0 dknum=0
/usr/local/bin/estcmd: INFO: status: name=/tmp/casket/ dnum=3 wnum=24 fsiz=6900785 crnum=0 csiz=0 dknum=0
/usr/local/bin/estcmd: INFO: 1 (http://localhost/casket/detail.cgi?id=1): extracted
/usr/local/bin/estcmd: INFO: 2 (http://localhost/casket/detail.cgi?id=2): extracted
/usr/local/bin/estcmd: INFO: 3 (http://localhost/casket/detail.cgi?id=3): extracted
/usr/local/bin/estcmd: INFO: flushing auxiliary keywords: name=/tmp/casket/ dnum=3 wnum=24 fsiz=6901210 crnum=11 csiz=638 dknum=0
/usr/local/bin/estcmd: INFO: closing: name=/tmp/casket/ dnum=3 wnum=24 fsiz=6901458 crnum=0 csiz=0 dknum=0
/usr/local/bin/estcmd: INFO: finished successfully: elapsed time: 0h 0m 0s

estcmd が吐く情報が表示されてインデックスの作成は完了です。


コマンドラインからの検索

さて、うまくインデックスが作成できたかどうかコマンドラインから確認してみます。
検索は estcmd search を使用します。

まずは、1番目のエントリーでのみ使われている「こんにちは」というキーワードで検索してみます。
端末の文字コードが EUC-JP の場合の例ですので、適宜変更してください。

> /usr/local/bin/estcmd search -ic euc-jp -vh /tmp/casket こんにちは | nkf -e
--------[02D18ACF53AEE5C5]--------
VERSION 1.0
NODE local
HIT 1
HINT#1 こんにちは 1
TIME 0.000679
DOCNUM 3
WORDNUM 24
VIEW HUMAN

--------[02D18ACF53AEE5C5]--------

URI: http://localhost/casket/detail.cgi?id=2
Title: greeting2

こんにちは。livedoor 開発Blog です。 ...


--------[02D18ACF53AEE5C5]--------:END

一番目のデータのみ抽出できました。

次にすべてのエントリーで使用されている、「livedoor」というキーワードで検索してみます。

> /usr/local/bin/estcmd search -ic euc-jp -vh /tmp/casket livedoor | nkf -e
--------[21BF06E078309435]--------
VERSION 1.0
NODE local
HIT 3
HINT#1 livedoor 3
TIME 0.000640
DOCNUM 3
WORDNUM 24
VIEW HUMAN

--------[21BF06E078309435]--------

URI: http://localhost/casket/detail.cgi?id=1
Title: greeting1

おはようございます。livedoor 開発Blog です。 ...


--------[21BF06E078309435]--------

URI: http://localhost/casket/detail.cgi?id=2
Title: greeting2

こんにちは。livedoor 開発Blog です。 ...


--------[21BF06E078309435]--------

URI: http://localhost/casket/detail.cgi?id=3
Title: greeting3

こんばんは。livedoor 開発Blog です。 ...


--------[21BF06E078309435]--------:END


すべてのエントリーが抽出されたことが確認できると思います。


ブラウザからの検索

Hyper Estraier には「estseek.cgi」というCGIスクリプトも提供されていますのでこちらを使用して web ブラウザから検索してみます。

estseek.cgiが動作するには、以下のファイルが必要です。

estseek.conf - 設定ファイル
estseek.tmpl - テンプレートファイル
estseek.top - トップページファイル
estseek.help - ヘルプファイル


設定ファイルの名前は、cgiスクリプトの 拡張子を conf に変えたものなので hoge.cgi としたならば、hoge.conf が読み込まれます。
その他のファイルは設定ファイルの中で指定できます。

estseek.cgi は /usr/local/libexec/estseek.cgi としてインストールされていますので、こちらを適当なディレクトリーにコピーして使います。
また、設定ファイルや表示用テンプレートの日本語化されたサンプルが /usr/local/share/hyperestraier/locale/ja/ にありますので、こちらもコピーして使用します。

Hyper Estraier には検索結果をハイライト表示してくれる、estproxy.cgi も提供されています。
こちらも本体が /usr/local/libexec/estproxy.cgi、conf ファイルが /usr/local/share/hyperestraier/estproxy.conf にインストールされていますのでこれらを使用します。

今回のディレクトリ構成はこちらです。

/home/www/hyperestraier/etc - apache の設定ファイル
/htdocs - cgi スクリプトや estseek.conf など
/tmpl - 表示用テンプレート


各種ファイルの内容は下記のようにしました。
▼estseek.conf

indexname: /tmp/casket
tmplfile: /home/www/hyperestraier/tmpl/estseek.tmpl
topfile: /home/www/hyperestraier/tmpl/estseek.top
helpfile: /home/www/hyperestraier/tmpl/estseek.help
lockindex: true
pseudoindex:
showlreal: false
deftitle: 超迷子: 共同体的全文検索系
formtype: web
perpage: 10 100 10
attrselect: false
attrwidth: 80
showscore: true
snipwwidth: 480
sniphwidth: 96
snipawidth: 96
condgstep: 2
dotfidf: true
scancheck: 3
dispproxy: ./estproxy.cgi
phraseform: 2
candetail: true
candir: false
auxmin: 32
smlrvnum: 32
smlrtune: 16 1024 4096
clipview: 2
clipweight: none
relkeynum: 0
spcache:
wildmax: 256
qxpndcmd:
logfile:
logformat: {time}\t{REMOTE_ADDR}:{REMOTE_PORT}\t{cond}\t{hnum}\n

▼apache の conf ファイル

Alias /casket /home/www/hyperestraier/htdocs
AddHandler cgi-script .cgi
<Files ~ ".*\.conf$>
Deny from all
</Files>
<Directory "/home/www">
Options ExecCGI
AllowOverride None
Order allow,deny
Allow from all
</Directory>

▼詳細表示用 CGI スクリプト
detail.cgi

#!/usr/local/bin/perl
use strict;
use warnings;
use CGI;
use CGI::Carp 'fatalsToBrowser';
use DBI;
use Readonly;

Readonly my $datasources => [ 'dbi:mysql:hyper_estraier_test', 'root', '' ];

my $q = CGI->new;
my $entry = get_entry( $q->param('id') );
print $q->header(
-type => 'text/html',
-charset => 'utf-8'
);
print $q->start_html( -title => $entry->{title} );
print $entry->{body};
print $q->end_html;

sub get_entry {
my $id = shift;
my $dbh = DBI->connect( @{$datasources},
{ RootClass => "DBIx::ContextualFetch" } )
or die $DBI::errstr;
my $sql = qq{SELECT * FROM entry WHERE id = ?};
my $sth = $dbh->prepare($sql);
$sth->execute($id);
my $entry = $sth->fetch_hash;
$sth->finish;
$dbh->disconnect;
return $entry;
}

1;

__END__

estproxy.conf はサンプルそのままです。

apache の設定ファイルを Include するか、直接追記して再起動します。

再起動が完了したら、 ブラウザから http://localhost/casket/estseek.cgi にアクセスしてみてください。
検索のトップ画面が表示されればOKです。


まとめ

さて、如何でしたでしょうか。
いろいろなサイトで解説されているのですでに知ってる方にはつまらない内容だったかと思います。

今回のインデックスの登録方法ですと、estcmdでデータベースの更新作業を行っている間は、データベースがロックされてしまうので検索ができません。
それを回避させる方法はいろいろあるのですが、Hyper Estraier にはC/S(クライアント/サーバー)方式のプログラムも提供されていますので、次回はそちらも少し紹介したいと思います。

レスポンス
コメント(0)
トラックバック(0)

このエントリーをはてなブックマークに追加