FreyaSX開発メモ(4) -- 23 June, 2004, Yutaka Sato

なんちゃって機能拡張

2〜3日の移植作業で、とりあえず Freya が iMac 上で動くようになった (といっても実はあちこち問題が残っていたことが後になってわかったのですが) ので、早速 DeleGateメーリングリスト(http://www.delegate.org/delegate/goiken/) の検索用に使い始めたのですが、それまで使っていた検索エンジンと使い方が かなり異なるため、正直言って多少使いにくく、また機能の不足も感じました。 (ユーザ・インターフェイスでは、技術的に見ればほんのちょっとした違いが、 使い易さの上での大きな違いになって現れます。良い悪いという問題以上に、 従来のものや標準的な操作モデルと違うということが、ユーザの混乱を招いて 違和感のモトとなります)

そんなわけで、とりあえず違和感を感じた部分を変更し、必要と思った機能を 拡張することになりました。なにはともあれ、利用者としての立場から、 使い易さを向上したかったので、以下の機能を拡張しました。

その後、検索エンジンを運用する立場から、インストールや設定・維持の作業 をより簡便にするために、以下の機能を拡張しました。 いずれも、とりあえずやってみました程度の実装ですが。また、後半の拡張機能に ついては、実装面だけでなく仕様上でも、より未確定な感じではあります。 以下は、それぞれの拡張についての概要です。

AND検索をデフォルトに

やはり違和感を感じたのは、デフォルトがOR検索だということです。 Freyaが開発された1997年当時には色んな流儀があったようにも思いますが、 その後Googleをはじめとして、AND検索が完全に世界標準になりました。 日常的にGoogleを使っていて、すでに体がそれに慣れてしまっていますので、 FreyaでAND検索をする時に、検索語をスペースで区切って並べるだけでなく、 語の先頭に "+" をつけなければならないというのは、かなり負担に感じます。

実際FreyaによるDeleGate-MLの検索機能を公開してしばらく、検索キーのログを 見てみると、どのユーザもAND検索を前提にして検索を行っている様子がうかがえ ました。一応検索例として、AND検索には "+" を前置しないといけないという ことを示してはいたのですが、やはりこれをユーザに押しつけるのは無理だろうなと。 何より、自分としても慣れることができなそうに感じましたので、デフォルト はAND検索にしたいと思いました。

Freya のソースプログラムの中で、検索式を解釈しているのが、Expression.cc です。SimpleExpression::parse() の中で、先頭に "+" が付いていれば AND、 そうでなければ OR になるという形になっているようですので、この解釈を 逆転させるスイッチを付けました。つまり、逆転した状態では、 先頭に何もついていなければ AND、"+" がついていれば OR になるという具合で、 これをデフォルトとすることにしました。

Freyaのソースプログラムの中で、CGIによる検索を実装しているのが fsearch.cgi.cc です。ここに、このAND検索かOR検索かを切替えるインター フェイスとして sxop というパラメタをつけました。 sxop=OR ならOR検索、sxop=ANDあるいは無指定ならAND検索とします。

うーん、こんなものでいいのか?怪しげですが、とりあえず思ったように 動いてくれてるんで、まあいいかと。

日付順表示の導入

いちばん最初に困ったのは、Freyaには検索結果を日付順に表示する機能がないと いうことでした。私の当面の利用目的がメーリングリストの検索用であるため、 新しい順に検索結果を見るという機能は不可欠とも言えます。十年間の記事が 蓄積されているDeleGateメーリングリストですので、昔の話はもう現在では 通用しないことも多いため、検索キーとの適合度でスコア順に並べられて、 大昔の記事が出てくるのは害があるとさえ言えます。年代ごとに別の索引に するという対処法もあるかと思いますが、それはそれで年代によらない検索が 面倒になります。 ですので、ともかく日付順で表示する機能だけは追加したいと思いました。

当初は、できるだけ一般的な方法で実現しようかと考えました。 索引が巨大になった場合のことを考えれば、各ドキュメントの日付の値だけを 持ったファイルを独立に作るのが良さそうです。それなら、データ件数が 100万件でも4MBのサイズですから、ディスクアクセスがソートのネックに なるようなこともありません。また、日付だけでなく、場合によってはユーザ が定義した任意の値でソートもできるようにしたいものです。(私としては その昔、プロキシで集計しているアクセス頻度でソートするというのをやりたいと 思っていました。今思えば、ちょうどFreyaが公開された頃のことでしたが)。 実装方法としては、たとえば、索引名 indexname に対して、indexname-xxx.srt というようなファイル名でキー値のファイルを用意すれば、ユーザがソートの キーとしてxxx を指定できる、というような感じです。

また、元のプログラムに直接手を突っ込む量は最小限にしたいと思っていました ので、元のプログラムには検索結果のドキュメント番号の列を吐き出させる 機能を追加するだけにして、並べ変えや表示を行うプログラムは別途ゼロから 作ってパイプで結んではどうかとか、複数の検索結果を既存の sort, uniq, join などのコマンドで加工したり結合できるのではないか、などとも考えました。 がしかし、実装の手間も実行の時間もかなりかかりそうだと思ったのでパス しまして、元のプログラムに手を突っ込むことにしました。

Freyaのソースプログラムの中で、標準であるスコア順の並べ変えは、 Evaluator.cc で行っていますので、最初はソコ (Evaluator::rank) に手を 突っ込んで、 ドキュメント番号でソートするという極悪な方法でごまかし ました(そこまでに計算したスコアは無視しまして)。メーリングリストを 索引にする際には、各記事の入力の順番(ドキュメント番号)は自然に日付順に なるので、これでも結構ヨイわけです。 ところがこの時点では、英語版と日本語版のMLの索引を一つにしていたので、 例えば最初に英語版の結果だけが出てくるという問題がありました。 また、後に述べる、増分索引機能を追加した時にも、うまくいかなくなり ました。そんなわけで、やはりまがりなりにでも日付順のソートを実装する ことにしました。

結局、CGIによる検索を実装している fsearch.cgi.cc に手を加えることにしました。 FreyaではドキュメントのURLや日付や要約などを、各ドキュメントごとに (256バイトの)固定長レコードに収めて、index.dsc というファイルにして、これを 検索結果の表示の際に用いています。ですので、検索結果(ドキュメント番号 の列)が得られた後、各ドキュメントの日付をindex.dscから取り出し、 その値でソートするという、おそらくもっとも単純な方法をとることにしました。 (もし索引が巨大な場合にディスクアクセスのコストが問題になったら、 取り出した日付情報をキャッシュ化するようにして解消できると思います)

従来のスコアによるソートも必要ですので、ソートの方法を指定するための "sort"というパラメタを追加することにしました。日付順なら sort=date、 スコア順なら sort=score という具合です。 サイズ順とか URL順とかいうのも、必要なら簡単に追加できますが、 とりあえず使わないので、ここまでにしています。

複数索引の併合検索

オリジナル Freya では (というか以前使っていた検索エンジンもそうでしたが) 複数の索引の中から、検索者が任意に選んだ索引の組合せに対して検索を行う という機能が用意されていませんでした。 DeleGateメーリングリストには英語版と日本語版があり、それぞれ閲覧者層が あまり重ならないので、別々に検索できることが望まれます。しかし一方、 日本語版・英語版を問わず検索したいということも少なくはありません。 かといって、日本語版・英語版・統合版の索引をあらかじめ用意しておくと いうのも、ディスクの無駄です。

一般的に考えれば多数の独立した索引の中から、ユーザが任意に選択して組み 合わせ、その中から検索するというのができるに越したことはありません。 そういうわけで、複数の索引を検索して、結果を併合して表示する機能を作る ことにしました。

これも、fsearch.cgi.cc への変更で済ませました。Freyaでは、検索対象の 索引名はURL の query 部で

  index=idx
と指定されています。この部分に
  index=idx1+idx2
のように、複数の索引名を書けるようにすることにしました。 (FORM中のINPUTタグ中での値の表現、つまりユーザがページ中で見る表現は、 value="idx1 idx2"ですが、URLになる時に空白が"+"にエスケープされます。 エスケープされない文字を使えれば良いのですが、区切り記号はいずれも URL中でのエスケープの対象になってしまっています。エスケープなしで 使えるのは "-_.!~*'()" しかありません(RFC2396のunreserved "mark"))

実装は単純に、fsearch.cgi.cc の中で indexパラメタが "idx1 idx2" のような 形式になっていたなら、idx1 と idx2 のそれぞれに対して検索を行い、 両者の結果を併合してソートして結果とする、というのを素直に実現しています。

索引の更新の高速化(増分索引の導入)

検索時に複数の索引を併合して検索できるということは、逆に、検索者から 見れば一つに見える索引を実際には複数の分割された索引で構成することが できることを意味します。そしてその機能を少し拡張すれば、もうひとつ別の 利点が得られます。索引の更新の高速化です。

メーリングリストのアーカイブでは、古い記事の削除や変更はされず、新しい 記事が追加されるだけです。ある時点で索引を作成した時の記事の数に較べて、 その後に追加された記事の数が大幅に小さい場合、その追加分を反映するため に索引全体を作り直さなければならないのは無駄な感じです。 例えば日本語DeleGateメーリングリストには15,000の記事があり、この全体の 索引を作るのはけっこう大変(以前使っていた検索エンジンだと2時間くらい? かかった)です。 一方、一日あたりの記事数は平均して数通程度ですから、一週間ぶんの増加分 にしても、1秒で索引が作成できます。これなら毎分索引を作り直しても問題 ありません。管理者としては楽チンですし、利用者から見れば新着記事が すぐに検索できて便利になります。

実現はやはり単純です。索引の本体を index としますと、追加分を index+1, index+2, index+3, ... という名前にすることにしました。 fsearch.cgi の実行時、index という索引を検索する場合、index+N という索引 も探して、存在すればそれらを自動的に併合して検索します。 索引を作るときに、これらの増分をどのように更新するかは使い方次第ですが、 例えば一日ごとに増分を index+1 から index+7 のように作成して行き、週末 にこれらを本体に統合するというように使えばいいかも、と思います。

このあたりの処理をカプセル化して、見た目にはインクリメンタルに一つずつ ドキュメントを索引に追加できるようにして、実際には、更新処理時間と検索 性能との兼ね合いから、適当に実際の索引の併合を(自動的に、バック グラウンドで)行うようなコマンド、あるいはサーバを作るのが、面白そうです。

HTMLのインクルード機能

FreyaのCGI用に配られているHTMLファイルのテンプレートでは、検索用のFORMの 中に直接索引名が書かれています。 それが複数のHTMLファイルの中に現れるので、設定・維持の手間がちょっとだけ 余計にかかると思います。同じ情報が複数のファイル中に現れるのは、索引名に 限ったことでも無いので、ここでは一般的なインクルード機能を導入することに しました。

Freyaでは、検索エンジンの状態をHTMLのテンプレートに渡す方法として、例えば 検索対象の索引なら "$index"、検索文字列なら "$query" のような表記を、 テンプレートの中で使えるようになっていますので、これを拡張して以下の ようなインクルードができるようにしました。

  $include{file}
例えば索引の選択のために、dblist.html というファイルとして以下のような 内容を作り、
  <OPTION VALUE="$index">$index</OPTION>
  <OPTION VALUE="dgmlEn">dgmlEnglish</OPTION>
  <OPTION VALUE="dgmlJa">dgmlJapanese</OPTION>
  <OPTION VALUE="dgmlEn dgmlJa">dgmlEn+dgmlJa</OPTION>
これを、問い合わせフォームの中で以下のようにインクルードします。
  <SELECT NAME="index">
  $include{dblist.html}
  </SELECT>
ユーザに提供するヘルプ情報の中でもこのインクルード機能を使いたくなるので、 Freya関係の情報は fsearch.cgi を経て解釈されるようにしたいと思います。 そこで検索パラメタとして、"url=fsearch.html" のようにすると、CGIのカレント ディレクトリの pub-fsearch.html がインクルードされるようにしました。 この機能の悪用を避けるように、"pub-" を前置した場合だけアクセス可能 とすることにしています。使い方としては例えば、fsearch.html というファイル にpub-fsearch.html というシンボリックリンクを張って提供可能にするという 運用が適切かと思います。

表示テンプレートの選択機能

Freya では、CGI経由の検索結果やヘルプ情報を表示する際のHTMLのテンプレート として、head.html, foot.html, error.html, fsearch.html というファイルを 用意しています。 DeleGate-MLでは、同じサーバで日本語版と英語版を提供していますので、この それぞれに対するテンプレートは切替えたいと思います。さらに、一般的には、 幾つかのカスタマイズされた版を並行して提供して、検索者が選択できるように できると良いと思います。そこで、例えば

  pfx=en
という指定があった時に、取り込むべきファイルの名前が
  head.html
であったなら、まず
  head-en.html
を探して、それがあればそれを使用する。なければ head.html を使用する、 ということにしました。

表示言語の選択機能

この "pfx" というパラメタは、特に「言語」の切替えに限らない一般的な スイッチです。ところで、オリジナルFreyaの中で、fsearch.cgi.cc の中で エラーメッセージが日本語になっているところが何ヶ所かあって、これは 英語版を提供する時に困ります。そこで、日本語メッセージは "pfx=ja" の 場合にだけ出力するようにしました。 ですので、"pfx=ja" だけは予約された値で、日本語出力を意味することに なります。

表示テンプレートの選択だけでも、表示言語の切替えはできるのですが、 それだと異なる言語の間でテンプレートがあまり共有できなくなります。 また、ユーザが見るテキストだけでなく、検索エンジンが解釈するパラメタ値 やファイル名などの文字列まで日本語化した場合には、それをURLとして 受け取り、解釈する前に、検索エンジンで解釈使用できる文字列に変換する 必要があります。 そこで、生成したHTML出力の文字列に対して、また入力したURLの文字列に 対して、置き換えを行う機能を追加しました。 例えば "ja.cnv" というファイルに以下のような変換規則を記述します。

  #### ja.cnv ######################
  "Search"		"検索"
  >dgmlEnglish<		>英語版ML<
  >dgmlJapanese<	>日本語版ML<
  >dgmlEn+dgmlJa<       >日英ML<
これにより、前述の "dblist.html" は以下のように変換されます。
  <OPTION VALUE="dgmlEn">英語版ML</OPTION>
  <OPTION VALUE="dgmlJa">日本語版ML</OPTION>
  <OPTION VALUE="dgmlEn dgmlJa">日英ML</OPTION>
また、レスポンスHTML中に出力される
  <INPUT type=submit name=go value="Search">
というタグは、
  <INPUT type=submit name=go value="検索">
に変換され、一方このタグに基づいて生成されるリクエストURL中の
  go=検索
とうパラメタは、
  go=Search
に変換されてから解釈されます。

辞書情報をポータブルに

DeleGateを活用して作った"any2fdif"を使うことで、FreyaSXの実行時には Perlは不要になりました。 残る部分でPerlを使用しているのは、インストール時に辞書情報をPatricia木 にする際の前処理のところです。この部分も小さなPerlスクリプトですから、 やはり手書きで作ってもよかったのですが。実は、この辞書のPatricia化の 作業はインストール時に必ずしも必要でなく、もしPatricia化した辞書 ファイルがポータブルであれば、それを配布すれば十分です。 ですので、patricia.cc における書き出しと読み込みの部分で整数の読み書き をネットワークバイトオーダーにするように変更することで、これをポータブル にしました。

その他のファイルもポータブルにすれば、索引作成や検索実行時に異機種間 でファイルを共有できるのですが、とりあえず必要ないのでやっていません。

おわりに

今回のタイトルは「なんちゃって機能拡張」でして、「なんちゃって拡張機能」 ではありません(^^)。拡張した機能はそれぞれ意味のあるものだと思っています。 が、実装がなんちゃってなわけです。

今後やってみれば面白そうだと思うのは、増分索引の管理機能をちゃんと実現 して、仮想的に高速にインクリメンタルに追加ができるようなインターフェイス を作ることです。

また、たぶん役には立たないでしょうけど面白いと思うのは、検索式と同様に、 索引の組合せ方を論理式で表現できるようにしたり(index="idx1 -idx2" とか)、 ソートを多段階にすることです(sort="score/5 year"とか)。

他の検索エンジンではやっているので、あったほうが良さそうに思うのは、 検索結果として検索語の出現文脈を表示することです。IndexとLexiconから 文脈が再現できればそれを使えますが、たぶんだめなのでしょう。 FDIFを保存するとしたら、できるだけ圧縮したいものです。一般にテキストの 圧縮には辞書を使った圧縮が有効なわけですが、検索エンジンはそもそも基礎に 広域的な辞書があるので、かなり効率的な圧縮ができるんではないかとか、 ならばDSCファイルも同様にできるだろうなとか思います。

検索結果をブラウズする際に「ある検索結果の内容を見た後、次の検索結果に ジャンプする」という機能があると便利です。フレームを使えば、対象のページ によらず一般的に適用できて実装も簡単なので、オプションとして加えようかと 思います。一方、検索結果のオリジナルを提供しているウェブサーバと連係して、 そのサーバに対して「次の検索結果を取り出すための手段(URL)」を (RefererかCookieあたりで)渡してやることで連係するようにすれば、より スマートな感じにできるかも知れません。もちろん元データのサーバがこれを 理解して、オリジナルに提供する情報中にアンカーを埋め込むようにしないと いけないわけですが。DeleGate-MLのサーバはDeleGateなので、いずれこれを やってみたいと思っています。もっとも簡単には「Refererにジャンプする」 というボタンをつければOKだと思います。

性能面では、検索の結果をキャッシュすることで高速化ができます。これに ついてはすでにやりかけていて、試作版を運用しています。特に日付順で 表示する場合には、ひとつの検索式の結果を複数のページとして見ることが 多いですので効果的です。が、そもそも対象の索引が1万件程度ですと、 オリジナルのFreyaでも1秒以下で検索できますので、いまいちありがたみを 感じません。また、多くの場合検索自体は数倍に高速化しますが、検索時間 はほぼゼロでも表示用データの生成のほうにそこそこ時間がかかるので、 それを含めた全体を高速化しないといけません。

------------------------------------------------------------------------
以下は(3.移植)からの差分です。

diff -cr ../../freyasx-0.93/src/Expression.cc ./Expression.cc
*** ../../freyasx-0.93/src/Expression.cc	Thu Jun 17 07:29:28 2004
--- ./Expression.cc	Tue Jun 22 06:37:52 2004
***************
*** 12,17 ****
--- 12,18 ----
  #include "Query.h"
  #include "Expression.h"
  #include "kcnvlib.h"
+ int SXopAND = 1;
  
  char *Expression::errmsgs[] = {
      "",
***************
*** 74,79 ****
--- 75,84 ----
      out << "SimpleExpression: ";
      out << " queryNum = " << queryNum << ' ';
      for(int i = 0; i < queryNum; i++){
+         if(SXopAND){
+             if(reqtypes[i] == BASIC) out << '+';
+             else if(reqtypes[i] == EXCLUDE) out << '-';
+         }else
          if(reqtypes[i] == REQUIRE) out << '+';
          else if(reqtypes[i] == EXCLUDE) out << '-';
          queries[i]->print(out);
***************
*** 85,90 ****
--- 90,99 ----
  ostream &
  SimpleExpression::print(ostream &out){
      for(int i = 0; i < queryNum; i++){
+         if(SXopAND){
+             if(reqtypes[i] == BASIC) out << '+';
+             else if(reqtypes[i] == EXCLUDE) out << '-';
+         }else
          if(reqtypes[i] == REQUIRE) out << '+';
          else if(reqtypes[i] == EXCLUDE) out << '-';
          queries[i]->print(out);
***************
*** 119,124 ****
--- 128,143 ----
              do { cp++; } while(isspace(*(unsigned char *)cp));
              continue;
          }
+         if(SXopAND){
+            if(*cp == '+'){
+                cp++;
+            }else
+            if(*cp == '-'){
+                reqtype = EXCLUDE;
+                cp++;
+            }else
+                reqtype = REQUIRE;
+         }else
          if(*cp == '+'){
              reqtype = REQUIRE;
              cp++;
diff -cr ../../freyasx-0.93/src/fsearchcgi.cc ./fsearchcgi.cc
*** ../../freyasx-0.93/src/fsearchcgi.cc	Thu Jun 17 07:51:55 2004
--- ./fsearchcgi.cc	Wed Jun 23 08:45:43 2004
***************
*** 19,24 ****
--- 19,25 ----
  #include <fcntl.h>
  #include <limits.h>
  #include <time.h>
+ #include <sys/stat.h>
  }
  #include "Retriever.h"
  #include "Unifier.h"
***************
*** 65,70 ****
--- 66,88 ----
  
  #define FSEARCHCGI_LOGPATH "fsearch.log"
  
+ int fileMtime(char *path){
+     struct stat st;
+     int time;
+     if(stat(path,&st) == 0)
+         time = st.st_mtime;
+     else
+         time = -1;
+     return time;
+ }
+ static double Start;
+ static char elapsedTimes[16];
+ double Time(){
+     struct timeval tv;
+     gettimeofday(&tv,NULL);
+     return tv.tv_sec + (double)tv.tv_usec/1000000;
+ }
+ 
  #ifdef DISABLE_LOGGING
  #define writelog(q,r) ;
  #else
***************
*** 86,91 ****
--- 104,115 ----
               "%d %b %Y %H:%M:%S %Z", localtime(&now));
  
      if(NULL == query || '\0' == query[0]) query = "-";
+ 
+     char etime[32];
+     sprintf(etime,"%0.3f",Time()-Start);
+     for(char *sp = param_index; *sp; sp++)
+         if(*sp == ' ')
+             *sp = '+';
  /*
      ostrstream line;
  */
***************
*** 93,98 ****
--- 117,123 ----
      line << datebuf << ' ' << host << ' ' << user << ' '
           << param_index << ' ' << param_style << ' '
           << resultNum << ' ' << param_from << ' ' << param_n << ' '
+          << elapsedTimes << '/' << etime << ' '
           << query << '\n' << '\0';
  
      int fd = open(FSEARCHCGI_LOGPATH, O_WRONLY|O_CREAT, 0666);
***************
*** 136,145 ****
--- 161,491 ----
      return replaced;
  }
  
+ //-FX
+ //-FX - Externsions for FreyaSX
+ //-FX
+ #define MAX_IDX 16
+ char param_sxop[16];       // "sxop=AND|OR"
+ char param_sort[16];       // "sort=date|date-reverse|score"
+ char param_url[LINE_MAX];  // "url=fsearch.html"
+ char param_pfx[16];        // "pfx=ja|en"
+ 
+ extern int SXopAND;
+ #define SXsdate    1
+ #define SXsdocid   2
+ static int SXsort;
+ 
+ static int scan_sort(char *sort){
+   int SXsort = 0;
+   if(sort[0]){
+     if(0 == strcmp(sort,"date"))
+        SXsort = SXsdate;
+     else
+     if(0 == strcmp(sort,"date-reverse"))
+        SXsort = -SXsdate;
+     else
+     if(0 == strcmp(sort,"docid"))
+        SXsort = SXsdocid;
+     else
+     if(0 == strcmp(sort,"docid-reverse"))
+        SXsort = -SXsdocid;
+   }
+   return SXsort;
+ }
+ class XResult {
+ public:
+     short indexid;   //
+     int oresx;       // offset in oresult
+     int docid;       // document ID
+     float weight;    // original weight to be printed
+     int iweight;     // extended weight to be sorted by "sort=xxx"
+     XResult() : indexid(0), docid(0), weight(0), iweight(0) { }
+ };
+ 
+ static int cmpweightX(XResult *r1, XResult *r2){
+     if( SXsort == 0 )
+         return (r2->weight > r1->weight) ? 1 : -1;
+     if( 0 < SXsort )
+         return (r2->iweight > r1->iweight) ? 1 : -1;
+     else
+         return (r1->iweight > r2->iweight) ? 1 : -1;
+ }
+ static void SXsortXResults(XResult *result, int len){
+     qsort(result, len, sizeof(XResult),
+         (int(*)(const void*,const void*))cmpweightX);
+ }
+ 
+ typedef struct {
+     char     *name;      // name of the index
+     DescFile *descF;     // .dsc file
+     int       mtime;     // modified time of .dsc file
+     Result   *oresult;   // original Result info.
+     XResult  *result;    // extended Result info. based on .dsc
+     int       resultLen; // the number of results
+ } Index;
+ static Index Indexes[MAX_IDX];
+ 
+ //-FX - scan index list in URL as "index=idx1 idx2"
+ //-FX - add increment indexes named as "idx+N"
+ //-FX - should suport "index=name*"
+ //-FX
+ static int scan_indexes(char *xlist, Index *Xlist){
+     int nix = 0;
+     char *indexp = xlist;
+     for( int ix = 0; indexp && nix < MAX_IDX; ix++ ){
+         char index1[PATH_MAX],index1f[PATH_MAX],*dp = index1;
+         for(; int ch = *indexp; indexp++ ){
+             if( ch == ' ' || ch == ',' ){
+                 indexp++;
+                 break;
+             }
+             *dp++ = ch;
+         }
+         *dp = 0;
+         if( dp == index1 )
+             break;
+         sprintf(index1f,"%s.idx",index1);
+         Xlist[nix].name = strdup(index1);
+         Xlist[nix++].mtime = fileMtime(index1f);
+   
+         for( int ax = 1; nix < MAX_IDX; ax++ ){
+             char indexa[PATH_MAX],indexaf[PATH_MAX];
+             sprintf(indexa,"%s+%d",index1,ax);
+             sprintf(indexaf,"%s.idx",indexa);
+             int fd;
+             if( 0 <= (fd = open(indexaf,0)) ){
+                 close(fd);
+                 Xlist[nix].name = strdup(indexa);
+                 Xlist[nix++].mtime = fileMtime(indexaf);
+             }else{
+                 break;
+             }
+         }
+     }
+     return nix;
+ }
+ 
+ int getLastMod(Index *X1, int docid){
+     DescRecord rec;
+     X1->descF->getRecord(rec,docid);
+     return rec.getLastModified();
+ }
+ 
+ static int getDescList(Index *Xlist, int ix){
+     Index *X1 = &Xlist[ix];
+     char descpath[PATH_MAX];
+     strcpy(descpath, X1->name); strcat(descpath, ".dsc");
+     fstream descfile(descpath, ios::in);
+     if(! descfile.good())
+         return -1;
+   
+     fstream *descf = new fstream(descpath, ios::in);
+     X1->descF = new DescFile(*descf);
+     X1->result = new XResult[X1->resultLen];
+ 
+     XResult *xresult = X1->result;
+     Result *result = X1->oresult;
+     for(int r = 0; r < X1->resultLen; r++){
+         xresult[r].oresx = r;
+         xresult[r].indexid = ix;
+         xresult[r].docid = result[r].docid;
+         xresult[r].weight = result[r].weight;
+     }
+ 
+     if(SXsort){
+         for(int r = 0; r < X1->resultLen; r++){
+             switch(SXsort){
+                 case  SXsdocid:
+                 case -SXsdocid:
+                     xresult[r].iweight = result[r].docid;
+                     break;
+                 case  SXsdate:
+                 case -SXsdate:
+                     xresult[r].iweight = getLastMod(X1,result[r].docid);
+                     break;
+             }
+         }
+     }
+     return 0;
+ }
+ //-FX - merge results
+ //-FX - sort by information in .dsc
+ //-FX - normarlize score
+ //-FX
+ static XResult *mergeResult(Index *Xlist, int nix, int ixn, int &resultLenp){
+     if(ixn <= 0)
+         return NULL;
+     XResult *result = NULL;
+     int resLen = 0;
+     for( int ix = 0; ix < nix; ix++ ){
+         if( Xlist[ix].resultLen <= 0 )
+             continue;
+         if(getDescList(Xlist,ix) != 0){
+             return NULL;
+         }
+         resLen += Xlist[ix].resultLen;
+     }
+     if(ixn == 1){
+         for( int ix = 0; ix < nix; ix++ ){
+             if( 0 < Xlist[ix].resultLen ){
+                 result = Xlist[ix].result;
+                 break;
+             }
+         }
+     }else{
+         result = new XResult[resLen];
+         int ro = 0;
+         for( int ix = 0; ix < nix; ix++ ){
+             if( Xlist[ix].resultLen <= 0 )
+                 continue;
+             for( int r = 0; r < Xlist[ix].resultLen; r++ ){
+                 result[ro++] = Xlist[ix].result[r];
+             }
+         }
+     }
+     SXsortXResults(result, resLen);
+     float maxweight = 0;
+     for(int r = 0; r < resLen; r++){
+         if(maxweight < result[r].weight)
+             maxweight = result[r].weight;
+     } 
+     double mag = 999 / maxweight;
+     for(int r = 0; r < resLen; r++){
+         result[r].weight *= mag;
+     }
+     resultLenp = resLen;
+     return result;
+ }
+ 
+ //-FX - use "head-ja.html" for "head.html" if "pfx=ja"
+ //-FX
+ static const char *postfixedPath(const char *filepath, char *xpath)
+ {
+     if(param_pfx[0]){
+         strcpy(xpath,filepath);
+         if(char *dp = strrchr(xpath,'.')){
+             char ext[PATH_MAX];
+             strcpy(ext,dp+1);
+             sprintf(dp,"-%s.%s",param_pfx,ext);
+             fstream hf(xpath, ios::in);
+             if(hf.good()){
+                 hf.close();
+                 return xpath;
+             }
+         }
+     }
+     return filepath;
+ }
+ 
+ //-FX - conversion table file "ja.cnv" is used if "pfx=ja"
+ //-FX - each line of the file consists of InRep TAB* ExRep
+ //-FX - HTML output is converted from InRep to ExRep
+ //-FX - URL input is converted from ExRep to InRep
+ //-FX - the most naive implementation of text converter.
+ //-FX
+ #define CONV_MAX 64
+ typedef struct {
+     char *in; // Internal representation
+     char *ex; // External representation
+ } Conv;
+ static Conv conv[CONV_MAX];
+ static int nconv = 0;
+ static int setConv(char *cnv)
+ {   char path[PATH_MAX];
+     char cnv1[LINE_MAX],*dp;
+ 
+     sprintf(path,"%s.cnv",cnv);
+     fstream convf(path, ios::in);
+     if(!convf.good())
+         return -1;
+ 
+     while( nconv < CONV_MAX ){
+         convf.getline(cnv1, LINE_MAX, '\n');
+         if(convf.eof())
+             break;
+         if(cnv1[0] == '#')
+             continue;
+         if((dp = strchr(cnv1,'\t')) != 0){
+             *dp++ = 0;
+             while(*dp == '\t')
+                *dp++;
+             if(*dp != 0){
+                 conv[nconv].in = strdup(cnv1);
+                 conv[nconv].ex = strdup(dp);
+                 nconv++;
+            }
+         }
+     }
+     convf.close();
+     return 0;
+ }
+ static void de_conv(char *key, char *ex){
+     for(int i = 0; i < nconv; i++)
+         replace_string(ex, conv[i].ex, conv[i].in);
+ }
+ static void en_conv(char *in){
+     for(int i = 0; i < nconv; i++)
+         replace_string(in, conv[i].in, conv[i].ex);
+ }
+ 
+ void FX_query(char *k, char *v){
+     de_conv(k,v);
+     if(0 == strcmp(k, "sort")){
+         strncpy(param_sort, v, sizeof(param_sort) - 1);
+     }else if(0 == strcmp(k, "sxop")){
+         strncpy(param_sxop, v, sizeof(param_sxop) - 1);
+     }else if(0 == strcmp(k, "url")){
+         strncpy(param_url, v, sizeof(param_url) - 1);
+     }else if(0 == strcmp(k, "pfx")){
+         strncpy(param_pfx, v, sizeof(param_pfx) - 1);
+     }
+ }
+ 
+ static char resultFroms[16];
+ static char resultTos[16];
+ //-FX
+ //-FX - $include{filename}
+ //-FX - sohould support $include(n1=v1,n2=v2,...){filename}
+ //-FX
+ int FX_display(char *name, char *linebuf){
+     if(char *inc = strstr(linebuf,"$include{")){
+         char *top = inc+9;
+         if(char *end = strstr(top,"}") ){
+             int len = end - top;
+             for(char *sp = linebuf; sp < inc && *sp != 0; sp++)
+                 cout << *sp;
+             strncpy(name,top,len);
+             name[len] = 0;
+             fstream sub(name, ios::in);
+             if(!sub.good()){
+                 cout << "<!-- ## Failed opening a file ## -->";
+             }else{
+                 sub.close();
+             }
+             char *dp = linebuf;
+             for(char *sp = end+1; (*dp++ = *sp++) != 0;);
+             return 1;
+         }
+     }
+     replace_string(linebuf, "$from", resultFroms);
+     replace_string(linebuf, "$to", resultTos);
+     replace_string(linebuf, "$etime", elapsedTimes);
+     replace_string(linebuf, "$pfx", param_pfx);
+     replace_string(linebuf, "$sort", param_sort);
+     replace_string(linebuf, "$sxop", param_sxop);
+     en_conv(linebuf);
+     return 0;
+ }
+ ////////////////////////////////////////////////////////////////
+ 
  int
  display_html(ostream &out, const char *filepath,
               const char *title, const char *query, const char *errormsg,
               int resultNum){
+ 
+     char xpath[PATH_MAX];
+     filepath = postfixedPath(filepath,xpath);
+ 
      fstream htmlfile(filepath, ios::in);
      if(! htmlfile.good())
          return -1;
***************
*** 187,192 ****
--- 533,546 ----
          replace_string(linebuf, "$dispnum", dispnum);
  */
          replace_string(linebuf, "$dispnum", dispnum.c_str());
+ 
+         char name[LINE_MAX];
+         if(FX_display(name,linebuf)){
+             if(display_html(out,name,title,query,errormsg,resultNum)!=0)
+                 cout << "<!-- ## Failed loading a file ## -->";
+             if( linebuf[0] == 0 )
+                 continue;
+         }
          out << linebuf << endl;
      }
      htmlfile.close();
***************
*** 227,232 ****
--- 581,587 ----
          cgi_error("Error", "No query information to decode.");
          return 1;
      }
+     qs = strdup(qs); // for repetitive get_query() after setConv()
      char *k, *v;
      while((k = strtok(qs, "&"))){
          qs = NULL;
***************
*** 234,239 ****
--- 589,596 ----
          if(v != NULL){
              *v++ = '\0';
              unescape_url(v);
+             FX_query(k,v);
+ 
              // cout << "k = \"" << k << "\", param = \"" << param << "\"\n";
              if(0 == strcmp(k, "key")){
                  if(NULL == conv2euc(key, v, CODE_UNKNOWN)){
***************
*** 408,414 ****
--- 765,774 ----
  }
  
  int
+ /*
  print_results(fstream &descfile, const Result *result, const char *key,
+ */
+ print_results(Index *Xlist, const XResult *result, const char *key,
                int resultNum, int disp, int from, int to){
      assert(to >= from);
      assert(to - from < resultNum);
***************
*** 442,454 ****
--- 802,823 ----
          printfunc = printgeneric;
  #endif
  
+ /*
      DescFile descf(descfile);
+ */
      int r;
      cout << "<DL>\n";
      for(r = from; r <= to; r++){
          DescRecord rec;
+         Xlist[result[r].indexid].descF->getRecord(rec, result[r].docid);
+         /*
          descf.getRecord(rec, result[r].docid);
+         */
+ /*
          cout << "<!-- id: " << result[r].docid << "-->" << endl;
+ */
+         cout << "<!-- id: " << result[r].indexid << ":"
+                             << result[r].docid << "-->" << endl;
  #ifndef ODIN
          (*printfunc)(r + 1, rec, (int)result[r].weight);
  #else  // ODIN
***************
*** 467,472 ****
--- 836,844 ----
          cout << "<A HREF=\"" << cgipath << "?key=" << escaped_key
               << "&from=" << ((from - disp < 0) ? 0 : from - disp)
               << "&n=" << disp << "&index=" << param_index
+              << "&sort=" << param_sort
+              << "&sxop=" << param_sxop
+              << "&pfx=" << param_pfx
               << "&style=" << param_style
               << "\">[PREV]</A>" << endl;
      }
***************
*** 483,488 ****
--- 855,863 ----
              cout << "<A HREF=\"" << cgipath << "?key=" << escaped_key
                   << "&from=" << i << "&n=" << disp
                   << "&index=" << param_index
+                  << "&sort=" << param_sort
+                  << "&sxop=" << param_sxop
+                  << "&pfx=" << param_pfx
         	         << "&style=" << param_style << "\">"
                   << '[' << p << "]</A>" << endl;
          }
***************
*** 491,496 ****
--- 866,874 ----
          cout << "<A HREF=\"" << cgipath << "?key=" << escaped_key
               << "&from=" << to + 1 << "&n=" << disp
               << "&index=" << param_index 
+              << "&sort=" << param_sort
+              << "&sxop=" << param_sxop
+              << "&pfx=" << param_pfx
               << "&style=" << param_style<< "\">[NEXT]</A>" << endl;
      }
      cout << "</SMALL>\n";
***************
*** 501,525 ****
--- 879,930 ----
  
  int
  main(){
+     Start = Time();
      param_from = 0;
      param_n = RESULTSFORDISPLAY;
      strcpy(param_index, "default");
      strcpy(param_style, "html");
+     strcpy(param_sort, "score");
+     strcpy(param_sxop, "AND");
  
      param_key[0] = '\0';
      if(0 != get_query(param_key)){ // error
          return 0;
      }
+ 
+     if(param_pfx[0]){
+         setConv(param_pfx);   // setup text conversion.
+         get_query(param_key); // re-scan query with the conversion.
+     }
+     if(param_url[0]){
+         char path[4+sizeof(param_url)];
+         strcpy(path,"pub-");
+         sscanf(param_url,"%[-_a-zA-Z0-9.]",path+strlen(path));
+         cout << "Content-type: text/html\n\n";
+         if(display_html(cout, path, "", "", "(no error)", 0) == 0)
+           return 0;
+     }
+     SXsort = scan_sort(param_sort);
+     if(0 == strcmp(param_sxop,"OR"))
+         SXopAND = 0;
+ 
      param_from++;
      if(param_key[0] == '\0'){
+         if(0 == strcmp(param_pfx,"ja"))
          cgi_error("エラー: 検索式が見つかりません",
                    "検索式を入力してください", "");
+         else
+         cgi_error("Error: empty query","input a query", "");
          return 0;
      }
      if(param_n < 0 || param_n > MAXRESULTSFORDISPLAY){
          param_n = RESULTSFORDISPLAY;
+         if(0 == strcmp(param_pfx,"ja"))
          cgi_error("エラー: 出力範囲が不正です",
                    "n=...を適切な値にしてください", "");
+         else
+         cgi_error("Error: Invalid range of output",
+                   "n=...must be a proper value", "");
          return 0;
      }
  
***************
*** 531,540 ****
--- 936,959 ----
      unif.unify(uquery, param_key);
      ComplexExpression cexp(uquery);
      if(cexp.error()){
+         if(0 == strcmp(param_pfx,"ja"))
          cgi_error("エラー: 検索式が不正です",
                    cexp.errmsgs[cexp.error()], uquery);
+         else
+         cgi_error("Error: Invalid query expression",
+                   cexp.errmsgs[cexp.error()], uquery);
          return 2;
      }
+ 
+   /////////////////////////////////////
+   int resultLen = -1;
+   XResult *result = NULL;
+   int nix = scan_indexes(param_index, Indexes);
+   int ixn = 0; // indexes with non-empty result
+ 
+   for( int ix = 0; ix < nix; ix++ ){
+     char *param_index = Indexes[ix].name;
+ 
      int resultLen = -1;
      Result *result = NULL;
      // result = checkCache(uquery, resultLen);
***************
*** 570,589 ****
--- 989,1024 ----
          Evaluator evaluator;           // use standard ranking policy
  
          result = cexp.eval(retr, evaluator, resultLen);
+ /*
+ normalize "weight" after all indexes are searched and merged.
          if(resultLen > 0) evaluator.rank(result, resultLen);
+ */
          // saveCache(result, resultLen);
      }
+     Indexes[ix].oresult = result;
+     Indexes[ix].resultLen = resultLen;
+     if(0 < resultLen)
+         ixn++;
+   }
+   if((result = mergeResult(Indexes,nix,ixn,resultLen)) == NULL){
+       cgi_error("Error", "cannot open description file.", uquery);
+       return 3;
+   }
  
      //
      // display results
      //
      if(resultLen <= 0){
          resultLen = 0;
+         if(0 == strcmp(param_pfx,"ja"))
          cgi_error("見つかりませんでした", "nothing found. ", uquery, 0);
+         else
+         cgi_error("Not found", "nothing found. ", uquery, 0);
          writelog(uquery, 0);
      }else{
          cout << "Content-type: text/html\n\n";
  
+ /*
          char descpath[PATH_MAX];
          strcpy(descpath, param_index); strcat(descpath, ".dsc");
          fstream descfile(descpath, ios::in);
***************
*** 591,596 ****
--- 1026,1032 ----
              cgi_error("Error", "cannot open description file.", uquery);
              return 3;
          }
+ */
  
          if(param_from <= 0 || param_from > resultLen){
              cgi_error("Error", "parameter 'from' out of range.", uquery);
***************
*** 621,629 ****
--- 1057,1071 ----
  /*
          display_html(cout, "head.html", titlestr, querystr,
  */
+         sprintf(elapsedTimes,"%0.3f",Time()-Start);
+         sprintf(resultFroms,"%d",param_from);
+         sprintf(resultTos,"%d",rto);
          display_html(cout, "head.html", titlestr.c_str(), querystr.c_str(),
                       "(no error)", resultLen);
+ /*
          print_results(descfile, result, uquery,
+ */
+         print_results(Indexes, result, uquery,
                        resultLen, param_n, param_from -1, rto - 1);
  /*
          display_html(cout, "foot.html", titlestr, querystr,
diff -cr ../../freyasx-0.93/src/patricia.cc ./patricia.cc
*** ../../freyasx-0.93/src/patricia.cc	Thu Jun 17 07:41:20 2004
--- ./patricia.cc	Thu Jun 17 18:55:05 2004
***************
*** 6,11 ****
--- 6,12 ----
  extern "C" {
  #include <assert.h>
  #include <string.h>
+ #include <arpa/inet.h> // for ntohl()/htonl()
  }
  #include "fstream.h" // for streampos
  
***************
*** 206,211 ****
--- 207,213 ----
      patfile.read(&magic, sizeof(magic));
  */
      patfile.read((char*)&magic, sizeof(magic));
+     magic = ntohl(magic);
      if(magic != MAGIC){
          cerr << "Patricia::load(): invalid PAT file." << endl;
          return -1;
***************
*** 216,221 ****
--- 218,225 ----
  */
      patfile.read((char*)&keyNum, sizeof(keyNum));
      patfile.read((char*)&root_idx, sizeof(root_idx));
+     keyNum = ntohl(keyNum);
+     root_idx = ntohl(root_idx);
  
      // prepare body
      Pat_node *node_table[keyNum];
***************
*** 245,250 ****
--- 249,256 ----
  */
          patfile.read((char*)& node->content, sizeof(node->content));
          patfile.read((char*)& node->checkbit, sizeof(node->checkbit));
+         node->content = ntohl(node->content);
+         node->checkbit = ntohl(node->checkbit);
  
          // read branches
          int left_idx, right_idx;
***************
*** 254,259 ****
--- 260,267 ----
  */
          patfile.read((char*)&left_idx, sizeof(int));
          patfile.read((char*)&right_idx, sizeof(int));
+         left_idx = ntohl(left_idx);
+         right_idx = ntohl(right_idx);
  
          node->left = node_table[left_idx];
          node->right = node_table[right_idx];
***************
*** 282,321 ****
--- 290,354 ----
      int total = dump_all(node_table, keyNum);
  
      // write header
+ /*
      unsigned int magic = MAGIC;
      int root_idx = root_node->dump_idx;
+ */
  /*
      patfile.write(&magic, sizeof(magic));
      patfile.write(&keyNum, sizeof(keyNum));
      patfile.write(&root_idx, sizeof(root_idx));
  */
+ /*
      patfile.write((char*)&magic, sizeof(magic));
      patfile.write((char*)&keyNum, sizeof(keyNum));
      patfile.write((char*)&root_idx, sizeof(root_idx));
+ */
+     unsigned int Nmagic = htonl(MAGIC);
+     int NkeyNum = htonl(keyNum);
+     int Nroot_idx = htonl(root_node->dump_idx);
+     patfile.write((char*)&Nmagic, sizeof(Nmagic));
+     patfile.write((char*)&NkeyNum, sizeof(NkeyNum));
+     patfile.write((char*)&Nroot_idx, sizeof(Nroot_idx));
  
      // write body
      for(int i = 0; i < total; i++){
          Pat_node *node = node_table[i];
          int len = strlen(node->key);
  
+         int Ncontent = htonl(node->content);
+         int Ncheckbit = htonl(node->checkbit);
+         int Nleft_idx = htonl(node->left->dump_idx);
+         int Nright_idx = htonl(node->right->dump_idx);
+ 
          patfile.put((unsigned char)len);
          patfile.write(node->key, len); // may len be 0
  /*
          patfile.write(& node->content, sizeof(node->content));
          patfile.write(& node->checkbit, sizeof(node->checkbit));
  */
+ /*
          patfile.write((char*)& node->content, sizeof(node->content));
          patfile.write((char*)& node->checkbit, sizeof(node->checkbit));
+ */
+         patfile.write((char*)&Ncontent, sizeof(Ncontent));
+         patfile.write((char*)&Ncheckbit, sizeof(Ncheckbit));
          assert(node->left->dump_idx != -1);
  /*
          patfile.write(& node->left->dump_idx, sizeof(int));
  */
+ /*
          patfile.write((char*)& node->left->dump_idx, sizeof(int));
+ */
+         patfile.write((char*)&Nleft_idx, sizeof(int));
          assert(node->right->dump_idx != -1);
  /*
          patfile.write(& node->right->dump_idx, sizeof(int));
  */
+ /*
          patfile.write((char*)& node->right->dump_idx, sizeof(int));
+ */
+         patfile.write((char*)&Nright_idx, sizeof(int));
      }
      patfile << flush;
  
diff -cr ../../freyasx-0.93/src/patricia.h ./patricia.h
*** ../../freyasx-0.93/src/patricia.h	Fri May  8 00:56:23 1998
--- ./patricia.h	Thu Jun 17 09:17:18 2004
***************
*** 40,46 ****
--- 40,49 ----
  public:
      enum { MAXKEYLENGTH = 64 };
      enum { EDUPLICATE = -1, INSERTOK = 0 };
+ /*
      enum { MAGIC = 0x19970924 };
+ */
+     enum { MAGIC = 0x20040617 };
      Patricia();
      Patricia(istream &file);
      ~Patricia();
------------------------------------------------------------------------