Skip to content

Latest commit

ย 

History

History
603 lines (475 loc) ยท 31.6 KB

File metadata and controls

603 lines (475 loc) ยท 31.6 KB

MagicDictionary โ†’ otzaria_search_engine โ€” ืžืกืžืš ืžื™ื’ืจืฆื™ื”

ืžืกืžืš ื–ื” ืžืชืืจ ืื™ืš ืœืฉืœื‘ ืืช ืžื ื’ื ื•ืŸ ื”ื”ืจื—ื‘ื” ื”ืžื•ืจืคื•ืœื•ื’ื™ืช MagicDictionary ืฉืœ kdroidFilter/SeforimLibrary ื•-kdroidFilter/SeforimMagicIndexer ืœืชื•ืš otzaria_search_engine (Rust/Tantivy ืขื Flutter UI).


โš ๏ธ ืขื“ื›ื•ืŸ ืื™ืžื•ืช (2026-06-23)

ื”ืžืกืžืš ืื•ืžืช ืžื•ืœ ื”ืงื•ื“ ื‘ืคื•ืขืœ (ืžืงื•ืžื™ + ืžืงื•ืจื•ืช ื—ื™ืฆื•ื ื™ื™ื). ืฉื™ื ื•ื™ื™ื ืฉื—ืœื• ืžืื– ื”ื›ืชื™ื‘ื” ื”ืžืงื•ืจื™ืช:

  • ื”ืจื™ืคื• 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.

TL;DR

  • ื”ืžื˜ืจื”: ื—ื™ืคื•ืฉ ื‘ืขื‘ืจื™ืช ืฉืžื•ืฆื ืฆื•ืจื•ืช ื”ื˜ื™ื” (ืœืžื”โ†’ื”ื”ื˜ื™ื•ืช) โ€” "ื”ืœืš" ื™ื—ื–ื™ืจ ื’ื "ื”ืœื›ืชื™", "ื”ื•ืœืš", "ืชืœืš"โ€ฆ
  • ื”ื ื›ืก: ืงื•ื‘ืฅ 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.

ื—ืœืง 1 โ€” ืžื” ื–ื” MagicDictionary (ื•ืœืžื” ื–ื” ืœื ื˜ื•ืงื ื™ื™ื–ืจ)

ื”"ื˜ื•ืงื ื™ื™ื–ืจ" ืฉื”ืžืคืชื— ืžืชื™ื™ื—ืก ืืœื™ื• ื”ื•ื ื‘ืขืฆื ืฉื ื™ ืจื›ื™ื‘ื™ื ืฉื•ื ื™ื:

ืฉืœื‘ ืžืชื™ ืจืฅ ืžื” ื”ื•ื ืขื•ืฉื” ืชืœื•ื™ ื‘-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 ืงื•ืจื ื‘ื–ืžืŸ ื—ื™ืคื•ืฉ

ื—ืœืง 2 โ€” ืกื›ืžืช lexical.db

ืืจื‘ืข ื˜ื‘ืœืื•ืช ืขื™ืงืจื™ื•ืช. ื”ืกื›ืžื” ื”ืžืœืื” ื‘-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)
);

ื”ืฉืื™ืœืชื” ื”ืงืจื™ื˜ื™ืช (LOOKUP_SQL)

ืœื˜ื•ืงืŸ ื ืชื•ืŸ, ืžื—ื–ื™ืจ ืืช ื›ืœ ื”ืฆื•ืจื•ืช ื”ืžืงื•ืฉืจื•ืช ื“ืจืš ืื•ืชื” ืœืžื”:

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).


ื—ืœืง 3 โ€” ืื™ืš ื–ื” ืžืฉื•ืœื‘ ื‘ืžื ื•ืข ื”ืžืงื•ืจื™ (Lucene/Kotlin)

ื‘-LuceneSearchEngine.kt ื‘ื›ืœ ืฉืื™ืœืชื”:

  1. ื ื™ืจืžื•ืœ ืขื‘ืจื™ืช ืขื HebrewTextUtils.normalizeHebrew (ื ื™ืงื•ื“, ื˜ืขืžื™ื, ื’ืจืฉื™ื™ื, ืกื•ืคื™ื•ืช).
  2. ื˜ื•ืงื ื™ื–ืฆื™ื” ืขื StandardAnalyzer ืฉืœ Lucene.
  3. ืœื›ืœ ื˜ื•ืงืŸ: ืงืจื™ืื” ืœ-magicDict.expansionFor(token) โ†’ ืžืงื‘ืœ Expansion(surface[], variants[], base[]).
  4. ืฉืœื•ืฉ ืฉืื™ืœืชื•ืช 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 ื‘ื–ืžืŸ ืจื™ืฆื”.

ื—ืœืง 4 โ€” ืชื•ื›ื ื™ืช ื”ืžื™ื’ืจืฆื™ื” ืœ-Tantivy

4.1 ืงื‘ืฆื™ื ื—ื“ืฉื™ื

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)

4.2 ืชื•ืกืคื•ืช ืœ-Cargo.toml

[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).

4.3 normalize.rs โ€” ื ื™ืจืžื•ืœ ืขื‘ืจื™ืช

ื ื“ืจืฉ ืœื ืจืžืœ ืืช ื”-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()
}

4.4 dictionary.rs โ€” ืฉื›ื‘ืช ื”-SQLite

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!()
    }
}

4.5 ืฉื™ืœื•ื‘ ื‘ืžื ื•ืข โ€” ื ืงื•ื“ืช ื”ืฉื™ืœื•ื‘ ื”ื ื›ื•ื ื” ื”ื™ื hebrew_query.rs

ื—ืฉื•ื‘ โ€” ืชื™ืงื•ืŸ ืื“ืจื™ื›ืœื™ ืžื”ื’ืจืกื” ื”ืžืงื•ืจื™ืช. ื”ื’ืจืกื” ื”ืžืงื•ืจื™ืช ืฉืœ ื—ืœืง ื–ื” ื”ื ื™ื—ื” ืฉ-build_query ื”ื™ื ืคื•ื ืงืฆื™ื” ื—ื•ืคืฉื™ืช ืฉืžืงื‘ืœืช ื˜ื•ืงื ื™ื ื’ื•ืœืžื™ื™ื, ื•ืฉืžืกืœื•ืœ ื”ื˜ื•ืงืŸ-ื”ื™ื—ื™ื“ ื”ื•ื RegexQuery::from_pattern. ืฉื ื™ ื”ื“ื‘ืจื™ื ืฉื’ื•ื™ื™ื ื‘ืงื•ื“ ื‘ืคื•ืขืœ. ืจืื” ืœืžื˜ื” ืืช ื”ืžืฆื‘ ื”ืืžื™ืชื™.

ืžื” ืงื•ืจื” ื‘ืคื•ืขืœ ื”ื™ื•ื

  1. build_query ื”ื™ื ืžืชื•ื“ื” (&self) ื‘-search_engine.rs:1185 โ€” ืœื ืคื•ื ืงืฆื™ื” ื—ื•ืคืฉื™ืช, ื•ื™ืฉ ืœื” ื’ื™ืฉื” ืœ-self.index_reader.
  2. ื”ืงืœื˜ ืฉืœื” ื”ื•ื regex_terms: Vec<String> โ€” ื“ืคื•ืกื™ regex, ืœื ืžื™ืœื™ื ื’ื•ืœืžื™ื•ืช. ืžืกืœื•ืœ ื”ื˜ื•ืงืŸ-ื”ื™ื—ื™ื“ ื”ื•ื single_regex_term_query, ืฉืžื”ื“ืจ ืืช ื”-regex ืขื tantivy_fst::Regex, ืžื–ืจื™ื ืืช ื›ืœ ืžื•ื ื—ื™ ื”ืื™ื ื“ืงืก ื”ืชื•ืืžื™ื, ื•ื‘ื•ื ื” ืžื”ื TermSetQuery (ืขื ืื›ื™ืคืช max_expansions). ืžืกืœื•ืœ ืจื‘-ื˜ื•ืงื ื™ ื‘ื•ื ื” RegexPhraseQuery.
  3. ื”ืžื ื•ืข ื›ื‘ืจ ืžื‘ืฆืข ื”ืจื—ื‘ื” ืžื•ืจืคื•ืœื•ื’ื™ืช โ€” ื”-regex_terms ื ื‘ื ื™ื ื‘-hebrew_query.rs ืข"ื™ create_search_pattern, ืฉืžืจื›ื™ื‘ ืœื›ืœ ืžื™ืœื” ื“ืคื•ืก regex ืœืคื™ ื”ืืคืฉืจื•ื™ื•ืช: ืงื™ื“ื•ืžื•ืช/ืกื™ื•ืžื•ืช (ื“ืงื“ื•ืงื™ื•ืช ื•ื›ืœืœื™ื•ืช), ื›ืชื™ื‘ ืžืœื/ื—ืกืจ, ื—ืœืง-ืžืžื™ืœื”, ื•ืฉื’ื™ืื•ืช-ื›ืชื™ื‘. ื ืงื•ื“ืช ื”ื›ื ื™ืกื”: build_advanced_query โ†’ hebrew_query::prepare_advanced_query.

ื”ืžืฉืžืขื•ืช: ื”"ื”ืจื—ื‘ื”" ื›ืืŸ ื”ื™ื ืžื‘ื•ืกืกืช-ื›ืœืœื™ื (regex ืฉืœ ืื•ืชื™ื•ืช ืฉื™ืžื•ืฉ), ื‘ืขื•ื“ MagicDictionary ื”ื™ื ืžื‘ื•ืกืกืช-ืœืงืกื™ืงื•ืŸ (ืฆื•ืจื•ืช ืฉื˜ื— ืืžื™ืชื™ื•ืช ืฉื ืœืžื“ื•). ืืœื• ืฉื ื™ ืžื ื’ื ื•ื ื™ื ืžืฉืœื™ืžื™ื, ื•ื”ืžื™ืœื•ืŸ ื ื›ื ืก ื‘ื“ื™ื•ืง ื‘ืื•ืชื• ืฉืœื‘ ืฉื‘ื• ื ื‘ื ื™ื ื”-regex_terms.

ื”ื’ื™ืฉื” ื”ืžื•ืžืœืฆืช: ื”ื–ืจืงืช ืฆื•ืจื•ืช ืœืงืกื™ืงืœื™ื•ืช ืœืชื•ืš ื”-regex term

ื‘ืžืงื•ื ืœื’ืขืช ื‘-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) โ€” ืœื ื—ืœืง ืžื”ืฉืœื‘ ื”ืจืืฉื•ืŸ.

ื”ื™ื›ืŸ ืžื–ืจื™ืงื™ื ืืช magic_dict

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.


ื—ืœืง 5 โ€” ื”ื‘ื“ืœ ืื“ืจื™ื›ืœื™ ืงืจื™ื˜ื™: ืื™ืŸ MultiPhraseQuery ื‘-Tantivy

ื‘-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 ืจืง ืื ื ื“ืจืฉ ื“ื™ืจื•ื’ ืขื“ื™ืŸ ืœืคื™ ืกื•ื’ ื”ืฆื•ืจื”.


ื—ืœืง 6 โ€” Hallucination Blacklist

ื”-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 ื–ื” ื‘ืขื™ื™ืชื™ (ืžืฉืชืžืฉ ืจื•ืื” ืžื™ืœื” ืœื ืงืฉื•ืจื” ืฆื‘ื•ืขื” ืื“ื•ื).

ื™ื™ืฉื•ื ืžื•ืžืœืฅ ื‘-Rust: include_str! ื‘ื–ืžืŸ ืงื•ืžืคื™ืœืฆื™ื”

ืืœ ืชื•ืจื™ื“ ื‘ื–ืžืŸ 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)))
}

ืขื“ื›ื•ืŸ ื™ื“ื ื™ (script ืœื-ืžื•ื‘ื ื” ื‘ื‘ื ื™ื™ื”)

# 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 ืœืœื ืชื•ืขืœืช ืืžื™ืชื™ืช.


ื—ืœืง 7 โ€” downloader.rs โ€” ื”ื•ืจื“ืช lexical.db

API endpoint

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.


ื—ืœืง 8 โ€” ืฆื“ Flutter/Dart

/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.


ื—ืœืง 9 โ€” ืฉืœื‘ื™ ื‘ื™ืฆื•ืข ืžื•ืžืœืฆื™ื

ืฉืœื‘ ืคืขื•ืœื” ืžืืžืฅ ืงืจื™ื˜ืจื™ื•ืŸ ื”ืฆืœื—ื”
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 ืฉืขื•ืช ืขื‘ื•ื“ื”.


ื—ืœืง 10 โ€” ื ืงื•ื“ื•ืช ื–ื”ื™ืจื•ืช ื•ื˜ื™ืคื™ื ืžื”ืงื•ื“ ื”ืžืงื•ืจื™

ืกื˜ื•ืค-ื•ื•ืจื“ืก ืœืคื ื™ ื”ืจื—ื‘ื”

ื‘-Lucene: ืื•ืชื™ื•ืช ืขื‘ืจื™ื•ืช ื‘ื•ื“ื“ื•ืช ืžืกื•ื ื ื•ืช ืœืคื ื™ ืงืจื™ืื” ืœ-MagicDict (ื—ื™ืกื›ื•ืŸ ืขื ืง ื‘ื–ืžืŸ+ืชื•ืฆืื•ืช ืžื™ื•ืชืจื•ืช). ืจืื” ืฉื•ืจื•ืช 399-409. ื—ืจื™ื’ื™ื:

  • ืฉืžื™ืจืช "ื”" ืื ื”ืฉืื™ืœืชื” ื”ืžืงื•ืจื™ืช ื›ืœืœื” "ื”ืณ" (ืฉื ื”ืฉื)
  • ืฉืžื™ืจืช ื˜ื•ืงื ื™ื ืžืกืคืจื™ื™ื

candidates ืฉืœ final form

ืœืคื ื™ lookup, ื”-Kotlin ื‘ื•ื ื” ืจืฉื™ืžื” ืฉืœ 4 ื ื•ืกื—ื™ื: raw, normalized, raw ืขื ืกื•ืคื™ืช, normalized ืขื ืกื•ืคื™ืช. ืจืื” buildLookupCandidates. ื—ืฉื•ื‘ ืœืคื•ืจื˜.

Thread-safety

ื‘-Kotlin ื”ื•ื ืžืฉืชืžืฉ ื‘-ThreadLocal<LookupContext> โ€” connection ื ืคืจื“ ืคืจ thread. ื‘-Rust ืขื Mutex<Connection> ื–ื” ืคืฉื•ื˜ ื™ื•ืชืจ ืื‘ืœ ื™ืขื™ืœ ืคื—ื•ืช ืชื—ืช load ืžืงื‘ื™ืœื™. ืื ื–ื” ืฆื•ื•ืืจ ื‘ืงื‘ื•ืง:

  • r2d2_sqlite (connection pool)
  • ืื• thread_local! ืขื connection ืคืจ thread

ื’ื•ื“ืœ ื”-cache

  • Desktop: 1024 tokens + 512 bases (ื›ืžื• ื”ืžืงื•ืจื™)
  • Android: 128 + 64 (ื–ื™ื”ื•ื™ cfg!(target_os = "android"))

ื—ืœืง 11 โ€” ืงื™ืฉื•ืจื™ื

ืงื•ื“ ืžืงื•ืจ

API

Tantivy resources

Rust crates


ื ืกืคื— A โ€” ื ืชื•ื ื™ื ืฉืื•ืžืชื• ืขืœ lexical.db ื‘ืคื•ืขืœ

ืžื‘ื•ืกืก ืขืœ ืงื•ื‘ืฅ ืืžื™ืชื™ (ื’ืจืกื” ืž-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) ืžื•ืœ ื”ืงื•ื“ ื‘ืคื•ืขืœ โ€” ืจืื” ื‘ืื ืจ ื”ืื™ืžื•ืช ื‘ืจืืฉ ื”ืžืกืžืš.