Satzrecord
Volle Datensätze, abstrakte Schlüssel, DuckDB und Polars 2.0, NLI über normalisierte Sätze
Die Filterung liest nicht mehr Postings aus Zeichenketten, sondern Attribute mit numerischen Schlüsseln (key_id), die ein Vorverarbeitungsschritt je Seite, Absatz, Satz und Klausel schon berechnet hat: Personen-Id, Ortsschlüssel, Begriff, Datum als Tagesintervall, Entität, Lesart des Gebrauchs. DuckDB scannt, Polars 2.0 hält die Treffermenge einer Sitzung im Speicher, und die Inferenz über Sätze (streng, Standard, Abduktion, NLI) liest dieselben normalisierten Klauseln.
Status der Abschnitte: gebaut heißt, es steht im Code und im Test; Entwurf heißt, es ist beschrieben, aber noch nicht gemessen. Die Messung am Kármán-Nachlass steht im letzten Abschnitt.
Ausgangslage
Der Filter (Filterung) arbeitet im Kern schon über Schlüssel: der Fast Index bildet einen Wert der Frage auf Schlüssel ab, theorie.sql übersetzt den Suchbaum in eine Anweisung mit drei Werten. Drei Dinge hielten ihn zurück:
- Die Schlüssel sind Zeichenketten (
fx_post.schluessel), und die Lesart des Gebrauchs, die Satzrolle und der Zeitbezug werden bei jeder Anfrage aus den lebenden Stanza-Tabellen berechnet, mitLIKE,regexp_matchesundstarts_with. - Volle Datensätze bleiben außerhalb. Satz, Token, Merkmale und Abhängigkeiten liegen in
*_stanza.duckdb, Absätze infq_paragraphs; die Anweisung holt den Text nur für die Anzeige. - Die NLI-Stufe liest Rohtext. Die Normalisierung der Famulus Normal Form (FNF) gilt nur der FNF-Stufe, und ihr Index ist auf den Kármán-Nachlass festgelegt.
Das Modell: Records und Schlüssel
Der Satzrecord ist ein Sidecar {coll}_satz.duckdb, neu baubar aus {coll}_fql.duckdb (fx_*, fq_*) und {coll}_stanza.duckdb. Er wird zusammen mit beiden verbunden (ATTACH … AS sr); die Evidenzobjekte (fx_objekt, fx_enthalten, fx_traeger) bleiben, wo sie sind.
| Tabelle | Inhalt |
|---|---|
sr_page, sr_paragraph |
der volle Text mit Elternschlüsseln, in der Objekt-Id der Evidenzobjekte (P:, A:) |
sr_sentence |
Text, Sprache und die Stanza-Darstellung des Satzes: tokens als Liste von Strukturen (Form, Lemma, UPOS, XPOS, Merkmale als Map, Kopf, Deprel, Zeichenoffsets, Entität); dazu text_norm, der renormalisierte Satz |
sr_clause |
Klauseln aus dem Abhängigkeitsbaum (ccomp, advcl, conj, parataxis), mit eigenem text_norm |
sr_key |
key_id (UBIGINT, Hash aus Art und kanonischem Wert), Art, kanonischer Wert, Bezeichnung |
sr_alias |
gefaltete Oberflächenform → key_id, mit Quelle und Sicherheit. Der einzige Ort, an dem eine Schreibweise aufgelöst wird, und nur beim Klassifizieren |
sr_concept_up |
Begriffshierarchie als transitive Hülle: ein Oberbegriff ist ein Schlüssel-Nachschlag |
sr_attr |
ein Attribut eines Objekts: Art, key_id, Rolle, Intervall lo/hi, Tokenspanne, lesart, Kanal, Sicherheit |
sr_review |
Zeilen, die nur eine Person bestätigen kann (gebeugte oder entstellte Namensformen) |
fnf_* |
die Rahmen und Literale der FNF je Satz und Klausel (Schema aus fnf/schema.sql) |
Schlüssel statt Zeichenketten
key_id ist der 64-Bit-Hash aus Art und kanonischem Wert. Er ist deterministisch (ein Neubau ändert keinen Schlüssel) und der Bau prüft, dass er eindeutig ist. Eine Anfrage vergleicht nur noch Zahlen: der Fast Index klassifiziert den Wert der Frage in sr_alias und sr_key, die Anweisung verbindet sr_attr(kind, key_id) mit sortierten Schlüsseln. Zeichenketten treten nur an zwei Stellen auf, beim Klassifizieren und in der Anzeige.
Art (kind) |
Schlüssel | Quelle beim Bau |
|---|---|---|
person, place, institution |
Einzelding des Registers (ind_id) |
Verknüpfungen fq_mentions, Lemmaklassen des Registernachnamens, Lesart des Gebrauchs |
entity |
Cluster je Collection (Entitätstyp und gefaltete Form) | Stanza-NER ohne Registerverknüpfung |
concept |
Begriff des FNF-Wörterbuchs (c:…, wn:…) |
Lemma und Sprache → Begriff; die Hülle in sr_concept_up |
date |
Tagesintervall [lo, hi], kein Wert |
Datumsangaben im Satz (EDTF → Intervall); das Briefdatum als Rolle kontext |
dep |
Satzrolle deprel und gefaltetes Lemma |
Abhängigkeitsbaum |
lemma, form, period, topic, geschlossene Listen |
wie bisher | fx_post |
Die Lesart wird beim Bau berechnet
Die Lesart des Gebrauchs entscheidet je Vorkommen eines Namens, ob es die Person meint, einen Eponym, ein Beiwort oder eine Literaturangabe. Sie liest nur das Token, seinen Satz und die Registerverknüpfungen des Satzes; nichts davon hängt von der Frage ab, außer welche Person gesucht wird. Deshalb entsteht je (Token, Person) eine Zeile in sr_attr mit lesart, und die Anfrage liest lesart IN (…). Die Faktoren der Theorie (gebrauch/person.yaml) bleiben, ihre LIKE- und Regex-Ausdrücke sind durch Prädikate über Strukturspalten ersetzt: die Basis der Deprel (split_part(deprel, ':', 1)) und die Form des Tokens als beim Bau gesetzte Merkmale (Bindestrichgefüge, Schlusstrennstrich).
Gebeugte Formen: bestätigt, nicht still gewählt
Stanza führt Hitlers oder Prandtls manchmal unter einem eigenen Lemma. Der Bau bildet solche Formen so auf die Person ab:
- Kandidat ist eine Form, die der Registernachname mit einem geschlossenen Satz von Flexionsendungen je Sprache erreicht (de
-s,-ens; en-s; la-i,-o,-um,-ii). - Sie wird angenommen (
cert = flexion), wenn Stanza die Tokens überwiegend alsPROPNliest und mindestensLEMMA_MINTokens die Form tragen. - Sonst und für entstellte Schreibungen im Abstand 1 (Prandl) geht die Form in
sr_review. Sie zählt nicht, bis ein Eigentümer sie bestätigt; die Queue nennt Form, Häufigkeit und einen Beleg.
Filterung: DuckDB und Polars 2.0
DuckDB bleibt der Speicher und führt die eine Anweisung aus (theorie.sql). Die Blätter verbinden sr_attr(kind, key_id); ein Datumsblatt ist ein Intervallvergleich auf lo/hi; die Satzrolle und die Lesart sind gespeicherte Spalten. Die drei Werte (erfüllt, verletzt, unentschieden), die Stufen und der Raumbeweis famulus-raum/1 ändern sich nicht.
Polars 2.0 (Release) ist die schnelle Zwischenschicht einer Sitzung (evidenzraum/sitzung.py):
- DuckDB gibt die Treffermenge mit ihren
sr_attr-Zeilen als Arrow-Tabelle heraus (.arrow()), Polars liest sie ohne Kopie alsLazyFrame. - Verfeinerung, Facettenzählung, Negation und strenge Blätter laufen im Speicher, ohne neuen SQL-Durchgang.
- Die vollen Records für die Inferenz (
text_norm, Klauseln, FNF-Literale) werden in einem Stapel je Treffermenge geholt.
Vier Eigenschaften von Polars 2.0 sind Entwurfsentscheidungen:
| Eigenschaft | Folge |
|---|---|
collect() nutzt die Streaming-Engine, die nach join und group_by keine Reihenfolge garantiert |
wo die Reihenfolge zählt (Raumbeweis, Rangfolge), maintain_order=True oder Sortierung nach obj_id |
neuer Map-Typ |
Stanza-Merkmale kommen als Map(String, String) an: map.get("Case") |
strengere Typen, collect_schema() |
das Schema des Sidecars wird vor jedem Lesen geprüft; Bau-Gate |
| Auslagern auf die Platte ab etwa 80 % des Speichers; Joins und Gruppierungen lagern noch nicht aus | der Bau arbeitet in Stapeln je Dokument (M1, 16 GB) |
Welche Engine? Polars SQL ist seit 2.0 eine gleichrangige Schnittstelle, und die veröffentlichten Benchmarks (TPC-H, TPC-DS) führen Polars vor DuckDB 1.5.6. Unsere Anfragen sind klein und wählen nach Schlüsseln; ob der Vorsprung dort bleibt, ist offen und wird gemessen (Messung).
NLI über normalisierte Sätze
Renormalisierung der Terme
Der Bau schreibt zu jedem Satz und jeder Klausel text_norm. Die Regeln sind die der FNF (Famulus Normal Form), auf die Oberfläche angewandt:
- eine Nennung der Person wird zum Standardnamen des Registers, der Schlüssel bleibt daneben;
- ein Datum wird zum ISO-Intervall (
1933-03→1933-03-01/1933-03-31); - ein Lemma mit Begriff wird zum Stichwort des Begriffs (
Vertrages→Vertrag); - Konversen und komplementäre Gegensätze werden auf eine Richtung gebracht (
normalise.py); - Quantoren und Modalität bleiben ausdrücklich.
Die Sonde durchläuft dieselbe Normalisierung. Prämisse und Hypothese teilen dann ein Vokabular, und ein Kreuz-Encoder muss weder Namen noch Zeitangaben auflösen.
Eingabemodi
Die NLI-Stufe in famulus-logic bekommt drei Modi: raw (wie heute), norm (Satz und Sonde renormalisiert) und clause (je Klausel ein Paar; das Satzurteil ergibt sich aus den Klauselurteilen nach der Vier-Zellen-Logik von match.classify). Die Messung vergleicht sie am selben Needle-Satz.
Reihenfolge der Kaskade
- Schlüsselfilter: die Treffermenge des Satzrecords.
- FNF streng (N/P) über
match_sqlgegen den Sidecar. - Schwacher Durchgang: Standardregeln (D), Implikatur (I), Abduktion (A).
- NLI über normalisierte Klauseln.
- Schnelles, dann starkes LLM.
Erweiterte Inferenz: Abduktion über Sätze
Gebaut und synthetisch getestet; an der Scheibe ohne Schluss (siehe Messung). Die 13 abduktiven Regeln (derived.yaml, Grad A) schließen heute innerhalb eines Satzes. Die Erweiterung sucht die verdeckte Prämisse in den Schwestersätzen desselben Trägers (des Briefs): ein Satz ist Kandidat, wenn er mit dem Anker einen Schlüssel teilt (dieselbe Person, ein überlappendes Datumsintervall, derselbe Begriff). Die Auswahl läuft in Polars über sr_attr.
Das Ergebnis ist nie streng. Es trägt den Grad A, die Stützungsmarke der Argumentrekonstruktion (linked, convergent, a_suffices), die offenen Rivalen (andere Erklärungen, die dieselbe Beobachtung erklären) und die Verteidiger, die es kippen. Neue Regeln kommen als Daten (derived.yaml, strength.json), nie als Code.
Messung
Die Messung läuft an einer Scheibe von 8 Dokumenten des Kármán-Nachlasses (2.269 Sätze), nicht an den 15.976 Briefen. Der Netzwerkspeicher liefert etwa 400 KB/s; die ganze Sammlung ließ sich nicht lesen. Es gibt kein Gold für die Scheibe: der Filter wird gegen eine Referenz durch Tokengleichheit verglichen (Recall und Präzision gegen eine Zählung, kein Urteil über die Wahrheit), die NLI-Stufe gegen die Needle-Suite und gegen interne Konsistenzproben. Alle Zahlen sind Zahlen dieser Scheibe.
Bau
Der Sidecar der Scheibe baut in 18 s (2.269 Sätze) und wiegt 25 MB. Eine Hochrechnung auf die 594.000 Sätze der Sammlung ergibt rund 80 Minuten; das ist eine Extrapolation aus einer einzigen Messung und nicht gemessen. Die Größe lässt sich nicht hochrechnen, weil die Scheibe von Fixkosten (Tabellenköpfe, Schlüsseltabelle) dominiert wird.
Filter
Fünf Personen als Erwähnung, eine als Absender; die Anweisung kommt aus theorie.ausfuehren, die Referenz aus Tokengleichheit über die Stanza-Tokens. Median der Anweisung 24 ms (Maximum 26 ms, fünf Wiederholungen).
| Frage | Briefe im Raum | Briefe der Referenz | Präzision | Recall |
|---|---|---|---|---|
| Erwähnung Kármán | 62 | 69 | 1,00 | 0,90 |
| Erwähnung Amira | 8 | 9 | 1,00 | 0,89 |
| Erwähnung Wattendorf | 6 | 5 | 0,83 | 1,00 |
| Erwähnung Allard | 3 | 4 | 1,00 | 0,75 |
| Absender Kármán | 64 | 31 | 0,48 | 1,00 |
Wie die Abweichungen zu lesen sind, soweit sie geprüft wurden:
- Absender Kármán. Der Raum enthält 64 Briefe, davon sind 31 belegt, 13 indiziert und 20 offen. Die 31 belegten sind genau die Referenz (alle Briefe, deren Absender die Person ist); die Präzision 0,48 zählt die offenen Kandidaten mit, die eine spätere Stufe prüft. Das ist die Auslegung des Raumes (Kandidaten zuerst, Beweis danach), kein Fehler der Schlüssel.
- Allard, Amira (je ein Brief fehlt). Geprüft: Die Sätze tragen den Personenschlüssel richtig (
P003850,P005524). Der fehlende Träger ist je der Brief…/001mit leerem Register-Kopf (Absender, Empfänger und Datum fehlen; sein Text ist ein Mappenschild wie „1.12 Allard , Emile 1928 …“). Der Brief fällt in der Kopfstufe heraus. Das ist eine Eigenschaft der Daten und der Stufenordnung, nicht des Satzschlüssels. - Kármán (sieben fehlen), Wattendorf (ein Brief zu viel). Nicht untersucht. Bei Kármán stehen 18 Briefe im Raum nur als offen; ob die fehlenden dazugehören, ist nicht geprüft.
Eine Frage blieb ohne Zahl: für Abody-Anderlik bricht die Klassifikation im Fast Index mit FileNotFoundError ab, weil sie die Fertigkeitsdatei /Users/openscience/ai-mac-studio/pdai/skills/register/identitaet/SKILL.md liest, die auf diesem Rechner nicht existiert. Das ist ein Umgebungspfad, nicht der Satzrecord; die Zeile steht im Bericht als Fehler und wurde nicht ersetzt.
Engine
Dieselbe Schlüsselabfrage (Objekte mit einem Personenschlüssel, 188 Objekte) in DuckDB und in Polars 2.0.0 (DuckDB 1.5.6) auf der gleichen Arrow-Tabelle. Die Ergebnismengen sind gleich.
| Weg | Median |
|---|---|
| DuckDB, gespeicherte Tabelle | 0,77 ms |
| Polars, Arrow-Export plus Filter | 8,2 ms (der Export überwiegt) |
| Polars SQL, Export ausgenommen | 0,44 ms |
| Verfeinerungsschritte der Sitzung (im Speicher) | 14,9 ms |
Auf dieser Größe lohnt Polars nur als Sitzungsspeicher für wiederholte Verfeinerung, nicht als Ersatz für den ersten Scan. Ob sich das bei 594.000 Sätzen ändert, ist nicht gemessen. Die kompilierte theorie-Anweisung nutzt DuckDB-Funktionen (hash, list_contains) und läuft nur in DuckDB.
NLI
Needle-Suite (703 Aufgaben, 1.545 Paare, mDeBERTa-xnli):
raw gegen norm
| Modus | streng | grob | Über-Folgerung | Monotonieverletzung | Wanduhr |
|---|---|---|---|---|---|
raw |
0,475 | 0,687 | 0,363 | 0,235 | 215 s |
norm |
0,471 | 0,681 | 0,354 | 0,247 | 136 s |
Die Normalisierung bringt auf der Needle-Suite nichts: der Unterschied von −0,4 Punkten liegt im Rauschen, und die abduktive Aufgabe bleibt bei 0 (ein Klassifikator ohne Regeln kann sie nicht lösen). Das ist kein Beleg für die Normalisierung und keiner dagegen: die Needle-Sätze sind synthetisch, ohne Stanza-Parse, und die Normalisierung der Scheibe findet in ihnen kaum Namen, Daten oder Begriffe. clause ist hier nicht gemessen, weil es ohne Klauseln dasselbe wie norm liest. Der Zeitunterschied (215 s gegen 136 s) ist nicht der Modus; die Läufe liefen unter verschiedener Last und sind nicht vergleichbar.
Echte Sätze der Scheibe (150 Mehrklauselsätze, im Median 3 Klauseln je Satz, keine menschliche Referenz):
- Satz gegen den Hauptsatz seiner selbst (er muss ihn folgern):
raw93,3 %entailsoderequivalent,norm88,7 %. Die Normalisierung verschlechtert hier leicht und erzeugt mehr Widersprüche (1 gegen 6). Warum, ist nicht untersucht; der Verdacht, dass die ersetzten Namen und Daten die Oberflächenübereinstimmung stören, ist nicht geprüft. - Satz gegen den Hauptsatz eines anderen Satzes desselben Dokuments:
rawundnormändern 4,7 % der Etiketten,clauseändert 18 % gegenüberraw. Ohne Referenz sagt das nur, dassclauseanders liest, nicht, dass es besser liest. - Relevanz-Gate. Ohne das Gate erzeugte der Klauselmodus 58 Widersprüche, mit dem Gate 2. Das Gate zählt einen Widerspruch nur aus einer Klausel, die mit der Probe ein Inhaltswort teilt. Ob die 56 entfernten falsch waren, ist nicht von Hand geprüft; sie entstehen aus Klauselpaaren ohne gemeinsames Inhaltswort, was für unabhängige Aussagen spricht.
Abduktion
Auf der Scheibe gibt es 4.385 Schwesterpaare in 154 zusammenhängenden Gruppen (982 Sätze, größte Gruppe 37). Die Regeln von derived.yaml erzeugten über den gepoolten Literalen 0 Schlüsse. Die Querableitung ist durch synthetische Tests belegt (8 Tests), an echten Daten aber nicht gezeigt: acht Dokumente reichen offenbar nicht, damit eine abduktive Regel mit Sätzen aus zwei Stellen feuert, oder die Regeln feuern im Kármán-Korpus nicht. Welche der beiden Erklärungen stimmt, ist offen. Der Recall der abduktiven Needle-Aufgaben bleibt 0 für den NLI-Klassifikator und 16,7 % für die FNF-Stufe, unverändert.
Entfernt und neu
Entfernt: evidenzraum/baum.py, evidenzraum/traeger.py, scripts/audit_evidenzraum.py, die Funktionen beweisen und facetten_der_frage sowie die alten Stufen in lauf.py und entfaltung/ops.py; die Präfix- und LIKE-Faktoren in identitaet.py und gebrauch.py. Der Orchestrator-Partitur famulus_evidenzraum_agent (außerhalb dieses Repos) verweist noch auf die entfernten Ops und muss angepasst werden.
Neu: satzrecord/ (Schema, Bau, Normalisierung, Klauseln, CLI), fnf/abduktion.py, evidenzraum/sitzung.py, in famulus-logic classifiers/eingabe.py und die Skripte satz_pilot.py und satz_nli_pilot.py.
Die Ergebnisse trägt scripts/satz_pilot.py in runs/satz-pilot.json ein; die Zahlen dieses Abschnitts stammen daraus. Wiederholung: PDAI_COLLECTIONS=/tmp/pdai/sample uv run python scripts/satz_pilot.py --ohne-bau.