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