singlestore-langchain-core 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,38 @@
1
+ Metadata-Version: 2.4
2
+ Name: singlestore-langchain-core
3
+ Version: 0.1.0
4
+ Summary: Shared SingleStore helpers for langchain-singlestore and langgraph-singlestore
5
+ License: MIT
6
+ Requires-Python: >=3.10,<4.0
7
+ Classifier: License :: OSI Approved :: MIT License
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3.10
10
+ Classifier: Programming Language :: Python :: 3.11
11
+ Classifier: Programming Language :: Python :: 3.12
12
+ Classifier: Programming Language :: Python :: 3.13
13
+ Classifier: Programming Language :: Python :: 3.14
14
+ Requires-Dist: singlestoredb (>=1.16.9,<2.0.0)
15
+ Requires-Dist: sqlalchemy (>=2.0.40,<3.0.0)
16
+ Project-URL: Repository, https://github.com/singlestore-labs/langchain-singlestore
17
+ Project-URL: Release Notes, https://github.com/singlestore-labs/langchain-singlestore/releases
18
+ Project-URL: Source Code, https://github.com/singlestore-labs/langchain-singlestore/tree/main/libs/singlestore-langchain-core
19
+ Description-Content-Type: text/markdown
20
+
21
+ # singlestore-langchain-core
22
+
23
+ Shared, internal helpers used by the SingleStore integrations for LangChain
24
+ and LangGraph:
25
+
26
+ - Connection attribute helpers (`set_connector_attributes`,
27
+ `compute_connector_version`).
28
+ - SingleStore capability enums (`DistanceStrategy`, `FullTextIndexVersion`,
29
+ `FullTextScoringMode`).
30
+ - Metadata filter DSL (`FilterTypedDict`, `_parse_filter`) shared by vector
31
+ stores and stores.
32
+
33
+ This package is not intended to be depended on directly by application code.
34
+ Install one of the higher-level packages instead:
35
+
36
+ - [`langchain-singlestore`](../langchain-singlestore)
37
+ - [`langgraph-singlestore`](../langgraph-singlestore)
38
+
@@ -0,0 +1,17 @@
1
+ # singlestore-langchain-core
2
+
3
+ Shared, internal helpers used by the SingleStore integrations for LangChain
4
+ and LangGraph:
5
+
6
+ - Connection attribute helpers (`set_connector_attributes`,
7
+ `compute_connector_version`).
8
+ - SingleStore capability enums (`DistanceStrategy`, `FullTextIndexVersion`,
9
+ `FullTextScoringMode`).
10
+ - Metadata filter DSL (`FilterTypedDict`, `_parse_filter`) shared by vector
11
+ stores and stores.
12
+
13
+ This package is not intended to be depended on directly by application code.
14
+ Install one of the higher-level packages instead:
15
+
16
+ - [`langchain-singlestore`](../langchain-singlestore)
17
+ - [`langgraph-singlestore`](../langgraph-singlestore)
@@ -0,0 +1,58 @@
1
+ [build-system]
2
+ requires = ["poetry-core>=1.0.0"]
3
+ build-backend = "poetry.core.masonry.api"
4
+
5
+ [tool.poetry]
6
+ name = "singlestore-langchain-core"
7
+ version = "0.1.0"
8
+ description = "Shared SingleStore helpers for langchain-singlestore and langgraph-singlestore"
9
+ authors = []
10
+ readme = "README.md"
11
+ repository = "https://github.com/singlestore-labs/langchain-singlestore"
12
+ license = "MIT"
13
+ packages = [{ include = "singlestore_langchain_core" }]
14
+
15
+ [tool.poetry.urls]
16
+ "Source Code" = "https://github.com/singlestore-labs/langchain-singlestore/tree/main/libs/singlestore-langchain-core"
17
+ "Release Notes" = "https://github.com/singlestore-labs/langchain-singlestore/releases"
18
+
19
+ [tool.poetry.dependencies]
20
+ python = ">=3.10,<4.0"
21
+ singlestoredb = "^1.16.9"
22
+ sqlalchemy = "^2.0.40"
23
+
24
+ [tool.mypy]
25
+ disallow_untyped_defs = "True"
26
+
27
+ [tool.ruff.lint]
28
+ select = ["E", "F", "I", "T201"]
29
+
30
+ [tool.pytest.ini_options]
31
+ addopts = "--strict-markers --strict-config --durations=5"
32
+ asyncio_mode = "auto"
33
+
34
+ [tool.poetry.group.test]
35
+ optional = true
36
+
37
+ [tool.poetry.group.test_integration]
38
+ optional = true
39
+
40
+ [tool.poetry.group.lint]
41
+ optional = true
42
+
43
+ [tool.poetry.group.typing]
44
+ optional = true
45
+
46
+ [tool.poetry.group.test.dependencies]
47
+ pytest = ">=7.4.3,<10.0.0"
48
+ pytest-asyncio = ">=0.23.2,<2.0.0"
49
+ pytest-socket = "^0.7.0"
50
+
51
+ [tool.poetry.group.test_integration.dependencies]
52
+ docker = "^7.1.0"
53
+
54
+ [tool.poetry.group.lint.dependencies]
55
+ ruff = "^0.5"
56
+
57
+ [tool.poetry.group.typing.dependencies]
58
+ mypy = "^1.10"
@@ -0,0 +1,48 @@
1
+ """Shared SingleStore helpers used by ``langchain-singlestore`` and
2
+ ``langgraph-singlestore``.
3
+
4
+ This package contains only SingleStore/SQL primitives with no dependency on
5
+ ``langchain-core`` or ``langgraph``. It is intended to be an internal
6
+ implementation detail; direct use by application code is not supported.
7
+ """
8
+
9
+ from importlib import metadata
10
+
11
+ from singlestore_langchain_core._connection import (
12
+ create_connection_pool,
13
+ )
14
+ from singlestore_langchain_core._filter import (
15
+ FilterTypedDict,
16
+ _get_match_param_function,
17
+ _parse_filter,
18
+ )
19
+ from singlestore_langchain_core._utils import (
20
+ DEFAULT_CONNECTOR_NAME,
21
+ DistanceStrategy,
22
+ FullTextIndexVersion,
23
+ FullTextScoringMode,
24
+ compute_connector_version,
25
+ hash,
26
+ set_connector_attributes,
27
+ )
28
+
29
+ try:
30
+ __version__ = metadata.version(__package__)
31
+ except metadata.PackageNotFoundError:
32
+ __version__ = ""
33
+ del metadata
34
+
35
+ __all__ = [
36
+ "DEFAULT_CONNECTOR_NAME",
37
+ "DistanceStrategy",
38
+ "FilterTypedDict",
39
+ "FullTextIndexVersion",
40
+ "FullTextScoringMode",
41
+ "_get_match_param_function",
42
+ "_parse_filter",
43
+ "compute_connector_version",
44
+ "hash",
45
+ "set_connector_attributes",
46
+ "create_connection_pool",
47
+ "__version__",
48
+ ]
@@ -0,0 +1,154 @@
1
+ """Connection-pool utilities for SingleStore integrations.
2
+
3
+ Two SQLAlchemy ``Pool`` implementations are provided:
4
+
5
+ * :class:`SingleConnectionPool` — always hands out the same, caller-owned
6
+ connection. Useful when the caller manages the connection lifecycle itself
7
+ (tests, notebooks, a shared long-lived connection).
8
+ * :class:`QueueConnectionPool` — a thin wrapper around
9
+ :class:`sqlalchemy.pool.QueuePool` that lazily opens
10
+ :func:`singlestoredb.connect` connections using a stored kwargs mapping.
11
+
12
+ Use :func:`create_connection_pool` as the single entry point; it picks the
13
+ right implementation based on the arguments the caller supplied.
14
+ """
15
+
16
+ from typing import Any, Optional
17
+
18
+ from singlestoredb.connection import Connection, connect
19
+ from sqlalchemy.pool import Pool, QueuePool
20
+
21
+
22
+ class _CallerOwnedConnection:
23
+ """DBAPI-like proxy over a caller-owned :class:`Connection`.
24
+
25
+ Consumers of a pool follow the ``connect()``/``close()`` idiom, but for
26
+ :class:`SingleConnectionPool` the underlying connection is owned by the
27
+ caller and must outlive the checkout. This proxy forwards every attribute
28
+ to the wrapped connection while making ``close()`` a no-op so subsequent
29
+ checkouts keep working.
30
+ """
31
+
32
+ __slots__ = ("_connection",)
33
+
34
+ def __init__(self, connection: Connection) -> None:
35
+ object.__setattr__(self, "_connection", connection)
36
+
37
+ def close(self) -> None:
38
+ return None
39
+
40
+ def __getattr__(self, name: str) -> Any:
41
+ if name == "_connection":
42
+ raise AttributeError(name)
43
+ return getattr(self._connection, name)
44
+
45
+ def __enter__(self) -> Connection:
46
+ return self._connection
47
+
48
+ def __exit__(self, exc_type: Any, exc: Any, tb: Any) -> None:
49
+ return None
50
+
51
+
52
+ class SingleConnectionPool(Pool):
53
+ """Pool that returns the same pre-established connection on every call.
54
+
55
+ The connection is owned by the caller; this class does not open or close
56
+ it. Intended for scenarios where a single, long-lived connection is
57
+ reused across operations. Each :meth:`connect` returns a lightweight
58
+ proxy so callers may follow the standard ``connect()``/``close()``
59
+ idiom without tearing down the shared connection.
60
+ """
61
+
62
+ def __init__(self, connection: Connection) -> None:
63
+ self._connection = connection
64
+
65
+ def connect(self) -> Any: # type: ignore[override]
66
+ return _CallerOwnedConnection(self._connection)
67
+
68
+ def dispose(self) -> None: # type: ignore[override]
69
+ # Caller owns the underlying connection; nothing to release here.
70
+ return None
71
+
72
+
73
+ class QueueConnectionPool(Pool):
74
+ """Lazy, size-bounded pool backed by :class:`sqlalchemy.pool.QueuePool`.
75
+
76
+ Connections are opened on demand via :func:`singlestoredb.connect` using
77
+ ``connection_kwargs``. See the ``singlestoredb.connect`` documentation for
78
+ the full list of accepted keyword arguments (``host``, ``user``,
79
+ ``password``, ``port``, ``database``, TLS options, etc.).
80
+
81
+ Args:
82
+ pool_size: Number of persistent connections kept in the pool.
83
+ max_overflow: Additional connections that may be opened beyond
84
+ ``pool_size`` under load.
85
+ timeout: Seconds to wait for a free connection before raising.
86
+ connection_kwargs: Keyword arguments forwarded to
87
+ :func:`singlestoredb.connect` when a new connection is opened.
88
+ """
89
+
90
+ def __init__(
91
+ self,
92
+ pool_size: int = 5,
93
+ max_overflow: int = 10,
94
+ timeout: float = 30,
95
+ connection_kwargs: Optional[dict] = None,
96
+ ) -> None:
97
+ self._pool_size = pool_size
98
+ self._max_overflow = max_overflow
99
+ self._timeout = timeout
100
+ self._connection_kwargs: dict = dict(connection_kwargs or {})
101
+ self._pool = QueuePool(
102
+ self._open_connection,
103
+ pool_size=self._pool_size,
104
+ max_overflow=self._max_overflow,
105
+ timeout=self._timeout,
106
+ )
107
+
108
+ def _open_connection(self) -> Any:
109
+ return connect(**self._connection_kwargs)
110
+
111
+ def connect(self) -> Any: # type: ignore[override]
112
+ return self._pool.connect()
113
+
114
+ def dispose(self) -> None: # type: ignore[override]
115
+ self._pool.dispose()
116
+
117
+
118
+ def create_connection_pool(
119
+ connection: Optional[Connection] = None,
120
+ connection_pool: Optional[Pool] = None,
121
+ pool_size: int = 5,
122
+ max_overflow: int = 10,
123
+ timeout: float = 30,
124
+ connection_kwargs: Optional[dict] = None,
125
+ ) -> Pool:
126
+ """Return the connection pool that matches the supplied arguments.
127
+
128
+ Dispatch rules (checked in order):
129
+
130
+ 1. If both ``connection`` and ``connection_pool`` are given, raise
131
+ :class:`ValueError` — the caller must pick one.
132
+ 2. If ``connection`` is given, wrap it in a :class:`SingleConnectionPool`.
133
+ 3. If ``connection_pool`` is given, return it unchanged.
134
+ 4. Otherwise build a :class:`QueueConnectionPool` from ``pool_size``,
135
+ ``max_overflow``, ``timeout`` and ``connection_kwargs``.
136
+
137
+ See :class:`QueueConnectionPool` and :func:`singlestoredb.connect` for the
138
+ meaning of the remaining arguments.
139
+ """
140
+ if connection is not None and connection_pool is not None:
141
+ raise ValueError("Cannot specify both a connection and a connection pool.")
142
+
143
+ if connection is not None:
144
+ return SingleConnectionPool(connection)
145
+
146
+ if connection_pool is not None:
147
+ return connection_pool
148
+
149
+ return QueueConnectionPool(
150
+ pool_size=pool_size,
151
+ max_overflow=max_overflow,
152
+ timeout=timeout,
153
+ connection_kwargs=connection_kwargs,
154
+ )
@@ -0,0 +1,247 @@
1
+ """
2
+ Filter module for VectorStore.
3
+
4
+ This module provides typed filter definitions and utilities to convert
5
+ filter dictionaries into SQL query fragments for metadata filtering.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ from typing import Any, Dict, List, Literal, Tuple, Union, cast
12
+
13
+ # Type definitions for filter values
14
+ FieldValue = Union[str, int, float, bool]
15
+ NumericFieldValue = Union[int, float]
16
+
17
+ # Simple filter types
18
+ ExactMatchFilter = Dict[str, FieldValue]
19
+
20
+ # Comparison operators
21
+ EqFilter = Dict[Literal["$eq"], FieldValue]
22
+ NeFilter = Dict[Literal["$ne"], FieldValue]
23
+ GtFilter = Dict[Literal["$gt"], NumericFieldValue]
24
+ GteFilter = Dict[Literal["$gte"], NumericFieldValue]
25
+ LtFilter = Dict[Literal["$lt"], NumericFieldValue]
26
+ LteFilter = Dict[Literal["$lte"], NumericFieldValue]
27
+
28
+ # Collection operators
29
+ InFilter = Dict[Literal["$in"], List[FieldValue]]
30
+ NinFilter = Dict[Literal["$nin"], List[FieldValue]]
31
+
32
+ # Existence operator
33
+ ExistsFilter = Dict[Literal["$exists"], bool]
34
+
35
+ # Combined filter types
36
+ FieldFilter = Union[
37
+ EqFilter,
38
+ NeFilter,
39
+ GtFilter,
40
+ GteFilter,
41
+ LtFilter,
42
+ LteFilter,
43
+ InFilter,
44
+ NinFilter,
45
+ ExistsFilter,
46
+ ]
47
+
48
+ SimpleFilter = Union[
49
+ ExactMatchFilter,
50
+ Dict[str, FieldFilter],
51
+ ]
52
+
53
+ # Logical operators
54
+ AndFilter = Dict[Literal["$and"], List["FilterTypedDict"]]
55
+ OrFilter = Dict[Literal["$or"], List["FilterTypedDict"]]
56
+
57
+ # Overall filter type
58
+ FilterTypedDict = Union[SimpleFilter, AndFilter, OrFilter]
59
+
60
+
61
+ def _get_match_param_function(value: Any) -> str:
62
+ """
63
+ Determine the appropriate match parameter function based on value type.
64
+
65
+ Args:
66
+ value: The value to match against
67
+
68
+ Returns:
69
+ String representing the SQL match function to use
70
+
71
+ Raises:
72
+ ValueError: If the value type is not supported
73
+ """
74
+ # Check bool first since bool is a subclass of int
75
+ if isinstance(value, bool):
76
+ return "MATCH_PARAM_BOOL_STRICT()"
77
+ elif isinstance(value, str):
78
+ return "MATCH_PARAM_STRING_STRICT()"
79
+ elif isinstance(value, (int, float)):
80
+ return "MATCH_PARAM_DOUBLE_STRICT()"
81
+ else:
82
+ raise ValueError(f"Unsupported value type: {type(value)}")
83
+
84
+
85
+ def _parse_filter(
86
+ filter_dict: FilterTypedDict, metadata_field: str
87
+ ) -> tuple[str, list[Any]]:
88
+ """
89
+ Parse a filter dictionary into an SQL query fragment and parameters.
90
+
91
+ Args:
92
+ filter_dict: Filter specification following the FilterTypedDict schema
93
+
94
+ Returns:
95
+ Tuple containing:
96
+ - SQL query fragment string
97
+ - List of parameter values to be substituted into the query
98
+
99
+ Raises:
100
+ ValueError: If the filter format is invalid
101
+ """
102
+ if not isinstance(filter_dict, dict):
103
+ raise ValueError("Filter must be a dictionary")
104
+
105
+ if len(filter_dict) != 1:
106
+ raise ValueError("Filter must contain exactly one key")
107
+
108
+ # Handle logical operators
109
+ if "$and" in filter_dict:
110
+ return _handle_and_filter( # type: ignore[arg-type]
111
+ cast(AndFilter, filter_dict), metadata_field
112
+ )
113
+ elif "$or" in filter_dict:
114
+ return _handle_or_filter( # type: ignore[arg-type]
115
+ cast(OrFilter, filter_dict), metadata_field
116
+ )
117
+ else:
118
+ # Handle field filters
119
+ field_name = next(iter(filter_dict))
120
+ field_value = filter_dict[field_name] # type: ignore[index]
121
+
122
+ if isinstance(field_value, dict):
123
+ # Handle operator-based field filter
124
+ return _handle_operator_filter(field_name, field_value, metadata_field)
125
+ else:
126
+ # Handle exact match filter
127
+ match_func = _get_match_param_function(field_value)
128
+ return (
129
+ f"JSON_MATCH_ANY({match_func} = %s, {metadata_field}, %s)",
130
+ [field_value, field_name],
131
+ )
132
+
133
+
134
+ def _handle_and_filter(
135
+ filter_dict: AndFilter, metadata_field: str
136
+ ) -> tuple[str, list[Any]]:
137
+ """Handle $and operator filter."""
138
+ sub_filters = filter_dict["$and"] # type: ignore[index]
139
+ if not isinstance(sub_filters, list):
140
+ raise ValueError("$and must be a list of filters")
141
+
142
+ # Process each sub-filter
143
+ parsed_filters = [
144
+ _parse_filter(sub_filter, metadata_field) for sub_filter in sub_filters
145
+ ]
146
+
147
+ # Join conditions with AND
148
+ query = " AND ".join(item[0] for item in parsed_filters)
149
+
150
+ # Flatten parameter lists
151
+ params = [param for parsed_filter in parsed_filters for param in parsed_filter[1]]
152
+
153
+ return query, params
154
+
155
+
156
+ def _handle_or_filter(
157
+ filter_dict: OrFilter, metadata_field: str
158
+ ) -> tuple[str, list[Any]]:
159
+ """Handle $or operator filter."""
160
+ sub_filters = filter_dict["$or"] # type: ignore[index]
161
+ if not isinstance(sub_filters, list):
162
+ raise ValueError("$or must be a list of filters")
163
+
164
+ # Process each sub-filter
165
+ parsed_filters = [
166
+ _parse_filter(sub_filter, metadata_field) for sub_filter in sub_filters
167
+ ]
168
+
169
+ # Join conditions with OR and wrap in parentheses
170
+ query = "(" + " OR ".join(item[0] for item in parsed_filters) + ")"
171
+
172
+ # Flatten parameter lists
173
+ params = [param for parsed_filter in parsed_filters for param in parsed_filter[1]]
174
+
175
+ return query, params
176
+
177
+
178
+ def _handle_operator_filter(
179
+ field_name: str, field_filter: Dict, metadata_field: str
180
+ ) -> Tuple[str, List[Any]]:
181
+ """Handle operator-based field filters like $eq, $gt, etc."""
182
+ if len(field_filter) != 1:
183
+ raise ValueError("Field filter must contain exactly one key")
184
+
185
+ operator = next(iter(field_filter))
186
+ field_value = field_filter[operator]
187
+
188
+ # Equal operator
189
+ if operator == "$eq":
190
+ match_func = _get_match_param_function(field_value)
191
+ return (
192
+ f"JSON_MATCH_ANY({match_func} = %s, {metadata_field}, %s)",
193
+ [field_value, field_name],
194
+ )
195
+
196
+ # Not equal operator
197
+ elif operator == "$ne":
198
+ match_func = _get_match_param_function(field_value)
199
+ return (
200
+ f"NOT JSON_MATCH_ANY({match_func} = %s, {metadata_field}, %s) AND "
201
+ f"JSON_MATCH_ANY_EXISTS({metadata_field}, %s)",
202
+ [field_value, field_name, field_name],
203
+ )
204
+
205
+ # Numeric comparison operators
206
+ elif operator in ("$gt", "$gte", "$lt", "$lte"):
207
+ if not isinstance(field_value, (int, float)):
208
+ raise ValueError(f"{operator} must be a numeric value")
209
+
210
+ comparison_op = {"$gt": ">", "$gte": ">=", "$lt": "<", "$lte": "<="}[operator]
211
+
212
+ return (
213
+ f"JSON_EXTRACT_DOUBLE({metadata_field}, %s) {comparison_op} %s",
214
+ [field_name, field_value],
215
+ )
216
+
217
+ # Collection operators
218
+ elif operator in ("$in", "$nin"):
219
+ if not isinstance(field_value, list):
220
+ raise ValueError(f"{operator} must be a list")
221
+
222
+ if operator == "$in":
223
+ return (
224
+ f"JSON_MATCH_ANY(JSON_ARRAY_CONTAINS_JSON(%s, MATCH_PARAM_JSON()), "
225
+ f"{metadata_field}, %s)",
226
+ [json.dumps(field_value), field_name],
227
+ )
228
+ else: # $nin
229
+ return (
230
+ f"NOT JSON_MATCH_ANY(JSON_ARRAY_CONTAINS_JSON(%s, MATCH_PARAM_JSON()), "
231
+ f"{metadata_field}, %s) AND "
232
+ f"JSON_MATCH_ANY_EXISTS({metadata_field}, %s)",
233
+ [json.dumps(field_value), field_name, field_name],
234
+ )
235
+
236
+ # Existence operator
237
+ elif operator == "$exists":
238
+ if not isinstance(field_value, bool):
239
+ raise ValueError("$exists must be a boolean")
240
+
241
+ if field_value:
242
+ return f"JSON_MATCH_ANY_EXISTS({metadata_field}, %s)", [field_name]
243
+ else:
244
+ return f"NOT JSON_MATCH_ANY_EXISTS({metadata_field}, %s)", [field_name]
245
+
246
+ else:
247
+ raise ValueError(f"Unsupported operator: {operator}")
@@ -0,0 +1,103 @@
1
+ """SingleStore connection helpers and shared enums.
2
+
3
+ No LangChain / LangGraph imports live in this module so it can be shared by
4
+ every SingleStore integration package.
5
+ """
6
+
7
+ import hashlib
8
+ from enum import Enum
9
+ from importlib.metadata import PackageNotFoundError, version
10
+ from typing import Optional
11
+
12
+ DEFAULT_CONNECTOR_NAME = "langchain python sdk"
13
+
14
+
15
+ def compute_connector_version(package_name: str, *, fallback: str = "3.0.0") -> str:
16
+ """Return the connector version to advertise to SingleStore.
17
+
18
+ Historically the connector version is the package version with ``2`` added
19
+ to the major component (``1.5.0`` -> ``3.5.0``). This preserves the wire
20
+ contract that pre-monorepo releases used.
21
+ """
22
+ try:
23
+ pkg_version = version(package_name)
24
+ version_parts = pkg_version.split(".")
25
+ major_version = int(version_parts[0]) + 2
26
+ version_parts[0] = str(major_version)
27
+ return ".".join(version_parts)
28
+ except (PackageNotFoundError, ValueError):
29
+ return fallback
30
+
31
+
32
+ def set_connector_attributes(
33
+ connection_kwargs: dict,
34
+ *,
35
+ connector_name: str = DEFAULT_CONNECTOR_NAME,
36
+ connector_version: Optional[str] = None,
37
+ ) -> None:
38
+ """Stamp connector identity onto ``connection_kwargs['conn_attrs']``.
39
+
40
+ ``connector_version`` may be ``None`` when the caller has no versioned
41
+ package to report; the attribute is simply omitted in that case.
42
+ """
43
+ if "conn_attrs" not in connection_kwargs:
44
+ connection_kwargs["conn_attrs"] = {}
45
+
46
+ connection_kwargs["conn_attrs"]["_connector_name"] = connector_name
47
+ if connector_version is not None:
48
+ connection_kwargs["conn_attrs"]["_connector_version"] = connector_version
49
+
50
+
51
+ class DistanceStrategy(str, Enum):
52
+ """Distance strategies for calculating similarity between vectors.
53
+
54
+ Attributes:
55
+ EUCLIDEAN_DISTANCE: Computes the Euclidean (L2) distance between vectors.
56
+ Lower scores indicate more similar vectors. Not compatible with
57
+ WEIGHTED_SUM search strategy.
58
+ DOT_PRODUCT: Computes the dot product (inner product) between vectors.
59
+ Higher scores indicate more similar vectors. This is the default
60
+ and recommended strategy for most embedding models.
61
+ """
62
+
63
+ EUCLIDEAN_DISTANCE = "EUCLIDEAN_DISTANCE"
64
+ DOT_PRODUCT = "DOT_PRODUCT"
65
+
66
+
67
+ class FullTextIndexVersion(str, Enum):
68
+ """Full-text index versions supported by SingleStore.
69
+
70
+ Attributes:
71
+ V1: Original full-text index implementation. Compatible with all
72
+ SingleStore versions that support full-text search. Only supports
73
+ MATCH scoring mode.
74
+ V2: New full-text index implementation available in SingleStore 8.7+.
75
+ Offers improved performance and supports additional scoring modes
76
+ (BM25, BM25_GLOBAL).
77
+ """
78
+
79
+ V1 = "V1"
80
+ V2 = "V2"
81
+
82
+
83
+ class FullTextScoringMode(str, Enum):
84
+ """Scoring algorithms for full-text search ranking.
85
+
86
+ Attributes:
87
+ MATCH: Uses SingleStore's native MATCH() AGAINST() function.
88
+ Compatible with both V1 and V2 full-text indexes.
89
+ BM25: Best Matching 25 algorithm with TF-IDF scoring and document
90
+ length normalization. Requires V2 full-text index.
91
+ BM25_GLOBAL: BM25 with global IDF statistics across all partitions.
92
+ Provides consistent scoring in distributed environments.
93
+ Requires V2 full-text index.
94
+ """
95
+
96
+ MATCH = "MATCH"
97
+ BM25 = "BM25"
98
+ BM25_GLOBAL = "BM25_GLOBAL"
99
+
100
+
101
+ def hash(_input: str) -> str:
102
+ """Use a deterministic hashing approach."""
103
+ return hashlib.md5(_input.encode()).hexdigest()