Skip to main content

glossdex

DOI

Glossed locally, indexed by meaning, gated by evidence.

glossdex is private semantic search over a folder of photos and documents. A local model writes a short gloss for every file: a list of independent phrases saying what is in it. glossdex embeds the gloss and its phrases, and at query time it decides how many results the evidence supports. Often that is one or two results. Sometimes it is none.

Most semantic search returns a fixed top 10 whether or not anything matches. Ask a photo library for "a giraffe" and you get ten photos anyway, and a language model fed those ten passages will answer from them. glossdex returns nothing when the collection has nothing like the query. It needs no training, no click logs and no relevance labels for your collection. Nothing leaves your machine.

On the ten sample documents in examples/sample-docs:

$ glossdex search examples/sample-docs "how much is the electricity bill" --describer text-only
 1. match 0.440  words 87%  hydro-bill-june-2026.txt
      matched: Electricity bill for Alex Morgan, 42 Birch Lane.

1 item(s) within the band (score >= 0.396). Showing 1.

$ glossdex search examples/sample-docs "mortgage statement" --describer text-only
Nothing found.

Nothing shown: the best match (0.281) plus keyword evidence (0.000) is below the evidence gate (0.520).

$ glossdex search examples/sample-docs "mortgage statement" --describer text-only --top-k 3   # fixed top-k
 1. match 0.281  words 37%  hydro-bill-june-2026.txt
 2. match 0.268  lease-agreement.md
 3. match 0.257  words 37%  donation-receipt.txt

On 40 sample photos, glossdex shows nothing for "a giraffe", while a fixed top-10 shows ten unrelated photos:

glossdex returns nothing for "a giraffe"; a fixed top-10 returns ten unrelated photos

Photos: Open Images V7, CC BY 2.0; credits in docs/SCREENSHOT_CREDITS.md.

When there is an answer, it shows only the answer, and explains why:

Two results for "a little boy with a book", with each decision explained

Quick start (about 10 minutes)

You need Python 3.9+ and Ollama.

ollama pull embeddinggemma          # 622 MB, the embedding model
pip install glossdex
glossdex doctor                     # checks Ollama and the models

Documents in one minute, with no language model. Each document's own sentences become its phrases:

git clone https://github.com/miranpoor/glossdex && cd glossdex
glossdex index examples/sample-docs --describer text-only
glossdex serve examples/sample-docs --describer text-only

Your browser opens the glossdex page. Try when does my lease end, my son's doctor visit and mortgage statement (not in the folder). Move the strictness slider, or tick Compare with a fixed top-10 to see what conventional search would have shown.

Photos. Add a vision-language model. Gemma 4 E4B reads both images and text:

ollama pull gemma4:e4b                       # about 10 GB; gemma4:e2b is smaller
python examples/get_sample_images.py         # 40 CC BY 2.0 photos from Open Images
glossdex serve examples/sample-images/photos # then press "Index folder"

Describing takes a few seconds per photo on a recent GPU and longer on a CPU. Indexing is incremental: only new or changed files are described.

Your own folder: glossdex serve ~/Pictures/2025 or glossdex serve ~/Documents/scans. The index lives in a .glossdex/ folder inside the folder you index. Install glossdex[pdf] to include PDFs.

How it works

Indexing: file, local model writes a gloss, phrase parser, embed whole gloss and every phrase, multi-vector and keyword index. Query: score, noise floor, evidence gate, band anchored at the noise floor, tighten if one item clearly answers, pad short lists with near misses, result set.

Diagram source: docs/how-it-works.mmd.

  1. Gloss. A vision-language model (for photos) or a language model (for documents) lists the item's elements as short, independent phrases. Single-concept queries match one phrase strongly. The whole gloss carries the combination of elements that compound queries need.
  2. Score. Each item's semantic score blends its whole-gloss similarity with a soft maximum over its phrase similarities. A rarity-weighted keyword-coverage boost can promote an item that literally contains the query's words. It applies only to items the embedding already considers related.
  3. Decide how many. Every threshold is derived from the best semantic score, so keyword evidence can never raise a threshold:
    • Noise floor. If the best match is weaker than unrelated text usually scores, show nothing.
    • Evidence gate. A higher bar that keyword evidence can help clear: BM25 from the items that would be shown, plus a bonus if any item contains every query word. It lets a name or a document type through and stops weak, uncorroborated matches. It can permit a list but never lengthens one.
    • Band. Keep items within a ratio of the best match, measured from the noise floor rather than from zero, so that weak queries get short lists.
    • Coverage gap. If one item accounts for the query's words far better than any other, tighten the band. If several tie, leave it alone.
    • Conditional padding. Top up a one-item list only with a near miss that shares query words.

The strictness control moves the band and scales the noise floor, headroom and evidence gate together. At its loosest those three are off and only the band ratio (and any coverage-gap tightening) remains, so "show me more" reaches the ranked items.

See docs/architecture.md for the components and the adapter interfaces.

Python API

from glossdex import Glossdex

gx = Glossdex.for_folder("~/Documents/scans")   # Ollama: gemma4:e4b + embeddinggemma
gx.index()                                       # incremental

res = gx.search("insurance renewal for the Honda")
if res.found:
    for hit in res.hits:
        print(hit.title, round(hit.score, 3), hit.matched)
print(res.trace.verdict)                         # why this many results

gx.search("...", strictness=0.5)                 # lower shows more
gx.explain("...")                                # full ranking before any cutoff

Retrieval-augmented generation that can say "not found"

ctx = gx.context("When is the field trip?", max_chars=6000)
if not ctx:
    answer = "Not found in your documents."      # the model is never called
else:
    answer = llm(f"Answer from these sources, cite [n].\n\n{ctx.text}\n\nQ: ...")

The number of passages in the context is the size of the result set. It varies per question and is not a fixed k. See examples/rag_ollama.py.

Bring your own models and sources

glossdex has three adapter interfaces, all plain Python protocols:

Adapter Built in Implement
Content source FolderSource (images, .txt, .md, .pdf) items() -> Iterator[Item]
Describer OllamaDescriber, TextOnlyDescriber describe(item) -> Description
Embedder OllamaEmbedder, SentenceTransformersEmbedder embed_documents(texts), embed_query(text)
from glossdex import Glossdex, OllamaDescriber, SentenceTransformersEmbedder

gx = Glossdex.for_folder(
    "photos",
    describer=OllamaDescriber(model="gemma4:e2b"),
    embedder=SentenceTransformersEmbedder(),     # pip install "glossdex[st]"
)

Command line

Command What it does
glossdex doctor Checks that Ollama is running and the models are pulled
glossdex index FOLDER [--rebuild] Describes and embeds new or changed files, and drops deleted ones
glossdex search FOLDER "query" [--strictness 0.5] [--top-k 10] [--json] Searches, and prints the decision
glossdex explain FOLDER "query" Shows the full ranking with every score, and which items were shown
glossdex serve FOLDER Opens the local web page

Common options: --vision-model, --text-model, --embed-model, --describer text-only, --embedder st, --ollama http://host:11434.

Thresholds are per embedding model and kind of content

A similarity of 0.30 means "unrelated" for one embedding model and "a good match" for another. glossdex therefore keeps its thresholds in a profile per embedding model, not per collection. This release ships two profiles for EmbeddingGemma-300M, one for each kind of content:

Profile Used for Chosen automatically when
embeddinggemma-300m Short glosses written by a model (photos, and documents described by a model). The configuration that ships in the Memoyad app. a describer model writes the gloss (the default)
embeddinggemma-300m/passages Raw text passages indexed as written, up to about 1,500 characters each --describer text-only

Both rank the same way; the passages profile shows a shorter, more confident list (band 0.90, evidence gate 0.52, minimum 1 result). Its values were chosen once on three public collections and then tested, unchanged, on three others (see Benchmark). Pass profile= to Glossdex to override either.

With another embedding model glossdex still works, but it labels its thresholds uncalibrated and they may cut too much or too little. Profiles for other models are on the roadmap. If you need one now, see COMMERCIAL-LICENSE.md.

Limitations

  • Indexing costs a model call per item. Describing an image takes a few seconds on a laptop GPU and longer on a CPU. Pure image-embedding models (CLIP and similar) index far faster. glossdex trades indexing time for result sets that can be trusted, including empty ones.
  • The gloss bounds retrieval. If the model never writes "festival", a query about a festival must succeed on meaning alone. A better describer gives better search.
  • English vocabulary. The stopword list and the synonym classes are English. A multilingual embedder will match other languages by meaning, but the keyword evidence will not help them.
  • Exhaustive search. Scoring is a matrix product over every stored vector. That is fast up to a few hundred thousand phrases. Beyond that, put an approximate nearest-neighbour index in front and apply the result-set stages to its candidates.

Benchmark

On three public text collections from BEIR, glossdex's result sets are compared with dense (semantic), BM25 (keyword) and hybrid search showing a fixed top 5 or top 10, with a similarity threshold (alone and capped at a top k), and with Adaptive-k (Taguchi et al., EMNLP 2025). All systems use the same EmbeddingGemma-300M vectors. Every system's settings, glossdex's included, were chosen once on three other collections.

Task score: F2 of the list shown on answerable queries, and 100 for showing nothing (0 otherwise) on queries the collection cannot answer.

Test collection glossdex Strongest baseline: threshold, top 5 Ahead by (95% CI) Best fixed top-k Ahead by (95% CI)
SciFact 70.2 56.8 +13.4 (+9.9 to +17.0) 42.2 (dense, top 5) +28.0 (+23.4 to +32.6)
NFCorpus 25.7 16.9 +8.8 (+7.0 to +10.7) 14.2 (dense, top 10) +11.6 (+7.8 to +15.4)
FiQA 39.7 38.1 +1.7 (+0.0 to +3.3) 31.0 (dense, top 5) +8.7 (+6.4 to +11.1)

glossdex shows nothing for 96–100% of the questions a collection cannot answer. A similarity threshold chosen on the same development collections also does, but only by showing nothing for 8.5–72% of the questions that do have answers (glossdex: 0.6–22%). glossdex ranks as well as dense retrieval. The protocol, every table, per-query results and the scripts to reproduce them are in benchmarks/beir.

Used in

Memoyad: private, fully on-device search over photos, screenshots and photographed documents for Android (in closed testing on Google Play), built on the same method.

License

glossdex is free software under the GNU Affero General Public License v3.0 (LICENSE). You can use, study, change and share it, including in open-source products and services that are also AGPL. To embed glossdex in a closed-source product or service, a commercial license is available: see COMMERCIAL-LICENSE.md.

Patent pending. The evidence-gated result-set method implemented here is the subject of U.S. Provisional Patent Application No. 64/172,444. See PATENTS.md for what the open-source license grants.

Citing

If you use glossdex in research, please cite it (see CITATION.cff):

@software{iranpour_glossdex,
  author  = {Iranpour, Mehran},
  title   = {glossdex: evidence-gated semantic search over locally generated descriptions},
  year    = {2026},
  version = {0.1.0},
  doi     = {10.5281/zenodo.23269314},
  url     = {https://github.com/miranpoor/glossdex}
}

Metadata

Release files for glossdex 0.1.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for glossdex 0.1.1
File Size Uploaded Provenance Actions
glossdex-0.1.1.tar.gz 803.5 kB Without provenance Details for glossdex-0.1.1.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for glossdex 0.1.1
File Interpreter ABI Platform Actions
glossdex-0.1.1-py3-none-any.whl Python 3 none any Details for glossdex-0.1.1-py3-none-any.whl

Total release size: 860.3 kB

Release files / glossdex-0.1.1.tar.gz

Download URL glossdex-0.1.1.tar.gz
Size 803.5 kB
Tags Source
SHA-256 checksum
How to use checksums
b51a40e942bacfc7599a3b3500b2c3365ebde6e4ccf1d114f15856b2bdc960c6
BLAKE2b-256 checksum
How to use checksums
fad1eda0c01ad56a2b18ebd690072512f52eb08a221fcb32487380a8fa3e1629
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.10

Release files / glossdex-0.1.1-py3-none-any.whl

Download URL glossdex-0.1.1-py3-none-any.whl
Size 56.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
641a5bf20fe3ef01c6c9d2484cfe902acb9b664348272485f773ddb34f1ec3a9
BLAKE2b-256 checksum
How to use checksums
b3cc27fcb6c83d5f8f2cd660c74bf13915a723924c995bf19c30fbb9f480e558
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.10

Security for glossdex 0.1.1

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

0.1.1 This release

2 release files No provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page