Fix multiword terms created from the reading pane silently failing to match (context-sensitive parsers)#700
Conversation
renaming finalurl to url closes LuteOrg#692
Updated sentence translation handling to use URIs instead of dictionaries, ensuring proper context for translations. Adjusted related functions to accommodate the new structure.
Add tests for term handling with pretokenized text and zws.
Added tests for find_or_new method to handle pre-tokenized text and ensure it correctly finds existing terms or creates new ones without re-parsing.
|
Thanks for the PR, appreciated. This is odd as I recall adding code to not reparse terms that are created via the reading pane ... this was a big change, in #347 :-) That PR changed the code to create "status 0 (unknown)" terms during the parse, so things would be handled correctly and avoid the problem mentioned in this PR. I thought I had it all covered! But bugs happen, so thanks for checking! |
|
My prior comment was actually not useful here -- that old PR was to prevent re-parsing of terms from the reading window, but in this case, the user is creating a new multi-word term from the terms in the reading pane. I still thought that the code handled this correctly! If it didn't, that's a big error, and it's surprising it went so long undetected. Thanks, checking :-) |
jzohrab
left a comment
There was a problem hiding this comment.
Some small changes, and a bigger one with the javascript change and the '*' url hack. Cheers!
| let url = userdict.replace('[LUTE]', lookup); | ||
| url = url.replace('###', lookup); // TODO remove_old_###_placeholder: remove | ||
| if (dict.dicttype == "popuphtml") { | ||
| if (url[0] == '*') { |
There was a problem hiding this comment.
This is concerning, the code was changed to use the "dicttype", and not rely on special tokens within the URL. Changed in #331. This code as given here should never work.
There was a problem hiding this comment.
Well, not "should never work", "would never work".
| assert_sql_result(sql, ["集めれ; 集めれ"], "have term") | ||
|
|
||
|
|
||
| @pytest.mark.term_case |
There was a problem hiding this comment.
This isn't a "term_case" test :-)
|
|
||
|
|
||
| @pytest.mark.term_case | ||
| def test_new_term_without_zws_is_parsed_from_scratch_as_before(japanese): |
There was a problem hiding this comment.
Not a term_case test.
"_as_before" should be removed, this test will be read as a standalone thing by future maintainers, so references to history gets a bit confusing (unless this is a characterization test, which is a different kind of thing)
Fix: multiword terms created from the reading pane can silently fail to match (context-sensitive parsers)
Base:
3.10.1(09bb17b)Summary
Creating a multiword term by selecting text in the reading pane (the
documented, supported way to do it) can silently produce a term that
never highlights again when reading — it just falls back to the
individual single-word tokens, with no error shown anywhere.
This reproduces reliably for Japanese (MeCab), and will affect any
other parser whose tokenization is context-sensitive.
Repro
...それはそれで問題になる...それ | は | それ | でand create a new term from them.
Actual: they render as 4 separate tokens again, as if the term
didn't exist.
Inspecting the DB confirms the term was saved with the wrong
token count:
instead of the correct:
Root cause
MeCab's segmentation is context-sensitive. Parsed on its own,
それはそれでcomes back as 3 tokens, because MeCab treats trailing
それでas a singleconjunction (接続詞, "so"/"therefore"):
Parsed as part of its actual sentence, MeCab correctly keeps
でas itsown particle attaching to
問題になる:Both segmentations are individually "correct" — it's genuine
disambiguation that depends on what follows.
Tracing this through the code:
The reading pane builds the text for a new multiword term by
reading
.text()off each selected (already-tokenized, on-screen)word span, then joining them with
'', discarding the tokenboundaries the page's own parse had already established:
lute/static/js/lute.js,show_multiword_term_edit_form():That flat string is sent to
GET/POST /read/termform/<langid>/<text>(
lute/read/routes.py), which callsRepository.find_or_new(langid, usetext).find_or_new(lute/term/model.py) builds a "spec" term viaDBTerm(lang, text), which goes throughTerm.textsetter →Term._parse_string_add_zws()(lute/models/term.py). That methodalways re-parses the string from scratch via
lang.get_parsed_tokens(t),regardless of any structure already present — it even explicitly strips
any pre-existing zero-width-space (zws) separators before doing so
(
t = t.replace(zws, "")).So even though the reading pane had the correct, in-context token
boundaries at the moment of selection, they never reach the backend,
and the backend actively discards them even if they did.
This isn't a new discovery for the codebase —
find_or_new's owndocstring already flags the general problem:
and there's an existing characterization test for it,
test_find_or_new_ambiguous_japanese_terms, using the same style ofexample (
集めれ→集め/れ). There's alsoTerm.create_term_no_parsing(),added specifically to let other parts of the code (page rendering,
in
lute/read/render/calculate_textitems.py) skip this re-parsing stepwhen they already have trustworthy, in-context tokens. This PR extends
that same idea to the one remaining place that needed it: multiword term
creation from the reading pane.
The fix
Two small, surgical changes, no new dependencies or config:
1.
lute/static/js/lute.jsshow_multiword_term_edit_form()now joins the selected token textswith a zero-width space (
\u200B) — the same separator Lute alreadyuses internally to mark token boundaries — instead of
''. Thispreserves the exact in-context tokenization at the point of creation.
2.
lute/models/term.pyTerm._parse_string_add_zws()now checks whether the incoming textalready contains zws. If it does, it's treated as pre-tokenized and
used as-is (just trimming/tidying), instead of being re-parsed. If it
doesn't contain zws, behaviour is completely unchanged — plain text
(CSV import, manually typed text in term forms/search) still gets
parsed fresh, exactly as before.
This keeps the fix scoped to exactly the case that has good
information available (reading-pane selections), without changing
behaviour for cases that don't (CSV import, freeform typed text),
which remain as ambiguous as before — that's a separate, larger
problem (also called out in
find_or_new's docstring) that wouldneed actual sentence context to be passed around, not just addressed
by trusting zws.
Tests
Added:
tests/orm/test_Term.pytest_new_term_with_pretokenized_zws_text_is_not_reparsed— azws-joined
それ/は/それ/でis kept as 4 tokens, not collapsed to 3.test_new_term_without_zws_is_parsed_from_scratch_as_before—plain (non pre-tokenized) text keeps the old (ambiguous, 3-token)
behaviour, unchanged.
tests/unit/term/test_Repository.pytest_find_or_new_with_pretokenized_text_finds_in_context_term—find_or_newgiven pre-tokenized text finds the existingin-context term instead of missing it and creating a mismatched one.
test_find_or_new_with_pretokenized_text_creates_correct_new_term—same, for the not-yet-existing case.
All 4 new tests fail on
3.10.1(reproducing the bug) and pass withthis change. Full existing suite (
tests/unit+tests/orm, 424tests, plus the 56 in the two files touched) still passes unchanged —
this includes the pre-existing
test_find_or_new_ambiguous_japanese_termscharacterization test, which is unaffected since its input never
contains zws.
Out of scope
reliable source of in-context tokenization to draw on, so they're
intentionally left as-is (still subject to the same MeCab ambiguity,
same as today).
were already mis-tokenized by this bug before upgrading. That's a
data migration, not a code fix; the DB fields involved are
WoText/WoTextLC/WoTokenCounton thewordstable, in casea follow-up cleanup script is wanted.
Checklist
3.10.1checkout.lute.jsandlute/models/term.py.pass with it.
tests/unit+tests/ormsuite passes (424 passed).blackclean on all touched files.term in Japanese, confirm it highlights correctly after reload) —
worth doing once more before merge, since the JS side isn't
covered by an automated test.