Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions manual/chinese/Changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
* 🆕 添加了对 Grafana 13.1 和 13.2 版本的测试。
* 🆕 添加了对 Logstash 9.5 版本的测试。

### Bug 修复
* 🪲 [Issue #4827](https://github.com/manticoresoftware/manticoresearch/issues/4827) 新的 `german` 字符集别名在使用 `charset_table=non_cont,german` 时会保留德语元音变音字母和尖音 s。德语 AOT 词形处理现在通过 Unicode 完整大小写折叠,将 `ß`、大写 `ẞ` 和 `ss` 视为搜索等价形式,同时保留精确词查询中的区别。使用 `lemmatize_de` 或 `lemmatize_de_all` 的现有表必须重建;词形指纹现在会报告过期索引。

## 版本 29.0.2
**发布日期**:2026 年 8 月 14 日

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ U+410..U+42F->U+430..U+44F, U+430..U+44F, U+401->U+451, U+451
charset_table = 0..9, english, _

# you can override character mappings by redefining them, e.g. for case insensitive search with German umlauts you can use:
charset_table = non_cont, U+00E4, U+00C4->U+00E4, U+00F6, U+00D6->U+00F6, U+00FC, U+00DC->U+00FC, U+00DF, U+1E9E->U+00DF
charset_table = non_cont, german
```

<!-- example charset_table -->
Expand Down Expand Up @@ -171,6 +171,7 @@ table products {
* `cjk`
* `cont`
* `english`
* `german`
* `japanese`
* `korean`
* `non_cont` (`non_cjk`)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ Manticore 支持多种语言,大多数语言通过 `charset_table = non_cont`
| 法语 | charset_table=non_cont | fr | morphology=libstemmer_fr | |
| 加利西亚语 | charset_table=non_cont | gl | - | |
| 加罗语 | 需手动指定charset_table | - | - | |
| 德语 | charset_table=non_cont | de | morphology=lemmatize_de (单根形式); morphology=lemmatize_de_all (所有根形式); morphology=libstemmer_de | |
| 德语 | charset_table=non_cont,german | de | morphology=lemmatize_de (单根形式); morphology=lemmatize_de_all (所有根形式); morphology=libstemmer_de | 在应用词形处理前保留德语字母 |
| 希腊语 | charset_table=non_cont | el | morphology=libstemmer_el | |
| 希伯来语 | charset_table=non_cont | he | - | |
| 印地语 | charset_table=non_cont | hi | morphology=libstemmer_hi | |
Expand Down Expand Up @@ -88,4 +88,19 @@ Manticore 支持多种语言,大多数语言通过 `charset_table = non_cont`
| 越南语 | charset_table=non_cont | - | - | 使用拉丁字母。越南语变音符号(ă, â, ê, ô, ơ, ư, đ 和声调符号)默认会自动映射到其基本拉丁字符,因此无需额外配置,“tiếng”会匹配“tieng” |
| 约鲁巴语 | charset_table=non_cont | yo | - | |
| 祖鲁语 | charset_table=non_cont | zu | - | |

### 德语尖音 s

使用德语词形处理时,请在 `non_cont` 后添加内置的 `german` 字符集别名,使分词器在词形处理之前保留德语元音变音字母以及尖音 s 的两种形式:

```ini
charset_table = non_cont, german
```

使用 `lemmatize_de` 和 `lemmatize_de_all` 时,Manticore Search 会在 AOT 词形还原之前对尖音 s 应用 Unicode 完整大小写折叠(`ß` 和 `ẞ` → `ss`)。因此,`Straße`/`Strasse`、`Maße`/`Masse` 和 `Fußball`/`Fussball` 等拼写在搜索中等价。配合上述字符集使用时,`libstemmer_de` 也会折叠这些拼写。

对于普通全文匹配,此折叠会有意消除 `Maße` 与 `Masse` 等词之间的区别。若要保留该区别,请启用 `index_exact_words=1`,并使用 `MATCH('=straße')` 等精确词查询。

升级使用 `lemmatize_de` 或 `lemmatize_de_all` 的现有表后,请重建或重新索引该表。德语 AOT 规范化版本包含在词形指纹中,因此当旧磁盘索引需要重建时,Manticore Search 会发出警告。

<!-- proofread -->
3 changes: 3 additions & 0 deletions manual/english/Changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
* 🆕 Added testing for Grafana versions 13.1 and 13.2.
* 🆕 Added testing for Logstash version 9.5.

### Bug Fixes
* 🪲 [Issue #4827](https://github.com/manticoresoftware/manticoresearch/issues/4827) The new `german` charset alias preserves German umlauts and sharp s when used as `charset_table=non_cont,german`. German AOT morphology now treats `ß`, capital `ẞ`, and `ss` as search equivalents using Unicode full case folding while retaining exact-word distinctions. Existing tables that use `lemmatize_de` or `lemmatize_de_all` must be rebuilt; the morphology fingerprint now reports stale indexes.

## Version 29.0.2
**Released**: August 14th 2026

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ U+410..U+42F->U+430..U+44F, U+430..U+44F, U+401->U+451, U+451
charset_table = 0..9, english, _

# you can override character mappings by redefining them, e.g. for case insensitive search with German umlauts you can use:
charset_table = non_cont, U+00E4, U+00C4->U+00E4, U+00F6, U+00D6->U+00F6, U+00FC, U+00DC->U+00FC, U+00DF, U+1E9E->U+00DF
charset_table = non_cont, german
```

<!-- example charset_table -->
Expand Down Expand Up @@ -171,6 +171,7 @@ Besides definitions of characters and mappings, there are several built-in alias
* `cjk`
* `cont`
* `english`
* `german`
* `japanese`
* `korean`
* `non_cont` (`non_cjk`)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ The table below lists all supported languages and indicates how to enable:
| French | charset_table=non_cont | fr | morphology=libstemmer_fr | |
| Galician | charset_table=non_cont | gl | - | |
| Garo | specify charset_table manually | - | - | |
| German | charset_table=non_cont | de | morphology=lemmatize_de (single root form); morphology=lemmatize_de_all (all root forms); morphology=libstemmer_de | |
| German | charset_table=non_cont,german | de | morphology=lemmatize_de (single root form); morphology=lemmatize_de_all (all root forms); morphology=libstemmer_de | Preserve German letters before applying morphology |
| Greek | charset_table=non_cont | el | morphology=libstemmer_el | |
| Hebrew | charset_table=non_cont | he | - | |
| Hindi | charset_table=non_cont | hi | morphology=libstemmer_hi | |
Expand Down Expand Up @@ -88,4 +88,19 @@ The table below lists all supported languages and indicates how to enable:
| Vietnamese | charset_table=non_cont | - | - | Uses Latin script. Vietnamese diacritics (ă, â, ê, ô, ơ, ư, đ, and tone marks) are automatically mapped to their base Latin characters by default, so "tiếng" matches "tieng" without additional configuration. |
| Yoruba | charset_table=non_cont | yo | - | |
| Zulu | charset_table=non_cont | zu | - | |

### German sharp s

When using German morphology, add the built-in `german` charset alias after `non_cont` so the tokenizer preserves German umlauts and both forms of sharp s before morphology runs:

```ini
charset_table = non_cont, german
```

With `lemmatize_de` and `lemmatize_de_all`, Manticore Search applies Unicode full case folding for sharp s (`ß` and `ẞ` → `ss`) before AOT lemmatization. This makes spellings such as `Straße`/`Strasse`, `Maße`/`Masse`, and `Fußball`/`Fussball` search equivalents. `libstemmer_de` also folds these spellings when used with the charset above.

The fold intentionally removes distinctions such as `Maße` versus `Masse` for normal full-text matching. To retain that distinction, enable `index_exact_words=1` and use exact-word queries such as `MATCH('=straße')`.

After upgrading an existing table that uses `lemmatize_de` or `lemmatize_de_all`, rebuild or reindex it. The German AOT normalization version is included in the morphology fingerprint, so Manticore Search warns when an older disk index needs rebuilding.

<!-- proofread -->
3 changes: 3 additions & 0 deletions manual/russian/Changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
* 🆕 Добавлено тестирование Grafana версий 13.1 и 13.2.
* 🆕 Добавлено тестирование Logstash версии 9.5.

### Исправления ошибок
* 🪲 [Issue #4827](https://github.com/manticoresoftware/manticoresearch/issues/4827) Новый алиас `german` сохраняет немецкие умлауты и ß при `charset_table=non_cont,german`. Немецкая AOT-морфология теперь считает `ß`, заглавную `ẞ` и `ss` эквивалентными при поиске благодаря полному Unicode-приведению регистра, сохраняя при этом различия в запросах точного слова. Существующие таблицы с `lemmatize_de` или `lemmatize_de_all` необходимо пересобрать; fingerprint морфологии теперь сообщает об устаревших индексах.

## Версия 29.0.2
**Выпуск**: 14 августа 2026

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ U+410..U+42F->U+430..U+44F, U+430..U+44F, U+401->U+451, U+451
charset_table = 0..9, english, _

# you can override character mappings by redefining them, e.g. for case insensitive search with German umlauts you can use:
charset_table = non_cont, U+00E4, U+00C4->U+00E4, U+00F6, U+00D6->U+00F6, U+00FC, U+00DC->U+00FC, U+00DF, U+1E9E->U+00DF
charset_table = non_cont, german
```

<!-- example charset_table -->
Expand Down Expand Up @@ -171,6 +171,7 @@ table products {
* `cjk`
* `cont`
* `english`
* `german`
* `japanese`
* `korean`
* `non_cont` (`non_cjk`)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ Manticore поддерживает широкий спектр языков, п
| Французский | charset_table=non_cont | fr | morphology=libstemmer_fr | |
| Галисийский | charset_table=non_cont | gl | - | |
| Гаро | задать charset_table вручную | - | - | |
| Немецкий | charset_table=non_cont | de | morphology=lemmatize_de (единственная корневая форма); morphology=lemmatize_de_all (все корневые формы); morphology=libstemmer_de | |
| Немецкий | charset_table=non_cont,german | de | morphology=lemmatize_de (единственная корневая форма); morphology=lemmatize_de_all (все корневые формы); morphology=libstemmer_de | Сохраняйте немецкие буквы до применения морфологии |
| Греческий | charset_table=non_cont | el | morphology=libstemmer_el | |
| Иврит | charset_table=non_cont | he | - | |
| Хинди | charset_table=non_cont | hi | morphology=libstemmer_hi | |
Expand Down Expand Up @@ -88,4 +88,19 @@ Manticore поддерживает широкий спектр языков, п
| Вьетнамский | charset_table=non_cont | - | - | Использует латинский алфавит. Вьетнамские диакритические знаки (ă, â, ê, ô, ơ, ư, đ и знаки тонов) автоматически преобразуются в их базовые латинские символы по умолчанию, поэтому "tiếng" соответствует "tieng" без дополнительной конфигурации. |
| Йоруба | charset_table=non_cont | yo | - | |
| Зулу | charset_table=non_cont | zu | - | |

### Немецкая буква ß

При использовании немецкой морфологии добавьте встроенный алиас `german` после `non_cont`, чтобы токенизатор сохранял немецкие умлауты и обе формы буквы ß до применения морфологии:

```ini
charset_table = non_cont, german
```

С `lemmatize_de` и `lemmatize_de_all` Manticore Search выполняет полное Unicode-приведение регистра для ß (`ß` и `ẞ` → `ss`) перед AOT-лемматизацией. Поэтому варианты `Straße`/`Strasse`, `Maße`/`Masse` и `Fußball`/`Fussball` становятся эквивалентными при поиске. `libstemmer_de` также сводит эти варианты при использовании набора символов выше.

Такое преобразование намеренно устраняет различие, например, между `Maße` и `Masse` при обычном полнотекстовом поиске. Чтобы сохранить различие, включите `index_exact_words=1` и используйте запросы точного слова, например `MATCH('=straße')`.

После обновления существующей таблицы с `lemmatize_de` или `lemmatize_de_all` пересоберите или переиндексируйте её. Версия нормализации немецкой AOT-морфологии включена в fingerprint морфологии, поэтому Manticore Search предупреждает, если старый дисковый индекс нужно пересобрать.

<!-- proofread -->
8 changes: 8 additions & 0 deletions src/charsets/german.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
U+00E4,
U+00C4->U+00E4,
U+00F6,
U+00D6->U+00F6,
U+00FC,
U+00DC->U+00FC,
U+00DF,
U+1E9E->U+00DF
21 changes: 16 additions & 5 deletions src/dict/template_dict_traits.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,17 @@ int TemplateDictTraits_c::InitMorph ( const char* szMorph, int iLength, CSphStri
return AddMorph ( (int)EMORPH::STEM_RU_UTF8 );
}

auto AddAotFingerprint = [this] ( int iLang )
{
auto [sDict, uCRC] = sphAotDictinfo ( iLang );
if ( m_sMorphFingerprint.IsEmpty() )
m_sMorphFingerprint.SetSprintf ( "%s:%08x", sDict.cstr(), uCRC );
else
m_sMorphFingerprint.SetSprintf ( "%s;%s:%08x", m_sMorphFingerprint.cstr(), sDict.cstr(), uCRC );
if ( iLang == AOT_DE )
m_sMorphFingerprint.SetSprintf ( "%s;de-sharp-s:1", m_sMorphFingerprint.cstr() );
};

for ( int j = 0; j < AOT_LENGTH; ++j )
{
char buf[20];
Expand Down Expand Up @@ -234,10 +245,7 @@ int TemplateDictTraits_c::InitMorph ( const char* szMorph, int iLength, CSphStri

if ( !m_dMorph.Contains ( iMorph ) )
{
if ( m_sMorphFingerprint.IsEmpty() )
m_sMorphFingerprint.SetSprintf ( "%s:%08x", sphAotDictinfo ( j ).first.cstr(), sphAotDictinfo ( j ).second );
else
m_sMorphFingerprint.SetSprintf ( "%s;%s:%08x", m_sMorphFingerprint.cstr(), sphAotDictinfo ( j ).first.cstr(), sphAotDictinfo ( j ).second );
AddAotFingerprint ( j );
m_dMorph.Add ( iMorph );
}
return ST_OK;
Expand All @@ -264,7 +272,10 @@ int TemplateDictTraits_c::InitMorph ( const char* szMorph, int iLength, CSphStri
if ( j == AOT_UK && !m_tLemmatizer )
m_tLemmatizer = CreateLemmatizer ( j );

return AddMorph ( (int)EMORPH::AOTLEMMER_BASE_ALL + j );
int iMorph = (int)EMORPH::AOTLEMMER_BASE_ALL + j;
if ( j == AOT_DE && !m_dMorph.Contains ( iMorph ) )
AddAotFingerprint ( j );
return AddMorph ( iMorph );
}
}

Expand Down
10 changes: 4 additions & 6 deletions src/sphinx.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -9739,9 +9739,8 @@ CSphIndex_VLN::LOAD_E CSphIndex_VLN::LoadHeaderLegacy ( const CSphString& sHeade
return LOAD_E::GeneralError_e;

if ( tDictSettings.m_sMorphFingerprint!=pDict->GetMorphDataFingerprint() )
sWarning.SetSprintf ( "different lemmatizer dictionaries (table='%s', current='%s')",
tDictSettings.m_sMorphFingerprint.cstr(),
pDict->GetMorphDataFingerprint().cstr() );
sWarning.SetSprintf ( "morphology data fingerprint changed (stored='%s', current='%s'); rebuild or reindex the table",
tDictSettings.m_sMorphFingerprint.cstr(), pDict->GetMorphDataFingerprint().cstr() );

SetDictionary ( pDict );

Expand Down Expand Up @@ -9879,9 +9878,8 @@ CSphIndex_VLN::LOAD_E CSphIndex_VLN::LoadHeaderJson ( const CSphString& sHeaderN
return LOAD_E::GeneralError_e;

if ( tDictSettings.m_sMorphFingerprint!=pDict->GetMorphDataFingerprint() )
sWarning.SetSprintf ( "different lemmatizer dictionaries (table='%s', current='%s')",
tDictSettings.m_sMorphFingerprint.cstr(),
pDict->GetMorphDataFingerprint().cstr() );
sWarning.SetSprintf ( "morphology data fingerprint changed (stored='%s', current='%s'); rebuild or reindex the table",
tDictSettings.m_sMorphFingerprint.cstr(), pDict->GetMorphDataFingerprint().cstr() );

SetDictionary ( pDict );

Expand Down
31 changes: 27 additions & 4 deletions src/sphinxaot.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -863,7 +863,8 @@ inline BYTE * Emit ( BYTE * sOut, BYTE uChar )
{
if ( uChar=='-' )
return sOut;
*sOut++ = uChar | 0x20;
// German sharp s has no one-byte uppercase equivalent; applying the ASCII bit trick turns 0xDF into 0xFF.
*sOut++ = uChar == 0xDF ? uChar : ( uChar | 0x20 );
return sOut;
}

Expand Down Expand Up @@ -1266,12 +1267,30 @@ static inline int Utf8ToWin1251 ( BYTE * pOut, const BYTE * pWord )
}

/// returns length in bytes (aka chars) if all letters were converted
/// returns 0 and aborts early if non-western letters are encountered
static inline int Utf8ToWin1252 ( BYTE * pOut, const BYTE * pWord )
/// returns 0 and aborts early if non-western letters are encountered or the output buffer is too small
template<size_t SIZE>
static inline int Utf8ToWin1252 ( BYTE ( &dOut )[SIZE], const BYTE * pWord )
{
BYTE * pStart = pOut;
BYTE * pOut = dOut;
BYTE * pStart = dOut;
BYTE * pEnd = dOut + SIZE - 1;
while ( *pWord )
{
// Unicode full case folding maps both forms of German sharp s to "ss".
// Normalize before AOT so Straße and Strasse follow the same lemmatization path.
if ( ( pWord[0]==0xC3 && pWord[1]==0x9F ) ||
( pWord[0]==0xE1 && pWord[1]==0xBA && pWord[2]==0x9E ) )
{
if ( pEnd-pOut<2 )
return 0;
*pOut++ = 's';
*pOut++ = 's';
pWord += pWord[0]==0xC3 ? 2 : 3;
continue;
}

if ( pOut==pEnd )
return 0;
if ( (*pWord)&0x80 )
{
if ( ((*pWord)&0xFC)==0xC0 )
Expand Down Expand Up @@ -1303,6 +1322,10 @@ static inline bool IsGermanAlphaUtf8 ( const BYTE * pWord )
if ( pWord[0]==0xC2 && pWord[1]==0xB5 )
return true;

// capital sharp s U+1E9E
if ( pWord[0]==0xE1 && pWord[1]==0xBA && pWord[2]==0x9E )
return true;

// some upper
if ( pWord[0]==0xC3 )
{
Expand Down
14 changes: 14 additions & 0 deletions src/sphinxrt.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5611,6 +5611,18 @@ std::unique_ptr<CSphIndex> RtIndex_c::PreallocDiskChunk ( const CSphString& sChu
return pDiskChunk;
}

static void CheckMorphFingerprint ( const CSphDictSettings & tStored, const DictRefPtr_c & pCurrent, StrVec_t & dWarnings )
{
if ( tStored.m_sMorphFingerprint==pCurrent->GetMorphDataFingerprint() )
return;

CSphString sWarning;
sWarning.SetSprintf ( "morphology data fingerprint changed (stored='%s', current='%s'); rebuild or reindex the table",
tStored.m_sMorphFingerprint.cstr(), pCurrent->GetMorphDataFingerprint().cstr() );
dWarnings.Add ( sWarning );
}


RtIndex_c::LOAD_E RtIndex_c::LoadMetaLegacy ( FilenameBuilder_i * pFilenameBuilder, bool bStripPath, DWORD & uVersion, bool & bRebuildInfixes, StrVec_t & dWarnings )
{
CSphString sMeta = GetFilename ( "meta" );
Expand Down Expand Up @@ -5706,6 +5718,7 @@ RtIndex_c::LOAD_E RtIndex_c::LoadMetaLegacy ( FilenameBuilder_i * pFilenameBuild
if ( !m_sLastError.IsEmpty() )
dWarnings.Add(m_sLastError);

CheckMorphFingerprint ( tDictSettings, m_pDict, dWarnings );
Tokenizer::AddToMultiformFilterTo ( m_pTokenizer, m_pDict->GetMultiWordforms () );

m_iWordsCheckpoint = rdMeta.GetDword();
Expand Down Expand Up @@ -5847,6 +5860,7 @@ RtIndex_c::LOAD_E RtIndex_c::LoadMetaJson ( FilenameBuilder_i * pFilenameBuilder
if ( !m_sLastError.IsEmpty() )
dWarnings.Add(m_sLastError);

CheckMorphFingerprint ( tDictSettings, m_pDict, dWarnings );
Tokenizer::AddToMultiformFilterTo ( m_pTokenizer, m_pDict->GetMultiWordforms () );

m_iWordsCheckpoint = (int)Int ( tBson.ChildByName ( "words_checkpoint" ) );
Expand Down
2 changes: 1 addition & 1 deletion test/test_098/model.bin

Large diffs are not rendered by default.

14 changes: 14 additions & 0 deletions test/test_098/test.xml
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,18 @@ index dup2
charset_table = non_cjk, U+00E4, U+00C4->U+00E4, U+00F6, U+00D6->U+00F6, U+00DC->U+00FC, U+00DF
}

index german_explicit
{
type = template
charset_table = non_cont, U+00E4, U+00C4->U+00E4, U+00F6, U+00D6->U+00F6, U+00FC, U+00DC->U+00FC, U+00DF, U+1E9E->U+00DF
}

index german_alias
{
type = template
charset_table = non_cont, german
}

index rt_crc
{
type = rt
Expand Down Expand Up @@ -184,6 +196,8 @@ index rt_wild
<!-- regression duplicated charset mapping sorted wrong on charset_table parsing -->
CALL KEYWORDS('Öö', 'dup1', 1 as stats, 'hits' as sort_mode);
CALL KEYWORDS('Öö', 'dup2', 1 as stats, 'hits' as sort_mode);
CALL KEYWORDS('Ää Öö Üü ßẞ', 'german_explicit');
CALL KEYWORDS('Ää Öö Üü ßẞ', 'german_alias');

<!-- regression wrong wildcard matching path at the new default of the local_df -->
INSERT INTO rt_crc (id, title) VALUES ( 1, 'the google box' );
Expand Down
2 changes: 1 addition & 1 deletion test/test_219/model.bin

Large diffs are not rendered by default.

Loading