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,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 < Minitest::Test | @@ -23,8 +22,9 @@ class GeoShapeTest < 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 < Minitest::Test | @@ -35,21 +35,9 @@ class GeoShapeTest < 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 < Minitest::Test | @@ -60,8 +48,9 @@ class GeoShapeTest < 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 < Minitest::Test | @@ -75,8 +64,9 @@ class GeoShapeTest < 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 < Minitest::Test | @@ -88,8 +78,9 @@ class GeoShapeTest < 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 < Minitest::Test | @@ -101,8 +92,9 @@ class GeoShapeTest < 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 < Minitest::Test | @@ -113,7 +105,9 @@ class GeoShapeTest < 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 < Minitest::Test | @@ -126,20 +120,18 @@ class GeoShapeTest < 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