Apache Solrを使ったドキュメント検索 – FastVector Highlighter

ApacheSolr

前回、「Standard Highlighter」を使ったドキュメント検索、および、ハイライトを紹介しました。



今回は、ハイライト表示をおこなうことができる別の方法である「FastVector Highlighter」の使用方法を紹介します。


環境情報


環境情報は以下になります。

使用するのは、JavaとApache Solrの2つのみです。

  • Java 12.0.1
  • ApacheSolr 6.4.2

設定変更


「Standard Highlighter」でのハイライト表示をおこなった後に、「FastVector Highlighter」を使えるように設定変更をおこないます。
「Standard Highlighter」のスキーマ定義については、前回の記事を参考にしてください。



「Standard Highlighter」を使っている状態から、「FastVector Highlighter」を使える状態に変更します。


まずはスキーマ定義です。
今回は、文書の中身に対して検索をおこない、検索結果をハイライト表示することを想定します。
文書の中身を格納するフィールドである「content」について、以下のように設定変更をおこないます。
変更するファイルは「managed-schema」です。


<field name="content" type="text_general_content" multiValued="false" indexed="true" stored="true"/>

以下のように変更します。 ↓↓↓

<field name="content" type="text_general_content" multiValued="false" indexed="true" stored="true" termVectors="true" termPositions="true" termOffsets="true"/>

3つの設定値について、「true」を設定しています。


フィールド名

設定値

説明

termVectors

true=設定する false=設定しない

検索キーワードが含まれる数やキーワードの開始、終了位置などを結果に含めるかどうか(termPositionsとtermOffsetsをtrueに設定した場合、termVectorsもtrue扱いとなる)

termPositions

true=設定する false=設定しない

検索キーワードの含まれる位置を返すかどうか

termOffsets

true=設定する false=設定しない

検索キーワードの含まれる位置を返すかどうか


変更したら、Apache Solrのサービスを再起動して、変更した内容がコア定義に反映されているかを確認します。
Apache Solrのダッシュボードをひらき、コアの選択で対象コアを選択します。


コアを選択したら、左ペーンで「schema」を選択し、「content」フィールドを確認します。


「TermVector Stored」「Store Offset With TermVector」「Store Position With TermVector」が表示されチェックされていれば、スキーマの設定変更OKです。


ApacheSolrの設定確認

筆者は、この定義をおこなうことで「FastVector Highlighter」でのハイライト表示について思ったような結果が返却されるようになりました。


今回は、「Standard Highlighter」を使ったハイライト表示とまったく同じ結果にすることを目標にします。



その場合、ハイライト表示する際に検索ワードを囲むHTMLタグを固定させてしまおうと思います。
その場合、以下の定義変更が必要になります。
変更するファイルは「solrconfig.xml」です。


<fragmentsBuilder name="colored" class="solr.highlight.ScoreOrderFragmentsBuilder">
  <lst name="defaults">
    <str name="hl.tag.pre"><![CDATA[
      <b style="background:yellow">,<b style="background:lawgreen">,
      <b style="background:aquamarine">,<b style="background:magenta">,
      <b style="background:palegreen">,<b style="background:coral">,
      <b style="background:wheat">,<b style="background:khaki">,
      <b style="background:lime">,<b style="background:deepskyblue">]]></str>
    <str name="hl.tag.post"><![CDATA[</b>]]></str>
  </lst>
</fragmentsBuilder>

↓↓↓

<fragmentsBuilder name="colored" class="solr.highlight.ScoreOrderFragmentsBuilder">
  <lst name="defaults">
    <str name="hl.tag.pre"><![CDATA[<b>]]></str>
    <str name="hl.tag.post"><![CDATA[</b>]]></str>
  </lst>
</fragmentsBuilder>

「hl.tag.pre」について、<b>タグのみを使用するように変更します。


こうすることにより、検索ワードをbタグで囲みます。
こんな感じです。


  • <b>検索ワード</b>

FastVector Highlighterのサンプルコード


「FastVector Highlighter」を使った検索のサンプルコードは以下になります。


import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.File;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;

import org.apache.solr.client.solrj.request.ContentStreamUpdateRequest;
import org.apache.solr.client.solrj.SolrQuery;
import org.apache.solr.client.solrj.SolrClient;
import org.apache.solr.client.solrj.SolrServerException;
import org.apache.solr.client.solrj.impl.HttpSolrClient;
import org.apache.solr.client.solrj.util.ClientUtils;
import org.apache.solr.client.solrj.response.QueryResponse;
import org.apache.solr.client.solrj.response.Group;
import org.apache.solr.client.solrj.response.GroupCommand;
import org.apache.solr.client.solrj.response.GroupResponse;
import org.apache.solr.common.util.ContentStreamBase;
import org.apache.solr.common.SolrDocumentList;
import org.apache.solr.common.SolrDocument;

public class SolrSearchFast {

    public static void main(String[] args){
        System.out.print("start: main\r\n");

        // Solrのインスタンス作成
        SolrClient client = new HttpSolrClient.Builder(
            "http://localhost:8983/solr/java_sample").build();

        SolrQuery solrQuery = new SolrQuery();

        // 検索結果として、文書IDを返却するよう設定
        solrQuery.setFields("id");

        // 検索結果の上限は100件
        solrQuery.setRows(100);

        try {
            StringBuilder queryString = new StringBuilder();
            String keyword = args[0];
            if (keyword.equals("")) {
                queryString.append("*");
            } else {
                String queryPhrase = "\"" + ClientUtils.escapeQueryChars(keyword) + "\"";
                queryString.append("(");
                queryString.append("content:");
                queryString.append(queryPhrase);
                queryString.append(")");
            }

            // 検索実行
            System.out.println("q=" + queryString.toString());

            solrQuery.set("hl.useFastVectorHighlighter", "true");
            solrQuery.set("hl.fragmentsBuilder", "colored");
            solrQuery.set("hl", true);
            solrQuery.set("hl.fl", "content");
            solrQuery.set("hl.fragsize", 100);

            QueryResponse response = client.query(solrQuery);

            // 検索結果を表示
            SolrDocumentList list = response.getResults();
            if (list == null) {
                System.out.println("文書は存在しませんでした。");
            } else {
                System.out.println(list.getNumFound() + "件ヒットしました。");

                // ハイライト情報を取得して加工
                Map<String,Map<String,List<String>>> highlighting = response.getHighlighting();
                for (SolrDocument doc : list) {
                    System.out.println(doc.get("id"));
                    String id = (String) doc.getFieldValue("id");
                    Map<String, List<String>> map = highlighting.get(id);
                    List<String> contentList = map.get("content");
                    for(String val : contentList) {
                        val = val.replaceAll("[\r\n\t]", "");
                        System.out.println("val=" + val);
                    }
                }
            }
        } catch (SolrServerException e) {
            System.out.print("SolrServerException Occured!\r\n");
            e.printStackTrace();
        } catch (IOException e) {
            System.out.print("IOException Occured!\r\n");
            e.printStackTrace();
        } finally {
            try {
                // コミットして、コネクションをクローズ
                client.commit();
                client.close();
            } catch (Exception e) {
                System.out.print("Exception Occured!\r\n");
                e.printStackTrace();
            }
        }
        System.out.print("end: main\r\n");
    }
}

「Standard Highlighter」を使ったハイライト表示をおこなうサンプルコードと、ほぼほぼ同様です。
違う箇所は、クエリパラメータだけ。



検索結果のハイライト文字列についても、「Standard Highlighter」を使用したサンプルコード検索結果と、ほぼ同様となります。


「Standard Highlighter」と「FastVector Highlighter」、どっちを使うべきか?


どちらを使っても同じハイライト表示をおこなうことができるので、機能としては同等です。

では、どちらを使った方がいいのか?ということですが、筆者は「FastVector Highlighter」を推します。
理由はパフォーマンスです。


筆者が検証した限りでは、「Standard Highlighter」と「FastVector Highlighter」では、約5倍の性能差がありそうです。

以下は、500ファイルが登録されたコアに対して、30ファイルがヒットする状態に対してのパフォーマンス比較です。


ApacheSolrの検索比較

縦軸はmsecです。
なので、「Standard Highlighter」が1秒ちょっとかかる検索に対して、「FastVector Highlighter」は0.2秒くらい。
パフォーマンスが圧倒的に違います。
こうなってくると、「FastVector Highlighter」を使った方がよいという話になります。


では、「FastVector Highlighter」を使うデメリットはまったくないかというとそうではないです。


「設定変更」で説明したスキーマ定義をおこなうと、ファイルを登録した際にコアに登録されている内容が変わってきます。
コアのファイルサイズが、変更前のスキーマ定義と比べると1.5倍になりました。


あまり深く調べられてまとめいないのですが、「termVectors」「termPositions」「termOffsets」をオンにすると、「FastVector Highlighter」を使うためのインデックス情報をApacheSolrが大量に作るようです。
そのために、ファイルサイズが膨大になるが、高速な検索が可能になるということなります。


まとめ


いかがでしたでしょうか?


「FastVector Highlighter」について説明してきましたが、前記事の「Standard Highlighter」と比べると違いがわかるかと。


まとめ
  • 「Standard Highlighter」ではなく「FastVectorHighlighter」を使った方が高速
  • でも、「Standard Highlighter」より「FastVectorHighlighter」の方がディスク使用率が高い

結論としては筆者は「FastVector Highlighter」推しです。
やはり、検索パフォーマンスが高いのは魅力的なので。


それではまた!



ファイルが削除されない原因は何?java.io.Fileとjava.nio.file.Filesの違いを検証

Java

この前、実際の業務でちょっと困ったことがおきました。
ちょっと前に作ったシステムなのですが、“ある特定のファイルが削除されない”という現象です。


今回は、実際におきた現象をもとに、「java.io.File」と「java.nio.file.Files」の違いを実際のプログラムを使って紹介していきます。


実際に起きた現象自体は解消されたのですが、一番困ったのは調査です。
原因は、削除するファイルが他プロセスにつかまれていることが原因だったのですが、その原因がすぐにはわからなかったです。


「java.io.File」を使ってファイル削除していたのですが、他プロセスが削除対象ファイルをつかんでいる場合は、返り値で失敗(false)を返却しているのみで、例外が発生しないです。
なので、削除されない原因がすぐにはわかりませんでした。


そのため、「java.io.File」ではなく「java.nio.file.Files」に切り替えて例外をログ出力するようにすることで、ようやく原因がわかりました。


サンプルプログラム

二つのプログラムで試してみます。
「java.io.File」で削除するプログラムは以下。

import java.io.IOException;
import java.io.File;

public class FileDelete {

    public static void main(String[] args){
        System.out.print("start: main\r\n");

        boolean isDone = false;
        try {
            File file = new File("C:\\Temp\\file.txt");
            isDone = file.delete();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            System.out.print("isDone=" + isDone + "\r\n");
        }
        System.out.print("end: main\r\n");
    }
}

「java.nio.file.Files」で削除するプログラムは以下。

import java.io.IOException;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Paths;

public class FileDeleteIO {

    public static void main(String[] args){
        System.out.print("start: main\r\n");
        try {
            Files.delete((Paths.get("C:\\Temp\\file.txt")));
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            System.out.print("finally\r\n");
        }
        System.out.print("end: main\r\n");
    }
}

「java.io.File」は削除結果を返却してくれるので削除結果を標準出力していますが、「java.nio.file.Files」の方は削除結果を返却しないので何も標準出力していません。


この2つのプログラムを使用して、「java.io.File」と「java.nio.file.Files」の違いを検証します。
ファイルの削除について挙動の違いを検証します
以下の3パターンです。


  • 削除対象ファイルが存在しない場合
  • 削除対象ファイルに対して削除権限がない場合
  • 削除対象ファイルが他プロセスに開かれている場合

存在しないファイルを削除

削除対象のファイルが存在しない場合の挙動を試してみます。
「java.io.File」の結果は以下になります。

start: main
isDone=false
end: main

「java.io.File」の結果がfalse(失敗)を返しました。


「java.nio.file.Files」の結果は以下になります。

start: main
java.nio.file.NoSuchFileException: C:\Temp\file.txt
        at java.base/sun.nio.fs.WindowsException.translateToIOException(WindowsException.java:85)
        at java.base/sun.nio.fs.WindowsException.rethrowAsIOException(WindowsException.java:103)
        at java.base/sun.nio.fs.WindowsException.rethrowAsIOException(WindowsException.java:108)
        at java.base/sun.nio.fs.WindowsFileSystemProvider.implDelete(WindowsFileSystemProvider.java:274)
        at java.base/sun.nio.fs.AbstractFileSystemProvider.delete(AbstractFileSystemProvider.java:105)
        at java.base/java.nio.file.Files.delete(Files.java:1144)
        at FileDeleteIO.main(FileDeleteIO.java:11)
finally
end: main

例外が発生しました。
「NoSuchFileException」というファイルが存在しません、という例外です。
この例外を嫌う場合は「delete」ではなく「deleteIfExists」を使えばいいです。


削除権限がないファイルを削除

削除対象のファイルについて削除権限が存在しない場合の挙動を試してみます。
「java.io.File」の結果は以下になります。

start: main
isDone=false
end: main

「java.io.File」の結果がfalse(失敗)を返しました。


「java.nio.file.Files」の結果は以下になります。

start: main
java.nio.file.AccessDeniedException: C:\Temp\file.txt
        at java.base/sun.nio.fs.WindowsException.translateToIOException(WindowsException.java:89)
        at java.base/sun.nio.fs.WindowsException.rethrowAsIOException(WindowsException.java:103)
        at java.base/sun.nio.fs.WindowsException.rethrowAsIOException(WindowsException.java:108)
        at java.base/sun.nio.fs.WindowsFileSystemProvider.implDelete(WindowsFileSystemProvider.java:274)
        at java.base/sun.nio.fs.AbstractFileSystemProvider.delete(AbstractFileSystemProvider.java:105)
        at java.base/java.nio.file.Files.delete(Files.java:1144)
        at FileDeleteIO.main(FileDeleteIO.java:11)
finally
end: main

例外が発生しました。
「AccessDeniedException」という、ファイルへのアクセス権限が存在しません、という例外です。


他のプロセスが開いているファイルを削除

削除対象のファイルについて、他のプロセスがファイルを開いている場合の挙動について試してみます。


あえて他のプロセスがファイルをつかんでいる状態を発生させるために、簡単なプログラムを作りました。

import java.io.IOException;
import java.io.FileOutputStream;
import java.io.File;

import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;

public class FileLocker {

    public static void main(String[] args){
        System.out.print("start: main\r\n");

        // 削除対象ファイルを読み込み
        File file = new File("C:\\Temp\\file.txt");
        FileChannel fc = null;
        FileLock lock = null;

        try {
            // ファイルを読み込み、ファイルをロック
            FileOutputStream fos = new FileOutputStream(file);
            fc = fos.getChannel();
            lock = fc.tryLock();

            // ロックに失敗した場合、例外をスロー
            if (lock == null) {
                throw new Exception("lock failer");
            }

            // 1秒スリープ
            System.out.print("sleeping...\r\n");
            Thread.sleep(60000); 
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (lock != null) {
                try {
                    lock.release();
                } catch (IOException ex) {
                    ex.printStackTrace();
                }
            }
        }
        System.out.print("end: main\r\n");
    }
}

このプログラムでファイルをつかんでいる状態を作り出して、ファイル削除をためしてみます。


「java.io.File」の結果は以下になります。

start: main
isDone=false
end: main

「java.io.File」の結果がfalse(失敗)を返しました。


「java.nio.file.Files」の結果は以下になります。

start: main
java.nio.file.FileSystemException: C:\Temp\file.txt: プロセスはファイルにアクセスできません。別のプロセスが使用中です。
        at java.base/sun.nio.fs.WindowsException.translateToIOException(WindowsException.java:92)
        at java.base/sun.nio.fs.WindowsException.rethrowAsIOException(WindowsException.java:103)
        at java.base/sun.nio.fs.WindowsException.rethrowAsIOException(WindowsException.java:108)
        at java.base/sun.nio.fs.WindowsFileSystemProvider.implDelete(WindowsFileSystemProvider.java:274)
        at java.base/sun.nio.fs.AbstractFileSystemProvider.delete(AbstractFileSystemProvider.java:105)
        at java.base/java.nio.file.Files.delete(Files.java:1144)
        at FileDeleteIO.main(FileDeleteIO.java:11)
finally
end: main

例外が発生しました。
「FileSystemException」という例外で、ファイルシステムとしてはこのファイルが他プロセスにロックされてるという例外です。


筆者が直面した問題がまさにこれでして、「java.io.File」だと「false」とだけ出力されるので、何か原因で削除されなかったのかがわからない。。。
「java.nio.file.Files」だと例外が出力されるので、原因についてある程度のあたりがつきます。


まとめ

「java.io.File」と「java.nio.file.Files」、どっちを使う方がいいのでしょうか?
実装するアプリケーションの特性にもよるので一概にはどちらがいい、とは言えないのですが、筆者は今後は極力「java.nio.file.Files」を使うようにしようと思っています。


理由はやはり、なにかしらエラー(削除失敗、など)が発生した場合の原因調査。
例外がトレースとして出力されるのとされないのでは、障害発生時の調査に掛かる時間が圧倒的に違います。


今後は「java.nio.file.Files」を使うようにしよう。。。


それではまた!




Apache Solrを使ったドキュメント検索 – Standard Highlighter

ApacheSolr

前回の記事で、Apache Solrを使って、登録・削除・検索を紹介しました。



今回は、検索結果をハイライト表示する方法を紹介します。

Apache Solrでのハイライト表示とは、検索結果として一致したキーワード前後の文字列を通知して、かつ、検索したキーワード部分を強調表示する機能になります。

本記事では、Apache Solrで最も一般的なハイライト表示である「Standard Hilighter」を使ってみます。

環境情報


環境情報は以下になります。

使用するのは、JavaとApache Solrの2つのみです。

  • Java 12.0.1
  • Apache Solr 6.4.2

スキーマ定義

ハイライト表示するためには、ハイライト表示するための準備が必要です。

Apache Solrのコンフィグである「managed-schema」に、フィールドタイプ定義とフィールド定義をおこないます。


フィールドタイプ定義

<fieldType name="text_general_content" class="solr.TextField" 
  positionIncrementGap="100" multiValued="true" 
  autoGeneratePhraseQueries="true">
  <analyzer type="index">
    <tokenizer class="solr.NGramTokenizerFactory" 
      minGramSize="1" maxGramSize="1"/>
    <filter class="solr.StopFilterFactory" 
      words="stopwords.txt" ignoreCase="true"/>
    <filter class="solr.LowerCaseFilterFactory"/>
  </analyzer>
  <analyzer type="query">
    <tokenizer class="solr.NGramTokenizerFactory" 
      minGramSize="1" maxGramSize="1"/>
    <filter class="solr.StopFilterFactory" 
      words="stopwords.txt" ignoreCase="true"/>
    <filter class="solr.SynonymFilterFactory" 
      expand="true" ignoreCase="true" synonyms="synonyms.txt"/>
    <filter class="solr.LowerCaseFilterFactory"/>
  </analyzer>
</fieldType>

フィールド定義

<field name="content" type="text_general_content" multiValued="false" indexed="true" stored="true"/>

「managed-schema」の定義を追加したら、ApacheSolrのサービスを再起動してください。


これで準備完了です。

以降に登録したドキュメントについて、ハイライト表示の文字列取得が可能になります。


Standard Highlighterのサンプルコード


「Standard Highlighter」を使った検索のサンプルコードは以下になります。

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.File;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;

import org.apache.solr.client.solrj.request.ContentStreamUpdateRequest;
import org.apache.solr.client.solrj.SolrQuery;
import org.apache.solr.client.solrj.SolrClient;
import org.apache.solr.client.solrj.SolrServerException;
import org.apache.solr.client.solrj.impl.HttpSolrClient;
import org.apache.solr.client.solrj.util.ClientUtils;
import org.apache.solr.client.solrj.response.QueryResponse;
import org.apache.solr.client.solrj.response.Group;
import org.apache.solr.client.solrj.response.GroupCommand;
import org.apache.solr.client.solrj.response.GroupResponse;
import org.apache.solr.common.util.ContentStreamBase;
import org.apache.solr.common.SolrDocumentList;
import org.apache.solr.common.SolrDocument;

public class SolrSearch {

    public static void main(String[] args){
        System.out.print("start: main\r\n");

        // Solrのインスタンス作成
        SolrClient client = new HttpSolrClient.Builder(
            "http://localhost:8983/solr/java_sample").build();

        SolrQuery solrQuery = new SolrQuery();

        // 検索結果として、文書IDを返却するよう設定
        solrQuery.setFields("id");

        // 検索結果の上限は100件
        solrQuery.setRows(100);

        try {
            StringBuilder queryString = new StringBuilder();
            String keyword = args[0];
            if (keyword.equals("")) {
                queryString.append("*");
            } else {
                String queryPhrase = "\"" + ClientUtils.escapeQueryChars(keyword) + "\"";
                queryString.append("(");
                queryString.append("content:");
                queryString.append(queryPhrase);
                queryString.append(")");
            }

            // 検索実行
            System.out.println("q=" + queryString.toString());

            solrQuery.set("hl", true);
            solrQuery.set("hl.fl", "content");
            solrQuery.set("hl.simple.pre", "<b>");
            solrQuery.set("hl.simple.post", "</b>");
            solrQuery.set("hl.fragsize", 20);
            solrQuery.set("hl.maxAnalyzedChars", 100);
            solrQuery.set("q", queryString.toString());

            QueryResponse response = client.query(solrQuery);

            // 検索結果を表示
            SolrDocumentList list = response.getResults();
            if (list == null) {
                System.out.println("文書は存在しませんでした。");
            } else {
                System.out.println(list.getNumFound() + "件ヒットしました。");

                // ハイライト情報を取得して加工
                Map<String,Map<String,List<String>>> highlighting = response.getHighlighting();
                for (SolrDocument doc : list) {
                    String id = (String) doc.getFieldValue("id");
                    Map<String, List<String>> map = highlighting.get(id);
                    List<String> contentList = map.get("content");
                    for(String val : contentList) {
                        val = val.replaceAll("[\r\n\t]", "");
                        System.out.println("val=" + val);
                    }
                }
            }
        } catch (SolrServerException e) {
            System.out.print("SolrServerException Occured!\r\n");
            e.printStackTrace();
        } catch (IOException e) {
            System.out.print("IOException Occured!\r\n");
            e.printStackTrace();
        } finally {
            try {
                // コミットして、コネクションをクローズ
                client.commit();
                client.close();
            } catch (Exception e) {
                System.out.print("Exception Occured!\r\n");
                e.printStackTrace();
            }
        }
        System.out.print("end: main\r\n");
    }
}

「Standard Highlighter」を使用するためのパラメータ設定は57~63行目です。

検索結果をハイライト文字列として取得しているのが76~83行目。

ハイライト文字をWEB画面上に出力することを想定して、改行とタブを削除しています。


以下のようなテキストファイルをApacheSolrに登録して、検索を試してみます。

主食と野菜をしっかり食べさせよう

このテキストファイルが登録されている状態で『野菜』というキーワードで検索してみます。

すると、以下のような文字列がハイライト文字としてApacheSolrから返却されてきます。

主食と<b>野</b><b>菜</b>をしっかり食べさせよう


これをWEBページに出力すると以下のような表示になります。

主食とをしっかり食べさせよう


検索してヒットした「野菜」という文字が強調表示されています。

これが、ハイライト表示の基本的な動きになります。


Standard Highlighter のパラメータ

サンプルプログラムで、Standard Highlighter を使用した検索をおこない、ハイライト文字を取得できます。

このハイライト表示はパラメータでチューニング可能です。

具体的には以下の部分。

    solrQuery.set("hl", true);
    solrQuery.set("hl.fl", "content");
    solrQuery.set("hl.simple.pre", "<b>");
    solrQuery.set("hl.simple.post", "</b>");
    solrQuery.set("hl.fragsize", 20);
    solrQuery.set("hl.maxAnalyzedChars", 100);

「Standard Highlighter」を使用するには、上記のパラメータを設定するだけでOKです。

各パラメータの意味合いは以下になります。


パラメータ

デフォルト

説明

hl

blank

このパラメータを「true」にする、ハイライトがオンになります。

デフォルトはブランク(オフ)です。

hl.fl

blank

ハイライトの対象となるフィールド。

複数定義する場合は、カンマ区切りで指定します。

hl.simple.pre

<em>

ハイライト表示する文字の前に挿入する文字。

hl.simple.post

</em>

ハイライト表示する文字の後に挿入する文字。

hl.fragsize

100

スニペットひとつあたりの最大文字数。

hl.maxAnalyzedChars

51200

スニペットの処理対象最大文字数。


スニペットとは、”検索キーワードを含む文書”です。

今回の例では「hl.fragsize」は20としているので、”検索キーワードを含む文書”は最大20文字です。


例えば、下のような150文字の文字列があるとします。

ああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああ主食と野菜をしっかり食べさせよういいいいいいいいいいいいいいいいいいいいいいいいいいいいいいいいいいいいいいいいいいいいいいいいいいいい

この文字に対して「野菜」を検索キーワードとして指定します。

その際の返却文字列は以下となります。20文字です。

ああああ主食とをしっかり食べさせよう

「hl.fragsize」は20なので、返却される文字列は最大20文字、ということですね。


まとめ

いかがでしたでしょうか?

「Standard Highlighter」の使い方について、ある程度わかって頂けたかと思います。


ハイライト表示は、「Standard Highlighter」以外に以下の2つがあります。

  • FastVector Highlighter
  • Postings Highlighter

次回は、「FastVector Highlighter」を使ったハイライト表示を紹介したいと思います。


それではまた!



Apache Solrを使ったドキュメント検索 – 環境構築とJavaからの実行まで

ApacheSolr

文書を管理するシステムの場合、文書の中身を検索する機能が必要だったりします。


例えば、”システムに登録されているWordファイルについて、「○△□」といった文字が存在するファイルだけ注出する”といった機能です。


でも、そういった機能をゼロから作るのは大変ですよね。

プログラムを作るとしたら、”ファイルを開いて”→ “ファイルの中身の文字を注出して”→”キーワードと一致する文字を検索して”といった作り込みが必要です。


今回は、Apache SolrをJavaから呼び出し、文書を登録、編集、削除する方法を説明していきます。


『Apache Solr』を導入すると、文書の管理(登録、編集、削除)や文書の検索をAPI形式で実行することができ、簡単に文書管理をおこなうシステムを構築することができます。

  • 文書の管理(登録、編集、削除)
  • 文書の検索
  • 文書検索結果の返却

今回は、Apache Solrの開発環境構築から、JavaからAPIを呼び出して結果を取得するまでを紹介します。


Apache Solrのインストール(サービス化)


まずは、Apache Solrをインストールします。

今回は、Apache Solrをサービス化する形でインストールします。

サービス化は、「nssm」を使用します。

nssmは、Microsoft Windows用のサービスマネージャです。

サービス化すると、バックグラウンドおよびフォアグラウンドのサービスとプロセスを管理する無料のユーティリティです。 プログラムは、失敗したサービスを自動的に再始動するように設定できます。

以下サイトからダウンロード可能です。


という訳で、環境情報としては以下になります。


  • 文書検索エンジン:Apache Solr 6.4.2
  • サービス化ツール:nssm 2.24

公式サイトからダウンロードしたApache Solrのモジュールとnssmを、任意のフォルダに配置します。

筆者は、Apache Solrを「C:\ApacheSolr」直下に、nssmを「C:\SolrInstall」に配置しました。


以下のような感じです。

ApacheSolrの配置
Solrの配置

Apache Solrの”インストール”というのは不要で、マシンに配置して完了です。

nssmを使用して、配置したApache Solrをサービス化します。


それでは、nssmでのサービス化をおこないます。

DOSプロンプトで、nssmを配置したディレクトリに移動し、nssm installコマンドを実行します。



コマンドを実行するとサービスインストーラのウィンドウがひらくので、必要な情報を入力します。


  • Path:Apache Solrの起動コマンドパス
  • Startup directory:ApacheSolrの起動ディレクトリ
  • Arguments:起動パラメータ
  • Service name:サービス名

以下のように入力します。


ApacheSolrのサービス化

「Arguments」には、起動パラメータと同時に使用するポートを記載します。

筆者は、8983ポートを使用するようにしました。


全ての入力が完了したら、「Install service」ボタンを押下します。

以下のダイアログが表示されたらインストール完了です。

ApacheSolrのサービス化完了

それでは、きちんとサービス化されたかどうかを確認してみましょう。 

サービス一覧で「Apace Solr」が存在すれば、サービス化完了です。

「開始」を選択して、Apache Solrを起動してください。

ApacheSolrの起動

ステータスが「開始」になったことを確認しましょう。


きちんと起動できたかを確認するためには、ブラウザでApache SolrのWeb画面を表示します。

ブラウザで入力するURLは「http://localhost:8984」です。

以下のようにダッシュボードが表示されれば、起動成功です。

ApacheSolrのダッシュボード

コアを作成


Apache Solrの準備ができたら、Apache Solrで『コア』を作成して使用するための準備をおこないます。


『コア』とは何か?という説明は、いまは省略します。

とりあえず動かしたいので。

イメージとしては、データベースのスキーマでしょうかね?


コアの作成方法はいろいろあるみたいなのですが、筆者はDOSプロンプトでコマンドを実行する形でコアを作成します。

コアの名前は「java_sample」としました。


ApacheSolrのコア作成

コアの作成が完了したら、Apache Solrのサービスを再起動します。

再起動後にダッシュボードを確認します。

「java_sample」というコアが作成されています。


コアを選択すると、ダッシュボードにコアの情報が表示されます。

ApacheSolrでのコア作成

これで、Apache Solrの準備完了です。


文書を登録


文書の登録を、JavaからAPIを実行する形でおこなうことが可能です。

サンプルプログラムは以下となります。


import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.File;

import org.apache.solr.client.solrj.request.ContentStreamUpdateRequest;
import org.apache.solr.client.solrj.SolrClient;
import org.apache.solr.client.solrj.SolrServerException;
import org.apache.solr.client.solrj.impl.HttpSolrClient;
import org.apache.solr.common.util.ContentStreamBase;

public class SolrInsert {

    public static void main(String[] args){
        System.out.print("start: main\r\n");

        // idとファイル名をパラメータから取得
        String id = args[0];
        String fileName = args[1];

        // Solrのインスタンス作成
        SolrClient client = new HttpSolrClient.Builder(
            "http://localhost:8983/solr/java_sample").build();

        try {
            // APIの実行準備
            File file = new File(fileName);
            ContentStreamUpdateRequest update = 
                new ContentStreamUpdateRequest("/update/extract");
            update.addContentStream(
                new ContentStreamBase.FileStream(file));

            // idとファイル名をパラメータに指定
            // 既に登録済のIDを指定した場合、登録ではなく更新となる
            update.setParam("literal.id", id);
            update.setParam("literal.filename", file.getName());

            // コマンドを実行
            client.request(update);
        } catch (SolrServerException e) {
            System.out.print("SolrServerException Occured!\r\n");
            e.printStackTrace();
        } catch (IOException e) {
            System.out.print("IOException Occured!\r\n");
            e.printStackTrace();
        } finally {
            try {
                // コミットして、コネクションをクローズ
                client.commit();
                client.close();
            } catch (Exception e) {
                System.out.print("Exception Occured!\r\n");
                e.printStackTrace();
            }
        }
        System.out.print("end: main\r\n");
    }
}

重要な部分は、強調表示している26行目から37行目の部分です。

実際にSolrに対して、「/update/extract」というAPIを呼び出して登録コマンドを実行しています。


メインとなる使用クラスは「ContentStreamUpdateRequest 」。「addContentStream」で電子ファイルの実態を指定し、setParamでパラメータを指定しています。

指定しているパラメータは2つで、IDとファイル名。


コメントにも記載していますが、同じIDを指定した場合はドキュメントの更新になります


プログラムの動作結果を、ApacheSolrのダッシュボードで確認してみます。

まだ、1つドキュメントが登録されていない状態であれば、以下のようになります。


ApacheSolrのコンソール。ドキュメント数が0。

この状態でプログラムを実行してみます。


IDとファイル名はパラメータで指定可能となっていますが、IDは101、ファイル名はtemplate.xlsxを指定して、プログラムを実行します。

プログラムの実行後にコンソール画面を確認すると、ドキュメントが登録されていることが確認できます。


ApacheSolrのドキュメント数。0から1に。

登録した結果を、もうちょっと詳しくみてみます。

Apache Solrのダッシュボードでは、ドキュメントの検索が可能です。

この検索を使って、登録したドキュメントを確認してみます。


ApacheSolrダッシュボード。

登録のときに指定したIDとファイル名のドキュメントが登録されていることがわかります。


次に「更新」をおこなってみます。

プログラムは「登録」の時のプログラムと同様です。

同じIDを指定すれば、Apache Solrが更新をおこなってくれます。

試しに、IDは登録の時に指定したIDと同じ「101」を指定し、ファイル名を「template_2.xlsx」に変更して、登録の時と同じプログラムを実行してみます。


実行した後に、Apache Solrのダッシュボードを確認すると、以下のようになります。


ApacheSolrダッシュボード。更新後。

検索結果のドキュメント数は変わっておらず(numFoundが1)、id=101のドキュメントファイル名が、「template.xlsx」から「template_2.xlsx」に変わっています。

つまり、「登録」ではなく「更新」をおこなったことがわかります。


文書を全部削除


文書の削除もAPIから可能です。

まずは、全削除のAPIを作成して実行してみます。


削除するまえに、ApacheSolrにドキュメントを3つ登録してみました。

ApacheSolrのダッシュボードで、ドキュメントが3つ登録されていることを確認しておきます。


ApacheSolrダッシュボード。

ドキュメントが3つ登録されていることが確認できました。

この状態で、ドキュメント全削除のプログラムを実行します。


サンプルプログラムは以下です。

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.File;

import org.apache.solr.client.solrj.request.ContentStreamUpdateRequest;
import org.apache.solr.client.solrj.SolrClient;
import org.apache.solr.client.solrj.SolrServerException;
import org.apache.solr.client.solrj.impl.HttpSolrClient;
import org.apache.solr.common.util.ContentStreamBase;

public class SolrAllDelete {

    public static void main(String[] args){
        System.out.print("start: main\r\n");

        // Solrのインスタンス作成
        SolrClient client = new HttpSolrClient.Builder(
            "http://localhost:8983/solr/java_sample").build();

        try {
            // APIを実行
            String deleteQuery = "*:*";
            client.deleteByQuery(deleteQuery);
        } catch (SolrServerException e) {
            System.out.print("SolrServerException Occured!\r\n");
            e.printStackTrace();
        } catch (IOException e) {
            System.out.print("IOException Occured!\r\n");
            e.printStackTrace();
        } finally {
            try {
                // コミットして、コネクションをクローズ
                client.commit();
                client.close();
            } catch (Exception e) {
                System.out.print("Exception Occured!\r\n");
                e.printStackTrace();
            }
        }
        System.out.print("end: main\r\n");
    }
}

実際の削除処理をおこなっている部分は、23行目と24行目になります。。

「SolrClient」クラスの「deleteByQuery」メソッドを使用して削除をおこなっています。

クエリパラメータに「*:*」を指定すると全削除、になります。


ApacheSolrのダッシュボードを確認するとドキュメント数が0になっており、全削除されていることがわかります。


ApacheSolrのダッシュボード。全削除後。

文書を一部削除


文書を全削除する方法を紹介しましたが、実際のシステムではあまりニーズはないかと思います。

あるとすれば、全削除ではなく指定したドキュメントのみを削除、ですかね。


指定したドキュメントのみを削除することも可能です。

サンプルプログラムは以下になります。


import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.File;

import org.apache.solr.client.solrj.request.ContentStreamUpdateRequest;
import org.apache.solr.client.solrj.SolrClient;
import org.apache.solr.client.solrj.SolrServerException;
import org.apache.solr.client.solrj.impl.HttpSolrClient;
import org.apache.solr.common.util.ContentStreamBase;

public class SolrOneDelete {

    public static void main(String[] args){
        System.out.print("start: main\r\n");

        // Solrのインスタンス作成
        SolrClient client = new HttpSolrClient.Builder(
            "http://localhost:8983/solr/java_sample").build();

        try {
            // APIを実行
            String deleteQuery = "id:(102)";
            client.deleteByQuery(deleteQuery);
        } catch (SolrServerException e) {
            System.out.print("SolrServerException Occured!\r\n");
            e.printStackTrace();
        } catch (IOException e) {
            System.out.print("IOException Occured!\r\n");
            e.printStackTrace();
        } finally {
            try {
                // コミットして、コネクションをクローズ
                client.commit();
                client.close();
            } catch (Exception e) {
                System.out.print("Exception Occured!\r\n");
                e.printStackTrace();
            }
        }
        System.out.print("end: main\r\n");
    }
}

idが102のドキュメントを削除しています。

解り易いように、IDをハードコーディングしていますが、23行目と24行目です。


複数のドキュメントを削除する場合は、ID指定をORで繋いで指定すればよいです。

    String deleteQuery = "id:(201 OR 202)";
    client.deleteByQuery(deleteQuery);

文書を検索


次に文書の検索です。

ApacheSolrの肝となる部分ですね。

今回は「とりあえず動かす編」ということで、ファイル名での検索をおこないます。


本来であれば、登録したドキュメントの中身に対する検索をおこないたいところですが、それについてはまた次回、という事で。

今回は、とりあえず動かしちゃいましょう!


import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.File;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;

import org.apache.solr.client.solrj.request.ContentStreamUpdateRequest;
import org.apache.solr.client.solrj.SolrQuery;
import org.apache.solr.client.solrj.SolrClient;
import org.apache.solr.client.solrj.SolrServerException;
import org.apache.solr.client.solrj.impl.HttpSolrClient;
import org.apache.solr.client.solrj.util.ClientUtils;
import org.apache.solr.client.solrj.response.QueryResponse;
import org.apache.solr.client.solrj.response.Group;
import org.apache.solr.client.solrj.response.GroupCommand;
import org.apache.solr.client.solrj.response.GroupResponse;
import org.apache.solr.common.util.ContentStreamBase;
import org.apache.solr.common.SolrDocumentList;
import org.apache.solr.common.SolrDocument;

public class SolrSearch {

    public static void main(String[] args){
        System.out.print("start: main\r\n");

        // Solrのインスタンス作成
        SolrClient client = new HttpSolrClient.Builder(
            "http://localhost:8983/solr/java_sample").build();

        SolrQuery solrQuery = new SolrQuery();

        // 検索結果として、文書IDを返却するよう設定
        solrQuery.setFields("id");

        // 検索結果の上限は100件
        solrQuery.setRows(100);

        try {
            StringBuilder queryString = new StringBuilder();
            String keyword = args[0];
            if (keyword.equals("")) {
                queryString.append("*");
            } else {
                String queryPhrase = "\"" + 
                    ClientUtils.escapeQueryChars(keyword) + "\"";
                queryString.append("(");
                queryString.append("filename:");
                queryString.append("*" + queryPhrase + "*");
                queryString.append(")");
            }

            // 検索実行
            solrQuery.set("q", queryString.toString());
            QueryResponse response = client.query(solrQuery);

            // 検索結果を表示
            SolrDocumentList list = response.getResults();
            if (list == null) {
                System.out.println("文書は存在しませんでした。");
            } else {
                System.out.println(list.getNumFound() + 
                    "件ヒットしました。");
                for (SolrDocument doc : list) {
                    System.out.println(doc.get("id"));
                }
            }
        } catch (SolrServerException e) {
            System.out.print("SolrServerException Occured!\r\n");
            e.printStackTrace();
        } catch (IOException e) {
            System.out.print("IOException Occured!\r\n");
            e.printStackTrace();
        } finally {
            try {
                // コミットして、コネクションをクローズ
                client.commit();
                client.close();
            } catch (Exception e) {
                System.out.print("Exception Occured!\r\n");
                e.printStackTrace();
            }
        }
        System.out.print("end: main\r\n");
    }
}

まず、35行目と36行目で、返却するフィールドを定義しています。

このサンプルプログラムでは、「id」を返却するようにしています


検索条件の設定部分は、47行目から51行目部分です。

サンプルプログラムでは、ファイル名を格納している「filename」フィールドに対して検索をおこなうようにしています

あと、検索ワードはエスケープしています。


まとめ

いかがでしたでしょうか?


紹介した方法でApache Solrを構築してサンプルプログラムを実行すれば、といあえず一連の動作は動きます。

しかし、ApacheSolrの醍醐味はやっぱり検索です。

ApacheSolrの検索には、いろいろなオプションが存在しますので、次回の記事でそこら辺を紹介していきます。


それではまた!