diff --git a/spacy/lang/ru/lemmatizer.py b/spacy/lang/ru/lemmatizer.py index 8833f1a16..300d61c52 100644 --- a/spacy/lang/ru/lemmatizer.py +++ b/spacy/lang/ru/lemmatizer.py @@ -9,18 +9,19 @@ from ...compat import unicode_ class RussianLemmatizer(Lemmatizer): _morph = None - def __init__(self, pymorphy2_lang='ru'): + def __init__(self): super(RussianLemmatizer, self).__init__() try: from pymorphy2 import MorphAnalyzer except ImportError: raise ImportError( "The Russian lemmatizer requires the pymorphy2 library: " - 'try to fix it with "pip install pymorphy2==0.8"' + 'try to fix it with "pip install pymorphy2==0.8" ' + 'or "pip install git+https://github.com/kmike/pymorphy2.git pymorphy2-dicts-uk"' + "if you need Ukrainian too" ) - if RussianLemmatizer._morph is None: - RussianLemmatizer._morph = MorphAnalyzer(lang=pymorphy2_lang) + RussianLemmatizer._morph = MorphAnalyzer() def __call__(self, string, univ_pos, morphology=None): univ_pos = self.normalize_univ_pos(univ_pos) diff --git a/spacy/lang/uk/lemmatizer.py b/spacy/lang/uk/lemmatizer.py index 867cd3943..f41f36fe2 100644 --- a/spacy/lang/uk/lemmatizer.py +++ b/spacy/lang/uk/lemmatizer.py @@ -1,15 +1,207 @@ # coding: utf8 -from __future__ import unicode_literals - -from ..ru.lemmatizer import RussianLemmatizer +from ...symbols import ADJ, DET, NOUN, NUM, PRON, PROPN, PUNCT, VERB, POS +from ...lemmatizer import Lemmatizer -class UkrainianLemmatizer(RussianLemmatizer): - def __init__(self, pymorphy2_lang="ru"): +class UkrainianLemmatizer(Lemmatizer): + _morph = None + + def __init__(self): + super(UkrainianLemmatizer, self).__init__() try: - super(UkrainianLemmatizer, self).__init__(pymorphy2_lang="uk") - except ImportError: + from pymorphy2 import MorphAnalyzer + + if UkrainianLemmatizer._morph is None: + UkrainianLemmatizer._morph = MorphAnalyzer(lang="uk") + except (ImportError, TypeError): raise ImportError( - "The Ukrainian lemmatizer requires the pymorphy2 library and dictionaries: " - 'try to fix it with "pip install git+https://github.com/kmike/pymorphy2.git pymorphy2-dicts-uk"' + "The Ukrainian lemmatizer requires the pymorphy2 library and + 'dictionaries: try to fix it with "pip uninstall pymorphy2" and' + '"pip install git+https://github.com/kmike/pymorphy2.git pymorphy2-dicts-uk"' ) + + def __call__(self, string, univ_pos, morphology=None): + univ_pos = self.normalize_univ_pos(univ_pos) + if univ_pos == "PUNCT": + return [PUNCT_RULES.get(string, string)] + + if univ_pos not in ("ADJ", "DET", "NOUN", "NUM", "PRON", "PROPN", "VERB"): + # Skip unchangeable pos + return [string.lower()] + + analyses = self._morph.parse(string) + filtered_analyses = [] + for analysis in analyses: + if not analysis.is_known: + # Skip suggested parse variant for unknown word for pymorphy + continue + analysis_pos, _ = oc2ud(str(analysis.tag)) + if analysis_pos == univ_pos or ( + analysis_pos in ("NOUN", "PROPN") and univ_pos in ("NOUN", "PROPN") + ): + filtered_analyses.append(analysis) + + if not len(filtered_analyses): + return [string.lower()] + if morphology is None or (len(morphology) == 1 and POS in morphology): + return list(set([analysis.normal_form for analysis in filtered_analyses])) + + if univ_pos in ("ADJ", "DET", "NOUN", "PROPN"): + features_to_compare = ["Case", "Number", "Gender"] + elif univ_pos == "NUM": + features_to_compare = ["Case", "Gender"] + elif univ_pos == "PRON": + features_to_compare = ["Case", "Number", "Gender", "Person"] + else: # VERB + features_to_compare = [ + "Aspect", + "Gender", + "Mood", + "Number", + "Tense", + "VerbForm", + "Voice", + ] + + analyses, filtered_analyses = filtered_analyses, [] + for analysis in analyses: + _, analysis_morph = oc2ud(str(analysis.tag)) + for feature in features_to_compare: + if ( + feature in morphology + and feature in analysis_morph + and morphology[feature] != analysis_morph[feature] + ): + break + else: + filtered_analyses.append(analysis) + + if not len(filtered_analyses): + return [string.lower()] + return list(set([analysis.normal_form for analysis in filtered_analyses])) + + @staticmethod + def normalize_univ_pos(univ_pos): + if isinstance(univ_pos, str): + return univ_pos.upper() + + symbols_to_str = { + ADJ: "ADJ", + DET: "DET", + NOUN: "NOUN", + NUM: "NUM", + PRON: "PRON", + PROPN: "PROPN", + PUNCT: "PUNCT", + VERB: "VERB", + } + if univ_pos in symbols_to_str: + return symbols_to_str[univ_pos] + return None + + def is_base_form(self, univ_pos, morphology=None): + # TODO + raise NotImplementedError + + def det(self, string, morphology=None): + return self(string, "det", morphology) + + def num(self, string, morphology=None): + return self(string, "num", morphology) + + def pron(self, string, morphology=None): + return self(string, "pron", morphology) + + def lookup(self, string): + analyses = self._morph.parse(string) + if len(analyses) == 1: + return analyses[0].normal_form + return string + + +def oc2ud(oc_tag): + gram_map = { + "_POS": { + "ADJF": "ADJ", + "ADJS": "ADJ", + "ADVB": "ADV", + "Apro": "DET", + "COMP": "ADJ", # Can also be an ADV - unchangeable + "CONJ": "CCONJ", # Can also be a SCONJ - both unchangeable ones + "GRND": "VERB", + "INFN": "VERB", + "INTJ": "INTJ", + "NOUN": "NOUN", + "NPRO": "PRON", + "NUMR": "NUM", + "NUMB": "NUM", + "PNCT": "PUNCT", + "PRCL": "PART", + "PREP": "ADP", + "PRTF": "VERB", + "PRTS": "VERB", + "VERB": "VERB", + }, + "Animacy": {"anim": "Anim", "inan": "Inan"}, + "Aspect": {"impf": "Imp", "perf": "Perf"}, + "Case": { + "ablt": "Ins", + "accs": "Acc", + "datv": "Dat", + "gen1": "Gen", + "gen2": "Gen", + "gent": "Gen", + "loc2": "Loc", + "loct": "Loc", + "nomn": "Nom", + "voct": "Voc", + }, + "Degree": {"COMP": "Cmp", "Supr": "Sup"}, + "Gender": {"femn": "Fem", "masc": "Masc", "neut": "Neut"}, + "Mood": {"impr": "Imp", "indc": "Ind"}, + "Number": {"plur": "Plur", "sing": "Sing"}, + "NumForm": {"NUMB": "Digit"}, + "Person": {"1per": "1", "2per": "2", "3per": "3", "excl": "2", "incl": "1"}, + "Tense": {"futr": "Fut", "past": "Past", "pres": "Pres"}, + "Variant": {"ADJS": "Brev", "PRTS": "Brev"}, + "VerbForm": { + "GRND": "Conv", + "INFN": "Inf", + "PRTF": "Part", + "PRTS": "Part", + "VERB": "Fin", + }, + "Voice": {"actv": "Act", "pssv": "Pass"}, + "Abbr": {"Abbr": "Yes"}, + } + + pos = "X" + morphology = dict() + unmatched = set() + + grams = oc_tag.replace(" ", ",").split(",") + for gram in grams: + match = False + for categ, gmap in sorted(gram_map.items()): + if gram in gmap: + match = True + if categ == "_POS": + pos = gmap[gram] + else: + morphology[categ] = gmap[gram] + if not match: + unmatched.add(gram) + + while len(unmatched) > 0: + gram = unmatched.pop() + if gram in ("Name", "Patr", "Surn", "Geox", "Orgn"): + pos = "PROPN" + elif gram == "Auxt": + pos = "AUX" + elif gram == "Pltm": + morphology["Number"] = "Ptan" + + return pos, morphology + + +PUNCT_RULES = {"«": '"', "»": '"'} diff --git a/spacy/lang/uk/stop_words.py b/spacy/lang/uk/stop_words.py index 83e86d937..cdf24dd70 100644 --- a/spacy/lang/uk/stop_words.py +++ b/spacy/lang/uk/stop_words.py @@ -6,8 +6,10 @@ STOP_WORDS = set( """а або адже +аж але алло +б багато без безперервно @@ -16,6 +18,7 @@ STOP_WORDS = set( більше біля близько +бо був буває буде @@ -29,22 +32,27 @@ STOP_WORDS = set( були було бути -бывь в -важлива -важливе -важливий -важливі вам вами вас ваш ваша ваше +вашим +вашими +ваших ваші +вашій +вашого +вашої +вашому +вашою +вашу вгорі вгору вдалині +весь вже ви від @@ -59,7 +67,15 @@ STOP_WORDS = set( вони воно восьмий +все +всею +всі +всім +всіх всього +всьому +всю +вся втім г геть @@ -95,16 +111,15 @@ STOP_WORDS = set( досить другий дуже +дякую +е +є +ж же -життя з за завжди зазвичай -зайнята -зайнятий -зайняті -зайнято занадто зараз зате @@ -112,22 +127,28 @@ STOP_WORDS = set( звідси звідусіль здається +зі значить знову зовсім -ім'я +і +із +її +їй +їм іноді інша інше інший інших інші -її -їй їх +й його йому +каже ким +кілька кого кожен кожна @@ -136,13 +157,13 @@ STOP_WORDS = set( коли кому краще -крейдуючи -кругом +крім куди ласка +ледве лише -люди -людина +м +має майже мало мати @@ -157,20 +178,27 @@ STOP_WORDS = set( мій мільйонів мною +мого могти моє -мож +моєї +моєму +моєю може можна можно можуть -можхо мої -мор +моїй +моїм +моїми +моїх +мою моя на навіть навіщо +навколо навкруги нагорі над @@ -183,10 +211,21 @@ STOP_WORDS = set( наш наша наше +нашим +нашими +наших наші +нашій +нашого +нашої +нашому +нашою +нашу не небагато +небудь недалеко +неї немає нерідко нещодавно @@ -199,17 +238,22 @@ STOP_WORDS = set( них ні ніби +ніж +ній ніколи нікуди +нім нічого ну -нх нього +ньому о +обидва обоє один одинадцятий одинадцять +однак однієї одній одного @@ -218,11 +262,16 @@ STOP_WORDS = set( он особливо ось +п'ятий +п'ятнадцятий +п'ятнадцять +п'ять перед перший під пізніше пір +після по повинно подів @@ -233,18 +282,14 @@ STOP_WORDS = set( потім потрібно почала -прекрасне -прекрасно +початку при про просто проте проти -п'ятий -п'ятнадцятий -п'ятнадцять -п'ять раз +разу раніше рано раптом @@ -252,6 +297,7 @@ STOP_WORDS = set( роки років року +році сам сама саме @@ -264,15 +310,15 @@ STOP_WORDS = set( самого самому саму -світу свого своє +своєї свої своїй своїх свою -сеаой себе +сих сім сімнадцятий сімнадцять @@ -300,7 +346,17 @@ STOP_WORDS = set( також там твій +твого твоє +твоєї +твоєму +твоєю +твої +твоїй +твоїм +твоїми +твоїх +твою твоя те тебе @@ -312,15 +368,19 @@ STOP_WORDS = set( тисяч тих ті +тієї тією +тій тільки +тім +то тобі тобою того тоді той -том тому +тою треба третій три @@ -338,7 +398,6 @@ STOP_WORDS = set( усім усіма усіх -усію усього усьому усю @@ -356,6 +415,7 @@ STOP_WORDS = set( цими цих ці +цієї цій цього цьому @@ -368,11 +428,24 @@ STOP_WORDS = set( через четвертий чи +чиє +чиєї +чиєму +чиї +чиїй +чиїм +чиїми +чиїх +чий +чийого +чийому чим численна численне численний численні +чию +чия чого чому чотири @@ -385,6 +458,8 @@ STOP_WORDS = set( ще що щоб +щодо +щось я як яка @@ -393,6 +468,6 @@ STOP_WORDS = set( які якій якого -якщо -""".split() +якої +якщо""".split() ) diff --git a/spacy/language.py b/spacy/language.py index c1abb62b4..0c0cf8854 100644 --- a/spacy/language.py +++ b/spacy/language.py @@ -573,7 +573,7 @@ class Language(object): proc._rehearsal_model = deepcopy(proc.model) return self._optimizer - def evaluate(self, docs_golds, verbose=False): + def evaluate(self, docs_golds, verbose=False, batch_size=256): scorer = Scorer() docs, golds = zip(*docs_golds) docs = list(docs) @@ -582,7 +582,7 @@ class Language(object): if not hasattr(pipe, "pipe"): docs = (pipe(doc) for doc in docs) else: - docs = pipe.pipe(docs, batch_size=256) + docs = pipe.pipe(docs, batch_size=batch_size) for doc, gold in zip(docs, golds): if verbose: print(doc) diff --git a/spacy/tests/conftest.py b/spacy/tests/conftest.py index 2d50e3048..3f35bba8c 100644 --- a/spacy/tests/conftest.py +++ b/spacy/tests/conftest.py @@ -179,6 +179,7 @@ def tt_tokenizer(): @pytest.fixture(scope="session") def uk_tokenizer(): pytest.importorskip("pymorphy2") + pytest.importorskip("pymorphy2.lang") return get_lang_class("uk").Defaults.create_tokenizer() diff --git a/spacy/tests/lang/uk/test_tokenizer.py b/spacy/tests/lang/uk/test_tokenizer.py index 860d21953..f744b32b0 100644 --- a/spacy/tests/lang/uk/test_tokenizer.py +++ b/spacy/tests/lang/uk/test_tokenizer.py @@ -92,6 +92,7 @@ def test_uk_tokenizer_splits_open_appostrophe(uk_tokenizer, text): assert tokens[0].text == "'" +@pytest.mark.xfail(reason="See #3327") @pytest.mark.parametrize("text", ["Тест''"]) def test_uk_tokenizer_splits_double_end_quote(uk_tokenizer, text): tokens = uk_tokenizer(text) diff --git a/spacy/tests/regression/test_issue3328.py b/spacy/tests/regression/test_issue3328.py new file mode 100644 index 000000000..fce25ca1c --- /dev/null +++ b/spacy/tests/regression/test_issue3328.py @@ -0,0 +1,21 @@ +# coding: utf-8 +from __future__ import unicode_literals + +import pytest +from spacy.matcher import Matcher +from spacy.tokens import Doc + + +@pytest.mark.xfail +def test_issue3328(en_vocab): + doc = Doc(en_vocab, words=["Hello", ",", "how", "are", "you", "doing", "?"]) + matcher = Matcher(en_vocab) + patterns = [ + [{"LOWER": {"IN": ["hello", "how"]}}], + [{"LOWER": {"IN": ["you", "doing"]}}], + ] + matcher.add("TEST", None, *patterns) + matches = matcher(doc) + assert len(matches) == 4 + matched_texts = [doc[start:end].text for _, start, end in matches] + assert matched_texts == ["Hello", "how", "you", "doing"] diff --git a/website/meta/site.json b/website/meta/site.json index 847a14b55..2b65e1d84 100644 --- a/website/meta/site.json +++ b/website/meta/site.json @@ -22,6 +22,10 @@ "id": "83b0498b1e7fa3c91ce68c3f1", "list": "89ad33e698" }, + "docSearch": { + "apiKey": "371e26ed49d29a27bd36273dfdaf89af", + "indexName": "spacy" + }, "spacyVersion": "2.1", "binderUrl": "ines/spacy-io-binder", "binderBranch": "nightly", diff --git a/website/src/components/icon.js b/website/src/components/icon.js index 246820a2f..d9f0ce7c8 100644 --- a/website/src/components/icon.js +++ b/website/src/components/icon.js @@ -18,6 +18,7 @@ import { ReactComponent as YesIcon } from '../images/icons/yes.svg' import { ReactComponent as NoIcon } from '../images/icons/no.svg' import { ReactComponent as NeutralIcon } from '../images/icons/neutral.svg' import { ReactComponent as OfflineIcon } from '../images/icons/offline.svg' +import { ReactComponent as SearchIcon } from '../images/icons/search.svg' import classes from '../styles/icon.module.sass' @@ -39,6 +40,7 @@ const icons = { no: NoIcon, neutral: NeutralIcon, offline: OfflineIcon, + search: SearchIcon, } const Icon = ({ name, width, height, inline, variant, className }) => { diff --git a/website/src/components/navigation.js b/website/src/components/navigation.js index abf8a913b..bd83f7c44 100644 --- a/website/src/components/navigation.js +++ b/website/src/components/navigation.js @@ -1,6 +1,7 @@ import React from 'react' import PropTypes from 'prop-types' import classNames from 'classnames' +import { navigate } from 'gatsby' import Link from './link' import Icon from './icon' @@ -8,36 +9,64 @@ import { github } from './util' import { ReactComponent as Logo } from '../images/logo.svg' import classes from '../styles/navigation.module.sass' -const Navigation = ({ title, items, section, children }) => ( - + ) +} Navigation.defaultProps = { items: [], @@ -52,6 +81,7 @@ Navigation.propTypes = { }) ), section: PropTypes.string, + search: PropTypes.node, } export default Navigation diff --git a/website/src/components/search.js b/website/src/components/search.js new file mode 100644 index 000000000..0ea4ec993 --- /dev/null +++ b/website/src/components/search.js @@ -0,0 +1,52 @@ +import React, { useEffect, useState } from 'react' +import PropTypes from 'prop-types' +import { window } from 'browser-monads' + +import Icon from './icon' +import classes from '../styles/search.module.sass' + +const Search = ({ id, placeholder, settings }) => { + const { apiKey, indexName } = settings + const [isInitialized, setIsInitialized] = useState(false) + useEffect(() => { + if (!isInitialized) { + setIsInitialized(true) + window.docsearch({ + apiKey, + indexName, + inputSelector: `#${id}`, + debug: false, + }) + } + }, window.docsearch) + return ( +
+ + +
+ ) +} + +Search.defaultProps = { + id: 'docsearch', + placeholder: 'Search docs', +} + +Search.propTypes = { + settings: PropTypes.shape({ + apiKey: PropTypes.string.isRequired, + indexName: PropTypes.string.isRequired, + }).isRequired, + id: PropTypes.string.isRequired, + placeholder: PropTypes.string.isRequired, +} + +export default Search diff --git a/website/src/components/title.js b/website/src/components/title.js index 285ad9192..1eadb3deb 100644 --- a/website/src/components/title.js +++ b/website/src/components/title.js @@ -1,5 +1,6 @@ import React from 'react' import PropTypes from 'prop-types' +import classNames from 'classnames' import Button from './button' import Tag from './tag' @@ -34,7 +35,7 @@ const Title = ({ title, tag, version, teaser, source, image, children, ...props )} - {teaser &&
{teaser}
} + {teaser &&
{teaser}
} {children} diff --git a/website/src/html.js b/website/src/html.js new file mode 100644 index 000000000..53e50bc8a --- /dev/null +++ b/website/src/html.js @@ -0,0 +1,43 @@ +import React from 'react' +import PropTypes from 'prop-types' + +export default function HTML(props) { + return ( + + + + + + {props.headComponents} + + + + {props.preBodyComponents} + +
+ {props.postBodyComponents} + +