8000 Recall by meaning — a text-derived vector layer over tags and recognized content · Issue #32 · ynishi/asterism · GitHub
[go: up one dir, main page]

Skip to content

Recall by meaning — a text-derived vector layer over tags and recognized content #32

Open

Description

@ynishi

Problem

Recall comes in two shapes. "The one from earlier today" is a time
question, and the grid already answers it. "That one with the …" is a
content question, and the store answers it only when the words in the
query happen to match words that got attached to the asset. Full-text
search (asset_fts) and channel tags are exact instruments: they find
what was written down, and nothing else.

The pain at volume: you remember an image by what it looks like, not by
its checkpoint name or the keywords the auto-tagger chose. When the
remembered phrase never made it into a tag or a prompt, the current
answer is scrolling thumbnails — workable at hundreds, not at tens of
thousands. Picked up from #13's Open section (tag-based
vectorisation).

Approach to explore

Vectorise text derived from the asset — its tags, captions, and
recognized content — rather than embedding raw pixels.

  • The auto_tag job already extracts keywords into channel tags; this
    layer extends that pipeline rather than adding a parallel one.
  • The embedded text is inspectable. When a query misses, the text is
    the diagnosis: either the recognizer never wrote the concept down,
    or the embedding failed to bridge the words. A pixel embedding
    offers no such seam.
  • Vectors propose, metadata disposes. Semantic candidates are ranked
    suggestions; structured metadata and FTS remain the instruments
    that filter what is actually usable. The vector layer must not
    replace them, and search must degrade cleanly to FTS when the index
    is absent.

Working constraint: the index should live comfortably beside the
SQLite store on a personal machine — the working hypothesis is that a
text-derived index stays small (target: on the order of 1 GB at
expected scale). If it cannot, that is a finding, not a tuning
problem.

The blocking decision is model choice. Changing the embedding
model invalidates every stored vector and forces a full re-index, so
the model (and its local-inference cost) has to be chosen before
indexing at scale, not after.

Desired property

  • A query in natural words returns ranked candidates whose derivation
    is inspectable (the text that matched is showable).
  • Fully local: indexing and query embedding run offline.
  • Incremental: new and re-tagged assets enter the index through the
    existing job pipeline, not through periodic full rebuilds.
  • Honest failure: a query that finds nothing says so, instead of
    padding with weak matches.

Verification

Write the recall queries first. Before any index exists, collect ~20
real content-recall queries from actual use ("the one with …"), each
with its known intended answer in the store. Build the index, run
them, count the misses. That count is the go/no-go for the approach,
and the query set becomes the regression suite afterward — a model or
pipeline change reruns the same 20 and reports the same number.

Open

  • Embedding model choice under the local-inference constraint — the
    decision everything downstream waits on.
  • What "recognized content" includes in v1: captions only, or OCR for
    text-bearing assets as well. Each recognizer widens recall and adds
    a runtime dependency.
  • Storage: inside SQLite (extension) or a sidecar index file beside
    the profile. Backup and the profile marker should keep working
    either way.
  • Where query understanding stops: v1 embeds the query text as-is;
    anything smarter (query rewriting, filters parsed from prose) needs
    its own justification.
  • Video and audio: whether their text derivation (transcripts, frame
    captions) is v1 or a follow-up.

Not in scope

  • Raw-pixel visual similarity. It answers a different question
    ("looks like this image"), needs a reference image in hand, and
    carries a different index cost. Nothing here precludes adding it
    later as its own layer.
  • Hosted inference as a requirement. A hosted model may be an option;
    the local path is the contract.
  • Replacing FTS, tags, or any structured search surface.

Activity

  1. ynishi commented on Aug 15, 2026

    @ynishi
    OwnerAuthor

    Investigation: the gap is wiring, not embeddings

    Before choosing a model, I read what the store already does. The finding
    changes the shape of this issue: every surface that could hold words
    about an asset already exists, and none of them reaches the search
    index.

    What is already built (measured)

    The lexical side is further along than the issue body assumes.

    • Tantivy with BM25 ranking, mixed_body tokenizer: Lindera (IPADIC,
      Normal) morphological segmentation for Japanese → LowerCaser →
      AsciiFoldingFilter → Porter stemmer → 40-char cap
      (crates/asterism-infra/src/search/tokenizer.rs:32-48).
    • Query-time typo tolerance already ships: each term is wrapped as
      TermQuery OR FuzzyTermQuery(distance=1), gated at 4+ characters
      (crates/asterism-infra/src/search/tantivy_index.rs:61-67,204).
    • SQLite FTS5 with the trigram tokenizer for the exhaustive substring
      predicate; 3+ characters seek the index, 1-2 fall back to LIKE over
      asset_body (migrations.rs:3402-3422,3452-3457).
    • The Query / Retrieval split is already stated as a design position
      (migrations.rs:3396-3400).

    So "add fuzzy matching", "add trigram", "segment Japanese", "rank by
    BM25" are all done. That is not where the recall is lost.

    Where the recall is actually lost

    There are five places an asset can carry words. Not one of them is
    indexed.

    Surface Written by In FTS / Tantivy?
    material.meta_kv — PNG tEXt, i.e. the generation prompt material_hash job via the PNG probe no
    material.meta_raw — raw bytes of all five metadata chunks same no
    Image.alt / Image.labels on the importer footprint importer / caller no
    AlbumMeta (asset_declare_meta over MCP) any agent, after the fact no
    asset_comment user / agent no

    Only asset_body reaches the index, and index_rebuild returns
    "is not text, skipped" the moment TextLocator::new declines a
    non-text payload (crates/asterism-infra/src/jobs/handlers.rs:2687-2689).
    Images therefore enter neither asset_fts nor Tantivy at all.

    auto_tag does not close this either. It reads labels, the register
    note, the filename split on -_. , and the first three # heading
    lines of a text file — capped at 8 keywords
    (handlers.rs:269-329). It never looks at what the image contains,
    so the issue body's premise that it "already extracts keywords" holds
    only for text assets.

    Two consequences worth stating separately:

    1. The generation prompt is already in the database and has been
      unreadable to search this whole time. meta_raw was added
      specifically so a later build could decide how to expand metadata
      "without opening the file"
      (crates/asterism-infra/src/probes/png.rs:96-111). That is exactly
      the affordance this issue needs, already paid for.
    2. meta_kv is lossy in two known ways: tEXt is Latin-1 by spec and
      goes through from_utf8_lossy, and zTXt / iTXt are read by
      neither axis — a stated gap (migrations.rs:5128-5132). This is not
      academic: ComfyUI workflow JSON is large enough to be compressed
      into zTXt, and prompts containing Japanese need iTXt to be
      UTF-8. The bytes survive in meta_raw; nothing reads them.

    What the external research says

    Two independent literature/implementation reviews were run. They
    converged.

    Document expansion beats query expansion. Enriching the document
    with generated terms moved BM25 on MS MARCO from MRR@10 0.184 → 0.277
    and Recall@1000 0.853 → 0.947
    (docTTTTTquery).
    The paper's own conclusion is that expanding documents is more
    effective than expanding queries. Translated here: put the words in at
    ingest, do not try to be clever at query time.

    The vocabulary problem is the actual adversary. Furnas et al. 1987
    measured that two people pick the same term for the same object with
    probability below 0.20, so single-term access fails 80–90% of the time;
    the prescription is "unlimited aliasing"
    (CACM 30(11)). Tags
    written by whoever generated the asset are exactly the single-term case.

    Lexical degradation is a cliff, not a slope. Stratified by
    query/document term overlap, BM25 holds Hits@10 ≈ 0.94 in the 0.8–1.0
    band and collapses toward 0 as overlap thins, while an embedding model
    holds (arXiv:2201.00118). What
    lexical cannot reach is the phrasing that is not derivable from any
    word present — mood, abstraction, relation ("that lonely-looking one").

    Vectors, in this product category, are what you reach for when there
    are no words.
    Of the comparable OSS tools, only Immich ships vector
    search (CLIP, 512-dim), and its stated motivation is finding untagged
    photos. PhotoPrism discretises via a classifier into labels, digiKam
    uses perceptual fingerprints, Hydrus uses tag set intersection with
    sibling/parent relations — none of them embed. That is the same
    conclusion from the other direction: the embedding layer earns its
    place where the words are absent.

    Nobody's numbers can settle this for us. There is no public
    benchmark for tag-shaped retrieval; BEIR and MTEB measure sentences and
    passages, and BEIR itself reverses per domain (DPR at roughly a quarter
    of BM25 on BioASQ; ANCE above BM25 on Quora). The verification section
    of this issue — collect real recall queries first, count the misses —
    is the only thing that can decide the question. Both reviews arrived at
    that independently.

    Two smaller findings worth keeping:

    • FUZZY_MIN_LEN = 4 counts characters, and Lindera's Japanese
      morphemes are mostly 1–3, so fuzzy never fires on Japanese. This
      is correct and should not be "fixed": Meilisearch and Typesense both
      refuse typo tolerance below a similar length, because edit distance 1
      over short CJK tokens changes the word. The right instrument for 6948
      Japanese is a reading (yomi) field, not a smaller threshold.
    • spellfix1 is not a candidate. Its matching is ASCII
      transliteration plus a phonetic hash, and spellfix1_scriptcode
      understands Latin / Cyrillic / Greek only.

    Plan

    Ordered so that each step is independently useful and the expensive
    decision comes last, with numbers in hand.

    1. Derive text from what is already stored, and index it. Read
      meta_raw / meta_kv, recover zTXt and iTXt (currently a
      stated gap) and the Latin-1 bytes from_utf8_lossy dropped, and
      feed the result into the existing indexing path. The narrow change
      is index_rebuild's gate: from "are these bytes text" to "is there
      text derivable from this asset". Rows ingested before meta_raw
      existed carry the not-captured marker and need one re-read. No new
      dependency, no model, no new index — the existing BM25 + Lindera +
      Porter + fuzzy stack starts answering for images.
    2. Fill the normalisation holes. The analyzer chain normalises
      Latin only, so full/half width, katakana/hiragana, long-vowel
      variants (サーバ / サーバー) and inflected forms pass through
      unfolded. Adding NFKC + width folding + base-form normalisation, and
      a reading field, is a few filters in the chain. These are
      deterministic wins: queries that returned nothing start returning
      the right row.
    3. Synonym expansion at query time, one-way. Danbooru publishes tag
      alias and implication graphs, which fits this corpus without hand-
      authoring a thesaurus. Apply at query time, not index time, and
      place it before the stemmer. Do not generate a thesaurus from
      co-occurrence — synonyms do not co-occur, and the classical result
      is that it does not help (Peat & Willett 1991).
    4. Write the recall query set and measure. ~20 real "the one
      with …" queries with their known answers, run against the store
      after 1–3. The miss count is the go/no-go, and the set becomes the
      regression suite.
    5. Decide on a vector layer from that number, not before. If the
      residual misses cluster on mood/abstraction/relation, that is the
      signal an embedding layer addresses something lexical cannot. If
      they cluster on missing words, the answer is upstream instead.
      Sizing is not the constraint either way: 50k × 512-dim float32 is
      ~102 MB, an order of magnitude inside the 1 GB working budget.

    Steps 1 and 2 are independent of each other and of everything above
    them.

    Deliberately not doing

    Batch captioning through an external vision API, i.e. tiling N
    thumbnails into one image and asking a cheap model to name what is in
    them. It was considered and dropped. It adds a moving part to the
    pipeline, and it produces a weaker version of something the operator
    already has: an agent driving an import can call its own model and
    write the result through the existing surfaces — Image.alt,
    Image.labels, or asset_declare_meta after the fact. Owning a
    degraded copy of a capability that is available everywhere is not worth
    the construction.

    That decision has a prerequisite this investigation confirmed: those
    write surfaces do exist.
    Image.alt ("alt text / caption, becomes the
    cover hint") and Image.labels ("additional free-form labels") are on
    the importer footprint, asset_declare_meta records an after-the-fact
    statement and attributes it to the agent that made it, and there is a
    sidecar path (<name>.meta.json) for metadata written beside the file.
    So the ingest side is open for an agent to write into.

    What is not open is the same thing step 1 fixes: none of those
    surfaces is indexed. Whatever an agent writes into alt, labels,
    AlbumMeta or a comment today is as invisible to search as the prompt
    is. Step 1 is therefore also what makes agent-written descriptions
    worth writing
    — and it may be worth widening its scope to cover all
    five surfaces rather than the PNG chunks alone.

    Raw-pixel similarity remains out of scope as the issue states.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions

      0