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.
Files changed (13) hide show
  1. {langgraph_checkpoint_postgres-2.0.6 → langgraph_checkpoint_postgres-2.0.8}/PKG-INFO +1 -1
  2. {langgraph_checkpoint_postgres-2.0.6 → langgraph_checkpoint_postgres-2.0.8}/langgraph/checkpoint/postgres/__init__.py +8 -10
  3. {langgraph_checkpoint_postgres-2.0.6 → langgraph_checkpoint_postgres-2.0.8}/langgraph/checkpoint/postgres/aio.py +9 -12
  4. {langgraph_checkpoint_postgres-2.0.6 → langgraph_checkpoint_postgres-2.0.8}/langgraph/checkpoint/postgres/base.py +11 -0
  5. {langgraph_checkpoint_postgres-2.0.6 → langgraph_checkpoint_postgres-2.0.8}/langgraph/store/postgres/aio.py +81 -16
  6. {langgraph_checkpoint_postgres-2.0.6 → langgraph_checkpoint_postgres-2.0.8}/langgraph/store/postgres/base.py +163 -32
  7. {langgraph_checkpoint_postgres-2.0.6 → langgraph_checkpoint_postgres-2.0.8}/pyproject.toml +1 -1
  8. {langgraph_checkpoint_postgres-2.0.6 → langgraph_checkpoint_postgres-2.0.8}/README.md +0 -0
  9. {langgraph_checkpoint_postgres-2.0.6 → langgraph_checkpoint_postgres-2.0.8}/langgraph/checkpoint/postgres/_ainternal.py +0 -0
  10. {langgraph_checkpoint_postgres-2.0.6 → langgraph_checkpoint_postgres-2.0.8}/langgraph/checkpoint/postgres/_internal.py +0 -0
  11. {langgraph_checkpoint_postgres-2.0.6 → langgraph_checkpoint_postgres-2.0.8}/langgraph/checkpoint/postgres/py.typed +0 -0
  12. {langgraph_checkpoint_postgres-2.0.6 → langgraph_checkpoint_postgres-2.0.8}/langgraph/store/postgres/__init__.py +0 -0
  13. {langgraph_checkpoint_postgres-2.0.6 → langgraph_checkpoint_postgres-2.0.8}/langgraph/store/postgres/py.typed +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: langgraph-checkpoint-postgres
3
- Version: 2.0.6
3
+ Version: 2.0.8
4
4
  Summary: Library with a Postgres implementation of LangGraph checkpoint saver.
5
5
  Home-page: https://www.github.com/langchain-ai/langgraph
6
6
  License: MIT
@@ -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
- try:
80
- row = cur.execute(
81
- "SELECT v FROM checkpoint_migrations ORDER BY v DESC LIMIT 1"
82
- ).fetchone()
83
- if row is None:
84
- version = -1
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 PostgresSaver instance from a connection string.
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
- try:
85
- results = await cur.execute(
86
- "SELECT v FROM checkpoint_migrations ORDER BY v DESC LIMIT 1"
87
- )
88
- row = await results.fetchone()
89
- if row is None:
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
- try:
154
- await cur.execute(f"SELECT v FROM {table} ORDER BY v DESC LIMIT 1")
155
- row = await cur.fetchone()
156
- if row is None:
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
- # TODO: Add an HNSW or IVFFlat index depending on config
108
- # First must improve the search query when filtering by
109
- # namespace
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 score
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
- WHERE s.prefix LIKE %s {filter_str}
360
- ORDER BY {score_operator} DESC
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
- f"{_namespace_to_text(op.namespace_prefix)}%",
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
- try:
740
- cur.execute(f"SELECT v FROM {table} ORDER BY v DESC LIMIT 1")
741
- row = cast(dict, cur.fetchone())
742
- if row is None:
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
- return "1 - (sv.embedding <-> %s::%s)"
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
- return "-(sv.embedding <#> %s::%s)"
943
- else: # cosine
944
- return "1 - (sv.embedding <=> %s::%s)"
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(
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "langgraph-checkpoint-postgres"
3
- version = "2.0.6"
3
+ version = "2.0.8"
4
4
  description = "Library with a Postgres implementation of LangGraph checkpoint saver."
5
5
  authors = []
6
6
  license = "MIT"