6.1
The article saves normally. The indexer either treats the two spelling variants of the same word ("korišćenja" from the title/body, "koriscenja" from the alias) as one term or stores them as two distinct terms, but never surfaces an SQL exception to the editor.
Saving fails with:
Save failed with the following error: Duplicate entry 'koriscenja-sr' for key '#__finder_terms.idx_term_language'
The article row itself is stored before the indexing event, so every retry creates another duplicate article (with alias suffix -2, -3, ...) and the "Alias already existed" message appears on top of the SQL error, which is very confusing for editors.
Joomla 6.1.1, PHP 8.4, MySQL 8.4.7, tables collation utf8mb4_unicode_ci, multilingual site (30 content languages).
Root cause (verified by reading the code and reproducing with a minimal script):
The indexer tokenises the item path/alias in addition to title and body: Result::$defaultInstructions includes Indexer::PATH_CONTEXT => ['path', 'alias'] (Result.php:51), and dashes are converted to spaces (Indexer.php:410-413), so the transliterated alias contributes the ASCII spelling koriscenja while the title/body contribute korišćenja.
#__finder_terms has UNIQUE KEY idx_term_language (term, language) and utf8mb4_unicode_ci is accent-insensitive, so the two spellings count as the SAME key value.
The INSERT that moves new terms from #__finder_tokens_aggregate into #__finder_terms (Indexer.php:522-536) groups by term, stem, ..., SOUNDEX(term). The two spellings differ in stem and SOUNDEX, so they survive as two rows within the same INSERT, and the second row violates the unique key. Whether a given word crashes depends on SOUNDEX/stem/weight quirks, which is why the bug looks random to users.
Because the plain INSERT throws, the save is aborted mid-way (article saved, index not updated).
Historic context: the same class of problem was reported for German ß/ss during the 3.5 utf8mb4 conversion (#9249), addressed by switching com_finder tables to general_ci (#9387), then reverted to unicode_ci in 4.0 (#28425). general_ci does not help for most diacritics (š, ć, ž, í, ş are still equal to their base letters), so the runtime collision is still present in 4.x/5.x/6.x.
Minimal repro (CLI, any 6.1.1 install): build a Result with title = 'Pravila i uslovi korišćenja usluga i servisa', alias = 'pravila-i-uslovi-koriscenja-usluga-i-servisa', language = 'sr-YU', then call (new Indexer())->index($item). It throws Duplicate entry 'koriscenja-sr' deterministically.
Suggested fixes (any of these resolves it): make the final terms INSERT tolerant (INSERT IGNORE / ON CONFLICT DO NOTHING); or group the aggregate strictly by (term, language) before inserting; or use an accent-sensitive collation (e.g. utf8mb4_0900_as_ci) for the term columns; or fold diacritics per language at tokenisation time so index and query meet on the same ASCII form.
Workaround we use in production (option 4, no core change): a small system plugin registers per-language Language subclasses via an spl autoloader appended AFTER the core autoloader (a future core class for the same language wins automatically). Each subclass folds Latin diacritics to ASCII before parent::tokenise(), which makes indexing collision-free and search accent-tolerant on both the index and the query side. Happy to submit this as a PR if there is interest; otherwise approach 1 is a one-line fix.
abstract class AbstractFoldingLanguage extends Language
{
protected const EXTRA_MAP = [];
protected const FOLD_MAP = ['š' => 's', 'ć' => 'c', 'ž' => 'z', 'đ' => 'd', 'č' => 'c', /* ... full Latin map ... */];
public function __construct($locale = null)
{
parent::__construct($this->language);
}
public function tokenise($input)
{
return parent::tokenise($this->fold((string) $input));
}
protected function fold(string $input): string
{
$input = strtr($input, static::EXTRA_MAP + static::FOLD_MAP);
return (string) preg_replace('/[\x{0300}-\x{036F}]/u', '', $input);
}
}
spl_autoload_register(static function (string $class): void {
$prefix = 'Joomla\\Component\\Finder\\Administrator\\Indexer\\Language\\';
if (strncmp($class, $prefix, \strlen($prefix)) !== 0) {
return;
}
$code = strtolower(substr($class, \strlen($prefix)));
if (!preg_match('/^[a-z]{2,3}$/', $code)) {
return;
}
$file = __DIR__ . '/map/' . $code . '.php';
if (is_file($file)) {
require_once $file;
}
});
| Labels |
Added:
No Code Attached Yet
bug
|
||
Hi, I'd like to work on this issue.
I'll first reproduce the problem and investigate the existing Smart Search indexing flow, then add a regression test for the diacritic/transliterated alias collision before implementing the fix.
Since there are multiple possible approaches mentioned here, is there a preferred direction from the maintainers for resolving the collision in
#__finder_terms?If no one is already working on this, I'd be happy to prepare a PR.
Hi, and thanks for picking this up — please go ahead, nobody is working on a core PR.
To set expectations: I'm the reporter, not a maintainer, so I can't tell you which direction the project will accept. There's been no maintainer response on this issue yet. What I can give you is the evidence I gathered while debugging it on a live 30-language install, which should save you some time and keep the regression test from silently passing.
The most important thing for your regression test: the collation decides whether the bug reproduces at all.
The collision only exists because the term column comparison is accent-insensitive. Measured on MySQL 8.4.7:
SELECT 'koriscenja' = 'korišćenja' COLLATE utf8mb4_unicode_ci; -- 1
SELECT 'koriscenja' = 'korišćenja' COLLATE utf8mb4_general_ci; -- 1
SELECT 'koriscenja' = 'korišćenja' COLLATE utf8mb4_0900_as_ci; -- 0
So a test that runs against a database created with an accent-sensitive collation will pass while the bug is still there. Worth asserting the collation of #__finder_terms.term inside the test rather than assuming it. This also rules out the historic remedy: #9249 was "fixed" for German ß by switching com_finder to general_ci in #9387, but as the second row shows, general_ci is equally accent-insensitive for š/ć/ž/í/ş — so that change never addressed the general case, and 4.0 reverted it anyway in #28425.
Second: this is almost certainly MySQL-specific in its symptom. PostgreSQL's default collations are accent-sensitive, so the two spellings would be distinct terms there and the insert would not collide. Worth confirming on your side, but it means a MySQL-only test plus a MySQL-only fix would leave the two backends behaving differently, which is probably the thing reviewers will push back on.
On the four options, with what I ran into:
Option 1, INSERT IGNORE / ON CONFLICT DO NOTHING. One line, and I think it's functionally sound rather than just papering over: the mapping step that follows joins on t.term = ta.term AND t.language = ta.language (Indexer.php:545 in 6.1.2), and that join uses the same accent-insensitive collation, so both spellings map onto the single surviving term row and both remain findable. I have not actually tested this, so treat it as reasoning from the code, not a result. The objection to expect: INSERT IGNORE downgrades unrelated errors (truncation, bad values) to warnings, so ON DUPLICATE KEY UPDATE term = term is the safer MySQL form.
Option 2, group the aggregate strictly by (term, language). Probably the most honest fix, but note the current statement selects ta.term, ta.stem, ta.common, ta.phrase, ta.term_weight, SOUNDEX(ta.term), ta.language and groups by all of them (Indexer.php:523-534). Narrowing the GROUP BY means the remaining columns need aggregates, otherwise it breaks under ONLY_FULL_GROUP_BY, which is in MySQL 8's default sql_mode. And you have to decide which stem and weight win when two spellings collapse — that's a real behavioural choice, not a mechanical edit.
Option 3, accent-sensitive column collation. Fixes the insert but changes search semantics (searching "koriscenja" would stop matching "korišćenja"), and needs a schema migration on every existing site. I'd expect this to be the hardest to get merged.
Option 4, fold diacritics at tokenisation. This is what I run in production, deliberately as a site plugin rather than a core patch, because it needs a per-language map and I didn't want to propose 35 new core classes. It makes indexing collision-free and search accent-tolerant in both directions. One non-obvious detail if you go this way: normalise to NFC before mapping, otherwise decomposed input (s + U+0301) folds differently from precomposed input — that bit me.
Context on why it looks random to users: the two spellings differ in stem and SOUNDEX, so they survive as two distinct rows inside the same INSERT and only then hit the unique key. Whether a given word crashes depends on those values, which is why editors report it sporadically.
Line references above are from 6.1.2: Result.php:51 (PATH_CONTEXT => ['path', 'alias']), Indexer.php:432 (dashes to spaces), Indexer.php:523-534 (the failing INSERT), Indexer.php:545 (the mapping join).
Happy to test your PR against a 30-language install with Serbian, German, Turkish and Czech content if that's useful — that's the setup where I hit this originally.
Hi, I'd like to work on this issue.
I'll first reproduce the problem and investigate the existing Smart Search indexing flow, then add a regression test for the diacritic/transliterated alias collision before implementing the fix.
Since there are multiple possible approaches mentioned here, is there a preferred direction from the maintainers for resolving the collision in
#__finder_terms?If no one is already working on this, I'd be happy to prepare a PR.