pgvector-haystack 6.0.0__tar.gz → 6.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.
- {pgvector_haystack-6.0.0 → pgvector_haystack-6.1.0}/CHANGELOG.md +11 -0
- {pgvector_haystack-6.0.0 → pgvector_haystack-6.1.0}/PKG-INFO +1 -1
- {pgvector_haystack-6.0.0 → pgvector_haystack-6.1.0}/src/haystack_integrations/document_stores/pgvector/document_store.py +594 -0
- {pgvector_haystack-6.0.0 → pgvector_haystack-6.1.0}/tests/test_document_store.py +315 -0
- {pgvector_haystack-6.0.0 → pgvector_haystack-6.1.0}/tests/test_document_store_async.py +349 -0
- {pgvector_haystack-6.0.0 → pgvector_haystack-6.1.0}/.gitignore +0 -0
- {pgvector_haystack-6.0.0 → pgvector_haystack-6.1.0}/LICENSE.txt +0 -0
- {pgvector_haystack-6.0.0 → pgvector_haystack-6.1.0}/README.md +0 -0
- {pgvector_haystack-6.0.0 → pgvector_haystack-6.1.0}/examples/embedding_retrieval.py +0 -0
- {pgvector_haystack-6.0.0 → pgvector_haystack-6.1.0}/examples/hybrid_retrieval.py +0 -0
- {pgvector_haystack-6.0.0 → pgvector_haystack-6.1.0}/pydoc/config_docusaurus.yml +0 -0
- {pgvector_haystack-6.0.0 → pgvector_haystack-6.1.0}/pyproject.toml +0 -0
- {pgvector_haystack-6.0.0 → pgvector_haystack-6.1.0}/src/haystack_integrations/components/retrievers/pgvector/__init__.py +0 -0
- {pgvector_haystack-6.0.0 → pgvector_haystack-6.1.0}/src/haystack_integrations/components/retrievers/pgvector/embedding_retriever.py +0 -0
- {pgvector_haystack-6.0.0 → pgvector_haystack-6.1.0}/src/haystack_integrations/components/retrievers/pgvector/keyword_retriever.py +0 -0
- {pgvector_haystack-6.0.0 → pgvector_haystack-6.1.0}/src/haystack_integrations/components/retrievers/py.typed +0 -0
- {pgvector_haystack-6.0.0 → pgvector_haystack-6.1.0}/src/haystack_integrations/document_stores/pgvector/__init__.py +0 -0
- {pgvector_haystack-6.0.0 → pgvector_haystack-6.1.0}/src/haystack_integrations/document_stores/pgvector/converters.py +0 -0
- {pgvector_haystack-6.0.0 → pgvector_haystack-6.1.0}/src/haystack_integrations/document_stores/pgvector/filters.py +0 -0
- {pgvector_haystack-6.0.0 → pgvector_haystack-6.1.0}/src/haystack_integrations/document_stores/py.typed +0 -0
- {pgvector_haystack-6.0.0 → pgvector_haystack-6.1.0}/tests/__init__.py +0 -0
- {pgvector_haystack-6.0.0 → pgvector_haystack-6.1.0}/tests/conftest.py +0 -0
- {pgvector_haystack-6.0.0 → pgvector_haystack-6.1.0}/tests/test_converters.py +0 -0
- {pgvector_haystack-6.0.0 → pgvector_haystack-6.1.0}/tests/test_filters.py +0 -0
- {pgvector_haystack-6.0.0 → pgvector_haystack-6.1.0}/tests/test_retrieval.py +0 -0
- {pgvector_haystack-6.0.0 → pgvector_haystack-6.1.0}/tests/test_retrieval_async.py +0 -0
- {pgvector_haystack-6.0.0 → pgvector_haystack-6.1.0}/tests/test_retrievers.py +0 -0
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [integrations/pgvector-v6.0.0] - 2026-01-12
|
|
4
|
+
|
|
5
|
+
### 🧹 Chores
|
|
6
|
+
|
|
7
|
+
- Make fmt command more forgiving (#2671)
|
|
8
|
+
- [**breaking**] Pgvector - drop Python 3.9 and use X|Y typing (#2722)
|
|
9
|
+
|
|
10
|
+
### 🌀 Miscellaneous
|
|
11
|
+
|
|
12
|
+
- Fix: Fix doc links (#2661)
|
|
13
|
+
|
|
3
14
|
## [integrations/pgvector-v5.5.0] - 2026-01-02
|
|
4
15
|
|
|
5
16
|
### 🚀 Features
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pgvector-haystack
|
|
3
|
-
Version: 6.
|
|
3
|
+
Version: 6.1.0
|
|
4
4
|
Summary: An integration of pgvector (vector search extension for Postgres) with Haystack
|
|
5
5
|
Project-URL: Source, https://github.com/deepset-ai/haystack-core-integrations
|
|
6
6
|
Project-URL: Documentation, https://github.com/deepset-ai/haystack-core-integrations/blob/main/integrations/pgvector/README.md
|
|
@@ -1438,3 +1438,597 @@ class PgvectorDocumentStore:
|
|
|
1438
1438
|
records = await result.fetchall()
|
|
1439
1439
|
docs = _from_pg_to_haystack_documents(records)
|
|
1440
1440
|
return docs
|
|
1441
|
+
|
|
1442
|
+
def _prepare_filters_count_documents(self, filters):
|
|
1443
|
+
_validate_filters(filters)
|
|
1444
|
+
sql_count = SQL("SELECT COUNT(*) FROM {schema_name}.{table_name}").format(
|
|
1445
|
+
schema_name=Identifier(self.schema_name), table_name=Identifier(self.table_name)
|
|
1446
|
+
)
|
|
1447
|
+
params = ()
|
|
1448
|
+
if filters:
|
|
1449
|
+
sql_where_clause, params = _convert_filters_to_where_clause_and_params(filters)
|
|
1450
|
+
sql_count += sql_where_clause
|
|
1451
|
+
return params, sql_count
|
|
1452
|
+
|
|
1453
|
+
def count_documents_by_filter(self, filters: dict[str, Any]) -> int:
|
|
1454
|
+
"""
|
|
1455
|
+
Returns the number of documents that match the provided filters.
|
|
1456
|
+
|
|
1457
|
+
:param filters: The filters to apply to count documents.
|
|
1458
|
+
For filter syntax, see [Haystack metadata filtering](https://docs.haystack.deepset.ai/docs/metadata-filtering)
|
|
1459
|
+
:returns: The number of documents that match the filters.
|
|
1460
|
+
"""
|
|
1461
|
+
params, sql_count = self._prepare_filters_count_documents(filters)
|
|
1462
|
+
|
|
1463
|
+
self._ensure_db_setup()
|
|
1464
|
+
assert self._cursor is not None
|
|
1465
|
+
result = self._execute_sql(
|
|
1466
|
+
cursor=self._cursor,
|
|
1467
|
+
sql_query=sql_count,
|
|
1468
|
+
params=params,
|
|
1469
|
+
error_msg="Could not count documents by filter in PgvectorDocumentStore",
|
|
1470
|
+
).fetchone()
|
|
1471
|
+
|
|
1472
|
+
if result is not None:
|
|
1473
|
+
return result[0]
|
|
1474
|
+
return 0
|
|
1475
|
+
|
|
1476
|
+
async def count_documents_by_filter_async(self, filters: dict[str, Any]) -> int:
|
|
1477
|
+
"""
|
|
1478
|
+
Asynchronously returns the number of documents that match the provided filters.
|
|
1479
|
+
|
|
1480
|
+
:param filters: The filters to apply to count documents.
|
|
1481
|
+
For filter syntax, see [Haystack metadata filtering](https://docs.haystack.deepset.ai/docs/metadata-filtering)
|
|
1482
|
+
:returns: The number of documents that match the filters.
|
|
1483
|
+
"""
|
|
1484
|
+
params, sql_count = self._prepare_filters_count_documents(filters)
|
|
1485
|
+
|
|
1486
|
+
await self._ensure_db_setup_async()
|
|
1487
|
+
assert self._async_cursor is not None
|
|
1488
|
+
result = await (
|
|
1489
|
+
await self._execute_sql_async(
|
|
1490
|
+
cursor=self._async_cursor,
|
|
1491
|
+
sql_query=sql_count,
|
|
1492
|
+
params=params,
|
|
1493
|
+
error_msg="Could not count documents by filter in PgvectorDocumentStore",
|
|
1494
|
+
)
|
|
1495
|
+
).fetchone()
|
|
1496
|
+
|
|
1497
|
+
if result is not None:
|
|
1498
|
+
return result[0]
|
|
1499
|
+
return 0
|
|
1500
|
+
|
|
1501
|
+
@staticmethod
|
|
1502
|
+
def _normalize_metadata_field_name(field_name: str) -> str:
|
|
1503
|
+
"""
|
|
1504
|
+
Normalizes metadata field names by removing 'meta.' prefix if present.
|
|
1505
|
+
|
|
1506
|
+
:param field_name: The field name to normalize.
|
|
1507
|
+
:returns: The normalized field name.
|
|
1508
|
+
"""
|
|
1509
|
+
if field_name.startswith("meta."):
|
|
1510
|
+
field_name = field_name[5:] # Remove "meta." prefix
|
|
1511
|
+
|
|
1512
|
+
# Validate field name to prevent SQL injection
|
|
1513
|
+
# Only allow alphanumeric characters, underscores, and hyphens
|
|
1514
|
+
if not all(c.isalnum() or c in ("_", "-") for c in field_name):
|
|
1515
|
+
msg = (
|
|
1516
|
+
f"Invalid metadata field name: '{field_name}'. Field names can only contain alphanumeric "
|
|
1517
|
+
f"characters, underscores, and hyphens."
|
|
1518
|
+
)
|
|
1519
|
+
raise ValueError(msg)
|
|
1520
|
+
|
|
1521
|
+
return field_name
|
|
1522
|
+
|
|
1523
|
+
def _build_count_unique_metadata_query(
|
|
1524
|
+
self, normalized_fields: list[str], filters: dict[str, Any]
|
|
1525
|
+
) -> tuple[Composed, tuple]:
|
|
1526
|
+
"""
|
|
1527
|
+
Builds the SQL query for counting unique metadata values.
|
|
1528
|
+
|
|
1529
|
+
:param normalized_fields: List of normalized metadata field names.
|
|
1530
|
+
:param filters: The filters to apply to select documents.
|
|
1531
|
+
:returns: A tuple containing (sql_query, params).
|
|
1532
|
+
"""
|
|
1533
|
+
# Build SELECT clause with COUNT(DISTINCT ...) for each field
|
|
1534
|
+
count_expressions = []
|
|
1535
|
+
for field in normalized_fields:
|
|
1536
|
+
# Use SQLLiteral for the JSONB key (validated field name)
|
|
1537
|
+
count_expressions.append(
|
|
1538
|
+
SQL("COUNT(DISTINCT meta->>{} ) AS {}").format(SQLLiteral(field), Identifier(field))
|
|
1539
|
+
)
|
|
1540
|
+
|
|
1541
|
+
sql_select = SQL("SELECT ") + SQL(", ").join(count_expressions)
|
|
1542
|
+
sql_from = SQL(" FROM {schema_name}.{table_name}").format(
|
|
1543
|
+
schema_name=Identifier(self.schema_name), table_name=Identifier(self.table_name)
|
|
1544
|
+
)
|
|
1545
|
+
|
|
1546
|
+
sql_query = sql_select + sql_from
|
|
1547
|
+
|
|
1548
|
+
params = ()
|
|
1549
|
+
if filters:
|
|
1550
|
+
sql_where_clause, params = _convert_filters_to_where_clause_and_params(filters)
|
|
1551
|
+
sql_query += sql_where_clause
|
|
1552
|
+
|
|
1553
|
+
return sql_query, params
|
|
1554
|
+
|
|
1555
|
+
@staticmethod
|
|
1556
|
+
def _process_count_unique_metadata_result(
|
|
1557
|
+
result: dict[str, Any] | None, normalized_fields: list[str]
|
|
1558
|
+
) -> dict[str, int]:
|
|
1559
|
+
"""
|
|
1560
|
+
Processes the result from counting unique metadata values.
|
|
1561
|
+
|
|
1562
|
+
:param result: The database result row, or None if no results.
|
|
1563
|
+
:param normalized_fields: List of normalized metadata field names.
|
|
1564
|
+
:returns: A dictionary mapping field names to their unique value counts.
|
|
1565
|
+
"""
|
|
1566
|
+
if result is None:
|
|
1567
|
+
return dict.fromkeys(normalized_fields, 0)
|
|
1568
|
+
|
|
1569
|
+
# Return dictionary with normalized field names
|
|
1570
|
+
return {field: result.get(field, 0) for field in normalized_fields}
|
|
1571
|
+
|
|
1572
|
+
def count_unique_metadata_by_filter(self, filters: dict[str, Any], metadata_fields: list[str]) -> dict[str, int]:
|
|
1573
|
+
"""
|
|
1574
|
+
Returns the count of unique values for each specified metadata field,
|
|
1575
|
+
considering only documents that match the provided filters.
|
|
1576
|
+
|
|
1577
|
+
:param filters: The filters to apply to select documents.
|
|
1578
|
+
For filter syntax, see [Haystack metadata filtering](https://docs.haystack.deepset.ai/docs/metadata-filtering)
|
|
1579
|
+
:param metadata_fields: List of metadata field names to count unique values for.
|
|
1580
|
+
Field names can include or omit the "meta." prefix.
|
|
1581
|
+
:returns: A dictionary mapping field names to their unique value counts.
|
|
1582
|
+
"""
|
|
1583
|
+
_validate_filters(filters)
|
|
1584
|
+
|
|
1585
|
+
if not metadata_fields:
|
|
1586
|
+
msg = "metadata_fields must be a non-empty list"
|
|
1587
|
+
raise ValueError(msg)
|
|
1588
|
+
|
|
1589
|
+
normalized_fields = [PgvectorDocumentStore._normalize_metadata_field_name(field) for field in metadata_fields]
|
|
1590
|
+
sql_query, params = self._build_count_unique_metadata_query(normalized_fields, filters)
|
|
1591
|
+
|
|
1592
|
+
self._ensure_db_setup()
|
|
1593
|
+
assert self._dict_cursor is not None
|
|
1594
|
+
result = self._execute_sql(
|
|
1595
|
+
cursor=self._dict_cursor,
|
|
1596
|
+
sql_query=sql_query,
|
|
1597
|
+
params=params,
|
|
1598
|
+
error_msg="Could not count unique metadata values in PgvectorDocumentStore",
|
|
1599
|
+
).fetchone()
|
|
1600
|
+
|
|
1601
|
+
return PgvectorDocumentStore._process_count_unique_metadata_result(result, normalized_fields)
|
|
1602
|
+
|
|
1603
|
+
async def count_unique_metadata_by_filter_async(
|
|
1604
|
+
self, filters: dict[str, Any], metadata_fields: list[str]
|
|
1605
|
+
) -> dict[str, int]:
|
|
1606
|
+
"""
|
|
1607
|
+
Asynchronously returns the count of unique values for each specified metadata field,
|
|
1608
|
+
considering only documents that match the provided filters.
|
|
1609
|
+
|
|
1610
|
+
:param filters: The filters to apply to select documents.
|
|
1611
|
+
For filter syntax, see [Haystack metadata filtering](https://docs.haystack.deepset.ai/docs/metadata-filtering)
|
|
1612
|
+
:param metadata_fields: List of metadata field names to count unique values for.
|
|
1613
|
+
Field names can include or omit the "meta." prefix.
|
|
1614
|
+
:returns: A dictionary mapping field names to their unique value counts.
|
|
1615
|
+
"""
|
|
1616
|
+
_validate_filters(filters)
|
|
1617
|
+
|
|
1618
|
+
if not metadata_fields:
|
|
1619
|
+
msg = "metadata_fields must be a non-empty list"
|
|
1620
|
+
raise ValueError(msg)
|
|
1621
|
+
|
|
1622
|
+
normalized_fields = [PgvectorDocumentStore._normalize_metadata_field_name(field) for field in metadata_fields]
|
|
1623
|
+
sql_query, params = self._build_count_unique_metadata_query(normalized_fields, filters)
|
|
1624
|
+
|
|
1625
|
+
await self._ensure_db_setup_async()
|
|
1626
|
+
assert self._async_dict_cursor is not None
|
|
1627
|
+
result = await (
|
|
1628
|
+
await self._execute_sql_async(
|
|
1629
|
+
cursor=self._async_dict_cursor,
|
|
1630
|
+
sql_query=sql_query,
|
|
1631
|
+
params=params,
|
|
1632
|
+
error_msg="Could not count unique metadata values in PgvectorDocumentStore",
|
|
1633
|
+
)
|
|
1634
|
+
).fetchone()
|
|
1635
|
+
|
|
1636
|
+
return PgvectorDocumentStore._process_count_unique_metadata_result(result, normalized_fields)
|
|
1637
|
+
|
|
1638
|
+
@staticmethod
|
|
1639
|
+
def _infer_metadata_field_type(value: Any) -> str:
|
|
1640
|
+
"""
|
|
1641
|
+
Infers the PostgreSQL/JSONB type from a Python value.
|
|
1642
|
+
|
|
1643
|
+
:param value: The value to infer the type from.
|
|
1644
|
+
:returns: The inferred type name (text, integer, real, boolean).
|
|
1645
|
+
"""
|
|
1646
|
+
if isinstance(value, bool):
|
|
1647
|
+
return "boolean"
|
|
1648
|
+
if isinstance(value, int):
|
|
1649
|
+
return "integer"
|
|
1650
|
+
if isinstance(value, float):
|
|
1651
|
+
return "real"
|
|
1652
|
+
if isinstance(value, str):
|
|
1653
|
+
return "text"
|
|
1654
|
+
return "text" # Default fallback
|
|
1655
|
+
|
|
1656
|
+
@staticmethod
|
|
1657
|
+
def _analyze_metadata_fields_from_records(records: list[dict[str, Any]]) -> dict[str, dict[str, str]]:
|
|
1658
|
+
"""
|
|
1659
|
+
Analyzes metadata fields from database records and infers their types.
|
|
1660
|
+
|
|
1661
|
+
:param records: List of database records containing 'meta' field.
|
|
1662
|
+
:returns: A dictionary mapping field names to their type information.
|
|
1663
|
+
"""
|
|
1664
|
+
fields_info: dict[str, dict[str, str]] = {"content": {"type": "text"}}
|
|
1665
|
+
|
|
1666
|
+
# Analyze metadata from all documents
|
|
1667
|
+
for record in records:
|
|
1668
|
+
meta = record.get("meta")
|
|
1669
|
+
if not isinstance(meta, dict):
|
|
1670
|
+
continue
|
|
1671
|
+
|
|
1672
|
+
for field_name, field_value in meta.items():
|
|
1673
|
+
if field_name not in fields_info:
|
|
1674
|
+
# Infer type from first non-null value encountered
|
|
1675
|
+
if field_value is not None:
|
|
1676
|
+
inferred_type = PgvectorDocumentStore._infer_metadata_field_type(field_value)
|
|
1677
|
+
fields_info[field_name] = {"type": inferred_type}
|
|
1678
|
+
else:
|
|
1679
|
+
# Default to text for null values
|
|
1680
|
+
fields_info[field_name] = {"type": "text"}
|
|
1681
|
+
|
|
1682
|
+
return fields_info
|
|
1683
|
+
|
|
1684
|
+
def _analyze_metadata_from_docs_query(self):
|
|
1685
|
+
# query all documents to analyze metadata structure
|
|
1686
|
+
sql_query = SQL("SELECT meta FROM {schema_name}.{table_name} WHERE meta IS NOT NULL").format(
|
|
1687
|
+
schema_name=Identifier(self.schema_name), table_name=Identifier(self.table_name)
|
|
1688
|
+
)
|
|
1689
|
+
return sql_query
|
|
1690
|
+
|
|
1691
|
+
def get_metadata_fields_info(self) -> dict[str, dict[str, str]]:
|
|
1692
|
+
"""
|
|
1693
|
+
Returns the information about the metadata fields in the document store.
|
|
1694
|
+
|
|
1695
|
+
Since metadata is stored in a JSONB field, this method analyzes actual data
|
|
1696
|
+
to infer field types.
|
|
1697
|
+
|
|
1698
|
+
Example return:
|
|
1699
|
+
```python
|
|
1700
|
+
{
|
|
1701
|
+
'content': {'type': 'text'},
|
|
1702
|
+
'category': {'type': 'text'},
|
|
1703
|
+
'status': {'type': 'text'},
|
|
1704
|
+
'priority': {'type': 'integer'},
|
|
1705
|
+
}
|
|
1706
|
+
```
|
|
1707
|
+
|
|
1708
|
+
:returns: A dictionary mapping field names to their type information.
|
|
1709
|
+
"""
|
|
1710
|
+
self._ensure_db_setup()
|
|
1711
|
+
assert self._dict_cursor is not None
|
|
1712
|
+
|
|
1713
|
+
sql_query = self._analyze_metadata_from_docs_query()
|
|
1714
|
+
|
|
1715
|
+
result = self._execute_sql(
|
|
1716
|
+
cursor=self._dict_cursor,
|
|
1717
|
+
sql_query=sql_query,
|
|
1718
|
+
error_msg="Could not retrieve metadata fields info from PgvectorDocumentStore",
|
|
1719
|
+
)
|
|
1720
|
+
|
|
1721
|
+
records = result.fetchall()
|
|
1722
|
+
return PgvectorDocumentStore._analyze_metadata_fields_from_records(records)
|
|
1723
|
+
|
|
1724
|
+
async def get_metadata_fields_info_async(self) -> dict[str, dict[str, str]]:
|
|
1725
|
+
"""
|
|
1726
|
+
Asynchronously returns the information about the metadata fields in the document store.
|
|
1727
|
+
|
|
1728
|
+
Since metadata is stored in a JSONB field, this method analyzes actual data
|
|
1729
|
+
to infer field types.
|
|
1730
|
+
|
|
1731
|
+
:returns: A dictionary mapping field names to their type information.
|
|
1732
|
+
"""
|
|
1733
|
+
await self._ensure_db_setup_async()
|
|
1734
|
+
assert self._async_dict_cursor is not None
|
|
1735
|
+
|
|
1736
|
+
sql_query = self._analyze_metadata_from_docs_query()
|
|
1737
|
+
|
|
1738
|
+
result = await self._execute_sql_async(
|
|
1739
|
+
cursor=self._async_dict_cursor,
|
|
1740
|
+
sql_query=sql_query,
|
|
1741
|
+
error_msg="Could not retrieve metadata fields info from PgvectorDocumentStore",
|
|
1742
|
+
)
|
|
1743
|
+
|
|
1744
|
+
records = await result.fetchall()
|
|
1745
|
+
return PgvectorDocumentStore._analyze_metadata_fields_from_records(records)
|
|
1746
|
+
|
|
1747
|
+
def _build_min_max_query(self, normalized_field: str, field_type: str) -> Composed:
|
|
1748
|
+
"""
|
|
1749
|
+
Builds the SQL query for getting min/max values based on the field type.
|
|
1750
|
+
|
|
1751
|
+
:param normalized_field: The normalized metadata field name.
|
|
1752
|
+
:param field_type: The type of the field (integer, real, text, or boolean).
|
|
1753
|
+
:returns: The SQL query for min/max calculation.
|
|
1754
|
+
"""
|
|
1755
|
+
field_literal = SQLLiteral(normalized_field)
|
|
1756
|
+
|
|
1757
|
+
if field_type == "integer":
|
|
1758
|
+
# For integer fields, cast directly to integer
|
|
1759
|
+
sql_query = SQL(
|
|
1760
|
+
"""
|
|
1761
|
+
SELECT
|
|
1762
|
+
MIN((meta->>{} )::integer) AS min_value,
|
|
1763
|
+
MAX((meta->>{} )::integer) AS max_value
|
|
1764
|
+
FROM {}.{}
|
|
1765
|
+
WHERE meta->>{} IS NOT NULL
|
|
1766
|
+
""" # noqa: W291
|
|
1767
|
+
).format(
|
|
1768
|
+
field_literal,
|
|
1769
|
+
field_literal,
|
|
1770
|
+
Identifier(self.schema_name),
|
|
1771
|
+
Identifier(self.table_name),
|
|
1772
|
+
field_literal,
|
|
1773
|
+
)
|
|
1774
|
+
elif field_type == "real":
|
|
1775
|
+
# For real (float) fields, cast directly to real
|
|
1776
|
+
sql_query = SQL(
|
|
1777
|
+
"""
|
|
1778
|
+
SELECT
|
|
1779
|
+
MIN((meta->>{} )::real) AS min_value,
|
|
1780
|
+
MAX((meta->>{} )::real) AS max_value
|
|
1781
|
+
FROM {}.{}
|
|
1782
|
+
WHERE meta->>{} IS NOT NULL
|
|
1783
|
+
""" # noqa: W291
|
|
1784
|
+
).format(
|
|
1785
|
+
field_literal,
|
|
1786
|
+
field_literal,
|
|
1787
|
+
Identifier(self.schema_name),
|
|
1788
|
+
Identifier(self.table_name),
|
|
1789
|
+
field_literal,
|
|
1790
|
+
)
|
|
1791
|
+
else:
|
|
1792
|
+
# For text and other non-numeric fields, use text comparison
|
|
1793
|
+
# Use COLLATE "C" for case-sensitive comparison (byte-order comparison)
|
|
1794
|
+
# This ensures uppercase and lowercase letters are treated differently
|
|
1795
|
+
sql_query = SQL(
|
|
1796
|
+
"""
|
|
1797
|
+
SELECT
|
|
1798
|
+
MIN(meta->>{} COLLATE "C") AS min_value,
|
|
1799
|
+
MAX(meta->>{} COLLATE "C") AS max_value
|
|
1800
|
+
FROM {}.{}
|
|
1801
|
+
WHERE meta->>{} IS NOT NULL
|
|
1802
|
+
""" # noqa: W291
|
|
1803
|
+
).format(
|
|
1804
|
+
field_literal,
|
|
1805
|
+
field_literal,
|
|
1806
|
+
Identifier(self.schema_name),
|
|
1807
|
+
Identifier(self.table_name),
|
|
1808
|
+
field_literal,
|
|
1809
|
+
)
|
|
1810
|
+
|
|
1811
|
+
return sql_query
|
|
1812
|
+
|
|
1813
|
+
@staticmethod
|
|
1814
|
+
def _process_min_max_result(metadata_field: str, result: dict[str, Any] | None) -> tuple[Any, Any]:
|
|
1815
|
+
"""
|
|
1816
|
+
Processes the result from min/max query.
|
|
1817
|
+
|
|
1818
|
+
:param metadata_field: The metadata field name (for error messages).
|
|
1819
|
+
:param result: The database result row, or None if no results.
|
|
1820
|
+
:returns: A tuple containing (max_value, min_value).
|
|
1821
|
+
:raises ValueError: If the field has no values.
|
|
1822
|
+
"""
|
|
1823
|
+
if result is None:
|
|
1824
|
+
msg = f"Metadata field '{metadata_field}' has no values"
|
|
1825
|
+
raise ValueError(msg)
|
|
1826
|
+
min_value = result.get("min_value")
|
|
1827
|
+
max_value = result.get("max_value")
|
|
1828
|
+
if min_value is None and max_value is None:
|
|
1829
|
+
msg = f"Metadata field '{metadata_field}' has no values"
|
|
1830
|
+
raise ValueError(msg)
|
|
1831
|
+
return max_value, min_value
|
|
1832
|
+
|
|
1833
|
+
def get_metadata_field_min_max(self, metadata_field: str) -> dict[str, Any]:
|
|
1834
|
+
"""
|
|
1835
|
+
Returns the minimum and maximum values for a given metadata field.
|
|
1836
|
+
|
|
1837
|
+
:param metadata_field: The name of the metadata field. Can include or omit the "meta." prefix.
|
|
1838
|
+
:returns: A dictionary with 'min' and 'max' keys containing the minimum and maximum values.
|
|
1839
|
+
For numeric fields (integer, real), returns numeric min/max.
|
|
1840
|
+
For text fields, returns lexicographic min/max based on database collation.
|
|
1841
|
+
:raises ValueError: If the field doesn't exist or has no values.
|
|
1842
|
+
"""
|
|
1843
|
+
normalized_field = PgvectorDocumentStore._normalize_metadata_field_name(metadata_field)
|
|
1844
|
+
|
|
1845
|
+
# Get field type information from metadata fields info
|
|
1846
|
+
fields_info = self.get_metadata_fields_info()
|
|
1847
|
+
if normalized_field not in fields_info:
|
|
1848
|
+
msg = f"Metadata field '{metadata_field}' not found in document store"
|
|
1849
|
+
raise ValueError(msg)
|
|
1850
|
+
|
|
1851
|
+
field_type = fields_info[normalized_field]["type"]
|
|
1852
|
+
sql_query = self._build_min_max_query(normalized_field, field_type)
|
|
1853
|
+
|
|
1854
|
+
self._ensure_db_setup()
|
|
1855
|
+
assert self._dict_cursor is not None
|
|
1856
|
+
result = self._execute_sql(
|
|
1857
|
+
cursor=self._dict_cursor,
|
|
1858
|
+
sql_query=sql_query,
|
|
1859
|
+
error_msg=f"Could not get min/max for metadata field '{metadata_field}' in PgvectorDocumentStore",
|
|
1860
|
+
).fetchone()
|
|
1861
|
+
|
|
1862
|
+
max_value, min_value = PgvectorDocumentStore._process_min_max_result(metadata_field, result)
|
|
1863
|
+
|
|
1864
|
+
return {"min": min_value, "max": max_value}
|
|
1865
|
+
|
|
1866
|
+
async def get_metadata_field_min_max_async(self, metadata_field: str) -> dict[str, Any]:
|
|
1867
|
+
"""
|
|
1868
|
+
Asynchronously returns the minimum and maximum values for a given metadata field.
|
|
1869
|
+
|
|
1870
|
+
:param metadata_field: The name of the metadata field. Can include or omit the "meta." prefix.
|
|
1871
|
+
:returns: A dictionary with 'min' and 'max' keys containing the minimum and maximum values.
|
|
1872
|
+
For numeric fields (integer, real), returns numeric min/max.
|
|
1873
|
+
For text fields, returns lexicographic min/max based on database collation.
|
|
1874
|
+
:raises ValueError: If the field doesn't exist or has no values.
|
|
1875
|
+
"""
|
|
1876
|
+
normalized_field = PgvectorDocumentStore._normalize_metadata_field_name(metadata_field)
|
|
1877
|
+
|
|
1878
|
+
# Get field type information from metadata fields info
|
|
1879
|
+
fields_info = await self.get_metadata_fields_info_async()
|
|
1880
|
+
if normalized_field not in fields_info:
|
|
1881
|
+
msg = f"Metadata field '{metadata_field}' not found in document store"
|
|
1882
|
+
raise ValueError(msg)
|
|
1883
|
+
|
|
1884
|
+
field_type = fields_info[normalized_field]["type"]
|
|
1885
|
+
sql_query = self._build_min_max_query(normalized_field, field_type)
|
|
1886
|
+
|
|
1887
|
+
await self._ensure_db_setup_async()
|
|
1888
|
+
assert self._async_dict_cursor is not None
|
|
1889
|
+
result = await (
|
|
1890
|
+
await self._execute_sql_async(
|
|
1891
|
+
cursor=self._async_dict_cursor,
|
|
1892
|
+
sql_query=sql_query,
|
|
1893
|
+
error_msg=f"Could not get min/max for metadata field '{metadata_field}' in PgvectorDocumentStore",
|
|
1894
|
+
)
|
|
1895
|
+
).fetchone()
|
|
1896
|
+
|
|
1897
|
+
max_value, min_value = PgvectorDocumentStore._process_min_max_result(metadata_field, result)
|
|
1898
|
+
|
|
1899
|
+
return {"min": min_value, "max": max_value}
|
|
1900
|
+
|
|
1901
|
+
def _build_unique_values_queries(
|
|
1902
|
+
self, normalized_field: str, search_term: str | None, from_: int, size: int
|
|
1903
|
+
) -> tuple[Composed, Composed, tuple]:
|
|
1904
|
+
"""
|
|
1905
|
+
Builds SQL queries for getting unique metadata field values.
|
|
1906
|
+
|
|
1907
|
+
:param normalized_field: The normalized metadata field name.
|
|
1908
|
+
:param search_term: Optional search term to filter documents by content.
|
|
1909
|
+
:param from_: The offset for pagination (0-based).
|
|
1910
|
+
:param size: The number of unique values to return.
|
|
1911
|
+
:returns: A tuple containing (count_query, select_query, params).
|
|
1912
|
+
"""
|
|
1913
|
+
field_literal = SQLLiteral(normalized_field)
|
|
1914
|
+
|
|
1915
|
+
# base query components
|
|
1916
|
+
sql_select = SQL("SELECT DISTINCT meta->>{} AS value").format(field_literal)
|
|
1917
|
+
sql_from = SQL(" FROM {schema_name}.{table_name}").format(
|
|
1918
|
+
schema_name=Identifier(self.schema_name), table_name=Identifier(self.table_name)
|
|
1919
|
+
)
|
|
1920
|
+
sql_where = SQL(" WHERE meta->>{} IS NOT NULL").format(field_literal)
|
|
1921
|
+
|
|
1922
|
+
params: tuple = ()
|
|
1923
|
+
if search_term:
|
|
1924
|
+
# Use full-text search with word boundaries (similar to keyword retrieval)
|
|
1925
|
+
sql_where += SQL(" AND to_tsvector({language}, content) @@ plainto_tsquery({language}, %s)").format(
|
|
1926
|
+
language=SQLLiteral(self.language)
|
|
1927
|
+
)
|
|
1928
|
+
params = (search_term,)
|
|
1929
|
+
|
|
1930
|
+
# count query
|
|
1931
|
+
sql_count = SQL("SELECT COUNT(DISTINCT meta->>{} ) AS total").format(field_literal)
|
|
1932
|
+
sql_count += sql_from + sql_where
|
|
1933
|
+
|
|
1934
|
+
# paginated select query
|
|
1935
|
+
sql_query = sql_select + sql_from + sql_where
|
|
1936
|
+
sql_query += SQL(" ORDER BY value LIMIT {size} OFFSET {from_}").format(
|
|
1937
|
+
size=SQLLiteral(size), from_=SQLLiteral(from_)
|
|
1938
|
+
)
|
|
1939
|
+
|
|
1940
|
+
return sql_count, sql_query, params
|
|
1941
|
+
|
|
1942
|
+
@staticmethod
|
|
1943
|
+
def _process_unique_values_result(
|
|
1944
|
+
count_result: dict[str, Any] | None, records: list[dict[str, Any]]
|
|
1945
|
+
) -> tuple[list[str], int]:
|
|
1946
|
+
"""
|
|
1947
|
+
Processes the results from unique values queries.
|
|
1948
|
+
|
|
1949
|
+
:param count_result: The count query result row, or None if no results.
|
|
1950
|
+
:param records: The list of records from the select query.
|
|
1951
|
+
:returns: A tuple containing (unique_values, total_count).
|
|
1952
|
+
"""
|
|
1953
|
+
total_count = count_result.get("total", 0) if count_result else 0
|
|
1954
|
+
unique_values = [str(record.get("value", "")) for record in records if record.get("value") is not None]
|
|
1955
|
+
return unique_values, total_count
|
|
1956
|
+
|
|
1957
|
+
def get_metadata_field_unique_values(
|
|
1958
|
+
self, metadata_field: str, search_term: str | None, from_: int, size: int
|
|
1959
|
+
) -> tuple[list[str], int]:
|
|
1960
|
+
"""
|
|
1961
|
+
Returns unique values for a given metadata field, optionally filtered by a search term.
|
|
1962
|
+
|
|
1963
|
+
:param metadata_field: The name of the metadata field. Can include or omit the "meta." prefix.
|
|
1964
|
+
:param search_term: Optional search term to filter documents by content before extracting unique values.
|
|
1965
|
+
If None, all documents are considered.
|
|
1966
|
+
:param from_: The offset for pagination (0-based).
|
|
1967
|
+
:param size: The number of unique values to return.
|
|
1968
|
+
:returns: A tuple containing:
|
|
1969
|
+
- A list of unique values (as strings)
|
|
1970
|
+
- The total count of unique values
|
|
1971
|
+
"""
|
|
1972
|
+
normalized_field = PgvectorDocumentStore._normalize_metadata_field_name(metadata_field)
|
|
1973
|
+
sql_count, sql_query, params = self._build_unique_values_queries(normalized_field, search_term, from_, size)
|
|
1974
|
+
|
|
1975
|
+
self._ensure_db_setup()
|
|
1976
|
+
assert self._dict_cursor is not None
|
|
1977
|
+
|
|
1978
|
+
count_result = self._execute_sql(
|
|
1979
|
+
cursor=self._dict_cursor,
|
|
1980
|
+
sql_query=sql_count,
|
|
1981
|
+
params=params,
|
|
1982
|
+
error_msg=f"Could not count unique values for metadata field '{metadata_field}' in PgvectorDocumentStore",
|
|
1983
|
+
).fetchone()
|
|
1984
|
+
|
|
1985
|
+
result = self._execute_sql(
|
|
1986
|
+
cursor=self._dict_cursor,
|
|
1987
|
+
sql_query=sql_query,
|
|
1988
|
+
params=params,
|
|
1989
|
+
error_msg=f"Could not get unique values for metadata field '{metadata_field}' in PgvectorDocumentStore",
|
|
1990
|
+
)
|
|
1991
|
+
|
|
1992
|
+
records = result.fetchall()
|
|
1993
|
+
return PgvectorDocumentStore._process_unique_values_result(count_result, records)
|
|
1994
|
+
|
|
1995
|
+
async def get_metadata_field_unique_values_async(
|
|
1996
|
+
self, metadata_field: str, search_term: str | None, from_: int, size: int
|
|
1997
|
+
) -> tuple[list[str], int]:
|
|
1998
|
+
"""
|
|
1999
|
+
Asynchronously returns unique values for a given metadata field, optionally filtered by a search term.
|
|
2000
|
+
|
|
2001
|
+
:param metadata_field: The name of the metadata field. Can include or omit the "meta." prefix.
|
|
2002
|
+
:param search_term: Optional search term to filter documents by content before extracting unique values.
|
|
2003
|
+
If None, all documents are considered.
|
|
2004
|
+
:param from_: The offset for pagination (0-based).
|
|
2005
|
+
:param size: The number of unique values to return.
|
|
2006
|
+
:returns: A tuple containing:
|
|
2007
|
+
- A list of unique values (as strings)
|
|
2008
|
+
- The total count of unique values
|
|
2009
|
+
"""
|
|
2010
|
+
normalized_field = PgvectorDocumentStore._normalize_metadata_field_name(metadata_field)
|
|
2011
|
+
sql_count, sql_query, params = self._build_unique_values_queries(normalized_field, search_term, from_, size)
|
|
2012
|
+
|
|
2013
|
+
await self._ensure_db_setup_async()
|
|
2014
|
+
assert self._async_dict_cursor is not None
|
|
2015
|
+
|
|
2016
|
+
count_result = await (
|
|
2017
|
+
await self._execute_sql_async(
|
|
2018
|
+
cursor=self._async_dict_cursor,
|
|
2019
|
+
sql_query=sql_count,
|
|
2020
|
+
params=params,
|
|
2021
|
+
error_msg=f"Could not count unique values for metadata field '{metadata_field}' in "
|
|
2022
|
+
f"PgvectorDocumentStore",
|
|
2023
|
+
)
|
|
2024
|
+
).fetchone()
|
|
2025
|
+
|
|
2026
|
+
result = await self._execute_sql_async(
|
|
2027
|
+
cursor=self._async_dict_cursor,
|
|
2028
|
+
sql_query=sql_query,
|
|
2029
|
+
params=params,
|
|
2030
|
+
error_msg=f"Could not get unique values for metadata field '{metadata_field}' in PgvectorDocumentStore",
|
|
2031
|
+
)
|
|
2032
|
+
|
|
2033
|
+
records = await result.fetchall()
|
|
2034
|
+
return PgvectorDocumentStore._process_unique_values_result(count_result, records)
|