Apache Solr

 

solrjで独自のインデクサを実装する

solrjで独自のインデクサを実装してインデックス生成します。Data Import Request Handlerを使わずに独自のインデクサを実装します。RDBに保存しておいたデータを使ってインデキシングするサンプルです。仮型引数を使って汎用的なインデクサになるようにしています。

新サイト、tree-mapsを公開しました!!

tree-maps: 地図のWEB TOOLの事ならtree-mapsにお任せ!

地図に関するWEB TOOL専門サイトです!!

大画面で大量の緯度経度を一気にプロット、ジオコーディング、DMS<->DEGの相互変換等ができます!

◯ 広告

Data Import Request Handler

solrの標準機能で、xmlファイルやRDBからデータを取得し、インデクシングする機能です。

今回はData Import Request Handlerは使いません

Data Import Request Handlerを使わない理由は以下の通りです。(DataSourceがRDBの場合)

  • data-config.xmlに実装を書いてしまうので、IDEによるリファクタリング機能が効かない。
  • data-config.xmlの実装ルールを知っていないといけないので、実装できる人が限られる。
  • ORMによるタイプセーフな実装ができない。
  • data-config.xmlにはSQLしか書けない。インデックス生成がDBだけで完結するほど単純でない場合は多い。

Data Import Request Handlerを使わなくても問題無くインデクサは実装できるので、実装しましょう!

ポイントはインターフェースと実装を切り離し、複数のコアに対応できるようにすることです。

住所インデックスは生成できても、緯度経度インデックスは生成できないようなインデクサは好ましくありません。

今回はIteratorと仮型引数を使う事で共通処理を実現しています。

インデクサ(インデックス生成)はサーチャー(インデックス検索)とは別世界なので、頭を切り替えていきましょう。

以下の環境でサンプルを実装しています。

住所.jpの住所データをコアとして使います。

package tree.solr.search.address;

import org.apache.solr.client.solrj.beans.Field;

/**
 * 住所インデックスのドキュメントです。
 * @author tree
 */
public class AddressDocument {

    @Field(AddressDocumentNames.ID)
    public String id;

    @Field(AddressDocumentNames.ADDRESS_CD)
    public Integer addressCd;

    @Field(AddressDocumentNames.PREF_CD)
    public Integer prefCd;

    @Field(AddressDocumentNames.CITY_CD)
    public Integer cityCd;

    @Field(AddressDocumentNames.TOWN_AREA_CD)
    public Integer townAreaCd;

    @Field(AddressDocumentNames.ZIP_CD)
    public String zipCd;

    @Field(AddressDocumentNames.OFFICE_FLG)
    public Boolean officeFlg;

    @Field(AddressDocumentNames.ABOLITION_FLG)
    public Boolean abolitionFlg;

    @Field(AddressDocumentNames.PREF_NAME)
    public String prefName;

    @Field(AddressDocumentNames.PREF_NAME_KANA)
    public String prefNameKana;

    @Field(AddressDocumentNames.CITY_NAME)
    public String cityName;

    @Field(AddressDocumentNames.CITY_NAME_KANA)
    public String cityNameKana;

    @Field(AddressDocumentNames.TOWN_AREA_NAME)
    public String townAreaName;

    @Field(AddressDocumentNames.TOWN_AREA_NAME_KANA)
    public String townAreaNameKana;

    @Field(AddressDocumentNames.TOWN_AREA_SUPPLEMENT)
    public String townAreaSupplement;

    @Field(AddressDocumentNames.KYOTO_STREET_NAME)
    public String kyotoStreetName;

    @Field(AddressDocumentNames.VILLAGE_NAME)
    public String villageName;

    @Field(AddressDocumentNames.VILLAGE_NAME_KANA)
    public String villageNameKana;

    @Field(AddressDocumentNames.SUPPLEMENT)
    public String supplement;

    @Field(AddressDocumentNames.OFFICE_NAME)
    public String officeName;

    @Field(AddressDocumentNames.OFFICE_NAME_KANA)
    public String officeNameKana;

    @Field(AddressDocumentNames.OFFICE_ADDRESS)
    public String officeAddress;

    @Field(AddressDocumentNames.NEW_ADDRESS_CD)
    public Integer newAddressCd;

    @Field(AddressDocumentNames.LATITUDE)
    public Double latitude;

    @Field(AddressDocumentNames.LONGITUDE)
    public Double longitude;

}

AddressDocumentNamesの定数をここで使っています。

package tree.solr.search.address;

/**
 * 住所インデックスのフィールド名です。
 * @author tree
 */
public class AddressDocumentNames {

    // ========================================================================
    // Static field
    // ========================================================================
    public static final String ID = "id";

    // ========================================================================
    // Dynamic field
    // ========================================================================
    public static final String ADDRESS_CD = "address_cd_int";

    public static final String PREF_CD = "pref_cd_int";

    public static final String CITY_CD = "city_cd_int";

    public static final String TOWN_AREA_CD = "town_area_cd_int";

    public static final String ZIP_CD = "zip_cd_str";

    public static final String OFFICE_FLG = "office_flg_bool";

    public static final String ABOLITION_FLG = "abolition_flg_bool";

    public static final String PREF_NAME = "pref_name_str";

    public static final String PREF_NAME_KANA = "pref_name_kana_str";

    public static final String CITY_NAME = "city_name_str";

    public static final String CITY_NAME_KANA = "city_name_kana_str";

    public static final String TOWN_AREA_NAME = "town_area_name_str";

    public static final String TOWN_AREA_NAME_KANA = "town_area_name_kana_str";

    public static final String TOWN_AREA_SUPPLEMENT = "town_area_supplement_str";

    public static final String KYOTO_STREET_NAME = "kyoto_street_name_str";

    public static final String VILLAGE_NAME = "village_name_str";

    public static final String VILLAGE_NAME_KANA = "village_name_kana_str";

    public static final String SUPPLEMENT = "supplement_str";

    public static final String OFFICE_NAME = "office_name_str";

    public static final String OFFICE_NAME_KANA = "office_name_kana_str";

    public static final String OFFICE_ADDRESS = "office_address_str";

    public static final String NEW_ADDRESS_CD = "new_address_cd_int";

    public static final String LATITUDE = "latitude_tdouble";

    public static final String LONGITUDE = "longitude_tdouble";
}

Namesクラスはサーチャーでも使うので、定数にしておくと後々実装・保守が楽になります。

Dynamic Fieldを使っています。schema.xmlのサンプルは行数が多すぎるので割愛します。

package tree.solr.search.address;

import tree.solr.s2.entity.Address;

/**
 * 住所インデックスのドキュメントを生成するクラスです。
 * @author tree
 */
public interface AddressDocumentBuilder {

    /**
     * 住所データをドキュメントに変換します。
     * @param address 住所データ
     * @return 住所ドキュメント
     */
    AddressDocument toDocument(Address address);
}

ようやくインデクサの登場です。Iteratorインターフェースを無名クラスでOverrideして実装しています。

package tree.solr.search.address;

import java.util.Iterator;
import java.util.List;

import org.apache.commons.collections.CollectionUtils;
import org.seasar.framework.container.SingletonS2Container;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import tree.solr.s2.entity.Address;
import tree.solr.s2.service.AddressService;
import tree.solr.search.BaseIndexer;

/**
 * 住所インデクサです。
 * @author tree
 */
public class AddressIndexer extends BaseIndexer<AddressDocument> {

    private static Logger LOGGER = LoggerFactory.getLogger(AddressIndexer.class);

    private AddressService addressService;

    @Override
    public void setUp() {
        addressService = SingletonS2Container.getComponent(AddressService.class);
    }

    @Override
    public Iterator<AddressDocument> iterator() {

        return new Iterator<AddressDocument>() {

            private Iterator<Address> iterator;

            private static final int LOG_INTERVAL = 10000;

            private static final int SQL_SELECT_LIMIT = 1000;

            private int offset = 0;

            /**
             * インデクシングするデータを取得する
             */
            private void nextData() {
                if (iterator != null && iterator.hasNext())
                    return;
                List<Address> list = addressService.findAll(offset, SQL_SELECT_LIMIT);
                if (CollectionUtils.isEmpty(list))
                    return;
                iterator = list.iterator();
            }

            @Override
            public boolean hasNext() {
                nextData();
                boolean hasNext = iterator.hasNext();
                if (!hasNext)
                    LOGGER.info("インデックス{}件生成しました。", offset);
                return hasNext;
            }

            @Override
            public AddressDocument next() {
                offset++;
                if (offset % LOG_INTERVAL == 0)
                    LOGGER.info("インデックス{}件生成中です。", offset);
                return toDocument(iterator.next());
            }

            @Override
            public void remove() {
                throw new UnsupportedOperationException("未実装です。");
            }
        };
    }

    /**
     * 住所データをドキュメントに変換します。
     * @param address
     * @return
     */
    private AddressDocument toDocument(Address address) {
        return new AddressDocumentBuilder() {

            @Override
            public AddressDocument toDocument(Address address) {
                AddressDocument doc = new AddressDocument();
                doc.id = String.valueOf(address.addressCd);
                doc.addressCd = address.addressCd;
                doc.prefCd = address.prefCd;
                doc.cityCd = address.cityCd;
                doc.townAreaCd = address.townAreaCd;
                doc.zipCd = address.zipCd;
                doc.officeFlg = address.officeFlg;
                doc.abolitionFlg = address.abolitionFlg;
                doc.prefName = address.prefName;
                doc.prefNameKana = address.prefNameKana;
                doc.cityName = address.cityName;
                doc.cityNameKana = address.cityNameKana;
                doc.townAreaName = address.townAreaName;
                doc.townAreaNameKana = address.townAreaNameKana;
                doc.townAreaSupplement = address.townAreaSupplement;
                doc.kyotoStreetName = address.kyotoStreetName;
                doc.villageName = address.villageName;
                doc.villageNameKana = address.villageNameKana;
                doc.supplement = address.supplement;
                doc.officeName = address.officeName;
                doc.officeNameKana = address.officeNameKana;
                doc.officeAddress = address.officeAddress;
                doc.newAddressCd = address.newAddressCd;
                doc.latitude = address.latitude;
                doc.longitude = address.longitude;
                return doc;
            }

        }.toDocument(address);
    }
}

toDocumentメソッドで、Entity(DBのデータ)からDocument(Solrのデータ)に値を詰め替えています。

インデクサの実装自体はほとんどtoDocumentメソッドに集約されます

実際はDBのテーブルの構造とSolrのフィールドの構造は一致しないと思うので、ここで加工してDocumentを生成します。

package tree.solr.search;

import java.io.IOException;
import java.util.Iterator;
import java.util.List;

import org.apache.solr.client.solrj.SolrServerException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import tree.solr.exception.SolrException;

/**
 * インデクサ基底クラスです。
 * @author tree
 * @param <T>
 */
public abstract class BaseIndexer<T> extends BaseServer {

    private static Logger LOGGER = LoggerFactory.getLogger(BaseIndexer.class);

    /**
     * 1件インデクシングします。
     * @param t ドキュメント
     */
    private void indexing(T t) {
        try {
            // getSolrServer().addBean(t);
            SolrInputDocument document = getSolrServer().getBinder().toSolrInputDocument(t);
            getSolrServer().add(document);
        } catch (IOException e) {
            rollback();
            throw new SolrException(e);
        } catch (SolrServerException e) {
            rollback();
            throw new SolrException(e);
        }
    }

    /**
     * 複数件インデクシングします。
     * @param t ドキュメントのlist
     */
    private void indexing(List<T> t) {
        try {
            getSolrServer().addBeans(t);
        } catch (IOException e) {
            rollback();
            throw new SolrException(e);
        } catch (SolrServerException e) {
            rollback();
            throw new SolrException(e);
        }
    }

    /**
     * 1件ドキュメントを削除します。<br>
     * 固定フィールド「id」に対する値を指定して下さい。
     * @param filedValue idの値
     */
    private void delete(String filedValue) {
        try {
            getSolrServer().deleteByQuery("id:" + filedValue);
        } catch (SolrServerException e) {
            rollback();
            throw new SolrException(e);
        } catch (IOException e) {
            rollback();
            throw new SolrException(e);
        }
    }

    /**
     * ドキュメントを全削除します。
     */
    private void deleteAll() {
        try {
            getSolrServer().deleteByQuery("*:*");
        } catch (SolrServerException e) {
            rollback();
            throw new SolrException(e);
        } catch (IOException e) {
            rollback();
            throw new SolrException(e);
        }
    }

    /**
     * ロールバックします。
     */
    private void rollback() {
        try {
            getSolrServer().rollback();
        } catch (SolrServerException e) {
            throw new SolrException(e);
        } catch (IOException e) {
            throw new SolrException(e);
        }
    }

    /**
     * コミットします。
     */
    private void commit() {
        try {
            getSolrServer().commit();
        } catch (SolrServerException e) {
            throw new SolrException(e);
        } catch (IOException e) {
            throw new SolrException(e);
        }
    }

    /**
     * オプティマイズします。
     */
    private void optimize() {
        try {
            getSolrServer().optimize();
        } catch (SolrServerException e) {
            throw new SolrException(e);
        } catch (IOException e) {
            throw new SolrException(e);
        }
    }

    /**
     * インデックスを全件生成します。
     */
    public void fullImport() {
        ping();
        try {
            setUp();
            deleteAll();
            Iterator<T> it = iterator();
            while (it.hasNext())
                indexing(it.next());
            commit();
            optimize();
        } catch (Exception e) {
            LOGGER.error(e.getMessage(), e);
            rollback();
        } finally {
            tearDown();
        }
    }

    /**
     * インデックスを差分更新します。
     * @param id idの値
     */
    public void deltaImport(String id) {
        ping();
        try {
            setUp();
            delete(id);
            Iterator<T> it = iterator();
            while (it.hasNext())
                indexing(it.next());
            commit();
            optimize();
        } catch (Exception e) {
            LOGGER.error(e.getMessage(), e);
            rollback();
        } finally {
            tearDown();
        }
    }

    /**
     * Iteratorを取得します。
     * @return Iterator
     */
    public abstract Iterator<T> iterator();

    /**
     * 処理前に実行するメソッドです。必要な場合はoverrideして下さい。
     */
    public void setUp() {
    }

    /**
     * 処理後に実行するメソッドです。必要な場合はoverrideして下さい。
     */
    public void tearDown() {
    }
}

コアが複数ある場合の実装の違いはクラスの仮型引数<T>Iteratorで吸収しています。

抽象メソッドを軸に実装しているため、コアが変わってもBaseIndexerの実装は変わりません。

バッチクラスからは「fullImport」または「deltaImport」を呼び出してインデックス生成します。

package tree.solr.search;

import java.io.IOException;

import org.apache.solr.client.solrj.SolrServer;
import org.apache.solr.client.solrj.SolrServerException;

import tree.solr.exception.SolrException;

/**
 * SolrServer基底クラスです。
 * @author tree
 */
public abstract class BaseServer {

    private SolrServer server;

    /**
     * SolrServerを設定します。<br>
     * solr.diconでインジェクションするためのメソッドです。
     * @param server SolrServer
     */
    public void setSolrServer(SolrServer server) {
        this.server = server;
    }

    /**
     * SolrServerを取得します。
     * @return
     */
    protected SolrServer getSolrServer() {
        return server;
    }

    /**
     * SolrServerにpingします。
     */
    protected void ping() {
        try {
            server.ping();
        } catch (SolrServerException e) {
            throw new SolrException(e);
        } catch (IOException e) {
            throw new SolrException(e);
        }
    }
}
◯ 広告