Coverage for src/lilbee/retrieval/embedding_profiles.py: 100%
19 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-08-14 11:46 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-08-14 11:46 +0000
1"""Per-family embedding instruction profiles for asymmetric query/document embedding."""
3from __future__ import annotations
5from dataclasses import dataclass
8@dataclass(frozen=True)
9class EmbeddingProfile:
10 """Instruction prefixes an embedder applies to a query vs a document."""
12 query_instruction: str = ""
13 doc_prefix: str = ""
14 # META_SCHEMA_VERSION that first stamped this doc_prefix at ingest; an
15 # older store warns to rebuild rather than silently mixing prefixed
16 # queries with unprefixed documents.
17 doc_prefix_since: int = 1
20# Instruction-tuned embedders (Qwen3-Embedding, *-instruct: e5/gte/...) share the
21# Instruct/Query format with no document prefix. Base e5 uses query:/passage:,
22# nomic-embed requires search_query:/search_document: (trained with them), bge
23# v1/v1.5 wants a query-side instruction only. Everything else (bge-m3,
24# gte-large, ...) stays symmetric: correct-but-symmetric, never silently wrong.
25# Order is specific-first.
26_INSTRUCT = EmbeddingProfile(
27 query_instruction=(
28 "Instruct: Given a web search query, retrieve relevant passages that "
29 "answer the query\nQuery: "
30 ),
31)
32_E5 = EmbeddingProfile(query_instruction="query: ", doc_prefix="passage: ")
33_NOMIC = EmbeddingProfile(
34 query_instruction="search_query: ", doc_prefix="search_document: ", doc_prefix_since=2
35)
36_BGE_V1 = EmbeddingProfile(
37 query_instruction="Represent this sentence for searching relevant passages: "
38)
39_SYMMETRIC = EmbeddingProfile()
40_FAMILY_PROFILES: tuple[tuple[str, EmbeddingProfile], ...] = (
41 ("qwen3-embedding", _INSTRUCT),
42 # "instructor" contains "instruct" but is a different dialect: the Instructor
43 # family (hkunlp/instructor-*, a t5encoder the engine can load) prefixes a
44 # "Represent the ... for retrieval:" instruction on the document as well as
45 # the query. Handing it the Instruct/Query query prefix with no doc prefix
46 # would be the asymmetric-in-the-wrong-dialect case this module promises not
47 # to produce, so it takes the symmetric fallback until the real prefixes are
48 # wired.
49 ("instructor", _SYMMETRIC),
50 ("instruct", _INSTRUCT), # any instruction-tuned embedder: Instruct/Query, no doc prefix
51 ("multilingual-e5", _E5),
52 ("e5-", _E5),
53 ("nomic-embed", _NOMIC),
54 ("bge-m3", _SYMMETRIC), # genuinely symmetric; must not fall through to bge v1
55 ("bge-", _BGE_V1),
56)
59def resolve_embedding_profile(model_ref: str) -> EmbeddingProfile:
60 """Profile for *model_ref*, or a symmetric (empty) profile when unrecognized."""
61 ref = model_ref.lower()
62 for needle, profile in _FAMILY_PROFILES:
63 if needle in ref:
64 return profile
65 return EmbeddingProfile()