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.
Files changed (27) hide show
  1. {pgvector_haystack-6.0.0 → pgvector_haystack-6.1.0}/CHANGELOG.md +11 -0
  2. {pgvector_haystack-6.0.0 → pgvector_haystack-6.1.0}/PKG-INFO +1 -1
  3. {pgvector_haystack-6.0.0 → pgvector_haystack-6.1.0}/src/haystack_integrations/document_stores/pgvector/document_store.py +594 -0
  4. {pgvector_haystack-6.0.0 → pgvector_haystack-6.1.0}/tests/test_document_store.py +315 -0
  5. {pgvector_haystack-6.0.0 → pgvector_haystack-6.1.0}/tests/test_document_store_async.py +349 -0
  6. {pgvector_haystack-6.0.0 → pgvector_haystack-6.1.0}/.gitignore +0 -0
  7. {pgvector_haystack-6.0.0 → pgvector_haystack-6.1.0}/LICENSE.txt +0 -0
  8. {pgvector_haystack-6.0.0 → pgvector_haystack-6.1.0}/README.md +0 -0
  9. {pgvector_haystack-6.0.0 → pgvector_haystack-6.1.0}/examples/embedding_retrieval.py +0 -0
  10. {pgvector_haystack-6.0.0 → pgvector_haystack-6.1.0}/examples/hybrid_retrieval.py +0 -0
  11. {pgvector_haystack-6.0.0 → pgvector_haystack-6.1.0}/pydoc/config_docusaurus.yml +0 -0
  12. {pgvector_haystack-6.0.0 → pgvector_haystack-6.1.0}/pyproject.toml +0 -0
  13. {pgvector_haystack-6.0.0 → pgvector_haystack-6.1.0}/src/haystack_integrations/components/retrievers/pgvector/__init__.py +0 -0
  14. {pgvector_haystack-6.0.0 → pgvector_haystack-6.1.0}/src/haystack_integrations/components/retrievers/pgvector/embedding_retriever.py +0 -0
  15. {pgvector_haystack-6.0.0 → pgvector_haystack-6.1.0}/src/haystack_integrations/components/retrievers/pgvector/keyword_retriever.py +0 -0
  16. {pgvector_haystack-6.0.0 → pgvector_haystack-6.1.0}/src/haystack_integrations/components/retrievers/py.typed +0 -0
  17. {pgvector_haystack-6.0.0 → pgvector_haystack-6.1.0}/src/haystack_integrations/document_stores/pgvector/__init__.py +0 -0
  18. {pgvector_haystack-6.0.0 → pgvector_haystack-6.1.0}/src/haystack_integrations/document_stores/pgvector/converters.py +0 -0
  19. {pgvector_haystack-6.0.0 → pgvector_haystack-6.1.0}/src/haystack_integrations/document_stores/pgvector/filters.py +0 -0
  20. {pgvector_haystack-6.0.0 → pgvector_haystack-6.1.0}/src/haystack_integrations/document_stores/py.typed +0 -0
  21. {pgvector_haystack-6.0.0 → pgvector_haystack-6.1.0}/tests/__init__.py +0 -0
  22. {pgvector_haystack-6.0.0 → pgvector_haystack-6.1.0}/tests/conftest.py +0 -0
  23. {pgvector_haystack-6.0.0 → pgvector_haystack-6.1.0}/tests/test_converters.py +0 -0
  24. {pgvector_haystack-6.0.0 → pgvector_haystack-6.1.0}/tests/test_filters.py +0 -0
  25. {pgvector_haystack-6.0.0 → pgvector_haystack-6.1.0}/tests/test_retrieval.py +0 -0
  26. {pgvector_haystack-6.0.0 → pgvector_haystack-6.1.0}/tests/test_retrieval_async.py +0 -0
  27. {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.0.0
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)