ืืกืื ืื ืืชืืจ ืืื ืืฉืื ืืช ืื ืื ืื ืืืจืืื ืืืืจืคืืืืืืช MagicDictionary ืฉื kdroidFilter/SeforimLibrary ื-kdroidFilter/SeforimMagicIndexer ืืชืื otzaria_search_engine (Rust/Tantivy ืขื Flutter UI).
ืืืกืื ืืืืช ืืื ืืงืื ืืคืืขื (ืืงืืื + ืืงืืจืืช ืืืฆืื ืืื). ืฉืื ืืืื ืฉืืื ืืื ืืืชืืื ืืืงืืจืืช:
- ืืจืืคื
SeforimLibraryLMืฉืื ื ืฉืื ื-SeforimLibrary, ืืขื ืฃ ืืจืืจืช ืืืืื ืืืmaster(ืืmain). ืื ืืงืืฉืืจืื ืืืกืื ืขืืืื ื ืืืชืื. ืื ื-SeforimMagicIndexerืขื ืฃ ืืจืืจืช ืืืืื ืืืmaster.- ื-cap
MAX_SYNONYM_BOOST_TERMSืืื ืืขืช256(ืื 8), ืื ืืกืฃ ืงืืืขMAX_SYNONYM_TERMS_PER_TOKEN = 32. ืจืื ืืืงืื 3, 4.5, 5.- ืืืง 4.5 ื ืืชื ืืืืฉ ืืื ืืชืืื ืืช ืืืจืืืืงืืืจื ืืคืืขื:
build_queryืืื ืืชืืื (&self), ืืงืื ืืืregex_terms(ืืคืืกื regex, ืื ืืืงื ืื ืืืืืืื), ืืืื ืืข ืืืจ ืืืฆืข ืืจืืื ืืืจืคืืืืืืช ืืจื hebrew_query.rs. ืื ื ืงืืืช ืืฉืืืื ืืืืขืืช ืืืืืื.- ืืจืกืช Tantivy ืืคืืขื:
=0.26.1. ื-release ืืืืจืื ืฉื ื-DB:v0.3.0(2026-04-26),lexical.db~54.5MB.- ืืืืช ืื ืืื: ืกืืืช ื-DB,
LOOKUP_SQL, ืขืจืื ื-boost (2.0/1.5/1.0), ืืืืืงืืืกื (25 entries, highlight-only),buildLookupCandidates, ื-normalizeHebrew.
- ืืืืจื: ืืืคืืฉ ืืขืืจืืช ืฉืืืฆื ืฆืืจืืช ืืืื (ืืืโืืืืืืช) โ "ืืื" ืืืืืจ ืื "ืืืืชื", "ืืืื", "ืชืื"โฆ
- ืื ืืก: ืงืืืฅ SQLite ืืฉื
lexical.dbืฉื ืื ื offline ืข"ื Gemini AI ืขื ืงืืจืคืืก Sefaria+Otzaria. - ืืืื ืืคืืขื: 54 MB. ืืืื 24,559 ืืืืช, 137,631 ืฆืืจืืช ืฉืื, 190,151 ืืจืืื ืืื, 594,428 ืงืืฉืืจืื.
- ืืืืืจืฆืื ืืคืฉืจืืช. ~250 ืฉืืจืืช Rust. ื ืงืืืช ืืฉืืืื ืืื hebrew_query.rs (ืื ืืืช
regex_terms), ืืbuild_queryืืฉืืจืืช โ ืจืื ืืืง 4.5. - ืฆื Flutter/Dart ืื ืฆืจืื ืืืฉืชื ืืช ืื ืืฉืืืื ืคื ืืืืช.
- ืจืืฉืืื: AGPL-3.0 ืืฉื ื ืืืงืืจืืช โ ืชืืื ืืคืจืืืงื ืืขื GPL/AGPL.
ื"ืืืงื ืืืืจ" ืฉืืืคืชื ืืชืืืืก ืืืื ืืื ืืขืฆื ืฉื ื ืจืืืืื ืฉืื ืื:
| ืฉืื | ืืชื ืจืฅ | ืื ืืื ืขืืฉื | ืชืืื ื-AI? |
|---|---|---|---|
| SeforimMagicIndexer | Offline, ืคืขื ืืืช | ืงืืจื ื-Gemini API ืืื ืืืื ืืงืืจืคืืก ืืืคืืง LexicalEntry(surface, base, variants) |
ืื โ GEMINI_API_KEY |
| MagicDictionaryIndex | Runtime, ืืื ืฉืืืืชื | SQLite read-only lookup ืฉื ืืฆืืจืืช ืฉืืืจ ื ืืฆืจื | ืื |
ืืฉืืขืืช: ืืืืฆืจ ืืกืืคื (lexical.db) ืืื ืจืง SQLite. ื-Rust ืจืง ืฆืจืื ืืงืจืื ืืื ื ืขื rusqlite. ืืื AI calls ืืืื ืจืืฆื.
SeforimMagicIndexer + Gemini
โ (offline, slow, expensive)
lexical.db (SQLite, ~54.5 MB)
โ (downloaded once)
otzaria_search_engine ืงืืจื ืืืื ืืืคืืฉ
ืืจืืข ืืืืืืช ืขืืงืจืืืช. ืืกืืื ืืืืื ื-Database.sq:
-- ืืื (root form) - "ืืื"
CREATE TABLE base (
id INTEGER PRIMARY KEY AUTOINCREMENT,
value TEXT NOT NULL UNIQUE
);
-- ืฆืืจืืช ืฉืื (ืื ืืืืืืช ืื ืคืืฆืืช) - "ืืืืชื", "ืืืื", "ืชืื"
CREATE TABLE surface (
id INTEGER PRIMARY KEY AUTOINCREMENT,
value TEXT NOT NULL UNIQUE,
base_id INTEGER NOT NULL REFERENCES base(id),
notes TEXT
);
-- ืืจืืื ืืื (ืืชืื ืืืืคื, ื ืืงืื ืืืจ, ืืื')
CREATE TABLE variant (
id INTEGER PRIMARY KEY AUTOINCREMENT,
value TEXT NOT NULL UNIQUE
);
-- many-to-many ืืื surfaces ื-variants
CREATE TABLE surface_variant (
surface_id INTEGER NOT NULL REFERENCES surface(id),
variant_id INTEGER NOT NULL REFERENCES variant(id),
PRIMARY KEY (surface_id, variant_id)
);ืืืืงื ื ืชืื, ืืืืืจ ืืช ืื ืืฆืืจืืช ืืืงืืฉืจืืช ืืจื ืืืชื ืืื:
WITH matches AS (
SELECT s.base_id FROM surface s WHERE s.value = ?
UNION
SELECT b.id FROM base b WHERE b.value = ?
UNION
SELECT s.base_id FROM variant v
JOIN surface_variant sv ON sv.variant_id = v.id
JOIN surface s ON sv.surface_id = s.id
WHERE v.value = ?
)
SELECT b.id as base_id, b.value as base,
s.value as surface, v.value as variant
FROM base b
JOIN matches m ON m.base_id = b.id
LEFT JOIN surface s ON s.base_id = b.id
LEFT JOIN surface_variant sv ON sv.surface_id = s.id
LEFT JOIN variant v ON sv.variant_id = v.id;3 ืืกืืื ื ืฉืืื ืืงืืืื ืืช ืืืชื ืืืื (ืืจืืฉืื ื โ ืืืคืฉ ืืคื surface, ืืฉื ืืื โ ืืคื base, ืืฉืืืฉืืช โ ืืคื variant).
ื-LuceneSearchEngine.kt ืืื ืฉืืืืชื:
- ื ืืจืืื ืขืืจืืช ืขื
HebrewTextUtils.normalizeHebrew(ื ืืงืื, ืืขืืื, ืืจืฉืืื, ืกืืคืืืช). - ืืืงื ืืืฆืื ืขื StandardAnalyzer ืฉื Lucene.
- ืืื ืืืงื: ืงืจืืื ื-
magicDict.expansionFor(token)โ ืืงืืExpansion(surface[], variants[], base[]). - ืฉืืืฉ ืฉืืืืชืืช Lucene ื ืคืจืืืช ื ืื ืืช ืืืืืชื ืืจืืืืช:
| ืฉืืืืชื | ืชืคืงืื | ืืื ื |
|---|---|---|
buildPresenceFilterForTokens |
FILTER โ ืืืื |
ืืื ืืืงื: (token OR surfaceโ OR surfaceโ OR ...) MUST |
buildSynonymPhraseQuery |
ืคืจืืื ืืืืฉื | MultiPhraseQuery โ ืืื ืืืงืื ืืืชืจ variant |
buildMagicBoostQuery |
ืืืจืื | SHOULD ืขื boosts: surface=2.0, variants=1.5, base=1.0 |
ื-3 ืืืจืืืืช ื-BooleanQuery ืกืืคื.
- Cap ืขื ืืกืคืจ ืืืจืืืืช:
MAX_SYNONYM_BOOST_TERMS = 256ื-MAX_SYNONYM_TERMS_PER_TOKEN = 32(ืืฉืชื ืื ืืืืืืืื, ืฉืืจืืช 42-44). (ืืืจืกื ืืืงืืจืืช ืฉื ืืกืื ืื ืฆืืื 8 โ ืืขืจื ืขืื ืืื.) - Hallucination blacklist: AI ืืคืขืืื ืืืฆืจ ืืืคืืืื ืฉืืืืื. ืจืื ืฉืืจืืช 50-95 โ ืืืืงืืืกื ืืื ื ืฉืืกืื ื ืจืง ืืืืืืืืืื ื (ืื ืืืืคืืฉ ืขืฆืื, ืืื ืืฉืืืจ recall).
- ืกืืืค-ืืืจืืก: ืืืชืืืช ืืืืืืช ืืกืื ื ืืช ืืคื ื ืืืจืืื, ืคืจื ื-"ื" ืื ืืฉืืืืชื ืืืื "ืืณ" (Hashem).
- ืงืืฉืื: LRU ืฉื 1024 tokens + 512 bases ืืืื ืจืืฆื.
rust/src/api/magic/
โโโ mod.rs -- pub use ืฉื ืืจืืืืื
โโโ normalize.rs -- port ืฉื HebrewTextUtils (~80 LOC)
โโโ dictionary.rs -- port ืฉื MagicDictionaryIndex (~150 LOC)
โโโ blacklist.rs -- ืืืืงืืืกื ืืืืืช (~30 LOC)
โโโ downloader.rs -- ืืืจืืช lexical.db ื-GitHub Releases (~80 LOC)
[dependencies]
rusqlite = { version = "0.32", features = ["bundled"] } # bundled = ืืื ืชืืืช ืืขืจืืช ื-libsqlite
lru = "0.12"
regex = "1.10"
reqwest = { version = "0.12", features = ["json", "blocking"] } # ืจืง ืื ืืืจืื ื-Rustืืขืจื ืืื ืืจืืืื: rusqlite ืขื bundled ืขืืื ืืื ืขื Android (Tantivy ืฉืื ืืืจ ืืฉืชืืฉ ื-libc native build).
ื ืืจืฉ ืื ืจืื ืืช ื-input ืฉื ืืืฉืชืืฉ ืืืืชื ืฆืืจื ืฉืื ืืืืืข ื-DB:
use regex::Regex;
use once_cell::sync::Lazy;
// ืืขืืื U+0591โU+05AF
static TEAMIM: Lazy<Regex> = Lazy::new(|| Regex::new(r"[\u{0591}-\u{05AF}]").unwrap());
// ื ืืงืื + meteg + qamatz qatan.
// ืืขืจื: ื-`normalizeHebrew` ืืืงืืจื ื-Kotlin ืืกืืจ ืืช ืืืืื 05B0โ05BD + 05C1,05C2,05C7
// ืืื **ืื** ืืช 05BF (rafe). ืืฉืืจื ื 05BF ืืื ืื ืืื ื ืืืจ ืืงืืจืคืืก; ืื ืจืืฆืื ืืชืืื
// ืืืืืงืช 1:1 ื-DB โ ืืืกืืจ ืืช \u{05BF} ืืืืืื.
static NIKUD: Lazy<Regex> = Lazy::new(|| Regex::new(
r"[\u{05B0}-\u{05BD}\u{05BF}\u{05C1}\u{05C2}\u{05C7}]"
).unwrap());
pub fn normalize_hebrew(s: &str) -> String {
let s = TEAMIM.replace_all(s.trim(), "");
let s = NIKUD.replace_all(&s, "");
let s = s.replace('\u{05BE}', " "); // maqaf โ space
let s = s.replace('\u{05F4}', "") // gershayim
.replace('\u{05F3}', ""); // geresh
let s = replace_finals_with_base(&s);
// collapse whitespace
s.split_whitespace().collect::<Vec<_>>().join(" ")
}
fn replace_finals_with_base(s: &str) -> String {
s.chars().map(|c| match c {
'ื' => 'ื', 'ื' => 'ื', 'ื' => 'ื ', 'ืฃ' => 'ืค', 'ืฅ' => 'ืฆ',
c => c,
}).collect()
}use rusqlite::{Connection, params};
use lru::LruCache;
use std::sync::Mutex;
use std::num::NonZeroUsize;
use std::path::Path;
pub struct Expansion {
pub surface: Vec<String>,
pub variants: Vec<String>,
pub base: Vec<String>,
}
pub struct MagicDictionary {
conn: Mutex<Connection>,
cache: Mutex<LruCache<String, Vec<Expansion>>>,
}
const LOOKUP_SQL: &str = r#"
WITH matches AS (
SELECT s.base_id FROM surface s WHERE s.value = ?
UNION
SELECT b.id FROM base b WHERE b.value = ?
UNION
SELECT s.base_id FROM variant v
JOIN surface_variant sv ON sv.variant_id = v.id
JOIN surface s ON sv.surface_id = s.id
WHERE v.value = ?
)
SELECT b.id as base_id, b.value as base,
s.value as surface, v.value as variant
FROM base b
JOIN matches m ON m.base_id = b.id
LEFT JOIN surface s ON s.base_id = b.id
LEFT JOIN surface_variant sv ON sv.surface_id = s.id
LEFT JOIN variant v ON sv.variant_id = v.id
"#;
impl MagicDictionary {
pub fn open(path: &Path) -> anyhow::Result<Self> {
let conn = Connection::open_with_flags(
path,
rusqlite::OpenFlags::SQLITE_OPEN_READ_ONLY,
)?;
conn.execute_batch("PRAGMA query_only=ON;")?;
// ืขืืืจ Desktop: cache=512. ืขืืืจ Android: cache=128 (RAM-conscious).
let cache_size = if cfg!(target_os = "android") { 128 } else { 512 };
Ok(Self {
conn: Mutex::new(conn),
cache: Mutex::new(LruCache::new(NonZeroUsize::new(cache_size).unwrap())),
})
}
pub fn expansion_for(&self, token: &str) -> Option<Expansion> {
let normalized = crate::api::magic::normalize::normalize_hebrew(token);
if normalized.is_empty() { return None; }
if let Some(cached) = self.cache.lock().unwrap().get(&normalized) {
return cached.iter().max_by_key(|e| e.surface.len()).cloned();
}
let expansions = self.fetch_expansions(&normalized)?;
let best = expansions.iter().max_by_key(|e| e.surface.len()).cloned();
self.cache.lock().unwrap().put(normalized, expansions);
best
}
fn fetch_expansions(&self, token: &str) -> Option<Vec<Expansion>> {
// ... ืืืฉืื ืฉื LOOKUP_SQL + ืฆืืืจื ืืคื base_id
// ... ืืืื candidates ืฉื final form (ืกืืคืืืช)
todo!()
}
}ืืฉืื โ ืชืืงืื ืืืจืืืื ืืืืจืกื ืืืงืืจืืช. ืืืจืกื ืืืงืืจืืช ืฉื ืืืง ืื ืื ืืื ืฉ-
build_queryืืื ืคืื ืงืฆืื ืืืคืฉืืช ืฉืืงืืืช ืืืงื ืื ืืืืืืื, ืืฉืืกืืื ืืืืงื-ืืืืื ืืืRegexQuery::from_pattern. ืฉื ื ืืืืจืื ืฉืืืืื ืืงืื ืืคืืขื. ืจืื ืืืื ืืช ืืืฆื ืืืืืชื.
build_queryืืื ืืชืืื (&self) ื-search_engine.rs:1185 โ ืื ืคืื ืงืฆืื ืืืคืฉืืช, ืืืฉ ืื ืืืฉื ื-self.index_reader.- ืืงืื ืฉืื ืืื
regex_terms: Vec<String>โ ืืคืืกื regex, ืื ืืืืื ืืืืืืืช. ืืกืืื ืืืืงื-ืืืืื ืืื single_regex_term_query, ืฉืืืืจ ืืช ื-regex ืขืtantivy_fst::Regex, ืืืจืื ืืช ืื ืืื ืื ืืืื ืืงืก ืืชืืืืื, ืืืื ื ืืืTermSetQuery(ืขื ืืืืคืชmax_expansions). ืืกืืื ืจื-ืืืงื ื ืืื ืRegexPhraseQuery. - ืืื ืืข ืืืจ ืืืฆืข ืืจืืื ืืืจืคืืืืืืช โ ื-
regex_termsื ืื ืื ื-hebrew_query.rs ืข"ืcreate_search_pattern, ืฉืืจืืื ืืื ืืืื ืืคืืก regex ืืคื ืืืคืฉืจืืืืช: ืงืืืืืืช/ืกืืืืืช (ืืงืืืงืืืช ืืืืืืืช), ืืชืื ืืื/ืืกืจ, ืืืง-ืืืืื, ืืฉืืืืืช-ืืชืื. ื ืงืืืช ืืื ืืกื: build_advanced_query โ hebrew_query::prepare_advanced_query.
ืืืฉืืขืืช: ื"ืืจืืื" ืืื ืืื ืืืืกืกืช-ืืืืื (regex ืฉื ืืืชืืืช ืฉืืืืฉ), ืืขืื MagicDictionary ืืื ืืืืกืกืช-ืืงืกืืงืื (ืฆืืจืืช ืฉืื ืืืืชืืืช ืฉื ืืืื). ืืื ืฉื ื ืื ืื ืื ืื ืืฉืืืืื, ืืืืืืื ื ืื ืก ืืืืืง ืืืืชื ืฉืื ืฉืื ื ืื ืื ื-regex_terms.
ืืืงืื ืืืขืช ื-build_query, ืืืืกืืฃ ื-hebrew_query.rs ืืกืืื ืฉืืจืืื ืืืื
ืืจืฉืืืช ืฆืืจืืช ืืืืืืื, ืืื ืืฉืื ืืืชื ื-alternation ืืชืื ืืืชื ืืคืืก regex ืฉืืืจ ื ืื ื:
// ืืชืื hebrew_query.rs, ืืืฉืจ magic_dict ืืืื ืืืืคืขื ืขืืืจ ืืืืื:
fn create_lexical_pattern(word: &str, dict: &MagicDictionary) -> Option<String> {
let exp = dict.expansion_for(word)?; // surface[] + variants[] + base[]
// ืืืืื ืืฆืืจืืช (ืืืื ืืืืื ืขืฆืื), ื ืืจืืื, escape, ืืงืืืืข ื-anchored alternation
let mut forms: Vec<String> = std::iter::once(word.to_string())
.chain(exp.surface)
.chain(exp.variants)
.chain(exp.base)
.map(|f| escape_regex(&normalize_for_index(&f)))
.collect();
forms.sort();
forms.dedup();
forms.truncate(MAX_LEXICAL_FORMS); // cap โ ืจืื ืืขืจื ืืืื
Some(format!("(?:{})", forms.join("|")))
}- ืืืคืืก ืืื ืืชืืื ืขื ืืืคืืกืื ืืงืืืืื (ืงืืืืืช/ืกืืืืช/ืืชืื) ืืืืืง ืืื ืื ืืจืืื ื ืืืจ ื-
create_search_pattern. - ืืืืืื ืฉืืื ืืข ืืืืื ืืืืจ regexโ
TermSetQuery/RegexPhraseQueryืืืืืฃmax_expansions, ืืื ืฆืืจื ืืื ืืชBooleanQueryืืื ื ืขื boosts โ ืืื ืืื ืืืจืื boost ืืืื ื ืืืกืืื ื-regex ืฉื Tantivy. ืื ืจืืฆืื ืืช ืืืืจืื surface=2.0/variant=1.5/base=1.0 ืืื ื-Lucene, ืฆืจืื ืืกืืื ื ืคืจื (ืจืื ืืืง 5, ืืืคืฆืื A/C) โ ืื ืืืง ืืืฉืื ืืจืืฉืื.
MagicDictionary ืืื ื ืืก ืงืจืืื-ืืืื ืืื ืืืจื ืืื ืืื ืืข. ื ืืื ืืืืกื ืืืชื ืขื SearchEngine
ืขืฆืื (ืืื index_reader), ืืืืขืืืจ reference ืืจื prepare_advanced_query:
// search_engine.rs โ ืฉืื ืืืฉ ืขื ืืืื ื:
struct SearchEngine {
// ... index, index_reader, schema ...
magic_dict: Option<MagicDictionary>, // ื ืืขื ื-open() ืื lexical.db ืงืืื
}ืื ืืชืืืืช ื-API ืืืืฆืื ืืืช (search/count/...) ืื ืืฉืชื ืืช ืืื, ืืฆื ื-Dart ืื ืืืฉืคืข (ืจืื ืืืง 8).
ืืขืจื ืขื cap: ืืขืจืืื ืืงืื ื-Lucene ืืืงืืจื (
MAX_SYNONYM_BOOST_TERMS = 256,MAX_SYNONYM_TERMS_PER_TOKEN = 32) ื ืืืขืื ื-boost-queries ืฉื Lucene ืืื ืจืืืื ืืืื ืืฉืืจืืช ืืื. ืืืกืืื ื-regex, ื-cap ืืืคืงืืืื ืืืmax_expansionsืฉื ืืื ืืข (ืฉืืืจ ื ืืืฃ ื-single_regex_term_query). ืืืืืฅMAX_LEXICAL_FORMSื ืืื ืืฉืื (ืืืฉื 32) ืืื ืื ืื ืคื ืืช ื-alternation.
ื-Lucene, MultiPhraseQuery ืืืคืฉืจืช ืืื ืืืงืื ืืคืจืืื ืืืืืจ ืืืื n ืืืืคืืช.
ื-Tantivy ืื ืื ืงืืื ืืคืืฆ'ืจ ืืฉืืจ.
ืืขืจื ืืฉืืื (ืืืืช ืืงืื): ืืื ืืข ืืืจ ืืฉืชืืฉ ื-
RegexPhraseQueryืืืกืืื ืืจื-ืืืงื ื (build_query).RegexPhraseQueryืืืคืฉืจืช ืืื ืืืงืื ืืคืจืืื ืืคืืก regex โ ืืืืืจ alternation ืืื ืืืงืื โ ืืื ืืืืืง ืื ืฉ-MultiPhraseQueryื ืืชื ื-Lucene. ืืื ืขื ืืืืฉื ืฉื ืืืง 4.5 (ืืืจืงืช ืฆืืจืืช ืืงืกืืงืืืืช ื-alternation ืืชืื ื-regex ืฉื ืื ืืืื) ืืงืืืื ืืช ืืืืืืช ืืจื-ืืืงืืืืช "ืืืื ื" โ ืืื ืฆืืจื ื-Cartesian product ืืื ื. ืืืคืฉืจืืืืช ืืืื ืจืืืื ืืืืช ืืขืืงืจ ืื ืจืืฆืื ืืืจืื boost (ืฉืืื ืื ืชืืืื ืืฉืืจื ืืืกืืื ื-regex).
ืืืคืฆืื A โ Cartesian Product (ืืืืจืื boost ืืืื):
ืืื ืืช BooleanQuery { SHOULD: PhraseQuery_i } ืืื ืงืืืืื ืฆืื ืืคืฉืจืืช, ืืื ืืืืื boost ืฉืื ื ืืื ืฆืืจื.
ืฉืื ืื: ื-cap ืืงืื ื-Lucene ืืื ืืืื 256 (MAX_SYNONYM_BOOST_TERMS) ืืื 8 โ ืืื Cartesian ืืื
ืขื ืขืจืืื ืืืื ืืื explosion ืืืชื-ืืคืฉืจื. ืื ืืืืืื ืืืืืื ืื, ืืืื cap ื ืืื ืืืื (โค4 ืืื ืืืงื)
ืืืืืืื ื-2-3 ืืืงื ืื. ืืืจืืืช ืืืงืจืื ืืืืชืจ โ RegexPhraseQuery ืืืจ ื ืืชื ืืช ื-recall.
ืืืคืฆืื B โ Synonym injection ืืืื indexing:
ืืืชืื MagicSynonymFilter ืฉืคืืื position_inc=0 ืืื variant.
ืื ืืืืืฅ:
- ื ืืคืื ืืื ืืงืก ืคื 5-10
- ืื ืขืืืื ืฉื lexical.db ืืืจืฉ reindex ืืื ืฉื ืืงืืจืคืืก
- ืงืืฉื ืื ืื cap ืืื ืื
ืืืคืฆืื C โ ืืืจืงืช alternation ื-regex (ืืืืืฅ โ ืชืืื 4.5):
ืืืืจืืง ืืช ืืฆืืจืืช ืืืงืกืืงืืืืช ื-alternation ืืชืื ื-regex_terms (ืืืง 4.5). ืืกืืื ืืืงื-ืืืื โ TermSetQuery,
ืืกืืื ืจื-ืืืงื ื โ RegexPhraseQuery ืขื alternation ืืื ืืืงืื. ืืื ืืืจืื boost, ืืื ื-recall ืืื ืืืฉืชืื
ืืืง ืขื ืืืจืืื ืืืืจืคืืืืืืช ืืงืืืืช.
ืืืืฆื: ืืชืื ื-C (ืืจืืื ืืืขืืช ื-4.5). ืขืืืจ ื-A ืจืง ืื ื ืืจืฉ ืืืจืื ืขืืื ืืคื ืกืื ืืฆืืจื.
ื-AI ืืคืืง ืืคืขืืื ืืืคืืืื ืฉืืืืื. ืืงืื ืืืงืืจื ืื ืื ืืืืงืืืกื ื-TSV ื ืคืจื.
- ืืืงืื ืืืงืืจ: search/src/jvmMain/resources/hallucination_blacklist.tsv
- URL ืืฉืืจ:
https://raw.githubusercontent.com/kdroidFilter/SeforimLibrary/master/search/src/jvmMain/resources/hallucination_blacklist.tsv - ืืืื: 29 ืฉืืจืืช (25 entries + 4 ืฉืืจืืช ืืขืจื/ืืกืืจ)
# Hallucination Blacklist
# Format: token<TAB>base
# Lines starting with # are comments
ืืืชืื ืื
ืืืงืฉ ืจืฆื
ืืชืคืื ืืขื
...
ืืืงื (col 1) ืืืกืืก ืฉืืื (col 2). ืฉื ืืื ืื ืืจืืืื ืขื normalizeHebrew ืืคื ื ืืืกืื ื-map.
ืืืขืื ื: loadHallucinationBlacklist.
ืืืืืงืืืกื ืืงืื ืืืงืืจื ืืฉืืฉ ืจืง ืืกืื ืื ืืืืืืืืื ื, ืื ืืืืคืืฉ ืขืฆืื. ืืืืืืื: ืืืื ื-recall ืื ืืกืืจ (ืืืชืจ ืชืืฆืืืช), ืืื ืืืื ื-highlight ืื ืืขืืืชื (ืืฉืชืืฉ ืจืืื ืืืื ืื ืงืฉืืจื ืฆืืืขื ืืืื).
ืื ืชืืจืื ืืืื build ืื ืจืืฆื. ืืงืืืฅ ืงืื ืืืชืขืืื ืจืง ืืขืืชืื ื ืืืจืืช. ืคืฉืื ืืืืข ืืืชื ืืืื ืืจื:
// rust/src/api/magic/blacklist.rs
use std::collections::{HashMap, HashSet};
use once_cell::sync::Lazy;
use super::normalize::normalize_hebrew;
const BLACKLIST_TSV: &str =
include_str!("../../../resources/hallucination_blacklist.tsv");
pub static HALLUCINATION_BLACKLIST: Lazy<HashMap<String, HashSet<String>>> =
Lazy::new(|| {
let mut map: HashMap<String, HashSet<String>> = HashMap::new();
for raw in BLACKLIST_TSV.lines() {
let line = raw.trim();
if line.is_empty() || line.starts_with('#') { continue; }
let mut cols = line.splitn(2, '\t');
if let (Some(t), Some(b)) = (cols.next(), cols.next()) {
let token = normalize_hebrew(t);
let base = normalize_hebrew(b);
map.entry(token).or_default().insert(base);
}
}
map
});
pub fn is_hallucinated(token: &str, expansion: &Expansion) -> bool {
let nt = normalize_hebrew(token);
let Some(bad) = HALLUCINATION_BLACKLIST.get(&nt) else { return false; };
expansion.base.iter().any(|b| bad.contains(&normalize_hebrew(b)))
}# scripts/sync_blacklist.sh
curl -fsSL \
https://raw.githubusercontent.com/kdroidFilter/SeforimLibrary/master/search/src/jvmMain/resources/hallucination_blacklist.tsv \
-o rust/resources/hallucination_blacklist.tsvืชืจืืฅ ืืื ืืช ืื ืืื ืืืืฉืื. ืื ืชืื ืืก ืืื ืืื ืืืืืืืืช โ ืื ืืืกืืฃ ืชืืืช ืจืฉืช ื-CI ืืืฉืืืจ ืืืืื mobile cross-compilation ืืจื CargoKit ืืื ืชืืขืืช ืืืืชืืช.
https://api.github.com/repos/kdroidFilter/SeforimMagicIndexer/releases/latest
ืืืคืฉืื asset ืฉื-browser_download_url ืืกืชืืื ื-/lexical.db.
const LATEST_API: &str = "https://api.github.com/repos/kdroidFilter/SeforimMagicIndexer/releases/latest";
pub fn ensure_lexical_db(dest: &Path) -> anyhow::Result<PathBuf> {
let db_path = dest.join("lexical.db");
if db_path.exists() && db_path.metadata()?.len() > 0 {
return Ok(db_path);
}
let body: serde_json::Value = reqwest::blocking::Client::new()
.get(LATEST_API)
.header("User-Agent", "otzaria_search_engine/0.1")
.send()?.json()?;
let url = body["assets"].as_array().unwrap().iter()
.find_map(|a| {
let u = a["browser_download_url"].as_str()?;
u.ends_with("/lexical.db").then(|| u.to_string())
})
.ok_or_else(|| anyhow::anyhow!("no lexical.db asset in latest release"))?;
// ... reqwest download to db_path
Ok(db_path)
}Android: ืืคืฉืจ ืืขืฉืืช ืืช ืืืืจืื ืืฆื Dart (Dio ืื http) ืืืงืจืื ื-Rust ืจืง ืขื ืื ืชืื. ืขืืืฃ, ืื Flutter ืืืจ ืื ืื permissions/storage.
/lib/ ืืื wrapper ืฉื ืืฆืจ ืืืืืืืืช ืข"ื flutter_rust_bridge ืืืืชืืืืช ืฉื search_engine.rs.
ืื ืืืกืคืช ืืืืืื ื ืขืฉืืช ืคื ืืืืช ื-build_query:
- ืืชืืืช
search()/search_and_count()/count()ืื ืืฉืชื ื - ืืจืืฆืื
flutter_rust_bridge_codegen generateโ ืืื ืฉืื ืื ืืงืื ืฉืืฉืชืืฉ ื-API ืืฆื Dart - ืืืคืืืงืฆืื ืืืฉืืื ืืืืจืืง "regex_terms" ืืืงืื ืชืืฆืืืช
ืื ืืืกืืคืื flag enable_magic: bool:
- ืฉืื ืื ืืื ' ืืฆื Dart โ ืชืืกืคืช ืคืจืืืจ ืืื ืงืจืืื
- ืืืคืฉืจ fallback ืืืฆื ืืฉื
ืืืืฆื: ืืชืื ืขื ืืฉืชื ื ืืืืืื ืื config ืคื ืืื, ืืื ืฉืื ืื API. ืคืืื ืจืง ืื ืชืจืฆื ืืืืฉืืช ื-runtime ื-Dart.
| ืฉืื | ืคืขืืื | ืืืืฅ | ืงืจืืืจืืื ืืฆืืื |
|---|---|---|---|
| 0 | ืืืืืช ืจืืฉืืื AGPL โ AGPL (ืืืจ ืืืืช) | โ done | - |
| 1 | ืงืื magic/normalize.rs + ืืืื ื ืืืืื |
~2h | normalize_hebrew("ืึธืึทึฃืึฐ") โ "ืืื" |
| 2 | ืงืื magic/dictionary.rs + downloader |
~4h | ืืืืื ื ืืขื, expansion_for("ืืื") ืืืืืจ โฅ5 ืฆืืจืืช |
| 3 | ืืืืงืืืกื ืืืืืช (port ืื-Kotlin) | ~1h | ืืืคืื HashMap<&str, Vec<&str>> ืกืืื |
| 4 | ืฉืื ืื build_query + flag ืคื ืืื |
~3h | search("ืืื") ืืืฆื ืชืืฆืืืช ืฉืืืืืืช "ืืืืชื" |
| 5 | ืื ืฆ'ืืจืง ืืคื ื/ืืืจื | ~2h | latency < +20ms, recall โโ |
| 6 | ืืืืงื ื-Android (memory profile) | ~2h | RSS < 50MB ืขื cache=128 |
ืกื"ื: ~14-15 ืฉืขืืช ืขืืืื.
ื-Lucene: ืืืชืืืช ืขืืจืืืช ืืืืืืช ืืกืื ื ืืช ืืคื ื ืงืจืืื ื-MagicDict (ืืืกืืื ืขื ืง ืืืื+ืชืืฆืืืช ืืืืชืจืืช). ืจืื ืฉืืจืืช 399-409. ืืจืืืื:
- ืฉืืืจืช "ื" ืื ืืฉืืืืชื ืืืงืืจืืช ืืืื "ืืณ" (ืฉื ืืฉื)
- ืฉืืืจืช ืืืงื ืื ืืกืคืจืืื
ืืคื ื lookup, ื-Kotlin ืืื ื ืจืฉืืื ืฉื 4 ื ืืกืืื: raw, normalized, raw ืขื ืกืืคืืช, normalized ืขื ืกืืคืืช. ืจืื buildLookupCandidates. ืืฉืื ืืคืืจื.
ื-Kotlin ืืื ืืฉืชืืฉ ื-ThreadLocal<LookupContext> โ connection ื ืคืจื ืคืจ thread.
ื-Rust ืขื Mutex<Connection> ืื ืคืฉืื ืืืชืจ ืืื ืืขืื ืคืืืช ืชืืช load ืืงืืืื. ืื ืื ืฆืืืืจ ืืงืืืง:
r2d2_sqlite(connection pool)- ืื
thread_local!ืขื connection ืคืจ thread
- Desktop: 1024 tokens + 512 bases (ืืื ืืืงืืจื)
- Android: 128 + 64 (ืืืืื
cfg!(target_os = "android"))
- SeforimMagicIndexer (offline AI builder): https://github.com/kdroidFilter/SeforimMagicIndexer
- SeforimLibrary (consumer + integration): https://github.com/kdroidFilter/SeforimLibrary
- ืงืืืฅ ื-Lucene integration: LuceneSearchEngine.kt
- ืงืืืฅ ื-Dictionary index: MagicDictionaryIndex.kt
- ืงืืืฅ Hebrew utils: HebrewTextUtils.kt
- ืกืืืช ื-DB: Database.sq
- Latest release (lexical.db): https://api.github.com/repos/kdroidFilter/SeforimMagicIndexer/releases/latest
- Releases page: https://github.com/kdroidFilter/SeforimMagicIndexer/releases
- Tantivy docs: https://docs.rs/tantivy/0.26.1/tantivy/
- BoostQuery: https://docs.rs/tantivy/0.26.1/tantivy/query/struct.BoostQuery.html
- PhraseQuery: https://docs.rs/tantivy/0.26.1/tantivy/query/struct.PhraseQuery.html
- Tantivy custom tokenizers: https://docs.rs/tantivy/0.26.1/tantivy/tokenizer/index.html
- rusqlite: https://docs.rs/rusqlite/
- lru: https://docs.rs/lru/
ืืืืกืก ืขื ืงืืืฅ ืืืืชื (ืืจืกื ื-2026-05-03). ืขืืืื: ื-release ืืจืฉืื ืืืืจืื ืืื v0.3.0
(2026-04-26), lexical.db ืืืฉืงื 57,122,816 ืืืื (~54.5MB). ืืืกืคืจืื ืืืื (ืืกืคืจื ืฉืืจืืช ืืืืืืืช)
ืื ื ืืืงื ืืืืฉ ืืื v0.3.0 ืืขืฉืืืื ืืกืืืช ืงืืืช, ืื ืกืืจื ืืืืื ืชืงืคืื:
| ืืื | ืขืจื | ืืฉืืขืืช ืืคืืชืื |
|---|---|---|
| ืืืื ื-DB | ~54.5 MB (57,122,816 B) | OK ื-Desktop. ืืื ืืจืืืื ืฉืืื ืืืืืง ืืื ืืืืจืื ืืืจื first-run ืืื ืืืจืื |
ืืกืคืจ ืืืืช (base) |
24,559 | ืงืฆืช ืืขืืจ ื-WordNet ืืขืืจื ืืคืชืื |
ืฆืืจืืช ืฉืื (surface) |
137,631 | ืืืืก ~5.6 surfaces/lemma โ ืกืืืจ ืืืืื ื ืืืจืคืืืืืืช |
ืืจืืื ืืื (variant) |
190,151 | ืืืชืจ ืืจืืื ืืื ื-surfaces โ ืืจืื ืืชืื ืืืืคื |
surface_variant ืงืืฉืืจืื |
594,428 | ืืืก cardinality ืืืื |
- ืฉืื cache LRU: ืขื 137K surfaces, cache ืฉื 1024 ื-Desktop ืืืกื ืจืง 0.7% โ ืืื ื-locality ืฉื ืฉืืืืชืืช ืืืืชืืืช ืืืื, ืื ืื ืืกืืจ. ื-Android cache=128 ืืกืคืืง.
- First-load latency: ืืื ืฆืืจื ื-pre-warming โ lookup ืืื ืืืคืืฉ ืืคื index ื-SQLite, ืืืืืฉื ืืืช ืืืืืืช.
- ืืืฉื ืืคืืืฉ ืืื ืืจืืืื: 54 MB SSD random read sequential ืืจื MMap โ ืชืื ื ืืืขืชืืง ืืช ื-DB ื-internal storage ืืคื ื ืฉืืืืฉ, ืื ืืฉืชืืฉ ื-
temp_store=MEMORYืขื connection pragmas.
- 25 entries ืืืื ื ืืื ืืืืื
- ืืชืขืืื ื-git history ืฉื hallucination_blacklist.tsv
- ืจืื ืืืง 6 โ embed ืขื
include_str!, ืจืขื ื ืืื ืืช.
ืืกืื ืื ื ืืฆืจ ื-2026-05-17 ืืชืืฆืื ืืกืฉื ืืืงืจ ืฉื Claude Opus 4.7 ืขื ืฉืืืฉืช ืืจืืคื ืืจืืืื ืืืื. ืืืืช ืืขืืืื ื-2026-06-23 (Claude Opus 4.8) ืืื ืืงืื ืืคืืขื โ ืจืื ืืื ืจ ืืืืืืช ืืจืืฉ ืืืกืื.