001 Notes
Semantic Image Search From Embeddings To Ranked Results
1. What Semantic Search Is Actually Doing
does filename contain "receipt"?
does folder name contain "invoice"?
was this image modified last week?
which images are visually or conceptually close to the query?
find images like this one
find images of whiteboard notes
find screenshots with error messages
find outdoor photos with cars
image -> embedding vector
text query -> embedding vector
compare vectors -> ranked results
2. Why This Exists
IMG_2031.jpg
Screenshot_2026-07-10.png
download-4.jpeg
3. Visual Pipeline
flowchart LR
A[Folder images] --> B[Image encoder]
B --> C[Image vectors]
C --> D[(Vector index)]
E[Query image or text] --> F[Query encoder]
F --> G[Query vector]
G --> H[Similarity search]
D --> H
H --> I[Ranked candidates]
I --> J[Metadata filters and rerank]
J --> K[Explainable results]
4. What Is An Embedding?
image
text
audio
video frame
email
PDF page
product listing
source code
image: beach_photo.jpg
vector: [0.12, -0.44, 0.03, 0.81, ...]
similar things should land near each other
different things should land farther apart
photo of a dog in grass
photo of another dog in a park
photo of a dog
screenshot of a terminal error
5. Embeddings As Coordinates Of Meaning
Delhi -> latitude, longitude
Mumbai -> latitude, longitude
beach image -> vector
forest image -> vector
invoice image -> vector
receipt image -> vector
beach images are near ocean photos
receipts are near invoices and document photos
screenshots are near other screenshots
whiteboard notes are near diagrams and handwriting
6. A Toy Embedding Space
| Image | dimension 1: outdoor | dimension 2: document-like |
|---|---|---|
| beach | 0.95 | 0.05 |
| forest | 0.88 | 0.04 |
| receipt | 0.05 | 0.94 |
| invoice | 0.04 | 0.90 |
| dog park | 0.82 | 0.12 |
beach is near forest and dog park
receipt is near invoice
beach is far from receipt
7. How Images Become Embeddings
flowchart LR
A[Image file] --> B[Decode image]
B --> C[Resize/crop/normalize pixels]
C --> D[Vision model]
D --> E[Feature vector]
E --> F[Normalize vector]
F --> G[Store embedding]
for image in images:
pixels = decode(image)
input_tensor = preprocess(pixels)
vector = model(input_tensor)
vector = normalize(vector)
store(image_id, vector, model_name, dimension)
resize to expected size
center crop or pad
convert to RGB
scale pixel values
normalize by model-specific mean/std
8. How Text Becomes An Embedding
flowchart LR
A[Text query] --> B[Tokenize]
B --> C[Text model]
C --> D[Feature vector]
D --> E[Normalize vector]
E --> F[Use as query embedding]
image encoder: image -> vector
text encoder: text -> vector
9. Why Embeddings Work
Classification Models
image -> model -> label probability
edges
textures
shapes
parts
object-level patterns
scene-level patterns
images that need similar features for classification often have similar internal representations
visual similarity
object/category grouping
basic image organization
not naturally aligned with text queries
may focus on classification categories more than general meaning
Contrastive Image-Text Models
matching image and caption should have high similarity
non-matching pairs should have lower similarity
image: dog running on grass
text: "a dog running on grass"
image meaning and language meaning are trained into a shared space
text-to-image search
image-to-image search
natural-language visual queries
zero-shot categories
depends heavily on training data
can miss fine-grained visual details
can reflect caption biases
Self-Supervised Vision Models
the model learns visual structure that remains stable under transformations
visual grouping
near-scene similarity
image clustering
fine visual structure
does not automatically support text queries unless paired with a text model or alignment step
10. What To Use For Embeddings
| Use Case | Good Embedding Choice | Why |
|---|---|---|
| text-to-image search | CLIP/SigLIP-style image-text model | text and image share a space |
| image clustering only | DINO/visual encoder/CLIP image encoder | strong visual representation |
| duplicate-ish visual grouping | perceptual hash + embedding | hash catches cheap near-duplicates, embedding ranks |
| mobile/local app | MobileCLIP/small ONNX model | faster and easier to ship |
| server/GPU batch processing | larger CLIP/DINO-style model | better quality if latency allows |
| documents/email | sentence-transformer/text embedding model | optimized for text chunks |
| code search | code embedding model | trained on code/text relationships |
If the user searches with text, use a model trained for text-image alignment.
If the user searches with example images only, a strong visual embedding may be enough.
If the user needs duplicates, do not rely only on semantic embeddings.
11. Model Choice Changes What Similar Means
dog
receipt
screenshot
whiteboard
texture
layout
shape
scene composition
same image resized
same screenshot compressed
same photo lightly edited
these are objectively similar
these are similar under this model and this scoring method
model name
model version
embedding dimension
preprocessing config
normalization policy
created time
source file fingerprint
12. Embedding Dimension, Storage, And Index Cost
384 dimensions
512 dimensions
768 dimensions
1024 dimensions
| Higher Dimension | Lower Dimension |
|---|---|
| may preserve more information | less storage |
| more expensive to search | faster search |
| larger index | easier local deployment |
| may improve quality | may be enough for simple tasks |
N images * D dimensions * bytes_per_value
100,000 * 512 * 4 = 204,800,000 bytes
about 195 MB
about 97 MB
13. Normalization
v_normalized = v / ||v||
dot(normalize(a), normalize(b)) = cosine(a, b)
longer vectors can score higher because of magnitude
direction matters more than length
14. Cosine Similarity
score(query, image) = dot(normalize(query), normalize(image))
| Vector | Values |
|---|---|
| query “receipt” | [0.0, 1.0] |
| receipt image | [0.1, 0.99] |
| beach image | [0.99, 0.1] |
| Candidate | Dot score | Rank |
|---|---|---|
| receipt image | 0.99 | 1 |
| beach image | 0.10 | 2 |
15. Embedding Quality Checks
query: one receipt image
expected: other receipts/invoices near top
query: one screenshot
expected: other screenshots near top
query: one outdoor photo
expected: outdoor photos near top
query: "whiteboard notes"
expected: whiteboards, diagrams, handwritten boards
query: "terminal error"
expected: terminal screenshots or error dialogs
similar colors but different meaning
same object but different context
documents with similar layout but different content
screenshots from different apps
16. Why Embeddings Are Not Enough
the wrong folder is searched
the index is stale
the query model differs from the stored model
metadata filters are hidden
top_k is too small
the user wanted duplicates, not semantic similarity
metadata filters
file freshness checks
model compatibility checks
hashes for duplicates
reranking
result explanations
17. Flat Search
best = []
for image in indexed_images:
score = dot(query_vector, image.vector)
if score >= min_score:
best.add(image, score)
return top_k(best)
query vector
|
v
compare with image 1
compare with image 2
compare with image 3
...
compare with image N
sort by score
O(N * D)
N = number of images
D = embedding dimensions
10,000 * 512 = 5,120,000 multiply-add operations
18. Metadata Filters Before Vector Search
| Filter | Why it helps |
|---|---|
| folder | search only selected project or album |
| file extension | skip unsupported formats |
| date range | avoid old irrelevant results |
| dimensions | focus on screenshots, wallpapers, or photos |
| camera/device | separate capture sources |
| duplicate mode | use hashes before embeddings |
flowchart LR
A[All indexed images] --> B[Folder/date/type filters]
B --> C[Candidate IDs]
C --> D[Vector scoring]
D --> E[Top results]
19. Image Query Search
query image -> query embedding -> nearest image embeddings
20. Text Query Search
flowchart LR
A[Text: red car at night] --> B[Text encoder]
B --> C[Text vector]
C --> D[Compare against image vectors]
D --> E[Images that match the text meaning]
text search is model-dependent
21. Approximate Nearest Neighbor Search
Can we return nearly the same top results faster?
Can we change what similarity means?
HNSW
query -> enter graph -> move to closer neighbor -> move again -> collect candidates
far node --- mid node --- close node --- nearest node
\ \
\ other candidate
skip many unrelated points
IVF/PQ
flowchart TD
A[All vectors] --> B[Train coarse centroids]
B --> C[Assign vectors to buckets]
D[Query] --> E[Find closest buckets]
E --> F[Search only probed buckets]
F --> G[Return approximate candidates]
| Knob | Effect |
|---|---|
| number of buckets | memory/search partitioning |
| probes | more probes improve recall but cost time |
| PQ code size | smaller memory, more approximation |
22. Reranking
use a cheap or approximate method to get candidates
use a stronger or more specific method to reorder them
flowchart LR
A[All images] --> B[pHash/dHash/wHash prefilter]
B --> C[Candidate duplicates]
C --> D[Embedding score]
D --> E[Optional ORB feature rerank]
E --> F[Final review order]
| Signal | Good at | Not good at |
|---|---|---|
| perceptual hash | near duplicate narrowing | broad semantic meaning |
| embedding cosine | visual/semantic similarity | exact local structure |
| ORB | local feature overlap | abstract meaning |
| metadata | narrowing and explanation | visual understanding |
23. Stale Indexes
modified
deleted
moved
renamed
replaced
old embeddings: model A, 512 dimensions
new query: model B, 768 dimensions
file id or path
file size
modified time
embedding model id
embedding dimension
embedding vector
index version
if file changed:
recompute embedding
if model id mismatch:
require reindex
if dimension mismatch:
reject query
24. Result Explanation
score: 0.83 cosine similarity
backend: flat or FAISS HNSW
filters: folder=Screenshots, type=png
rerank: ORB score added
hash distance: not used for this query
model: selected embedding model
index freshness: current
25. Failure Modes
| Failure | Symptom | Product response |
|---|---|---|
| stale index | missing or outdated images | detect mtime/size changes and reindex |
| wrong model | weird scores or dimension mismatch | require reindex |
| low-quality embedding | poor relevance | expose model choice |
| approximate miss | true neighbor missing | allow flat search or higher recall settings |
| overly broad query | generic results | suggest filters |
| hidden metadata filter | good result excluded | show active filters |
26. What Was Done And Why
27. Implementation Blueprint
for image in folder:
embedding = embed_image(image)
embedding = normalize(embedding)
save(image_id, embedding, model_id, dimension, mtime, size)
query = embed_query(query_input)
query = normalize(query)
results = []
for record in records:
if record.model_id != query.model_id:
skip_or_reindex()
if metadata_filter_fails(record):
continue
score = dot(query, record.embedding)
results.append((record, score))
return top_k(results)
add batch embedding
add cache invalidation
add query-time filters
add result explanations
add ANN index for scale
add reranking
add debug view
add rebuild controls
28. Implementation Checklist
index gate: every embedding has model id, dimension, file fingerprint, and timestamp
query gate: query vector dimension matches index dimension
filter gate: folder/date/type filters run before expensive scoring when possible
score gate: score threshold and top_k are visible in diagnostics
rerank gate: candidate order explains which signals were used
freshness gate: changed or deleted files invalidate stale records
| Step | Why this order |
|---|---|
| flat cosine search | easiest to validate |
| metadata filters | reduces bad results and work |
| stale-index detection | prevents confusing misses |
| result explanations | makes search trustable |
| ANN index | performance upgrade after correctness |
| reranking | improves ordering after candidate quality exists |
Comments