dynamodb_geo 0.0.2 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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'