SpringSecurityで認証機能を実現する!セキュアなWEBサイトへの第1歩

SpringSecurity

認証機能が存在するWEBアプリケーションを開発する場合、認証に関連する機能をゼロから開発するのは大変ですね。
ちょっと考えただけで、以下のような制御が必要になります。


  • ログイン認証
  • セッション管理
  • 不正アクセスに対する制御
  • ワンタイムトークン

これらの、いわゆる認証で必要な機能を提供しているのが「SpringSecurity」です。
簡単な実装と設定ファイルへの設定記載をおこなえば、認証に必要な最低限の機能が使えるようになります。


今回は、認証機能が存在するWEBアプリケーションに対するSpringSecurityの使いかたを説明していきます。


ログイン認証

ログイン認証に関して、SpringSecurityでの実装はお決まりです。


applicationSecurity.xmlの基本的な定義


まずは、「applicationSecurity.xml」にSpringSecurityを使用するための定義が必要です。
以下に、定義例を記述します。


<sec:form-login login-page="/xxx.jsp"
    username-parameter="username"
    password-parameter="password"
    default-target-url="/xxx"
    always-use-default-target="true"
    login-processing-url="/j_spring_security_check"
    authentication-failure-handler-ref="authenticationFailureHandler"
    authentication-success-handler-ref="authenticationSuccessHandler" />

    <bean id="authenticationSuccessHandler" class="jp.co.xxx.AuthenticationSuccessHandler">
        <property name="alwaysUseDefaultTargetUrl" value="true" />
        <property name="defaultTargetUrl" value="/xxx/xxx" />
    </bean>

    <bean id="authenticationFailureHandler" class="jp.co.xxx.ExceptionMappingAuthenticationFailureHandler">
        <property name="defaultFailureUrl" value="/xxx" />
    </bean>

「xxx」の箇所は、SpringSecurityを組み込むWEBアプリケーション固有の部分になります。
<sec>タグで、SpringSecurityの基本動作を定義しています。
<sec>タグで、認証成功時と認証失敗時の制御クラスを定義しています。
「authenticationSuccessHandler」「authenticationFailureHandler」です。
その2つのクラスについて、<bean>タグでクラスの属性等を定義しています。


認証でのJava側の実装


Java側の実装においてポイントとなるのは、UserDetailsServiceクラスです。

UserDetailsServiceクラスのJavaDocは以下です。



で、UserDetailServiceを使った認証ロジックは以下になります。


import javax.servlet.http.HttpServletRequest;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;

public class MyUserDetailsService implements UserDetailsService {

    @Override
    public UserDetails loadUserByUsername(String loginID) {
        RequestAttributes attrs = RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = (HttpServletRequest) attrs.resolveReference(RequestAttributes.REFERENCE_REQUEST);
        String password = request.getParameter("password");
    }
}

「loadUserByUsername」のパラメータである「loginID」が、ログイン画面で入力されたログインID。
HTTPリクエストから取得している「password」が、ログイン画面で入力されたパスワードです。


この実装をおこなうこで、「loadUserByUsername」でログインIDとパスワードがわかります。
後は、ログインIDとパスワードが格納されているテーブルを検索して、ログインIDとパスワードの一致判定をやればOKです。


ログインIDとパスワードが一致しているユーザ情報の取得でさえも、SprinSecurityが機能を提供をしています。
以下のような実装をおこなえば、DBに登録されているログインID・パスワードと一致するユーザ情報を取得できます。


import java.util.Collection;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.User;

public class MyUserDetails extends User {
    public MyUserDetails(
        String userName,
        String password,
        String dispName,
        boolean enabled,
        boolean accountNonExpired,
        boolean credentialsNonExpired,
        boolean accountNonLocked,
        Collection<GrantedAuthority> authorities) {
        super(userName, password, enabled, accountNonExpired,
            credentialsNonExpired, accountNonLocked, authorities);
    }
}

※SpringSecurityは、ユーザロール(ユーザ毎の権限制御)についても制御可能ですが、今回のサンプルでは省略します。


上記クラスを作成し、「loadUserByUsername」において「MyUserDetails」のインスタンスを取得します。
以下のような感じです。


user = new MyUserDetails(
     ※ログインID,
     ※パスワード,
     ※ユーザ名,
     true,
     true,
     true,
     true,
     authorities);

インスタンスが取得できたら認証成功、インスタンスが取得できなかったら認証失敗、です


システムによっては認証が成功したとしても、ログイン成功とはみなさないケースもあるかと思います。
例えば、アカウントロック。
認証失敗した数をDBに保存しておいて、その数が規定値を超えたら画面にエラーメッセージを表示してログインさせない。


そういったシステム固有の認証チェックについても、この「loadUserByUsername」に実装するとよいでしょう。


認証に成功したら、ログイン情報をセッションに格納します。


    UserSession userSession = new UserSession();
    userSession.setLoginID(※ログインID);
    userSession.setUserName(※ユーザ名);
    RequestContextHolder.getRequestAttributes().setAttribute("session", userSession, RequestAttributes.SCOPE_SESSION);

userSessionというのは、システム固有でセッションとして保持しておきたい情報を格納するインスタンス。
そのインスタンスをセッションとして格納しています。


上記のサンプルプログラムは、ログインIDとユーザ名をセッションに格納する例です。
ここら辺はシステムによって変わってくるかと思います。
セッションとして保持するべきユーザ情報を見極めて、セッションに格納するようにしましょう。


認証に成功してセッションに情報を格納したら、認証については完了です。


ここまで記述した認証をおこなうためには、前述したSpringSecurityの定義の他に、「applicationSecurity.xml」に以下の定義が必要です。


<sec:authentication-manager>
    <sec:authentication-provider user-service-ref="myUserDetailsService" >
        <sec:password-encoder ref="passwordEncoder" />
    </sec:authentication-provider>
    </sec:authentication-manager>
    <sec:global-method-security pre-post-annotations="enabled"/>
    <bean id="myUserDetailsService" class="jp.co.xxx.MyUserDetailsService" />
    <bean id="sessionRegistry" class="org.springframework.security.core.session.SessionRegistryImpl" />

「jp.co.xxx.MyUserDetailsService」の「xxx」部分は、このクラスがシステム固有のクラスになるために「xxx」にしています。
システム固有のパッケージになります。


「jp.co.xxx.AuthenticationSuccessHandler」「xxx」部分は、このクラスがシステム固有のクラスになるために「xxx」にしています。
システム固有のパッケージになります。
この「AuthenticationSuccessHandler」に、ログイン成功時の制御を記述します。
固有のログを出すとか、ログイン成功回数をカウントするとか、業務に合わせた制御について、このクラスに処理を記述すればよいかと思います。


「value=”/xxx/xxx”」が、ログイン成功時に遷移するURLです。
デフォルトのURLになりますので、例外的に他URLに遷移させたい場合は、「AuthenticationSuccessHandler」で他URLに遷移する制御をおこなえばよいです。


パスワードの暗号化


パスワードはDBに登録することになりますが、一般的に平文(そのままの文字列)で登録することはセキュリティの観点からNGです。
何かしら暗号化して登録することが一般的です。


SpringSecurityは、暗号化されたパスワード文字列を参照しての認証についても自動で行ってくれます。
暗号化の種類は選べるのですが、以下の例は「SHA-1」で暗号化(ハッシュ化)された文字列をパスワードとして扱う場合の例です。
以下の定義を「applicationSecurity.xml」に定義すればよいです。


<bean id="passwordEncoder" class="org.springframework.security.authentication.encoding.ShaPasswordEncoder" >
    <constructor-arg value="1"/>
</bean>

「constructor-arg」がSHA強度です。
「1, 256, 384, 512」といった値を設定するのが一般的。


「applicationSecurity.xml」に定義するのは認証時の制御について必要になるのですが、パスワード文字列を登録する処理は別途開発が必要です。
多分、ほとんどのシステムがユーザ登録画面があり、そこからパスワード文字列を登録することになるかと思うので、その制御にパスワード文字列を暗号化してDBに登録する制御が必要になります。


SpringSecurityとは別に開発が必要ですが、難しい処理ではないです。


public static String getHashString(String str) {

    // ダイジェスト文字列取得
    MessageDigest md = null;
    try {
        md = MessageDigest.getInstance("SHA1");
    } catch (NoSuchAlgorithmException e) {
        return "";
    }

    // ダイジェスト文字列をハッシュ文字列に変換する
    byte[] dat = str.getBytes();
    md.update(dat);
    byte[] byt = md.digest();
    StringBuffer buff = new StringBuffer(byt.length * 2);
    for (int lc = 0; lc < byt.length; lc++) {
        int d = byt[lc] & 0xFF;
        if (d < 16) {
            buff.append("0");
        }
        buff.append(Integer.toHexString(d));
    }
    return buff.toString();
    }

実際のシステムでは、ユーザ情報の登録編集時に上記のようなロジックを組み込み、DBに暗号化されたパスワード文字列を登録します。


不正アクセスに対する制御(フィルター)


SpringSecurityを使って認証をおこなった場合、ログイン後の画面は認証に成功した状態じゃないとアクセスできません。
しかし、SpringSecurityの対象外にしたい画面があったります。
例えば以下のような画面。


  • ログインしてなくても使える画面(パスワード再発行画面、など)
  • CSSやJSファイル

上記のようにSpringSecutiryの対象外にしたい画面やファイルについては、「applicationSecurity.xml」に定義をおこなうことで設定できます。
フィルター定義ですね。


    <!-- SpringSecurity Filter対象外URL設定 -->
    <sec:http pattern="/css/**" security="none"/>
    <sec:http pattern="/js/**" security="none"/>

上記定義例は、ルートフォルダにある「css」フォルダと「js」フォルダ直下の全てのファイルについて、SpringSecurityの対象外にしています。
この定義をおこなわないと、画面にCSSが反映されず、かつ、JavaScriptも動かないので必須の定義になります。


CSRFトークン


SpringSecurityは、CSRFチェックをおこなうトークン値を自動で発行してくれます。
CSRFはクロスサイトリクエストフォージェリ”のことで、サイバー攻撃の一種です。


CSRFの詳しい説明は、以下のサイトにきれいまとまっています。



要はリクエストの改ざんです。
悪意のある第三者が、リクエストを偽造してWEBサイトにアクセスするサイバー攻撃になります。


これを防ぐ手段の一つがCSRFトークンです。
HTTPリクエスト内でCSRFトークンというシステムが発行した文字列を受け渡します。

このトークン値がシステムが期待している値と違ったら不正なアクセスと判断し、SpringSecurityがアクセスを遮断します。


ちょっと小難しいですが、こういった制御をSpringSecurityは自動でやってくれます。
JSP内にCSRFトークンを埋め込む処理を実装するだけです。


具体的には、以下の記述をおこないます。
これはJSPの場合、ですが。


<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}">

基本はログイン画面でのみ埋め込みばよい。
SpringSecurityは、どうやら1回発行したトークン値を使い回すようです。


業務システムだとブラウザの戻るボタンを許可しない場合もありますが、その場合は上記のコードを全画面のJSPの埋め込む必要があります。
全ての画面に埋めこむと、戻るボタンで戻った先の画面のCSRFトークン値が古くなってしまい、トークン値不一致でSpringSecurityが不正アクセスと判断します


仮に不正アクセスが発生した場合の制御については、「applicationSecurity.xml」に定義したクラスで制御可能。


<bean id="requestDataValueProcessor"
    class="jp.co.xxx.CompositeRequestDataValueProcessor">
    <property name="processors">
        <list>
            <bean class="org.springframework.security.web.servlet.support.csrf.CsrfRequestDataValueProcessor" />
        </list>
    </property>
</bean>

まとめ


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


世の中に存在するほぼすべてのWEBシステムが認証機能をもっているかと思いますが、認証機能をゼロから作るのは骨が折れます。。。
SpringSecurityを導入すれば、認証に必要な機能をゼロから作る必要はありません。


XMLの定義で結構必要なところが解り辛い部分なのですが、使わない手はないかと。


それではまた!



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」推しです。
やはり、検索パフォーマンスが高いのは魅力的なので。


それではまた!



MySQLデータのインポートとエクスポート!圧縮と解凍する方法を紹介

Linux

こんにちは。
さくさくTECHブロガーのさくです。


今回は、Linux + MySQL の環境において、シェルを使ってエクスポートとインポートをおこなう方法を紹介します。


どういった場面を想定しているかといいますと、一番は定期実行のバッチですね。
例えば、1日1回、特定テーブルのデータをエクスポートする、といった要件があった場合。


その場合、エクスポートするコマンドをシェル化してクーロンに設定すると思います。
そういった時に使える手法です。


インポートについては、例えば実データが壊れてしまった場合の復旧ですね。
エクスポートしたデータをインポートすれば、データは元通りになります。


環境情報


OS:Linux
DB:MySQL


MySQLには「saku」というDBを準備します。
そのDBには「person」というテーブルが存在します。
テーブル構成は以下。


mysql> desc person;
+----------+--------------+------+-----+---------+----------------+
| Field    | Type         | Null | Key | Default | Extra          |
+----------+--------------+------+-----+---------+----------------+
| personID | int(11)      | NO   | PRI | NULL    | auto_increment |
| name     | varchar(32)  | NO   | MUL | NULL    |                |
| kana     | varchar(128) | NO   |     | NULL    |                |
| address  | varchar(512) | YES  |     | NULL    |                |
| gender   | char(1)      | NO   |     | NULL    |                |
+----------+--------------+------+-----+---------+----------------+
5 rows in set (0.00 sec)

また、あらかじめデータも格納しておきます。

mysql> select * from person;
+----------+--------+--------------+-----------+--------+
| personID | name   | kana         | address   | gender |
+----------+--------+--------------+-----------+--------+
|        4 | 山田   | ヤマダ       | 東京      | 0      |
|        5 | 高橋   | タカハシ     | 神奈川    | 0      |
|        6 | 田中   | タナカ       | 千葉      | 1      |
+----------+--------+--------------+-----------+--------+
3 rows in set (0.00 sec)

このテーブルに対して、エクスポート、および、インポートをおこないます。


エクスポート

まずはエクスポート。
エクスポートする条件も指定します。
「gender」が「1」のレコードのみをエクスポートします。
シェルは以下になります。

#!/bin/sh
# 
# エクスポートシェル
# 
mysqldump -h localhost \--single-transaction --skip-opt --extended-insert --quick --set-charset --no-create-info --no-autocommit \
  -u ※ユーザ名 -p※パスワード -h ※ホスト名 ※DB名 person \
  --where="gender = '0'" \
  | gzip > person.sql.gz 2>&1 | tee -a export.log

使用するコマンドは mysqldump コマンドです。
mysqldump コマンドで、「gender」が「1」のレコードのみをエクスポートします。
かつ、エクスポートするファイルはperson.sql.gzという圧縮ファイルとして出力し、エクスポート時のログをexport.logに出力します。


エクスポート結果を圧縮することで、仮に大量データをエクスポートすることになったとしても、ある程度はディスク使用率おさえることができます。


様々なオプションを指定していますが、各オプションの用途は以下になります。


オプション 説明
–single-transaction これを設定しておくと、InnoDBについてエクスポートする際、他SQLがロックされないです。
運用中システムについてエクスポートする際は、このオプションを指定した方が無難。
–skip-opt 「–opt」の設定をオフにします。
「–opt」の設定の中に「–add-locks」がありますが、これが有効だとエクスポート中にテーブルがロックされてしまう可能性があります。
それを防ぐために、–skip-optを設定して、「–opt」の設定をオフにします。
–extended-insert 出力されるダンプファイルについて、バルクインサートの形でSQLを作成します。
–quick MySQLのマニュアルをみる限りでは、大量データをエクスポートする際はつけた方がいいみたいです。
意味を理解しきれませんでしたが、たぶん、メモリ上にエクスポートデータを展開するタイミングの話のようです。
このオプションを指定した方が、より高速なやり方を実施するようです。
–set-charset 出力するエクスポートするファイルに SET NAMES default_character_set を出力します。
–no-create-info エクスポートするファイルに CREATE文を出力しません。
–no-autocommit エクスポートするファイルに AutoCommitをオフにします。
具体的には、エクスポートするファイルのSQL先頭に「set autocommit=0;」を出力します。
かつ、最後に「commit;」を出力します。

エクスポートはこんな感じです。
エクスポートが完了するとperson.sql.gzというファイルが作成されます。


インポート


次にインポート。
インポートは、エクスポートシェルで作成したperson.sql.gzを解凍して、解凍したファイルを参照してインポートします。


シェルは以下になります。

#!/bin/sh
# 
# インポートシェル
# 
gunzip -d person.sql.gz >> insert.sql
mysql -u ※ユーザ名 -h ※ホスト名 -D ※DB名 -p※パスワード < insert.sql 2>&1 \ | tee -a import.log

まずは guzip コマンドで解凍して、「insert.sql」を出力。
gunzipに-d オプションをつけているので、元ファイルの「person.sql.gz」を削除して「insert.sql」に解凍結果を出力。


インポート自体はmysqlコマンドで実行しています。
リダイレクト「<」を指定して、解凍したSQLファイルをそのままインポートします。


まとめ


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


今回はたった3行しか入っていないテーブルに対してのエクスポートなので性能は意識する必要はないのですが、数億件のエクスポートする場合は性能が大事になります。
紹介したオプションを指定すれば、極力、サーバ性能に沿ったパフォーマンスを出せると思っています
参考にしてください。


それではまた!



ファイルが削除されない原因は何?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」を使うようにしよう。。。


それではまた!




JREが複数動いている環境向け。Javaプログラムで、実行しているJREのバージョンを確認する方法。

Java

複数の異なるJREがインストールされているマシン上でJavaアプリケーションを動かす場合、パスの記述方法を誤っていると意図していないJRE上でアプリケーションが動いてしまうことがあります。


アプリケーションで動作保証しているJREのバージョンは決めている事がほとんどなので、動作しているJREのバージョンをきちんと把握しておく事はとても大事です。


今回は、Javaアプリケーション内で、動作しているJREを判別する方法を紹介します。


皆さんは、開発したJavaアプリケーションを動かす場合、Javaアプリケーションを動かすサーバやパソコンにJREをインストールしているかと思います。
その際、環境変数にJREのパスを記述したり、もしくは、Javaアプリケーションの起動シェル(バッチ)にJREのパスを記述したり、といった方法をとっているのではないでしょうか。


単純に、1つのバージョンのJREをインストールし、そのサーバやパソコンで動作するアプリケーションはインストールした1つのバージョンのJRE上で動かす場合は問題ないのですが、例えば、1つのサーバやパソコン内で、あるアプリケーションはJava1.7で動かしたいが、あるアプリケーションはJava1.8で動かしたいといったパターンがまれにあります。


その場合、「本当にこのアプリ、Java1.8で動いてるっけ?」って不安になりませんか?
だって、Javaは2つ入っている訳でありまして。。。


こういう心配性な技術者向けに、Javaプログラム内で参照しているJavaのパスを取得する方法を紹介します。


複数のJREがインストールされている環境


サーバ上で10個のアプリケーションが動かす必要があり、そのうち9個はJava1.7で動かす必要があるが、1個はJava1.8で動かす必要がある、といった場面です。
この場合、ほとんどの人が以下のような方法をとるのではないか?


  • JREは、1.7と1.8の両方をインストールする。
  • 環境変数には、Java1.7のパスを定義する。
  • Java1.8で動かす必要があるアプリについてどうにかする。

環境変数にはJava1.7とJava1.8のパスを両方定義しても無意味です。
参照先パスは、先に見つかった方を採用するので、後に定義したJavaのパスは無視されます


Java1.7が採用される環境変数の記述方法

C:\Program Files\Java\jre1.7\bin;C:\Program Files\Java\jre1.8\bin

Java1.8が採用される環境変数の記述方法

C:\Program Files\Java\jre1.8\bin;C:\Program Files\Java\jre1.7\bin

2つ定義するのは無意味なので、環境変数の定義は以下のように記載するのが正解です。


正しい環境変数の定義

C:\Program Files\Java\jre1.7\bin

Java1.7で動くアプリケーションは上記でいいとして、問題はJava1.8で動かす方です。
王道としてはアプリケーションの起動シェルやバッチにJava1.8のパスを定義する方法がメジャーです。


set PATH=C:\Program Files\Java\jre1.8\bin;%PATH%

%PATH%の前にJava1.8のパスを記載するのがポイントです。


環境変数にはJava1.7のパスが定義されているので、%PATH%の後にJava1.8のパスを記載してしまうとJava1.7の方が採用されてしまいます。%PATH%の前にJava1.8のパスを定義することで、Java1.8の方が採用されます。


本当に意図したJREで動いているか?を確認


動かすアプリケーションに以下の試験プログラムを入れてみて、意図したJavaのパスを参照しているかを確認しましょう。
「System.getProperty」を使います。

public class getProperty {

    public static void main(String[] args){
        System.out.println("sun.boot.library.path=" + 
            System.getProperty("sun.boot.library.path"));
    }
}

実行結果は以下になります。

sun.boot.library.path=C:\Program Files\Java\jre1.8\bin

「sun.boot.library.path」がJavaのパスです。
これが、Java1.8になっていればOK。


Javaのプロパティ情報を出力する


Javaのパスを確認してみましたが、Javaのパス以外にもプロパティ情報が存在します。
以下のプログラムでリスト形式で確認できます。

import java.util.Properties;

public class getProperty2 {

    public static void main(String[] args){
        Properties props = System.getProperties();
        props.list(System.out);
    }
}

実行結果は以下になります。

-- listing properties --
sun.desktop=windows
awt.toolkit=sun.awt.windows.WToolkit
java.specification.version=12
sun.cpu.isalist=amd64
sun.jnu.encoding=MS932
java.class.path=.
java.vm.vendor=Oracle Corporation
sun.arch.data.model=64
user.variant=
java.vendor.url=https://java.oracle.com/
java.vm.specification.version=12
os.name=Windows 7
sun.java.launcher=SUN_STANDARD
user.country=JP
sun.boot.library.path=C:\Program Files\Java\jdk-1.8\bin
sun.java.command=getProperty
jdk.debug=release
sun.cpu.endian=little
user.home=C:\Users\user
user.language=ja
sun.stderr.encoding=ms932
java.specification.vendor=Oracle Corporation
java.version.date=2019-04-16
java.home=C:\Program Files\Java\jdk1.8
file.separator=\
java.vm.compressedOopsMode=32-bit
line.separator=
java.vm.specification.vendor=Oracle Corporation
java.specification.name=Java Platform API Specification
java.awt.graphicsenv=sun.awt.Win32GraphicsEnvironmen
user.script=
sun.management.compiler=HotSpot 64-Bit Tiered Compilers
java.runtime.version=12.0.1+12
user.name=saku
path.separator=;
os.version=6.1
java.runtime.name=Java(TM) SE Runtime Environment
file.encoding=MS932
java.vm.name=Java HotSpot(TM) 64-Bit Server VM
java.vendor.url.bug=https://bugreport.java.com/bugreport/
java.io.tmpdir=C:\Users\Temp\
java.version=12.0.1
user.dir=C:\blog\systemProperty
os.arch=amd64
java.vm.specification.name=Java Virtual Machine Specification
sun.os.patch.level=Service Pack 1
java.library.path=C:\Program Files\Java\jre1.8\bin;...
java.vm.info=mixed mode, sharing
java.vendor=Oracle Corporation
java.vm.version=12.0.1+12
sun.io.unicode.encoding=UnicodeLittle
java.class.version=56.0

「sun.boot.library.path」も出力されていますね。

48行目です。

まとめ


いかがでしたでしょうか?
もし、“このアプリって本当に、意図したJavaみてるっけ?”と気になった時は、「System.getProperty」を使ってJavaのパスを確認した方がよいかと思います。
パスの設定が間違っていても、なかなか気が付かなかったりするので、実際にアプリケーションにデバッグブログラムをいれて確認すると安心ですね。


それではまた!



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」を使ったハイライト表示を紹介したいと思います。


それではまた!