intra-mart Accel Platform IM-ContentsSearch プログラミングガイド 第6版 2023-10-01

6. 検索対象の作成・登録・削除

この章では、任意のコンテンツを作成、および、登録、削除のプログラミング方法についてサンプルを交えながら説明します。

6.1. はじめに

全文検索対象を追加するためにはコンテンツを作成し、APIを利用して登録する必要があります。
登録用のコンテンツを作成する手順は以下の通りです。
  1. コンテンツの各フィールドに格納するデータを設計する。
  2. APIを利用してコンテンツを作成する。
  3. APIを利用してコンテンツを登録する。
具体的な作成処理をイメージするため、次のようなRDBの製品情報マスタテーブルを検索対象のコンテンツとして登録する処理を考えます。
テーブル内の各情報をどのフィールドに登録するか、事前に設計しておく必要があります。
表 製品情報マスタテーブル
No. 論理名 物理名 データ型 備考
1 製品コード code varcher (20) 必須、主キー
2 製品名 name varcher (200) 必須
3 分類 category varchar (50) 必須
4 価格 price number (13,0) 必須
5 説明 description varcher (1024)  
6 参考ファイル reference_file varcher (200)
説明用に添付するファイルのパブリックストレージ上のパスを保存します。
7 作成日時 create_date timestamp 必須
8 更新日時 record_date timestamp 必須

6.1.1. 標準フィールドの設計

登録用コンテンツの各フィールドに格納する情報を設計する必要があります。
サンプルでは標準フィールドとして製品情報マスタの情報を以下のように設定します。
まず、標準フィールドに設定するTYPE以外の設定値について検討します。

TYPEフィールド

標準フィールドにて必須に定義されている TYPEフィールド は、標準検索画面にて下記の3つの用途に使用されます。
  • 検索時の絞り込み条件、 コンテンツ種別 として利用
  • 検索結果の表示に利用するテンプレート画面の切り替え
  • 検索結果に表示される絞り込み条件 (画面左のツリー)
また、TYPEフィールドに複数の値を指定することでコンテンツ種別に階層情報を持たせることが可能です。
階層情報を持たせることで、検索結果に表示される絞り込み条件が階層化されます。
サンプルでは製品情報マスタに対するTYPEとして “product_master” という値を指定します。
さらに、製品情報マスタの分類(category)に応じて下記のTYPEフィールドを追加で指定します。
  • 製品情報マスタの「分類」の値が”Base”である場合

    • “product_master$Base”
  • 製品情報マスタの「分類」の値が”Product”である場合

    • “product_master$Product”
  • 製品情報マスタの「分類」の値が”eBuilder”である場合

    • “product_master$eBuilder”

注意

検索するコンテンツを分類するため、静的ファセットは検索対象ごとに一意となる値を指定する必要があります。
製品にて利用しているTYPE を確認して、これらの値と重複しない文字列をTYPEフィールドに指定してください。

その他のフィールド

表 標準フィールド(製品情報マスタ)
No. フィールド名 設定値 説明
1 ID “product_master_” + %製品コード% 製品情報マスタ の主キーである 製品コード を設定します。システム一意である必要があるため、商品マスタ検索であることを表すプレフィックスを付与します。
2 TYPE
“product_master”
“product_master$Base” (「分類」の値が”Base”である場合)
“product_master$Product” (「分類」の値が”Product”である場合)
“product_master$eBuilder” (「分類」の値が”eBuilder”である場合)
 
3 URL “product_master/product”
検索結果のタイトル(リンク)をクリックした際にポップアップ表示するURLを設定します。
4 ID_ORIGINAL %製品コード% 製品情報マスタ の主キーである 製品コード を設定します。
5 TITLE %製品名%
タイトルとして表示するため 製品名 を設定します。
6 TEXT %説明%
説明 に値が登録されている場合に設定します。
7 ATTACHMENT
%参考ファイル%
参考ファイル に登録されているパスのパブリックストレージ上に保存されたファイル内容を設定します。
ファイルの設定に関する詳細は、 検索対象の作成・登録・削除 にて説明します。
8 RECORD_DATE %更新日付%  

TYPEフィールドについて

標準フィールドにて必須に定義されているTYPEフィールドは、標準検索画面にて下記の3つの用途に使用されます。
  • 検索時の絞り込み条件、コンテンツ種別として利用
  • 検索結果の表示に利用するテンプレート画面の切り替え
  • 検索結果に表示される絞り込み条件 (画面左のツリー)
また、TYPEフィールドに複数の値を指定することでコンテンツ種別に階層情報を持たせることが可能です。
階層情報を持たせることで、検索結果に表示される絞り込み条件が階層化されます。
サンプルでは製品情報マスタに対するTYPEとして“product_master”という値を指定します。
さらに、製品情報マスタの分類(category)に応じてTYPEフィールドを追加で指定しています。(「表 標準フィールド(製品情報マスタ)」参照)

注意

検索するコンテンツを分類するため、静的ファセットは検索対象ごとに一意となる値を指定する必要があります。
製品にて利用しているTYPE を確認して、これらの値と重複しない文字列をTYPEフィールドに指定してください。

6.1.2. 動的フィールドの設計

動的フィールドに設定する設定値について検討します。設定した値は次のような用途に利用可能です。
  • 標準の全文検索画面において検索結果に表示する
  • 検索APIを用いた任意の検索処理にて検索条件に指定する
サンプルでは製品情報マスタの情報のうち、検索結果画面に表示させる以下の情報を設定します。
表 動的フィールド(製品情報マスタ)
No. フィールド名 データ型 設定値 説明
1 category STRING %分類%  
2 price INT %価格%  

6.2. コンテンツを作成する

検索結果に独自のコンテンツを表示するためには、登録用コンテンツを作成し登録処理を行う必要があります。
まずは登録用コンテンツのインスタンスを作成し、値を設定していきます。
登録用コンテンツのインスタンス作成サンプル
// 登録用コンテンツをインスタンス化します
InputContent content = new StandardInputContent();
インスタンスを生成したら、設計した内容に沿って登録用コンテンツにそれぞれの値を設定していきます。
なお前提として、設定する値は事前にRDBから取得してモデル( product )に格納してあるものとします。

6.2.1. 標準フィールドの設定

標準フィールドの設計 にて設計した通りに標準フィールドに値を設定していきます。
標準フィールドを設定するサンプル
// 標準フィールドを設定
content.setId("product_master_" + product.getCode()).
        setUrl("product_master/product").
        setOriginalId(product.getCode()).
        setTitle(product.getName()).
        setRecordDate(product.getRecordDate());

// 説明が存在した場合は追加
if (!StringUtil.isBlank(product.getDescription())) {
    content.addText(product.getDescription());
}

6.2.1.1. 複数のTYPEフィールドを指定する

標準フィールドの設計 にて設計した複数のTYPEフィールドに値を設定していきます。
複数のTYPEフィールドを指定するサンプル
content.setTypes("product_master", "product_master$" + product.getCategory());

検索結果画面に表示する絞り込み条件(ファセット)の名称について

検索結果画面に表示する絞り込み条件の名称は、テンプレート設定ファイルにプロパティキーを設定することで表示されます。(詳しくは テンプレート設定ファイルの詳細 を参照してください。)
テンプレート設定ファイルにプロパティキーを設定していない場合は、TYPEフィールドに設定した “product_master” に対応する表示名が設定されていないため、検索結果画面の絞り込み条件の欄が「未定義(日本語ロケールの場合)」と表示されます。
テンプレート設定ファイルに設定せず、プログラム内で定義した値やRDBから取得した値などを絞り込み条件の名称として表示するには、動的プロパティ保存API( DynamicPropertiesHolder )を利用して、別途 RDB に登録する必要があります。

コラム

IM-ContentsSearch for Accel Platform 2023 Autumn(Hollyhock) 以降、動的プロパティ保存APIの保存先は RDB に変更されました。
それ以前のバージョンでは、PublicStorage に JSON 形式で下記のディレクトリに保存されていました。
  • %PUBLIC_STORAGE_PATH%/products/im_contents_search/store/%テナントID%/dynamic_property/type.json
絞り込み条件の名称(カテゴリ名)を RDB に保存するサンプル
// ... 省略 ...

import jp.co.intra_mart.foundation.contentssearch.common.DynamicPropertiesHolder;
import jp.co.intra_mart.foundation.contentssearch.exception.ContentsSearchCrawlingException;
import jp.co.intra_mart.foundation.contentssearch.model.field.Fields;
import jp.co.intra_mart.foundation.contentssearch.web.model.DynamicFacetInputValue;
import jp.co.intra_mart.foundation.contentssearch.web.util.FacetUtil;
import jp.co.intra_mart.foundation.i18n.locale.LocaleInfo;
import jp.co.intra_mart.foundation.i18n.locale.SystemLocale;

// ... 省略 ...

    /** タイプ用の動的プロパティホルダー */
    private final DynamicPropertiesHolder dynamicPropertiesHolder = DynamicPropertiesHolder.getHolder(Fields.TYPE.getName());

    /**
     * コンテンツのタイプを取得
     * @return タイプ
     */
    protected String getType() {
        return "product_master";
    }

// ... 省略 ...

    /**
     * 動的ファセットデータとしてカテゴリのキーを保存します。
     * @param category カテゴリ
     * @throws ContentsSearchCrawlingException
     */
    private void saveFacetType(final String category) throws ContentsSearchCrawlingException {

        // 動的ファセット一覧を作成
        final List<DynamicFacetInputValue> facets = new ArrayList<>();
        facets.add(new DynamicFacetInputValue(getType()));
        facets.add(createCategoryFacetValue(category));

        final Collection<List<DynamicFacetInputValue>> dynamicProperties = new ArrayList<List<DynamicFacetInputValue>>();
        dynamicProperties.add(facets);

        // ファセット情報を保存
        try {
            dynamicPropertiesHolder.putLocalizedValues(FacetUtil.convertLocalizedValues(dynamicProperties, Arrays.asList(new String[] { getType() })));
        } catch (final DynamicPropertiesException e) {
            throw new ContentsSearchCrawlingException("ファセット情報の保存に失敗しました。", e);
        }
    }
    
    /**
     * カテゴリの動的ファセットを作成します。
     * @param category カテゴリ
     * @return カテゴリの動的ファセット
     */
    private DynamicFacetInputValue createCategoryFacetValue(final String category) {
        // 動的ファセットをカテゴリで初期化
        final DynamicFacetInputValue facetValue = new DynamicFacetInputValue(category);

        // システムで利用可能な全ロケール分設定
        for (final LocaleInfo localeInfo : SystemLocale.getLocaleInfos()) {
            facetValue.addLocalizedValue(localeInfo.getLocale(), category);
        }

        return facetValue;
    }

6.2.2. 動的フィールドの設定

動的フィールドの設計 にて設計した通りに標準フィールドに値を設定していきます。
動的フィールドを設定するサンプル
// 動的フィールドの設定
// 分類, 価格を設定
content.setValue(Fields.STRING.toField("code"), product.getCode()).
        setValue(Fields.INT.toField("price"), product.getPrice());

6.2.3. 添付ファイルの設定

登録用コンテンツに対して任意のファイルを設定できます。
登録用コンテンツに設定されたファイルは、登録処理の中でファイル内のテキストデータが抽出されます。
抽出されたテキストデータはファイル名と合わせて、全文検索の対象として扱われます。
ここでは、参考ファイルにて指定されたパスにあるファイルを登録します。
添付ファイルを設定するサンプル
// 参照ファイルパスが存在した場合は追加
if (!StringUtil.isBlank(product.getReferenceFile())) {
    // パブリックストレージを取得
    PublicStorage storage = new PublicStorage(product.getReferenceFile());
    try {
        // ファイルだった場合のみ追加
        if (storage.isFile()) {
            content.addAttachment(new PublicStorageAttachment(storage));
        }
    } catch (IOException e) {
        // PublicStorageからファイルが取得できなかった場合
    }
}

6.2.4. 権限情報の設定

登録するコンテンツに検索可能な権限を設定します。
登録用コンテンツの作成では権限を直接設定するのではなく、権限を生成するビルダーを設定します。
権限情報を設定するサンプル
// 認証済みユーザであれば参照可能とする権限を追加
content.addACIBuilder(new EveryoneACIBuilder());

// 特定ユーザ(青柳、円山、上田)を参照可能とする権限を追加
content.addACIBuilder(new StandardUserACIBuilder("aoyagi", "maruyama", "ueda"));

// 特定ロールの保持者を参照可能とする権限を追加
content.addACIBuilder(new StandardRoleACIBuilder("im_cs_user"));
利用可能な権限の一覧については、登録用コンテンツに指定可能な権限 を参照してください。

注意

権限情報が設定されていないコンテンツは検索できないため、必ずコンテンツに対して権限情報を設定してください。
適切な権限情報は検索対象により異なります。通常は元となるデータと同じ権限情報を設定します。

6.3. コンテンツを登録する

前項で作成した登録用コンテンツを、登録API( ContentsSearchManager#add(InputContent) )を利用して登録します。
その後 ContentsSearchManager#commit() を実行することで、登録したコンテンツを検索結果へ反映させています。
コンテンツを登録するサンプル
try {
    // マネージャのインスタンスを取得
    ContentsSearchManager manager = new ContentsSearchManager();

    // コンテンツを登録
    manager.add(content);

    // 登録内容の確定(検索結果への反映)
    manager.commit();

} catch (ContentsSearchExecutionException e) {
    // 登録、または、確定処理に失敗した場合
}

注意

IM-ContentsSearch の制限解除ライセンスが未登録の状態で登録済みのコンテンツ数が2万を超えた場合には、ContentsSearchManager#add()を実行したタイミングで LicenseLimitReachedException がスローされます。
LicenseLimitReachedExceptionContentsSearchExecutionException のサブクラスなため、上記サンプルの例外処理で捕捉可能です。

6.4. コンテンツを削除する

登録済みのコンテンツを削除したい場合、 ContentsSearchManagerdelete系メソッドを利用します。
コンテンツを削除するサンプル
try {
    // マネージャのインスタンスを取得
    ContentsSearchManager manager = new ContentsSearchManager();

    // 任意の条件を指定してマッチしたコンテンツを削除(例:登録日が2ヶ月以上前のコンテンツ)
    Calendar maxDate = Env.getSystemDate();
    maxDate.add(Calendar.MONTH, -2);
    manager.delete(Condition.lessThan(Fields.RECORD_DATE, maxDate.getTime(), true));


    // TYPEフィールドを指定して削除
    manager.deleteByType("product_master");
    // 下記の条件を指定した場合と同じ
    manager.delete(Condition.type("product_master"));


    // すべてのコンテンツを削除
    manager.deleteAll();
    // 下記の条件を指定した場合と同じ
    manager.delete(Condition.all());


    // 削除内容の確定(検索結果への反映)
    manager.commit();

} catch (InvalidSearchConditionException e) {
    // 検索条件が不正だった場合
} catch (ContentsSearchExecutionException e) {
    // 削除、または、確定処理に失敗した場合
}

注意

RDBと違いロールバック処理が用意されていないため、削除したコンテンツを復元することはできません。
commit() 処理は、削除したコンテンツを同じタイミングで検索結果に反映するためだけに利用します。

6.5. クローラを作成する

ここではコンテンツ登録処理をまとめて行うジョブであるクローラの作成方法について説明します。
次の理由から、コンテンツの作成処理はジョブによる非同期実行を推奨します。
  • コンテンツの登録処理は大量のデータ件数を対象とする場合が予想される。
  • 添付ファイルからテキストを抽出するにはファイルサイズやファイルの種類に応じて処理の負荷が大きくなる。
  • 通常のリクエスト処理と比較して処理時間が長くなる傾向がある。
尚、作成したジョブはジョブ管理機能を利用して登録し、実行の設定を行う必要があります。
ジョブの設定について関する詳細は、「テナント管理者操作ガイド - ジョブを設定する」を参照してください。

6.5.1. クローリングの種類

IM-ContentsSearch ではクローリングを以下の3種類に分類しています。
  1. 差分クローリング

    新規追加、または、更新された検索対象データに対してコンテンツの作成、および、登録処理を行います。
    作成するには、何らかの方法で「新規追加された、または、更新された」という情報が取得可能である必要があります。
  2. 削除クローリング

    登録済みのコンテンツを必要に応じて削除します。
    検索結果として表示すべきでは無くなった過去のコンテンツを削除したり、一律ですべてのコンテンツ(通常はTYPEごと)を削除します。
  3. 再作成クローリング

    既存で登録されたコンテンツをすべて破棄し、すべてのコンテンツを再度作成します。
    通常は削除クローリング処理差分クローリング処理を続けて呼ぶことで代替できますが、検索対象のデータ保持方法により実装は異なります。
IM-ContentsSearch には上記のクローリングジョブを登録するためのジョブネットが用意されています。
クローリング用のジョブネットに関する詳細は、「ジョブ・ジョブネット リファレンス - IM-ContentsSearch クローラ」を参照してください。

6.5.2. 基底クラスを利用した実装

ここでは IM-ContentsSearch にて用意しているクローラジョブの基底クラス BaseCrawlingJob を利用した実装について説明します。
尚、 BaseCrawlingJob では以下の機能が提供されます。
  • 削除クローリング( executeDelete )、再作成クローリング( executeReindex )の標準実装

    上記2つのメソッドは、必要に応じてオーバーライドしてください。

  • 各クローリングメソッドで利用可能な UpdateService インスタンスの提供

    UpdateService は、 ContentsSearchManager の内部でも利用している、更新処理専用の機能を提供するAPIです。

  • ジョブの実行パラメータによる、差分クローリング / 削除クローリング / 再作成クローリング の動作切替

    実行パラメータ crawlingType の値によって動作が切り替わるため、一つのジョブで3つのクローラを実装できます。

  • ジョブの実行パラメータによる、コミット処理、および、最適化処理の実行指定

    実行パラメータ withCommit によってコミット処理実行を、 withOptimize によって最適化処理実行を制御可能です。

実装例

BaseCrawlingJob を利用したジョブのサンプル実装を記載します。処理対象の判別には更新日時を利用しています。
また、更新日時の検索条件として利用するためにクローラの最終実行日時を扱うメソッドを使用しています。
クローラの最終実行日時と各メソッドの詳細については次項にて説明します。
クローラのサンプル
public class ProductMasterCrawlingJob extends BaseCrawlingJob {

    /**
     * コンテンツのタイプを取得(実装必須)
     * @return タイプ
     */
    @Override
    protected String getType() {
        return "product_master";
    }


    /**
     * 差分クローリング処理(実装必須)
     * @param  updateService ジョブが初期化した更新用サービス
     * @throws JobExecuteException クローリングに失敗した場合
     */
    @Override
    protected void executeDelta(UpdateService updateService) throws JobExecuteException {

        // クローラの最終実行日の取得(後述)
        Date lastCrawlingDate = getLastCrawlingDate();

        // クローラ実行日時を保存
        Date crawlingDate = Env.getSystemDate().getTime();

        // 作成した登録用コンテンツを保存
        List<InputContent> contents = new ArrayList<>();

        // 実行日時以降に更新された製品情報をRDBから取得
        try (
                Connection conn = new TenantDatabase().getConnection();
                PreparedStatement stmt = conn.prepareStatement("SELECT * FROM sample_product_master WHERE record_date >= ?");
            ) {
            // パラメータの設定
            stmt.setTimestamp(1, new Timestamp(lastCrawlingDate.getTime()));

            // RDBから値を取得
            ResultSet result = stmt.executeQuery();

            while (result.next()) {
                contents.add(/* ResultSetからコンテンツを作成する処理 */);
            }
        } catch (SQLException e) {
            throw new JobExecuteException(e.getMessage(), e);
        }

        // コンテンツを登録
        try {
            updateService.add(contents);
        } catch (ContentsSearchExecutionException e) {
            throw new JobExecuteException(e.getMessage(), e);
        }

        // クローラの最終実行日を更新(後述)
        updateLastCrawlingDate(crawlingDate);
    }


    /**
     * 削除クローリング処理
     * @param  updateService ジョブが初期化した更新用サービス
     * @throws JobExecuteException クローリングに失敗した場合
     */
    @Override
    protected void executeDelete(UpdateService updateService) throws JobExecuteException {

        // 削除クローリング(基底実装の呼び出し)
        super.executeDelete(updateService);

        // クローラの最終実行日時をクリア(後述)
        clearLastCrawlingDate();
    }


    /**
     * 再作成クローリング処理(基底実装と同じです)
     * @param  updateService ジョブが初期化した更新用サービス
     * @throws JobExecuteException クローリングに失敗した場合
     */
    @Override
    protected void executeReindex(UpdateService updateService) throws JobExecuteException {

        // 削除クローリング処理(基底実装の呼び出し)
        executeDelete(updateService);

        // 差分クローリング処理(基底実装の呼び出し)
        executeDelta(updateService);
    }

}

6.5.3. 最終実行日時の保存方法

前項の基底クラスを利用した実装にて利用していた、クローラの最終実行日時に関するメソッドについて説明します。
クローラのサンプル実装において以下のメソッドを利用しています。
  • updateLastCrawlingDate(Date) : クローラの最終実行日時を保存します
  • getLastCrawlingDate() : クローラの最終実行日時を取得します
  • clearLastCrawlingDate() : クローラの最終実行日時をクリアします
下記サンプルでは IM-ContentsSearch にて提供している、ストレージ上に永続化するAPI( LastCrawlingDateHolder )を利用しています。
また、 BaseCrawlingJob にて定義されている getType() メソッドも利用しています。
最終実行日時処理のサンプル
public class ProductMasterCrawlingJob extends BaseCrawlingJob {

    // ... 省略 ...

    /**
     * クローラの最終実行日時を取得します。
     * @return String クローラの最終実行日時
     * @throws JobExecuteException 取得に失敗した場合
     */
    protected Date getLastCrawlingDate() throws JobExecuteException {
        // ホルダーを取得
        LastCrawlingDateHolder crawlingDateHolder = LastCrawlingDateHolder.getHolder(getType());

        try {
            // 最終実行日時を取得
            return crawlingDateHolder.getLastCrawlingDate();
        } catch (ContentsSearchException e) {
            throw new JobExecuteException(e.getMessage(), e);
        }
    }

    /**
     * クローラの最終実行日時を更新します。
     * @param updateDate 最終実行日時
     * @throws JobExecuteException 更新に失敗した場合
     */
    protected void updateLastCrawlingDate(Date updateDate) throws JobExecuteException {
        // ホルダーを取得
        LastCrawlingDateHolder crawlingDateHolder = LastCrawlingDateHolder.getHolder(getType());

        try {
            // 最終実行日時を更新
            crawlingDateHolder.updateLastCrawlingDate(updateDate);
        } catch (ContentsSearchException e) {
            throw new JobExecuteException(e.getMessage(), e);
        }
    }

    /**
     * クローラの最終実行日時をクリアします。
     * @throws JobExecuteException クリアに失敗した場合
     */
    protected void clearLastCrawlingDate() throws JobExecuteException {
        // 最終実行日時を更新
        updateLastCrawlingDate(new Date(0L));
    }

}