llama-index-vector-stores-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.
- llama_index_vector_stores_oceanbase-0.1.0/PKG-INFO +80 -0
- llama_index_vector_stores_oceanbase-0.1.0/README.md +61 -0
- llama_index_vector_stores_oceanbase-0.1.0/llama_index/BUILD +4 -0
- llama_index_vector_stores_oceanbase-0.1.0/llama_index/py.typed +0 -0
- llama_index_vector_stores_oceanbase-0.1.0/llama_index/vector_stores/oceanbase/BUILD +1 -0
- llama_index_vector_stores_oceanbase-0.1.0/llama_index/vector_stores/oceanbase/__init__.py +3 -0
- llama_index_vector_stores_oceanbase-0.1.0/llama_index/vector_stores/oceanbase/base.py +529 -0
- llama_index_vector_stores_oceanbase-0.1.0/pyproject.toml +57 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: llama-index-vector-stores-oceanbase
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: llama-index vector-stores oceanbase integration
|
|
5
|
+
License: MIT
|
|
6
|
+
Author: GITHUBear
|
|
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
|
+
Requires-Dist: llama-index-core (>=0.11.0,<0.12.0)
|
|
16
|
+
Requires-Dist: pyobvector (>=0.1.6,<0.2.0)
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
|
|
19
|
+
# LlamaIndex Vector-Stores Integration: Oceanbase
|
|
20
|
+
|
|
21
|
+
[OceanBase Database](https://github.com/oceanbase/oceanbase) is a distributed relational database.
|
|
22
|
+
It is developed entirely by Ant Group. The OceanBase Database is built on a common server cluster.
|
|
23
|
+
Based on the Paxos protocol and its distributed structure, the OceanBase Database provides high availability and linear scalability.
|
|
24
|
+
|
|
25
|
+
OceanBase currently has the ability to store vectors. Users can easily perform the following operations with SQL:
|
|
26
|
+
|
|
27
|
+
- Create a table containing vector type fields;
|
|
28
|
+
- Create a vector index table based on the HNSW algorithm;
|
|
29
|
+
- Perform vector approximate nearest neighbor queries;
|
|
30
|
+
- ...
|
|
31
|
+
|
|
32
|
+
## Limitations
|
|
33
|
+
|
|
34
|
+
The vector storage capability of OceanBase is still being enhanced, and currently has the following limitations:
|
|
35
|
+
|
|
36
|
+
- Cosine vector distance function is not yet supported for vector index. Currently only inner product distance and Euclidean distance are supported, and Euclidean distance is used by default.
|
|
37
|
+
- It should be noted that `OceanBase` currently only supports post-filtering (i.e., filtering based on metadata after performing an approximate nearest neighbor search).
|
|
38
|
+
|
|
39
|
+
## Install dependencies
|
|
40
|
+
|
|
41
|
+
We use `pyobvector` to integrate OceanBase vector store into LlamaIndex.
|
|
42
|
+
So it is necessary to install it with `pip install pyobvector` before starting.
|
|
43
|
+
|
|
44
|
+
## Setup OceanBase
|
|
45
|
+
|
|
46
|
+
We recommend using Docker to deploy OceanBase:
|
|
47
|
+
|
|
48
|
+
```shell
|
|
49
|
+
docker run --name=ob433 -e MODE=slim -p 2881:2881 -d oceanbase/oceanbase-ce:4.3.3.0-100000142024101215
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Usage
|
|
53
|
+
|
|
54
|
+
```sh
|
|
55
|
+
%pip install llama-index-vector-stores-oceanbase
|
|
56
|
+
%pip install llama-index
|
|
57
|
+
# choose dashscope as embedding and llm model, your can also use default openai or other model to test
|
|
58
|
+
%pip install llama-index-embeddings-dashscope
|
|
59
|
+
%pip install llama-index-llms-dashscope
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
from llama_index.vector_stores.oceanbase import OceanBaseVectorStore
|
|
64
|
+
from pyobvector import ObVecClient
|
|
65
|
+
|
|
66
|
+
client = ObVecClient()
|
|
67
|
+
|
|
68
|
+
client.perform_raw_text_sql(
|
|
69
|
+
"ALTER SYSTEM ob_vector_memory_limit_percentage = 30"
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
# Initialize OceanBaseVectorStore
|
|
73
|
+
oceanbase = OceanBaseVectorStore(
|
|
74
|
+
client=client,
|
|
75
|
+
dim=1536,
|
|
76
|
+
drop_old=True,
|
|
77
|
+
normalize=True,
|
|
78
|
+
)
|
|
79
|
+
```
|
|
80
|
+
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# LlamaIndex Vector-Stores Integration: Oceanbase
|
|
2
|
+
|
|
3
|
+
[OceanBase Database](https://github.com/oceanbase/oceanbase) is a distributed relational database.
|
|
4
|
+
It is developed entirely by Ant Group. The OceanBase Database is built on a common server cluster.
|
|
5
|
+
Based on the Paxos protocol and its distributed structure, the OceanBase Database provides high availability and linear scalability.
|
|
6
|
+
|
|
7
|
+
OceanBase currently has the ability to store vectors. Users can easily perform the following operations with SQL:
|
|
8
|
+
|
|
9
|
+
- Create a table containing vector type fields;
|
|
10
|
+
- Create a vector index table based on the HNSW algorithm;
|
|
11
|
+
- Perform vector approximate nearest neighbor queries;
|
|
12
|
+
- ...
|
|
13
|
+
|
|
14
|
+
## Limitations
|
|
15
|
+
|
|
16
|
+
The vector storage capability of OceanBase is still being enhanced, and currently has the following limitations:
|
|
17
|
+
|
|
18
|
+
- Cosine vector distance function is not yet supported for vector index. Currently only inner product distance and Euclidean distance are supported, and Euclidean distance is used by default.
|
|
19
|
+
- It should be noted that `OceanBase` currently only supports post-filtering (i.e., filtering based on metadata after performing an approximate nearest neighbor search).
|
|
20
|
+
|
|
21
|
+
## Install dependencies
|
|
22
|
+
|
|
23
|
+
We use `pyobvector` to integrate OceanBase vector store into LlamaIndex.
|
|
24
|
+
So it is necessary to install it with `pip install pyobvector` before starting.
|
|
25
|
+
|
|
26
|
+
## Setup OceanBase
|
|
27
|
+
|
|
28
|
+
We recommend using Docker to deploy OceanBase:
|
|
29
|
+
|
|
30
|
+
```shell
|
|
31
|
+
docker run --name=ob433 -e MODE=slim -p 2881:2881 -d oceanbase/oceanbase-ce:4.3.3.0-100000142024101215
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Usage
|
|
35
|
+
|
|
36
|
+
```sh
|
|
37
|
+
%pip install llama-index-vector-stores-oceanbase
|
|
38
|
+
%pip install llama-index
|
|
39
|
+
# choose dashscope as embedding and llm model, your can also use default openai or other model to test
|
|
40
|
+
%pip install llama-index-embeddings-dashscope
|
|
41
|
+
%pip install llama-index-llms-dashscope
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
from llama_index.vector_stores.oceanbase import OceanBaseVectorStore
|
|
46
|
+
from pyobvector import ObVecClient
|
|
47
|
+
|
|
48
|
+
client = ObVecClient()
|
|
49
|
+
|
|
50
|
+
client.perform_raw_text_sql(
|
|
51
|
+
"ALTER SYSTEM ob_vector_memory_limit_percentage = 30"
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
# Initialize OceanBaseVectorStore
|
|
55
|
+
oceanbase = OceanBaseVectorStore(
|
|
56
|
+
client=client,
|
|
57
|
+
dim=1536,
|
|
58
|
+
drop_old=True,
|
|
59
|
+
normalize=True,
|
|
60
|
+
)
|
|
61
|
+
```
|
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
python_sources()
|
|
@@ -0,0 +1,529 @@
|
|
|
1
|
+
"""OceanBase Vector Store."""
|
|
2
|
+
|
|
3
|
+
import math
|
|
4
|
+
import logging
|
|
5
|
+
import json
|
|
6
|
+
from typing import Any, Optional, List
|
|
7
|
+
|
|
8
|
+
from llama_index.core.bridge.pydantic import PrivateAttr
|
|
9
|
+
from llama_index.core.utils import iter_batch
|
|
10
|
+
from llama_index.core.schema import BaseNode, MetadataMode
|
|
11
|
+
from llama_index.core.vector_stores.types import (
|
|
12
|
+
BasePydanticVectorStore,
|
|
13
|
+
MetadataFilters,
|
|
14
|
+
FilterOperator,
|
|
15
|
+
VectorStoreQuery,
|
|
16
|
+
VectorStoreQueryResult,
|
|
17
|
+
)
|
|
18
|
+
from llama_index.core.vector_stores.utils import (
|
|
19
|
+
metadata_dict_to_node,
|
|
20
|
+
node_to_metadata_dict,
|
|
21
|
+
)
|
|
22
|
+
from sqlalchemy import Column, Table, String, JSON, text, func
|
|
23
|
+
from sqlalchemy.dialects.mysql import LONGTEXT
|
|
24
|
+
from pyobvector import ObVecClient, VECTOR
|
|
25
|
+
import numpy as np
|
|
26
|
+
|
|
27
|
+
logger = logging.getLogger(__name__)
|
|
28
|
+
|
|
29
|
+
DEFAULT_OCEANBASE_BATCH_SIZE = 100
|
|
30
|
+
DEFAULT_OCEANBASE_VECTOR_TABLE_NAME = "llama_vector"
|
|
31
|
+
DEFAULT_OCEANBASE_HNSW_BUILD_PARAM = {"M": 16, "efConstruction": 256}
|
|
32
|
+
DEFAULT_OCEANBASE_HNSW_SEARCH_PARAM = {"efSearch": 64}
|
|
33
|
+
OCEANBASE_SUPPORTED_VECTOR_INDEX_TYPE = "HNSW"
|
|
34
|
+
DEFAULT_OCEANBASE_VECTOR_METRIC_TYPE = "l2"
|
|
35
|
+
|
|
36
|
+
DEFAULT_OCEANBASE_PFIELD = "id"
|
|
37
|
+
DEFAULT_OCEANBASE_DOCID_FIELD = "doc_id"
|
|
38
|
+
DEFAULT_OCEANBASE_VEC_FIELD = "embedding"
|
|
39
|
+
DEFAULT_OCEANBASE_DOC_FIELD = "document"
|
|
40
|
+
DEFAULT_OCEANBASE_METADATA_FIELD = "metadata"
|
|
41
|
+
|
|
42
|
+
DEFAULT_OCEANBASE_VEC_INDEX_NAME = "vidx"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _parse_filter_value(filter_value: any, is_text_match: bool = False):
|
|
46
|
+
if filter_value is None:
|
|
47
|
+
return filter_value
|
|
48
|
+
|
|
49
|
+
if is_text_match:
|
|
50
|
+
return f"'{filter_value!s}%'"
|
|
51
|
+
|
|
52
|
+
if isinstance(filter_value, str):
|
|
53
|
+
return f"'{filter_value!s}'"
|
|
54
|
+
|
|
55
|
+
if isinstance(filter_value, list):
|
|
56
|
+
if all(isinstance(item, str) for item in filter_value):
|
|
57
|
+
return "(" + ",".join([f"'{v!s}'" for v in filter_value]) + ")"
|
|
58
|
+
return "(" + ",".join([str(v) for v in filter_value]) + ")"
|
|
59
|
+
|
|
60
|
+
return str(filter_value)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _euclidean_similarity(distance: float) -> float:
|
|
64
|
+
return 1.0 - distance / math.sqrt(2)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _neg_inner_product_similarity(distance: float) -> float:
|
|
68
|
+
return -distance
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _normalize(vector: List[float]) -> List[float]:
|
|
72
|
+
arr = np.array(vector)
|
|
73
|
+
norm = np.linalg.norm(arr)
|
|
74
|
+
arr = arr / norm
|
|
75
|
+
return arr.tolist()
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class OceanBaseVectorStore(BasePydanticVectorStore):
|
|
79
|
+
"""OceanBase Vector Store.
|
|
80
|
+
|
|
81
|
+
You need to install `pyobvector` and run a standalone observer or OceanBase cluster.
|
|
82
|
+
|
|
83
|
+
See the following documentation for how to deploy OceanBase:
|
|
84
|
+
https://github.com/oceanbase/oceanbase-doc/blob/V4.3.1/en-US/400.deploy/500.deploy-oceanbase-database-community-edition/100.deployment-overview.md
|
|
85
|
+
|
|
86
|
+
IF USING L2/INNER_PRODUCT metric, IT IS HIGHLY SUGGESTED TO set `normalize = True`.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
_client (ObVecClient): OceanBase vector store client.
|
|
90
|
+
Refer to `pyobvector` for more information.
|
|
91
|
+
dim (int): Dimension of embedding vector.
|
|
92
|
+
table_name (str): Which table name to use. Defaults to "llama_vector".
|
|
93
|
+
vidx_metric_type (str): Metric method of distance between vectors.
|
|
94
|
+
This parameter takes values in `l2` and `inner_product`. Defaults to `l2`.
|
|
95
|
+
vidx_algo_params (Optional[dict]): Which index params to use. Now OceanBase
|
|
96
|
+
supports HNSW only. Refer to `DEFAULT_OCEANBASE_HNSW_BUILD_PARAM`
|
|
97
|
+
for example.
|
|
98
|
+
drop_old (bool): Whether to drop the current table. Defaults
|
|
99
|
+
to False.
|
|
100
|
+
primary_field (str): Name of the primary key column. Defaults to "id".
|
|
101
|
+
doc_id_field (str): Name of the doc id column. Defaults to "doc_id".
|
|
102
|
+
vector_field (str): Name of the vector column. Defaults to "embedding".
|
|
103
|
+
text_field (str): Name of the text column. Defaults to "document".
|
|
104
|
+
metadata_field (Optional[str]): Name of the metadata column.
|
|
105
|
+
Defaults to "metadata". When `metadata_field` is specified,
|
|
106
|
+
the document's metadata will store as json.
|
|
107
|
+
vidx_name (str): Name of the vector index table.
|
|
108
|
+
partitions (ObPartition): Partition strategy of table. Refer to `pyobvector`'s
|
|
109
|
+
documentation for more examples.
|
|
110
|
+
extra_columns (Optional[List[Column]]): Extra sqlalchemy columns
|
|
111
|
+
to add to the table.
|
|
112
|
+
normalize (bool): normalize vector or not.
|
|
113
|
+
|
|
114
|
+
Examples:
|
|
115
|
+
`pip install llama-index-vector-stores-oceanbase`
|
|
116
|
+
|
|
117
|
+
```python
|
|
118
|
+
from llama_index.vector_stores.oceanbase import OceanBaseVectorStore
|
|
119
|
+
|
|
120
|
+
# Setup ObVecClient
|
|
121
|
+
from pyobvector import ObVecClient
|
|
122
|
+
|
|
123
|
+
client = ObVecClient(
|
|
124
|
+
uri=os.getenv("OB_URI", "127.0.0.1:2881"),
|
|
125
|
+
user=os.getenv("OB_USER", "root@test"),
|
|
126
|
+
password=os.getenv("OB_PWD", ""),
|
|
127
|
+
db_name=os.getenv("OB_DBNAME", "test"),
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
# Initialize OceanBaseVectorStore
|
|
131
|
+
oceanbase = OceanBaseVectorStore(
|
|
132
|
+
client=client,
|
|
133
|
+
dim=1024,
|
|
134
|
+
)
|
|
135
|
+
```
|
|
136
|
+
"""
|
|
137
|
+
|
|
138
|
+
stores_text: bool = True
|
|
139
|
+
|
|
140
|
+
_client: ObVecClient = PrivateAttr()
|
|
141
|
+
_dim: int = PrivateAttr()
|
|
142
|
+
_table_name: str = PrivateAttr()
|
|
143
|
+
_vidx_metric_type: str = PrivateAttr()
|
|
144
|
+
_vidx_algo_params: dict = PrivateAttr()
|
|
145
|
+
_primary_field: str = PrivateAttr()
|
|
146
|
+
_doc_id_field: str = PrivateAttr()
|
|
147
|
+
_vector_field: str = PrivateAttr()
|
|
148
|
+
_text_field: str = PrivateAttr()
|
|
149
|
+
_metadata_field: str = PrivateAttr()
|
|
150
|
+
_vidx_name: str = PrivateAttr()
|
|
151
|
+
_partitions: Optional[Any] = PrivateAttr()
|
|
152
|
+
_extra_columns: Optional[List[Column]] = PrivateAttr()
|
|
153
|
+
_hnsw_ef_search: int = PrivateAttr()
|
|
154
|
+
_normalize: bool = PrivateAttr()
|
|
155
|
+
|
|
156
|
+
def __init__(
|
|
157
|
+
self,
|
|
158
|
+
client: ObVecClient,
|
|
159
|
+
dim: int,
|
|
160
|
+
table_name: str = DEFAULT_OCEANBASE_VECTOR_TABLE_NAME,
|
|
161
|
+
vidx_metric_type: str = DEFAULT_OCEANBASE_VECTOR_METRIC_TYPE,
|
|
162
|
+
vidx_algo_params: Optional[dict] = None,
|
|
163
|
+
drop_old: bool = False,
|
|
164
|
+
*,
|
|
165
|
+
primary_field: str = DEFAULT_OCEANBASE_PFIELD,
|
|
166
|
+
doc_id_field: str = DEFAULT_OCEANBASE_DOCID_FIELD,
|
|
167
|
+
vector_field: str = DEFAULT_OCEANBASE_VEC_FIELD,
|
|
168
|
+
text_field: str = DEFAULT_OCEANBASE_DOC_FIELD,
|
|
169
|
+
metadata_field: str = DEFAULT_OCEANBASE_METADATA_FIELD,
|
|
170
|
+
vidx_name: str = DEFAULT_OCEANBASE_VEC_INDEX_NAME,
|
|
171
|
+
partitions: Optional[Any] = None,
|
|
172
|
+
extra_columns: Optional[List[Column]] = None,
|
|
173
|
+
normalize: bool = False,
|
|
174
|
+
**kwargs,
|
|
175
|
+
):
|
|
176
|
+
super().__init__()
|
|
177
|
+
|
|
178
|
+
try:
|
|
179
|
+
from pyobvector import ObVecClient
|
|
180
|
+
except ImportError:
|
|
181
|
+
raise ImportError(
|
|
182
|
+
"Could not import pyobvector package. "
|
|
183
|
+
"Please install it with `pip install pyobvector`."
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
if client is not None:
|
|
187
|
+
if not isinstance(client, ObVecClient):
|
|
188
|
+
raise ValueError("client must be of type pyobvector.ObVecClient")
|
|
189
|
+
else:
|
|
190
|
+
raise ValueError("client not specified")
|
|
191
|
+
|
|
192
|
+
self._dim = dim
|
|
193
|
+
self._client: ObVecClient = client
|
|
194
|
+
self._table_name = table_name
|
|
195
|
+
self._extra_columns = extra_columns
|
|
196
|
+
self._vidx_metric_type = vidx_metric_type.lower()
|
|
197
|
+
if self._vidx_metric_type not in ("l2", "inner_product"):
|
|
198
|
+
raise ValueError(
|
|
199
|
+
"`vidx_metric_type` should be set in `l2`/`inner_product`."
|
|
200
|
+
)
|
|
201
|
+
self._vidx_algo_params = vidx_algo_params or DEFAULT_OCEANBASE_HNSW_BUILD_PARAM
|
|
202
|
+
|
|
203
|
+
self._primary_field = primary_field
|
|
204
|
+
self._doc_id_field = doc_id_field
|
|
205
|
+
self._vector_field = vector_field
|
|
206
|
+
self._text_field = text_field
|
|
207
|
+
self._metadata_field = metadata_field
|
|
208
|
+
self._vidx_name = vidx_name
|
|
209
|
+
self._partition = partitions
|
|
210
|
+
self._hnsw_ef_search = -1
|
|
211
|
+
self._normalize = normalize
|
|
212
|
+
|
|
213
|
+
if drop_old:
|
|
214
|
+
self._client.drop_table_if_exist(table_name=self._table_name)
|
|
215
|
+
|
|
216
|
+
self._create_table_with_index()
|
|
217
|
+
|
|
218
|
+
def _enhance_filter_key(self, filter_key: str) -> str:
|
|
219
|
+
return f"{self._metadata_field}->'$.{filter_key}'"
|
|
220
|
+
|
|
221
|
+
def _to_oceanbase_filter(
|
|
222
|
+
self, metadata_filters: Optional[MetadataFilters] = None
|
|
223
|
+
) -> str:
|
|
224
|
+
filters = []
|
|
225
|
+
for filter in metadata_filters.filters:
|
|
226
|
+
if isinstance(filter, MetadataFilters):
|
|
227
|
+
filters.append(f"({self._to_oceanbase_filter(filter)})")
|
|
228
|
+
continue
|
|
229
|
+
|
|
230
|
+
filter_value = _parse_filter_value(filter.value)
|
|
231
|
+
if filter_value is None and filter.operator != FilterOperator.IS_EMPTY:
|
|
232
|
+
continue
|
|
233
|
+
|
|
234
|
+
if filter.operator == FilterOperator.EQ:
|
|
235
|
+
filters.append(f"{self._enhance_filter_key(filter.key)}={filter_value}")
|
|
236
|
+
elif filter.operator == FilterOperator.GT:
|
|
237
|
+
filters.append(f"{self._enhance_filter_key(filter.key)}>{filter_value}")
|
|
238
|
+
elif filter.operator == FilterOperator.LT:
|
|
239
|
+
filters.append(f"{self._enhance_filter_key(filter.key)}<{filter_value}")
|
|
240
|
+
elif filter.operator == FilterOperator.NE:
|
|
241
|
+
filters.append(
|
|
242
|
+
f"{self._enhance_filter_key(filter.key)}!={filter_value}"
|
|
243
|
+
)
|
|
244
|
+
elif filter.operator == FilterOperator.GTE:
|
|
245
|
+
filters.append(
|
|
246
|
+
f"{self._enhance_filter_key(filter.key)}>={filter_value}"
|
|
247
|
+
)
|
|
248
|
+
elif filter.operator == FilterOperator.LTE:
|
|
249
|
+
filters.append(
|
|
250
|
+
f"{self._enhance_filter_key(filter.key)}<={filter_value}"
|
|
251
|
+
)
|
|
252
|
+
elif filter.operator == FilterOperator.IN:
|
|
253
|
+
filters.append(
|
|
254
|
+
f"{self._enhance_filter_key(filter.key)} in {filter_value}"
|
|
255
|
+
)
|
|
256
|
+
elif filter.operator == FilterOperator.NIN:
|
|
257
|
+
filters.append(
|
|
258
|
+
f"{self._enhance_filter_key(filter.key)} not in {filter_value}"
|
|
259
|
+
)
|
|
260
|
+
elif filter.operator == FilterOperator.TEXT_MATCH:
|
|
261
|
+
filters.append(
|
|
262
|
+
f"{self._enhance_filter_key(filter.key)} like {_parse_filter_value(filter.value, True)}"
|
|
263
|
+
)
|
|
264
|
+
elif filter.operator == FilterOperator.IS_EMPTY:
|
|
265
|
+
filters.append(f"{self._enhance_filter_key(filter.key)} IS NULL")
|
|
266
|
+
else:
|
|
267
|
+
raise ValueError(
|
|
268
|
+
f'Operator {filter.operator} ("{filter.operator.value}") is not supported by OceanBase.'
|
|
269
|
+
)
|
|
270
|
+
return f" {metadata_filters.condition.value} ".join(filters)
|
|
271
|
+
|
|
272
|
+
def _parse_metric_type_str_to_dist_func(self) -> Any:
|
|
273
|
+
if self._vidx_metric_type == "l2":
|
|
274
|
+
return func.l2_distance
|
|
275
|
+
if self._vidx_metric_type == "cosine":
|
|
276
|
+
return func.cosine_distance
|
|
277
|
+
if self._vidx_metric_type == "inner_product":
|
|
278
|
+
return func.negative_inner_product
|
|
279
|
+
raise ValueError(f"Invalid vector index metric type: {self._vidx_metric_type}")
|
|
280
|
+
|
|
281
|
+
def _load_table(self) -> None:
|
|
282
|
+
table = Table(
|
|
283
|
+
self._table_name,
|
|
284
|
+
self._client.metadata_obj,
|
|
285
|
+
autoload_with=self._client.engine,
|
|
286
|
+
)
|
|
287
|
+
column_names = [column.name for column in table.columns]
|
|
288
|
+
optional_len = len(self._extra_columns or [])
|
|
289
|
+
assert len(column_names) == (5 + optional_len)
|
|
290
|
+
|
|
291
|
+
logging.info(f"load exist table with {column_names} columns")
|
|
292
|
+
self._primary_field = column_names[0]
|
|
293
|
+
self._doc_id_field = column_names[1]
|
|
294
|
+
self._vector_field = column_names[2]
|
|
295
|
+
self._text_field = column_names[3]
|
|
296
|
+
self._metadata_field = column_names[4]
|
|
297
|
+
|
|
298
|
+
def _create_table_with_index(self):
|
|
299
|
+
if self._client.check_table_exists(self._table_name):
|
|
300
|
+
self._load_table()
|
|
301
|
+
return
|
|
302
|
+
|
|
303
|
+
cols = [
|
|
304
|
+
Column(
|
|
305
|
+
self._primary_field, String(4096), primary_key=True, autoincrement=False
|
|
306
|
+
),
|
|
307
|
+
Column(self._doc_id_field, String(4096)),
|
|
308
|
+
Column(self._vector_field, VECTOR(self._dim)),
|
|
309
|
+
Column(self._text_field, LONGTEXT),
|
|
310
|
+
Column(self._metadata_field, JSON),
|
|
311
|
+
]
|
|
312
|
+
if self._extra_columns is not None:
|
|
313
|
+
cols.extend(self._extra_columns)
|
|
314
|
+
|
|
315
|
+
vidx_params = self._client.prepare_index_params()
|
|
316
|
+
vidx_params.add_index(
|
|
317
|
+
field_name=self._vector_field,
|
|
318
|
+
index_type=OCEANBASE_SUPPORTED_VECTOR_INDEX_TYPE,
|
|
319
|
+
index_name=self._vidx_name,
|
|
320
|
+
metric_type=self._vidx_metric_type,
|
|
321
|
+
params=self._vidx_algo_params,
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
self._client.create_table_with_index_params(
|
|
325
|
+
table_name=self._table_name,
|
|
326
|
+
columns=cols,
|
|
327
|
+
indexes=None,
|
|
328
|
+
vidxs=vidx_params,
|
|
329
|
+
partitions=self._partition,
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
@classmethod
|
|
333
|
+
def class_name(cls) -> str:
|
|
334
|
+
return "OceanBaseVectorStore"
|
|
335
|
+
|
|
336
|
+
@property
|
|
337
|
+
def client(self) -> Any:
|
|
338
|
+
"""Get client."""
|
|
339
|
+
return self._client
|
|
340
|
+
|
|
341
|
+
def get_nodes(
|
|
342
|
+
self,
|
|
343
|
+
node_ids: Optional[List[str]] = None,
|
|
344
|
+
filters: Optional[MetadataFilters] = None,
|
|
345
|
+
) -> List[BaseNode]:
|
|
346
|
+
"""Get nodes from OceanBase.
|
|
347
|
+
|
|
348
|
+
Args:
|
|
349
|
+
node_ids (Optional[List[str]], optional): IDs of nodes to delete.
|
|
350
|
+
Defaults to None.
|
|
351
|
+
filters (Optional[MetadataFilters], optional): Metadata filters.
|
|
352
|
+
Defaults to None.
|
|
353
|
+
|
|
354
|
+
Returns:
|
|
355
|
+
List[BaseNode]: List of text nodes.
|
|
356
|
+
"""
|
|
357
|
+
if filters is not None:
|
|
358
|
+
filter = self._to_oceanbase_filter(filters)
|
|
359
|
+
else:
|
|
360
|
+
filter = None
|
|
361
|
+
|
|
362
|
+
res = self._client.get(
|
|
363
|
+
table_name=self._table_name,
|
|
364
|
+
ids=node_ids,
|
|
365
|
+
where_clause=[text(filter)] if filter is not None else None,
|
|
366
|
+
output_column_name=[
|
|
367
|
+
self._text_field,
|
|
368
|
+
self._metadata_field,
|
|
369
|
+
],
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
return [
|
|
373
|
+
metadata_dict_to_node(
|
|
374
|
+
metadata=(json.loads(r[1]) if not isinstance(r[1], dict) else r[1]),
|
|
375
|
+
text=r[0],
|
|
376
|
+
)
|
|
377
|
+
for r in res.fetchall()
|
|
378
|
+
]
|
|
379
|
+
|
|
380
|
+
def add(
|
|
381
|
+
self,
|
|
382
|
+
nodes: List[BaseNode],
|
|
383
|
+
batch_size: Optional[int] = None,
|
|
384
|
+
extras: Optional[List[dict]] = None,
|
|
385
|
+
) -> List[str]:
|
|
386
|
+
"""Add nodes into OceanBase.
|
|
387
|
+
|
|
388
|
+
Args:
|
|
389
|
+
nodes (List[BaseNode]): List of nodes with embeddings
|
|
390
|
+
to insert.
|
|
391
|
+
batch_size (Optional[int]): Insert nodes in batch.
|
|
392
|
+
extras (Optional[List[dict]]): If `extra_columns` is set
|
|
393
|
+
when initializing `OceanBaseVectorStore`, you can add
|
|
394
|
+
nodes with extra infos.
|
|
395
|
+
|
|
396
|
+
Returns:
|
|
397
|
+
List[str]: List of ids inserted.
|
|
398
|
+
"""
|
|
399
|
+
batch_size = batch_size or DEFAULT_OCEANBASE_BATCH_SIZE
|
|
400
|
+
|
|
401
|
+
extra_data = extras or [{} for _ in nodes]
|
|
402
|
+
if len(nodes) != len(extra_data):
|
|
403
|
+
raise ValueError("nodes size & extras size mismatch")
|
|
404
|
+
|
|
405
|
+
data = [
|
|
406
|
+
{
|
|
407
|
+
self._primary_field: node.id_,
|
|
408
|
+
self._doc_id_field: node.ref_doc_id or None,
|
|
409
|
+
self._vector_field: (
|
|
410
|
+
node.get_embedding()
|
|
411
|
+
if not self._normalize
|
|
412
|
+
else _normalize(node.get_embedding())
|
|
413
|
+
),
|
|
414
|
+
self._text_field: node.get_content(metadata_mode=MetadataMode.NONE),
|
|
415
|
+
self._metadata_field: node_to_metadata_dict(node, remove_text=True),
|
|
416
|
+
**extra,
|
|
417
|
+
}
|
|
418
|
+
for node, extra in zip(nodes, extra_data)
|
|
419
|
+
]
|
|
420
|
+
for data_batch in iter_batch(data, batch_size):
|
|
421
|
+
self._client.insert(self._table_name, data_batch)
|
|
422
|
+
return [node.id_ for node in nodes]
|
|
423
|
+
|
|
424
|
+
def delete(self, ref_doc_id: str, **delete_kwargs: Any) -> None:
|
|
425
|
+
"""Delete nodes using with ref_doc_id.
|
|
426
|
+
|
|
427
|
+
Args:
|
|
428
|
+
ref_doc_id (str): The doc_id of the document to delete.
|
|
429
|
+
"""
|
|
430
|
+
self._client.delete(
|
|
431
|
+
table_name=self._table_name,
|
|
432
|
+
where_clause=[text(f"{self._doc_id_field}='{ref_doc_id}'")],
|
|
433
|
+
)
|
|
434
|
+
|
|
435
|
+
def delete_nodes(
|
|
436
|
+
self,
|
|
437
|
+
node_ids: Optional[List[str]] = None,
|
|
438
|
+
filters: Optional[MetadataFilters] = None,
|
|
439
|
+
**delete_kwargs: Any,
|
|
440
|
+
) -> None:
|
|
441
|
+
"""Deletes nodes.
|
|
442
|
+
|
|
443
|
+
Args:
|
|
444
|
+
node_ids (Optional[List[str]], optional): IDs of nodes to delete.
|
|
445
|
+
Defaults to None.
|
|
446
|
+
filters (Optional[MetadataFilters], optional): Metadata filters.
|
|
447
|
+
Defaults to None.
|
|
448
|
+
"""
|
|
449
|
+
if filters is not None:
|
|
450
|
+
filter = self._to_oceanbase_filter(filters)
|
|
451
|
+
else:
|
|
452
|
+
filter = None
|
|
453
|
+
|
|
454
|
+
self._client.delete(
|
|
455
|
+
table_name=self._table_name,
|
|
456
|
+
ids=node_ids,
|
|
457
|
+
where_clause=[text(filter)] if filter is not None else None,
|
|
458
|
+
)
|
|
459
|
+
|
|
460
|
+
def clear(self) -> None:
|
|
461
|
+
"""Clears table."""
|
|
462
|
+
self._client.perform_raw_text_sql(f"TRUNCATE TABLE {self._table_name}")
|
|
463
|
+
|
|
464
|
+
def _parse_distance_to_similarities(self, distance: float) -> float:
|
|
465
|
+
if self._vidx_metric_type == "l2":
|
|
466
|
+
return _euclidean_similarity(distance)
|
|
467
|
+
elif self._vidx_metric_type == "inner_product":
|
|
468
|
+
return _neg_inner_product_similarity(distance)
|
|
469
|
+
raise ValueError(f"Metric Type {self._vidx_metric_type} is not supported")
|
|
470
|
+
|
|
471
|
+
def query(
|
|
472
|
+
self, query: VectorStoreQuery, param: Optional[dict] = None, **kwargs: Any
|
|
473
|
+
) -> VectorStoreQueryResult:
|
|
474
|
+
"""Perform top-k ANN search.
|
|
475
|
+
|
|
476
|
+
Args:
|
|
477
|
+
query (VectorStoreQuery): query infos
|
|
478
|
+
param (Optional[dict]): The search params for the index type.
|
|
479
|
+
Defaults to None. Refer to `DEFAULT_OCEANBASE_HNSW_SEARCH_PARAM`
|
|
480
|
+
for example.
|
|
481
|
+
"""
|
|
482
|
+
search_param = (
|
|
483
|
+
param if param is not None else DEFAULT_OCEANBASE_HNSW_SEARCH_PARAM
|
|
484
|
+
)
|
|
485
|
+
ef_search = search_param.get(
|
|
486
|
+
"efSearch", DEFAULT_OCEANBASE_HNSW_SEARCH_PARAM["efSearch"]
|
|
487
|
+
)
|
|
488
|
+
if ef_search != self._hnsw_ef_search:
|
|
489
|
+
self._client.set_ob_hnsw_ef_search(ef_search)
|
|
490
|
+
self._hnsw_ef_search = ef_search
|
|
491
|
+
|
|
492
|
+
if query.filters:
|
|
493
|
+
qfilters = self._to_oceanbase_filter(query.filters)
|
|
494
|
+
else:
|
|
495
|
+
qfilters = None
|
|
496
|
+
|
|
497
|
+
res = self._client.ann_search(
|
|
498
|
+
table_name=self._table_name,
|
|
499
|
+
vec_data=(
|
|
500
|
+
query.query_embedding
|
|
501
|
+
if not self._normalize
|
|
502
|
+
else _normalize(query.query_embedding)
|
|
503
|
+
),
|
|
504
|
+
vec_column_name=self._vector_field,
|
|
505
|
+
distance_func=self._parse_metric_type_str_to_dist_func(),
|
|
506
|
+
with_dist=True,
|
|
507
|
+
output_column_names=[
|
|
508
|
+
self._primary_field,
|
|
509
|
+
self._text_field,
|
|
510
|
+
self._metadata_field,
|
|
511
|
+
],
|
|
512
|
+
topk=query.similarity_top_k,
|
|
513
|
+
where_clause=([text(qfilters)] if qfilters else None),
|
|
514
|
+
)
|
|
515
|
+
|
|
516
|
+
records = []
|
|
517
|
+
for r in res.fetchall():
|
|
518
|
+
records.append(r)
|
|
519
|
+
return VectorStoreQueryResult(
|
|
520
|
+
nodes=[
|
|
521
|
+
metadata_dict_to_node(
|
|
522
|
+
metadata=json.loads(r[2]),
|
|
523
|
+
text=r[1],
|
|
524
|
+
)
|
|
525
|
+
for r in records
|
|
526
|
+
],
|
|
527
|
+
similarities=[self._parse_distance_to_similarities(r[3]) for r in records],
|
|
528
|
+
ids=[r[0] for r in records],
|
|
529
|
+
)
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
build-backend = "poetry.core.masonry.api"
|
|
3
|
+
requires = ["poetry-core"]
|
|
4
|
+
|
|
5
|
+
[tool.codespell]
|
|
6
|
+
check-filenames = true
|
|
7
|
+
check-hidden = true
|
|
8
|
+
# Feel free to un-skip examples, and experimental, you will just need to
|
|
9
|
+
# work through many typos (--write-changes and --interactive will help)
|
|
10
|
+
skip = "*.csv,*.html,*.json,*.jsonl,*.pdf,*.txt,*.ipynb"
|
|
11
|
+
|
|
12
|
+
[tool.llamahub]
|
|
13
|
+
contains_example = false
|
|
14
|
+
import_path = "llama_index.vector_stores.oceanbase"
|
|
15
|
+
|
|
16
|
+
[tool.llamahub.class_authors]
|
|
17
|
+
OceanBaseVectorStore = "GITHUBear"
|
|
18
|
+
|
|
19
|
+
[tool.mypy]
|
|
20
|
+
disallow_untyped_defs = true
|
|
21
|
+
# Remove venv skip when integrated with pre-commit
|
|
22
|
+
exclude = ["_static", "build", "examples", "notebooks", "venv"]
|
|
23
|
+
ignore_missing_imports = true
|
|
24
|
+
python_version = "3.8"
|
|
25
|
+
|
|
26
|
+
[tool.poetry]
|
|
27
|
+
authors = ["GITHUBear <shanhaikang.shk@oceanbase.com>"]
|
|
28
|
+
description = "llama-index vector-stores oceanbase integration"
|
|
29
|
+
license = "MIT"
|
|
30
|
+
name = "llama-index-vector-stores-oceanbase"
|
|
31
|
+
packages = [{include = "llama_index/"}]
|
|
32
|
+
readme = "README.md"
|
|
33
|
+
version = "0.1.0"
|
|
34
|
+
|
|
35
|
+
[tool.poetry.dependencies]
|
|
36
|
+
python = ">=3.9,<4.0"
|
|
37
|
+
llama-index-core = "^0.11.0"
|
|
38
|
+
pyobvector = "^0.1.6"
|
|
39
|
+
|
|
40
|
+
[tool.poetry.group.dev.dependencies]
|
|
41
|
+
black = {extras = ["jupyter"], version = "<=23.9.1,>=23.7.0"}
|
|
42
|
+
codespell = {extras = ["toml"], version = ">=v2.2.6"}
|
|
43
|
+
ipython = "8.10.0"
|
|
44
|
+
jupyter = "^1.0.0"
|
|
45
|
+
mypy = "0.991"
|
|
46
|
+
pre-commit = "3.2.0"
|
|
47
|
+
pylint = "2.15.10"
|
|
48
|
+
pytest = "7.2.1"
|
|
49
|
+
pytest-mock = "3.11.1"
|
|
50
|
+
ruff = "0.0.292"
|
|
51
|
+
tree-sitter-languages = "^1.8.0"
|
|
52
|
+
types-Deprecated = ">=0.1.0"
|
|
53
|
+
types-PyYAML = "^6.0.12.12"
|
|
54
|
+
types-protobuf = "^4.24.0.4"
|
|
55
|
+
types-redis = "4.5.5.0"
|
|
56
|
+
types-requests = "2.28.11.8" # TODO: unpin when mypy>0.991
|
|
57
|
+
types-setuptools = "67.1.0.0"
|