Commit 9bdb41583b1b49ea725ca84cd764ee5c3bf0a0a2

Authored by will-r
1 parent 40bee0a0

ankane readme and test improvements, other tidying up

@@ -834,7 +834,7 @@ product = Product.first @@ -834,7 +834,7 @@ product = Product.first
834 product.similar(fields: ["name"], where: {size: "12 oz"}) 834 product.similar(fields: ["name"], where: {size: "12 oz"})
835 ``` 835 ```
836 836
837 -### Simple Geospatial Indexing: geo_points 837 +### Geospatial Searches
838 838
839 If your data consists of point values, searchkick offers a useful shorthand: 839 If your data consists of point values, searchkick offers a useful shorthand:
840 840
@@ -883,7 +883,7 @@ City.search "san", boost_by_distance: {field: :location, origin: {lat: 37, lon: @@ -883,7 +883,7 @@ City.search "san", boost_by_distance: {field: :location, origin: {lat: 37, lon:
883 ``` 883 ```
884 884
885 885
886 -### Complex Geospatial Indexing: geo_shapes 886 +### Geo Shapes
887 887
888 You can also pass through complex or varied shapes as GeoJSON objects. 888 You can also pass through complex or varied shapes as GeoJSON objects.
889 889
@@ -911,28 +911,23 @@ end @@ -911,28 +911,23 @@ end
911 911
912 The `geo_shapes` hash is passed through to elasticsearch without modification. Please see the [geo_shape data type documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/geo-shape.html) for options. 912 The `geo_shapes` hash is passed through to elasticsearch without modification. Please see the [geo_shape data type documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/geo-shape.html) for options.
913 913
914 -Any geospatial data type can be used in the index or in the search. It is up to you to ensure that it is a valid geoJSON representation. The possible shapes are: 914 +Any geospatial data type can be held in the index or give as a search query. It is up to you to ensure that it is a valid geoJSON representation. The possible shapes are:
915 915
916 * **point**: single lat/lon pair 916 * **point**: single lat/lon pair
917 * **multipoint**: array of points 917 * **multipoint**: array of points
918 * **linestring**: array of at least two lat/lon pairs 918 * **linestring**: array of at least two lat/lon pairs
919 * **multilinestring**: array of lines 919 * **multilinestring**: array of lines
920 -* **polygon**: an array of paths, each being an array of at least four lat/lon pairs whose first and last points are the same. Paths after the first represent exclusions. 920 +* **polygon**: an array of paths, each being an array of at least four lat/lon pairs whose first and last points are the same. Paths after the first represent exclusions. Elasticsearch will return an error if a polygon contains two consecutive identical points, intersects itself or is not closed.
921 * **multipolygon**: array of polygons 921 * **multipolygon**: array of polygons
922 * **envelope**: a bounding box defined by top left and bottom right points 922 * **envelope**: a bounding box defined by top left and bottom right points
923 * **circle**: a bounding circle defined by center point and radius 923 * **circle**: a bounding circle defined by center point and radius
924 * **geometrycollection**: an array of separate geoJSON objects possibly of various types 924 * **geometrycollection**: an array of separate geoJSON objects possibly of various types
925 925
926 -See the [Elasticsearch documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/geo-shape.html) for details. GeoJSON coordinates are usually given as an array of `[lon, lat]` points but this often causes swapping errors so searchkick can also take objects with `lon` and `lat` keys. 926 +See the [Elasticsearch documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/geo-shape.html) for details. GeoJSON coordinates are usually given as an array of `[lon, lat]` points but searchkick can also take objects with `lon` and `lat` keys.
927 927
928 -Elasticsearch is sensitive about geo_shape validity. For example it will throw an exception if a polygon contains two consecutive identical points, intersects itself or is not properly closed. 928 +Once a geo_shape index is established, you can include a geo_shape filter in any search. This also takes a geoJSON shape, and will return a list of items based on their overlap with that shape.
929 929
930 -  
931 -### Geospatial searching  
932 -  
933 -Once a geo_shape index is established, you can apply a geo_shape filter to any search. This also takes a geoJSON shape, and will return a list of items based on their overlap with that shape.  
934 -  
935 -Intersecting with the query shape: 930 +Find shapes (of any kind) intersecting with the query shape:
936 931
937 ```ruby 932 ```ruby
938 City.search "san", where: {bounds: {geo_shape: {type: "polygon", coordinates: [[{lat: 38, lon: -123}, ...]]}}} 933 City.search "san", where: {bounds: {geo_shape: {type: "polygon", coordinates: [[{lat: 38, lon: -123}, ...]]}}}
@@ -941,22 +936,21 @@ City.search "san", where: {bounds: {geo_shape: {type: "polygon", coordinates: [[ @@ -941,22 +936,21 @@ City.search "san", where: {bounds: {geo_shape: {type: "polygon", coordinates: [[
941 Falling entirely within the query shape: 936 Falling entirely within the query shape:
942 937
943 ```ruby 938 ```ruby
944 -City.search "san", where: {relation: "within", bounds: {geo_shape: {type: "circle", coordinates: [{lat: 38, lon: -123}], radius: "1km"}}} 939 +City.search "san", where: {bounds: {geo_shape: {type: "circle", relation: "within", coordinates: [{lat: 38, lon: -123}], radius: "1km"}}}
945 ``` 940 ```
946 941
947 Not touching the query shape: 942 Not touching the query shape:
948 943
949 ```ruby 944 ```ruby
950 -City.search "san", where: {relation: "disjoint", bounds: {geo_shape: {type: "envelope", coordinates: [{lat: 38, lon: -123}, {lat: 37, lon: -122}]}}} 945 +City.search "san", where: {bounds: {geo_shape: {type: "envelope", relation: "disjoint", coordinates: [{lat: 38, lon: -123}, {lat: 37, lon: -122}]}}}
951 ``` 946 ```
952 947
953 -Envelope is a special case. For consistency, searchkick also understands coordinates given as top_left and bottom_right: 948 +Containing the query shape (ElasticSearch 2.2+):
954 949
955 ```ruby 950 ```ruby
956 -City.search "san", where: {relation: "within", bounds: {geo_shape: {type: "envelope", top_left: {lat: 38, lon: -123}, bottom_right: {lat: 37, lon: -122}}}} 951 +City.search "san", where: {bounds: {geo_shape: {type: "envelope", relation: "contains", coordinates: [{lat: 38, lon: -123}, {lat: 37, lon: -122}]}}}
957 ``` 952 ```
958 953
959 -  
960 ### Routing 954 ### Routing
961 955
962 Searchkick supports [Elasticsearchโ€™s routing feature](https://www.elastic.co/blog/customizing-your-document-routing). 956 Searchkick supports [Elasticsearchโ€™s routing feature](https://www.elastic.co/blog/customizing-your-document-routing).
lib/searchkick/query.rb
@@ -809,15 +809,7 @@ module Searchkick @@ -809,15 +809,7 @@ module Searchkick
809 } 809 }
810 } 810 }
811 when :geo_shape 811 when :geo_shape
812 - if op_value[:type] == "envelope" && op_value[:top_left].present? && op_value[:bottom_right].present?  
813 - op_value[:coordinates] = [coordinate_array(op_value[:top_left]), coordinate_array(op_value[:bottom_right])]  
814 - op_value.delete(:top_left)  
815 - op_value.delete(:bottom_right)  
816 - elsif op_value[:type] == "circle"  
817 - op_value[:coordinates] = coordinate_array(op_value[:coordinates] || [])  
818 - else  
819 - op_value[:coordinates] = (op_value[:coordinates] || []).map { |loc| coordinate_array(loc) }  
820 - end 812 + op_value[:coordinates] = coordinate_array(op_value[:coordinates]) if op_value[:coordinates]
821 relation = op_value.delete(:relation) || 'intersects' 813 relation = op_value.delete(:relation) || 'intersects'
822 filters << { 814 filters << {
823 geo_shape: { 815 geo_shape: {
@@ -943,9 +935,7 @@ module Searchkick @@ -943,9 +935,7 @@ module Searchkick
943 end 935 end
944 936
945 # Recursively descend through nesting of arrays until we reach either a lat/lon object or an array of numbers, 937 # Recursively descend through nesting of arrays until we reach either a lat/lon object or an array of numbers,
946 - # eventually returning the same structure with all values transformed to [lon, lat]. Question: should we reverse  
947 - # the array order so that arguments can be given as [lat, lon], as happens elsewhere in searchkick? We are moving  
948 - # GeoJSON around so it seems better to stick to that specification, though the lat/lon objects are already a deviation. 938 + # eventually returning the same structure with all values transformed to [lon, lat].
949 # 939 #
950 def coordinate_array(value) 940 def coordinate_array(value)
951 if value.is_a?(Hash) 941 if value.is_a?(Hash)
test/geo_shape_test.rb
1 -require "pp"  
2 require_relative "test_helper" 1 require_relative "test_helper"
3 2
4 class GeoShapeTest < Minitest::Test 3 class GeoShapeTest < Minitest::Test
5 -  
6 - def test_geo_shape  
7 - regions = [ 4 + def setup
  5 + super
  6 + store [
8 {name: "Region A", text: "The witch had a cat", territory: "30,40,35,45,40,40,40,30,30,30,30,40"}, 7 {name: "Region A", text: "The witch had a cat", territory: "30,40,35,45,40,40,40,30,30,30,30,40"},
9 {name: "Region B", text: "and a very tall hat", territory: "50,60,55,65,60,60,60,50,50,50,50,60"}, 8 {name: "Region B", text: "and a very tall hat", territory: "50,60,55,65,60,60,60,50,50,50,50,60"},
10 - {name: "Region C", text: "and long ginger hair which she wore in a plait.", territory: "10,20,15,25,20,20,20,10,10,10,10,20"},  
11 - ]  
12 - store regions, Region 9 + {name: "Region C", text: "and long ginger hair which she wore in a plait", territory: "10,20,15,25,20,20,20,10,10,10,10,20"}
  10 + ], Region
  11 + end
13 12
14 - # circle 13 + def test_circle
15 assert_search "*", ["Region A"], { 14 assert_search "*", ["Region A"], {
16 where: { 15 where: {
17 territory: { 16 territory: {
@@ -23,8 +22,9 @@ class GeoShapeTest &lt; Minitest::Test @@ -23,8 +22,9 @@ class GeoShapeTest &lt; Minitest::Test
23 } 22 }
24 } 23 }
25 }, Region 24 }, Region
  25 + end
26 26
27 - # envelope 27 + def test_envelope
28 assert_search "*", ["Region A"], { 28 assert_search "*", ["Region A"], {
29 where: { 29 where: {
30 territory: { 30 territory: {
@@ -35,21 +35,9 @@ class GeoShapeTest &lt; Minitest::Test @@ -35,21 +35,9 @@ class GeoShapeTest &lt; Minitest::Test
35 } 35 }
36 } 36 }
37 }, Region 37 }, Region
  38 + end
38 39
39 - # envelope as corners  
40 - assert_search "*", ["Region A"], {  
41 - where: {  
42 - territory: {  
43 - geo_shape: {  
44 - type: "envelope",  
45 - top_left: {lat: 42.0, lon: 28.0},  
46 - bottom_right: {lat: 38.0, lon: 32.0}  
47 - }  
48 - }  
49 - }  
50 - }, Region  
51 -  
52 - # polygon 40 + def test_polygon
53 assert_search "*", ["Region A"], { 41 assert_search "*", ["Region A"], {
54 where: { 42 where: {
55 territory: { 43 territory: {
@@ -60,8 +48,9 @@ class GeoShapeTest &lt; Minitest::Test @@ -60,8 +48,9 @@ class GeoShapeTest &lt; Minitest::Test
60 } 48 }
61 } 49 }
62 }, Region 50 }, Region
  51 + end
63 52
64 - # multipolygon 53 + def test_multipolygon
65 assert_search "*", ["Region A", "Region B"], { 54 assert_search "*", ["Region A", "Region B"], {
66 where: { 55 where: {
67 territory: { 56 territory: {
@@ -75,8 +64,9 @@ class GeoShapeTest &lt; Minitest::Test @@ -75,8 +64,9 @@ class GeoShapeTest &lt; Minitest::Test
75 } 64 }
76 } 65 }
77 }, Region 66 }, Region
  67 + end
78 68
79 - # disjoint 69 + def test_disjoint
80 assert_search "*", ["Region B", "Region C"], { 70 assert_search "*", ["Region B", "Region C"], {
81 where: { 71 where: {
82 territory: { 72 territory: {
@@ -88,8 +78,9 @@ class GeoShapeTest &lt; Minitest::Test @@ -88,8 +78,9 @@ class GeoShapeTest &lt; Minitest::Test
88 } 78 }
89 } 79 }
90 }, Region 80 }, Region
  81 + end
91 82
92 - # within 83 + def test_within
93 assert_search "*", ["Region A"], { 84 assert_search "*", ["Region A"], {
94 where: { 85 where: {
95 territory: { 86 territory: {
@@ -101,8 +92,9 @@ class GeoShapeTest &lt; Minitest::Test @@ -101,8 +92,9 @@ class GeoShapeTest &lt; Minitest::Test
101 } 92 }
102 } 93 }
103 }, Region 94 }, Region
  95 + end
104 96
105 - # with search 97 + def test_search_math
106 assert_search "witch", ["Region A"], { 98 assert_search "witch", ["Region A"], {
107 where: { 99 where: {
108 territory: { 100 territory: {
@@ -113,7 +105,9 @@ class GeoShapeTest &lt; Minitest::Test @@ -113,7 +105,9 @@ class GeoShapeTest &lt; Minitest::Test
113 } 105 }
114 } 106 }
115 }, Region 107 }, Region
  108 + end
116 109
  110 + def test_search_no_match
117 assert_search "ginger hair", [], { 111 assert_search "ginger hair", [], {
118 where: { 112 where: {
119 territory: { 113 territory: {
@@ -126,20 +120,18 @@ class GeoShapeTest &lt; Minitest::Test @@ -126,20 +120,18 @@ class GeoShapeTest &lt; Minitest::Test
126 }, Region 120 }, Region
127 end 121 end
128 122
129 - def test_geo_shape_contains 123 + def test_contains
130 skip if elasticsearch_below22? 124 skip if elasticsearch_below22?
131 -  
132 - assert_search "*", ["Region A"], { 125 + assert_search "*", ["Region C"], {
133 where: { 126 where: {
134 territory: { 127 territory: {
135 geo_shape: { 128 geo_shape: {
136 type: "envelope", 129 type: "envelope",
137 relation: "contains", 130 relation: "contains",
138 - coordinates: [[32, 33], [33, 32]] 131 + coordinates: [[12, 13], [13,12]]
139 } 132 }
140 } 133 }
141 } 134 }
142 }, Region 135 }, Region
143 -  
144 end 136 end
145 end 137 end
test/test_helper.rb
@@ -435,6 +435,7 @@ class Minitest::Test @@ -435,6 +435,7 @@ class Minitest::Test
435 Store.destroy_all 435 Store.destroy_all
436 Animal.destroy_all 436 Animal.destroy_all
437 Speaker.destroy_all 437 Speaker.destroy_all
  438 + Region.destroy_all
438 end 439 end
439 440
440 protected 441 protected