Excelデータベースをブラウザで絞り込み検索

# 概要
* サンプルデータの準備
* バックエンド実装
* HTMLテンプレート
* フロントエンド実装

## モチベーション
Excelで管理されているデータベース（）のデータをFZFライクに絞り込み検索できるようにします。
全員のPC内にfzfやらターミナルやらxlsx2csvのようなツールが入っていないので、ブラウザの表示を通してサーバー経由でExcelのFuzzy検索をできるようにします。

![demo-fzf.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/113494/d6df9b8d-322a-a539-cd64-f8326423d930.gif)
FZF本家の実行画面。ファイルを絞り込み検索しています。



## 完成形
![demo-excel-fzf.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/113494/f501670d-b7e0-4903-b57a-0b7718a3b077.gif)
Excelデータをブラウザ上で絞り込み検索

## ディレクトリ構成

```ディレクトリ構成
.
├── data
│   └── sample.xlsx
├── main.go
├── go.mod
├── go.sum
├── template
│   └── index.tmpl
└── static
    ├── main.js
    ├── main.ts
    ├── node_modules
    ├── package-lock.json
    ├── package.json
    └── tsconfig.json
```

# サンプルデータの準備
[Sample excel data for analysis](https://www.learningcontainer.com/sample-excel-data-for-analysis/#Sample_Xlsx_file_download)の[xlsxファイル](https://www.learningcontainer.com/download/sample-xlsx-file-for-testing/)をいただきました。
ダウンロードしてdata/sample.xlsx へ入れておきます。


# バックエンド実装
Go言語のginというフレームワークでRESTFUL APIサーバーを立てます。

また、Excelデータベース()を解析するため、excelizeというライブラリも使用しますので、あらかじめインストールしておきます。

```
$ go install github.com/gin-gonic/gin
$ go install github.com/xuri/excelize/v2
```

main.goはこちらです。
簡単なサンプルを目指すため、バックエンドはmain.go の1ファイルのみです。

```go:main.go
package main

import (
	"net/http"
	"strings"

	"github.com/gin-gonic/gin"
	"github.com/xuri/excelize/v2"
)

var lines []string

// Excelデータ読み込み
// 時間がかかるとmain関数へなかなか行かない、
// つまりブラウザのページ読み込みがストップしてしまうので、
// init()の中身は非同期処理で裏で読み込みをかけておきます。
// ページを読み込んだ時点でのデータを配信するので、
// 不完全なデータになる場合もあります。
// そういうときは、ブラウザ上でリロードかけると
// 最新のデータを配信してくれます。
func init() {
	go func() {
		path := "./data/sample.xlsx"
		f, _ := excelize.OpenFile(path)
		defer f.Close()
		// 行ごとに読み込み
		// 2次元配列で返す
		rows, _ := f.GetRows("Sheet1")
		lines = make([]string, len(rows))
		for i, row := range rows {
			lines[i] = strings.Join(row, " ")
		}
	}()
}

// サーバー立ち上げ
func main() {
	r := gin.Default()
	r.Static("/static", "./static")
	r.LoadHTMLGlob("template/*.tmpl")
	// エントリポイント
	r.GET("/", func(c *gin.Context) {
		c.HTML(http.StatusOK, "index.tmpl", gin.H{})
	})
	// list形式のJSONを配信するAPI
	r.GET("/list", func(c *gin.Context) {
		c.IndentedJSON(http.StatusOK, lines)
	})
	r.Run()
}
```


main.goを書いたら、プロジェクトルートディレクトリにて`$ go mod init && go mod tidy`してgo.mod, go.sumを生成します。


# HTMLテンプレート

フロントエンドはJavaScript(TypeScript)で動作をつけるため、HTMLは簡単なものにします。

```html:template/index.tmpl
<html lang="ja">
<body>
  <label for="search-form">Excel検索</label>
  <!-- ここで検索 -->
  <input type="text" name="search-form" id="search-form" placeholder="検索キーワードを入力" >
  <!-- ここに結果を表示 -->
  <div id="search-result" ></div>
  <script type="module" src="/static/main.js"></script>
</body>
</html>
```

input要素で検索ワードを入力し、divに結果を表示します。
Goのmain関数の ` r.LoadHTMLGlob("template/*.tmpl") `で読み込み指定しています。
` <script type="module" src="/static/main.js"></script> `でstatic/main.jsを読み込みます。


# フロントエンド実装
Fuzzy検索を実装していきます。
staticにmain.tsを書きます。
staticディレクトリにて、`$ tsc --init`を実行し、tsconfig.jsonを生成します。
`$ npm i --save fzf`で`node_modules`にfzfをインストールしておきます。

```typescript:static/main.ts
import { Fzf } from "./node_modules/fzf/dist/fzf.es.js";

main();

async function fetchPath(url: string) {
  return await fetch(url)
    .then((response) => {
      return response.json();
    })
    .catch((response) => {
      return Promise.reject(
        new Error(`{${response.status}: ${response.statusText}`),
      );
    });
}

// ほぼ公式通りの実装
function fzfSearch(list: string[], keyword: string): string[] {
  const fzf = new Fzf(list);
  const entries = fzf.find(keyword);
  const ranking: string[] = entries.map((entry: Fzf) => entry.item);
  return ranking;
}

async function main() {
  const url: URL = new URL(window.location.href);
  // サーバーからExcelの行を取得
  const list = await fetchPath(url.origin + "/list");
  const searchInput = document.getElementById("search-form");
  const resultOutput = document.getElementById("search-result");
  // キーを押すたびにページ内容更新
  searchInput.addEventListener("keyup", () => {
    // 要素クリア
    while (resultOutput.firstChild) {
      resultOutput.removeChild(resultOutput.firstChild);
    }
    // fzf検索
    const result: string[] = fzfSearch(list, searchInput.value);
    // 検索結果をコンソールに表示
    console.log(result);
    // 検索結果を結果要素に表示
    result.map((line: string) => {
      const p = document.createElement("p");
      const text = document.createTextNode(line);
      p.appendChild(text);
      resultOutput.append(p);
    });
  });
}
```

main.jsがindex.tmplが読み込まれるとmain()関数が呼ばれ、
main.goのAPI `localhost:8080/list` を呼び出してリスト形式のJSONに格納されたExcelデータを読み込みます。
さらに"search-form" 要素にイベントリスナーを登録し、input要素にキー入力するたびに `serchInput.addEventListener()`のコールバック関数が実行されます。
これでキー入力のたびに"resultOutput"要素のリセットと、`fzfSearch()`関数が働き、
`fzfSearch()`の結果を"resultOutput" 要素にpタグで書き込みます。

main.tsが書けたら`$ npx tsc`を実行し、main.jsを生成します.


# 実行
`$ go run main.go`でサーバーを起動し、ブラウザのアドレス欄に`localhost:8080` で接続します。
最後に、検索窓に適当な文字列を打って、データが絞り込まれて表示されることを確認します。