postfix

Fixes for post import issues

Post-import fixes

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.


source

usearch_fix

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.


source

usearch_lib

def usearch_lib()->str:

Path of usearch’s SQLite extension without suffix, downloaded under a lock so no process loads a half-written file.


source

cpu_model

def cpu_model()->str:

The CPU’s model name: /proc/cpuinfo on Linux, sysctl on macOS, else the machine type.


source

cache_dir

def cache_dir()->pathlib.Path:

litesearch’s cache folder: LITESEARCH_CACHE, else $XDG_CACHE_HOME/litesearch, else ~/.cache/litesearch.


source

usearch_ok

def usearch_ok(
    part:str='sql'
)->bool:

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, contextlib
from unittest.mock import patch
from fastcore.all import first
# the macOS hint used to name `libsqlite3-dev`, an apt package that Homebrew does not have
def _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' not in _hint('Darwin')
assert 'apt install libsqlite3-dev' in _hint('Linux')
assert 'pip install usearch' in _hint('Windows') and 'SQLite is missing' not in _hint('Windows')
import tempfile, warnings, json
try: import usearch; _imports = True
except ImportError: _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.contextmanager
def _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: yield
    finally: ns.update(old)
@contextlib.contextmanager
def _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' not in env: os.environ.pop('LITESEARCH_USEARCH', None)
        warnings.simplefilter('always')
        yield w
def _cached(): return json.loads((cache_dir()/'usearch.json').read_text())
def _offs(w): return [str(x.message) for x in w if ' is off ' in str(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 build
if _imports:
    with _fresh() as w:
        assert usearch_ok() and usearch_ok('index'), _verdict
        assert not _offs(w), _offs(w)
        (k, v), = _cached().items()
        assert k.startswith(cpu_model()) and f'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 why
if _imports:
    with _fresh(_CRASH) as w:
        assert not usearch_ok() and not usearch_ok('index') and not usearch_ok()
        assert len(_offs(w)) == 1 and '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 off
if _imports:
    _calls, _real = [], _lib
    with _fresh(_HALF) as w, _swap(globals(), _lib=lambda fresh=False: _calls.append(fresh) or _real()):
        assert usearch_ok('index') and not usearch_ok('sql')
        assert True in _calls and len(_offs(w)) == 1 and 'SQLite extension is off' in _offs(w)[0], _offs(w)
# the cached verdict is reused: a probe that would crash never runs
if _imports:
    with _fresh(_CRASH, cache=_ok_cache) as w: assert usearch_ok() and usearch_ok('index') and not _offs(w)
# LITESEARCH_USEARCH wins both ways, without a probe or a warning
with _fresh(_CRASH, LITESEARCH_USEARCH='1') as w:
    assert usearch_ok() and usearch_ok('index') and not _offs(w) and not (cache_dir()/'usearch.json').exists()
with _fresh(LITESEARCH_USEARCH='0') as w:
    assert not usearch_ok() and not usearch_ok('index') and not _offs(w) and not (cache_dir()/'usearch.json').exists()
# a usearch that does not import, as on macOS 14, turns both parts off without running the probe
def _noimp(fresh=False): raise ImportError("symbol not found in flat namespace '_nk_capabilities'")
with _fresh(_CRASH) as w, _swap(globals(), _lib=_noimp):
    assert not usearch_ok() and not usearch_ok('index') and not usearch_ok()
    assert len(_offs(w)) == 1 and 'does not import' in _offs(w)[0] and '_nk_capabilities' in _offs(w)[0], _offs(w)
    assert 'SIGSEGV' not in _offs(w)[0] and not (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 probe
with _fresh(_CRASH) as w, patch.object(sys, 'frozen', True, create=True), patch.dict(sys.modules, {'usearch.index': None}):
    assert not usearch_ok() and not usearch_ok('index') and len(_offs(w)) == 1 and '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') and not _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 alone
import 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 pf
pf._PROBE = "import os; print(os.isatty(0))"
import subprocess; _run = subprocess.run
def run(*a, **kw): r = _run(*a, **kw); print(r.stdout.strip()); return r
pf.subprocess.run = run
pf._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 np
with 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], offs
print('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