Víceúrovňová RAG architektura pro vyhledávání v dokumentaci 1C na MCP serverech
MCP servery documents1c a metadata1c implementují víceúrovňový RAG systém pro přesné vyhledávání v dokumentaci 1C a konfiguracích. Každá úroveň hodnocení řeší konkrétní problémy: od kontextu chunků až po kombinaci vektorového a fulltextového vyhledávání. Systém využívá PostgreSQL s pgvector, Redis pro ukládání do mezipaměti a vlastní tokenizaci pro termíny 1C.
Proč se vzdát vektorových databází s hierarchií
Standardní vektorová úložiště jako Qdrant nebo Weaviate si neporadí s dynamikou dokumentace 1C. Dokumenty nejsou samostatné bez kontextu: článek "Nastavení" vyžaduje uvedení oddílu, objekt Složka.Zaměstnanci — popis atributů a modulů.
Dokumentace se často aktualizuje — nové verze BSP, metody platformy. Přestavba stromu ve vektorové databázi je nákladná. Řešení: PostgreSQL s pgvector + heading_path vložený do každého chunku. Embeddingy se generují pro heading_path + text, ukládá se pouze text chunku.
Úspora: sloupec embedding halfvec(1024) v tabulce article_chunks místo samostatné vektorové databáze.
Úroveň 1: Chunky s kontextem nadpisů
Rozdělení markdownu na chunky bere v úvahu strukturu: YAML se odstraní, dělení podle nadpisů, bloky kódu se nepřerušují, překrytí 220 znaků.
def split_markdown_into_chunks(
text: str,
max_len: int = 800,
overlap: int = 220,
include_heading_path: str = "full_path",
) -> list[str]:
body = _strip_frontmatter(text)
sections = _split_by_headers(body)
chunks: list[str] = []
for section, heading_path, current_heading in sections:
context_heading = " > ".join(heading_path)
section_with_context = f"{context_heading}\n\n{section}"
if len(section_with_context) <= max_len:
chunks.append(section_with_context)
else:
sub_chunks = _split_section_by_paragraphs(section_with_context, max_len)
chunks.extend(sub_chunks)
return _add_overlap(chunks, overlap)
Příklad chunku:
Mzda a řízení personálu > Výpočet mezd > Nastavení daně z příjmu
Pro správný výpočet daně z příjmu je nutné uvést sazbu daně...
V databázi meta ukládá heading_path zvlášť pro filtry a odkazy. Embedding: heading_path + chunk_text.
Úroveň 2: Mezipaměť embeddingů v Redis
Generování embeddingů (text-embedding-qwen3-embedding-4b) trvá 50–200 ms. Pro opakující se dotazy — Redis s TTL, klíč SHA256 od model:dimensions:text.
async def embed_text(text: str) -> list[float]:
model = _get_embed_model_name()
dims = _get_embed_dimensions()
if _cache_enabled():
cached = await cache_get(text, model, dims)
if cached is not None:
return cached
response = await to_thread(client.embeddings.create, model=model, input=text)
vector = response.data[0].embedding
if _cache_enabled():
await cache_set(text, model, dims, vector, _cache_ttl())
return vector
Mezipaměť se automaticky zneplatní při změně modelu.
Úroveň 3: Vektorové vyhledávání s halfvec
Kosinová vzdálenost přes <=> v pgvector, typ halfvec(1024) šetří paměť.
SELECT
ac.id, ac.article_id, ac.chunk_text, ac.meta,
ac.embedding::halfvec(1024) <=> CAST(:query_embedding AS halfvec(1024)) AS distance
FROM article_chunks ac
INNER JOIN articles a ON ac.article_id = a.id
WHERE a.job_id IN (SELECT id FROM jobs WHERE load_mode IN ('hbk', 'docs'))
AND a.destination = :destination
ORDER BY distance
LIMIT :limit;
Score = 1 - distance.
Úroveň 4: Sloučení hodnocení RRF
Vektorové vyhledávání je slabé pro přesné termíny jako &NaKlientoviNaServeru. RRF slučuje vektorový a FTS seznamy:
@staticmethod
def _rrf_merge(vector_results, fts_results, top_k: int, k: int = 60) -> list[dict]:
scores: dict[int, float] = {}
items: dict[int, dict] = {}
for rank, item in enumerate(vector_results, start=1):
chunk_id = item["id"]
scores[chunk_id] = scores.get(chunk_id, 0.0) + 1.0 / (k + rank)
items[chunk_id] = item
for rank, item in enumerate(fts_results, start=1):
chunk_id = item["id"]
scores[chunk_id] = scores.get(chunk_id, 0.0) + 1.0 / (k + rank)
if chunk_id not in items:
items[chunk_id] = item
sorted_ids = sorted(scores, key=lambda cid: scores[cid], reverse=True)
# Normalizace a vrácení top_k
HBK-boost: pro dotazy bez filtru se přidáván chunky vestavěného jazyka 1C (load_mode='hbk') na začátek seznamu.
- Top HBK-chunky podle query_emb
- Vyloučení duplikátů
- Umístění před hlavními výsledky
Úroveň 5: BM25 přehodnocení s 1C tokenizací
Pro 50–100 kandidátů z RRF se aplikuje BM25 s váhami (vector 0.6, bm25 0.4). Efektivní pro:
- Specifické termíny:
ObecnýModul,RegistrInformací,&NaServeru - Názvy objektů:
Složka.Nomenklatura - Krátké technické dotazy
Vlastní tokenizace:
GUID_RE = re.compile(r"\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b", re.I)
VERSION_RE = re.compile(r"\bv?8\.\d+(?:\.\d+){1,3}\b", re.I)
DIRECTIVE_RE = re.compile(r"[&#][And-IA-Z][And-YaYoA-Za-z0-9_]*", re.I)
def _tokenize(self, text: str, expand_query: bool = False) -> list[str]:
text = _norm_text(text)
out = []
for g in GUID_RE.findall(text): out.append(g.lower())
for v in VERSION_RE.findall(text): out.append(v.lower())
for d in DIRECTIVE_RE.findall(text): out.append(d.lower())
for t in TOKEN_RE.findall(text):
if t in RU_STOP: continue
for p in _split_identifier(t):
out.append(p.lower())
return out
Rozděluje CamelCase, snake_case, vyčleňuje direktivy a GUID.
Co je důležité
- heading_path v chuncích zajišťuje kontext bez stromu dokumentů
- Redis-mezipaměť snižuje latenci embeddingů ze 100+ ms na 0 ms
- RRF kombinuje sémantiku a přesné vyhledávání bez normalizace
- BM25 s 1C tokenizací zvyšuje relevantní technické termíny
- halfvec(1024) šetří paměť v pgvector při přesnosti cosine
— Editorial Team
Zatím žádné komentáře.