Mixed-Media Library
Goal: a single bucket holds images, videos, audio clips, and PDFs; K3 embeds them all into one Vector collection; you search by file (image-by-image, audio-by-audio) or by text (“a brown leather handbag”). Visual + cross-modal retrieval, no glue code.
Primitives used: Storage (mixed-media uploads via S3 SDK or CLI) → Pipelines (auto-rule wired by Vector’s visual_embedding_index template) → Vector (one collection, searched by uploaded file or by text).
Shape:
images / video / audio / PDFs ──upload──► Storage bucket
│
▼ auto-rule fires
visual_embedding_index Scriptum
(handles each modality:
- image → single embed
- video → sampled frame embeds
- audio → windowed embeds
- pdf → page-render embeds)
│
▼
Vector — `assets` collection
(one collection, multimodal)
│
┌─────────────────────────┴─────────────────────────┐
│ │
▼ ▼
Search by file (multipart upload) Search by text (JSON)
"find similar to this image" "brown handbag"Prerequisites
dodilCLI +dodil auth login- A bucket —
kb-platform:dodil data bucket create kb-platform -d "Mixed-media asset library" - No engine setup step — vector capacity is provisioned on demand when you run
dodil data vector collection addbelow.
1. Create the multimodal collection
visual_embedding_index handles all four modalities — image, video frames, audio, and PDF page renders — through one collection. Its declared extension set is
jpg, jpeg, png, gif, webp, mp4, mov, webm, mp3, wav, flac, pdf. Schema and
embed_model come from the template’s contract:
dodil data template get visual_embedding_index -o json | jq '.contract.inputs'Defaults worth knowing: embed_model jina-embeddings-v4, dimensions 1024
(the only permitted value for this template — the text one defaults to 768),
image_max_width / image_max_height 512, frame_every 10 s,
key_frames_only true, and max_size_bytes 10485760 (10 MB — a tenth of the
text template’s ceiling). Override any of them with --set:
dodil data vector collection add assets -b kb-platform \
--description "Mixed-media library — images / video / audio / PDFs" \
--template visual_embedding_index \
--set frame_every=5
# → Vector collection 'assets' (pipeline pl_…) created in bucket 'kb-platform'.
export PIPELINE_ID=$(dodil data vector collection list -b kb-platform -o json \
| jq -r '.pipelines[] | select(.name == "assets") | .pipelineId')Inspect the resolved collection (by pipeline id — collection get does not
take the name):
dodil data vector collection get "$PIPELINE_ID" -b kb-platform -o jsonVisual collections are dense-only, like every other collection on the platform: no shipping template enables BM25, and the plane has no BM25 implementation.
Inspect the auto-rule’s globs — they should cover image / video / audio / PDF extensions:
dodil data ingest list -b kb-platform -p "$PIPELINE_ID" -o json \
| jq '.rules[] | {ruleId, includePatterns, includeMimeTypes, enabled}'2. Upload mixed media — pick your client
K3 speaks native S3 — see Storage → S3 Compatibility for setup.
aws-cli
# Set up profile once
aws configure --profile dodil-k3
export AWS_PROFILE=dodil-k3
export AWS_ENDPOINT_URL=https://object.uk-lon-1.dodil.io
# Sync a folder of mixed assets
aws s3 sync ./local-assets/ s3://kb-platform/library/ \
--content-type "image/jpeg" \
--exclude "*" --include "*.jpg" --include "*.jpeg"
aws s3 sync ./local-assets/ s3://kb-platform/library/ \
--content-type "video/mp4" \
--exclude "*" --include "*.mp4"
aws s3 sync ./local-assets/ s3://kb-platform/library/ \
--content-type "audio/wav" \
--exclude "*" --include "*.wav"Setting
--content-typeper upload matters. K3’s auto-rule matches on MIME types; the visual pipeline also dispatches to different embedders based on content-type (image vs video vs audio). For mass-uploads where MIME is wrong, fix it before ingest fires — the search side has no content-type override for stored objects (a file query carries its own content type on the multipart part, but that only affects the query).
Per-file, one key at a time:
dodil data object create ./bag-001.jpg -b kb-platform -k library/bags/bag-001.jpg
dodil data object create ./audio-001.mp3 -b kb-platform -k library/audio/audio-001.mp3
dodil data object create ./clip-001.mp4 -b kb-platform -k library/video/clip-001.mp4
dodil data object create ./catalog.pdf -b kb-platform -k library/pdf/catalog.pdf3. Watch the ingest jobs
dodil data ingest jobs -b kb-platform -p "$PIPELINE_ID" -o json \
| jq '.jobs[] | {object: .object.key, status, chunksCreated, embeddingsWritten}'Modality-specific behavior to expect:
| Modality | chunksCreated typical | Notes |
|---|---|---|
| Image (jpg / png) | 1 | one embedding per image |
| Video (mp4) | 8–24 | sampled frames → one embedding each |
| Audio (wav / mp3 / flac) | 3–10 | windowed → one embedding per window |
| N (= pages) | one page-render embedding per page |
If embeddings_written is consistently lower than chunks_created, see Pipelines → Replay & Retry.
4. Search
The route takes two request shapes, distinguished by content type:
| Shape | Content-Type | Body |
|---|---|---|
| Text query | application/json | text, collectionNames, topK, minScore |
| File query | multipart/form-data | file (required), text, top_k, collection_name / collection_names, search_mode, rerank |
There is no s3Key field. A file query means uploading the bytes on the
request; K3 stages them at a temp key under .k3-tmp/search/, hands the
template a 5-minute presigned URL, and reaps the object when the search returns.
Max upload 50 MiB.
A. By an image — file query (multipart)
curl
curl -sS -X POST "https://api.data.dodil.io/kb-platform/search/vector" \
-H "Authorization: Bearer $DODIL_TOKEN" \
-F "file=@./query-bag.jpg;type=image/jpeg" \
-F "collection_name=assets" \
-F "top_k=20" | jq '.results[] | {score, object: .object.key}'The file’s own content type routes it to the right modality — image/*,
audio/* and video/* all go to the visual embedder. Set it explicitly
(;type=image/jpeg in curl, the 3-tuple in requests) rather than relying on
sniffing.
B. By text
Visual embedders trained CLIP-style understand text-to-image matching directly. Send a text description; K3 embeds it on the same vector axis as your images:
dodil data
dodil data search "brown leather handbag with gold hardware" \
-b kb-platform --table assets --top-k 20Quality of text-to-image recall depends on the underlying visual embedder. Test both shapes against your corpus to see which works better for your queries.
C. File + text together
Send text alongside file in the same multipart request. It is carried as the
rerank text, not as a second query vector:
curl -sS -X POST "https://api.data.dodil.io/kb-platform/search/vector" \
-H "Authorization: Bearer $DODIL_TOKEN" \
-F "file=@./query-bag.jpg;type=image/jpeg" \
-F "text=brown leather handbag with gold hardware" \
-F "collection_name=assets" \
-F "top_k=50" | jq '.results[] | {score, object: .object.key}'Honest caveat: the combined signal is inert today. The
text,rerankandsearch_modefields are parsed into the search request and then never read — ranking comes from the file’s embedding alone. The fields are accepted for wire compatibility, so the request above succeeds; it just behaves like §A. Send the two queries separately and merge client-side if you need both signals now.
See Vector → Multimodal Search for more combined-signal patterns.
D. Audio similarity — same shape
curl -sS -X POST "https://api.data.dodil.io/kb-platform/search/vector" \
-H "Authorization: Bearer $DODIL_TOKEN" \
-F "file=@./query-sound.wav;type=audio/wav" \
-F "collection_name=assets" \
-F "top_k=10" | jq '.results[] | {score, object: .object.key}'E. Narrowing results
There is no metadata pre-filter on this route. k3-api sends the search template a hardcoded empty filter, so you cannot scope a query to
library/bags/in the request. Two things that do work: pincollection_name/collectionNamesto separate collections per category, or filter the returnedresults[].object.keyclient-side. For genuine predicate-plus-KNN in one statement, use pgvector operators in SQL on the data plane.
5. Display results — building a product-search UI
# Sketch: build a "more like this" UI
import os, requests
K3 = "https://api.data.dodil.io"
AUTH = {"Authorization": f"Bearer {os.environ['DODIL_TOKEN']}"}
def find_similar(image_path: str, top_k: int = 20):
"""File query — the bytes ride on the request; there is no s3Key shape."""
with open(image_path, "rb") as fh:
resp = requests.post(
f"{K3}/kb-platform/search/vector",
headers=AUTH,
files={"file": (os.path.basename(image_path), fh, "image/jpeg")},
data={"collection_name": "assets", "top_k": str(top_k)},
).json()
# Each result has object.key — turn into a presigned download URL for the UI
return [
{
"score": hit["score"],
"key": hit["object"]["key"],
"url": presigned_url(hit["object"]["bucket"], hit["object"]["key"]),
}
for hit in resp["results"]
]
def presigned_url(bucket: str, key: str) -> str:
# Storage's GetObjectUrl — a GET, key in the path
r = requests.get(f"{K3}/{bucket}/objects/{key}/url", headers=AUTH).json()
return r["url"]The presigned URL flow is critical for UIs — Storage’s GetObjectUrl is
GET /:bucket/objects/:key/url and returns a K3-signed URL good for 3600 s by
default (dodil data object url <key> -b <bucket> --expires <seconds> from the
CLI); you don’t need to proxy bytes through your app.
Common gotchas
| Symptom | Cause | Fix |
|---|---|---|
| Audio uploaded but no embedding written | Audio embedder rejects rare codecs (e.g. very old AMR) | Transcode to common formats (wav, mp3, ogg) before upload |
| Video search hits one frame consistently — others ignored | Frame-sampling is sparse (frame_every defaults to 10 s, key_frames_only to true); corpus has near-identical frames | Recreate the collection with --set frame_every=2 --set key_frames_only=false |
| Text-to-image queries are worse than expected | Visual embedder isn’t CLIP-quality on your domain | Test domain-tuned embedders; consider an External Collection with a custom embedder |
| Different objects with same content (e.g. JPEG + WebP of the same photo) rank apart | Hash differs but embeddings should be close | This is correct — embeddings are content-aware. If you want exact dedup, use object eTags in metadata |
| PDF page-render search returns whole-pdf results without page granularity | PDF chunks are per-page; each result’s chunkIndex IS the page number | Display chunkIndex in the UI; use it to deep-link to the page in your viewer |
| Latency for video queries is 700-1500 ms | Server-side video frame extraction + embedding | Pre-extract a representative frame on your side; use External Collection for query-side control |
Cleanup
RULE_ID=$(dodil data ingest list -b kb-platform -p "$PIPELINE_ID" -o json | jq -r '.rules[0].ruleId')
dodil data ingest update "$RULE_ID" -b kb-platform --enabled=false
dodil data ingest delete "$RULE_ID" -b kb-platform
dodil data vector collection delete "$PIPELINE_ID" -b kb-platform
dodil data bucket delete kb-platformSee also
- Vector → Multimodal Search — deeper on the multimodal patterns (face-search, open-vocab object detection)
- Storage → S3 Compatibility — bulk-upload via aws-cli / SDKs
- Storage → Browser Upload — presigned PUT for direct-from-browser uploads (great for user-generated content libraries)
- Vector → External Collection — when you want a different visual embedder than K3’s default
- RAG Knowledge Base — same shape but for text; pair both for a multimodal-RAG product