Skip to content

Commit

Permalink
A workaround is removed, and word highlighting is systematically impr…
Browse files Browse the repository at this point in the history
…oved for cases where the Porter stemmer produces stems that are not substrings of original words (closes #25).
  • Loading branch information
parpalak committed Nov 16, 2023
1 parent 5a56448 commit a9acae7
Show file tree
Hide file tree
Showing 6 changed files with 80 additions and 11 deletions.
14 changes: 11 additions & 3 deletions src/S2/Rose/Entity/ResultItem.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
13 changes: 10 additions & 3 deletions src/S2/Rose/Snippet/SnippetBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -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)
);

Expand All @@ -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
Expand Down
20 changes: 20 additions & 0 deletions src/S2/Rose/Stemmer/IrregularWordsStemmerInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
12 changes: 12 additions & 0 deletions src/S2/Rose/Stemmer/PorterStemmerEnglish.php
Original file line number Diff line number Diff line change
Expand Up @@ -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() : []);
}
}
10 changes: 10 additions & 0 deletions src/S2/Rose/Stemmer/PorterStemmerRussian.php
Original file line number Diff line number Diff line change
Expand Up @@ -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() : []);
}
}
22 changes: 17 additions & 5 deletions tests/unit/Rose/IntegrationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ public function testFeatures(
'Тут есть тонкость - нужно проверить, как происходит экранировка в <i>сущностях</i> вроде + и &amp;<i>plus</i>;. Для этого нужно включить в текст само сочетание букв "<i>plus</i>".',
$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('эпл'));
Expand All @@ -171,7 +171,7 @@ public function testFeatures(
'Русский текст. <b>Красным заголовком</b>',
$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('русский'));
Expand All @@ -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(''));
Expand Down Expand Up @@ -226,6 +226,18 @@ public function testFeatures(
$resultSet9->getItems()[0]->getSnippet()
);

$resultSet9 = $finder->find(new Query('Gallery'));
$this->assertEquals(
'Или что-то может называться словом <b>Gallery</b>.',
$resultSet9->getItems()[0]->getSnippet()
);

$resultSet9 = $finder->find(new Query('legacy'));
$this->assertEquals(
'Some <b>legacy</b>. To be continued...',
$resultSet9->getItems()[0]->getHighlightedTitle($stemmer)
);

// Query 10
$resultSet10 = $finder->find(new Query('singlekeyword'));
$this->assertCount(1, $resultSet10->getItems());
Expand Down Expand Up @@ -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', 'Русский текст. Красным заголовком', '<p>Для проверки работы нужно написать побольше слов. В 1,7 раз больше. Вот еще одно предложение.</p><p>Тут есть тонкость - нужно проверить, как происходит экранировка в сущностях вроде &plus; и &amp;plus;. Для этого нужно включить в текст само сочетание букв "plus".</p><p>Еще одна особенность - наличие слов с дефисом. Например, красно-черный, эпл-вотчем, и другие интересные комбинации. Встречаются и другие знаки препинания, например, цифры. Я не помню Windows 3.1, но помню Turbo Pascal 7.0. Надо отдельно посмотреть, что ищется по одной цифре 7... Учитель не должен допускать такого...</p><p>А еще текст бывает на других языках. Например, в украинском есть слово ціна.</p>', 20))
(new Indexable('id_3', 'Русский текст. Красным заголовком', '<p>Для проверки работы нужно написать побольше слов. В 1,7 раз больше. Вот еще одно предложение.</p><p>Тут есть тонкость - нужно проверить, как происходит экранировка в сущностях вроде &plus; и &amp;plus;. Для этого нужно включить в текст само сочетание букв "plus".</p><p>Еще одна особенность - наличие слов с дефисом. Например, красно-черный, эпл-вотчем, и другие интересные комбинации. Встречаются и другие знаки препинания, например, цифры. Я не помню Windows 3.1, но помню Turbo Pascal 7.0. Надо отдельно посмотреть, что ищется по одной цифре 7... Учитель не должен допускать такого...</p><p>А еще текст бывает на других языках. Например, в украинском есть слово ціна. Или что-то может называться словом Gallery.</p>', 20))
->setKeywords('ключевые слова')
->setDescription('')
->setDate(new \DateTime('2016-08-22 00:00:00'))
Expand Down

0 comments on commit a9acae7

Please sign in to comment.