Skip to content

Commit 81c717b

Browse files
committed
Switched to array for internal storage format for Vector and HalfVector [skip ci]
1 parent e771e69 commit 81c717b

2 files changed

Lines changed: 45 additions & 35 deletions

File tree

pgvector/halfvec.py

Lines changed: 24 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
from __future__ import annotations
2+
import array
23
import struct
4+
import sys
35

46
try:
57
import numpy as np
@@ -11,20 +13,24 @@
1113
class HalfVector:
1214
def __init__(self, value: list[float] | np.ndarray[tuple[int], np.dtype[np.floating]]) -> None:
1315
if isinstance(value, list):
16+
dim = len(value)
1417
try:
15-
self._value = [float(v) for v in value]
16-
except (TypeError, ValueError):
18+
self._value = array.array('H', struct.pack(f'{dim}e', *value))
19+
except struct.error:
1720
raise ValueError('expected list[float]')
1821
elif NUMPY_AVAILABLE and isinstance(value, np.ndarray):
1922
if value.ndim != 1:
2023
raise ValueError('expected ndim to be 1')
2124

22-
self._value = [float(v) for v in value]
25+
if value.dtype != np.float16:
26+
value = np.asarray(value, dtype=np.float16)
27+
28+
self._value = array.array('H', value.tobytes())
2329
else:
2430
raise ValueError('expected list or ndarray')
2531

2632
def __repr__(self) -> str:
27-
return f'HalfVector({self._value})'
33+
return f'HalfVector({self.to_list()})'
2834

2935
def __eq__(self, other: object) -> bool:
3036
if isinstance(other, self.__class__):
@@ -35,23 +41,26 @@ def dimensions(self) -> int:
3541
return len(self._value)
3642

3743
def to_list(self) -> list[float]:
38-
return self._value
44+
dim = len(self._value)
45+
return list(struct.unpack(f'{dim}e', self._value.tobytes()))
3946

4047
def to_numpy(self) -> np.ndarray[tuple[int], np.dtype[np.float16]]:
41-
return np.array(self._value, dtype=np.float16)
48+
return np.frombuffer(self._value, dtype=np.float16)
4249

4350
def to_text(self) -> str:
44-
return f'[{",".join([str(v) for v in self._value])}]'
51+
return f'[{",".join([str(v) for v in self.to_list()])}]'
4552

4653
def to_binary(self) -> bytes:
47-
dim = len(self._value)
48-
return struct.pack(f'>HH{dim}e', dim, 0, *self._value)
54+
if sys.byteorder == 'big':
55+
value = self._value
56+
else:
57+
value = array.array('H', self._value)
58+
value.byteswap()
59+
return struct.pack(f'>HH', len(value), 0) + value.tobytes()
4960

5061
@classmethod
5162
def from_text(cls, value: str) -> HalfVector:
52-
vec = cls.__new__(cls)
53-
vec._value = [float(v) for v in value[1:-1].split(',')]
54-
return vec
63+
return cls([float(v) for v in value[1:-1].split(',')])
5564

5665
@classmethod
5766
def from_binary(cls, value: bytes) -> HalfVector:
@@ -64,7 +73,9 @@ def from_binary(cls, value: bytes) -> HalfVector:
6473
raise ValueError('expected unused to be 0')
6574

6675
vec = cls.__new__(cls)
67-
vec._value = list(struct.unpack_from(f'>{dim}e', value[4:]))
76+
vec._value = array.array('H', value[4:])
77+
if sys.byteorder != 'big':
78+
vec._value.byteswap()
6879
return vec
6980

7081
@classmethod

pgvector/vector.py

Lines changed: 21 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
from __future__ import annotations
2+
import array
23
import struct
4+
import sys
35

46
try:
57
import numpy as np
@@ -12,19 +14,22 @@ class Vector:
1214
def __init__(self, value: list[float] | np.ndarray[tuple[int], np.dtype[np.floating]]) -> None:
1315
if isinstance(value, list):
1416
try:
15-
self._value = [float(v) for v in value]
16-
except (TypeError, ValueError):
17+
self._value = array.array('f', value)
18+
except TypeError:
1719
raise ValueError('expected list[float]')
1820
elif NUMPY_AVAILABLE and isinstance(value, np.ndarray):
1921
if value.ndim != 1:
2022
raise ValueError('expected ndim to be 1')
2123

22-
self._value = [float(v) for v in value]
24+
if value.dtype != np.float32:
25+
value = np.asarray(value, dtype=np.float32)
26+
27+
self._value = array.array('f', value.tobytes())
2328
else:
2429
raise ValueError('expected list or ndarray')
2530

2631
def __repr__(self) -> str:
27-
return f'Vector({self._value})'
32+
return f'Vector({self.to_list()})'
2833

2934
def __eq__(self, other: object) -> bool:
3035
if isinstance(other, self.__class__):
@@ -35,23 +40,25 @@ def dimensions(self) -> int:
3540
return len(self._value)
3641

3742
def to_list(self) -> list[float]:
38-
return self._value
43+
return self._value.tolist()
3944

4045
def to_numpy(self) -> np.ndarray[tuple[int], np.dtype[np.float32]]:
41-
return np.array(self._value, dtype=np.float32)
46+
return np.frombuffer(self._value, dtype=np.float32)
4247

4348
def to_text(self) -> str:
4449
return f'[{",".join([str(v) for v in self._value])}]'
4550

4651
def to_binary(self) -> bytes:
47-
dim = len(self._value)
48-
return struct.pack(f'>HH{dim}f', dim, 0, *self._value)
52+
if sys.byteorder == 'big':
53+
value = self._value
54+
else:
55+
value = array.array('f', self._value)
56+
value.byteswap()
57+
return struct.pack(f'>HH', len(value), 0) + value.tobytes()
4958

5059
@classmethod
5160
def from_text(cls, value: str) -> Vector:
52-
vec = cls.__new__(cls)
53-
vec._value = [float(v) for v in value[1:-1].split(',')]
54-
return vec
61+
return cls([float(v) for v in value[1:-1].split(',')])
5562

5663
@classmethod
5764
def from_binary(cls, value: bytes) -> Vector:
@@ -64,7 +71,9 @@ def from_binary(cls, value: bytes) -> Vector:
6471
raise ValueError('expected unused to be 0')
6572

6673
vec = cls.__new__(cls)
67-
vec._value = list(struct.unpack_from(f'>{dim}f', value[4:]))
74+
vec._value = array.array('f', value[4:])
75+
if sys.byteorder != 'big':
76+
vec._value.byteswap()
6877
return vec
6978

7079
@classmethod
@@ -86,16 +95,6 @@ def _to_db_binary(cls, value: object) -> bytes | None:
8695
if value is None:
8796
return value
8897

89-
# fast path for NumPy
90-
if NUMPY_AVAILABLE and isinstance(value, np.ndarray):
91-
if value.ndim != 1:
92-
raise ValueError('expected ndim to be 1')
93-
94-
if value.dtype != '>f4':
95-
value = np.asarray(value, dtype='>f4')
96-
97-
return struct.pack('>HH', len(value), 0) + value.tobytes()
98-
9998
if not isinstance(value, cls):
10099
value = cls(value) # type: ignore
101100

0 commit comments

Comments
 (0)