Files
Hy-Memory/tests/test_clone_qdrant_sparse.py
T
2026-10-16 16:26:27 +08:00

109 lines
4.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
"""
clone_user 在 BM25 sparse qdrant 集合上的回归。
0.1.6.4 起 qdrant 启用 fastembed BM25 后,scroll/retrieve 返回的 point.vector
是命名多向量 dict({"": dense, "bm25": SparseVector})。clone 走 list_by_user
读 src 节点,旧代码把整个 dict 赋给 node.embedding;clone 复制后再 upsert,
_build_vector_field 把 dict 当 List[float] 又包一层 → 非法嵌套 → PointStruct
ValidationError → rollback → cloned_count=0。
本测试在带 sparse 的 qdrant 集合上:
1. list_by_user 返回的 embedding 必须是 List[float](不是 dict);
2. clone_user 成功,cloned_count == src 节点数;
3. dst 可被 list_by_user 读回,数量一致。
"""
import asyncio
import uuid
import pytest
from hy_memory.config import MemoryConfig
from hy_memory.models.memory import MemoryNode, MemoryLayer, MemoryStatus
def run(coro):
loop = asyncio.new_event_loop()
try:
return loop.run_until_complete(coro)
finally:
loop.close()
def _emb(dims=32):
import random
v = [random.gauss(0, 1) for _ in range(dims)]
n = sum(x * x for x in v) ** 0.5 or 1.0
return [x / n for x in v]
def _qdrant_sparse_ok():
try:
import qdrant_client # noqa
from hy_memory.pipelines._retrieval import bm25_fastembed
return bm25_fastembed.is_available()
except Exception:
return False
def _mk(node_id, content, uid="src_u", aid="a1"):
n = MemoryNode(
node_id=node_id, user_id=uid, agent_id=aid, session_id="s1",
content=content, layer=MemoryLayer.L2_FACT,
status=MemoryStatus.ACTIVE, is_latest=True,
)
n.embedding = _emb()
return n
@pytest.mark.skipif(not _qdrant_sparse_ok(),
reason="qdrant-client + fastembed (BM25 sparse) required")
class TestCloneQdrantSparse:
def test_clone_on_bm25_sparse_collection(self, tmp_path):
from hy_memory.data.vector_store_qdrant import QdrantVectorStore
cfg = MemoryConfig.from_dict({"vector_store": {
"provider": "qdrant",
"collection_name": f"t_clone_{uuid.uuid4().hex[:8]}",
"persist_directory": str(tmp_path / "qdrant"),
"embedding_dims": 32,
}})
vs = QdrantVectorStore(cfg)
run(vs.initialize())
try:
# 该集合应启用 BM25 sparse(否则测不到回归)
assert vs._supports_fulltext, "expected BM25 sparse collection"
src_uid = "src_user_clone"
nodes = [_mk(f"n{i}", f"用户喜欢运动项目{i}", uid=src_uid) for i in range(5)]
run(vs.upsert_batch(nodes))
# 1) list_by_user 的 embedding 必须是 List[float],不能是多向量 dict
listed = run(vs.list_by_user(src_uid, agent_id="a1"))
assert len(listed) == 5
for nd in listed:
assert isinstance(nd.embedding, list), \
f"embedding 应为 List[float],实际 {type(nd.embedding)}"
assert nd.embedding and not isinstance(nd.embedding[0], dict)
# 2) 模拟 clone:复制 list_by_user 读到的节点到新 user 再 upsert
# (等价于 async_clone_user 的核心数据流,复现「读→复制→写」)
cloned = []
for nd in listed:
c = MemoryNode(
node_id=str(uuid.uuid4()), user_id="dst_user_clone",
agent_id="a1", session_id="s1", content=nd.content,
layer=nd.layer, status=MemoryStatus.ACTIVE, is_latest=True,
)
c.embedding = nd.embedding # 复制读路径的 embedding
cloned.append(c)
written = run(vs.upsert_batch(cloned))
assert len(written) == 5, f"clone upsert 应写入 5 条,实际 {len(written)}"
# 3) dst 可读回,数量一致
dst = run(vs.list_by_user("dst_user_clone", agent_id="a1"))
assert len(dst) == 5
finally:
run(vs.close())