mirror of
https://github.com/opencloud-eu/opencloud.git
synced 2026-09-12 21:58:58 -04:00
refactor(search): resolve geo fields via mapping overrides, reject non-geo fields
Replaces the ad-hoc key+"_geopoint" concatenation with a proper lookup (query.ResolveGeoField) derived from the TypeGeopoint field overrides, so it works for any geopoint field, not just location. Geo predicates on non-geopoint fields now error instead of querying a nonexistent field.
This commit is contained in:
1 parent
95a953ad86
commit
ea47069d7c
5 files changed
+79
-14
No files matched your search
@@ -45,4 +45,9 @@ var _ = Describe("Geo KQL predicates", func() {
|
||||
Expect(hits(near + " AND location:geo.bbox(49.0, 11.0, 50.0, 12.0)")).To(Equal(1))
|
||||
Expect(hits(near + " AND location:geo.bbox(52.0, 13.0, 53.0, 14.0)")).To(Equal(0))
|
||||
})
|
||||
|
||||
It("rejects a geo predicate on a non-geopoint field", func() {
|
||||
_, err := qbleve.DefaultCreator.Create("name:geo.distance(48.2, 16.3, 5km)")
|
||||
Expect(err).To(HaveOccurred())
|
||||
})
|
||||
})
|
||||
@@ -15,10 +15,14 @@ import (
|
||||
"github.com/opencloud-eu/opencloud/services/search/pkg/query"
|
||||
)
|
||||
|
||||
// geoField maps a KQL geo key to its indexed geopoint sibling, e.g.
|
||||
// "location" to "location_geopoint".
|
||||
func geoField(key string) string {
|
||||
return strings.ToLower(key) + mapping.GeopointSuffix
|
||||
// geoQueryField resolves a KQL geo key to its indexed geopoint field, erroring
|
||||
// when the key is not a geopoint field.
|
||||
func geoQueryField(key string) (string, error) {
|
||||
field, ok := query.ResolveGeoField(key)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("geo predicate on non-geo field %q", key)
|
||||
}
|
||||
return field, nil
|
||||
}
|
||||
|
||||
func TranspileKQLToOpenSearch(nodes []ast.Node) (osu.Builder, error) {
|
||||
@@ -193,14 +197,26 @@ func (t kqlOpensearchTranspiler) toBuilder(node ast.Node) (osu.Builder, error) {
|
||||
|
||||
return group, nil
|
||||
case *ast.GeoDistanceNode:
|
||||
return osu.NewGeoDistanceQuery(geoField(node.Key)).
|
||||
field, err := geoQueryField(node.Key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return osu.NewGeoDistanceQuery(field).
|
||||
Distance(strconv.FormatFloat(node.Radius, 'f', -1, 64)+"m").
|
||||
Point(node.Lat, node.Lon), nil
|
||||
case *ast.GeoBoundingBoxNode:
|
||||
return osu.NewGeoBoundingBoxQuery(geoField(node.Key)).
|
||||
field, err := geoQueryField(node.Key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return osu.NewGeoBoundingBoxQuery(field).
|
||||
Box(node.MinLat, node.MinLon, node.MaxLat, node.MaxLon), nil
|
||||
case *ast.GeoPolygonNode:
|
||||
q := osu.NewGeoPolygonQuery(geoField(node.Key))
|
||||
field, err := geoQueryField(node.Key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
q := osu.NewGeoPolygonQuery(field)
|
||||
for _, p := range node.Points {
|
||||
q.Point(p.Lat, p.Lon)
|
||||
}
|
||||
|
||||
@@ -473,3 +473,10 @@ func TestTranspileKQLToOpenSearch(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTranspileRejectsGeoOnNonGeoField(t *testing.T) {
|
||||
_, err := convert.TranspileKQLToOpenSearch([]ast.Node{
|
||||
&ast.GeoDistanceNode{Key: "name", Lat: 48.2, Lon: 16.3, Radius: 5000},
|
||||
})
|
||||
assert.Error(t, err)
|
||||
}
|
||||
@@ -42,10 +42,14 @@ var bleveEscaper = strings.NewReplacer(
|
||||
` `, `\ `,
|
||||
)
|
||||
|
||||
// geoField maps a KQL geo key to its indexed geopoint sibling, e.g.
|
||||
// "location" to "location_geopoint".
|
||||
func geoField(key string) string {
|
||||
return strings.ToLower(key) + mapping.GeopointSuffix
|
||||
// geoQueryField resolves a KQL geo key to its indexed geopoint field, erroring
|
||||
// when the key is not a geopoint field.
|
||||
func geoQueryField(key string) (string, error) {
|
||||
field, ok := searchQuery.ResolveGeoField(key)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("geo predicate on non-geo field %q", key)
|
||||
}
|
||||
return field, nil
|
||||
}
|
||||
|
||||
// Compiler represents a KQL query search string to the bleve query formatter.
|
||||
@@ -237,29 +241,41 @@ func walk(offset int, nodes []ast.Node) (bleveQuery.Query, int, error) {
|
||||
next = q
|
||||
}
|
||||
case *ast.GeoDistanceNode:
|
||||
field, err := geoQueryField(n.Key)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
q := bleveQuery.NewGeoDistanceQuery(n.Lon, n.Lat, strconv.FormatFloat(n.Radius, 'f', -1, 64)+"m")
|
||||
q.SetField(geoField(n.Key))
|
||||
q.SetField(field)
|
||||
if prev == nil {
|
||||
prev = q
|
||||
} else {
|
||||
next = q
|
||||
}
|
||||
case *ast.GeoBoundingBoxNode:
|
||||
field, err := geoQueryField(n.Key)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
// bleve takes the top-left and bottom-right corners.
|
||||
q := bleveQuery.NewGeoBoundingBoxQuery(n.MinLon, n.MaxLat, n.MaxLon, n.MinLat)
|
||||
q.SetField(geoField(n.Key))
|
||||
q.SetField(field)
|
||||
if prev == nil {
|
||||
prev = q
|
||||
} else {
|
||||
next = q
|
||||
}
|
||||
case *ast.GeoPolygonNode:
|
||||
field, err := geoQueryField(n.Key)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
points := make([]geo.Point, 0, len(n.Points))
|
||||
for _, p := range n.Points {
|
||||
points = append(points, geo.Point{Lon: p.Lon, Lat: p.Lat})
|
||||
}
|
||||
q := bleveQuery.NewGeoBoundingPolygonQuery(points)
|
||||
q.SetField(geoField(n.Key))
|
||||
q.SetField(field)
|
||||
if prev == nil {
|
||||
prev = q
|
||||
} else {
|
||||
|
||||
@@ -102,3 +102,24 @@ func FieldIsFulltext(field string) bool {
|
||||
func FieldIsWordBroken(field string) bool {
|
||||
return siblingFields()[field].Words
|
||||
}
|
||||
|
||||
// geopointFields maps a lowercased KQL key to its indexed geopoint sibling field,
|
||||
// derived from the TypeGeopoint entries in the resource field overrides (e.g.
|
||||
// "location" -> "location_geopoint", "journey.start" -> "journey.start_geopoint").
|
||||
var geopointFields = sync.OnceValue(func() map[string]string {
|
||||
out := map[string]string{}
|
||||
for key, opts := range (search.Resource{}).SearchFieldOverrides() {
|
||||
if opts.Type == mapping.TypeGeopoint {
|
||||
out[strings.ToLower(key)] = key + mapping.GeopointSuffix
|
||||
}
|
||||
}
|
||||
return out
|
||||
})
|
||||
|
||||
// ResolveGeoField maps a KQL key to its indexed geopoint field name. ok is false
|
||||
// when the key is not a geopoint field, so callers can reject geo predicates on
|
||||
// non-geo fields.
|
||||
func ResolveGeoField(name string) (string, bool) {
|
||||
f, ok := geopointFields()[strings.ToLower(name)]
|
||||
return f, ok
|
||||
}
|
||||
Reference in new issue
Block a user