🔍

日本語のあいまい検索を試す(C#)

2023/07/24に公開

概要

「ignore case」をご存知でしょうか?
プログラミングでは、「大文字/小文字を無視する」という意味で、大文字と小文字を区別せずに文字列の比較や検索を行う際に使われる用語です。

たとえば、C# の String.Compare メソッド(文字列比較メソッド)では、StringComparison.CurrentCultureIgnoreCase(末尾が IgnoreCase)をオプション指定したときに、大文字と小文字を区別せずに文字列比較が行われます。

また、C言語では、strcmpstricmp という文字列比較の関数がありまして、関数名としては真ん中にiが付くか付かないかの違いで、機能としてはiが付く方は、大文字/小文字の区別をせず比較を行います。おそらく「ignore」の i だと思います。

ちなみに「大文字/小文字を区別しない」とは、アルファベットの大文字(Aなど)と小文字(aなど)を区別せず同じものとして扱って比較や検索を行うもので、"ABC""abc""Abc" もすべて同じものと判定されます。

さらに日本語を含めたとき、「全角と半角を区別しない」や「ひらがなとカタカナを区別しない」なども考えられます。本記事では、このような日本語も含めて何かを区別しない検索のことを「あいまい検索」と呼ぶこととします。

以下では、CSVデータからキーワード検索するサンプルプログラムにて、C#に用意されているクラスやメソッドを使用してあいまい検索を試してみたコードを紹介します。

CompareInfo クラスによるあいまい検索

CompareInfo クラスと IndexOf メソッド

C# には、System.Globalization.CompareInfo クラスがあります。

参考:
CompareInfo クラス (System.Globalization) - Microsoft Learn

このドキュメントによりますと、CompareInfo クラスは、

カルチャごとに異なる文字列比較を行うための一連のメソッドを実装します。

とのことで、このクラスを利用することで、あいまい検索を行うことができます。

たとえば、CompareInfo.IndexOf メソッドは、ある文字列からキーワード検索を行い、見つかった場合はその文字位置を返し(たとえば、5文字目に見つかれば、0始まりで「4」を返します)、見つからなかった場合は -1 を返します。

したがって、文字列が納められた変数 str とキーワードが納められた変数 key があったときに、str の中に key の内容が含まれるかどうかを調べるコードは、次のように記述します。

CompareInfo cmp_info = CultureInfo.CurrentCulture.CompareInfo;
int pos = cmp_info.IndexOf(str, key);
if ( pos >= 0) {
	//キーワードが見つかった
}
else {
	//キーワードは見つからなかった
}

1行目は、System.Globalization.CultureInfo クラスにより、現在のカルチャ(日本)のCompareInfo インスタンスを取得しています。この例のように、CompareInfo インスタンスは、CultureInfo クラスから生成するのが一般的です。

実際の文字列検索は、2行目で行っています。キーワードが見つかった場合、0以上の文字位置が返されるので、3行目のif文にて0以上かどうかで判定しています。

なお、1行目で現在のカルチャではなく、「日本」を指定して CompareInfo インスタンスを生成するには、次のように記述します。

CompareInfo cmp_info = new CultureInfo("ja-JP").CompareInfo;

参考:
CultureInfo クラス (System.Globalization) - Microsoft Learn

CompareOptions 列挙型

上記 CompareInfo クラスであいまい検索を行うには、何をあいまいにするのかをオプション指定します。
このために使用するのが、CompareOptions 列挙型です。

参考:
CompareOptions 列挙型 (System.Globalization) - Microsoft Learn

主なメンバーを以下に示します。

メンバ 意味
IgnoreCase 大文字・小文字を区別しない
IgnoreKanaType ひらがな・カタカナを区別しない
IgnoreWidth 全角・半角を区別しない
IgnoreNonSpace 濁点・半濁点を無視

CompareOptions 列挙型はフラグとなっていますので、or演算子(|)で列記することで、複数のオプションが指定できます。

たとえば、ひらがな/カタカナと全角/半角の両方を区別しない場合、以下のように記述します。

CompareOptions cmp_opt = CompareOptions.IgnoreKanaType | CompareOptions.IgnoreWidth;

これを、CompareInfo.IndexOf メソッドの第3引数などに指定します。

あいまい検索する関数

以上をまとめて、あいまい検索を行う FuzzySearch_IndexOf 関数を記述します。

public static int FuzzySearch_IndexOf(string str, string key)
{
    CompareInfo cmp_info = CultureInfo.CurrentCulture.CompareInfo;
    CompareOptions cmp_opt =
        CompareOptions.IgnoreCase           //大文字・小文字を区別しない
        | CompareOptions.IgnoreKanaType     //ひらがな・カタカナを区別しない
        | CompareOptions.IgnoreWidth        //全角・半角を区別しない
        | CompareOptions.IgnoreNonSpace     //濁点・半濁点を無視
        ;

    return cmp_info.IndexOf(str, key, cmp_opt);
}

呼び出しは、

int pos = FuzzySearch_IndexOf( "元となる文字列" , "検索キーワード" );

のように行い、戻り値は、CompareInfo.IndexOf と同様、キーワードが見つかれば文字位置(0はじまり)、見つからなければ負の値(-1)となります。

もっとあいまいにする

CompareInfo クラスがあいまい処理しない文字

日本語では、「キャ」、「キュ」、「キョ」など「ヤユヨ」の小さい文字があります。また、「アイウエオ」の小さい文字もあります。さらに、促音(小さい「ツ」)もあります。しかも、いずれも、ひらがなもカタカナもあります。

CompareInfo クラスでは、これらの文字をあいまい検索できません。たとえば、小さい「ャ」が含まれる「キャンプ」から、通常の大きさの「ヤ」をキーワード検索しても、結果は「見つかりません」となります。

また、伸ばす棒「ー」とハイフン(マイナス記号)「-」は異なる文字ですが、あいまい検索なら「群馬産業技術センター」(末尾が伸ばす棒)と「群馬産業技術センタ-」(末尾がハイフン)を同一に扱ってほしいものです。

さらに、「ゐ」や「ゑ」などのように古い仮名遣いがありますが、「ゑびす様」からキーワード「え」で検索しても見つかりません。

CompareInfo クラスが処理できる文字に置換

上のような文字も区別なく検索させるには、CompareInfo クラスが処理できる文字に置換する必要があります。

たとえば、「キャンプ」の小さい「ャ」を通常の「ヤ」に置換してから、通常の「ヤ」でキーワード検索すれば、2文字目に見つかるはずです。

この置換処理を組み込んだ FuzzySearch_IndexOf 関数は次のようになります。

//もっとあいまいにするための置換元と置換先 ■■■ (1) ■■■
private static string[] moto = {
    "ァ", "ィ", "ゥ", "ェ", "ォ", 
    "ャ", "ュ", "ョ", "ッ", "-", "ゐ", "ゑ" };
private static string[] saki = {
    "ア", "イ", "ウ", "エ", "オ", 
    "ヤ", "ユ", "ヨ", "ツ", "ー", "い", "え" };

public static int FuzzySearch_IndexOf(string str, string key)
{
    CompareInfo cmp_info = CultureInfo.CurrentCulture.CompareInfo;
    CompareOptions cmp_opt =
        CompareOptions.IgnoreCase           //大文字・小文字を区別しない
        | CompareOptions.IgnoreKanaType     //ひらがな・カタカナを区別しない
        | CompareOptions.IgnoreWidth        //全角・半角を区別しない
        | CompareOptions.IgnoreNonSpace     //濁点・半濁点を無視
        ;

    //もっとあいまいにするための置換 ■■■ (2) ■■■
    for (int i = 0; i < moto.Length; i++)
    {
        str = str.Replace(moto[i], saki[i]);
    }

    return cmp_info.IndexOf(str, key, cmp_opt);
}

コメントの(1)の部分にて、変数 moto は置換元の文字列配列で、変数 saki は置換先の文字列配列です。moto は小さい文字や古い仮名遣いの文字で、saki は通常の大きさの文字と現在の仮名遣いの文字です。motosaki は、各要素が置換元と置換先で対になっています。具体的には、moto[0]saki[0] に置換され、moto[1]saki[1] に置換され、、、といった具合です。
(見やすさのため要素数を少なくしています。下のサンプルプログラムでは、もっとたくさんの文字列を置換しています。)

この motosaki を使って実際に置換の処理を行っているのが コメント(2)部分の for文です。引数で受け取った変数 str の内容を、繰り返し置換しています。
なお、この処理では、motosaki の要素数が一致していることを前提としています。もし saki[i] が未定義の場合、エラー(例外)となりますので、要素数が不一致となる入力間違えには注意してください。

この FuzzySearch_IndexOf 関数に対して、次のように呼び出すと、結果は「見つかった」が返されます(実際には2文字目を意味する「1」が変数 pos に代入されます)。

int pos = FuzzySearch_IndexOf("キャンプ", "ヤ"); //第1引数は小さい'ヤ'、第2引数は通常の'ヤ'

なお、本記事では、上記コードのコメント(2)部分の置換処理を「もっとあいまいにするための置換」という意を込めて「モット置換」と呼んでいます。

サンプルプログラム

作成したプロジェクト

サンプルプログラムとして、Visual Studio 2019 を使用して、以下のプロジェクトを作成しました。

項目 内容
開発ツール Visual Studio 2019 Community
言語 C#
テンプレート Windowsフォームアプリケーション
フレームワーク .NET Framework 4.7.2
プロジェクト名 CsvViewerTiny
実際のコード
(掲載しているコード)
FuzzySearch.cs
Form1.cs
Form1.Designer.cs
data.csv (テスト用CSVデータ)

概要

CSVファイルを読み込み、キーワード検索するプログラムです。この検索に、あいまい検索処理を実装して、動作を確認します。

CSVファイルを読み込んだ状態
CSVファイルを読み込んだ状態(ファイルを読み込ませるには、CSVファイルをドラッグ&ドロップする)

キーワード「や」で検索
キーワード「や」で検索

上図の2番目は、キーワード「や」で検索した画面です。[趣味]列の「キャンプ」(全角の小さい'ヤ')や「キャンプ」(半角の小さい'ヤ')、「やきゅう」(全角の通常の'や')などが検索されていることがわかります。

簡単な解説

あいまい検索の関数

あいまい検索用の関数として、FuzzySearch.IndexOfFuzzySearchクラスのIndexOfメソッド)と FuzzySearch.ReplaceFuzzySearchクラスのReplaceメソッド) を用意しました。上で紹介した FuzzySearch_IndexOf関数を2つのメソッドに分割したものです。

FuzzySearch.IndexOf は、上で紹介したFuzzySearch_IndexOf関数とほぼ同じですが、次のコードのように引数を2つ増やしています。
第3引数 bool is_str_replace は第1引数 str をモット置換(小さい'ャ'→通常の'ヤ'など)するかどうか、第4引数 bool is_key_replace は第2引数 key をモット置換するかどうかのフラグです。事前にモット置換しておいた文字列を使用することがあるため(今回のサンプルがそうです)追加した引数です。

public class FuzzySearch
{
    ...

    public static int IndexOf(
        string str,
        string key,
        bool is_str_replace = true,  //strをモット置換する(true)/しない(false)
        bool is_key_replace = true ) //keyをモット置換する(true)/しない(false)
    {
        CompareInfo cmp_info = CultureInfo.CurrentCulture.CompareInfo;
        CompareOptions cmp_opt =
            CompareOptions.IgnoreCase           //大文字・小文字を区別しない
            | CompareOptions.IgnoreKanaType     //ひらがな・カタカナを区別しない
            | CompareOptions.IgnoreWidth        //全角・半角を区別しない
            | CompareOptions.IgnoreNonSpace     //濁点・半濁点を無視
            ;

        //モット置換
        if (is_str_replace) str = FuzzySearch.Replace(str);
        if (is_key_replace) key = FuzzySearch.Replace(key);

        return cmp_info.IndexOf(str, key, cmp_opt);
    }

FuzzySearch.Replace は、モット置換を行うメソッドです。引数で受け取った文字列を、モット置換して返します。何度も同じ文字列をあいまい検索する際に、その都度モット置換すると処理が重くなるため、事前にモット置換しておくような場合に使用します。

public class FuzzySearch
{
    ...

    public static string Replace(string str)
    {
        for (int i = 0; i < replaceMoto.Length; i++)
        {
            str = str.Replace(replaceMoto[i], replaceSaki[i]);
        }

        return str;
    }

データを保持する変数

ファイルから読み込んだCSVデータは、変数へ保存しています。このための変数が、Form1 クラス内の以下の2つです。

//データリスト表示用
private List<string[]> dataListDisp;
//データリスト検索用
private List<string[]> dataListSearch;

dataListDisp は、CSVデータをそのまま保持しており、画面表示用に使用しています。
dataListSearch は、モット置換(小さい'ャ'→通常の'ヤ'など)済みのCSVデータです。キーワード検索の度にモット置換するのではなく、全データを事前にモット置換しておき、処理軽減を図っています。(その分、メモリを消費します。)

両者は、ファイル読み込み時に生成しています。

あいまい検索の実行

実際に、キーワードからデータ内をあいまい検索する処理が、Form1.dispData メソッド内の以下のコードです。(実際のコードから、一部簡略化しています。)

//キーワードをモット置換 ■■■ (1) ■■■
string key_replaced = FuzzySearch.Replace(key);

//行で繰り返し ■■■ (2) ■■■
for (int rr = 0; rr < this.dataListSearch.Count; rr++)
{
    //キーワードが見つかったかどうかのフラグ ■■■ (3) ■■■
    bool is_found = false;

    //現在行の列で繰り返し ■■■ (4) ■■■
    for(int cc = 0; cc < this.dataListSearch[rr].Length; cc++)
    {
        //あいまい検索 ■■■ (5) ■■■
        int pos = FuzzySearch.IndexOf(this.dataListSearch[rr][cc], key_replaced, false, false);
        //見つかった? ■■■ (6) ■■■
        if (pos >= 0)
        {
            is_found = true;
        }
    }

    //見つからなかった? ■■■ (7) ■■■
    if (is_found == false) continue;

    //ListView項目生成
    ListViewItem lv_item = new ListViewItem(this.dataListDisp[rr]);
    //ListViewへ追加 ■■■ (8) ■■■
    this.listView1.Items.Add(lv_item);
}

最初に、検索キーワード自体をモット置換しています(コメント(1))。モット置換済みのキーワードを何度も使用するため、事前に置換しておきます。

検索は、全行、全列に対して1セルずつ行いますので、二重のfor文で処理しています。外側のfor文(コメント(2))で行単位で繰り返し処理し、内側のfor文(コメント(4))で現在行の各列(セル)を1つずつ処理します。

内側のfor文内では、FuzzySearch.IndexOf メソッドを呼び出し(コメント(5))、着目しているセルにキーワードが含まれるかどうか、あいまい検索を実行します。
FuzzySearch.IndexOf メソッド呼び出しの第3、第4引数は、いずれも false としています。これは、第1引数も第2引数も、すでにモット置換済みで、FuzzySearch.IndexOf メソッド内では モット置換をする必要がないことを指示しています。

そして、FuzzySearch.IndexOf で 0以上の値が返されたなら、キーワードが見つかったことになるので、見つかったかどうかのフラグ(is_found)をtrueにします(コメント(6))。なお、このフラグは、内側のfor文の実行前に、事前にfalseで初期化しています(コメント(3))ので、内側のfor文が終了した時点で、is_foundtrueなら現在処理中の行のどこかにキーワードが見つかった(falseなら見つからなかった)、と判断できます。

内側のfor文終了後、現在処理している行のどのセルにもキーワードが見つからなければ(is_foundfalseなら)、continue文により、これ以降のデータ画面表示などの処理をスキップして、次の行の処理へ移ります(コメント(7))。

もしキーワードが見つかっていれば、見つかった行をListViewへ追加します(コメント(8))。ListViewは、CSVデータを一覧表示するために使用しています。

以上により、キーワードが見つかった行のみ、ListViewにて画面上に表示されます。

なお、実際のプログラムでは、見つかったセルの背景色を変更する処理等も行っています。このため、is_foundではない変数を使用して、見つかったかどうかを管理しています。実際のコードで確認してみてください。

実際のコード

C# サンプルプログラムの試し方

実際のコードの動作確認方法は、以下を参照してください。

C# サンプルプログラムの試し方

このうち、今回示すサンプルプログラムでは、以下の手順を実行します。

2.掲載コードをファイル保存 【FuzzySearch.cs、Form1.cs、Form1.Designer.cs】
3.Visual Studio で、プロジェクト(ソリューション)作成 【プロジェクト名:CsvViewerTiny】
5.すべてのタブを閉じる
6.プロジェクトへファイルを追加 【2番の3つのファイル】
8.試しに実行 【テスト用に data.csv あり】

FuzzySearch.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

using System.Globalization;

namespace Gitc
{
    public class FuzzySearch
    {
        //モット置換 置換元
        private static string[] replaceMoto = { 
            "ぁ", "ぃ", "ぅ", "ぇ", "ぉ", 
            "ァ", "ィ", "ゥ", "ェ", "ォ", "ァ", "ィ", "ゥ", "ェ", "ォ", 
            "ゃ", "ゅ", "ょ", "ャ", "ュ", "ョ", "ャ", "ュ", "ョ", "っ", "ッ", "ッ", 
            "-", "-", "ゐ", "ヰ", "ゑ", "ヱ", " ",
        };
        //モット置換 置換先
        private static string[] replaceSaki = { 
            "あ", "い", "う", "え", "お", 
            "ア", "イ", "ウ", "エ", "オ", "ア", "イ", "ウ", "エ", "オ", 
            "や", "ゆ", "よ", "ヤ", "ユ", "ヨ", "ヤ", "ユ", "ヨ", "つ", "ツ", "ツ", 
            "ー", "ー", "い", "イ", "え", "エ", " ",
        };

        /// <summary>
        /// あいまい検索を実行し、キーワードが含まれる文字位置を返す。
        /// </summary>
        /// <param name="str">検索対象文字列</param>
        /// <param name="key">検索キーワード</param>
        /// <param name="is_str_replace">引数strのモット置換をする(true)かしない(false)か</param>
        /// <param name="is_key_replace">引数keyのモット置換をする(true)かしない(false)か</param>
        /// <returns>文字位置。見つからない場合は負の値</returns>
        public static int IndexOf(
            string str, 
            string key, 
            bool is_str_replace = true,  //strをモット置換する(true)/しない(false)
            bool is_key_replace = true)  //keyをモット置換する(true)/しない(false)
        {
            CompareInfo cmp_info = CultureInfo.CurrentCulture.CompareInfo;
            CompareOptions cmp_opt =
                CompareOptions.IgnoreCase           //大文字・小文字を区別しない
                | CompareOptions.IgnoreKanaType     //ひらがな・カタカナを区別しない
                | CompareOptions.IgnoreWidth        //全角・半角を区別しない
                | CompareOptions.IgnoreNonSpace     //濁点・半濁点を無視
                ;

            //モット置換
            if (is_str_replace) str = Replace(str);
            if (is_key_replace) key = Replace(key);

            return cmp_info.IndexOf(str, key, cmp_opt);
        }

        /// <summary>
        /// モット置換(もっとあいまいにするための置換)を行う。
        /// </summary>
        /// <param name="str">置換元文字列</param>
        /// <returns>置換済み文字列</returns>
        public static string Replace(string str)
        {
            for (int i = 0; i < replaceMoto.Length; i++)
            {
                str = str.Replace(replaceMoto[i], replaceSaki[i]);
            }

            return str;
        }
    }
}

Form1.cs

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

using System.IO;
using Gitc;

namespace CsvViewerTiny
{
    public partial class Form1 : Form
    {
        //CSVファイルエンコード
        private Encoding csvEnc = Encoding.UTF8;
        //CSVファイル改行文字
        private string csvNewLine = "\r\n";
        //CSVファイル区切り文字
        private string csvDelim = ",";
        //CSVファイルの拡張子
        private string[] csvExt = { ".csv", ".txt" };
        //CSVデータの1行目が見出しかどうか
        private bool csvFirstLineIsHeaderLine = true;

        //ファイル未読時の表示名
        private const string NO_FILE = "NoFile";

        //データリスト表示用
        private List<string[]> dataListDisp;
        //データリスト検索用
        private List<string[]> dataListSearch;
        //見出し
        private string[] headerLine;
        //列数の最大値
        private int dataColsMax;
        //アプリ名
        private string appName;

        public Form1()
        {
            InitializeComponent();
        }

        private void Form1_Load(object sender, EventArgs e)
        {
            //ドラッグ&ドロップを許可
            this.AllowDrop = true;
            //イベントハンドラ登録
            this.DragDrop += new System.Windows.Forms.DragEventHandler(this.Form1_DragDrop);
            this.DragEnter += new System.Windows.Forms.DragEventHandler(this.Form1_DragEnter);

            //コマンドライン引数
            string[] args = Environment.GetCommandLineArgs();
            //アプリ名
            this.appName = Path.GetFileNameWithoutExtension(args[0]);

            //初期化
            this.clearData();
        }

        //DragEnterイベントハンドラ
        private void Form1_DragEnter(object sender, DragEventArgs e)
        {
            if (e.Data.GetDataPresent(DataFormats.FileDrop)) e.Effect = DragDropEffects.Copy;
            else e.Effect = DragDropEffects.None;
        }

        //DragDropイベントハンドラ
        private void Form1_DragDrop(object sender, DragEventArgs e)
        {
            if (e.Data.GetDataPresent(DataFormats.FileDrop))
            {
                string[] files = (string[])(e.Data.GetData(DataFormats.FileDrop));

                if (File.Exists(files[0]) && this.isCsvExt(files[0])) this.readFile(files[0]);
                else MessageBox.Show("未対応のファイル");
            }
        }

        //CSVファイルの拡張子かどうか
        private bool isCsvExt(string fn)
        {
            string ext = Path.GetExtension(fn);
            foreach (string ee in this.csvExt)
            {
                if (string.Compare(ext, ee, true) == 0) return true;
            }

            return false;
        }

        //キーワードTextBoxのTextChanged
        private void textBox1_TextChanged(object sender, EventArgs e)
        {
            this.dispData(this.textBox1.Text.Trim());
        }

        //ファイル読み込み
        private void readFile(string fn)
        {
            //既存データクリア
            this.clearData();

            try
            {
                //読み込み
                string f_all = File.ReadAllText(fn, this.csvEnc).Trim();
                //ファイル名をタイトルバーへ
                this.Text = Path.GetFileName(fn) + " - " + this.appName;

                //ファイル内容が空?
                if (string.IsNullOrEmpty(f_all)) return;

                //行で分割
                string[] f_lines = f_all.Split(new string[] { this.csvNewLine }, StringSplitOptions.RemoveEmptyEntries);

                //区切り文字
                string[] delim_arr = { this.csvDelim };

                int max_len = 0;
                //行で繰り返し
                foreach (string ll in f_lines)
                {
                    string ll_trim = ll.Trim();
                    //行は空?
                    if (string.IsNullOrEmpty(ll_trim)) continue;

                    //セル分割
                    string[] ll_sep = ll_trim.Split(delim_arr, StringSplitOptions.None);
                    //リストへ追加
                    this.dataListDisp.Add(ll_sep);

                    //モット置換
                    string ll_search = FuzzySearch.Replace(ll_trim);
                    //セル分割
                    string[] ll_sep_search = ll_search.Split(delim_arr, StringSplitOptions.None);
                    //リストへ追加
                    this.dataListSearch.Add(ll_sep_search);

                    //列数
                    if (max_len < ll_sep.Length) max_len = ll_sep.Length;
                }
                //列数の最大値
                this.dataColsMax = max_len;

                //見出し行
                if (csvFirstLineIsHeaderLine)
                {
                    //1行目を見出しに

                    this.headerLine = this.dataListDisp[0];
                    this.dataListDisp.RemoveAt(0);
                    this.dataListSearch.RemoveAt(0);
                }
                else
                {
                    //見出し=A,B,C,...

                    this.headerLine = new string[this.dataColsMax];
                    //列数分繰り返し
                    for (int i = 0; i < this.dataColsMax; i++)
                    {
                        //i >= 26 → AA, AB, ...
                        if (i >= 26)
                            this.headerLine[i] = 
                                Convert.ToChar('A' - 1 + i / 26).ToString() 
                                + Convert.ToChar('A' + i % 26).ToString();
                        //i < 26 → A, B, ...
                        else
                            this.headerLine[i] = Convert.ToChar('A' + i).ToString();
                    }
                }

                //表示
                this.dispData();
            }
            catch (Exception ex)
            {
                MessageBox.Show(ex.Message);
            }
        }

        //データ、ListViewなどクリア
        private void clearData()
        {
            this.dataListDisp = new List<string[]>();
            this.dataListSearch = new List<string[]>();
            this.headerLine = null;
            this.dataColsMax = 0;
            this.Text = NO_FILE + " - " + this.appName;
            this.textBox1.Text = "";

            this.clearListView();
        }

        //ListViewクリア
        private void clearListView()
        {
            this.listView1.Items.Clear();
            this.listView1.Columns.Clear();
        }

        //データをListViewへ表示
        private void dispData(string key = "")
        {
            this.clearListView();

            //列名あり?
            if (this.headerLine != null)
            {
                for (int i = 0; i < this.headerLine.Length; i++)
                {
                    ColumnHeader hh = new ColumnHeader();
                    hh.Text = this.headerLine[i];
                    this.listView1.Columns.Add(hh);
                }
            }

            //データが空?
            if (this.dataListDisp == null || this.dataListDisp.Count == 0) return;

            this.listView1.BeginUpdate();

            //キーワードが空?
            if (string.IsNullOrEmpty(key))
            {
                //全データを表示
                foreach (string[] s_arr in this.dataListDisp)
                {
                    ListViewItem lv_item = new ListViewItem(s_arr);
                    this.listView1.Items.Add(lv_item);
                }
            }
            else
            {
                //キーワード検索して表示

                //キーワードをモット置換
                string key_replaced = FuzzySearch.Replace(key);

                //行で繰り返し
                for (int rr = 0; rr < this.dataListSearch.Count; rr++)
                {
                    //見つけた列のID(色付用)
                    List<int> found_colid = new List<int>();

                    //現在行の列で繰り返し
                    for(int cc = 0; cc < this.dataListSearch[rr].Length; cc++)
                    {
                        //あいまい検索
                        int pos = FuzzySearch.IndexOf(this.dataListSearch[rr][cc], key_replaced, false, false);
                        //見つかった?
                        if (pos >= 0)
                        {
                            //列idを保存
                            found_colid.Add(cc);
                        }
                    }

                    //列id用変数が空?(見つからなかった?)
                    if (found_colid.Count == 0) continue;

                    //ListView項目生成
                    ListViewItem lv_item = new ListViewItem(this.dataListDisp[rr]);
                    //見つかった列(セル)に色付
                    lv_item.UseItemStyleForSubItems = false;
                    for (int  i = 0;  i < found_colid.Count;  i++)
                    {
                        lv_item.SubItems[found_colid[i]].BackColor = Color.Yellow;
                    }
                    //ListViewへ追加
                    this.listView1.Items.Add(lv_item);
                }
            }

            //列幅を自動調整
            this.listView1.AutoResizeColumns(ColumnHeaderAutoResizeStyle.HeaderSize);

            this.listView1.EndUpdate();
        }

    }
}

Form1.Designer.cs


namespace CsvViewerTiny
{
    partial class Form1
    {
        /// <summary>
        /// 必要なデザイナー変数です。
        /// </summary>
        private System.ComponentModel.IContainer components = null;

        /// <summary>
        /// 使用中のリソースをすべてクリーンアップします。
        /// </summary>
        /// <param name="disposing">マネージド リソースを破棄する場合は true を指定し、その他の場合は false を指定します。</param>
        protected override void Dispose(bool disposing)
        {
            if (disposing && (components != null))
            {
                components.Dispose();
            }
            base.Dispose(disposing);
        }

        #region Windows フォーム デザイナーで生成されたコード

        /// <summary>
        /// デザイナー サポートに必要なメソッドです。このメソッドの内容を
        /// コード エディターで変更しないでください。
        /// </summary>
        private void InitializeComponent()
        {
            this.label1 = new System.Windows.Forms.Label();
            this.textBox1 = new System.Windows.Forms.TextBox();
            this.listView1 = new System.Windows.Forms.ListView();
            this.SuspendLayout();
            // 
            // label1
            // 
            this.label1.AutoSize = true;
            this.label1.Location = new System.Drawing.Point(10, 15);
            this.label1.Name = "label1";
            this.label1.Size = new System.Drawing.Size(59, 12);
            this.label1.TabIndex = 0;
            this.label1.Text = "キーワード:";
            // 
            // textBox1
            // 
            this.textBox1.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) 
            | System.Windows.Forms.AnchorStyles.Right)));
            this.textBox1.Location = new System.Drawing.Point(75, 12);
            this.textBox1.Name = "textBox1";
            this.textBox1.Size = new System.Drawing.Size(497, 19);
            this.textBox1.TabIndex = 1;
            this.textBox1.TextChanged += new System.EventHandler(this.textBox1_TextChanged);
            // 
            // listView1
            // 
            this.listView1.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) 
            | System.Windows.Forms.AnchorStyles.Left) 
            | System.Windows.Forms.AnchorStyles.Right)));
            this.listView1.FullRowSelect = true;
            this.listView1.GridLines = true;
            this.listView1.HideSelection = false;
            this.listView1.Location = new System.Drawing.Point(12, 37);
            this.listView1.Name = "listView1";
            this.listView1.Size = new System.Drawing.Size(560, 312);
            this.listView1.TabIndex = 2;
            this.listView1.UseCompatibleStateImageBehavior = false;
            this.listView1.View = System.Windows.Forms.View.Details;
            // 
            // Form1
            // 
            this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 12F);
            this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
            this.ClientSize = new System.Drawing.Size(584, 361);
            this.Controls.Add(this.listView1);
            this.Controls.Add(this.textBox1);
            this.Controls.Add(this.label1);
            this.Name = "Form1";
            this.Text = "Form1";
            this.Load += new System.EventHandler(this.Form1_Load);
            this.ResumeLayout(false);
            this.PerformLayout();

        }

        #endregion

        private System.Windows.Forms.Label label1;
        private System.Windows.Forms.TextBox textBox1;
        private System.Windows.Forms.ListView listView1;
    }
}

data.csv

テスト用のCSVデータです。
文字コード:UTF-8、改行コード:CR+LF
で保存してください。

id,氏名,都道府県,電話番号,メールアドレス,趣味
1,岩田 俊哉,徳島県,090-708-684,vpp9gvum8r@sample.com,キャンプ
2,高谷 順,長崎県,080-650-499,cx61y@example.org,キャンプ
3,河辺 裕之,鹿児島県,080-140-155,dt8xnbvtne@sample.jp,スキー
4,金原 千絵,大分県,090-204-970,s7eopi@sample.co.jp,釣り
5,姫野 真菜,宮城県,080-898-226,c_ovkmew2@example.co.jp,やきゅう
6,吉武 未来,栃木県,090-715-523,fahb3@test.com,読書
7,笠井 亜矢,群馬県,090-771-392,xr6ref@example.jp,刺しゅう
8,曾根 芽生,三重県,080-877-852,kfmzaxxu@test.net,ショッピング
9,冨永 敏幸,新潟県,080-506-934,xm_ibq@example.jp,フェス
10,伊原 千明,青森県,090-303-334,fp3br6@test.com,ゲーム
11,橋本 ゑり子,埼玉県,090-924-311,ghjqf6c@test.co.jp,スポ-ツ観戦
12,堀越 一司,茨城県,080-952-998,tbmnd@example.com,からおけ
13,前島 章,愛知県,090-495-800,e89b6js@sample.co.jp,カラオケ
14,松島 優依,沖縄県,090-580-252,unrpr@test.co.jp,書道
15,日下 亜紀子,群馬県,080-436-023,edxi_8vyml@example.com,ダンス
16,清原 喜一,熊本県,080-678-876,ews9wizrh@test.co.jp,おどり
17,門間 やすし,埼玉県,080-977-645,urkcp@example.com,写真
18,小柳 真奈美,茨城県,080-812-589,ezf7j@example.com,あみ物
19,新川 桂子,和歌山県,090-832-477,jecvgbk1sx@test.com,旅行
20,薄井 未来,東京都,090-016-210,tsmosr@example.net,読書
21,安井 咲奈,長野県,080-353-753,pmqq_h@test.jp,映画
22,吉見 景子,岡山県,080-614-343,qljg76hsji@test.jp,ゲーム
23,大門 柚月,鳥取県,080-996-997,grjxlb@sample.org,バイク
24,藤本 徹,滋賀県,080-943-459,gdjd4feldk@example.co.jp,旅行
25,桜井 信行,山形県,090-006-864,dwgjmmaxa@test.com,スポーツ
26,服部 真帆,愛媛県,080-541-053,wwr0j@sample.jp,旅行
27,山野 新次郎,岩手県,080-126-908,ccto0sf_@sample.jp,カラオケ
28,野間 竜也,広島県,090-968-034,g7x_n@sample.org,ショッピング
29,道下 エマ,福島県,080-719-924,ftjfxekvpi@example.jp,読書
30,結城 紀夫,神奈川県,090-422-541,hll1ar_5k@sample.com,映画鑑賞

Discussion