langgraph-checkpoint-postgres 2.0.6__tar.gz → 2.0.8__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.
- {langgraph_checkpoint_postgres-2.0.6 → langgraph_checkpoint_postgres-2.0.8}/PKG-INFO +1 -1
- {langgraph_checkpoint_postgres-2.0.6 → langgraph_checkpoint_postgres-2.0.8}/langgraph/checkpoint/postgres/__init__.py +8 -10
- {langgraph_checkpoint_postgres-2.0.6 → langgraph_checkpoint_postgres-2.0.8}/langgraph/checkpoint/postgres/aio.py +9 -12
- {langgraph_checkpoint_postgres-2.0.6 → langgraph_checkpoint_postgres-2.0.8}/langgraph/checkpoint/postgres/base.py +11 -0
- {langgraph_checkpoint_postgres-2.0.6 → langgraph_checkpoint_postgres-2.0.8}/langgraph/store/postgres/aio.py +81 -16
- {langgraph_checkpoint_postgres-2.0.6 → langgraph_checkpoint_postgres-2.0.8}/langgraph/store/postgres/base.py +163 -32
- {langgraph_checkpoint_postgres-2.0.6 → langgraph_checkpoint_postgres-2.0.8}/pyproject.toml +1 -1
- {langgraph_checkpoint_postgres-2.0.6 → langgraph_checkpoint_postgres-2.0.8}/README.md +0 -0
- {langgraph_checkpoint_postgres-2.0.6 → langgraph_checkpoint_postgres-2.0.8}/langgraph/checkpoint/postgres/_ainternal.py +0 -0
- {langgraph_checkpoint_postgres-2.0.6 → langgraph_checkpoint_postgres-2.0.8}/langgraph/checkpoint/postgres/_internal.py +0 -0
- {langgraph_checkpoint_postgres-2.0.6 → langgraph_checkpoint_postgres-2.0.8}/langgraph/checkpoint/postgres/py.typed +0 -0
- {langgraph_checkpoint_postgres-2.0.6 → langgraph_checkpoint_postgres-2.0.8}/langgraph/store/postgres/__init__.py +0 -0
- {langgraph_checkpoint_postgres-2.0.6 → langgraph_checkpoint_postgres-2.0.8}/langgraph/store/postgres/py.typed +0 -0
|
@@ -5,7 +5,6 @@ from typing import Any, Optional
|
|
|
5
5
|
|
|
6
6
|
from langchain_core.runnables import RunnableConfig
|
|
7
7
|
from psycopg import Capabilities, Connection, Cursor, Pipeline
|
|
8
|
-
from psycopg.errors import UndefinedTable
|
|
9
8
|
from psycopg.rows import DictRow, dict_row
|
|
10
9
|
from psycopg.types.json import Jsonb
|
|
11
10
|
from psycopg_pool import ConnectionPool
|
|
@@ -76,16 +75,15 @@ class PostgresSaver(BasePostgresSaver):
|
|
|
76
75
|
the first time checkpointer is used.
|
|
77
76
|
"""
|
|
78
77
|
with self._cursor() as cur:
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
else:
|
|
86
|
-
version = row["v"]
|
|
87
|
-
except UndefinedTable:
|
|
78
|
+
cur.execute(self.MIGRATIONS[0])
|
|
79
|
+
results = cur.execute(
|
|
80
|
+
"SELECT v FROM checkpoint_migrations ORDER BY v DESC LIMIT 1"
|
|
81
|
+
)
|
|
82
|
+
row = results.fetchone()
|
|
83
|
+
if row is None:
|
|
88
84
|
version = -1
|
|
85
|
+
else:
|
|
86
|
+
version = row["v"]
|
|
89
87
|
for v, migration in zip(
|
|
90
88
|
range(version + 1, len(self.MIGRATIONS)),
|
|
91
89
|
self.MIGRATIONS[version + 1 :],
|
|
@@ -5,7 +5,6 @@ from typing import Any, Optional
|
|
|
5
5
|
|
|
6
6
|
from langchain_core.runnables import RunnableConfig
|
|
7
7
|
from psycopg import AsyncConnection, AsyncCursor, AsyncPipeline, Capabilities
|
|
8
|
-
from psycopg.errors import UndefinedTable
|
|
9
8
|
from psycopg.rows import DictRow, dict_row
|
|
10
9
|
from psycopg.types.json import Jsonb
|
|
11
10
|
from psycopg_pool import AsyncConnectionPool
|
|
@@ -55,7 +54,7 @@ class AsyncPostgresSaver(BasePostgresSaver):
|
|
|
55
54
|
pipeline: bool = False,
|
|
56
55
|
serde: Optional[SerializerProtocol] = None,
|
|
57
56
|
) -> AsyncIterator["AsyncPostgresSaver"]:
|
|
58
|
-
"""Create a new
|
|
57
|
+
"""Create a new AsyncPostgresSaver instance from a connection string.
|
|
59
58
|
|
|
60
59
|
Args:
|
|
61
60
|
conn_string (str): The Postgres connection info string.
|
|
@@ -81,17 +80,15 @@ class AsyncPostgresSaver(BasePostgresSaver):
|
|
|
81
80
|
the first time checkpointer is used.
|
|
82
81
|
"""
|
|
83
82
|
async with self._cursor() as cur:
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
version = -1
|
|
91
|
-
else:
|
|
92
|
-
version = row["v"]
|
|
93
|
-
except UndefinedTable:
|
|
83
|
+
await cur.execute(self.MIGRATIONS[0])
|
|
84
|
+
results = await cur.execute(
|
|
85
|
+
"SELECT v FROM checkpoint_migrations ORDER BY v DESC LIMIT 1"
|
|
86
|
+
)
|
|
87
|
+
row = await results.fetchone()
|
|
88
|
+
if row is None:
|
|
94
89
|
version = -1
|
|
90
|
+
else:
|
|
91
|
+
version = row["v"]
|
|
95
92
|
for v, migration in zip(
|
|
96
93
|
range(version + 1, len(self.MIGRATIONS)),
|
|
97
94
|
self.MIGRATIONS[version + 1 :],
|
|
@@ -57,6 +57,17 @@ MIGRATIONS = [
|
|
|
57
57
|
PRIMARY KEY (thread_id, checkpoint_ns, checkpoint_id, task_id, idx)
|
|
58
58
|
);""",
|
|
59
59
|
"ALTER TABLE checkpoint_blobs ALTER COLUMN blob DROP not null;",
|
|
60
|
+
"""
|
|
61
|
+
""",
|
|
62
|
+
"""
|
|
63
|
+
CREATE INDEX CONCURRENTLY IF NOT EXISTS checkpoints_thread_id_idx ON checkpoints(thread_id);
|
|
64
|
+
""",
|
|
65
|
+
"""
|
|
66
|
+
CREATE INDEX CONCURRENTLY IF NOT EXISTS checkpoint_blobs_thread_id_idx ON checkpoint_blobs(thread_id);
|
|
67
|
+
""",
|
|
68
|
+
"""
|
|
69
|
+
CREATE INDEX CONCURRENTLY IF NOT EXISTS checkpoint_writes_thread_id_idx ON checkpoint_writes(thread_id);
|
|
70
|
+
""",
|
|
60
71
|
]
|
|
61
72
|
|
|
62
73
|
SELECT_SQL = f"""
|
|
@@ -6,7 +6,6 @@ from typing import Any, Callable, Optional, Union, cast
|
|
|
6
6
|
|
|
7
7
|
import orjson
|
|
8
8
|
from psycopg import AsyncConnection, AsyncCursor, AsyncPipeline, Capabilities
|
|
9
|
-
from psycopg.errors import UndefinedTable
|
|
10
9
|
from psycopg.rows import DictRow, dict_row
|
|
11
10
|
from psycopg_pool import AsyncConnectionPool
|
|
12
11
|
|
|
@@ -37,6 +36,75 @@ logger = logging.getLogger(__name__)
|
|
|
37
36
|
|
|
38
37
|
|
|
39
38
|
class AsyncPostgresStore(AsyncBatchedBaseStore, BasePostgresStore[_ainternal.Conn]):
|
|
39
|
+
"""Asynchronous Postgres-backed store with optional vector search using pgvector.
|
|
40
|
+
|
|
41
|
+
!!! example "Examples"
|
|
42
|
+
Basic setup and key-value storage:
|
|
43
|
+
```python
|
|
44
|
+
from langgraph.store.postgres import AsyncPostgresStore
|
|
45
|
+
|
|
46
|
+
async with AsyncPostgresStore.from_conn_string(
|
|
47
|
+
"postgresql://user:pass@localhost:5432/dbname"
|
|
48
|
+
) as store:
|
|
49
|
+
await store.setup()
|
|
50
|
+
|
|
51
|
+
# Store and retrieve data
|
|
52
|
+
await store.aput(("users", "123"), "prefs", {"theme": "dark"})
|
|
53
|
+
item = await store.aget(("users", "123"), "prefs")
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Vector search using LangChain embeddings:
|
|
57
|
+
```python
|
|
58
|
+
from langchain.embeddings import init_embeddings
|
|
59
|
+
from langgraph.store.postgres import AsyncPostgresStore
|
|
60
|
+
|
|
61
|
+
async with AsyncPostgresStore.from_conn_string(
|
|
62
|
+
"postgresql://user:pass@localhost:5432/dbname",
|
|
63
|
+
index={
|
|
64
|
+
"dims": 1536,
|
|
65
|
+
"embed": init_embeddings("openai:text-embedding-3-small"),
|
|
66
|
+
"fields": ["text"] # specify which fields to embed. Default is the whole serialized value
|
|
67
|
+
}
|
|
68
|
+
) as store:
|
|
69
|
+
await store.setup() # Do this once to run migrations
|
|
70
|
+
|
|
71
|
+
# Store documents
|
|
72
|
+
await store.aput(("docs",), "doc1", {"text": "Python tutorial"})
|
|
73
|
+
await store.aput(("docs",), "doc2", {"text": "TypeScript guide"})
|
|
74
|
+
# Don't index the following
|
|
75
|
+
await store.aput(("docs",), "doc3", {"text": "Other guide"}, index=False)
|
|
76
|
+
|
|
77
|
+
# Search by similarity
|
|
78
|
+
results = await store.asearch(("docs",), query="python programming")
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Using connection pooling for better performance:
|
|
82
|
+
```python
|
|
83
|
+
from langgraph.store.postgres import AsyncPostgresStore, PoolConfig
|
|
84
|
+
|
|
85
|
+
async with AsyncPostgresStore.from_conn_string(
|
|
86
|
+
"postgresql://user:pass@localhost:5432/dbname",
|
|
87
|
+
pool_config=PoolConfig(
|
|
88
|
+
min_size=5,
|
|
89
|
+
max_size=20
|
|
90
|
+
)
|
|
91
|
+
) as store:
|
|
92
|
+
await store.setup()
|
|
93
|
+
# Use store with connection pooling...
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Warning:
|
|
97
|
+
Make sure to:
|
|
98
|
+
1. Call `setup()` before first use to create necessary tables and indexes
|
|
99
|
+
2. Have the pgvector extension available to use vector search
|
|
100
|
+
3. Use Python 3.10+ for async functionality
|
|
101
|
+
|
|
102
|
+
Note:
|
|
103
|
+
Semantic search is disabled by default. You can enable it by providing an `index` configuration
|
|
104
|
+
when creating the store. Without this configuration, all `index` arguments passed to
|
|
105
|
+
`put` or `aput`will have no effect.
|
|
106
|
+
"""
|
|
107
|
+
|
|
40
108
|
__slots__ = (
|
|
41
109
|
"_deserializer",
|
|
42
110
|
"pipe",
|
|
@@ -150,22 +218,19 @@ class AsyncPostgresStore(AsyncBatchedBaseStore, BasePostgresStore[_ainternal.Con
|
|
|
150
218
|
"""
|
|
151
219
|
|
|
152
220
|
async def _get_version(cur: AsyncCursor[DictRow], table: str) -> int:
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
version = -1
|
|
158
|
-
else:
|
|
159
|
-
version = row["v"]
|
|
160
|
-
except UndefinedTable:
|
|
161
|
-
version = -1
|
|
162
|
-
await cur.execute(
|
|
163
|
-
f"""
|
|
164
|
-
CREATE TABLE IF NOT EXISTS {table} (
|
|
165
|
-
v INTEGER PRIMARY KEY
|
|
166
|
-
)
|
|
167
|
-
"""
|
|
221
|
+
await cur.execute(
|
|
222
|
+
f"""
|
|
223
|
+
CREATE TABLE IF NOT EXISTS {table} (
|
|
224
|
+
v INTEGER PRIMARY KEY
|
|
168
225
|
)
|
|
226
|
+
"""
|
|
227
|
+
)
|
|
228
|
+
await cur.execute(f"SELECT v FROM {table} ORDER BY v DESC LIMIT 1")
|
|
229
|
+
row = cast(dict, await cur.fetchone())
|
|
230
|
+
if row is None:
|
|
231
|
+
version = -1
|
|
232
|
+
else:
|
|
233
|
+
version = row["v"]
|
|
169
234
|
return version
|
|
170
235
|
|
|
171
236
|
async with self._cursor() as cur:
|
|
@@ -21,7 +21,6 @@ from typing import (
|
|
|
21
21
|
|
|
22
22
|
import orjson
|
|
23
23
|
from psycopg import Capabilities, Connection, Cursor, Pipeline
|
|
24
|
-
from psycopg.errors import UndefinedTable
|
|
25
24
|
from psycopg.rows import DictRow, dict_row
|
|
26
25
|
from psycopg.types.json import Jsonb
|
|
27
26
|
from psycopg_pool import ConnectionPool
|
|
@@ -56,6 +55,7 @@ class Migration(NamedTuple):
|
|
|
56
55
|
|
|
57
56
|
sql: str
|
|
58
57
|
params: Optional[dict[str, Any]] = None
|
|
58
|
+
condition: Optional[Callable[["BasePostgresStore"], bool]] = None
|
|
59
59
|
|
|
60
60
|
|
|
61
61
|
MIGRATIONS: Sequence[str] = [
|
|
@@ -72,7 +72,7 @@ CREATE TABLE IF NOT EXISTS store (
|
|
|
72
72
|
""",
|
|
73
73
|
"""
|
|
74
74
|
-- For faster lookups by prefix
|
|
75
|
-
CREATE INDEX IF NOT EXISTS store_prefix_idx ON store USING btree (prefix text_pattern_ops);
|
|
75
|
+
CREATE INDEX CONCURRENTLY IF NOT EXISTS store_prefix_idx ON store USING btree (prefix text_pattern_ops);
|
|
76
76
|
""",
|
|
77
77
|
]
|
|
78
78
|
|
|
@@ -104,11 +104,29 @@ CREATE TABLE IF NOT EXISTS store_vectors (
|
|
|
104
104
|
),
|
|
105
105
|
},
|
|
106
106
|
),
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
107
|
+
Migration(
|
|
108
|
+
"""
|
|
109
|
+
CREATE INDEX CONCURRENTLY IF NOT EXISTS store_vectors_embedding_idx ON store_vectors
|
|
110
|
+
USING %(index_type)s (embedding %(ops)s)%(index_params)s;
|
|
111
|
+
""",
|
|
112
|
+
condition=lambda store: bool(
|
|
113
|
+
store.index_config and _get_index_params(store)[0] != "flat"
|
|
114
|
+
),
|
|
115
|
+
params={
|
|
116
|
+
"index_type": lambda store: _get_index_params(store)[0],
|
|
117
|
+
"ops": lambda store: _get_vector_type_ops(store),
|
|
118
|
+
"index_params": lambda store: (
|
|
119
|
+
" WITH ("
|
|
120
|
+
+ ", ".join(f"{k}={v}" for k, v in _get_index_params(store)[1].items())
|
|
121
|
+
+ ")"
|
|
122
|
+
if _get_index_params(store)[1]
|
|
123
|
+
else ""
|
|
124
|
+
),
|
|
125
|
+
},
|
|
126
|
+
),
|
|
110
127
|
]
|
|
111
128
|
|
|
129
|
+
|
|
112
130
|
C = TypeVar("C", bound=Union[_pg_internal.Conn, _ainternal.Conn])
|
|
113
131
|
|
|
114
132
|
|
|
@@ -140,6 +158,8 @@ class PoolConfig(TypedDict, total=False):
|
|
|
140
158
|
class ANNIndexConfig(TypedDict, total=False):
|
|
141
159
|
"""Configuration for vector index in PostgreSQL store."""
|
|
142
160
|
|
|
161
|
+
kind: Literal["hnsw", "ivfflat", "flat"]
|
|
162
|
+
"""Type of index to use: 'hnsw' for Hierarchical Navigable Small World, or 'ivfflat' for Inverted File Flat."""
|
|
143
163
|
vector_type: Literal["vector", "halfvec"]
|
|
144
164
|
"""Type of vector storage to use.
|
|
145
165
|
Options:
|
|
@@ -148,6 +168,35 @@ class ANNIndexConfig(TypedDict, total=False):
|
|
|
148
168
|
"""
|
|
149
169
|
|
|
150
170
|
|
|
171
|
+
class HNSWConfig(ANNIndexConfig, total=False):
|
|
172
|
+
"""Configuration for HNSW (Hierarchical Navigable Small World) index."""
|
|
173
|
+
|
|
174
|
+
kind: Literal["hnsw"] # type: ignore[misc]
|
|
175
|
+
m: int
|
|
176
|
+
"""Maximum number of connections per layer. Default is 16."""
|
|
177
|
+
ef_construction: int
|
|
178
|
+
"""Size of dynamic candidate list for index construction. Default is 64."""
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
class IVFFlatConfig(ANNIndexConfig, total=False):
|
|
182
|
+
"""IVFFlat index divides vectors into lists, and then searches a subset of those lists that are closest to the query vector. It has faster build times and uses less memory than HNSW, but has lower query performance (in terms of speed-recall tradeoff).
|
|
183
|
+
|
|
184
|
+
Three keys to achieving good recall are:
|
|
185
|
+
1. Create the index after the table has some data
|
|
186
|
+
2. Choose an appropriate number of lists - a good place to start is rows / 1000 for up to 1M rows and sqrt(rows) for over 1M rows
|
|
187
|
+
3. When querying, specify an appropriate number of probes (higher is better for recall, lower is better for speed) - a good place to start is sqrt(lists)
|
|
188
|
+
"""
|
|
189
|
+
|
|
190
|
+
kind: Literal["ivfflat"] # type: ignore[misc]
|
|
191
|
+
nlist: int
|
|
192
|
+
"""Number of inverted lists (clusters) for IVF index.
|
|
193
|
+
|
|
194
|
+
Determines the number of clusters used in the index structure.
|
|
195
|
+
Higher values can improve search speed but increase index size and build time.
|
|
196
|
+
Typically set to the square root of the number of vectors in the index.
|
|
197
|
+
"""
|
|
198
|
+
|
|
199
|
+
|
|
151
200
|
class PostgresIndexConfig(IndexConfig, total=False):
|
|
152
201
|
"""Configuration for vector embeddings in PostgreSQL store with pgvector-specific options.
|
|
153
202
|
|
|
@@ -321,7 +370,7 @@ class BasePostgresStore(Generic[C]):
|
|
|
321
370
|
if op.query and self.index_config:
|
|
322
371
|
embedding_requests.append((idx, op.query))
|
|
323
372
|
|
|
324
|
-
score_operator = _get_distance_operator(self)
|
|
373
|
+
score_operator, post_operator = _get_distance_operator(self)
|
|
325
374
|
vector_type = (
|
|
326
375
|
cast(PostgresIndexConfig, self.index_config)
|
|
327
376
|
.get("ann_index_config", {})
|
|
@@ -351,18 +400,28 @@ class BasePostgresStore(Generic[C]):
|
|
|
351
400
|
if not filter_conditions
|
|
352
401
|
else " AND " + " AND ".join(filter_conditions)
|
|
353
402
|
)
|
|
403
|
+
if op.namespace_prefix:
|
|
404
|
+
prefix_filter_str = f"WHERE s.prefix LIKE %s {filter_str} "
|
|
405
|
+
ns_args: Sequence = (f"{_namespace_to_text(op.namespace_prefix)}%",)
|
|
406
|
+
else:
|
|
407
|
+
ns_args = ()
|
|
408
|
+
if filter_str:
|
|
409
|
+
prefix_filter_str = f"WHERE {filter_str} "
|
|
410
|
+
else:
|
|
411
|
+
prefix_filter_str = ""
|
|
412
|
+
|
|
354
413
|
base_query = f"""
|
|
355
414
|
WITH scored AS (
|
|
356
|
-
SELECT s.prefix, s.key, s.value, s.created_at, s.updated_at, {score_operator} AS
|
|
415
|
+
SELECT s.prefix, s.key, s.value, s.created_at, s.updated_at, {score_operator} AS neg_score
|
|
357
416
|
FROM store s
|
|
358
417
|
JOIN store_vectors sv ON s.prefix = sv.prefix AND s.key = sv.key
|
|
359
|
-
|
|
360
|
-
ORDER BY {score_operator}
|
|
418
|
+
{prefix_filter_str}
|
|
419
|
+
ORDER BY {score_operator} ASC
|
|
361
420
|
LIMIT %s
|
|
362
421
|
)
|
|
363
422
|
SELECT * FROM (
|
|
364
423
|
SELECT DISTINCT ON (prefix, key)
|
|
365
|
-
prefix, key, value, created_at, updated_at, score
|
|
424
|
+
prefix, key, value, created_at, updated_at, {post_operator} as score
|
|
366
425
|
FROM scored
|
|
367
426
|
ORDER BY prefix, key, score DESC
|
|
368
427
|
) AS unique_docs
|
|
@@ -372,7 +431,7 @@ class BasePostgresStore(Generic[C]):
|
|
|
372
431
|
"""
|
|
373
432
|
params = [
|
|
374
433
|
_PLACEHOLDER, # Vector placeholder
|
|
375
|
-
|
|
434
|
+
*ns_args,
|
|
376
435
|
*filter_params,
|
|
377
436
|
_PLACEHOLDER,
|
|
378
437
|
expanded_limit,
|
|
@@ -474,6 +533,58 @@ class BasePostgresStore(Generic[C]):
|
|
|
474
533
|
|
|
475
534
|
|
|
476
535
|
class PostgresStore(BaseStore, BasePostgresStore[_pg_internal.Conn]):
|
|
536
|
+
"""Postgres-backed store with optional vector search using pgvector.
|
|
537
|
+
|
|
538
|
+
!!! example "Examples"
|
|
539
|
+
Basic setup and key-value storage:
|
|
540
|
+
```python
|
|
541
|
+
from langgraph.store.postgres import PostgresStore
|
|
542
|
+
|
|
543
|
+
store = PostgresStore(
|
|
544
|
+
connection_string="postgresql://user:pass@localhost:5432/dbname"
|
|
545
|
+
)
|
|
546
|
+
store.setup()
|
|
547
|
+
|
|
548
|
+
# Store and retrieve data
|
|
549
|
+
store.put(("users", "123"), "prefs", {"theme": "dark"})
|
|
550
|
+
item = store.get(("users", "123"), "prefs")
|
|
551
|
+
```
|
|
552
|
+
|
|
553
|
+
Vector search using LangChain embeddings:
|
|
554
|
+
```python
|
|
555
|
+
from langchain.embeddings import init_embeddings
|
|
556
|
+
from langgraph.store.postgres import PostgresStore
|
|
557
|
+
|
|
558
|
+
store = PostgresStore(
|
|
559
|
+
connection_string="postgresql://user:pass@localhost:5432/dbname",
|
|
560
|
+
index={
|
|
561
|
+
"dims": 1536,
|
|
562
|
+
"embed": init_embeddings("openai:text-embedding-3-small"),
|
|
563
|
+
"fields": ["text"] # specify which fields to embed. Default is the whole serialized value
|
|
564
|
+
}
|
|
565
|
+
)
|
|
566
|
+
store.setup() # Do this once to run migrations
|
|
567
|
+
|
|
568
|
+
# Store documents
|
|
569
|
+
store.put(("docs",), "doc1", {"text": "Python tutorial"})
|
|
570
|
+
store.put(("docs",), "doc2", {"text": "TypeScript guide"})
|
|
571
|
+
store.put(("docs",), "doc2", {"text": "Other guide"}, index=False) # don't index
|
|
572
|
+
|
|
573
|
+
# Search by similarity
|
|
574
|
+
results = store.search(("docs",), query="python programming")
|
|
575
|
+
```
|
|
576
|
+
|
|
577
|
+
Note:
|
|
578
|
+
Semantic search is disabled by default. You can enable it by providing an `index` configuration
|
|
579
|
+
when creating the store. Without this configuration, all `index` arguments passed to
|
|
580
|
+
`put` or `aput`will have no effect.
|
|
581
|
+
|
|
582
|
+
Warning:
|
|
583
|
+
Make sure to call `setup()` before first use to create necessary tables and indexes.
|
|
584
|
+
The pgvector extension must be available to use vector search.
|
|
585
|
+
|
|
586
|
+
"""
|
|
587
|
+
|
|
477
588
|
__slots__ = (
|
|
478
589
|
"_deserializer",
|
|
479
590
|
"pipe",
|
|
@@ -702,7 +813,6 @@ class PostgresStore(BaseStore, BasePostgresStore[_pg_internal.Conn]):
|
|
|
702
813
|
_paramslist[i] = embedding
|
|
703
814
|
|
|
704
815
|
for (idx, _), (query, params) in zip(search_ops, queries):
|
|
705
|
-
# Execute the actual query
|
|
706
816
|
cur.execute(query, params)
|
|
707
817
|
rows = cast(list[Row], cur.fetchall())
|
|
708
818
|
results[idx] = [
|
|
@@ -736,22 +846,19 @@ class PostgresStore(BaseStore, BasePostgresStore[_pg_internal.Conn]):
|
|
|
736
846
|
"""
|
|
737
847
|
|
|
738
848
|
def _get_version(cur: Cursor[dict[str, Any]], table: str) -> int:
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
version = -1
|
|
744
|
-
else:
|
|
745
|
-
version = row["v"]
|
|
746
|
-
except UndefinedTable:
|
|
747
|
-
version = -1
|
|
748
|
-
cur.execute(
|
|
749
|
-
f"""
|
|
750
|
-
CREATE TABLE IF NOT EXISTS {table} (
|
|
751
|
-
v INTEGER PRIMARY KEY
|
|
752
|
-
)
|
|
753
|
-
"""
|
|
849
|
+
cur.execute(
|
|
850
|
+
f"""
|
|
851
|
+
CREATE TABLE IF NOT EXISTS {table} (
|
|
852
|
+
v INTEGER PRIMARY KEY
|
|
754
853
|
)
|
|
854
|
+
"""
|
|
855
|
+
)
|
|
856
|
+
cur.execute(f"SELECT v FROM {table} ORDER BY v DESC LIMIT 1")
|
|
857
|
+
row = cast(dict, cur.fetchone())
|
|
858
|
+
if row is None:
|
|
859
|
+
version = -1
|
|
860
|
+
else:
|
|
861
|
+
version = row["v"]
|
|
755
862
|
return version
|
|
756
863
|
|
|
757
864
|
with self._cursor() as cur:
|
|
@@ -765,6 +872,8 @@ class PostgresStore(BaseStore, BasePostgresStore[_pg_internal.Conn]):
|
|
|
765
872
|
for v, migration in enumerate(
|
|
766
873
|
self.VECTOR_MIGRATIONS[version + 1 :], start=version + 1
|
|
767
874
|
):
|
|
875
|
+
if migration.condition and not migration.condition(self):
|
|
876
|
+
continue
|
|
768
877
|
sql = migration.sql
|
|
769
878
|
if migration.params:
|
|
770
879
|
params = {
|
|
@@ -823,6 +932,18 @@ def _get_vector_type_ops(store: BasePostgresStore) -> str:
|
|
|
823
932
|
return f"{type_prefix}_{distance_suffix}"
|
|
824
933
|
|
|
825
934
|
|
|
935
|
+
def _get_index_params(store: Any) -> tuple[str, dict[str, Any]]:
|
|
936
|
+
"""Get the index type and configuration based on config."""
|
|
937
|
+
if not store.index_config:
|
|
938
|
+
return "hnsw", {}
|
|
939
|
+
|
|
940
|
+
config = cast(PostgresIndexConfig, store.index_config)
|
|
941
|
+
index_config = config.get("ann_index_config", _DEFAULT_ANN_CONFIG).copy()
|
|
942
|
+
kind = index_config.pop("kind", "hnsw")
|
|
943
|
+
index_config.pop("vector_type", None)
|
|
944
|
+
return kind, index_config
|
|
945
|
+
|
|
946
|
+
|
|
826
947
|
def _namespace_to_text(
|
|
827
948
|
namespace: tuple[str, ...], handle_wildcards: bool = False
|
|
828
949
|
) -> str:
|
|
@@ -915,7 +1036,7 @@ def _decode_ns_bytes(namespace: Union[str, bytes, list]) -> tuple[str, ...]:
|
|
|
915
1036
|
return tuple(namespace.split("."))
|
|
916
1037
|
|
|
917
1038
|
|
|
918
|
-
def _get_distance_operator(store: Any) -> str:
|
|
1039
|
+
def _get_distance_operator(store: Any) -> tuple[str, str]:
|
|
919
1040
|
"""Get the distance operator and score expression based on config."""
|
|
920
1041
|
# Note: Today, we are not using ANN indices due to restrictions
|
|
921
1042
|
# on PGVector's support for mixing vector and non-vector filters
|
|
@@ -936,12 +1057,22 @@ def _get_distance_operator(store: Any) -> str:
|
|
|
936
1057
|
config = cast(PostgresIndexConfig, store.index_config)
|
|
937
1058
|
distance_type = config.get("distance_type", "cosine")
|
|
938
1059
|
|
|
1060
|
+
# Return the operator and the score expression
|
|
1061
|
+
# The operator is used in the CTE and will be compatible with an ASCENDING ORDER
|
|
1062
|
+
# sort clause.
|
|
1063
|
+
# The score expression is used in the final query and will be compatible with
|
|
1064
|
+
# a DESCENDING ORDER sort clause and the user's expectations of what the similarity score
|
|
1065
|
+
# should be.
|
|
939
1066
|
if distance_type == "l2":
|
|
940
|
-
|
|
1067
|
+
# Final: "-(sv.embedding <-> %s::%s)"
|
|
1068
|
+
# We return the "l2 similarity" so that the sorting order is the same
|
|
1069
|
+
return "sv.embedding <-> %s::%s", "-scored.neg_score"
|
|
941
1070
|
elif distance_type == "inner_product":
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
1071
|
+
# Final: "-(sv.embedding <#> %s::%s)"
|
|
1072
|
+
return "sv.embedding <#> %s::%s", "-(scored.neg_score)"
|
|
1073
|
+
else: # cosine similarity
|
|
1074
|
+
# Final: "1 - (sv.embedding <=> %s::%s)"
|
|
1075
|
+
return "sv.embedding <=> %s::%s", "1 - scored.neg_score"
|
|
945
1076
|
|
|
946
1077
|
|
|
947
1078
|
def _ensure_index_config(
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|