Skip to content

Commit 0b8fbd7

Browse files
committed
Switched to list for internal storage format for Vector and HalfVector [skip ci]
1 parent b07fdb1 commit 0b8fbd7

4 files changed

Lines changed: 38 additions & 54 deletions

File tree

pgvector/halfvec.py

Lines changed: 15 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
from __future__ import annotations
22
import struct
3-
from typing import cast
43

54
try:
65
import numpy as np
@@ -12,50 +11,47 @@
1211
class HalfVector:
1312
def __init__(self, value: list[float] | np.ndarray[tuple[int], np.dtype[np.floating]]) -> None:
1413
if isinstance(value, list):
15-
dim = len(value)
1614
try:
17-
self._value = struct.pack(f'>HH{dim}e', dim, 0, *value)
18-
except struct.error:
15+
self._value = [float(v) for v in value]
16+
except (TypeError, ValueError):
1917
raise ValueError('expected list[float]')
2018
elif NUMPY_AVAILABLE and isinstance(value, np.ndarray):
2119
if value.ndim != 1:
2220
raise ValueError('expected ndim to be 1')
2321

24-
# asarray still copies if same dtype
25-
if value.dtype != '>f2':
26-
value = np.asarray(value, dtype='>f2')
27-
28-
self._value = struct.pack('>HH', value.shape[0], 0) + value.tobytes()
22+
self._value = [float(v) for v in value]
2923
else:
3024
raise ValueError('expected list or ndarray')
3125

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

3529
def __eq__(self, other: object) -> bool:
3630
if isinstance(other, self.__class__):
37-
return self.to_binary() == other.to_binary()
31+
return self._value == other._value
3832
return False
3933

4034
def dimensions(self) -> int:
41-
dim, = cast(tuple[int], struct.unpack_from('>H', self._value))
42-
return dim
35+
return len(self._value)
4336

4437
def to_list(self) -> list[float]:
45-
return list(struct.unpack_from(f'>{self.dimensions()}e', self._value[4:]))
38+
return self._value
4639

4740
def to_numpy(self) -> np.ndarray[tuple[int], np.dtype[np.float16]]:
48-
return np.frombuffer(self._value, dtype='>f2', count=self.dimensions(), offset=4)
41+
return np.array(self._value, dtype=np.float16)
4942

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

5346
def to_binary(self) -> bytes:
54-
return self._value
47+
dim = len(self._value)
48+
return struct.pack(f'>HH{dim}e', dim, 0, *self._value)
5549

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

6056
@classmethod
6157
def from_binary(cls, value: bytes) -> HalfVector:
@@ -68,7 +64,7 @@ def from_binary(cls, value: bytes) -> HalfVector:
6864
raise ValueError('expected unused to be 0')
6965

7066
vec = cls.__new__(cls)
71-
vec._value = value
67+
vec._value = list(struct.unpack_from(f'>{dim}e', value[4:]))
7268
return vec
7369

7470
@classmethod

pgvector/vector.py

Lines changed: 15 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
from __future__ import annotations
22
import struct
3-
from typing import cast
43

54
try:
65
import numpy as np
@@ -12,50 +11,47 @@
1211
class Vector:
1312
def __init__(self, value: list[float] | np.ndarray[tuple[int], np.dtype[np.floating]]) -> None:
1413
if isinstance(value, list):
15-
dim = len(value)
1614
try:
17-
self._value = struct.pack(f'>HH{dim}f', dim, 0, *value)
18-
except struct.error:
15+
self._value = [float(v) for v in value]
16+
except (TypeError, ValueError):
1917
raise ValueError('expected list[float]')
2018
elif NUMPY_AVAILABLE and isinstance(value, np.ndarray):
2119
if value.ndim != 1:
2220
raise ValueError('expected ndim to be 1')
2321

24-
# asarray still copies if same dtype
25-
if value.dtype != '>f4':
26-
value = np.asarray(value, dtype='>f4')
27-
28-
self._value = struct.pack('>HH', value.shape[0], 0) + value.tobytes()
22+
self._value = [float(v) for v in value]
2923
else:
3024
raise ValueError('expected list or ndarray')
3125

3226
def __repr__(self) -> str:
33-
return f'Vector({self.to_list()})'
27+
return f'Vector({self._value})'
3428

3529
def __eq__(self, other: object) -> bool:
3630
if isinstance(other, self.__class__):
37-
return self.to_binary() == other.to_binary()
31+
return self._value == other._value
3832
return False
3933

4034
def dimensions(self) -> int:
41-
dim, = cast(tuple[int], struct.unpack_from('>H', self._value))
42-
return dim
35+
return len(self._value)
4336

4437
def to_list(self) -> list[float]:
45-
return list(struct.unpack_from(f'>{self.dimensions()}f', self._value[4:]))
38+
return self._value
4639

4740
def to_numpy(self) -> np.ndarray[tuple[int], np.dtype[np.float32]]:
48-
return np.frombuffer(self._value, dtype='>f4', count=self.dimensions(), offset=4)
41+
return np.array(self._value, dtype=np.float32)
4942

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

5346
def to_binary(self) -> bytes:
54-
return self._value
47+
dim = len(self._value)
48+
return struct.pack(f'>HH{dim}f', dim, 0, *self._value)
5549

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

6056
@classmethod
6157
def from_binary(cls, value: bytes) -> Vector:
@@ -68,7 +64,7 @@ def from_binary(cls, value: bytes) -> Vector:
6864
raise ValueError('expected unused to be 0')
6965

7066
vec = cls.__new__(cls)
71-
vec._value = value
67+
vec._value = list(struct.unpack_from(f'>{dim}f', value[4:]))
7268
return vec
7369

7470
@classmethod

tests/test_half_vector.py

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@
1111

1212
class TestHalfVector:
1313
def test_list(self) -> None:
14-
assert HalfVector([1, 2, 3]).to_list() == [1, 2, 3]
14+
arr = [1.0, 2.0, 3.0]
15+
assert HalfVector(arr).to_list() == arr
16+
assert HalfVector(arr).to_list() is not arr
1517

1618
def test_list_empty(self) -> None:
1719
assert HalfVector([]).to_list() == []
@@ -31,6 +33,7 @@ def test_ndarray(self) -> None:
3133
arr = np.array([1, 2, 3])
3234
assert HalfVector(arr).to_list() == [1, 2, 3]
3335
assert HalfVector(arr).to_numpy() is not arr
36+
assert HalfVector(arr).to_numpy().dtype == np.float16
3437

3538
def test_int(self) -> None:
3639
with pytest.raises(ValueError) as error:
@@ -48,13 +51,6 @@ def test_equality(self) -> None:
4851
def test_dimensions(self) -> None:
4952
assert HalfVector([1, 2, 3]).dimensions() == 3
5053

51-
@pytest.mark.skipif(not NUMPY_AVAILABLE, reason='NumPy required')
52-
def test_to_numpy_readonly(self) -> None:
53-
arr = HalfVector([1, 2, 3]).to_numpy()
54-
with pytest.raises(ValueError) as error:
55-
arr[0] = 4
56-
assert str(error.value) == 'assignment destination is read-only'
57-
5854
def test_from_text(self) -> None:
5955
vec = HalfVector.from_text('[1.5,2,3]')
6056
assert vec.to_list() == [1.5, 2, 3]

tests/test_vector.py

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@
1111

1212
class TestVector:
1313
def test_list(self) -> None:
14-
assert Vector([1, 2, 3]).to_list() == [1, 2, 3]
14+
arr = [1.0, 2.0, 3.0]
15+
assert Vector(arr).to_list() == arr
16+
assert Vector(arr).to_list() is not arr
1517

1618
def test_list_empty(self) -> None:
1719
assert Vector([]).to_list() == []
@@ -31,6 +33,7 @@ def test_ndarray(self) -> None:
3133
arr = np.array([1, 2, 3])
3234
assert Vector(arr).to_list() == [1, 2, 3]
3335
assert Vector(arr).to_numpy() is not arr
36+
assert Vector(arr).to_numpy().dtype == np.float32
3437

3538
def test_int(self) -> None:
3639
with pytest.raises(ValueError) as error:
@@ -48,13 +51,6 @@ def test_equality(self) -> None:
4851
def test_dimensions(self) -> None:
4952
assert Vector([1, 2, 3]).dimensions() == 3
5053

51-
@pytest.mark.skipif(not NUMPY_AVAILABLE, reason='NumPy required')
52-
def test_to_numpy_readonly(self) -> None:
53-
arr = Vector([1, 2, 3]).to_numpy()
54-
with pytest.raises(ValueError) as error:
55-
arr[0] = 4
56-
assert str(error.value) == 'assignment destination is read-only'
57-
5854
def test_from_text(self) -> None:
5955
vec = Vector.from_text('[1.5,2,3]')
6056
assert vec.to_list() == [1.5, 2, 3]

0 commit comments

Comments
 (0)