Pythonで全文検索を実装してみた

JavaScript でやってるのを見かけたので。

 cf. JavaScriptで全文検索(N-gram)を実装してみる! – Simple is Beautiful.

アルゴリズムは N-gram っていう方法のうちでも簡単な uni-gram っていうみたい。詳しくはリンク先の記事を見て。

記事の説明を読んで、なんとなく理解したので Python で書いてみた。リンク先の JavaScript の実装はファイル名を参考にしたくらいで、ちゃんと読んでない。

できたのはこんな感じ。

  • create_index.py でインデックスを作って、search.py で検索
  • 検索対象のファイルはテキストファイルのみ。documents ディレクトリに入ってる
  • インデックスファイルは indexes ディレクトリ内につくる
  • document ID とファイル名の対応は docs.json ファイル

インデックスを作る create_index.py

from unigram import document
import json
import os


DOC_DIR = 'documents'
INDEX_DIR = 'indexes'
DOC_DATA = 'docs.json'


files = [os.path.join(DOC_DIR, f) for f in os.listdir(DOC_DIR)]
docs = {}
doc_id = 0
for file in files:
    with open(file, 'r') as f:
        text = f.read()
        tokens = document.tokenize(text)
        index = document.classify(tokens)
        document.save_index(index, doc_id, INDEX_DIR)
    docs[str(doc_id)] = {'name': os.path.basename(file), 'path': file}
    doc_id += 1

with open(DOC_DATA, 'w') as f:
    json.dump(docs, f, indent=2)

検索コマンド search.py

from unigram import document
import json
import os
import sys


INDEX_DIR = 'indexes'
DOC_DATA = 'docs.json'


string = sys.argv[1]

with open(DOC_DATA, 'r') as f:
    docs = json.load(f)
fcount = len(docs)

index_files = list(map(lambda x: os.path.join(INDEX_DIR, x), os.listdir(INDEX_DIR)))
index = {}
for file in index_files:
    c = chr(int(os.path.basename(file).replace('.index', '')))
    with open(file, 'r') as f:
        index[c] = document.parse_index(f.read())

m = list(map(lambda x: index[x], list(string)))

for i in range(fcount):
    doc_id = str(i)
    if not all(map(lambda x: doc_id in x.keys(), m)):
        continue
    n = list(map(lambda x: x[doc_id], m))
    s = set(n[0])
    for s1 in n[1:]:
        s = set(list(map(lambda x: x + 1, s)))
        s = s & set(s1)
    if len(s) > 0:
        pos = list(map(lambda x: x - len(string) + 1, s))
        pos.sort()
        print(docs[doc_id]['name'], pos)

両方から使うモジュール unigram/document.py

import os


def tokenize(text):
    return list(text)


def classify(token_list):
    tokens = {}
    pos = 0
    for t in token_list:
        if not t in tokens:
            tokens[t] = []
        tokens[t].append(pos)
        pos += 1
    return tokens


def save_index(index, doc_id, index_dir):
    for c, idx in index.items():
        l = str(doc_id) + ':' + ','.join(list(map(lambda x: str(x), idx))) + '\n'
        with open(os.path.join(index_dir, str(ord(c)) + '.index'), 'a') as f:
            f.write(l)


def parse_index(content):
    l = content.split('\n')
    l.pop()
    index = {}
    for l2 in l:
        a = l2.split(':')
        index[a[0]] = [int(x) for x in a[1].split(',')]
    return index

とにかくまずは動くものを、ってことで作ったので、コードが整理されてないのは目を瞑ってほしい(後で直す)。

検索対象ファイルのサンプルには、別のプロジェクトで書いた Ruby のソースファイル。

takatoh@apostrophe $ ls documents
Gemfile       Rakefile  boot.rb    config.yaml
Gemfile.lock  app.rb    config.ru  config.yaml.example

インデックスを作る。

takatoh@apostrophe $ python create_index.py

できたインデックスファイルがこれ。

takatoh@apostrophe $ ls indexes
10.index   111.index  123.index  44.index  58.index  71.index  84.index
100.index  112.index  124.index  45.index  60.index  72.index  85.index
101.index  113.index  125.index  46.index  61.index  73.index  87.index
102.index  114.index  126.index  47.index  62.index  74.index  89.index
103.index  115.index  32.index   48.index  63.index  75.index  91.index
104.index  116.index  34.index   49.index  64.index  76.index  93.index
105.index  117.index  35.index   50.index  65.index  77.index  95.index
106.index  118.index  36.index   51.index  66.index  78.index  97.index
107.index  119.index  39.index   52.index  67.index  79.index  98.index
108.index  120.index  40.index   53.index  68.index  80.index  99.index
109.index  121.index  41.index   55.index  69.index  82.index
110.index  122.index  43.index   56.index  70.index  83.index

「require」という文字列を検索してみる。ファイル名と出現位置(のリスト)が出力される。

takatoh@apostrophe $ python search.py require
boot.rb [0, 17]
Rakefile [0, 15, 32, 71]
config.ru [0]
app.rb [0, 23, 40, 56, 71]

大丈夫そうだ。

MyDNSに独自ドメインを設定してみる

マイIPが遅いので、何か別の方法はないかと調べていたところ、別のサーバで使っている MyDNS が独自ドメインも使えるということが分かった。

MyDNS を使っているのは、WiMAX でインターネットに接続してるサーバだ。WiMAX はグローバルアドレスを付与してくれるサービスがあるけど、そのアドレスは固定ではないので DDNS の MyDNS を使っていたわけ。で、これを使い始めた時には、無料で使えるのは MyDNS のサブドメインだけだと思ったんだけど、いま調べたら独自ドメインでも無料で使えるようだ。早速試してみよう。

MyDNS のドメイン登録情報を変更する

独自ドメインのホスト名を仮に server3.panicblanket.com としよう。

MyDNS にログインして、DOMAIN INFO のページに移動する。このページで Domain、MX、Hostname を新しいホスト名に合わせて書き換えて、変更を反映させる。

つぎに、サーバのほうで、IP アドレスを通知するスクリプトを実行して、実際に使っているアドレスを通知する。

さくらインターネットでゾーン編集

ドメイン自体はさくらインターネットで管理しているので、server3.panicblanket.com だけ、MyDNS のネームサーバを参照するように編集する。

具体的には、ゾーン編集の画面でつぎのように設定する。

  • エントリ名: server3.panicblanket.com
  • 種別: NS
  • 値: ns0.mydns.jp.

ns0.mydns.jp は MyDNS のネームサーバ。最後に . (ドット)をつけるのを忘れないこと。server3.panicblanket.com の名前解決には ns0.mydns.jp を参照しろってことだな。

さて、これで設定は完了のはず。

結果

新しい名前で ping もとおるし、ssh での接続もできるようになった。大丈夫のようだ。

マイIPは遅い

先日、インターリンク社のマイIPサービスを使って固定IPアドレスを割り当てたわけだけど、はっきり言って通信速度が遅い。どのくらい遅いか、マイIPを使わない場合と比べてみた。

  • マイIPなし: 770Mbps,アップロード 560Mbps
  • マイIPあり: 5.9Mbps、 アップドード 5.6Mbps

通信速度は fast.com ってページで測った。マイIP経由だと約1/100だ。何度か測ってみたけどだいたいこのオーダー。これじゃせっかくの NURO光回線(下り2Gbps,上り1Gbps)がもったいない。

そりゃもちろん、VPN を介してインターリンク社のネットワークを経由するんだから、その分遅くなるのは承知してたけど、これほどとは。これは何かほかの方法を考えるかなぁ。

[追記]

インターリンクの FAQ に通信速度について書いてあった。曰く:

実際の通信速度は弊社のマイIPサーバーとお客様のPCとの間の接続環境によって変化します。
弊社ではおよそ 2Mbps~4Mbpsと推測しております。

マイIPサービスの通信速度はどれくらいですか? – インターリンク FAQ

だいたい実測通りだ。さきに言ってよ……(いや、見つけられなかったオレが悪いんだけどさ)。

インターリンクのマイIPサービスでCentOS8マシンに固定IPアドレスを割り当てる

インターリンクのマイIPサービス

NURO光は固定IPアドレスを割り当てるサービスを提供していないので、インターリンク社のマイIPというサービスを使って固定IPアドレスを割り当てる。

 cf. INTERLINK

上のページから、マイIPサービスに申し込んだ。申し込み自体は何も難しくはない。クレジット払いにしたのですぐに利用できるようになる。

問題はここから。マイIPサービスは、VPN を使って PC とインターリンクのサーバを繋ぎ、PC の ppp0 インターフェイスに固定IPアドレスを割り当てるようになっている(ようだ)。そのためには PPTP クライアントの設定が必要なんだけど、これでハマった。

環境

まずは環境を整理しておこう。

  • OS: CentOS 8.0
  • マイIPに必要な情報は取得済み

設定作業(1)

インターリンクの公開しているマニュアルに沿って作業する。

 cf. https://faq.interlink.or.jp/faq2/View/wcDisplayContent.aspx?id=501

マイIP接続ツールをダウンロードして /etc 以下に展開。

[takatoh@rollo ~]$ curl -O https://www.interlink.or.jp/support/vpn/myip/myiptools/myiptools.tar.gz
[takatoh@rollo ~]$ sudo tar xvzf myiptools.tar.gz -C /etc

設定ファイル /etc/myip/myip.conf を自分の接続情報に合わせて編集。

[takatoh@rollo ~]$ sudo vim /etc/myip/myip.conf

初期設定スクリプトを実行。

[takatoh@rollo ~]$ sudo /etc/myip/myip-setup

ここでエラーが起きた。メッセージを抜粋する。

一致した引数がありません: pptp-setup
エラー: 一致するものが見つかりません
/etc/myip/myip-setup: 行 111: pptpsetup: コマンドが見つかりません
/etc/myip/myip-setup: 行 117: /etc/ppp/peers/myip_miXXXXXX: そのようなファイルやディレクトリはありません

myip_miXXXXXX っていうファイルは自分用の設定ファイル(実際には miXXXXXX の部分に自分のログインIDが入る)。

初期設定スクリプト myip-setup の中を覗いてみて分かったのは、pptp-setup っていうパッケージをインストールしようとしているけどそれが失敗しているということと、そのせいか pptpsetup コマンドが見つからないようだ、ってことだ。

じゃあ、手動で pptp-setup パッケージをインストールしてやればいいかというと、そんなパッケージは無いらしい。

[takatoh@rollo ~]$ dnf search pptp-setup
一致する項目はありませんでした。

というところまでが昨日の話。

設定作業(2)

今日になっていろいろググってみたところ、PPTP Client の公式サイトを見つけた。

 cf. http://pptpclient.sourceforge.net/

このページの Download セクションには、pptp には pptpsetup が含まれている、と書いてある。

pptp, the helper program used by pppd to make a tunnel, which also includes pptpsetup,

なら、ここからダウンロードして pptpsetup をコピーしてやったらいいんじゃないか。というわけで早速ダウンローとして展開。するとたしかに含まれていた。

[takatoh@rollo ~]$ wget https://sourceforge.net/projects/pptpclient/files/pptp/pptp-1.10.0/pptp-1.10.0.tar.gz
[takatoh@rollo ~]$ tar xzvf pptp-1.10.0.tar.gz
[takatoh@rollo ~]$ ls pptp-1.10.0
AUTHORS            dirutil.c        pptp_compat.h   routing.c
COPYING            dirutil.h        pptp_ctrl.c     routing.h
ChangeLog          options.pptp     pptp_ctrl.h     test-redirections.c
DEVELOPERS         orckit_quirks.c  pptp_gre.c      test-redirections.h
Documentation      orckit_quirks.h  pptp_gre.h      util.c
INSTALL            ppp_fcs.c        pptp_msg.h      util.h
Makefile           ppp_fcs.h        pptp_options.h  vector.c
NEWS               pptp.8           pptp_quirks.c   vector.h
PROTOCOL-SECURITY  pptp.c           pptp_quirks.h   vector_test.c
README             pptp_callmgr.c   pptpsetup       version.c
TODO               pptp_callmgr.h   pqueue.c        version.h
USING              pptp_compat.c    pqueue.h

この pptpsetup、中を覗いてみると Perl のスクリプトのようだ。/usr/sbin にコピーして実行権限をつけてやる。

[takatoh@rollo ~]$ sudo cp pptp-1.10.0/pptpsetup /usr/sbin
[takatoh@rollo ~]$ sudo chmod 755 /usr/sbin/pptpsetup

さあ、これでどうだろう。初期設定スクリプトを実行。

[takatoh@rollo ~]$ sudo /etc/myip/myip-setup
CentOS-8 - AppStream                            2.0 kB/s | 4.3 kB     00:02    
CentOS-8 - Base                                 3.2 kB/s | 3.9 kB     00:01    
CentOS-8 - Extras                               673  B/s | 1.5 kB     00:02    
Package gawk-4.2.1-1.el8.x86_64 is already installed.
Package sed-4.5-1.el8.x86_64 is already installed.
依存関係が解決しました。
行うべきことはありません。
完了しました!
/sbin/pptpsetup: can't write to '/etc/ppp/peers/myip_miXXXXXX': No such file or directory
/etc/myip/myip-setup: 行 117: /etc/ppp/peers/myip_miXXXXXX: そのようなファイルやディレクトリはありません
done.

pptpsetup コマンドは実行できたようだけど、ファイルが書き込めないといっている。調べてみると /etc/ppp/peers ディレクトリが無いようなので作ってやる。

[takatoh@rollo ~]$ sudo mkdir /etc/ppp/peers

こんどはどうだ。

[takatoh@rollo ~]$ sudo /etc/myip/myip-setup
メタデータの期限切れの最終確認: 0:01:04 時間前の 2019年12月15日 10時20分25秒 に実施しました。
Package gawk-4.2.1-1.el8.x86_64 is already installed.
Package sed-4.5-1.el8.x86_64 is already installed.
依存関係が解決しました。
行うべきことはありません。
完了しました!
done.

エラーなし。うまくいったようだ。これでやっと次にすすめる。

設定作業(3)

マニュアルによると:

PPTP接続に必要なTCPポート1723およびGREプロトコルを通過できるようにしておく必要があります。

と書いてあるけど、やり方が書いてない。ポートの開け方はわかる。

[takatoh@rollo ~]$ sudo firewall-cmd --zone=public --add-port=1723/tcp --permanent

GREプロトコルの方はググったら↓のページを見つけた。

 cf. https://www.vultr.com/docs/how-to-establish-a-gre-tunnel-between-two-centos-7-servers
 cf. https://github.com/firewalld/firewalld/issues/30

ip_gre モジュールをロードする。

[takatoh@rollo ~]$ modprobe ip_gre

GRE プロトコルを通過させるのは firewall-cmd でできるようだ。

[takatoh@rollo ~]$ sudo firewall-cmd --zone=public --add-rich-rule='rule protocol value="47" accept' --permanent

さあ、これで準備は整ったはずだ。

接続開始

myip-up コマンドで接続を開始する。

[takatoh@rollo ~]$ sudo /etc/myip/myip-up
Using interface ppp0
Connect: ppp0 <--> /dev/pts/1
CHAP authentication succeeded
MPPE 128-bit stateless compression enabled
local  IP address 202.241.129.128
remote IP address 202.241.128.1
private IP mode.
done.

ifconfig コマンドの出力(下はその抜粋)をみると、ppp0 インターフェイスに固定IPアドレスが割り振られているのがわかる。

ppp0: flags=4305  mtu 1346
        inet 202.241.129.128  netmask 255.255.255.255  destination 202.241.128.1
        ppp  txqueuelen 3  (Point-to-Point Protocol)
        RX packets 7  bytes 100 (100.0 B)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 7  bytes 134 (134.0 B)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

ちなみに、接続を終了するには /etc/myip/myip-down コマンドを実行する。

確認

別のネットワークに繋がってる Windows マシンから、http://202.241.129.128/hello.html にアクセスしたところ、ちゃんと「Hello, this is rollo.」と表示されるのを確認した(rollo ってのは VPN 接続したホストの名前)。

これにて無事完了。

[追記 12/16]

一昨日(12/14)、スクリプトがエラーになって先に進めなくなったところで、インターリンク社に問い合わせをしたんだけど、今日になってその回答が来た。曰く:

大変申し訳ございませんが、マイIPはCentOS 8.0に対応しておりません。

……まぁ、予想はしてたけどね。リリースされたばっかりのディストリビューションだしね。

それに、パッケージが無かったりすりのはディストリビューションの方の問題であって、インターリンク社が悪いわけではない。

ともかく、CentOS 8.0 でマイIPサービスを使おうという人がいたら、上に書いた方法でうまくいったということを参考にしてもらおう。

NURO光が開通した

昨日のことなんだけど、時間がとれなかったので今日書く。

回線の工事をやってもらって、NURO光が開通した。工事は宅内工事と屋外工事が必要なんだけど、1日でやってもらった(追加工事費がかかる)。宅内工事は午前中に、屋外工事は午後4時ころには終わった。

で、設置された ONU というルータみたいな機器に、apostrophe と rollo の2台を有線で繋いで試してみた。結果、2台のコンピュータ同士も通信できるし、もちろんインターネットにも接続できる。まずは大丈夫のようだ。この記事もNURO光で接続して書いてる。

ネットワークアドレスを変更

デフォルトではネットワークアドレスが 192.168.1.0/24 になってるんだけど、これでは既存のネットワークとかぶるので、192.168.2.0/24 に変更する。

ONU は web 画面で設定できるようになっているので、http://192.168.1.1 にブラウザでアクセスしてログイン。ONU 自身のアドレスを始め、DHCP で割り当てるアドレスの範囲、デフォルトゲートウェイなどを 192.168.2.0/24 になるように変更した。これで電源を入れなおせばひとまずは完了。

あとは、web サーバをインターネットに公開するために、固定IPアドレスの設定なんかをやってたんだけどなかなかうまくいかずに昨日は時間切れ。というわけで今日書いてる次第。これについては記事を分ける。

Samba がこわれた

ローカルネットワーク上に作った Samba サーバがこわれた。先週の土曜日の話で、細かいことは覚えてないのでおおまかに書く。

最初に書いておくと、この Samba サーバは、ホスト名を wplj といい、OS は Ubuntu 16.04。

事の発端は Samba サーバにユーザを限定した共有フォルダを作ろうとしたことだ。下のページあたりを参考に /etc/samba/smb.conf ファイルに追記して samba サービスをリスタートしたところ、エラーになった。

 cf. アクセス権付の共有フォルダ作成 – Server World

追記した部分を削除してリスタートしてもエラーになる。仕方がないので Samba 自体をいったん削除してインストールしなおしても同じだった。再インストール後は最低限の設定だけで、共有フォルダを作らなくてもエラーになるんだから訳がわからない。設定自体は、さっきまで動いていた設定ファイル(バックアップしておいた)からコピペしたんだから間違ってないはずだ。どういうわけさ。

Samba サーバは Windows マシンのデータバックアップ用で、データ自体は残っているので大丈夫といえば大丈夫なんだけど、Windows からアクセスできなくなったのは不便だ。どうしようか。

[追記]

とりあえず、データバックアップ用サーバの nightschool にデータを移動して、Samba サーバを立てた。nightschool は CentOS 7.7 だけど、すんなりといった。何が違うんだ。

とにかく、データに Windows からもアクセスできるようにはなったので良しとする。上に書いたように Samba は Windows マシンのデータバックアップ用途だったので、そういう意味では nightschool にまとまってよかったのかも。

ユーザを限定した共有フォルダを作る件についてはまた今度。