mirror of
https://github.com/semantica-agi/semantica.git
synced 2026-09-02 04:00:40 +00:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8e7aaee4f5 |
@@ -30,8 +30,7 @@ each file's own autogenerated header comment for its exact command).
|
||||
| `pep517-build.txt` | ci.yml, benchmark.yml, Dockerfile | exact `[build-system] requires` from `pyproject.toml` (setuptools, wheel) - installed with `--no-build-isolation` before any `pip install -e .` / `pip install .`, since `--no-deps` alone doesn't stop pip's PEP 517 build isolation from fetching those two *unhashed* |
|
||||
| `explorer-extra-py311.txt` | ci.yml | semantica's base deps + the `explorer` extra, resolved for python 3.11 |
|
||||
| `explorer-extra-py313.txt` | Dockerfile | the same, resolved for python 3.13 (the image's actual interpreter) |
|
||||
| `pgvector-extra.txt` | integration.yml | semantica's base deps + the `vectorstore-pgvector` extra, resolved for python 3.11 |
|
||||
| `pytest-tool.txt` | ci.yml, integration.yml | pytest, for the pre-all-extras deterministic test |
|
||||
| `pytest-tool.txt` | ci.yml | pytest, for the pre-all-extras deterministic test |
|
||||
| `uv-tool.txt` | ci.yml | uv, to verify requirements-ci.txt is current |
|
||||
| `build-tools.txt` | ci.yml, release.yml | build, wheel |
|
||||
| `twine.txt` | release.yml | twine |
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,91 +0,0 @@
|
||||
name: Integration Tests
|
||||
|
||||
# Separate from ci.yml, which is a required check: a slow image pull or a
|
||||
# container flake must not block unrelated merges.
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [main]
|
||||
paths-ignore:
|
||||
- 'docs/**'
|
||||
- 'docs_check.py'
|
||||
- '**/*.md'
|
||||
schedule:
|
||||
- cron: '0 5 * * 1'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
pgvector:
|
||||
name: pgvector (live PostgreSQL)
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
|
||||
services:
|
||||
postgres:
|
||||
# pgvector/pgvector:pg16 as published 2026-08-13. Pinned by digest like
|
||||
# the action pins, though verify-action-pins.sh does not check images.
|
||||
image: pgvector/pgvector@sha256:ccc6e83d6e35e931dc7c5def2022729d5a6c370318d099181995567ff1fb4d6b
|
||||
env:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_DB: test
|
||||
# Throwaway container reachable only from this job, so trust auth
|
||||
# avoids putting a credential in the workflow at all.
|
||||
POSTGRES_HOST_AUTH_METHOD: trust
|
||||
ports:
|
||||
- 5432:5432
|
||||
options: >-
|
||||
--health-cmd "pg_isready -U postgres -d test"
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 10
|
||||
|
||||
env:
|
||||
TEST_PGVECTOR_URL: postgresql://postgres@localhost:5432/test
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
|
||||
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7
|
||||
with:
|
||||
python-version: '3.11'
|
||||
cache: 'pip'
|
||||
|
||||
- name: Install semantica with the pgvector extra
|
||||
# Hash-verified installs throughout, matching ci.yml/security.yml/etc
|
||||
# (OpenSSF Scorecard's Pinned-Dependencies check). --no-deps here
|
||||
# skips runtime dependency resolution for the editable install itself
|
||||
# (nothing to hash); pep517-build.txt + --no-build-isolation stops
|
||||
# its PEP 517 build from separately fetching an unhashed
|
||||
# setuptools/wheel via build isolation.
|
||||
run: |
|
||||
pip install -r .github/requirements/bootstrap.txt --require-hashes
|
||||
pip install -r .github/requirements/pep517-build.txt --require-hashes
|
||||
pip install --no-deps --no-build-isolation -e .
|
||||
pip install -r .github/requirements/pgvector-extra.txt --require-hashes
|
||||
pip install -r .github/requirements/pytest-tool.txt --require-hashes
|
||||
|
||||
- name: Create the vector extension
|
||||
# PgVectorStore._verify_pgvector_extension() requires it and refuses to
|
||||
# create it. Doubles as the connectivity gate.
|
||||
run: |
|
||||
python - <<'PY'
|
||||
import os
|
||||
|
||||
import psycopg
|
||||
|
||||
with psycopg.connect(os.environ["TEST_PGVECTOR_URL"]) as conn:
|
||||
conn.execute("CREATE EXTENSION IF NOT EXISTS vector")
|
||||
conn.commit()
|
||||
print("vector extension ready")
|
||||
PY
|
||||
|
||||
- name: Run the live pgvector suite
|
||||
# pg_available raises rather than skipping when TEST_PGVECTOR_URL was
|
||||
# set explicitly (which this job always does), so a service that's
|
||||
# actually unreachable fails this step instead of the suite quietly
|
||||
# reporting green having run nothing.
|
||||
run: |
|
||||
pytest tests/vector_store/test_pgvector_store.py -v -rs
|
||||
@@ -438,6 +438,46 @@ class MilvusStore:
|
||||
raise ProcessingError(f"Collection {collection_name} does not exist")
|
||||
|
||||
collection = Collection(collection_name)
|
||||
# Reject schemas that don't match create_collection()'s shape:
|
||||
# id/VARCHAR pk + vector + metadata. Otherwise an incompatible
|
||||
# collection attaches and fails far later in get_vector/get_metadata.
|
||||
schema = getattr(collection, "schema", None)
|
||||
fields = list(getattr(schema, "fields", None) or [])
|
||||
pk = [f for f in fields if getattr(f, "is_primary", False)]
|
||||
if (
|
||||
not pk
|
||||
or pk[0].name != "id"
|
||||
or getattr(getattr(pk[0], "dtype", None), "name", None) != "VARCHAR"
|
||||
or getattr(pk[0], "auto_id", False)
|
||||
):
|
||||
raise ProcessingError(
|
||||
f"Collection '{collection_name}' has an invalid primary key: "
|
||||
"expected VARCHAR field 'id' without auto_id"
|
||||
)
|
||||
vector_field = next((f for f in fields if f.name == "vector"), None)
|
||||
if vector_field is None:
|
||||
raise ProcessingError(
|
||||
f"Collection '{collection_name}' is missing required field 'vector'"
|
||||
)
|
||||
if (
|
||||
getattr(getattr(vector_field, "dtype", None), "name", None)
|
||||
!= "FLOAT_VECTOR"
|
||||
):
|
||||
raise ProcessingError(
|
||||
f"Collection '{collection_name}' has an invalid vector field: "
|
||||
"expected FLOAT_VECTOR 'vector'"
|
||||
)
|
||||
metadata_field = next((f for f in fields if f.name == "metadata"), None)
|
||||
if metadata_field is None:
|
||||
raise ProcessingError(
|
||||
f"Collection '{collection_name}' is missing required field 'metadata'"
|
||||
)
|
||||
if getattr(getattr(metadata_field, "dtype", None), "name", None) != "JSON":
|
||||
raise ProcessingError(
|
||||
f"Collection '{collection_name}' has an invalid metadata field: "
|
||||
"expected JSON 'metadata'"
|
||||
)
|
||||
|
||||
self.collection = MilvusCollection(collection, collection_name)
|
||||
self.search_engine = MilvusSearch(self.collection)
|
||||
return self.collection
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
"""Tests for MilvusStore.get_collection schema validation (#1331)."""
|
||||
|
||||
from unittest import TestCase
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from semantica.vector_store.milvus_store import MilvusStore
|
||||
from semantica.utils.exceptions import ProcessingError
|
||||
|
||||
|
||||
def _field(name, dtype_name, primary=False, auto_id=False):
|
||||
f = MagicMock()
|
||||
f.name = name
|
||||
f.is_primary = primary
|
||||
f.auto_id = auto_id
|
||||
f.dtype.name = dtype_name
|
||||
return f
|
||||
|
||||
|
||||
class MilvusGetCollectionSchemaTest(TestCase):
|
||||
def setUp(self):
|
||||
self.patches = [
|
||||
patch("semantica.vector_store.milvus_store.MILVUS_AVAILABLE", True),
|
||||
patch("semantica.vector_store.milvus_store.utility"),
|
||||
patch("semantica.vector_store.milvus_store.Collection"),
|
||||
]
|
||||
for p in self.patches:
|
||||
p.start()
|
||||
# utility.has_collection() must return truthy
|
||||
import semantica.vector_store.milvus_store as m
|
||||
|
||||
m.utility.has_collection.return_value = True
|
||||
|
||||
def tearDown(self):
|
||||
for p in reversed(self.patches):
|
||||
p.stop()
|
||||
|
||||
def _make_store(self, coll):
|
||||
store = MilvusStore()
|
||||
store.client = MagicMock() # skip real connect
|
||||
import semantica.vector_store.milvus_store as m
|
||||
|
||||
m.Collection.return_value = coll
|
||||
return store
|
||||
|
||||
def _assert_rejected(self, store, expected_msg):
|
||||
with self.assertRaises(ProcessingError) as ctx:
|
||||
store.get_collection("c")
|
||||
self.assertIn(expected_msg, str(ctx.exception))
|
||||
self.assertIsNone(store.collection)
|
||||
|
||||
def test_accepts_matching_schema(self):
|
||||
coll = MagicMock()
|
||||
coll.schema.fields = [
|
||||
_field("id", "VARCHAR", primary=True),
|
||||
_field("vector", "FLOAT_VECTOR"),
|
||||
_field("metadata", "JSON"),
|
||||
]
|
||||
store = self._make_store(coll)
|
||||
result = store.get_collection("c")
|
||||
self.assertIsNotNone(result)
|
||||
self.assertIsNotNone(store.collection)
|
||||
self.assertIsNotNone(store.search_engine)
|
||||
self.assertEqual(store.collection.collection_name, "c")
|
||||
|
||||
def test_rejects_non_varchar_primary_key(self):
|
||||
coll = MagicMock()
|
||||
coll.schema.fields = [
|
||||
_field("id", "INT64", primary=True),
|
||||
_field("vector", "FLOAT_VECTOR"),
|
||||
_field("metadata", "JSON"),
|
||||
]
|
||||
store = self._make_store(coll)
|
||||
self._assert_rejected(store, "has an invalid primary key")
|
||||
|
||||
def test_rejects_missing_primary_key(self):
|
||||
coll = MagicMock()
|
||||
coll.schema.fields = [
|
||||
_field("id", "VARCHAR"),
|
||||
_field("vector", "FLOAT_VECTOR"),
|
||||
_field("metadata", "JSON"),
|
||||
]
|
||||
store = self._make_store(coll)
|
||||
self._assert_rejected(store, "has an invalid primary key")
|
||||
|
||||
def test_rejects_wrongly_named_primary_key(self):
|
||||
coll = MagicMock()
|
||||
coll.schema.fields = [
|
||||
_field("pk", "VARCHAR", primary=True),
|
||||
_field("vector", "FLOAT_VECTOR"),
|
||||
_field("metadata", "JSON"),
|
||||
]
|
||||
store = self._make_store(coll)
|
||||
self._assert_rejected(store, "has an invalid primary key")
|
||||
|
||||
def test_rejects_missing_metadata_field(self):
|
||||
coll = MagicMock()
|
||||
coll.schema.fields = [
|
||||
_field("id", "VARCHAR", primary=True),
|
||||
_field("vector", "FLOAT_VECTOR"),
|
||||
]
|
||||
store = self._make_store(coll)
|
||||
self._assert_rejected(store, "is missing required field 'metadata'")
|
||||
|
||||
def test_rejects_missing_vector_field(self):
|
||||
coll = MagicMock()
|
||||
coll.schema.fields = [
|
||||
_field("id", "VARCHAR", primary=True),
|
||||
_field("metadata", "JSON"),
|
||||
]
|
||||
store = self._make_store(coll)
|
||||
self._assert_rejected(store, "is missing required field 'vector'")
|
||||
|
||||
def test_rejects_wrong_vector_dtype(self):
|
||||
coll = MagicMock()
|
||||
coll.schema.fields = [
|
||||
_field("id", "VARCHAR", primary=True),
|
||||
_field("vector", "BINARY_VECTOR"),
|
||||
_field("metadata", "JSON"),
|
||||
]
|
||||
store = self._make_store(coll)
|
||||
self._assert_rejected(store, "has an invalid vector field")
|
||||
|
||||
def test_rejects_auto_id_primary_key(self):
|
||||
coll = MagicMock()
|
||||
coll.schema.fields = [
|
||||
_field("id", "VARCHAR", primary=True, auto_id=True),
|
||||
_field("vector", "FLOAT_VECTOR"),
|
||||
_field("metadata", "JSON"),
|
||||
]
|
||||
store = self._make_store(coll)
|
||||
self._assert_rejected(store, "has an invalid primary key")
|
||||
|
||||
def test_rejects_non_json_metadata(self):
|
||||
coll = MagicMock()
|
||||
coll.schema.fields = [
|
||||
_field("id", "VARCHAR", primary=True),
|
||||
_field("vector", "FLOAT_VECTOR"),
|
||||
_field("metadata", "STRING"),
|
||||
]
|
||||
store = self._make_store(coll)
|
||||
self._assert_rejected(store, "has an invalid metadata field")
|
||||
@@ -10,7 +10,7 @@ To run these tests locally with Docker:
|
||||
-e POSTGRES_PASSWORD=postgres \
|
||||
-e POSTGRES_DB=test \
|
||||
-p 5432:5432 \
|
||||
pgvector/pgvector:pg16
|
||||
ankane/pgvector:latest
|
||||
|
||||
pytest tests/vector_store/test_pgvector_store.py -v
|
||||
|
||||
@@ -63,37 +63,29 @@ TEST_CONNECTION_STRING = os.getenv(
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def pg_available() -> bool:
|
||||
"""Check if PostgreSQL with pgvector is available.
|
||||
|
||||
A connection failure only means "skip" when TEST_PGVECTOR_URL wasn't set
|
||||
explicitly, i.e. this is a local run falling back to the documented
|
||||
default. CI sets it on purpose, so a failure there means the service is
|
||||
genuinely broken and the suite should fail loudly instead of skipping.
|
||||
"""
|
||||
"""Check if PostgreSQL with pgvector is available."""
|
||||
if not psycopg_available:
|
||||
return False
|
||||
|
||||
explicit_url = "TEST_PGVECTOR_URL" in os.environ
|
||||
|
||||
try:
|
||||
try:
|
||||
import psycopg
|
||||
if psycopg_available:
|
||||
try:
|
||||
import psycopg
|
||||
|
||||
conn = psycopg.connect(TEST_CONNECTION_STRING, connect_timeout=5)
|
||||
except ImportError:
|
||||
import psycopg2
|
||||
conn = psycopg.connect(TEST_CONNECTION_STRING, connect_timeout=5)
|
||||
except ImportError:
|
||||
import psycopg2
|
||||
|
||||
conn = psycopg2.connect(TEST_CONNECTION_STRING, connect_timeout=5)
|
||||
conn = psycopg2.connect(TEST_CONNECTION_STRING, connect_timeout=5)
|
||||
|
||||
cur = conn.cursor()
|
||||
cur.execute("SELECT 1")
|
||||
cur.close()
|
||||
conn.close()
|
||||
return True
|
||||
cur = conn.cursor()
|
||||
cur.execute("SELECT 1")
|
||||
cur.close()
|
||||
conn.close()
|
||||
return True
|
||||
except Exception:
|
||||
if explicit_url:
|
||||
raise
|
||||
return False
|
||||
return False
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -199,13 +191,7 @@ class TestPgVectorStoreAdd:
|
||||
ids = store.add(vectors, metadata)
|
||||
|
||||
assert len(ids) == 5
|
||||
assert len(set(ids)) == 5
|
||||
# add() assigns uuid4 identifiers, not a "vec_" prefix
|
||||
for vector_id in ids:
|
||||
try:
|
||||
uuid.UUID(vector_id)
|
||||
except ValueError:
|
||||
pytest.fail(f"{vector_id!r} is not a valid uuid4 id")
|
||||
assert all(id.startswith("vec_") for id in ids)
|
||||
|
||||
def test_add_auto_generate_ids(self, store):
|
||||
"""Test that IDs are auto-generated if not provided."""
|
||||
@@ -304,37 +290,38 @@ class TestPgVectorStoreSearch:
|
||||
if not pg_available:
|
||||
pytest.skip("PostgreSQL not available")
|
||||
|
||||
from semantica.vector_store.pgvector_store import PgVectorStore, psycopg_sql
|
||||
from semantica.vector_store.pgvector_store import PgVectorStore
|
||||
|
||||
# setup_vectors is autouse and seeds unique_table_name, and fixtures are
|
||||
# cached per test, so this needs a table of its own to be empty at all.
|
||||
empty_table = f"{unique_table_name}_empty"
|
||||
empty_store = PgVectorStore(
|
||||
connection_string=TEST_CONNECTION_STRING,
|
||||
table_name=empty_table,
|
||||
table_name=unique_table_name,
|
||||
dimension=128,
|
||||
distance_metric="cosine",
|
||||
)
|
||||
|
||||
try:
|
||||
query = np.random.rand(128).astype(np.float32)
|
||||
results = empty_store.search(query, top_k=5)
|
||||
query = np.random.rand(128).astype(np.float32)
|
||||
results = empty_store.search(query, top_k=5)
|
||||
|
||||
assert len(results) == 0
|
||||
finally:
|
||||
try:
|
||||
with empty_store._get_connection() as conn:
|
||||
cur = conn.cursor()
|
||||
cur.execute(
|
||||
psycopg_sql.SQL("DROP TABLE IF EXISTS {}").format(
|
||||
psycopg_sql.Identifier(empty_table)
|
||||
)
|
||||
)
|
||||
conn.commit()
|
||||
cur.close()
|
||||
empty_store.close()
|
||||
except Exception:
|
||||
pass
|
||||
assert len(results) == 0
|
||||
|
||||
# Cleanup: Drop test table after test completes
|
||||
# Uses best-effort cleanup - failures are silently ignored since
|
||||
# this is teardown of optional test resources
|
||||
try:
|
||||
with empty_store._get_connection() as conn:
|
||||
cur = conn.cursor()
|
||||
from semantica.vector_store.pgvector_store import psycopg_sql
|
||||
drop_sql = psycopg_sql.SQL("DROP TABLE IF EXISTS {}").format(
|
||||
psycopg_sql.Identifier(unique_table_name)
|
||||
)
|
||||
cur.execute(drop_sql)
|
||||
conn.commit()
|
||||
cur.close()
|
||||
empty_store.close()
|
||||
except Exception:
|
||||
# Best-effort cleanup: PostgreSQL may be unavailable during teardown
|
||||
# This is expected when tests are skipped or connection is lost
|
||||
pass
|
||||
|
||||
|
||||
class TestPgVectorStoreGet:
|
||||
|
||||
Reference in New Issue
Block a user