Azure FunctionsでMeCabを実行する例

#はじめに
サーバレスアーキテクチャでアプリを作る中で、Azure Functions上にてMecabを実行するにあたりちょっと面倒だったので手段について投稿します。

#Mecabとは
ググればいくらでもありますがとりあえず以下参照。
[【技術解説】形態素解析とは？MeCabインストール手順からPythonでの実行例まで](https://mieruca-ai.com/ai/morphological_analysis_mecab/)

#ざっくりやりたいこと
+ Azure Functions上でMecabを動かしたい
+ システム辞書は頻繁に更新しないので埋め込む
+ ユーザー辞書は適度に追加したいので外部から読み込みたい


#ざっくり仕組み
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/109302/9c22e68e-0975-ebff-d33d-fe343f0d79a0.png)

Mecabの辞書はファイルパスを指定する方式なのでTMPに書き出す必要がありました。

0. ユーザー辞書はAzure Storageに置いておく
1. Logic Appsの繰り返しトリガーで発動(別にトリガーは何でも良い)
2. Logic AppsのBLOBコンテンツの取得アクションを用いてAzure Functionsにつなげる<br>(Azure Functions上から取得しても良いけど柔軟性がなくなる気がする)
3. base64で入力される文字列をファイルで生成してTMPに格納
4. 埋め込みリソースからsys.dic(etc)を取得しファイルを生成してTMPに格納

Logic Appsのデザイナーで見ると以下のような感じ
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/109302/811b1657-eaf3-1f4d-9359-cd9a644bef92.png)

#TMPに置いたら毎回置き直す必要あるんじゃない？

[Azure Functionsでtemp fileを使う](https://qiita.com/tkykm/items/2435aeea56fccf605156)

にて挙動を調査していた記事がありました。(以下引用)
> インスタンス間では共有されない
kuduのdebug consoleでは関数で作ったtemp fileは見えない
再起動すれば消える
同一Function App内であれば他関数とTMPディレクトリは共通（他関数が作ったファイルが参照できる）
再起動しなければ結構残ってる（いつ消えるか検証中。再デプロイくらいでは消えない）
容量は５００MBまで。

実際に初回の起動は25秒程度かかりましたが即座に二回目を起動すると4秒程度でした。
もちろん、TMPに存在していたらファイルを生成しない、とかしてます。
ユーザー辞書は毎回置き直してます。

#ユーザー辞書毎回HttpRequestに載せたらお金かかるんじゃない？
そこはなんともです。頻度とかユーザー辞書の大きさによるかなと思っています。

+ WebAppとしてリリース
+ コンテナ化してリリース
+ 埋め込みリソースにしてユーザー辞書更新の度にAzure Functions再配置

等々いくつか手段は考えられます。

#まとめ
他に良い方法ありましたらご教示願います。

#ソース例
githubに上げたいんですけどまだ整ってないので抜粋です。必要なところだけ抜いただけでこの状態で動かしていないので動かなかったらごめんなさい。

環境

+ [NMeCabNetStandard:0.7.0](https://github.com/milleniumbug/nmecab)
+ [MicroBatchFramework](https://github.com/Cysharp/MicroBatchFramework)

```C#:Azure_Functions抜粋
    public static class Function1
    {
        [FunctionName("Function1")]
        public static async Task<IActionResult> Run(
            [HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)] HttpRequest req,
            ILogger log)
        {
            string requestBody = await new StreamReader(req.Body).ReadToEndAsync();
            dynamic data = JObject.Parse(requestBody);
            string userdicpath = ConvertUserDic(data);

            await Program.Main(new[] { (string)data.exec, "-userdic", userdicpath });

            return (ActionResult)new OkObjectResult("OK");
        }

        private static string ConvertUserDic(dynamic data)
        {
            if ((data is JObject j) && (j["userdic"]["$content"].Value<string>() is string s))
            {
                var dic = System.IO.Directory.CreateDirectory(Path.Combine(Environment.GetEnvironmentVariable("TMP"), "dic"));
                var userdic = Path.Combine(dic.FullName, "user.dic");

                using (var stream = new MemoryStream(System.Convert.FromBase64String(s)))
                {
                    using (var file = File.Create(userdic))
                    {
                        stream.CopyTo(file);
                        file.Close();
                    }
                }

                return userdic;
            }
            else
            {
                throw new ArgumentException("userdic", data?.userdic);
            }
        }
    }
```

```C#:Program例
    public class Program
    {
        public static async Task Main(string[] args)
        {
            await BatchHost.CreateDefaultBuilder().RunBatchEngineAsync(args);
        }
    }
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/109302/ef57506d-ff74-4c10-2b5e-f8177f8895bd.png)


という風に埋め込みリソースがあるとして

```C#:NMecab例
    public class MecabBatch : BatchBase
    {
        private ILogger<MecabBatch> Logger { get; }
        private string DicPath { get; }

        public MecabBatch(ILogger<MecabBatch> _logger)
        {
            Logger = _logger;
            DicPath = Path.Combine(Environment.GetEnvironmentVariable("TMP"), "dic");

            Task.WaitAll(new[] {
                CreateFileAsync(DicPath, "char.bin"),
                CreateFileAsync(DicPath, "dicrc"),
                CreateFileAsync(DicPath, "matrix.bin"),
                CreateFileAsync(DicPath, "sys.dic"),
                CreateFileAsync(DicPath, "unk.dic"),
            });
        }

        public void Execute(
            [Option("userdic", "user dictionary path.")]string userdic
            )
        {
            using (var m = MeCabTagger.Create(
                new MeCabParam
                {
                    DicDir = DicPath,
                    UserDic = new[] { userdic },
                }
                ))
            {
                getMeCabNode(m.ParseToNode("本日も晴天なり"))
                    .Where(n => String.IsNullOrEmpty(n.Feature) == false)
                    .ToList()
                    .ForEach(n => Logger.LogInformation(n.Feature));
            }
        }

        private async Task CreateFileAsync(string dicPath, string fileName)
        {
            if (File.Exists(Path.Combine(dicPath, fileName)))
                return;

            Directory.CreateDirectory(dicPath);

            var assembly = Assembly.GetExecutingAssembly();
            using (var stream = assembly.GetManifestResourceStream("MecabImpl.dic." + fileName))
            {
                using (var file = File.Create(Path.Combine(dicPath, fileName)))
                {
                    await stream.CopyToAsync(file);
                    file.Close();
                }
            }
        }

        private IEnumerable<MeCabNode> getMeCabNode(MeCabNode n)
        {
            yield return n;

            while (n.Next != null)
            {
                n = n.Next;
                yield return n;
            }
        }
    }
```

したら

```text
名詞,副詞可能,*,*,*,*,本日,ホンジツ,ホンジツ
助詞,係助詞,*,*,*,*,も,モ,モ
名詞,一般,*,*,*,*,晴天,セイテン,セイテン
助動詞,*,*,*,文語・ナリ,基本形,なり,ナリ,ナリ
```

とできました。