Python: Bug: ChromaCollection mishandles storage_name on the key and vector fields, depending on whether the record is a dict, dataclass or pydantic model

Đang mở
#14,755 0 bình luận 0 reaction 0 người được giao Xem trên GitHub

Maintainer thường phản hồi trong vòng 5 ngày

Chưa có ai nhận issue này.

Đánh giá

Độ khó
3/5
Thời gian dự kiến
1-2 ngày
Mức phù hợp với người mới
35/100
Loại issue
Lỗi
Độ rõ ràng
Đặc tả rõ ràng
Mức độ hoạt động
Sôi nổi
Công nghệ
python
Lĩnh vực
backend, database

Hướng nghiên cứu

Start with python/semantic_kernel/connectors/chroma.py, especially _serialize_dicts_to_store_models and _deserialize_store_models_to_dicts, then read tests/unit/connectors/memory/test_chroma.py. Reproduce the dict, dataclass, and pydantic cases using an ephemeral Chroma client. Done means upsert, get, search with vectors, and filters using storage names behave consistently for all three record types; the issue reports a ready branch and no pull request.

Do mô hình lập chỉ mục viết ra từ nội dung của issue.

Mô tả

python triage
Description

ChromaCollection handles a field with a storage_name in four different ways, depending on the record type and the field:

  • A dict or dataclass record with a storage_name on the key field fails to upsert: VectorStoreModelSerializationException: Error serializing records: 'id'.
  • A pydantic record with a storage_name on the vector field fails to upsert: Error serializing records: 'doc_vector'.
  • A dict or dataclass record with a storage_name on the vector field upserts, but get and search return vector=None even with include_vectors=True.
  • A pydantic record is stored with its metadata under the declared field names. Filters only accept storage names, so a filter on a field with a storage_name finds nothing.

The base class hands the connector a dict keyed by storage name for dict and dataclass records, and keyed by field name for pydantic models. The Chroma serializer and deserializer assume one shape for some fields and the other shape for the rest.

Reproduction

I ran this on current main (df1cb33d0), Python 3.13, chromadb 1.5.9 with chromadb.EphemeralClient(), so no server is involved.

import asyncio
import uuid
import warnings
from dataclasses import dataclass

warnings.filterwarnings("ignore")
import chromadb
from pydantic import BaseModel

from semantic_kernel.connectors.chroma import ChromaCollection
from semantic_kernel.data.vector import VectorStoreCollectionDefinition, VectorStoreField

CLIENT = chromadb.EphemeralClient()


def definition(key_alias=None, vector_alias=None):
    return VectorStoreCollectionDefinition(
        fields=[
            VectorStoreField("key", name="id", type="str", storage_name=key_alias),
            VectorStoreField("data", name="text", type="str", storage_name="doc_text", is_indexed=True),
            VectorStoreField("vector", name="vector", dimensions=2, type="float", storage_name=vector_alias),
        ]
    )


@dataclass
class DC:
    id: str
    text: str
    vector: list[float] | None = None


class PM(BaseModel):
    id: str
    text: str
    vector: list[float] | None = None


def record(kind):
    values = {"id": "k1", "text": "a", "vector": [1.0, 0.0]}
    return values if kind is dict else kind(**values)


def show(rec):
    values = rec if isinstance(rec, dict) else vars(rec) if isinstance(rec, DC) else rec.model_dump()
    return {k: ([float(x) for x in v] if k == "vector" and v is not None else v) for k, v in values.items()}


async def roundtrip(kind, defn, label):
    collection = ChromaCollection(
        record_type=kind, definition=defn, collection_name=f"t-{uuid.uuid4().hex}", client=CLIENT
    )
    await collection.ensure_collection_exists()
    try:
        await collection.upsert(record(kind))
        print(f"{kind.__name__:5s} {label}: get -> {show(await collection.get('k1', include_vectors=True))}")
    except Exception as ex:
        print(f"{kind.__name__:5s} {label}: {type(ex).__name__}: {ex}")


async def filtered(kind):
    collection = ChromaCollection(
        record_type=kind, definition=definition(), collection_name=f"t-{uuid.uuid4().hex}", client=CLIENT
    )
    await collection.ensure_collection_exists()
    await collection.upsert(record(kind))
    results = await collection.search(vector=[1.0, 0.0], filter=lambda x: x.doc_text == "a")
    print(f"{kind.__name__:5s} filter on doc_text: {len([r async for r in results.results])} result(s)")


async def main():
    for kind in (dict, DC, PM):
        await roundtrip(kind, definition(vector_alias="doc_vector"), "vector storage_name")
    for kind in (dict, DC, PM):
        await roundtrip(kind, definition(key_alias="point_id"), "key storage_name   ")
    for kind in (dict, DC, PM):
        await filtered(kind)


asyncio.run(main())

Output on main:

dict  vector storage_name: get -> {'id': 'k1', 'text': 'a', 'vector': None}
DC    vector storage_name: get -> {'id': 'k1', 'text': 'a', 'vector': None}
PM    vector storage_name: VectorStoreModelSerializationException: Error serializing records: 'doc_vector'
dict  key storage_name   : VectorStoreModelSerializationException: Error serializing records: 'id'
DC    key storage_name   : VectorStoreModelSerializationException: Error serializing records: 'id'
PM    key storage_name   : get -> {'id': 'k1', 'text': 'a', 'vector': [1.0, 0.0]}
dict  filter on doc_text: 1 result(s)
DC    filter on doc_text: 1 result(s)
PM    filter on doc_text: 0 result(s)

The data field text has a storage_name in every run. That alone works for dict and dataclass records.

Cause

In python/semantic_kernel/connectors/chroma.py:

  • _serialize_dicts_to_store_models reads the key with record[self.definition.key_name] (declared name) and the vector with record[vector_field.storage_name or vector_field.name] (storage name). Each is right for only one of the two record shapes. Everything else stays in the metadata under the key it arrived with, so pydantic metadata keeps the declared names.
  • _deserialize_store_models_to_dicts puts the key and the vector back under the declared names. For dict and dataclass records the data model step reads record.get(field.storage_name or field.name), so a vector with a storage_name is lost.

The same kind of mismatch exists in the Qdrant connector, see #14754.

Expected

Upsert, get and search behave the same for dict, dataclass and pydantic records, with or without storage_name. The metadata uses the storage names, which is what filters use.

Possible fix

I have a branch ready: https://github.com/VANDRANKI/semantic-kernel/tree/fix/chroma-storage-name-record-keys

  • _serialize_dicts_to_store_models renames declared names to storage names in a copy of the record (only when the storage name is not already there), and then reads the key and the vector by storage name.
  • _deserialize_store_models_to_dicts returns the key and the vector under their storage names. The data model step maps them back for pydantic records.

It adds 6 tests to tests/unit/connectors/memory/test_chroma.py (dict, dataclass and pydantic, each with and without a key storage_name). They upsert, get, search with vectors and filter on a storage name, on an ephemeral Chroma client. All 6 fail on main. The 26 tests in that file pass on the branch, and ruff check and format are clean.

I can't open a pull request from this account right now, so the compare link is https://github.com/microsoft/semantic-kernel/compare/main...VANDRANKI:semantic-kernel:fix/chroma-storage-name-record-keys

Ngôn ngữ chính
C#
Star
28.7k
Fork
4.9k
Merge trung bình
5 ngày 3 giờ
Pull request đã merge (30 ngày)
63

Chuẩn bị môi trường

Mở trong Codespaces

Khởi chạy dev container của dự án ngay trên trình duyệt, bằng tài khoản GitHub của bạn.

Bắt đầu từ đâu

  1. Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
  2. Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
  3. Fork repository và làm thay đổi trên một nhánh.
  4. Mở pull request có tham chiếu số hiệu của issue.

Issue khác của microsoft/semantic-kernel

Tất cả issue của microsoft/semantic-kernel

Issue tương tự

Thêm issue về C#

Nhận issue mới trong hộp thư của bạn

Bản tóm tắt ngắn những issue GitHub phù hợp với người mới.