Commit 9bdb41583b1b49ea725ca84cd764ee5c3bf0a0a2
1 parent
40bee0a0
Exists in
master
and in
21 other branches
ankane readme and test improvements, other tidying up
Showing
4 changed files
with
38 additions
and
61 deletions
Show diff stats
README.md
... | ... | @@ -834,7 +834,7 @@ product = Product.first |
834 | 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 | 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 | 883 | ``` |
884 | 884 | |
885 | 885 | |
886 | -### Complex Geospatial Indexing: geo_shapes | |
886 | +### Geo Shapes | |
887 | 887 | |
888 | 888 | You can also pass through complex or varied shapes as GeoJSON objects. |
889 | 889 | |
... | ... | @@ -911,28 +911,23 @@ end |
911 | 911 | |
912 | 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 | 916 | * **point**: single lat/lon pair |
917 | 917 | * **multipoint**: array of points |
918 | 918 | * **linestring**: array of at least two lat/lon pairs |
919 | 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 | 921 | * **multipolygon**: array of polygons |
922 | 922 | * **envelope**: a bounding box defined by top left and bottom right points |
923 | 923 | * **circle**: a bounding circle defined by center point and radius |
924 | 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 | 932 | ```ruby |
938 | 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 | 936 | Falling entirely within the query shape: |
942 | 937 | |
943 | 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 | 942 | Not touching the query shape: |
948 | 943 | |
949 | 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 | 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 | 954 | ### Routing |
961 | 955 | |
962 | 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 | 809 | } |
810 | 810 | } |
811 | 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 | 813 | relation = op_value.delete(:relation) || 'intersects' |
822 | 814 | filters << { |
823 | 815 | geo_shape: { |
... | ... | @@ -943,9 +935,7 @@ module Searchkick |
943 | 935 | end |
944 | 936 | |
945 | 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 | 940 | def coordinate_array(value) |
951 | 941 | if value.is_a?(Hash) | ... | ... |
test/geo_shape_test.rb
1 | -require "pp" | |
2 | 1 | require_relative "test_helper" |
3 | 2 | |
4 | 3 | class GeoShapeTest < Minitest::Test |
5 | - | |
6 | - def test_geo_shape | |
7 | - regions = [ | |
4 | + def setup | |
5 | + super | |
6 | + store [ | |
8 | 7 | {name: "Region A", text: "The witch had a cat", territory: "30,40,35,45,40,40,40,30,30,30,30,40"}, |
9 | 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 | 14 | assert_search "*", ["Region A"], { |
16 | 15 | where: { |
17 | 16 | territory: { |
... | ... | @@ -23,8 +22,9 @@ class GeoShapeTest < Minitest::Test |
23 | 22 | } |
24 | 23 | } |
25 | 24 | }, Region |
25 | + end | |
26 | 26 | |
27 | - # envelope | |
27 | + def test_envelope | |
28 | 28 | assert_search "*", ["Region A"], { |
29 | 29 | where: { |
30 | 30 | territory: { |
... | ... | @@ -35,21 +35,9 @@ class GeoShapeTest < Minitest::Test |
35 | 35 | } |
36 | 36 | } |
37 | 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 | 41 | assert_search "*", ["Region A"], { |
54 | 42 | where: { |
55 | 43 | territory: { |
... | ... | @@ -60,8 +48,9 @@ class GeoShapeTest < Minitest::Test |
60 | 48 | } |
61 | 49 | } |
62 | 50 | }, Region |
51 | + end | |
63 | 52 | |
64 | - # multipolygon | |
53 | + def test_multipolygon | |
65 | 54 | assert_search "*", ["Region A", "Region B"], { |
66 | 55 | where: { |
67 | 56 | territory: { |
... | ... | @@ -75,8 +64,9 @@ class GeoShapeTest < Minitest::Test |
75 | 64 | } |
76 | 65 | } |
77 | 66 | }, Region |
67 | + end | |
78 | 68 | |
79 | - # disjoint | |
69 | + def test_disjoint | |
80 | 70 | assert_search "*", ["Region B", "Region C"], { |
81 | 71 | where: { |
82 | 72 | territory: { |
... | ... | @@ -88,8 +78,9 @@ class GeoShapeTest < Minitest::Test |
88 | 78 | } |
89 | 79 | } |
90 | 80 | }, Region |
81 | + end | |
91 | 82 | |
92 | - # within | |
83 | + def test_within | |
93 | 84 | assert_search "*", ["Region A"], { |
94 | 85 | where: { |
95 | 86 | territory: { |
... | ... | @@ -101,8 +92,9 @@ class GeoShapeTest < Minitest::Test |
101 | 92 | } |
102 | 93 | } |
103 | 94 | }, Region |
95 | + end | |
104 | 96 | |
105 | - # with search | |
97 | + def test_search_math | |
106 | 98 | assert_search "witch", ["Region A"], { |
107 | 99 | where: { |
108 | 100 | territory: { |
... | ... | @@ -113,7 +105,9 @@ class GeoShapeTest < Minitest::Test |
113 | 105 | } |
114 | 106 | } |
115 | 107 | }, Region |
108 | + end | |
116 | 109 | |
110 | + def test_search_no_match | |
117 | 111 | assert_search "ginger hair", [], { |
118 | 112 | where: { |
119 | 113 | territory: { |
... | ... | @@ -126,20 +120,18 @@ class GeoShapeTest < Minitest::Test |
126 | 120 | }, Region |
127 | 121 | end |
128 | 122 | |
129 | - def test_geo_shape_contains | |
123 | + def test_contains | |
130 | 124 | skip if elasticsearch_below22? |
131 | - | |
132 | - assert_search "*", ["Region A"], { | |
125 | + assert_search "*", ["Region C"], { | |
133 | 126 | where: { |
134 | 127 | territory: { |
135 | 128 | geo_shape: { |
136 | 129 | type: "envelope", |
137 | 130 | relation: "contains", |
138 | - coordinates: [[32, 33], [33, 32]] | |
131 | + coordinates: [[12, 13], [13,12]] | |
139 | 132 | } |
140 | 133 | } |
141 | 134 | } |
142 | 135 | }, Region |
143 | - | |
144 | 136 | end |
145 | 137 | end | ... | ... |