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.
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
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. 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
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, 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
meaning, meaning_own_bThe 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
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, 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_hash, pixel_hashThe 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
embeddings1024 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
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 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
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.
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. On a plan without vector search the field type is still declared but no document carries a vector, and search is on words only.
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, 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
- How it works
- The phone's index
- The Solr schema