{"id":15014129,"url":"https://github.com/gandersen101/spaczz","last_synced_at":"2025-04-04T08:05:47.176Z","repository":{"id":40403088,"uuid":"272294049","full_name":"gandersen101/spaczz","owner":"gandersen101","description":"Fuzzy matching and more functionality for spaCy.","archived":false,"fork":false,"pushed_at":"2024-07-06T01:42:50.000Z","size":1469,"stargazers_count":256,"open_issues_count":7,"forks_count":28,"subscribers_count":9,"default_branch":"main","last_synced_at":"2025-03-28T07:04:10.465Z","etag":null,"topics":["ai","artificial-intelligence","data-science","fuzzy-matching","natural-language-processing","nlp","nlp-library","python","rapidfuzz","regex","spacy","spacy-extension","spacy-extensions"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/gandersen101.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2020-06-14T22:31:37.000Z","updated_at":"2025-03-15T22:07:11.000Z","dependencies_parsed_at":"2024-05-03T23:25:13.724Z","dependency_job_id":"3c66c116-d992-4e47-ac66-023316fd0c31","html_url":"https://github.com/gandersen101/spaczz","commit_stats":{"total_commits":222,"total_committers":6,"mean_commits":37.0,"dds":0.1711711711711712,"last_synced_commit":"0b774b68e2495008a9579700194bb88789a1af45"},"previous_names":[],"tags_count":15,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gandersen101%2Fspaczz","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gandersen101%2Fspaczz/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gandersen101%2Fspaczz/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gandersen101%2Fspaczz/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/gandersen101","download_url":"https://codeload.github.com/gandersen101/spaczz/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247141162,"owners_count":20890604,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["ai","artificial-intelligence","data-science","fuzzy-matching","natural-language-processing","nlp","nlp-library","python","rapidfuzz","regex","spacy","spacy-extension","spacy-extensions"],"created_at":"2024-09-24T19:45:13.890Z","updated_at":"2025-04-04T08:05:47.145Z","avatar_url":"https://github.com/gandersen101.png","language":"Python","funding_links":[],"categories":["Python"],"sub_categories":[],"readme":"[![Tests](https://github.com/gandersen101/spaczz/workflows/Lint,%20Typecheck%20and%20Test/badge.svg)](https://github.com/gandersen101/spaczz/actions?workflow=Lint,%20Typecheck%20and%20Test)\n[![Codecov](https://codecov.io/gh/gandersen101/spaczz/branch/master/graph/badge.svg)](https://codecov.io/gh/gandersen101/spaczz)\n[![PyPI](https://img.shields.io/pypi/v/spaczz.svg)](https://pypi.org/project/spaczz/)\n[![Read the Docs](https://readthedocs.org/projects/spaczz/badge/)](https://spaczz.readthedocs.io/)\n\n# spaczz: Fuzzy matching and more for spaCy\n\nspaczz provides fuzzy matching and additional regex matching functionality for [spaCy](https://spacy.io/).\nspaczz's components have similar APIs to their spaCy counterparts and spaczz pipeline components can integrate into spaCy pipelines where they can be saved/loaded as models.\n\nFuzzy matching is currently performed with matchers from [RapidFuzz](https://github.com/maxbachmann/rapidfuzz)'s fuzz module and regex matching currently relies on the [regex](https://pypi.org/project/regex/) library. spaczz certainly takes additional influence from other libraries and resources. For additional details see the references section.\n\n**Supports spaCy \u003e= 3.0**\n\nspaczz has been tested on Ubuntu, MacOS, and Windows Server.\n\n*v0.6.0 Release Notes:*\n- *Returning the matching pattern for all matchers, this is a breaking change as matches are now tuples of length 5 instead of 4.*\n- *Regex and token matches now return match ratios.*\n- *Support for `python\u003c=3.11,\u003e=3.7`, along with `rapidfuzz\u003e=1.0.0`.*\n- *Dropped support for spaCy v2. Sorry to do this without a deprecation cycle, but I stepped away from this project for a long time.*\n- *Removed support of `\"spaczz_\"` preprended optional `SpaczzRuler` init arguments. Also, sorry to do this without a deprecation cycle.*\n- *`Matcher.pipe` methods, which were deprecated, are now removed.*\n- *`spaczz_span` custom attribute, which was deprecated, is now removed.*\n\nPlease see the [changelog](https://github.com/gandersen101/spaczz/blob/master/CHANGELOG.md) for previous release notes. This will eventually be moved to the [Read the Docs](https://spaczz.readthedocs.io/en/latest/) page.\n\n\u003ch1\u003eTable of Contents\u003cspan class=\"tocSkip\"\u003e\u003c/span\u003e\u003c/h1\u003e\n\u003cdiv class=\"toc\"\u003e\u003cul class=\"toc-item\"\u003e\u003cli\u003e\u003cspan\u003e\u003ca href=\"#Installation\" data-toc-modified-id=\"Installation-1\"\u003eInstallation\u003c/a\u003e\u003c/span\u003e\u003c/li\u003e\u003cli\u003e\u003cspan\u003e\u003ca href=\"#Basic-Usage\" data-toc-modified-id=\"Basic-Usage-2\"\u003eBasic Usage\u003c/a\u003e\u003c/span\u003e\u003cul class=\"toc-item\"\u003e\u003cli\u003e\u003cspan\u003e\u003ca href=\"#FuzzyMatcher\" data-toc-modified-id=\"FuzzyMatcher-2.1\"\u003eFuzzyMatcher\u003c/a\u003e\u003c/span\u003e\u003c/li\u003e\u003cli\u003e\u003cspan\u003e\u003ca href=\"#RegexMatcher\" data-toc-modified-id=\"RegexMatcher-2.2\"\u003eRegexMatcher\u003c/a\u003e\u003c/span\u003e\u003c/li\u003e\u003cli\u003e\u003cspan\u003e\u003ca href=\"#SimilarityMatcher\" data-toc-modified-id=\"SimilarityMatcher-2.3\"\u003eSimilarityMatcher\u003c/a\u003e\u003c/span\u003e\u003c/li\u003e\u003cli\u003e\u003cspan\u003e\u003ca href=\"#TokenMatcher\" data-toc-modified-id=\"TokenMatcher-2.4\"\u003eTokenMatcher\u003c/a\u003e\u003c/span\u003e\u003c/li\u003e\u003cli\u003e\u003cspan\u003e\u003ca href=\"#SpaczzRuler\" data-toc-modified-id=\"SpaczzRuler-2.5\"\u003eSpaczzRuler\u003c/a\u003e\u003c/span\u003e\u003c/li\u003e\u003cli\u003e\u003cspan\u003e\u003ca href=\"#Custom-Attributes\" data-toc-modified-id=\"Custom-Attributes-2.6\"\u003eCustom Attributes\u003c/a\u003e\u003c/span\u003e\u003c/li\u003e\u003cli\u003e\u003cspan\u003e\u003ca href=\"#Saving/Loading\" data-toc-modified-id=\"Saving/Loading-2.7\"\u003eSaving/Loading\u003c/a\u003e\u003c/span\u003e\u003c/li\u003e\u003c/ul\u003e\u003c/li\u003e\u003cli\u003e\u003cspan\u003e\u003ca href=\"#Known-Issues\" data-toc-modified-id=\"Known-Issues-3\"\u003eKnown Issues\u003c/a\u003e\u003c/span\u003e\u003cul class=\"toc-item\"\u003e\u003cli\u003e\u003cspan\u003e\u003ca href=\"#Performance\" data-toc-modified-id=\"Performance-3.1\"\u003ePerformance\u003c/a\u003e\u003c/span\u003e\u003c/li\u003e\u003c/ul\u003e\u003c/li\u003e\u003cli\u003e\u003cspan\u003e\u003ca href=\"#Roadmap\" data-toc-modified-id=\"Roadmap-4\"\u003eRoadmap\u003c/a\u003e\u003c/span\u003e\u003c/li\u003e\u003cli\u003e\u003cspan\u003e\u003ca href=\"#Development\" data-toc-modified-id=\"Development-5\"\u003eDevelopment\u003c/a\u003e\u003c/span\u003e\u003c/li\u003e\u003cli\u003e\u003cspan\u003e\u003ca href=\"#References\" data-toc-modified-id=\"References-6\"\u003eReferences\u003c/a\u003e\u003c/span\u003e\u003c/li\u003e\u003c/ul\u003e\u003c/div\u003e\n\n## Installation\n\nSpaczz can be installed using pip.\n\n```python\npip install spaczz\n```\n\n## Basic Usage\n\nSpaczz's primary features are the `FuzzyMatcher`, `RegexMatcher`, and \"fuzzy\" `TokenMatcher` that function similarly to spaCy's `Matcher` and `PhraseMatcher`, and the `SpaczzRuler` which integrates the spaczz matchers into a spaCy pipeline component similar to spaCy's `EntityRuler`.\n\n### FuzzyMatcher\n\nThe basic usage of the fuzzy matcher is similar to spaCy's `PhraseMatcher` except it returns the fuzzy ratio and matched pattern, along with match id, start and end information, so make sure to include variables for the ratio and pattern when unpacking results.\n\n\n```python\nimport spacy\nfrom spaczz.matcher import FuzzyMatcher\n\nnlp = spacy.blank(\"en\")\ntext = \"\"\"Grint M Anderson created spaczz in his home at 555 Fake St,\nApt 5 in Nashv1le, TN 55555-1234 in the US.\"\"\"  # Spelling errors intentional.\ndoc = nlp(text)\n\nmatcher = FuzzyMatcher(nlp.vocab)\nmatcher.add(\"NAME\", [nlp(\"Grant Andersen\")])\nmatcher.add(\"GPE\", [nlp(\"Nashville\")])\nmatches = matcher(doc)\n\nfor match_id, start, end, ratio, pattern in matches:\n    print(match_id, doc[start:end], ratio, pattern)\n```\n\n    NAME Grint M Anderson 80 Grant Andersen\n    GPE Nashv1le 82 Nashville\n\n\nUnlike spaCy matchers, spaczz matchers are written in pure Python. While they are required to have a spaCy vocab passed to them during initialization, this is purely for consistency as the spaczz matchers do not use currently use the spaCy vocab. This is why the `match_id` above is simply a string instead of an integer value like in spaCy matchers.\n\nSpaczz matchers can also make use of on-match rules via callback functions. These on-match callbacks need to accept the matcher itself, the doc the matcher was called on, the match index and the matches produced by the matcher.\n\n\n```python\nimport spacy\nfrom spacy.tokens import Span\nfrom spaczz.matcher import FuzzyMatcher\n\nnlp = spacy.blank(\"en\")\ntext = \"\"\"Grint M Anderson created spaczz in his home at 555 Fake St,\nApt 5 in Nashv1le, TN 55555-1234 in the US.\"\"\"  # Spelling errors intentional.\ndoc = nlp(text)\n\n\ndef add_name_ent(matcher, doc, i, matches):\n    \"\"\"Callback on match function. Adds \"NAME\" entities to doc.\"\"\"\n    # Get the current match and create tuple of entity label, start and end.\n    # Append entity to the doc's entity. (Don't overwrite doc.ents!)\n    _match_id, start, end, _ratio, _pattern = matches[i]\n    entity = Span(doc, start, end, label=\"NAME\")\n    doc.ents += (entity,)\n\n\nmatcher = FuzzyMatcher(nlp.vocab)\nmatcher.add(\"NAME\", [nlp(\"Grant Andersen\")], on_match=add_name_ent)\nmatches = matcher(doc)\n\nfor ent in doc.ents:\n    print((ent.text, ent.start, ent.end, ent.label_))\n```\n\n    ('Grint M Anderson', 0, 3, 'NAME')\n\n\nLike spaCy's `EntityRuler`, a very similar entity updating logic has been implemented in the `SpaczzRuler`. The `SpaczzRuler` also takes care of handling overlapping matches. It is discussed in a later section.\n\nUnlike spaCy's matchers, rules added to spaczz matchers have optional keyword arguments that can modify the matching behavior. Take the below fuzzy matching examples:\n\n\n```python\nimport spacy\nfrom spaczz.matcher import FuzzyMatcher\n\nnlp = spacy.blank(\"en\")\n# Let's modify the order of the name in the text.\ntext = \"\"\"Anderson, Grint created spaczz in his home at 555 Fake St,\nApt 5 in Nashv1le, TN 55555-1234 in the US.\"\"\"  # Spelling errors intentional.\ndoc = nlp(text)\n\nmatcher = FuzzyMatcher(nlp.vocab)\nmatcher.add(\"NAME\", [nlp(\"Grant Andersen\")])\nmatches = matcher(doc)\n\n# The default fuzzy matching settings will not find a match.\nfor match_id, start, end, ratio, pattern in matches:\n    print(match_id, doc[start:end], ratio, pattern)\n```\n\nNext we change the fuzzy matching behavior for the pattern in the \"NAME\" rule.\n\n\n```python\nimport spacy\nfrom spaczz.matcher import FuzzyMatcher\n\nnlp = spacy.blank(\"en\")\n# Let's modify the order of the name in the text.\ntext = \"\"\"Anderson, Grint created spaczz in his home at 555 Fake St,\nApt 5 in Nashv1le, TN 55555-1234 in the US.\"\"\"  # Spelling errors intentional.\ndoc = nlp(text)\n\nmatcher = FuzzyMatcher(nlp.vocab)\nmatcher.add(\"NAME\", [nlp(\"Grant Andersen\")], kwargs=[{\"fuzzy_func\": \"token_sort\"}])\nmatches = matcher(doc)\n\n# The default fuzzy matching settings will not find a match.\nfor match_id, start, end, ratio, pattern in matches:\n    print(match_id, doc[start:end], ratio, pattern)\n```\n\n    NAME Anderson, Grint 83 Grant Andersen\n\n\nThe full list of keyword arguments available for fuzzy matching settings includes:\n\n- `ignore_case` (bool): Whether to lower-case text before matching. Default is `True`.\n- `min_r` (int): Minimum match ratio required.\n- `thresh` (int): If this ratio is exceeded in initial scan, and `flex \u003e 0`, no optimization will be attempted. If `flex == 0`, `thresh` has no effect. Default is `100`.\n- `fuzzy_func` (str): Key name of fuzzy matching function to use. All rapidfuzz matching functions with default settings are available. Additional fuzzy matching functions can be registered by users. Default is `\"simple\"`:\n    * `\"simple\"` = `ratio`\n    * `\"partial\"` = `partial_ratio`\n    * `\"token\"` = `token_ratio`\n    * `\"token_set\"` = `token_set_ratio`\n    * `\"token_sort\"` = `token_sort_ratio`\n    * `\"partial_token\"` = `partial_token_ratio`\n    * `\"partial_token_set\"` = `partial_token_set_ratio`\n    * `\"partial_token_sort\"` = `partial_token_sort_ratio`\n    * `\"weighted\"` = `WRatio`\n    * `\"quick\"` = `QRatio`\n    * `\"partial_alignment\"` = `partial_ratio_alignment` (Requires `rapidfuzz\u003e=2.0.3`)\n- `flex` (int|Literal['default', 'min', 'max']): Number of tokens to move match boundaries left and right during optimization. Can be an `int` with a max of `len(pattern)` and a min of `0`, (will warn and change if higher or lower). `\"max\"`, `\"min\"`, or `\"default\"` are also valid. Default is `\"default\"`: `len(pattern) // 2`.\n- `min_r1` (int|None): Optional granular control over the minimum match ratio required for selection during the initial scan. If `flex == 0`, `min_r1` will be overwritten by `min_r2`. If `flex \u003e 0`, `min_r1` must be lower than `min_r2` and \"low\" in general because match boundaries are not flexed initially. Default is `None`, which will result in `min_r1` being set to `round(min_r / 1.5)`.\n\n### RegexMatcher\n\nThe basic usage of the regex matcher is also fairly similar to spaCy's `PhraseMatcher`. It accepts regex patterns as strings so flags must be inline. Regexes are compiled with the [regex](https://pypi.org/project/regex/) package so approximate \"fuzzy\" matching is supported. To provide access to these \"fuzzy\" match results the matcher returns a calculated fuzzy ratio and matched pattern, along with match id, start and end information, so make sure to include variables for the ratio and pattern when unpacking results.\n\n\n```python\nimport spacy\nfrom spaczz.matcher import RegexMatcher\n\nnlp = spacy.blank(\"en\")\ntext = \"\"\"Anderson, Grint created spaczz in his home at 555 Fake St,\nApt 5 in Nashv1le, TN 55555-1234 in the US.\"\"\"  # Spelling errors intentional.\ndoc = nlp(text)\n\nmatcher = RegexMatcher(nlp.vocab)\n# Use inline flags for regex strings as needed\nmatcher.add(\n    \"ZIP\",\n    [r\"\\b\\d{5}(?:[-\\s]\\d{4})?\\b\"],\n)\nmatcher.add(\"GPE\", [r\"(usa){d\u003c=1}\"])  # Fuzzy regex.\nmatches = matcher(doc)\n\nfor match_id, start, end, ratio, pattern in matches:\n    print(match_id, doc[start:end], ratio, pattern)\n```\n\n    ZIP 55555-1234 100 \\b\\d{5}(?:[-\\s]\\d{4})?\\b\n    GPE US 80 (usa){d\u003c=1}\n\n\nSpaczz matchers can also make use of on-match rules via callback functions. These on-match callbacks need to accept the matcher itself, the doc the matcher was called on, the match index and the matches produced by the matcher. See the fuzzy matcher usage example above for details.\n\nLike the fuzzy matcher, the regex matcher has optional keyword arguments that can modify matching behavior. Take the below regex matching example.\n\n\n```python\nimport spacy\nfrom spaczz.matcher import RegexMatcher\n\nnlp = spacy.blank(\"en\")\ntext = \"\"\"Anderson, Grint created spaczz in his home at 555 Fake St,\nApt 5 in Nashv1le, TN 55555-1234 in the USA.\"\"\"  # Spelling errors intentional. Notice 'USA' here.\ndoc = nlp(text)\n\nmatcher = RegexMatcher(nlp.vocab)\n# Use inline flags for regex strings as needed\nmatcher.add(\n    \"STREET\", [\"street_addresses\"], kwargs=[{\"predef\": True}]\n)  # Use predefined regex by key name.\n# Below will not expand partial matches to span boundaries.\nmatcher.add(\"GPE\", [r\"(?i)[U](nited|\\.?) ?[S](tates|\\.?)\"], kwargs=[{\"partial\": False}])\nmatches = matcher(doc)\n\nfor match_id, start, end, ratio, pattern in matches:\n    print(\n        match_id, doc[start:end], ratio, pattern\n    )  # comma in result isn't ideal - see \"Roadmap\"\n```\n\n    STREET 555 Fake St, 100 street_addresses\n\n\nThe full list of keyword arguments available for regex matching settings includes:\n\n- `ignore_case` (bool): Whether to lower-case text before matching. Default is `True`.\n- `min_r` (int): Minimum match ratio required.\n- `fuzzy_weights` (str): Name of weighting method for regex insertion, deletion, and substituion counts. Additional weighting methods can be registered by users. Default is `\"indel\"`.\n    * `\"indel\"` = `(1, 1, 2)`\n    * `\"lev\"` = `(1, 1, 1)`\n- `partial`: (bool): Whether partial matches should be extended to `Token` or `Span` boundaries in `doc` or not. For example, the regex only matches part of a `Token` or `Span` in `doc`. Default is `True`.\n- `predef` (string): Whether the regex string should be interpreted as a key to a predefined regex pattern or not. Additional predefined regex patterns can be registered by users. Default is `False.`\n    * `\"dates\"`\n    * `\"times\"`\n    * `\"phones\"`\n    * `\"phones_with_exts\"`\n    * `\"links\"`\n    * `\"emails\"`\n    * `\"ips\"`\n    * `\"ipv6s\"`\n    * `\"prices\"`\n    * `\"hex_colors\"`\n    * `\"credit_cards\"`\n    * `\"btc_addresses\"`\n    * `\"street_addresses\"`\n    * `\"zip_codes\"`\n    * `\"po_boxes\"`\n    * `\"ssn_numbers\"`\n\n### SimilarityMatcher\n\nThe basic usage of the similarity matcher is similar to spaCy's `PhraseMatcher` except it returns the vector similarity ratio and matched pattern, along with match id, start and end information, so make sure to include variables for the ratio and pattern when unpacking results.\n\nIn order to produce meaningful results from the similarity matcher, a spaCy model with word vectors (ex. medium or large English models) must be used to initialize the matcher, process the target document, and process any patterns added.\n\n\n```python\nimport spacy\nfrom spaczz.matcher import SimilarityMatcher\n\nnlp = spacy.load(\"en_core_web_md\")\ntext = \"I like apples, grapes and bananas.\"\ndoc = nlp(text)\n\n# lowering min_r2 from default of 75 to produce matches in this example\nmatcher = SimilarityMatcher(nlp.vocab, min_r2=65)\nmatcher.add(\"FRUIT\", [nlp(\"fruit\")])\nmatches = matcher(doc)\n\nfor match_id, start, end, ratio, pattern in matches:\n    print(match_id, doc[start:end], ratio, pattern)\n```\n\n    FRUIT apples 70 fruit\n    FRUIT grapes 73 fruit\n    FRUIT bananas 70 fruit\n\n\nPlease note that even for the mostly pure-Python spaczz, this process is currently extremely slow so be mindful of the scope in which it is applied. Enabling GPU support in spaCy ([see here](https://spacy.io/usage#gpu)) should improve the speed somewhat, but I believe the process will still be bottlenecked in the pure-Python search algorithm until I develop a better search algorithm and/or drop the search to lower-level code (ex C).\n\nAlso as a somewhat experimental feature, the similarity matcher is not currently part of the `SpaczzRuler` nor does it have a separate ruler. If you need to add similarity matches to a `Doc`'s entities you will need to use an on-match callback for the time being. Please see the fuzzy matcher on-match callback example above for ideas. If there is enough interest in integrating/creating a ruler for the similarity matcher this can be done.\n\nThe full list of keyword arguments available for similarity matching settings includes:\n\n- `ignore_case` (bool): Whether to lower-case text before fuzzy matching. Default is `True`.\n- `min_r` (int): Minimum match ratio required.\n- `thresh` (int): If this ratio is exceeded in initial scan, and `flex \u003e 0`, no optimization will be attempted. If `flex == 0`, `thresh` has no effect. Default is `100`.\n- `flex` (int|Literal['default', 'min', 'max']): Number of tokens to move match boundaries left and right during optimization. Can be an `int` with a max of `len(pattern)` and a min of `0`, (will warn and change if higher or lower). `\"max\"`, `\"min\"`, or `\"default\"` are also valid. Default is `\"default\"`: `len(pattern) // 2`.\n- `min_r1` (int|None): Optional granular control over the minimum match ratio required for selection during the initial scan. If `flex == 0`, `min_r1` will be overwritten by `min_r2`. If `flex \u003e 0`, `min_r1` must be lower than `min_r2` and \"low\" in general because match boundaries are not flexed initially. Default is `None`, which will result in `min_r1` being set to `round(min_r / 1.5)`.\n- `min_r2` (int|None): Optional granular control over the minimum match ratio required for selection during match optimization. Needs to be higher than `min_r1` and \"high\" in general to ensure only quality matches are returned. Default is `None`, which will result in `min_r2` being set to `min_r`.\n\n### TokenMatcher\n\n*Note: spaCy's `Matcher` now supports [fuzzy matching](https://spacy.io/usage/v3-5#fuzzy), so unless you need a specific feature from spaczz's `TokenMatcher`, it is highly recommended to use spaCy's much faster `Matcher`.*\n\nThe basic usage of the token matcher is similar to spaCy's `Matcher`. It accepts labeled patterns in the form of lists of dictionaries where each list describes an individual pattern and each dictionary describes an individual token.\n\nThe token matcher accepts all the same token attributes and pattern syntax as it's spaCy counterpart but adds fuzzy and fuzzy-regex support.\n\n`\"FUZZY\"` and `\"FREGEX\"` are the two additional spaCy token pattern options.\n\nFor example:\n\n```python\n[\n    {\"TEXT\": {\"FREGEX\": \"(database){e\u003c=1}\"}},\n    {\"LOWER\": {\"FUZZY\": \"access\", \"MIN_R\": 85, \"FUZZY_FUNC\": \"quick_lev\"}},\n]\n```\n\n**Make sure to use uppercase dictionary keys in patterns.**\n\nThe full list of keyword arguments available for token matching settings includes:\n\n- `ignore_case` (bool): Whether to lower-case text before matching. Can only be set at the pattern level. For \"FUZZY\" and \"FREGEX\" patterns. Default is `True`.\n- `min_r` (int): Minimum match ratio required. For \"FUZZY\" and \"FREGEX\" patterns.\n- `fuzzy_func` (str): Key name of fuzzy matching function to use. Can only be set at the pattern level. For \"FUZZY\" patterns only. All rapidfuzz matching functions with default settings are available, however any token-based functions provide no utility at the individual token level. Additional fuzzy matching functions can be registered by users. Included, and useful, functions are (the default is `simple`):\n    * `\"simple\"` = `ratio`\n    * `\"partial\"` = `partial_ratio`\n    * `\"quick\"` = `QRatio`\n    * `\"partial_alignment\"` = `partial_ratio_alignment` (Requires `rapidfuzz\u003e=2.0.3`)\n- `fuzzy_weights` (str): Name of weighting method for regex insertion, deletion, and substituion counts. Additional weighting methods can be registered by users. Default is `\"indel\"`.\n    * `\"indel\"` = `(1, 1, 2)`\n    * `\"lev\"` = `(1, 1, 1)`\n- `predef`: Whether regex should be interpreted as a key to a predefined regex pattern or not. Can only be set at the pattern level. For \"FREGEX\" patterns only. Default is `False`.\n\n\n```python\nimport spacy\nfrom spaczz.matcher import TokenMatcher\n\n# Using model results like POS tagging in token patterns requires model that provides these.\nnlp = spacy.load(\"en_core_web_md\")\ntext = \"\"\"The manager gave me SQL databesE acess so now I can acces the Sequal DB.\nMy manager's name is Grfield\"\"\"\ndoc = nlp(text)\n\nmatcher = TokenMatcher(vocab=nlp.vocab)\nmatcher.add(\n    \"DATA\",\n    [\n        [\n            {\"TEXT\": \"SQL\"},\n            {\"LOWER\": {\"FREGEX\": \"(database){s\u003c=1}\"}},\n            {\"LOWER\": {\"FUZZY\": \"access\"}},\n        ],\n        [{\"TEXT\": {\"FUZZY\": \"Sequel\"}, \"POS\": \"PROPN\"}, {\"LOWER\": \"db\"}],\n    ],\n)\nmatcher.add(\"NAME\", [[{\"TEXT\": {\"FUZZY\": \"Garfield\"}}]])\nmatches = matcher(doc)\n\nfor match_id, start, end, ratio, pattern in matches:\n    print(match_id, doc[start:end], ratio, pattern)\n```\n\n    DATA SQL databesE acess 91 [{\"TEXT\":\"SQL\"},{\"LOWER\":{\"FREGEX\":\"(database){s\u003c=1}\"}},{\"LOWER\":{\"FUZZY\":\"access\"}}]\n    DATA Sequal DB 87 [{\"TEXT\":{\"FUZZY\":\"Sequel\"},\"POS\":\"PROPN\"},{\"LOWER\":\"db\"}]\n    NAME Grfield 93 [{\"TEXT\":{\"FUZZY\":\"Garfield\"}}]\n\n\nEven though the token matcher can be a drop-in replacement for spaCy's `Matcher`, it is still recommended to use spaCy's `Matcher` if you do not need the spaczz token matcher's fuzzy capabilities - it will slow processing down unnecessarily.\n\n*Reminder: spaCy's `Matcher` now supports [fuzzy matching](https://spacy.io/usage/v3-5#fuzzy), so unless you need a specific feature from spaczz's `TokenMatcher`, it is highly recommended to use spaCy's much faster `Matcher`.*\n\n### SpaczzRuler\n\nThe spaczz ruler combines the fuzzy and regex phrase matchers, and the \"fuzzy\" token matcher, into one pipeline component that can update a `Doc.ents` similar to spaCy's `EntityRuler`.\n\nPatterns must be added as an iterable of dictionaries in the format of *{label (str), pattern(str or list), type(str), optional kwargs (dict), and optional id (str)}*.\n\nFor example, a fuzzy phrase pattern:\n\n`{'label': 'ORG', 'pattern': 'Apple' 'kwargs': {'min_r2': 90}, 'type': 'fuzzy'}`\n\nOr, a token pattern:\n\n`{'label': 'ORG', 'pattern': [{'TEXT': {'FUZZY': 'Apple'}}], 'type': 'token'}`\n\n\n```python\nimport spacy\nfrom spaczz.pipeline import SpaczzRuler\n\nnlp = spacy.blank(\"en\")\ntext = \"\"\"Anderson, Grint created spaczz in his home at 555 Fake St,\nApt 5 in Nashv1le, TN 55555-1234 in the USA.\nSome of his favorite bands are Converg and Protet the Zero.\"\"\"  # Spelling errors intentional.\ndoc = nlp(text)\n\npatterns = [\n    {\n        \"label\": \"NAME\",\n        \"pattern\": \"Grant Andersen\",\n        \"type\": \"fuzzy\",\n        \"kwargs\": {\"fuzzy_func\": \"token_sort\"},\n    },\n    {\n        \"label\": \"STREET\",\n        \"pattern\": \"street_addresses\",\n        \"type\": \"regex\",\n        \"kwargs\": {\"predef\": True},\n    },\n    {\"label\": \"GPE\", \"pattern\": \"Nashville\", \"type\": \"fuzzy\"},\n    {\n        \"label\": \"ZIP\",\n        \"pattern\": r\"\\b(?:55554){s\u003c=1}(?:(?:[-\\s])?\\d{4}\\b)\",\n        \"type\": \"regex\",\n    },  # fuzzy regex\n    {\"label\": \"GPE\", \"pattern\": \"(?i)[U](nited|\\.?) ?[S](tates|\\.?)\", \"type\": \"regex\"},\n    {\n        \"label\": \"BAND\",\n        \"pattern\": [{\"LOWER\": {\"FREGEX\": \"(converge){e\u003c=1}\"}}],\n        \"type\": \"token\",\n    },\n    {\n        \"label\": \"BAND\",\n        \"pattern\": [\n            {\"TEXT\": {\"FUZZY\": \"Protest\"}},\n            {\"IS_STOP\": True},\n            {\"TEXT\": {\"FUZZY\": \"Hero\"}},\n        ],\n        \"type\": \"token\",\n    },\n]\n\nruler = SpaczzRuler(nlp)\nruler.add_patterns(patterns)\ndoc = ruler(doc)\n\n\nfor ent in doc.ents:\n    print(\n        (\n            ent.text,\n            ent.start,\n            ent.end,\n            ent.label_,\n            ent._.spaczz_ratio,\n            ent._.spaczz_type,\n            ent._.spaczz_pattern,\n        )\n    )\n```\n\n    ('Anderson, Grint', 0, 3, 'NAME', 83, 'fuzzy', 'Grant Andersen')\n    ('555 Fake St,', 9, 13, 'STREET', 100, 'regex', 'street_addresses')\n    ('Nashv1le', 17, 18, 'GPE', 82, 'fuzzy', 'Nashville')\n    ('55555-1234', 20, 23, 'ZIP', 90, 'regex', '\\\\b(?:55554){s\u003c=1}(?:(?:[-\\\\s])?\\\\d{4}\\\\b)')\n    ('USA', 25, 26, 'GPE', 100, 'regex', '(?i)[U](nited|\\\\.?) ?[S](tates|\\\\.?)')\n    ('Converg', 34, 35, 'BAND', 93, 'token', '[{\"LOWER\":{\"FREGEX\":\"(converge){e\u003c=1}\"}}]')\n    ('Protet the Zero', 36, 39, 'BAND', 89, 'token', '[{\"TEXT\":{\"FUZZY\":\"Protest\"}},{\"IS_STOP\":true},{\"TEXT\":{\"FUZZY\":\"Hero\"}}]')\n\n\nWe see in the example above that we are referencing some custom attributes, which are explained below.\n\nFor more `SpaczzRuler` examples see [here](https://github.com/gandersen101/spaczz/blob/master/examples/fuzzy_matching_tweaks.ipynb). In particular this provides details about the ruler's sorting process and fuzzy matching parameters.\n\n### Custom Attributes\n\nSpaczz initializes some custom attributes upon importing. These are under spaCy's `._.` attribute and are further prepended with `spaczz_` so there should be not conflicts with your own custom attributes. If there are spaczz will force overwrite them.\n\nThese custom attributes are only set via the spaczz ruler at the token level. Span and doc versions of these attributes are getters that reference the token level attributes.\n\nThe following `Token` attributes are available. All are mutable:\n\n- `spaczz_token`: default = `False`. Boolean that denotes if the token is part of an entity set by the spaczz ruler.\n- `spaczz_type`: default = `None`. String that shows which matcher produced an entity using the token.\n- `spaczz_ratio`: default = `None`. If the token is part of a matched entity, it will return fuzzy ratio.\n- `spaczz_pattern`: default = `None`. If the token is part of a matched entity, it will return the pattern as a string (JSON-formatted for token patterns) that produced the match.\n\nThe following `Span` attributes reference the token attributes included in the span. All are immutable:\n\n- `spaczz_ent`: default = `False`. Boolean that denotes if all tokens in the span are part of an entity set by the spaczz ruler.\n- `spaczz_type`: default = `None`. String that denotes which matcher produced an entity using the included tokens.\n- `spaczz_types`: default = `set()`. Set that shows which matchers produced entities using the included tokens. An entity span should only have one type, but this allows you to see the types included in any arbitrary span.\n- `spaczz_ratio`: default = `None`. If all the tokens in span are part of a matched entity, it will return the fuzzy ratio.\n- `spaczz_pattern`: default = `None`. If all the tokens in a span are part of a matched entity, it will return the pattern as a string (JSON-formatted for token patterns) that produced the match.\n\nThe following `Doc` attributes reference the token attributes included in the doc. All are immutable:\n\n- `spaczz_doc`: default = `False`. Boolean that denotes if any tokens in the doc are part of an entity set by the spaczz ruler.\n- `spaczz_types`: default = `set()`. Set that shows which matchers produced entities in the doc.\n\n### Saving/Loading\n\nThe `SpaczzRuler` has it's own to/from disk/bytes methods and will accept `config` parameters passed to `spacy.load()`. It also has it's own spaCy factory entry point so spaCy is aware of the `SpaczzRuler`. Below is an example of saving and loading a spacy pipeline with the small English model, the `EntityRuler`, and the `SpaczzRuler`.\n\n\n```python\nimport spacy\nfrom spaczz.pipeline import SpaczzRuler\n\nnlp = spacy.load(\"en_core_web_md\")\ntext = \"\"\"Anderson, Grint created spaczz in his home at 555 Fake St,\nApt 5 in Nashv1le, TN 55555-1234 in the USA.\nSome of his favorite bands are Converg and Protet the Zero.\"\"\"  # Spelling errors intentional.\ndoc = nlp(text)\n\nfor ent in doc.ents:\n    print((ent.text, ent.start, ent.end, ent.label_))\n```\n\n    ('Anderson, Grint', 0, 3, 'ORG')\n    ('555', 9, 10, 'CARDINAL')\n    ('Apt 5', 14, 16, 'PRODUCT')\n    ('Nashv1le', 17, 18, 'GPE')\n    ('TN 55555-1234', 19, 23, 'ORG')\n    ('USA', 25, 26, 'GPE')\n    ('Converg', 34, 35, 'PERSON')\n    ('Zero', 38, 39, 'CARDINAL')\n\n\nWhile spaCy does a decent job of identifying that named entities are present in this example, we can definitely improve the matches - particularly with the types of labels applied.\n\nLet's add an entity ruler for some rules-based matches.\n\n\n```python\nfrom spacy.pipeline import EntityRuler\n\nentity_ruler = nlp.add_pipe(\"entity_ruler\", before=\"ner\") #spaCy v3 syntax\nentity_ruler.add_patterns(\n    [{\"label\": \"GPE\", \"pattern\": \"Nashville\"}, {\"label\": \"GPE\", \"pattern\": \"TN\"}]\n)\n\ndoc = nlp(text)\n\nfor ent in doc.ents:\n    print((ent.text, ent.start, ent.end, ent.label_))\n```\n\n    ('Anderson, Grint', 0, 3, 'ORG')\n    ('555', 9, 10, 'CARDINAL')\n    ('Apt 5', 14, 16, 'PRODUCT')\n    ('Nashv1le', 17, 18, 'GPE')\n    ('TN', 19, 20, 'GPE')\n    ('USA', 25, 26, 'GPE')\n    ('Converg', 34, 35, 'PERSON')\n    ('Zero', 38, 39, 'CARDINAL')\n\n\nWe're making progress, but Nashville is spelled wrong in the text so the entity ruler does not find it, and we still have other entities to fix/find.\n\nLet's add a spaczz ruler to round this pipeline out. We will also include the `spaczz_span` custom attribute in the results to denote which entities were set via spaczz.\n\n\n```python\nspaczz_ruler = nlp.add_pipe(\"spaczz_ruler\", before=\"ner\") #spaCy v3 syntax\nspaczz_ruler.add_patterns(\n    [\n        {\n            \"label\": \"NAME\",\n            \"pattern\": \"Grant Andersen\",\n            \"type\": \"fuzzy\",\n            \"kwargs\": {\"fuzzy_func\": \"token_sort\"},\n        },\n        {\n            \"label\": \"STREET\",\n            \"pattern\": \"street_addresses\",\n            \"type\": \"regex\",\n            \"kwargs\": {\"predef\": True},\n        },\n        {\"label\": \"GPE\", \"pattern\": \"Nashville\", \"type\": \"fuzzy\"},\n        {\n            \"label\": \"ZIP\",\n            \"pattern\": r\"\\b(?:55554){s\u003c=1}(?:[-\\s]\\d{4})?\\b\",\n            \"type\": \"regex\",\n        },  # fuzzy regex\n        {\n            \"label\": \"BAND\",\n            \"pattern\": [{\"LOWER\": {\"FREGEX\": \"(converge){e\u003c=1}\"}}],\n            \"type\": \"token\",\n        },\n        {\n            \"label\": \"BAND\",\n            \"pattern\": [\n                {\"TEXT\": {\"FUZZY\": \"Protest\"}},\n                {\"IS_STOP\": True},\n                {\"TEXT\": {\"FUZZY\": \"Hero\"}},\n            ],\n            \"type\": \"token\",\n        },\n    ]\n)\n\ndoc = nlp(text)\n\nfor ent in doc.ents:\n    print((ent.text, ent.start, ent.end, ent.label_, ent._.spaczz_ent))\n```\n\n    ('Anderson, Grint', 0, 3, 'NAME', True)\n    ('555 Fake St,', 9, 13, 'STREET', True)\n    ('Apt 5', 14, 16, 'PRODUCT', False)\n    ('Nashv1le', 17, 18, 'GPE', True)\n    ('TN', 19, 20, 'GPE', False)\n    ('55555-1234', 20, 23, 'ZIP', True)\n    ('USA', 25, 26, 'GPE', False)\n    ('Converg', 34, 35, 'BAND', True)\n    ('Protet the Zero', 36, 39, 'BAND', True)\n\n\nAwesome! The small English model still makes a named entity recognition mistake (\"5\" in \"Apt 5\" as `CARDINAL`), but we're satisfied overall.\n\nLet's save this pipeline to disk and make sure we can load it back correctly.\n\n\n```python\nimport tempfile\n\nwith tempfile.TemporaryDirectory() as tmp_dir:\n    nlp.to_disk(f\"{tmp_dir}/example_pipeline\")\n    nlp = spacy.load(f\"{tmp_dir}/example_pipeline\")\n\nnlp.pipe_names\n```\n\n\n\n\n    ['tok2vec',\n     'tagger',\n     'parser',\n     'attribute_ruler',\n     'lemmatizer',\n     'entity_ruler',\n     'spaczz_ruler',\n     'ner']\n\n\n\nWe can even ensure all the spaczz ruler patterns are still present.\n\n\n```python\nspaczz_ruler = nlp.get_pipe(\"spaczz_ruler\")\nspaczz_ruler.patterns\n```\n\n\n\n\n    [{'label': 'NAME',\n      'pattern': 'Grant Andersen',\n      'type': 'fuzzy',\n      'kwargs': {'fuzzy_func': 'token_sort'}},\n     {'label': 'GPE', 'pattern': 'Nashville', 'type': 'fuzzy'},\n     {'label': 'STREET',\n      'pattern': 'street_addresses',\n      'type': 'regex',\n      'kwargs': {'predef': True}},\n     {'label': 'ZIP',\n      'pattern': '\\\\b(?:55554){s\u003c=1}(?:[-\\\\s]\\\\d{4})?\\\\b',\n      'type': 'regex'},\n     {'label': 'BAND',\n      'pattern': [{'LOWER': {'FREGEX': '(converge){e\u003c=1}'}}],\n      'type': 'token'},\n     {'label': 'BAND',\n      'pattern': [{'TEXT': {'FUZZY': 'Protest'}},\n       {'IS_STOP': True},\n       {'TEXT': {'FUZZY': 'Hero'}}],\n      'type': 'token'}]\n\n\n\n## Known Issues\n\n### Performance\n\nThe main reason for spaczz's slower speed is that the *c* in it's name is not capitalized like it is in spa*C*y.\nSpaczz is written in pure Python and it's matchers do not currently utilize spaCy language vocabularies, which means following it's logic should be easy to those familiar with Python. However this means spaczz components will run slower and likely consume more memory than their spaCy counterparts, especially as more patterns are added and documents get longer. It is therefore recommended to use spaCy components like the EntityRuler for entities with little uncertainty, like consistent spelling errors. Use spaczz components when there are not viable spaCy alternatives.\n\nI am *not* currently working on performance optimizations to spaczz, but algorithmic and optimization suggestions are welcome.\n\nThe primary methods for speeding up the `FuzzyMatcher` and `SimilarityMatcher` are by decreasing the `flex` parameter towards `0`, or if `flex \u003e 0`, increasing the `min_r1` parameter towards the value of `min_r2` and/or lowering the `thresh` parameter towards `min_r2`. Be aware that all of these \"speed-ups\" come at the opportunity cost of potentially improved matches.\n\nAs mentioned in the `SimilarityMatcher` description, utilizing a GPU may also help speed up it's matching process.\n\n## Roadmap\n\nI am always open and receptive to feature requests but just be aware, as a solo-dev with a lot left to learn, development can move pretty slow. The following is my roadmap for spaczz so you can see where issues raised might fit into my current priorities.\n\n*Note: while I want to keep `spaczz` functional, I am **not** actively developing it. I try to be responsive to issues and requests but this project is not currently a focus of mine.*\n\n**High Priority**\n\n1. Bug fixes - both breaking and behavioral. Hopefully these will be minimal.\n1. Ease of use and error/warning handling and messaging enhancements.\n1. Building out Read the Docs.\n1. Option to prioritize match quality over length and/or weighing options.\n1. Profiling - hopefully to find \"easy\" performance optimizations.\n\n**Enhancements**\n\n1. Having spaczz matchers utilize spaCy vocabularies.\n1. Rewrite the phrase and token searching algorithms in Cython to utilize C speed.\n    1. Try to integrate closer with spaCy.\n\n## Development\n\nPull requests and contributors are welcome.\n\nspaczz is linted with [Flake8](https://flake8.pycqa.org/en/latest/), formatted with [Black](https://black.readthedocs.io/en/stable/), type-checked with [MyPy](http://mypy-lang.org/) (although this could benefit from improved specificity), tested with [Pytest](https://docs.pytest.org/en/stable/), automated with [Nox](https://nox.thea.codes/en/stable/), and built/packaged with [Poetry](https://python-poetry.org/). There are a few other development tools detailed in the noxfile.py, along with Git pre-commit hooks.\n\nTo contribute to spaczz's development, fork the repository then install spaczz and it's dev dependencies with Poetry. If you're interested in being a regular contributor please contact me directly.\n\n```python\npoetry install # Within spaczz's root directory.\n```\n\nI keep Nox and pre-commit outside of my poetry environment as part of my Python toolchain environments. With pre-commit installed you may also need to run the below to commit changes.\n\n```python\npre-commit install\n```\n\nThe only other package that will not be installed via Poetry but is used for testing and in-documentation examples is the spaCy medium English model (`en-core-web-md`). This will need to be installed separately. The command below should do the trick:\n\n```python\npoetry run python -m spacy download \"en_core_web_md\"\n```\n\n## References\n\n- spaczz tries to stay as close to [spaCy](https://spacy.io/)'s API as possible. Whenever it made sense to use existing spaCy code within spaczz this was done.\n- Fuzzy matching is performed using [RapidFuzz](https://github.com/maxbachmann/rapidfuzz).\n- Regexes are performed using the [regex](https://pypi.org/project/regex/) library.\n- The search algorithm for phrased-based fuzzy and similarity matching was heavily influnced by Stack Overflow user Ulf Aslak's answer in this [thread](https://stackoverflow.com/questions/36013295/find-best-substring-match).\n- spaczz's predefined regex patterns were borrowed from the [commonregex](https://github.com/madisonmay/CommonRegex) package.\n- spaczz's development and CI/CD patterns were inspired by Claudio Jolowicz's [*Hypermodern Python*](https://cjolowicz.github.io/posts/hypermodern-python-01-setup/) article series.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgandersen101%2Fspaczz","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fgandersen101%2Fspaczz","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgandersen101%2Fspaczz/lists"}