Skip to content

Backend Entity Search

Sam Tyson edited this page Jul 12, 2018 · 5 revisions

Full text search can create fast, intuitive, and simple user interfaces when trying to find an entity in the system. We have a conventional way to provide search capabilities which return models projected from search results.

modem-search

When to use Entity Search

When relating one domain entity to another one it is common to show the user a picker control for the entity being related to the current object. Our picker controls sometimes will use client side full text search when the potential data set is small. When the data set is large (thousands of sites, millions of modems) we use Dovetail Seeker and conventional Entity Search.

Code Example

The following is a model that has been marked up to support Entity Search.

[SearchDomain("modem")]
public class Modem : ISearchEntity
{
	public int Id { get; set; }
	public string DeviceName { get; set; }
	public string HostName { get; set; }
}

Notice that all that is really required is to derive from ISearchEntity and add a SearchDomain attribute.

Route

When this model is registered with the IoC container on app startup a server side route will be created. GET /modem/search. This route's action will handled by EntitySearchAction<Model>.

Entity Search Endpoint API

The input model of this endpoint action looks like this:

public class EntitySearchInputModel<T> : ISearchRequest where T : ISearchEntity
{
	public EntitySearchInputModel()
	{
		Page = 1;
		PageSize = ApplicationDefaults.PageSize; //Default 20
		UseWildcard = false;
	}

	public string[] ExcludedIds { get; set; }
	public string Query { get; set; }
	public int Page { get; set; }
	public int PageSize { get; set; }
	public int StartResultIndex { get; set; }
	public bool UseWildcard { get; set; }
}

Query

This is the search query which will run against Dovetail Seeker. Out of the box the query will only be filtered with search domain specified in the SearchDomain attribute. In this case the Seeker query will be domain:modem AND {query}

ExcludedIds

The front-end can specify an array of ids to be excluded. This is done by editing the Seeker query to add AND NOT id:{id} for each id given.

UseWildcard

When this is true the user's query will be appended with a "*". You should be careful when using wildcard searches with very simple queries (e.g. "a") as Seeker can throw exceptions which is why we typically require 3 characters be typed before doing a search.

Page / PageSize / StartResultIndex

These are pagination settings which are usually not used by entity search as no pagination UI is shown. The default page size is 20.

Back-End Step By Step

  1. Add the SearchDomain attribute to your model and enter the proper Seeker search domain.
  2. Have your model implement ISearchEntity
  3. Ensure this entity is registered with the IoC container
  4. Implement a ModelMap<YourModel> which will be used to project search results into model objects.
  5. Ensure your ModelMap is registered with the IoC container.
  6. Test http://localhost:25120/modem/search?Query=hayes

Add SearchDomain attribute

Entity Search needs to know what search domain to look in for your model's search documents.

If the search domain attribute is not specified the model's type name will be used as a default. In the example above this would be modem so it would still work, but be less explicit.

Domains

Dovetail Seeker partitions its search index into domains. Each domain has a document specification which defines what data gets indexed. Here is an example:

<dovetailDocumentSpecification description="Sites" tags="agent5">
	<identification displayName="site" table="site" idColumnName="objid"/>
	<documentSelectionCriteria><![CDATA[update_stamp > ${lastIndexUpdate} AND objid > 0 AND status <> 2]]></documentSelectionCriteria>
	<title>
		<path>name</path>
	</title>
	<summary>
		<path>notes</path>
	</summary>
	<contents>
		<path>site_id</path>
		<path>name</path>
		<path>notes</path>
	</contents>
	<customField title="status" description="Site's status.">
		<path>status</path>
	</customField>
</dovetailDocumentSpecification>

The displayName is the search domain. Each document for this specification added to the index has a field with this domain set. This allows you to search for only documents in this domain.

Setting the SearchDomain attribute instructs the entity search action to filter on the correct search domain.

Implement ISearchEntity

At app startup FubuMVC has an action source (provided by the Search bottle) which looks for all ISearchEntity implmentations registered in the IoC container. All you need to do is add : ISearchEntity to the end of your model class to make this happen.

Next you need to ensure your ISearchEntity model is added to the container. Look for a StructureMap.Registry in your project and add this line.

c.For<ISearchEntity>().Add<Modem>();

Sometimes this is not needed as a scanner is doing this.

s.AddAllTypesOf<ISearchEntity>();

There are a lot of examples of IoC registration throughout Dovetail Agent.

ModelMap For Search Entity

Finally you need to create a ModelMap for your searchable entity. This map is used by the search action to project a model for each search result that comes back from Dovetail Seeker.

This example should be familiar to you.

public class ModemMap : ModelMap<Modem>
{
	protected override void MapDefinition()
	{
		FromTable("modem")
			.Assign(d => d.Id).FromIdentifyingField("objid")
			.Assign(d => d.DeviceName).FromField("device_name")
			.Assign(d => d.HostName).FromField("hostname");
	}
}

Most likely your model maps are automatically already being registered with the IoC container. Just in case they are not add this scanner to your project's registry.

x.ConnectImplementationsToTypesClosing(typeof(ModelMap<>));

Front-end Step By Step

Starting with 5.7 we have simplified the amount of front end code needed to do entity search. The SearchPickerFactory module does away with most of the hard work. Under the hood an open source widget called SelectizeJS is being used.

Defining a Modem Search Picker

Let's take a look at the minimum required code to create a modem picker.

define([
  'app',
  'app/core/searchPickerFactory',
],
function (app, searchPickerFactory) {
  "use strict";

  //this simple example is using the same function to show the selected modem and modem options
  var renderModem = function (modem, escape) {
    return '<div>' + escape(modem.deviceName) + ' - ' + escape(modem.hostName) + '</div>';
  };

  var requirements = {
    entity: 'modem',
    input: { //characteristics of the <input> created
      'class': 'pick-modem',
      'name': 'Modem',
      'id': 'modem-id',
      'placeholder': 'Find a modem.'
    },
    queryUrl: app.base + 'contact/modem',
    valueField: 'id', //which object field is used in the input's value
    renderItemFunction: renderModem, //returns html for the selected modem
    renderOptionFunction: renderModem //returns html for modem options
  };

  return searchPickerFactory(requirements);
});

Using a Search Picker

Here is the code to render a modem search picker.

var contactPickerModel = contactPicker.show(this.contactPickerRegion);

this.listenTo(contactPickerModel, 'change:modem', function (model) {
  console.log('modem has been picked', model.get('modem'));
  // or
  console.log('getting selected modem id via jQuery', $('#modem-id').val());
});

This picker would typically be used in a layout and given a region within to render itself. The picker returns a model object whose modem property will change when a modem is selected. You do not need to listenTo the model for changes, the example is doing so to illustrate reacting to selection of search items.

If you would like to see a more complex example in action search the repo for createCase.js and see a complex interaction between Contact and Site pickers.

Entity Search Front-End Before Agent 5.7

** Obsolete: A few versions down the road this guidance will get removed **

Since we're using the Select2 plugin, we can leverage it's support for automatically querying the backend for search results. To make this process a little easier and handle setting common default values, we've created a wrapper module for it called searchDropdown. Let's walk through setting up our picker:

// Call the 'Dropdown' method to create the Select2 dropdown
this.modemSearch = new searchDropdown.Dropdown($('#modem-id'), {

The next part is to set up the ajax request that should be made for the search. This is the key to calling the backend that we've just created.

  ajax: {
    url: app.base + 'modem/search', // url to query
    dataType: 'json', // data type the endpoint expects

		// Data to send to the backend, 'term' is the current term in the Select2
		// that the user has typed
    data: function(term) {
      return {
        Query: term,
        UseWildcard: true
      };
    },

		// Handles the response from the backend. In this case, returns an object
		// of filtered results
    results: function(data) {
      return {
        results: _.filter(data.hits, function(hit) {
          return hit.isActive;
        })
      };
    }
  },

Now that the ajax request is set up, we need to set up the rendering of the selection and results:

	// What the results from the search will look like
  formatResult: function(data) {
    if (!data) return null;

    // Compile the template and return it to Select2
    return '<div>' + _.escape(data.deviceName) + '</div>' +
           '<div>' + _.escape(data.hostName) + '</div>';
  },

  // What the input will look like once the user has picked a result
  formatSelection: function(data) {
    if (!data.id) {
      return null;
    }

    return _.escape(data.deviceName) + ' - ' + _.escape(data.hostName);
  }
});

Now we have a Select2 dropdown that uses our new entity search endpoint.

Clone this wiki locally