Files
Baradb/src/barabadb/fts/multilang.nim
T
dimgigov 805dbc8ad6 fix: comprehensive bug audit — 15 critical/high/medium fixes
Critical fixes:
- B-Tree splitChild: fixed out-of-bounds on child.children.len (mid+1..len → mid+1..<len)
- Wire protocol: all deserializeValue cases now properly assign to result
- Wire protocol: fkBool/fkInt8 now increment pos after reading
- MVCC: write-write conflict now detected for completed transactions
- MVCC: commit only marks visible versions as deleted (not concurrent uncommitted)
- Lexer: buffer overrun on backslash escape at end of input — added bounds check
- Bulgarian/Russian stemmers: fixed UTF-8 byte offsets for Cyrillic suffixes (2 bytes per char)

High fixes:
- WebSocket: added missing std/tables import
- Raft: added randomize() call for election timeouts
- MemTable.get: binary search O(log n) instead of linear scan O(n)

Medium fixes:
- Zerocopy: string length now written as 4-byte int32 (was single byte)
- Raft appendEntries: guard against nextIdx underflow
- Columnar: null tracking with nulls seq on ColumnPtr
- Removed dead code in lexer readIdent (redundant true/false checks)
- Updated Bulgarian stemming test to match corrected byte offsets

All 214 tests pass across 48 suites with 0 failures.
2026-05-06 02:46:37 +03:00

214 lines
7.9 KiB
Nim
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
## Multi-Language FTS — tokenizers for different languages
import std/tables
import std/unicode
import std/strutils
import std/sets
type
Language* = enum
langEnglish = "en"
langSpanish = "es"
langFrench = "fr"
langGerman = "de"
langRussian = "ru"
langBulgarian = "bg"
langChinese = "zh"
langJapanese = "ja"
langArabic = "ar"
langAuto = "auto"
Stemmer* = proc(word: string): string {.gcsafe.}
StopWords* = HashSet[string]
LanguageConfig* = object
language*: Language
stemmer*: Stemmer
stopWords*: StopWords
tokenizer*: proc(text: string): seq[string] {.gcsafe.}
const
stopWordsEn* = [
"a", "an", "the", "is", "it", "in", "on", "at", "to", "for",
"of", "with", "by", "from", "as", "into", "through", "during",
"and", "or", "not", "but", "if", "then", "else", "when",
"be", "was", "were", "been", "being", "have", "has", "had",
"do", "does", "did", "will", "would", "could", "should", "may",
"this", "that", "these", "those", "i", "you", "he", "she", "we", "they",
]
stopWordsBg* = [
"и", "в", "на", "за", "от", "да", "се", "е", "са", "по",
"не", "че", "с", "към", "но", "или", "ако", "при", "до",
"как", "какво", "кой", "коя", "кое", "кои", "този", "тази",
"това", "тези", "със", "между", "след", "преди", "без",
"още", "вече", "само", "може", "трябва", "има", "няма",
]
stopWordsDe* = [
"der", "die", "das", "ein", "eine", "und", "oder", "aber", "nicht",
"ist", "sind", "war", "hat", "haben", "werden", "wird", "kann",
"mit", "von", "für", "auf", "in", "an", "zu", "bei", "nach",
"über", "unter", "vor", "zwischen", "durch", "gegen", "ohne",
]
stopWordsFr* = [
"le", "la", "les", "un", "une", "des", "et", "ou", "mais", "pas",
"est", "sont", "était", "a", "ont", "sera", "peut", "avec",
"de", "du", "pour", "sur", "dans", "par", "entre", "sous", "chez",
"je", "tu", "il", "elle", "nous", "vous", "ils", "elles",
]
stopWordsRu* = [
"и", "в", "на", "не", "что", "он", "я", "с", "а", "как",
"это", "по", "но", "они", "мы", "за", "от", "из", "у", "к",
"бы", "ты", "его", "её", "их", "её", "мой", "твой", "наш",
"ваш", "свой", "который", "этот", "тот", "такой", "каждый",
]
proc stemEnglish*(word: string): string =
if word.len <= 3: return word
if word.endsWith("ing"): return word[0..^4]
if word.endsWith("tion"): return word[0..^5]
if word.endsWith("ness"): return word[0..^5]
if word.endsWith("ment"): return word[0..^5]
if word.endsWith("able"): return word[0..^5]
if word.endsWith("ies"): return word[0..^4] & "y"
if word.endsWith("es") and word.len > 4: return word[0..^3]
if word.endsWith("ed") and word.len > 4: return word[0..^3]
if word.endsWith("ly") and word.len > 4: return word[0..^3]
if word.endsWith("s") and not word.endsWith("ss") and word.len > 3: return word[0..^2]
return word
proc stemBulgarian*(word: string): string =
if word.len <= 3: return word
# Bulgarian suffixes (all Cyrillic chars are 2 bytes)
# 3 Cyrillic chars = 6 bytes -> ^7
if word.endsWith("ища"): return word[0..^7]
if word.endsWith("ище"): return word[0..^7]
if word.endsWith("ция"): return word[0..^7]
if word.endsWith("ние"): return word[0..^7]
if word.endsWith("ост"): return word[0..^7]
if word.endsWith("ски"): return word[0..^7]
# 4 Cyrillic chars = 8 bytes -> ^9
if word.endsWith("ство"): return word[0..^9]
# 2 Cyrillic chars = 4 bytes -> ^5
if word.endsWith("на"): return word[0..^5]
if word.endsWith("та"): return word[0..^5]
return word
proc stemGerman*(word: string): string =
if word.len <= 3: return word
if word.endsWith("ung"): return word[0..^4]
if word.endsWith("heit"): return word[0..^5]
if word.endsWith("keit"): return word[0..^5]
if word.endsWith("lich"): return word[0..^5]
if word.endsWith("isch"): return word[0..^5]
if word.endsWith("chen"): return word[0..^5]
if word.endsWith("schaft"): return word[0..^7]
if word.endsWith("en"): return word[0..^3]
if word.endsWith("er"): return word[0..^3]
if word.endsWith("es"): return word[0..^3]
return word
proc stemFrench*(word: string): string =
if word.len <= 3: return word
if word.endsWith("ement"): return word[0..^6]
if word.endsWith("ment"): return word[0..^5]
if word.endsWith("tion"): return word[0..^5]
if word.endsWith("eur"): return word[0..^4]
if word.endsWith("euse"): return word[0..^5]
if word.endsWith("ique"): return word[0..^5]
if word.endsWith("esse"): return word[0..^5]
if word.endsWith("eux"): return word[0..^4]
if word.endsWith("er"): return word[0..^3]
if word.endsWith("es"): return word[0..^3]
return word
proc stemRussian*(word: string): string =
if word.len <= 3: return word
# Russian suffixes (all Cyrillic chars are 2 bytes)
# 4 Cyrillic chars = 8 bytes -> ^9
if word.endsWith("ость"): return word[0..^9]
if word.endsWith("ение"): return word[0..^9]
if word.endsWith("ание"): return word[0..^9]
if word.endsWith("тель"): return word[0..^9]
if word.endsWith("ский"): return word[0..^9]
# 3 Cyrillic chars = 6 bytes -> ^7
if word.endsWith("ция"): return word[0..^7]
if word.endsWith("ние"): return word[0..^7]
if word.endsWith("ать"): return word[0..^7]
if word.endsWith("ить"): return word[0..^7]
if word.endsWith("ыть"): return word[0..^7]
return word
proc getLanguageConfig*(lang: Language): LanguageConfig =
case lang
of langEnglish:
LanguageConfig(language: lang, stemmer: stemEnglish,
stopWords: stopWordsEn.toHashSet())
of langBulgarian:
LanguageConfig(language: lang, stemmer: stemBulgarian,
stopWords: stopWordsBg.toHashSet())
of langGerman:
LanguageConfig(language: lang, stemmer: stemGerman,
stopWords: stopWordsDe.toHashSet())
of langFrench:
LanguageConfig(language: lang, stemmer: stemFrench,
stopWords: stopWordsFr.toHashSet())
of langRussian:
LanguageConfig(language: lang, stemmer: stemRussian,
stopWords: stopWordsRu.toHashSet())
else:
LanguageConfig(language: lang, stemmer: stemEnglish,
stopWords: stopWordsEn.toHashSet())
proc tokenize*(text: string, config: LanguageConfig): seq[string] =
result = @[]
var word = ""
for ch in text:
let cp = ord(ch)
# Accept ASCII alphanumeric, Cyrillic, CJK, Arabic, and common separators
if (cp >= 0x30 and cp <= 0x39) or # digits
(cp >= 0x41 and cp <= 0x5A) or # A-Z
(cp >= 0x61 and cp <= 0x7A) or # a-z
(cp >= 0xC0 and cp <= 0xFF) or # Latin Extended + Cyrillic start
(cp >= 0x400 and cp <= 0x4FF) or # Cyrillic
ch == '_' or ch == '-':
word &= ch
else:
if word.len > 0:
var token = word.toLower()
if config.stemmer != nil:
token = config.stemmer(token)
if token.len >= 2 and token notin config.stopWords:
result.add(token)
word = ""
if word.len > 0:
var token = word.toLower()
if config.stemmer != nil:
token = config.stemmer(token)
if token.len >= 2 and token notin config.stopWords:
result.add(token)
proc detectLanguage*(text: string): Language =
# Simple heuristic based on byte patterns
# UTF-8 Cyrillic: bytes 0xD0-0xD1 followed by 0x80-0xBF
var cyrillicCount = 0
var latinCount = 0
var i = 0
let bytes = cast[seq[byte]](text)
while i < bytes.len:
let b = bytes[i]
if b >= 0xD0'u8 and b <= 0xD1'u8 and i + 1 < bytes.len and bytes[i+1] >= 0x80'u8:
inc cyrillicCount
i += 2
elif b >= 0x41'u8 and b <= 0x7A'u8:
inc latinCount
i += 1
else:
i += 1
if cyrillicCount > latinCount:
return langRussian # Could be BG or RU — default to RU
return langEnglish