dynamodb_geo 0.0.2 → 0.1.0

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: 2462d22ee572cb65c0af6c6c924bb32f7cbc29fabbf60b15f63be209da271a8a
4
+ data.tar.gz: 2c611dfab3f4caa02c25911e4fcb16f6b4a47006a467b37a8df1cbb8b646a182
5
5
  SHA512:
6
- metadata.gz: 9d3d2f46f82c2e9ece62dc006dbda654753d01fb7d82d06d803448171a9852cef5933ce814ce20057017ed9fcb318b9f6ff37dbe79cb7f5167c82aea65f3258c
7
- data.tar.gz: 8df26e8041dfbb59020496faf2ae5014fae971b9b7d0f84274ff4eeb58c0c70487623a7a44343a4443647d08cfe72acc48c8115004c287853c2896dc46e91b9a
6
+ metadata.gz: e7f45d33d1b1ff2ed85dfcaf06c2336ad9ecae230483ff9da9039ce38b69b37f632dfa896bb3ce91611eff32a98249f52ed2a257483cdbbda9a0f478e6d92787
7
+ data.tar.gz: d60bdecd1555c1dfae86bbb034d8c38ede68f55c44d824ad6e7e1c3a1c5f71bd364192d5d8ae4a51affaeede24d23bd1194a1b722a9e21eb10cbbbe2b59782a4
data/README.md CHANGED
@@ -1,7 +1,127 @@
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. Returned in an attempt at the closest items first.
3
6
 
4
- # Geohashing
7
+ Items are searched as the following.
8
+ Given the lat, long we calculate a hash 9x0qz. We go up one level to 9x0q and calculate all neighbouring hashes.
9
+
10
+ 9x0p 9x0r 9x0x
11
+ 9x0n 9x0q 9x0w => ["9x0q", "9x0r", "9x0x", "9x0w", "9x0t", "9x0m", "9x0j", "9x0n", "9x0p"]
12
+ 9x0j 9x0m 9x0t
13
+
14
+ 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.
15
+ We then calculate the neighbours of our more local hash 9x0qz, and sort the results of the larger hashes using similar rules.
16
+
17
+ 9x0rn 9x0rp 9x0x0
18
+ 9x0qy 9x0qz 9x0wb => ["9x0qz", "9x0rp", "9x0x0", "9x0wb", "9x0w8", "9x0qx", "9x0qw", "9x0qy", "9x0rn"]
19
+ 9x0qw 9x0qx 9x0w8
20
+
21
+ Using the same pattern as before (Middle -> North -> Clockwise), we sort the returned stores giving priority to the localization.
22
+
23
+ ### Real talk
24
+ 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.
25
+
26
+ 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.
27
+
28
+ 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.
29
+
30
+ ## Objects
31
+ ### DynamodbManager
32
+
33
+ **Attributes**
34
+ table_name:
35
+ Sets the DynamoDB Table Name
36
+
37
+ hash_key:
38
+ Sets the name of the primary hash attribute
39
+
40
+ range_key:
41
+ Sets the name of the sort/range attribute
42
+
43
+ geohash_key:
44
+ Sets the name of the localized hash attribute
45
+
46
+ geojson_key:
47
+ Sets the name of the metadata attribute
48
+
49
+ hash_key_length:
50
+ Sets size of the outer hash length
51
+
52
+ local_area_size:
53
+ Sets the size of the inner hash length
54
+
55
+ max_item_return:
56
+ Sets the max number of items to return
57
+
58
+ **Methods**
59
+ Initialization
60
+
61
+ Description: Creates a new DynamodbManager object. Inputs are variables to your AWS account.
62
+ If access_key_id and secret_access_key are provided they are used.
63
+ If not provided, it falls back to ENV variables, then secret credential storage (profile name).
64
+ All arguments are keyword arguments
65
+ Input: region => String
66
+ table_name => String
67
+ access_key_id => String
68
+ secret_access_key => String
69
+ profile_name => 'default'
70
+ Output: Aws::DynamoDB::Client
71
+
72
+ #new => Object
73
+
74
+ Building and describing a DynamoDB Table
75
+
76
+ Description: Shows the current configured table, or creates a table to configured as requested
77
+ Input:
78
+ Output: Aws::DynamoDB::Types::DescribeTableOutput
79
+
80
+ #table => Object
81
+
82
+ Creating a new item
83
+
84
+ Description: Inserts a new item
85
+ Input: Store
86
+ Output: Aws::DynamoDB::Types::PutItemOutput
87
+
88
+ #put_store => Object
89
+
90
+ Querying stores
91
+
92
+ Description: Look for stores dependent on the input Lat, Long (as described above)
93
+ Input: Store
94
+ Output: Array[Store]
95
+
96
+ #get_stores => Array[Store]
97
+
98
+ ### Store
99
+
100
+ **Methods**
101
+ Initialization
102
+
103
+ Description: Initialization
104
+ Input: Hash => {
105
+ latitude # Required
106
+ longitude # Required
107
+ address
108
+ city
109
+ state
110
+ zip
111
+ area_code
112
+ phone
113
+ name
114
+ geohash # Calculated based on lat,long if not provided
115
+ }
116
+ Output: Store
117
+
118
+ #new => Store
119
+
120
+ ### DynamodbGeo
121
+ 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
122
+
123
+
124
+ ## Geohashing
5
125
  Includes a Geohash implimentation written in C
6
126
  <https://github.com/simplegeo/libgeohash>
7
127
 
@@ -43,6 +163,19 @@ Viewing all my neighbours
43
163
 
44
164
  Geohash.neighbours(hash) => Array[Geohash]
45
165
 
166
+ Viewing one of my neighbours
167
+
168
+ Description: Takes in a Geohash and shows the neighbour in the cardinal direction
169
+ NORTH = 0
170
+ EAST = 1
171
+ SOUTH = 2
172
+ WEST = 3
173
+ Input: Geohash => String
174
+ Direction => Integer
175
+ Output: Geohash
176
+
177
+ Geohash.neighbour(hash) => Geohash
178
+
46
179
  Get width and height about a specific precision
47
180
 
48
181
  Description: Takes in a precision value, returns the height and width of the box
@@ -51,5 +184,6 @@ Get width and height about a specific precision
51
184
 
52
185
  Geohash.dimensions(precision) => Dimension
53
186
 
187
+ ### Objects
54
188
  #### Geocoord(Object) => attr_accessor: :latitude, :longitude, :north, :south, :east, :west, :dimension
55
189
  #### 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,15 @@
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')
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
+ )
13
+ end
14
+ end
15
+ end
@@ -1,5 +1,5 @@
1
1
  module DynamodbGeo
2
- module Version
3
- STRING = '0.0.1'
2
+ class Version
3
+ STRING = '0.1.0'
4
4
  end
5
5
  end
@@ -0,0 +1,136 @@
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')
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
+ @client = Aws::DynamoDB::Client.new(
27
+ region: region,
28
+ credentials: credentials,
29
+ )
30
+ end
31
+
32
+ def table
33
+ create_table unless @client.list_tables.table_names.include?(@table_name)
34
+
35
+ @client.describe_table(table_name: @table_name)
36
+ end
37
+
38
+ def put_store(store)
39
+ hash = store.geohash[0..(@local_area_size - 1)]
40
+ json = {
41
+ latitude: store.lat,
42
+ longitude: store.long,
43
+ address: store.address,
44
+ city: store.city,
45
+ state: store.state,
46
+ zip: store.zip,
47
+ area_code: store.area_code,
48
+ phone: store.phone,
49
+ name: store.name,
50
+ }
51
+ put_point(hash, json)
52
+ end
53
+
54
+ def get_stores(lat, long)
55
+ geohash = Geohash.encode(lat, long, @local_area_size)
56
+ hash = geohash[0..(@hash_key_length - 1)]
57
+ all_stores = []
58
+ neighbours = Geohash.neighbours(hash)
59
+ neighbours.unshift(hash)
60
+
61
+ neighbours.each do |neighbour|
62
+ resp = query(neighbour)
63
+ resp.items.each do |item|
64
+ all_stores << Store.new(item['geoJson'], item['geohash'])
65
+ end
66
+ break if all_stores.length >= max_item_return
67
+ end
68
+
69
+ # We got all the stores in the biggest possible area, we increase the hash by one and search around now
70
+ neighbours = Geohash.neighbours(geohash)
71
+
72
+ closest_stores = all_stores.select { |store| store.geohash == geohash }
73
+ surrounding_stores = (all_stores - closest_stores).select { |store| neighbours.include?(store.geohash) }
74
+ remaining_stores = all_stores - (closest_stores + surrounding_stores)
75
+
76
+ return closest_stores + surrounding_stores + remaining_stores
77
+ end
78
+
79
+ private
80
+
81
+ def query(hash)
82
+ client.query({
83
+ table_name: @table_name,
84
+ index_name: @geohash_index,
85
+ expression_attribute_values: {
86
+ ':hash' => hash,
87
+ },
88
+ key_condition_expression: "#{@hash_key} = :hash",
89
+ })
90
+ end
91
+
92
+ def put_point(hash, json)
93
+ uuid = SecureRandom.uuid
94
+
95
+ @client.put_item({
96
+ table_name: @table_name,
97
+ item: {
98
+ @hash_key => hash[0..(@hash_key_length - 1)],
99
+ @range_key => uuid,
100
+ @geohash_key => hash,
101
+ @geojson => json
102
+ }
103
+ })
104
+ end
105
+
106
+ def create_table
107
+ @client.create_table({
108
+ attribute_definitions: [
109
+ { attribute_name: @hash_key, attribute_type: 'S' },
110
+ { attribute_name: @range_key, attribute_type: 'S' },
111
+ { attribute_name: @geohash_key, attribute_type: 'S' }
112
+ ],
113
+ key_schema: [
114
+ { attribute_name: @hash_key, key_type: 'HASH' },
115
+ { attribute_name: @range_key, key_type: 'RANGE' }
116
+ ],
117
+ local_secondary_indexes: [
118
+ {
119
+ index_name: @geohash_index,
120
+ key_schema: [
121
+ { attribute_name: @hash_key, key_type: 'HASH' },
122
+ { attribute_name: @geohash_key, key_type: 'RANGE' }
123
+ ],
124
+ projection: {
125
+ projection_type: "ALL"
126
+ }
127
+ }
128
+ ],
129
+ provisioned_throughput: {
130
+ read_capacity_units: 10,
131
+ write_capacity_units: 5,
132
+ },
133
+ table_name: @table_name
134
+ })
135
+ end
136
+ 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
+ debugger;1
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.0
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.0
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'