Záznam rozhodnutí 0002
0002 — Samotné vektorové vyhledávání nestačí: hybridní vyhledávání s filtrem relevance
Stav: nahrazeno rozhodnutím 0006 · Datum: 2026-10
Nahrazeno rozhodnutím 0006. Hybridní vyhledávání, slučování RRF a filtr relevance popsané níže platí dál. Párování slov ne: čtyřznakové kmeny z předpon dávaly falešné shody („Pavlovi“ ~ „pavlač“, „pesto“ šlo vyhledat, zatímco „pes“ ne) a míjely střídání hlásek („psa“ → „pes“). Slova se teď párují klíči lemma + kmen, spočítanými v Pythonu a uloženými v databázi; předpona zůstává jen jako pojistka s poloviční vahou. Záznam níže je ponechaný beze změny jako historie rozhodnutí.
Kontext
Vyhledávání začalo jako čistě vektorové (vícejazyčné větné embeddingy, pgvector, nejlepších k podle kosinové vzdálenosti). Na ukázkách s dlouhými, dobře napsanými dokumenty fungovalo. Selhalo na skutečném případu:
- Krátký záznam (dvě věty) zmiňoval člověka ve vyskloňovaném tvaru — „Novákovi“.
- Otázka byla krátká a použila základní tvar: „Kdo je Novák?“
- Kosinová vzdálenost krátkého záznamu byla horší než u několika dlouhých dokumentů, které se tématu týkaly jen volně. U tohoto embeddingového modelu leží vzdálenosti „relevantních“ i „nesouvisejících“ kusů v úzkém pásmu (zhruba 0,10–0,17), takže práh, který záznam pustí, pustí i šum, a práh, který šum odmítne, odmítne i záznam.
Ten případ jsme nejdřív zapsali jako test. Se samotným vektorovým vyhledáváním selhal.
Možnosti
- Doladit práh. Zamítnuto: pásma se překrývají, žádný práh je neoddělí.
- Větší nebo jiný embeddingový model. Na okraji pomůže, stojí latenci a paměť, a krátký text proti dlouhému dokumentu je strukturální slabina hustého vyhledávání, ne chyba modelu.
- Stemming podle jazyka (Snowball/Hunspell pro češtinu). Lepší úplnost, ale závislost na slovníku pro každý jazyk a riziko, že se Python a databáze rozejdou.
- Hybrid: vektor + fulltext nad kmeny slov, sloučené přes RRF, potom filtr relevance.
Rozhodnutí
Možnost 4.
- Fulltext: generovaný sloupec
tsvectornad textem bez diakritiky s konfiguracísimple(zachová každý tvar slova), dotazovaný předponamikmen:*. Kmen jsou první čtyři znaky slova bez diakritiky — hrubé, bez slovníku a v Pythonu i v SQL totožné, takže se obě poloviny shodnou na tom, co je slovo. V ohebném jazyce to spáruje „liška / lišku / lišky“ bez jakékoli znalosti gramatiky. - Sloučení: Reciprocal Rank Fusion (
1/(60 + pořadí)sečtené přes oba seznamy). Žádná normalizace skóre, žádné váhy k ladění. - Filtr relevance: kus se stane zdrojem jen s dokladem — buď blízkým vektorem (v rámci prahu a zároveň jen o malý kus horším než nejlepší kandidát), nebo vzácnými slovy z otázky (vyskytují se nejvýš ve 2 % kusů, které uživatel smí vidět), jež dohromady pokrývají víc než polovinu otázky. Pravidlo pokrytí umožňuje říct „nevím“: otázka na něco, co korpus neobsahuje („zelený kolibřík“), může s nesouvisejícím kusem sdílet dvě vzácná slova, ale neznámé slovo udrží pokrytí pod polovinou.
Když neprojde nic, odpověď zní „tohle ve znalostní bázi není“ a model se nevolá.
Cena
- Dva dotazy plus dotaz na počty kmenů za každou otázku. Při tisících kusů v pořádku; při milionech by četnosti v dokumentech chtěly mezipaměť.
- Stemmer má známé meze (střídání souhlásek: „lišce“ → jiný kmen). Přijato, protože fulltext je druhé vyhledávání — většinu parafrází dál pokrývá vektorová strana.
- Prahy jsou nastavené pro jeden embeddingový model. Změna modelu znamená nové nastavení proti regresní sadě (tests/test_hybrid.py), a právě proto v ní skutečné selhání žije jako test.