Пошук: канали, склейка, поріг¶
Пошук у прочитаному — не grep. Машинний текст скоропису XVIII–XIX ст. має
частку помилок у десятки відсотків, і потрібне прізвище виходить із рушія
спотвореним до невпізнання. Тому шукається схожість нормалізованих токенів,
а не підрядок.
Це документ про пошук у вже прочитаному. Де взяти матеріал, якого в
просторі ще немає, у які джерела йти і в якому порядку — where-to-dig.md.
Три сховища, а не три «канали»¶
search.run має поле where, і це вибір сховища, а не способу пошуку:
where |
що шукає | коли брати |
|---|---|---|
decode (дефолт) |
машинний текст усіх прогонів | шукаємо там, де ще ніхто не дивився |
pages |
прізвища, виписані оком у облік | перевірити, чи не знаходили це раніше |
records |
учасники розібраних записів (батьки, восприємники, свідки) | шукаємо роль, а не згадку |
Роль і тип акту — поля, а не читання очима. У where=records є role
(father, godfather, deceased…) і rtype (birth, death…): «хто був
батьком із цим прізвищем» і «хто був восприємником» — різні питання, і саме
заради них записи розбирають структурою. Фільтр, який не діє в обраному
сховищі, відмовляється, а не мовчить: тихо ширша вибірка виглядає як
звужена, і нуль по ній закриває напрям, якого не перевіряли.
Вісь: прізвище чи місце. axis=place (у pages і 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 означає, що пошук по цих прогонах
відповідає не тим, чим по решті.
Регекс по сирому тексту замість rg — nysh 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, а не пошук.