{"id":2140,"date":"2016-08-08T03:00:00","date_gmt":"2016-08-08T03:00:00","guid":{"rendered":"https:\/\/www.microsoft.com\/reallifecode\/index.php\/2016\/08\/08\/building-an-ar-navigation-system-for-visually-impaired-users\/"},"modified":"2020-03-15T07:21:06","modified_gmt":"2020-03-15T14:21:06","slug":"building-an-ar-navigation-system-for-visually-impaired-users","status":"publish","type":"post","link":"https:\/\/devblogs.microsoft.com\/ise\/building-an-ar-navigation-system-for-visually-impaired-users\/","title":{"rendered":"Building an AR Navigation System for Visually Impaired Users"},"content":{"rendered":"<h2 id=\"partner-scenario---guide-dogs\">Partner Scenario &#8211; Guide Dogs<\/h2>\n<p>Over the last two years, we partnered with the Guide Dogs association to build Cities Unlocked. An iOS <a href=\"https:\/\/www.microsoft.com\/cognitive-services\/en-us\/speech-api\/documentation\/getstarted\/get-started-objectivec-ios\">voice-guided<\/a> navigation platform that aims to improve the overall mobility and safety of blind people. The goal of Cities Unlocked is empowering blind users to feel more independent to embark on journeys, and act in much the same way as a sighted person would. Cities Unlocked runs in the background while the phone remains in a users pocket or bag, freeing both hands to use for their cane and guide dog. We built a custom remote control and wearable headset with head-tracking motion sensors and a microphone to listen for voice commands like <code class=\"highlighter-rouge\">\"Where Am I\", \"Take me to &lt;&lt;the park&gt;&gt;\", \"What's around me\"<\/code>. <a href=\"https:\/\/www.microsoft.com\/en-us\/research\/project\/spatial-audio\/\">spatial audio<\/a> is used to provide more context for spatial direction of the information being called out to lighten their cognitive load and deliver an immersive user experience.<\/p>\n<p> <img decoding=\"async\" src=\"https:\/\/devblogs.microsoft.com\/cse\/wp-content\/uploads\/sites\/55\/2016\/08\/citiesUnlockedApp.png\" alt=\"Image citiesUnlockedApp\" width=\"891\" height=\"554\" class=\"aligncenter size-full wp-image-11124\" srcset=\"https:\/\/devblogs.microsoft.com\/ise\/wp-content\/uploads\/sites\/55\/2016\/08\/citiesUnlockedApp.png 891w, https:\/\/devblogs.microsoft.com\/ise\/wp-content\/uploads\/sites\/55\/2016\/08\/citiesUnlockedApp-300x187.png 300w, https:\/\/devblogs.microsoft.com\/ise\/wp-content\/uploads\/sites\/55\/2016\/08\/citiesUnlockedApp-768x478.png 768w\" sizes=\"(max-width: 891px) 100vw, 891px\" \/><\/p>\n<h3 id=\"features--functionalities\">Features \/ Functionalities<\/h3>\n<ul>\n<li>Take Me To &#8211; turn-by-turn navigation routing instructions.<\/li>\n<li>Location Context &#8211; automatic announcement of nearby points of interest(i.e. landmarks, sidewalks, crossings, etc) and location surroundings(i.e. roads, intersections, etc).<\/li>\n<li>More Information &#8211; providing more contextual information about the closest points of interest.<\/li>\n<\/ul>\n<h2 id=\"technical-requirements--challenges\">Technical Requirements \/ Challenges<\/h2>\n<p>Cities Unlocked announces location information throughout positional or head turn movements. The responsiveness and availability of our backend service had to be realtime. The planetary shapes we\u2019re working with are often comprised of complex geometry structures(i.e. parks and buildings). The expectation was our services can seamlessly onboard new cities to the platform, while maintaining acceptable performance for data volume growth.<\/p>\n<p>Our mapping data provider would also have to have a strong story around accessibility(i.e. crossings, curbs, sidewalks, stairs, etc). The data model also had to be flexible so we can easily add new places, features and tags.<\/p>\n<h2 id=\"elasticsearch-overview\">Elasticsearch Overview<\/h2>\n<p>Elasticsearch is an open source search engine based on <a href=\"https:\/\/en.wikipedia.org\/wiki\/Lucene\">Lucene<\/a>, and engineered to take data from any source to search and analyze it in realtime. Elasticsearch is considered an industry leader for developers seeking a distributed and highly scalable search solution.<\/p>\n<p>Elasticsearch is a document database ideal for high search volume scenarios such as full text-relevancy scoring or geo-spatial search. Data documents are serialized in JSON format and held in indexes internally managed by Lucene.<\/p>\n<p>Elasticsearch follows a clustered architecture where indices are evenly divided into shards, and horizontally scaled and distributed across a network of data nodes. Each data node on the cluster manages the delegated operations for its hosted shards. Elasticsearch offers a RESTful JSON based service API over HTTP allowing you to manage data indices and cluster nodes.<\/p>\n<h2 id=\"overview-of-the-solution\">Overview of the Solution<\/h2>\n<h3 id=\"architecture\">Architecture<\/h3>\n<p> <img decoding=\"async\" src=\"https:\/\/devblogs.microsoft.com\/cse\/wp-content\/uploads\/sites\/55\/2016\/08\/guidedogs-architecture.png\" alt=\"Image guidedogs architecture\" width=\"947\" height=\"637\" class=\"aligncenter size-full wp-image-11126\" srcset=\"https:\/\/devblogs.microsoft.com\/ise\/wp-content\/uploads\/sites\/55\/2016\/08\/guidedogs-architecture.png 947w, https:\/\/devblogs.microsoft.com\/ise\/wp-content\/uploads\/sites\/55\/2016\/08\/guidedogs-architecture-300x202.png 300w, https:\/\/devblogs.microsoft.com\/ise\/wp-content\/uploads\/sites\/55\/2016\/08\/guidedogs-architecture-768x517.png 768w\" sizes=\"(max-width: 947px) 100vw, 947px\" \/><\/p>\n<p>All our mapping data is sourced from <a href=\"https:\/\/en.wikipedia.org\/wiki\/OpenStreetMap\">OpenStreetmap<\/a> which allowed us to tap into a growing community of mapping enthusiasts contributing location information on a daily basis. OSM is a free, open and editable map of the world, which is often referred to as the Wikipedia for Maps. Our backend navigation services query OSM data using Elasticsearch running on Azure. Azure offers a managed search <a href=\"https:\/\/azure.microsoft.com\/en-us\/services\/search\/\">service<\/a> based on Lucene, but in this case study we\u2019ll discuss deploying our own Elasticsearch cluster to gain access to geo-spatial features not available in the Azure platform offering.<\/p>\n<h4 id=\"vector-tiles\">Vector Tiles<\/h4>\n<p> <img decoding=\"async\" src=\"https:\/\/devblogs.microsoft.com\/cse\/wp-content\/uploads\/sites\/55\/2016\/08\/vector-tile-example.png\" alt=\"Image vector tile example\" width=\"500\" height=\"400\" class=\"aligncenter size-full wp-image-11127\" srcset=\"https:\/\/devblogs.microsoft.com\/ise\/wp-content\/uploads\/sites\/55\/2016\/08\/vector-tile-example.png 500w, https:\/\/devblogs.microsoft.com\/ise\/wp-content\/uploads\/sites\/55\/2016\/08\/vector-tile-example-300x240.png 300w\" sizes=\"(max-width: 500px) 100vw, 500px\" \/><\/p>\n<div class=\"highlighter-rouge\">\n<pre class=\"highlight\"><code>Image Credit: Mapzen\r\n<\/code><\/pre>\n<\/div>\n<p>We implemented a vector tile based solution that collects mapping data as square shaped <a href=\"https:\/\/en.wikipedia.org\/wiki\/Vector_tiles\">GeoTiles<\/a> into Elasticsearch. Each tile is a square area of land with a defined size and location, identified by a row, height and zoom level(X\/Y\/Z). The highest zoom level comprises of 4 tiles. Each zoom level divides the parent tile into four separate tiles. We used a zoom level of 16(so an average bounding box of ~460 meters x ~460 meters)<\/p>\n<p>We crowdsourced nearby geotile data based on the users position and a 3 kilometer radius distance. We\u2019re using Mapzens <a href=\"https:\/\/en.wikipedia.org\/wiki\/GeoJSON\">GeoJSON<\/a>-based <a href=\"https:\/\/mapzen.com\/projects\/vector-tiles\/\">Vector Tile Service<\/a>. Streets, buildings and parks are stored as GeoJSON LineString, while addresses and points of interest as a single GeoJSON point.<\/p>\n<p>The summary description for nearby points of interest is sourced from Wikipedia. We have created an Azure WebJob that keeps EleasticSearch up-to-date with any recent changes made in OpenStreetMap.<\/p>\n<h2 id=\"why-elasticsearch\">Why Elasticsearch<\/h2>\n<ul>\n<li>We preferred an architecture that can scale horizontally and adapt to the expected data growth rate, support a multitenancy architecture, data-schema agnostic, support search\/filter\/analyze operations off complex geo-shape indices under 50 milliseconds.<\/li>\n<li>The search engine needed to be distributed, fault-tolerant and load-balanced.<\/li>\n<li><a href=\"https:\/\/www.elastic.co\/guide\/en\/elasticsearch\/guide\/current\/language-intro.html\">Full-Text Search<\/a> &#8211; With no visual interface available to validate the results of arbitrary user navigation request(s), finding an accurate full-text search engine was crucial. Lucene is a natural language search engine by design, supporting natural language relevancy ranking and fuzzy queries.<\/li>\n<li>Our search ranking requirement was one of the reasons why we decided to not integrate with a more traditional geo-spatial offering like Postgres \/PostGIS.<\/li>\n<li>Combining geoshape search with full-text search is powerful.<\/li>\n<\/ul>\n<h2 id=\"deploy-elasticsearch-on-azure\">Deploy Elasticsearch on Azure<\/h2>\n<p>Developers can now utilize preconfigured templates built by the Elasticsearch team to simplify the process of deploying a cluster to Azure.<\/p>\n<h3 id=\"prerequisites\">Prerequisites<\/h3>\n<ul>\n<li>Understand the <a href=\"https:\/\/www.elastic.co\/guide\/en\/elasticsearch\/guide\/current\/_finding_your_feet.html\">fundamentals<\/a> of working with Elasticsearch indexes and queries.<\/li>\n<li>An active azure subscription.<\/li>\n<\/ul>\n<h3 id=\"elasticsearch-cluster-setup\">Elasticsearch Cluster Setup<\/h3>\n<p>You can checkout this <a href=\"https:\/\/gist.github.com\/erikschlegel\/0f4330009c7c5ae83831889609a8bb7c\">Gist<\/a> which will help you get started with setting up Elasticsearch on Azure.<\/p>\n<h3 id=\"verify-your-cluster-nodes-are-online\">Verify your Cluster Nodes are Online<\/h3>\n<p>Elasticsearch is packaged with Marvel: a visualization tool for monitoring your cluster status and health. At this point, your cluster should be showing a green status state in Marvel(http:\/\/my-kibana-url:5601\/app\/marvel).<\/p>\n<h3 id=\"loading-planetary-files-into-elasticsearch\">Loading planetary files into Elasticsearch<\/h3>\n<p>This section covers loading an OpenStreetmap planet file into the Elasticsearch cluster you created. I wrote a utility to automate the file parsing and data import <a href=\"https:\/\/erikschlegel.github.io\/planet2elastic\/\">here<\/a>. This utility works with <a href=\"http:\/\/wiki.openstreetmap.org\/wiki\/PBF_Format\">binary planetary files<\/a>(.pbf) as a data source. You can follow the detailed instructions in the <a href=\"https:\/\/erikschlegel.github.io\/planet2elastic\">readme<\/a> to get started.<\/p>\n<p><a href=\"https:\/\/www.geofabrik.de\/\">Geofabrik<\/a> provides pbf files at the continent, country and state \/ regional level. These files are updated on a daily basis.<\/p>\n<p>Mapzen recently released <a href=\"https:\/\/mapzen.com\/data\/metro-extracts\/\">metro extracts<\/a>, which generates a custom planet file at the city or locality level. Custom bounding boxes are also supported. planet files from metro can take up to 20 minutes to generate. Metro extracts are updated on a weekly basis.<\/p>\n<p>You can start small with <a href=\"http:\/\/download.geofabrik.de\/north-america\/us\/rhode-island-latest.osm.pbf\">Rhode Island<\/a> for your first import.<\/p>\n<p>You can confirm that the OSM data was imported into your places index by accessing the document count API http:\/\/your-elastic-cluster:9200\/places\/place\/_count<\/p>\n<p>The utility uses a Go based pbf parser called <a href=\"https:\/\/github.com\/shafreeck\/pb2json\">pb2json<\/a>.<\/p>\n<pre><code class=\"language-JSON\">    let config = {\r\n        tags: featureTags.FEATURE_TAGS,\r\n        leveldb: '.\/tmp',\r\n        batch: 5000,\r\n        file: process.env.PBF_DIRECTORY + '\/' + filename\r\n    };\r\n\r\n    pbf2json.createReadStream(config).pipe(denormalizer.stream(elasticService.elasticConnection()));\r\n<\/code><\/pre>\n<p>I\u2019m using the <a href=\"https:\/\/www.elastic.co\/guide\/en\/elasticsearch\/client\/javascript-api\/current\/index.html\">elasticsearch.js<\/a> client to connect to my cluster.<\/p>\n<pre><code class=\"language-JSON\">function elasticClient(){\r\n    if(VerifyEnvironmentVarsExist([\"ES_HOST\", \"ES_PORT\", \"ES_AUTH_USER\", \"PBF_DIRECTORY\", \"ES_AUTH_PWD\"])){\r\n        return new elasticsearch.Client({\r\n            host: [\r\n                {\r\n                host: process.env.ES_HOST,\r\n                auth: process.env.ES_AUTH_USER + \":\" + process.env.ES_AUTH_PWD\r\n                }\r\n            ],\r\n            log: {\r\n                type: 'file',\r\n                level: 'info',\r\n                path: '.\/logs\/elasticsearch.log'\r\n            },\r\n            requestTimeout: 20000\r\n        });\r\n    }\r\n}\r\n<\/code><\/pre>\n<p>The bulk API is useful for writing a list of documents across multiple indexes. Elasticsearch provides the detailed response status code for all requested documents in the batch, as well as the processing time.<\/p>\n<pre><code class=\"language-JavaScript\">    let body = [];\r\n\r\n    body = body.concat(transformToElasticBulk(tilesToIndex, tilesIndexHeader));\r\n    body = body.concat(transformToElasticBulk(placesToIndex, placeIndexHeader));\r\n    console.log('Requesting ' + body.length + ' for elastic indexing.');\r\n\r\n    elasticClient().bulk({\r\n        body: body\r\n    },\r\n    (err, response) =&gt; {\r\n        if(err){\r\n<\/code><\/pre>\n<h3 id=\"realtime-on-demand-openstreetmap-elasticsearch\">Realtime On-Demand OpenStreetmap Elasticsearch<\/h3>\n<p>We open sourced the <a href=\"https:\/\/github.com\/CatalystCode\/elastic-osm-tiles\">vector maps elastic<\/a> library that\u2019s also used in Cities Unlocked. This library caches the resulting map data from Mapzens Vector Tile Service into Elasticsearch and exposes a service allowing you to interface with the location data. The idea is to use this library as a starting point to build custom location-based queries and filters that solves a spatial data problem. All the services are abstracted out, so extending them is a breeze. Appveyor can be used as a continuous integration solution to deploy your services to a cloud provider.<\/p>\n<h4 id=\"getting-started\">Getting Started<\/h4>\n<p>The library is well documented. You can follow these <a href=\"https:\/\/github.com\/CatalystCode\/elastic-osm-tiles#prerequisites\">steps<\/a> to help get yourself started. You can skip the Elasticsearch cluster and index setup step if you completed that from the previous section.<\/p>\n<h4 id=\"validate-setup---fetch-elasticsearch-location-information\">Validate Setup &#8211; Fetch Elasticsearch location information<\/h4>\n<p>Once the library is installed \/ configured locally you can test the setup by making a POST request to <code class=\"highlighter-rouge\">http:\/\/localhost:port\/api\/explore<\/code><\/p>\n<h3 id=\"the-service-api\">The Service API<\/h3>\n<p>The main implementation service is LocationExploreElasticService. You can directly call the service as highlighted below<\/p>\n<div class=\"language-csharp highlighter-rouge\">\n<pre class=\"highlight\"><code><span class=\"kt\">var<\/span> <span class=\"n\">requestFactory<\/span> <span class=\"p\">=<\/span> <span class=\"k\">new<\/span> <span class=\"nf\">HttpRequestFactory<\/span><span class=\"p\">();<\/span>\r\n<span class=\"kt\">var<\/span> <span class=\"n\">tileServerBaseUri<\/span> <span class=\"p\">=<\/span> <span class=\"n\">ConfigurationHelper<\/span><span class=\"p\">.<\/span><span class=\"nf\">GetSetting<\/span><span class=\"p\">(<\/span><span class=\"s\">\"TileServer\"<\/span><span class=\"p\">);<\/span>\r\n<span class=\"kt\">var<\/span> <span class=\"n\">tileKey<\/span> <span class=\"p\">=<\/span> <span class=\"n\">ConfigurationHelper<\/span><span class=\"p\">.<\/span><span class=\"nf\">GetSetting<\/span><span class=\"p\">(<\/span><span class=\"s\">\"TileServerAPIKey\"<\/span><span class=\"p\">);<\/span>\r\n\r\n <span class=\"kt\">var<\/span> <span class=\"n\">service<\/span> <span class=\"p\">=<\/span> <span class=\"k\">new<\/span> <span class=\"nf\">LocationExploreElasticService<\/span><span class=\"p\">(<\/span><span class=\"n\">tileServerBaseUri<\/span><span class=\"p\">,<\/span> <span class=\"n\">tileKey<\/span><span class=\"p\">,<\/span> <span class=\"n\">TelemetryInstance<\/span><span class=\"p\">.<\/span><span class=\"n\">Current<\/span><span class=\"p\">,<\/span> <span class=\"n\">requestFactory<\/span><span class=\"p\">)<\/span>\r\n<span class=\"p\">{<\/span>\r\n                <span class=\"n\">ResultLimit<\/span> <span class=\"p\">=<\/span> <span class=\"n\">request<\/span><span class=\"p\">.<\/span><span class=\"n\">Limit<\/span>\r\n<span class=\"p\">};<\/span>\r\n\r\n<span class=\"kt\">var<\/span> <span class=\"n\">response<\/span> <span class=\"p\">=<\/span> <span class=\"k\">default<\/span><span class=\"p\">(<\/span><span class=\"n\">IEnumerable<\/span><span class=\"p\">&lt;<\/span><span class=\"n\">Place<\/span><span class=\"p\">&gt;);<\/span>\r\n<span class=\"n\">response<\/span> <span class=\"p\">=<\/span> <span class=\"k\">await<\/span> <span class=\"n\">service<\/span><span class=\"p\">.<\/span><span class=\"nf\">SearchAsync<\/span><span class=\"p\">(<\/span><span class=\"n\">request<\/span><span class=\"p\">.<\/span><span class=\"n\">CurrentLocation<\/span><span class=\"p\">,<\/span> <span class=\"n\">request<\/span><span class=\"p\">.<\/span><span class=\"n\">SearchRadius<\/span><span class=\"p\">);<\/span>\r\n<\/code><\/pre>\n<\/div>\n<p>You can invoke this example directly by running the SpatialDataServiceHandlerTests_SuccessWhatsAroundMeCheck unit test in Tests.Microsoft.PCT.OSM \/ ElasticServiceTest.cs<\/p>\n<p>To implement your own service just extend off BaseElasticService, and implement your own QueryPlacesAsync method.<\/p>\n<div class=\"language-csharp highlighter-rouge\">\n<pre class=\"highlight\"><code><span class=\"k\">protected<\/span> <span class=\"k\">override<\/span> <span class=\"k\">async<\/span> <span class=\"n\">Task<\/span><span class=\"p\">&lt;<\/span><span class=\"n\">IEnumerable<\/span><span class=\"p\">&lt;<\/span><span class=\"n\">Place<\/span><span class=\"p\">&gt;&gt;<\/span> <span class=\"nf\">QueryPlacesAsync<\/span><span class=\"p\">(<\/span><span class=\"n\">IGeoCoordinate<\/span> <span class=\"n\">userLocation<\/span><span class=\"p\">,<\/span> <span class=\"kt\">double<\/span> <span class=\"n\">searchRadiusMeters<\/span><span class=\"p\">)<\/span>\r\n        <span class=\"p\">{<\/span>\r\n            <span class=\"k\">if<\/span> <span class=\"p\">(<\/span><span class=\"n\">searchRadiusMeters<\/span> <span class=\"p\">==<\/span> <span class=\"m\">0<\/span><span class=\"p\">)<\/span>\r\n            <span class=\"p\">{<\/span>\r\n                <span class=\"k\">throw<\/span> <span class=\"k\">new<\/span> <span class=\"nf\">ArgumentNullException<\/span><span class=\"p\">(<\/span><span class=\"nf\">nameof<\/span><span class=\"p\">(<\/span><span class=\"n\">searchRadiusMeters<\/span><span class=\"p\">));<\/span>\r\n            <span class=\"p\">}<\/span>\r\n            <span class=\"k\">if<\/span> <span class=\"p\">(!<\/span><span class=\"n\">ResultLimit<\/span><span class=\"p\">.<\/span><span class=\"n\">HasValue<\/span><span class=\"p\">)<\/span>\r\n            <span class=\"p\">{<\/span>\r\n                <span class=\"n\">ResultLimit<\/span> <span class=\"p\">=<\/span> <span class=\"n\">SPATIAL_DATA_DEFAULT_RESPONSE_LIMIT<\/span><span class=\"p\">;<\/span>\r\n            <span class=\"p\">}<\/span>\r\n\r\n            <span class=\"kt\">var<\/span> <span class=\"n\">places<\/span> <span class=\"p\">=<\/span> <span class=\"k\">await<\/span> <span class=\"nf\">InvokeElasticSearchQueryAsync<\/span><span class=\"p\">(<\/span><span class=\"n\">userLocation<\/span><span class=\"p\">,<\/span> <span class=\"n\">searchRadiusMeters<\/span><span class=\"p\">,<\/span> <span class=\"k\">null<\/span><span class=\"p\">);<\/span>\r\n\r\n            <span class=\"k\">return<\/span> <span class=\"n\">places<\/span><span class=\"p\">.<\/span><span class=\"nf\">Take<\/span><span class=\"p\">(<\/span><span class=\"n\">ResultLimit<\/span><span class=\"p\">.<\/span><span class=\"n\">Value<\/span><span class=\"p\">);<\/span>\r\n        <span class=\"p\">}<\/span>\r\n<\/code><\/pre>\n<\/div>\n<p>BaseElasticService\u2019s InvokeElasticSearchQueryAsync method will use the provided location and radius to verify that all nearby tile data is cached before POI data is queried from elastic.<\/p>\n<p>Each geo tile fetch results in a single request to Mapzens Tile Server, which are made in parallel as Rx Observable subscriptions(Thanks Eric Rozell).<\/p>\n<div class=\"language-csharp highlighter-rouge\">\n<pre class=\"highlight\"><code>            <span class=\"kt\">var<\/span> <span class=\"n\">headers<\/span> <span class=\"p\">=<\/span> <span class=\"nf\">ParseRelevantHeaders<\/span><span class=\"p\">(<\/span><span class=\"n\">requestHeaders<\/span><span class=\"p\">);<\/span>\r\n            <span class=\"kt\">var<\/span> <span class=\"n\">tileRequests<\/span> <span class=\"p\">=<\/span> <span class=\"n\">serviceProviders<\/span><span class=\"p\">.<\/span><span class=\"nf\">Select<\/span><span class=\"p\">(<\/span><span class=\"n\">provider<\/span> <span class=\"p\">=&gt;<\/span> <span class=\"nf\">SendPostRequestAsync<\/span><span class=\"p\">(<\/span><span class=\"n\">provider<\/span><span class=\"p\">,<\/span> <span class=\"n\">request<\/span><span class=\"p\">,<\/span> <span class=\"n\">token<\/span><span class=\"p\">,<\/span> <span class=\"n\">headers<\/span><span class=\"p\">));<\/span>\r\n\r\n            <span class=\"c1\">\/\/Continous check if the response queue has been fully processed until the time to live has been triggered\r\n<\/span>            <span class=\"k\">return<\/span> <span class=\"k\">await<\/span> <span class=\"n\">tileRequests<\/span>\r\n                <span class=\"p\">.<\/span><span class=\"nf\">ToObservable<\/span><span class=\"p\">(<\/span><span class=\"n\">token<\/span><span class=\"p\">)<\/span>\r\n                <span class=\"p\">.<\/span><span class=\"nf\">Where<\/span><span class=\"p\">(<\/span><span class=\"n\">tileResponse<\/span> <span class=\"p\">=&gt;<\/span> <span class=\"p\">!<\/span><span class=\"n\">tileResponse<\/span><span class=\"p\">.<\/span><span class=\"n\">HasValue<\/span> <span class=\"p\">||<\/span> <span class=\"n\">tileResponse<\/span><span class=\"p\">.<\/span><span class=\"n\">Value<\/span> <span class=\"p\">!=<\/span> <span class=\"k\">null<\/span><span class=\"p\">)<\/span>\r\n                <span class=\"p\">.<\/span><span class=\"nf\">Select<\/span><span class=\"p\">(<\/span><span class=\"n\">Validate<\/span><span class=\"p\">)<\/span>\r\n                <span class=\"p\">.<\/span><span class=\"nf\">Do<\/span><span class=\"p\">(<\/span><span class=\"n\">tileResponse<\/span> <span class=\"p\">=&gt;<\/span> <span class=\"nf\">Log<\/span><span class=\"p\">(<\/span><span class=\"n\">tileResponse<\/span><span class=\"p\">,<\/span> <span class=\"n\">deserializedRequest<\/span><span class=\"p\">,<\/span> <span class=\"n\">headers<\/span><span class=\"p\">))<\/span>\r\n                <span class=\"p\">.<\/span><span class=\"nf\">ToList<\/span><span class=\"p\">();<\/span>\r\n<\/code><\/pre>\n<\/div>\n<h2 id=\"performance\">Performance<\/h2>\n<p>This table provides some details about the cluster I used for my benchmark testing.<\/p>\n<table>\n<thead>\n<tr>\n<th style=\"text-align: left\"><\/th>\n<th style=\"text-align: center\">Nodes<\/th>\n<th style=\"text-align: center\">Shards<\/th>\n<th style=\"text-align: center\">Node Size<\/th>\n<th style=\"text-align: left\">Geo Shape Documents<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td style=\"text-align: left\">Cluster<\/td>\n<td style=\"text-align: center\">5(2 Data, 2 Master, 1 Client)<\/td>\n<td style=\"text-align: center\">29 per data node<\/td>\n<td style=\"text-align: center\">Standard DS1 v2<\/td>\n<td style=\"text-align: left\">&gt; 15 Million<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<h4 id=\"indexing-performance\">Indexing Performance<\/h4>\n<p>I ran a test to clock the bulk indexing API for 500,000 documents.<\/p>\n<table>\n<thead>\n<tr>\n<th style=\"text-align: left\"><\/th>\n<th style=\"text-align: left\">Avg Per Document<\/th>\n<th style=\"text-align: center\">Avg Per Batch<\/th>\n<th style=\"text-align: center\">Avg Batch Size<\/th>\n<th style=\"text-align: center\">Slowest Batch<\/th>\n<th style=\"text-align: left\">Fastest Batch<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td style=\"text-align: left\">Bulk Indexing<\/td>\n<td style=\"text-align: left\">1.5 ms<\/td>\n<td style=\"text-align: center\">300 ms<\/td>\n<td style=\"text-align: center\">~320 Documents<\/td>\n<td style=\"text-align: center\">4,174 ms<\/td>\n<td style=\"text-align: left\">100 ms<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<h4 id=\"geo-distance-search-performance\">Geo-Distance Search Performance<\/h4>\n<p>The second test measures the response time of 1000 geo distance search queries fanned out in parallel. Search areas were New York City, London and Paris.<\/p>\n<table>\n<thead>\n<tr>\n<th style=\"text-align: left\"><\/th>\n<th style=\"text-align: center\">Mean<\/th>\n<th style=\"text-align: center\">Median<\/th>\n<th style=\"text-align: center\">Max<\/th>\n<th style=\"text-align: center\">Min<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td style=\"text-align: left\">Geo Search<\/td>\n<td style=\"text-align: center\">25 ms<\/td>\n<td style=\"text-align: center\">26 ms<\/td>\n<td style=\"text-align: center\">51 ms<\/td>\n<td style=\"text-align: center\">2 ms<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<h2 id=\"summary--challenges\">Summary \/ Challenges<\/h2>\n<p>I was pleasantly surprised of how well Elasticsearch scaled with our data volume. Utilizing an open spatial data solution like OpenStreetMap was critical to our success, as we needed full read\/write access to sidewalk objects(hydrants, mailboxes, trash cans, etc) to maintain an acceptable level of data quality. The current population of sidewalks and crossings in OSM are sparse. This presented a challenge as the open source <a href=\"https:\/\/mapzen.com\/blog\/valhalla-intro\/\">Valhalla<\/a> routing engine factors roads and intersections in the absence of pedestrian data in OSM. We\u2019re currently undergoing an exploratory effort that uses machine learning models paired with aerial and streetview imagery to infer sidewalk and crossing mapping features, with the goal of contributing that dataset back to OpenStreetMap to improve the data quality of Valhalla pedestrian routes.<\/p>\n<h3 id=\"contributing\">Contributing<\/h3>\n<p>We welcome pull requests in all shapes and sizes for the repos mentioned above. If you happen to have experience with using ML with streetview imagery then drop me a note via twitter @erikschlegel1.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Creating an augmented reality navigation system for the visually impaired using Elasticsearch, Microsoft Azure and Openstreetmap.<\/p>\n","protected":false},"author":21362,"featured_media":11125,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[16],"tags":[117,164,192,215,269,335],"class_list":["post-2140","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-devops","tag-cities-unlocked","tag-elasticsearch","tag-guide-dogs","tag-ios","tag-navigation","tag-spatial-audio"],"acf":[],"blog_post_summary":"<p>Creating an augmented reality navigation system for the visually impaired using Elasticsearch, Microsoft Azure and Openstreetmap.<\/p>\n","_links":{"self":[{"href":"https:\/\/devblogs.microsoft.com\/ise\/wp-json\/wp\/v2\/posts\/2140","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/devblogs.microsoft.com\/ise\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/devblogs.microsoft.com\/ise\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/ise\/wp-json\/wp\/v2\/users\/21362"}],"replies":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/ise\/wp-json\/wp\/v2\/comments?post=2140"}],"version-history":[{"count":0,"href":"https:\/\/devblogs.microsoft.com\/ise\/wp-json\/wp\/v2\/posts\/2140\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/ise\/wp-json\/wp\/v2\/media\/11125"}],"wp:attachment":[{"href":"https:\/\/devblogs.microsoft.com\/ise\/wp-json\/wp\/v2\/media?parent=2140"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/ise\/wp-json\/wp\/v2\/categories?post=2140"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/ise\/wp-json\/wp\/v2\/tags?post=2140"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}