Using Azure Search in Your ASP.NET MVC Website

Consider a scenario where you have a database-driven website and you want to enable full text search based on the data stored in your data back end. Typically, approaches to fulfill this scenario consist of using the database’s full text search capabilities or setting up third-party components that, in turn, you need to manage. In both cases, if you need to scale out, maintaining the search index (containing the searchable data of your application) becomes a challenging endeavor.

Enter Azure Search

Azure Search is a platform-as-a-service offering that handles the hurdles of maintaining a search index while giving you full flexibility to define the index and scale out when you need more resources and scale down when you don’t need them anymore. In my last post, I described how you can enable full text search for an Azure Mobile Services back end. As a continuation of the theme, I thought it would be useful to showcase how you can enable a similar functionality for a database-driven ASP.NET website. So let’s get to it.

Revisiting the Basics

Similar to the previous post, this process consists of enqueueing data changes, then having a process that reads from the queue and updates the index.

We will also use the Azure Service Bus’s queue (hence guaranteeing orderly delivery) and an attribute-based implementation to tag the data entities with metadata. The metadata defines the index’s schema and the properties that will be written to the index. In this instance, we will leverage an Azure WebJob to perform the index operations.

Prerequisites

To follow the steps below, you need to download and make a reference to the project that contains the attribute and infrastructure code. You can get it here. Also, I am assuming that you already have an instance of Azure Search and Azure Service Bus running. For more information about getting started with Azure Search, see this article.

Building Our Model and Capturing Changes

In an empty ASP.NET MVC website, let’s create a model and decorate it with the custom Azure Search attribute.

namespace aspnet_mvc_azuresearch.Models
{
public class RockBand
{
[Indexable("Edm.String", false, Key = true, Name = "id")]
public string Id { get; set; }

[Indexable("Edm.String", true, Name = "name", Retrievable=true)]
public string Name { get; set; }

[Indexable("Edm.String", true, Name = "genre", Retrievable = true)]
public string Genre { get; set; }

[Indexable("Edm.String", true, Name = "description", Retrievable = true)]
public string Description { get; set;}
}
}

Next, using the scaffolding capabilities of Visual Studio, let’s wire up a controller with Entity Framework and the corresponding write, edit, delete, and list views.

image

image

It’s considered a best practice to implement a view model between your view and your entities. But for simplicity, we will use the scaffolded code as is.

Let’s include the code (highlighted below) in the applicable actions that will send the entity changes to the queue .

namespace aspnet_mvc_azuresearch.Controllers
{
public class RockBandsController : Controller
{
...
private IndexManager ixMan = new IndexManager();

...

// POST: RockBands/Create
// To protect from overposting attacks, please enable the specific properties you want to bind to, for
// more details see http://go.microsoft.com/fwlink/?LinkId=317598.
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create([Bind(Include = "Id,Name,Genre,Description")] RockBand rockBand)
{
if (ModelState.IsValid)
{
db.RockBands.Add(rockBand);
db.SaveChanges();
ixMan.EnqueueEntity<RockBand>(rockBand, "upload");
return RedirectToAction("Index");
}

return View(rockBand);
}


// POST: RockBands/Edit/5
// To protect from overposting attacks, please enable the specific properties you want to bind to, for
// more details see http://go.microsoft.com/fwlink/?LinkId=317598.
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Edit([Bind(Include = "Id,Name,Genre,Description")] RockBand rockBand)
{
if (ModelState.IsValid)
{
db.Entry(rockBand).State = EntityState.Modified;
db.SaveChanges();
ixMan.EnqueueEntity<RockBand>(rockBand, "merge");
return RedirectToAction("Index");
}
return View(rockBand);
}

// POST: RockBands/Delete/5
[HttpPost, ActionName("Delete")]
[ValidateAntiForgeryToken]
public ActionResult DeleteConfirmed(string id)
{
RockBand rockBand = db.RockBands.Find(id);
db.RockBands.Remove(rockBand);
db.SaveChanges();
ixMan.EnqueueEntity<RockBand>(rockBand, "delete");
return RedirectToAction("Index");
}

...

}
}

In the WebConfig, you need to add the following settings:

Setting Name

Value

ASEM_SbConnString

Endpoint to the service bus instance that contains the queue. More information here

ASEM_SearchServiceName

Name of the search service

ASEM_IndexName

The desired index name that will be created and used by the solution

ASEM_ApiVersion

Azure Search API version. See Search Service Version for details

ASEM_ApiKey

API key to call the Azure Search service

<appSettings>
...
<add key="ASEM_SbConnString" value="YOUR SERVICE BUS CONNECTION STRING"/>
<add key="ASEM_SearchServiceName" value="giventocodems"/>
<add key="ASEM_IndexName" value="ixrockbands"/>
<add key="ASEM_ApiVersion" value="2014-10-20-Preview"/>
<add key="ASEM_ApiKey" value="YOUR AZURE SEARCH API KEY"/>
</appSettings>

Search Results

Next, let’s create a search user interface and we will start with a new view model.

namespace aspnet_mvc_azuresearch.Models
{

public class SearchViewModel
{
public string Criteria { get; set; }
public SearchResultViewModel[] Results { get; set; }
}

public class SearchResultViewModel
{
public string Id { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public string Genre { get; set; }
}
}

Add a view for our view model.

@model aspnet_mvc_azuresearch.Models.SearchViewModel

@{
ViewBag.Title = "Index";
Layout = "~/Views/Shared/_Layout.cshtml";
}

<h2>Search</h2>

<p>
@using (Html.BeginForm("","Search",FormMethod.Get))
{


<div class="form-horizontal">
<h4>RockBand Search</h4>
<hr />
<div class="form-group">
@Html.LabelFor(model => model.Criteria, htmlAttributes: new { @class = "control-label col-md-2" })
<div class="col-md-10">
@Html.EditorFor(model => model.Criteria, new { htmlAttributes = new { @class = "form-control" } })
</div>
</div>

<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<input type="submit" value="Search" class="btn btn-default" />
</div>
</div>
</div>
}

</p>



<ul>
@foreach (var item in Model.Results)
{
<li>
<strong>@Html.DisplayFor(model => item.Name)</strong>
<p>
<small>Genre: @Html.DisplayFor(model => item.Genre)</small>
</p>
<p>
@Html.DisplayFor(model => item.Description)
</p>

</li>
}
</ul>

And the search controller.

namespace aspnet_mvc_azuresearch.Controllers
{
public class SearchController : Controller
{
private AzureSearchManager searchMan = new AzureSearchManager();

[HttpGet]
public async Task<ActionResult> Index(string criteria)
{

if (string.IsNullOrEmpty(criteria))
{
return View(new SearchViewModel() { Results = new SearchResultViewModel[0] });
}

var results = await searchMan.SearchAsync(criteria);
var resultsVM = ((JArray)results["value"])
.Select<JToken, SearchResultViewModel>(t => new SearchResultViewModel() { Id = (string)t["id"], Name = (string)t["name"], Genre = (string)t["genre"], Description = (string)t["description"] })
.ToArray<SearchResultViewModel>();

return View(new SearchViewModel() { Results = resultsVM });
}
}
}

Processing Changes and Index Operations

Finally, we need to implement the process that will read the messages from the queue and perform the index operations. For this, we will use an Azure WebJob using the WebJobs SDK. By using the SDK, we don’t need to implement polling logic because it provides built-in capabilities to trigger the WebJob when new messages arrive automatically. So let’s add a new Azure WebJob project to our solution.

image

After adding a reference to the AzureEntityManager project, let’s add a function that receives the message from the queue and performs the corresponding index operation.

Note: The AzureSearchManager, before performing the corresponding index operation, checks if the index exists, and if it doesn’t, it creates one according to the attributes defined in the data entity.

namespace Giventocode.AzureSearchWebJob
{
public class Functions
{
// This function will get triggered/executed when a new message is written
// on an Azure Queue called queue.

public static async Task IndexEntity( [ServiceBusTrigger(IndexManager.INDEXER_QUEUE)] string msg ,
TextWriter log)
{
var searchMan = new AzureSearchManager();

await searchMan.IndexDocumentAsync(msg);
}

}
}

Azure WebJob require a storage account where execution logs will be stored, and in the configuration file, you must include the connection string to the storage account and the service bus instance that contains the queue.

<connectionStrings>
<!-- The format of the connection string is "DefaultEndpointsProtocol=https;AccountName=NAME;AccountKey=KEY" -->
<!-- For local execution, the value can be set either in this config file or through environment variables -->
<add name="AzureWebJobsDashboard" connectionString="YOUR STORAGE ACCOUNT CONNECTION STRING" />
<add name="AzureWebJobsStorage" connectionString="YOUR STORAGE ACCOUNT CONNECTION STRING" />
<add name="AzureWebJobsServiceBus" connectionString="YOUR SERVICE BUS CONNECTION STRING" />

</connectionStrings>

We also need to add the AzureEntityManager settings to the App.config.

<appSettings>
<add key="ASEM_SearchServiceName" value="giventocodems"/>
<add key="ASEM_IndexName" value="ixrockbands"/>
<add key="ASEM_ApiVersion" value="2014-10-20-Preview"/>
<add key="ASEM_ApiKey" value="YOUR AZURE SEARCH API KEY"/>
</appSettings>

Running with It!

We are now ready to run the website. So let’s create a new entry and then edit its description.

image

image

 

After running the web job, we can see that it processed two messages: upload (insert) and then merge (update) operations.

image

Search for using a keyword from the updated description and results!

image

If we go to the Azure preview portal, we can see the newly created index and our document.

image

Final Thoughts

It is worth mentioning that this approach is suitable for on-premises scenarios. So you can have your website hosted outside of Azure—and the WebJob and Azure Search service running on Azure. In addition, you can implement your own queue processing job easily. To facilitate this task, the IndexManager exposes a method that reads from the service bus queue and performs the index operation accordingly.

Also, you can download the full solution from here.


No Comments

Post Reply