Skip to content

Commit 23ca0b5

Browse files
authored
feat: Add Neo4j Vector type support (#290)
1 parent 8ab9bec commit 23ca0b5

7 files changed

Lines changed: 145 additions & 2 deletions

File tree

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,9 +241,12 @@ Cypher values and types map to these php types and classes:
241241
| Node | `\Laudis\Neo4j\Types\Node` |
242242
| Relationship | `\Laudis\Neo4j\Types\Relationship` |
243243
| Path | `\Laudis\Neo4j\Types\Path` |
244+
| Vector | `\Laudis\Neo4j\Types\Vector` *** |
244245

245246
(*) These items can also be used as parameters in the bolt protocol and will automatically be converted by the driver, so they can be used in Cypher.
246247

248+
(***) Vector (e.g. embedding) is only produced when decoding results from the server; it is not supported as a query parameter.
249+
247250
Besides these examples, `\DateTimeInterface` will map to `DateTimeZoneId` in Cypher. An empty or list-type `array` will be converted to a cypher `List`, and an `associative array` will be converted to a `map`.
248251

249252
(**) A point can be one of four types implementing PointInterface: `\Laudis\Neo4j\Types\CartesianPoint` `\Laudis\Neo4j\Types\Cartesian3DPoint` `\Laudis\Neo4j\Types\WGS84Point` `\Laudis\Neo4j\Types\WGS843DPoint`

src/Enum/VectorTypeMarker.php

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/*
6+
* This file is part of the Neo4j PHP Client and Driver package.
7+
*
8+
* (c) Nagels <https://nagels.tech>
9+
*
10+
* For the full copyright and license information, please view the LICENSE
11+
* file that was distributed with this source code.
12+
*/
13+
14+
namespace Laudis\Neo4j\Enum;
15+
16+
/**
17+
* Type marker for Neo4j Vector element encoding (Bolt structure semantics).
18+
* Indicates how the vector payload is encoded (integer or float, and width).
19+
*
20+
* @see https://neo4j.com/docs/bolt/current/bolt/structure-semantics/#structure-vector
21+
*/
22+
enum VectorTypeMarker: int
23+
{
24+
case INT_8 = 0xC8;
25+
case INT_16 = 0xC9;
26+
case INT_32 = 0xCA;
27+
case INT_64 = 0xCB;
28+
case FLOAT_32 = 0xC6;
29+
case FLOAT_64 = 0xC1;
30+
}

src/Formatter/Specialised/BoltOGMTranslator.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@
2626
use Bolt\protocol\v1\structures\Relationship as BoltRelationship;
2727
use Bolt\protocol\v1\structures\Time as BoltTime;
2828
use Bolt\protocol\v1\structures\UnboundRelationship as BoltUnboundRelationship;
29+
use Bolt\protocol\v6\structures\Vector as BoltVector;
30+
use Laudis\Neo4j\Enum\VectorTypeMarker;
2931
use Laudis\Neo4j\Formatter\SummarizedResultFormatter;
3032
use Laudis\Neo4j\Types\Abstract3DPoint;
3133
use Laudis\Neo4j\Types\AbstractPoint;
@@ -44,6 +46,7 @@
4446
use Laudis\Neo4j\Types\Relationship;
4547
use Laudis\Neo4j\Types\Time;
4648
use Laudis\Neo4j\Types\UnboundRelationship;
49+
use Laudis\Neo4j\Types\Vector;
4750
use Laudis\Neo4j\Types\WGS843DPoint;
4851
use Laudis\Neo4j\Types\WGS84Point;
4952
use UnexpectedValueException;
@@ -81,6 +84,7 @@ public function __construct()
8184
BoltPoint2D::class => $this->makeFromBoltPoint2D(...),
8285
BoltPoint3D::class => $this->makeFromBoltPoint3D(...),
8386
BoltDateTimeZoneId::class => $this->makeBoltTimezoneIdentifier(...),
87+
BoltVector::class => $this->makeFromBoltVector(...),
8488
'array' => $this->mapArray(...),
8589
'int' => static fn (int $x): int => $x,
8690
'null' => static fn (): ?object => null,
@@ -268,6 +272,18 @@ private function makeFromBoltPoint3D(BoltPoint3D $x): Abstract3DPoint
268272
throw new UnexpectedValueException('An srid of '.$x->srid.' has been returned, which has not been implemented.');
269273
}
270274

275+
private function makeFromBoltVector(BoltVector $value): Vector
276+
{
277+
/** @psalm-suppress ImpureMethodCall Vector::decode() only reads protocol data but Psalm treats Bolt structures as potentially stateful */
278+
$decoded = $value->decode();
279+
// Cast to string then read first byte to avoid Bytes::offsetGet (ImpureMethodCall) and to satisfy Psalm that ord() never receives null
280+
$bytesStr = (string) $value->type_marker;
281+
$markerByte = $bytesStr !== '' ? ord($bytesStr[0]) : null;
282+
$typeMarker = $markerByte !== null ? VectorTypeMarker::tryFrom($markerByte) : null;
283+
284+
return new Vector(array_values($decoded), $typeMarker);
285+
}
286+
271287
private function makeFromBoltPath(BoltPath $path): Path
272288
{
273289
$nodes = [];

src/Formatter/SummarizedResultFormatter.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
use Laudis\Neo4j\Types\Path;
4848
use Laudis\Neo4j\Types\Relationship;
4949
use Laudis\Neo4j\Types\Time;
50+
use Laudis\Neo4j\Types\Vector;
5051
use Laudis\Neo4j\Types\WGS843DPoint;
5152
use Laudis\Neo4j\Types\WGS84Point;
5253

@@ -55,7 +56,7 @@
5556
/**
5657
* Decorates the result of the provided format with an extensive summary.
5758
*
58-
* @psalm-type OGMTypes = string|int|float|bool|null|Date|DateTime|Duration|LocalDateTime|LocalTime|Time|Node|Relationship|Path|Cartesian3DPoint|CartesianPoint|WGS84Point|WGS843DPoint|DateTimeZoneId|CypherList<mixed>|CypherMap<mixed>
59+
* @psalm-type OGMTypes = string|int|float|bool|null|Date|DateTime|Duration|LocalDateTime|LocalTime|Time|Node|Relationship|Path|Cartesian3DPoint|CartesianPoint|WGS84Point|WGS843DPoint|DateTimeZoneId|Vector|CypherList<mixed>|CypherMap<mixed>
5960
* @psalm-type OGMResults = CypherList<CypherMap<OGMTypes>>
6061
* @psalm-type CypherStats = array{
6162
* nodes_created: int,

src/ParameterHelper.php

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,8 @@ public static function asParameter(
8383
mixed $value,
8484
ConnectionProtocol $protocol,
8585
): iterable|int|float|bool|string|stdClass|IStructure|null {
86-
return self::cypherMapToStdClass($value) ??
86+
return self::passThroughBoltStructure($value) ??
87+
self::cypherMapToStdClass($value) ??
8788
self::emptySequenceToArray($value) ??
8889
self::convertBoltConvertibles($value) ??
8990
self::convertTemporalTypes($value, $protocol) ??
@@ -92,6 +93,15 @@ public static function asParameter(
9293
self::filterInvalidType($value);
9394
}
9495

96+
private static function passThroughBoltStructure(mixed $value): ?IStructure
97+
{
98+
if ($value instanceof IStructure) {
99+
return $value;
100+
}
101+
102+
return null;
103+
}
104+
95105
/**
96106
* @pure
97107
*/

src/Types/Vector.php

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/*
6+
* This file is part of the Neo4j PHP Client and Driver package.
7+
*
8+
* (c) Nagels <https://nagels.tech>
9+
*
10+
* For the full copyright and license information, please view the LICENSE
11+
* file that was distributed with this source code.
12+
*/
13+
14+
namespace Laudis\Neo4j\Types;
15+
16+
use Laudis\Neo4j\Enum\VectorTypeMarker;
17+
18+
/**
19+
* Neo4j Vector type (e.g. embedding). Holds a list of numbers.
20+
*
21+
* This type is only produced when decoding results from the server (Bolt). It is not supported
22+
* as a query parameter; use a plain list of numbers if you need to pass vector-like data.
23+
*
24+
* @psalm-immutable
25+
*
26+
* @extends AbstractPropertyObject<list<int|float>, list<int|float>>
27+
*/
28+
final class Vector extends AbstractPropertyObject
29+
{
30+
/**
31+
* @param list<int|float> $values
32+
* @param VectorTypeMarker|null $typeMarker Bolt type marker (how values were encoded); set when received from server
33+
*/
34+
public function __construct(
35+
private readonly array $values,
36+
private readonly ?VectorTypeMarker $typeMarker = null,
37+
) {
38+
}
39+
40+
/**
41+
* @return list<int|float>
42+
*/
43+
public function getValues(): array
44+
{
45+
return $this->values;
46+
}
47+
48+
/**
49+
* Bolt type marker indicating how the vector payload is encoded (e.g. FLOAT_64, INT_32).
50+
* Set when the vector was received from the server.
51+
*/
52+
public function getTypeMarker(): ?VectorTypeMarker
53+
{
54+
return $this->typeMarker;
55+
}
56+
57+
/**
58+
* @return array{values: list<int|float>, typeMarker: string|null}
59+
*
60+
* @psalm-suppress ImplementedReturnTypeMismatch parent expects array<string, list<int|float>> but we add typeMarker (string|null) for clarity
61+
*/
62+
public function toArray(): array
63+
{
64+
return [
65+
'values' => $this->values,
66+
'typeMarker' => $this->typeMarker?->name,
67+
];
68+
}
69+
70+
public function getProperties(): CypherMap
71+
{
72+
return new CypherMap($this);
73+
}
74+
}

testkit-backend/src/Responses/Types/CypherObject.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
use Laudis\Neo4j\Types\Path;
2323
use Laudis\Neo4j\Types\Relationship;
2424
use Laudis\Neo4j\Types\UnboundRelationship;
25+
use Laudis\Neo4j\Types\Vector;
2526
use RuntimeException;
2627

2728
/**
@@ -84,6 +85,14 @@ public static function autoDetect($value): TestkitResponseInterface
8485
$tbr = new CypherObject('CypherMap', new CypherMap($map));
8586
}
8687
break;
88+
case Vector::class:
89+
/** @var Vector $value */
90+
$list = [];
91+
foreach ($value->getValues() as $item) {
92+
$list[] = self::autoDetect($item);
93+
}
94+
$tbr = new CypherObject('Vector', new CypherList($list));
95+
break;
8796
case 'int':
8897
$tbr = new CypherObject('CypherInt', $value);
8998
break;

0 commit comments

Comments
 (0)