Making usearch safe to load: fetch its SQLite extension once, check it in a throwaway process, and fall back to numpy when it fails.
litesearch uses two compiled parts of the usearch package. Its HNSW index is a Python extension. Its vector distance functions for SQL are a SQLite extension that usearch downloads from GitHub the first time usearch.sqlite_path() is called.
usearch_fix adds /usr/lib to the SQLite extension’s rpath with Apple’s install_name_tool, without which the extension cannot find the system SQLite on macOS. It does nothing on Linux and Windows, prints failures instead of raising them, and prints each step with v=True. litesearch calls it before the first load, so you do not call it yourself.
def usearch_fix( v:bool=False, # print each step path:str=None, # the extension's path without suffix (None: ask usearch)):
Add /usr/lib to the usearch SQLite extension’s rpath on macOS so the extension can load; does nothing elsewhere.
Probing usearch before the first load
Loading usearch’s SQLite extension has crashed Python with a segmentation fault on GitHub’s AMD EPYC 9V74 and 9V45 runners, about one run in four, at loadextension in database(). A crash in a compiled library cannot be caught, so litesearch tests usearch in a throwaway subprocess before it loads anything into your process.
The probe builds a 4-number HNSW index and searches it, then loads the SQLite extension into an in-memory database and calls distance_cosine_f32. It runs once per machine. Its verdict is cached in cache_dir()/usearch.json, keyed by the CPU model, the usearch version and the extension’s size in bytes, so a new usearch release or a new download probes again. A part that fails is turned off, with one warning per process:
SQLite extension off: vec_search computes distances with numpy.
HNSW index off: get_index returns an ExactIndex, which searches every vector with numpy, so ann_search, ann_neighbors, sync, rebuild_index, Database.search(ann=True) and Index.search return exact results.
usearch writes its download straight to its final path, so a second process can load a half-written file. usearch_lib downloads under a file lock in cache_dir(). When the HNSW index passes and the extension fails, the extension is downloaded once more and probed again, since a half-written file fails the same way. A failed download is not cached.
To override the probe, set LITESEARCH_USEARCH:
LITESEARCH_USEARCH=0 keeps usearch out of the process, with no probe and no warning.
LITESEARCH_USEARCH=1 uses usearch with no probe.
To probe again, delete usearch.json. LITESEARCH_CACHE moves the cache folder.
A usearch that does not import turns both parts off without a probe, since the probe would import it too. On macOS 14, import usearch raises ImportError: symbol not found in flat namespace '_nk_capabilities': every numkong wheel from 7.5 links ___sme_memset, which only macOS 15 has, and usearch 2.26 needs numkong 7.5 or later. litesearch then searches with numpy, with one warning per process. That verdict is not cached, because the import is what decides it.
A frozen app is not probed, because its sys.executable may not run python -c. It imports usearch’s HNSW index in its own process instead, and turns both parts off when that import fails.
Whether usearch’s part, ‘sql’ (the SQLite extension) or ‘index’ (HNSW), is on: LITESEARCH_USEARCH=0|1, else the probe’s verdict.
import io, sys, contextlibfrom unittest.mock import patchfrom fastcore.allimport first
# the macOS hint used to name `libsqlite3-dev`, an apt package that Homebrew does not havedef _hint(sysname):with patch.dict(sys.modules, usearch=None), patch('platform.system', return_value=sysname), contextlib.redirect_stdout(io.StringIO()) as o: usearch_fix()return o.getvalue()assert'brew install sqlite`'in _hint('Darwin') and'libsqlite3-dev'notin _hint('Darwin')assert'apt install libsqlite3-dev'in _hint('Linux')assert'pip install usearch'in _hint('Windows') and'SQLite is missing'notin _hint('Windows')
import tempfile, warnings, jsontry: import usearch; _imports =TrueexceptImportError: _imports =False# macOS 14: the probe cells below need a usearch that imports
_CRASH ="import os, signal; os.kill(os.getpid(), signal.SIGSEGV)"_HALF ="print('index', flush=True); import os, signal; os.kill(os.getpid(), signal.SIGSEGV)"@contextlib.contextmanagerdef _swap(ns, **kw):"Set the names `kw` in `ns` for the block, then put the old values back." old = {k: ns[k] for k in kw}; ns.update(kw)try: yieldfinally: ns.update(old)@contextlib.contextmanagerdef _fresh(probe=None, cache=None, **env):"A new cache folder, no verdict in this process, `probe` as the probe code, and `env` set."with patch.dict(os.environ, LITESEARCH_CACHE=cache or tempfile.mkdtemp(), **env), \ _swap(globals(), _verdict=None, _PROBE=probe or _PROBE), warnings.catch_warnings(record=True) as w:if'LITESEARCH_USEARCH'notin env: os.environ.pop('LITESEARCH_USEARCH', None) warnings.simplefilter('always')yield wdef _cached(): return json.loads((cache_dir()/'usearch.json').read_text())def _offs(w): return [str(x.message) for x in w if' is off 'instr(x.message)] # usearch's own download notice is not one
# the probe passes here, and its verdict is cached under this CPU and this usearch buildif _imports:with _fresh() as w:assert usearch_ok() and usearch_ok('index'), _verdictassertnot _offs(w), _offs(w) (k, v), = _cached().items()assert k.startswith(cpu_model()) andf'usearch {usearch.__version__}'in k and v['sql'] and v['index'], (k, v) _ok_cache =str(cache_dir())
# a probe that dies of SIGSEGV turns usearch off, warns once, and caches whyif _imports:with _fresh(_CRASH) as w:assertnot usearch_ok() andnot usearch_ok('index') andnot usearch_ok()assertlen(_offs(w)) ==1and'SIGSEGV'in _offs(w)[0] and'LITESEARCH_USEARCH=1'in _offs(w)[0], [str(x.message) for x in w]assert'SIGSEGV'in first(_cached().values())['why']
# the HNSW index passes and the extension crashes: the extension is fetched once more, then only it is offif _imports: _calls, _real = [], _libwith _fresh(_HALF) as w, _swap(globals(), _lib=lambda fresh=False: _calls.append(fresh) or _real()):assert usearch_ok('index') andnot usearch_ok('sql')assertTruein _calls andlen(_offs(w)) ==1and'SQLite extension is off'in _offs(w)[0], _offs(w)
# the cached verdict is reused: a probe that would crash never runsif _imports:with _fresh(_CRASH, cache=_ok_cache) as w: assert usearch_ok() and usearch_ok('index') andnot _offs(w)
# LITESEARCH_USEARCH wins both ways, without a probe or a warningwith _fresh(_CRASH, LITESEARCH_USEARCH='1') as w:assert usearch_ok() and usearch_ok('index') andnot _offs(w) andnot (cache_dir()/'usearch.json').exists()with _fresh(LITESEARCH_USEARCH='0') as w:assertnot usearch_ok() andnot usearch_ok('index') andnot _offs(w) andnot (cache_dir()/'usearch.json').exists()
# a usearch that does not import, as on macOS 14, turns both parts off without running the probedef _noimp(fresh=False): raiseImportError("symbol not found in flat namespace '_nk_capabilities'")with _fresh(_CRASH) as w, _swap(globals(), _lib=_noimp):assertnot usearch_ok() andnot usearch_ok('index') andnot usearch_ok()assertlen(_offs(w)) ==1and'does not import'in _offs(w)[0] and'_nk_capabilities'in _offs(w)[0], _offs(w)assert'SIGSEGV'notin _offs(w)[0] andnot (cache_dir()/'usearch.json').exists()
# a frozen app imports usearch in its own process: a failed import turns it off, a good one needs no probewith _fresh(_CRASH) as w, patch.object(sys, 'frozen', True, create=True), patch.dict(sys.modules, {'usearch.index': None}):assertnot usearch_ok() andnot usearch_ok('index') andlen(_offs(w)) ==1and'does not import'in _offs(w)[0], _offs(w)if _imports:with _fresh(_CRASH) as w, patch.object(sys, 'frozen', True, create=True):assert usearch_ok() and usearch_ok('index') andnot _offs(w)
# the probe child does not get the terminal: run from a process whose stdin is a pty, it sees no tty and leaves the typed input aloneimport pty, termios, subprocess_m, _s = pty.openpty()_t = termios.tcgetattr(_s); _t[3] &=~termios.ECHO; termios.tcsetattr(_s, termios.TCSANOW, _t)os.write(_m, b'x\n')_parent ="""import sys, litesearch.postfix as pfpf._PROBE = "import os; print(os.isatty(0))"import subprocess; _run = subprocess.rundef run(*a, **kw): r = _run(*a, **kw); print(r.stdout.strip()); return rpf.subprocess.run = runpf._probe()print(repr(sys.stdin.readline()))"""_r = subprocess.run([sys.executable, '-c', _parent], stdin=_s, capture_output=True, text=True, timeout=60)os.close(_s); os.close(_m)assert _r.stdout.split('\n')[:2] == ['False', repr('x\n')], (_r.stdout, _r.stderr)
# end to end, with a usearch that fails to import first on the path: litesearch imports, stores and searches with numpy_BROKEN ="""import warnings, numpy as npwith warnings.catch_warnings(record=True) as w: warnings.simplefilter('always') from litesearch import database V = np.random.default_rng(0).normal(size=(20, 8)).astype(np.float16) d = database(); s = d.get_store('b', hash=True, ann=True) s.sync([dict(content=f'doc {i}', embedding=v.tobytes()) for i, v in enumerate(V)], embed=False) q = V[3].tobytes() assert s.vec_search(q, ['content'], limit=1)[0]['content'] == 'doc 3' assert s.ann_search(q, ['content'], limit=1)[0]['content'] == 'doc 3' assert type(d.get_index('b')).__name__ == 'ExactIndex'offs = [str(x.message) for x in w if ' is off ' in str(x.message)]assert len(offs) == 1 and '_nk_capabilities' in offs[0], offsprint('ok')"""with tempfile.TemporaryDirectory() as t: (Path(t)/'usearch').mkdir() (Path(t)/'usearch'/'__init__.py').write_text("raise ImportError(\"dlopen(compiled.so): symbol not found in flat namespace '_nk_capabilities'\")") env = {k: v for k, v in os.environ.items() if k !='LITESEARCH_USEARCH'} r = subprocess.run([sys.executable, '-c', _BROKEN], capture_output=True, text=True, env={**env, 'PYTHONPATH': t, 'LITESEARCH_CACHE': t})assert r.stdout.strip() =='ok', r.stdout + r.stderr