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

Záznam rozhodnutí 0006

0006 — Klíče lemma + kmen nahrazují kmeny z předpon

Stav: přijato · Datum: 2026-10 · Nahrazuje: 0002 (část o párování slov; hybridní vyhledávání, RRF a filtr relevance zůstávají)

Kontext

Rozhodnutí 0002 přidalo vedle vektorového vyhledávání fulltextové a slova párovalo podle prvních čtyř znaků bez diakritiky. Skutečný stemming zamítlo kvůli „závislosti na slovníku pro každý jazyk a riziku, že se Python a databáze rozejdou“.

V provozu se ukázalo, že předpona selhává oběma směry — a jsou to přesně selhání, která recenzent zkusí jako první. Příkladovým jazykem je čeština:

  • Falešné shody. Nesouvisející slova mají stejnou předponu: „Pavlovi“ (jméno ve 3. pádě) i „pavlač“ se stanou pavl, takže otázka na člověka vytáhla poznámky o opravě pavlače; „mechu“ i „mechanika“ se stanou mech. Krátká slova předponu neměla vůbec, takže „pes“ se vyhledat nedal, zatímco „pesto“ ano.
  • Chybějící shody. Skloňování nejsou jen koncovky. „pes / psa / psovi“ nemají společnou čtyřznakovou předponu; „liška / lišce“ se liší střídáním souhlásek; „den / dne“ ztrácí samohlásku.
  • Obava z rozchodu se týkala počítání klíčů na dvou místech (Python pro filtr relevance, slovník v databázi pro index). Zmizí, když se klíče počítají jednou, v Pythonu, a databáze je jen ukládá.

Možnosti

  1. Ponechat předponu a doladit její délku. Delší předpona ztratí krátká slova, kratší přidá kolize. Střídání hlásek neřeší vůbec.
  2. Slovníky fulltextu v Postgresu (konfigurace ispell / snowball). Klíče by se počítaly v SQL a filtr relevance v Pythonu by potřeboval stejný slovník — přesně rozchod, kterého se bálo 0002.
  3. Lemma + kmen spočítané v Pythonu a uložené jako klíče tsvector. Jedna funkce vyrábí klíče pro uložené kusy (při nahrání) i pro otázku (při dotazu).
  4. Morfologické analyzátory s neuronovými nebo velkými statistickými modely (např. plné taggery trénované na anotovaných korpusech). Nejlepší kvalita, ale modely pro češtinu mají licenci jen pro nekomerční použití, což je pro produkt vylučuje.

Rozhodnutí

Možnost 3.

  • Každé obsahové slovo dostane l0<lemma> (simplemma, podle slovníku: psa → pes, lišce → liška, dne → den) a s0<kmen> (Snowball, podle pravidel: zvládne jména a slova, která slovník nezná, Pavlovi → pavl). Klíče jsou malými písmeny, bez diakritiky, alfanumerické; značka drží klíč jako jeden token. Při nahrání se zapíšou do chunks.lex (tsvector simple + index GIN) a z otázky se vyrobí stejné klíče. Python a SQL se nemohou rozejít, protože implementace je jen jedna (app/textmatch.py).
  • Slovo se shoduje silně, když sdílí klíč lemmatu nebo kmene. Stará předpona bez diakritiky (chunks.tsv, pref:*) zůstává jen jako slabá pojistka: poloviční váha ve slučování RRF a poloviční shoda ve filtru relevance, který vyžaduje silné shody vzácných slov otázky. „Pavlovi“ ~ „pavlač“ je teď nanejvýš slabá shoda a sama o sobě nikdy není doklad.
  • Relevance váží slova podle idf (vzácnosti v korpusu tazatele); běžná slova se do pokrytí nepočítají; neznámé slovo váží nejvíc, ale konečně.
  • Oprava překlepů. Slovo, které korpus tazatele vůbec neobsahuje, by mělo nejvyšší váhu a potopilo jinak dobrou shodu — stejně jako překlep nebo chybějící diakritika. Před řazením se takové slovo opraví na nejbližší slovo vlastního slovníku tazatele (kmen v editační vzdálenosti 1 u kmenů aspoň o 5 znacích, nebo dlouhý společný začátek s tvarem bez diakritiky). Krátké kmeny se neopravují nikdy („lesem“ se nesmí stát „lesnatý“). Slovník se skládá jen z kusů, které tazatel smí vidět (rozhodnutí 0001), a v mezipaměti zůstává jen dokud se tahle množina kusů nezmění.
  • Pojistka indexu. Statistiky lemmat a kmenů se používají jen tehdy, když klíče má každý kus; napůl vyplněný index (přerušená migrace, kusy zapsané starší verzí) by udělal vzácným každé slovo. Do té doby hledání zůstává na cestě s předponou. Migrace 0004 doplní existující kusy; python -m app.lex_backfill --apply dokončí přerušené doplňování.
  • Negativní testy jsou plnohodnotné. Regresní sada má seznam „musí najít“ (skloňování, střídání hlásek, jména) a seznam „nesmí najít“ (Pavlovi/pavlač, pes/pesto, mech/mechanika, ruka/rukavice) a k tomu end-to-end nad Postgresem „nesouvisející slova dají nula výsledků“ (tests/test_textmatch.py, tests/integration/test_search_pg.py).

Cena

  • Závislosti a licence. simplemma (kód MIT; data lemmat pod ODbL, CC BY-SA a CC BY) a snowballstemmer (BSD-3-Clause). Uvedení autorů je v NOTICE. Modely s nekomerční licencí jsou zamítnuté záměrně.
  • Nastavené jazyky. Klíče se generují pro každý jazyk v SEARCH_LANGUAGES (výchozí en,cs). Víc jazyků znamená víc klíčů a malou šanci kolize napříč jazyky (české lemma anglického „ran“ je české podstatné jméno); nasazení má uvést jen jazyky svého korpusu. Změna seznamu znamená přepočítat klíče (lex_backfill --all).
  • Stemmery přeřezávají. Anglický Snowball sloučí „policy“ a „police“ do jednoho kmene. Přijato: silná shoda pořád musí být vzácné slovo pokrývající většinu otázky a vedle ní řadí i vektorová strana.
  • Práce při zápisu. Každý kus se při nahrání lemmatizuje, existující kusy jednou v migraci. Levné ve srovnání s výpočtem embeddingu.
  • Jeden sloupec navíc (chunks.lex) vedle chunks.tsv, který zůstává pro pojistku a slovník překlepů.