Search Record Indexing - FreeUKGen/MyopicVicar GitHub Wiki
Part of FreeREG-Core-Records.
Models/libs: app/models/search_record.rb, search_name.rb, emendation_rule.rb, emendation_type.rb, lib/freereg1_translator.rb, lib/emendor.rb, lib/ucf_transformer.rb, lib/date_parser.rb, lib/name_role.rb
Search never reads Freereg1CsvEntry. Every searchable entry has one denormalised SearchRecord, built by SearchRecord.update_create_search_record(entry, search_version, place). That method is called from the CSV processor, from online entry edits, and from rebuild rake tasks. This page covers how a search record is built. Search-Query-System covers how it's queried.
SearchRecord is shared with FreeCEN (and has FreeCEN-only fields such as freecen_csv_entry_id, birth_place, freecen2_place_id). FreeREG records are the ones with freereg1_csv_entry_id set.
change, embargo_record = entry.process_embargo # 1. apply register embargo rules (Embargoes-and-Gaps)
entry.embargo_records << embargo_record if change
params = Freereg1Translator.translate(file, entry) # 2. entry → record_type, transcript_dates, transcript_names
record = entry.search_record || SearchRecord.new
record.assign_attributes(params)
record[:embargoed] = entry.embargo_records.last.embargoed # 3. copy embargo state
record[:release_year] = entry.embargo_records.last.release_year
record.transform # 4. build the searchable fields
record.digest = record.cal_digest
return 'no update' if existing && digest unchanged # 5. skip the write if nothing changed
record.place_id, chapman_code = place.id, place.chapman_code
record.possible_last_names = transcript_names' last names, downcased
record.saveThe digest check in step 5 makes reprocessing an unchanged file cheap. The digest (cal_digest) covers the location string, names, soundex and all search dates, plus release_year when the record is embargoed, so an embargo being applied or released changes the digest and gets written. A change to anything outside that list (e.g. line_id) isn't persisted by this path if nothing else changed.
transform_names(entry) produces transcript_names: an array of {role, type, first_name, last_name}, one per person mentioned. Roles come from lib/name_role.rb:
| Record | Primary (type: 'primary') |
Others (type: 'other') |
Witnesses (type: 'witness') |
|---|---|---|---|
| Baptism |
ba baptised person |
f father, m mother |
wt (baptism witnesses, DEF files) |
| Marriage |
g groom, b bride |
gf, bf, gm, bm parents |
wt from multiple_witnesses
|
| Burial |
bu deceased |
mr male relative, fr female relative |
none |
Surname inheritance: when the primary person has no surname of their own, the translator borrows one. A baptised person without person_surname takes the father's surname (or the mother's). A burial without burial_person_surname takes relative_surname / female_relative_surname, and if both exist, two primary names are emitted. If no surname can be found, a primary name with last_name: nil is still emitted (fixed Sept 2026, 25729ed8f; before that such entries had no primary name and couldn't be found by forename).
SearchRecord#transform runs these in order. They're all in search_record.rb unless noted.
-
populate_search_from_transcript: oneSearchNameper transcript name, withtypemapped top(primary) /f(family, i.e.'other') /w(witness), andgenderinferred from the role (gender_from_role: father/groom/… →m; forbait usesperson_sex; forbuit guesses from therelationshiptext: "son" → m, "dau"/"wife"/"wid" → f). If a name contains. : ; ' - \", a second **cleaned** copy with those characters removed is added too (soO'Brienis findable asobrien`). -
downcase_all: all search names are stored lower-case. Queries downcase too. -
separate_all: multi-part names are split into extra search names."Mary Ann"addsmaryandann(originsep)."Smith-Jones"addssmithandjones(originsepl), except surname particles (de,van,mc,o,of… seeis_surname_stopword). -
emend_all(lib/emendor.rb): for each forename that exactly matches anEmendationRule.original, adds a copy withrule.replacement(origine). Example:wm→william. Rules can be gender-specific and are skipped when the name's gender is known and different. Rules are loaded once per process into a class variable (@@emendations), so edits in the emendation admin screens only take effect after a restart.rake load_emendations/apply_emendations/correct_all_emendationsmanage the rule set and bulk re-emendation. -
transform_ucf(lib/ucf_transformer.rb): expands bracketed alternatives.Jo[hn]doesn't mean "John":[hn]means "one of h or n", so it addsjohandjon. Brackets containing editorial words (blank,sic,illegible,unnamed,son,widow…) aren't expanded. A tentative common name in brackets ([John]) is just unwrapped. A trailing?is dropped (Smith?→smith). -
create_soundex:search_soundexgets a{first_name, last_name, type}triple ofText::Soundexcodes for each search name (skipping names where both parts are blank or*; fixed Sept 2026,e0877f1a4). -
transform_date: see below. -
populate_location:location_names = ["<place> ||| <church>", " [<register type display name>]"]. See Gazetteer-Place-Church-Register for the legacy"Place (Church)"format still in the data.
Every added name carries origin (transcript, sep, sepl, e, ucf, u), so you can always tell why a record matched.
transcript_dates holds the raw date strings from the entry. DateParser.searchable converts each to a sortable YYYY-MM-DD string with * for unknown parts:
-
6 Mar 1690/1→1691-03-06. Dual-dated years take the later year (Old Style/New Style: 1690/1 is 1691 in modern reckoning). -
Mar 1690→1690-03-*;1690→1690-*-*. - Unclear years:
169_→1695,16__→1650,17?5→175… (the?is just stripped).
From these, transform_date sets:
-
search_date: the primary event date. Baptism: baptism → birth → confirmation → received-into-church, first one present. Marriage: marriage → contract. Burial: burial → death. -
secondary_search_date: the next one in the same list, if present (e.g. birth date when there's also a baptism date).
Date-range searches match on either field (see Search-Query-System). A record with neither date only appears in searches without a date range.
Transcribers mark unreadable characters with _ (one character), * (any run), ? or {m,n} quantifiers. Names like Sm_th can't go into the exact-match index usefully. So:
- At index time, bracket UCF is expanded (above), but wildcard UCF (
* _ ? { }) is stored literally. -
Freereg1CsvFile#search_record_ids_with_wildcard_ucffinds search records in a batch whosesearch_namescontain any of those characters.Place#update_ucf_list(file)stores their ids inplace.ucf_list[<file_id>], with countersucf_list_record_count/ucf_list_file_count. - At query time, when
config.ucf_supportis on and the search is limited to specific places, those records are fetched and matched in Ruby against the query usingUcfTransformer.ucf_to_regex(_→.,*→\w+,_{1,2}→.{1,2}), and shown as a separate "possible matches" list. See Search-Query-System.
The UCF list is maintained after processing (update_place_after_processing), after each online edit (update_place_ucf_list), and cleaned when a batch is removed (clean_up_place_ucf_list_atomic, which uses $unset/$inc to avoid lost updates) or an entry is deleted. ManageCountiesController#clean_ucf_list_for_all_places and Place#clean_up_ucf_list repair lists that point at batches which have since moved.
Indexes are declared as constant hashes on SearchRecord, not via Mongoid index calls, because they are also used by index_hint to choose which one to hint at query time. FreeREG uses three families:
-
REG_PLACE_INDEXES: led byplace_id(searches limited to places) -
REG_CHAPMAN_INDEXES: led bychapman_code(county searches) -
REG_BASIC_INDEXES: no location (nationwide)
Each index then continues search_names.first_name/last_name (or their search_soundex equivalents), record_type, search_date, secondary_search_date. The indexes themselves are not created by Mongoid's create_indexes. They're raw createIndexes commands in doc/design/indexes/search_records.reg (FreeCEN: .cen), run by hand against each database, or locally via bin/dev-create-search-indexes (see Development-Setup). Changing the constants doesn't create or drop anything in MongoDB, and hinting a name that doesn't exist makes every search that selects it fail with "hint provided does not correspond to an existing index". To add an index, add it to the .reg file and create it in every environment before deploying the constant.
Plain Mongoid indexes also exist: freereg1_csv_entry_id, place_location, chapman_record_type (added Sept 2026), and search_names.first_name + last_name.