【AI人材検索×C#①】LLMで自由文を検索キーワードへ変換する ~SQL Serverの部分一致とプロンプト設計~【C#実践アプリ開発ラボ】
「組み込みの経験がある人は?」──こう聞かれて、社員名簿から答えを返せる検索を作ります。
この記事では、利用者が入力した検索文をLLM(文章を理解・生成するAIモデル)に検索条件へ変換し、データベースの部分一致で候補を絞る仕組みを、C#の実コードで確かめます。
この記事は、次のような方に向いています。
- 社内の文書や履歴を「自然文で探せる」ようにしたい方
- RAG(検索で集めた情報をLLMに渡して答えを作る仕組み)を作ろうとして、検索の精度で詰まっている方
- C#の基本が分かり、AI連携を業務データに使ってみたい方
- ローコードでスキル検索を作ろうとして、うまく当たらない方
(おおまかな考え方の部分は参考になると思います)
全3回の連載です。AI人材検索を題材として、同じ10名のデータと同じ4つの依頼で比べます。
- 第1回:キーワード検索
- 第2回:ベクトル検索
- 第3回:RAG(検索拡張生成)
順に試すことで、AIを使った自然文による業務データ検索と、RAGの基本を学べます!
使用したコードと実測ログは第1~3回分をまとめて、GitHubで公開しています。
課題 ── 自然な言葉で業務データを探すと、検索条件が決まらない
社内には、人材の経歴、問い合わせ履歴、手順書、案件実績など、多くの業務データが蓄積されています。
それでも探せないのは、データに書かれた言葉と、利用者が尋ねる言葉が違うからです。
利用者はデータベースの語彙で質問しない
利用者は、テーブルの列名や登録済みの技術名を知りません。自分の仕事の言葉で、次のように尋ねます。
「組み込みの経験がある人は?」
と聞かれたとき、データベースに入っているのが「組込み」なのか「組み込み」なのかは、質問する人にとってはどうでもいいことです。
ところが検索する側の都合では、この違いが決定的です。「組み込み」で探すと「組込み」に当たりません。
「クラウド」で探しても、紹介文に「Azure」としか書いていない人は見つかりません。書き手が自由に書いた文章を相手にすると、この食い違いが起きやすくなります。
これは、タレントマネジメント(人材情報を配置や育成に生かす取り組み)の定番機能でもあります。人事システムのスキル検索がうまく当たらない、という相談はよく聞きます。
人材検索は、業務検索の縮小モデルである
この記事の題材は人材検索ですが、扱う問題はもっと広く、利用者の検索文に合う情報を、既存の業務データからどう探すかです。
同じ問題は、次のような検索でも起こります。以下は代表例であり、検索用途を網羅した分類ではありません。
| 検索の用途 | 利用者の検索文 | 検索する業務データ |
|---|---|---|
| スキル・人材検索 | 車載ソフトの経験者を探す | プロフィール、職務経歴 |
| 社内ナレッジ検索 | 障害時の切り戻し手順を探す | 手順書、過去の障害報告 |
| 問い合わせ履歴検索 | 同じ請求トラブルの対応を探す | チケット、メール要約 |
| 案件検索 | クラウド未経験でも担当できる案件を探す | 案件説明、募集条件 |
| 製品検索 | オフラインでも使える製品を探す | 製品仕様、カタログ |
いずれも、利用者が仕事の言葉で入力した検索文と、書き手や詳しさの異なる業務データを照合します。検索文と業務データで使われる言葉が一致しないことが共通の難しさです。
人材検索を選んだのは、表記ゆれ・該当なし・否定・語彙のずれという4つの難しさを、10名という小さなデータで一通り再現できるからです。
今回使う10名と4つの依頼
検索対象は、次の10名の紹介文です。全件を先に示すことで、どの言葉が検索に当たるのかを具体的に確認できます。
| 氏名 | 紹介文 |
|---|---|
| 佐藤 健一 | 10年以上C#でバックエンド開発を担当。ASP.NET CoreとAzureを用いた大規模Webサービスの設計・運用経験が豊富。 |
| 鈴木 美咲 | フロントエンドエンジニア。ReactとTypeScriptが得意で、デザインシステムの構築やアクセシビリティ改善の実績あり。 |
| 松本 涼 | バックエンド担当。社内の勉強会を毎週開いていて、入ったばかりのメンバーのコードレビューを引き受けることが多い。 |
| 高橋 大輔 | データサイエンティスト。Pythonによる機械学習モデルの開発、需要予測や異常検知プロジェクトをリード。 |
| 伊藤 翔太 | インフラエンジニア。KubernetesとTerraformによるクラウド基盤構築が専門。SREとして可用性改善に従事。 |
| 井上 拓海 | オンプレのサーバー運用が長い。監視とバックアップ設計、深夜の障害対応まで一通りやってきた。 |
| 岡田 里奈 | 社内システムのサーバ移行を担当。オンプレからAzureへ、止められない業務を止めずに運んだ。 |
| 加藤 修平 | 組込みエンジニア。C/C++での車載ソフトウェア開発が専門。リアルタイムOSと通信プロトコルに精通。 |
| 村上 早紀 | 組み込み機器のユーザーインターフェース設計。小さい画面と少ないボタンで迷わせない工夫を考えてきた。 |
| 中村 亮 | セキュリティエンジニア。脆弱性診断とペネトレーションテストを担当。CISSP保持。 |
「サーバ/サーバー」「組込み/組み込み」の表記ゆれは意図的です。元データはData.csで確認できます。
この連載では次の4つの依頼を、全3回を通して使い続けます。
| 依頼 | この連載で確かめること |
|---|---|
| A 組み込みの経験がある人は? | 表記ゆれを吸収できるか |
| B Salesforceの導入案件が来た。経験者はいる? | 該当者がいないと判断できるか |
| C 経験の浅いメンバーを指導できる人は? | 字面が違っても意味で探せるか |
| D クラウドの経験がないと思われる人は? | 否定を検索条件として扱えるか |
同じ4つの検索文を使い続け、3つの方式で結果がどう変わるかを比べます。
検索文の語と、業務データ内の語は一致するとは限りません。そのため、両者の言葉のずれを埋める仕組みが必要です。
解決アプローチ ── 照合の基準を何にするか
では、何を基準に照合すればよいでしょうか。検索方法はこの3つだけではなく、メタデータ検索や複数方式の組み合わせもあります。
本連載では比較対象として、文字列・意味ベクトル・AIの読解を基準にする3方式を扱います。
本連載で比べる3つの方式 ── 字面で探す・意味で探す・読んで選ぶ
違いは、何を基準に候補を決めるかです。

①は文字列、②は意味を数値にしたベクトル、③はAIの読解を基準にします。①と②は条件に合う候補を検索し、③は集めた候補をAIが読んで選びます。
本記事が扱うのは①です。③の答えは、どの候補を渡すかに左右されます。そこで①、②の順に候補を集める方法を確かめ、最後に③へ進みます。
方式① キーワード検索 ── 条件を作る側と、照合する側に分ける
方式①では、LLMが検索条件を作り、データベースがその条件で照合します。

ここで大事なのは、AIは候補者を選んでいないということです。AIがするのは「組み込みの経験がある人は?」という文を、照合できる語へ翻訳することだけ。
どの紹介文に当たるかを決めるのは、データベース側の文字列照合です。
検索条件は2つのリストでできています。含める語は、そのうち1つでも紹介文に含まれていれば候補になります。除く語は、1つでも含まれていれば候補から外れます。
この2つだけで、表記ゆれも否定も扱えます。
AIに直接「誰が該当しますか」と聞いたほうが早くない?
10名ならそれで動きます。でも1万名の紹介文はプロンプトに入りません。だから絞る必要があって、その絞り方の話が今回なんです。
実装 ── 方式①を、C#とSQL Serverで組み立てる
ここから、方式①を図3の2つの役割に分けて実装します。

LLMを動かすプラットフォームはMicrosoft Foundryです。ここへ gpt-5.6-luna をデプロイし、C#の Azure.AI.OpenAI 2.1.0 から呼び出します。(参考)
モデルの呼び出し方と、JSONで応答させる方法は次の2記事で扱っています。本記事では検索方式の実装に絞ります。
APIキーはuser-secrets(開発用の秘密情報ストア)に置き、appsettings.jsonには書きません。
Foundryリソースとモデルは、リポジトリのBicep(Azureの構成をコードで定義する仕組み)で作成できます。
モデルは、キーワード生成の品質を実データで確かめたうえで選びます。今回は品質が足りていたので、コストの低い gpt-5.6-luna にしました。
Standard Global(short context)の料金は、100万トークンあたり入力0.20ドル、出力1.20ドルです(公式発表・2026年8月30日時点)。
最新の価格はAzure OpenAI Serviceの価格ページで確認してください。
実行は2行です。seed でデータベースとサンプル10名を用意し、keyword で検索します。
dotnet run --project src/HrMatchingSearch -- seed
dotnet run --project src/HrMatchingSearch -- keyword "組み込みの経験がある人は?"検索対象はData.csに定義した10名の紹介文(Bio 列)だけです。表記ゆれは、検索の難しさを再現するために意図的に残しています。
LLMに作らせる ── 部分一致の癖から、6つの規則を決める
プロンプトを書く前に、照合する側の性質を先に見ておきます。どんな語を出させるべきかは、SQLの部分一致がどう当たるかで決まるからです。

同じ「表記ゆれ」でも、打ち手が違います。末尾の長音は、短いほうの「サーバ」を1語出せば「サーバー」にも当たります。共通部分がそのまま語として通用するからです。
ところが語の途中で送り仮名がずれると、この手が使えません。「組込み」と「組み込み」の共通部分は「込み」だけで、これは「申し込み」「振り込み」にも当たってしまいます。
語としての意味を失うので、検索語には使えません。だから今回の部分一致方式では、両方の表記を出します。
英数字1文字も検索語にできません。「C」は C#、C++、CISSPのすべてに当たります。短すぎる文字列は、語の意味を保てないからです。
ここまでの性質を、LLMへの規則に書き写します。部分一致で取りこぼしと誤検出を抑えるために採用した6つです。
| 規則 | 根拠 | 代表例 |
|---|---|---|
| 長音は最短形にする | 短い語が両表記に含まれる | サーバ |
| 語中の送り仮名は両方出す | 共通部分が語の意味を失う | 組込み / 組み込み |
| 略語と正式名は両方出す | 字面が異なる | DB / データベース |
| 一般語を出さない | 候補が広がりすぎる | 経験 を除く |
| 英数字1文字を出さない | 他の語の一部に当たる | C を除く |
| 否定の依頼は除く語に置き換える | 「〜がない人」は照合できないが、「〜がある人を除く」なら照合できる | クラウド未経験 → クラウド を除く語に出す |
出力の形式はJSONに固定します。ふつうの文章で返させると、含める語と除く語をC#側で安定して取り出せないためです。プロンプト全文はkeyword.txtにあります。
C#側は、この規則を書いたプロンプトと検索文を渡し、返ってきたJSONを読むだけです。
次はKeywordSearch.csからの抜粋です。JSON配列を取り出す補助メソッドは省略しています。
public static async Task<Keywords> GenerateKeywordsAsync(SearchEnv env, string query, string? variant = null)
{
var completion = await env.Chat.CompleteChatAsync(
[
new SystemChatMessage(Prompts.Keyword(variant)), // 6つの規則を書いたプロンプト
new UserChatMessage(query), // 利用者の検索文
],
// JSONオブジェクトで返すよう指定する
new ChatCompletionOptions { ResponseFormat = ChatResponseFormat.CreateJsonObjectFormat() });
using var doc = JsonDocument.Parse(completion.Value.Content[0].Text.Trim());
return new Keywords(ReadStrings(doc.RootElement, "include"), ReadStrings(doc.RootElement, "exclude"));
}
DBで照合する ── 返ってきた条件を、機械的に当てる
LLMが返すのは、含める語と除く語を分けた次のJSONです。
{"include": ["組み込み", "組込み"], "exclude": []}これを、2つのリストを一組にして持つ Keywords 型に受け取ります。
public record Keywords(List<string> Include, List<string> Exclude);あとは、この2つのリストを2つの Where へ順に渡すだけです。
照合の実装は、同じ KeywordSearch.cs にあります。次の抜粋では、説明と対応させるためにコメントへ (1)(2) の番号だけを加えています。
public static async Task<List<Candidate>> SearchByKeywordsAsync(SearchEnv env, Keywords keywords)
{
var (include, exclude) = (keywords.Include, keywords.Exclude);
await using var db = env.OpenDb();
return await db.People
// (1) include が空なら全員が対象。空でなければ、どれか1語を含む人だけ
.Where(p => include.Count == 0 || include.Any(kw => p.Bio.Contains(kw)))
// (2) exclude のどれかを含む人を落とす
.Where(p => !exclude.Any(kw => p.Bio.Contains(kw)))
.Select(p => new Candidate(p.Name, p.Bio))
.ToListAsync();
}(1) は、含める語が空なら全員を通し、空でなければどれか1語を含む人だけを残します。この「空なら全員」が、あとで「〜がない人」を探すときに効いてきます。
(2) は、除く語を1つでも含む人を落とします。
この2つの照合に、意味の判断は入っていません。候補を決めているのは、紹介文にその文字列が含まれるかどうかだけです。
なお Contains は、EF Core 10のSQL Serverプロバイダーが LIKE N'%...%' へ翻訳します(公式の関数マッピング)。
最終的にはこのコードはSQLクエリになって、DB検索した結果が返ってくるってことだね!
実験結果 ── 方式①に、4つの依頼を投げる
できあがった検索に、4つの依頼を投げます。以下はすべて2026年9月1日に各3回ずつ実行した結果で、3回とも同じ件数でした(実測ログ)。
取り上げる順はA→D→B→Cです。規則が効いた2件を先に見て、同じ0件でも意味が正反対のBとCを並べて対比します。
| 依頼 | 結果 | 判定 |
|---|---|---|
| A 組み込みの経験がある人は? | 2件 | ◎ 表記ゆれを越えた |
| D クラウドの経験がないと思われる人は? | 7件 | ◎ 否定を除く語に変えられた |
| B Salesforceの導入案件が来た。経験者はいる? | 0件 | ◎ 記載がないので正しい |
| C 経験の浅いメンバーを指導できる人は? | 0件 | ✕ 該当者がいるのに落とした |
A 組み込み ── 表記ゆれを越えて2名
「組み込み」と「組込み」の両方が出て、送り仮名の違う2人に当たりました。次は1回目の出力から、紹介文だけを短くした抜粋です。
検索文: 組み込みの経験がある人は?
含めるキーワード: 組み込み, 組込み, 組み込み開発
--- キーワード検索結果: 2件 ---
・加藤 修平: 組込みエンジニア。C/C++での車載ソフトウェア開発が専門。…
・村上 早紀: 組み込み機器のユーザーインターフェース設計。…なぜ2語必要なのかは図4のとおりです。「組み込み開発」は誰にも当たらなかったため、この実測では結果に影響しませんでした。
今回は無害でしたが、余分な語が別の人の紹介文に当たれば誤検出になります。一般語や英数字1文字を出さないという規則は、この広がりを抑えるためのものです。
D クラウド未経験 ── 否定を除く語に変えて7名
「〜がない人」は、探す語ではなく除く語に変換します。含める語は空になり、除く語だけが働きます。次は1回目の出力から、紹介文だけを短くした抜粋です。

検索文: クラウドの経験がないと思われる人は?
含めるキーワード: (指定なし)
除くキーワード: クラウド, AWS, Amazon Web Services, Azure, Microsoft Azure, GCP,
Google Cloud Platform, Google Cloud, IBM Cloud, Oracle Cloud, Alibaba Cloud
--- キーワード検索結果: 7件 ---
・鈴木 美咲: フロントエンドエンジニア。ReactとTypeScriptが得意で、…
・松本 涼: バックエンド担当。社内の勉強会を毎週開いていて、…
・高橋 大輔: データサイエンティスト。Pythonによる機械学習モデルの開発、…
・井上 拓海: オンプレのサーバー運用が長い。監視とバックアップ設計、…
・加藤 修平: 組込みエンジニア。C/C++での車載ソフトウェア開発が専門。…
・村上 早紀: 組み込み機器のユーザーインターフェース設計。…
・中村 亮: セキュリティエンジニア。脆弱性診断とペネトレーションテストを担当。…含める語が空なので、まず10名全員が対象になり、そこから除く語が佐藤・伊藤・岡田を落として7名になりました。
B Salesforce ── 該当なしを、正しく返す
Salesforce ほか関連語で探して0件。10名の紹介文に該当する記載がないため、この検索実験では正しい答えです。
C 指導できる人 ── 0件。だが適任者はいる
今回、これだけうまくいきませんでした。
以下のように、生成された語は妥当でした。
含めるキーワード: メンター, 新人教育, OJT, 育成
--- キーワード検索結果: 0件 ---
該当なしところが適任者はいます。松本さんの紹介文です。
バックエンド担当。社内の勉強会を毎週開いていて、入ったばかりのメンバーのコードレビューを引き受けることが多い。
「メンター」「新人教育」「OJT」「育成」のいずれも書かれていません。やっていることは指導そのものなのに、生成されたキーワードは1つも一致しません。
表記ゆれとは種類の違う問題です。紹介文にその語がない以上、どんな検索語を作っても当たりません。
同じ0件でも、BとCでは意味が正反対です。
| 依頼 | 結果 | 判定 |
|---|---|---|
| B Salesforceの導入案件が来た。経験者はいる? | 0件 | 正しい。10名の紹介文に記載がない |
| C 経験の浅いメンバーを指導できる人は? | 0件 | 誤り。適任の松本さんを落とした |
「勉強会」「コードレビュー」を辞書に足せば(プロンプトに追加すれば)、松本さんは拾えます。ただし別の言い回しが来るたびに足すことになり、解決にはなりません。
そこで第2回では、文字列ではなく、紹介文と依頼の意味を距離として測る方法へ進みます。
考察 ── 規則は、照合する側の性質から決まる
実装の章で並べた6つの規則は、思いつきで足したものではありません。規則を外した版で何が起きるかを、実際に測ってあります。
規則を減らした版もリポジトリにあり、引数で切り替えて再現できます。
規則を何も書かず「キーワードを作ってください」とだけ頼んだ素の版で、依頼Aを実行します。
$ dotnet run --project src/HrMatchingSearch -- keyword "組み込みの経験がある人は?" --prompt naive
含めるキーワード: 組み込み
--- キーワード検索結果: 1件 ---
・村上 早紀: 組み込み機器のユーザーインターフェース設計。…質問文の「組み込み」をそのまま返してきました。「組込み」と書いている加藤さんには届かず、結果は1件です。
6つの規則を与えた完成版と並べます。どちらも3回ずつ実行しました。
| 版 | 生成された語 | 結果 |
|---|---|---|
| 素の版(規則なし) | 3回とも1語 ── 組み込み | 1件 ×3(村上のみ) |
| 完成版(6規則) | 毎回3〜5語 ── 例)組み込み, 組込み, 組み込み開発 | 2件 ×3(加藤・村上) |
素の版では一度も正しい結果を得られず、完成版では3回とも正解でした。完成版が3回に出した語です。
1回目: 組み込み, 組込み, 組み込み開発
2回目: 組み込み, 組込み, 組み込み開発, 組込み開発, Embedded
3回目: 組み込み, 組込み, 組み込み開発, 組込み開発語は回ごとに増えたり減ったりします。それでも当たったのは3回とも加藤・村上の2名でした。
揺れないことを目指すんじゃなくて、揺れが答えに影響しにくいようにするんですね。
そのとおりです。生成される語は固定できません。規則が抑えているのは、揺れが検索結果へ与える影響のほうです。
依頼Dでも同じ形です。除く語を「クラウド」だけにした版と、完成版を並べます。
| 版 | 生成された除く語 | 結果 |
|---|---|---|
| 展開なしの版(概念を展開しない) | 3回とも1語 ── クラウド | 9名 ×3(佐藤・岡田が残る) |
| 完成版 | 毎回11〜12語 ── 例)クラウド, AWS, Azure, GCP … | 7名 ×3(正解) |
字面で照合する以上、概念も語として列挙しないと届きません。
プロンプトの規則は、LLMの側ではなく照合する側から決まります。部分一致が何に当たって何に当たらないか。6つの規則は、すべてそこから導いたものです。
まとめ
4つの依頼のうち3つに、正しく答えられました。
- 役割を分ける:LLMが検索文を語へ翻訳し、データベースが文字列で照合する
- 規則は照合する側から決まる:部分一致が何に当たるかで、出すべき語が決まる
- 根拠が残る:照合に使った語と除外条件を確認でき、「該当なし」も返せる
この形が使えるのは、利用者の言葉を、データに実際に書かれている語へ言い換えられる場合です。
ナレッジ検索や問い合わせ履歴の検索も、同じ形に当てはまります。
残った宿題は依頼Cです。
| 依頼C | 経験の浅いメンバーを指導できる人は? |
| 松本さんの紹介文 | バックエンド担当。社内の勉強会を毎週開いていて、入ったばかりのメンバーのコードレビューを引き受けることが多い。 |
依頼にある「指導」も、生成された「メンター」「OJT」も、紹介文には1語も出てきません。
やっていることは指導そのものなのに、字面が一つも重なりません。検索語を正しく作っても、紹介文にその語がなければ一致しません。
次回は紹介文と依頼から埋め込みベクトル(文章の意味を表す数値の並び)を作り、文字列の一致ではなく意味の近さで候補を探します。図1でいう方式②です。
引き続き、AIを使った自然文による業務データ検索について、一緒に学んでいきましょう!

LLMに作らせる ── 部分一致の癖から、6つの規則を決める
DBで照合する ── 返ってきた条件を、機械的に当てる



