Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ NEARBY searches a collection for objects that intersect a specified radius.
<BR CLEAR="ALL">

### Search options
**WHERE** - This option allows for filtering out results based on [field](#fields) values. For example<br>```nearby fleet where speed 70 +inf point 33.462 -112.268 6000``` will return only the objects in the 'fleet' collection that are within the 6 km radius **and** have a field named `speed` that is greater than `70`. <br><br>Multiple WHEREs are concatenated as **and** clauses. ```WHERE speed 70 +inf WHERE age -inf 24``` would be interpreted as *speed is over 70 <b>and</b> age is less than 24.*<br><br>The default value for a field is always `0`. Thus if you do a WHERE on the field `speed` and an object does not have that field set, the server will pretend that the object does and that the value is Zero.
**WHERE** - This option allows for filtering out results based on [field](#fields) values. For example<br>```nearby fleet where speed 70 +inf point 33.462 -112.268 6000``` will return only the objects in the 'fleet' collection that are within the 6 km radius **and** have a field named `speed` that is greater than `70`. <br><br>Multiple WHEREs are concatenated as **and** clauses. ```WHERE speed 70 +inf WHERE age -inf 24``` would be interpreted as *speed is over 70 <b>and</b> age is less than 24.*<br><br>The default value for a field is always `0`. Thus if you do a WHERE on the field `speed` and an object does not have that field set, the server will pretend that the object does and that the value is Zero.<br><br>WHERE expressions support the `=~` operator for regex matching. It uses Go's re2 regular expression engine to match string values in fields or GeoJSON properties within objects. For example, `WHERE properties.name =~ 'truck.*'` filters objects where the 'name' property matches the pattern, and `WHERE field_name =~ 'value.*'` works similarly for fields.

**MATCH** - MATCH is similar to WHERE except that it works on the object id instead of fields.<br>```nearby fleet match truck* point 33.462 -112.268 6000``` will return only the objects in the 'fleet' collection that are within the 6 km radius **and** have an object id that starts with `truck`. There can be multiple MATCH options in a single search. The MATCH value is a simple [glob pattern](https://en.wikipedia.org/wiki/Glob_(programming)).

Expand Down
46 changes: 37 additions & 9 deletions internal/server/expr.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package server

import (
"fmt"
"regexp"
"sync"

"github.com/tidwall/expr"
Expand All @@ -9,10 +11,12 @@
"github.com/tidwall/match"
"github.com/tidwall/tile38/internal/field"
"github.com/tidwall/tile38/internal/object"
"github.com/tidwall/tinylru"
)

type exprPool struct {
pool *sync.Pool
pool *sync.Pool
regexCache tinylru.LRUG[string, *regexp.Regexp]
}

func typeForObject(o *object.Object) expr.Value {
Expand Down Expand Up @@ -89,6 +93,8 @@
}

func newExprPool(s *Server) *exprPool {
pool := &exprPool{}

ext := expr.NewExtender(
// ref
func(info expr.RefInfo, ctx *expr.Context) (expr.Value, error) {
Expand All @@ -98,6 +104,10 @@
if info.Ident == "this" {
return expr.Object(o), nil
}
// Register regex as a root function
if info.Ident == "regex" {
return expr.Function("regex"), nil
}
return objExpr(o, info)
} else {
switch v := info.Value.Value().(type) {
Expand Down Expand Up @@ -132,19 +142,37 @@
},
// op
func(info expr.OpInfo, ctx *expr.Context) (expr.Value, error) {
switch info.Op {
case expr.OpRegex:

Check failure on line 146 in internal/server/expr.go

View workflow job for this annotation

GitHub Actions / Build

undefined: expr.OpRegex
field := info.Left.String()
pattern := info.Right.String()

re, ok := pool.regexCache.Get(pattern)
if !ok {
var err error
re, err = regexp.Compile(pattern)
if err != nil {
return expr.Undefined, fmt.Errorf("invalid regex pattern: %v", err)
}
pool.regexCache.Set(pattern, re)
}

return expr.Bool(re.MatchString(field)), nil
}
return expr.Undefined, nil
},
)
return &exprPool{
pool: &sync.Pool{
New: func() any {
ctx := &expr.Context{
Extender: ext,
}
return ctx
},

pool.pool = &sync.Pool{
New: func() any {
ctx := &expr.Context{
Extender: ext,
}
return ctx
},
}

return pool
}

func (p *exprPool) Get(o *object.Object) *expr.Context {
Expand Down
9 changes: 9 additions & 0 deletions tests/keys_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -560,11 +560,20 @@ func keys_FIELDS_test(mc *mockServer) error {
Do("SCAN", "fleet", "WHERE", "hello.world", "<=", `tom`, "IDS").JSON().Str(`{"ok":true,"ids":["truck1","truck2"],"count":2,"cursor":0}`),
Do("SCAN", "fleet", "WHERE", "hello.world", "<", `uom`, "IDS").JSON().Str(`{"ok":true,"ids":["truck1","truck2"],"count":2,"cursor":0}`),
Do("SCAN", "fleet", "WHERE", "hello.world", "!=", `tom`, "IDS").JSON().Str(`{"ok":true,"ids":["truck1"],"count":1,"cursor":0}`),
// Test REGEX on FIELD
Do("SCAN", "fleet", "WHERE", "regex(hello.world, 'tom.*')", "IDS").JSON().Str(`{"ok":true,"ids":["truck2"],"count":1,"cursor":0}`),
Do("SCAN", "fleet", "WHERE", "regex(hello.world, 'foo.*')", "IDS").JSON().Str(`{"ok":true,"ids":[],"count":0,"cursor":0}`),
Do("SCAN", "fleet", "WHERE", "regex(hello.world, '(*')", "IDS").JSON().Str(`{"ok":true,"ids":[],"count":0,"cursor":0}`),

Do("SET", "fleet", "truck1", "OBJECT", `{"type":"Feature","geometry":{"type":"Point","coordinates":[-112,33]},"properties":{"speed":50},"asdf":"Adsf"}`).JSON().OK(),
Do("SCAN", "fleet", "WHERE", "properties.speed", ">", 49, "IDS").JSON().Str(`{"ok":true,"ids":["truck1"],"count":1,"cursor":0}`),
Do("SCAN", "fleet", "WHERE", "properties.speed", ">", 50, "IDS").JSON().Str(`{"ok":true,"ids":[],"count":0,"cursor":0}`),
Do("SCAN", "fleet", "WHERE", "properties.speed", "<", 51, "IDS").JSON().Str(`{"ok":true,"ids":["truck1","truck2"],"count":2,"cursor":0}`),
// Test REGEX on OBJECT properties
Do("SET", "fleet", "truck3", "OBJECT", `{"type":"Feature","geometry":{"type":"Point","coordinates":[-112,33]},"properties":{"name":"truck01"}}`).JSON().OK(),
Do("SCAN", "fleet", "WHERE", "regex(properties.name, 'truck.*')", "IDS").JSON().Str(`{"ok":true,"ids":["truck3"],"count":1,"cursor":0}`),
Do("SCAN", "fleet", "WHERE", "regex(properties.name, 'foo.*')", "IDS").JSON().Str(`{"ok":true,"ids":[],"count":0,"cursor":0}`),
Do("SCAN", "fleet", "WHERE", "regex(properties.name, '(*')", "IDS").JSON().Str(`{"ok":true,"ids":[],"count":0,"cursor":0}`),

Do("DROP", "fleet").JSON().OK(),
Do("SET", "fleet", "truck1", "FIELD", "speed", "50", "POINT", "-112", "33").JSON().OK(),
Expand Down
Loading