Restore 0.1.5 version from stash
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
from .register import register_vector
|
||||
from ..utils import Vector, HalfVector, SparseVector
|
||||
|
||||
__all__ = [
|
||||
'register_vector',
|
||||
'Vector',
|
||||
'HalfVector',
|
||||
'SparseVector'
|
||||
]
|
||||
@@ -0,0 +1,31 @@
|
||||
from ..utils import Vector, HalfVector, SparseVector
|
||||
|
||||
|
||||
async def register_vector(conn, schema='public'):
|
||||
await conn.set_type_codec(
|
||||
'vector',
|
||||
schema=schema,
|
||||
encoder=Vector._to_db_binary,
|
||||
decoder=Vector._from_db_binary,
|
||||
format='binary'
|
||||
)
|
||||
|
||||
try:
|
||||
await conn.set_type_codec(
|
||||
'halfvec',
|
||||
schema=schema,
|
||||
encoder=HalfVector._to_db_binary,
|
||||
decoder=HalfVector._from_db_binary,
|
||||
format='binary'
|
||||
)
|
||||
|
||||
await conn.set_type_codec(
|
||||
'sparsevec',
|
||||
schema=schema,
|
||||
encoder=SparseVector._to_db_binary,
|
||||
decoder=SparseVector._from_db_binary,
|
||||
format='binary'
|
||||
)
|
||||
except ValueError as e:
|
||||
if not str(e).startswith('unknown type:'):
|
||||
raise e
|
||||
@@ -0,0 +1,26 @@
|
||||
from .bit import BitField
|
||||
from .extensions import VectorExtension
|
||||
from .functions import L2Distance, MaxInnerProduct, CosineDistance, L1Distance, HammingDistance, JaccardDistance
|
||||
from .halfvec import HalfVectorField
|
||||
from .indexes import IvfflatIndex, HnswIndex
|
||||
from .sparsevec import SparseVectorField
|
||||
from .vector import VectorField
|
||||
from ..utils import HalfVector, SparseVector
|
||||
|
||||
__all__ = [
|
||||
'VectorExtension',
|
||||
'VectorField',
|
||||
'HalfVectorField',
|
||||
'BitField',
|
||||
'SparseVectorField',
|
||||
'IvfflatIndex',
|
||||
'HnswIndex',
|
||||
'L2Distance',
|
||||
'MaxInnerProduct',
|
||||
'CosineDistance',
|
||||
'L1Distance',
|
||||
'HammingDistance',
|
||||
'JaccardDistance',
|
||||
'HalfVector',
|
||||
'SparseVector'
|
||||
]
|
||||
@@ -0,0 +1,32 @@
|
||||
from django import forms
|
||||
from django.db.models import Field
|
||||
|
||||
|
||||
# https://docs.djangoproject.com/en/5.0/howto/custom-model-fields/
|
||||
class BitField(Field):
|
||||
description = 'Bit string'
|
||||
|
||||
def __init__(self, *args, length=None, **kwargs):
|
||||
self.length = length
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def deconstruct(self):
|
||||
name, path, args, kwargs = super().deconstruct()
|
||||
if self.length is not None:
|
||||
kwargs['length'] = self.length
|
||||
return name, path, args, kwargs
|
||||
|
||||
def db_type(self, connection):
|
||||
if self.length is None:
|
||||
return 'bit'
|
||||
return 'bit(%d)' % self.length
|
||||
|
||||
def formfield(self, **kwargs):
|
||||
return super().formfield(form_class=BitFormField, **kwargs)
|
||||
|
||||
|
||||
class BitFormField(forms.CharField):
|
||||
def to_python(self, value):
|
||||
if isinstance(value, str) and value == '':
|
||||
return None
|
||||
return super().to_python(value)
|
||||
@@ -0,0 +1,6 @@
|
||||
from django.contrib.postgres.operations import CreateExtension
|
||||
|
||||
|
||||
class VectorExtension(CreateExtension):
|
||||
def __init__(self):
|
||||
self.name = 'vector'
|
||||
@@ -0,0 +1,55 @@
|
||||
from django.db.models import FloatField, Func, Value
|
||||
from ..utils import Vector, HalfVector, SparseVector
|
||||
|
||||
|
||||
class DistanceBase(Func):
|
||||
output_field = FloatField()
|
||||
|
||||
def __init__(self, expression, vector, **extra):
|
||||
if not hasattr(vector, 'resolve_expression'):
|
||||
if isinstance(vector, HalfVector):
|
||||
vector = Value(HalfVector._to_db(vector))
|
||||
elif isinstance(vector, SparseVector):
|
||||
vector = Value(SparseVector._to_db(vector))
|
||||
else:
|
||||
vector = Value(Vector._to_db(vector))
|
||||
super().__init__(expression, vector, **extra)
|
||||
|
||||
|
||||
class BitDistanceBase(Func):
|
||||
output_field = FloatField()
|
||||
|
||||
def __init__(self, expression, vector, **extra):
|
||||
if not hasattr(vector, 'resolve_expression'):
|
||||
vector = Value(vector)
|
||||
super().__init__(expression, vector, **extra)
|
||||
|
||||
|
||||
class L2Distance(DistanceBase):
|
||||
function = ''
|
||||
arg_joiner = ' <-> '
|
||||
|
||||
|
||||
class MaxInnerProduct(DistanceBase):
|
||||
function = ''
|
||||
arg_joiner = ' <#> '
|
||||
|
||||
|
||||
class CosineDistance(DistanceBase):
|
||||
function = ''
|
||||
arg_joiner = ' <=> '
|
||||
|
||||
|
||||
class L1Distance(DistanceBase):
|
||||
function = ''
|
||||
arg_joiner = ' <+> '
|
||||
|
||||
|
||||
class HammingDistance(BitDistanceBase):
|
||||
function = ''
|
||||
arg_joiner = ' <~> '
|
||||
|
||||
|
||||
class JaccardDistance(BitDistanceBase):
|
||||
function = ''
|
||||
arg_joiner = ' <%%> '
|
||||
@@ -0,0 +1,60 @@
|
||||
from django import forms
|
||||
from django.db.models import Field
|
||||
from ..utils import HalfVector
|
||||
|
||||
|
||||
# https://docs.djangoproject.com/en/5.0/howto/custom-model-fields/
|
||||
class HalfVectorField(Field):
|
||||
description = 'Half vector'
|
||||
empty_strings_allowed = False
|
||||
|
||||
def __init__(self, *args, dimensions=None, **kwargs):
|
||||
self.dimensions = dimensions
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def deconstruct(self):
|
||||
name, path, args, kwargs = super().deconstruct()
|
||||
if self.dimensions is not None:
|
||||
kwargs['dimensions'] = self.dimensions
|
||||
return name, path, args, kwargs
|
||||
|
||||
def db_type(self, connection):
|
||||
if self.dimensions is None:
|
||||
return 'halfvec'
|
||||
return 'halfvec(%d)' % self.dimensions
|
||||
|
||||
def from_db_value(self, value, expression, connection):
|
||||
return HalfVector._from_db(value)
|
||||
|
||||
def to_python(self, value):
|
||||
if value is None or isinstance(value, HalfVector):
|
||||
return value
|
||||
elif isinstance(value, str):
|
||||
return HalfVector._from_db(value)
|
||||
else:
|
||||
return HalfVector(value)
|
||||
|
||||
def get_prep_value(self, value):
|
||||
return HalfVector._to_db(value)
|
||||
|
||||
def value_to_string(self, obj):
|
||||
return self.get_prep_value(self.value_from_object(obj))
|
||||
|
||||
def formfield(self, **kwargs):
|
||||
return super().formfield(form_class=HalfVectorFormField, **kwargs)
|
||||
|
||||
|
||||
class HalfVectorWidget(forms.TextInput):
|
||||
def format_value(self, value):
|
||||
if isinstance(value, HalfVector):
|
||||
value = value.to_list()
|
||||
return super().format_value(value)
|
||||
|
||||
|
||||
class HalfVectorFormField(forms.CharField):
|
||||
widget = HalfVectorWidget
|
||||
|
||||
def to_python(self, value):
|
||||
if isinstance(value, str) and value == '':
|
||||
return None
|
||||
return super().to_python(value)
|
||||
@@ -0,0 +1,46 @@
|
||||
from django.contrib.postgres.indexes import PostgresIndex
|
||||
|
||||
|
||||
class IvfflatIndex(PostgresIndex):
|
||||
suffix = 'ivfflat'
|
||||
|
||||
def __init__(self, *expressions, lists=None, **kwargs):
|
||||
self.lists = lists
|
||||
super().__init__(*expressions, **kwargs)
|
||||
|
||||
def deconstruct(self):
|
||||
path, args, kwargs = super().deconstruct()
|
||||
if self.lists is not None:
|
||||
kwargs['lists'] = self.lists
|
||||
return path, args, kwargs
|
||||
|
||||
def get_with_params(self):
|
||||
with_params = []
|
||||
if self.lists is not None:
|
||||
with_params.append('lists = %d' % self.lists)
|
||||
return with_params
|
||||
|
||||
|
||||
class HnswIndex(PostgresIndex):
|
||||
suffix = 'hnsw'
|
||||
|
||||
def __init__(self, *expressions, m=None, ef_construction=None, **kwargs):
|
||||
self.m = m
|
||||
self.ef_construction = ef_construction
|
||||
super().__init__(*expressions, **kwargs)
|
||||
|
||||
def deconstruct(self):
|
||||
path, args, kwargs = super().deconstruct()
|
||||
if self.m is not None:
|
||||
kwargs['m'] = self.m
|
||||
if self.ef_construction is not None:
|
||||
kwargs['ef_construction'] = self.ef_construction
|
||||
return path, args, kwargs
|
||||
|
||||
def get_with_params(self):
|
||||
with_params = []
|
||||
if self.m is not None:
|
||||
with_params.append('m = %d' % self.m)
|
||||
if self.ef_construction is not None:
|
||||
with_params.append('ef_construction = %d' % self.ef_construction)
|
||||
return with_params
|
||||
@@ -0,0 +1,55 @@
|
||||
from django import forms
|
||||
from django.db.models import Field
|
||||
from ..utils import SparseVector
|
||||
|
||||
|
||||
# https://docs.djangoproject.com/en/5.0/howto/custom-model-fields/
|
||||
class SparseVectorField(Field):
|
||||
description = 'Sparse vector'
|
||||
empty_strings_allowed = False
|
||||
|
||||
def __init__(self, *args, dimensions=None, **kwargs):
|
||||
self.dimensions = dimensions
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def deconstruct(self):
|
||||
name, path, args, kwargs = super().deconstruct()
|
||||
if self.dimensions is not None:
|
||||
kwargs['dimensions'] = self.dimensions
|
||||
return name, path, args, kwargs
|
||||
|
||||
def db_type(self, connection):
|
||||
if self.dimensions is None:
|
||||
return 'sparsevec'
|
||||
return 'sparsevec(%d)' % self.dimensions
|
||||
|
||||
def from_db_value(self, value, expression, connection):
|
||||
return SparseVector._from_db(value)
|
||||
|
||||
def to_python(self, value):
|
||||
return SparseVector._from_db(value)
|
||||
|
||||
def get_prep_value(self, value):
|
||||
return SparseVector._to_db(value)
|
||||
|
||||
def value_to_string(self, obj):
|
||||
return self.get_prep_value(self.value_from_object(obj))
|
||||
|
||||
def formfield(self, **kwargs):
|
||||
return super().formfield(form_class=SparseVectorFormField, **kwargs)
|
||||
|
||||
|
||||
class SparseVectorWidget(forms.TextInput):
|
||||
def format_value(self, value):
|
||||
if isinstance(value, SparseVector):
|
||||
value = value.to_text()
|
||||
return super().format_value(value)
|
||||
|
||||
|
||||
class SparseVectorFormField(forms.CharField):
|
||||
widget = SparseVectorWidget
|
||||
|
||||
def to_python(self, value):
|
||||
if isinstance(value, str) and value == '':
|
||||
return None
|
||||
return super().to_python(value)
|
||||
@@ -0,0 +1,73 @@
|
||||
from django import forms
|
||||
from django.db.models import Field
|
||||
import numpy as np
|
||||
from ..utils import Vector
|
||||
|
||||
|
||||
# https://docs.djangoproject.com/en/5.0/howto/custom-model-fields/
|
||||
class VectorField(Field):
|
||||
description = 'Vector'
|
||||
empty_strings_allowed = False
|
||||
|
||||
def __init__(self, *args, dimensions=None, **kwargs):
|
||||
self.dimensions = dimensions
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def deconstruct(self):
|
||||
name, path, args, kwargs = super().deconstruct()
|
||||
if self.dimensions is not None:
|
||||
kwargs['dimensions'] = self.dimensions
|
||||
return name, path, args, kwargs
|
||||
|
||||
def db_type(self, connection):
|
||||
if self.dimensions is None:
|
||||
return 'vector'
|
||||
return 'vector(%d)' % self.dimensions
|
||||
|
||||
def from_db_value(self, value, expression, connection):
|
||||
return Vector._from_db(value)
|
||||
|
||||
def to_python(self, value):
|
||||
if isinstance(value, list):
|
||||
return np.array(value, dtype=np.float32)
|
||||
return Vector._from_db(value)
|
||||
|
||||
def get_prep_value(self, value):
|
||||
return Vector._to_db(value)
|
||||
|
||||
def value_to_string(self, obj):
|
||||
return self.get_prep_value(self.value_from_object(obj))
|
||||
|
||||
def validate(self, value, model_instance):
|
||||
if isinstance(value, np.ndarray):
|
||||
value = value.tolist()
|
||||
super().validate(value, model_instance)
|
||||
|
||||
def run_validators(self, value):
|
||||
if isinstance(value, np.ndarray):
|
||||
value = value.tolist()
|
||||
super().run_validators(value)
|
||||
|
||||
def formfield(self, **kwargs):
|
||||
return super().formfield(form_class=VectorFormField, **kwargs)
|
||||
|
||||
|
||||
class VectorWidget(forms.TextInput):
|
||||
def format_value(self, value):
|
||||
if isinstance(value, np.ndarray):
|
||||
value = value.tolist()
|
||||
return super().format_value(value)
|
||||
|
||||
|
||||
class VectorFormField(forms.CharField):
|
||||
widget = VectorWidget
|
||||
|
||||
def has_changed(self, initial, data):
|
||||
if isinstance(initial, np.ndarray):
|
||||
initial = initial.tolist()
|
||||
return super().has_changed(initial, data)
|
||||
|
||||
def to_python(self, value):
|
||||
if isinstance(value, str) and value == '':
|
||||
return None
|
||||
return super().to_python(value)
|
||||
@@ -0,0 +1,14 @@
|
||||
from .bit import FixedBitField
|
||||
from .halfvec import HalfVectorField
|
||||
from .sparsevec import SparseVectorField
|
||||
from .vector import VectorField
|
||||
from ..utils import HalfVector, SparseVector
|
||||
|
||||
__all__ = [
|
||||
'VectorField',
|
||||
'HalfVectorField',
|
||||
'FixedBitField',
|
||||
'SparseVectorField',
|
||||
'HalfVector',
|
||||
'SparseVector'
|
||||
]
|
||||
@@ -0,0 +1,21 @@
|
||||
from peewee import Expression, Field
|
||||
|
||||
|
||||
class FixedBitField(Field):
|
||||
field_type = 'bit'
|
||||
|
||||
def __init__(self, max_length=None, *args, **kwargs):
|
||||
self.max_length = max_length
|
||||
super(FixedBitField, self).__init__(*args, **kwargs)
|
||||
|
||||
def get_modifiers(self):
|
||||
return self.max_length and [self.max_length] or None
|
||||
|
||||
def _distance(self, op, vector):
|
||||
return Expression(lhs=self, op=op, rhs=self.to_value(vector))
|
||||
|
||||
def hamming_distance(self, vector):
|
||||
return self._distance('<~>', vector)
|
||||
|
||||
def jaccard_distance(self, vector):
|
||||
return self._distance('<%%>', vector)
|
||||
@@ -0,0 +1,34 @@
|
||||
from peewee import Expression, Field
|
||||
from ..utils import HalfVector
|
||||
|
||||
|
||||
class HalfVectorField(Field):
|
||||
field_type = 'halfvec'
|
||||
|
||||
def __init__(self, dimensions=None, *args, **kwargs):
|
||||
self.dimensions = dimensions
|
||||
super(HalfVectorField, self).__init__(*args, **kwargs)
|
||||
|
||||
def get_modifiers(self):
|
||||
return self.dimensions and [self.dimensions] or None
|
||||
|
||||
def db_value(self, value):
|
||||
return HalfVector._to_db(value)
|
||||
|
||||
def python_value(self, value):
|
||||
return HalfVector._from_db(value)
|
||||
|
||||
def _distance(self, op, vector):
|
||||
return Expression(lhs=self, op=op, rhs=self.to_value(vector))
|
||||
|
||||
def l2_distance(self, vector):
|
||||
return self._distance('<->', vector)
|
||||
|
||||
def max_inner_product(self, vector):
|
||||
return self._distance('<#>', vector)
|
||||
|
||||
def cosine_distance(self, vector):
|
||||
return self._distance('<=>', vector)
|
||||
|
||||
def l1_distance(self, vector):
|
||||
return self._distance('<+>', vector)
|
||||
@@ -0,0 +1,34 @@
|
||||
from peewee import Expression, Field
|
||||
from ..utils import SparseVector
|
||||
|
||||
|
||||
class SparseVectorField(Field):
|
||||
field_type = 'sparsevec'
|
||||
|
||||
def __init__(self, dimensions=None, *args, **kwargs):
|
||||
self.dimensions = dimensions
|
||||
super(SparseVectorField, self).__init__(*args, **kwargs)
|
||||
|
||||
def get_modifiers(self):
|
||||
return self.dimensions and [self.dimensions] or None
|
||||
|
||||
def db_value(self, value):
|
||||
return SparseVector._to_db(value)
|
||||
|
||||
def python_value(self, value):
|
||||
return SparseVector._from_db(value)
|
||||
|
||||
def _distance(self, op, vector):
|
||||
return Expression(lhs=self, op=op, rhs=self.to_value(vector))
|
||||
|
||||
def l2_distance(self, vector):
|
||||
return self._distance('<->', vector)
|
||||
|
||||
def max_inner_product(self, vector):
|
||||
return self._distance('<#>', vector)
|
||||
|
||||
def cosine_distance(self, vector):
|
||||
return self._distance('<=>', vector)
|
||||
|
||||
def l1_distance(self, vector):
|
||||
return self._distance('<+>', vector)
|
||||
@@ -0,0 +1,34 @@
|
||||
from peewee import Expression, Field
|
||||
from ..utils import Vector
|
||||
|
||||
|
||||
class VectorField(Field):
|
||||
field_type = 'vector'
|
||||
|
||||
def __init__(self, dimensions=None, *args, **kwargs):
|
||||
self.dimensions = dimensions
|
||||
super(VectorField, self).__init__(*args, **kwargs)
|
||||
|
||||
def get_modifiers(self):
|
||||
return self.dimensions and [self.dimensions] or None
|
||||
|
||||
def db_value(self, value):
|
||||
return Vector._to_db(value)
|
||||
|
||||
def python_value(self, value):
|
||||
return Vector._from_db(value)
|
||||
|
||||
def _distance(self, op, vector):
|
||||
return Expression(lhs=self, op=op, rhs=self.to_value(vector))
|
||||
|
||||
def l2_distance(self, vector):
|
||||
return self._distance('<->', vector)
|
||||
|
||||
def max_inner_product(self, vector):
|
||||
return self._distance('<#>', vector)
|
||||
|
||||
def cosine_distance(self, vector):
|
||||
return self._distance('<=>', vector)
|
||||
|
||||
def l1_distance(self, vector):
|
||||
return self._distance('<+>', vector)
|
||||
@@ -0,0 +1,11 @@
|
||||
from .register import register_vector, register_vector_async
|
||||
from ..utils import Bit, HalfVector, SparseVector, Vector
|
||||
|
||||
__all__ = [
|
||||
'register_vector',
|
||||
'register_vector_async',
|
||||
'Vector',
|
||||
'HalfVector',
|
||||
'Bit',
|
||||
'SparseVector'
|
||||
]
|
||||
@@ -0,0 +1,31 @@
|
||||
from psycopg.adapt import Dumper
|
||||
from psycopg.pq import Format
|
||||
from ..utils import Bit
|
||||
|
||||
|
||||
class BitDumper(Dumper):
|
||||
|
||||
format = Format.TEXT
|
||||
|
||||
def dump(self, obj):
|
||||
return Bit._to_db(obj).encode('utf8')
|
||||
|
||||
|
||||
class BitBinaryDumper(BitDumper):
|
||||
|
||||
format = Format.BINARY
|
||||
|
||||
def dump(self, obj):
|
||||
return Bit._to_db_binary(obj)
|
||||
|
||||
|
||||
def register_bit_info(context, info):
|
||||
info.register(context)
|
||||
|
||||
# add oid to anonymous class for set_types
|
||||
text_dumper = type('', (BitDumper,), {'oid': info.oid})
|
||||
binary_dumper = type('', (BitBinaryDumper,), {'oid': info.oid})
|
||||
|
||||
adapters = context.adapters
|
||||
adapters.register_dumper(Bit, text_dumper)
|
||||
adapters.register_dumper(Bit, binary_dumper)
|
||||
@@ -0,0 +1,53 @@
|
||||
from psycopg.adapt import Loader, Dumper
|
||||
from psycopg.pq import Format
|
||||
from ..utils import HalfVector
|
||||
|
||||
|
||||
class HalfVectorDumper(Dumper):
|
||||
|
||||
format = Format.TEXT
|
||||
|
||||
def dump(self, obj):
|
||||
return HalfVector._to_db(obj).encode('utf8')
|
||||
|
||||
|
||||
class HalfVectorBinaryDumper(HalfVectorDumper):
|
||||
|
||||
format = Format.BINARY
|
||||
|
||||
def dump(self, obj):
|
||||
return HalfVector._to_db_binary(obj)
|
||||
|
||||
|
||||
class HalfVectorLoader(Loader):
|
||||
|
||||
format = Format.TEXT
|
||||
|
||||
def load(self, data):
|
||||
if isinstance(data, memoryview):
|
||||
data = bytes(data)
|
||||
return HalfVector._from_db(data.decode('utf8'))
|
||||
|
||||
|
||||
class HalfVectorBinaryLoader(HalfVectorLoader):
|
||||
|
||||
format = Format.BINARY
|
||||
|
||||
def load(self, data):
|
||||
if isinstance(data, memoryview):
|
||||
data = bytes(data)
|
||||
return HalfVector._from_db_binary(data)
|
||||
|
||||
|
||||
def register_halfvec_info(context, info):
|
||||
info.register(context)
|
||||
|
||||
# add oid to anonymous class for set_types
|
||||
text_dumper = type('', (HalfVectorDumper,), {'oid': info.oid})
|
||||
binary_dumper = type('', (HalfVectorBinaryDumper,), {'oid': info.oid})
|
||||
|
||||
adapters = context.adapters
|
||||
adapters.register_dumper(HalfVector, text_dumper)
|
||||
adapters.register_dumper(HalfVector, binary_dumper)
|
||||
adapters.register_loader(info.oid, HalfVectorLoader)
|
||||
adapters.register_loader(info.oid, HalfVectorBinaryLoader)
|
||||
@@ -0,0 +1,37 @@
|
||||
from psycopg.types import TypeInfo
|
||||
from .bit import register_bit_info
|
||||
from .halfvec import register_halfvec_info
|
||||
from .sparsevec import register_sparsevec_info
|
||||
from .vector import register_vector_info
|
||||
|
||||
|
||||
def register_vector(context):
|
||||
info = TypeInfo.fetch(context, 'vector')
|
||||
register_vector_info(context, info)
|
||||
|
||||
info = TypeInfo.fetch(context, 'bit')
|
||||
register_bit_info(context, info)
|
||||
|
||||
info = TypeInfo.fetch(context, 'halfvec')
|
||||
if info is not None:
|
||||
register_halfvec_info(context, info)
|
||||
|
||||
info = TypeInfo.fetch(context, 'sparsevec')
|
||||
if info is not None:
|
||||
register_sparsevec_info(context, info)
|
||||
|
||||
|
||||
async def register_vector_async(context):
|
||||
info = await TypeInfo.fetch(context, 'vector')
|
||||
register_vector_info(context, info)
|
||||
|
||||
info = await TypeInfo.fetch(context, 'bit')
|
||||
register_bit_info(context, info)
|
||||
|
||||
info = await TypeInfo.fetch(context, 'halfvec')
|
||||
if info is not None:
|
||||
register_halfvec_info(context, info)
|
||||
|
||||
info = await TypeInfo.fetch(context, 'sparsevec')
|
||||
if info is not None:
|
||||
register_sparsevec_info(context, info)
|
||||
@@ -0,0 +1,53 @@
|
||||
from psycopg.adapt import Loader, Dumper
|
||||
from psycopg.pq import Format
|
||||
from ..utils import SparseVector
|
||||
|
||||
|
||||
class SparseVectorDumper(Dumper):
|
||||
|
||||
format = Format.TEXT
|
||||
|
||||
def dump(self, obj):
|
||||
return SparseVector._to_db(obj).encode('utf8')
|
||||
|
||||
|
||||
class SparseVectorBinaryDumper(SparseVectorDumper):
|
||||
|
||||
format = Format.BINARY
|
||||
|
||||
def dump(self, obj):
|
||||
return SparseVector._to_db_binary(obj)
|
||||
|
||||
|
||||
class SparseVectorLoader(Loader):
|
||||
|
||||
format = Format.TEXT
|
||||
|
||||
def load(self, data):
|
||||
if isinstance(data, memoryview):
|
||||
data = bytes(data)
|
||||
return SparseVector._from_db(data.decode('utf8'))
|
||||
|
||||
|
||||
class SparseVectorBinaryLoader(SparseVectorLoader):
|
||||
|
||||
format = Format.BINARY
|
||||
|
||||
def load(self, data):
|
||||
if isinstance(data, memoryview):
|
||||
data = bytes(data)
|
||||
return SparseVector._from_db_binary(data)
|
||||
|
||||
|
||||
def register_sparsevec_info(context, info):
|
||||
info.register(context)
|
||||
|
||||
# add oid to anonymous class for set_types
|
||||
text_dumper = type('', (SparseVectorDumper,), {'oid': info.oid})
|
||||
binary_dumper = type('', (SparseVectorBinaryDumper,), {'oid': info.oid})
|
||||
|
||||
adapters = context.adapters
|
||||
adapters.register_dumper(SparseVector, text_dumper)
|
||||
adapters.register_dumper(SparseVector, binary_dumper)
|
||||
adapters.register_loader(info.oid, SparseVectorLoader)
|
||||
adapters.register_loader(info.oid, SparseVectorBinaryLoader)
|
||||
@@ -0,0 +1,58 @@
|
||||
import psycopg
|
||||
from psycopg.adapt import Loader, Dumper
|
||||
from psycopg.pq import Format
|
||||
from ..utils import Vector
|
||||
|
||||
|
||||
class VectorDumper(Dumper):
|
||||
|
||||
format = Format.TEXT
|
||||
|
||||
def dump(self, obj):
|
||||
return Vector._to_db(obj).encode('utf8')
|
||||
|
||||
|
||||
class VectorBinaryDumper(VectorDumper):
|
||||
|
||||
format = Format.BINARY
|
||||
|
||||
def dump(self, obj):
|
||||
return Vector._to_db_binary(obj)
|
||||
|
||||
|
||||
class VectorLoader(Loader):
|
||||
|
||||
format = Format.TEXT
|
||||
|
||||
def load(self, data):
|
||||
if isinstance(data, memoryview):
|
||||
data = bytes(data)
|
||||
return Vector._from_db(data.decode('utf8'))
|
||||
|
||||
|
||||
class VectorBinaryLoader(VectorLoader):
|
||||
|
||||
format = Format.BINARY
|
||||
|
||||
def load(self, data):
|
||||
if isinstance(data, memoryview):
|
||||
data = bytes(data)
|
||||
return Vector._from_db_binary(data)
|
||||
|
||||
|
||||
def register_vector_info(context, info):
|
||||
if info is None:
|
||||
raise psycopg.ProgrammingError('vector type not found in the database')
|
||||
info.register(context)
|
||||
|
||||
# add oid to anonymous class for set_types
|
||||
text_dumper = type('', (VectorDumper,), {'oid': info.oid})
|
||||
binary_dumper = type('', (VectorBinaryDumper,), {'oid': info.oid})
|
||||
|
||||
adapters = context.adapters
|
||||
adapters.register_dumper('numpy.ndarray', text_dumper)
|
||||
adapters.register_dumper('numpy.ndarray', binary_dumper)
|
||||
adapters.register_dumper(Vector, text_dumper)
|
||||
adapters.register_dumper(Vector, binary_dumper)
|
||||
adapters.register_loader(info.oid, VectorLoader)
|
||||
adapters.register_loader(info.oid, VectorBinaryLoader)
|
||||
@@ -0,0 +1,8 @@
|
||||
from .register import register_vector
|
||||
from ..utils import HalfVector, SparseVector
|
||||
|
||||
__all__ = [
|
||||
'register_vector',
|
||||
'HalfVector',
|
||||
'SparseVector'
|
||||
]
|
||||
@@ -0,0 +1,20 @@
|
||||
from psycopg2.extensions import adapt, new_type, register_adapter, register_type
|
||||
from ..utils import HalfVector
|
||||
|
||||
|
||||
class HalfvecAdapter:
|
||||
def __init__(self, value):
|
||||
self._value = value
|
||||
|
||||
def getquoted(self):
|
||||
return adapt(HalfVector._to_db(self._value)).getquoted()
|
||||
|
||||
|
||||
def cast_halfvec(value, cur):
|
||||
return HalfVector._from_db(value)
|
||||
|
||||
|
||||
def register_halfvec_info(oid, scope):
|
||||
halfvec = new_type((oid,), 'HALFVEC', cast_halfvec)
|
||||
register_type(halfvec, scope)
|
||||
register_adapter(HalfVector, HalfvecAdapter)
|
||||
@@ -0,0 +1,28 @@
|
||||
import psycopg2
|
||||
from psycopg2.extensions import cursor
|
||||
from .halfvec import register_halfvec_info
|
||||
from .sparsevec import register_sparsevec_info
|
||||
from .vector import register_vector_info
|
||||
|
||||
|
||||
# TODO make globally False by default in 0.4.0
|
||||
# note: register_adapter is always global
|
||||
def register_vector(conn_or_curs=None, globally=True):
|
||||
conn = conn_or_curs if hasattr(conn_or_curs, 'cursor') else conn_or_curs.connection
|
||||
cur = conn.cursor(cursor_factory=cursor)
|
||||
scope = None if globally else conn_or_curs
|
||||
|
||||
# use to_regtype to get first matching type in search path
|
||||
cur.execute("SELECT typname, oid FROM pg_type WHERE oid IN (to_regtype('vector'), to_regtype('halfvec'), to_regtype('sparsevec'))")
|
||||
type_info = dict(cur.fetchall())
|
||||
|
||||
if 'vector' not in type_info:
|
||||
raise psycopg2.ProgrammingError('vector type not found in the database')
|
||||
|
||||
register_vector_info(type_info['vector'], scope)
|
||||
|
||||
if 'halfvec' in type_info:
|
||||
register_halfvec_info(type_info['halfvec'], scope)
|
||||
|
||||
if 'sparsevec' in type_info:
|
||||
register_sparsevec_info(type_info['sparsevec'], scope)
|
||||
@@ -0,0 +1,20 @@
|
||||
from psycopg2.extensions import adapt, new_type, register_adapter, register_type
|
||||
from ..utils import SparseVector
|
||||
|
||||
|
||||
class SparsevecAdapter:
|
||||
def __init__(self, value):
|
||||
self._value = value
|
||||
|
||||
def getquoted(self):
|
||||
return adapt(SparseVector._to_db(self._value)).getquoted()
|
||||
|
||||
|
||||
def cast_sparsevec(value, cur):
|
||||
return SparseVector._from_db(value)
|
||||
|
||||
|
||||
def register_sparsevec_info(oid, scope):
|
||||
sparsevec = new_type((oid,), 'SPARSEVEC', cast_sparsevec)
|
||||
register_type(sparsevec, scope)
|
||||
register_adapter(SparseVector, SparsevecAdapter)
|
||||
@@ -0,0 +1,21 @@
|
||||
import numpy as np
|
||||
from psycopg2.extensions import adapt, new_type, register_adapter, register_type
|
||||
from ..utils import Vector
|
||||
|
||||
|
||||
class VectorAdapter:
|
||||
def __init__(self, value):
|
||||
self._value = value
|
||||
|
||||
def getquoted(self):
|
||||
return adapt(Vector._to_db(self._value)).getquoted()
|
||||
|
||||
|
||||
def cast_vector(value, cur):
|
||||
return Vector._from_db(value)
|
||||
|
||||
|
||||
def register_vector_info(oid, scope):
|
||||
vector = new_type((oid,), 'VECTOR', cast_vector)
|
||||
register_type(vector, scope)
|
||||
register_adapter(np.ndarray, VectorAdapter)
|
||||
@@ -0,0 +1,19 @@
|
||||
from .bit import BIT
|
||||
from .functions import avg, sum
|
||||
from .halfvec import HALFVEC
|
||||
from .sparsevec import SPARSEVEC
|
||||
from .vector import VECTOR
|
||||
from .vector import VECTOR as Vector
|
||||
from ..utils import HalfVector, SparseVector
|
||||
|
||||
__all__ = [
|
||||
'Vector',
|
||||
'VECTOR',
|
||||
'HALFVEC',
|
||||
'BIT',
|
||||
'SPARSEVEC',
|
||||
'HalfVector',
|
||||
'SparseVector',
|
||||
'avg',
|
||||
'sum'
|
||||
]
|
||||
@@ -0,0 +1,26 @@
|
||||
from sqlalchemy.dialects.postgresql.base import ischema_names
|
||||
from sqlalchemy.types import UserDefinedType, Float
|
||||
|
||||
|
||||
class BIT(UserDefinedType):
|
||||
cache_ok = True
|
||||
|
||||
def __init__(self, length=None):
|
||||
super(UserDefinedType, self).__init__()
|
||||
self.length = length
|
||||
|
||||
def get_col_spec(self, **kw):
|
||||
if self.length is None:
|
||||
return 'BIT'
|
||||
return 'BIT(%d)' % self.length
|
||||
|
||||
class comparator_factory(UserDefinedType.Comparator):
|
||||
def hamming_distance(self, other):
|
||||
return self.op('<~>', return_type=Float)(other)
|
||||
|
||||
def jaccard_distance(self, other):
|
||||
return self.op('<%>', return_type=Float)(other)
|
||||
|
||||
|
||||
# for reflection
|
||||
ischema_names['bit'] = BIT
|
||||
@@ -0,0 +1,14 @@
|
||||
# https://docs.sqlalchemy.org/en/20/core/functions.html
|
||||
# include sum for a consistent API
|
||||
from sqlalchemy.sql.functions import ReturnTypeFromArgs, sum
|
||||
|
||||
|
||||
class avg(ReturnTypeFromArgs):
|
||||
inherit_cache = True
|
||||
package = 'pgvector'
|
||||
|
||||
|
||||
__all__ = [
|
||||
'avg',
|
||||
'sum'
|
||||
]
|
||||
@@ -0,0 +1,51 @@
|
||||
from sqlalchemy.dialects.postgresql.base import ischema_names
|
||||
from sqlalchemy.types import UserDefinedType, Float, String
|
||||
from ..utils import HalfVector
|
||||
|
||||
|
||||
class HALFVEC(UserDefinedType):
|
||||
cache_ok = True
|
||||
_string = String()
|
||||
|
||||
def __init__(self, dim=None):
|
||||
super(UserDefinedType, self).__init__()
|
||||
self.dim = dim
|
||||
|
||||
def get_col_spec(self, **kw):
|
||||
if self.dim is None:
|
||||
return 'HALFVEC'
|
||||
return 'HALFVEC(%d)' % self.dim
|
||||
|
||||
def bind_processor(self, dialect):
|
||||
def process(value):
|
||||
return HalfVector._to_db(value, self.dim)
|
||||
return process
|
||||
|
||||
def literal_processor(self, dialect):
|
||||
string_literal_processor = self._string._cached_literal_processor(dialect)
|
||||
|
||||
def process(value):
|
||||
return string_literal_processor(HalfVector._to_db(value, self.dim))
|
||||
return process
|
||||
|
||||
def result_processor(self, dialect, coltype):
|
||||
def process(value):
|
||||
return HalfVector._from_db(value)
|
||||
return process
|
||||
|
||||
class comparator_factory(UserDefinedType.Comparator):
|
||||
def l2_distance(self, other):
|
||||
return self.op('<->', return_type=Float)(other)
|
||||
|
||||
def max_inner_product(self, other):
|
||||
return self.op('<#>', return_type=Float)(other)
|
||||
|
||||
def cosine_distance(self, other):
|
||||
return self.op('<=>', return_type=Float)(other)
|
||||
|
||||
def l1_distance(self, other):
|
||||
return self.op('<+>', return_type=Float)(other)
|
||||
|
||||
|
||||
# for reflection
|
||||
ischema_names['halfvec'] = HALFVEC
|
||||
@@ -0,0 +1,51 @@
|
||||
from sqlalchemy.dialects.postgresql.base import ischema_names
|
||||
from sqlalchemy.types import UserDefinedType, Float, String
|
||||
from ..utils import SparseVector
|
||||
|
||||
|
||||
class SPARSEVEC(UserDefinedType):
|
||||
cache_ok = True
|
||||
_string = String()
|
||||
|
||||
def __init__(self, dim=None):
|
||||
super(UserDefinedType, self).__init__()
|
||||
self.dim = dim
|
||||
|
||||
def get_col_spec(self, **kw):
|
||||
if self.dim is None:
|
||||
return 'SPARSEVEC'
|
||||
return 'SPARSEVEC(%d)' % self.dim
|
||||
|
||||
def bind_processor(self, dialect):
|
||||
def process(value):
|
||||
return SparseVector._to_db(value, self.dim)
|
||||
return process
|
||||
|
||||
def literal_processor(self, dialect):
|
||||
string_literal_processor = self._string._cached_literal_processor(dialect)
|
||||
|
||||
def process(value):
|
||||
return string_literal_processor(SparseVector._to_db(value, self.dim))
|
||||
return process
|
||||
|
||||
def result_processor(self, dialect, coltype):
|
||||
def process(value):
|
||||
return SparseVector._from_db(value)
|
||||
return process
|
||||
|
||||
class comparator_factory(UserDefinedType.Comparator):
|
||||
def l2_distance(self, other):
|
||||
return self.op('<->', return_type=Float)(other)
|
||||
|
||||
def max_inner_product(self, other):
|
||||
return self.op('<#>', return_type=Float)(other)
|
||||
|
||||
def cosine_distance(self, other):
|
||||
return self.op('<=>', return_type=Float)(other)
|
||||
|
||||
def l1_distance(self, other):
|
||||
return self.op('<+>', return_type=Float)(other)
|
||||
|
||||
|
||||
# for reflection
|
||||
ischema_names['sparsevec'] = SPARSEVEC
|
||||
@@ -0,0 +1,51 @@
|
||||
from sqlalchemy.dialects.postgresql.base import ischema_names
|
||||
from sqlalchemy.types import UserDefinedType, Float, String
|
||||
from ..utils import Vector
|
||||
|
||||
|
||||
class VECTOR(UserDefinedType):
|
||||
cache_ok = True
|
||||
_string = String()
|
||||
|
||||
def __init__(self, dim=None):
|
||||
super(UserDefinedType, self).__init__()
|
||||
self.dim = dim
|
||||
|
||||
def get_col_spec(self, **kw):
|
||||
if self.dim is None:
|
||||
return 'VECTOR'
|
||||
return 'VECTOR(%d)' % self.dim
|
||||
|
||||
def bind_processor(self, dialect):
|
||||
def process(value):
|
||||
return Vector._to_db(value, self.dim)
|
||||
return process
|
||||
|
||||
def literal_processor(self, dialect):
|
||||
string_literal_processor = self._string._cached_literal_processor(dialect)
|
||||
|
||||
def process(value):
|
||||
return string_literal_processor(Vector._to_db(value, self.dim))
|
||||
return process
|
||||
|
||||
def result_processor(self, dialect, coltype):
|
||||
def process(value):
|
||||
return Vector._from_db(value)
|
||||
return process
|
||||
|
||||
class comparator_factory(UserDefinedType.Comparator):
|
||||
def l2_distance(self, other):
|
||||
return self.op('<->', return_type=Float)(other)
|
||||
|
||||
def max_inner_product(self, other):
|
||||
return self.op('<#>', return_type=Float)(other)
|
||||
|
||||
def cosine_distance(self, other):
|
||||
return self.op('<=>', return_type=Float)(other)
|
||||
|
||||
def l1_distance(self, other):
|
||||
return self.op('<+>', return_type=Float)(other)
|
||||
|
||||
|
||||
# for reflection
|
||||
ischema_names['vector'] = VECTOR
|
||||
@@ -0,0 +1,11 @@
|
||||
from .bit import Bit
|
||||
from .halfvec import HalfVector
|
||||
from .sparsevec import SparseVector
|
||||
from .vector import Vector
|
||||
|
||||
__all__ = [
|
||||
'Vector',
|
||||
'HalfVector',
|
||||
'Bit',
|
||||
'SparseVector'
|
||||
]
|
||||
@@ -0,0 +1,61 @@
|
||||
import numpy as np
|
||||
from struct import pack, unpack_from
|
||||
|
||||
|
||||
class Bit:
|
||||
def __init__(self, value):
|
||||
if isinstance(value, str):
|
||||
self._value = self.from_text(value)._value
|
||||
else:
|
||||
# TODO change in 0.4.0
|
||||
# TODO raise if dtype not bool or uint8
|
||||
# if isinstance(value, np.ndarray) and value.dtype == np.uint8:
|
||||
# value = np.unpackbits(value)
|
||||
# else:
|
||||
# value = np.asarray(value, dtype=bool)
|
||||
|
||||
value = np.asarray(value, dtype=bool)
|
||||
|
||||
if value.ndim != 1:
|
||||
raise ValueError('expected ndim to be 1')
|
||||
|
||||
self._value = value
|
||||
|
||||
def __repr__(self):
|
||||
return f'Bit({self.to_text()})'
|
||||
|
||||
def to_list(self):
|
||||
return self._value.tolist()
|
||||
|
||||
def to_numpy(self):
|
||||
return self._value
|
||||
|
||||
def to_text(self):
|
||||
return ''.join(self._value.astype(np.uint8).astype(str))
|
||||
|
||||
def to_binary(self):
|
||||
return pack('>i', len(self._value)) + np.packbits(self._value).tobytes()
|
||||
|
||||
@classmethod
|
||||
def from_text(cls, value):
|
||||
return cls(np.asarray([v != '0' for v in value], dtype=bool))
|
||||
|
||||
@classmethod
|
||||
def from_binary(cls, value):
|
||||
count = unpack_from('>i', value)[0]
|
||||
buf = np.frombuffer(value, dtype=np.uint8, offset=4)
|
||||
return cls(np.unpackbits(buf, count=count).astype(bool))
|
||||
|
||||
@classmethod
|
||||
def _to_db(cls, value):
|
||||
if not isinstance(value, cls):
|
||||
raise ValueError('expected bit')
|
||||
|
||||
return value.to_text()
|
||||
|
||||
@classmethod
|
||||
def _to_db_binary(cls, value):
|
||||
if not isinstance(value, cls):
|
||||
raise ValueError('expected bit')
|
||||
|
||||
return value.to_binary()
|
||||
@@ -0,0 +1,78 @@
|
||||
import numpy as np
|
||||
from struct import pack, unpack_from
|
||||
|
||||
|
||||
class HalfVector:
|
||||
def __init__(self, value):
|
||||
# asarray still copies if same dtype
|
||||
if not isinstance(value, np.ndarray) or value.dtype != '>f2':
|
||||
value = np.asarray(value, dtype='>f2')
|
||||
|
||||
if value.ndim != 1:
|
||||
raise ValueError('expected ndim to be 1')
|
||||
|
||||
self._value = value
|
||||
|
||||
def __repr__(self):
|
||||
return f'HalfVector({self.to_list()})'
|
||||
|
||||
def dimensions(self):
|
||||
return len(self._value)
|
||||
|
||||
def to_list(self):
|
||||
return self._value.tolist()
|
||||
|
||||
def to_numpy(self):
|
||||
return self._value
|
||||
|
||||
def to_text(self):
|
||||
return '[' + ','.join([str(float(v)) for v in self._value]) + ']'
|
||||
|
||||
def to_binary(self):
|
||||
return pack('>HH', self.dimensions(), 0) + self._value.tobytes()
|
||||
|
||||
@classmethod
|
||||
def from_text(cls, value):
|
||||
return cls([float(v) for v in value[1:-1].split(',')])
|
||||
|
||||
@classmethod
|
||||
def from_binary(cls, value):
|
||||
dim, unused = unpack_from('>HH', value)
|
||||
return cls(np.frombuffer(value, dtype='>f2', count=dim, offset=4))
|
||||
|
||||
@classmethod
|
||||
def _to_db(cls, value, dim=None):
|
||||
if value is None:
|
||||
return value
|
||||
|
||||
if not isinstance(value, cls):
|
||||
value = cls(value)
|
||||
|
||||
if dim is not None and value.dimensions() != dim:
|
||||
raise ValueError('expected %d dimensions, not %d' % (dim, value.dimensions()))
|
||||
|
||||
return value.to_text()
|
||||
|
||||
@classmethod
|
||||
def _to_db_binary(cls, value):
|
||||
if value is None:
|
||||
return value
|
||||
|
||||
if not isinstance(value, cls):
|
||||
value = cls(value)
|
||||
|
||||
return value.to_binary()
|
||||
|
||||
@classmethod
|
||||
def _from_db(cls, value):
|
||||
if value is None or isinstance(value, cls):
|
||||
return value
|
||||
|
||||
return cls.from_text(value)
|
||||
|
||||
@classmethod
|
||||
def _from_db_binary(cls, value):
|
||||
if value is None or isinstance(value, cls):
|
||||
return value
|
||||
|
||||
return cls.from_binary(value)
|
||||
@@ -0,0 +1,156 @@
|
||||
import numpy as np
|
||||
from struct import pack, unpack_from
|
||||
|
||||
NO_DEFAULT = object()
|
||||
|
||||
|
||||
class SparseVector:
|
||||
def __init__(self, value, dimensions=NO_DEFAULT, /):
|
||||
if value.__class__.__module__.startswith('scipy.sparse.'):
|
||||
if dimensions is not NO_DEFAULT:
|
||||
raise ValueError('extra argument')
|
||||
|
||||
self._from_sparse(value)
|
||||
elif isinstance(value, dict):
|
||||
if dimensions is NO_DEFAULT:
|
||||
raise ValueError('missing dimensions')
|
||||
|
||||
self._from_dict(value, dimensions)
|
||||
else:
|
||||
if dimensions is not NO_DEFAULT:
|
||||
raise ValueError('extra argument')
|
||||
|
||||
self._from_dense(value)
|
||||
|
||||
def __repr__(self):
|
||||
elements = dict(zip(self._indices, self._values))
|
||||
return f'SparseVector({elements}, {self._dim})'
|
||||
|
||||
def dimensions(self):
|
||||
return self._dim
|
||||
|
||||
def indices(self):
|
||||
return self._indices
|
||||
|
||||
def values(self):
|
||||
return self._values
|
||||
|
||||
def to_coo(self):
|
||||
from scipy.sparse import coo_array
|
||||
|
||||
coords = ([0] * len(self._indices), self._indices)
|
||||
return coo_array((self._values, coords), shape=(1, self._dim))
|
||||
|
||||
def to_list(self):
|
||||
vec = [0.0] * self._dim
|
||||
for i, v in zip(self._indices, self._values):
|
||||
vec[i] = v
|
||||
return vec
|
||||
|
||||
def to_numpy(self):
|
||||
vec = np.repeat(0.0, self._dim).astype(np.float32)
|
||||
for i, v in zip(self._indices, self._values):
|
||||
vec[i] = v
|
||||
return vec
|
||||
|
||||
def to_text(self):
|
||||
return '{' + ','.join([f'{int(i) + 1}:{float(v)}' for i, v in zip(self._indices, self._values)]) + '}/' + str(int(self._dim))
|
||||
|
||||
def to_binary(self):
|
||||
nnz = len(self._indices)
|
||||
return pack(f'>iii{nnz}i{nnz}f', self._dim, nnz, 0, *self._indices, *self._values)
|
||||
|
||||
def _from_dict(self, d, dim):
|
||||
elements = [(i, v) for i, v in d.items() if v != 0]
|
||||
elements.sort()
|
||||
|
||||
self._dim = int(dim)
|
||||
self._indices = [int(v[0]) for v in elements]
|
||||
self._values = [float(v[1]) for v in elements]
|
||||
|
||||
def _from_sparse(self, value):
|
||||
value = value.tocoo()
|
||||
|
||||
if value.ndim == 1:
|
||||
self._dim = value.shape[0]
|
||||
elif value.ndim == 2 and value.shape[0] == 1:
|
||||
self._dim = value.shape[1]
|
||||
else:
|
||||
raise ValueError('expected ndim to be 1')
|
||||
|
||||
if hasattr(value, 'coords'):
|
||||
# scipy 1.13+
|
||||
self._indices = value.coords[0].tolist()
|
||||
else:
|
||||
self._indices = value.col.tolist()
|
||||
self._values = value.data.tolist()
|
||||
|
||||
def _from_dense(self, value):
|
||||
self._dim = len(value)
|
||||
self._indices = [i for i, v in enumerate(value) if v != 0]
|
||||
self._values = [float(value[i]) for i in self._indices]
|
||||
|
||||
@classmethod
|
||||
def from_text(cls, value):
|
||||
elements, dim = value.split('/', 2)
|
||||
indices = []
|
||||
values = []
|
||||
# split on empty string returns single element list
|
||||
if len(elements) > 2:
|
||||
for e in elements[1:-1].split(','):
|
||||
i, v = e.split(':', 2)
|
||||
indices.append(int(i) - 1)
|
||||
values.append(float(v))
|
||||
return cls._from_parts(int(dim), indices, values)
|
||||
|
||||
@classmethod
|
||||
def from_binary(cls, value):
|
||||
dim, nnz, unused = unpack_from('>iii', value)
|
||||
indices = unpack_from(f'>{nnz}i', value, 12)
|
||||
values = unpack_from(f'>{nnz}f', value, 12 + nnz * 4)
|
||||
return cls._from_parts(int(dim), indices, values)
|
||||
|
||||
@classmethod
|
||||
def _from_parts(cls, dim, indices, values):
|
||||
vec = cls.__new__(cls)
|
||||
vec._dim = dim
|
||||
vec._indices = indices
|
||||
vec._values = values
|
||||
return vec
|
||||
|
||||
@classmethod
|
||||
def _to_db(cls, value, dim=None):
|
||||
if value is None:
|
||||
return value
|
||||
|
||||
if not isinstance(value, cls):
|
||||
value = cls(value)
|
||||
|
||||
if dim is not None and value.dimensions() != dim:
|
||||
raise ValueError('expected %d dimensions, not %d' % (dim, value.dimensions()))
|
||||
|
||||
return value.to_text()
|
||||
|
||||
@classmethod
|
||||
def _to_db_binary(cls, value):
|
||||
if value is None:
|
||||
return value
|
||||
|
||||
if not isinstance(value, cls):
|
||||
value = cls(value)
|
||||
|
||||
return value.to_binary()
|
||||
|
||||
@classmethod
|
||||
def _from_db(cls, value):
|
||||
if value is None or isinstance(value, cls):
|
||||
return value
|
||||
|
||||
return cls.from_text(value)
|
||||
|
||||
@classmethod
|
||||
def _from_db_binary(cls, value):
|
||||
if value is None or isinstance(value, cls):
|
||||
return value
|
||||
|
||||
return cls.from_binary(value)
|
||||
@@ -0,0 +1,78 @@
|
||||
import numpy as np
|
||||
from struct import pack, unpack_from
|
||||
|
||||
|
||||
class Vector:
|
||||
def __init__(self, value):
|
||||
# asarray still copies if same dtype
|
||||
if not isinstance(value, np.ndarray) or value.dtype != '>f4':
|
||||
value = np.asarray(value, dtype='>f4')
|
||||
|
||||
if value.ndim != 1:
|
||||
raise ValueError('expected ndim to be 1')
|
||||
|
||||
self._value = value
|
||||
|
||||
def __repr__(self):
|
||||
return f'Vector({self.to_list()})'
|
||||
|
||||
def dimensions(self):
|
||||
return len(self._value)
|
||||
|
||||
def to_list(self):
|
||||
return self._value.tolist()
|
||||
|
||||
def to_numpy(self):
|
||||
return self._value
|
||||
|
||||
def to_text(self):
|
||||
return '[' + ','.join([str(float(v)) for v in self._value]) + ']'
|
||||
|
||||
def to_binary(self):
|
||||
return pack('>HH', self.dimensions(), 0) + self._value.tobytes()
|
||||
|
||||
@classmethod
|
||||
def from_text(cls, value):
|
||||
return cls([float(v) for v in value[1:-1].split(',')])
|
||||
|
||||
@classmethod
|
||||
def from_binary(cls, value):
|
||||
dim, unused = unpack_from('>HH', value)
|
||||
return cls(np.frombuffer(value, dtype='>f4', count=dim, offset=4))
|
||||
|
||||
@classmethod
|
||||
def _to_db(cls, value, dim=None):
|
||||
if value is None:
|
||||
return value
|
||||
|
||||
if not isinstance(value, cls):
|
||||
value = cls(value)
|
||||
|
||||
if dim is not None and value.dimensions() != dim:
|
||||
raise ValueError('expected %d dimensions, not %d' % (dim, value.dimensions()))
|
||||
|
||||
return value.to_text()
|
||||
|
||||
@classmethod
|
||||
def _to_db_binary(cls, value):
|
||||
if value is None:
|
||||
return value
|
||||
|
||||
if not isinstance(value, cls):
|
||||
value = cls(value)
|
||||
|
||||
return value.to_binary()
|
||||
|
||||
@classmethod
|
||||
def _from_db(cls, value):
|
||||
if value is None or isinstance(value, np.ndarray):
|
||||
return value
|
||||
|
||||
return cls.from_text(value).to_numpy().astype(np.float32)
|
||||
|
||||
@classmethod
|
||||
def _from_db_binary(cls, value):
|
||||
if value is None or isinstance(value, np.ndarray):
|
||||
return value
|
||||
|
||||
return cls.from_binary(value).to_numpy().astype(np.float32)
|
||||
Reference in New Issue
Block a user