Fess 15.5.0のリリース

内容的な話はリリースノートを参照してもらえば良いかと思いますが、今回もいろいろな修正が入っています。

今回は実験的な機能として、AI検索モードが追加されています。AI検索モードは、AIエージェントが検索をしてくれるみたいな感じの機能です。なので、入力された質問文に対して、AIエージェントが検索クエリーを作成して、検索して、検索結果を評価した上で、検索結果を返してくれるみたいな感じです。なので、従来通りのクロールしてインデクシングできるので、ベクトル検索などを使う難しさなどはなく、利用することができます。実験的な機能なので、継続的に改善していければと思います。

他には、インデックスされたドキュメントをダンプする機能が欲しかったので、JSONやHTMLとしてファイルに出力する機能を追加しました。スケジューラーのジョブに登録してあるので、実行するとfess_config.propertiesで指定したパスに出力することができます。スクロールAPIとかでも全件取得はできますが、Fessのジョブとして、実行するだけでファイルに書き出せる感じです。

あとは、Fessをデモ的に公開していると、激しくアクセスしてくるクローラーとかがいて、Lightsailとかで運用していると負荷でクレジットを使い切ってしまい、サーバーがフリーズしてしまうことがあったので、検索画面へのアクセスにおいて、OpenSearchの負荷が高い場合は、混み合ってます的に429を返すみたいな処理を入れました。

という感じで、これら以外にもhttpclient5に移行したり、いろいろと修正は入っているので、試してみてください。今回もほぼほぼClaude Codeで開発して、自らコードをほとんど書いてない感じです。IDEも問題調査が必要なときにデバッグして、確認するくらいしか、Eclipseを開くことがなくなってきました。今も、この文を書いている裏で、実装してくれていますし。Fessの開発の仕方も変わってきてます。

次回のリリースもいろいろと変更が入ったりするかもしれませんが、何か要望があれば、フォーラムとかに投げてもらえれば、Claude Codeにやってもらって実現できるかもしれません。

libdxfrwのDWGパース時の整数アンダーフロー修正

libdxfrwは、DXF/DWGファイルの読み書きを行うC++ライブラリです。FessでDWGファイルのテキスト抽出に利用しているのですが、特定のDWGファイルをパースする際に、メモリを80GB以上消費して実質的に無限ループに陥るという問題が発生しました。

原因

問題の原因は、DWGファイルのクラス情報をパースする処理における符号なし整数のアンダーフローでした。

dwgreader18.cppにある元のコードは以下のようになっていました。

duint32 endDataPos = maxClassNum - 499;

DWGフォーマットでは、クラス番号0〜499は予約済みの組み込みクラスで、カスタムクラスは500番から始まります。そのため、maxClassNumが499以下の場合、カスタムクラスは存在しません。

しかし、duint32(符号なし32ビット整数)でmaxClassNum - 499を計算すると、maxClassNumが499以下の場合にアンダーフローが発生します。例えば、DWG TrueViewで作成されたAC1032形式のファイルではmaxClassNum = 236となることがあり、この場合:

  • 元のコード: 236 - 499 = 4294967033(符号なし整数のラップアラウンド)

この巨大な値がループ回数として使われるため、メモリの大量消費と実質的な無限ループが発生していました。

修正内容

修正はシンプルで、減算前に境界チェックを追加しました。

// Classes 0-499 are built-in; custom classes start at 500.
// If maxClassNum <= 499, there are no custom classes to parse.
duint32 endDataPos = (maxClassNum > 499) ? (maxClassNum - 499) : 0;

maxClassNumが499以下の場合はendDataPos = 0となり、カスタムクラスのパースをスキップします。この修正はdwgreader18.cppdwgreader21.cppの両方に適用しました。

修正後の計算結果:

  • maxClassNum = 236: endDataPos = 0(カスタムクラスなし)
  • maxClassNum = 499: endDataPos = 0(カスタムクラスなし)
  • maxClassNum = 500: endDataPos = 1(カスタムクラス1つ)
  • maxClassNum = 1000: endDataPos = 501(カスタムクラス501個)

テスト

今回の修正に合わせて、ユニットテストとインテグレーションテストも追加しました。境界値のテストや、メモリ安全性の確認を行っています。

符号なし整数のアンダーフローは、C/C++ではよくあるバグですが、今回のようにメモリ消費が爆発するケースは影響が大きいので、早めに対処できてよかったです。

関連リンク

Fess Crawlerのエクストラクターにweight設定を追加

Fess Crawlerのエクストラクターにweightを指定できるようにしました。これにより、同じMIMEタイプに対して複数のエクストラクターが登録されている場合に、優先度を制御できるようになります。

背景

Fess Crawlerでは、ドキュメントからテキストを抽出するためにエクストラクター(Extractor)を利用しています。エクストラクターはMIMEタイプに基づいて選択されますが、同じMIMEタイプに対して複数のエクストラクターが存在する場合、どちらを優先するかを制御する仕組みがありませんでした。

変更内容

ExtractorインターフェースにgetWeight()メソッドをデフォルトメソッドとして定義し、AbstractExtractor基底クラスにweightフィールドを追加しました。

Extractorインターフェースでは、デフォルトのweightとして1を返すようになっています。

public interface Extractor {
    ExtractData getText(InputStream in, Map<String, String> params);

    default int getWeight() {
        return 1;
    }
}

AbstractExtractorでは、weightフィールドとsetter/getterを実装しています。

public abstract class AbstractExtractor implements Extractor {
    protected int weight = 1;

    @Override
    public int getWeight() {
        return weight;
    }

    public void setWeight(final int weight) {
        this.weight = weight;
    }
}

設定方法

Fessの設定ファイル(XML)で、エクストラクターのweightを指定できます。weightの値が大きいエクストラクターが優先的に使用されます。

<component name="tikaExtractor" class="org.codelibs.fess.crawler.extractor.impl.TikaExtractor">
    <property name="weight">10</property>
</component>

デフォルトのweightは1なので、特に設定しなければ従来と同じ動作になります。

まとめ

この変更により、エクストラクターの優先度をweight値で柔軟に制御できるようになりました。カスタムエクストラクターを追加する際に、既存のエクストラクターとの優先順位を設定ファイルで簡単に調整できます。