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. Version 2.5 changed none of them: no field was added and none was removed.
From 2.5 the phone keeps its own copy of every document. It is read once — at install or after a reinstall — and kept in step by every write, and the index is filled back from it after a reset, so browsing, grouping, tag and name suggestions are answered without asking the index. That first read asks for the stored fields below — id, path, file_name, size_bytes, the dates, the camera and EXIF fields, the place fields, meaning, labels, ocr_t, persons_t, persons_ss, custom_tags, clip_model, embed_model, faces_json, pixel_hash and file_hash — and every write answers with the document it wrote. Two families are left out of both: embeddings, which is large and of no use off the index, and the dup_…_hash keys, which are derived and only ever read as a facet. See sync for what the copy is used for.
idmd5 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
pathAbsolute path on the phone that last synced the photo
folderMediaStore folder, e.g. DCIM/Camera/
file_nameFile name
media_idMediaStore id, used to open the photo
mime, size_bytesType and file size
width, heightPixels as displayed, EXIF rotation applied
orientationlandscape, portrait or square
taken_atEXIF 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. (Older versions wrote tags into the file, which changed MediaStore's date to today)
year, monthFrom 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
modified_atFile modification time
camera_make, camera_model, lensFrom EXIF. 2.5 dropped the Camera make filter, because the model already reads Nikon Z6; the field itself is still filled, still searchable and still feeds camera_text, text, suggest and spell
iso, exposure, f_number, focal_length, flashFrom EXIF; exposure as 1/250
has_location, location, altitudeGPS as lat,lon when usable; location is a spatial field, so it answers radius searches
city, region, province, community, country, country_codeThe nearest named place to the position, in words, from Opensolr's nearby_places
meaningThe one sentence the image model wrote about the photo, or your own wording when you have written one. On a plan without vector search nothing is sent to be read at all, so the field holds only what you have written yourself, and is empty until you do
ocr_tThe 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
labelsThe object names the image model found, one by one: none, one or up to five, never a sentence. Empty on a plan without vector search
labels_tThe same labels as one searchable text, written by Opensolr, so a search for a word finds the photos labeled with it
custom_tagsThe 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
persons_tThe 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
persons_ssThe 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
faces_jsonThe 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.
file_hashThe md5 of the original file, worked out on the phone: Opensolr only ever sees the 1024 px copy, so it cannot work this out for itself. It has two jobs. It is the last stop of the duplicates slider, where only true copies group together; and it is the proof that a file is unchanged, which is what allows a pass that could read nothing to keep the meaning, labels and ocr_t already in the index
embeddings1024 dimensions, cosine; the vector of the people, your tags, what the photo shows and where it was taken. Not stored, so it cannot be read back: a write that changes only the words makes it again from the new words, in the same call. Empty on a plan without vector search, and search is then purely lexical
clip_model, embed_modelWhat 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
indexed_atWhen the document was written
dup_…_hashThe keys behind similar photos, written at indexing from the image model's words and the EXIF: dup_w2_hash, dup_w3_hash, dup_w4_hash, dup_w5_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 and so a document read back with fl=* still carries them, which is how a rewrite keeps them
Four fields can change after a photo is indexed without the photo being read again: custom_tags (and with it custom_tags_text, by copy field), persons_t, persons_ss and meaning, your own wording. Tagging many photos at once either adds the given values to what each photo already carries or replaces that field on every one of them; the phone works out the result from its own copy and sends the finished list, never an instruction. Solr has no partial update for a document with a vector field, so the write is a whole document: the server reads the document back with fl=*, puts the new words in and writes it complete. That is what keeps the fields the phone does not hold — the dup_…_hash keys come back with the read because they have docValues, and embeddings cannot be read back at all, so it is made again from the new words in the same call.
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) are there for anyone extending the app.
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.
The vectors come from Opensolr's embedding service, the same one every Opensolr vector index uses. The dimension is fixed by that service, and neither it nor the service changed in 2.5. On a plan without vector search the field type is still declared but no document carries a vector, and search is lexical over the dates, the camera, the place, the file name and your own words.
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.
A /opensolr-photos-config handler answers config_version, the version of the configuration the index runs; it is 11, and 2.5 did not change it, because no file in solr/conf changed. The app compares it with its own: an older index is reset and fully re-synced, with the owner's consent and with their tags and wording kept. See sync.