diff --git a/src/S2/Rose/Entity/ResultItem.php b/src/S2/Rose/Entity/ResultItem.php index b256abb..6be17c2 100644 --- a/src/S2/Rose/Entity/ResultItem.php +++ b/src/S2/Rose/Entity/ResultItem.php @@ -165,12 +165,20 @@ public function getHighlightedTitle(StemmerInterface $stemmer): string throw new InvalidArgumentException('Highlight template must contain "%s" substring for sprintf() function.'); } - $stems = $this->foundWords; + $stems = $this->foundWords; + $stemsForRegex = $stems; if ($stemmer instanceof IrregularWordsStemmerInterface) { $stems = array_merge($stems, $stemmer->irregularWordsFromStems($this->foundWords)); + + $regexRules = $stemmer->getRegexTransformationRules(); + $stemsForRegex = array_map(static fn(string $stem): string => preg_replace( + array_keys($regexRules), + array_values($regexRules), + $stem + ), $stems); } - $joinedStems = implode('|', $stems); - $joinedStems = str_replace('е', '[её]', $joinedStems); + + $joinedStems = implode('|', $stemsForRegex); // Check the text for the query words // TODO: Make sure the modifier S works correct on cyrillic diff --git a/src/S2/Rose/Snippet/SnippetBuilder.php b/src/S2/Rose/Snippet/SnippetBuilder.php index d5dd9e2..c6b5501 100644 --- a/src/S2/Rose/Snippet/SnippetBuilder.php +++ b/src/S2/Rose/Snippet/SnippetBuilder.php @@ -65,7 +65,7 @@ public function buildSnippet(array $foundPositionsByStems, string $highlightTemp } $introSnippetLines = array_map( - static fn (SnippetSource $s) => SnippetLine::createFromSnippetSourceWithoutFoundWords($s), + static fn(SnippetSource $s) => SnippetLine::createFromSnippetSourceWithoutFoundWords($s), \array_slice($snippetSources, 0, 2) ); @@ -79,12 +79,19 @@ public function buildSnippet(array $foundPositionsByStems, string $highlightTemp return $snippet; } + $stemsForRegex = $stems; if ($this->stemmer instanceof IrregularWordsStemmerInterface) { $stems = array_merge($stems, $this->stemmer->irregularWordsFromStems($stems)); + + $regexRules = $this->stemmer->getRegexTransformationRules(); + $stemsForRegex = array_map(static fn(string $stem): string => preg_replace( + array_keys($regexRules), + array_values($regexRules), + $stem + ), $stems); } - $joinedStems = implode('|', $stems); - $joinedStems = str_replace('е', '[её]', $joinedStems); + $joinedStems = implode('|', $stemsForRegex); foreach ($snippetSources as $snippetSource) { // Check the text for the query words diff --git a/src/S2/Rose/Stemmer/IrregularWordsStemmerInterface.php b/src/S2/Rose/Stemmer/IrregularWordsStemmerInterface.php index 716bde1..497f0f9 100644 --- a/src/S2/Rose/Stemmer/IrregularWordsStemmerInterface.php +++ b/src/S2/Rose/Stemmer/IrregularWordsStemmerInterface.php @@ -14,4 +14,24 @@ interface IrregularWordsStemmerInterface * @return string[] */ public function irregularWordsFromStems(array $stems): array; + + /** + * Special method that returns transformation rules for stems provided by the stemmer. + * + * Transformation rules are regular expression patterns used to convert stems into patterns for + * searching words in the text that may match the stem. Each rule consists of a key, which is a + * regular expression to be applied to every stem returned by the stemmer, and a value, + * which is the replaced part of resulting regex applied to the text. + * + * For instance, in the English Porter stemmer, words like 'legacy' have the stem 'legaci'. + * To find words in the text with the stem 'legaci', a pattern like '\wlegac[iy]' is required. + * Therefore, the English Porter stemmer should return a rule like ['#i$#i' => '[iy]'] + * that replaces the last entry of 'i' into entry of either 'i' or 'y'. + * + * Possible false positive matches are not mistakes since found matches are checked + * through the stemmer. + * + * @return mixed + */ + public function getRegexTransformationRules(): array; } diff --git a/src/S2/Rose/Stemmer/PorterStemmerEnglish.php b/src/S2/Rose/Stemmer/PorterStemmerEnglish.php index 125296a..927ec92 100644 --- a/src/S2/Rose/Stemmer/PorterStemmerEnglish.php +++ b/src/S2/Rose/Stemmer/PorterStemmerEnglish.php @@ -695,4 +695,16 @@ protected function getIrregularWords(): array { return self::$irregularWords; } + + /** + * {@inheritdoc} + */ + public function getRegexTransformationRules(): array + { + return array_merge([ + '#i$#i' => '[iy]', // legaci -> legacy + '#e$#i' => '', // live -> living, rate -> rating + '#bl$#i' => 'bi?l', // possibl -> possibility, but abl -> able + ], $this->nextStemmer !== null ? $this->nextStemmer->getRegexTransformationRules() : []); + } } diff --git a/src/S2/Rose/Stemmer/PorterStemmerRussian.php b/src/S2/Rose/Stemmer/PorterStemmerRussian.php index 72bf198..08fdb30 100644 --- a/src/S2/Rose/Stemmer/PorterStemmerRussian.php +++ b/src/S2/Rose/Stemmer/PorterStemmerRussian.php @@ -413,4 +413,14 @@ protected function getIrregularWords(): array { return self::$irregularWords; } + + /** + * {@inheritdoc} + */ + public function getRegexTransformationRules(): array + { + return array_merge([ + '#е#i' => '[её]', + ], $this->nextStemmer !== null ? $this->nextStemmer->getRegexTransformationRules() : []); + } } diff --git a/tests/unit/Rose/IntegrationTest.php b/tests/unit/Rose/IntegrationTest.php index ff8703e..100dfd4 100644 --- a/tests/unit/Rose/IntegrationTest.php +++ b/tests/unit/Rose/IntegrationTest.php @@ -146,7 +146,7 @@ public function testFeatures( 'Тут есть тонкость - нужно проверить, как происходит экранировка в сущностях вроде + и +. Для этого нужно включить в текст само сочетание букв "plus".', $resultSet3->getItems()[0]->getSnippet() ); - $this->assertEquals(18.29895819783989, $resultSet3->getItems()[0]->getRelevance()); + $this->assertEquals(18.327969620020077, $resultSet3->getItems()[0]->getRelevance()); // Query 4 $resultSet4 = $finder->find(new Query('эпл')); @@ -171,7 +171,7 @@ public function testFeatures( 'Русский текст. Красным заголовком', $resultItems4[0]->getHighlightedTitle($stemmer) ); - $this->assertEquals(50.94436446750919, $resultSet4->getItems()[0]->getRelevance()); + $this->assertEquals(50.95596903638126, $resultSet4->getItems()[0]->getRelevance()); // Query 5 $resultSet5 = $finder->find(new Query('русский')); @@ -185,7 +185,7 @@ public function testFeatures( // Query 6 $resultSet6 = $finder->find(new Query('учитель не должен')); $this->assertCount(1, $resultSet6->getItems()); - $this->assertEquals(55.02261191427482, $resultSet6->getItems()[0]->getRelevance()); + $this->assertEquals(55.06322790532708, $resultSet6->getItems()[0]->getRelevance()); // Query 7: Test empty queries $resultSet7 = $finder->find(new Query('')); @@ -226,6 +226,18 @@ public function testFeatures( $resultSet9->getItems()[0]->getSnippet() ); + $resultSet9 = $finder->find(new Query('Gallery')); + $this->assertEquals( + 'Или что-то может называться словом Gallery.', + $resultSet9->getItems()[0]->getSnippet() + ); + + $resultSet9 = $finder->find(new Query('legacy')); + $this->assertEquals( + 'Some legacy. To be continued...', + $resultSet9->getItems()[0]->getHighlightedTitle($stemmer) + ); + // Query 10 $resultSet10 = $finder->find(new Query('singlekeyword')); $this->assertCount(1, $resultSet10->getItems()); @@ -381,14 +393,14 @@ public function indexableProvider() ->setDate(new \DateTime('2016-08-24 00:00:00')) ->setUrl('url1') , - (new Indexable('id_2', 'To be continued...', 'This is the second page to be indexed. Let\'s compose something new.', 20)) + (new Indexable('id_2', 'Some legacy. To be continued...', 'This is the second page to be indexed. Let\'s compose something new.', 20)) ->setKeywords('content, ') ->setDescription('') ->setDate(new \DateTime('2016-08-20 00:00:00+00:00')) ->setUrl('any string') ->setRelevanceRatio(3.14) , - (new Indexable('id_3', 'Русский текст. Красным заголовком', '
Для проверки работы нужно написать побольше слов. В 1,7 раз больше. Вот еще одно предложение.
Тут есть тонкость - нужно проверить, как происходит экранировка в сущностях вроде + и +. Для этого нужно включить в текст само сочетание букв "plus".
Еще одна особенность - наличие слов с дефисом. Например, красно-черный, эпл-вотчем, и другие интересные комбинации. Встречаются и другие знаки препинания, например, цифры. Я не помню Windows 3.1, но помню Turbo Pascal 7.0. Надо отдельно посмотреть, что ищется по одной цифре 7... Учитель не должен допускать такого...
А еще текст бывает на других языках. Например, в украинском есть слово ціна.
', 20)) + (new Indexable('id_3', 'Русский текст. Красным заголовком', 'Для проверки работы нужно написать побольше слов. В 1,7 раз больше. Вот еще одно предложение.
Тут есть тонкость - нужно проверить, как происходит экранировка в сущностях вроде + и +. Для этого нужно включить в текст само сочетание букв "plus".
Еще одна особенность - наличие слов с дефисом. Например, красно-черный, эпл-вотчем, и другие интересные комбинации. Встречаются и другие знаки препинания, например, цифры. Я не помню Windows 3.1, но помню Turbo Pascal 7.0. Надо отдельно посмотреть, что ищется по одной цифре 7... Учитель не должен допускать такого...
А еще текст бывает на других языках. Например, в украинском есть слово ціна. Или что-то может называться словом Gallery.
', 20)) ->setKeywords('ключевые слова') ->setDescription('') ->setDate(new \DateTime('2016-08-22 00:00:00'))