XMLとJSONで検索結果を出力する

Solrができるけど、Fessとしても検索結果として表示しているものと同じ内容のXMLとJSONを出力できるようになりました。まぁ、それらの形式で出力できるのは大きなことではないのだけど、この機能追加に伴い、検索結果のデータの持ち方を変更した。今まではDocumentというような独自のBeanクラスでやっていたけど、これをやめてMapに変えた。これによって、Solrでダイナミックフィールドとかにデータを投入しておけばそれも扱えるわけだ。そんなわけで、diconでクロール設定のSolrに投げるフィールドを設定すれば、FessのSolrスキーマでない、Solrにもドキュメント投入などが可能かと思う(ちょっとはスキーマ調整が必要かも)。つまり、Solrを利用するアプリ開発でドキュメント投入部分だけをFessに任せる案も可能では、と思い始める。そんなわけで、今まで Fess がターゲットユーザーと考えていなかった、既存の Solr ユーザーにも利用してもらえる可能性がある変更が入ったかな。

データベースクロール機能

Fessをいろいろな方に紹介すると、データベース内のデータも対象にしたいのだよね~、と毎回言われる。まぁ、SolrにはDataImportHandlerというのがあるから、これを使ってSolrに直接入れれば良いとも思うのだが、使い勝手をウリにする Fess としては導入の敷居が一気に上がってしまうことになる。solrconfig.xmlを編集して、data-config.xmlを作って、コマンド叩いてね…というのもね。というわけで、データストア機能という感じでFessに実装した。
ウェブクロール設定とかと同じように、データストアクロール設定が追加してみた。パラメータを設定すれば、指定したSQL文からデータを取得して、Solrに投入する。たとえば、

CREATE TABLE job (
id BIGINT NOT NULL AUTO_INCREMENT
, title VARCHAR(100) NOT NULL
, content VARCHAR(255) NOT NULL
, versionNo INTEGER NOT NULL
, PRIMARY KEY (id)
);

みたいなテーブルがあったとすると、データストアクロール設定のパラメータに「キー=値」形式で

driver=com.mysql.jdbc.Driver
url=jdbc:mysql://localhost:3306/jobdb?useUnicode=true&characterEncoding=UTF-8
username=hogeuser
password=fugapass
sql=select * from job

というように入力して、ハンドラには同様に「キー=値」形式で

url="http://localhost/" + id
host="localhost"
site="localhost"
title=title
content=content
cache=content
digest=content
anchor=
contentLength=content.length()
lastModified=content.length()

というように Fess の Solr スキーマに必要なマッピングルールを書く。キーについて簡単にまとめると

url URL(検索結果に表示されるリンク)
host ホスト名
site サイトパス
title タイトル
content コンテンツ(インデックス対象文字列)
cache コンテンツのキャッシュ(インデックス対象ではない)
digest 検索結果に表示されるダイジェスト部分
anchor コンテンツに含まれるリンク(普通は指定する必要ないと思う)
contentLength コンテンツの長さ
lastModified コンテンツの最終更新日

という感じ。値の方は OGNL で記述する。文字列は “~” で書けば良い。データベースのカラム名で記述すればその値になる。OGNLなので、やろうと思えばいろいろとできるかと(^^;。あとは、他のクロール設定と同様の使い方です。実際にデータベースにアクセスするときにはドライバが必要になるので、webapps/fess/WEB-INF/cmd/libにjarファイルを入れておく必要がある。
現時点では、データベースだけだけど、XMLやExcelなどから取る機能も追加したいところ。
Fess 3.0 も Solr 1.4 にするだけかと思っていたら、あれこれと大玉が追加してしまったな…。あとは、設定ウィザードページを追加したら、3.0 をリリースしようかと思っている感じ。

clearReferences系のエラーログの対応方法

調査を進めた。clearReferencesThreadsとclearThreadLocalMapのエラーを黙らせるには以下のようなコードをサーブレットのdestroyに書けば良い(サーブレットの定義順には注意)。

@Override
public void destroy() {
stopAllThreads();
}
private void stopAllThreads() {
Thread[] threads = getThreads();
ClassLoader cl = this.getClass().getClassLoader();
List<String> jvmThreadGroupList = new ArrayList<String>();
jvmThreadGroupList.add("system");
jvmThreadGroupList.add("RMI Runtime");
// Iterate over the set of threads
for (Thread thread : threads) {
if (thread != null) {
ClassLoader ccl = thread.getContextClassLoader();
if (ccl != null && ccl == cl) {
// Don't warn about this thread
if (thread == Thread.currentThread()) {
continue;
}
// Don't warn about JVM controlled threads
ThreadGroup tg = thread.getThreadGroup();
if (tg != null && jvmThreadGroupList.contains(tg.getName())) {
continue;
}
waitThread(thread);
// Skip threads that have already died
if (!thread.isAlive()) {
continue;
}
if (logger.isInfoEnabled()) {
logger.info("Interrupting a thread ["
+ thread.getName() + "]...");
}
thread.interrupt();
waitThread(thread);
// Skip threads that have already died
if (!thread.isAlive()) {
continue;
}
if (logger.isInfoEnabled()) {
logger.info("Stopping a thread [" + thread.getName()
+ "]...");
}
thread.stop();
}
}
}
Field threadLocalsField = null;
Field inheritableThreadLocalsField = null;
Field tableField = null;
try {
threadLocalsField = Thread.class.getDeclaredField("threadLocals");
threadLocalsField.setAccessible(true);
inheritableThreadLocalsField = Thread.class
.getDeclaredField("inheritableThreadLocals");
inheritableThreadLocalsField.setAccessible(true);
// Make the underlying array of ThreadLoad.ThreadLocalMap.Entry objects
// accessible
Class<?> tlmClass = Class
.forName("java.lang.ThreadLocal$ThreadLocalMap");
tableField = tlmClass.getDeclaredField("table");
tableField.setAccessible(true);
} catch (Exception e) {
// ignore
}
for (Thread thread : threads) {
if (thread != null) {
Object threadLocalMap;
try {
// Clear the first map
threadLocalMap = threadLocalsField.get(thread);
clearThreadLocalMap(cl, threadLocalMap, tableField);
} catch (Exception e) {
// ignore
}
try { // Clear the second map
threadLocalMap = inheritableThreadLocalsField.get(thread);
clearThreadLocalMap(cl, threadLocalMap, tableField);
} catch (Exception e) {
// ignore
}
}
}
}
private void waitThread(Thread thread) {
int count = 0;
while (thread.isAlive() && count < 5) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
}
count++;
}
}
/*
* Get the set of current threads as an array.
*/
private Thread[] getThreads() {
// Get the current thread group
ThreadGroup tg = Thread.currentThread().getThreadGroup();
// Find the root thread group
while (tg.getParent() != null) {
tg = tg.getParent();
}
int threadCountGuess = tg.activeCount() + 50;
Thread[] threads = new Thread[threadCountGuess];
int threadCountActual = tg.enumerate(threads);
// Make sure we don't miss any threads
while (threadCountActual == threadCountGuess) {
threadCountGuess *= 2;
threads = new Thread[threadCountGuess];
// Note tg.enumerate(Thread[]) silently ignores any threads that
// can't fit into the array
threadCountActual = tg.enumerate(threads);
}
return threads;
}
private void clearThreadLocalMap(ClassLoader cl, Object map,
Field internalTableField) throws NoSuchMethodException,
IllegalAccessException, NoSuchFieldException,
InvocationTargetException {
if (map != null) {
Method mapRemove = map.getClass().getDeclaredMethod("remove",
ThreadLocal.class);
mapRemove.setAccessible(true);
Object[] table = (Object[]) internalTableField.get(map);
if (table != null) {
for (int j = 0; j < table.length; j++) {
if (table[j] != null) {
boolean remove = false;
// Check the key
Field keyField = Reference.class
.getDeclaredField("referent");
keyField.setAccessible(true);
Object key = keyField.get(table[j]);
if (cl.equals(key)
|| (key != null && cl == key.getClass()
.getClassLoader())) {
remove = true;
}
// Check the value
Field valueField = table[j].getClass()
.getDeclaredField("value");
valueField.setAccessible(true);
Object value = valueField.get(table[j]);
if (cl.equals(value)
|| (value != null && cl == value.getClass()
.getClassLoader())) {
remove = true;
}
if (remove) {
Object entry = ((Reference<?>) table[j]).get();
if (logger.isInfoEnabled()) {
logger.info("Removing " + key.toString()
+ " from a thread local...");
}
mapRemove.invoke(map, entry);
}
}
}
}
}
}

上記のコードで、スレッドが終了するのを待ち、Tomcatが処理する前にスレッド系のものたちをクリーンするのでエラーが出なくなる(TimerThreadを使っているものがあれば、上記のコードは未対応なので処理が必要)。Tomcatがやりたい、メモリリークをしないようにというのは分かるのだけど、これを 6.0.24 からエラーレベルでログは吐くのは間違っているんじゃないかね…。WARNくらいなレベルな気がするのだけど。しかも、シャットダウンする時なので、そもそも全部捨てるから吐かれてもな、という気がする(再配備するようなケースではこの処理があると有効かも)。
今回はアプリ側で直したけど、S2とかのライブラリ側で対応するとなると、ThreadLocalをextendedしないとか、ThreadLocalは使い終わりにremove()を呼んでおくとか、そういう対応になるような気が。たとえば、S2のHttpServletExternalContextとか、スレッドローカルがあるけど、removeな状態にしておくのは問題がある気がするし。しかも、スレッドローカルだから消すのがめんどい…。そんな感じで、そもそもの解決策は、どうせ値を消しているのだから、Tomcatがエラーレベルでログを吐かなければ良いだけじゃないかね。
そのうち、みんなが 6.0.24 を使い始めて文句が多発で変更が入りそうな気がする動きだな…。