diff --git a/src/harmony/matching/matcher.py b/src/harmony/matching/matcher.py index 46a59db..ac39444 100644 --- a/src/harmony/matching/matcher.py +++ b/src/harmony/matching/matcher.py @@ -115,7 +115,22 @@ def add_text_to_vec(text, texts_cached_vectors, text_vectors, is_negated_, is_qu return text_vectors -def process_questions(questions: list, texts_cached_vectors: dict, is_negate: bool) -> list[TextVector]: +def get_question_languages(instruments: List[Instrument]) -> dict: + """ + Map each question text to the language of the instrument it came from, so that negation uses the right rules. + """ + question_languages = {} + for instrument in instruments: + language = getattr(instrument.language, "value", instrument.language) or "en" + for question in instrument.questions: + question_languages[question.question_text] = language + return question_languages + + +def process_questions(questions: list, texts_cached_vectors: dict, is_negate: bool, + question_languages: Optional[dict] = None) -> list[TextVector]: + if question_languages is None: + question_languages = {} text_vectors: List[TextVector] = [] for question_text in questions: # Skip None or whitespace-only texts @@ -134,7 +149,7 @@ def process_questions(questions: list, texts_cached_vectors: dict, is_negate: bo text_vectors = add_text_to_vec(question_text, texts_cached_vectors, text_vectors, False, False) if is_negate: - negated_text = negate(question_text, 'en') + negated_text = negate(question_text, question_languages.get(question_text, "en")) else: negated_text = question_text text_vectors = add_text_to_vec(negated_text, texts_cached_vectors, text_vectors, True, False) @@ -173,14 +188,16 @@ def create_full_text_vectors( query: Optional[str], vectorisation_function: Callable, texts_cached_vectors: dict[str, list[float]], - is_negate: bool + is_negate: bool, + question_languages: Optional[dict] = None ) -> tuple[List[TextVector], dict]: """ Create full text vectors. """ # Create a list of text vectors - text_vectors = process_questions(all_questions, texts_cached_vectors, is_negate=is_negate) + text_vectors = process_questions(all_questions, texts_cached_vectors, is_negate=is_negate, + question_languages=question_languages) # Add query if query: @@ -236,7 +253,8 @@ def match_instruments_with_catalogue_instruments( query=None, vectorisation_function=vectorisation_function, texts_cached_vectors=texts_cached_vectors, - is_negate=is_negate + is_negate=is_negate, + question_languages=get_question_languages(instruments) ) # For each instrument, find the best instrument matches for it in the catalogue @@ -626,7 +644,8 @@ def match_instruments_with_function( query=query, vectorisation_function=vectorisation_function, texts_cached_vectors=texts_cached_vectors, - is_negate=is_negate + is_negate=is_negate, + question_languages=get_question_languages(instruments) ) # get vectors for all original texts and vectors for negated texts diff --git a/tests/test_negation_language.py b/tests/test_negation_language.py new file mode 100644 index 0000000..5c37bf3 --- /dev/null +++ b/tests/test_negation_language.py @@ -0,0 +1,68 @@ +''' +MIT License + +Copyright (c) 2023 Ulster University (https://www.ulster.ac.uk). +Project: Harmony (https://harmonydata.ac.uk) +Maintainer: Thomas Wood (https://fastdatascience.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +''' + +import sys +import unittest + +sys.path.append("../src") + +from harmony.matching.matcher import get_question_languages, process_questions +from harmony.schemas.requests.text import Instrument, Question + + +class TestNegationLanguage(unittest.TestCase): + """Regression tests for issue #142: negation must use the instrument's language, not always English.""" + + def setUp(self): + self.instrument_en = Instrument(language="en", questions=[Question(question_text="I feel depressed")]) + self.instrument_pt = Instrument(language="pt", questions=[Question(question_text="eu me sinto deprimido")]) + + def get_negated_texts(self, instruments): + question_languages = get_question_languages(instruments) + all_questions = [q.question_text for i in instruments for q in i.questions] + text_vectors = process_questions(all_questions, {}, is_negate=True, question_languages=question_languages) + return [tv.text for tv in text_vectors if tv.is_negated] + + def test_question_languages_follow_instrument(self): + question_languages = get_question_languages([self.instrument_en, self.instrument_pt]) + self.assertEqual("en", question_languages["I feel depressed"]) + self.assertEqual("pt", question_languages["eu me sinto deprimido"]) + + def test_portuguese_item_uses_portuguese_negation(self): + negated = self.get_negated_texts([self.instrument_en, self.instrument_pt]) + self.assertIn("never I feel depressed", negated) + self.assertIn("não eu me sinto deprimido", negated) + self.assertNotIn("never eu me sinto deprimido", negated) + + def test_default_is_english(self): + text_vectors = process_questions(["I feel depressed"], {}, is_negate=True) + negated = [tv.text for tv in text_vectors if tv.is_negated] + self.assertEqual(["never I feel depressed"], negated) + + +if __name__ == '__main__': + unittest.main()