MVC google maps search using Elasticsearch

This article shows how to create an MVC application which uses google maps and Elasticsearch to do a geo_distance search and find the closest point (document) to your location.
The Elasticsearch index uses a geo_point to define the location of each document. Elasticsearch supports GeoJson formats.

Code: https://github.com/damienbod/WebAppGeoElasticsearch

Other Tutorials:

Part 1: ElasticsearchCRUD introduction
Part 2: MVC application search with simple documents using autocomplete, jQuery and jTable
Part 3: MVC Elasticsearch CRUD with nested documents
Part 4: Data Transfer from MS SQL Server using Entity Framework to Elasticsearch
Part 5: MVC Elasticsearch with child, parent documents
Part 6: MVC application with Entity Framework and Elasticsearch
Part 7: Live Reindex in Elasticsearch
Part 8: CSV export using Elasticsearch and Web API
Part 9: Elasticsearch Parent, Child, Grandchild Documents and Routing
Part 10: Elasticsearch Type mappings with ElasticsearchCRUD
Part 11: Elasticsearch Synonym Analyzer using ElasticsearchCRUD
Part 12: Using Elasticsearch German Analyzer
Part 13: MVC google maps search using Elasticsearch
Part 14: Search Queries and Filters with ElasticsearchCRUD
Part 15: Elasticsearch Bulk Insert
Part 16: Elasticsearch Aggregations With ElasticsearchCRUD
Part 17: Searching Multiple Indices and Types in Elasticsearch
Part 18: MVC searching with Elasticsearch Highlighting
Part 19: Index Warmers with ElasticsearchCRUD

The Elasticsearch index and type is created using the following model:

public class MapDetail
{
	public long Id { get; set; }

	public string Name { get; set; }

	public string Details { get; set; }

	public string Information { get; set; }

	public string DetailsType { get; set; }

	[ElasticsearchGeoPoint]
	public GeoPoint DetailsCoordinates { get; set; }
}

The DetailsCoordinates property uses the GeoPoint class, which is used for the geo_distance search. The mapping in Elasticsearch is created using the IndexCreate method from ElasticsearchCRUD. The Geo types in Elasticsearch require an ElasticsearchGeoPoint attribute if its a geo_point, or an ElasticsearchGeoShape attribute if it’s a shape type. The Geo properties must be mapped and cannot be automatically created when indexing new documents.

public void InitMapDetailMapping()
{
	using (var context = new ElasticsearchContext(
		ConnectionString, 
		new ElasticsearchSerializerConfiguration(_elasticsearchMappingResolver)))
	{
		context.TraceProvider = new ConsoleTraceProvider();
		context.IndexCreate<MapDetail>();
	}
}

The mapping can be viewed using:
http://localhost:9200/_mapping

geoSearch_01

Once the index and the type has been created, some data is added using the _bulk API. The documents are all sent with the SaveChanges() method.

public void AddMapDetailData()
{
	var dotNetGroup = new MapDetail { DetailsCoordinates = new GeoPoint(7.47348, 46.95404), Id = 1, Name = ".NET User Group Bern", Details = "http://www.dnug-bern.ch/", DetailsType = "Work" };
	var dieci = new MapDetail { DetailsCoordinates = new GeoPoint(7.41148, 46.94450), Id = 2, Name = "Dieci Pizzakurier Bern", Details = "http://www.dieci.ch", DetailsType = "Pizza" };
	var babylonKoeniz = new MapDetail { DetailsCoordinates = new GeoPoint(7.41635, 46.92737), Id = 3, Name = "PIZZERIA BABYLON Köniz", Details = "http://www.pizza-babylon.ch/home-k.html", DetailsType = "Pizza" };
	var babylonOstermundigen = new MapDetail { DetailsCoordinates = new GeoPoint(7.48256, 46.95578), Id = 4, Name = "PIZZERIA BABYLON Ostermundigen", Details = "http://www.pizza-babylon.ch/home-o.html", DetailsType = "Pizza" };
	using (var context = new ElasticsearchContext(ConnectionString, new ElasticsearchSerializerConfiguration(_elasticsearchMappingResolver)))
	{
		context.TraceProvider = new ConsoleTraceProvider();
		context.AddUpdateDocument(dotNetGroup, dotNetGroup.Id);
		context.AddUpdateDocument(dieci, dieci.Id);
		context.AddUpdateDocument(babylonKoeniz, babylonKoeniz.Id);
		context.AddUpdateDocument(babylonOstermundigen, babylonOstermundigen.Id);
		context.SaveChanges();
	}
}

The index and type in Elasticsearch is initialized in the global.asax Application_Start method. This checks if an index exists, and creates a new one, if it doesn’t.

private void InitSearchEngine()
{
	var searchProvider = new SearchProvider();

	if (!searchProvider.MapDetailsIndexExists())
	{
		searchProvider.InitMapDetailMapping();
		searchProvider.AddMapDetailData();
	}
}

The index is queried using a geo_distance filter and query. This searches for all documents within the max distance and sorts the hit results from the closest to your search location in an ascending order.

{
  "query" :
  {
	"filtered" : {
		"query" : {
			"match_all" : {}
		},
		"filter" : {
			"geo_distance" : {
				"distance" : "300m",
				 "detailscoordinates" : [7.41148,46.9445]
			}
		}
	}
  },
 "sort" : [
		{
			"_geo_distance" : {
				"detailscoordinates" : [7.41148,46.9445],
				"order" : "asc",
				"unit" : "m"
			}
		}
	]
	}
}

The above Elasticsearch Query looks like this in C#

var search = new Search
{
	Query = new Query(
		new Filtered( 
			new Filter(
				new GeoDistanceFilter( 
					"detailscoordinates", 
					new GeoPoint(centerLongitude, centerLatitude), 
					new DistanceUnitMeter(maxDistanceInMeter)
				)
			)
		)
		{
			Query = new Query(new MatchAllQuery())
		}
	),
	Sort = new SortHolder(
		new List<ISort>
		{
			new SortGeoDistance("detailscoordinates", DistanceUnitEnum.m)
			{
				Order = OrderEnum.asc
			}
		}
	)
};

This is then used in the HomeController as follows:

public ActionResult Search(int maxDistanceInMeter, double centerLongitude, double centerLatitude)
{
	var searchResult = _searchProvider.SearchForClosest(maxDistanceInMeter, centerLongitude, centerLatitude);
	var mapModel = new MapModel
	{
		MapData = new JavaScriptSerializer().Serialize(searchResult),
		CenterLongitude = centerLongitude,
		CenterLatitude = centerLatitude,
		MaxDistanceInMeter = maxDistanceInMeter
	};

	return View("Index", mapModel);
}	

The razor index view uses this data in the map display. This closest document to your search location is displayed using a green image. All hits within the max search distance are also displayed in the map. You can move your center location around, increase or decrease the max allowed distances and the results will be displayed correctly.


@*Bern	Lat 46.94792, Long 7.44461 *@
@model WebAppGeoElasticsearch.Models.MapModel

<input type="hidden" value="@Model.MapData" id="mapdata" name="mapdata" />

@using (Html.BeginForm("Search", "Home"))
{
    <fieldset class="form">
        <legend>SEARCH for closest document in the search engine using geo distance</legend>
        <table width="800">
            <tr>
                <th></th>
            </tr>
            <tr>
            
            </tr>
            <tr>
                <td>
                    <input type="submit" value="Search fo closest: " style="width: 300px">
                </td>
                <td>
                    <input type="hidden" value="@Model.CenterLongitude" id="centerLongitude" name="centerLongitude" />
                    <input type="hidden" value="@Model.CenterLatitude" id="centerLatitude" name="centerLatitude" />

                </td>
                <td>
                    <p style="width: 300px">Max distance in meter:</p>
                    <input id="maxDistanceInMeter" name="maxDistanceInMeter" type="text" title="" value="@Model.MaxDistanceInMeter" style="width: 200px" />
                </td>
            </tr>
        </table>
    </fieldset>

}

<div class="row">
    @*Bern	Lat 46.94792, Long 7.44461 *@
    <div id="googleMap" style="width: 1000px; height: 800px;">
    </div>
</div>

@section scripts
{
    <script type="text/javascript" src="http://maps.googleapis.com/maps/api/js?sensor=false"></script>
    <script type="text/javascript" src="http://google-maps-utility-library-v3.googlecode.com/svn/trunk/markermanager/src/markermanager.js"></script>

    <script language="javascript" type="text/javascript">
        var map;
        var mgr;

        function initialize() {
            var myOptions = {
                zoom: 13,
                center: new google.maps.LatLng(46.94792, 7.44461),
                mapTypeId: google.maps.MapTypeId.ROADMAP
            };
            map = new google.maps.Map(document.getElementById("googleMap"), myOptions);
            mgr = new MarkerManager(map);
            var infoWindow = new google.maps.InfoWindow({ content: "contents" });
            google.maps.event.addListener(mgr, 'loaded', function() {

                var modelData = $.parseJSON($("#mapdata").val());

                var first = true;
                $.each(modelData, function(entryIndex, entry) {
                    //alert("Data" + entry.DetailsCoordinates + ", " + entry.Details);

                    var htmlString = "<a href=\"" + entry.Details + "\">" + entry.Name + "</a>";
                    var coor = entry.DetailsCoordinates.toString();
                    var array = coor.split(',');

                   // alert("Lat" + array[1] + "Long" + array[0]);
                    if (first) {
                        var marker = new google.maps.Marker({
                            position: new google.maps.LatLng(array[1], array[0]),
                            html: htmlString,
                            icon: "http://localhost:2765/Content/yourposition.png"
                        });

                        first = false;
                    } else {
                        var marker = new google.maps.Marker({
                            position: new google.maps.LatLng(array[1], array[0]),
                            html: htmlString
                        });
                    }
                   
                    google.maps.event.addListener(marker, "click", function() {
                        infoWindow.setContent(this.html);
                        infoWindow.open(map, this);
                    });

                    mgr.addMarker(marker, 0);

                });

               // alert('homemarker: ' + $("#centerLatitude").val() + ' Current Lng: ' + $("#centerLongitude").val());

                var homemarker = new google.maps.Marker({

                    position: new google.maps.LatLng($("#centerLatitude").val(), $("#centerLongitude").val()),
                    html: "YOU",
                    draggable: true,
                    icon: "http://localhost:2765/Content/ort.png"
                });

                google.maps.event.addListener(homemarker, 'dragend', function(evt) {
                   // alert('Marker dropped: Current Lat: ' + evt.latLng.lat().toFixed(3) + ' Current Lng: ' + evt.latLng.lng().toFixed(3));
                    $("#centerLongitude").val(evt.latLng.lng().toFixed(3));
                    $("#centerLatitude").val(evt.latLng.lat().toFixed(3));
                });

                mgr.addMarker(homemarker, 0);

                mgr.refresh();
            });
        }

        google.maps.event.addDomListener(window, 'load', initialize);
    </script>
}

The application view after a search:
geoSearch_02

Conclusion

As you can see, it is really easy to do a Geo search using Elasticsearch. A whole range of Geo search filters are supported, Geo Bounding Box Filter, Geo Distance Filter, Geo Distance Range Filter, Geo Polygon Filter, GeoShape Filter, Geohash Cell Filter as well as most geoJSON shapes and a GeoShape query. Optimal searches can be created to match most requirements.

Links:

http://gauth.fr/2012/09/find-closest-subway-station-with-elasticsearch/

http://dbsgeo.com/latlon/

http://markswanderingthoughts.nl/post/84327066530/geo-distance-searching-in-elasticserach-with-nest

http://exploringelasticsearch.com/overview.html#ch-overview

One comment

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: