[Azure Search] Indexerを作る

さて、今日は`Azure Search`の`Indexer`を作ります。　

# Indexerとは
`Indexer`は`Azure Search`で作成した`Index`にデータをデータソースから流し込み機能です。一度だけ流し込むパターンもあれば、定期的（スケジュール）に流し込むこともできます。

## 対応データソース

対応しているデータソースは下記です

- Azure Blob Storage
- Azure Data Lake Storage Gen2 (in preview)
- Azure Table Storage
- Azure Cosmos DB
- Azure SQL Database
- SQL Server on Azure Virtual Machines
- SQL Managed instances on Azure

# サンプルコード

```csharp
public static async Task Main(string[] args)
{
  IConfigurationBuilder builder = new ConfigurationBuilder().AddJsonFile("appsettings.json");
  IConfigurationRoot configuration = builder.Build();

  if (configuration["SearchServiceName"] == "Put your search service name here")
  {
    Console.Error.WriteLine("Specify SearchServiceName in appsettings.json");
    Environment.Exit(-1);
  }

  if (configuration["SearchServiceAdminApiKey"] == "Put your primary or secondary API key here")
  {
    Console.Error.WriteLine("Specify SearchServiceAdminApiKey in appsettings.json");
    Environment.Exit(-1);
  }

  if (configuration["AzureSQLConnectionString"] == "Put your Azure SQL database connection string here")
  {
    Console.Error.WriteLine("Specify AzureSQLConnectionString in appsettings.json");
    Environment.Exit(-1);
  }

  SearchServiceClient searchService = new
    SearchServiceClient(searchServiceName: configuration["SearchServiceName"],
    credentials: new 
    SearchCredentials(configuration["SearchServiceAdminApiKey"]));

  Console.WriteLine("Creating index...");
  Index index = new Index(
    name: "hotels",
    fields: FieldBuilder.BuildForType<Hotel>());
  
  // If we have run the sample before, this index will be populated
  // We can clear the index by deleting it if it exists and creating
  // it again
  bool exists = await searchService.Indexes.ExistsAsync(index.Name);
  if (exists)
  {
    await searchService.Indexes.DeleteAsync(index.Name);
  }

  await searchService.Indexes.CreateAsync(index);

  Console.WriteLine("Creating data source...");

  // The sample data set has a table name of "hotels"
  // The sample data set table has a "soft delete" column named IsDeleted
  // When this column is set to true and the indexer sees it, it will remove the
  // corresponding document from the search service
  // See this link for more information
  // https://docs.microsoft.com/en-us/dotnet/api/microsoft.azure.search.models.softdeletecolumndeletiondetectionpolicy
  // The sample data set uses SQL integrated change tracking for change detection
  // This means that when the indexer runs, it will be able to detect which data has
  // changed since the last run using built in change tracking
  // See this link for more information
  // https://docs.microsoft.com/en-us/sql/relational-databases/track-changes/about-change-tracking-sql-server
  DataSource dataSource = DataSource.AzureSql(
    name: "azure-sql",
    sqlConnectionString: configuration["AzureSQLConnectionString"],
    tableOrViewName: "hotels",
    deletionDetectionPolicy: new SoftDeleteColumnDeletionDetectionPolicy(
    softDeleteColumnName: "IsDeleted",
    softDeleteMarkerValue: "true"));
    dataSource.DataChangeDetectionPolicy = new SqlIntegratedChangeTrackingPolicy();
    // The data source does not need to be deleted if it was already created,
    // but the connection string may need to be updated if it was changed
    await searchService.DataSources.CreateOrUpdateAsync(dataSource);

    Console.WriteLine("Creating Azure SQL indexer...");
    Indexer indexer = new Indexer(
    name: "azure-sql-indexer",
    dataSourceName: dataSource.Name,
    targetIndexName: index.Name,
    schedule: new IndexingSchedule(TimeSpan.FromDays(1)));
    // Indexers contain metadata about how much they have already indexed
    // If we already ran the sample, the indexer will remember that it already
    // indexed the sample data and not run again
    // To avoid this, reset the indexer if it exists
    exists = await searchService.Indexers.ExistsAsync(indexer.Name);
    if (exists)
    {
      await searchService.Indexers.ResetAsync(indexer.Name);
    }

    await searchService.Indexers.CreateOrUpdateAsync(indexer);

    // We created the indexer with a schedule, but we also
    // want to run it immediately
    Console.WriteLine("Running Azure SQL indexer...");

    try
    {
      await searchService.Indexers.RunAsync(indexer.Name);
    }
    catch (CloudException e) when (e.Response.StatusCode == (HttpStatusCode)429)
    {
      Console.WriteLine("Failed to run indexer: {0}", e.Response.Content);
    }

    Console.WriteLine("Press any key to continue...");
    Console.ReadKey();
    Environment.Exit(0);
}
```

## データソースの生成

サンプルコードではIndexの生成から始まっていますが、Indexの生成に関しては前回の投稿を参照ください。

:point_right_tone1:[[Azure Search] で高速テキスト検索](https://qiita.com/syantien/items/8a45cdc7ddfdc4ae9525)

データソースは下記のラインで生成しています。

```csharp
DataSource dataSource = DataSource.AzureSql(
  name: "azure-sql",
  sqlConnectionString: configuration["AzureSQLConnectionString"],
  tableOrViewName: "hotels",
  deletionDetectionPolicy: new SoftDeleteColumnDeletionDetectionPolicy(
  softDeleteColumnName: "IsDeleted",
  softDeleteMarkerValue: "true"));
  dataSource.DataChangeDetectionPolicy = new 
    SqlIntegratedChangeTrackingPolicy();
    // The data source does not need to be deleted if it was already created,
    // but the connection string may need to be updated if it was changed
  
  await searchService.DataSources.CreateOrUpdateAsync(dataSource);
```

DataSourceクラスの詳細はこちらを参照してください。
https://docs.microsoft.com/en-us/dotnet/api/microsoft.azure.search.models.datasource?view=azure-dotnet

今回のサンプルでは`AzureSql`を使っています。`AzureSql`メソッドの詳細はこちらです。
https://docs.microsoft.com/en-us/dotnet/api/microsoft.azure.search.models.datasource.azuresql?view=azure-dotnet

まずはデータソースの名前を指定します。

```csharp
name: "azure-sql",
```

次にSQLへの接続文字列を指定

```csharp
sqlConnectionString: configuration["AzureSQLConnectionString"],
```

次にTable or View の名前を指定します

```csharp
tableOrViewName: "hotels",
```

次に削除されたデータ（Searchから削除をするべきデータ）を指定します。

```csharp
deletionDetectionPolicy: new SoftDeleteColumnDeletionDetectionPolicy(
softDeleteColumnName: "IsDeleted",
softDeleteMarkerValue: "true"));
```

`SoftDeleteColumnDeletionDetectionPolicy`メセッドの詳細は下記です。
https://docs.microsoft.com/en-us/dotnet/api/microsoft.azure.search.models.softdeletecolumndeletiondetectionpolicy?view=azure-dotnet 簡単に言うと `softDeleteColumnName`と `softDeleteMarkerValue`の2フィールドです。

`softDeleteColumnName`でフィールド名を指定し
`softDeleteMarkerValue`で削除を示す値を指定します。

次はデータが変更されたことをトラッキングする手法を指定します。

```csharp
dataSource.DataChangeDetectionPolicy = new 
    SqlIntegratedChangeTrackingPolicy();
```
`DataChangeDetectionPolicy`メソッドの詳細はこちらを参照ください。
https://docs.microsoft.com/en-us/dotnet/api/microsoft.azure.search.models.datachangedetectionpolicy?view=azure-dotnet

ここは、コンストラクターの初期化以外に指定するものがなさそうです。

最後にデータソースを生成します。

```csharp
await searchService.DataSources.CreateOrUpdateAsync(dataSource);
```

## Indexerの生成

次は先ほど作成したデータソースをベースにIndexerを生成します。

```csharp
Indexer indexer = new Indexer(
  name: "azure-sql-indexer",
  dataSourceName: dataSource.Name,
  targetIndexName: index.Name,
  schedule: new IndexingSchedule(TimeSpan.FromDays(1)));

  // Indexers contain metadata about how much they have already indexed
  // If we already ran the sample, the indexer will remember that it already
  // indexed the sample data and not run again
  // To avoid this, reset the indexer if it exists
  exists = await searchService.Indexers.ExistsAsync(indexer.Name);
  if (exists)
  {
    await searchService.Indexers.ResetAsync(indexer.Name);
  }

  await searchService.Indexers.CreateOrUpdateAsync(indexer);
```


こちらが`Indexer`を生成するファンクションです。

```csharp
Indexer indexer = new Indexer(
  name: "azure-sql-indexer",
  dataSourceName: dataSource.Name,
  targetIndexName: index.Name,
  schedule: new IndexingSchedule(TimeSpan.FromDays(1)));
```

ここで`Indexer`の名前を指定します。

```csharp
name: "azure-sql-indexer",
```

先ほど生成したデータソースを指定します

```csharp
dataSourceName: dataSource.Name,
```

ターゲットのIndex名を指定します

```csharp
targetIndexName: index.Name,
```

ここでIndexerが始動するタイミングを指定します。

```csharp
schedule: new IndexingSchedule(TimeSpan.FromDays(1)))
```

`IndexingSchedule`の詳細は下記を参照ください
https://docs.microsoft.com/en-us/dotnet/api/microsoft.azure.search.models.indexingschedule?view=azure-dotnet

下記が基本構成です。

```csharp
IndexingSchedule(TimeSpan, Nullable<DateTimeOffset>)
```

`TimeSpan` (Interval) と `Nullable<DateTimeOffset>` (StartTime)を指定します。StartTimeはオプションのようです。指定しない場合はこのメソッドを読んだ時から（以降）該当するタイミングで実行されます。

TimeSpan に関しては下記を参照ください。
https://docs.microsoft.com/en-us/dotnet/api/system.timespan?view=netframework-4.8

このメソッドが呼ばれた同じ時間（タイミング）で毎日`Indexer`が稼働するようになっています。

## 実行

サンプルは下記のようにIndexerが存在する場合はリセットしていますが、本来必要ありません。サンプルとしてテストする際にすでにIndexerが実行されていると前回からデータソースに更新がないためIndexerの実行がないのでリセットしています。

```csharp
exists = await searchService.Indexers.ExistsAsync(indexer.Name);
if (exists)
{
  await searchService.Indexers.ResetAsync(indexer.Name);
}
```

下記で実行します。

```csharp
await searchService.Indexers.CreateOrUpdateAsync(indexer);
```

上記でスケジュールはセットされましたが、次のスケジュールまで実行されないため、ここでスケジュールと関係なく実行するように指示します。

```csharp
// We created the indexer with a schedule, but we also
// want to run it immediately
Console.WriteLine("Running Azure SQL indexer...");

try
{
　　await searchService.Indexers.RunAsync(indexer.Name);
}

catch (CloudException e) when (e.Response.StatusCode == (HttpStatusCode)429)
{
　　Console.WriteLine("Failed to run indexer: {0}", e.Response.Content);
}

Console.WriteLine("Press any key to continue...");
Console.ReadKey();
Environment.Exit(0);
```

# 最後に

`Azure Search`はウェブサービスを作る際にとても便利なサービスです。よく使う場合はマイライブラリー化して使うことをお勧めします。

# 参照

Indexer Overview
https://docs.microsoft.com/en-us/azure/search/search-indexer-overview