Browse Source

feat expand_blended search option to expand blended variants into the OR node (#4130)

* feat expand_blended search option to expand blended variants into the OR node; added send of the query option from master to agent; set master agent version to 27; added cases to test 480; documented option; fix #1065
---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Sergey Nikolaev <[email protected]>
Stas 3 weeks ago
parent
commit
b30eafdf06

+ 1 - 0
.gitignore

@@ -11,6 +11,7 @@
 /*.sdf
 /*.opensdf
 /.hg
+**/*.bak
 
 # cmake local presets
 /CMakeUserPresets.json

File diff suppressed because it is too large
+ 0 - 0
.translation-cache/Searching/Options.md.json


+ 86 - 76
manual/chinese/Searching/Options.md

@@ -1,20 +1,20 @@
 # 搜索选项
 
-SQL [SELECT](../Searching/Full_text_matching/Basic_usage.md#SQL) 子句和HTTP [/search](../Searching/Full_text_matching/Basic_usage.md#HTTP-JSON) 端点支持一些选项,可以用来精细调整搜索行为。
+SQL [SELECT](../Searching/Full_text_matching/Basic_usage.md#SQL) 子句和 HTTP [/search](../Searching/Full_text_matching/Basic_usage.md#HTTP-JSON) 端点支持许多选项,可用于微调搜索行为。
 
-## 选项
+## OPTION
 
-### 基本语法
+### 一般语法
 
 <!-- example options -->
 
-**SQL**
+**SQL**:
 
 ```sql
 SELECT ... [OPTION <optionname>=<value> [ , ... ]] [/*+ [NO_][ColumnarScan|DocidIndex|SecondaryIndex(<attribute>[,...])]] /*]
 ```
 
-**HTTP**
+**HTTP**:
 ```json
 POST /search
 {
@@ -29,7 +29,7 @@ POST /search
 
 
 <!-- intro -->
-SQL
+SQL:
 <!-- request SQL -->
 ```sql
 SELECT * FROM test WHERE MATCH('@title hello @body world')
@@ -48,7 +48,7 @@ field_weights=(title=10, body=3), agent_query_timeout=10000
 ```
 
 <!-- intro -->
-JSON
+JSON:
 <!-- request JSON -->
 
 ```json
@@ -103,44 +103,44 @@ POST /search
 
 支持的选项包括:
 
-### 准确聚合
-整数。在多线程运行分组查询时启用或禁用保证聚合准确性的功能。默认值为 0。
+### accurate_aggregation
+整数。启用或禁用在多线程运行 groupby 查询时的保证聚合准确性。默认值为 0。
 
-在运行分组查询时,可以在没有伪分片的情况下将其并行运行到一个普通表上的几个伪分片上(如果启用了 `pseudo_sharding`)。类似的方法也适用于 RT 表。每个分片/块执行查询,但分组的数量由 `max_matches` 限制。如果来自不同分片/块的结果集有不同的分组,则分组计数和聚合可能不准确。请注意,Manticore 尝试根据分组属性的唯一值数量(从次要索引检索)增加 `max_matches` 至 [`max_matches_increase_threshold`](../Searching/Options.md#max_matches_increase_threshold)。如果成功,将不会丢失准确性
+当运行 groupby 查询时,可以在普通表上使用多个伪分片并行运行(如果 `pseudo_sharding` 开启)。类似的方法适用于 RT 表。每个分片/块执行查询,但组的数量受 `max_matches` 限制。如果不同分片/块的结果集包含不同的组,组计数和聚合可能会不准确。注意 Manticore 会根据 groupby 属性的唯一值数量(从二级索引中检索)将 `max_matches` 增加到 [`max_matches_increase_threshold`](../Searching/Options.md#max_matches_increase_threshold)。如果成功,将不会出现准确性损失
 
-然而,如果分组属性的唯一值数量很高,进一步增加 `max_matches` 可能不是一个好策略,因为它可能导致性能下降和更高的内存使用。将 `accurate_aggregation` 设置为 1 强制分组搜索在一个线程中运行,从而解决了准确性问题。请注意,当 `max_matches` 无法设置足够高时,才会强制执行单线程操作;否则,`accurate_aggregation=1` 的搜索仍然会在多个线程中运行。
+然而,如果 groupby 属性的唯一值数量很高,进一步增加 `max_matches` 可能不是一个好策略,因为它可能导致性能下降和内存使用增加。将 `accurate_aggregation` 设置为 1 会强制 groupby 搜索在单线程中运行,从而解决准确性问题。注意,仅当 `max_matches` 无法设置得足够高时,才会强制单线程运行;否则,带有 `accurate_aggregation=1` 的搜索仍会在多线程中运行。
 
-总体而言,将 `accurate_aggregation` 设置为 1 确保 RT 表和 `pseudo_sharding=1` 的普通表中的组计数和聚合准确性。缺点是搜索会变慢,因为它们将被强制在一个线程中运行。
+总体而言,将 `accurate_aggregation` 设置为 1 确保 RT 表和 `pseudo_sharding=1` 的普通表中的组计数和聚合准确性。缺点是搜索会变慢,因为它们将被强制在线程中运行。
 
-然而,如果我们有一个 RT 表和包含相同数据的普通表,并运行带有 `accurate_aggregation=1` 的查询,我们可能收到不同的结果。这是因为守护进程可能会由于 [`max_matches_increase_threshold`](../Searching/Options.md#max_matches_increase_threshold) 设置的不同而为 RT 和普通表选择不同的 `max_matches` 设置
+然而,如果我们有一个 RT 表和一个包含相同数据的普通表,并运行带有 `accurate_aggregation=1` 的查询,我们可能收到不同的结果。这是因为守护进程可能会根据 [`max_matches_increase_threshold`](../Searching/Options.md#max_matches_increase_threshold) 设置为 RT 表和普通表选择不同的 `max_matches` 值
 
 ### agent_query_timeout
-整数。等待远程查询完成的最大毫秒数,参见 [此部分](../Creating_a_table/Creating_a_distributed_table/Remote_tables.md#agent_query_timeout)。
+整数。等待远程查询完成的最大时间(以毫秒为单位),请参见 [此部分](../Creating_a_table/Creating_a_distributed_table/Remote_tables.md#agent_query_timeout)。
 
 ### boolean_simplify
-`0` 或 `1`(默认为 `1`)。`boolean_simplify=1` 启用 [简化查询](../Searching/Full_text_matching/Boolean_optimization.md) 以加快查询速度。
+`0` 或 `1`(默认为 `1`)。`boolean_simplify=1` 启用 [查询简化](../Searching/Full_text_matching/Boolean_optimization.md) 以加快速度。
 
-此选项也可以全局设置在 [searchd 配置](../Server_settings/Searchd.md#boolean_simplify) 中,以更改所有查询的默认行为。查询级别的选项会覆盖全局设置。
+此选项也可以在 [searchd 配置](../Server_settings/Searchd.md#boolean_simplify) 中全局设置,以更改所有查询的默认行为。每个查询的选项将覆盖全局设置。
 
 ### comment
 字符串,用户评论会被复制到查询日志文件中。
 
 ### cutoff
-整数。指定要处理的最大匹配项数。未设置时,Manticore 会自动选择适当的值。
+整数。指定要处理的最大匹配数。如果未设置,Manticore 将自动选择一个适当的值。
 
 <!-- example cutoff_aggregation -->
 
-* `N = 0`:禁用匹配数的限制。
-* `N > 0`:指示 Manticore 在找到 `N` 个匹配文档后停止处理结果。
-* 未设置:Manticore 自动决定阈值。
+* `N = 0`:禁用匹配数的限制。
+* `N > 0`:指示 Manticore 在找到 `N` 个匹配文档后立即停止处理结果。
+* 未设置:Manticore 自动决定阈值。
 
-当 Manticore 无法确定匹配文档的确切数量时,查询 [元信息](../Node_info_and_management/SHOW_META.md#SHOW-META) 中的 `total_relation` 字段将显示 `gte`,即 **大于或等于**。这表示实际的匹配数量至少为报告的 `total_found`(在 SQL 中)或 `hits.total`(在 JSON 中)。当数量确切时,`total_relation` 将显示 `eq`。
+当 Manticore 无法确定匹配文档的确切数量时,查询 [元信息](../Node_info_and_management/SHOW_META.md#SHOW-META) 中的 `total_relation` 字段将显示 `gte`,表示 **大于或等于**。这表明实际匹配数至少为报告的 `total_found`(在 SQL 中)或 `hits.total`(在 JSON 中)。当计数准确时,`total_relation` 将显示 `eq`。
 
 注意:在聚合查询中使用 `cutoff` 不推荐,因为它可能导致不准确或不完整的结果。
 
 <!-- request Example -->
 
-在聚合查询中使用 `cutoff` 可能会导致不准确或误导性的结果,如下例所示:
+在聚合查询中使用 `cutoff` 可能导致不正确或误导性的结果,如下例所示:
 ```
 drop table if exists t
 --------------
@@ -180,7 +180,7 @@ select avg(a) from t option cutoff=1 facet a
 --- 1 out of 1 results in 0ms ---
 ```
 
-与没有 `cutoff` 的相同查询进行比较:
+与不使用 `cutoff` 的相同查询进行比较:
 ```
 --------------
 select avg(a) from t facet a
@@ -208,21 +208,31 @@ select avg(a) from t facet a
 <!-- end -->
 
 ### distinct_precision_threshold
-整数。默认值为 `3500`。此选项设置了 `count distinct` 返回精确计数的阈值,在普通表中
+整数。默认值为 `3500`。此选项设置在普通表中 `count distinct` 返回的计数保证精确的阈值
 
-接受的值范围从 `500` 到 `15500`。超出此范围的值将被钳位
+接受的值范围为 `500` 到 `15500`。超出此范围的值将被限制
 
-当此选项设置为 `0` 时,启用确保精确计数的算法。该算法收集 `{group, value}` 对,对其进行排序,并定期消除重复项。结果是在普通表中具有精确计数。但是,这种方法不适合高基数的数据集,因为它消耗大量内存并且查询执行缓慢
+当此选项设置为 0 时,启用一种确保精确计数的算法。该算法收集 `{group, value}` 对,对其进行排序,并定期消除重复项。结果是在普通表中精确的计数。然而,由于其高内存消耗和慢查询执行,这种方法不适合高基数数据集
 
-当 `distinct_precision_threshold` 设置为大于 `0` 的值时,Manticore 使用不同的算法。它将计数加载到哈希表中并返回表的大小。如果哈希表变得太大,其内容将移动到 `HyperLogLog` 数据结构。此时,计数变得近似,因为 `HyperLogLog` 是一种概率算法。这种方法保持每个组的最大固定内存使用量,但计数准确性有所权衡。
+当 `distinct_precision_threshold` 设置为大于 `0` 的值时,Manticore 用不同的算法。它将计数加载到哈希表中并返回表的大小。如果哈希表变得太大,其内容将被移动到 `HyperLogLog` 数据结构中。此时,计数变得近似,因为 HyperLogLog 是一种概率算法。这种方法保持每组的最大内存使用量固定,但计数准确性存在权衡。
 
-`HyperLogLog` 的准确性以及从哈希表转换为 `HyperLogLog` 的阈值是从 `distinct_precision_threshold` 设置推导出来的。重要的是谨慎使用此选项,因为将其值加倍也会加倍计算计数所需的最大内存。最大内存使用量可以通过以下公式粗略估计:`64 * max_matches * distinct_precision_threshold`,尽管实际上,计数计算通常使用的内存比最坏情况下的更少
+`HyperLogLog` 的精度和从哈希表转换到 HyperLogLog 的阈值源自 `distinct_precision_threshold` 设置。使用此选项时需谨慎,因为将其值加倍也会使计算计数所需的最大内存加倍。最大内存使用量可以大致通过此公式估算:`64 * max_matches * distinct_precision_threshold`,尽管在实践中,计数计算通常使用的内存少于最坏情况下的内存
 
 ### expand_keywords
-`0` 或 `1`(默认值为 `0`)。在可能的情况下扩展关键词为精确形式和/或通配符。详情请参见 [expand_keywords](../Creating_a_table/NLP_and_tokenization/Wildcard_searching_settings.md#expand_keywords)。
+`0` 或 `1`(默认为 `0`)。尽可能扩展精确形式和/或带星号的关键词。有关更多详细信息,请参阅 [expand_keywords](../Creating_a_table/NLP_and_tokenization/Wildcard_searching_settings.md#expand_keywords)。
+
+### expand_blended
+`0`、`off`、`1` 或任何 `blend_mode` 选项的组合(默认为 `0`)。在查询解析期间,将混合关键词(包含通过 [blend_chars](../Creating_a_table/NLP_and_tokenization/Low-level_tokenization.md#blend_chars) 配置的字符的标记)扩展为其构成变体。启用后,像 "well-being"(如果 `-` 在 `blend_chars` 中配置)这样的关键词将被扩展为 "well-being"、"wellbeing"、"well" 和 "being" 等变体,然后在查询树中被分组为 OR 子树。
+
+支持的值为:
+* `0` 或 `off` - 禁用混合扩展(默认)。混合关键词将按正常方式处理,不进行扩展。
+* `1` - 启用混合扩展,并使用表的 [blend_mode](../Creating_a_table/NLP_and_tokenization/Low-level_tokenization.md#blend_mode) 设置来确定生成哪些变体。
+* 任何混合模式选项 - 启用混合扩展并使用指定的混合模式,覆盖表的 `blend_mode` 设置。
+
+有关选项的更多详细信息,请参阅 [blend_mode](../Creating_a_table/NLP_and_tokenization/Low-level_tokenization.md#blend_mode)。
 
 ### field_weights
-命名的整数列表(按字段的用户权重,用于排名)。
+命名整数列表(按字段的用户权重用于排序)。
 
 示例:
 ```sql
@@ -230,75 +240,75 @@ SELECT ... OPTION field_weights=(title=10, body=3)
 ```
 
 ### global_idf
-使用 [global_idf](../Creating_a_table/NLP_and_tokenization/Low-level_tokenization.md#global_idf) 文件中的全局统计数据(频率)进行 IDF 计算。
+使用 [global_idf](../Creating_a_table/NLP_and_tokenization/Low-level_tokenization.md#global_idf) 文件中的全局统计信息(频率)进行 IDF 计算。
 
 ### idf
-带引号的、逗号分隔的 IDF 计算标志列表。已知标志包括:
+带引号的、逗号分隔的 IDF 计算标志列表。已知标志包括:
 
-* `normalized`:BM25 变体,idf = log((N-n+1)/n),根据 Robertson 等人
-* `plain`:纯变体,idf = log(N/n),根据 Sparck-Jones
-* `tfidf_normalized`:额外按查询词数除以 IDF,使 `TF*IDF` 适合于 [0, 1] 范围内
-* `tfidf_unnormalized`:不额外按查询词数除以 IDF,其中 N 是集合大小,n 是匹配文档数
+* `normalized`:BM25 变体,idf = log((N-n+1)/n),如 Robertson 等人所述
+* `plain`:普通变体,idf = log(N/n),如 Sparck-Jones 所述
+* `tfidf_normalized`:此外将 IDF 除以查询词数,使 `TF*IDF` 保持在 [0, 1] 范围内
+* `tfidf_unnormalized`:不将 IDF 除以查询词数,其中 N 是集合大小,n 是匹配文档数
 
-Manticore 历史默认的逆文档频率(IDF)等价于 `OPTION idf='normalized,tfidf_normalized'`,这些归一化可能导致若干不期望的效果
+Manticore 历史默认的 IDF(逆文档频率)等同于 `OPTION idf='normalized,tfidf_normalized'`,这些归一化可能会导致一些不良影响
 
-首先,`idf=normalized` 会导致关键词惩罚。例如,如果搜索 `the | something` 且 `the` 出现在超过 50% 的文档中,则同时含有 `the` 和 `something` 两个关键词的文档权重会低于只含有一个关键词 `something` 的文档。使用 `OPTION idf=plain` 可避免此问题。纯 IDF 变动范围为 `[0, log(N)]`,关键词不会被惩罚;而归一化的 IDF 变动范围为 `[-log(N), log(N)]`,过于频繁的关键词会被惩罚。
+首先,`idf=normalized` 会导致关键词惩罚。例如,如果你搜索 `the | something`,并且 `the` 出现在超过 50% 的文档中,那么同时包含 `the` 和 `something` 的文档的权重将低于仅包含 `something` 的文档。使用 `OPTION idf=plain` 可避免此问题。普通 IDF 的范围在 [0, log(N)] 内,关键词永远不会被惩罚;而归一化 IDF 的范围在 [-log(N), log(N)] 内,过于频繁的关键词会被惩罚。
 
-其次,`idf=tfidf_normalized` 会导致 IDF 在查询间漂移。历史上,IDF 还会被查询关键词数量除以,确保所有关键词的 `sum(tf*idf)` 保持在 [0,1] 范围内。然而,这意味着像 `word1` 和 `word1 | nonmatchingword2` 这样的查询会给同一结果集分配不同权重,因为 `word1` 和 `nonmatchingword2` 的 IDF 都被除以 2。使用 `OPTION idf='tfidf_unnormalized'` 可解决此问题。请记住,当您禁用此归一化时,BM25、BM25A、BM25F() 排名因子将相应调整。
+其次,`idf=tfidf_normalized` 会导致 IDF 在查询之间漂移。历史上,IDF 也会除以查询关键词数,确保所有关键词的 `sum(tf*idf)` 总和保持在 [0,1] 范围内。然而,这意味着像 `word1` 和 `word1 | nonmatchingword2` 这样的查询会为完全相同的结果集分配不同的权重,因为 `word1` 和 `nonmatchingword2` 的 IDF 都被除以 2。使用 `OPTION idf='tfidf_unnormalized'` 可解决此问题。请注意,当禁用此归一化时,BM25、BM25A、BM25F() 排名因素将相应调整。
 
-IDF 标志可以组合;`plain` 和 `normalized` 互斥;`tfidf_unnormalized` 和 `tfidf_normalized` 也互斥;在此类互斥组中未指定的标志默认为其原始设置。这意味着 `OPTION idf=plain` 等同于完整指定 `OPTION idf='plain,tfidf_normalized'`。
+IDF 标志可以组合;`plain` 和 `normalized` 互斥;`tfidf_unnormalized` 和 `tfidf_normalized` 也互斥;未指定的标志在这些互斥组中默认使用其原始设置。这意味着 `OPTION idf=plain` 等同于完整指定 `OPTION idf='plain,tfidf_normalized'`。
 
 ### jieba_mode
 指定查询的 Jieba 分词模式。
 
-使用 Jieba 中文分词时,文档和查询的分词模式可能不同,从而带来帮助。完整模式列表请参见 [jieba_mode](../Creating_a_table/NLP_and_tokenization/Morphology.md#jieba_mode)。
+在使用 Jieba 中文分词时,有时需要为文档和查询使用不同的分词模式。有关完整模式列表,请参阅 [jieba_mode](../Creating_a_table/NLP_and_tokenization/Morphology.md#jieba_mode)。
 
 ### index_weights
-命名的整数列表。按表的用户权重,用于排名
+命名整数列表。按表的用户权重用于排序
 
 ### local_df
-`0` 或 `1`,自动对分布式表的所有本地部分汇总文档频率,确保局部分片表中的 IDF 一致且准确。默认在 RT 表的磁盘区块启用。带通配符的查询词被忽略。
+`0` 或 `1`,自动汇总分布式表的所有本地部分的 DF,确保本地分片表的 IDF 一致(且准确)。默认启用用于 RT 表的磁盘分片。带有通配符的查询术语被忽略。
 
 ### low_priority
-`0` 或 `1`(默认为 `0`)。设置 `low_priority=1` 以较低优先级执行查询,其作业调度频率比正常优先级查询低 10 倍。
+`0` 或 `1`(默认为 `0`)。设置 `low_priority=1` 以较低优先级执行查询,其任务重新调度的频率比其他正常优先级查询低 10 倍。
 
 ### max_matches
-整数。每查询最大匹配值。
+整数。每查询最大匹配值。
 
-服务器为每个表保留在 RAM 中的最大匹配数,并可返回给客户端。默认值为 1000。
+服务器为每个表在 RAM 中保留的最大匹配数,并可返回给客户端。默认值为 1000。
 
-此设置用于控制和限制内存使用量。`max_matches` 确定在搜索每个表时保留在内存中的匹配数量。每个匹配都会被处理,但只有最优的 N 个匹配会被保存在内存中,并最终返回给客户端。例如,假设某表针对一个查询有 2,000,000 个匹配。您很少需要检索全部匹配。相反,您需要扫描所有匹配,但只选择“最优”的 500 个(基于某些标准,例如相关性、价格或其他因素),并分页显示这 500 个匹配,单页 20 到 100 条。仅跟踪最优的 500 个匹配比保留全部 2,000,000 个匹配且排序,然后丢弃除前20个结果外的结果要高效得多。`max_matches` 控制这个“最优 N 数量”
+引入 `max_matches` 设置是为了控制和限制 RAM 使用,该设置决定了在搜索每个表时将保留多少匹配项在 RAM 中。每个找到的匹配项仍会被处理,但只有最佳的 N 个匹配项会被保留在内存中,并最终返回给客户端。例如,假设一个表包含 2,000,000 个匹配项用于某个查询。很少需要检索所有匹配项。相反,你需要扫描所有匹配项,但仅选择基于某些标准(例如按相关性、价格或其他因素排序)的“最佳”500 个匹配项,并将这 500 个匹配项以每页 20 到 100 个匹配项的形式显示给最终用户。仅跟踪最佳的 500 个匹配项比保留所有 2,000,000 个匹配项、排序后丢弃除前 20 个外的所有内容,对 RAM 和 CPU 更加高效。`max_matches` 控制该“最佳 N”中的 N 值
 
-此参数显著影响每个查询的内存和 CPU 使用。通常 1,000 到 10,000 的值是可接受的,但过高值需要谨慎使用。任意增加 max_matches 到 1,000,000 意味着 `searchd` 会为每个查询分配和初始化一个包含一百万条目的匹配缓冲区,这必然增加每查询的内存使用,有时会明显影响性能。
+此参数会显著影响每个查询的RAM和CPU使用量。值在1,000到10,000之间通常是可以接受的,但提高限制时应谨慎使用。随意将max_matches增加到1,000,000意味着`searchd`必须为每个查询分配并初始化一个包含100万条匹配项的缓冲区。这不可避免地会增加每个查询的RAM使用量,并且在某些情况下可能明显影响性能。
 
-有关其如何影响 `max_matches` 选项行为的附加信息,请参见 [max_matches_increase_threshold](../Searching/Options.md#max_matches_increase_threshold)。
+有关它如何影响`max_matches`选项行为的更多信息,请参阅[max_matches_increase_threshold](../Searching/Options.md#max_matches_increase_threshold)。
 
 ### max_matches_increase_threshold
 
-整数。设置可增加的 `max_matches` 阈值。默认值是 16384。
+整数。设置`max_matches`可以增加的阈值。默认值为16384。
 
-当启用 `pseudo_sharding` 并且检测到 groupby 属性唯一值数量小于此阈值时,Manticore 可能会增大 `max_matches` 以提高 groupby 和/或聚合的准确性。精度损失可能出现在 pseudo-sharding 在多线程中执行查询,或 RT 表对磁盘区块进行并行搜索时
+当启用`pseudo_sharding`且检测到分组属性的唯一值数量小于此阈值时,Manticore可能会增加`max_matches`以提高分组和/或聚合的准确性。当伪分片在多个线程中执行查询或RT表在磁盘块中进行并行搜索时,可能会发生准确性下降
 
-如果 groupby 属性的唯一值数量小于此阈值,则 `max_matches` 会被设置为该数量。否则,使用默认的 `max_matches`。
+如果分组属性的唯一值数量小于阈值,`max_matches`将设置为该数量。否则,将使用默认的`max_matches`。
 
-如果查询选项中显式设置了 `max_matches`,此阈值无效。
+如果查询选项中显式设置了`max_matches`,此阈值无效。
 
-请注意,阈值设置过高会导致内存使用增加和整体性能下降。
+请注意,如果此阈值设置得过高,会导致内存消耗增加和整体性能下降。
 
-您还可以使用 [accurate_aggregation](../Searching/Options.md#accurate_aggregation) 选项强制执行保障的 groupby/聚合准确模式。
+您还可以使用[accurate_aggregation](../Searching/Options.md#accurate_aggregation)选项强制启用保证的分组/聚合准确性模式。
 
 ### max_query_time
-设置最大搜索查询时间,单位为毫秒。必须是非负整数。默认值为 0,表示“不限制”。本地搜索查询将在指定时间结束后停止。请注意,如果您执行的是查询多个本地表的搜索,则此限制分别适用于每个表。需注意,由于不断检查是否应停止查询的开销,这可能会稍微增加查询的响应时间。
+设置最大搜索查询时间(以毫秒为单位)。必须是非负整数。默认值为0,表示“不限制”。本地搜索查询一旦达到指定时间就会停止。请注意,如果您执行的搜索查询了多个本地表,此限制适用于每个表。请注意,由于不断跟踪是否需要停止查询所产生的开销,这可能会略微增加查询的响应时间。
 
 ### max_predicted_time
-整数。最大预测搜索时间;详见 [predicted_time_costs](../Server_settings/Searchd.md#predicted_time_costs)。
+整数。最大预测搜索时间;请参阅[predicted_time_costs](../Server_settings/Searchd.md#predicted_time_costs)。
 
 ### morphology
-`none` 允许用它们的精确形式替换所有查询词,如果表格是使用启用的 [index_exact_words](../Creating_a_table/NLP_and_tokenization/Morphology.md#index_exact_words) 构建的。这对于防止查询词被词干化或词形还原非常有用。
+`none`允许在表使用[index_exact_words](../Creating_a_table/NLP_and_tokenization/Morphology.md#index_exact_words)启用时,将所有查询术语替换为它们的确切形式。这对于防止对查询术语进行词干提取或词形还原很有用。
 
 ### not_terms_only_allowed
 <!-- example not_terms_only_allowed -->
-`0`  `1` 允许查询中使用独立的[否定](../Searching/Full_text_matching/Operators.md#Negation-operator)操作符。默认值是 0。另请参见相应的[全局设置](../Server_settings/Searchd.md#not_terms_only_allowed)。
+`0`或`1`允许查询中使用独立的[否定](../Searching/Full_text_matching/Operators.md#Negation-operator)。默认值为0。另请参阅相应的[全局设置](../Server_settings/Searchd.md#not_terms_only_allowed)。
 
 <!-- request SQL -->
 ```sql
@@ -314,7 +324,7 @@ MySQL [(none)]> select * from t where match('-donald') option not_terms_only_all
 <!-- end -->
 
 ### ranker
-从以下选项中选择:
+从以下选项中选择:
 * `proximity_bm25`
 * `bm25`
 * `none`
@@ -326,49 +336,49 @@ MySQL [(none)]> select * from t where match('-donald') option not_terms_only_all
 * `expr`
 * `export`
 
-有关每个排序器的更多详细信息,请参阅 [搜索结果排名](../Searching/Sorting_and_ranking.md#Available-built-in-rankers)。
+有关每个排序器的更多详细信息,请参阅[搜索结果排序](../Searching/Sorting_and_ranking.md#Available-built-in-rankers)。
 
 ### rand_seed
-允许您为 `ORDER BY RAND()` 查询指定特定的整数种子值,例如:`... OPTION rand_seed=1234`。默认情况下,每个查询都会自动生成一个新的不同的种子值。
+允许您为`ORDER BY RAND()`查询指定特定的整数种子值,例如:`... OPTION rand_seed=1234`。默认情况下,每个查询都会自动生成一个新的不同的种子值。
 
 ### retry_count
 整数。分布式重试次数。
 
 ### retry_delay
-整数。分布式重试延迟,单位为毫秒
+整数。分布式重试延迟(以毫秒为单位)
 
 ### scroll
 
-字符串。使用[滚动分页方法](../Searching/Pagination.md#Scroll-Search-Option)进行结果分页的滚动令牌。
+字符串。用于使用[Scroll分页方法](../Searching/Pagination.md#Scroll-Search-Option)分页结果的滚动令牌。
 
 ### sort_method
 * `pq` - 优先队列,默认设置
-* `kbuffer` - 对已预排序的数据提供更快的排序,例如按 id 排序的表数据
-两种情况下的结果集是相同的;选择其中一个选项可能仅仅是改善(或恶化)性能。
+* `kbuffer` - 为已预先排序的数据(例如按id排序的表数据)提供更快的排序
+两种情况下结果集相同;选择其中一个选项可能会简单地提高(或降低)性能。
 
 ### threads
-限制当前查询处理使用的最大线程数。默认 - 无限制(查询可以使用所有全局定义的 [threads](../Server_settings/Searchd.md#threads))。
-对于一批查询,该选项必须附加于该批中的第一个查询,然后在创建工作队列时应用,并对整个批次生效。此选项与 [max_threads_per_query](../Server_settings/Searchd.md#max_threads_per_query) 具有相同含义,但仅应用于当前查询或查询批次。
+限制当前查询处理使用的最大线程数。默认值 - 无限制(查询可以占用全局定义的[threads](../Server_settings/Searchd.md#threads)中的所有线程)。
+对于查询批次,该选项必须附加到批次中的第一个查询,并在创建工作队列时应用,对整个批次有效。此选项与选项[max_threads_per_query](../Server_settings/Searchd.md#max_threads_per_query)含义相同,但仅适用于当前查询或查询批次。
 
 ### token_filter
-带引号的、用冒号分隔的字符串格式为 `library name:plugin name:optional string of settings`。每当涉及的每个表调用全文搜索时,都会为该查询创建一个查询时令牌过滤器,允许您实现根据自定义规则生成令牌的自定义分词器
+用引号括起的、以冒号分隔的`library name:plugin name:optional string of settings`字符串。当每个表调用全文搜索时,为每个搜索创建一个查询时的标记过滤器,允许您实现自定义分词器,根据自定义规则生成标记
 ```sql
 SELECT * FROM index WHERE MATCH ('yes@no') OPTION token_filter='mylib.so:blend:@'
 ```
 ### expansion_limit
-限制单个通配符的最大扩展关键字数量,默认值为 0表示无限制。更多详细信息,请参阅 [expansion_limit](../Server_settings/Searchd.md#expansion_limit)。
+限制单个通配符扩展关键字最大数量,默认值为0表示无限制。有关更多详细信息,请参阅[expansion_limit](../Server_settings/Searchd.md#expansion_limit)。
 
 ## 查询优化器提示
 
 <!-- example options_force -->
 
-在极少数情况下,Manticore 内置的查询分析器可能无法正确理解查询并确定应使用 docid 索引、二级索引还是列扫描。为了覆盖查询优化器的决策,您可以在查询中使用以下提示:
+在极少数情况下,Manticore内置的查询分析器可能无法正确理解查询并确定是否应使用docid索引、二级索引或列扫描。要覆盖查询优化器的决策,您可以在查询中使用以下提示:
 
-* `/*+ DocidIndex(id) */`  强制使用 docid 索引,`/*+ NO_DocidIndex(id) */`  告诉优化器忽略它
-* `/*+ SecondaryIndex(<attr_name1>[, <attr_nameN>]) */`  强制使用二级索引(如果可用),`/*+ NO_SecondaryIndex(id) */`  告诉优化器忽略它
-* `/*+ ColumnarScan(<attr_name1>[, <attr_nameN>]) */`  强制使用列扫描(如果属性为列式),`/*+ NO_ColumnarScan(id) */` 告诉优化器忽略它
+* `/*+ DocidIndex(id) */` 强制使用docid索引,`/*+ NO_DocidIndex(id) */` 告诉优化器忽略它
+* `/*+ SecondaryIndex(<attr_name1>[, <attr_nameN>]) */` 强制使用二级索引(如果可用),`/*+ NO_SecondaryIndex(id) */` 告诉优化器忽略它
+* `/*+ ColumnarScan(<attr_name1>[, <attr_nameN>]) */` 强制使用列扫描(如果属性是列式的),`/*+ NO_ColumnarScan(id) */` 告诉优化器忽略它
 
-请注意,当执行带有过滤器的全文查询时,查询优化器会决定是将全文树结果与过滤器结果进行交集,还是使用标准的匹配后过滤方法。指定*任何*提示将强制守护进程使用执行全文树结果与过滤器结果交集的代码路径。
+请注意,当使用过滤器执行全文查询时,查询优化器会决定是对全文树的结果与过滤器结果进行交集运算,还是采用标准的先匹配后过滤方法。指定任何提示都将强制守护进程使用对全文树结果与过滤器结果进行交集运算的代码路径。
 
 有关查询优化器工作原理的更多信息,请参阅[基于成本的优化器](../Searching/Cost_based_optimizer.md)页面。
 
@@ -381,7 +391,7 @@ SELECT * FROM students where age > 21 /*+ SecondaryIndex(age) */
 <!-- end -->
 
 <!-- example comments -->
-使用 MySQL/MariaDB 客户端时,请确保添加 `--comments` 标志以启用查询中的提示。
+当使用MySQL/MariaDB客户端时,请确保包含`--comments`标志以在查询中启用提示。
 
 <!-- request mysql -->
 ```bash

+ 10 - 0
manual/english/Searching/Options.md

@@ -221,6 +221,16 @@ The accuracy of the `HyperLogLog` and the threshold for converting from the hash
 ### expand_keywords
 `0` or `1` (`0` by default). Expands keywords with exact forms and/or stars when possible. Refer to [expand_keywords](../Creating_a_table/NLP_and_tokenization/Wildcard_searching_settings.md#expand_keywords) for more details.
 
+### expand_blended
+`0`, `off`, `1`, or any combination of `blend_mode` options (`0` by default). Expands blended keywords (tokens that contain characters configured via [blend_chars](../Creating_a_table/NLP_and_tokenization/Low-level_tokenization.md#blend_chars)) into their constituent variants during query parsing. When enabled, keywords like "well-being" (if `-` is configured in `blend_chars`) are expanded into variants such as "well-being", "wellbeing", "well", and "being", which are then grouped into OR subtrees in the query tree.
+
+The supported values are:
+* `0` or `off` - Blended expansion is disabled (default). Blended keywords are processed normally without expansion.
+* `1` - Blended expansion is enabled and uses the table's [blend_mode](../Creating_a_table/NLP_and_tokenization/Low-level_tokenization.md#blend_mode) setting to determine which variants to generate.
+* Any blend mode option(s) - Blended expansion is enabled with the specified blend mode(s), overriding the table's `blend_mode` setting.
+
+Refer to [blend_mode](../Creating_a_table/NLP_and_tokenization/Low-level_tokenization.md#blend_mode) for more details on options.
+
 ### field_weights
 Named integer list (per-field user weights for ranking).
 

+ 57 - 47
manual/russian/Searching/Options.md

@@ -219,10 +219,20 @@ select avg(a) from t facet a
 Точность `HyperLogLog` и порог преобразования из хеш-таблицы в HyperLogLog определяются настройкой `distinct_precision_threshold`. Важно использовать эту опцию с осторожностью, поскольку удвоение ее значения также удвоит максимальный объем памяти, необходимый для вычисления подсчетов. Максимальное использование памяти можно приблизительно оценить по формуле: `64 * max_matches * distinct_precision_threshold`, хотя на практике вычисления подсчетов часто используют меньше памяти, чем в худшем случае.
 
 ### expand_keywords
-`0` или `1` (по умолчанию `0`). Расширяет ключевые слова точными формами и/или звёздочками, если это возможно. Подробности смотрите в [expand_keywords](../Creating_a_table/NLP_and_tokenization/Wildcard_searching_settings.md#expand_keywords).
+`0` или `1` (по умолчанию `0`). Расширяет ключевые слова точными формами и/или звездочками, когда это возможно. Подробнее см. [expand_keywords](../Creating_a_table/NLP_and_tokenization/Wildcard_searching_settings.md#expand_keywords).
+
+### expand_blended
+`0`, `off`, `1` или любая комбинация опций `blend_mode` (по умолчанию `0`). Расширяет смешанные ключевые слова (токены, содержащие символы, настроенные через [blend_chars](../Creating_a_table/NLP_and_tokenization/Low-level_tokenization.md#blend_chars)) на их составные варианты во время разбора запроса. При включении ключевые слова, такие как "well-being" (если `-` настроен в `blend_chars`), расширяются в варианты, такие как "well-being", "wellbeing", "well" и "being", которые затем группируются в поддеревья ИЛИ в дереве запроса.
+
+Поддерживаемые значения:
+* `0` или `off` - Расширение смешанных слов отключено (по умолчанию). Смешанные ключевые слова обрабатываются обычным образом без расширения.
+* `1` - Расширение смешанных слов включено и использует настройку [blend_mode](../Creating_a_table/NLP_and_tokenization/Low-level_tokenization.md#blend_mode) таблицы для определения, какие варианты генерировать.
+* Любая опция(и) режима смешивания - Расширение смешанных слов включено с указанным режимом(ами) смешивания, переопределяя настройку `blend_mode` таблицы.
+
+Подробнее об опциях см. [blend_mode](../Creating_a_table/NLP_and_tokenization/Low-level_tokenization.md#blend_mode).
 
 ### field_weights
-Именованный список целых чисел (веса пользователя для ранжирования по полям).
+Именованный список целых чисел (пользовательские веса по полям для ранжирования).
 
 Пример:
 ```sql
@@ -233,72 +243,72 @@ SELECT ... OPTION field_weights=(title=10, body=3)
 Использовать глобальную статистику (частоты) из файла [global_idf](../Creating_a_table/NLP_and_tokenization/Low-level_tokenization.md#global_idf) для вычислений IDF.
 
 ### idf
-Кавычками, через запятую перечисленные флаги вычисления IDF. Известные флаги:
+Заключенный в кавычки список флагов вычисления IDF, разделенных запятыми. Известные флаги:
 
 * `normalized`: вариант BM25, idf = log((N-n+1)/n), согласно Robertson et al
 * `plain`: простой вариант, idf = log(N/n), согласно Sparck-Jones
-* `tfidf_normalized`: дополнительно делить IDF на количество слов запроса, чтобы `TF*IDF` находился в диапазоне [0, 1]
-* `tfidf_unnormalized`: не делить дополнительно IDF на количество слов запроса, где N — размер коллекции, а n — количество совпадающих документов
+* `tfidf_normalized`: дополнительно делить IDF на количество слов в запросе, чтобы `TF*IDF` попадал в диапазон [0, 1]
+* `tfidf_unnormalized`: не делить дополнительно IDF на количество слов в запросе, где N - размер коллекции, а n - количество совпадающих документов
 
 Исторически используемый по умолчанию IDF (обратная частота документа) в Manticore эквивалентен `OPTION idf='normalized,tfidf_normalized'`, и эти нормализации могут вызывать несколько нежелательных эффектов.
 
-Во-первых, `idf=normalized` приводит к штрафованию ключевых слов. Например, если вы ищете `the | something` и `the` встречается более чем в 50% документов, тогда документы с двумя ключевыми словами `the` и `something` будут иметь меньший вес, чем документы с одним ключевым словом `something`. Использование `OPTION idf=plain` это избегает. Простой IDF варьируется в диапазоне `[0, log(N)]`, и ключевые слова никогда не штрафуются; а нормализованный IDF варьируется в диапазоне `[-log(N), log(N)]`, и слишком частотные ключевые слова штрафуются.
+Во-первых, `idf=normalized` приводит к штрафованию ключевых слов. Например, если вы ищете `the | something` и `the` встречается более чем в 50% документов, то документы с обоими ключевыми словами `the` и `something` получат меньший вес, чем документы только с одним ключевым словом `something`. Использование `OPTION idf=plain` позволяет избежать этого. Простой IDF варьируется в диапазоне `[0, log(N)]`, и ключевые слова никогда не штрафуются; в то время как нормализованный IDF варьируется в диапазоне `[-log(N), log(N)]`, и слишком частые ключевые слова штрафуются.
 
-Во-вторых, `idf=tfidf_normalized` приводит к дрейфу IDF между запросами. Исторически IDF также делился на количество ключевых слов в запросе, гарантируя, что сумма по всем ключевым словам `sum(tf*idf)` оставалась в диапазоне [0,1]. Однако это означало, что запросы, например, `word1` и `word1 | nonmatchingword2` присваивали разный вес абсолютно одинаковому множеству результатов, так как IDF для обоих слов делилось на 2. Использование `OPTION idf='tfidf_unnormalized'` решает эту проблему. Имейте в виду, что факторы ранжирования BM25, BM25A, BM25F() будут соответственно скорректированы при отключении этой нормализации.
+Во-вторых, `idf=tfidf_normalized` приводит к смещению IDF между запросами. Исторически IDF также делился на количество ключевых слов в запросе, гарантируя, что вся `sum(tf*idf)` по всем ключевым словам остается в пределах диапазона [0,1]. Однако это означало, что запросы типа `word1` и `word1 | nonmatchingword2` присваивали разные веса одному и тому же набору результатов, поскольку IDF как для `word1`, так и для `nonmatchingword2` делились на 2. Использование `OPTION idf='tfidf_unnormalized'` решает эту проблему. Имейте в виду, что факторы ранжирования BM25, BM25A, BM25F() будут соответствующим образом скорректированы при отключении этой нормализации.
 
-Флаги IDF могут комбинироваться; `plain` и `normalized` взаимоисключающие; `tfidf_unnormalized` и `tfidf_normalized` также взаимоисключающие; неуказанные флаги в таких взаимоисключающих группах принимают значения по умолчанию. Это означает, что `OPTION idf=plain` эквивалентен полному указанию `OPTION idf='plain,tfidf_normalized'`.
+Флаги IDF можно комбинировать; `plain` и `normalized` являются взаимоисключающими; `tfidf_unnormalized` и `tfidf_normalized` также являются взаимоисключающими; и неуказанные флаги в таких взаимоисключающих группах по умолчанию сохраняют свои исходные настройки. Это означает, что `OPTION idf=plain` эквивалентно полному указанию `OPTION idf='plain,tfidf_normalized'`.
 
 ### jieba_mode
-Задает режим сегментации Jieba для запроса.
+Определяет режим сегментации Jieba для запроса.
 
-При использовании китайской сегментации Jieba иногда полезно применять разные режимы сегментации для токенизации документов и запроса. Полный список режимов смотрите в [jieba_mode](../Creating_a_table/NLP_and_tokenization/Morphology.md#jieba_mode).
+При использовании китайской сегментации Jieba иногда может быть полезно использовать разные режимы сегментации для токенизации документов и запроса. Полный список режимов см. в [jieba_mode](../Creating_a_table/NLP_and_tokenization/Morphology.md#jieba_mode).
 
 ### index_weights
-Именованный список целых чисел. Веса пользователя для ранжирования по таблицам.
+Именованный список целых чисел. Пользовательские веса по таблицам для ранжирования.
 
 ### local_df
-`0` или `1`, автоматически суммирует DFs по всем локальным частям распределенной таблицы, обеспечивая согласованный (и точный) IDF для локально шардированной таблицы. По умолчанию включено для дисковых частей RT-таблицы. Термины запроса со звёздочками игнорируются.
+`0` или `1`, автоматически суммировать DF по всем локальным частям распределенной таблицы, обеспечивая согласованные (и точные) IDF для локально шардированной таблицы. По умолчанию включено для дисковых чанков RT-таблицы. Термины запроса с подстановочными знаками игнорируются.
 
 ### low_priority
-`0` или `1` (по умолчанию `0`). Установка `low_priority=1` выполняет запрос с более низким приоритетом, пересматривая задания для него в 10 раз реже, чем для запросов с обычным приоритетом.
+`0` или `1` (по умолчанию `0`). Установка `low_priority=1` выполняет запрос с более низким приоритетом, перепланируя его задачи в 10 раз реже, чем другие запросы с обычным приоритетом.
 
 ### max_matches
-Целое число. Максимальное количество совпадений, сохраняемых для каждого запроса.
+Целое число. Значение максимального количества совпадений на запрос.
 
-Максимальное количество совпадений, которые сервер сохраняет в ОЗУ для каждой таблицы и может вернуть клиенту. По умолчанию 1000.
+Максимальное количество совпадений, которое сервер хранит в оперативной памяти для каждой таблицы и может вернуть клиенту. По умолчанию равно 1000.
 
-Введено для контроля и ограничения использования ОЗУ, настройка `max_matches` определяет, сколько совпадений будет храниться в ОЗУ при поиске по каждой таблице. Все найденные совпадения обрабатываются, но в память сохраняются и в итоге клиенту возвращаются только лучшие N из них. Например, предположим, что таблица содержит 2 000 000 совпадений для запроса. Редко бывает нужным получить их все. Вместо этого нужно просмотреть все совпадения, но выбрать «лучшие» 500, например, по какому-то критерию (сортировка по релевантности, цене или другим факторам), и показать во всплывающих страницах для пользователя по 20–100 совпадений. Отслеживание только лучших 500 совпадений гораздо эффективнее с точки зрения ОЗУ и ЦП, чем хранение всех 2 000 000, сортировка и затем отбрасывание всего, кроме первых 20 для страницы результатов поиска. Параметр `max_matches` контролирует количество N в этом «лучшие N».
+Введенная для контроля и ограничения использования оперативной памяти, настройка `max_matches` определяет, сколько совпадений будет храниться в оперативной памяти при поиске по каждой таблице. Каждое найденное совпадение все равно обрабатывается, но только лучшие N из них будут сохранены в памяти и в конечном итоге возвращены клиенту. Например, предположим, что таблица содержит 2 000 000 совпадений для запроса. Редко возникает необходимость получить их все. Вместо этого вам нужно просканировать их все, но выбрать только "лучшие" 500, например, на основе некоторых критериев (например, отсортированных по релевантности, цене или другим факторам) и отобразить эти 500 совпадений конечному пользователю страницами по 20-100 совпадений. Отслеживание только лучших 500 совпадений гораздо эффективнее по использованию оперативной памяти и процессора, чем хранение всех 2 000 000 совпадений, их сортировка, а затем отбрасывание всего, кроме первых 20, необходимых для страницы результатов поиска. `max_matches` контролирует N в этом количестве "лучших N".
 
-Этот параметр существенно влияет на использование ОЗУ и ЦП для каждого запроса. Обычно приемлемы значения от 1000 до 10000, но более высокие лимиты следует использовать с осторожностью. Небрежное увелечение max_matches до 1 000 000 означает, что `searchd` потребуется выделить и инициализировать буфер совпадений с 1 миллионом записей для каждого запроса. Это неизбежно увеличит использование ОЗУ на запрос и, в некоторых случаях, может заметно повлиять на производительность.
+Этот параметр значительно влияет на использование оперативной памяти и процессора для каждого запроса. Значения от 1 000 до 10 000 обычно приемлемы, но более высокие лимиты следует использовать с осторожностью. Бездумное увеличение max_matches до 1 000 000 означает, что `searchd` должен будет выделять и инициализировать буфер совпадений на 1 миллион записей для каждого запроса. Это неизбежно увеличит использование оперативной памяти на запрос и, в некоторых случаях, может заметно повлиять на производительность.
 
-Дополнительную информацию о влиянии параметра `max_matches` смотрите в [max_matches_increase_threshold](../Searching/Options.md#max_matches_increase_threshold).
+Обратитесь к [max_matches_increase_threshold](../Searching/Options.md#max_matches_increase_threshold) для получения дополнительной информации о том, как это может повлиять на поведение опции `max_matches`.
 
 ### max_matches_increase_threshold
 
 Целое число. Устанавливает порог, до которого можно увеличить `max_matches`. По умолчанию 16384.
 
-Manticore может увеличить `max_matches` для повышения точности группировки и/или агрегации при включенном `pseudo_sharding`, если обнаружено, что количество уникальных значений атрибута группировки меньше этого порога. Потеря точности может произойти при выполнении запроса в нескольких потоках через псевдо-шардинг или при параллельном поиске в дисковых частях RT-таблицы.
+Manticore может увеличить `max_matches` для повышения точности группировки и/или агрегации, когда включен `pseudo_sharding`, и если он обнаруживает, что количество уникальных значений атрибута группировки меньше этого порога. Потеря точности может произойти, когда псевдошардирование выполняет запрос в нескольких потоках или когда RT-таблица проводит параллельный поиск в дисковых чанках.
 
-Если количество уникальных значений атрибута группировки меньше порога, `max_matches` будет установлено в это значение. В противном случае будет использовано значение `max_matches` по умолчанию.
+Если количество уникальных значений атрибута группировки меньше порога, `max_matches` будет установлено в это число. В противном случае будет использовано значение `max_matches` по умолчанию.
 
-Если `max_matches` был явно задан в опциях запроса, этот порог не действует.
+Если `max_matches` было явно установлено в параметрах запроса, этот порог не действует.
 
-Имейте в виду, что слишком высокое значение этого порога приведет к увеличению потребления памяти и общему снижению производительности.
+Имейте в виду, что если этот порог установлен слишком высоко, это приведет к увеличению потребления памяти и общему снижению производительности.
 
-Вы также можете применить режим гарантированной точности группировки/агрегации с помощью опции [accurate_aggregation](../Searching/Options.md#accurate_aggregation).
+Вы также можете принудительно включить режим гарантированной точности группировки/агрегации с помощью опции [accurate_aggregation](../Searching/Options.md#accurate_aggregation).
 
 ### max_query_time
-Устанавливает максимальное время выполнения поискового запроса в миллисекундах. Должно быть неотрицательным целым числом. Значение по умолчанию — 0, что означает «не ограничиваться». Локальные поисковые запросы будут прерваны после истечения заданного времени. Обратите внимание, что если вы выполняете поиск, обращающийся к нескольким локальным таблицам, это ограничение применяется к каждой таблице отдельно. Будьте готовы к тому, что это может немного увеличить время отклика запроса из-за накладных расходов, связанных с постоянным отслеживанием момента, когда нужно остановить запрос.
+Устанавливает максимальное время выполнения поискового запроса в миллисекундах. Должно быть неотрицательным целым числом. Значение по умолчанию — 0, что означает «не ограничивать». Локальные поисковые запросы будут остановлены, как только истечет указанное время. Обратите внимание, что если вы выполняете поиск, который запрашивает несколько локальных таблиц, этот лимит применяется к каждой таблице отдельно. Имейте в виду, что это может немного увеличить время отклика запроса из-за накладных расходов, вызванных постоянным отслеживанием, не пора ли остановить запрос.
 
 ### max_predicted_time
-Целое число. Максимальное прогнозируемое время поиска; смотрите [predicted_time_costs](../Server_settings/Searchd.md#predicted_time_costs).
+Целое число. Максимальное прогнозируемое время поиска; см. [predicted_time_costs](../Server_settings/Searchd.md#predicted_time_costs).
 
 ### morphology
-`none` позволяет заменять все термины запроса их точными формами, если таблица была создана с включённой опцией [index_exact_words](../Creating_a_table/NLP_and_tokenization/Morphology.md#index_exact_words). Это полезно для предотвращения стемминга или лемматизации терминов запроса.
+`none` позволяет заменять все термины запроса их точными формами, если таблица была построена с включенной опцией [index_exact_words](../Creating_a_table/NLP_and_tokenization/Morphology.md#index_exact_words). Это полезно для предотвращения стемминга или лемматизации терминов запроса.
 
 ### not_terms_only_allowed
 <!-- example not_terms_only_allowed -->
-`0` или `1` разрешает самостоятельное [отрицание](../Searching/Full_text_matching/Operators.md#Negation-operator) для запроса. Значение по умолчанию — 0. См. также соответствующую [глобальную настройку](../Server_settings/Searchd.md#not_terms_only_allowed).
+`0` или `1` разрешает автономное [отрицание](../Searching/Full_text_matching/Operators.md#Negation-operator) для запроса. По умолчанию 0. См. также соответствующую [глобальную настройку](../Server_settings/Searchd.md#not_terms_only_allowed).
 
 <!-- request SQL -->
 ```sql
@@ -314,7 +324,7 @@ MySQL [(none)]> select * from t where match('-donald') option not_terms_only_all
 <!-- end -->
 
 ### ranker
-Выберите один из следующих вариантов:
+Выберите из следующих вариантов:
 * `proximity_bm25`
 * `bm25`
 * `none`
@@ -326,51 +336,51 @@ MySQL [(none)]> select * from t where match('-donald') option not_terms_only_all
 * `expr`
 * `export`
 
-Подробнее о каждом ранжировщике смотрите в разделе [Ранжирование результатов поиска](../Searching/Sorting_and_ranking.md#Available-built-in-rankers).
+Для получения более подробной информации о каждом ранкере обратитесь к [Ранжирование результатов поиска](../Searching/Sorting_and_ranking.md#Available-built-in-rankers).
 
 ### rand_seed
-Позволяет указать конкретное значение начального «семени» для запроса `ORDER BY RAND()`, например: `... OPTION rand_seed=1234`. По умолчанию для каждого запроса автоматически создаётся новое иное значение.
+Позволяет указать конкретное целочисленное значение сида для запроса `ORDER BY RAND()`, например: `... OPTION rand_seed=1234`. По умолчанию для каждого запроса автоматически генерируется новое и разное значение сида.
 
 ### retry_count
-Целое число. Количество повторных попыток в распределённом режиме.
+Целое число. Количество повторных попыток для распределенного поиска.
 
 ### retry_delay
-Целое число. Задержка между попытками в распределённом режиме, в миллисекундах.
+Целое число. Задержка между повторными попытками для распределенного поиска, в миллисекундах.
 
 ### scroll
 
-Строка. Токен scroll для поэтапного получения результатов с помощью [Scroll pagination approach](../Searching/Pagination.md#Scroll-Search-Option).
+Строка. Токен прокрутки для постраничного вывода результатов с использованием [Подхода к пагинации Scroll](../Searching/Pagination.md#Scroll-Search-Option).
 
 ### sort_method
-* `pq` - очередь с приоритетом, используется по умолчанию
-* `kbuffer` - обеспечивает более быструю сортировку для уже частично отсортированных данных, например, для данных таблицы, отсортированных по id
-Набор результатов в обоих случаях одинаков; выбор одного из вариантов может лишь улучшить (или ухудшить) производительность.
+* `pq` - очередь с приоритетом, установлена по умолчанию
+* `kbuffer` - обеспечивает более быструю сортировку для уже предварительно отсортированных данных, например, данных таблицы, отсортированных по id
+Результирующий набор одинаков в обоих случаях; выбор того или иного варианта может просто улучшить (или ухудшить) производительность.
 
 ### threads
-Ограничивает максимальное число потоков, используемых для обработки текущего запроса. По умолчанию — без ограничений (запрос может использовать все [потоки](../Server_settings/Searchd.md#threads), установленные глобально).
-Для пакета запросов опция должна быть применена к первому запросу в пакете, после чего она применяется при создании рабочей очереди и действует на весь пакет. Эта опция эквивалентна опции [max_threads_per_query](../Server_settings/Searchd.md#max_threads_per_query), но применяется только к текущему запросу или пакету запросов.
+Ограничивает максимальное количество потоков, используемых для обработки текущего запроса. По умолчанию — без ограничений (запрос может занять все [потоки](../Server_settings/Searchd.md#threads), определенные глобально).
+Для пакета запросов опция должна быть прикреплена к самому первому запросу в пакете, и затем она применяется при создании рабочей очереди и действует на весь пакет. Эта опция имеет тот же смысл, что и опция [max_threads_per_query](../Server_settings/Searchd.md#max_threads_per_query), но применяется только к текущему запросу или пакету запросов.
 
 ### token_filter
начение в кавычках, строка с разделителем двоеточием в формате `имя_библиотеки:имя_плагина:необязательная_строка_настроек`. При вызове полнотекстового поиска для каждой задействованной таблицы создаётся фильтр токенов в режиме выполнения запроса, что позволяет реализовать собственный токенизатор, генерирующий токены по пользовательским правилам.
аключенная в кавычки строка, разделенная двоеточиями: `имя библиотеки:имя плагина:необязательная строка настроек`. Фильтр токенов времени запроса создается для каждого поиска, когда полнотекстовый поиск вызывается каждой задействованной таблицей, позволяя реализовать пользовательский токенизатор, который генерирует токены в соответствии с пользовательскими правилами.
 ```sql
 SELECT * FROM index WHERE MATCH ('yes@no') OPTION token_filter='mylib.so:blend:@'
 ```
 ### expansion_limit
-Ограничивает максимальное число расширенных ключевых слов для одного подстановочного знака, по умолчанию 0 означает отсутствие ограничений. Дополнительную информацию см. в разделе [expansion_limit](../Server_settings/Searchd.md#expansion_limit).
+Ограничивает максимальное количество расширенных ключевых слов для одного подстановочного знака, значение по умолчанию 0 означает отсутствие ограничений. Для получения дополнительных сведений обратитесь к [expansion_limit](../Server_settings/Searchd.md#expansion_limit).
 
 ## Подсказки оптимизатора запросов
 
 <!-- example options_force -->
 
-В редких случаях встроенный анализатор запросов Manticore может ошибочно интерпретировать запрос и определить, следует ли использовать индекс docid, вторичные индексы или посколонный скан. Чтобы переопределить решения оптимизатора запросов, можно использовать следующие подсказки в вашем запросе:
+В редких случаях встроенный анализатор запросов Manticore может ошибаться в понимании запроса и определении того, следует ли использовать индекс docid, вторичные индексы или колоночное сканирование. Чтобы переопределить решения оптимизатора запросов, вы можете использовать следующие подсказки в своем запросе:
 
-* `/*+ DocidIndex(id) */` — принудительно использовать индекс docid, `/*+ NO_DocidIndex(id) */` — указать оптимизатору игнорировать его
-* `/*+ SecondaryIndex(<attr_name1>[, <attr_nameN>]) */` — принудительно использовать вторичный индекс (если доступен), `/*+ NO_SecondaryIndex(id) */` — указать оптимизатору игнорировать его
-* `/*+ ColumnarScan(<attr_name1>[, <attr_nameN>]) */` — принудительно использовать посколонный скан (если атрибут колонный), `/*+ NO_ColumnarScan(id) */` — указать оптимизатору игнорировать его
+* `/*+ DocidIndex(id) */`  для принудительного использования индекса docid, `/*+ NO_DocidIndex(id) */` чтобы указать оптимизатору игнорировать его
+* `/*+ SecondaryIndex(<attr_name1>[, <attr_nameN>]) */` для принудительного использования вторичного индекса (если доступен), `/*+ NO_SecondaryIndex(id) */`  чтобы указать оптимизатору игнорировать его
+* `/*+ ColumnarScan(<attr_name1>[, <attr_nameN>]) */`  для принудительного использования колоночного сканирования (если атрибут колоночный), `/*+ NO_ColumnarScan(id) */` чтобы указать оптимизатору игнорировать его
 
-Обратите внимание, что при выполнении полнотекстового запроса с фильтрами оптимизатор запроса выбирает между пересечением результатов полнотекстового дерева с результатами фильтра или стандартным подходом «сначала совпадение, затем фильтр». Указание *любой* подсказки заставит демон использовать путь кода, который выполняет пересечение результатов полнотекстового дерева с фильтрующими результатами.
+Обратите внимание, что при выполнении полнотекстового запроса с фильтрами оптимизатор запросов выбирает между пересечением результатов полнотекстового дерева с результатами фильтров или использованием стандартного подхода "сначала сопоставление, затем фильтрация". Указание *любой* подсказки заставит демона использовать путь выполнения, который осуществляет пересечение результатов полнотекстового дерева с результатами фильтров.
 
-Подробнее о работе оптимизатора запросов смотрите на странице [Оптимизатор на основе стоимости](../Searching/Cost_based_optimizer.md).
+Для получения дополнительной информации о том, как работает оптимизатор запросов, обратитесь к странице [Стоимостной оптимизатор](../Searching/Cost_based_optimizer.md).
 
 <!-- request SQL -->
 
@@ -381,7 +391,7 @@ SELECT * FROM students where age > 21 /*+ SecondaryIndex(age) */
 <!-- end -->
 
 <!-- example comments -->
-При использовании клиента MySQL/MariaDB не забудьте включить флаг `--comments`, чтобы подсказки были активны в ваших запросах.
+При использовании клиента MySQL/MariaDB убедитесь, что включен флаг `--comments`, чтобы активировать подсказки в ваших запросах.
 
 <!-- request mysql -->
 ```bash

+ 5 - 0
src/daemon/api_search.cpp

@@ -303,6 +303,8 @@ void SearchRequestBuilder_c::SendQuery ( const char * sIndexes, ISphOutputBuffer
 		tOut.SendFloat ( i.m_fValue );
 		tOut.SendString ( i.m_sValue.cstr() );
 	}
+
+	tOut.SendString ( q.m_sExpandBlended.cstr() );
 }
 
 
@@ -1100,6 +1102,9 @@ bool ParseSearchQuery ( InputBuffer_c & tReq, ISphOutputBuffer & tOut, CSphQuery
 		}
 	}
 
+	if ( uMasterVer>=27 )
+		tQuery.m_sExpandBlended = tReq.GetString();
+
 	/////////////////////
 	// additional checks
 	/////////////////////

+ 1 - 1
src/searchdaemon.h

@@ -135,7 +135,7 @@ const char* szCommand ( int );
 /// master-agent API SEARCH command protocol extensions version
 enum
 {
-	VER_COMMAND_SEARCH_MASTER = 26
+	VER_COMMAND_SEARCH_MASTER = 27
 };
 
 

+ 9 - 2
src/searchdsql.cpp

@@ -628,6 +628,7 @@ enum class Option_e : BYTE
 	JOIN_BATCH_SIZE,
 	FORCE,
 	FORMAT_OUTPUT_WORDS,
+	EXPAND_BLENDED,
 
 	INVALID_OPTION
 };
@@ -642,7 +643,7 @@ void InitParserOption()
 		"max_matches", "max_predicted_time", "max_query_time", "morphology", "rand_seed", "ranker", "retry_count",
 		"retry_delay", "reverse_scan", "sort_method", "strict", "sync", "threads", "token_filter", "token_filter_options",
 		"not_terms_only_allowed", "store", "accurate_aggregation", "max_matches_increase_threshold", "distinct_precision_threshold",
-		"threads_ex", "switchover", "expansion_limit", "jieba_mode", "scroll", "join_batch_size", "force", "output_words" };
+		"threads_ex", "switchover", "expansion_limit", "jieba_mode", "scroll", "join_batch_size", "force", "output_words", "expand_blended" };
 
 	for ( BYTE i = 0u; i<(BYTE) Option_e::INVALID_OPTION; ++i )
 		g_hParseOption.Add ( (Option_e) i, dOptions[i] );
@@ -676,7 +677,7 @@ static bool CheckOption ( SqlStmt_e eStmt, Option_e eOption )
 			Option_e::RETRY_COUNT, Option_e::RETRY_DELAY, Option_e::REVERSE_SCAN, Option_e::SORT_METHOD,
 			Option_e::THREADS, Option_e::TOKEN_FILTER, Option_e::NOT_ONLY_ALLOWED, Option_e::ACCURATE_AGG,
 			Option_e::MAXMATCH_THRESH, Option_e::DISTINCT_THRESH, Option_e::THREADS_EX, Option_e::EXPANSION_LIMIT,
-			Option_e::JIEBA_MODE, Option_e::SCROLL, Option_e::JOIN_BATCH_SIZE };
+			Option_e::JIEBA_MODE, Option_e::SCROLL, Option_e::JOIN_BATCH_SIZE, Option_e::EXPAND_BLENDED };
 
 	static Option_e dInsertOptions[] = { Option_e::TOKEN_FILTER_OPTIONS };
 
@@ -827,6 +828,7 @@ AddOption_e AddOption ( CSphQuery & tQuery, const CSphString & sOpt, const CSphS
 	case Option_e::STRICT_:						tQuery.m_bStrict = iValue!=0; break;
 	case Option_e::SYNC: 						tQuery.m_bSync = iValue!=0; break;
 	case Option_e::EXPAND_KEYWORDS:				tQuery.m_eExpandKeywords = ( iValue!=0 ? QUERY_OPT_ENABLED : QUERY_OPT_DISABLED ); break;
+	case Option_e::EXPAND_BLENDED:				tQuery.m_sExpandBlended = ( sValue ); break;
 	case Option_e::THREADS:						tQuery.m_iConcurrency = (int)iValue; break;
 	case Option_e::NOT_ONLY_ALLOWED:			tQuery.m_bNotOnlyAllowed = iValue!=0; break;
 	case Option_e::RAND_SEED:					tQuery.m_iRandSeed = int64_t(DWORD(iValue)); break;
@@ -939,6 +941,11 @@ AddOption_e AddOption ( CSphQuery & tQuery, const CSphString & sOpt, const CSphS
 			return FAILED ( "morphology could be only disabled with option none, got %s", sVal.cstr() );
 		break;
 
+	case Option_e::EXPAND_BLENDED:
+		if ( sVal!="0" )
+			tQuery.m_sExpandBlended = sVal;
+		break;
+
 	case Option_e::STORE: //} else if ( sOpt=="store" )
 		tQuery.m_sStore = sVal;
 		break;

+ 1 - 0
src/sphinx.h

@@ -561,6 +561,7 @@ struct CSphQuery
 	DWORD			m_uDebugFlags = 0;
 	QueryOption_e	m_eExpandKeywords = QUERY_OPT_DEFAULT;	///< control automatic query-time keyword expansion
 	int				m_iExpansionLimit = DEFAULT_QUERY_EXPANSION_LIMIT;	///< whether to limit wildcard expansion, default use index settings
+	CSphString		m_sExpandBlended;			///< control blend_chars expansion during search tokenization
 
 	bool			m_bAccurateAggregation = false;			///< setting via options
 	bool			m_bExplicitAccurateAggregation = false; ///< whether anything was set via options

+ 179 - 1
src/sphinxquery/parse_helper.cpp

@@ -406,6 +406,8 @@ XQNode_t * XQParseHelper_c::FixupTree ( XQNode_t * pRoot, const XQLimitSpec_t &
 	if constexpr ( bDump ) Dump ( pRoot, "raw FixupTree" );
 	FixupDestForms ();
 	if constexpr ( bDump ) Dump ( pRoot, "FixupDestForms" );
+	FixupBlend ( pRoot );
+	if constexpr ( bDump ) Dump ( pRoot, "FixupBlend" );
 	DeleteNodesWOFields ( pRoot );
 	if constexpr ( bDump ) Dump ( pRoot, "DeleteNodesWOFields" );
 	pRoot = SweepNulls ( pRoot, bOnlyNotAllowed );
@@ -775,4 +777,180 @@ void XQParseHelper_c::DeleteSpawned ( XQNode_t * pNode ) noexcept
 	});
 	pNode->ResetChildren();
 	SafeDelete ( pNode );
-}
+}
+
+struct BlendedKw_t
+{
+	XQKeyword_t m_tWord;
+	XQNode_t * m_pParent = nullptr;
+	int m_iOrder = 0;
+	uint64_t m_uName = 0; // name hash or duplicate flag if 0
+
+	BlendedKw_t() = default;
+
+	BlendedKw_t ( XQKeyword_t && tWord, XQNode_t * pParent, int iOrder )
+		: m_tWord ( std::move ( tWord ) )
+		, m_pParent ( pParent )
+		, m_iOrder ( iOrder )
+	{
+		m_uName = sphFNV64 ( m_tWord.m_sWord.cstr() );
+	}
+};
+
+using BlendedVec_t = CSphVector<BlendedKw_t>;
+using FieldStartEnd_t = std::pair<bool, bool>;
+
+static void FlushOrGroup ( XQNode_t * pOr, XQNode_t * pParent, CSphVector<XQNode_t *> & dKeywordNodes, const FieldStartEnd_t & tFieldStartEnd )
+{
+	if ( !pOr || dKeywordNodes.IsEmpty() )
+		return;
+
+	assert ( pParent );
+	
+	// field modifiers to first keyword nodes
+	dKeywordNodes[0]->dWord(0).m_bFieldStart = tFieldStartEnd.first;
+	dKeywordNodes[0]->dWord(0).m_bFieldEnd = tFieldStartEnd.second;
+	
+	// OR node children and add to parent
+	pOr->SetOp ( SPH_QUERY_OR, dKeywordNodes );
+	pParent->AddNewChild ( pOr );
+	dKeywordNodes.Resize ( 0 );
+}
+
+static void CollectBlended ( XQNode_t * pNode, BlendedVec_t & dBlended )
+{
+	if ( !pNode )
+		return;
+
+	pNode->WithChildren ( [&dBlended] ( auto & dChildren )
+	{
+		for ( auto & pChild : dChildren )
+			CollectBlended ( pChild, dBlended );
+	});
+
+	if ( !pNode->dWords().IsEmpty() )
+	{
+		pNode->WithWords ( [&dBlended, pNode] ( auto & dWords )
+		{
+			if ( !dWords.any_of ( []( const auto & tWord ){ return tWord.m_iBlendedGroup>=0; } ) )
+				return;
+
+			int iOut = 0;
+			ARRAY_FOREACH ( i, dWords )
+			{
+				auto & tWord = dWords[i];
+				if ( tWord.m_iBlendedGroup>=0 )
+				{
+					dBlended.Add ( BlendedKw_t ( std::move ( tWord ), pNode, dBlended.GetLength() ) );
+				} else
+				{
+					if ( iOut!=i )
+						dWords[iOut] = std::move ( dWords[i] );
+					iOut++;
+				}
+			}
+			dWords.Resize ( iOut );
+		});
+	}
+}
+
+void XQParseHelper_c::FixupBlend ( XQNode_t * pNode )
+{
+	if ( !m_bExpandBlended )
+		return;
+
+	if ( !pNode )
+		return;
+
+	CSphVector<BlendedKw_t> dBlended;
+	CollectBlended ( pNode, dBlended );
+	
+	if ( dBlended.IsEmpty() )
+		return;
+
+	// sort by gen asc then name dups then order asc
+	dBlended.Sort ( Lesser ( [] ( const BlendedKw_t & tA, const BlendedKw_t & tB )
+	{
+		int iGenA = tA.m_tWord.m_iBlendedGroup;
+		int iGenB = tB.m_tWord.m_iBlendedGroup;
+		if ( iGenA!=iGenB )
+			return ( iGenA<iGenB );
+		if ( tA.m_uName!=tB.m_uName )
+			return ( tA.m_uName<tB.m_uName );
+
+		return ( tA.m_iOrder<tB.m_iOrder );
+	}));
+
+	// mark duplicates
+	for ( int i=1; i<dBlended.GetLength(); i++ )
+	{
+		const auto & tPrev = dBlended[i-1];
+		auto & tCur = dBlended[i];
+		if ( tPrev.m_tWord.m_iBlendedGroup==tCur.m_tWord.m_iBlendedGroup && tPrev.m_uName==tCur.m_uName )
+			tCur.m_uName = 0;
+	}
+
+	// sort by gen asc then order asc
+	dBlended.Sort ( Lesser ( [] ( const BlendedKw_t & tA, const BlendedKw_t & tB )
+	{
+		int iGenA = tA.m_tWord.m_iBlendedGroup;
+		int iGenB = tB.m_tWord.m_iBlendedGroup;
+		if ( iGenA!=iGenB )
+			return ( iGenA<iGenB );
+
+		return ( tA.m_iOrder<tB.m_iOrder );
+	}));
+
+	// OR nodes for each gen group
+	XQNode_t * pOr = nullptr;
+	XQNode_t * pParent = nullptr;
+	CSphVector<XQNode_t *> dKeywordNodes; // keyword nodes for current OR group
+	FieldStartEnd_t tFieldStartEnd { false, false }; // track field modifiers: first = field start, second = field end
+	int iCurrentGen = -1;
+	
+	for ( const auto & tBlended : dBlended )
+	{
+		// skip duplicates with 0 in name hash
+		if ( !tBlended.m_uName )
+			continue;
+
+		// new group
+		if ( iCurrentGen!=tBlended.m_tWord.m_iBlendedGroup )
+		{
+			// flush previous group
+			FlushOrGroup ( pOr, pParent, dKeywordNodes, tFieldStartEnd );
+			pOr = nullptr;
+			pParent = nullptr;
+			tFieldStartEnd = { false, false };
+
+			// atart new group
+			pParent = tBlended.m_pParent;
+			const XQLimitSpec_t & tSpec = pParent->m_dSpec;
+			iCurrentGen = tBlended.m_tWord.m_iBlendedGroup;
+
+			// OR node for this blended group
+			pOr = SpawnNode ( tSpec );
+			pOr->SetOp ( SPH_QUERY_OR );
+			
+			// field start from first word in new group
+			tFieldStartEnd.first = tBlended.m_tWord.m_bFieldStart;
+		}
+
+		// create KEYWORD node for this word
+		assert ( pOr && pParent );
+		const XQLimitSpec_t & tSpec = pParent->m_dSpec;
+		
+		// update field end from current word
+		tFieldStartEnd.second = tBlended.m_tWord.m_bFieldEnd;
+		
+		// keyword node with the word (field modifiers will be applied later)
+		XQKeyword_t tWordCopy = tBlended.m_tWord;
+		
+		XQNode_t * pKeywordNode = SpawnNode ( tSpec );
+		pKeywordNode->AddDirtyWord ( std::move ( tWordCopy ) );
+		dKeywordNodes.Add ( pKeywordNode );
+	}
+
+	// flush last group
+	FlushOrGroup ( pOr, pParent, dKeywordNodes, tFieldStartEnd );
+}

+ 2 - 0
src/sphinxquery/parse_helper.h

@@ -65,6 +65,7 @@ protected:
 	int						m_iAtomPos {0};
 	bool					m_bEmptyStopword {false};
 	bool					m_bWasBlended {false};
+	bool					m_bExpandBlended { false };
 
 	CSphVector<XQNode_t*>		m_dSpawned;
 	StrVec_t					m_dDestForms;
@@ -82,4 +83,5 @@ private:
 	void			FixupDestForms();
 	bool			CheckQuorumProximity ( const XQNode_t * pNode );
 	bool			CheckNear ( const XQNode_t * pNode );
+	void			FixupBlend ( XQNode_t * pNode );
 };

+ 1 - 0
src/sphinxquery/sphinxquery.h

@@ -32,6 +32,7 @@ struct XQKeyword_t
 	mutable bool		m_bMorphed = false;		///< morphology processing (wordforms, stemming etc) already done
 	mutable void *		m_pPayload = nullptr;
 	mutable bool		m_bRegex = false;
+	mutable int			m_iBlendedGroup = -1;	///< blended token group (-1 - not blended, >0 - group number)
 
 	XQKeyword_t() = default;
 	XQKeyword_t ( const char * sWord, int iPos )

+ 46 - 5
src/sphinxquery/xqparser.cpp

@@ -149,6 +149,8 @@ public:
 	int						m_iQuorumQuote = -1;
 	int						m_iQuorumFSlash = -1;
 	bool					m_bCheckNumber = false;
+	int						m_iBlendedGroup = 0;
+	int						m_iBlendedStepDelta = 1;
 
 	StrVec_t				m_dIntTokens;
 
@@ -429,10 +431,13 @@ int XQParser_t::GetToken ( YYSTYPE * lvalp )
 		int iSkippedPosBeforeToken = 0;
 		if ( m_bWasBlended )
 		{
-			iSkippedPosBeforeToken = m_pTokenizer->SkipBlended();
-			// just add all skipped blended parts except blended head (already added to atomPos)
-			if ( iSkippedPosBeforeToken>1 )
-				m_iAtomPos += iSkippedPosBeforeToken - 1;
+			if ( !m_bExpandBlended || ( m_bExpandBlended && m_pTokenizer->IsPhraseMode() ) )
+			{
+				iSkippedPosBeforeToken = m_pTokenizer->SkipBlended();
+				// just add all skipped blended parts except blended head (already added to atomPos)
+				if ( iSkippedPosBeforeToken>1 )
+					m_iAtomPos += iSkippedPosBeforeToken - 1;
+			}
 		}
 
 		// tricky stuff
@@ -471,6 +476,10 @@ int XQParser_t::GetToken ( YYSTYPE * lvalp )
 		m_bWasBlended = m_pTokenizer->TokenIsBlended();
 		m_bEmpty = false;
 
+		// track blended group when expand is enabled
+		if ( m_bExpandBlended && !m_pTokenizer->IsPhraseMode() && m_pTokenizer->TokenIsBlendedHead() )
+			m_iBlendedGroup++; // new blended group
+
 		int iPrevDeltaPos = 0;
 		if ( m_pPlugin && m_pPlugin->m_fnPushToken )
 			sToken = m_pPlugin->m_fnPushToken ( m_pPluginData, const_cast<char*>(sToken), &iPrevDeltaPos, m_pTokenizer->GetTokenStart(), int ( m_pTokenizer->GetTokenEnd() - m_pTokenizer->GetTokenStart() ) );
@@ -479,7 +488,17 @@ int XQParser_t::GetToken ( YYSTYPE * lvalp )
 			return 0;
 
 		m_iPendingNulls = m_pTokenizer->GetOvershortCount() * m_iOvershortStep;
-		m_iAtomPos += 1 + m_iPendingNulls;
+		if ( m_bExpandBlended && !m_pTokenizer->IsPhraseMode() )
+		{
+			// step from previous token to increment current token position
+			m_iAtomPos += m_iBlendedStepDelta + m_iPendingNulls;
+			// step for next token based on current token
+			m_iBlendedStepDelta = m_pTokenizer->TokenIsBlended() ? 0 : 1;
+		} else
+		{
+			m_iAtomPos += 1 + m_iPendingNulls;
+			m_iBlendedStepDelta = 1;
+		}
 		if ( iPrevDeltaPos>1 ) // to match with condifion of m_bWasBlended above
 			m_iAtomPos += ( iPrevDeltaPos - 1);
 
@@ -833,6 +852,14 @@ XQNode_t * XQParser_t::AddKeyword ( const char * sKeyword, int iSkippedPosBefore
 	XQKeyword_t tAW ( sKeyword, m_iAtomPos );
 	tAW.m_iSkippedBefore = iSkippedPosBeforeToken;
 	HandleModifiers ( tAW );
+	
+	// Set blended generation if blend_expand is enabled and token is blended
+	if ( m_bExpandBlended && !m_pTokenizer->IsPhraseMode() )
+	{
+		if ( m_pTokenizer->TokenIsBlended() || m_pTokenizer->TokenIsBlendedPart() )
+			tAW.m_iBlendedGroup = m_iBlendedGroup;
+	}
+	
 	XQNode_t * pNode = SpawnNode ( *m_dStateSpec.Last() );
 	pNode->AddDirtyWord ( std::move(tAW) );
 	return pNode;
@@ -1043,6 +1070,18 @@ bool XQParser_t::Parse ( XQQuery_t & tParsed, const char * sQuery, const CSphQue
 	DictRefPtr_c pMyDict = GetStatelessDict ( pDict );
 
 	Setup ( pSchema, pTokenizer->Clone ( SPH_CLONE ), pMyDict, &tParsed, tSettings );
+	
+	// blend variants if blended_expand option used
+	if ( pQuery && !pQuery->m_sExpandBlended.IsEmpty() )
+	{
+		const CSphTokenizerSettings & tTokSettings = pTokenizer->GetSettings();
+		const CSphString & sBlendMode = ( pQuery->m_sExpandBlended=="1" ? tTokSettings.m_sBlendMode.cstr() : pQuery->m_sExpandBlended );
+		if ( !tTokSettings.m_sBlendMode.IsEmpty() && !m_pTokenizer->SetBlendMode ( sBlendMode.cstr(), tParsed.m_sParseError ) )
+			return false;
+
+		m_bExpandBlended = true;
+	}
+	
 	m_sQuery = (BYTE*)const_cast<char*>(sQuery);
 	m_iQueryLen = sQuery ? (int) strlen(sQuery) : 0;
 	m_iPendingNulls = 0;
@@ -1050,6 +1089,8 @@ bool XQParser_t::Parse ( XQQuery_t & tParsed, const char * sQuery, const CSphQue
 	m_pRoot = nullptr;
 	m_bEmpty = true;
 	m_iOvershortStep = tSettings.m_iOvershortStep;
+	m_iBlendedGroup = 0;
+	m_iBlendedStepDelta = 1;
 
 	m_pTokenizer->SetBuffer ( m_sQuery, m_iQueryLen );
 	int iRes = yyparse ( this );

+ 1 - 0
src/tokenizer/tokenizer.h

@@ -167,6 +167,7 @@ public:
 
 	virtual bool					TokenIsBlended () const noexcept { return m_bBlended; }
 	virtual bool					TokenIsBlendedPart () const noexcept { return m_bBlendedPart; }
+	virtual bool					TokenIsBlendedHead () const noexcept { return false; }
 	virtual int						SkipBlended () { return 0; }
 
 public:

+ 8 - 0
src/tokenizer/tokenizer_multiform.cpp

@@ -32,6 +32,7 @@ struct StoredToken_t
 	bool m_bSpecial;
 	bool m_bBlended;
 	bool m_bBlendedPart;
+	bool m_bBlendedHead;
 };
 
 void FillStoredTokenInfo ( StoredToken_t& tToken, const BYTE* sToken, const TokenizerRefPtr_c& pTokenizer )
@@ -48,6 +49,7 @@ void FillStoredTokenInfo ( StoredToken_t& tToken, const BYTE* sToken, const Toke
 	tToken.m_bSpecial = pTokenizer->WasTokenSpecial();
 	tToken.m_bBlended = pTokenizer->TokenIsBlended();
 	tToken.m_bBlendedPart = pTokenizer->TokenIsBlendedPart();
+	tToken.m_bBlendedHead = pTokenizer->TokenIsBlendedHead();
 }
 
 /// token filter for multiforms support
@@ -94,6 +96,10 @@ public:
 	{
 		return m_iStart < m_dStoredTokens.GetLength() ? m_dStoredTokens[m_iStart].m_bBlendedPart : Base::TokenIsBlendedPart();
 	}
+	bool TokenIsBlendedHead() const noexcept final
+	{
+		return m_iStart < m_dStoredTokens.GetLength() ? m_dStoredTokens[m_iStart].m_bBlendedHead : Base::TokenIsBlendedHead();
+	}
 	int SkipBlended() final;
 
 public:
@@ -156,6 +162,7 @@ BYTE* MultiformTokenizer::GetToken()
 			tStart.m_bSpecial = false;
 			tStart.m_bBlended = false;
 			tStart.m_bBlendedPart = false;
+			tStart.m_bBlendedHead = false;
 			return tStart.m_sToken;
 		}
 	}
@@ -301,6 +308,7 @@ BYTE* MultiformTokenizer::GetToken()
 			tEnd.m_bSpecial = false;
 			tEnd.m_bBlended = false;
 			tEnd.m_bBlendedPart = false;
+			tEnd.m_bBlendedHead = false;
 
 			if ( pCurForm->m_dNormalForm.GetLength() > 1 )
 			{

+ 8 - 0
src/tokenizer/tokenizerbase.cpp

@@ -198,12 +198,19 @@ void CSphTokenizerBase::SetBufferPtr ( const char* sNewPtr )
 	m_pAccum = m_sAccum;
 	m_pTokenStart = m_pTokenEnd = nullptr;
 	m_pBlendStart = m_pBlendEnd = nullptr;
+	m_bBlended = false;
+	m_bBlendedPart = false;
+	m_bBlendAdd = false;
+	m_uBlendVariantsPending = 0;
+	m_bBlendedHead = false;
 }
 
 /// adjusts blending magic when we're about to return a token (any token)
 /// returns false if current token should be skipped, true otherwise
 bool CSphTokenizerBase::BlendAdjust ( const BYTE* pCur )
 {
+	m_bBlendedHead = false;
+
 	// check if all we got is a bunch of blended characters (pure-blended case)
 	if ( m_bBlended && !m_bNonBlended )
 	{
@@ -230,6 +237,7 @@ bool CSphTokenizerBase::BlendAdjust ( const BYTE* pCur )
 		m_pBlendEnd = pCur;
 		m_pBlendStart = nullptr;
 		m_bBlendedPart = true;
+		m_bBlendedHead = true;
 	} else if ( pCur >= m_pBlendEnd )
 	{
 		// tricky bit, as at this point, token we're about to return

+ 4 - 0
src/tokenizer/tokenizerbase2.cpp

@@ -80,6 +80,10 @@ int CSphTokenizerBase2::SkipBlended()
 		iBlended++;
 
 	m_pBufferMax = pMax;
+	// reset flags
+	m_bBlendAdd = false;
+	m_uBlendVariantsPending = 0;
+
 	return iBlended;
 }
 

+ 3 - 0
src/tokenizer/tokenizerbase2_impl.h

@@ -98,7 +98,10 @@ protected:
 		{
 			BYTE* pVar = GetBlendedVariant();
 			if ( pVar )
+			{
+				m_bBlendedHead = false;
 				return pVar;
+			}
 			m_bBlendedPart = ( m_pBlendEnd != nullptr );
 		}
 

+ 2 - 0
src/tokenizer/tokenizerbase_impl.h

@@ -52,6 +52,7 @@ public:
 	{
 		return false;
 	}
+	bool TokenIsBlendedHead () const noexcept final { return m_bBlendedHead; }
 
 	bool IsQueryTok() const noexcept final
 	{
@@ -84,6 +85,7 @@ protected:
 	bool m_bHasBlend = false;
 	const BYTE* m_pBlendStart = nullptr;
 	const BYTE* m_pBlendEnd = nullptr;
+	bool m_bBlendedHead = false;
 
 	ESphTokenizerClone m_eMode { SPH_CLONE_INDEX };
 };

+ 65 - 0
test/test_482/model.bin

@@ -0,0 +1,65 @@
+a:1:{i:0;a:30:{i:0;a:2:{s:8:"sphinxql";s:83:"INSERT INTO t (id, f) VALUES (1, 'well being'), (2, 'well-being'), (3, 'wellbeing')";s:14:"total_affected";i:3;}i:1;a:3:{s:8:"sphinxql";s:57:"SELECT * FROM t WHERE MATCH('well-being') ORDER BY id ASC";s:10:"total_rows";i:1;s:4:"rows";a:1:{i:0;a:2:{s:2:"id";s:1:"2";s:1:"f";s:10:"well-being";}}}i:2;a:3:{s:8:"sphinxql";s:83:"SELECT * FROM t WHERE MATCH('well-being') ORDER BY id ASC OPTION expand_blended='1'";s:10:"total_rows";i:3;s:4:"rows";a:3:{i:0;a:2:{s:2:"id";s:1:"1";s:1:"f";s:10:"well being";}i:1;a:2:{s:2:"id";s:1:"2";s:1:"f";s:10:"well-being";}i:2;a:2:{s:2:"id";s:1:"3";s:1:"f";s:9:"wellbeing";}}}i:3;a:2:{s:8:"sphinxql";s:257:"INSERT INTO t1 (id, f) VALUES
+	(4, 'well-being test-case'),
+	(5, 'well-being test'),
+	(6, 'well-being case'),
+	(7, 'well test-case'),
+	(8, 'being test-case'),
+	(9, 'wellbeing testcase'),
+	(10, 'well-being'),
+	(11, 'test-case'),
+	(12, 'well being test case')";s:14:"total_affected";i:9;}i:4;a:3:{s:8:"sphinxql";s:94:"SELECT * FROM t1 WHERE MATCH('well-being test-case') ORDER BY id ASC OPTION expand_blended='1'";s:10:"total_rows";i:7;s:4:"rows";a:7:{i:0;a:2:{s:2:"id";s:1:"4";s:1:"f";s:20:"well-being test-case";}i:1;a:2:{s:2:"id";s:1:"5";s:1:"f";s:15:"well-being test";}i:2;a:2:{s:2:"id";s:1:"6";s:1:"f";s:15:"well-being case";}i:3;a:2:{s:2:"id";s:1:"7";s:1:"f";s:14:"well test-case";}i:4;a:2:{s:2:"id";s:1:"8";s:1:"f";s:15:"being test-case";}i:5;a:2:{s:2:"id";s:1:"9";s:1:"f";s:18:"wellbeing testcase";}i:6;a:2:{s:2:"id";s:2:"12";s:1:"f";s:20:"well being test case";}}}i:5;a:2:{s:8:"sphinxql";s:226:"INSERT INTO t2 (id, f) VALUES
+	(1, 'well test'),
+	(2, 'well- test-'),
+	(3, 'well-test'),
+	(4, 'well'),
+	(5, 'test'),
+	(6, 'well-'),
+	(7, 'test-'),
+	(8, 'well being'),
+	(9, 'test case'),
+	(10, 'well- case'),
+	(11, 'well test-')";s:14:"total_affected";i:11;}i:6;a:3:{s:8:"sphinxql";s:85:"SELECT * FROM t2 WHERE MATCH('well- test-') ORDER BY id ASC OPTION expand_blended='1'";s:10:"total_rows";i:4;s:4:"rows";a:4:{i:0;a:2:{s:2:"id";s:1:"1";s:1:"f";s:9:"well test";}i:1;a:2:{s:2:"id";s:1:"2";s:1:"f";s:11:"well- test-";}i:2;a:2:{s:2:"id";s:1:"3";s:1:"f";s:9:"well-test";}i:3;a:2:{s:2:"id";s:2:"11";s:1:"f";s:10:"well test-";}}}i:7;a:2:{s:8:"sphinxql";s:170:"INSERT INTO t1 (id, f) VALUES
+	(20, 'well-being test-case'),
+	(21, 'well being test case'),
+	(22, 'wellbeing testcase'),
+	(23, 'well-being test'),
+	(24, 'well test-case')";s:14:"total_affected";i:5;}i:8;a:3:{s:8:"sphinxql";s:96:"SELECT * FROM t1 WHERE MATCH('"well-being test-case"') ORDER BY id ASC OPTION expand_blended='1'";s:10:"total_rows";i:2;s:4:"rows";a:2:{i:0;a:2:{s:2:"id";s:1:"4";s:1:"f";s:20:"well-being test-case";}i:1;a:2:{s:2:"id";s:2:"20";s:1:"f";s:20:"well-being test-case";}}}i:9;a:2:{s:8:"sphinxql";s:202:"INSERT INTO t1 (id, f) VALUES
+	(30, 'hello well-being world'),
+	(31, 'hello well being world'),
+	(32, 'hello wellbeing world'),
+	(33, 'hello well-being'),
+	(34, 'hello world'),
+	(35, 'well-being world')";s:14:"total_affected";i:6;}i:10;a:3:{s:8:"sphinxql";s:96:"SELECT * FROM t1 WHERE MATCH('hello well-being world') ORDER BY id ASC OPTION expand_blended='1'";s:10:"total_rows";i:3;s:4:"rows";a:3:{i:0;a:2:{s:2:"id";s:2:"30";s:1:"f";s:22:"hello well-being world";}i:1;a:2:{s:2:"id";s:2:"31";s:1:"f";s:22:"hello well being world";}i:2;a:2:{s:2:"id";s:2:"32";s:1:"f";s:21:"hello wellbeing world";}}}i:11;a:2:{s:8:"sphinxql";s:141:"INSERT INTO t (id, f) VALUES
+	(10, 'well-being'),
+	(11, 'well-being test'),
+	(12, 'test well-being'),
+	(13, 'wellbeing'),
+	(14, 'well being')";s:14:"total_affected";i:5;}i:12;a:3:{s:8:"sphinxql";s:85:"SELECT * FROM t WHERE MATCH('^well-being$') ORDER BY id ASC OPTION expand_blended='1'";s:10:"total_rows";i:8;s:4:"rows";a:8:{i:0;a:2:{s:2:"id";s:1:"1";s:1:"f";s:10:"well being";}i:1;a:2:{s:2:"id";s:1:"2";s:1:"f";s:10:"well-being";}i:2;a:2:{s:2:"id";s:1:"3";s:1:"f";s:9:"wellbeing";}i:3;a:2:{s:2:"id";s:2:"10";s:1:"f";s:10:"well-being";}i:4;a:2:{s:2:"id";s:2:"11";s:1:"f";s:15:"well-being test";}i:5;a:2:{s:2:"id";s:2:"12";s:1:"f";s:15:"test well-being";}i:6;a:2:{s:2:"id";s:2:"13";s:1:"f";s:9:"wellbeing";}i:7;a:2:{s:2:"id";s:2:"14";s:1:"f";s:10:"well being";}}}i:13;a:2:{s:8:"sphinxql";s:235:"INSERT INTO t1 (id, f) VALUES
+	(40, 'well-being test-case multi-part'),
+	(41, 'well being test case multi part'),
+	(42, 'wellbeing testcase multipart'),
+	(43, 'well-being test-case'),
+	(44, 'multi-part'),
+	(45, 'well-being multi-part')";s:14:"total_affected";i:6;}i:14;a:3:{s:8:"sphinxql";s:105:"SELECT * FROM t1 WHERE MATCH('well-being test-case multi-part') ORDER BY id ASC OPTION expand_blended='1'";s:10:"total_rows";i:3;s:4:"rows";a:3:{i:0;a:2:{s:2:"id";s:2:"40";s:1:"f";s:31:"well-being test-case multi-part";}i:1;a:2:{s:2:"id";s:2:"41";s:1:"f";s:31:"well being test case multi part";}i:2;a:2:{s:2:"id";s:2:"42";s:1:"f";s:28:"wellbeing testcase multipart";}}}i:15;a:2:{s:8:"sphinxql";s:138:"INSERT INTO t1 (id, f) VALUES
+	(70, 'a well-being b'),
+	(71, 'a well being b'),
+	(72, 'a well-being'),
+	(73, 'well-being b'),
+	(74, 'a b')";s:14:"total_affected";i:5;}i:16;a:3:{s:8:"sphinxql";s:62:"SELECT * FROM t1 WHERE MATCH('a well-being b') ORDER BY id ASC";s:10:"total_rows";i:1;s:4:"rows";a:1:{i:0;a:2:{s:2:"id";s:2:"70";s:1:"f";s:14:"a well-being b";}}}i:17;a:3:{s:8:"sphinxql";s:88:"SELECT * FROM t1 WHERE MATCH('a well-being b') ORDER BY id ASC OPTION expand_blended='1'";s:10:"total_rows";i:2;s:4:"rows";a:2:{i:0;a:2:{s:2:"id";s:2:"70";s:1:"f";s:14:"a well-being b";}i:1;a:2:{s:2:"id";s:2:"71";s:1:"f";s:14:"a well being b";}}}i:18;a:2:{s:8:"sphinxql";s:15:"set profiling=1";s:14:"total_affected";i:0;}i:19;a:3:{s:8:"sphinxql";s:62:"SELECT * FROM t1 WHERE MATCH('a well-being b') ORDER BY id ASC";s:10:"total_rows";i:1;s:4:"rows";a:1:{i:0;a:2:{s:2:"id";s:2:"70";s:1:"f";s:14:"a well-being b";}}}i:20;a:3:{s:8:"sphinxql";s:9:"show plan";s:10:"total_rows";i:1;s:4:"rows";a:1:{i:0;a:2:{s:8:"Variable";s:16:"transformed_tree";s:5:"Value";s:108:"AND(
+  AND(KEYWORD(a, querypos=1)), 
+  AND(KEYWORD(well-being, querypos=2)), 
+  AND(KEYWORD(b, querypos=4)))";}}}i:21;a:3:{s:8:"sphinxql";s:88:"SELECT * FROM t1 WHERE MATCH('a well-being b') ORDER BY id ASC OPTION expand_blended='1'";s:10:"total_rows";i:2;s:4:"rows";a:2:{i:0;a:2:{s:2:"id";s:2:"70";s:1:"f";s:14:"a well-being b";}i:1;a:2:{s:2:"id";s:2:"71";s:1:"f";s:14:"a well being b";}}}i:22;a:3:{s:8:"sphinxql";s:9:"show plan";s:10:"total_rows";i:1;s:4:"rows";a:1:{i:0;a:2:{s:8:"Variable";s:16:"transformed_tree";s:5:"Value";s:234:"AND(
+  AND(KEYWORD(a, querypos=1)), 
+  OR(
+    AND(KEYWORD(well-being, querypos=2)), 
+    AND(KEYWORD(wellbeing, querypos=2)), 
+    AND(KEYWORD(well, querypos=2)), 
+    AND(KEYWORD(being, querypos=3))), 
+  AND(KEYWORD(b, querypos=4)))";}}}i:23;a:3:{s:8:"sphinxql";s:83:"SELECT * FROM t WHERE MATCH('well-being') ORDER BY id ASC OPTION expand_blended='1'";s:10:"total_rows";i:8;s:4:"rows";a:8:{i:0;a:2:{s:2:"id";s:1:"1";s:1:"f";s:10:"well being";}i:1;a:2:{s:2:"id";s:1:"2";s:1:"f";s:10:"well-being";}i:2;a:2:{s:2:"id";s:1:"3";s:1:"f";s:9:"wellbeing";}i:3;a:2:{s:2:"id";s:2:"10";s:1:"f";s:10:"well-being";}i:4;a:2:{s:2:"id";s:2:"11";s:1:"f";s:15:"well-being test";}i:5;a:2:{s:2:"id";s:2:"12";s:1:"f";s:15:"test well-being";}i:6;a:2:{s:2:"id";s:2:"13";s:1:"f";s:9:"wellbeing";}i:7;a:2:{s:2:"id";s:2:"14";s:1:"f";s:10:"well being";}}}i:24;a:3:{s:8:"sphinxql";s:9:"show plan";s:10:"total_rows";i:1;s:4:"rows";a:1:{i:0;a:2:{s:8:"Variable";s:16:"transformed_tree";s:5:"Value";s:154:"OR(
+  AND(KEYWORD(well-being, querypos=1)), 
+  AND(KEYWORD(wellbeing, querypos=1)), 
+  AND(KEYWORD(well, querypos=1)), 
+  AND(KEYWORD(being, querypos=2)))";}}}i:25;a:3:{s:8:"sphinxql";s:91:"SELECT * FROM t WHERE MATCH('well-being') ORDER BY id ASC OPTION expand_blended='trim_none'";s:10:"total_rows";i:6;s:4:"rows";a:6:{i:0;a:2:{s:2:"id";s:1:"1";s:1:"f";s:10:"well being";}i:1;a:2:{s:2:"id";s:1:"2";s:1:"f";s:10:"well-being";}i:2;a:2:{s:2:"id";s:2:"10";s:1:"f";s:10:"well-being";}i:3;a:2:{s:2:"id";s:2:"11";s:1:"f";s:15:"well-being test";}i:4;a:2:{s:2:"id";s:2:"12";s:1:"f";s:15:"test well-being";}i:5;a:2:{s:2:"id";s:2:"14";s:1:"f";s:10:"well being";}}}i:26;a:3:{s:8:"sphinxql";s:9:"show plan";s:10:"total_rows";i:1;s:4:"rows";a:1:{i:0;a:2:{s:8:"Variable";s:16:"transformed_tree";s:5:"Value";s:114:"OR(
+  AND(KEYWORD(well-being, querypos=1)), 
+  AND(KEYWORD(well, querypos=1)), 
+  AND(KEYWORD(being, querypos=2)))";}}}i:27;a:3:{s:8:"sphinxql";s:95:"SELECT * FROM dist1 WHERE MATCH('well-being') ORDER BY id ASC OPTION expand_blended='trim_none'";s:10:"total_rows";i:6;s:4:"rows";a:6:{i:0;a:2:{s:2:"id";s:1:"1";s:1:"f";s:10:"well being";}i:1;a:2:{s:2:"id";s:1:"2";s:1:"f";s:10:"well-being";}i:2;a:2:{s:2:"id";s:2:"10";s:1:"f";s:10:"well-being";}i:3;a:2:{s:2:"id";s:2:"11";s:1:"f";s:15:"well-being test";}i:4;a:2:{s:2:"id";s:2:"12";s:1:"f";s:15:"test well-being";}i:5;a:2:{s:2:"id";s:2:"14";s:1:"f";s:10:"well being";}}}i:28;a:3:{s:8:"sphinxql";s:83:"SELECT * FROM t WHERE MATCH('well-being') ORDER BY id ASC OPTION expand_blended='0'";s:10:"total_rows";i:4;s:4:"rows";a:4:{i:0;a:2:{s:2:"id";s:1:"2";s:1:"f";s:10:"well-being";}i:1;a:2:{s:2:"id";s:2:"10";s:1:"f";s:10:"well-being";}i:2;a:2:{s:2:"id";s:2:"11";s:1:"f";s:15:"well-being test";}i:3;a:2:{s:2:"id";s:2:"12";s:1:"f";s:15:"test well-being";}}}i:29;a:3:{s:8:"sphinxql";s:9:"show plan";s:10:"total_rows";i:1;s:4:"rows";a:1:{i:0;a:2:{s:8:"Variable";s:16:"transformed_tree";s:5:"Value";s:36:"AND(KEYWORD(well-being, querypos=1))";}}}}}

+ 186 - 0
test/test_482/test.xml

@@ -0,0 +1,186 @@
+<?xml version="1.0" encoding="utf-8"?>
+<test>
+
+<name>blended token expansion during search</name>
+
+<requires>
+<force-rt/>
+</requires>
+
+<skip_indexer/>
+
+<config>
+searchd
+{
+	<searchd_settings/>
+}
+
+index t
+{
+	type			= rt
+	path			= <data_path/>/t
+	rt_field		= f
+	blend_chars		= -
+	blend_mode		= trim_all, trim_none
+}
+
+index t1
+{
+	type			= rt
+	path			= <data_path/>/t1
+	rt_field		= f
+	blend_chars		= -
+	blend_mode		= trim_all, trim_none
+}
+
+index t2
+{
+	type			= rt
+	path			= <data_path/>/t2
+	rt_field		= f
+	blend_chars		= -
+	blend_mode		= trim_all
+}
+
+index dist1
+{
+    type = distributed
+    agent = <my_address/>:t
+}
+</config>
+
+<queries>
+<sphinxql>
+	INSERT INTO t (id, f) VALUES (1, 'well being'), (2, 'well-being'), (3, 'wellbeing');
+    
+	SELECT * FROM t WHERE MATCH('well-being') ORDER BY id ASC;
+	SELECT * FROM t WHERE MATCH('well-being') ORDER BY id ASC OPTION expand_blended='1';
+</sphinxql>
+
+<sphinxql>
+	<!-- two blended token -->
+	INSERT INTO t1 (id, f) VALUES
+	(4, 'well-being test-case'),
+	(5, 'well-being test'),
+	(6, 'well-being case'),
+	(7, 'well test-case'),
+	(8, 'being test-case'),
+	(9, 'wellbeing testcase'),
+	(10, 'well-being'),
+	(11, 'test-case'),
+	(12, 'well being test case');
+</sphinxql>
+
+<sphinxql>
+	<!-- Should create: OR(well-being, well, being, wellbeing) AND OR(test-case, test, case, testcase) -->
+	<!-- Should match documents 4, 5, 6, 7, 8, 9, 12 (both tokens present) -->
+	<!-- Should NOT match documents 10, 11 (only one token present) -->
+	SELECT * FROM t1 WHERE MATCH('well-being test-case') ORDER BY id ASC OPTION expand_blended='1';
+</sphinxql>
+
+<sphinxql>
+	INSERT INTO t2 (id, f) VALUES
+	(1, 'well test'),
+	(2, 'well- test-'),
+	(3, 'well-test'),
+	(4, 'well'),
+	(5, 'test'),
+	(6, 'well-'),
+	(7, 'test-'),
+	(8, 'well being'),
+	(9, 'test case'),
+	(10, 'well- case'),
+	(11, 'well test-');
+</sphinxql>
+
+<sphinxql>
+	<!-- Should match documents 1, 2, 3, 11 (both "well" and "test" present) -->
+	<!-- Should NOT match documents 4, 5, 6, 7, 8, 9, 10 (only one token present) -->
+	SELECT * FROM t2 WHERE MATCH('well- test-') ORDER BY id ASC OPTION expand_blended='1';
+	</sphinxql>
+
+<sphinxql>
+	<!-- phrase mode - blended tokens should NOT expand inside phrases -->
+	INSERT INTO t1 (id, f) VALUES
+	(20, 'well-being test-case'),
+	(21, 'well being test case'),
+	(22, 'wellbeing testcase'),
+	(23, 'well-being test'),
+	(24, 'well test-case');
+	
+	SELECT * FROM t1 WHERE MATCH('"well-being test-case"') ORDER BY id ASC OPTION expand_blended='1';
+</sphinxql>
+
+<sphinxql>
+	INSERT INTO t1 (id, f) VALUES
+	(30, 'hello well-being world'),
+	(31, 'hello well being world'),
+	(32, 'hello wellbeing world'),
+	(33, 'hello well-being'),
+	(34, 'hello world'),
+	(35, 'well-being world');
+	
+	SELECT * FROM t1 WHERE MATCH('hello well-being world') ORDER BY id ASC OPTION expand_blended='1';
+</sphinxql>
+
+<sphinxql>
+	<!-- field modifiers with blended tokens -->
+	INSERT INTO t (id, f) VALUES
+	(10, 'well-being'),
+	(11, 'well-being test'),
+	(12, 'test well-being'),
+	(13, 'wellbeing'),
+	(14, 'well being');
+	
+	SELECT * FROM t WHERE MATCH('^well-being$') ORDER BY id ASC OPTION expand_blended='1';
+</sphinxql>
+
+<sphinxql>
+	<!-- three blended tokens -->
+	INSERT INTO t1 (id, f) VALUES
+	(40, 'well-being test-case multi-part'),
+	(41, 'well being test case multi part'),
+	(42, 'wellbeing testcase multipart'),
+	(43, 'well-being test-case'),
+	(44, 'multi-part'),
+	(45, 'well-being multi-part');
+	
+	SELECT * FROM t1 WHERE MATCH('well-being test-case multi-part') ORDER BY id ASC OPTION expand_blended='1';
+</sphinxql>
+
+<sphinxql>
+	<!-- position consistency -->
+	INSERT INTO t1 (id, f) VALUES
+	(70, 'a well-being b'),
+	(71, 'a well being b'),
+	(72, 'a well-being'),
+	(73, 'well-being b'),
+	(74, 'a b');
+	
+	<!-- First verify matching behavior -->
+	SELECT * FROM t1 WHERE MATCH('a well-being b') ORDER BY id ASC;
+	SELECT * FROM t1 WHERE MATCH('a well-being b') ORDER BY id ASC OPTION expand_blended='1';
+    
+	set profiling=1;
+	SELECT * FROM t1 WHERE MATCH('a well-being b') ORDER BY id ASC;
+	show plan;
+	SELECT * FROM t1 WHERE MATCH('a well-being b') ORDER BY id ASC OPTION expand_blended='1';
+	show plan;
+</sphinxql>
+
+<sphinxql>
+<!-- expand set in options -->
+SELECT * FROM t WHERE MATCH('well-being') ORDER BY id ASC OPTION expand_blended='1';
+show plan;
+SELECT * FROM t WHERE MATCH('well-being') ORDER BY id ASC OPTION expand_blended='trim_none';
+show plan;
+
+<!-- expand to distirbuted agent -->
+SELECT * FROM dist1 WHERE MATCH('well-being') ORDER BY id ASC OPTION expand_blended='trim_none';
+
+SELECT * FROM t WHERE MATCH('well-being') ORDER BY id ASC OPTION expand_blended='0';
+show plan;
+</sphinxql>
+
+</queries>
+</test>

Some files were not shown because too many files changed in this diff