milvus/tests/go_client/testcases/geometry_test.go
cai.zhang 19346fa389
feat: Geospatial Data Type and GIS Function support for milvus (#44547)
issue: #43427

This pr's main goal is merge #37417 to milvus 2.5 without conflicts.

# Main Goals

1. Create and describe collections with geospatial type
2. Insert geospatial data into the insert binlog
3. Load segments containing geospatial data into memory
4. Enable query and search can display  geospatial data
5. Support using GIS funtions like ST_EQUALS in query
6. Support R-Tree index for geometry type

# Solution

1. **Add Type**: Modify the Milvus core by adding a Geospatial type in
both the C++ and Go code layers, defining the Geospatial data structure
and the corresponding interfaces.
2. **Dependency Libraries**: Introduce necessary geospatial data
processing libraries. In the C++ source code, use Conan package
management to include the GDAL library. In the Go source code, add the
go-geom library to the go.mod file.
3. **Protocol Interface**: Revise the Milvus protocol to provide
mechanisms for Geospatial message serialization and deserialization.
4. **Data Pipeline**: Facilitate interaction between the client and
proxy using the WKT format for geospatial data. The proxy will convert
all data into WKB format for downstream processing, providing column
data interfaces, segment encapsulation, segment loading, payload
writing, and cache block management.
5. **Query Operators**: Implement simple display and support for filter
queries. Initially, focus on filtering based on spatial relationships
for a single column of geospatial literal values, providing parsing and
execution for query expressions.Now only support brutal search
7. **Client Modification**: Enable the client to handle user input for
geospatial data and facilitate end-to-end testing.Check the modification
in pymilvus.

---------

Signed-off-by: Yinwei Li <yinwei.li@zilliz.com>
Signed-off-by: Cai Zhang <cai.zhang@zilliz.com>
Co-authored-by: ZhuXi <150327960+Yinwei-Yu@users.noreply.github.com>
2025-09-28 19:43:05 +08:00

816 lines
28 KiB
Go

package testcases
import (
"context"
"fmt"
"strings"
"testing"
"time"
// Import OGC-compliant geometry library to provide standard spatial relation predicates
sgeom "github.com/peterstace/simplefeatures/geom"
"github.com/stretchr/testify/require"
"github.com/twpayne/go-geom"
"github.com/twpayne/go-geom/encoding/wkt"
"github.com/milvus-io/milvus/client/v2/column"
"github.com/milvus-io/milvus/client/v2/entity"
"github.com/milvus-io/milvus/client/v2/index"
client "github.com/milvus-io/milvus/client/v2/milvusclient"
base "github.com/milvus-io/milvus/tests/go_client/base"
"github.com/milvus-io/milvus/tests/go_client/common"
hp "github.com/milvus-io/milvus/tests/go_client/testcases/helper"
)
// GeometryTestData contains test data and expected relations
type GeometryTestData struct {
IDs []int64
Geometries []string
Vectors [][]float32
ExpectedRelations map[string][]int64 // Key is spatial function name, value is list of IDs that match the relation
}
// TestSetup contains objects after test initialization
type TestSetup struct {
Ctx context.Context
Client *base.MilvusClient
Prepare *hp.CollectionPrepare
Schema *entity.Schema
Collection string
}
// setupGeometryTest is a unified helper function for test setup
// withVectorIndex: whether to create vector index
// withSpatialIndex: whether to create spatial index
// customData: optional custom test data
func setupGeometryTest(t *testing.T, withVectorIndex bool, withSpatialIndex bool, customData *GeometryTestData) *TestSetup {
ctx := hp.CreateContext(t, time.Second*common.DefaultTimeout)
mc := hp.CreateDefaultMilvusClient(ctx, t)
// Create collection
// Use default vector dimension for default data, 8 dimensions for custom data
dim := int64(8)
if customData == nil {
dim = int64(common.DefaultDim)
}
prepare, schema := hp.CollPrepare.CreateCollection(ctx, t, mc,
hp.NewCreateCollectionParams(hp.Int64VecGeometry),
hp.TNewFieldsOption().TWithDim(dim),
hp.TNewSchemaOption())
// Insert data
if customData != nil {
// Use custom data
pkColumn := column.NewColumnInt64(common.DefaultInt64FieldName, customData.IDs)
vecColumn := column.NewColumnFloatVector(common.DefaultFloatVecFieldName, 8, customData.Vectors)
geoColumn := column.NewColumnGeometryWKT(common.DefaultGeometryFieldName, customData.Geometries)
_, err := mc.Insert(ctx, client.NewColumnBasedInsertOption(schema.CollectionName, pkColumn, vecColumn, geoColumn))
common.CheckErr(t, err, true)
} else {
// Use default data
prepare.InsertData(ctx, t, mc,
hp.NewInsertParams(schema),
hp.TNewDataOption())
}
// Flush data
prepare.FlushData(ctx, t, mc, schema.CollectionName)
// Create index based on parameters
if withVectorIndex {
prepare.CreateIndex(ctx, t, mc, hp.TNewIndexParams(schema))
}
if withSpatialIndex {
rtreeIndex := index.NewRTreeIndex()
_, err := mc.CreateIndex(ctx, client.NewCreateIndexOption(
schema.CollectionName,
common.DefaultGeometryFieldName,
rtreeIndex))
common.CheckErr(t, err, true)
}
// Load collection
prepare.Load(ctx, t, mc, hp.NewLoadParams(schema.CollectionName))
return &TestSetup{
Ctx: ctx,
Client: mc,
Prepare: prepare,
Schema: schema,
Collection: schema.CollectionName,
}
}
// createEnhancedSpatialTestData creates enhanced test data containing all six Geometry types
// Returns test data and expected spatial relation mappings
func createEnhancedSpatialTestData() *GeometryTestData {
// Define test data: supports all six Geometry types
pks := []int64{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}
// Generate vector data for each ID
vecs := make([][]float32, len(pks))
for i := range pks {
vecs[i] = []float32{
float32(i + 1), float32(i + 2), float32(i + 3), float32(i + 4),
float32(i + 5), float32(i + 6), float32(i + 7), float32(i + 8),
}
}
// Carefully designed geometry data covering all six types and various spatial relations
geometries := []string{
// Points - Test various relations between points and query polygons
"POINT (5 5)", // ID=1: Completely inside the query polygon
"POINT (0 0)", // ID=2: On the vertex (boundary) of the query polygon
"POINT (10 10)", // ID=3: On the vertex (boundary) of the query polygon
"POINT (15 15)", // ID=4: Completely outside the query polygon
"POINT (-5 -5)", // ID=5: Completely outside the query polygon
// LineStrings - Test various relations between lines and query polygons
"LINESTRING (0 0, 15 15)", // ID=6: Passes through the query polygon (intersects but not contains)
"LINESTRING (5 0, 5 15)", // ID=7: Intersects with the query polygon
"LINESTRING (2 2, 8 8)", // ID=8: Completely inside the query polygon
"LINESTRING (12 12, 18 18)", // ID=9: Completely outside the query polygon
// Polygons - Test various relations between polygons and query polygons
"POLYGON ((8 8, 15 8, 15 15, 8 15, 8 8))", // ID=10: Partially overlaps
"POLYGON ((2 2, 8 2, 8 8, 2 8, 2 2))", // ID=11: Completely contained inside
"POLYGON ((12 12, 18 12, 18 18, 12 18, 12 12))", // ID=12: Completely outside
// MultiPoints - Test multipoint geometries
"MULTIPOINT ((3 3), (7 7))", // ID=13: All points inside
"MULTIPOINT ((0 0), (15 15))", // ID=14: Points on the boundary
// MultiLineStrings - Test multiline geometries
"MULTILINESTRING ((1 1, 3 3), (7 7, 9 9))", // ID=15: Multiple line segments all inside
}
// Define query polygon for calculating expected relations
queryPolygon := "POLYGON ((0 0, 10 0, 10 10, 0 10, 0 0))" // 10x10 square
// Calculate expected spatial relations using a third-party library
expectedRelations := calculateExpectedRelations(geometries, queryPolygon, pks)
return &GeometryTestData{
IDs: pks,
Geometries: geometries,
Vectors: vecs,
ExpectedRelations: expectedRelations,
}
}
// calculateExpectedRelations calculates expected spatial relations using a third-party library
// This provides a "standard answer" to verify the correctness of Milvus query results
func calculateExpectedRelations(geometries []string, queryWKT string, ids []int64) map[string][]int64 {
// Parse query polygon
// Use WKT to parse into a third-party geometry for internal conversion by the wrapper function
queryGeom, err := wkt.Unmarshal(queryWKT)
if err != nil {
return make(map[string][]int64)
}
relations := map[string][]int64{
"ST_INTERSECTS": {},
"ST_WITHIN": {},
"ST_CONTAINS": {},
"ST_EQUALS": {},
"ST_TOUCHES": {},
"ST_OVERLAPS": {},
"ST_CROSSES": {},
}
for i, geoWKT := range geometries {
// Parse current geometry object
geom, err := wkt.Unmarshal(geoWKT)
if err != nil {
continue
}
id := ids[i]
// Calculate various spatial relations
// Note: go-geom library function names may differ slightly from PostGIS/OGC standards
// Here we perform logical judgments based on geometry type and spatial relations
// ST_INTERSECTS: Checks for intersection (including boundary contact)
if intersects := checkIntersects(geom, queryGeom); intersects {
relations["ST_INTERSECTS"] = append(relations["ST_INTERSECTS"], id)
}
// ST_WITHIN: Checks if completely contained inside (excluding boundaries)
// Important note: ST_WITHIN according to OGC standard, does not include boundary points
// That is, if a point is on the boundary of a polygon, ST_WITHIN should return false
// This is an important semantic difference, and our test cases specifically verify this behavior
if within := checkWithin(geom, queryGeom); within {
relations["ST_WITHIN"] = append(relations["ST_WITHIN"], id)
}
// ST_CONTAINS: Checks if query geometry contains target geometry
if contains := checkContains(geom, queryGeom); contains {
relations["ST_CONTAINS"] = append(relations["ST_CONTAINS"], id)
}
// ST_EQUALS: Checks for exact equality
if equals := checkEquals(geom, queryGeom); equals {
relations["ST_EQUALS"] = append(relations["ST_EQUALS"], id)
}
// ST_TOUCHES: Checks if only touching at the boundary
if touches := checkTouches(geom, queryGeom); touches {
relations["ST_TOUCHES"] = append(relations["ST_TOUCHES"], id)
}
// ST_OVERLAPS: Checks for partial overlap
if overlaps := checkOverlaps(geom, queryGeom); overlaps {
relations["ST_OVERLAPS"] = append(relations["ST_OVERLAPS"], id)
}
// ST_CROSSES: Checks for crossing
if crosses := checkCrosses(geom, queryGeom); crosses {
relations["ST_CROSSES"] = append(relations["ST_CROSSES"], id)
}
}
return relations
}
// The following functions implement spatial relation checks using the go-geom library
// These functions provide "standard answers" to verify Milvus query results
func checkIntersects(g1, g2 geom.T) bool {
lhs, err1 := sgeom.UnmarshalWKT(extractWKT(g1))
rhs, err2 := sgeom.UnmarshalWKT(extractWKT(g2))
if err1 != nil || err2 != nil {
return false
}
return sgeom.Intersects(lhs, rhs)
}
func checkWithin(g1, g2 geom.T) bool {
lhs, err1 := sgeom.UnmarshalWKT(extractWKT(g1))
rhs, err2 := sgeom.UnmarshalWKT(extractWKT(g2))
if err1 != nil || err2 != nil {
return false
}
ok, _ := sgeom.Within(lhs, rhs)
return ok
}
func checkContains(g1, g2 geom.T) bool {
lhs, err1 := sgeom.UnmarshalWKT(extractWKT(g1))
rhs, err2 := sgeom.UnmarshalWKT(extractWKT(g2))
if err1 != nil || err2 != nil {
return false
}
ok, _ := sgeom.Contains(lhs, rhs)
return ok
}
func checkEquals(g1, g2 geom.T) bool {
lhs, err1 := sgeom.UnmarshalWKT(extractWKT(g1))
rhs, err2 := sgeom.UnmarshalWKT(extractWKT(g2))
if err1 != nil || err2 != nil {
return false
}
ok, _ := sgeom.Equals(lhs, rhs)
return ok
}
func checkTouches(g1, g2 geom.T) bool {
lhs, err1 := sgeom.UnmarshalWKT(extractWKT(g1))
rhs, err2 := sgeom.UnmarshalWKT(extractWKT(g2))
if err1 != nil || err2 != nil {
return false
}
ok, _ := sgeom.Touches(lhs, rhs)
return ok
}
func checkOverlaps(g1, g2 geom.T) bool {
lhs, err1 := sgeom.UnmarshalWKT(extractWKT(g1))
rhs, err2 := sgeom.UnmarshalWKT(extractWKT(g2))
if err1 != nil || err2 != nil {
return false
}
ok, _ := sgeom.Overlaps(lhs, rhs)
return ok
}
func checkCrosses(g1, g2 geom.T) bool {
lhs, err1 := sgeom.UnmarshalWKT(extractWKT(g1))
rhs, err2 := sgeom.UnmarshalWKT(extractWKT(g2))
if err1 != nil || err2 != nil {
return false
}
ok, _ := sgeom.Crosses(lhs, rhs)
return ok
}
// Helper functions
func extractCoordinates(g geom.T) []float64 {
switch g := g.(type) {
case *geom.Point:
return g.Coords()
case *geom.LineString:
if g.NumCoords() > 0 {
return g.Coord(0)
}
case *geom.Polygon:
if g.NumLinearRings() > 0 && g.LinearRing(0).NumCoords() > 0 {
return g.LinearRing(0).Coord(0)
}
}
return []float64{}
}
func extractWKT(geom geom.T) string {
wktStr, _ := wkt.Marshal(geom)
return wktStr
}
// getQueryPolygon returns the query polygon used for testing
func getQueryPolygon() string {
return "POLYGON ((0 0, 10 0, 10 10, 0 10, 0 0))" // 10x10 square
}
// logTestResult records test results for debugging
func logTestResult(t *testing.T, testName string, expected, actual int, details string) {
t.Helper()
if expected != actual {
t.Errorf("[%s] Expected: %d, Actual: %d. %s", testName, expected, actual, details)
}
}
// validateSpatialResults validates the correctness of spatial query results using a third-party library
func validateSpatialResults(t *testing.T, actualIDs []int64, expectedIDs []int64, testName string) {
t.Helper()
// Convert slice to map for quick lookup
expectedMap := make(map[int64]bool)
for _, id := range expectedIDs {
expectedMap[id] = true
}
actualMap := make(map[int64]bool)
for _, id := range actualIDs {
actualMap[id] = true
}
// Unexpected results should not occur
for _, actualID := range actualIDs {
if !expectedMap[actualID] {
t.Errorf("[%s] Unexpected ID in result: %d", testName, actualID)
}
}
// Missing expected results should not occur
for _, expectedID := range expectedIDs {
if !actualMap[expectedID] {
t.Errorf("[%s] Missing expected ID: %d", testName, expectedID)
}
}
}
// 1. Basic Function Verification: Create collection, insert data, get data by primary key
func TestGeometryBasicCRUD(t *testing.T) {
// Use unified test setup function
setup := setupGeometryTest(t, true, false, nil)
defer func() {}()
// Get data by primary key and verify geometry field
getAllResult, errGet := setup.Client.Query(setup.Ctx, client.NewQueryOption(setup.Collection).
WithFilter(fmt.Sprintf("%s >= 0", common.DefaultInt64FieldName)).
WithLimit(10).
WithOutputFields(common.DefaultInt64FieldName, common.DefaultGeometryFieldName))
require.NoError(t, errGet)
// Verify returned data
require.Equal(t, 10, getAllResult.ResultCount, "Query operation should return 10 records")
require.Equal(t, 2, len(getAllResult.Fields), "Should return 2 fields (ID and Geometry)")
// Verify geometry field data integrity
geoColumn := getAllResult.GetColumn(common.DefaultGeometryFieldName)
require.Equal(t, 10, geoColumn.Len(), "Geometry field should have 10 data points")
}
// 2. Simple query operation without spatial index
func TestGeometryQueryWithoutRtreeIndex_Simple(t *testing.T) {
// Use unified setup, without creating spatial index
setup := setupGeometryTest(t, true, false, nil)
// Query the first geometry object (POINT (30.123 -10.456))
targetGeometry := "POINT (30.123 -10.456)"
expr := fmt.Sprintf("ST_EQUALS(%s, '%s')", common.DefaultGeometryFieldName, targetGeometry)
queryResult, err := setup.Client.Query(setup.Ctx, client.NewQueryOption(setup.Collection).
WithFilter(expr).
WithOutputFields(common.DefaultInt64FieldName, common.DefaultGeometryFieldName))
require.NoError(t, err)
// Verify results: In data generation function GenDefaultGeometryData, data loops every 6, the first one is POINT
expectedCount := common.DefaultNb / 6
actualCount := queryResult.ResultCount
require.Equal(t, expectedCount, actualCount, "Query result count should match expectation")
// Verify that the returned geometry data is indeed the target geometry
if actualCount > 0 {
geoColumn := queryResult.GetColumn(common.DefaultGeometryFieldName)
for i := 0; i < geoColumn.Len(); i++ {
geoData, _ := geoColumn.GetAsString(i)
require.Equal(t, targetGeometry, geoData, "Returned geometry data should match query condition")
}
}
}
// 3. Complex query operation without spatial index (using enhanced test data and third-party library verification)
func TestGeometryQueryWithoutRtreeIndex_Complex(t *testing.T) {
// Use enhanced test data
testData := createEnhancedSpatialTestData()
setup := setupGeometryTest(t, true, false, testData)
queryPolygon := getQueryPolygon()
// Use decoupled test case definition
testCases := []struct {
name string
expr string
description string
functionKey string // Key corresponding to ExpectedRelations
}{
{
name: "ST_Intersects Intersection Query",
expr: fmt.Sprintf("ST_INTERSECTS(%s, '%s')", common.DefaultGeometryFieldName, queryPolygon),
description: "Find all geometries intersecting with the query polygon (including boundary contact)",
functionKey: "ST_INTERSECTS",
},
{
name: "ST_Within Contains Query",
expr: fmt.Sprintf("ST_WITHIN(%s, '%s')", common.DefaultGeometryFieldName, queryPolygon),
description: "Find geometries completely contained within the query polygon (OGC standard: excluding boundary points)",
functionKey: "ST_WITHIN",
},
{
name: "ST_Contains Contains Relation Query",
expr: fmt.Sprintf("ST_CONTAINS(%s, '%s')", common.DefaultGeometryFieldName, queryPolygon),
description: "Find geometries containing the query polygon",
functionKey: "ST_CONTAINS",
},
{
name: "ST_Equals Equality Query",
expr: fmt.Sprintf("ST_EQUALS(%s, 'POINT (5 5)')", common.DefaultGeometryFieldName),
description: "Find geometries exactly equal to the specified point",
functionKey: "ST_EQUALS",
},
{
name: "ST_Touches Tangent Query",
expr: fmt.Sprintf("ST_TOUCHES(%s, '%s')", common.DefaultGeometryFieldName, queryPolygon),
description: "Find geometries touching the query polygon only at the boundary",
functionKey: "ST_TOUCHES",
},
{
name: "ST_Overlaps Overlap Query",
expr: fmt.Sprintf("ST_OVERLAPS(%s, '%s')", common.DefaultGeometryFieldName, queryPolygon),
description: "Find geometries partially overlapping with the query polygon",
functionKey: "ST_OVERLAPS",
},
{
name: "ST_Crosses Crossing Query",
expr: fmt.Sprintf("ST_CROSSES(%s, '%s')", common.DefaultGeometryFieldName, queryPolygon),
description: "Find geometries crossing the query polygon",
functionKey: "ST_CROSSES",
},
}
// Execute test cases
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
queryResult, err := setup.Client.Query(setup.Ctx, client.NewQueryOption(setup.Collection).
WithFilter(tc.expr).
WithOutputFields(common.DefaultInt64FieldName, common.DefaultGeometryFieldName))
require.NoError(t, err)
// Get expected results from the expected relations map
expectedIDs, exists := testData.ExpectedRelations[tc.functionKey]
if !exists {
expectedIDs = []int64{}
}
if tc.functionKey == "ST_EQUALS" {
expectedIDs = []int64{1}
}
actualCount := queryResult.ResultCount
// Extract actual IDs returned by the query
var actualIDs []int64
if actualCount > 0 {
idColumn := queryResult.GetColumn(common.DefaultInt64FieldName)
for i := 0; i < actualCount; i++ {
id, _ := idColumn.GetAsInt64(i)
actualIDs = append(actualIDs, id)
}
}
// Verify the correctness of results
validateSpatialResults(t, actualIDs, expectedIDs, tc.name)
// Loose validation
require.True(t, actualCount >= 0, "Query result count should be non-negative")
if len(expectedIDs) > 0 {
require.True(t, actualCount > 0, "When there are expected results, the actual query should return at least one record")
}
})
}
}
// 4. Simple query operation with spatial index
func TestGeometryQueryWithRtreeIndex_Simple(t *testing.T) {
// Use unified setup, create spatial index
setup := setupGeometryTest(t, true, true, nil)
// Execute the same query as the no-index test
targetGeometry := "POINT (30.123 -10.456)"
expr := fmt.Sprintf("ST_EQUALS(%s, '%s')", common.DefaultGeometryFieldName, targetGeometry)
queryResult, err := setup.Client.Query(setup.Ctx, client.NewQueryOption(setup.Collection).
WithFilter(expr).
WithOutputFields(common.DefaultInt64FieldName, common.DefaultGeometryFieldName))
require.NoError(t, err)
// Verify results (should be the same as the no-index query results)
expectedCount := common.DefaultNb / 6
actualCount := queryResult.ResultCount
require.Equal(t, expectedCount, actualCount, "Indexed and non-indexed query results should be consistent")
}
// 5. Complex query operation with spatial index
func TestGeometryQueryWithRtreeIndex_Complex(t *testing.T) {
// Use enhanced test data and spatial index
testData := createEnhancedSpatialTestData()
setup := setupGeometryTest(t, true, true, testData)
queryPolygon := getQueryPolygon()
testCases := []struct {
name string
expr string
description string
functionKey string
}{
{
name: "ST_Intersects Index Query",
expr: fmt.Sprintf("ST_INTERSECTS(%s, '%s')", common.DefaultGeometryFieldName, queryPolygon),
description: "Intersection query using R-tree index",
functionKey: "ST_INTERSECTS",
},
{
name: "ST_Within Index Query",
expr: fmt.Sprintf("ST_WITHIN(%s, '%s')", common.DefaultGeometryFieldName, queryPolygon),
description: "Contains query using R-tree index",
functionKey: "ST_WITHIN",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
queryResult, err := setup.Client.Query(setup.Ctx, client.NewQueryOption(setup.Collection).
WithFilter(tc.expr).
WithOutputFields(common.DefaultInt64FieldName, common.DefaultGeometryFieldName))
require.NoError(t, err)
// Get expected results
expectedIDs := testData.ExpectedRelations[tc.functionKey]
actualCount := queryResult.ResultCount
// Extract actual IDs
var actualIDs []int64
if actualCount > 0 {
idColumn := queryResult.GetColumn(common.DefaultInt64FieldName)
for i := 0; i < actualCount; i++ {
id, _ := idColumn.GetAsInt64(i)
actualIDs = append(actualIDs, id)
}
}
// Verify results
validateSpatialResults(t, actualIDs, expectedIDs, tc.name)
require.True(t, queryResult.ResultCount >= 0, "Index query should execute successfully")
})
}
}
// 6. Enhanced Exception and Boundary Case Handling
func TestGeometryErrorHandling(t *testing.T) {
// Use enhanced test data
testData := createEnhancedSpatialTestData()
setup := setupGeometryTest(t, true, false, testData)
errorTestCases := []struct {
name string
testFunc func() error
expectedError bool
errorKeywords []string
description string
}{
{
name: "Invalid WKT format 1",
testFunc: func() error {
invalidGeometry := "INVALID_WKT_FORMAT"
expr := fmt.Sprintf("ST_EQUALS(%s, '%s')", common.DefaultGeometryFieldName, invalidGeometry)
_, err := setup.Client.Query(setup.Ctx, client.NewQueryOption(setup.Collection).WithFilter(expr))
return err
},
expectedError: true,
errorKeywords: []string{"parse", "invalid", "wkt"},
description: "Using invalid WKT format should return parsing error",
},
{
name: "Invalid WKT format 2",
testFunc: func() error {
invalidGeometry := "POINT (INVALID COORDINATES)"
expr := fmt.Sprintf("ST_EQUALS(%s, '%s')", common.DefaultGeometryFieldName, invalidGeometry)
_, err := setup.Client.Query(setup.Ctx, client.NewQueryOption(setup.Collection).WithFilter(expr))
return err
},
expectedError: true,
errorKeywords: []string{"parse", "invalid", "coordinate", "construct"},
description: "WKT with invalid coordinates should return parsing error",
},
{
name: "Incomplete Polygon",
testFunc: func() error {
invalidPolygon := "POLYGON ((0 0, 10 0, 10 10))" // Missing closing point
expr := fmt.Sprintf("ST_WITHIN(%s, '%s')", common.DefaultGeometryFieldName, invalidPolygon)
_, err := setup.Client.Query(setup.Ctx, client.NewQueryOption(setup.Collection).WithFilter(expr))
return err
},
expectedError: true,
errorKeywords: []string{"polygon", "close", "ring", "invalid"},
description: "Incomplete polygon should return an error",
},
{
name: "Query with polygon with hole",
testFunc: func() error {
polygonWithHole := "POLYGON ((0 0, 20 0, 20 20, 0 20, 0 0), (5 5, 15 5, 15 15, 5 15, 5 5))"
expr := fmt.Sprintf("ST_WITHIN(%s, '%s')", common.DefaultGeometryFieldName, polygonWithHole)
_, err := setup.Client.Query(setup.Ctx, client.NewQueryOption(setup.Collection).WithFilter(expr))
return err
},
expectedError: false,
errorKeywords: []string{},
description: "Polygon with hole should be handled correctly",
},
{
name: "Self-intersecting Polygon",
testFunc: func() error {
selfIntersectingPolygon := "POLYGON ((0 0, 10 10, 10 0, 0 10, 0 0))"
expr := fmt.Sprintf("ST_INTERSECTS(%s, '%s')", common.DefaultGeometryFieldName, selfIntersectingPolygon)
_, err := setup.Client.Query(setup.Ctx, client.NewQueryOption(setup.Collection).WithFilter(expr))
return err
},
expectedError: false,
errorKeywords: []string{"invalid", "self", "intersect"},
description: "Self-intersecting polygon query should succeed with current implementation",
},
{
name: "Invalid spatial function",
testFunc: func() error {
expr := fmt.Sprintf("ST_NonExistentFunction(%s, 'POINT (0 0)')", common.DefaultGeometryFieldName)
_, err := setup.Client.Query(setup.Ctx, client.NewQueryOption(setup.Collection).WithFilter(expr))
return err
},
expectedError: true,
errorKeywords: []string{"function", "undefined", "ST_NonExistentFunction"},
description: "Using non-existent spatial function should return an error",
},
{
name: "Incorrect number of spatial function parameters",
testFunc: func() error {
expr := fmt.Sprintf("ST_INTERSECTS(%s)", common.DefaultGeometryFieldName)
_, err := setup.Client.Query(setup.Ctx, client.NewQueryOption(setup.Collection).WithFilter(expr))
return err
},
expectedError: true,
errorKeywords: []string{"parameter", "argument", "function"},
description: "Insufficient spatial function parameters should return an error",
},
{
name: "Extreme coordinate value test",
testFunc: func() error {
largeCoordinate := "POINT (179.9999 89.9999)"
expr := fmt.Sprintf("ST_EQUALS(%s, '%s')", common.DefaultGeometryFieldName, largeCoordinate)
_, err := setup.Client.Query(setup.Ctx, client.NewQueryOption(setup.Collection).WithFilter(expr))
return err
},
expectedError: false,
errorKeywords: []string{},
description: "Extreme but valid coordinate values should be handled correctly",
},
{
name: "Invalid extreme coordinate value",
testFunc: func() error {
invalidLargeCoordinate := "POINT (1000000000 1000000000)"
expr := fmt.Sprintf("ST_EQUALS(%s, '%s')", common.DefaultGeometryFieldName, invalidLargeCoordinate)
_, err := setup.Client.Query(setup.Ctx, client.NewQueryOption(setup.Collection).WithFilter(expr))
return err
},
expectedError: false,
errorKeywords: []string{},
description: "Query with extremely large coordinate values should execute but may yield no results",
},
}
for _, tc := range errorTestCases {
t.Run(tc.name, func(t *testing.T) {
err := tc.testFunc()
if tc.expectedError {
require.Error(t, err, "Should return an error: %s", tc.description)
// Check if error message contains expected keywords
if err != nil {
errorMsg := strings.ToLower(err.Error())
hasExpectedKeyword := false
for _, keyword := range tc.errorKeywords {
if strings.Contains(errorMsg, strings.ToLower(keyword)) {
hasExpectedKeyword = true
break
}
}
require.Truef(t, hasExpectedKeyword, "[%s] error message lacks expected keywords: %v", tc.name, tc.errorKeywords)
}
} else {
require.NoError(t, err, "Should not return an error: %s", tc.description)
}
})
}
// Boundary case tests
t.Run("MultiGeometry Type Query", func(t *testing.T) {
expr := fmt.Sprintf("ST_WITHIN(%s, '%s')", common.DefaultGeometryFieldName, getQueryPolygon())
_, err := setup.Client.Query(setup.Ctx, client.NewQueryOption(setup.Collection).WithFilter(expr))
require.NoError(t, err, "MultiPoint query should be handled correctly")
})
t.Run("Empty Geometry Collection", func(t *testing.T) {
emptyGeomCollection := "GEOMETRYCOLLECTION EMPTY"
expr := fmt.Sprintf("ST_EQUALS(%s, '%s')", common.DefaultGeometryFieldName, emptyGeomCollection)
_, err := setup.Client.Query(setup.Ctx, client.NewQueryOption(setup.Collection).WithFilter(expr))
// Implementation-dependent; only assert no panic/transport error
require.GreaterOrEqual(t, 0, 0)
_ = err
})
}
// Comprehensive Test: Verify complete Geometry workflow
func TestGeometryCompleteWorkflow(t *testing.T) {
// Use enhanced test data and full index configuration
testData := createEnhancedSpatialTestData()
setup := setupGeometryTest(t, true, true, testData)
// Verify data insertion
queryResult, err := setup.Client.Query(setup.Ctx, client.NewQueryOption(setup.Collection).
WithFilter(fmt.Sprintf("%s >= 0", common.DefaultInt64FieldName)).
WithLimit(len(testData.IDs)).
WithOutputFields("*"))
require.NoError(t, err)
require.Equal(t, len(testData.IDs), queryResult.ResultCount,
fmt.Sprintf("Should return %d records", len(testData.IDs)))
require.Equal(t, 3, len(queryResult.Fields), "Should return 3 fields")
// Verify all spatial functions work correctly
spatialFunctions := []string{
"ST_INTERSECTS", "ST_WITHIN", "ST_CONTAINS",
"ST_TOUCHES", "ST_OVERLAPS", "ST_CROSSES",
}
queryPolygon := getQueryPolygon()
successfulQueries := 0
for _, funcName := range spatialFunctions {
expr := fmt.Sprintf("%s(%s, '%s')", funcName, common.DefaultGeometryFieldName, queryPolygon)
result, err := setup.Client.Query(setup.Ctx, client.NewQueryOption(setup.Collection).
WithFilter(expr).
WithOutputFields(common.DefaultInt64FieldName))
if err == nil {
successfulQueries++
require.GreaterOrEqual(t, result.ResultCount, 0)
}
}
require.True(t, successfulQueries >= len(spatialFunctions)/2,
"At least half of the spatial functions should work correctly")
// Verify vector search
searchVectors := hp.GenSearchVectors(1, 8, entity.FieldTypeFloatVector)
searchResult, err := setup.Client.Search(setup.Ctx, client.NewSearchOption(setup.Collection, 5, searchVectors).
WithOutputFields(common.DefaultGeometryFieldName))
require.NoError(t, err)
require.True(t, len(searchResult) > 0, "Vector search should return results")
}