Repository: WeblateOrg/weblate

Issue:
Consider using ahocorasick-rs instead of pyahocorasick
### Describe the problem

https://pypi.org/project/ahocorasick-rs/ seems faster alternative to pyahocorasick.

### Describe the solution you'd like

It would be useful to benchmark it in Weblate use-case and switch to it in case it outperforms pyahocorasick.

### Describe alternatives you've considered

_No response_

### Screenshots

_No response_

### Additional context

> That being said, I've seen ahocorasick_rs run 1.5× to 7× as fast as pyahocorasick, depending on the options used.


Discussion:
This issue seems to be a good fit for newbie contributors. You are welcome to contribute to Weblate! Don't hesitate to ask any questions you would have while implementing this.

You can learn about how to get started in our [contributors documentation](https://docs.weblate.org/en/latest/contributing/index.html).
I've done some really basic benchmark and it seems that it's reasonable to switch:

```py
from weblate.checks.data import IGNORE_WORDS
import ahocorasick
import ahocorasick_rs
import timeit


def build_py():
    automaton = ahocorasick.Automaton()
    for term in IGNORE_WORDS:
        automaton.add_word(term, term)
    automaton.make_automaton()
    return automaton


def build_rs():
    return ahocorasick_rs.AhoCorasick(
        IGNORE_WORDS,
        implementation=ahocorasick_rs.Implementation.ContiguousNFA,
        store_patterns=False,
    )


print("Build")
print(timeit.timeit("build_py", globals={"build_py": build_py}))
print(timeit.timeit("build_rs", globals={"build_rs": build_rs}))

ac_py = build_py()
ac_rs = build_rs()

print("Find")
print(
    timeit.timeit(
        "list(ac_py.iter('Please enter the correct username and password.'))",
        globals={"ac_py": ac_py},
    )
)

print(
    timeit.timeit(
        "ac_rs.find_matches_as_indexes('Please enter the correct username and password.')",
        globals={"ac_rs": ac_rs},
    )
)
```

Resolving patch:
diff --git a/weblate/glossary/models.py b/weblate/glossary/models.py
--- a/weblate/glossary/models.py
+++ b/weblate/glossary/models.py
@@ -5,7 +5,7 @@
 import re
 from itertools import chain
 
-import ahocorasick
+import ahocorasick_rs
 import sentry_sdk
 from django.db.models import Q
 from django.db.models.functions import Lower
@@ -39,11 +39,11 @@ def get_glossary_automaton(project):
             )
         )
         # Build automaton for efficient Aho-Corasick search
-        automaton = ahocorasick.Automaton()
-        for term in terms:
-            automaton.add_word(term, term)
-        automaton.make_automaton()
-        return automaton
+        return ahocorasick_rs.AhoCorasick(
+            terms,
+            implementation=ahocorasick_rs.Implementation.ContiguousNFA,
+            store_patterns=False,
+        )
 
 
 def get_glossary_terms(unit):
@@ -76,15 +76,16 @@ def get_glossary_terms(unit):
 
     terms = set()
     automaton = project.glossary_automaton
-    if automaton.kind == ahocorasick.AHOCORASICK:
-        # Extract terms present in the source
-        with sentry_sdk.start_span(op="glossary.match", description=project.slug):
-            for end, term in automaton.iter(source):
-                if uses_ngram or (
-                    (end + 1 == len(term) or NON_WORD_RE.match(source[end - len(term)]))
-                    and (end + 1 == len(source) or NON_WORD_RE.match(source[end + 1]))
-                ):
-                    terms.add(term)
+    # Extract terms present in the source
+    with sentry_sdk.start_span(op="glossary.match", description=project.slug):
+        for _termno, start, end in automaton.find_matches_as_indexes(
+            source, overlapping=True
+        ):
+            if uses_ngram or (
+                (start == 0 or NON_WORD_RE.match(source[start - 1]))
+                and (end >= len(source) or NON_WORD_RE.match(source[end]))
+            ):
+                terms.add(source[start:end])
 
     if using_postgresql():
         match = r"^({})$".format("|".join(re_escape(term) for term in terms))
diff --git a/weblate/utils/requirements.py b/weblate/utils/requirements.py
--- a/weblate/utils/requirements.py
+++ b/weblate/utils/requirements.py
@@ -54,7 +54,7 @@
     "GitPython",
     "borgbackup",
     "pyparsing",
-    "pyahocorasick",
+    "ahocorasick_rs",
     "python-redis-lock",
     "charset-normalizer",
 ]


Test patch: