Skip to content

Add API to extract terms from provided text - #4516

Merged
flodolo merged 4 commits into
mozilla:mainfrom
flodolo:issue4512_api_terms_strings
Sep 17, 2026
Merged

flodolo merged 4 commits into
mozilla:mainfrom
flodolo:issue4512_api_terms_strings

Conversation

@flodolo

@flodolo flodolo commented Sep 11, 2026

Copy link
Copy Markdown
Collaborator

Fixes #4512.

Opening up for review since it's self-contained, while the second part (extract from file) will touch and likely refactor some code being reviewed in #4511.

@flodolo
flodolo requested a review from mathjazz September 11, 2026 08:23
@flodolo flodolo changed the title Add API to extract terms from a string Add API to extract terms from provided text Sep 11, 2026
@flodolo

flodolo commented Sep 14, 2026

Copy link
Copy Markdown
Collaborator Author

(solving merge conflicts)

Comment thread pontoon/api/tests/test_views.py Outdated


@pytest.fixture
def terminology_extraction_setup():

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Extraction means candidate discovery in terminology lingo, whereas this API matches against an existing glossary.


def get_translation_text(self, obj):
if obj.do_not_translate:
return obj.text

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This also changes the existing endpoint /api/v2/search/terminology/, but I guess it's the right behaviour and possibly also not impacting the output.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, forgot to call it out in the PR.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: it doesn't impact search/terminology, because that doesn't return do-no-translate terms (they don't have an entity when created, so no translations).

return queryset.filter(translations__locale__code=value)

I wonder if that behavior should be changed, but in case it's probably a follow-up?

Comment thread pontoon/api/README.md Outdated
| Parameter | Description |
| --------- | --------------------------------------------------------- |
| `locale` | Locale code |
| `text` | Text to extract terminology from, at most 2048 characters |

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Technically this is TERMINOLOGY_API_MAX_CHARS characters. But we can also leave it out in the docs.

Comment thread pontoon/api/views.py
Comment on lines +521 to +526
if not text.strip():
errors["text"] = ["This field is required."]
elif len(text) > TERMINOLOGY_API_MAX_CHARS:
errors["text"] = [
f"Text exceeds maximum length of {TERMINOLOGY_API_MAX_CHARS} characters."
]

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: These two conditions are independent, so the elif misreports a 5000-character whitespace-only text as {"text": ["This field is required."]} instead of the length error.

Comment thread pontoon/api/urls.py Outdated
path(
"terminology/extract-from-text/",
views.TermExtractFromTextView.as_view(),
name="term-extract-from-text",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we settle the path before this ships? "Extract" in terminology parlance means discovering candidate terms from a corpus, whereas this does the inverse — matching text against the existing glossary. Also, -from-text just repeats the text parameter.

Suggested change
name="term-extract-from-text",
"terminology/matches/",
views.TermMatchListView.as_view(),
name="term-matches",

@flodolo flodolo Sep 16, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reason for using from-text was that this would be coupled with from-file. But at this point, the idea is to leave that part alone for now, and probably have a more generic analysis that does more than just terminology.

On that point, I think I'm going to split the original issue to make that clear, and have this PR fix the existing one.

Comment thread pontoon/api/views.py Outdated
return qs


class TermExtractFromTextView(generics.ListAPIView):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shall we add a throttle class here? Each call SELECTs every Term, prefetches their TermTranslations, and runs one re.search per term over up to 2048 chars.

Comment thread pontoon/terminology/utils.py Outdated
return "\n".join(text_parts)


def join_text_fragments(texts: Iterable[str]) -> str:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This has no production caller. What did you have in mind for it?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

D'oh. It's a left-over from local iterations where I was testing "upload from file" (this would be needed for complex Fluent strings), making sure that the structure could be build on top.

Comment thread pontoon/terminology/utils.py Outdated
)
).order_by("text", "id")

return terms.for_string(text)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Prefetch above will not prevent the TermTranslation rows to load for every candidate term rather than the matches — a text matching 1 of 1000 terms still pulls 1000 rows and discards them.

I'd suggest something like this:

matched = Term.objects.for_string(text)
return (
    Term.objects.filter(pk__in=[t.pk for t in matched])
    .prefetch_related(Prefetch(...))
    .order_by("text", "id")
)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using slightly different approach that should use one less query?

    prefetch_related_objects(
        terms,
        Prefetch(
            "translations",
            queryset=TermTranslation.objects.filter(locale=locale),
            to_attr="filtered_translations",
        ),
    )

Comment thread pontoon/api/README.md Outdated

Extract the terms appearing in a text, with their translation in a given locale.

Unlike [`/api/v2/search/terminology/`](#json-mode), which looks up terms by name, this

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Unlike [`/api/v2/search/terminology/`](#json-mode), which looks up terms by name, this
Unlike [`/api/v2/search/terminology/`](#/search/search_terminology_list), which looks up terms by name, this

@flodolo
flodolo requested a review from mathjazz September 17, 2026 04:43

@mathjazz mathjazz left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great work! Just left one minor suggestion inline for the README consistency.

Comment thread pontoon/api/README.md
The endpoint is rate limited per user, or per IP address for anonymous requests, with a
burst limit of 60 calls per minute and a sustained limit of 600 calls per hour by default
(configurable via `API_TERMINOLOGY_THROTTLE_BURST` and
`API_TERMINOLOGY_THROTTLE_SUSTAINED`). Calls over the limit are rejected with `429`.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Like for the Upload throttle variables, I'd mention these are independent:
#4503 (comment)

@flodolo
flodolo merged commit 22dd6c8 into mozilla:main Sep 17, 2026
7 checks passed
@flodolo
flodolo deleted the issue4512_api_terms_strings branch September 17, 2026 06:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Create API to extract terminology matches from provided text

2 participants