langchain-oceanbase 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,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 LangChain, Inc.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,55 @@
1
+ Metadata-Version: 2.1
2
+ Name: langchain-oceanbase
3
+ Version: 0.1.0
4
+ Summary: An integration package connecting OceanBase and LangChain
5
+ License: MIT
6
+ Author: shanhaikang.shk
7
+ Author-email: shanhaikang.shk@oceanbase.com
8
+ Requires-Python: >=3.9,<4.0
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.9
12
+ Classifier: Programming Language :: Python :: 3.10
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Requires-Dist: langchain-core (>=0.3.15,<0.4.0)
17
+ Requires-Dist: pyobvector (>=0.1.17,<0.2.0)
18
+ Project-URL: Release Notes, https://github.com/langchain-ai/langchain/releases?q=tag%3A%22oceanbase%3D%3D0%22&expanded=true
19
+ Project-URL: Source Code, https://github.com/langchain-ai/langchain/tree/master/libs/partners/oceanbase
20
+ Description-Content-Type: text/markdown
21
+
22
+ # langchain-oceanbase
23
+
24
+ This package contains the LangChain integration with OceanBase.
25
+
26
+ [OceanBase Database](https://github.com/oceanbase/oceanbase) is a distributed relational database.
27
+ It is developed entirely by Ant Group. The OceanBase Database is built on a common server cluster.
28
+ Based on the Paxos protocol and its distributed structure, the OceanBase Database provides high availability and linear scalability.
29
+
30
+ OceanBase currently has the ability to store vectors. Users can easily perform the following operations with SQL:
31
+
32
+ - Create a table containing vector type fields;
33
+ - Create a vector index table based on the HNSW algorithm;
34
+ - Perform vector approximate nearest neighbor queries;
35
+ - ...
36
+
37
+ ## Installation
38
+
39
+ ```bash
40
+ pip install -U langchain-oceanbase
41
+ ```
42
+
43
+ We recommend using Docker to deploy OceanBase:
44
+
45
+ ```shell
46
+ docker run --name=ob433 -e MODE=slim -p 2881:2881 -d oceanbase/oceanbase-ce:4.3.3.0-100000132024100711
47
+ ```
48
+
49
+ [More methods to deploy OceanBase cluster](https://github.com/oceanbase/oceanbase-doc/blob/V4.3.1/en-US/400.deploy/500.deploy-oceanbase-database-community-edition/100.deployment-overview.md)
50
+
51
+ ### Usage
52
+
53
+ For a more detailed walkthrough of the OceanBase Wrapper, see [this notebook](./docs/vectorstores.ipynb)
54
+
55
+
@@ -0,0 +1,33 @@
1
+ # langchain-oceanbase
2
+
3
+ This package contains the LangChain integration with OceanBase.
4
+
5
+ [OceanBase Database](https://github.com/oceanbase/oceanbase) is a distributed relational database.
6
+ It is developed entirely by Ant Group. The OceanBase Database is built on a common server cluster.
7
+ Based on the Paxos protocol and its distributed structure, the OceanBase Database provides high availability and linear scalability.
8
+
9
+ OceanBase currently has the ability to store vectors. Users can easily perform the following operations with SQL:
10
+
11
+ - Create a table containing vector type fields;
12
+ - Create a vector index table based on the HNSW algorithm;
13
+ - Perform vector approximate nearest neighbor queries;
14
+ - ...
15
+
16
+ ## Installation
17
+
18
+ ```bash
19
+ pip install -U langchain-oceanbase
20
+ ```
21
+
22
+ We recommend using Docker to deploy OceanBase:
23
+
24
+ ```shell
25
+ docker run --name=ob433 -e MODE=slim -p 2881:2881 -d oceanbase/oceanbase-ce:4.3.3.0-100000132024100711
26
+ ```
27
+
28
+ [More methods to deploy OceanBase cluster](https://github.com/oceanbase/oceanbase-doc/blob/V4.3.1/en-US/400.deploy/500.deploy-oceanbase-database-community-edition/100.deployment-overview.md)
29
+
30
+ ### Usage
31
+
32
+ For a more detailed walkthrough of the OceanBase Wrapper, see [this notebook](./docs/vectorstores.ipynb)
33
+
@@ -0,0 +1,15 @@
1
+ from importlib import metadata
2
+
3
+ from langchain_oceanbase.vectorstores import OceanbaseVectorStore
4
+
5
+ try:
6
+ __version__ = metadata.version(__package__)
7
+ except metadata.PackageNotFoundError:
8
+ # Case where package metadata is not available.
9
+ __version__ = ""
10
+ del metadata # optional, avoids polluting the results of dir(__package__)
11
+
12
+ __all__ = [
13
+ "OceanbaseVectorStore",
14
+ "__version__",
15
+ ]
File without changes
@@ -0,0 +1,724 @@
1
+ """Oceanbase vector stores."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import logging
7
+ import math
8
+ import traceback
9
+ import uuid
10
+ from typing import Any, Callable, Iterable, List, Optional, Sequence, Tuple
11
+
12
+ import numpy as np
13
+ from langchain_core.documents import Document
14
+ from langchain_core.embeddings import Embeddings
15
+ from langchain_core.vectorstores import VectorStore
16
+ from pyobvector import VECTOR, ObVecClient # type: ignore
17
+ from sqlalchemy import JSON, Column, String, Table, func, text
18
+ from sqlalchemy.dialects.mysql import LONGTEXT
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+ DEFAULT_OCEANBASE_CONNECTION = {
23
+ "host": "localhost",
24
+ "port": "2881",
25
+ "user": "root@test",
26
+ "password": "",
27
+ "db_name": "test",
28
+ }
29
+ DEFAULT_OCEANBASE_VECTOR_TABLE_NAME = "langchain_vector"
30
+ DEFAULT_OCEANBASE_HNSW_BUILD_PARAM = {"M": 16, "efConstruction": 256}
31
+ DEFAULT_OCEANBASE_HNSW_SEARCH_PARAM = {"efSearch": 64}
32
+ OCEANBASE_SUPPORTED_VECTOR_INDEX_TYPE = "HNSW"
33
+ DEFAULT_OCEANBASE_VECTOR_METRIC_TYPE = "l2"
34
+
35
+ DEFAULT_METADATA_FIELD = "metadata"
36
+
37
+
38
+ def _euclidean_similarity(distance: float) -> float:
39
+ return 1.0 - distance / math.sqrt(2)
40
+
41
+
42
+ def _neg_inner_product_similarity(distance: float) -> float:
43
+ return -distance
44
+
45
+
46
+ class OceanbaseVectorStore(VectorStore):
47
+ """Oceanbase vector store integration.
48
+
49
+ Setup:
50
+ Install ``langchain-oceanbase`` and deploy a standalone OceanBase server with docker.
51
+
52
+ .. code-block:: bash
53
+
54
+ pip install -U langchain-oceanbase
55
+ docker run --name=ob433 -e MODE=mini -e OB_SERVER_IP=127.0.0.1 -p 2881:2881 -d quay.io/oceanbase/oceanbase-ce:4.3.3.1-101000012024102216
56
+
57
+ More methods to deploy OceanBase cluster:
58
+ https://github.com/oceanbase/oceanbase-doc/blob/V4.3.1/en-US/400.deploy/500.deploy-oceanbase-database-community-edition/100.deployment-overview.md
59
+
60
+ Key init args — indexing params:
61
+ vidx_metric_type: str
62
+ Metric method of distance between vectors.
63
+ This parameter takes values in `l2` and `inner_product`. Defaults to `l2`.
64
+ vidx_algo_params: Optional[dict]
65
+ Which index params to use. Now OceanBase supports HNSW only.
66
+ Refer to `DEFAULT_OCEANBASE_HNSW_BUILD_PARAM` for example.
67
+ drop_old: bool
68
+ Whether to drop the current table. Defaults to False.
69
+ primary_field: str
70
+ Name of the primary key column. Defaults to "id".
71
+ vector_field: str
72
+ Name of the vector column. Defaults to "embedding".
73
+ text_field: str
74
+ Name of the text column. Defaults to "document".
75
+ metadata_field: Optional[str]
76
+ Name of the metadata column. Defaults to "metadata".
77
+ When `metadata_field` is specified, the document's metadata will store as json.
78
+ vidx_name: str
79
+ Name of the vector index table.
80
+ partitions: ObPartition
81
+ Partition strategy of table.
82
+ Refer to `pyobvector`'s documentation for more examples.
83
+ extra_columns: Optional[List[Column]]
84
+ Extra sqlalchemy columns to add to the table.
85
+
86
+ Key init args — client params:
87
+ embedding_function: Embeddings
88
+ Function used to embed the text.
89
+ table_name: str
90
+ Which table name to use. Defaults to "langchain_vector".
91
+ connection_args: Optional[dict[str, any]]
92
+ The connection args used for this class comes in the form of a dict. Refer to
93
+ `DEFAULT_OCEANBASE_CONNECTION` for example.
94
+
95
+ Instantiate:
96
+ .. code-block:: python
97
+
98
+ from langchain_oceanbase.vectorstores import OceanbaseVectorStore
99
+ from langchain_community.embeddings import DashScopeEmbeddings
100
+
101
+ DASHSCOPE_API = os.environ.get("DASHSCOPE_API_KEY", "")
102
+ connection_args = {
103
+ "host": "127.0.0.1",
104
+ "port": "2881",
105
+ "user": "root@test",
106
+ "password": "",
107
+ "db_name": "test",
108
+ }
109
+ embeddings = DashScopeEmbeddings(
110
+ model="text-embedding-v1", dashscope_api_key=DASHSCOPE_API
111
+ )
112
+
113
+ vector_store = OceanbaseVectorStore(
114
+ embedding_function=embeddings,
115
+ table_name="langchain_vector",
116
+ connection_args=connection_args,
117
+ vidx_metric_type="l2",
118
+ drop_old=True,
119
+ )
120
+
121
+ Add Documents:
122
+ .. code-block:: python
123
+
124
+ from langchain_core.documents import Document
125
+
126
+ document_1 = Document(page_content="foo", metadata={"baz": "bar"})
127
+ document_2 = Document(page_content="thud", metadata={"bar": "baz"})
128
+ document_3 = Document(page_content="i will be deleted :(")
129
+
130
+ documents = [document_1, document_2, document_3]
131
+ ids = ["1", "2", "3"]
132
+ vector_store.add_documents(documents=documents, ids=ids)
133
+
134
+ Delete Documents:
135
+ .. code-block:: python
136
+
137
+ vector_store.delete(ids=["3"])
138
+
139
+ Search:
140
+ .. code-block:: python
141
+
142
+ results = vector_store.similarity_search(query="thud",k=1)
143
+ for doc in results:
144
+ print(f"* {doc.page_content} [{doc.metadata}]")
145
+
146
+ Search with filter:
147
+ .. code-block:: python
148
+
149
+ results = vector_store.similarity_search(query="thud",k=1,filter={"bar": "baz"})
150
+ for doc in results:
151
+ print(f"* {doc.page_content} [{doc.metadata}]")
152
+
153
+ Search with score:
154
+ .. code-block:: python
155
+
156
+ results = vector_store.similarity_search_with_score(query="qux",k=1)
157
+ for doc, score in results:
158
+ print(f"* [SIM={score:3f}] {doc.page_content} [{doc.metadata}]")
159
+
160
+ Use as Retriever:
161
+ .. code-block:: python
162
+
163
+ retriever = vector_store.as_retriever(
164
+ search_kwargs={"k": 1, "fetch_k": 2, "lambda_mult": 0.5},
165
+ )
166
+ retriever.invoke("thud")
167
+
168
+ """ # noqa: E501
169
+
170
+ def __init__( # type: ignore[no-untyped-def]
171
+ self,
172
+ embedding_function: Embeddings,
173
+ table_name: str = DEFAULT_OCEANBASE_VECTOR_TABLE_NAME,
174
+ connection_args: Optional[dict[str, Any]] = None,
175
+ vidx_metric_type: str = DEFAULT_OCEANBASE_VECTOR_METRIC_TYPE,
176
+ vidx_algo_params: Optional[dict] = None,
177
+ drop_old: bool = False,
178
+ *,
179
+ primary_field: str = "id",
180
+ vector_field: str = "embedding",
181
+ text_field: str = "document",
182
+ metadata_field: Optional[str] = DEFAULT_METADATA_FIELD,
183
+ vidx_name: str = "vidx",
184
+ partitions: Optional[Any] = None,
185
+ extra_columns: Optional[List[Column]] = None,
186
+ normalize: bool = False,
187
+ embedding_dim: Optional[int] = None,
188
+ **kwargs,
189
+ ):
190
+ """Initialize the OceanBase vector store."""
191
+
192
+ self.embedding_function = embedding_function
193
+ self.table_name = table_name
194
+ self.connection_args = (
195
+ connection_args
196
+ if connection_args is not None
197
+ else DEFAULT_OCEANBASE_CONNECTION
198
+ )
199
+ self.extra_columns = extra_columns
200
+ self.normalize = normalize
201
+ self._create_client(**kwargs)
202
+ assert self.obvector is not None
203
+
204
+ self.vidx_metric_type = vidx_metric_type.lower()
205
+ if self.vidx_metric_type not in ("l2", "inner_product"):
206
+ raise ValueError(
207
+ "`vidx_metric_type` should be set in `l2`/`inner_product`."
208
+ )
209
+
210
+ self.vidx_algo_params = (
211
+ vidx_algo_params
212
+ if vidx_algo_params is not None
213
+ else DEFAULT_OCEANBASE_HNSW_BUILD_PARAM
214
+ )
215
+
216
+ self.drop_old = drop_old
217
+ self.primary_field = primary_field
218
+ self.vector_field = vector_field
219
+ self.text_field = text_field
220
+ self.metadata_field = metadata_field or DEFAULT_METADATA_FIELD
221
+ self.vidx_name = vidx_name
222
+ self.partition = partitions
223
+ self.hnsw_ef_search = -1
224
+
225
+ if self.drop_old:
226
+ self.obvector.drop_table_if_exist(self.table_name)
227
+
228
+ if not self.obvector.check_table_exists(self.table_name):
229
+ if embedding_dim is not None:
230
+ self._create_table_with_index_by_embedding_dim(embedding_dim)
231
+ self._load_table()
232
+ else:
233
+ self._load_table()
234
+
235
+ @property
236
+ def embeddings(self) -> Embeddings:
237
+ return self.embedding_function
238
+
239
+ def _create_client(self, **kwargs): # type: ignore[no-untyped-def]
240
+ host = self.connection_args.get("host", "localhost")
241
+ port = self.connection_args.get("port", "2881")
242
+ user = self.connection_args.get("user", "root@test")
243
+ password = self.connection_args.get("password", "")
244
+ db_name = self.connection_args.get("db_name", "test")
245
+
246
+ self.obvector = ObVecClient(
247
+ uri=host + ":" + port,
248
+ user=user,
249
+ password=password,
250
+ db_name=db_name,
251
+ **kwargs,
252
+ )
253
+
254
+ def _load_table(self) -> None:
255
+ table = Table(
256
+ self.table_name,
257
+ self.obvector.metadata_obj,
258
+ autoload_with=self.obvector.engine,
259
+ )
260
+ column_names = [column.name for column in table.columns]
261
+ optional_len = len(self.extra_columns or []) + 1
262
+ assert len(column_names) == (3 + optional_len)
263
+
264
+ logging.info(f"load exist table with {column_names} columns")
265
+ self.primary_field = column_names[0]
266
+ self.vector_field = column_names[1]
267
+ self.text_field = column_names[2]
268
+ self.metadata_field = column_names[3]
269
+
270
+ def _create_table_with_index_by_embedding_dim(self, dim: int) -> None:
271
+ cols = [
272
+ Column(
273
+ self.primary_field, String(4096), primary_key=True, autoincrement=False
274
+ ),
275
+ Column(self.vector_field, VECTOR(dim)),
276
+ Column(self.text_field, LONGTEXT),
277
+ Column(self.metadata_field, JSON),
278
+ ]
279
+ if self.extra_columns is not None:
280
+ cols.extend(self.extra_columns)
281
+
282
+ vidx_params = self.obvector.prepare_index_params()
283
+ vidx_params.add_index(
284
+ field_name=self.vector_field,
285
+ index_type=OCEANBASE_SUPPORTED_VECTOR_INDEX_TYPE,
286
+ index_name=self.vidx_name,
287
+ metric_type=self.vidx_metric_type,
288
+ params=self.vidx_algo_params,
289
+ )
290
+
291
+ self.obvector.create_table_with_index_params(
292
+ table_name=self.table_name,
293
+ columns=cols,
294
+ indexes=None,
295
+ vidxs=vidx_params,
296
+ partitions=self.partition,
297
+ )
298
+
299
+ def _create_table_with_index(self, embeddings: list) -> None:
300
+ if self.obvector.check_table_exists(self.table_name):
301
+ self._load_table()
302
+ return
303
+
304
+ dim = len(embeddings[0])
305
+ self._create_table_with_index_by_embedding_dim(dim)
306
+
307
+ def _parse_metric_type_str_to_dist_func(self) -> Any:
308
+ if self.vidx_metric_type == "l2":
309
+ return func.l2_distance
310
+ if self.vidx_metric_type == "cosine":
311
+ return func.cosine_distance
312
+ if self.vidx_metric_type == "inner_product":
313
+ return func.negative_inner_product
314
+ raise ValueError(f"Invalid vector index metric type: {self.vidx_metric_type}")
315
+
316
+ def _normalize(self, vector: List[float]) -> List[float]:
317
+ arr = np.array(vector)
318
+ norm = np.linalg.norm(arr)
319
+ arr = arr / norm
320
+ return arr.tolist()
321
+
322
+ def add_texts(
323
+ self,
324
+ texts: Iterable[str],
325
+ metadatas: Optional[List[dict]] = None,
326
+ batch_size: int = 1000,
327
+ *,
328
+ ids: Optional[List[str]] = None,
329
+ extras: Optional[List[dict]] = None,
330
+ partition_name: Optional[str] = None,
331
+ **kwargs: Any,
332
+ ) -> List[str]:
333
+ """Insert text data into OceanBase.
334
+
335
+ Inserting data when the table has not be created yet will result
336
+ in creating a new table. The data of the first record decides
337
+ the schema of the new table, the dim is extracted from the first
338
+ embedding.
339
+
340
+ Args:
341
+ texts (Iterable[str]): The texts to embed. OceanBase use a `LONGTEXT`
342
+ type column to hold the data.
343
+ metadatas (Optional[List[dict]]): Metadata dicts attached to each of
344
+ the texts. Defaults to None.
345
+ batch_size (int, optional): Batch size to use for insertion.
346
+ Defaults to 1000.
347
+ ids (Optional[List[str]]): List of text ids.
348
+ extras (Optional[List[dict]]): Extra data to store in the table.
349
+ partition_name (Optional[str]): The partition name to insert data into.
350
+
351
+ Raises:
352
+ Exception: Failure to add texts
353
+
354
+ Returns:
355
+ List[str]: The resulting ids for each inserted element.
356
+ """
357
+ texts = list(texts)
358
+
359
+ try:
360
+ embeddings = self.embedding_function.embed_documents(texts)
361
+ except NotImplementedError:
362
+ embeddings = [self.embedding_function.embed_query(x) for x in texts]
363
+
364
+ total_count = len(embeddings)
365
+ if total_count == 0:
366
+ return []
367
+
368
+ self._create_table_with_index(embeddings)
369
+
370
+ if ids is None:
371
+ ids = [str(uuid.uuid4()) for _ in texts]
372
+
373
+ if not metadatas:
374
+ metadatas = [{} for _ in texts]
375
+
376
+ extra_data = extras or [{} for _ in texts]
377
+
378
+ pks: list[str] = []
379
+ for i in range(0, total_count, batch_size):
380
+ data = [
381
+ {
382
+ self.primary_field: id,
383
+ self.vector_field: (
384
+ embedding if not self.normalize else self._normalize(embedding)
385
+ ),
386
+ self.text_field: text,
387
+ self.metadata_field: metadata,
388
+ **extra,
389
+ }
390
+ for id, embedding, text, metadata, extra in zip(
391
+ ids[i : i + batch_size],
392
+ embeddings[i : i + batch_size],
393
+ texts[i : i + batch_size],
394
+ metadatas[i : i + batch_size],
395
+ extra_data[i : i + batch_size],
396
+ )
397
+ ]
398
+ try:
399
+ self.obvector.upsert(
400
+ table_name=self.table_name,
401
+ data=data,
402
+ partition_name=(partition_name or ""),
403
+ )
404
+ pks.extend(ids[i : i + batch_size])
405
+ except Exception:
406
+ traceback.print_exc()
407
+ logger.error(
408
+ f"Failed to insert batch starting at entity:[{i}, {i + batch_size})"
409
+ )
410
+ return pks
411
+
412
+ def delete( # type: ignore[no-untyped-def]
413
+ self, ids: Optional[List[str]] = None, fltr: Optional[str] = None, **kwargs
414
+ ) -> Optional[bool]:
415
+ """Delete by vector ID or boolean expression.
416
+
417
+ Args:
418
+ ids (Optional[List[str]]): List of ids to delete.
419
+ fltr (Optional[str]): Boolean filter that specifies the entities to delete.
420
+ """
421
+ self.obvector.delete(
422
+ table_name=self.table_name,
423
+ ids=ids,
424
+ where_clause=([text(fltr)] if fltr is not None else None),
425
+ )
426
+ return None
427
+
428
+ def get_by_ids(self, ids: Sequence[str], /) -> list[Document]:
429
+ """Get entities by vector ID.
430
+
431
+ Args:
432
+ ids (Optional[List[str]]): List of ids to get.
433
+
434
+ Returns:
435
+ List[Document]: Document results for search.
436
+ """
437
+ res = self.obvector.get(
438
+ table_name=self.table_name,
439
+ ids=ids,
440
+ output_column_name=[
441
+ self.text_field,
442
+ self.metadata_field,
443
+ self.primary_field,
444
+ ],
445
+ )
446
+ return [
447
+ Document(
448
+ id=r[2],
449
+ page_content=r[0],
450
+ metadata=(
451
+ json.loads(r[1])
452
+ if isinstance(r[1], str) or isinstance(r[1], bytes)
453
+ else r[1]
454
+ ),
455
+ )
456
+ for r in res.fetchall()
457
+ ]
458
+
459
+ def similarity_search(
460
+ self,
461
+ query: str,
462
+ k: int = 10,
463
+ param: Optional[dict] = None,
464
+ fltr: Optional[str] = None,
465
+ **kwargs: Any,
466
+ ) -> list[Document]:
467
+ """Perform a similarity search against the query string.
468
+
469
+ Args:
470
+ query (str): The text to search.
471
+ k (int, optional): How many results to return. Defaults to 10.
472
+ param (Optional[dict]): The search params for the index type.
473
+ Defaults to None. Refer to `DEFAULT_OCEANBASE_HNSW_SEARCH_PARAM`
474
+ for example.
475
+ fltr (Optional[str]): Boolean filter. Defaults to None.
476
+
477
+ Returns:
478
+ List[Document]: Document results for search.
479
+ """
480
+ if k < 0:
481
+ return []
482
+
483
+ query_vector = self.embedding_function.embed_query(query)
484
+ return self.similarity_search_by_vector(
485
+ embedding=query_vector, k=k, param=param, fltr=fltr, **kwargs
486
+ )
487
+
488
+ def similarity_search_with_score(
489
+ self,
490
+ query: str,
491
+ k: int = 10,
492
+ param: Optional[dict] = None,
493
+ fltr: Optional[str] = None,
494
+ **kwargs: Any,
495
+ ) -> List[Tuple[Document, float]]:
496
+ """Perform a search on a query string and return results with score.
497
+
498
+ Args:
499
+ query (str): The text being searched.
500
+ k (int, optional): How many results to return. Defaults to 10.
501
+ param (Optional[dict]): The search params for the index type.
502
+ Defaults to None. Refer to `DEFAULT_OCEANBASE_HNSW_SEARCH_PARAM`
503
+ for example.
504
+ fltr (Optional[str]): Boolean filter. Defaults to None.
505
+
506
+ Returns:
507
+ List[Tuple[Document, float]]: Document results with score for search.
508
+ """
509
+ if k < 0:
510
+ return []
511
+
512
+ query_vector = self.embedding_function.embed_query(query)
513
+ return self.similarity_search_with_score_by_vector(
514
+ embedding=query_vector, k=k, param=param, fltr=fltr, **kwargs
515
+ )
516
+
517
+ def similarity_search_by_vector(
518
+ self,
519
+ embedding: List[float],
520
+ k: int = 4,
521
+ param: Optional[dict] = None,
522
+ fltr: Optional[str] = None,
523
+ **kwargs: Any,
524
+ ) -> List[Document]:
525
+ """Perform a similarity search against the query string.
526
+
527
+ Args:
528
+ embedding (List[float]): The embedding vector to search.
529
+ k (int, optional): How many results to return. Defaults to 10.
530
+ param (Optional[dict]): The search params for the index type.
531
+ Defaults to None. Refer to `DEFAULT_OCEANBASE_HNSW_SEARCH_PARAM`
532
+ for example.
533
+ fltr (Optional[str]): Boolean filter. Defaults to None.
534
+
535
+ Returns:
536
+ List[Document]: Document results for search.
537
+ """
538
+ if k < 0:
539
+ return []
540
+
541
+ search_param = (
542
+ param if param is not None else DEFAULT_OCEANBASE_HNSW_SEARCH_PARAM
543
+ )
544
+ ef_search = search_param.get(
545
+ "efSearch", DEFAULT_OCEANBASE_HNSW_SEARCH_PARAM["efSearch"]
546
+ )
547
+ if ef_search != self.hnsw_ef_search:
548
+ self.obvector.set_ob_hnsw_ef_search(ef_search)
549
+ self.hnsw_ef_search = ef_search
550
+
551
+ res = self.obvector.ann_search(
552
+ table_name=self.table_name,
553
+ vec_data=(embedding if not self.normalize else self._normalize(embedding)),
554
+ vec_column_name=self.vector_field,
555
+ distance_func=self._parse_metric_type_str_to_dist_func(),
556
+ topk=k,
557
+ output_column_names=[
558
+ self.text_field,
559
+ self.metadata_field,
560
+ self.primary_field,
561
+ ],
562
+ where_clause=([text(fltr)] if fltr is not None else None),
563
+ **kwargs,
564
+ )
565
+ return [
566
+ Document(
567
+ id=r[2],
568
+ page_content=r[0],
569
+ metadata=json.loads(r[1]),
570
+ )
571
+ for r in res.fetchall()
572
+ ]
573
+
574
+ def similarity_search_with_score_by_vector(
575
+ self,
576
+ embedding: List[float],
577
+ k: int = 10,
578
+ param: Optional[dict] = None,
579
+ fltr: Optional[str] = None,
580
+ **kwargs: Any,
581
+ ) -> List[Tuple[Document, float]]:
582
+ """Perform a search on a query string and return results with score.
583
+
584
+ Args:
585
+ embedding (List[float]): The embedding vector being searched.
586
+ k (int, optional): The amount of results to return. Defaults to 10.
587
+ param (Optional[dict]): The search params for the index type.
588
+ Defaults to None. Refer to `DEFAULT_OCEANBASE_HNSW_SEARCH_PARAM`
589
+ for example.
590
+ fltr (Optional[str]): Boolean filter. Defaults to None.
591
+
592
+ Returns:
593
+ List[Tuple[Document, float]]: Document results with score for search.
594
+ """
595
+ if k < 0:
596
+ return []
597
+
598
+ search_param = (
599
+ param if param is not None else DEFAULT_OCEANBASE_HNSW_SEARCH_PARAM
600
+ )
601
+ ef_search = search_param.get(
602
+ "efSearch", DEFAULT_OCEANBASE_HNSW_SEARCH_PARAM["efSearch"]
603
+ )
604
+ if ef_search != self.hnsw_ef_search:
605
+ self.obvector.set_ob_hnsw_ef_search(ef_search)
606
+ self.hnsw_ef_search = ef_search
607
+
608
+ res = self.obvector.ann_search(
609
+ table_name=self.table_name,
610
+ vec_data=(embedding if not self.normalize else self._normalize(embedding)),
611
+ vec_column_name=self.vector_field,
612
+ distance_func=self._parse_metric_type_str_to_dist_func(),
613
+ with_dist=True,
614
+ topk=k,
615
+ output_column_names=[
616
+ self.text_field,
617
+ self.metadata_field,
618
+ self.primary_field,
619
+ ],
620
+ where_clause=([text(fltr)] if fltr is not None else None),
621
+ **kwargs,
622
+ )
623
+ return [
624
+ (
625
+ Document(
626
+ id=r[2],
627
+ page_content=r[0],
628
+ metadata=json.loads(r[1]),
629
+ ),
630
+ r[3],
631
+ )
632
+ for r in res.fetchall()
633
+ ]
634
+
635
+ def max_marginal_relevance_search(
636
+ self,
637
+ query: str,
638
+ k: int = 4,
639
+ fetch_k: int = 20,
640
+ lambda_mult: float = 0.5,
641
+ **kwargs: Any,
642
+ ) -> List[Document]:
643
+ raise NotImplementedError
644
+
645
+ def max_marginal_relevance_search_by_vector(
646
+ self,
647
+ embedding: List[float],
648
+ k: int = 4,
649
+ fetch_k: int = 20,
650
+ lambda_mult: float = 0.5,
651
+ **kwargs: Any,
652
+ ) -> List[Document]:
653
+ raise NotImplementedError
654
+
655
+ @classmethod
656
+ def from_texts(
657
+ cls,
658
+ texts: list[str],
659
+ embedding: Embeddings,
660
+ metadatas: Optional[list[dict]] = None,
661
+ table_name: str = DEFAULT_OCEANBASE_VECTOR_TABLE_NAME,
662
+ connection_args: Optional[dict[str, Any]] = None,
663
+ vidx_metric_type: str = DEFAULT_OCEANBASE_VECTOR_METRIC_TYPE,
664
+ vidx_algo_params: Optional[dict] = None,
665
+ drop_old: bool = False,
666
+ *,
667
+ ids: Optional[List[str]] = None,
668
+ extra_columns: Optional[List[Column]] = None,
669
+ normalize: bool = False,
670
+ extras: Optional[List[dict]] = None,
671
+ **kwargs: Any,
672
+ ) -> "OceanbaseVectorStore":
673
+ """Create a OceanBase table, indexes it with HNSW, and insert data.
674
+
675
+ Args:
676
+ texts (List[str]): Text data.
677
+ embedding (Embeddings): Embedding function.
678
+ metadatas (Optional[List[dict]]): Metadata for each text if it exists.
679
+ Defaults to None.
680
+ table_name (str): Table name to use. Defaults to "langchain_vector".
681
+ connection_args (Optional[dict[str, Any]]): Refer to
682
+ `DEFAULT_OCEANBASE_CONNECTION` for example.
683
+ vidx_metric_type (str): Metric method of distance between vectors.
684
+ This parameter takes values in `l2` and `inner_product`.
685
+ Defaults to `l2`.
686
+ vidx_algo_params (Optional[dict]): Which index params to use. Now OceanBase
687
+ supports HNSW only. Refer to `DEFAULT_OCEANBASE_HNSW_BUILD_PARAM`
688
+ for example.
689
+ drop_old (bool): Whether to drop the current table. Defaults
690
+ to False.
691
+ ids (Optional[List[str]]): List of text ids. Defaults to None.
692
+ extra_columns (Optional[List[Column]]): Extra columns to add to the table.
693
+ extras (Optional[List[dict]]): Extra data to insert. Defaults to None.
694
+
695
+ Returns:
696
+ OceanBase: OceanBase Vector Store
697
+ """
698
+ oceanbase = cls(
699
+ embedding_function=embedding,
700
+ table_name=table_name,
701
+ connection_args=connection_args,
702
+ vidx_metric_type=vidx_metric_type,
703
+ vidx_algo_params=vidx_algo_params,
704
+ drop_old=drop_old,
705
+ extra_columns=extra_columns,
706
+ normalize=normalize,
707
+ **kwargs,
708
+ )
709
+ oceanbase.add_texts(texts, metadatas, ids=ids, extras=extras)
710
+ return oceanbase
711
+
712
+ def _select_relevance_score_fn(self) -> Callable[[float], float]:
713
+ """
714
+ Select the relevance score function based on the distance strategy.
715
+ """
716
+ if self.vidx_metric_type == "inner_product":
717
+ return _neg_inner_product_similarity
718
+ elif self.vidx_metric_type == "l2":
719
+ return _euclidean_similarity
720
+ else:
721
+ raise ValueError(
722
+ "No supported normalization function"
723
+ f" for distance_strategy of {self.vidx_metric_type}."
724
+ )
@@ -0,0 +1,74 @@
1
+ [build-system]
2
+ requires = ["poetry-core>=1.0.0"]
3
+ build-backend = "poetry.core.masonry.api"
4
+
5
+ [tool.poetry]
6
+ name = "langchain-oceanbase"
7
+ version = "0.1.0"
8
+ description = "An integration package connecting OceanBase and LangChain"
9
+ authors = ["shanhaikang.shk <shanhaikang.shk@oceanbase.com>"]
10
+ readme = "README.md"
11
+ license = "MIT"
12
+
13
+ [tool.mypy]
14
+ disallow_untyped_defs = "True"
15
+
16
+ [tool.poetry.urls]
17
+ "Source Code" = "https://github.com/langchain-ai/langchain/tree/master/libs/partners/oceanbase"
18
+ "Release Notes" = "https://github.com/langchain-ai/langchain/releases?q=tag%3A%22oceanbase%3D%3D0%22&expanded=true"
19
+
20
+ [tool.poetry.dependencies]
21
+ python = ">=3.9,<4.0"
22
+ langchain-core = "^0.3.15"
23
+ pyobvector = "^0.1.17"
24
+
25
+ [tool.ruff.lint]
26
+ select = ["E", "F", "I", "T201"]
27
+
28
+ [tool.coverage.run]
29
+ omit = ["tests/*"]
30
+
31
+ [tool.pytest.ini_options]
32
+ addopts = "--strict-markers --strict-config --durations=5"
33
+ markers = [
34
+ "compile: mark placeholder test used to compile integration tests without running them",
35
+ ]
36
+ asyncio_mode = "auto"
37
+
38
+ [tool.poetry.group.test]
39
+ optional = true
40
+
41
+ [tool.poetry.group.codespell]
42
+ optional = true
43
+
44
+ [tool.poetry.group.test_integration]
45
+ optional = true
46
+
47
+ [tool.poetry.group.lint]
48
+ optional = true
49
+
50
+ [tool.poetry.group.dev]
51
+ optional = true
52
+
53
+ [tool.poetry.group.dev.dependencies]
54
+ ipykernel = "^6.29.5"
55
+
56
+ [tool.poetry.group.test.dependencies]
57
+ pytest = "^7.4.3"
58
+ pytest-asyncio = "^0.23.2"
59
+ pytest-socket = "^0.7.0"
60
+ pytest-watcher = "^0.3.4"
61
+ langchain-tests = "^0.3.5"
62
+ langchain-community = "^0.3.12"
63
+ dashscope = "^1.20.14"
64
+
65
+ [tool.poetry.group.codespell.dependencies]
66
+ codespell = "^2.2.6"
67
+
68
+ [tool.poetry.group.test_integration.dependencies]
69
+
70
+ [tool.poetry.group.lint.dependencies]
71
+ ruff = "^0.5"
72
+
73
+ [tool.poetry.group.typing.dependencies]
74
+ mypy = "^1.10"