Перейти до змісту

Пошук: канали, склейка, поріг

Пошук у прочитаному — не grep. Машинний текст скоропису XVIII–XIX ст. має частку помилок у десятки відсотків, і потрібне прізвище виходить із рушія спотвореним до невпізнання. Тому шукається схожість нормалізованих токенів, а не підрядок.

Це документ про пошук у вже прочитаному. Де взяти матеріал, якого в просторі ще немає, у які джерела йти і в якому порядку — where-to-dig.md.


Три сховища, а не три «канали»

search.run має поле where, і це вибір сховища, а не способу пошуку:

where що шукає коли брати
decode (дефолт) машинний текст усіх прогонів шукаємо там, де ще ніхто не дивився
pages прізвища, виписані оком у облік перевірити, чи не знаходили це раніше
records учасники розібраних записів (батьки, восприємники, свідки) шукаємо роль, а не згадку

Роль і тип акту — поля, а не читання очима. У where=records є role (father, godfather, deceased…) і rtype (birth, death…): «хто був батьком із цим прізвищем» і «хто був восприємником» — різні питання, і саме заради них записи розбирають структурою. Фільтр, який не діє в обраному сховищі, відмовляється, а не мовчить: тихо ширша вибірка виглядає як звужена, і нуль по ній закриває напрям, якого не перевіряли.

Вісь: прізвище чи місце. axis=placepages і records) шукає не по прізвищу, а по поселенню — «які акти згадують це село, байдуже під яким прізвищем». Місце тримається двічі: на записі (де відбулась подія) і на учаснику (звідки він), і в шлюбі це різні села.

nysh records grep "Ковальський" --role father --json
nysh records grep "Городківка" --axis place --json

Шифра — це запит іншого роду

catalog.search (nysh find) розрізняє два питання. «Мястківка» — це «де взагалі є щось про моє село», і відповідь на нього перелік. «127-1078-1662» — це адреса конкретної справи, і відповідь одна.

Доти на адресу відповідав текстовий пошук, тобто не відповідав ніколи: три числа поспіль не трапляються в жодному заголовку. Нуль давала й «ДАВіО-172-4-112» — рядок, який пошук САМ друкує в кожному хіті як адресу справи, тобто показане не можна було набрати назад.

Адресний маршрут питає інші місця й у такому порядку: бібліотека (справа може вже лежати на цій машині) → реєстр опису (заголовок, роки, аркуші, звідки взяти) → каталоги, які вміють шукати за шифрою.

🔴 Це видно у відповіді, і мусить бути видно: coverage.searched містить рівно те, що питали, а попередження address_route каже прямо, що повнотекстового пошуку по каталогах НЕ було. Джерело, яке вміє лише текстом, іде в unavailable з причиною — його нуль не є нулем про цю справу.

Якщо адреса не знайшлась ніде, запит іде текстом як звичайний: рядок може бути лише схожий на шифру (дата «1858-03-14»). Примусово текстовий маршрут — nysh find … --text.

🔴 Нуль у кожному з трьох означає різне. «Немає в decode» — немає в машинному тексті, який міг це слово скалічити. «Немає в pages» — ніхто не виписував, і це майже нічого не каже про джерело. «Немає в records» — ніхто не розбирав записи, навіть якщо прізвище там є.

Тому знаменник у відповіді різний за змістом, і в звіті його треба називати разом зі сховищем, а не окремо.


Де лежить текст: стор

Пошук по декоду читає не теки прогонів, а текстовий стор — один файл SQLite (data/derived/text_store.sqlite) із сирим текстом сторінок, рядками й рамками, тригамним індексом і готовими кандидатами: склейки через рядок і за колонкою вже в них. Обхід тек прогонів на великому корпусі коштує хвилини, стор відповідає за секунди. search.run бере стор сам, щойно той покриває область, і каже про це в coverage.backend; прогони поза стором ідуть старим шляхом.

nysh text state            # скільки прогонів у сторі, скільки застаріло
nysh text index            # догнати після нових прогонів
nysh text state --verify   # довести однорідність після обірваної перебудови

🔴 Відбиток правил склейки, який друкує state, — це заява, а не доказ. Якщо перебудову обірвано (брак пам'яті, Ctrl+C, закрите вікно), приймач — --verify: він перераховує кандидатів звірених сторінок чинним кодом і називає прогони, де вони розійшлись. Код виходу 1 означає, що пошук по цих прогонах відповідає не тим, чим по решті.

Регекс по сирому тексту замість rgnysh text grep; усі канали пошуку роду разом, із журналом — nysh text find (див. surface.md).


Що робить пошук по декоду сам

Нормалізація обох сторін

Запит і текст зводяться до спільної форми: кирилиця й латинка, історичні літери, типові плутанини самого рушія. Тому запит кирилицею знаходить латинський запис і навпаки — а точний підрядок не знайшов би нічого.

🔑 Гніздо написань імені

Нормалізація вирівнює письмо, але не знає, що Явдоха — це Євдокія. Тому запит іменем додатково розкривається довідником церковних імен: шукається все гніздо, а не лише набране слово.

Чому це не косметика — заміряно на живому rapidfuzz при порозі 80:

набрали у книзі стоїть бал знайшлось би без довідника
Явдоха Євдокія 61.5 ні
Осип Іосиф 66.7 ні
Оксана Ксенія 66.7 ні
Семен Симеон 72.7 ні
Ганна Анна 88.9 так

Тобто для більшої частини гнізда це різниця між знахідкою й мовчазним нулем, а не «трохи нижче порога».

Прізвища так НЕ зводяться. «Гончаръ» і «Ганчаръ» можуть бути різними родами; їх зближує тільки fuzzy, і рішення лишається за оком.

Вимикач — --no-given. Він потрібен рівно для одного: порівняти, що додав довідник, коли хітів забагато.

🔑 Написання прізвища з профілю

Профіль простору знає прізвище десятком написань (відмінки × орфографії), і пошук бере їх усі, коли запит справді про це прізвище. Одне число знаменника замість десятка окремих нулів.

Що це дає, заміряно при порозі 80 на профілі «Сікорський»:

у книзі стоїть лише набране + написання профілю
корскаго (з'їдено початок) 58.8 85.7 ← інакше не знайдеться ніколи
Сикорскаго 87.5 100.0
Сикорского 87.5 100.0

Рушій з'їдає початок слова частіше, ніж кінець, і саме такий уламок лишається поза порогом, поки шукають однією формою.

⚠ Чужий запит форми роду за собою не тягне: пошук по назві села лишається пошуком по назві села. У відповіді видно, чиї написання підмішано.

Вимкнути — --no-profile.

🔑 Ранг: чуже слово опускає хіт, але не забирає

Профіль тримає confusers (сусідні прізвища на той самий хвіст) і rank_down (іменовані регекси службових формул). Хіт, який чуже слово пояснює краще, ніж шукане прізвище, падає вниз видачі з названою причиною — і лишається у видачі.

🔴 Це не фільтр, і різниця тут не косметична. Ціна помилки асиметрична: хибний плюс людина відсіює за секунди, а пропущений аркуш не відсівається нічим, бо його ніколи не буде в списку.

Навіщо взагалі: бал сам по собі не відділяє рід від рутини. Замір приватного конвеєра — 60 сильних кандидатів, з них рубрика книги 26, сусідній рід 19, шуканий рід нуль, і всі три верхні місця за балом займала рубрика.

rank_down — регекси, і порівнюються вони з нормалізованим текстом (щ→sc, ч→c, и/і→i, кирилиця в латинку). Правило, написане кирилицею, не спрацює ніколи; пошук каже про це вголос, а не мовчить.

⚓ Канал імен: коли прізвища в тексті вже немає

--anchors шукає рядки, де ім'я й по батькові роду стоять поруч. Це інший вхід, а не ширший поріг: рушій калічить довге прізвище сильніше, ніж коротке формулярне слово, тож у приватному конвеєрі обидва золоті рятунки мали по прізвищу бал нуль, а по батькові той самий рушій прочитав дослівно.

Заміряний виграш там: прізвищний канал 61-72%, разом із якорями 85-91% (знаменники різні — 97, 75 і 73 аркуші, і число без знаменника не переносьте).

Три правила, кожне куплене заміром:

  • пара обов'язкова. Одиночне «Іосифовъ» стоїть у метриці на кожному аркуші; канал, який приймає одинака, знаходить усе, тобто нічого;
  • вікно років, і воно РІЗНЕ. Ім'я корисне, поки людина жива; по батькові — поки живі її діти (нар+18 … нар+100), тобто воно переживає носія на покоління. Без прив'язки до років якір вироджувався до чверті рядків книги;
  • без дати особа якір не звужує і за замовчуванням у нього не йде — але про це сказано числом, а не мовчки.

Якорі беруться з поля kin профілю, а канон осіб (якщо він у просторі є) їх доповнює. Канал без kin мовчить — це не нуль.

⚠ Працює в межах справи: вікно береться з її років. Поза справою операція відмовляється, а не шукає наосліп.

🎯 Самоперевірка: чи бачить пошук те, що око вже знайшло

--selfcheck міряє recall на аркушах, де прізвище виписала людина, дивлячись на скан. Це третє з трьох чисел, без яких нуль не є відповіддю.

  • знаменник — позитиви ∩ фактично декодовані сторінки: аркуш, який око знає, а рушій не читав, у знаменник не входить, інакше ми міряли б повноту декоду;
  • два числа, не одне: знайдено і подано на око. Розрив між ними — те місце, де вимірювач починає брехати у свій бік;
  • позитивів немає → відмова, а не нуль. Порожній recall і нульовий recall означають протилежне.

🧾 Чим справу вже шукали

Пошук у межах справи лишає слід: запит, поріг, скільки знайшов, якими моделями. Якщо справу вже прочісували іншим рушієм, наступний пошук каже про це — бо «обшукано» протухає мовчки: та сама справа тим самим запитом дала +8 аркушів роду від самої лише зміни моделі.

🔑 Побутове ім'я — окремий шар, вимкнений

Хрестильне ім'я за святцями й побутове, яким людину звали далі, бувають різними іменами: у метриці Васса, у всіх пізніших документах Анна. Це не варіанти написання — це дві різні святі (бал 44.4), і жодна фонетика їх не зблизить.

Такі пари живуть окремим шаром і вмикаються прапорцем --folk. За замовчуванням шар вимкнений: на чужому матеріалі він злипає двох різних жінок, а хибний позитив коштує дорожче за пропущений — пропущене шукають далі, а зліплене вважають знайденим і закривають напрям.

🔴 Кожен хіт несе stem_origin. Значення folk означає, що зв'язок біографічний, а не орфографічний: така знахідка сама по собі нічого не доводить, доки не показано, що йдеться про ту саму людину.

Свої пари додаються в просторі — config/folk_names.yaml:

Мотрона: [Матрёна, Мотря]

🔑 Склейка розірваного прізвища

Прізвище на межі рядка розривається переносом, і жодна половинка сама по собі не схожа на ціле. Пошук тримає хвости трьох попередніх рядків і пробує зшити їх із початком поточного.

Чому саме трьох, а не одного: у табличних бланках (сповідки, ревізії, формуляри) сегментація зшиває колонки, і половинки розходяться на два-три рядки. З вікном в один рядок такі записи не знаходяться взагалі.

Далекі склейки суворіші за сусідню. Через рядок-два половинки злітаються вже не за законом переносу, а випадково — і без цієї суворості пошук почав би знаходити прізвище в будь-якому шумі.

Практичний наслідок для тебе: не роби склейку руками. Побачивши в тексті обірваний токен, не дописуй його — це вже зроблено, і зроблено з порогами, які перевірені на контрольних запитах.

🔑 Вікно замість рядка

search.run має поле context (типово 1): кожен хіт приходить із сусідніми рядками, а не голим рядком.

Це не зручність. Рядок сам по собі не розрізняє прізвищ зі спільним коренем, а в одній парафії їх буває кілька — заміряно на метриках одного села: 78 кандидатів верхівки розклались на три різні роди з тим самим коренем плюс причт, і за самим рядком вони зливаються в купу однаково правдоподібних хітів.

Розрізняє їх сусідство:

    ↑ Балтскаго Повѣта Села Ковалевскаго      ← географія стоїть вище
    » Дьячисъ Григорій Ковалевскій сынъ Свя   ← сам хіт обривається
    ↓ щунническій получившей удостость        ← слово продовжується нижче

Вікно розсувне, а не ±N рядків. Огризок контекстом не вважається: сусідній рядок «на», «и», «3» не пояснює нічого, тож вікно розсувається далі, доки не набереться змістовного. Без цього контекст виявляється порожнім рівно там, де він найпотрібніший — між колонками щільного формуляра.

🔑 Другий голос поруч

Якщо справу читано двома рушіями, до хіта додається читання другого на тому самому рядку:

    » Дьячисъ Григорій Ковалевскій сынъ Свя
      2-й голос: Дьячисъ Григр̆ Ковааевскіи сынъ све

Збіг голосів означає надійне читання; розбіжність — сигнал, а не шум. Рушій із мовною моделлю підставляє правдоподібне слово, а другий судить кадри незалежно й калічить локально, зберігаючи корінь. Розбіжність саме на прізвищі (як вище: «Ковалевскій» проти «Ковааевскіи») означає, що ознака в пікселях — і вирішувати має око, а не третій алгоритм.

🛑 Але два голоси підтверджують читання, а не приналежність. Те, що обидва прочитали прізвище однаково, не робить особу вашою.

Поріг схожості

thresh за замовчуванням 80 зі 100. Нижче — більше шуму, вище — губляться саме ті спотворені форми, по які пошук і кликали.

🔴 Не піднімай поріг, щоб «прибрати шум». Рушій калічить середину слова сильніше за краї, тож найцінніші кандидати мають бал ближче до порогу, ніж очевидні. Замість підняття порогу — ранжуй видачу (workflows.md, крок 3).

⚠ Окремо: короткі токени відсіюються довжиною (одиночний — від чотирьох літер, пара — від шести сумарно). Спроба додатково глушити «рядки-огризки» з волокон паперу перевірялась заміром і дала 0.1% менше кандидатів при нулі втрачених хітів — тобто фільтр не потрібен, а от глушіння такого рядка разом із його хвостом з'їдало справжні склейки.


Чого в пошуку немає

Це чесна межа версії, а не прогалина в документації.

немає що це було б що робити натомість
канал за топонімом у декоді «чи бачить машинний текст село, яке точно там є» — знаменник для нуля звичайний запит назвою села; географію звіряти з каталогом. У виписаному й розібраному топонім має власну вісь — axis=place
канал за складом двору пошук родини за сусідами, коли прізвище скалічене читати сторінку цілком навколо знайденого
канал за станом + іменем «однодворець + Іосиф» — фаззі по обох половинах два окремі запити, звіряти сторінки
розрив між колонками окремим каналом коли прізвище розірване не переносом, а версткою склейка в три рядки плюс вікно контексту покривають більшу частину цього
перевірка віку/дат кандидата «чи міг цей запис бути нашою особою» арифметика документа вручну; цифри з декоду не факт

🛑 Якщо людина просить «прочешіть повіт» або «знайдіть усі згадки роду в фонді» — скажи прямо, що в цій версії є два канали (прізвище й імена) і три сховища. Обіцяти багатоканальний прочіс, а потім видати нуль з одного каналу — помилка, яку дорого виправляти: цей нуль виглядатиме як вичерпна відповідь.


Як читати нуль по кожному сховищу

Разом із результатом приходить попередження. Читай його, а не лише число:

  • zero_with_denominator — «не знайшлось у N прогонах (M сторінок)». Це справжній нуль, і в звіті він так і пишеться: з N і M.
  • no_denominator — не шукало ніщо. Звітувати «немає» не можна.
  • попередження про метод — якщо сторінки заносились із позначкою «читав лише декод», їхній вміст успадкував помилки рушія.

І головне, що не приходить попередженням: нуль у decode не покриває сторінок, яких ніхто не читав машиною. Скільки їх — питай cases.list і pages.status, а не пошук.