Hibernate search 導入
Hibernate search は 全文検索の機能を利用できるようにした JBoss Hibernate へのアドオン・コンポーネントである。全文検索機能へのアクセスには JBoss Hibernate の API とアノテーションを通じて行う[参考]。増田内科様が既に導入されているので,コードを使わせて頂いた。最初のインデックス作成にちょっと時間がかかるが,インデックスができてしまえば,後は全文検索が劇的に早くなる。インデックス作成は最初だけでよく,以降はデータ作成ごとに自動的に作成される。
約 30万件の ModuleModel の全文検索にかかった時間
元町皮ふ科方式 | Hibernate Search | |
---|---|---|
初期インデックス作成 | − | 3分50秒 |
検索にかかった時間 | 4分30秒 | 5 秒以下 |
- Hibernate search 3.1.1GA をダウンロードする
- Hibernate search の以下のファイルを OpenDolphin の lib フォルダに入れてライブラリに加える。同じファイルを JBoss サーバの $JBOSS_HOME/common/lib にも入れて JBoss を再起動する。
- hibernate-search-3.1.1.GA.jar
- lucene-analyzers-2.4.1.jar
- lucene-core-2.4.1.jar
- persistence.xml に以下の様にプロパティーを加える。下記の例では,/var/lucene/indexes にインデックスが作られる。
<property name="hibernate.search.default.directory_provider" value="org.hibernate.search.store.FSDirectoryProvider"/> <property name="hibernate.search.default.indexBase" value="/var/lucene/indexes"/>
- Hibernate Search 用のアノテーションを加える
infomodel/ModuleModel.java
@Entity @Indexed(index="module") // masuda @Table(name = "d_module") public class ModuleModel extends KarteEntryBean implements Stamp { private static final long serialVersionUID = -8781968977231876023L;; @Embedded private ModuleInfoBean moduleInfo; @Transient private IInfoModel model; @Lob // masuda @Field(index=Index.TOKENIZED) @FieldBridge(impl = ModuleModelBridge.class) @Analyzer(impl = CJKAnalyzer.class) // masuda @Column(nullable=false) private byte[] beanBytes;
- ModuleModel からインデックス用のキーワードを取り出すブリッジ
infomodel/ModuleModelBridge.java
package open.dolphin.infomodel; import java.beans.XMLDecoder; import java.io.BufferedInputStream; import java.io.ByteArrayInputStream; import org.hibernate.search.bridge.StringBridge; /** * ModuleModelのbeanBytesからテキストを取り出すブリッジ * * @author masuda, Masuda Naika */ public class ModuleModelBridge implements StringBridge { @Override public String objectToString(Object object) { byte[] beanBytes = (byte[]) object; InfoModel im = (InfoModel) xmlDecode(beanBytes); String text = ""; if (im instanceof ProgressCourse) { String xml = ((ProgressCourse) im).getFreeText(); text = extractText(xml); } else { text = im.toString(); } return text; } private String extractText(String xml) { StringBuilder sb = new StringBuilder(); String head[] = xml.split("
"); for (String str : head) { String tail[] = str.split(" "); if (tail.length == 2) { sb.append(tail[0].trim()); } } return sb.toString(); } private Object xmlDecode(byte[] bytes) { XMLDecoder d = new XMLDecoder( new BufferedInputStream( new ByteArrayInputStream(bytes))); return d.readObject(); } } - インデックス作成と検索の ejb
ejb/RemoteDocumentPeekerServiceImpl.java
/** * hibernate search のインデックスをクリアする */ public void clearIndex() { FullTextEntityManager fullTextEntityManager = Search.getFullTextEntityManager(em); fullTextEntityManager.purgeAll(ModuleModel.class); } /** * hibernate search のインデックスを作る - fromModuleId から toModuleId まで * @param fromModuleId * @param toModuleId */ public void makeInitialIndex(Long fromModuleId, Long toModuleId) { FullTextEntityManager fullTextEntityManager = Search.getFullTextEntityManager(em); List<ModuleModel> modules = getModuleModel(fromModuleId, toModuleId); for (ModuleModel mm : modules) { fullTextEntityManager.index(mm); } } /** * create and execute a search * @param text * @return */ public List<PatientModel> getPatientTextSearch2(String text) { FullTextEntityManager fullTextEntityManager = Search.getFullTextEntityManager(em); try { // create native Lucene query org.apache.lucene.queryParser.QueryParser parser = new QueryParser("beanBytes", new org.apache.lucene.analysis.cjk.CJKAnalyzer()); org.apache.lucene.search.Query luceneQuery = parser.parse(text); // wrap Lucene query in a javax.persistence.Query javax.persistence.Query persistenceQuery = fullTextEntityManager.createFullTextQuery(luceneQuery, ModuleModel.class); // execute search List<ModuleModel> result = persistenceQuery.getResultList(); // return result List<PatientModel> ret = new ArrayList(); for (ModuleModel mm : result) { PatientModel pm = mm.getKarte().getPatient(); if (!ret.contains(pm)) { ret.add(pm); } } return ret; } catch (ParseException ex) { ex.printStackTrace(); } return null; }
ejb/RemoteDocumentPeekerService.java
public interface RemoteDocumentPeekerService { ・ ・ public void clearIndex(); public void makeInitialIndex(Long fromModuleId, Long toModuleId); public List getPatientTextSearch2(String text); }
- Patient Search に組み込む
plugin/PatientSearchImpl.java
private void initComponents() { ・ ・ // HibernateSearchを使用するか masuda final JCheckBox cbUseHibernateSearch = view.getUseHibernateSearchCb(); cbUseHibernateSearch.setSelected(preferences.getBoolean(HIBERNATE_SEARCH, false)); cbUseHibernateSearch.addItemListener(new ItemListener() { @Override public void itemStateChanged(ItemEvent e) { preferences.putBoolean(HIBERNATE_SEARCH, cbUseHibernateSearch.isSelected()); } }); cbUseHibernateSearch.addMouseListener(new MouseAdapter() { @Override public void mousePressed(MouseEvent e) { maybePopup(e); } @Override public void mouseReleased(MouseEvent e) { maybePopup(e); } private void maybePopup(MouseEvent e) { if (e.isPopupTrigger() && cbUseHibernateSearch.isSelected() && e.isShiftDown()) { JPopupMenu popup = new JPopupMenu(); JMenuItem mi = new JMenuItem("インデックス作成"); popup.add(mi); mi.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { makeInitialIndex(); } }); popup.show(e.getComponent(), e.getX(), e.getY()); } } }); final JToggleButton rb_karteSearch = view.getKarteSearchBtn(); view.getUseHibernateSearchCb().setVisible(rb_karteSearch.isSelected()); rb_karteSearch.addItemListener(new ItemListener() { @Override public void itemStateChanged(ItemEvent e) { boolean b = rb_karteSearch.isSelected(); view.getUseHibernateSearchCb().setVisible(b); } }); } // Hibernate Search関連 masuda private void makeInitialIndex() { app = ClientContext.getApplicationContext().getApplication(); taskMonitor = ClientContext.getApplicationContext().getTaskMonitor(); IndexTask task = new IndexTask(app); MyInputBlocker blocker = new MyInputBlocker(task, getContext().getGlassPane(), keyBlocker); getContext().getGlassPane().setText("インデックス作成は時間がかかります。"); task.setInputBlocker(blocker); StatusMonitor bar = new StatusMonitor(task, taskMonitor, view.getProgressBar()); ClientContext.getApplicationContext().getTaskService().execute(task); } /** * Hibernate Search の index を作成する */ class IndexTask extends Task<Void, Void> { public IndexTask(Application app) { super(app); } @Override protected Void doInBackground() { logger.info("IndexTask started at " + new Date()); // progress bar 設定 String mess = "インデックス作成"; String note = "索引を作成中( 0% 完了)"; ProgressMonitor mon = new ProgressMonitor(view, mess, note, 0, 100); mon.setMillisToDecideToPopup(0); // この処理は絶対時間がかかるので,すぐ出す mon.setProgress(0); JDialog dialog = (JDialog) mon.getAccessibleContext().getAccessibleParent(); // 索引作成開始 DocumentPeekerDelegater dl = new DocumentPeekerDelegater(); boolean hasNext = dl.makeInitialIndex(0L, 5000L); // 0 から 5000 づつインデックス作成 while (hasNext) { // progress bar 表示 int ratio = (int) (100*(dl.fromModuleId)/dl.maxModuleId); mon.setProgress(ratio); mon.setNote("索引を作成中(" + ratio + "%完了)"); // キャンセルされた場合 if (!dialog.isVisible()) break; hasNext = dl.makeInitialIndex(); } mon.close(); return null; } @Override protected void succeeded(Void v) { logger.info("IndexTask completed at " + new Date()); } @Override protected void failed(Throwable cause) { logger.info("IndexTask failed at " + new Date()); //logger.info(cause.getCause()); //logger.info(cause.getMessage()); } @Override protected void cancelled() { logger.info("IndexTask cancelled at " + new Date()); } } ・ ・ class FindTask extends Task<Collection, Void> { ・ ・ protected Collection doInBackground() throws Exception { logger.debug("FindTask doInBackground"); Preferences preferences = Preferences.userNodeForPackage(this.getClass()); boolean inchiki = preferences.getBoolean(HIBERNATE_SEARCH, false); ・ ・ //pns -- 時間のかかるカルテ内検索 // カルテ内検索は時間がかかるので,moduleId で increaseStep ごと区切って検索 } else if (!inchiki) { ・ ・ } else { // Hibernate Search を使ったカルテ全文検索 // カルテ内検索をちょっとインチキする masuda DocumentPeekerDelegater dl = new DocumentPeekerDelegater(); result = new ArrayList<PatientModel>(); Collection pm = dl.getPatientOfKarte2(searchText); result.addAll(pm); } ・ ・