The Solr schema

Every field of a photo document, the analyzer, the vector field and the solrconfig.

The app uploads the files in solr/conf of its repository to the phone's index: schema.xml, solrconfig.xml, stopwords.txt, synonyms.txt, protwords.txt and mapping-ISOLatin1Accent.txt. The build zips that folder into the APK, so the app always uploads exactly what is in the repository.

The phone's copy of the index reads the fields below except embeddings and the dup_…_hash keys for similar photos, which are made on Opensolr's side and only read as facets. Every write answers with the document it wrote.

01 · Fields of a photo document
id

md5 of the path inside the phone's storage (e.g. DCIM/Camera/IMG_1.jpg), lower-case hex, so the same folders on another phone keep every identity

Type
string
path

Absolute path on the phone that last synced the photo

Type
string
folder

MediaStore folder, e.g. DCIM/Camera/

Type
string
file_name

File name

Type
string
media_id

MediaStore id, used to open the photo

Type
long
mime, size_bytes

Type and file size

Type
string, long
width, height

Pixels as displayed, EXIF rotation applied

Type
int
orientation

landscape, portrait or square

Type
string
taken_at

EXIF date taken with its offset when present, otherwise MediaStore's date. For a photo that carries no date of its own the indexed value is sticky: a later write keeps the taken_at already in the index instead of working it out again

Type
date
year, month

From taken_at, in the phone's time zone. There is no day field: the third level of the grid, Year > Month > Day, is worked out on the phone from the taken_at it holds in its copy

Type
int
modified_at

File modification time

Type
date
camera_make, camera_model, lens

From EXIF. There is no Camera make filter, because the model already reads Nikon Z6; the field is still filled, searchable, and feeds camera_text, text, suggest and spell

Type
string
iso, exposure, f_number, focal_length, flash

From EXIF; exposure as 1/250

Type
int, string, float, float, boolean
has_location, location, altitude

GPS as lat,lon when usable; location is a spatial field, so it answers radius searches

Type
boolean, location, float
city, region, province, community, country, country_code

The nearest named place to the position, in words, worked out by Opensolr inside photos_ingest (or photos_words, for a place you set). street_s and postal_code_s hold the street address and postal code when one is known

Type
string
meaning, meaning_own_b

The one sentence the image model wrote about the photo, or your own wording when you have written one; meaning_own_b says it is yours. On a plan without vector search no photo is read, so the field holds only what you have written yourself

Type
text
ocr_t

The text printed in the photo — a receipt, an invoice, a shelf label, a screenshot — read with tesseract on api.opensolr.com, kept only when it holds real text. Separate from meaning, which is what the photo shows: the words on a receipt are searchable but are never presented as a description of it. Stored, and carried over by any later pass that reads nothing — a plan without vector search, or the month’s AI allowance spent — as long as file_hash says it is the same file. meaning and labels are carried over on the same terms. A file that really changed starts again, and on a plan without vector search ocr_t is never filled in the first place

Type
text
labels

The object names the image model found, one by one, never a sentence. Empty on a plan without vector search

Type
string, multi
labels_t

The same labels as one searchable text, written by Opensolr, so a search for a word finds the photos labeled with it

Type
text
custom_tags

The owner's own tags, one by one (custom_tags_text holds them as words for search). The tag filter is a facet over it, fetched on the same terms as persons_ss; tag suggestions are counted on the phone

Type
string, multi
persons_t

The names of the people in the photo, as one line, for search. They are never worked out by Opensolr: they are the names the owner gave to faces in the app (on the phone), and the names another app already wrote into the file in the XMP property PersonInImage (read only; the app never writes to the file). Matches the *_t dynamic field, so the schema declares no field of its own for it

Type
text
persons_ss

The same names, each one whole, because faceting analysed text gives words and not names. This is what the People filter is built from: a facet over it, asked for once and asked for again only when a sync has actually written something. The name suggestions and the “already on these photos” list in the tag sheet are counted on the phone, from its own copy of this field. Matches the *_ss dynamic field

Type
string, multi
faces_json

The faces the phone found in the photo, as one string: the size of the file they were read from, the version of the face models, and for each face its frame, its name if it has one, whether the owner confirmed it, and its numeric fingerprint. Written by the phone, read back by a phone that has not read the photo itself, and never used by the server for anything: no face is compared anywhere but on the phone.

Type
string, stored
file_hash, pixel_hash

The md5 of the original file and a fingerprint of the picture itself, both worked out on the phone: Opensolr only ever sees the 1024 px copy. file_hash is the last stop of similar photos, where only true copies group together, and the proof that a file is unchanged, which lets a pass that could read nothing keep what was read before. pixel_hash tells a change of the picture from a change of its metadata only. Both match the *_hash dynamic field

Type
string
embeddings

1024 dimensions, cosine; the vector of the people, what the photo shows, where it was taken and your tags, in that order. Stored, so a document can be rewritten for its faces without the vector being made again; a write that changes the words makes it again from the new words, in the same call. Empty on a plan without vector search, and search is then on words only

Type
vector
clip_model, embed_model

What produced the words and the vector. Both empty on a plan without vector search; embed_model is also how the phone knows which photos have a vector and which have to be read again if the plan gets vector search back, which is why every write answers with it

Type
string
indexed_at

When the document was written

Type
date
dup_…_hash

The keys behind similar photos, written by Opensolr from the image model's words and the EXIF: dup_w2_hash, dup_w3_hash, dup_desc_hash and dup_exif_hash. They match the *_hash dynamic field: indexed, not stored, with docValues, so a facet over them is cheap. They are made again at every write

Type
string

Some fields can change after a photo is indexed without the photo being read again: custom_tags, persons_t, persons_ss, meaning, the place and faces_json. Tagging many photos at once either adds to what each photo carries or replaces it; the phone works out the result from its own copy and sends the finished list, never an instruction. The write is a whole document: Opensolr reads the document from the index, puts the new values in, makes the keys for similar photos and, when the words changed, the vector again, and writes it complete. See photos_words.

Copy fields feed the search fields: text gathers meaning, the printed text ocr_t, the names in persons_t, file name, folder, camera, city, region, country and your tags; file_name_text, folder_text, camera_text, place_text and custom_tags_text hold each of those on its own. Two more copy fields serve typing: suggest (your tags, labels, camera make and model, city, region, province, country) is what the search box completes from — the tag and people suggestions in the tag sheet are counted on the phone and never touch it — and spell (the same plus the file name and the community) is what Did you mean checks against. ocr_t is deliberately in neither: a receipt would fill autocomplete with its own numbers. Dynamic fields (*_s, *_ss, *_i, *_l, *_f, *_b, *_dt, *_t, *_json, *_hash) hold the app's extra fields and are there for anyone extending it.

02 · Text analysis

Every text field is text_general, and its analyzer is deliberately plain: an HTML strip and a mapping char filter (mapping-ISOLatin1Accent.txt), the ICU tokenizer, CJK width, English possessives, ASCII folding, stop words, a word delimiter graph (flattened at index time), lower case, a length filter of 1 to 500 characters, and duplicate removal. At query time a short synonym graph adds photo, picture, image, pic, sea, ocean and a few more. There is no stemming: a word is found as it is written, accents aside. The suggester and the spellchecker analyse with text_spell, a shorter chain of the same kind, so completions and corrections come back as real words.

03 · The vector field
<fieldType name="knn_vector" class="solr.DenseVectorField" vectorDimension="1024" similarityFunction="cosine"/>

The vectors come from Opensolr's embedding service, the same one every Opensolr vector index uses; the dimension is fixed by that service. On a plan without vector search the field type is still declared but no document carries a vector, and search is on words only.

04 · solrconfig.xml

Deliberately small: Lucene match version 9.0, the classic schema factory (so schema.xml is the schema), a soft commit every 10 seconds and a hard commit every 60 without opening a searcher, a /select handler answering JSON with 60 rows by default and carrying the spellcheck component (off unless the app asks for it), a /suggest handler with one suggester (AnalyzingInfixLookupFactory over the suggest field, rebuilt at every commit), and /update. Remote streaming and stream bodies are off. There are no <lib> directives, no script processors and no response writers that run templates.

When the configuration changes

A /opensolr-photos-config handler answers config_version, the version of the configuration the index runs, set in solrconfig.xml and raised whenever a file in solr/conf changes. The app compares it with its own: an older index is emptied, given the new configuration and filled again from the phone's copy, with your consent and with your tags and wording kept. See a new index configuration.

How it works

Opensolr Photos is open source and MIT licensed. Questions about your Opensolr account, index or plan go to opensolr.com/contact; questions about the app itself belong on GitHub.

Opensolr Photos Documentation