Elasticsearchを触ってみた

Elasticsearchがどんなものか全く知らなかったので、インストールして触ってみました。

### 準備
まずローカルに動作環境を構築します。

OS: Windows 11 Home 22H2
Engine: elasticsearch 8.10.2
Tool: kibana 8.10.2
Language: C#

以下のサイトからelasticsearchをダウンロードしてzipを適当なフォルダに解凍します。
https://www.elastic.co/jp/downloads/elasticsearch

また、以下のサイトからkibanaをダウンロードしてzipを適当なフォルダに解凍します。
kibanaはElasticsearchと連携してデータを探索、可視化、分析するツールだそうです。
https://www.elastic.co/jp/downloads/kibana

configファイルの設定は、ローカル環境で色々試すだけなのでelasticsearch.ymlのsecurityをfalseにしたのと、Elasticsearchの起動でOutOfMemoryが出たのでjvm.optionsをイジりました。

```:elasticsearch-8.10.2\config\elasticsearch.yml
xpack.security.enabled: false
```
```:elasticsearch-8.10.2\config\jvm.options
-Xms4g
-Xmx4g
```
また、コマンドプロンプトを立ち上げてelasticsearch-8.10.2\binに移動し、以下のコマンドを叩いてプラグインをインストールします。
```:elasticsearch-plugin
elasticsearch-plugin install analysis-kuromoji
elasticsearch-plugin install analysis-icu
```
コマンドプロンプトを立ち上げてelasticsearch-8.10.2\binに移動し、Elasticsearchを起動させます。
```:elasticsearch
elasticsearch.bat
```
ブラウザを起動させて以下のURLを叩き、諸情報が表示されればOKです。
```:URL
http://localhost:9200/
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/263269/bfa60767-41c7-18b7-9615-5023bad18acb.png)
Elasticsearchが起動が終わったら、別のコマンドプロンプトを立ち上げてkibana-8.10.2\binに移動し、kibanaを起動させます。
```:kibana
kibana.bat
```
ブラウザを起動させて以下のURLを叩き、ページが表示されればOKです。
```:URL
http://localhost:5601/
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/263269/d59ce470-f680-7eac-7f32-768a8037aa16.png)
### NGramを試してみる
以下のC#プログラムを実行するとTokenizerにNGramを利用したインデックス設定が作成されます。
```Xml:.csproj
<Project Sdk="Microsoft.NET.Sdk">
    <PropertyGroup>
        <OutputType>Exe</OutputType>
        <TargetFramework>net7.0</TargetFramework>
        <ImplicitUsings>enable</ImplicitUsings>
        <Nullable>enable</Nullable>
    </PropertyGroup>
    <ItemGroup>
        <PackageReference Include="Elastic.Clients.Elasticsearch" Version="8.10.0" />
        <PackageReference Include="System.Text.Encoding.CodePages" Version="7.0.0" />
    </ItemGroup>
</Project>
```

```Csharp:C# console application
using System.Text;
using Elastic.Clients.Elasticsearch;
using Elastic.Clients.Elasticsearch.Analysis;
using Elastic.Clients.Elasticsearch.IndexManagement;
using Elastic.Clients.Elasticsearch.Mapping;

Console.WriteLine(DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss.fff"));

Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);

var settings = new ElasticsearchClientSettings(new Uri("http://localhost:9200"));
var client = new ElasticsearchClient(settings);

var isa = new IndexSettingsAnalysis();

// Tokenizers
isa.Tokenizers = new Tokenizers();
isa.Tokenizers.Add("custom_ngram_tokenizer", new NGramTokenizer()
{
    MinGram = 1,
    MaxGram = 10
});

// Tokenizers
isa.TokenFilters = new TokenFilters();
// Analyzers
isa.Analyzers = new Analyzers();
isa.Analyzers.Add("custom_ngram_analyzer", new CustomAnalyzer()
{
    Tokenizer = "custom_ngram_tokenizer"
});
// delete index & create index
var delRes = await client.Indices.DeleteAsync("my-index");
var creRes = client.Indices.Create("my-index", c => c
    .Settings(s => s
        .Index(
            new IndexSettings()
            {
                Analysis = isa,
                MaxNgramDiff = 10
            })
    )
);

Console.WriteLine(DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss.fff"));
```
kibanaのDev Toolsを叩いて先ほど作成したインデックス設定を確認します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/263269/3777ea82-10f6-1dea-6ae5-da6757226b8c.png)
```:kibana: Dev Tools > console
GET /my-index/_settings
```
settings配下にC#プログラムから作成したインデックス設定があることを確認できました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/263269/dff86f55-5d3e-a394-e652-fce959e5821e.png)
続いてNGramのTokenizerの動きを確認します。先ほどC#プログラムを実行して作成したcustom_ngram_analyzerを使って、「ロキソプロフェン錠６０ｍｇ」をトークン化します。
以下のコマンドをkibanaのDev Toolsで叩いてみてください。
```:kibana: Dev Tools > console
GET /my-index/_analyze
{
  "analyzer": "custom_ngram_analyzer",
  "text": "ロキソプロフェン錠６０ｍｇ"
}
```
MaxGram = 10でNGramTokenizerを作成しているので最大10文字でトークン化されていることが確認できます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/263269/bba232b9-a102-cb64-3f8e-7157b4bd1d57.png)

### kuromojiを試してみる
今度は日本語に対応したkuromojiのTokenizerを使ってみます。以下のC#プログラムを実行するとcustom_kuromoji_analyzerが作成されます。
```Csharp:C# console application
using System.Text;
using Elastic.Clients.Elasticsearch;
using Elastic.Clients.Elasticsearch.Analysis;
using Elastic.Clients.Elasticsearch.IndexManagement;
using Elastic.Clients.Elasticsearch.Mapping;

Console.WriteLine(DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss.fff"));

Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);

var settings = new ElasticsearchClientSettings(new Uri("http://localhost:9200"));
var client = new ElasticsearchClient(settings);

var isa = new IndexSettingsAnalysis();

// Tokenizers
isa.Tokenizers = new Tokenizers();
isa.Tokenizers.Add("custom_ngram_tokenizer", new NGramTokenizer()
{
    MinGram = 1,
    MaxGram = 10
});
isa.Tokenizers.Add("custom_kuromoji_tokenizer", new KuromojiTokenizer()
{
    Mode = KuromojiTokenizationMode.Search,
    DiscardPunctuation = true
});
// TokenFilters
isa.TokenFilters = new TokenFilters();
// Analyzers
isa.Analyzers = new Analyzers();
isa.Analyzers.Add("custom_ngram_analyzer", new CustomAnalyzer()
{
    Tokenizer = "custom_ngram_tokenizer"
});
isa.Analyzers.Add("custom_kuromoji_analyzer", new CustomAnalyzer()
{
    Tokenizer = "custom_kuromoji_tokenizer",
});
// delete index & create index
var delRes = await client.Indices.DeleteAsync("my-index");
var creRes = client.Indices.Create("my-index", c => c
    .Settings(s => s
        .Index(
            new IndexSettings()
            {
                Analysis = isa,
                MaxNgramDiff = 10
            })
    )
);

Console.WriteLine(DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss.fff"));
```
先程と同様に「ロキソプロフェン錠６０ｍｇ」をcustom_kuromoji_analyzerを使ってトークン化します。
以下のコマンドをkibanaのDev Toolsで叩いてみてください。
```:kibana: Dev Tools > console
GET /my-index/_analyze
{
  "analyzer": "custom_kuromoji_analyzer",
  "text": "ロキソプロフェン錠６０ｍｇ"
}
```
良い感じにトークン化されていると思います。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/263269/40c8cb2e-b0aa-75ca-010e-b406595077a6.png)

### kuromojiを使って読み仮名に変換してみる
続いてkuromojiのTokenFilterを使って読み仮名に変換します。以下のC#プログラムを実行するとcustom_kuromoji_yomi_analyzerが作成されます。
```Csharp:C# console application
using System.Text;
using Elastic.Clients.Elasticsearch;
using Elastic.Clients.Elasticsearch.Analysis;
using Elastic.Clients.Elasticsearch.IndexManagement;
using Elastic.Clients.Elasticsearch.Mapping;

Console.WriteLine(DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss.fff"));

Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);

var settings = new ElasticsearchClientSettings(new Uri("http://localhost:9200"));
var client = new ElasticsearchClient(settings);

var isa = new IndexSettingsAnalysis();

// Tokenizers
isa.Tokenizers = new Tokenizers();
isa.Tokenizers.Add("custom_ngram_tokenizer", new NGramTokenizer()
{
    MinGram = 1,
    MaxGram = 10
});
isa.Tokenizers.Add("custom_kuromoji_tokenizer", new KuromojiTokenizer()
{
    Mode = KuromojiTokenizationMode.Search,
    DiscardPunctuation = true
});
// TokenFilters
isa.TokenFilters = new TokenFilters();
// Analyzers
isa.Analyzers = new Analyzers();
isa.Analyzers.Add("custom_ngram_analyzer", new CustomAnalyzer()
{
    Tokenizer = "custom_ngram_tokenizer"
});
isa.Analyzers.Add("custom_kuromoji_analyzer", new CustomAnalyzer()
{
    Tokenizer = "custom_kuromoji_tokenizer"
});
isa.Analyzers.Add("custom_kuromoji_yomi_analyzer", new CustomAnalyzer()
{
    Tokenizer = "custom_kuromoji_tokenizer",
    Filter = new List<string>{ "kuromoji_readingform" }
});
// delete index & create index
var delRes = await client.Indices.DeleteAsync("my-index");
var creRes = client.Indices.Create("my-index", c => c
    .Settings(s => s
        .Index(
            new IndexSettings()
            {
                Analysis = isa,
                MaxNgramDiff = 10
            })
    )
);

Console.WriteLine(DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss.fff"));
```
先程と同様に「ロキソプロフェン錠６０ｍｇ」をcustom_kuromoji_yomi_analyzerを使ってトークン化します。
以下のコマンドをkibanaのDev Toolsで叩いてみてください。
```:kibana: Dev Tools > console
GET /my-index/_analyze
{
  "analyzer": "custom_kuromoji_yomi_analyzer",
  "text": "ロキソプロフェン錠６０ｍｇ"
}
```
読み仮名でトークン化されていると思います。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/263269/0059eb66-2eb2-1289-05a5-cc45e57cef4a.png)

### 不要な文字を削除してみる
CharFilterを使って不要な文字を削除してみます。
```Csharp:C# console application
using System.Text;
using Elastic.Clients.Elasticsearch;
using Elastic.Clients.Elasticsearch.Analysis;
using Elastic.Clients.Elasticsearch.IndexManagement;
using Elastic.Clients.Elasticsearch.Mapping;

Console.WriteLine(DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss.fff"));

Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);

var settings = new ElasticsearchClientSettings(new Uri("http://localhost:9200"));
var client = new ElasticsearchClient(settings);

var isa = new IndexSettingsAnalysis();

// Charfilters
isa.CharFilters = new CharFilters();
isa.CharFilters.Add("custom_char_filter", new PatternReplaceCharFilter()
{
    Pattern = "[０-９]|ｍｇ",
    Replacement = ""
});
// Tokenizers
isa.Tokenizers = new Tokenizers();
isa.Tokenizers.Add("custom_ngram_tokenizer", new NGramTokenizer()
{
    MinGram = 1,
    MaxGram = 10
});
isa.Tokenizers.Add("custom_kuromoji_tokenizer", new KuromojiTokenizer()
{
    Mode = KuromojiTokenizationMode.Search,
    DiscardPunctuation = true
});
// TokenFilters
isa.TokenFilters = new TokenFilters();
// Analyzers
isa.Analyzers = new Analyzers();
isa.Analyzers.Add("custom_ngram_analyzer", new CustomAnalyzer()
{
    Tokenizer = "custom_ngram_tokenizer"
});
isa.Analyzers.Add("custom_kuromoji_analyzer", new CustomAnalyzer()
{
    Tokenizer = "custom_kuromoji_tokenizer"
});
isa.Analyzers.Add("custom_kuromoji_yomi_analyzer", new CustomAnalyzer()
{
    CharFilter = new List<string>{ "custom_char_filter" },
    Tokenizer = "custom_kuromoji_tokenizer",
    Filter = new List<string>{ "kuromoji_readingform" }
});
// delete index & create index
var delRes = await client.Indices.DeleteAsync("my-index");
var creRes = client.Indices.Create("my-index", c => c
    .Settings(s => s
        .Index(
            new IndexSettings()
            {
                Analysis = isa,
                MaxNgramDiff = 10
            })
    )
);

Console.WriteLine(DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss.fff"));
```
先程と同様に「ロキソプロフェン錠６０ｍｇ」をcustom_kuromoji_yomi_analyzerを使ってトークン化します。
以下のコマンドをkibanaのDev Toolsで叩いてみてください。
```:kibana: Dev Tools > console
GET /my-index/_analyze
{
  "analyzer": "custom_kuromoji_yomi_analyzer",
  "text": "ロキソプロフェン錠６０ｍｇ"
}
```
全角の数字とｍｇが削除されていますね。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/263269/8481a1cd-b1a0-a06c-e65d-e3f4dffa9767.png)
さらにEdgeNGramTokenFilterをかけます。
```Csharp:C# console application
isa.TokenFilters.Add("custom_edge_ngram_filter", new EdgeNGramTokenFilter()
{
    MinGram = 1,
    MaxGram = 10
});
```
```Csharp:C# console application
isa.Analyzers.Add("custom_kuromoji_yomi_analyzer", new CustomAnalyzer()
{
    CharFilter = new List<string>{ "custom_char_filter" },
    Tokenizer = "custom_kuromoji_tokenizer",
    Filter = new List<string>{ "kuromoji_readingform", "custom_edge_ngram_filter" }
});
```
先頭文字からのNGramでトークン化されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/263269/dd7c3a1e-58fc-eea4-ee7e-1fcee3e225e1.png)
合わせてcustom_ngram_analyzerとcustom_kuromoji_analyzerにもCharFilterを設定しました。
```Csharp:C# console application
isa.Analyzers.Add("custom_ngram_analyzer", new CustomAnalyzer()
{
    CharFilter = new List<string>{ "custom_char_filter" },
    Tokenizer = "custom_ngram_tokenizer",
});
```
```Csharp:C# console application
isa.Analyzers.Add("custom_kuromoji_analyzer", new CustomAnalyzer()
{
    CharFilter = new List<string>{ "custom_char_filter" },
    Tokenizer = "custom_kuromoji_tokenizer",
});
```

### インデックスを作成して検索してみる
ここまでで３つのAnalyzerを作成しました。最後にcustom_ngram_analyzerとcustom_kuromoji_yomi_analyzerを使って医薬品検索用のインデックスを作成してみましょう。

医薬品のマスターは以下のサイトから全件分ファイル(ZIP:898KB)をダウンロードします。
https://www.ssk.or.jp/smph/seikyushiharai/tensuhyo/kihonmasta/kihonmasta_04.html

以下のプログラムにて、custom_ngram_analyzerとcustom_kuromoji_yomi_analyzerをフィールドとマッピングさせてインデックスを作成します。
※CSVのパスはご自身の環境に合わせ書き換えください。

```Csharp:C# console application
using System.Text;
using Elastic.Clients.Elasticsearch;
using Elastic.Clients.Elasticsearch.Analysis;
using Elastic.Clients.Elasticsearch.IndexManagement;
using Elastic.Clients.Elasticsearch.Mapping;

Console.WriteLine(DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss.fff"));

Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);

var settings = new ElasticsearchClientSettings(new Uri("http://localhost:9200"));
var client = new ElasticsearchClient(settings);

var isa = new IndexSettingsAnalysis();

// Charfilters
isa.CharFilters = new CharFilters();
isa.CharFilters.Add("custom_char_filter", new PatternReplaceCharFilter()
{
    Pattern = "[０-９]|ｍｇ",
    Replacement = ""
});
// Tokenizers
isa.Tokenizers = new Tokenizers();
isa.Tokenizers.Add("custom_ngram_tokenizer", new NGramTokenizer()
{
    MinGram = 1,
    MaxGram = 10
});
isa.Tokenizers.Add("custom_kuromoji_tokenizer", new KuromojiTokenizer()
{
    Mode = KuromojiTokenizationMode.Search,
    DiscardPunctuation = true
});
// TokenFilters
isa.TokenFilters = new TokenFilters();
isa.TokenFilters.Add("custom_edge_ngram_filter", new EdgeNGramTokenFilter()
{
    MinGram = 1,
    MaxGram = 10
});
// Analyzers
isa.Analyzers = new Analyzers();
isa.Analyzers.Add("custom_ngram_analyzer", new CustomAnalyzer()
{
    CharFilter = new List<string>{ "custom_char_filter" },
    Tokenizer = "custom_ngram_tokenizer",
});
isa.Analyzers.Add("custom_kuromoji_analyzer", new CustomAnalyzer()
{
    CharFilter = new List<string>{ "custom_char_filter" },
    Tokenizer = "custom_kuromoji_tokenizer",
});
isa.Analyzers.Add("custom_kuromoji_yomi_analyzer", new CustomAnalyzer()
{
    CharFilter = new List<string>{ "custom_char_filter" },
    Tokenizer = "custom_kuromoji_tokenizer",
    Filter = new List<string>{ "kuromoji_readingform", "custom_edge_ngram_filter" }
});
isa.Analyzers.Add("keyword_search_analyzer", new CustomAnalyzer()
{
    CharFilter = new List<string>{ "custom_char_filter" },
    Tokenizer = "whitespace"
});

// delete index & create index
var delRes = await client.Indices.DeleteAsync("my-index");
var creRes = client.Indices.Create("my-index", c => c
    .Settings(s => s
        .Index(
            new IndexSettings()
            {
                Analysis = isa,
                MaxNgramDiff = 10
            })
    )
    .Mappings(map => map
        .Properties(
            new Properties<YakName>()
            {
                { "yakNameNgram", new TextProperty()
                    {
                        Analyzer = "custom_ngram_analyzer",
                        SearchAnalyzer = "keyword_search_analyzer"
                    }
                },
                { "yakNameKana", new TextProperty()
                    {
                        Analyzer = "custom_kuromoji_yomi_analyzer",
                        SearchAnalyzer = "keyword_search_analyzer"
                    }
                }
            }
        )
    )
);

using (StreamReader reader = new StreamReader(@"C:\es\y_ALL20230920.csv", Encoding.GetEncoding("Shift_JIS")))
{
    while (!reader.EndOfStream)
    {
        string line = reader.ReadLine();
        string[] values = line.Split(',');
        var yak_name = values[4].Trim(new char[] { '"' });
        if (String.IsNullOrEmpty(yak_name))
        {
            continue;
        }
        var yakName = new YakName
        {
            YakNameNgram = yak_name,
            YakNameKana = yak_name
        };
        var indexRes = await client.IndexAsync(yakName, "my-index");
    }
}

Console.WriteLine(DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss.fff"));

public class YakName
{
    public string YakNameNgram { get; set; }
    public string YakNameKana { get; set; }
}
```

数分でインデックスの作成が終わると思います。
終わったら以下のコマンドをkibanaのDev Toolsで叩いて、薬品を検索してみましょう。

```:kibana: Dev Tools > console
GET /my-index/_search
{
  "query": {
    "bool": {
      "should": [
	      { "match": { "yakNameNgram": "恵美須" } }
      ]
    }
  },
  "sort" : [{"_score":"desc"}]
}
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/263269/2df78011-2982-d623-66f1-81116214ea3f.png)
```:kibana: Dev Tools > console
GET /my-index/_search
{
  "query": {
    "bool": {
      "should": [
	      { "match": { "yakNameKana": "ショウサンギン" } }
      ]
    }
  },
  "sort" : [{"_score":"desc"}]
}
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/263269/ee7d576b-ba68-cc86-0026-0539e40263f7.png)
以上でした。