ENCSPožádat o přístup ke kódu

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

  1. Doladit práh. Zamítnuto: pásma se překrývají, žádný práh je neoddělí.
  2. 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.
  3. 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.
  4. Hybrid: vektor + fulltext nad kmeny slov, sloučené přes RRF, potom filtr relevance.

Rozhodnutí

Možnost 4.

  • Fulltext: generovaný sloupec tsvector nad textem bez diakritiky s konfigurací simple (zachová každý tvar slova), dotazovaný předponami kmen:*. 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.