210 lines
7.8 KiB
Python
210 lines
7.8 KiB
Python
from adapters.qdrant import QdrantRetriever, _source_refs
|
|
|
|
|
|
class _FakePoint:
|
|
def __init__(self, payload: dict) -> None:
|
|
self.payload = payload
|
|
self.score = 1.0
|
|
|
|
|
|
class _FakeScrollClient:
|
|
"""Mimics qdrant-client's `.scroll()` shape closely enough to exercise
|
|
`find_by_indication`'s keyword-matching logic directly — a fake filter
|
|
(not a real one), so it returns every payload handed to it regardless
|
|
of `scroll_filter`; the payloads given in each test already represent
|
|
what a real `section_key=chi_dinh, chunk_kind=prose` filter would have
|
|
returned, which is the part `find_by_indication` cannot get wrong on
|
|
its own (the filter construction itself is a one-line, inspectable
|
|
`Filter(must=[...])` — not worth a second fake layer to prove)."""
|
|
|
|
def __init__(self, payloads: list[dict]) -> None:
|
|
self._payloads = payloads
|
|
|
|
def scroll(self, collection_name, scroll_filter, limit, offset, with_payload): # noqa: ARG002
|
|
return [_FakePoint(p) for p in self._payloads], None
|
|
|
|
|
|
def _chi_dinh_payload(drug_id: str, text: str) -> dict:
|
|
return {
|
|
"chunk_id": f"{drug_id}__chi_dinh__0", "drug_id": drug_id,
|
|
"drug_name": drug_id.upper(), "section_key": "chi_dinh",
|
|
"chunk_kind": "prose", "text": text,
|
|
"heading_physical_page": 100, "printed_page_range": [101, 101],
|
|
}
|
|
|
|
|
|
def test_descriptor_source_ref_comes_from_attachment_not_heading_page():
|
|
refs = _source_refs({
|
|
"chunk_kind": "block_descriptor",
|
|
"heading_physical_page": 208,
|
|
"source_page_range": [209, 209],
|
|
"printed_page_range": [210, 210],
|
|
"attachments": [{
|
|
"block_id": "p209_t0",
|
|
"physical_page": 209,
|
|
"printed_page": 210,
|
|
"bbox": [49.5, 68.1, 289.4, 789.4],
|
|
"source_crop": "crops/p209_t0.png",
|
|
}],
|
|
})
|
|
|
|
assert len(refs) == 1
|
|
assert refs[0].physical_page == 209
|
|
assert refs[0].printed_page == 210
|
|
assert refs[0].block_id == "p209_t0"
|
|
assert refs[0].bbox == (49.5, 68.1, 289.4, 789.4)
|
|
assert refs[0].source_crop == "crops/p209_t0.png"
|
|
assert refs[0].precision == "region"
|
|
|
|
|
|
def test_prose_ref_uses_exact_chunk_range_and_keeps_attachment_region():
|
|
refs = _source_refs({
|
|
"chunk_kind": "prose",
|
|
"heading_physical_page": 100,
|
|
"source_page_range": [104, 105],
|
|
"printed_page_range": [105, 106],
|
|
"attachments": [{
|
|
"block_id": "p105_t0",
|
|
"physical_page": 105,
|
|
"printed_page": 106,
|
|
"bbox": [1.0, 2.0, 3.0, 4.0],
|
|
}],
|
|
})
|
|
|
|
assert refs[0].physical_page == 104
|
|
assert refs[0].page_range == (104, 105)
|
|
assert refs[0].printed_page_range == (105, 106)
|
|
assert refs[1].block_id == "p105_t0"
|
|
assert refs[1].physical_page == 105
|
|
assert refs[1].printed_page == 106
|
|
|
|
|
|
def test_find_by_indication_matches_a_drug_that_names_the_symptom():
|
|
client = _FakeScrollClient([
|
|
_chi_dinh_payload("paracetamol_acetaminophen", "Điều trị sốt, giảm đau nhẹ và vừa."),
|
|
_chi_dinh_payload("amoxicilin", "Điều trị nhiễm khuẩn đường hô hấp."),
|
|
])
|
|
retriever = QdrantRetriever(client, "duocthu_v1", embedder=None)
|
|
|
|
hits = retriever.find_by_indication("sốt", limit=8)
|
|
|
|
assert [h.document.drug_id for h in hits] == ["paracetamol_acetaminophen"]
|
|
|
|
|
|
def test_find_by_indication_requires_the_whole_phrase_not_a_scattered_match():
|
|
""""sốt xuất huyết" (dengue) must not match a chunk that only says "sốt"
|
|
— the phrase itself has to appear, not just each of its words somewhere."""
|
|
client = _FakeScrollClient([
|
|
_chi_dinh_payload("paracetamol_acetaminophen", "Điều trị sốt, giảm đau."),
|
|
])
|
|
retriever = QdrantRetriever(client, "duocthu_v1", embedder=None)
|
|
|
|
hits = retriever.find_by_indication("sốt xuất huyết", limit=8)
|
|
|
|
assert hits == []
|
|
|
|
|
|
def test_find_by_indication_matches_a_multi_word_phrase_contiguously():
|
|
client = _FakeScrollClient([
|
|
_chi_dinh_payload("paracetamol_acetaminophen", "Điều trị sốt cao ở người lớn."),
|
|
])
|
|
retriever = QdrantRetriever(client, "duocthu_v1", embedder=None)
|
|
|
|
hits = retriever.find_by_indication("sốt cao", limit=8)
|
|
|
|
assert [h.document.drug_id for h in hits] == ["paracetamol_acetaminophen"]
|
|
|
|
|
|
def test_find_by_indication_rejects_a_scattered_bag_of_common_words():
|
|
"""Found live 2026-08-07: a token-SUBSET match (every word present
|
|
*somewhere*, any order) let a long nonsense phrase built from common
|
|
filler words false-positive against real chi_dinh text — the words are
|
|
common enough to appear scattered through nearly anything. Phrase
|
|
matching closes it: none of these words are contiguous in the target
|
|
text the way they are in the query."""
|
|
client = _FakeScrollClient([
|
|
_chi_dinh_payload(
|
|
"paracetamol_acetaminophen",
|
|
"Điều trị sốt. Không dùng quá liều khuyến cáo trong sách hướng dẫn.",
|
|
),
|
|
])
|
|
retriever = QdrantRetriever(client, "duocthu_v1", embedder=None)
|
|
|
|
hits = retriever.find_by_indication(
|
|
"bệnh chưa từng ghi nhận trong sách abcxyz123", limit=8
|
|
)
|
|
|
|
assert hits == []
|
|
|
|
|
|
def test_find_by_indication_returns_at_most_one_hit_per_drug():
|
|
client = _FakeScrollClient([
|
|
_chi_dinh_payload("paracetamol_acetaminophen", "Điều trị sốt."),
|
|
{**_chi_dinh_payload("paracetamol_acetaminophen", "Điều trị sốt cao ở trẻ em."),
|
|
"chunk_id": "paracetamol_acetaminophen__chi_dinh__1"},
|
|
])
|
|
retriever = QdrantRetriever(client, "duocthu_v1", embedder=None)
|
|
|
|
hits = retriever.find_by_indication("sốt", limit=8)
|
|
|
|
assert len(hits) == 1
|
|
|
|
|
|
def test_find_by_indication_respects_the_limit():
|
|
client = _FakeScrollClient([
|
|
_chi_dinh_payload(f"drug_{i}", "Điều trị đau.") for i in range(5)
|
|
])
|
|
retriever = QdrantRetriever(client, "duocthu_v1", embedder=None)
|
|
|
|
hits = retriever.find_by_indication("đau", limit=2)
|
|
|
|
assert len(hits) == 2
|
|
|
|
|
|
def _section_payload(drug_id: str, section_key: str, text: str) -> dict:
|
|
return {
|
|
"chunk_id": f"{drug_id}__{section_key}__0", "drug_id": drug_id,
|
|
"drug_name": drug_id.upper(), "section_key": section_key,
|
|
"chunk_kind": "prose", "text": text,
|
|
"heading_physical_page": 100, "printed_page_range": [101, 101],
|
|
}
|
|
|
|
|
|
def test_search_lexical_ranks_by_distinct_matched_term_count():
|
|
client = _FakeScrollClient([
|
|
_section_payload("aspirin", "than_trong", "Thận trọng với suy thận."),
|
|
_section_payload(
|
|
"aspirin", "chong_chi_dinh",
|
|
"Không dùng cho người có loét dạ dày tá tràng đang hoạt động.",
|
|
),
|
|
])
|
|
retriever = QdrantRetriever(client, "duocthu_v1", embedder=None)
|
|
|
|
hits = retriever.search_lexical(
|
|
"Thận trọng khi dùng aspirin cho bệnh nhân loét dạ dày", "aspirin", limit=5
|
|
)
|
|
|
|
assert [h.document.section_key for h in hits] == ["chong_chi_dinh", "than_trong"]
|
|
assert hits[0].score > hits[1].score
|
|
|
|
|
|
def test_search_lexical_drops_stopword_only_queries():
|
|
client = _FakeScrollClient([
|
|
_section_payload("aspirin", "than_trong", "Thận trọng với suy thận."),
|
|
])
|
|
retriever = QdrantRetriever(client, "duocthu_v1", embedder=None)
|
|
|
|
assert retriever.search_lexical("là gì và của", "aspirin", limit=5) == []
|
|
|
|
|
|
def test_search_lexical_excludes_non_matching_sections():
|
|
client = _FakeScrollClient([
|
|
_section_payload("aspirin", "than_trong", "Thận trọng với suy thận."),
|
|
_section_payload("aspirin", "chi_dinh", "Giảm đau hạ sốt."),
|
|
])
|
|
retriever = QdrantRetriever(client, "duocthu_v1", embedder=None)
|
|
|
|
hits = retriever.search_lexical("loét dạ dày", "aspirin", limit=5)
|
|
|
|
assert hits == []
|