Azure Cognitive Search の新機能ノーマライザーを使ってフィルター結果を改善する

# はじめに
組織内に貯まっている大量な構造化・非構造化データから、新たな価値を見出すためのフルマネージド全文検索サービスである [Azure Cognitive Search](https://www.youtube.com/watch?v=9GcjFofXdd0) を使えば、誰でも簡単に AI 搭載検索エンジンを開発することができます。今回は「[ノーマライザー](https://docs.microsoft.com/azure/search/search-normalizers?source=docs)」というプレビュー中の機能について紹介します。

# ノーマライザー
ノーマライザーは、フィルター処理、ファセット、並べ替えのクエリーに対して事前処理を行うことで、入力テキスト種の違いなどによる検索漏れを低減させることができます。

これまで、フィルター処理、ファセット、並べ替えのクエリーに対しては、例えば大文字・小文字の違いに対しても厳格に区別されていました。これは一般的な検索フィールドに対するクエリーとは異なり、字句解析されないためです。

```json
$filter: "keyphrases/any(t:t eq 'Azure Cognitive Search')"
```

例: keyphrases フィールドのフィルター結果
```
Azure Cognitive Search: ヒット
azure cognitive search: ヒットしない
AZURE COGNITIVE SEARCH: ヒットしない
```

では、上記の小文字のみのドキュメントをヒットさせるために、以下のクエリーにします？

```json
$filter: "keyphrases/any(t:t eq 'Azure Cognitive Search') or keyphrases/any(t:t eq 'azure cognitive search')"
```

うわっ、私のクエリー、長すぎ…？
case-sensitive をヒットさせるためにこんな事、したくないですよね。

しかもこの条件でファセットを生成しても、別個のものとして扱われます。


# ノーマライザーの指定方法
ノーマライザーは、"filterable"、"sortable"、または "facetable" プロパティの少なくとも 1 つが true に設定されているテキスト フィールド (Edm.String および Collection(Edm.String)) のインデックス定義で、フィールドごとに指定します。

```json
    {
      "name": "keyphrases",
      "type": "Collection(Edm.String)",
      "searchable": true,
      "filterable": true,
      "retrievable": true,
      "sortable": false,
      "facetable": true,
      "analyzer": "ja.microsoft",
      "normalizer": "lowercase",
      "synonymMaps": []
    },
```

例えば、一律で小文字に寄せてしまいたい場合は上記 `"normalizer": "lowercase"` のように指定します。他にも現在サポートされているノーマライザーの一覧は[こちら](https://docs.microsoft.com/azure/search/search-normalizers?source=docs#normalizers-reference)にあります。

先ほどと同様のフィルター条件で実行します。

```json
$filter: "keyphrases/any(t:t eq 'Azure Cognitive Search')"
```

```
Azure Cognitive Search: ヒット
azure cognitive search: ヒット
AZURE COGNITIVE SEARCH: ヒット
```

こうすることでフィルターの直前に内部ですべて小文字に変換してからマッチングするようになります。ファセットも単一の `azure cognitive search` として集約・カウントされます。

# カスタム ノーマライザーの指定方法
ノーマライザーは、アナライザーと同様に事前に準備されたトークンフィルターや文字フィルターを組み合わせて独自のノーマライザーを構成できます。

```json
  "normalizers": [
    {
      "@odata.type": "#Microsoft.Azure.Search.CustomNormalizer",
      "name": "my_custom_normalizer",
      "tokenFilters": [
        "cjk_width",
        "lowercase"
      ],
      "charFilters": [
        "map_moji"
      ]
    }
  ],
  "charFilters": [
    {
      "@odata.type": "#Microsoft.Azure.Search.MappingCharFilter",
      "name": "map_moji",
      "mappings": [
        "條=>条"
      ]
    }
  ],
```

インデックス定義でこのように設定して、フィールド定義に `"normalizer": "my_custom_normalizer"` のように指定します。

上記の例では、`CJKWidthFilter` によって全角英数字を半角に変換した後、小文字に正規化します。また、`MappingCharFilter` によって特定の文字を置換しています。フィルター条件が複数あった場合、インデックスの若いほうから順に適用されます。

```json
$filter: "keyphrases/any(t:t eq '北條氏')"
```
```
北条 時政, keyphrases:["北条氏"]: ヒット
北条 義時, keyphrases:["北条氏"]: ヒット
北条 泰時, keyphrases:["北条氏"]: ヒット
```

特殊な例で恐縮ですが、`MappingCharFilter` を使用して旧字体の "條" でもヒット可能にしています。一定ルールの型番やコード値に含まれる記号の揺れなどに使えると思います。

```json
$filter: "keyphrases/any(t:t eq '１２２４年')"
```
```
1224年    : ヒット
１２２４年 : ヒット
```

`CJKWidthFilter` は全角で入力された文字を半角に変換します。日本語圏で表記に揺れがある場合に重宝します。


# 利用シーン
ノーマライザー機能は、ファセットによって生成したカテゴリーリストをそのままフィルター条件に利用するような検索 UI ではなく、ユーザーに直接フィルタ条件を入力させるような検索 UI において効果を発揮する機能だと思います。文字種チェックや変換はフロントエンドで開発してもよいですが、ノーマライザー機能を使うとノーコードで簡単にこれらの機能を実装できます。

# 参考
[ノーマライザー ドキュメント](https://docs.microsoft.com/azure/search/search-normalizers?source=docs#normalizers-reference)