{"id":9628,"date":"2020-11-04T15:51:31","date_gmt":"2020-11-04T23:51:31","guid":{"rendered":"https:\/\/www.couchbase.com\/blog\/?p=9628"},"modified":"2024-03-01T09:18:35","modified_gmt":"2024-03-01T17:18:35","slug":"taming-the-gerrymander-with-geospatial-search","status":"publish","type":"post","link":"https:\/\/www.couchbase.com\/blog\/taming-the-gerrymander-with-geospatial-search\/","title":{"rendered":"Taming the Gerrymander with Geospatial Search"},"content":{"rendered":"<h4>The Basics<\/h4>\n<p>Before you read any further, please take a few minutes and read the excellent post on <a href=\"https:\/\/www.couchbase.com\/blog\/geospatial-search-how-do-i-use-thee-let-me-count-the-ways\/\">geospatial search<\/a> in Couchbase, as published by my friend and colleague Brian Kane: <a href=\"https:\/\/www.couchbase.com\/blog\/geospatial-search-how-do-i-use-thee-let-me-count-the-ways\/\" target=\"_blank\" rel=\"noopener noreferrer\">https:\/\/www.couchbase.com\/blog\/geospatial-search-how-do-i-use-thee-let-me-count-the-ways\/<\/a><\/p>\n<p>Go ahead; I&#8217;ll wait.<\/p>\n<p>Now that you&#8217;re back, you will know that one great way to leverage Couchbase&#8217;s full text search engine is to pass to it a series of vertices which identify a polygon (usually irregular) describing a geographic region. Brian&#8217;s example uses ten pairs of lat\/long points:<\/p>\n<pre class=\"toolbar-overlay:false toolbar-hide:false toolbar-delay:false show-title:false striped:false marking:false ranges:false nums:false nums-toggle:false wrap-toggle:false expand-toggle:false decode-attributes:false trim-whitespace:false trim-code-tag:false mixed:false show_mixed:false lang:default decode:true show_mixed:false\">\"polygon_points\": [\r\n  \"35.987374, -83.658937\",\r\n  \"35.971769, -83.654212\",\r\n  \"35.887168, -83.793874\",\r\n  \"35.686403, -83.678068\",\r\n  \"35.704374, -83.505435\",\r\n  \"35.769145, -83.275637\",\r\n  \"35.868423, -83.290819\",\r\n  \"35.919168, -83.350486\",\r\n  \"35.948053, -83.510420\",\r\n  \"35.990925, -83.568382\"\r\n]<\/pre>\n<p>These points roughly bound a region in Tennessee, south of the highway, north of the National Park Boundary, and within a single county&#8230;good enough for the required analysis, and easy to paste into your request. Given these, the Couchbase full text search (FTS) index engine can easily return all of the required data elements associated with points inside (or outside) the perimeter. (Brian gives a great example of this in his post.)<\/p>\n<h4>The Wrench (or Maybe, the Wrench-shaped District)<\/h4>\n<p>But what if your polygonal region is extremely detailed and complex, maybe requiring thousands of pairs of lat\/long points to describe? Do we have ready examples of these? Yes! Thanks to the hard work of the fifty State Legislatures and\/or their surrogates, we have plenty of examples of regions like this in the form of U.S. Congressional districts. And thanks to Couchbase N1QL and FTS geospatial search, we have the means to manage the data with ease.<\/p>\n<p>The average U.S. Congressional district requires 8,694 vertices to define it. Reasons for this are both practical (all of them are expected to comprise approximately the same number of citizens), political (the parties in power may contort district boundaries in such a way that the voters there will keep them that way&#8211;this is called gerrymandering) and geographical (a lot of them are based in part on rivers, lakes, ocean shores, mountains, and other natural boundaries). The most geographically complex district (i.e. the one requiring the largest number of vertices to describe) is the 5th Congressional District of Virginia, which takes a whopping 40,145 lat\/long pairs to describe (and looks like a reverse T. Rex rampant). The simplest, requiring only 422, is the 36th Congressional District of New York, which looks like a submarine sneaking away from Lake Erie.<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"alignnone wp-image-9629 size-full\" src=\"https:\/\/www.couchbase.com\/blog\/wp-content\/uploads\/2020\/11\/StateDistricts.png\" alt=\"\" width=\"1352\" height=\"340\" srcset=\"https:\/\/www.couchbase.com\/blog\/wp-content\/uploads\/sites\/1\/2020\/11\/StateDistricts.png 1352w, https:\/\/www.couchbase.com\/blog\/wp-content\/uploads\/sites\/1\/2020\/11\/StateDistricts-300x75.png 300w, https:\/\/www.couchbase.com\/blog\/wp-content\/uploads\/sites\/1\/2020\/11\/StateDistricts-1024x258.png 1024w, https:\/\/www.couchbase.com\/blog\/wp-content\/uploads\/sites\/1\/2020\/11\/StateDistricts-768x193.png 768w, https:\/\/www.couchbase.com\/blog\/wp-content\/uploads\/sites\/1\/2020\/11\/StateDistricts-20x5.png 20w, https:\/\/www.couchbase.com\/blog\/wp-content\/uploads\/sites\/1\/2020\/11\/StateDistricts-1320x332.png 1320w\" sizes=\"auto, (max-width: 1352px) 100vw, 1352px\" \/><\/p>\n<h4>The Data<\/h4>\n<p>Clearly, then, we&#8217;re going to want to store and retrieve our geo points from a database if we want to implement queries against them on a large scale. And because the points are likely to be found in the form of an embedded array, a JSON document in Couchbase is just the ticket. Below is an example of such a document:<\/p>\n<pre class=\"toolbar-overlay:false toolbar-hide:false toolbar-delay:false show-title:false striped:false marking:false ranges:false nums:false nums-toggle:false wrap-toggle:false expand-toggle:false decode-attributes:false trim-whitespace:false trim-code-tag:false mixed:false show_mixed:false lang:default decode:true show_mixed:false\">{\r\n\"geometry\": {\r\n\"type\": \"Polygon\",\r\n\"coordinates\": [\r\n{\r\n  \"geometry\": {\r\n    \"type\": \"Polygon\",\r\n    \"coordinates\": [\r\n      [\r\n        [-93.911307,44.546513999999995],\r\n        [-93.91024,44.548004999999996],\r\n        [-93.909904,44.548300999999995],\r\n        [-93.90922599999999,44.548843999999995],\r\n        [etc., etc., for hundreds or thousands of pairs]\r\n        [-93.911307,44.546513999999995]\r\n      ]\r\n    ]\r\n  },\r\n\"type\": \"Feature\",\r\n\"properties\": {\r\n\"INTPTLAT\": \"+44.4789680\",\r\n\"FUNCSTAT\": \"N\",\r\n\"INTPTLON\": \"-092.8530418\",\r\n\"LSAD\": \"C2\",\r\n\"GEOID\": \"2702\",\r\n\"AWATER\": 243358361,\r\n\"CD116FP\": \"02\",\r\n\"CDSESSN\": \"116\",\r\n\"MTFCC\": \"G5200\",\r\n\"NAMELSAD\": \"Congressional District 2\",\r\n\"STATEFP\": \"27\",\r\n\"ALAND\": 6314464923\r\n}\r\n}\r\n\r\n<\/pre>\n<p>Why is the data shaped like this, you may wonder, with the polygon points embedded in a nameless single-element array, embedded in another &#8220;coordinates&#8221; one, embedded in a &#8220;geometry&#8221; object? The simple answer is that sometimes you just work with the data you have. (It is based on the public source I was able to find, one which was remarkably easy to import into Couchbase. Maybe I&#8217;ll write a separate post describing that process.) And even though the data is bit cumbersome, the N1QL language, as we will see below, makes it easy to retrieve what we need.<\/p>\n<p>The other dataset which concerns us comprises the main portion of our example. It is a list of millions of registered voters (don&#8217;t worry; I faked the names and addresses), along with the party affiliation and voting history for each. A sample document looks like this:<\/p>\n<pre class=\"toolbar-overlay:false toolbar-hide:false toolbar-delay:false show-title:false striped:false marking:false ranges:false nums:false nums-toggle:false wrap-toggle:false expand-toggle:false decode-attributes:false trim-whitespace:false trim-code-tag:false mixed:false show_mixed:false lang:default decode:true show_mixed:false\">{\r\n  \"City\": \"Adelanto\",\r\n  \"doctype\": \"Voter\",\r\n  \"Name\": \"Ryan Johnson\",\r\n  \"County\": \"San Bernardino\",\r\n  \"Party\": \"Democrat\",\r\n  \"Reg\": [{\"Year\": 2018},\r\n          {\"Voted\": \"In person\",\"Year\": 2016},\r\n          {\"Year\": 2014},\r\n          {\"Voted\": \"In person\",\"Year\": 2012},\r\n          {\"Year\": 2010},\r\n          {\"Voted\": \"In person\",\"Year\": 2008},\r\n          {\"Year\": 2006},\r\n          {\"Voted\": \"In person\",\"Year\": 2004},\r\n          {\"Year\": 2002},\r\n          {\"Voted\": \"In person\",\"Year\": 2000},\r\n          {\"Year\": 1998},\r\n          {\"Voted\": \"In person\",\"Year\": 1996}],\r\n  \"Addr\": \"221 Cindy Inlet Suite 064\",\r\n  \"Zip\": \"92301\",\r\n  \"Geo\": {\"lat\": 34.6149071942612,\"lon\": -117.51442556265236}\r\n}<\/pre>\n<h4>The Use Case and the Set-up<\/h4>\n<p>Finally, then, our use case: Given an individual constituent on the phone, how does a member of Congress quickly determine whether or not the person is a member of his or her voting district? We will solve the problem with FTS and N1QL.<\/p>\n<p>First we must prepare the FTS index. In our case, we will index all documents based on the type field <em>_type<\/em>. We will index the <em>Name<\/em> field as a keyword, and the <em>Geo<\/em> field as a geopoint. Here is what it looks like on my console:<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"alignnone wp-image-9631 size-large\" src=\"https:\/\/www.couchbase.com\/blog\/wp-content\/uploads\/2020\/11\/Screen-Shot-2020-11-03-at-2.27.01-PM-1024x490.png\" alt=\"\" width=\"900\" height=\"431\" srcset=\"https:\/\/www.couchbase.com\/blog\/wp-content\/uploads\/sites\/1\/2020\/11\/Screen-Shot-2020-11-03-at-2.27.01-PM-1024x490.png 1024w, https:\/\/www.couchbase.com\/blog\/wp-content\/uploads\/sites\/1\/2020\/11\/Screen-Shot-2020-11-03-at-2.27.01-PM-300x144.png 300w, https:\/\/www.couchbase.com\/blog\/wp-content\/uploads\/sites\/1\/2020\/11\/Screen-Shot-2020-11-03-at-2.27.01-PM-768x368.png 768w, https:\/\/www.couchbase.com\/blog\/wp-content\/uploads\/sites\/1\/2020\/11\/Screen-Shot-2020-11-03-at-2.27.01-PM-1536x736.png 1536w, https:\/\/www.couchbase.com\/blog\/wp-content\/uploads\/sites\/1\/2020\/11\/Screen-Shot-2020-11-03-at-2.27.01-PM-20x10.png 20w, https:\/\/www.couchbase.com\/blog\/wp-content\/uploads\/sites\/1\/2020\/11\/Screen-Shot-2020-11-03-at-2.27.01-PM-1320x632.png 1320w, https:\/\/www.couchbase.com\/blog\/wp-content\/uploads\/sites\/1\/2020\/11\/Screen-Shot-2020-11-03-at-2.27.01-PM.png 1758w\" sizes=\"auto, (max-width: 900px) 100vw, 900px\" \/><\/p>\n<p>(Brian&#8217;s post goes into more detail on the steps you will follow to build an index.)<\/p>\n<p>Once this index is built, we will be able to pass it a series of polygon points and receive a series of hits. Following Brian&#8217;s lead, I tested this using a curl:<\/p>\n<pre class=\"toolbar-overlay:false toolbar-hide:false toolbar-delay:false show-title:false striped:false marking:false ranges:false nums:false nums-toggle:false wrap-toggle:false expand-toggle:false decode-attributes:false trim-whitespace:false trim-code-tag:false mixed:false show_mixed:false lang:default decode:true show_mixed:false\">curl -s -XPOST -H \"Content-Type: application\/json\" -u Administrator:password https:\/\/localhost:8094\/api\/index\/ftsVoterGeo\/query -d '\r\n{\r\n  \"fields\": [\"Name\"],\r\n  \"size\": 50,\r\n  \"query\": {\r\n    \"field\": \"Geo\",\r\n    \"polygon_points\": [\r\n      \"33.4328, -114.7322\",\r\n      \"33.5253, -114.6561\",\r\n      \"33.6178, -114.5883\",\r\n      \"34.6173, -117.4220\"\r\n    ]\r\n  }\r\n}' | jq '(\"result_count: \"+ (.total_hits | tostring)), (.hits[]| (.id + \" \" + .fields.Name))'<\/pre>\n<p>This shows me that a search against a simple polygonal region can and will return a list of names. Theoretically, we could stop there and let the app (or even the user) search through the hits to see if it finds the individual voter name in question. But we can do better. Let&#8217;s let the search engine narrow it down. We do this via a &#8220;conjunct&#8221; search. (Think of a conjunct as a logical AND and a disjunct as a logical OR.) Below is the example curl:<\/p>\n<pre class=\"toolbar-overlay:false toolbar-hide:false toolbar-delay:false show-title:false striped:false marking:false ranges:false nums:false nums-toggle:false wrap-toggle:false expand-toggle:false decode-attributes:false trim-whitespace:false trim-code-tag:false mixed:false show_mixed:false lang:default decode:true show_mixed:false\">curl -s -XPOST -H \"Content-Type: application\/json\" -u Administrator:password https:\/\/localhost:8094\/api\/index\/ftsVoterGeo\/query -d '\r\n{\r\n  \"fields\": [\"Name\"],\r\n  \"size\": 50,\r\n  \"query\":\r\n  {\r\n    \"conjuncts\":\r\n      [\r\n        {\r\n        \"field\": \"Geo\",\r\n        \"polygon_points\": [\r\n           \"33.4328, -114.7322\",\r\n           \"33.5253, -114.6561\",\r\n           \"33.6178, -114.5883\",\r\n           \"34.6173, -117.4220\"\r\n        ]\r\n        },\r\n        {\r\n        \"field\": \"Name\",\r\n        \"match\": \"Anne Murray\"\r\n        }\r\n      ]\r\n  }\r\n}' | jq '(\"result_count: \"+ (.total_hits | tostring)), (.hits[]| (.id + \" \" + .fields.Name))'<\/pre>\n<p>You can read this as &#8220;If the geopoint is within the polygon boundaries and the name matches the voter name, return the hit.&#8221; Works like a charm, so we know our FTS index is properly defined.<\/p>\n<h4>The Extraction<\/h4>\n<p>Now, then, we need to test the retrieval of an individual district&#8217;s boundaries from the database. Our first pass at this entails just a simple inspection of the data we are likely to use, maybe just for a single district:<\/p>\n<pre class=\"toolbar-overlay:false toolbar-hide:false toolbar-delay:false show-title:false striped:false marking:false ranges:false nums:false nums-toggle:false wrap-toggle:false expand-toggle:false decode-attributes:false trim-whitespace:false trim-code-tag:false mixed:false show_mixed:false lang:default decode:true show_mixed:false\">select properties.NAMELSAD, districts.geometry.coordinates from districts\r\n      use keys 'district::87';<\/pre>\n<p>This returns a result like this:<\/p>\n<pre class=\"toolbar-overlay:false toolbar-hide:false toolbar-delay:false show-title:false striped:false marking:false ranges:false nums:false nums-toggle:false wrap-toggle:false expand-toggle:false decode-attributes:false trim-whitespace:false trim-code-tag:false mixed:false show_mixed:false lang:default decode:true show_mixed:false\">[\r\n    {\r\n        \"NAMELSAD\": \"Congressional District 8\",\r\n        \"coordinates\": [\r\n            [\r\n                [\r\n                    -119.651375,\r\n                    38.286637999999996\r\n                ],\r\n                [\r\n                    -119.650185,\r\n                    38.287234\r\n                ],<\/pre>\n<p>&#8230;and on and on and on for another 1.3MB of a result set. No wonder we don&#8217;t want to cut and paste this.<\/p>\n<p>Our goal, remember, is to end up with something which looks like this:<\/p>\n<pre class=\"toolbar-overlay:false toolbar-hide:false toolbar-delay:false show-title:false striped:false marking:false ranges:false nums:false nums-toggle:false wrap-toggle:false expand-toggle:false decode-attributes:false trim-whitespace:false trim-code-tag:false mixed:false show_mixed:false lang:default decode:true show_mixed:false\">[\r\n   \"33.4328, -114.7322\",\r\n   \"33.5253, -114.6561\",\r\n   \"33.6178, -114.5883\",\r\n   \"34.6173, -117.4220\"\r\n]<\/pre>\n<p>Here&#8217;s how we end up with just that:<\/p>\n<pre class=\"toolbar-overlay:false toolbar-hide:false toolbar-delay:false show-title:false striped:false marking:false ranges:false nums:false nums-toggle:false wrap-toggle:false expand-toggle:false decode-attributes:false trim-whitespace:false trim-code-tag:false mixed:false show_mixed:false lang:default decode:true show_mixed:false\">select value concat(tostring(c[1]),\", \",tostring(c[0])) points\r\nfrom (\r\n      select value districts.geometry.coordinates[0] from districts\r\n      use keys 'district::87'\r\n      )[0] c;<\/pre>\n<p>This is quite a mouthful, so let&#8217;s unpack it. Remember that we&#8217;re working with the data that we have, rather than what we might ideally want, and the polygon points we&#8217;re after are embedded in a nameless single-element array, embedded in another &#8220;coordinates&#8221; one, embedded in a &#8220;geometry&#8221; object. We need to unwind them one-by-one. First, let&#8217;s eliminate the nameless array wrapper. We do this by simply requesting that only the single (first, or &#8220;zeroth&#8221;) member of the array be returned:<\/p>\n<pre class=\"toolbar-overlay:false toolbar-hide:false toolbar-delay:false show-title:false striped:false marking:false ranges:false nums:false nums-toggle:false wrap-toggle:false expand-toggle:false decode-attributes:false trim-whitespace:false trim-code-tag:false mixed:false show_mixed:false lang:default decode:true show_mixed:false\">select districts.geometry.coordinates[0] from districts\r\n      use keys 'district::87'<\/pre>\n<p>The returned JSON object from this query looks like this:<\/p>\n<pre class=\"toolbar-overlay:false toolbar-hide:false toolbar-delay:false show-title:false striped:false marking:false ranges:false nums:false nums-toggle:false wrap-toggle:false expand-toggle:false decode-attributes:false trim-whitespace:false trim-code-tag:false mixed:false show_mixed:false lang:default decode:true show_mixed:false\">[\r\n    {\r\n        \"$1\": [\r\n            [\r\n                -119.651375,\r\n                38.286637999999996\r\n            ],\r\n            [\r\n                -119.650185,\r\n                38.287234\r\n            ],\r\n            [\r\n                -119.650139,\r\n                38.287678\r\n            ],<\/pre>\n<p>We can convert this to an array (as opposed to a JSON object) by using select value:<\/p>\n<pre class=\"toolbar-overlay:false toolbar-hide:false toolbar-delay:false show-title:false striped:false marking:false ranges:false nums:false nums-toggle:false wrap-toggle:false expand-toggle:false decode-attributes:false trim-whitespace:false trim-code-tag:false mixed:false show_mixed:false lang:default decode:true show_mixed:false\">select value districts.geometry.coordinates[0] from districts\r\n      use keys 'district::87'<\/pre>\n<p>Now we have the very large array we&#8217;re after, still wrapped in the single element of another array:<\/p>\n<pre class=\"toolbar-overlay:false toolbar-hide:false toolbar-delay:false show-title:false striped:false marking:false ranges:false nums:false nums-toggle:false wrap-toggle:false expand-toggle:false decode-attributes:false trim-whitespace:false trim-code-tag:false mixed:false show_mixed:false lang:default decode:true show_mixed:false\">[\r\n    [\r\n        [\r\n            -119.651375,\r\n            38.286637999999996\r\n        ],\r\n        [\r\n            -119.650185,\r\n            38.287234\r\n        ],\r\n        [\r\n            -119.650139,\r\n            38.287678\r\n        ],\r\n        [\r\n            -119.650154,\r\n            38.288041<\/pre>\n<p>Let&#8217;s select from that return set:<\/p>\n<pre class=\"toolbar-overlay:false toolbar-hide:false toolbar-delay:false show-title:false striped:false marking:false ranges:false nums:false nums-toggle:false wrap-toggle:false expand-toggle:false decode-attributes:false trim-whitespace:false trim-code-tag:false mixed:false show_mixed:false lang:default decode:true show_mixed:false\">select * from (\r\nselect value districts.geometry.coordinates[0] from districts\r\nuse keys 'district::87')[0] c<\/pre>\n<p>This yields a bunch of little objects we can bend to our will:<\/p>\n<pre class=\"toolbar-overlay:false toolbar-hide:false toolbar-delay:false show-title:false striped:false marking:false ranges:false nums:false nums-toggle:false wrap-toggle:false expand-toggle:false decode-attributes:false trim-whitespace:false trim-code-tag:false mixed:false show_mixed:false lang:default decode:true show_mixed:false\">[\r\n    {\r\n        \"c\": [\r\n            -119.651375,\r\n            38.286637999999996\r\n        ]\r\n    },\r\n    {\r\n        \"c\": [\r\n            -119.650185,\r\n            38.287234\r\n        ]\r\n    },<\/pre>\n<p>Now that we can address them let&#8217;s convert the types and perform our concatenation:<\/p>\n<pre class=\"toolbar-overlay:false toolbar-hide:false toolbar-delay:false show-title:false striped:false marking:false ranges:false nums:false nums-toggle:false wrap-toggle:false expand-toggle:false decode-attributes:false trim-whitespace:false trim-code-tag:false mixed:false show_mixed:false lang:default decode:true show_mixed:false\">select concat(tostring(c[1]),\", \",tostring(c[0])) points from (\r\nselect value districts.geometry.coordinates[0] from districts\r\n      use keys 'district::87')[0] c<\/pre>\n<p>The resulting objects look like this:<\/p>\n<pre class=\"toolbar-overlay:false toolbar-hide:false toolbar-delay:false show-title:false striped:false marking:false ranges:false nums:false nums-toggle:false wrap-toggle:false expand-toggle:false decode-attributes:false trim-whitespace:false trim-code-tag:false mixed:false show_mixed:false lang:default decode:true show_mixed:false\">[\r\n  {\r\n    \"points\": \"38.286637999999996, -119.651375\"\r\n  },\r\n  {\r\n    \"points\": \"38.287234, -119.650185\"\r\n  },\r\n  {\r\n    \"points\": \"38.287678, -119.650139\"\r\n  },<\/pre>\n<p>Now use select value to receive them as an array:<\/p>\n<pre class=\"toolbar-overlay:false toolbar-hide:false toolbar-delay:false show-title:false striped:false marking:false ranges:false nums:false nums-toggle:false wrap-toggle:false expand-toggle:false decode-attributes:false trim-whitespace:false trim-code-tag:false mixed:false show_mixed:false lang:default decode:true show_mixed:false\">select value concat(tostring(c[1]),\", \",tostring(c[0])) points from (\r\nselect value districts.geometry.coordinates[0] from districts\r\n      use keys 'district::87')[0] c<\/pre>\n<p>And we have the results we&#8217;re looking for:<\/p>\n<pre class=\"toolbar-overlay:false toolbar-hide:false toolbar-delay:false show-title:false striped:false marking:false ranges:false nums:false nums-toggle:false wrap-toggle:false expand-toggle:false decode-attributes:false trim-whitespace:false trim-code-tag:false mixed:false show_mixed:false lang:default decode:true show_mixed:false\">[\r\n  \"38.286637999999996, -119.651375\",\r\n  \"38.287234, -119.650185\",\r\n  \"38.287678, -119.650139\",\r\n  \"38.288041, -119.650154\",\r\n  \"38.288593999999996, -119.649699\",<\/pre>\n<h4>The Ease of CTEs<\/h4>\n<p>The last trick up our sleeve to pull this all together is a good one. We need a way to reference the array containing the geopoints as a component of a larger SQL statement. Fortunately, N1QL provides us with the means to do so in the form of Common Table Expressions (CTE).\u00a0 CTE, which are added to a query via the <em>with<\/em> clause, are evaluated once per query block and can be introduced before a select. This is exactly what we&#8217;re looking for:<\/p>\n<pre class=\"toolbar-overlay:false toolbar-hide:false toolbar-delay:false show-title:false striped:false marking:false ranges:false nums:false nums-toggle:false wrap-toggle:false expand-toggle:false decode-attributes:false trim-whitespace:false trim-code-tag:false mixed:false show_mixed:false lang:default decode:true show_mixed:false\">with geopoints as\r\n(\r\nselect value concat(tostring(c[1]),\", \",tostring(c[0])) points\r\nfrom ((select value d.geometry.coordinates[0] from districts d use keys 'district::87')[0]) c\r\n)<\/pre>\n<p>We now have access to an evaluated return set &#8220;geopoints&#8221; which can be referenced in subsequent (or multiple subsequent) SQL statements. Perfect. Here it is used in the final query:<\/p>\n<pre class=\"toolbar-overlay:false toolbar-hide:false toolbar-delay:false show-title:false striped:false marking:false ranges:false nums:false nums-toggle:false wrap-toggle:false expand-toggle:false decode-attributes:false trim-whitespace:false trim-code-tag:false mixed:false show_mixed:false lang:default decode:true show_mixed:false\">with geopoints as\r\n(\r\nselect value concat(tostring(c[1]),\", \",tostring(c[0])) points\r\nfrom ((select value d.geometry.coordinates[0] from districts d use keys 'district::87')[0]) c\r\n)\r\n\r\nselect Name\r\nfrom voters AS v\r\nwhere v._type = \"Voter\" AND search(v.Geo,\r\n{\r\n  \"query\":\r\n  {\r\n    \"conjuncts\":\r\n      [\r\n        {\r\n        \"field\": \"Geo\",\r\n        \"polygon_points\": geopoints\r\n        },\r\n        {\r\n        \"field\": \"Name\",\r\n        \"match\": \"Anne Murray\"\r\n        }\r\n      ]\r\n  }\r\n}\r\n);<\/pre>\n<p>There it is, then:\u00a0 A simple single-screen code block which retrieves the complex boundaries of a district and leverages them as part of a N1QL-driven geospatial search.\u00a0 Give the technique a try and conquer your own geographical challenges.<\/p>\n<p><em>Thanks very much to Brian Kane for his original post and to Dmitry Lychagin for help in unraveling the nested arrays.<\/em><\/p>\n","protected":false},"excerpt":{"rendered":"<p>The Basics Before you read any further, please take a few minutes and read the excellent post on geospatial search in Couchbase, as published by my friend and colleague Brian Kane: https:\/\/www.couchbase.com\/blog\/geospatial-search-how-do-i-use-thee-let-me-count-the-ways\/ Go ahead; I&#8217;ll wait. Now that you&#8217;re back, [&hellip;]<\/p>\n","protected":false},"author":41576,"featured_media":9633,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"inline_featured_image":false,"footnotes":""},"categories":[2165,8683,1812],"tags":[],"ppma_author":[9066],"class_list":["post-9628","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-full-text-search","category-geospatial","category-n1ql-query"],"acf":[],"yoast_head":"<!-- This site is optimized with the Yoast SEO Premium plugin v25.8 (Yoast SEO v25.8) - https:\/\/yoast.com\/wordpress\/plugins\/seo\/ -->\n<title>Taming the Gerrymander with Geospatial Search - The Couchbase Blog<\/title>\n<meta name=\"description\" content=\"Combining Couchbase geospatial search with N1QL queries will allow you to conquer complex geographical boundaries.\" \/>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/www.couchbase.com\/blog\/taming-the-gerrymander-with-geospatial-search\/\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"Taming the Gerrymander with Geospatial Search\" \/>\n<meta property=\"og:description\" content=\"Combining Couchbase geospatial search with N1QL queries will allow you to conquer complex geographical boundaries.\" \/>\n<meta property=\"og:url\" content=\"https:\/\/www.couchbase.com\/blog\/taming-the-gerrymander-with-geospatial-search\/\" \/>\n<meta property=\"og:site_name\" content=\"The Couchbase Blog\" \/>\n<meta property=\"article:published_time\" content=\"2020-11-04T23:51:31+00:00\" \/>\n<meta property=\"article:modified_time\" content=\"2024-03-01T17:18:35+00:00\" \/>\n<meta property=\"og:image\" content=\"https:\/\/www.couchbase.com\/blog\/wp-content\/uploads\/2020\/11\/Taming-the-Gerrymander-with-Geospatial-Search.png\" \/>\n\t<meta property=\"og:image:width\" content=\"1200\" \/>\n\t<meta property=\"og:image:height\" content=\"627\" \/>\n\t<meta property=\"og:image:type\" content=\"image\/png\" \/>\n<meta name=\"author\" content=\"Peter Reale\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:label1\" content=\"Written by\" \/>\n\t<meta name=\"twitter:data1\" content=\"Peter Reale\" \/>\n\t<meta name=\"twitter:label2\" content=\"Est. reading time\" \/>\n\t<meta name=\"twitter:data2\" content=\"7 minutes\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\/\/schema.org\",\"@graph\":[{\"@type\":\"Article\",\"@id\":\"https:\/\/www.couchbase.com\/blog\/taming-the-gerrymander-with-geospatial-search\/#article\",\"isPartOf\":{\"@id\":\"https:\/\/www.couchbase.com\/blog\/taming-the-gerrymander-with-geospatial-search\/\"},\"author\":{\"name\":\"Peter Reale\",\"@id\":\"https:\/\/www.couchbase.com\/blog\/#\/schema\/person\/391cf559b28ca6b4c1660a1ce283752c\"},\"headline\":\"Taming the Gerrymander with Geospatial Search\",\"datePublished\":\"2020-11-04T23:51:31+00:00\",\"dateModified\":\"2024-03-01T17:18:35+00:00\",\"mainEntityOfPage\":{\"@id\":\"https:\/\/www.couchbase.com\/blog\/taming-the-gerrymander-with-geospatial-search\/\"},\"wordCount\":1342,\"commentCount\":0,\"publisher\":{\"@id\":\"https:\/\/www.couchbase.com\/blog\/#organization\"},\"image\":{\"@id\":\"https:\/\/www.couchbase.com\/blog\/taming-the-gerrymander-with-geospatial-search\/#primaryimage\"},\"thumbnailUrl\":\"https:\/\/www.couchbase.com\/blog\/wp-content\/uploads\/sites\/1\/2020\/11\/Gerrymander.jpg\",\"articleSection\":[\"Full-Text Search\",\"Geospatial\",\"SQL++ \/ N1QL Query\"],\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"CommentAction\",\"name\":\"Comment\",\"target\":[\"https:\/\/www.couchbase.com\/blog\/taming-the-gerrymander-with-geospatial-search\/#respond\"]}]},{\"@type\":\"WebPage\",\"@id\":\"https:\/\/www.couchbase.com\/blog\/taming-the-gerrymander-with-geospatial-search\/\",\"url\":\"https:\/\/www.couchbase.com\/blog\/taming-the-gerrymander-with-geospatial-search\/\",\"name\":\"Taming the Gerrymander with Geospatial Search - The Couchbase Blog\",\"isPartOf\":{\"@id\":\"https:\/\/www.couchbase.com\/blog\/#website\"},\"primaryImageOfPage\":{\"@id\":\"https:\/\/www.couchbase.com\/blog\/taming-the-gerrymander-with-geospatial-search\/#primaryimage\"},\"image\":{\"@id\":\"https:\/\/www.couchbase.com\/blog\/taming-the-gerrymander-with-geospatial-search\/#primaryimage\"},\"thumbnailUrl\":\"https:\/\/www.couchbase.com\/blog\/wp-content\/uploads\/sites\/1\/2020\/11\/Gerrymander.jpg\",\"datePublished\":\"2020-11-04T23:51:31+00:00\",\"dateModified\":\"2024-03-01T17:18:35+00:00\",\"description\":\"Combining Couchbase geospatial search with N1QL queries will allow you to conquer complex geographical boundaries.\",\"breadcrumb\":{\"@id\":\"https:\/\/www.couchbase.com\/blog\/taming-the-gerrymander-with-geospatial-search\/#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\/\/www.couchbase.com\/blog\/taming-the-gerrymander-with-geospatial-search\/\"]}]},{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\/\/www.couchbase.com\/blog\/taming-the-gerrymander-with-geospatial-search\/#primaryimage\",\"url\":\"https:\/\/www.couchbase.com\/blog\/wp-content\/uploads\/sites\/1\/2020\/11\/Gerrymander.jpg\",\"contentUrl\":\"https:\/\/www.couchbase.com\/blog\/wp-content\/uploads\/sites\/1\/2020\/11\/Gerrymander.jpg\",\"width\":1764,\"height\":1847},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\/\/www.couchbase.com\/blog\/taming-the-gerrymander-with-geospatial-search\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\/\/www.couchbase.com\/blog\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Taming the Gerrymander with Geospatial Search\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\/\/www.couchbase.com\/blog\/#website\",\"url\":\"https:\/\/www.couchbase.com\/blog\/\",\"name\":\"The Couchbase Blog\",\"description\":\"Couchbase, the NoSQL Database\",\"publisher\":{\"@id\":\"https:\/\/www.couchbase.com\/blog\/#organization\"},\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\/\/www.couchbase.com\/blog\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"en-US\"},{\"@type\":\"Organization\",\"@id\":\"https:\/\/www.couchbase.com\/blog\/#organization\",\"name\":\"The Couchbase Blog\",\"url\":\"https:\/\/www.couchbase.com\/blog\/\",\"logo\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\/\/www.couchbase.com\/blog\/#\/schema\/logo\/image\/\",\"url\":\"https:\/\/www.couchbase.com\/blog\/wp-content\/uploads\/2023\/04\/admin-logo.png\",\"contentUrl\":\"https:\/\/www.couchbase.com\/blog\/wp-content\/uploads\/2023\/04\/admin-logo.png\",\"width\":218,\"height\":34,\"caption\":\"The Couchbase Blog\"},\"image\":{\"@id\":\"https:\/\/www.couchbase.com\/blog\/#\/schema\/logo\/image\/\"}},{\"@type\":\"Person\",\"@id\":\"https:\/\/www.couchbase.com\/blog\/#\/schema\/person\/391cf559b28ca6b4c1660a1ce283752c\",\"name\":\"Peter Reale\",\"image\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\/\/www.couchbase.com\/blog\/#\/schema\/person\/image\/ec988e11a3058e02331c83244e993ea2\",\"url\":\"https:\/\/secure.gravatar.com\/avatar\/19012c1e7433fcde0d634a14a0f76610c40ce876b6c5a04b23d43c2181301761?s=96&d=mm&r=g\",\"contentUrl\":\"https:\/\/secure.gravatar.com\/avatar\/19012c1e7433fcde0d634a14a0f76610c40ce876b6c5a04b23d43c2181301761?s=96&d=mm&r=g\",\"caption\":\"Peter Reale\"},\"description\":\"Peter Reale is a Senior Solutions Engineer at Couchbase and has been in the data business since 1984. He is based in Los Angeles.\",\"url\":\"https:\/\/www.couchbase.com\/blog\/author\/peter-reale\/\"}]}<\/script>\n<!-- \/ Yoast SEO Premium plugin. -->","yoast_head_json":{"title":"Taming the Gerrymander with Geospatial Search - The Couchbase Blog","description":"Combining Couchbase geospatial search with N1QL queries will allow you to conquer complex geographical boundaries.","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/www.couchbase.com\/blog\/taming-the-gerrymander-with-geospatial-search\/","og_locale":"en_US","og_type":"article","og_title":"Taming the Gerrymander with Geospatial Search","og_description":"Combining Couchbase geospatial search with N1QL queries will allow you to conquer complex geographical boundaries.","og_url":"https:\/\/www.couchbase.com\/blog\/taming-the-gerrymander-with-geospatial-search\/","og_site_name":"The Couchbase Blog","article_published_time":"2020-11-04T23:51:31+00:00","article_modified_time":"2024-03-01T17:18:35+00:00","og_image":[{"width":1200,"height":627,"url":"https:\/\/www.couchbase.com\/blog\/wp-content\/uploads\/2020\/11\/Taming-the-Gerrymander-with-Geospatial-Search.png","type":"image\/png"}],"author":"Peter Reale","twitter_card":"summary_large_image","twitter_misc":{"Written by":"Peter Reale","Est. reading time":"7 minutes"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"Article","@id":"https:\/\/www.couchbase.com\/blog\/taming-the-gerrymander-with-geospatial-search\/#article","isPartOf":{"@id":"https:\/\/www.couchbase.com\/blog\/taming-the-gerrymander-with-geospatial-search\/"},"author":{"name":"Peter Reale","@id":"https:\/\/www.couchbase.com\/blog\/#\/schema\/person\/391cf559b28ca6b4c1660a1ce283752c"},"headline":"Taming the Gerrymander with Geospatial Search","datePublished":"2020-11-04T23:51:31+00:00","dateModified":"2024-03-01T17:18:35+00:00","mainEntityOfPage":{"@id":"https:\/\/www.couchbase.com\/blog\/taming-the-gerrymander-with-geospatial-search\/"},"wordCount":1342,"commentCount":0,"publisher":{"@id":"https:\/\/www.couchbase.com\/blog\/#organization"},"image":{"@id":"https:\/\/www.couchbase.com\/blog\/taming-the-gerrymander-with-geospatial-search\/#primaryimage"},"thumbnailUrl":"https:\/\/www.couchbase.com\/blog\/wp-content\/uploads\/sites\/1\/2020\/11\/Gerrymander.jpg","articleSection":["Full-Text Search","Geospatial","SQL++ \/ N1QL Query"],"inLanguage":"en-US","potentialAction":[{"@type":"CommentAction","name":"Comment","target":["https:\/\/www.couchbase.com\/blog\/taming-the-gerrymander-with-geospatial-search\/#respond"]}]},{"@type":"WebPage","@id":"https:\/\/www.couchbase.com\/blog\/taming-the-gerrymander-with-geospatial-search\/","url":"https:\/\/www.couchbase.com\/blog\/taming-the-gerrymander-with-geospatial-search\/","name":"Taming the Gerrymander with Geospatial Search - The Couchbase Blog","isPartOf":{"@id":"https:\/\/www.couchbase.com\/blog\/#website"},"primaryImageOfPage":{"@id":"https:\/\/www.couchbase.com\/blog\/taming-the-gerrymander-with-geospatial-search\/#primaryimage"},"image":{"@id":"https:\/\/www.couchbase.com\/blog\/taming-the-gerrymander-with-geospatial-search\/#primaryimage"},"thumbnailUrl":"https:\/\/www.couchbase.com\/blog\/wp-content\/uploads\/sites\/1\/2020\/11\/Gerrymander.jpg","datePublished":"2020-11-04T23:51:31+00:00","dateModified":"2024-03-01T17:18:35+00:00","description":"Combining Couchbase geospatial search with N1QL queries will allow you to conquer complex geographical boundaries.","breadcrumb":{"@id":"https:\/\/www.couchbase.com\/blog\/taming-the-gerrymander-with-geospatial-search\/#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/www.couchbase.com\/blog\/taming-the-gerrymander-with-geospatial-search\/"]}]},{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/www.couchbase.com\/blog\/taming-the-gerrymander-with-geospatial-search\/#primaryimage","url":"https:\/\/www.couchbase.com\/blog\/wp-content\/uploads\/sites\/1\/2020\/11\/Gerrymander.jpg","contentUrl":"https:\/\/www.couchbase.com\/blog\/wp-content\/uploads\/sites\/1\/2020\/11\/Gerrymander.jpg","width":1764,"height":1847},{"@type":"BreadcrumbList","@id":"https:\/\/www.couchbase.com\/blog\/taming-the-gerrymander-with-geospatial-search\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/www.couchbase.com\/blog\/"},{"@type":"ListItem","position":2,"name":"Taming the Gerrymander with Geospatial Search"}]},{"@type":"WebSite","@id":"https:\/\/www.couchbase.com\/blog\/#website","url":"https:\/\/www.couchbase.com\/blog\/","name":"The Couchbase Blog","description":"Couchbase, the NoSQL Database","publisher":{"@id":"https:\/\/www.couchbase.com\/blog\/#organization"},"potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/www.couchbase.com\/blog\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"en-US"},{"@type":"Organization","@id":"https:\/\/www.couchbase.com\/blog\/#organization","name":"The Couchbase Blog","url":"https:\/\/www.couchbase.com\/blog\/","logo":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/www.couchbase.com\/blog\/#\/schema\/logo\/image\/","url":"https:\/\/www.couchbase.com\/blog\/wp-content\/uploads\/2023\/04\/admin-logo.png","contentUrl":"https:\/\/www.couchbase.com\/blog\/wp-content\/uploads\/2023\/04\/admin-logo.png","width":218,"height":34,"caption":"The Couchbase Blog"},"image":{"@id":"https:\/\/www.couchbase.com\/blog\/#\/schema\/logo\/image\/"}},{"@type":"Person","@id":"https:\/\/www.couchbase.com\/blog\/#\/schema\/person\/391cf559b28ca6b4c1660a1ce283752c","name":"Peter Reale","image":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/www.couchbase.com\/blog\/#\/schema\/person\/image\/ec988e11a3058e02331c83244e993ea2","url":"https:\/\/secure.gravatar.com\/avatar\/19012c1e7433fcde0d634a14a0f76610c40ce876b6c5a04b23d43c2181301761?s=96&d=mm&r=g","contentUrl":"https:\/\/secure.gravatar.com\/avatar\/19012c1e7433fcde0d634a14a0f76610c40ce876b6c5a04b23d43c2181301761?s=96&d=mm&r=g","caption":"Peter Reale"},"description":"Peter Reale is a Senior Solutions Engineer at Couchbase and has been in the data business since 1984. He is based in Los Angeles.","url":"https:\/\/www.couchbase.com\/blog\/author\/peter-reale\/"}]}},"authors":[{"term_id":9066,"user_id":41576,"is_guest":0,"slug":"peter-reale","display_name":"Peter Reale","avatar_url":"https:\/\/secure.gravatar.com\/avatar\/19012c1e7433fcde0d634a14a0f76610c40ce876b6c5a04b23d43c2181301761?s=96&d=mm&r=g","author_category":"","last_name":"Reale, Senior Solutions Engineer, Couchbase","first_name":"Peter","job_title":"","user_url":"","description":"Peter Reale is a Senior Solutions Engineer at Couchbase and has been in the data business since 1984.  He is based in Los Angeles."}],"_links":{"self":[{"href":"https:\/\/www.couchbase.com\/blog\/wp-json\/wp\/v2\/posts\/9628","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.couchbase.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.couchbase.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.couchbase.com\/blog\/wp-json\/wp\/v2\/users\/41576"}],"replies":[{"embeddable":true,"href":"https:\/\/www.couchbase.com\/blog\/wp-json\/wp\/v2\/comments?post=9628"}],"version-history":[{"count":0,"href":"https:\/\/www.couchbase.com\/blog\/wp-json\/wp\/v2\/posts\/9628\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.couchbase.com\/blog\/wp-json\/wp\/v2\/media\/9633"}],"wp:attachment":[{"href":"https:\/\/www.couchbase.com\/blog\/wp-json\/wp\/v2\/media?parent=9628"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.couchbase.com\/blog\/wp-json\/wp\/v2\/categories?post=9628"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.couchbase.com\/blog\/wp-json\/wp\/v2\/tags?post=9628"},{"taxonomy":"author","embeddable":true,"href":"https:\/\/www.couchbase.com\/blog\/wp-json\/wp\/v2\/ppma_author?post=9628"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}