Metadata-Version: 2.4
Name: litesearch
Version: 0.0.36
Summary: search through files with fts5, vectors and get reranked results. Fast
Project-URL: Repository, https://github.com/Karthik777/litesearch
Project-URL: Documentation, https://Karthik777.github.io/litesearch
Author-email: 71293 <karthik.rajgopal@hotmail.com>
License: Apache-2.0
License-File: LICENSE
Keywords: document search,fts+vectors,nbdev,semantic search,text search,vector search
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Natural Language :: English
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Requires-Python: >=3.10
Requires-Dist: chonkie>=1.6.0
Requires-Dist: codesigs>=0.0.2
Requires-Dist: fastlite>=0.2.4
Requires-Dist: flashrank>=0.2.10
Requires-Dist: liteparse>=2.6.0
Requires-Dist: model2vec>=0.7.0
Requires-Dist: notebook>=7.5.4
Requires-Dist: onnx>=1.22.0
Requires-Dist: onnxruntime>=1.24.3
Requires-Dist: pandas>=2.3.3
Requires-Dist: pdf-oxide>=0.3.17
Requires-Dist: pillow>=12.1.1
Requires-Dist: tokenizers>=0.22.2
Requires-Dist: usearch>=2.23.0
Requires-Dist: yake>=0.7.3
Provides-Extra: eval
Requires-Dist: datasets>=5.0.0; extra == 'eval'
Requires-Dist: rishi>=0.0.2; extra == 'eval'
Description-Content-Type: text/markdown

# litesearch


<!-- WARNING: THIS FILE WAS AUTOGENERATED! DO NOT EDIT! -->

> **NB** Reading this on GitHub? The formatted [documentation](https://Karthik777.github.io/litesearch/) is nicer.

litesearch stores and searches documents in a single SQLite database. It combines FTS5 keyword search with SIMD vector similarity (via usearch), then merges the ranked lists using Reciprocal Rank Fusion. No server, no new infrastructure.

| Module | What you get |
|----|----|
| `litesearch` (core) | [`database()`](https://Karthik777.github.io/litesearch/core.html#database), `get_store()`, `db.search()`, [`rrf_merge()`](https://Karthik777.github.io/litesearch/core.html#rrf_merge), `vec_search()` `ann_search()` |
| `litesearch.data` | PDF extraction, file parsing ([`file_parse`](https://Karthik777.github.io/litesearch/data.html#file_parse)), code indexing ([`pkg2chunks`](https://Karthik777.github.io/litesearch/data.html#pkg2chunks), [`dir2chunks`](https://Karthik777.github.io/litesearch/data.html#dir2chunks)), FTS query preprocessing |
| `litesearch.utils` | ONNX text, image, and multimodal encoders ([`FastEncode`](https://Karthik777.github.io/litesearch/utils.html#fastencode), [`FastEncodeImage`](https://Karthik777.github.io/litesearch/utils.html#fastencodeimage), [`FastEncodeMultimodal`](https://Karthik777.github.io/litesearch/utils.html#fastencodemultimodal)) |

## Install

``` python
# usearch SQLite extensions are configured automatically on first import
# (macOS needs one extra step — see litesearch.postfix)
!uv add litesearch
```

    error: Requirement name `litesearch` matches project name `litesearch`, but self-dependencies are not permitted without the `--dev` or `--optional` flags. If your project name (`litesearch`) is shadowing that of a third-party dependency, consider renaming the project.

## Quick Start

Search your documents in eight lines of code:

``` python
db    = database()          # SQLite + usearch SIMD extensions loaded
store = db.get_store()      # table with FTS5 index + embedding column
enc = static_retrieval_embedder()
texts = ["attention mechanisms in neural networks",
         "transformer architecture for sequence modelling",
         "stochastic gradient descent and learning rate schedules",
         "positional encoding and token embeddings",
         "dropout regularisation reduces overfitting",]
embs  = enc.encode(texts)   # float32, shape (3, 512)
store.insert_all([dict(content=t, embedding=e.ravel().tobytes()) for t, e in zip(texts, embs)])

q = "self-attention mechanism"
db.search(q, enc.encode([q]).ravel().tobytes(), columns=['id','content'], dtype=np.float32)
```

    [{'rowid': 1,
      'id': 1,
      'content': 'attention mechanisms in neural networks',
      '_dist': 0.5670621395111084,
      '_rrf_score': 0.016666666666666666},
     {'rowid': 3,
      'id': 3,
      'content': 'stochastic gradient descent and learning rate schedules',
      '_dist': 0.8964616060256958,
      '_rrf_score': 0.01639344262295082},
     {'rowid': 4,
      'id': 4,
      'content': 'positional encoding and token embeddings',
      '_dist': 0.9507941007614136,
      '_rrf_score': 0.016129032258064516},
     {'rowid': 2,
      'id': 2,
      'content': 'transformer architecture for sequence modelling',
      '_dist': 0.9885404109954834,
      '_rrf_score': 0.015873015873015872},
     {'rowid': 5,
      'id': 5,
      'content': 'dropout regularisation reduces overfitting',
      '_dist': 1.0190094709396362,
      '_rrf_score': 0.015625}]

## Core API

### [`database()`](https://Karthik777.github.io/litesearch/core.html#database) — SQLite + SIMD

[`database()`](https://Karthik777.github.io/litesearch/core.html#database) returns a [fastlite](https://fastlite.answer.ai/) `Database` patched with usearch’s SIMD distance functions. Pass a file path for persistence; omit it for an in-memory store.

The usearch extension adds SIMD-accelerated distance functions directly into SQL. Four metrics are available: `cosine`, `sqeuclidean`, `inner`, and `divergence`. All variants support `f32`, `f16`, `f64`, and `i8` suffixes.

``` python
db = database()
vecs = dict(
    v1=np.ones((100,),  dtype=np.float32).tobytes(),   # ones
    v2=np.zeros((100,), dtype=np.float32).tobytes(),   # zeros
)
def dist_q(metric):
    return db.q(f'select distance_{metric}_f32(:v1,:v2) as {metric}', vecs)
print('comparing 1s and 0s', '\n---------------------')
for fn in ['sqeuclidean', 'divergence', 'inner', 'cosine']: print(dist_q(fn))
```

    comparing 1s and 0s 
    ---------------------
    [{'sqeuclidean': 100.0}]
    [{'divergence': 34.657352447509766}]
    [{'inner': 1.0}]
    [{'cosine': 1.0}]

> Cosine distance between v1 (ones) and v3 (0.25s) is **0.0** — they point in the same direction. Both `inner` and `divergence` are also available for different retrieval trade-offs.

### `get_store()` — FTS5 + Embedding Table

`db.get_store()` creates (or opens) a table with a `content` TEXT column, an `embedding` BLOB column, a JSON `metadata` column, and an FTS5 full-text index that stays in sync automatically via triggers.

``` python
store = db.get_store()   # idempotent — safe to call multiple times
store.schema
```

    'CREATE TABLE [store] (\n   [content] TEXT NOT NULL,\n   [embedding] BLOB,\n   [metadata] TEXT,\n   [uploaded_at] FLOAT DEFAULT CURRENT_TIMESTAMP,\n   [id] INTEGER PRIMARY KEY\n)'

Pass `hash=True` to use a **content-addressed id** (SHA-1 of the content). Useful for code search and deduplication — re-inserting the same content is a no-op:

``` python
code_store = db.get_store(name='code', hash=True)
code_store.insert_all([{'content':v} for v in ('hello world', 'hi there', 'goodbye now')], upsert=True, hash_id='id')
code_store(select='id,content')
```

    [{'id': '672a7555558b556be985cf2e48f650307a6f74d8', 'content': 'hello world'},
     {'id': 'bb979740c5bc3904c4011ecaa5627c33080b119a', 'content': 'hi there'},
     {'id': '2a432c1def89853eba5c30b06a5d5826f6af7ae1', 'content': 'goodbye now'}]

### `db.search()` — Hybrid FTS + Vector with RRF

`db.search()` runs **both** an FTS5 keyword query and a vector similarity search, then merges the ranked lists with Reciprocal Rank Fusion. Documents that appear in *both* lists get a score boost — the best of both worlds. `rrf` is turned on by default; pass `rrf=False` to see the raw FTS and vector legs separately.

``` python
q='attention mechanism'
db.search(q, enc.encode([q]).ravel().tobytes(), columns=['id','content'], dtype=np.float32, rrf=False)
```

    {'fts': [{'id': 1,
       'content': 'attention mechanisms in neural networks',
       'rank': -2.232348948909978}],
     'vec': [{'id': 1,
       'content': 'attention mechanisms in neural networks',
       '_dist': 0.49074459075927734},
      {'id': 3,
       'content': 'stochastic gradient descent and learning rate schedules',
       '_dist': 0.9048988223075867},
      {'id': 4,
       'content': 'positional encoding and token embeddings',
       '_dist': 0.9850721955299377},
      {'id': 5,
       'content': 'dropout regularisation reduces overfitting',
       '_dist': 1.0058910846710205},
      {'id': 2,
       'content': 'transformer architecture for sequence modelling',
       '_dist': 1.010542631149292}]}

> **Tip — dtype matters.** Always pass the same `dtype` used when encoding. `model2vec` and most ONNX models return `float32`; pass `dtype=np.float32`. The default is `float16` (matches [`FastEncode`](https://Karthik777.github.io/litesearch/utils.html#fastencode)).

> **Tip — custom schemas.** `get_store()` is a convenience. For custom schemas, call `db.t['my_table'].vec_search(emb, ...)` and `rrf_merge(fts_results, vec_results)` directly.

### ANN store — HNSW for large corpora

The default vector search is an exact brute-force scan (`distance_*` over every row) — great up to a few thousand rows. Past that, pass `ann=True` to `get_store()` to also maintain a [usearch](https://github.com/unum-cloud/usearch) **HNSW** index as a rebuildable sidecar file. The SQLite table stays the source of truth (content + embedding blob + FTS5); the index is a pure accelerator you can rebuild anytime with `store.rebuild_index()`.

- `store.sync(content, key_col=..., emb_fn=...)` — smart hash-diff update: deletes stale rows, embeds + upserts changed ones, and mirrors both into the index.
- `db.search(..., ann=True)` — uses the HNSW index for the vector leg instead of the brute-force scan.

Below we index 8,000 code snippets with `static_code_embedder` (a fast `model2vec` static embedder) and compare the two vector legs.

``` python
import time

emb = static_code_embedder()                    # model2vec potion-code-16M — 256-dim float16
de, qe = doc_encoder(emb), query_encoder(emb)   # -> emb_fn: list[str] -> vectors

adb   = database()
store = adb.get_store('code', hash=True, ann=True)   # dtype defaults to float16, matching the embedder

# 8,000 code snippets; sync embeds, upserts, and builds the HNSW index in one call
docs = [dict(content=f'def func_{i}(x, y={i}): "helper {i%97}"; return x*{i} + y - {i%13}') for i in range(8000)]
t = time.perf_counter(); stats = store.sync(docs, emb_fn=de)
print(f'sync {stats} in {time.perf_counter()-t:.2f}s — index size {adb.get_index("code").size}')
# sync {'changed': 8000, 'same': 0, 'removed': 0} in 0.57s — index size 8000
```

    sync {'changed': 8000, 'same': 0, 'removed': 0} in 0.64s — index size 8000

``` python
q  = 'function that multiplies its input by a constant and adds an offset'
qv = qe([q])[0].tobytes()

def bench(fn, reps=20):
    t = time.perf_counter()
    for _ in range(reps): fn()
    return (time.perf_counter()-t)/reps*1000   # ms per call

bf = bench(lambda: store.vec_search(qv, columns=['content'], limit=10))   # exact brute-force scan
an = bench(lambda: store.ann_search(qv, columns=['content'], limit=10))   # HNSW
print(f'brute-force {bf:.3f} ms   ann {an:.3f} ms   speedup {bf/an:.1f}x')
# brute-force 1.787 ms   ann 0.100 ms   speedup 17.8x

# HNSW is approximate but recall is high — top-10 matches the exact scan here
exact = {r['content'] for r in store.vec_search(qv, columns=['content'], limit=10)}
approx = {r['content'] for r in store.ann_search(qv, columns=['content'], limit=10)}
print(f'recall@10: {len(exact & approx)}/10')
# recall@10: 10/10
```

    brute-force 1.802 ms   ann 0.123 ms   speedup 14.6x
    recall@10: 9/10

``` python
# use it end-to-end via db.search — ann=True swaps in the HNSW index for the vector leg
adb.search(q, qv, columns=['content'], table_name='code', ann=True, limit=3)
```

    [{'content': 'def func_3601(x, y=3601): "helper 12"; return x*3601 + y - 0',
      'rowid': 3602,
      '_dist': 0.7722985148429871,
      '_rrf_score': 0.016666666666666666},
     {'content': 'def func_3605(x, y=3605): "helper 16"; return x*3605 + y - 4',
      'rowid': 3606,
      '_dist': 0.7867892384529114,
      '_rrf_score': 0.01639344262295082},
     {'content': 'def func_3608(x, y=3608): "helper 19"; return x*3608 + y - 7',
      'rowid': 3609,
      '_dist': 0.7960468530654907,
      '_rrf_score': 0.016129032258064516}]

### PDF Extraction

`litesearch.data` patches `pdf_oxide.PdfDocument` with bulk page-extraction methods. All methods take optional `st` / `end` page indices and return a fastcore `L` list:

``` python
doc = PdfDocument('pdfs/attention_is_all_you_need.pdf')
```

``` python
print(f'{doc.page_count()} pages, {len(doc.pdf_links())} links')
```

    15 pages, 18 links

[`pdf_parse`](https://Karthik777.github.io/litesearch/data.html#pdf_parse) parses a given pdf path or PdfDocument or bytes into a list of text per page. it has a smart ocr check and uses liteparse in the background if it needs to.

``` python
# markdown export — headings and tables are detected automatically
md = pdf_parse(doc)
print(f'Page 1 (markdown):\n{md[0][:400]}')
```

    Page 1 (markdown):
    Provided proper attribution is provided, Google hereby grants permission to reproduce the tables and figures in this paper solely for use in journalistic or scholarly works.

    ## Attention Is All You Need

    **Ashish** **Vaswani**

    **Noam** **Shazeer**

    **Niki** **Parmar**

    **Jakob** **Uszkoreit** Google Brain

    Google Brain

    Google Research Google Research [avaswani@google.com](mailto:avaswani@google

`doc.pdf_chunks()` wraps [`pdf_parse`](https://Karthik777.github.io/litesearch/data.html#pdf_parse) + chonkie’s `FastChunker`(or pass another chunker) into `(page, chunk_idx, text)` triples — the direct input for [`encode_pdf_texts`](https://Karthik777.github.io/litesearch/utils.html#encode_pdf_texts):

``` python
chunks = doc.pdf_chunks()
print(f'{len(chunks)} chunks from {doc.page_count()} pages')
# 31 chunks from 15 pages

# (page, chunk_idx, text) triples — direct input for encode_pdf_texts
pg, ci, text = chunks[0]
print(f'page {pg}, chunk {ci}: {text[:80]}...')
```

    16 chunks from 15 pages
    page 0, chunk 0: Provided proper attribution is provided, Google hereby grants permission to repr...

### Code & File Ingestion

[`pyparse`](https://Karthik777.github.io/litesearch/data.html#pyparse) splits a Python file or string into top-level code chunks (functions, classes, assignments) with source location metadata — ready to insert into a store:

``` python
txt = """
from fastcore.all import *
a=1
class SomeClass:
    def __init__(self,x): store_attr()
    def method(self): return self.x + a
"""
pyparse(code=txt)
```

    [{'content': 'class SomeClass:\n    def __init__(self,x): store_attr()\n    def method(self): return self.x + a', 'metadata': {'path': 'None', 'uploaded_at': None, 'name': 'SomeClass', 'lang': '.py', 'type': 'ClassDef', 'lineno': 4, 'end_lineno': 6}}]

[`pkg2chunks`](https://Karthik777.github.io/litesearch/data.html#pkg2chunks) indexes an **entire installed package** in one call — great for building a semantic code-search store over your dependencies:

``` python
chunks = pkg2chunks('fastlite')
print(f'{len(chunks)} chunks from fastlite')
chunks.filter(lambda d: d['metadata']['type'] == 'FunctionDef')[0]
```

    47 chunks from fastlite

    {'content': 'def t(self:Database): return _TablesGetter(self)',
     'metadata': {'path': '/Users/71293/code/litesearch/.venv/lib/python3.13/site-packages/fastlite/core.py',
      'uploaded_at': 1773452878.5692947,
      'name': 't',
      'lang': '.py',
      'type': 'FunctionDef',
      'lineno': 44,
      'end_lineno': 44,
      'package': 'fastlite',
      'version': '0.2.4'}}

[`file_parse`](https://Karthik777.github.io/litesearch/data.html#file_parse) is the single entry point for any file type — Python, Jupyter notebooks, PDF, Markdown, plain text, and compiled-language source files (JS/TS, Go, Java, Rust…). All return the same `{content, metadata}` dicts:

``` python
# Python → AST-parsed functions and classes
py=file_parse(repo_root()/'litesearch/core.py')[:2]

# Jupyter notebook → one dict per cell
nb=file_parse(repo_root()/'nbs/01_core.ipynb')[:2]

# PDF → markdown-chunked text (via pdf_chunks)
pdf=file_parse(Path('pdfs/attention_is_all_you_need.pdf'))[:2]
print(py[0], '\n------------\n', nb[0], '\n---------------\n', pdf[0][:400])
```

    Dictionary used where Stream expected, treating as empty stream
    Dictionary used where Stream expected, treating as empty stream
    Dictionary used where Stream expected, treating as empty stream
    Dictionary used where Stream expected, treating as empty stream

    {'content': "def _dtype_suffix(dtype=np.float16): return _dtype_suffixes.get(dtype, 'f32')", 'metadata': {'path': '/Users/71293/code/litesearch/litesearch/core.py', 'uploaded_at': 1784347629.5078557, 'name': '_dtype_suffix', 'lang': '.py', 'type': 'FunctionDef', 'lineno': 15, 'end_lineno': 15}} 
    ------------
     {'content': '---\ndescription: Building blocks for litesearch\noutput-file: core.html\ntitle: core\n\n---\n\n', 'metadata': {'path': '/Users/71293/code/litesearch/nbs/01_core.ipynb', 'uploaded_at': 1784345054.1480775, 'lang': '.ipynb', 'type': 'raw'}} 
    ---------------
     {'content': 'Provided proper attribution is provided, Google hereby grants permission to reproduce the tables and figures in this paper solely for use in journalistic or scholarly works.\n\n## Attention Is All You Need\n\n**Ashish** **Vaswani**\n\n**Noam** **Shazeer**\n\n**Niki** **Parmar**\n\n**Jakob** **Uszkoreit** Google Brain\n\nGoogle Brain\n\nGoogle Research Google Research [avaswani@google.com](mailto:avaswani@google.com) [noam@google.com](mailto:noam@google.com) [nikip@google.com](mailto:nikip@google.com) [usz@google.com](mailto:usz@google.com)\n\n**Llion** **Jones**\n\n**Aidan** **N.** **Gomez**∗ †\n\n**Łukasz** **Kaiser** Google Research\n\nUniversity of Toronto\n\nGoogle Brain [lukaszkaiser@google.com](mailto:lukaszkaiser@google.com)\n\n[llion@google.com](mailto:llion@google.com) [aidan@cs.toronto.edu](mailto:aidan@cs.toronto.edu)\n\n**Illia** **Polosukhin**∗ ‡ [illia.polosukhin@gmail.com](mailto:illia.polosukhin@gmail.com)\n\n#### Abstract\n\nThe dominant sequence transduction models are based on complex recurrent or convolutional neural networks that include an encoder and a decoder. The best performing models also connect the encoder and decoder through an attention mechanism. We propose a new simple network architecture, the Transformer, based solely on attention mechanisms, dispensing with recurrence and convolutions entirely. Experiments on two machine translation tasks show these models to be superior in quality while being more parallelizable and requiring significantly less time to train. Our model achieves 28.4 BLEU on the WMT 2014 English-to-German translation task, improving over the existing best results, including ensembles, by over 2 BLEU. On the WMT 2014 English-to-French translation task, our model establishes a new single-model state-of-the-art BLEU score of 41.8 after training for 3.5 days on eight GPUs, a small fraction of the training costs of the best models from the literature. We show that the Transformer generalizes well to\n\narXiv:1706.03762v7 [cs.CL] 2 Aug 2023  other tasks by applying it successfully to English constituency parsing both with large and limited training data.\n\nEqual contribution. Listing order is random. Jakob proposed replacing RNNs with self-attention and started the effort to evaluate this idea. Ashish, with Illia, designed and implemented the first Transformer models and has been crucially involved in every aspect of this work. Noam proposed scaled dot-product attention, multi-head attention and the parameter-free position representation and became the other person involved in nearly every detail. Niki designed, implemented, tuned and evaluated countless model variants in our original codebase and tensor2tensor. Llion also experimented with novel model variants, was responsible for our initial codebase, and efficient inference and visualizations. Lukasz and Aidan spent countless long days designing various parts of and implementing tensor2tensor, replacing our earlier codebase, greatly improving results and massively accelerating our research. Work performed while at Google Brain. Work performed while at Google Research.\n\n31st Conference on Neural Information Processing Systems (NIPS 2017), Long Beach, CA, USA.\n', 'metadata': {'path': Path('pdfs/attention_is_all_you_need.pdf'), 'uploaded_at': 1773199216.9632287, 'lang': '.pdf'}}

[`dir2chunks`](https://Karthik777.github.io/litesearch/data.html#dir2chunks) indexes every file in a directory tree — analogous to [`pkg2chunks`](https://Karthik777.github.io/litesearch/data.html#pkg2chunks) but for arbitrary directories rather than installed packages:

``` python
# Index all Python source files in a directory
chunks = dir2chunks(repo_root()/'litesearch', types='py')
print(f'{len(chunks)} chunks from litesearch/')

# Mix formats: notebooks, markdown, PDFs
chunks = dir2chunks(repo_root()/'nbs', types='ipynb,md,pdf')
print(f'{len(chunks)} chunks from nbs/')
```

    Dictionary used where Stream expected, treating as empty stream
    Dictionary used where Stream expected, treating as empty stream
    Dictionary used where Stream expected, treating as empty stream
    Dictionary used where Stream expected, treating as empty stream

    63 chunks from litesearch/
    2910 chunks from nbs/

## `litesearch.utils`

### [`FastEncode`](https://Karthik777.github.io/litesearch/utils.html#fastencode) — ONNX Text Encoder

[`FastEncode`](https://Karthik777.github.io/litesearch/utils.html#fastencode) wraps any ONNX model from HuggingFace Hub. It handles tokenisation, batching, optional parallel thread-pool execution, and runtime int8 quantization — all without PyTorch or Transformers.

| Config | Model | Dim | Notes |
|----|----|----|----|
| `embedding_gemma` (default) | `onnx-community/embeddinggemma-300m-ONNX` | 768 | Strong retrieval, ~300M params |
| `modernbert` | `nomic-ai/modernbert-embed-base` | 768 | BERT-style, fast |
| `nomic_text_v15` | `nomic-ai/nomic-embed-text-v1.5` | 768 | Shares embedding space with `nomic_vision_v15` |

`encode_document` and `encode_query` apply the model’s prompt templates automatically.

``` python
texts = [
    'Attention is all you need',
    'The transformer architecture uses self-attention',
    'BERT pretrains on masked language modeling',
    'GPT uses autoregressive generation',
]
```

``` python
# Default model — downloads once, cached
enc, enc_fast, enc_q = FastEncode(),FastEncode(batch_size=2, parallel=2),FastEncode(quantize='int8')
```

``` python
doc_embs=enc.encode_document(texts); doc_embs
```

    array([[ 0.06824 ,  0.015434,  0.005802, ..., -0.04044 ,  0.02196 ,
             0.03586 ],
           [ 0.02335 , -0.0157  ,  0.0168  , ..., -0.08057 , -0.03833 ,
             0.0258  ],
           [ 0.03894 ,  0.01215 ,  0.00667 , ..., -0.06915 , -0.00802 ,
             0.0077  ],
           [ 0.02185 ,  0.01064 , -0.01266 , ..., -0.0686  , -0.0412  ,
             0.02237 ]], shape=(4, 768), dtype=float16)

``` python
q_emb = enc.encode_query(['what paper introduced transformers?'])
print('doc shape:', doc_embs.shape, 'dtype:', doc_embs.dtype)  # (4, 768) float16
```

    doc shape: (4, 768) dtype: float16

``` python
# Batching + parallel thread-pool
embs = enc_fast.encode_document(texts); embs
```

<style>
    progress { appearance: none; border: none; border-radius: 4px; width: 300px;
        height: 20px; vertical-align: middle; background: #e0e0e0; }
&#10;    progress::-webkit-progress-bar { background: #e0e0e0; border-radius: 4px; }
    progress::-webkit-progress-value { background: #2196F3; border-radius: 4px; }
    progress::-moz-progress-bar { background: #2196F3; border-radius: 4px; }
&#10;    progress:not([value]) {
        background: repeating-linear-gradient(45deg, #7e7e7e, #7e7e7e 10px, #5c5c5c 10px, #5c5c5c 20px); }
&#10;    progress.progress-bar-interrupted::-webkit-progress-value { background: #F44336; }
    progress.progress-bar-interrupted::-moz-progress-value { background: #F44336; }
    progress.progress-bar-interrupted::-webkit-progress-bar { background: #F44336; }
    progress.progress-bar-interrupted::-moz-progress-bar { background: #F44336; }
    progress.progress-bar-interrupted { background: #F44336; }    
&#10;    table.fastprogress { border-collapse: collapse; margin: 1em 0; font-size: 0.9em; }
    table.fastprogress th, table.fastprogress td { padding: 8px 12px; border: 1px solid #ddd; text-align: left; }
    table.fastprogress thead tr { background: #f8f9fa; font-weight: bold; }
    table.fastprogress tbody tr:nth-of-type(even) { background: #f8f9fa; }
</style>

    array([[ 0.06824 ,  0.015434,  0.005802, ..., -0.04044 ,  0.02196 ,
             0.03586 ],
           [ 0.02335 , -0.0157  ,  0.0168  , ..., -0.08057 , -0.03833 ,
             0.0258  ],
           [ 0.03894 ,  0.01215 ,  0.00667 , ..., -0.06915 , -0.00802 ,
             0.0077  ],
           [ 0.02185 ,  0.01064 , -0.01266 , ..., -0.0686  , -0.0412  ,
             0.02237 ]], shape=(4, 768), dtype=float16)

``` python
# Runtime int8 quantization — creates model_int8.onnx on first run, reused after
enc_q.encode_document(texts)
```

    array([[ 0.09485 ,  0.01581 ,  0.005184, ..., -0.05066 ,  0.01775 ,
             0.02722 ],
           [ 0.02164 , -0.01174 ,  0.0182  , ..., -0.0741  , -0.03018 ,
             0.02805 ],
           [ 0.03583 ,  0.012405,  0.005947, ..., -0.05457 , -0.00577 ,
             0.003704],
           [ 0.02808 ,  0.02591 , -0.009514, ..., -0.05954 , -0.04718 ,
             0.01416 ]], shape=(4, 768), dtype=float16)

### [`FastEncodeImage`](https://Karthik777.github.io/litesearch/utils.html#fastencodeimage) — ONNX Image Encoder

[`FastEncodeImage`](https://Karthik777.github.io/litesearch/utils.html#fastencodeimage) encodes images with CLIP-style ONNX vision models. No Transformers dependency — preprocessing (resize → normalise → CHW) is done with PIL + NumPy using config stored in the model dict.

| Config | Model | Dim | Notes |
|----|----|----|----|
| `nomic_vision_v15` (default) | `nomic-ai/nomic-embed-vision-v1.5` | 768 | Same space as `nomic_text_v15` |
| `clip_vit_b32` | `Qdrant/clip-ViT-B-32-vision` | 512 | Classic CLIP |

Accepts PIL Images, file paths, or raw bytes — any mix.

### [`FastEncodeMultimodal`](https://Karthik777.github.io/litesearch/utils.html#fastencodemultimodal) — Cross-Modal Image + Text Search

[`FastEncodeMultimodal`](https://Karthik777.github.io/litesearch/utils.html#fastencodemultimodal) wraps a model repo that ships both text and vision ONNX encoders in a single shared embedding space — a text query can retrieve images directly. Below: index *Attention Is All You Need* (text chunks + figures) then search for `'attention mechanism diagram'`.

**Unified model** — `siglip2_so400m` (~800 MB, one download):

``` python
import json, base64, io
from PIL import Image
from IPython.display import display
```

``` python
enc = FastEncodeMultimodal(siglip2_so400m)   # single unified model, ~800 MB, cached on first run
doc = PdfDocument('pdfs/attention_is_all_you_need.pdf')
db  = database()
ts, ims = db.get_store('texts'), db.get_store('images')

for pg, ci, chunk, emb in encode_pdf_texts(doc, enc.text):
    ts.insert(dict(content=chunk, embedding=emb.tobytes(), metadata=json.dumps({'page': pg})))
for pg, img_bytes, emb in encode_pdf_images(doc, enc.vision):
    ims.insert(dict(content=f'page_{pg}', embedding=emb.tobytes(),
                    metadata=json.dumps({'page': pg, 'data': base64.b64encode(img_bytes).decode()})))

q = 'attention mechanism diagram'
q_emb = enc.text.encode([q])[0].tobytes()
txt_r = ts.db.search(pre(q), q_emb, table_name='texts', columns=['content']) or []
img_r = ims.vec_search(q_emb)
for r in rrf_merge(txt_r, img_r)[:6]:
    print(f"rrf={r['_rrf_score']:.4f}  {r['content'][:70]}")
    meta = json.loads(r.get('metadata', '{}'))
    if 'data' in meta:
        display(Image.open(io.BytesIO(base64.b64decode(meta['data']))).resize((200, 150)))
```

    /Users/71293/code/litesearch/.venv/lib/python3.13/site-packages/tqdm/auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html
      from .autonotebook import tqdm as notebook_tqdm
    Fetching 36 files: 100%|██████████| 36/36 [06:48<00:00, 11.35s/it]
    Dictionary used where Stream expected, treating as empty stream
    Dictionary used where Stream expected, treating as empty stream
    Dictionary used where Stream expected, treating as empty stream
    Dictionary used where Stream expected, treating as empty stream

    rrf=0.0167  Table 1: Maximum path lengths, per-layer complexity and minimum number
    rrf=0.0167  page_3

![](index_files/figure-commonmark/cell-26-output-3.png)

    rrf=0.0164  Table 4: The Transformer generalizes well to English constituency pars
    rrf=0.0164  page_2

![](index_files/figure-commonmark/cell-26-output-5.png)

    rrf=0.0161  Table 3: Variations on the Transformer architecture. Unlisted values a
    rrf=0.0161  page_3

![](index_files/figure-commonmark/cell-26-output-7.png)

**Paired models** — `nomic_text_v15` + `nomic_vision_v15` share the same 768-dim space; use [`FastEncode`](https://Karthik777.github.io/litesearch/utils.html#fastencode) and [`FastEncodeImage`](https://Karthik777.github.io/litesearch/utils.html#fastencodeimage) separately:

``` python
enc_text = FastEncode(nomic_text_v15)
enc_img  = FastEncodeImage(nomic_vision_v15)
db2  = database()
ts2, ims2 = db2.get_store('texts'), db2.get_store('images')

for pg, ci, chunk, emb in encode_pdf_texts(doc, enc_text):
    ts2.insert(dict(content=chunk, embedding=emb.tobytes(), metadata=json.dumps({'page': pg})))
for pg, img_bytes, emb in encode_pdf_images(doc, enc_img):
    ims2.insert(dict(content=f'page_{pg}', embedding=emb.tobytes(),
                     metadata=json.dumps({'page': pg, 'data': base64.b64encode(img_bytes).decode()})))

q_emb2 = enc_text.encode([q])[0].tobytes()
txt_r2 = ts2.db.search(pre(q), q_emb2, table_name='texts', columns=['content']) or []
img_r2 = ims2.vec_search(q_emb2)
for r in rrf_merge(txt_r2, img_r2)[:6]:
    print(f"rrf={r['_rrf_score']:.4f}  {r['content'][:70]}")
    meta = json.loads(r.get('metadata', '{}'))
    if 'data' in meta:
        display(Image.open(io.BytesIO(base64.b64decode(meta['data']))).resize((200, 150)))
```

    rrf=0.0167  Self-attention, sometimes called intra-attention is an attention mecha
    rrf=0.0167  page_3

![](index_files/figure-commonmark/cell-27-output-2.png)

    rrf=0.0164  Attention mechanisms have become an integral part of compelling sequen
    rrf=0.0164  page_2

![](index_files/figure-commonmark/cell-27-output-4.png)

    rrf=0.0161  2,[19]. Inall but a few cases27],[ however, such attention mechanisms
    rrf=0.0161  page_3

![](index_files/figure-commonmark/cell-27-output-6.png)

    rrf=0.0167  Self-attention, sometimes called intra-attention is an attention mecha
    rrf=0.0167  page_3

![](index_files/figure-commonmark/cell-27-output-8.png)

    rrf=0.0164  Attention mechanisms have become an integral part of compelling sequen
    rrf=0.0164  page_2

![](index_files/figure-commonmark/cell-27-output-10.png)

    rrf=0.0161  2,[19]. Inall but a few cases27],[ however, such attention mechanisms
    rrf=0.0161  page_3

![](index_files/figure-commonmark/cell-27-output-12.png)

## Next Steps

- **[examples/01_simple_rag.ipynb](examples/01_simple_rag.ipynb)** — ingest a folder of PDFs, chunk with chonkie, rerank with FlashRank
- **[examples/02_tool_use.ipynb](examples/02_tool_use.ipynb)** — wire litesearch into an LLM tool-use loop
- **[core docs](https://Karthik777.github.io/litesearch/core.html)** — full API reference for [`database`](https://Karthik777.github.io/litesearch/core.html#database), `get_store`, `search`, [`rrf_merge`](https://Karthik777.github.io/litesearch/core.html#rrf_merge), `vec_search`
- **[data docs](https://Karthik777.github.io/litesearch/data.html)** — PDF methods, [`pyparse`](https://Karthik777.github.io/litesearch/data.html#pyparse), [`pkg2chunks`](https://Karthik777.github.io/litesearch/data.html#pkg2chunks), query preprocessing
- **[utils docs](https://Karthik777.github.io/litesearch/utils.html)** — [`FastEncode`](https://Karthik777.github.io/litesearch/utils.html#fastencode), [`download_model`](https://Karthik777.github.io/litesearch/utils.html#download_model), image tools

## Acknowledgements

A big thank you to [@yfedoseev](https://github.com/yfedoseev) for [pdf-oxide](https://github.com/yfedoseev/pdf-oxide), which powers the PDF extraction functionality in `litesearch.data`.
