Search Record Indexing - FreeUKGen/MyopicVicar GitHub Wiki

Search Record Indexing

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.

update_create_search_record

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.save

The 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.

Step 1: transcript names (Freereg1Translator)

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).

Step 4: transform

SearchRecord#transform runs these in order. They're all in search_record.rb unless noted.

  1. populate_search_from_transcript: one SearchName per transcript name, with type mapped to p (primary) / f (family, i.e. 'other') / w (witness), and gender inferred from the role (gender_from_role: father/groom/… → m; for ba it uses person_sex; for bu it guesses from the relationship text: "son" → m, "dau"/"wife"/"wid" → f). If a name contains . : ; ' - \ ", a second **cleaned** copy with those characters removed is added too (so O'Brienis findable asobrien`).
  2. downcase_all: all search names are stored lower-case. Queries downcase too.
  3. separate_all: multi-part names are split into extra search names. "Mary Ann" adds mary and ann (origin sep). "Smith-Jones" adds smith and jones (origin sepl), except surname particles (de, van, mc, o, of… see is_surname_stopword).
  4. emend_all (lib/emendor.rb): for each forename that exactly matches an EmendationRule.original, adds a copy with rule.replacement (origin e). 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_emendations manage the rule set and bulk re-emendation.
  5. transform_ucf (lib/ucf_transformer.rb): expands bracketed alternatives. Jo[hn] doesn't mean "John": [hn] means "one of h or n", so it adds joh and jon. 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).
  6. create_soundex: search_soundex gets a {first_name, last_name, type} triple of Text::Soundex codes for each search name (skipping names where both parts are blank or *; fixed Sept 2026, e0877f1a4).
  7. transform_date: see below.
  8. 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.

Dates

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.

UCF wildcards and the place UCF list

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_ucf finds search records in a batch whose search_names contain any of those characters. Place#update_ucf_list(file) stores their ids in place.ucf_list[<file_id>], with counters ucf_list_record_count/ucf_list_file_count.
  • At query time, when config.ucf_support is on and the search is limited to specific places, those records are fetched and matched in Ruby against the query using UcfTransformer.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

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 by place_id (searches limited to places)
  • REG_CHAPMAN_INDEXES: led by chapman_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.

⚠️ **GitHub.com Fallback** ⚠️