Bing Search v7 クライアント ライブラリがないので、実装してみた

# はじめに

Azure Cognitive Services のカテゴリの一つに Web 検索があります。Web 検索では、Microsoft 社の Web 検索エンジンである Bing の検索を API 経由で利用できる Bing Search API がありましたが、これが、2020 年 10 月 30 日以降、利用できなくなったようです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172205/582abf04-f438-1f57-3468-394eeac9451a.png)
これ以前に、Azure Cognitive Services 内で Bing Search をインスタンス化している場合は、以降 3 年間は利用できるようですが、Azure Cognitive Services の Bing Search を新規に作成、利用することはできなくなりました。

今後、新規に Bing Search API を利用するには、Azure マーケットプレイスにある "Bing Search v7" を利用します。公式ページでも新規利用時のサービス有効化方法として、以下のページが案内されています。

- [Create Bing Search resource through Azure Marketplace](https://docs.microsoft.com/en-us/bing/search-apis/bing-web-search/create-bing-search-service-resource)

これまでとの変更点としては、Bing Search API のエンドポイントが、以下のように変更されました。ドメインの cognitive 部分が、bing に変更されています。

- 移行前: https://api.cognitive.microsoft.com/
- 移行後: https://api.bing.microsoft.com/

.NET から、Bing Search API を呼び出す場合は、SDK(クライアント ライブラリ) が利用できました。WebSearch を行いたい場合は、"Microsoft.Azure.CognitiveServices.Search.WebSearch" というライブラリが NuGet 経由で入手可能で、これを使って非常に簡単に、Bing Search API を利用できたのですが、2020 年 11 月 21 日時点では、新しいエンドポイントに対して、このライブラリが正しく動作しません。
現状では、Bing Search API クライアント ライブラリの最終更新日は、2018/03/22 となっており、今回の移行にあわせたクライアント ライブラリの更新が行われていないようでした。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172205/48dacd83-d065-8047-80ff-f299241f4ece.png)

よって、今回は、Bing Search API をクライアント ライブラリを使わず、利用する方法を説明したいと思います。HttpClient で、直接 API をコールします。今回は、.NET 5 を使用しましたが、.NET Core 3.1 でも動作すると思います。

# Bing Search v7 の有効化

Azure ポータルの [リソースの作成] から、"bing search" を検索し、[Bing Search v7] を選択して、Bing Search のインスタンスを作成します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172205/251940fb-cd06-4346-1d4c-6c24a793c568.png)
価格レベルについては、以下のサイトに記載があります。無料の Free も選択できて、1,000 トランザクション/月まで利用が可能です。

- [Bing Search API Pricing](https://www.microsoft.com/en-us/bing/apis/pricing)

Bing Search のインスタンスが作成されたら、[キーとエンドポイント] を選択し、[キー 1] または、[キー 2] の文字列をコピーしておきます。このキーは、後ほど、API 呼び出しの際に、HTTP ヘッダーに入れて使用します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172205/e722f12f-1406-d137-98cc-87dcf066fdb1.png)

# BingClient クライアント ライブラリの実装

以降で、.NET Core コンソール アプリ(.NET 5)で、実装を行っていきます。
まず、BingClient クラスと、検索結果を格納する各クラスを定義します。

```csharp:BingClient.cs

public class BingClient
{
    // Web ページ検索結果
    public class WebPage
    {
        public string Name;
        public string Url;
        public string Snippet;
        public DateTimeOffset DateLastCrawled;
    }

    // 画像検索結果
    public class Image
    {
        public string Name;
        public string ThumbnailUrl;
        public string ContentUrl;
        public DateTimeOffset DatePublished;
    }

    // 動画検索結果
    public class Video
    {
        public string Name;
        public string ThumbnailUrl;
        public string ContentUrl;
        public string Description;
        public string Publisher;
        public string Creator;
        public long? ViewCount;
        public DateTimeOffset DatePublished;
    }
}

```

次に、BingClient クラスのプライベートフィールドとコンストラクタを実装します。コンストラクタでは、Bing Search API のサブスクリプション キーを受け取りフィールドに保存しておきます。
また、HTTP 要求のために、HttpClient のインスタンスもフィールドに保存しておきます。

```csharp:BingClient.cs

public class BingClient
{
    ....
    
    private static readonly HttpClient _httpClient = new HttpClient();

    private readonly string _endpoint = "https://api.bing.microsoft.com/v7.0";
    private readonly string _subscriptionKey = null;

    public BingClient(string subscriptionKey)
    {
        this._subscriptionKey = subscriptionKey;
    }
}

```

次に、BingClient クラスに各検索を実行するメソッドを実装していきます。レスポンスは、JSON で返ってきますので、Json.NET で処理します。コードを実装する前に、Json.NET(Newtonsoft.Json) を NuGet パッケージでインストールしておいてください。
先に、Search メソッドで、各検索要求の共通部分を実装しておきます。

```csharp:BingClient.cs

public class BingClient
{
    ....

    private async Task<dynamic> Search(string url, string subscriptionKey)
    {
        HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, new Uri(url));
        request.Headers.Add("Ocp-Apim-Subscription-Key", subscriptionKey);

        HttpResponseMessage response = await _httpClient.SendAsync(request);
        response.EnsureSuccessStatusCode();

        string responseString = await response.Content.ReadAsStringAsync();
        dynamic json = JsonConvert.DeserializeObject<dynamic>(responseString);

        return json;
    }
}

```
次に、Web ページ検索を実行する SearchWeb メソッドを実装します。引数に、検索キーワード、マーケットコード、検索結果数をとります。
マーケットコードは、[Market and language codes used by Bing Web Search API](https://docs.microsoft.com/en-us/bing/search-apis/bing-web-search/reference/market-codes) に定義があります。
また、検索結果の各プロパティは、最小限のものを入れていますので、必要に応じて、[Web Search API v7 response objects
](https://docs.microsoft.com/en-us/bing/search-apis/bing-web-search/reference/response-objects) の "WebPage" を参照して、クラス、処理を拡張してください。

```csharp:BingClient.cs

public class BingClient
{
    ....

    public async Task<IEnumerable<WebPage>> SearchWeb(string keyword, string market = "ja-JP", int resultCount = 10)
    {
        string endpoint = $"{_endpoint}/search";
        string url = $"{endpoint}?q={Uri.EscapeDataString(keyword)}&mkt={market}&responseFilter=Webpages&count={resultCount}";

        dynamic json = await this.Search(url, _subscriptionKey);

        var webPages = (json.webPages.value as IEnumerable<dynamic>).Select(page =>
            new WebPage()
            {
                Name = page.name,
                Url = page.url,
                Snippet = page.snippet,
                DateLastCrawled = DateTime.SpecifyKind(page.dateLastCrawled.Value, DateTimeKind.Utc),
            });

        return webPages;
    }
}

```

同様に、画像検索、動画検索も実装していきます。
画像検索の結果の各プロパティの定義は、[Image Search APIs v7 response objects](https://docs.microsoft.com/en-us/bing/search-apis/bing-image-search/reference/response-objects) の Image に、動画検索の結果の各プロパティは、[Video Search APIs v7 response objects
](https://docs.microsoft.com/en-us/bing/search-apis/bing-video-search/reference/response-objects) の Video を参照してください。

```csharp:BingClient.cs

public class BingClient
{
    ....

    public async Task<IEnumerable<Image>> SearchImage(string keyword, string market = "ja-JP", int resultCount = 10)
    {
        string endpoint = $"{_endpoint}/images/search";
        string url = $"{endpoint}?q={Uri.EscapeDataString(keyword)}&mkt={market}&count={resultCount}";
            
        dynamic json = await this.Search(url, _subscriptionKey);

        var images = (json.value as IEnumerable<dynamic>).Select(img => 
            new Image()
            {
                Name = img.name,
                ThumbnailUrl = img.thumbnailUrl,
                ContentUrl = img.contentUrl,
                DatePublished = DateTime.SpecifyKind(img.datePublished.Value, DateTimeKind.Utc),
            });

        return images;
    }

    public async Task<IEnumerable<Video>> SearchVideo(string keyword, string market = "ja-JP", int resultCount = 10)
    {
        string endpoint = $"{_endpoint}/videos/search";
        string url = $"{endpoint}?q={Uri.EscapeDataString(keyword)}&mkt={market}&count={resultCount}";

        dynamic json = await this.Search(url, _subscriptionKey);

        var videos = (json.value as IEnumerable<dynamic>).Select(vdo =>
            new Video()
            {
                Name = vdo.name,
                ThumbnailUrl = vdo.thumbnailUrl,
                ContentUrl = vdo.contentUrl,
                Description = vdo.description,
                Publisher = vdo.publisher?[0]?.name,
                Creator = vdo.creator?.name,
                ViewCount = vdo.viewCount,
                DatePublished = DateTime.SpecifyKind(vdo.datePublished.Value, DateTimeKind.Utc),
            });           

        return videos;
    }

```

# BingClient クライアント ライブラリの利用

実装した BingClient クライアント ライブラリを利用します。
"yourKey" には、先に Azure ポータルで有効化した Bing Search v7 のキーを設定してください。

```Program.cs

class Program
{
    static async Task Main(string[] args)
    {
        string subscriptionKey = "<yourKey>";            
        string keyword = "猫";

        BingClient bingClient = new BingClient(subscriptionKey);

        var webPages = await bingClient.SearchWeb(keyword);
        webPages.ToList().ForEach(w => 
        {
            Console.WriteLine($"ページ タイトル: {w.Name}");
            Console.WriteLine($"概要: {w.Snippet}");
            Console.WriteLine($"URL: {w.Url}");
            Console.WriteLine();
        });            

        var images = await bingClient.SearchImage(keyword);
        images.ToList().ForEach(img =>
        {
            Console.WriteLine($"画像タイトル: {img.Name}");
            Console.WriteLine($"URL: {img.ContentUrl}");
            Console.WriteLine();
        });

        await Task.Delay(1000);

        var videos = await bingClient.SearchVideo(keyword);
        videos.ToList().ForEach(v =>
        {
            Console.WriteLine($"動画タイトル: {v.Name}");
            Console.WriteLine($"概要: {v.Description}");
            Console.WriteLine($"サムネイル URL: {v.ThumbnailUrl}");
            Console.WriteLine($"URL: {v.ContentUrl}");
            Console.WriteLine($"視聴数: {v.ViewCount}");
            Console.WriteLine();
        });

        var news = await bingClient.SearchNews(keyword);
        news.ToList().ForEach(n =>
        {
            Console.WriteLine($"ニュース タイトル: {n.Name}");
            Console.WriteLine($"概要: {n.Description}");
            Console.WriteLine($"サムネイル URL: {n.ThumbnailUrl}");
            Console.WriteLine($"URL: {n.Url}");                
            Console.WriteLine();
        });

        Console.ReadKey();
    }
}

```

実行結果は以下のようになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172205/a8fcfe96-8efd-d673-7b4c-565eab896996.png)

完全なコードは、以下の GitHub リポジトリに登録してあります。

https://github.com/Hiromasa-Masuda/BingSearchClientLibrary

# 参考サイト

- [Upgrade from Bing Web Search API v5 to v7](https://docs.microsoft.com/en-us/bing/search-apis/bing-web-search/bing-web-upgrade-guide-v5-to-v7)
- [Bing Search API returns the "Resource Not Found" error
](https://docs.microsoft.com/en-us/answers/questions/173628/bing-search-api-returns-the-34resource-not-found34.html)