Notebook / Esej
Proč nám samotné vektorové vyhledávání nestačilo
Říjen 2026
Většina systémů typu RAG začíná stejně: rozdělit dokumenty na kusy, spočítat jejich embeddingy, spočítat embedding otázky, vzít pár nejbližších kusů a předat je modelu. Náš začínal taky tak. Na ukázkových datech — dlouhé, pečlivě napsané dokumenty a dobře položené otázky — vypadal skvěle. Selhal na prvním případu, který připomínal skutečný život.
Případ
Zjednodušeně a se změněným jménem: uživatel nahrál krátkou poznámku — dvě věty, v jedné z nich, že poslal fakturu dodavateli Novákovi. Jméno je v poznámce ve třetím pádě. Později položil samozřejmou otázku: „Kdo je Novák?“
Poznámka se ve výsledcích neobjevila.
Ne proto, že by embeddingový model byl špatný. Je to dobrý vícejazyčný model. Problém byl v samotné stavbě:
- Krátké prohrává s dlouhým. Kosinová vzdálenost krátké otázky od dvouvětné poznámky byla horší než od několika dlouhých dokumentů, které se jen volně týkaly dodavatelů a faktur. Dlouhý dokument pokrývá víc témat, a tak je trochu blízko všemu.
- Pásmo je úzké. U tohoto modelu ležely vzdálenosti relevantních i nesouvisejících kusů v úzkém pásmu — zhruba 0,10 až 0,17. Práh, který poznámku pustí, pustí i šum; práh, který šum odmítne, odmítne i poznámku.
- Tvar slova se liší. Novák a Novákovi je tentýž člověk, ale jiné tokeny. Index přesných slov by nepomohl ani tak.
Nejdřív to zapsat jako test
Než jsme cokoli měnili, stal se z případu test: z daných kandidátních kusů s jejich skutečnými vzdálenostmi se musí vrátit poznámka a dlouhý dokument ne. Se samotným vektorovým vyhledáváním test selhal — práh nepřekonalo nic. Ten test je v repozitáři dodnes a každá budoucí změna vyhledávání ho musí udržet zelený.
Co jsme zkusili a proč jsme toho nechali
Doladit práh. Takový práh neexistuje. Pásma se překrývají.
Větší embeddingový model. Čísla se trochu posunou, latence a paměť vzrostou. Krátký text proti dlouhým dokumentům je vlastnost hustého vyhledávání, ne chyba jednoho modelu.
Co jsme postavili — dvakrát
Druhé vyhledávání: fulltext. Vektorové vyhledávání zůstává; vedle něj běží fulltextové
a oba seznamy kandidátů slučujeme metodou Reciprocal Rank Fusion: každý kus získá
1/(60 + pořadí) za každý seznam, ve kterém se objeví. Žádná normalizace skóre, žádné váhy
k ladění, a kus, který se líbí oběma vyhledáváním, vystoupá nahoru.
První verze: hrubé předpony. Pořádný stemmer jsme nejdřív zamítli, protože se musí chovat
úplně stejně v Pythonu (kde filtrujeme) i v databázi (kde hledáme). Ze slova se tedy staly jeho
první čtyři písmena bez diakritiky: liška, lišku, lišky → lisk, Novák, Novákovi → nova.
Záměrně hrubé, a původní případ to vyřešilo.
Pak bylo potřeba opravit i opravu. Předpony selhávají oběma směry. Minou slova, jejichž kmen
se mění: čeština střídá souhlásky a vypouští samohlásky, takže liška / lišce nemají společná
čtyři písmena, a krátká slova jako pes / psa nebo den / dne byla na index příliš krátká úplně —
pesto se najít dalo, pes ne. A nesouvisející slova čtyři písmena sdílejí: Pavlovi i pavlač
se stanou pavl, mechu i mechanika se stanou mech. Oba směry jsme zapsali jako testy
a předpony jimi neprošly.
Druhá verze: lemma a kmen, spočítané jednou. Každé slovo je teď uložené jako dva klíče: jeho slovníkový základní tvar (lemma, ví, že psa je pes) a jeho kmen podle Snowballu (zvládne jména a slova, která žádný slovník nemá). Obava, která stemmer napoprvé shodila — že se Python a databáze neshodnou —, zmizela ve chvíli, kdy se klíče počítají v Pythonu a do indexu se ukládají: databáze už na slova nemá vlastní názor, jen porovnává klíče. Stará předpona zůstává jako slabá pojistka s poloviční vahou.
Párování víc tvarů opačné riziko nezruší: sloučit slova, která si jsou jen podobná, teď když se počítají i krátká slova. Proto ke každému testu „musí najít“ patří i test „nesmí najít“ — pes nesmí najít pesto, Pavlovi nesmí najít pavlač, ruka nesmí najít rukavice.
Překlepy jen ve vlastním slovníku. Slovo v otázce s překlepem nebo bez diakritiky se opraví na nejbližší slovo, které se v dokumentech opravdu vyskytuje — ale jen v dokumentech, které tazatel smí vidět, takže oprava nemůže prozradit slova z cizích dokumentů.
Pak vyžadovat doklad. Sloučení zlepší úplnost, ale neřekne, jestli něco na otázku opravdu odpovídá. Kus se proto stane zdrojem jen tehdy, když má jeden ze dvou druhů dokladu:
- blízký vektor — v rámci prahu a zároveň jen o malý kus horší než nejlepší kandidát; nebo
- vzácná slova z otázky shodná podle lemmatu nebo kmene, která dohromady pokrývají víc než polovinu otázky, vážené podle vzácnosti — běžná slova se nepočítají a slovo, které korpus nikdy neviděl, váží nejvíc.
Právě poslední pravidlo umožňuje systému říct „nevím“. Zeptejte se „Co pije zelený kolibřík v Ostravě?“ a nesouvisející kus o zelené zelenině u Ostravy sdílí dvě vzácná slova. Jenže kolibřík se v korpusu nevyskytuje vůbec, váží nejvíc, pokrytí zůstane pod polovinou a odpověď je poctivá: tohle ve znalostní bázi není. Model se nevolá.
Jeden důležitý detail: „vzácnost“ se počítá jen z dokumentů, které uživatel smí vidět. Jinak by se řazení chovalo jinak kvůli dokumentům, ke kterým nemá přístup — a to je postranní kanál. (Víc v článku Oprávnění před modelem.)
Co to stálo
- Víc dotazů na otázku: vektorový, fulltextový a počet výskytů každého slova z otázky. Při tisících kusů levné; při milionech by počty slov chtěly mezipaměť.
- Slovník základních tvarů a stemmer jako závislosti, s ověřenými licencemi, a jednorázové dopočítání klíčů pro už nahrané dokumenty.
- Prahy jsou nastavené pro jeden embeddingový model. Změna modelu znamená znovu je nastavit proti regresním testům — a právě proto v nich skutečná selhání zůstávají, v obou směrech.
- Častější viditelné „nenalezeno“. O to přesně jde, a zároveň je to poctivá zpráva o tom, co znalostní bázi chybí.
Ponaučení, které si nesu: ukázková data z dlouhých, čistých dokumentů vám řeknou, že vektorové vyhledávání funguje. První krátký, neuspořádaný a vyskloňovaný záznam vám řekne, jestli opravdu.