読者です 読者をやめる 読者になる 読者になる

DjangoでHyper Estraierを使う

Django Python

 前回のエントリで紹介した、django.dispatchの使用例を紹介します。
 id:uemuさんのRandomNoteを自分でもやってみているのですが、これを題材に、Hyper Estraierを使ってRandomNoteの全文検索ができるようにしてみます。

search-apiブランチについて少々

 Djangoでの全文検索というと、個人的に期待している(していた)search-apiという名称でブランチがあります。これは、django.contrib.searchモジュールとして、Lucene, Xapian, HyperEstraierといった全文検索エンジン向けの抽象化レイヤーを提供しようという試みです。


Text indexing abstraction layer
http://code.djangoproject.com/wiki/TextIndexingAbstractionLayer


そのソースコード(root/django/branches/search-api)
http://code.djangoproject.com/browser/django/branches/search-api


 だいぶ前にちらっと眺めた記憶があり、Django勉強会のポジション・ペーパーを書いているときに思い出して、「search-apiブランチってどうなってるの?」と書いたのですが、今、あらめて確認してみると、最終チェックインが三ヶ月前、つまり初めて見たときから、まったく進行していないことに気がつきました。


 コードをざっと眺めた感じでは、チェックインされているコードも動作しそうもないし、不思議なブランチです。端的に言うと、死んでます。関係ないですけど、進行が止まっているプロジェクトを見るたびに、Monty PythonのDead Parrotスケッチを思い出すのは、僕だけでしょうか。"No, No, it's resting!!!"

RandomNoteに全文検索

 閑話休題。今回紹介する独自の解決案では、モデルオブジェクトの保存後(post_save), 削除前(pre_delete)に発生するシグナルを捕捉して、Hyper Estraierへの登録を行うようにしています。
 RandomNoteについてはuemuさんのブログにスクリーンキャスト付きで詳しく解説されているので、そちらを参照してください。今回は、汎用ビューを使って、CRUDを行えるようになった状態からスタートします。


Django再入門 RandomNoteを作る vol.1 下準備
http://ash1no0to.dyndns.org/htdocs/archives/2006/10/djangorandomnot.html#more


Django再入門 RandomNoteを作る vol.2 汎用ビュー(Generic.view)
http://ash1no0to.dyndns.org/htdocs/archives/2006/11/django_randomno2.html


 まず、モデルのpost_save, post_deleteシグナルの通知を受け取るように、myproject.randomnote.models.pyを変更します。

# -*- coding: utf-8 -*-
from django.dispatch import dispatcher
import myproject.randomnote.receivers

from django.db import models

class Page(models.Model):
    body = models.TextField(blank=False)
   
    def __str__(self):
        return self.body[:20]
       
    class Admin:
        pass

    def get_absolute_url(self):
        return "/randomnote/detail/%s/" % self.id

# ページがアップデートされた時に、Hyper Estraierの索引を更新する
dispatcher.connect(receiver = myproject.randomnote.receivers.post_save,
                   signal = models.signals.post_save,
                   sender = Page)

# ページが削除されたとき索引から削除
dispatcher.connect(receiver = myproject.randomnote.receivers.pre_delete,
                   signal = models.signals.pre_delete,
                   sender = Page)


 基本的にuemuさんのコードと同じですが、先頭と末尾に、シグナルの登録の処理を追加しています。
 注意すべき点は、索引を新規作成・変更するのはモデルが保存された後(post_save)、索引を削除するのはモデルが削除される前(pre_delete)でなくてはならないということです。逆にすると、モデルの主キーが採れないので、Estraierに正しい文書URIを渡すことができなくなってしまいます。

索引を更新する

 PythonHyper EstraierのノードAPIを使うために、やっつけ仕事ですが、ライブラリを用意しました。
 実際に試される方は、ここから最新版をダウンロードして、インストールして下さい。

$ tar xzf estraierpure-0.1.tar.gz
$ cd estraierpure-0.1
$ python setup.py install

 または、easy_installで

$ easy_install http://www.geocities.co.jp/ndpcw007/perezvon/estraierpure-0.1.tar.gz


 または、こんなものインストールしたくないという方は、このtar.gzに含まれているestraierpureディレクトリをRandomNoteのプロジェクト・ディレクトリに次のように配置すれば動作します。



 次に、randomnote/receivers.pyというモジュールを作成し、シグナルを受け取って索引の更新、削除をおこなう処理を記述しました。

# -*- coding: utf-8 -*-
from estraierpure import Node, Document

# Hyper Estraierのノードです
node = Node(name='node1', user='django', password='django')

# 文書の完全なURIを取得します。
# 実際にはdjango.contrib.sitesなどを使って、アプリケーションの
# 本番環境でのドメインを取得し、その文書が公開されている完全なURIを生成する必要があるでしょう。
def get_uri(page):
    return 'http://localhost:8000%s' % page.get_absolute_url()

def post_save(signal, sender, instance):
    try:
        # Hyper Estraier文書を作成
        doc = Document()
        # テキストは、RandomNoteの本文
        doc.text = instance.body
        # Hyper Estraier文書の属性を設定
        doc.attrib.update({
            'uri'   : get_uri(instance),
            'title' : str(instance).replace('\r', '').replace('\n', '')
        })
        # 登録
        node.put_doc(doc)
    except:
        pass

def pre_delete(signal, sender, instance):
    try:
        # URIに基づいて文書の索引を消去
        node.out_doc_by_uri(get_uri(instance))
    except:
        pass


 post_save, pre_deleteがモデルの保存/削除時に呼び出される関数で、第三引数instanceに、モデルのインスタンスが渡されます。
 例外を潰しているのは、今後のToDoということで。とはいっても、実際に例外処理を行うとしても、この場合は失敗ログを記録するといった処理内容になるでしょう。「全文検索の索引を更新できなかったから、メインの処理を中断させる」といったことはないでしょうから。


 最後に、Hyper Estraierノードマスターを起動し、djangoというユーザと、node1というノードを追加しました。この辺りの解説は、Hyper Estraierのユーザーガイドを参照して下さい。


Hyper Estraier P2P Guide
http://hyperestraier.sourceforge.net/nguide-ja.html#tutorial


 これで、RandomNote本文を、Hyper Estraierの索引に登録できるようになりました。早速、新規ページの作成画面から、何か登録してみます。



 モデルが保存された時点で、post_saveシグナルが発生し、Hyper Estraierの索引も更新されているはずです。Hyper Estraierの管理画面で確認してみます。




 無事、登録されているようです。文書URIにDjangoの生成したURIを使っているので、検索結果画面のリンクをクリックすると、RandomNoteの詳細画面に移動します。



 ページの編集や削除をすると、同時にHyper Estraierの索引も更新されます。

さらに先に

 これは、単にdjango.dispatchを使ってみたい、DjangoでHyper Estraierを使いたいという動機で、かなり書いた短期間で作ったアプリケーションなので、いろいろ不満はあります。

  • 検索、結果表示もDjangoから行いたい。
  • search-apiブランチを使うにせよ、使わないにせよ、何らかの抽象化レイヤーは必要でしょう。
  • MetaやAdminのように、モデル・クラスにネストしたクラスとして、SearchMeta(仮名)のようなクラスを用意すれば、全文検索、索引の維持を自動化することができるかな?


 また、ここで使っている自作Hyper Estraierライブラリも相当甘く、そもそも検索を行えないので、これもかなり手を入れなくてはなりません。


 今回は、単にdjango.dispatchを使ってみたいということが動機としてあったので、深追いはしていません。もう少し進展があれば、Django本家の方にも、Hyper Estraierの方にも、フィードバックできるようにしたいです。