dynamodb_geo 0.0.2 → 0.1.5

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: 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'