diff --git a/examples/vectorstore.ipynb b/examples/vectorstore.ipynb index 4bb720e..0957340 100644 --- a/examples/vectorstore.ipynb +++ b/examples/vectorstore.ipynb @@ -45,10 +45,10 @@ "metadata": { "tags": [] }, - "outputs": [], "source": [ "!pip install --quiet -U langchain_cohere" - ] + ], + "outputs": [] }, { "cell_type": "markdown", @@ -65,7 +65,6 @@ "metadata": { "tags": [] }, - "outputs": [], "source": [ "from langchain_cohere import CohereEmbeddings\n", "from langchain_postgres.vectorstores import PGVector\n", @@ -82,7 +81,8 @@ " connection=connection,\n", " use_jsonb=True,\n", ")" - ] + ], + "outputs": [] }, { "cell_type": "markdown", @@ -123,7 +123,6 @@ "metadata": { "tags": [] }, - "outputs": [], "source": [ "docs = [\n", " Document(page_content='there are cats in the pond', metadata={\"id\": 1, \"location\": \"pond\", \"topic\": \"animals\"}),\n", @@ -137,7 +136,8 @@ " Document(page_content='the library hosts a weekly story time for kids', metadata={\"id\": 9, \"location\": \"library\", \"topic\": \"reading\"}),\n", " Document(page_content='a cooking class for beginners is offered at the community center', metadata={\"id\": 10, \"location\": \"community center\", \"topic\": \"classes\"})\n", "]\n" - ] + ], + "outputs": [] }, { "cell_type": "code", @@ -146,21 +146,10 @@ "metadata": { "tags": [] }, - "outputs": [ - { - "data": { - "text/plain": [ - "[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]" - ] - }, - "execution_count": 6, - "metadata": {}, - "output_type": "execute_result" - } - ], "source": [ "vectorstore.add_documents(docs, ids=[doc.metadata['id'] for doc in docs])" - ] + ], + "outputs": [] }, { "cell_type": "code", @@ -169,30 +158,10 @@ "metadata": { "tags": [] }, - "outputs": [ - { - "data": { - "text/plain": [ - "[Document(page_content='there are cats in the pond', metadata={'id': 1, 'topic': 'animals', 'location': 'pond'}),\n", - " Document(page_content='the book club meets at the library', metadata={'id': 8, 'topic': 'reading', 'location': 'library'}),\n", - " Document(page_content='the library hosts a weekly story time for kids', metadata={'id': 9, 'topic': 'reading', 'location': 'library'}),\n", - " Document(page_content='the new art exhibit is fascinating', metadata={'id': 5, 'topic': 'art', 'location': 'museum'}),\n", - " Document(page_content='ducks are also found in the pond', metadata={'id': 2, 'topic': 'animals', 'location': 'pond'}),\n", - " Document(page_content='the market also sells fresh oranges', metadata={'id': 4, 'topic': 'food', 'location': 'market'}),\n", - " Document(page_content='a cooking class for beginners is offered at the community center', metadata={'id': 10, 'topic': 'classes', 'location': 'community center'}),\n", - " Document(page_content='fresh apples are available at the market', metadata={'id': 3, 'topic': 'food', 'location': 'market'}),\n", - " Document(page_content='a sculpture exhibit is also at the museum', metadata={'id': 6, 'topic': 'art', 'location': 'museum'}),\n", - " Document(page_content='a new coffee shop opened on Main Street', metadata={'id': 7, 'topic': 'food', 'location': 'Main Street'})]" - ] - }, - "execution_count": 7, - "metadata": {}, - "output_type": "execute_result" - } - ], "source": [ "vectorstore.similarity_search('kitty', k=10)" - ] + ], + "outputs": [] }, { "cell_type": "markdown", @@ -209,7 +178,6 @@ "metadata": { "tags": [] }, - "outputs": [], "source": [ "docs = [\n", " Document(page_content='there are cats in the pond', metadata={\"id\": 1, \"location\": \"pond\", \"topic\": \"animals\"}),\n", @@ -223,7 +191,8 @@ " Document(page_content='the library hosts a weekly story time for kids', metadata={\"id\": 9, \"location\": \"library\", \"topic\": \"reading\"}),\n", " Document(page_content='a cooking class for beginners is offered at the community center', metadata={\"id\": 10, \"location\": \"community center\", \"topic\": \"classes\"})\n", "]\n" - ] + ], + "outputs": [] }, { "cell_type": "markdown", @@ -259,26 +228,12 @@ "metadata": { "tags": [] }, - "outputs": [ - { - "data": { - "text/plain": [ - "[Document(page_content='there are cats in the pond', metadata={'id': 1, 'topic': 'animals', 'location': 'pond'}),\n", - " Document(page_content='the library hosts a weekly story time for kids', metadata={'id': 9, 'topic': 'reading', 'location': 'library'}),\n", - " Document(page_content='the new art exhibit is fascinating', metadata={'id': 5, 'topic': 'art', 'location': 'museum'}),\n", - " Document(page_content='ducks are also found in the pond', metadata={'id': 2, 'topic': 'animals', 'location': 'pond'})]" - ] - }, - "execution_count": 9, - "metadata": {}, - "output_type": "execute_result" - } - ], "source": [ "vectorstore.similarity_search('kitty', k=10, filter={\n", " 'id': {'$in': [1, 5, 2, 9]}\n", "})" - ] + ], + "outputs": [] }, { "cell_type": "markdown", @@ -295,25 +250,13 @@ "metadata": { "tags": [] }, - "outputs": [ - { - "data": { - "text/plain": [ - "[Document(page_content='ducks are also found in the pond', metadata={'id': 2, 'topic': 'animals', 'location': 'pond'}),\n", - " Document(page_content='there are cats in the pond', metadata={'id': 1, 'topic': 'animals', 'location': 'pond'})]" - ] - }, - "execution_count": 10, - "metadata": {}, - "output_type": "execute_result" - } - ], "source": [ "vectorstore.similarity_search('ducks', k=10, filter={\n", " 'id': {'$in': [1, 5, 2, 9]},\n", " 'location': {'$in': [\"pond\", \"market\"]}\n", "})" - ] + ], + "outputs": [] }, { "cell_type": "code", @@ -322,19 +265,6 @@ "metadata": { "tags": [] }, - "outputs": [ - { - "data": { - "text/plain": [ - "[Document(page_content='ducks are also found in the pond', metadata={'id': 2, 'topic': 'animals', 'location': 'pond'}),\n", - " Document(page_content='there are cats in the pond', metadata={'id': 1, 'topic': 'animals', 'location': 'pond'})]" - ] - }, - "execution_count": 11, - "metadata": {}, - "output_type": "execute_result" - } - ], "source": [ "vectorstore.similarity_search('ducks', k=10, filter={\n", " '$and': [\n", @@ -343,7 +273,8 @@ " ]\n", "}\n", ")" - ] + ], + "outputs": [] }, { "cell_type": "code", @@ -352,30 +283,12 @@ "metadata": { "tags": [] }, - "outputs": [ - { - "data": { - "text/plain": [ - "[Document(page_content='the book club meets at the library', metadata={'id': 8, 'topic': 'reading', 'location': 'library'}),\n", - " Document(page_content='the new art exhibit is fascinating', metadata={'id': 5, 'topic': 'art', 'location': 'museum'}),\n", - " Document(page_content='the library hosts a weekly story time for kids', metadata={'id': 9, 'topic': 'reading', 'location': 'library'}),\n", - " Document(page_content='a sculpture exhibit is also at the museum', metadata={'id': 6, 'topic': 'art', 'location': 'museum'}),\n", - " Document(page_content='the market also sells fresh oranges', metadata={'id': 4, 'topic': 'food', 'location': 'market'}),\n", - " Document(page_content='a cooking class for beginners is offered at the community center', metadata={'id': 10, 'topic': 'classes', 'location': 'community center'}),\n", - " Document(page_content='a new coffee shop opened on Main Street', metadata={'id': 7, 'topic': 'food', 'location': 'Main Street'}),\n", - " Document(page_content='fresh apples are available at the market', metadata={'id': 3, 'topic': 'food', 'location': 'market'})]" - ] - }, - "execution_count": 12, - "metadata": {}, - "output_type": "execute_result" - } - ], "source": [ "vectorstore.similarity_search('bird', k=10, filter={\n", " 'location': { \"$ne\": 'pond'}\n", "})" - ] + ], + "outputs": [] } ], "metadata": { diff --git a/langchain_postgres/vectorstores.py b/langchain_postgres/vectorstores.py index 044bece..80f34da 100644 --- a/langchain_postgres/vectorstores.py +++ b/langchain_postgres/vectorstores.py @@ -1,6 +1,7 @@ # pylint: disable=too-many-lines from __future__ import annotations +import json import contextlib import enum import logging @@ -1057,17 +1058,38 @@ async def asimilarity_search_with_score_by_vector( def _results_to_docs_and_scores(self, results: Any) -> List[Tuple[Document, float]]: """Return docs and scores from results.""" - docs = [ - ( - Document( - id=str(result.EmbeddingStore.id), - page_content=result.EmbeddingStore.document, - metadata=result.EmbeddingStore.cmetadata, - ), - result.distance if self.embeddings is not None else None, + docs = [] + for result in results: + metadata = result.EmbeddingStore.cmetadata + + # Attempt to convert metadata to a dict + try: + if isinstance(metadata, dict): + pass # Already a dict + elif isinstance(metadata, str): + metadata = json.loads(metadata) + elif hasattr(metadata, 'buf'): + # For Fragment types (e.g., from asyncpg) + metadata_bytes = metadata.buf + metadata_str = metadata_bytes.decode('utf-8') + metadata = json.loads(metadata_str) + elif hasattr(metadata, 'decode'): + # For other byte-like types + metadata_str = metadata.decode('utf-8') + metadata = json.loads(metadata_str) + else: + metadata = {} # Default to empty dict if unknown type + except Exception as e: + self.logger.warning(f"Failed to deserialize metadata: {e}") + metadata = {} + + doc = Document( + id=str(result.EmbeddingStore.id), + page_content=result.EmbeddingStore.document, + metadata=metadata, ) - for result in results - ] + score = result.distance if self.embeddings is not None else None + docs.append((doc, score)) return docs def _handle_field_filter( diff --git a/poetry.lock b/poetry.lock index a648c81..766ce72 100644 --- a/poetry.lock +++ b/poetry.lock @@ -3510,4 +3510,4 @@ cffi = ["cffi (>=1.11)"] [metadata] lock-version = "2.0" python-versions = "^3.9" -content-hash = "a9a2c1b3ebd06e93ff340bed08e0c69886694c51bbf2c07033c170b1b77878bb" +content-hash = "a9a2c1b3ebd06e93ff340bed08e0c69886694c51bbf2c07033c170b1b77878bb" \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index cc25b79..8d34ae6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -81,7 +81,7 @@ build-backend = "poetry.core.masonry.api" # https://github.com/tophat/syrupy # --snapshot-warn-unused Prints a warning on unused snapshots rather than fail the test suite. addopts = "--strict-markers --strict-config --durations=5" -# Global timeout for all tests. There should be a good reason for a test to +# Global timeout for all tests. There should be a good reason for a test to # takemore than 30 seconds. timeout = 30 # Registering custom markers. @@ -92,4 +92,4 @@ asyncio_mode = "auto" [tool.codespell] skip = '.git,*.pdf,*.svg,*.pdf,*.yaml,*.ipynb,poetry.lock,*.min.js,*.css,package-lock.json,example_data,_dist,examples,templates,*.trig' ignore-regex = '.*(Stati Uniti|Tense=Pres).*' -ignore-words-list = 'momento,collison,ned,foor,reworkd,parth,whats,aapply,mysogyny,unsecure,damon,crate,aadd,symbl,precesses,accademia,nin' +ignore-words-list = 'momento,collison,ned,foor,reworkd,parth,whats,aapply,mysogyny,unsecure,damon,crate,aadd,symbl,precesses,accademia,nin' \ No newline at end of file diff --git a/tests/integration/test_pgvector_metadata_integration.py b/tests/integration/test_pgvector_metadata_integration.py new file mode 100644 index 0000000..7452695 --- /dev/null +++ b/tests/integration/test_pgvector_metadata_integration.py @@ -0,0 +1,148 @@ +""" +Integration Test for PGVector Retrieval Methods Using a Temporary Test Database + +This integration test verifies that PGVector retrieval methods work as expected, including: + - Retriever interface (ainvoke) + - asimilarity_search_with_score (retrieval with score by query string) + - asimilarity_search_by_vector (retrieval by query embedding) + - asimilarity_search_with_score_by_vector (retrieval with score by query embedding) + +Steps: + 1. Dynamically create a temporary test database (named "langchain_test_") using a session-scoped + pytest fixture. This ensures an isolated environment for testing. + 2. Initialize PGVector with FakeEmbeddings while disabling automatic extension creation. + 3. Manually create the pgvector extension (using AUTOCOMMIT) so that the custom "vector" type becomes available. + 4. Create the collection and tables in the temporary database. + 5. Insert test documents with predefined metadata. + 6. Perform retrieval using: + a) the retriever interface, + b) asimilarity_search_with_score (by query string), + c) asimilarity_search_by_vector (by query embedding), + d) asimilarity_search_with_score_by_vector (by query embedding). + 7. Assert that each returned document's metadata is deserialized as a Python dict and that scores (if applicable) are floats. + 8. Finally, clean up by disposing the async engine and dropping the temporary database. + +Usage: + Ensure your PostgreSQL instance (with pgvector enabled) is running, then execute: + poetry run pytest -s tests/integration/test_pgvector_retrieval_integration.py +""" + +import uuid +import pytest +from sqlalchemy.ext.asyncio import create_async_engine +from sqlalchemy import text +from langchain_postgres.vectorstores import PGVector +from langchain_core.documents import Document +from langchain_core.embeddings.fake import FakeEmbeddings + + +@pytest.fixture(scope="session") +async def test_database_url(): + """ + Create a temporary test database for integration testing and drop it afterwards. + + The temporary database name is generated as "langchain_test_". + This fixture connects to the default "postgres" database (with AUTOCOMMIT enabled) to run + CREATE/DROP DATABASE commands outside of a transaction block. It yields the connection string + for the temporary test database. + """ + db_suffix = uuid.uuid4().hex + test_db = f"langchain_test_{db_suffix}" + default_db_url = "postgresql+psycopg://langchain:langchain@localhost:6024/postgres" + engine = create_async_engine(default_db_url, isolation_level="AUTOCOMMIT") + async with engine.connect() as conn: + await conn.execute(text(f"CREATE DATABASE {test_db}")) + await engine.dispose() + + test_db_url = f"postgresql+psycopg://langchain:langchain@localhost:6024/{test_db}" + yield test_db_url + + engine = create_async_engine(default_db_url, isolation_level="AUTOCOMMIT") + async with engine.connect() as conn: + await conn.execute(text(f""" + SELECT pg_terminate_backend(pg_stat_activity.pid) + FROM pg_stat_activity + WHERE pg_stat_activity.datname = '{test_db}' + AND pid <> pg_backend_pid(); + """)) + await conn.execute(text(f"DROP DATABASE IF EXISTS {test_db}")) + await engine.dispose() + + +@pytest.mark.asyncio +async def test_all_retrieval_methods(test_database_url: str) -> None: + """ + Integration test for all PGVector retrieval methods. + + This test verifies: + a) The retriever interface via ainvoke(). + b) asimilarity_search_with_score() using a query string. + c) asimilarity_search_by_vector() using a query embedding. + d) asimilarity_search_with_score_by_vector() using a query embedding. + + In all cases, the metadata of returned documents should be deserialized as a Python dict, + and where scores are provided, they must be floats. + """ + connection = test_database_url + embeddings = FakeEmbeddings(size=1536) + vectorstore = PGVector( + embeddings=embeddings, + connection=connection, + collection_name="integration_test_retrieval", + use_jsonb=True, + async_mode=True, + create_extension=False, # We'll manually create the extension. + pre_delete_collection=True, # Ensure a fresh collection. + ) + + # Manually create the pgvector extension on the test database. + async with vectorstore._async_engine.connect() as conn: + conn = await conn.execution_options(isolation_level="AUTOCOMMIT") + await conn.execute(text("CREATE EXTENSION IF NOT EXISTS vector;")) + + # Create the collection (and underlying tables). + await vectorstore.acreate_collection() + + # Insert sample documents with metadata. + documents = [ + Document(page_content="Document 1", metadata={"user": "foo"}), + Document(page_content="Document 2", metadata={"user": "bar"}), + Document(page_content="Another Document", metadata={"user": "baz"}), + ] + await vectorstore.aadd_texts( + texts=[doc.page_content for doc in documents], + metadatas=[doc.metadata for doc in documents] + ) + + # a) Test the retriever interface. + retriever = vectorstore.as_retriever() + retrieved_docs = await retriever.ainvoke("Document") + for doc in retrieved_docs: + assert isinstance(doc.metadata, dict), f"[Retriever] Metadata is not a dict: {doc.metadata}" + print(f"[Retriever] Document: {doc.page_content}, Metadata: {doc.metadata}") + + # b) Test asimilarity_search_with_score (by query string). + scored_results = await vectorstore.asimilarity_search_with_score("Document", k=2) + for doc, score in scored_results: + assert isinstance(doc.metadata, dict), f"[With Score] Metadata is not a dict: {doc.metadata}" + assert isinstance(score, float), f"[With Score] Score is not a float: {score}" + print(f"[With Score] Document: {doc.page_content}, Metadata: {doc.metadata}, Score: {score}") + + # Obtain a query embedding. + query_embedding = await vectorstore.embeddings.aembed_query("Document") + + # c) Test asimilarity_search_by_vector (by query embedding). + docs_by_vector = await vectorstore.asimilarity_search_by_vector(query_embedding, k=2) + for doc in docs_by_vector: + assert isinstance(doc.metadata, dict), f"[By Vector] Metadata is not a dict: {doc.metadata}" + print(f"[By Vector] Document: {doc.page_content}, Metadata: {doc.metadata}") + + # d) Test asimilarity_search_with_score_by_vector (by query embedding). + scored_docs_by_vector = await vectorstore.asimilarity_search_with_score_by_vector(query_embedding, k=2) + for doc, score in scored_docs_by_vector: + assert isinstance(doc.metadata, dict), f"[With Score By Vector] Metadata is not a dict: {doc.metadata}" + assert isinstance(score, float), f"[With Score By Vector] Score is not a float: {score}" + print(f"[With Score By Vector] Document: {doc.page_content}, Metadata: {doc.metadata}, Score: {score}") + + # Clean up: Dispose the async engine to close all connections. + await vectorstore._async_engine.dispose() diff --git a/tests/unit_tests/test_vectorstore.py b/tests/unit_tests/test_vectorstore.py index 2383daf..ef4eeb3 100644 --- a/tests/unit_tests/test_vectorstore.py +++ b/tests/unit_tests/test_vectorstore.py @@ -708,7 +708,7 @@ def test_pgvector_retriever_search_threshold_custom_normalization_fn() -> None: @pytest.mark.asyncio async def test_async_pgvector_retriever_search_threshold_custom_normalization_fn() -> ( - None + None ): """Test searching with threshold and custom normalization function""" texts = ["foo", "bar", "baz"] @@ -878,7 +878,7 @@ async def async_pgvector() -> AsyncGenerator[PGVector, None]: @contextlib.contextmanager def get_vectorstore( - *, embedding: Optional[Embeddings] = None + *, embedding: Optional[Embeddings] = None ) -> Generator[PGVector, None, None]: """Get a pre-populated-vectorstore""" store = PGVector.from_documents( @@ -898,7 +898,7 @@ def get_vectorstore( @contextlib.asynccontextmanager async def aget_vectorstore( - *, embedding: Optional[Embeddings] = None + *, embedding: Optional[Embeddings] = None ) -> AsyncGenerator[PGVector, None]: """Get a pre-populated-vectorstore""" store = await PGVector.afrom_documents( @@ -918,8 +918,8 @@ async def aget_vectorstore( @pytest.mark.parametrize("test_filter, expected_ids", TYPE_1_FILTERING_TEST_CASES) def test_pgvector_with_with_metadata_filters_1( - test_filter: Dict[str, Any], - expected_ids: List[int], + test_filter: Dict[str, Any], + expected_ids: List[int], ) -> None: """Test end to end construction and search.""" with get_vectorstore() as pgvector: @@ -930,8 +930,8 @@ def test_pgvector_with_with_metadata_filters_1( @pytest.mark.asyncio @pytest.mark.parametrize("test_filter, expected_ids", TYPE_1_FILTERING_TEST_CASES) async def test_async_pgvector_with_with_metadata_filters_1( - test_filter: Dict[str, Any], - expected_ids: List[int], + test_filter: Dict[str, Any], + expected_ids: List[int], ) -> None: """Test end to end construction and search.""" async with aget_vectorstore() as pgvector: @@ -941,9 +941,9 @@ async def test_async_pgvector_with_with_metadata_filters_1( @pytest.mark.parametrize("test_filter, expected_ids", TYPE_2_FILTERING_TEST_CASES) def test_pgvector_with_with_metadata_filters_2( - pgvector: PGVector, - test_filter: Dict[str, Any], - expected_ids: List[int], + pgvector: PGVector, + test_filter: Dict[str, Any], + expected_ids: List[int], ) -> None: """Test end to end construction and search.""" docs = pgvector.similarity_search("meow", k=5, filter=test_filter) @@ -953,9 +953,9 @@ def test_pgvector_with_with_metadata_filters_2( @pytest.mark.asyncio @pytest.mark.parametrize("test_filter, expected_ids", TYPE_2_FILTERING_TEST_CASES) async def test_async_pgvector_with_with_metadata_filters_2( - async_pgvector: PGVector, - test_filter: Dict[str, Any], - expected_ids: List[int], + async_pgvector: PGVector, + test_filter: Dict[str, Any], + expected_ids: List[int], ) -> None: """Test end to end construction and search.""" docs = await async_pgvector.asimilarity_search("meow", k=5, filter=test_filter) @@ -964,9 +964,9 @@ async def test_async_pgvector_with_with_metadata_filters_2( @pytest.mark.parametrize("test_filter, expected_ids", TYPE_3_FILTERING_TEST_CASES) def test_pgvector_with_with_metadata_filters_3( - pgvector: PGVector, - test_filter: Dict[str, Any], - expected_ids: List[int], + pgvector: PGVector, + test_filter: Dict[str, Any], + expected_ids: List[int], ) -> None: """Test end to end construction and search.""" docs = pgvector.similarity_search("meow", k=5, filter=test_filter) @@ -976,9 +976,9 @@ def test_pgvector_with_with_metadata_filters_3( @pytest.mark.asyncio @pytest.mark.parametrize("test_filter, expected_ids", TYPE_3_FILTERING_TEST_CASES) async def test_async_pgvector_with_with_metadata_filters_3( - async_pgvector: PGVector, - test_filter: Dict[str, Any], - expected_ids: List[int], + async_pgvector: PGVector, + test_filter: Dict[str, Any], + expected_ids: List[int], ) -> None: """Test end to end construction and search.""" docs = await async_pgvector.asimilarity_search("meow", k=5, filter=test_filter) @@ -987,9 +987,9 @@ async def test_async_pgvector_with_with_metadata_filters_3( @pytest.mark.parametrize("test_filter, expected_ids", TYPE_4_FILTERING_TEST_CASES) def test_pgvector_with_with_metadata_filters_4( - pgvector: PGVector, - test_filter: Dict[str, Any], - expected_ids: List[int], + pgvector: PGVector, + test_filter: Dict[str, Any], + expected_ids: List[int], ) -> None: """Test end to end construction and search.""" docs = pgvector.similarity_search("meow", k=5, filter=test_filter) @@ -999,9 +999,9 @@ def test_pgvector_with_with_metadata_filters_4( @pytest.mark.asyncio @pytest.mark.parametrize("test_filter, expected_ids", TYPE_4_FILTERING_TEST_CASES) async def test_async_pgvector_with_with_metadata_filters_4( - async_pgvector: PGVector, - test_filter: Dict[str, Any], - expected_ids: List[int], + async_pgvector: PGVector, + test_filter: Dict[str, Any], + expected_ids: List[int], ) -> None: """Test end to end construction and search.""" docs = await async_pgvector.asimilarity_search("meow", k=5, filter=test_filter) @@ -1010,9 +1010,9 @@ async def test_async_pgvector_with_with_metadata_filters_4( @pytest.mark.parametrize("test_filter, expected_ids", TYPE_5_FILTERING_TEST_CASES) def test_pgvector_with_with_metadata_filters_5( - pgvector: PGVector, - test_filter: Dict[str, Any], - expected_ids: List[int], + pgvector: PGVector, + test_filter: Dict[str, Any], + expected_ids: List[int], ) -> None: """Test end to end construction and search.""" docs = pgvector.similarity_search("meow", k=5, filter=test_filter) @@ -1022,9 +1022,9 @@ def test_pgvector_with_with_metadata_filters_5( @pytest.mark.asyncio @pytest.mark.parametrize("test_filter, expected_ids", TYPE_5_FILTERING_TEST_CASES) async def test_async_pgvector_with_with_metadata_filters_5( - async_pgvector: PGVector, - test_filter: Dict[str, Any], - expected_ids: List[int], + async_pgvector: PGVector, + test_filter: Dict[str, Any], + expected_ids: List[int], ) -> None: """Test end to end construction and search.""" docs = await async_pgvector.asimilarity_search("meow", k=5, filter=test_filter)