Pythonでコマンドラインで全文検索(whoosh)

# 概要
今までログファイルからパターンファイルを使用して目的の行を抽出するときは`grep -f`を使用していました。
今回対象のログとパターンが多く、メモリ不足で遅かったためコマンドラインツールの作成練習もかねて
全文検索のツールを作ろうと思いました。
使用言語はpythonです。

## 0.開発環境
```shell-session
$ python --version
Python 3.6.1
```

## 1.PipEnvをインストール
pipenvの練習もかねてpipenvを使用して仮想環境を構築します。

```shell-session
$ pip install pipenv
```
また、プロジェクト直下に`.venv`を作りたいので環境変数に`PIPENV_VENV_IN_PROJECT=true`を設定しておきます。
PyCharmを使っている人は`.venv`がプロジェクト直下にあるとインタプリタの設定がしやすいと思います。

## 2.仮想環境の構築
```shell-session
$ pipenv install

$ ls -a
Pipfile  Pipfile.lock  .venv
```

## 3.ライブラリのインストール
今回は全文検索に`whoosh`を使用します。
また、コマンドラインアプリとして使用したいので`fire`も入れます

```shell-session
$ pipenv install whoosh
$ pipenv install fire
```

## 4.コードを書いてみる
```python

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import os
import whoosh.index as index
import whoosh.qparser as qparser
import whoosh.fields as fields
import glob
import fire

"""A simple full text search example Fire CLI.
Example usage:
pipenv run fts.py create_index
pipenv run fts.py search pattern.txt
"""


def create_index(index_dir_name="./index", log_file_name="./logs/*.log"):
    ix = get_index(index_dir_name)
    writer = ix.writer()
    for abspath in list_abspath(log_file_name):
        with open(abspath, "r") as fp:
            for line in fp:
                writer.add_document(path=abspath, body=line)
    writer.commit()
    ix.close()


def search(pattern_file_name, index_dir_name="./index"):
    ix = get_index(index_dir_name)
    searcher = ix.searcher()
    query = qparser.QueryParser("body", schema=ix.schema)
    with open(pattern_file_name, "r") as fp:
        for line in fp:
            for search_result in searcher.search(query.parse(line)):
                print(search_result["path"], search_result["body"], end="")
    ix.close()


def get_index(idx):
    if os.path.exists(idx):
        ix = index.open_dir(idx)
    else:
        schema = fields.Schema(
            path=fields.ID(stored=True),
            body=fields.NGRAM(stored=True)
        )
        os.mkdir(idx)
        ix = index.create_in(idx, schema)
    return ix


def list_abspath(path):
    return [os.path.abspath(p) for p in glob.glob(path)]


def main():
    fire.Fire(name='fts')


if __name__ == '__main__':
    main()


```

## 5.ログファイルと検索パターンファイルを準備する
ログっぽいファイルを用意します。

```shell-session
$ ls ./logs
01.log  02.log

$ cat ./logs/01.log
2018-04-20 06:52:57,613 [main] INFO  (Task#begin():176) -
2018-04-20 06:52:57,614 [main] INFO  (Task#begin():177) -
2018-04-20 06:52:57,721 [main] INFO  (Task#begin():185) -
2018-04-20 06:52:57,722 [main] INFO  (Task#begin():186) -
2018-04-20 06:52:57,723 [main] INFO  (Task#begin():176) -
2018-04-20 06:52:57,724 [main] INFO  (Task#begin():177) -
2018-04-20 06:52:57,725 [main] INFO  (Task#begin():185) -
2018-04-20 06:52:57,736 [main] INFO  (Task#begin():186) -
2018-04-20 06:52:57,737 [main] ERROR  (Task#begin():176) -
2018-04-20 06:52:57,738 [main] INFO  (Task#begin():177) -
2018-04-20 06:52:57,739 [main] INFO  (Task#begin():185) -
2018-04-20 06:52:57,753 [main] INFO  (Task#begin():186) -

$ cat ./logs/02.log
2018-04-23 06:52:57,613 [main] INFO  (Task#begin():176) -
2018-04-23 06:52:57,614 [main] INFO  (Task#begin():177) -
2018-04-23 06:52:57,721 [main] ERROR  (Task#begin():185) -
2018-04-23 06:52:57,722 [main] INFO  (Task#begin():186) -
2018-04-23 06:52:57,723 [main] INFO  (Task#begin():176) -
2018-04-23 06:52:57,724 [main] INFO  (Task#begin():177) -
2018-04-23 06:52:57,725 [main] INFO  (Task#begin():185) -
2018-04-23 06:52:57,736 [main] INFO  (Task#begin():186) -
2018-04-23 06:52:57,737 [main] INFO  (Task#begin():176) -
2018-04-23 06:52:57,738 [main] INFO  (Task#begin():177) -
2018-04-23 06:52:57,739 [main] WARN  (Task#begin():185) -
2018-04-23 06:52:57,753 [main] INFO  (Task#begin():186) -

```

次に検索するパターンファイルを用意します。

```shell-session
$ ls
Pipfile  Pipfile.lock  fts.py  logs  pattern.txt

$ cat pattern.txt
ERROR
WARN
```

## 6.検索してみる
```shell-session
$ pipenv run fts.py create_index

$ pipenv run fts.py search pattern.txt
*****\logs\01.log 2018-04-20 06:52:57,737 [main] ERROR  (Task#begin():176) -
*****\logs\02.log 2018-04-23 06:52:57,721 [main] ERROR  (Task#begin():185) -
*****\logs\02.log 2018-04-23 06:52:57,739 [main] WARN  (Task#begin():185) -
```

## 7.まとめ
思ったより簡単にできました。
ツールは作ろうとするたびに何かを忘れて手間取ることが多いのでちょこちょこメモしていこうと思います。