dynamodb_geo 0.0.2 → 0.1.5

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 34aa9b9ef408a4556afbc74c173c40ccb5932fc0a47d29a3e519c0cb803fe043
4
- data.tar.gz: f7dd0b6c92a94e7da2c14417ee67296af8f04f86b768bfb2c8acd32572bdd8e9
3
+ metadata.gz: f54f79a1153702913ca165972a8936865949156e28970e62efc06e23076b0773
4
+ data.tar.gz: 4171efed028b0cc80b39b35fe951c065da01ac6b7978c11e4264e9da5e0d5be1
5
5
  SHA512:
6
- metadata.gz: 9d3d2f46f82c2e9ece62dc006dbda654753d01fb7d82d06d803448171a9852cef5933ce814ce20057017ed9fcb318b9f6ff37dbe79cb7f5167c82aea65f3258c
7
- data.tar.gz: 8df26e8041dfbb59020496faf2ae5014fae971b9b7d0f84274ff4eeb58c0c70487623a7a44343a4443647d08cfe72acc48c8115004c287853c2896dc46e91b9a
6
+ metadata.gz: 04fec0da4cf2f1f62df873fd364dda594028ad566b283bd6ad56573f5d8c156237c5eb691546697001195944a483038931ee4728a8f317d9600b5114bb22af57
7
+ data.tar.gz: d3a7da451c4e43850c0a3b44db65f967bd28df66ea35848be57d28eb5755a47a1cb1df7a2d87a29bb0af94eec583a889ba0231f35112f4bafa0003a86a539e68
data/README.md CHANGED
@@ -1,7 +1,129 @@
1
1
  # DynamoDB_Geo
2
+ ## DynamoDB
3
+ This is an attempt at storing and querying geohash data in DynamoDB.
2
4
 
5
+ We store objects in DynamoDB, when we query, we look for a customizable number of stored items by zooming out exactly one level.
6
+ We return the objects in an attempt at the closest items first.
3
7
 
4
- # Geohashing
8
+ Items are searched as the following.
9
+
10
+ Given the lat, long we calculate a hash - assume 9x0qz. We go up one level to 9x0q and calculate all neighbouring hashes.
11
+
12
+ 9x0p 9x0r 9x0x
13
+ 9x0n 9x0q 9x0w => ["9x0q", "9x0r", "9x0x", "9x0w", "9x0t", "9x0m", "9x0j", "9x0n", "9x0p"]
14
+ 9x0j 9x0m 9x0t
15
+
16
+ From this, we ask DynamoDB starting from our own cell, going north, and wrapping clockwise for all objects in these cells up to a configurable number of objects.
17
+ We then calculate the neighbours of our more local hash 9x0qz, and sort the results of the larger hashes using similar rules.
18
+
19
+ 9x0rn 9x0rp 9x0x0
20
+ 9x0qy 9x0qz 9x0wb => ["9x0qz", "9x0rp", "9x0x0", "9x0wb", "9x0w8", "9x0qx", "9x0qw", "9x0qy", "9x0rn"]
21
+ 9x0qw 9x0qx 9x0w8
22
+
23
+ Using the same pattern as before (Middle -> North -> Clockwise), we sort the returned stores giving priority to the localization.
24
+
25
+ ### Real talk
26
+ I know this isn't the most wonderful way to do this, I am still trying to think of something better. Currently it uses up to 8 queries to DynamoDB, I'd like to cut that down to a single query.
27
+
28
+ There is a similar library written in Java and JS, but it uses Google S2 for the Geohashing, which has properties that allow them to do the zoom-out technique with a single query, however Google S2 does not exist as a C library (or Ruby library for that matter). The other alternative is Uber H3, however it has the same issues of not being C, or Ruby.
29
+
30
+ If we were able to use a unique but deterministic way to calcuate the range key based off of the hash key that would allow us to query every single larger cell in a single batch query. I am currently thinking of more clever ways to do this - obviously I am open to suggestions.
31
+
32
+ ## Objects
33
+ ### DynamodbManager
34
+
35
+ **Attributes**
36
+ table_name:
37
+ Sets the DynamoDB Table Name
38
+
39
+ hash_key:
40
+ Sets the name of the primary hash attribute
41
+
42
+ range_key:
43
+ Sets the name of the sort/range attribute
44
+
45
+ geohash_key:
46
+ Sets the name of the localized hash attribute
47
+
48
+ geojson_key:
49
+ Sets the name of the metadata attribute
50
+
51
+ hash_key_length:
52
+ Sets size of the outer hash length
53
+
54
+ local_area_size:
55
+ Sets the size of the inner hash length
56
+
57
+ max_item_return:
58
+ Sets the max number of items to return
59
+
60
+ **Methods**
61
+ Initialization
62
+
63
+ Description: Creates a new DynamodbManager object. Inputs are variables to your AWS account.
64
+ If access_key_id and secret_access_key are provided they are used.
65
+ If not provided, it falls back to ENV variables, then secret credential storage (profile name).
66
+ All arguments are keyword arguments
67
+ Input: region => String
68
+ table_name => String
69
+ access_key_id => String
70
+ secret_access_key => String
71
+ profile_name => 'default'
72
+ Output: Aws::DynamoDB::Client
73
+
74
+ #new => Object
75
+
76
+ Building and describing a DynamoDB Table
77
+
78
+ Description: Shows the current configured table, or creates a table to configured as requested
79
+ Input:
80
+ Output: Aws::DynamoDB::Types::DescribeTableOutput
81
+
82
+ #table => Object
83
+
84
+ Creating a new item
85
+
86
+ Description: Inserts a new item
87
+ Input: Store
88
+ Output: Aws::DynamoDB::Types::PutItemOutput
89
+
90
+ #put_store => Object
91
+
92
+ Querying stores
93
+
94
+ Description: Look for stores dependent on the input Lat, Long (as described above)
95
+ Input: Store
96
+ Output: Array[Store]
97
+
98
+ #get_stores => Array[Store]
99
+
100
+ ### Store
101
+
102
+ **Methods**
103
+ Initialization
104
+
105
+ Description: Initialization
106
+ Input: Hash => {
107
+ latitude # Required
108
+ longitude # Required
109
+ address
110
+ city
111
+ state
112
+ zip
113
+ area_code
114
+ phone
115
+ name
116
+ geohash # Calculated based on lat,long if not provided
117
+ }
118
+ Output: Store
119
+
120
+ #new => Store
121
+
122
+ ### DynamodbGeo
123
+ This only exists as a quick and easy way to create a DynamodbManager. The only method is `.new` and it passes all arguments along to DynamodbManager and returns an instance of DynamodbManager
124
+
125
+
126
+ ## Geohashing
5
127
  Includes a Geohash implimentation written in C
6
128
  <https://github.com/simplegeo/libgeohash>
7
129
 
@@ -43,6 +165,19 @@ Viewing all my neighbours
43
165
 
44
166
  Geohash.neighbours(hash) => Array[Geohash]
45
167
 
168
+ Viewing one of my neighbours
169
+
170
+ Description: Takes in a Geohash and shows the neighbour in the cardinal direction
171
+ NORTH = 0
172
+ EAST = 1
173
+ SOUTH = 2
174
+ WEST = 3
175
+ Input: Geohash => String
176
+ Direction => Integer
177
+ Output: Geohash
178
+
179
+ Geohash.neighbour(hash) => Geohash
180
+
46
181
  Get width and height about a specific precision
47
182
 
48
183
  Description: Takes in a precision value, returns the height and width of the box
@@ -51,5 +186,6 @@ Get width and height about a specific precision
51
186
 
52
187
  Geohash.dimensions(precision) => Dimension
53
188
 
189
+ ### Objects
54
190
  #### Geocoord(Object) => attr_accessor: :latitude, :longitude, :north, :south, :east, :west, :dimension
55
191
  #### Dimension(Object) => attr_accessor: :length, :width
@@ -71,6 +71,8 @@ extern GeoCoord geohash_decode(char* hash);
71
71
  */
72
72
  extern char** geohash_neighbors(char* hash);
73
73
 
74
+ // sorry I added this, this was not in the original header
75
+ extern char* get_neighbor(char *hash, int direction);
74
76
  /*
75
77
  * Returns the width and height of a precision value.
76
78
  */
@@ -6,7 +6,8 @@
6
6
  void Init_geohash_wrapper();
7
7
  VALUE wrap_geohash_encode(VALUE self, VALUE lat, VALUE lng, VALUE precision);
8
8
  VALUE wrap_geohash_decode(VALUE self, VALUE hash);
9
- VALUE wrap_geohash_neighbors(VALUE self, VALUE hash);
9
+ VALUE wrap_geohash_neighbours(VALUE self, VALUE hash);
10
+ VALUE wrap_geohash_neighbour(VALUE self, VALUE hash, VALUE direction);
10
11
  VALUE wrap_geohash_dimensions_for_precision(VALUE self, VALUE precision);
11
12
 
12
13
 
@@ -17,7 +18,8 @@ void Init_geohash_wrapper()
17
18
 
18
19
  rb_define_singleton_method(Geohash, "encode", wrap_geohash_encode, 3);
19
20
  rb_define_singleton_method(Geohash, "wrap_decode", wrap_geohash_decode, 1);
20
- rb_define_singleton_method(Geohash, "neighbours", wrap_geohash_neighbors, 1);
21
+ rb_define_singleton_method(Geohash, "neighbours", wrap_geohash_neighbours, 1);
22
+ rb_define_singleton_method(Geohash, "neighbour", wrap_geohash_neighbour, 2);
21
23
  rb_define_singleton_method(Geohash, "dimensions_for_precision", wrap_geohash_dimensions_for_precision, 1);
22
24
  }
23
25
 
@@ -46,7 +48,7 @@ VALUE wrap_geohash_decode(VALUE self, VALUE hash) {
46
48
  return r_hash;
47
49
  }
48
50
 
49
- VALUE wrap_geohash_neighbors(VALUE self, VALUE hash) {
51
+ VALUE wrap_geohash_neighbours(VALUE self, VALUE hash) {
50
52
  char** hashed_neighbours = geohash_neighbors(StringValueCStr(hash));
51
53
  VALUE neighbours = rb_ary_new2(8);
52
54
  int i;
@@ -57,6 +59,10 @@ VALUE wrap_geohash_neighbors(VALUE self, VALUE hash) {
57
59
  return neighbours;
58
60
  }
59
61
 
62
+ VALUE wrap_geohash_neighbour(VALUE self, VALUE hash, VALUE direction) {
63
+ return rb_str_new_cstr(get_neighbor(StringValueCStr(hash), NUM2INT(direction)));
64
+ }
65
+
60
66
  VALUE wrap_geohash_dimensions_for_precision(VALUE self, VALUE precision) {
61
67
  GeoBoxDimension dimension;
62
68
  VALUE r_hash = rb_hash_new();
@@ -1 +1,16 @@
1
- require 'geohash'
1
+ require 'dynamodb_manager'
2
+
3
+ module DynamodbGeo
4
+ class << self
5
+ def new(region:, table_name:, access_key_id: nil, secret_access_key: nil, profile_name: 'default', endpoint: nil)
6
+ DynamodbManager.new(
7
+ region: region,
8
+ access_key_id: access_key_id,
9
+ secret_access_key: secret_access_key,
10
+ profile_name: profile_name,
11
+ table_name: table_name,
12
+ endpoint: endpoint
13
+ )
14
+ end
15
+ end
16
+ end
@@ -1,5 +1,5 @@
1
1
  module DynamodbGeo
2
- module Version
3
- STRING = '0.0.1'
2
+ class Version
3
+ STRING = '0.1.5'
4
4
  end
5
5
  end
@@ -0,0 +1,165 @@
1
+ require 'aws-sdk-dynamodb'
2
+ require 'geohash'
3
+ require 'store'
4
+
5
+ class DynamodbManager
6
+ attr_accessor :client, :table_name, :hash_key, :range_key, :geohash_key, :geojson, :geohash_index, :hash_key_length, :local_area_size, :max_item_return
7
+ def initialize(region:, table_name:, access_key_id: nil, secret_access_key: nil, profile_name: 'default', endpoint: nil)
8
+ if access_key_id.nil? && secret_access_key.nil?
9
+ access_key_id = ENV['AWS_ACCESS_KEY_ID']
10
+ secret_access_key = ENV['AWS_SECRET_ACCESS_KEY']
11
+
12
+ credentials = Aws::SharedCredentials.new(profile_name: profile_name).credentials if access_key_id.nil? && secret_access_key.nil?
13
+ end
14
+ credentials = Aws::Credentials.new(access_key_id, secret_access_key)
15
+
16
+ @table_name = table_name
17
+ @hash_key = 'hashkey'
18
+ @range_key = 'rangekey'
19
+ @geohash_key = 'geohash'
20
+ @geojson = 'geoJson'
21
+ @geohash_index = 'geohash-index'
22
+ @hash_key_length = 4
23
+ @local_area_size = 5
24
+ @max_item_return = 10
25
+
26
+ if endpoint
27
+ @client = Aws::DynamoDB::Client.new(
28
+ region: region,
29
+ credentials: credentials,
30
+ endpoint: endpoint
31
+ )
32
+ else
33
+ @client = Aws::DynamoDB::Client.new(
34
+ region: region,
35
+ credentials: credentials,
36
+ )
37
+ end
38
+ end
39
+
40
+ def table
41
+ create_table unless @client.list_tables.table_names.include?(@table_name)
42
+
43
+ @client.describe_table(table_name: @table_name)
44
+ end
45
+
46
+ def put_store(store)
47
+ hash = store.geohash[0..(@local_area_size - 1)]
48
+ json = {
49
+ latitude: store.lat,
50
+ longitude: store.long,
51
+ address: store.address,
52
+ city: store.city,
53
+ state: store.state,
54
+ zip: store.zip,
55
+ area_code: store.area_code,
56
+ phone: store.phone,
57
+ name: store.name,
58
+ }
59
+ put_point(hash, json)
60
+ end
61
+
62
+ def get_stores(lat, long)
63
+ geohash = Geohash.encode(lat, long, @local_area_size)
64
+ hash = geohash[0..(@hash_key_length - 1)]
65
+ all_stores = []
66
+ neighbours = Geohash.neighbours(hash)
67
+ neighbours.unshift(hash)
68
+
69
+ neighbours.each do |neighbour|
70
+ resp = query(neighbour)
71
+ resp.items.each do |item|
72
+ latitude = item[@geojson]['latitude']
73
+ longitude = item[@geojson]['longitude']
74
+ address = item[@geojson]['address']
75
+ city = item[@geojson]['city']
76
+ state = item[@geojson]['state']
77
+ zip = item[@geojson]['zip']
78
+ area_code = item[@geojson]['area_code']
79
+ phone = item[@geojson]['phone']
80
+ name = item[@geojson]['name']
81
+ geohash = item[@geohash_key]
82
+ all_stores << Store.new(
83
+ latitude: latitude,
84
+ longitude: longitude,
85
+ address: address,
86
+ city: city,
87
+ state: state,
88
+ zip: zip,
89
+ area_code: area_code,
90
+ phone: phone,
91
+ name: name,
92
+ geohash: geohash
93
+ )
94
+ end
95
+ break if all_stores.length >= max_item_return
96
+ end
97
+
98
+ # We got all the stores in the biggest possible area, we increase the hash by one and search around now
99
+ neighbours = Geohash.neighbours(geohash)
100
+
101
+ closest_stores = all_stores.select { |store| store.geohash == geohash }
102
+ surrounding_stores = (all_stores - closest_stores).select { |store| neighbours.include?(store.geohash) }
103
+ remaining_stores = all_stores - (closest_stores + surrounding_stores)
104
+
105
+ return closest_stores + surrounding_stores + remaining_stores
106
+ end
107
+
108
+ private
109
+
110
+ def query(hash)
111
+ client.query({
112
+ table_name: @table_name,
113
+ index_name: @geohash_index,
114
+ expression_attribute_values: {
115
+ ':hash' => hash,
116
+ },
117
+ key_condition_expression: "#{@hash_key} = :hash",
118
+ })
119
+ end
120
+
121
+ def put_point(hash, json)
122
+ uuid = SecureRandom.uuid
123
+
124
+ @client.put_item({
125
+ table_name: @table_name,
126
+ item: {
127
+ @hash_key => hash[0..(@hash_key_length - 1)],
128
+ @range_key => uuid,
129
+ @geohash_key => hash,
130
+ @geojson => json
131
+ }
132
+ })
133
+ end
134
+
135
+ def create_table
136
+ @client.create_table({
137
+ attribute_definitions: [
138
+ { attribute_name: @hash_key, attribute_type: 'S' },
139
+ { attribute_name: @range_key, attribute_type: 'S' },
140
+ { attribute_name: @geohash_key, attribute_type: 'S' }
141
+ ],
142
+ key_schema: [
143
+ { attribute_name: @hash_key, key_type: 'HASH' },
144
+ { attribute_name: @range_key, key_type: 'RANGE' }
145
+ ],
146
+ local_secondary_indexes: [
147
+ {
148
+ index_name: @geohash_index,
149
+ key_schema: [
150
+ { attribute_name: @hash_key, key_type: 'HASH' },
151
+ { attribute_name: @geohash_key, key_type: 'RANGE' }
152
+ ],
153
+ projection: {
154
+ projection_type: "ALL"
155
+ }
156
+ }
157
+ ],
158
+ provisioned_throughput: {
159
+ read_capacity_units: 10,
160
+ write_capacity_units: 5,
161
+ },
162
+ table_name: @table_name
163
+ })
164
+ end
165
+ end
@@ -26,10 +26,10 @@ class Geocoord
26
26
  end
27
27
 
28
28
  class Dimension
29
- attr_accessor :length, :width
29
+ attr_accessor :height, :width
30
30
 
31
31
  def initialize(geobox)
32
- length = geobox['length']
33
- width = geobox['width']
32
+ @height = geobox['height']
33
+ @width = geobox['width']
34
34
  end
35
35
  end
@@ -0,0 +1,15 @@
1
+ class Store
2
+ attr_accessor :lat, :long, :address, :city, :state, :zip, :area_code, :phone, :name, :geohash
3
+ def initialize(store_data)
4
+ @lat = store_data[:latitude]
5
+ @long = store_data[:longitude]
6
+ @address = store_data[:address]
7
+ @city = store_data[:city]
8
+ @state = store_data[:state]
9
+ @zip = store_data[:zip]
10
+ @area_code = store_data[:area_code]
11
+ @phone = store_data[:phone]
12
+ @name = store_data[:name]
13
+ @geohash = store_data[:geohash] || Geohash.encode(lat, long, 10)
14
+ end
15
+ end
@@ -0,0 +1,18 @@
1
+ require 'spec_helper'
2
+ require 'dynamodb_geo'
3
+
4
+ describe DynamodbGeo do
5
+ describe '.new' do
6
+ it 'imported properly' do
7
+ dynamo = DynamodbGeo.new(
8
+ region: 'us-east-2',
9
+ table_name: 'geo-test',
10
+ profile_name: 'test'
11
+ )
12
+ expect(dynamo).to be_a DynamodbManager
13
+
14
+ dynamo.get_stores(37.746825, -122.413637)
15
+ puts 'foo'
16
+ end
17
+ end
18
+ end
@@ -1,4 +1,5 @@
1
1
  require 'spec_helper'
2
+ require 'geohash'
2
3
 
3
4
  # Quick note, I'm not actually testing if the Geohash is accurate or not.
4
5
  # I'm assuming the C Lib I imported is doing that for me properly.
@@ -31,4 +32,11 @@ describe Geohash do
31
32
  expect(dimensions).to be_a Dimension
32
33
  end
33
34
  end
35
+
36
+ describe '.neighbour' do
37
+ it 'calculates the neighbour in a direction' do
38
+ north_neighbour = Geohash.neighbour('s', 0)
39
+ expect(north_neighbour).to be_a String
40
+ end
41
+ end
34
42
  end
@@ -1,7 +1,6 @@
1
1
  require 'bundler/setup'
2
2
  Bundler.setup
3
3
 
4
- require 'geohash'
5
4
  require 'byebug'
6
5
 
7
6
  RSpec.configure do |config|
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dynamodb_geo
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.2
4
+ version: 0.1.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Justin Ahn
@@ -97,9 +97,11 @@ files:
97
97
  - ext/geohash_wrapper/geohash_wrapper.c
98
98
  - lib/dynamodb_geo.rb
99
99
  - lib/dynamodb_geo/version.rb
100
+ - lib/dynamodb_manager.rb
100
101
  - lib/geohash.rb
101
102
  - lib/geohash/geohash_wrapper.so
102
- - spec/dynamodb_geo.rb
103
+ - lib/store.rb
104
+ - spec/dynamodb_geo_spec.rb
103
105
  - spec/geohash_spec.rb
104
106
  - spec/spec_helper.rb
105
107
  homepage:
@@ -125,8 +127,8 @@ requirements: []
125
127
  rubygems_version: 3.1.2
126
128
  signing_key:
127
129
  specification_version: 4
128
- summary: dynamodb_geo-0.0.1
130
+ summary: dynamodb_geo-0.1.5
129
131
  test_files:
130
- - spec/dynamodb_geo.rb
132
+ - spec/dynamodb_geo_spec.rb
131
133
  - spec/geohash_spec.rb
132
134
  - spec/spec_helper.rb
@@ -1 +0,0 @@
1
- require 'spec_helper'