Skip to content

Commit 5e20637

Browse files
authored
Bolt 6.0 and Vector Type (#1293)
Introduces the Neo4j Vector type, a wrapper for JavaScript TypedArrays, allowing Vectors to be sent to and from the database. To support these, the bolt protocol version 6.0 is introduced.
1 parent 84f345f commit 5e20637

29 files changed

+2639
-31
lines changed

README.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -503,3 +503,29 @@ var driver = neo4j.driver(
503503
{ disableLosslessIntegers: true }
504504
)
505505
```
506+
507+
#### Writing and reading Vectors
508+
509+
Neo4j supports storing vector embeddings in a dedicated vector type. Sending large lists with the driver will result in significant overhead as each value will be transmitted with type information, so the 6.0.0 release of the driver introduced the Neo4j Vector type.
510+
511+
The Vector type supports signed integers of 8, 16, 32 and 64 bits, and floats of 32 and 64 bits. The Vector type is a wrapper for JavaScript TypedArrays of those types.
512+
513+
To create a neo4j Vector in your code, do the following:
514+
515+
```javascript
516+
var neo4j = require('neo4j-driver')
517+
518+
var typedArray = Float32Array.from([1, 2, 3]) //this is how to convert a regular array of numbers into a TypedArray, useful if you handle vectors as regular arrays in your code
519+
520+
var neo4jVector = neo4j.vector(typedArray) //this creates a neo4j Vector of type Float32, containing the values [1, 2, 3]
521+
522+
driver.executeQuery('CREATE (n {embeddings: $myVectorParam})', { myVectorParam: neo4jVector })
523+
```
524+
525+
To access the data in a retrieved Vector you can do the following:
526+
527+
```javascript
528+
var retrievedTypedArray = neo4jVector.asTypedArray() //This will return a TypedArray of the same type as the Vector
529+
530+
var retrievedArray = Array.from(retrievedTypedArray) //This will convert the TypedArray to a regular array of Numbers. (Not safe for Int64 arrays)
531+
```

packages/bolt-connection/src/bolt/bolt-protocol-v1.transformer.js

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ import {
2323
UnboundRelationship,
2424
Path,
2525
toNumber,
26-
PathSegment
26+
PathSegment,
27+
Vector
2728
} from 'neo4j-driver-core'
2829

2930
import { structure } from '../packstream'
@@ -43,6 +44,8 @@ const UNBOUND_RELATIONSHIP_STRUCT_SIZE = 3
4344
const PATH = 0x50
4445
const PATH_STRUCT_SIZE = 3
4546

47+
const VECTOR = 0x56
48+
4649
/**
4750
* Creates the Node Transformer
4851
* @returns {TypeTransformer}
@@ -177,9 +180,27 @@ function createPathTransformer () {
177180
})
178181
}
179182

183+
/**
184+
* Creates a typeTransformer that throws errors if vectors are transmitted.
185+
* @returns {TypeTransformer}
186+
*/
187+
function createVectorTransformer () {
188+
return new TypeTransformer({
189+
signature: VECTOR,
190+
isTypeInstance: object => object instanceof Vector,
191+
toStructure: _ => {
192+
throw newError('Sending vector types require server and driver to be communicating with Bolt protocol 6.0 or later. Please update your database version.')
193+
},
194+
fromStructure: _ => {
195+
throw newError('Server tried to send Vector object, but server and driver are communicating on a version of the Bolt protocol that does not support vectors.')
196+
}
197+
})
198+
}
199+
180200
export default {
181201
createNodeTransformer,
182202
createRelationshipTransformer,
183203
createUnboundRelationshipTransformer,
184-
createPathTransformer
204+
createPathTransformer,
205+
createVectorTransformer
185206
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/**
2+
* Copyright (c) "Neo4j"
3+
* Neo4j Sweden AB [https://neo4j.com]
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
import BoltProtocolV5x8 from './bolt-protocol-v5x8'
18+
19+
import transformersFactories from './bolt-protocol-v6x0.transformer'
20+
import Transformer from './transformer'
21+
22+
import { internal } from 'neo4j-driver-core'
23+
24+
const {
25+
constants: { BOLT_PROTOCOL_V6_0 }
26+
} = internal
27+
28+
export default class BoltProtocol extends BoltProtocolV5x8 {
29+
get version () {
30+
return BOLT_PROTOCOL_V6_0
31+
}
32+
33+
get transformer () {
34+
if (this._transformer === undefined) {
35+
this._transformer = new Transformer(Object.values(transformersFactories).map(create => create(this._config, this._log)))
36+
}
37+
return this._transformer
38+
}
39+
}
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
/**
2+
* Copyright (c) "Neo4j"
3+
* Neo4j Sweden AB [https://neo4j.com]
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
import v5x8 from './bolt-protocol-v5x8.transformer'
19+
import { TypeTransformer } from './transformer'
20+
import { structure } from '../packstream'
21+
import { Vector, newError } from 'neo4j-driver-core'
22+
const VECTOR = 0x56
23+
const FLOAT_32 = 0xc6
24+
const FLOAT_64 = 0xc1
25+
const INT_8 = 0xc8
26+
const INT_16 = 0xc9
27+
const INT_32 = 0xca
28+
const INT_64 = 0xcb
29+
30+
const typeToTypeMarker = {
31+
INT8: INT_8,
32+
INT16: INT_16,
33+
INT32: INT_32,
34+
INT64: INT_64,
35+
FLOAT32: FLOAT_32,
36+
FLOAT64: FLOAT_64
37+
}
38+
39+
function createVectorTransformer () {
40+
return new TypeTransformer({
41+
signature: VECTOR,
42+
isTypeInstance: object => object instanceof Vector,
43+
toStructure: vector => {
44+
const typeMarker = typeToTypeMarker[vector.getType()]
45+
if (typeMarker === undefined) {
46+
throw newError(`Vector object has unknown type: ${vector.getType()}`)
47+
}
48+
const buffer = fixBufferEndianness(typeMarker, vector.asTypedArray().buffer)
49+
const struct = new structure.Structure(VECTOR, [Int8Array.from([typeMarker]), new Int8Array(buffer)])
50+
return struct
51+
},
52+
fromStructure: structure => {
53+
const typeMarker = Uint8Array.from(structure.fields[0])[0]
54+
const byteArray = structure.fields[1]
55+
const buffer = fixBufferEndianness(typeMarker, byteArray.buffer)
56+
switch (typeMarker) {
57+
case INT_8:
58+
return new Vector(new Int8Array(buffer))
59+
case INT_16:
60+
return new Vector(new Int16Array(buffer))
61+
case INT_32:
62+
return new Vector(new Int32Array(buffer))
63+
case INT_64:
64+
return new Vector(new BigInt64Array(buffer))
65+
case FLOAT_32:
66+
return new Vector(new Float32Array(buffer))
67+
case FLOAT_64:
68+
return new Vector(new Float64Array(buffer))
69+
default:
70+
throw newError(`Received Vector structure with unsupported type marker: ${typeMarker}`)
71+
}
72+
}
73+
})
74+
}
75+
76+
function fixBufferEndianness (typeMarker, buffer) {
77+
const isLittleEndian = checkLittleEndian()
78+
if (isLittleEndian) {
79+
const setview = new DataView(new ArrayBuffer(buffer.byteLength))
80+
// we want exact byte accuracy, so we cannot simply get the value from the typed array
81+
const getview = new DataView(buffer)
82+
let set
83+
let get
84+
let elementSize
85+
switch (typeMarker) {
86+
case INT_8:
87+
elementSize = 1
88+
set = setview.setInt8.bind(setview)
89+
get = getview.getInt8.bind(getview)
90+
break
91+
case INT_16:
92+
elementSize = 2
93+
set = setview.setInt16.bind(setview)
94+
get = getview.getInt16.bind(getview)
95+
break
96+
case INT_32:
97+
elementSize = 4
98+
set = setview.setInt32.bind(setview)
99+
get = getview.getInt32.bind(getview)
100+
break
101+
case INT_64:
102+
elementSize = 8
103+
set = setview.setBigInt64.bind(setview)
104+
get = getview.getBigInt64.bind(getview)
105+
break
106+
case FLOAT_32:
107+
elementSize = 4
108+
set = setview.setInt32.bind(setview)
109+
get = getview.getInt32.bind(getview)
110+
break
111+
case FLOAT_64:
112+
elementSize = 8
113+
set = setview.setBigInt64.bind(setview)
114+
get = getview.getBigInt64.bind(getview)
115+
break
116+
default:
117+
throw newError(`Vector is of unsupported type ${typeMarker}`)
118+
}
119+
for (let i = 0; i < buffer.byteLength; i += elementSize) {
120+
set(i, get(i, isLittleEndian))
121+
}
122+
return setview.buffer
123+
} else {
124+
return buffer
125+
}
126+
}
127+
128+
function checkLittleEndian () {
129+
const dataview = new DataView(new ArrayBuffer(2))
130+
dataview.setInt16(0, 1000, true)
131+
const typeArray = new Int16Array(dataview.buffer)
132+
return typeArray[0] === 1000
133+
}
134+
135+
export default {
136+
...v5x8,
137+
createVectorTransformer
138+
}

packages/bolt-connection/src/bolt/create.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import BoltProtocolV5x5 from './bolt-protocol-v5x5'
3333
import BoltProtocolV5x6 from './bolt-protocol-v5x6'
3434
import BoltProtocolV5x7 from './bolt-protocol-v5x7'
3535
import BoltProtocolV5x8 from './bolt-protocol-v5x8'
36+
import BoltProtocolV6x0 from './bolt-protocol-v6x0'
3637
// eslint-disable-next-line no-unused-vars
3738
import { Chunker, Dechunker } from '../channel'
3839
import ResponseHandler from './response-handler'
@@ -266,6 +267,14 @@ function createProtocol (
266267
log,
267268
onProtocolError,
268269
serversideRouting)
270+
case 6.0:
271+
return new BoltProtocolV6x0(server,
272+
chunker,
273+
packingConfig,
274+
createResponseHandler,
275+
log,
276+
onProtocolError,
277+
serversideRouting)
269278
default:
270279
throw newError('Unknown Bolt protocol version: ' + version)
271280
}

packages/bolt-connection/src/bolt/handshake.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import { alloc } from '../channel'
1919
import { newError } from 'neo4j-driver-core'
2020

2121
const BOLT_MAGIC_PREAMBLE = 0x6060b017
22-
const AVAILABLE_BOLT_PROTOCOLS = ['5.8', '5.7', '5.6', '5.4', '5.3', '5.2', '5.1', '5.0', '4.4', '4.3', '4.2', '3.0'] // bolt protocols the client will accept, ordered by preference
22+
const AVAILABLE_BOLT_PROTOCOLS = ['6.0', '5.8', '5.7', '5.6', '5.4', '5.3', '5.2', '5.1', '5.0', '4.4', '4.3', '4.2', '3.0'] // bolt protocols the client will accept, ordered by preference
2323
const DESIRED_CAPABILITES = 0
2424

2525
function version (major, minor) {

packages/bolt-connection/test/bolt/__snapshots__/bolt-protocol-v1.test.js.snap

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,5 @@
11
// Jest Snapshot v1, https://goo.gl/fbAQLP
22

3-
exports[`#unit BoltProtocolV1 .packable() should resultant function not pack graph types (Node) 1`] = `"It is not allowed to pass nodes in query parameters, given: (c:a {a:"b"})"`;
4-
5-
exports[`#unit BoltProtocolV1 .packable() should resultant function not pack graph types (Path) 1`] = `"It is not allowed to pass paths in query parameters, given: [object Object]"`;
6-
7-
exports[`#unit BoltProtocolV1 .packable() should resultant function not pack graph types (Relationship) 1`] = `"It is not allowed to pass relationships in query parameters, given: (e)-[:a {b:"c"}]->(f)"`;
8-
9-
exports[`#unit BoltProtocolV1 .packable() should resultant function not pack graph types (UnboundRelationship) 1`] = `"It is not allowed to pass unbound relationships in query parameters, given: -[:a {b:"c"}]->"`;
10-
113
exports[`#unit BoltProtocolV1 .packable() should pack types introduced afterwards as Map (Date) 1`] = `
124
{
135
"day": 1,
@@ -86,6 +78,16 @@ exports[`#unit BoltProtocolV1 .packable() should pack types introduced afterward
8678
}
8779
`;
8880

81+
exports[`#unit BoltProtocolV1 .packable() should resultant function not pack graph types (Node) 1`] = `"It is not allowed to pass nodes in query parameters, given: (c:a {a:"b"})"`;
82+
83+
exports[`#unit BoltProtocolV1 .packable() should resultant function not pack graph types (Path) 1`] = `"It is not allowed to pass paths in query parameters, given: [object Object]"`;
84+
85+
exports[`#unit BoltProtocolV1 .packable() should resultant function not pack graph types (Relationship) 1`] = `"It is not allowed to pass relationships in query parameters, given: (e)-[:a {b:"c"}]->(f)"`;
86+
87+
exports[`#unit BoltProtocolV1 .packable() should resultant function not pack graph types (UnboundRelationship) 1`] = `"It is not allowed to pass unbound relationships in query parameters, given: -[:a {b:"c"}]->"`;
88+
89+
exports[`#unit BoltProtocolV1 .unpack() should error out of unpacking Vectors 1`] = `"Server tried to send Vector object, but server and driver are communicating on a version of the Bolt protocol that does not support vectors."`;
90+
8991
exports[`#unit BoltProtocolV1 .unpack() should not unpack graph types with wrong size(Node with less fields) 1`] = `"Wrong struct size for Node, expected 3 but was 2"`;
9092

9193
exports[`#unit BoltProtocolV1 .unpack() should not unpack graph types with wrong size(Node with more fields) 1`] = `"Wrong struct size for Node, expected 3 but was 4"`;
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`#unit BoltProtocolV6x0 .packable() should resultant function not pack graph types (Node) 1`] = `"It is not allowed to pass nodes in query parameters, given: (c:a {a:"b"})"`;
4+
5+
exports[`#unit BoltProtocolV6x0 .packable() should resultant function not pack graph types (Path) 1`] = `"It is not allowed to pass paths in query parameters, given: [object Object]"`;
6+
7+
exports[`#unit BoltProtocolV6x0 .packable() should resultant function not pack graph types (Relationship) 1`] = `"It is not allowed to pass relationships in query parameters, given: (e)-[:a {b:"c"}]->(f)"`;
8+
9+
exports[`#unit BoltProtocolV6x0 .packable() should resultant function not pack graph types (UnboundRelationship) 1`] = `"It is not allowed to pass unbound relationships in query parameters, given: -[:a {b:"c"}]->"`;
10+
11+
exports[`#unit BoltProtocolV6x0 .unpack() should not unpack with wrong size (Date with less fields) 1`] = `"Wrong struct size for Date, expected 1 but was 0"`;
12+
13+
exports[`#unit BoltProtocolV6x0 .unpack() should not unpack with wrong size (Date with more fields) 1`] = `"Wrong struct size for Date, expected 1 but was 2"`;
14+
15+
exports[`#unit BoltProtocolV6x0 .unpack() should not unpack with wrong size (DateTimeWithZoneId with less fields) 1`] = `"Wrong struct size for DateTimeWithZoneId, expected 3 but was 2"`;
16+
17+
exports[`#unit BoltProtocolV6x0 .unpack() should not unpack with wrong size (DateTimeWithZoneId with more fields) 1`] = `"Wrong struct size for DateTimeWithZoneId, expected 3 but was 4"`;
18+
19+
exports[`#unit BoltProtocolV6x0 .unpack() should not unpack with wrong size (DateTimeWithZoneOffset with less fields) 1`] = `"Wrong struct size for DateTimeWithZoneOffset, expected 3 but was 2"`;
20+
21+
exports[`#unit BoltProtocolV6x0 .unpack() should not unpack with wrong size (DateTimeWithZoneOffset with more fields) 1`] = `"Wrong struct size for DateTimeWithZoneOffset, expected 3 but was 4"`;
22+
23+
exports[`#unit BoltProtocolV6x0 .unpack() should not unpack with wrong size (Duration with less fields) 1`] = `"Wrong struct size for Duration, expected 4 but was 3"`;
24+
25+
exports[`#unit BoltProtocolV6x0 .unpack() should not unpack with wrong size (Duration with more fields) 1`] = `"Wrong struct size for Duration, expected 4 but was 5"`;
26+
27+
exports[`#unit BoltProtocolV6x0 .unpack() should not unpack with wrong size (LocalDateTime with less fields) 1`] = `"Wrong struct size for LocalDateTime, expected 2 but was 1"`;
28+
29+
exports[`#unit BoltProtocolV6x0 .unpack() should not unpack with wrong size (LocalDateTime with more fields) 1`] = `"Wrong struct size for LocalDateTime, expected 2 but was 3"`;
30+
31+
exports[`#unit BoltProtocolV6x0 .unpack() should not unpack with wrong size (LocalTime with less fields) 1`] = `"Wrong struct size for LocalTime, expected 1 but was 0"`;
32+
33+
exports[`#unit BoltProtocolV6x0 .unpack() should not unpack with wrong size (LocalTime with more fields) 1`] = `"Wrong struct size for LocalTime, expected 1 but was 2"`;
34+
35+
exports[`#unit BoltProtocolV6x0 .unpack() should not unpack with wrong size (Node with less fields) 1`] = `"Wrong struct size for Node, expected 4 but was 3"`;
36+
37+
exports[`#unit BoltProtocolV6x0 .unpack() should not unpack with wrong size (Node with more fields) 1`] = `"Wrong struct size for Node, expected 4 but was 5"`;
38+
39+
exports[`#unit BoltProtocolV6x0 .unpack() should not unpack with wrong size (Path with less fields) 1`] = `"Wrong struct size for Path, expected 3 but was 2"`;
40+
41+
exports[`#unit BoltProtocolV6x0 .unpack() should not unpack with wrong size (Path with more fields) 1`] = `"Wrong struct size for Path, expected 3 but was 4"`;
42+
43+
exports[`#unit BoltProtocolV6x0 .unpack() should not unpack with wrong size (Point with less fields) 1`] = `"Wrong struct size for Point2D, expected 3 but was 2"`;
44+
45+
exports[`#unit BoltProtocolV6x0 .unpack() should not unpack with wrong size (Point with more fields) 1`] = `"Wrong struct size for Point2D, expected 3 but was 4"`;
46+
47+
exports[`#unit BoltProtocolV6x0 .unpack() should not unpack with wrong size (Point3D with less fields) 1`] = `"Wrong struct size for Point3D, expected 4 but was 3"`;
48+
49+
exports[`#unit BoltProtocolV6x0 .unpack() should not unpack with wrong size (Point3D with more fields) 1`] = `"Wrong struct size for Point3D, expected 4 but was 5"`;
50+
51+
exports[`#unit BoltProtocolV6x0 .unpack() should not unpack with wrong size (Relationship with less fields) 1`] = `"Wrong struct size for Relationship, expected 8 but was 5"`;
52+
53+
exports[`#unit BoltProtocolV6x0 .unpack() should not unpack with wrong size (Relationship with more fields) 1`] = `"Wrong struct size for Relationship, expected 8 but was 9"`;
54+
55+
exports[`#unit BoltProtocolV6x0 .unpack() should not unpack with wrong size (Time with less fields) 1`] = `"Wrong struct size for Time, expected 2 but was 1"`;
56+
57+
exports[`#unit BoltProtocolV6x0 .unpack() should not unpack with wrong size (Time with more fileds) 1`] = `"Wrong struct size for Time, expected 2 but was 3"`;
58+
59+
exports[`#unit BoltProtocolV6x0 .unpack() should not unpack with wrong size (UnboundRelationship with less fields) 1`] = `"Wrong struct size for UnboundRelationship, expected 4 but was 3"`;
60+
61+
exports[`#unit BoltProtocolV6x0 .unpack() should not unpack with wrong size (UnboundRelationship with more fields) 1`] = `"Wrong struct size for UnboundRelationship, expected 4 but was 5"`;

0 commit comments

Comments
 (0)