Skip to content

Commit

Permalink
Major ForestDB geo-query improvements
Browse files Browse the repository at this point in the history
* Can now index rectangles and query for rectangle intersection.
* GeoJSON data is preserved and can be accessed from the QueryRow.
* Emitted values are preserved
Fixes #485
  • Loading branch information
snej committed Jul 27, 2015
1 parent 66e111c commit 97ec102
Show file tree
Hide file tree
Showing 4 changed files with 71 additions and 49 deletions.
5 changes: 2 additions & 3 deletions Source/CBLSpecialKey.m
Expand Up @@ -37,8 +37,7 @@ - (instancetype) initWithPoint: (CBLGeoPoint)point {
self = [super init];
if (self) {
_rect = (CBLGeoRect){point, point};
_geoJSONData = [CBLJSON dataWithJSONObject: CBLGeoPointToJSON(point) options: 0 error:NULL];
_geoJSONData = [NSData data]; // Empty _geoJSONData means the bbox is a point
// nil _geoJSONData means a point
}
return self;
}
Expand All @@ -47,7 +46,7 @@ - (instancetype) initWithRect: (CBLGeoRect)rect {
self = [super init];
if (self) {
_rect = rect;
// Don't set _geoJSONData; if nil it defaults to the same as the bbox.
_geoJSONData = [NSData data]; // Empty _geoJSONData means a rect
}
return self;
}
Expand Down
111 changes: 68 additions & 43 deletions Source/CBL_ForestDBViewStorage.mm
Expand Up @@ -46,6 +46,17 @@ @interface CBL_ForestDBViewStorage () <CBL_QueryRowStorage>
#define kCloseDelay 60.0


static geohash::area geoRectToArea(CBLGeoRect rect) {
return geohash::area(geohash::coord(rect.min.y, rect.min.x), // lat/lon order
geohash::coord(rect.max.y, rect.max.x));
}

static CBLGeoRect areaToGeoRect(geohash::area area) {
return CBLGeoRect{{area.longitude.min, area.latitude.min},
{area.longitude.max, area.latitude.max}};
}


#pragma mark - C++ MAP/REDUCE GLUE:


Expand Down Expand Up @@ -141,7 +152,7 @@ virtual void operator() (const Mappable& mappable, EmitFn& emitFn) {
if (text) {
emitText(text, value, doc, emitFn);
} else {
emitGeo(specialKey.rect, value, doc, emitFn);
emitGeo(specialKey, value, doc, emitFn);
}
} else if (key) {
LogTo(ViewVerbose, @" emit(%@, %@) to %@", toJSONStr(key), toJSONStr(value), viewName);
Expand All @@ -163,16 +174,13 @@ void emitText(NSString* text, id value, NSDictionary* doc, EmitFn& emitFn) {
}

// Geo-index a rectangle
void emitGeo(CBLGeoRect rect, id value, NSDictionary* doc, EmitFn& emitFn) {
geohash::area area(geohash::coord(rect.min.x, rect.min.y),
geohash::coord(rect.max.x, rect.max.y));
Collatable collKey, collValue;
collKey << area.mid(); // HACK: Can only emit points for now
void emitGeo(CBLSpecialKey* geoKey, id value, NSDictionary* doc, EmitFn& emitFn) {
Collatable collValue;
if (value == doc)
collValue.addSpecial(); // placeholder for doc
else if (value)
collValue << value;
emitFn(collKey, collValue);
emitFn(geoRectToArea(geoKey.rect), slice(geoKey.geoJSONData), collValue);
}

// Emit a regular key/value pair
Expand Down Expand Up @@ -481,12 +489,13 @@ - (NSArray*) dump {
return nil;
NSMutableArray* result = $marray();

IndexEnumerator e = [self _runForestQueryWithOptions: [CBLQueryOptions new]];
while (e.next()) {
[result addObject: $dict({@"key", CBLJSONString(e.key().readNSObject())},
{@"value", CBLJSONString(e.value().readNSObject())},
{@"seq", @(e.sequence())})];
IndexEnumerator *e = [self _runForestQueryWithOptions: [CBLQueryOptions new]];
while (e->next()) {
[result addObject: $dict({@"key", CBLJSONString(e->key().readNSObject())},
{@"value", CBLJSONString(e->value().readNSObject())},
{@"seq", @(e->sequence())})];
}
delete e;
return result;
}
#endif
Expand All @@ -496,7 +505,7 @@ - (NSArray*) dump {


/** Starts a view query, returning a CBForest enumerator. */
- (IndexEnumerator) _runForestQueryWithOptions: (CBLQueryOptions*)options
- (IndexEnumerator*) _runForestQueryWithOptions: (CBLQueryOptions*)options
{
Assert(_index); // caller MUST call -openIndex: first
DocEnumerator::Options forestOpts = DocEnumerator::Options::kDefault;
Expand All @@ -506,21 +515,23 @@ - (IndexEnumerator) _runForestQueryWithOptions: (CBLQueryOptions*)options
forestOpts.descending = options->descending;
forestOpts.inclusiveStart = options->inclusiveStart;
forestOpts.inclusiveEnd = options->inclusiveEnd;
if (options.keys) {
if (options->bbox) {
return new GeoIndexEnumerator(_index, geoRectToArea(*options->bbox));
} else if (options.keys) {
std::vector<KeyRange> collatableKeys;
for (id key in options.keys)
collatableKeys.push_back(Collatable(key));
return IndexEnumerator(_index,
collatableKeys,
forestOpts);
return new IndexEnumerator(_index,
collatableKeys,
forestOpts);
} else {
id endKey = CBLKeyForPrefixMatch(options.endKey, options->prefixMatchLevel);
return IndexEnumerator(_index,
Collatable(options.startKey),
nsstring_slice(options.startKeyDocID),
Collatable(endKey),
nsstring_slice(options.endKeyDocID),
forestOpts);
return new IndexEnumerator(_index,
Collatable(options.startKey),
nsstring_slice(options.startKeyDocID),
Collatable(endKey),
nsstring_slice(options.endKeyDocID),
forestOpts);
}
}

Expand All @@ -542,25 +553,25 @@ - (CBLQueryIteratorBlock) regularQueryWithOptions: (CBLQueryOptions*)options
options->skip = 0;
}

__block IndexEnumerator e = [self _runForestQueryWithOptions: options];
__block std::auto_ptr<IndexEnumerator> e ([self _runForestQueryWithOptions: options]);

*outStatus = kCBLStatusOK;
return ^CBLQueryRow*() {
try{
if (limit-- == 0)
return nil;
while (e.next()) {
while (e->next()) {
CBL_Revision* docRevision = nil;
id key = e.key().readNSObject();
id key = e->key().readNSObject();
id value = nil;
NSString* docID = (NSString*)e.docID();
SequenceNumber sequence = e.sequence();
NSString* docID = (NSString*)e->docID();
SequenceNumber sequence = e->sequence();

if (options->includeDocs) {
NSDictionary* valueDict = nil;
NSString* linkedID = nil;
if (e.value().peekTag() == CollatableReader::kMap) {
value = e.value().readNSObject();
if (e->value().peekTag() == CollatableReader::kMap) {
value = e->value().readNSObject();
valueDict = $castIf(NSDictionary, value);
linkedID = valueDict.cbl_id;
}
Expand All @@ -579,16 +590,30 @@ - (CBLQueryIteratorBlock) regularQueryWithOptions: (CBLQueryOptions*)options
}

if (!value)
value = e.value().data().copiedNSData();
value = e->value().data().copiedNSData();

LogTo(QueryVerbose, @"Query %@: Found row with key=%@, value=%@, id=%@",
_name, CBLJSONString(key), value, CBLJSONString(docID));
auto row = [[CBLQueryRow alloc] initWithDocID: docID
sequence: sequence
key: key
value: value
docRevision: docRevision
storage: self];
CBLQueryRow* row;
if (options->bbox) {
GeoIndexEnumerator* ge = (GeoIndexEnumerator*)e.get();
CBLGeoRect bbox = areaToGeoRect(ge->keyBoundingBox());
NSData* geoJSON = ge->keyGeoJSON().copiedNSData();
row = [[CBLGeoQueryRow alloc] initWithDocID: docID
sequence: sequence
boundingBox: bbox
geoJSONData: geoJSON
value: value
docRevision: docRevision
storage: self];
} else {
row = [[CBLQueryRow alloc] initWithDocID: docID
sequence: sequence
key: key
value: value
docRevision: docRevision
storage: self];
}
if (filter) {
if (!filter(row))
continue;
Expand Down Expand Up @@ -643,14 +668,14 @@ - (CBLQueryIteratorBlock) reducedQueryWithOptions: (CBLQueryOptions*)options

if (![self openIndex: outStatus])
return nil;
__block IndexEnumerator e = [self _runForestQueryWithOptions: options];
__block std::auto_ptr<IndexEnumerator> e([self _runForestQueryWithOptions: options]);

*outStatus = kCBLStatusOK;
return ^CBLQueryRow*() {
try {
CBLQueryRow* row = nil;
do {
id key = e.next() ? e.key().readNSObject() : nil;
id key = e->next() ? e->key().readNSObject() : nil;
if (lastKey && (!key || (group && !groupTogether(lastKey, key, groupLevel)))) {
// key doesn't match lastKey; emit a grouped/reduced row for what came before:
row = [[CBLQueryRow alloc] initWithDocID: nil
Expand All @@ -670,12 +695,12 @@ - (CBLQueryIteratorBlock) reducedQueryWithOptions: (CBLQueryOptions*)options
if (key && reduce) {
// Add this key/value to the list to be reduced:
[keysToReduce addObject: key];
CollatableReader collatableValue = e.value();
CollatableReader collatableValue = e->value();
id value;
if (collatableValue.peekTag() == CollatableReader::kSpecial) {
CBLStatus status;
CBL_Revision* rev = [_dbStorage getDocumentWithID: (NSString*)e.docID()
sequence: e.sequence()
CBL_Revision* rev = [_dbStorage getDocumentWithID: (NSString*)e->docID()
sequence: e->sequence()
status: &status];
if (!rev)
Warn(@"%@: Couldn't load doc for row value: status %d", self, status);
Expand Down Expand Up @@ -804,7 +829,7 @@ - (CBLQueryIteratorBlock) fullTextQueryWithOptions: (CBLQueryOptions*)options
NSMutableDictionary* docRows = [[NSMutableDictionary alloc] init];
*outStatus = kCBLStatusOK;
DocEnumerator::Options forestOpts = DocEnumerator::Options::kDefault;
for (IndexEnumerator e = IndexEnumerator(index, collatableKeys, forestOpts); e.next(); ) {
for (IndexEnumerator e(index, collatableKeys, forestOpts); e.next(); ) {
std::string token = e.textToken();
NSString* docID = (NSString*)e.docID();
unsigned fullTextID;
Expand Down
2 changes: 0 additions & 2 deletions Unit-Tests/ViewInternal_Tests.m
Expand Up @@ -712,8 +712,6 @@ - (void) test13_NumericKeys {
}

- (void) test14_GeoQuery {
if (!self.isSQLiteDB)
return; //FIX: ForestDB doesn't support nontrivial geo queries (#485)
RequireTestCase(CBLGeometry);
RequireTestCase(Index);
[self putGeoDocs];
Expand Down

0 comments on commit 97ec102

Please sign in to comment.