Find Us and Follow Us

Keeping your Azure Search Index up-to-date with Azure Functions
Azure Search

Keeping your Azure Search Index up-to-date with Azure Functions

Content type Blog Post
Author Christos Matskas
Publication Date 1 Mar, 2018
Reading Time Less than 1 minute

Introduction

Azure Search Index can work really well with Azure Blob Storage. It can automatically index and analyse documents uploaded to a Storage Container to make it easy for you to expose the data in your application.When working with Blob data, Azure Search is designed to incrementally add new documents automatically. However, where it gets really tricky is when blobs are deleted from storage. The index doesn’t get updated automatically, so the data ends up in a stale state. There’s a way to use a soft delete approach to indicate to an Azure Search data source that a document/blob should be excluded from the index but this requires that the indexer is run to remove the excluded Documents from the index.We could, of course, use the Indexer scheduler to clear the index but this would still leave the soft deletedblobs in the storage account. This is where Functions come into action!For my approach to work, we need to implement the following bits:

  • An Azure Index DataSource with a soft delete option
  • A function to run the Indexer
  • A function to permanently delete the soft-deleted blobs

A FUNCTION TO RETRIEVE ALL SOFT-DELETED BLOBS

Once the indexer has run successfully, we can then go ahead and retrieve all the blobs that need to be deleted. The Function below makes use of Storage Queue as an output binding to populate a queue with the list of blobs that need to be deleted. This time I chose to use a NETStandard C# Function:

using Microsoft.Azure.WebJobs;

using Microsoft.Azure.WebJobs.Extensions.Http;

using Microsoft.AspNetCore.Http;

using Microsoft.Azure.WebJobs.Host;

using System.Threading.Tasks;

using System.Collections.Generic;

using Microsoft.WindowsAzure.Storage.Blob;

using Microsoft.WindowsAzure.Storage;

using System.Linq;

using System;



namespace SearchFunctions

{

    public static class Function1

    {

        [FunctionName("RetrieveDeletedBlobs")]

        public static async Task<ICollector<string>> Run(

            [HttpTrigger(AuthorizationLevel.Function, "get", Route = null)]HttpRequest req,

            [Queue("blobs-to-delete", Connection = "StorageQueueConnectionString")] ICollector<string> outputQueueItems,

            TraceWriter log)

        {

            log.Info("Adding blobs to the delete queue");

            var deletedBlobUris = await ListAllDeletedBlobUris();

            foreach (var blobUri in deletedBlobUris)

            {

                outputQueueItems.Add(blobUri);

            }



            return outputQueueItems;

        }



        private static async Task<List<string>> ListAllDeletedBlobUris()

        {

            var sa = CloudStorageAccount.Parse(Environment.GetEnvironmentVariable("StorageBlobConnectionString"));

            var blobClient = sa.CreateCloudBlobClient();

            var container = blobClient.GetContainerReference("searchdata");

            var results = await ListBlobsAsync(container);



            var deletedBlobs = results.Cast<CloudBlockBlob>()

                .Where(b => b.Metadata["IsDeleted"] == "false");



            return deletedBlobs.Select(b => b.Uri.AbsoluteUri).ToList();

        }



        public static async Task<List<IListBlobItem>> ListBlobsAsync(CloudBlobContainer container)

        {

            BlobContinuationToken continuationToken = null;

            List<IListBlobItem> results = new List<IListBlobItem>();

            do

            {

                var response = await container.ListBlobsSegmentedAsync(

                    null, 

                    false, 

                    BlobListingDetails.Metadata, 

                    null, 

                    continuationToken, 

                    null, null);

                continuationToken = response.ContinuationToken;

                results.AddRange(response.Results);

            }

            while (continuationToken != null);



            return results;

        }

    }

}

view rawazfuncRetrieveDeletedBlobs.cs hosted with ❤ by GitHub

For this Function to work, you’ll need to define the following Application Settings in your Function App:

"AzureWebJobsStorage": "<StorageConnectionString>",
"AzureWebJobsDashboard": "<StorageConnectionString>",
"StorageQueueConnectionString": "<StorageQueueConnectionString>",
"StorageBlobConnectionString": "<StorageBlobConnectionString>"

A FUNCTION TO DELETE THE BLOB DATA

Finally, we need a Function that gets triggered off the Azure Storage Queue and deletes the blob based on the queue message. The queue gets populated by the previous Function. By now you can see how powerful bindings are and how they make coding more trivial by removing much of the plumping. The code below shows how this Function is implemented:

using System;
using System.Threading.Tasks;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Host;
using Microsoft.WindowsAzure.Storage;
using Microsoft.WindowsAzure.Storage.Blob;

namespace SearchFunctions
{
    public static class DeleteBlob
    {
        [FunctionName("DeleteBlob")]
        public static async Task Run([QueueTrigger("blobs-to-delete", Connection = "StorageQueueConnectionString")]string myQueueItem, 
            TraceWriter log)
        {
            await DeleteBlobByUri(myQueueItem);

            log.Info($"C# Queue trigger function processed: {myQueueItem}");
        }

        private static async Task DeleteBlobByUri(string blobUri)
        {
            var sa = CloudStorageAccount.Parse(Environment.GetEnvironmentVariable("StorageBlobConnectionString"));
            var blobClient = sa.CreateCloudBlobClient();
            var container = blobClient.GetContainerReference("searchdata");
            var blockBlob = new CloudBlockBlob(new Uri(blobUri), sa.Credentials);
            await blockBlob.DeleteIfExistsAsync();
        }
    }
}

view rawazfuncDeleteBlobFromQueue.cs hosted with ❤ by GitHub

SUMMARY

I hope this post did a good job in showcasing the flexibility and power of Azure Serverless services (i.e. Functions in this instance) and how we can use multiple, small components to create a larger, more complex solution.

About the author:
Christos Matskas, a developer, speaker, writer, Microsoft Premier Field Engineer (PFE) and geek. I currently live with my family in the UK. Learn more at: https://cmatskas.com/about/

Reference:

Matskas, C. (2018). Keeping your Azure Search Index up-to-date with Azure Functions. [online] Available at: https://cmatskas.com/keeping-your-azure-search-index-up-to-date-with-azure-functions/ [Accessed 20 Feb. 2018].