Skip to content

Commit 317f9ef

Browse files
Refactor Vector implementation (#686)
* Refactor Vector implementation to use struct similar to other Neo4j Structures. * Remove trailing dot from String representation of Vector to avoid confusion. * Add tests for vector types in GetProperty and GetRecordValue functions
1 parent 581af70 commit 317f9ef

14 files changed

Lines changed: 191 additions & 253 deletions

README.md

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -310,16 +310,23 @@ Vector supports the following element types:
310310
| float32| float32 |
311311
| float64| float64 |
312312

313-
You can create a Vector value using:
313+
Creating a Vector value:
314314

315315
```go
316-
vec := neo4j.Vector[float64]{1.0, 2.0, 3.0, 4.0, 5.0}
317-
316+
vec := neo4j.Vector[float64]{
317+
Elems: []float64{1.0, 2.0, 3.0, 4.0, 5.0},
318+
}
318319
```
319320

320-
Receiving a vector value as driver type:
321+
Extracting a Vector from query results:
322+
321323
```go
322-
vecValue := record.Values[0].(neo4j.Vector[float64])
324+
// Using GetRecordValue to extract vector from a record
325+
recordVec, _, err := neo4j.GetRecordValue[neo4j.Vector[float64]](record, "vec")
326+
327+
// Using GetProperty to extract vector from a node or relationship
328+
node := record.Values[0].(neo4j.Node)
329+
propVec, err := neo4j.GetProperty[neo4j.Vector[float64]](node, "vec")
323330
```
324331

325332
## Logging

neo4j/dbtype/vector.go

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,17 +26,19 @@ import (
2626

2727
// VectorElement represents the supported element types for Vector.
2828
type VectorElement interface {
29-
~float64 | ~float32 | ~int8 | ~int16 | ~int32 | ~int64
29+
float64 | float32 | int8 | int16 | int32 | int64
3030
}
3131

3232
// Vector represents a fixed-length array of numeric values.
33-
type Vector[T VectorElement] []T
33+
type Vector[T VectorElement] struct {
34+
Elems []T
35+
}
3436

3537
// String returns the string representation of this Vector in the format:
3638
// vector([data], length, type NOT NULL)
3739
func (v Vector[T]) String() string {
38-
dataStr := formatVectorData(v)
39-
length := len(v)
40+
dataStr := formatVectorData(v.Elems)
41+
length := len(v.Elems)
4042
typeStr := getVectorTypeString[T]()
4143

4244
return fmt.Sprintf("vector([%s], %d, %s)", dataStr, length, typeStr)
@@ -62,7 +64,7 @@ func getVectorTypeString[T VectorElement]() string {
6264
}
6365
}
6466

65-
func formatVectorData[T VectorElement](v Vector[T]) string {
67+
func formatVectorData[T VectorElement](v []T) string {
6668
if len(v) == 0 {
6769
return ""
6870
}

neo4j/dbtype/vector_example_test.go

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -23,41 +23,51 @@ import (
2323
"os"
2424

2525
"github.com/neo4j/neo4j-go-driver/v6/neo4j"
26-
"github.com/neo4j/neo4j-go-driver/v6/neo4j/dbtype"
2726
)
2827

29-
// ExampleVector demonstrates how to use Vector with the Neo4j Go driver.
28+
// ExampleVector demonstrates how to use Vector with GetRecordValue and GetProperty.
3029
func ExampleVector() {
3130
driver, err := neo4j.NewDriver(getUrl(), neo4j.BasicAuth("neo4j", "password", ""))
3231
if err != nil {
3332
panic(err)
3433
}
3534
defer driver.Close(context.Background())
3635

37-
// Write the vector
3836
ctx := context.Background()
39-
vec := dbtype.Vector[float64]{1.0, 2.0, 3.0}
37+
vec := neo4j.Vector[float64]{Elems: []float64{1.0, 2.0, 3.0}}
4038

41-
_, err = neo4j.ExecuteQuery(ctx, driver,
42-
"CREATE (n:VectorExample {vec: $vec}) RETURN n",
39+
// Create a node with a vector property
40+
result, err := neo4j.ExecuteQuery(ctx, driver,
41+
"CREATE (n:VectorExample {vec: $vec}) RETURN n, n.vec AS vec",
4342
map[string]any{"vec": vec},
4443
neo4j.EagerResultTransformer)
4544
if err != nil {
4645
panic(err)
4746
}
4847

49-
// Read the vector back
50-
result, err := neo4j.ExecuteQuery(ctx, driver,
51-
"MATCH (n:VectorExample) RETURN n.vec AS vec LIMIT 1",
52-
nil,
53-
neo4j.EagerResultTransformer)
48+
record := result.Records[0]
49+
50+
// Direct map access with explicit type assertion
51+
rawRecordVec := record.AsMap()["vec"].(neo4j.Vector[float64])
52+
53+
// Typed access with GetRecordValue for clearer errors
54+
recordVec, _, err := neo4j.GetRecordValue[neo4j.Vector[float64]](record, "vec")
5455
if err != nil {
5556
panic(err)
5657
}
5758

58-
if v, ok := result.Records[0].Values[0].(dbtype.Vector[float64]); ok {
59-
fmt.Printf("Read vector: %v\n", v)
59+
// Direct property map access with explicit type assertion
60+
node := record.Values[0].(neo4j.Node)
61+
rawPropVec := node.GetProperties()["vec"].(neo4j.Vector[float64])
62+
63+
// Typed access with GetProperty for clearer errors
64+
propVec, err := neo4j.GetProperty[neo4j.Vector[float64]](node, "vec")
65+
if err != nil {
66+
panic(err)
6067
}
68+
69+
fmt.Printf("record raw=%v, record typed=%v, node raw=%v, node typed=%v\n",
70+
rawRecordVec, recordVec, rawPropVec, propVec)
6171
}
6272

6373
func getUrl() string {

neo4j/dbtype/vectortypes_test.go

Lines changed: 34 additions & 167 deletions
Original file line numberDiff line numberDiff line change
@@ -20,154 +20,21 @@ package dbtype
2020
import (
2121
"fmt"
2222
"math"
23-
"reflect"
2423
"testing"
2524

2625
"github.com/neo4j/neo4j-go-driver/v6/neo4j/internal/testutil"
2726
)
2827

29-
func TestVectorAPI(t *testing.T) {
28+
// TestVectorElementTypes verifies Vector compiles with all supported element types.
29+
func TestVectorElementTypes(t *testing.T) {
3030
t.Parallel()
31-
float64Vec := Vector[float64]{1.0, 2.0, 3.0, 4.0, 5.0}
32-
float32Vec := Vector[float32]{0.1, 0.2, 0.3, 0.4, 0.5}
3331

34-
// Test type assertions
35-
typeTests := []struct {
36-
name string
37-
vec any
38-
expected reflect.Type
39-
}{
40-
{"float64", float64Vec, reflect.TypeOf(Vector[float64]{})},
41-
{"float32", float32Vec, reflect.TypeOf(Vector[float32]{})},
42-
}
43-
44-
for _, tt := range typeTests {
45-
t.Run(tt.name, func(t *testing.T) {
46-
t.Parallel()
47-
if reflect.TypeOf(tt.vec) != tt.expected {
48-
t.Errorf("Expected %s to be of type %v", tt.name, tt.expected)
49-
}
50-
})
51-
}
52-
53-
// Test vector operations
54-
t.Run("length", func(t *testing.T) {
55-
t.Parallel()
56-
testutil.AssertLen(t, float64Vec, 5)
57-
testutil.AssertLen(t, float32Vec, 5)
58-
})
59-
60-
t.Run("access", func(t *testing.T) {
61-
t.Parallel()
62-
accessVec64 := Vector[float64]{1.0, 2.0, 3.0, 4.0, 5.0}
63-
accessVec32 := Vector[float32]{0.1, 0.2, 0.3, 0.4, 0.5}
64-
testutil.AssertDeepEquals(t, accessVec64[0], 1.0)
65-
testutil.AssertDeepEquals(t, accessVec32[1], float32(0.2))
66-
})
67-
68-
t.Run("modification", func(t *testing.T) {
69-
t.Parallel()
70-
modVec := Vector[float64]{1.0, 2.0, 3.0, 4.0, 5.0}
71-
modVec[0] = 10.0
72-
testutil.AssertDeepEquals(t, modVec[0], 10.0)
73-
})
74-
75-
t.Run("make", func(t *testing.T) {
76-
t.Parallel()
77-
largeVec := make(Vector[float64], 100)
78-
testutil.AssertLen(t, largeVec, 100)
79-
})
80-
81-
t.Run("append", func(t *testing.T) {
82-
t.Parallel()
83-
vec := Vector[float64]{1.0, 2.0}
84-
vec = append(vec, 3.0)
85-
testutil.AssertLen(t, vec, 3)
86-
testutil.AssertDeepEquals(t, vec[2], 3.0)
87-
})
88-
89-
t.Run("maps", func(t *testing.T) {
90-
t.Parallel()
91-
params := map[string]any{
92-
"float64_vec": float64Vec,
93-
"float32_vec": float32Vec,
94-
}
95-
96-
vec64, ok := params["float64_vec"].(Vector[float64])
97-
testutil.AssertTrue(t, ok)
98-
testutil.AssertLen(t, vec64, 5)
99-
100-
vec32, ok := params["float32_vec"].(Vector[float32])
101-
testutil.AssertTrue(t, ok)
102-
testutil.AssertLen(t, vec32, 5)
103-
})
104-
105-
t.Run("slices", func(t *testing.T) {
106-
t.Parallel()
107-
vecSlice := []Vector[float64]{float64Vec, {6.0, 7.0, 8.0}}
108-
testutil.AssertLen(t, vecSlice, 2)
109-
})
110-
111-
t.Run("comparison", func(t *testing.T) {
112-
t.Parallel()
113-
vec1 := Vector[float64]{1.0, 2.0, 3.0}
114-
vec2 := Vector[float64]{1.0, 2.0, 3.0}
115-
vec3 := Vector[float64]{1.0, 2.0, 4.0}
116-
117-
testutil.AssertDeepEquals(t, vec1, vec2)
118-
testutil.AssertNotDeepEquals(t, vec1, vec3)
119-
})
120-
}
121-
122-
func TestVectorElementInterface(t *testing.T) {
123-
t.Parallel()
124-
// Test all supported element types
125-
type testCase struct {
126-
name string
127-
vec any
128-
len int
129-
}
130-
131-
testCases := []testCase{
132-
{"float64", Vector[float64]{1.0, 2.0, 3.0}, 3},
133-
{"float32", Vector[float32]{1.0, 2.0, 3.0}, 3},
134-
{"int8", Vector[int8]{1, 2, 3}, 3},
135-
{"int16", Vector[int16]{1, 2, 3}, 3},
136-
{"int32", Vector[int32]{1, 2, 3}, 3},
137-
{"int64", Vector[int64]{1, 2, 3}, 3},
138-
}
139-
140-
for _, tc := range testCases {
141-
t.Run(tc.name, func(t *testing.T) {
142-
t.Parallel()
143-
// Test that the vector can be created (compilation test)
144-
testutil.AssertNotNil(t, tc.vec)
145-
146-
// Test length using reflection
147-
vecValue := reflect.ValueOf(tc.vec)
148-
testutil.AssertIntEqual(t, vecValue.Len(), tc.len)
149-
})
150-
}
151-
}
152-
153-
func TestVectorEmptyAndNil(t *testing.T) {
154-
t.Parallel()
155-
t.Run("empty", func(t *testing.T) {
156-
t.Parallel()
157-
emptyVec := Vector[float64]{}
158-
testutil.AssertLen(t, emptyVec, 0)
159-
})
160-
161-
t.Run("nil", func(t *testing.T) {
162-
t.Parallel()
163-
var nilVec Vector[float64]
164-
testutil.AssertLen(t, nilVec, 0)
165-
166-
// Test that we can append to nil vectors
167-
nilVec = append(nilVec, 1.0)
168-
testutil.AssertLen(t, nilVec, 1)
169-
testutil.AssertDeepEquals(t, nilVec[0], 1.0)
170-
})
32+
var _ Vector[float64]
33+
var _ Vector[float32]
34+
var _ Vector[int8]
35+
var _ Vector[int16]
36+
var _ Vector[int32]
37+
var _ Vector[int64]
17138
}
17239

17340
func TestVectorString(t *testing.T) {
@@ -179,50 +46,50 @@ func TestVectorString(t *testing.T) {
17946
expected string
18047
}{
18148
// Empty vectors
182-
{"empty int8", Vector[int8]{}, "vector([], 0, INTEGER8 NOT NULL)"},
183-
{"empty int16", Vector[int16]{}, "vector([], 0, INTEGER16 NOT NULL)"},
184-
{"empty int32", Vector[int32]{}, "vector([], 0, INTEGER32 NOT NULL)"},
185-
{"empty int64", Vector[int64]{}, "vector([], 0, INTEGER NOT NULL)"},
186-
{"empty float32", Vector[float32]{}, "vector([], 0, FLOAT32 NOT NULL)"},
187-
{"empty float64", Vector[float64]{}, "vector([], 0, FLOAT NOT NULL)"},
49+
{"empty int8", Vector[int8]{Elems: []int8{}}, "vector([], 0, INTEGER8 NOT NULL)"},
50+
{"empty int16", Vector[int16]{Elems: []int16{}}, "vector([], 0, INTEGER16 NOT NULL)"},
51+
{"empty int32", Vector[int32]{Elems: []int32{}}, "vector([], 0, INTEGER32 NOT NULL)"},
52+
{"empty int64", Vector[int64]{Elems: []int64{}}, "vector([], 0, INTEGER NOT NULL)"},
53+
{"empty float32", Vector[float32]{Elems: []float32{}}, "vector([], 0, FLOAT32 NOT NULL)"},
54+
{"empty float64", Vector[float64]{Elems: []float64{}}, "vector([], 0, FLOAT NOT NULL)"},
18855

18956
// Single element vectors
190-
{"single int32", Vector[int32]{42}, "vector([42], 1, INTEGER32 NOT NULL)"},
191-
{"single float64", Vector[float64]{3.14}, "vector([3.14], 1, FLOAT NOT NULL)"},
57+
{"single int32", Vector[int32]{Elems: []int32{42}}, "vector([42], 1, INTEGER32 NOT NULL)"},
58+
{"single float64", Vector[float64]{Elems: []float64{3.14}}, "vector([3.14], 1, FLOAT NOT NULL)"},
19259

19360
// Multiple element vectors
194-
{"int8 multiple", Vector[int8]{1, 2, 3}, "vector([1, 2, 3], 3, INTEGER8 NOT NULL)"},
195-
{"int16 multiple", Vector[int16]{10, 20, 30}, "vector([10, 20, 30], 3, INTEGER16 NOT NULL)"},
196-
{"int32 multiple", Vector[int32]{100, 200, 300}, "vector([100, 200, 300], 3, INTEGER32 NOT NULL)"},
197-
{"int64 multiple", Vector[int64]{1000, 2000, 3000}, "vector([1000, 2000, 3000], 3, INTEGER NOT NULL)"},
198-
{"float32 multiple", Vector[float32]{1.0, 2.0, 3.0}, "vector([1.0, 2.0, 3.0], 3, FLOAT32 NOT NULL)"},
199-
{"float64 multiple", Vector[float64]{1.1, 2.2, 3.3}, "vector([1.1, 2.2, 3.3], 3, FLOAT NOT NULL)"},
61+
{"int8 multiple", Vector[int8]{Elems: []int8{1, 2, 3}}, "vector([1, 2, 3], 3, INTEGER8 NOT NULL)"},
62+
{"int16 multiple", Vector[int16]{Elems: []int16{10, 20, 30}}, "vector([10, 20, 30], 3, INTEGER16 NOT NULL)"},
63+
{"int32 multiple", Vector[int32]{Elems: []int32{100, 200, 300}}, "vector([100, 200, 300], 3, INTEGER32 NOT NULL)"},
64+
{"int64 multiple", Vector[int64]{Elems: []int64{1000, 2000, 3000}}, "vector([1000, 2000, 3000], 3, INTEGER NOT NULL)"},
65+
{"float32 multiple", Vector[float32]{Elems: []float32{1.0, 2.0, 3.0}}, "vector([1.0, 2.0, 3.0], 3, FLOAT32 NOT NULL)"},
66+
{"float64 multiple", Vector[float64]{Elems: []float64{1.1, 2.2, 3.3}}, "vector([1.1, 2.2, 3.3], 3, FLOAT NOT NULL)"},
20067

20168
// Zero values
202-
{"int32 zeros", Vector[int32]{0, 0, 0}, "vector([0, 0, 0], 3, INTEGER32 NOT NULL)"},
203-
{"float64 zeros", Vector[float64]{0.0, 0.0, 0.0}, "vector([0.0, 0.0, 0.0], 3, FLOAT NOT NULL)"},
69+
{"int32 zeros", Vector[int32]{Elems: []int32{0, 0, 0}}, "vector([0, 0, 0], 3, INTEGER32 NOT NULL)"},
70+
{"float64 zeros", Vector[float64]{Elems: []float64{0.0, 0.0, 0.0}}, "vector([0.0, 0.0, 0.0], 3, FLOAT NOT NULL)"},
20471

20572
// Negative numbers
206-
{"int32 negative", Vector[int32]{-1, -2, -3}, "vector([-1, -2, -3], 3, INTEGER32 NOT NULL)"},
207-
{"float64 negative", Vector[float64]{-1.5, -2.5, -3.5}, "vector([-1.5, -2.5, -3.5], 3, FLOAT NOT NULL)"},
73+
{"int32 negative", Vector[int32]{Elems: []int32{-1, -2, -3}}, "vector([-1, -2, -3], 3, INTEGER32 NOT NULL)"},
74+
{"float64 negative", Vector[float64]{Elems: []float64{-1.5, -2.5, -3.5}}, "vector([-1.5, -2.5, -3.5], 3, FLOAT NOT NULL)"},
20875

20976
// Special float values
210-
{"special floats", Vector[float64]{math.NaN(), math.Inf(1), math.Inf(-1)}, "vector([NaN, Infinity, -Infinity], 3, FLOAT NOT NULL)"},
211-
{"mixed special floats", Vector[float64]{math.NaN(), 0.0, math.Inf(1), -1.0, math.Inf(-1)}, "vector([NaN, 0.0, Infinity, -1.0, -Infinity], 5, FLOAT NOT NULL)"},
77+
{"special floats", Vector[float64]{Elems: []float64{math.NaN(), math.Inf(1), math.Inf(-1)}}, "vector([NaN, Infinity, -Infinity], 3, FLOAT NOT NULL)"},
78+
{"mixed special floats", Vector[float64]{Elems: []float64{math.NaN(), 0.0, math.Inf(1), -1.0, math.Inf(-1)}}, "vector([NaN, 0.0, Infinity, -1.0, -Infinity], 5, FLOAT NOT NULL)"},
21279

21380
// Very large numbers
214-
{"very large int64", Vector[int64]{math.MaxInt64, math.MinInt64, 0}, fmt.Sprintf("vector([%d, %d, 0], 3, INTEGER NOT NULL)", math.MaxInt64, math.MinInt64)},
81+
{"very large int64", Vector[int64]{Elems: []int64{math.MaxInt64, math.MinInt64, 0}}, fmt.Sprintf("vector([%d, %d, 0], 3, INTEGER NOT NULL)", math.MaxInt64, math.MinInt64)},
21582

21683
// Scientific notation floats
217-
{"scientific floats", Vector[float64]{1e10, 2e-5, 3.14159e2}, "vector([10000000000.0, 2e-05, 314.159], 3, FLOAT NOT NULL)"},
84+
{"scientific floats", Vector[float64]{Elems: []float64{1e10, 2e-5, 3.14159e2}}, "vector([10000000000.0, 2e-05, 314.159], 3, FLOAT NOT NULL)"},
21885

21986
// Precision test cases
220-
{"float64 precision", Vector[float64]{0.123}, "vector([0.123], 1, FLOAT NOT NULL)"},
221-
{"float32 precision", Vector[float32]{0.123}, "vector([0.123], 1, FLOAT32 NOT NULL)"},
87+
{"float64 precision", Vector[float64]{Elems: []float64{0.123}}, "vector([0.123], 1, FLOAT NOT NULL)"},
88+
{"float32 precision", Vector[float32]{Elems: []float32{0.123}}, "vector([0.123], 1, FLOAT32 NOT NULL)"},
22289

22390
// Sub-normal floats
224-
{"subnormal float64", Vector[float64]{math.SmallestNonzeroFloat64}, "vector([5e-324], 1, FLOAT NOT NULL)"},
225-
{"subnormal float32", Vector[float32]{math.SmallestNonzeroFloat32}, "vector([1e-45], 1, FLOAT32 NOT NULL)"},
91+
{"subnormal float64", Vector[float64]{Elems: []float64{math.SmallestNonzeroFloat64}}, "vector([5e-324], 1, FLOAT NOT NULL)"},
92+
{"subnormal float32", Vector[float32]{Elems: []float32{math.SmallestNonzeroFloat32}}, "vector([1e-45], 1, FLOAT32 NOT NULL)"},
22693
}
22794

22895
for _, tc := range testCases {

neo4j/graph.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ type PropertyValue interface {
2626
bool | int64 | float64 | string |
2727
Point2D | Point3D |
2828
Date | LocalTime | LocalDateTime | Time | Duration | time.Time | /* OffsetTime == Time == dbtype.Time */
29+
Vector[int8] | Vector[int16] | Vector[int32] | Vector[int64] | Vector[float32] | Vector[float64] |
2930
[]byte | []any
3031
}
3132

neo4j/graph_test.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,24 @@ func TestGetProperty(outer *testing.T) {
203203
AssertDeepEquals(t, prop, []any{1, 2, 3})
204204
AssertNoError(t, err)
205205
})
206+
207+
inner.Run("vector int64", func(t *testing.T) {
208+
entity := test.make(singleProp("k", neo4j.Vector[int64]{Elems: []int64{1, 2, 3}}))
209+
210+
prop, err := neo4j.GetProperty[neo4j.Vector[int64]](entity, "k")
211+
212+
AssertDeepEquals(t, prop, neo4j.Vector[int64]{Elems: []int64{1, 2, 3}})
213+
AssertNoError(t, err)
214+
})
215+
216+
inner.Run("vector float64", func(t *testing.T) {
217+
entity := test.make(singleProp("k", neo4j.Vector[float64]{Elems: []float64{1.1, 2.2, 3.3}}))
218+
219+
prop, err := neo4j.GetProperty[neo4j.Vector[float64]](entity, "k")
220+
221+
AssertDeepEquals(t, prop, neo4j.Vector[float64]{Elems: []float64{1.1, 2.2, 3.3}})
222+
AssertNoError(t, err)
223+
})
206224
})
207225

208226
}

0 commit comments

Comments
 (0)