查询结果可以按全文排名权重、一个或多个属性或表达式进行排序。
全文 查询默认返回排序的匹配项。如果未指定任何内容,则按相关性排序,这相当于 SQL 格式中的 ORDER BY weight() DESC。
非全文 查询默认不执行任何排序。
当您通过添加 SQL 格式的 ORDER BY 子句或通过 HTTP JSON 使用 sort 选项显式提供排序规则时,自动启用扩展模式。
通用语法:
SELECT ... ORDER BY
{attribute_name | expr_alias | weight() | random() } [ASC | DESC],
...
{attribute_name | expr_alias | weight() | random() } [ASC | DESC]
在排序子句中,您可以使用最多 5 列的任意组合,每列后跟 asc 或 desc。函数和表达式不允许作为排序子句的参数,除了 weight() 和 random() 函数(后者只能通过 SQL 以 ORDER BY random() 形式使用)。但是,您可以在 SELECT 列表中使用任何表达式,并按其别名排序。
- SQL
select *, a + b alias from test order by alias desc;+------+------+------+----------+-------+
| id | a | b | f | alias |
+------+------+------+----------+-------+
| 1 | 2 | 3 | document | 5 |
+------+------+------+----------+-------+"sort" 指定一个数组,其中每个元素可以是属性名,或者如果您想按匹配权重排序则为 _score,如果想要随机匹配顺序则为 _random。在这种情况下,属性的排序顺序默认为升序,_score 默认为降序。
- JSON
- PHP
- Python
- Python-asyncio
- javascript
- Java
- C#
- Rust
- typescript
- go
{
"table":"test",
"query":
{
"match": { "title": "Test document" }
},
"sort": [ "_score", "id" ],
"_source": "title",
"limit": 3
}$search->setIndex("test")->match('Test document')->sort('_score')->sort('id');search_request.index = 'test'
search_request.fulltext_filter = manticoresearch.model.QueryFilter('Test document')
search_request.sort = ['_score', 'id']search_request.index = 'test'
search_request.fulltext_filter = manticoresearch.model.QueryFilter('Test document')
search_request.sort = ['_score', 'id']searchRequest.index = "test";
searchRequest.fulltext_filter = new Manticoresearch.QueryFilter('Test document');
searchRequest.sort = ['_score', 'id'];searchRequest.setIndex("test");
QueryFilter queryFilter = new QueryFilter();
queryFilter.setQueryString("Test document");
searchRequest.setFulltextFilter(queryFilter);
List<Object> sort = new ArrayList<Object>( Arrays.asList("_score", "id") );
searchRequest.setSort(sort);var searchRequest = new SearchRequest("test");
searchRequest.FulltextFilter = new QueryFilter("Test document");
searchRequest.Sort = new List<Object> {"_score", "id"};let query = SearchQuery {
query_string: Some(serde_json::json!("Test document").into()),
..Default::default(),
};
let sort: [String; 2] = ["_score".to_string(), "id".to_string()];
let search_req = SearchRequest {
table: "test".to_string(),
query: Some(Box::new(query)),
sort: Some(serde_json::json!(sort)),
..Default::default(),
};searchRequest = {
index: 'test',
query: {
query_string: {'Test document'},
},
sort: ['_score', 'id'],
}searchRequest.SetIndex("test")
query := map[string]interface{} {"query_string": "Test document"}
searchRequest.SetQuery(query)
sort := map[string]interface{} {"_score": "asc", "id": "asc"}
searchRequest.SetSort(sort) {
"took": 0,
"timed_out": false,
"hits": {
"total": 5,
"total_relation": "eq",
"hits": [
{
"_id": 5406864699109146628,
"_score": 2319,
"_source": {
"title": "Test document 1"
}
},
{
"_id": 5406864699109146629,
"_score": 2319,
"_source": {
"title": "Test document 2"
}
},
{
"_id": 5406864699109146630,
"_score": 2319,
"_source": {
"title": "Test document 3"
}
}
]
}
}您也可以显式指定排序顺序:
asc:升序排序desc:降序排序
- JSON
- PHP
- Python
- Python-asyncio
- javascript
- Java
- C#
- Rust
- typescript
- go
{
"table":"test",
"query":
{
"match": { "title": "Test document" }
},
"sort":
[
{ "id": "desc" },
"_score"
],
"_source": "title",
"limit": 3
}$search->setIndex("test")->match('Test document')->sort('id', 'desc')->sort('_score');search_request.index = 'test'
search_request.fulltext_filter = manticoresearch.model.QueryFilter('Test document')
sort_by_id = manticoresearch.model.SortOrder('id', 'desc')
search_request.sort = [sort_by_id, '_score']search_request.index = 'test'
search_request.fulltext_filter = manticoresearch.model.QueryFilter('Test document')
sort_by_id = manticoresearch.model.SortOrder('id', 'desc')
search_request.sort = [sort_by_id, '_score']searchRequest.index = "test";
searchRequest.fulltext_filter = new Manticoresearch.QueryFilter('Test document');
sortById = new Manticoresearch.SortOrder('id', 'desc');
searchRequest.sort = [sortById, 'id'];searchRequest.setIndex("test");
QueryFilter queryFilter = new QueryFilter();
queryFilter.setQueryString("Test document");
searchRequest.setFulltextFilter(queryFilter);
List<Object> sort = new ArrayList<Object>();
SortOrder sortById = new SortOrder();
sortById.setAttr("id");
sortById.setOrder(SortOrder.OrderEnum.DESC);
sort.add(sortById);
sort.add("_score");
searchRequest.setSort(sort);var searchRequest = new SearchRequest("test");
searchRequest.FulltextFilter = new QueryFilter("Test document");
searchRequest.Sort = new List<Object>();
var sortById = new SortOrder("id", SortOrder.OrderEnum.Desc);
searchRequest.Sort.Add(sortById);
searchRequest.Sort.Add("_score");let query = SearchQuery {
query_string: Some(serde_json::json!("Test document").into()),
..Default::default(),
};
let sort_by_id = HashMap::new();
sort_by_id.insert("id".to_string(), "desc".to_string());
let mut sort = Vec::new();
sort.push(sort_by_id);
sort.push("_score".to_string());
let search_req = SearchRequest {
table: "test".to_string(),
query: Some(Box::new(query)),
sort: Some(serde_json::json!(sort)),
..Default::default(),
};searchRequest = {
index: 'test',
query: {
query_string: {'Test document'},
},
sort: [{'id': 'desc'}, '_score'],
}searchRequest.SetIndex("test")
query := map[string]interface{} {"query_string": "Test document"}
searchRequest.SetQuery(query)
sortById := map[string]interface{} {"id": "desc"}
sort := map[string]interface{} {"id": "desc", "_score": "asc"}
searchRequest.SetSort(sort) {
"took": 0,
"timed_out": false,
"hits": {
"total": 5,
"total_relation": "eq",
"hits": [
{
"_id": 5406864699109146632,
"_score": 2319,
"_source": {
"title": "Test document 5"
}
},
{
"_id": 5406864699109146631,
"_score": 2319,
"_source": {
"title": "Test document 4"
}
},
{
"_id": 5406864699109146630,
"_score": 2319,
"_source": {
"title": "Test document 3"
}
}
]
}
}您还可以使用另一种语法,通过 order 属性指定排序顺序:
- JSON
- PHP
- Python
- Python-asyncio
- javascript
- Java
- C#
- Rust
- typescript
- go
{
"table":"test",
"query":
{
"match": { "title": "Test document" }
},
"sort":
[
{ "id": { "order":"desc" } }
],
"_source": "title",
"limit": 3
}$search->setIndex("test")->match('Test document')->sort('id', 'desc');search_request.index = 'test'
search_request.fulltext_filter = manticoresearch.model.QueryFilter('Test document')
sort_by_id = manticoresearch.model.SortOrder('id', 'desc')
search_request.sort = [sort_by_id]search_request.index = 'test'
search_request.fulltext_filter = manticoresearch.model.QueryFilter('Test document')
sort_by_id = manticoresearch.model.SortOrder('id', 'desc')
search_request.sort = [sort_by_id]searchRequest.index = "test";
searchRequest.fulltext_filter = new Manticoresearch.QueryFilter('Test document');
sortById = new Manticoresearch.SortOrder('id', 'desc');
searchRequest.sort = [sortById];searchRequest.setIndex("test");
QueryFilter queryFilter = new QueryFilter();
queryFilter.setQueryString("Test document");
searchRequest.setFulltextFilter(queryFilter);
List<Object> sort = new ArrayList<Object>();
SortOrder sortById = new SortOrder();
sortById.setAttr("id");
sortById.setOrder(SortOrder.OrderEnum.DESC);
sort.add(sortById);
searchRequest.setSort(sort);var searchRequest = new SearchRequest("test");
searchRequest.FulltextFilter = new QueryFilter("Test document");
searchRequest.Sort = new List<Object>();
var sortById = new SortOrder("id", SortOrder.OrderEnum.Desc);
searchRequest.Sort.Add(sortById);let query = SearchQuery {
query_string: Some(serde_json::json!("Test document").into()),
..Default::default(),
};
let mut sort_by_id = HashMap::new();
sort_by_id.insert("id".to_string(), "desc".to_string());
let sort = [HashMap; 1] = [sort_by_id];
let search_req = SearchRequest {
table: "test".to_string(),
query: Some(Box::new(query)),
sort: Some(serde_json::json!(sort)),
..Default::default(),
};searchRequest = {
index: 'test',
query: {
query_string: {'Test document'},
},
sort: { {'id': {'order':'desc'} },
}searchRequest.SetIndex("test")
query := map[string]interface{} {"query_string": "Test document"}
searchRequest.SetQuery(query)
sort := map[string]interface{} { "id": {"order":"desc"} }
searchRequest.SetSort(sort) {
"took": 0,
"timed_out": false,
"hits": {
"total": 5,
"total_relation": "eq",
"hits": [
{
"_id": 5406864699109146632,
"_score": 2319,
"_source": {
"title": "Test document 5"
}
},
{
"_id": 5406864699109146631,
"_score": 2319,
"_source": {
"title": "Test document 4"
}
},
{
"_id": 5406864699109146630,
"_score": 2319,
"_source": {
"title": "Test document 3"
}
}
]
}
}JSON 查询中也支持按 MVA 属性排序。排序模式可以通过 mode 属性设置。支持以下模式:
min:按最小值排序max:按最大值排序
- JSON
- PHP
- Python
- Python-asyncio
- javascript
- Java
- C#
- Rust
- typescript
- go
{
"table":"test",
"query":
{
"match": { "title": "Test document" }
},
"sort":
[
{ "attr_mva": { "order":"desc", "mode":"max" } }
],
"_source": "title",
"limit": 3
}$search->setIndex("test")->match('Test document')->sort('id','desc','max');search_request.index = 'test'
search_request.fulltext_filter = manticoresearch.model.QueryFilter('Test document')
sort = manticoresearch.model.SortMVA('attr_mva', 'desc', 'max')
search_request.sort = [sort]search_request.index = 'test'
search_request.fulltext_filter = manticoresearch.model.QueryFilter('Test document')
sort = manticoresearch.model.SortMVA('attr_mva', 'desc', 'max')
search_request.sort = [sort]searchRequest.index = "test";
searchRequest.fulltext_filter = new Manticoresearch.QueryFilter('Test document');
sort = new Manticoresearch.SortMVA('attr_mva', 'desc', 'max');
searchRequest.sort = [sort];searchRequest.setIndex("test");
QueryFilter queryFilter = new QueryFilter();
queryFilter.setQueryString("Test document");
searchRequest.setFulltextFilter(queryFilter);
SortMVA sort = new SortMVA();
sort.setAttr("attr_mva");
sort.setOrder(SortMVA.OrderEnum.DESC);
sort.setMode(SortMVA.ModeEnum.MAX);
searchRequest.setSort(sort);var searchRequest = new SearchRequest("test");
searchRequest.FulltextFilter = new QueryFilter("Test document");
var sort = new SortMVA("attr_mva", SortMVA.OrderEnum.Desc, SortMVA.ModeEnum.Max);
searchRequest.Sort.Add(sort);let query = SearchQuery {
query_string: Some(serde_json::json!("Test document").into()),
..Default::default(),
};
let mut sort_mva_opts = HashMap::new();
sort_mva_opts.insert("order".to_string(), "desc".to_string());
sort_mva_opts.insert("mode".to_string(), "max".to_string());
let mut sort_mva = HashMap::new();
sort_mva.insert("attr_mva".to_string(), sort_mva_opts);
let search_req = SearchRequest {
table: "test".to_string(),
query: Some(Box::new(query)),
sort: Some(serde_json::json!(sort_mva)),
..Default::default(),
};searchRequest = {
index: 'test',
query: {
query_string: {'Test document'},
},
sort: { "attr_mva": { "order":"desc", "mode":"max" } },
}searchRequest.SetIndex("test")
query := map[string]interface{} {"query_string": "Test document"}
searchRequest.SetQuery(query)
sort := map[string]interface{} { "attr_mva": { "order":"desc", "mode":"max" } }
searchRequest.SetSort(sort) {
"took": 0,
"timed_out": false,
"hits": {
"total": 5,
"total_relation": "eq",
"hits": [
{
"_id": 5406864699109146631,
"_score": 2319,
"_source": {
"title": "Test document 4"
}
},
{
"_id": 5406864699109146629,
"_score": 2319,
"_source": {
"title": "Test document 2"
}
},
{
"_id": 5406864699109146628,
"_score": 2319,
"_source": {
"title": "Test document 1"
}
}
]
}
}当按属性排序时,默认禁用匹配权重(分数)计算(不使用排名器)。您可以通过将 track_scores 属性设置为 true 来启用权重计算:
- JSON
- PHP
- Python
- Python-asyncio
- javascript
- Java
- C#
- Rust
- typescript
- go
{
"table":"test",
"track_scores": true,
"query":
{
"match": { "title": "Test document" }
},
"sort":
[
{ "attr_mva": { "order":"desc", "mode":"max" } }
],
"_source": "title",
"limit": 3
}$search->setIndex("test")->match('Test document')->sort('id','desc','max')->trackScores(true);search_request.index = 'test'
search_request.track_scores = true
search_request.fulltext_filter = manticoresearch.model.QueryFilter('Test document')
sort = manticoresearch.model.SortMVA('attr_mva', 'desc', 'max')
search_request.sort = [sort]search_request.index = 'test'
search_request.track_scores = true
search_request.fulltext_filter = manticoresearch.model.QueryFilter('Test document')
sort = manticoresearch.model.SortMVA('attr_mva', 'desc', 'max')
search_request.sort = [sort]searchRequest.index = "test";
searchRequest.trackScores = true;
searchRequest.fulltext_filter = new Manticoresearch.QueryFilter('Test document');
sort = new Manticoresearch.SortMVA('attr_mva', 'desc', 'max');
searchRequest.sort = [sort];searchRequest.setIndex("test");
searchRequest.setTrackScores(true);
QueryFilter queryFilter = new QueryFilter();
queryFilter.setQueryString("Test document");
searchRequest.setFulltextFilter(queryFilter);
SortMVA sort = new SortMVA();
sort.setAttr("attr_mva");
sort.setOrder(SortMVA.OrderEnum.DESC);
sort.setMode(SortMVA.ModeEnum.MAX);
searchRequest.setSort(sort);var searchRequest = new SearchRequest("test");
searchRequest.SetTrackScores(true);
searchRequest.FulltextFilter = new QueryFilter("Test document");
var sort = new SortMVA("attr_mva", SortMVA.OrderEnum.Desc, SortMVA.ModeEnum.Max);
searchRequest.Sort.Add(sort);let query = SearchQuery {
query_string: Some(serde_json::json!("Test document").into()),
..Default::default(),
};
let mut sort_mva_opts = HashMap::new();
sort_mva_opts.insert("order".to_string(), "desc".to_string());
sort_mva_opts.insert("mode".to_string(), "max".to_string());
let mut sort_mva = HashMap::new();
sort_mva.insert("attr_mva".to_string(), sort_mva_opts);
let search_req = SearchRequest {
table: "test".to_string(),
query: Some(Box::new(query)),
sort: Some(serde_json::json!(sort_mva)),
track_scores: Some(serde_json::json!(true)),
..Default::default(),
};searchRequest = {
index: 'test',
track_scores: true,
query: {
query_string: {'Test document'},
},
sort: { "attr_mva": { "order":"desc", "mode":"max" } },
}searchRequest.SetIndex("test")
searchRequest.SetTrackScores(true)
query := map[string]interface{} {"query_string": "Test document"}
searchRequest.SetQuery(query)
sort := map[string]interface{} { "attr_mva": { "order":"desc", "mode":"max" } }
searchRequest.SetSort(sort) {
"took": 0,
"timed_out": false,
"hits": {
"total": 5,
"total_relation": "eq",
"hits": [
{
"_id": 5406864699109146631,
"_score": 2319,
"_source": {
"title": "Test document 4"
}
},
{
"_id": 5406864699109146629,
"_score": 2319,
"_source": {
"title": "Test document 2"
}
},
{
"_id": 5406864699109146628,
"_score": 2319,
"_source": {
"title": "Test document 1"
}
}
]
}
}排名(也称为加权)搜索结果可以定义为一个过程,即针对每个匹配的文档计算所谓的相关性(权重),以对应匹配它的查询。因此,相关性最终只是附加到每个文档上的一个数字,用于估计该文档与查询的相关程度。然后可以基于该数字和/或一些附加参数对搜索结果进行排序,使得最受关注的结果出现在结果页的更高位置。
没有一种适用于所有场景的单一标准排名方法。更重要的是,永远不可能有这样的方式,因为相关性是主观的。也就是说,对你来说相关的内容,对我来说可能不相关。因此,在一般情况下,相关性不仅难以计算;从理论上讲是不可能的。
所以 Manticore 中的排名是可配置的。它有一个所谓的ranker(排名器)的概念。排名器可以形式化定义为一个函数,输入为文档和查询,输出为相关性值。通俗地说,排名器控制 Manticore 使用哪种具体算法来为文档分配权重。
Manticore 附带了几个适用于不同用途的内置排名器。它们中的许多使用两个因素:短语接近度(也称为 LCS)和 BM25。短语接近度基于关键词位置,而 BM25 基于关键词频率。基本上,文档正文与查询之间的短语匹配程度越好,短语接近度越高(当文档包含整个查询的逐字引用时达到最大值)。而 BM25 在文档包含更多稀有词时更高。详细讨论留待后面。
当前实现的排名器有:
proximity_bm25,默认排名模式,结合使用短语接近度和 BM25 排名。bm25,统计排名模式,仅使用 BM25 排名(类似大多数其他全文引擎)。此模式更快,但对于包含多个关键词的查询可能导致质量较差。none,无排名模式。此模式显然是最快的。所有匹配赋予权重 1。有时称为布尔搜索,只匹配文档但不排名。wordcount,按关键词出现次数排名。该排名器计算每个字段的关键词出现次数,然后乘以字段权重,最后求和。proximity返回原始短语接近度值。此模式内部用于模拟SPH_MATCH_ALL查询。matchany返回之前在SPH_MATCH_ANY模式下计算的排名,内部用于模拟SPH_MATCH_ANY查询。fieldmask返回一个 32 位掩码,第 N 位对应第 N 个全文字段(从 0 开始编号)。只有当相应字段有满足查询的关键词出现时,该位才会被设置。sph04通常基于默认的proximity_bm25排名器,但额外提升匹配出现在文本字段开头或结尾的权重。因此,如果字段等于精确查询,sph04应该将其排名高于包含精确查询但不完全相等的字段。(例如,当查询为“Hyde Park”时,标题为“Hyde Park”的文档应排名高于标题为“Hyde Park, London”或“The Hyde Park Cafe”的文档。)expr允许你在运行时指定排名公式。它暴露了几个内部文本因素,并允许你定义如何从这些因素计算最终权重。你可以在下面的小节中找到其语法和可用因素的参考详情。
排名器名称不区分大小写。示例:
SELECT ... OPTION ranker=sph04;
| 名称 | 级别 | 类型 | 概要 |
|---|---|---|---|
| max_lcs | query | int | 当前查询的最大可能 LCS 值 |
| bm25 | document | int | BM25(1.2, 0) 的快速估计 |
| bm25a(k1, b) | document | int | 具有可配置 K1、B 常数和语法支持的精确 BM25() 值 |
| bm25f(k1, b, {field=weight, ...}) | document | int | 具有额外可配置字段权重的精确 BM25F() 值 |
| field_mask | document | int | 匹配字段的位掩码 |
| query_word_count | document | int | 查询中唯一包含的关键词数量 |
| doc_word_count | document | int | 文档中匹配的唯一关键词数量 |
| lcs | field | int | 查询与文档之间的最长公共子序列(以词为单位) |
| user_weight | field | int | 用户字段权重 |
| hit_count | field | int | 关键词出现的总次数 |
| word_count | field | int | 匹配的唯一关键词数量 |
| tf_idf | 字段 | 浮点型 | 匹配关键词的 tf*idf 之和 == 出现次数的 idf 之和 |
| min_hit_pos | 字段 | 整型 | 第一个匹配出现的位置,以词为单位,从1开始 |
| min_best_span_pos | 字段 | 整型 | 第一个最大LCS跨度的位置,以词为单位,从1开始 |
| exact_hit | 字段 | 布尔型 | 查询是否 == 字段 |
| min_idf | 字段 | 浮点型 | 匹配关键词的最小 idf |
| max_idf | 字段 | 浮点型 | 匹配关键词的最大 idf |
| sum_idf | 字段 | 浮点型 | 匹配关键词的 idf 之和 |
| exact_order | 字段 | 布尔型 | 查询关键词是否a) 全部匹配且 b) 按查询顺序 |
| min_gaps | 字段 | 整型 | 匹配跨度中关键词之间的最小间隔数 |
| lccs | 字段 | 整型 | 查询和文档之间的最长连续公共子序列,以词为单位 |
| wlccs | 字段 | 浮点型 | 加权最长连续公共子序列,连续关键词跨度的 idf 之和 |
| atc | 字段 | 浮点型 | 聚合词项接近度,log(1+sum(idf1idf2pow(距离, -1.75)) 在最佳关键词对上 |
注意:对于使用短语、邻近或NEAR运算符且关键词超过31个的查询,依赖词频的排名因子(如 tf、idf、bm25、hit_count、word_count)可能会对第31个及以上位置的关键词进行低估。这是由于内部使用了32位掩码来跟踪这些复杂运算符中的词项出现情况。
文档级因子是排名引擎为每个匹配文档针对当前查询计算的数值。因此它不同于普通文档属性,属性不依赖于全文查询,而因子可能会依赖。这些因子可以在排名表达式的任何位置使用。目前实现的文档级因子包括:
bm25(整型),文档级BM25估计(不进行关键词出现过滤)。max_lcs(整型),查询级可能的最大值,sum(lcs*user_weight)表达式可能达到的值。这可用于权重提升缩放。例如,MATCHANY排名器公式使用此值以确保任何字段中的完全短语匹配都高于所有字段中任何部分匹配的组合。field_mask(整型),匹配字段的32位掩码。query_word_count(整型),查询中唯一关键词的数量,根据排除的关键词数量进行调整。例如,(one one one one)和(one !two)查询都应赋值为1,因为只有一个唯一的非排除关键词。doc_word_count(整型),在整个文档中匹配的唯一关键词数量。
字段级因子是排名引擎为每个匹配的文档内文本字段针对当前查询计算的数值。由于可能有多个字段匹配查询,但最终权重需要是单一整型值,这些值需要折叠为单一值。为实现这一点,字段级因子只能在字段聚合函数中使用,不能在表达式的任何位置使用。例如,不能使用(lcs+bm25)作为排名表达式,因为lcs有多个值(每个匹配字段一个)。应使用(sum(lcs)+bm25),该表达式对所有匹配字段的lcs求和,然后将bm25加到每字段和上。目前实现的字段级因子包括:
-
lcs(整型),文档和查询之间最大逐字匹配的长度,以词计算。LCS代表最长公共子序列(或子集)。当仅匹配零散关键词时取最小值1,当整个查询以精确查询关键词顺序在字段中逐字匹配时取最大值(查询关键词数量)。例如,如果查询是'hello world'且字段包含从查询直接引用的这两个词(即相邻且完全按查询顺序),lcs将为2。如果查询是'hello world program'且字段包含'hello world',lcs将为2。注意,查询关键词的任何子集都可以匹配,不仅限于相邻关键词。例如,如果查询是'hello world program'且字段包含'hello (test program)',lcs也将为2,因为'hello'和'program'分别在查询中的相同位置匹配。最后,如果查询是'hello world program'且字段包含'hello world program',lcs将为3。(希望这一点此时已经不足为奇了。) -
user_weight(整型),用户指定的每字段权重(参考OPTION field_weights中的SQL)。如果未明确指定,权重默认为1。 -
hit_count(整型),字段中匹配的关键词出现次数。注意单个关键词可能出现多次。例如,如果'hello'在字段中出现3次,'world'出现5次,hit_count将为8。 -
word_count(整数),匹配字段中唯一关键词的数量。例如,如果'hello'和'world'在字段中的任何位置出现,word_count将为2,无论这两个关键词出现多少次。 -
tf_idf(浮点数),匹配字段中所有关键词的TF/IDF之和。IDF是逆文档频率,是介于0和1之间的浮点值,描述关键词的频率(基本上,对于出现在索引的每个文档中的关键词为0,对于仅出现在单个文档中的唯一关键词为1)。TF是词频,是字段中匹配关键词出现的次数。顺便说一下,tf_idf实际上是通过对所有匹配出现的IDF求和来计算的。这从构造上等同于对所有匹配关键词求TF*IDF的和。 -
min_hit_pos(整数),以单词计算的第一个匹配关键词出现的位置因此,这是一个相对低级的"原始"因子,你可能需要在使用它进行排名之前进行调整。具体的调整取决于你的数据和最终的公式,但以下是一些起步的想法:(a) 当
word_count<2时,可以简单地忽略任何基于min_gaps的提升;(b) 对于非平凡的min_gaps值(即当
word_count>=2时),可以用某个"最坏情况"常数进行截断,而对于平凡值(即当min_gaps=0且word_count<2时),可以用该常数替换;(c) 可以应用类似1/(1+min_gaps)的传递函数(使得更好的、较小的min_gaps值会使其最大化,而更差的、较大的min_gaps值会缓慢下降);等等。
-
lccs(整数)。最长公共连续子序列。查询和文档之间最长的公共子短语的长度,以关键词计算。LCCS因子与LCS有些相似,但更加严格。虽然LCS即使没有两个查询词相邻也可能大于1,但LCCS只有在文档中存在完全的连续查询子短语时才会大于1。例如,(one two three four five)查询与(one hundred three hundred five hundred)文档会得到lcs=3,但lccs=1,因为尽管3个关键词(one, three, five)在查询和文档之间的相对位置相同,但实际上没有两个匹配位置相邻。
请注意,LCCS仍然无法区分频繁和罕见的关键词;对于这一点,请参见WLCCS。
-
wlccs(浮点数)。加权最长公共连续子序列。查询和文档之间最长子短语的关键词的IDF之和。WLCCS的计算方式类似于LCCS,但每个"合适的"关键词出现会使其增加关键词的IDF,而不是像LCS和LCCS那样简单地加1。这使得能够对罕见和更重要的关键词序列进行更高的排名,即使它们比频繁关键词的序列更短。例如,查询
(Zanzibar bed and breakfast)对于文档(hotels of Zanzibar)会得到lccs=1,而对于(London bed and breakfast)会得到lccs=3,尽管"Zanzibar"实际上比整个"bed and breakfast"短语更罕见。WLCCS因子通过使用关键词频率来解决这个问题。 -
atc(浮点数)。聚合词项接近度。一种基于接近度的度量,当文档包含更多更紧密定位且更重要(罕见)的查询关键词组时,其值会增加。警告:你应该将ATC与OPTION idf='plain,tfidf_unnormalized'一起使用(见下文);否则,你可能会得到意外的结果。
ATC基本上的工作原理如下。对于文档中的每个关键词出现,我们计算所谓的词项接近度。为此,我们检查主要出现位置左右所有查询关键词的最近其他出现位置,计算距离衰减系数k = pow(distance, -1.75),并对这些出现位置求和衰减后的IDF。因此,对于每个关键词的每次出现,我们得到一个"接近度"值,描述该出现的"邻居"。然后我们将这些每次出现的接近度乘以其各自关键词的IDF,将它们全部求和,最后计算该和的对数。
换句话说,我们处理文档中最佳(最近)的匹配关键词对,并计算成对的"接近度",作为其IDF乘积并按距离系数缩放:
pair_tc = idf(pair_word1) * idf(pair_word2) * pow(pair_distance, -1.75)
然后我们对这些接近度求和,并计算最终的对数衰减ATC值:
atc = log(1+sum(pair_tc))
请注意,这个最终的衰减对数正是你应该使用OPTION idf=plain的原因,因为没有它,log()内的表达式可能是负数。
更近的关键词出现对ATC的贡献远远多于更频繁的关键词。事实上,当关键词紧挨着时,distance=1且k=1;当它们之间只有一个词时,distance=2且k=0.297,两个词之间时,distance=3且k=0.146,以此类推。同时,IDF衰减得相对较慢。例如,在一个100万文档的集合中,匹配10、100和1000个文档的关键词的IDF值分别为0.833、0.667和0.500。所以,一对仅在10个文档中各出现一次但中间有2个其他词的相当罕见的关键词将产生pair_tc = 0.101,因此几乎可以与一对100个文档和1000个文档的关键词(中间有1个其他词)的pair_tc = 0.099相媲美。更重要的是,一对两个唯一的、1个文档的关键词(中间有3个词)将得到pair_tc = 0.088,输给一对1000个文档的关键词(紧挨着且pair_tc = 0.25)。因此,基本上,虽然ATC确实结合了关键词频率和接近度,但它仍然稍微偏好接近度。
一个字段聚合函数是一个单参数函数,接受带有字段级因子的表达式,遍历所有匹配的字段,并计算最终结果。当前已实现的字段聚合函数包括:
sum,它将参数表达式在所有匹配的字段上相加。例如sum(1)应返回匹配字段的数量。top,它返回所有匹配字段中参数的最高值。max_window_hits,管理命中位置的滑动窗口,以跟踪指定窗口大小内的最大命中数。它移除落在窗口外的过时命中,并添加最新命中,更新在该窗口内找到的最大命中数。
大多数其他排名器实际上可以使用基于表达式的排名器模拟。您只需提供适当的表达式。虽然这种模拟可能比使用内置的编译排名器慢,但如果您想从现有排名器开始微调排名公式,这仍可能很有趣。此外,这些公式以清晰、可读的方式描述了排名器的细节。
- proximity_bm25(默认排名器)=
sum(lcs*user_weight)*1000+bm25 - bm25 =
sum(user_weight)*1000+bm25 - none =
1 - wordcount =
sum(hit_count*user_weight) - proximity =
sum(lcs*user_weight) - matchany =
sum((word_count+(lcs-1)*max_lcs)*user_weight) - fieldmask =
field_mask - sph04 =
sum((4*lcs+2*(min_hit_pos==1)+exact_hit)*user_weight)*1000+bm25
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=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' 规范。
Manticore Search 默认返回结果集中排名前20的匹配文档。
在 SQL 中,可以使用 LIMIT 子句来导航结果集。
LIMIT 可以接受一个数字作为返回集的大小(偏移量为零),也可以接受一对偏移量和大小的值。
使用 HTTP JSON 时,节点 offset 和 limit 控制结果集的偏移量和返回集的大小。或者,也可以使用 size 和 from 这对参数。
- SQL
- JSON
SELECT ... FROM ... [LIMIT [offset,] row_count]
SELECT ... FROM ... [LIMIT row_count][ OFFSET offset]{
"table": "<table_name>",
"query": ...
...
"limit": 20,
"offset": 0
}
{
"table": "<table_name>",
"query": ...
...
"size": 20,
"from": 0
}默认情况下,Manticore Search 使用一个包含1000个最佳排名文档的结果集窗口,这些文档可以返回在结果集中。如果结果集分页超出此值,查询将以错误结束。
此限制可以通过查询选项 max_matches 进行调整。
只有在需要导航到如此深的位置时,才应将 max_matches 增加到非常高的值。较高的 max_matches 值需要更多内存,并可能增加查询响应时间。处理深度结果集的一种方法是将 max_matches 设置为偏移量和限制的总和。
将 max_matches 降低到1000以下的好处是减少查询使用的内存。它也可以减少查询时间,但在大多数情况下,这种提升可能不明显。
- SQL
- JSON
SELECT ... FROM ... OPTION max_matches=<value>{
"table": "<table_name>",
"query": ...
...
"max_matches":<value>
}
}滚动搜索选项提供了一种高效且可靠的方式来分页浏览大型结果集。与传统的基于偏移量的分页不同,滚动搜索在深度分页时性能更好,并且实现分页更简单。
虽然它使用与基于偏移量分页相同的 max_matches 窗口,但滚动搜索可以通过使用滚动令牌在多次请求中检索结果,返回超过 max_matches 值的文档。
使用滚动分页时,无需同时使用 offset 和 limit —— 这是多余的,通常被认为是过度设计。只需指定 limit 和 scroll 令牌即可获取每个后续页面。
带排序条件的初始查询
首先执行带有所需排序条件的初始查询。唯一要求是 id 必须包含在 ORDER BY 子句中,以确保分页的一致性。查询将返回结果和用于后续页面的滚动令牌。
SELECT ... ORDER BY [... ,] id {ASC|DESC};
- Initial Query Example
SELECT weight(), id FROM test WHERE match('hello') ORDER BY weight() desc, id asc limit 2;+----------+------+
| weight() | id |
+----------+------+
| 1281 | 1 |
| 1281 | 2 |
+----------+------+
2 rows in set (0.00 sec)获取滚动令牌
执行初始查询后,通过执行 SHOW SCROLL 命令获取滚动令牌。
必须在滚动序列中的每次查询后调用 SHOW SCROLL,以获取下一页的更新滚动令牌。
每次查询都会生成一个反映最新滚动位置的新令牌。
SHOW SCROLL;
响应:
| scroll_token |
|------------------------------------|
| <base64 encoded scroll token> |
- Scroll Token Example
SHOW SCROLL;+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| scroll_token |
+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| eyJvcmRlcl9ieV9zdHIiOiJ3ZWlnaHQoKSBkZXNjLCBpZCBhc2MiLCJvcmRlcl9ieSI6W3siYXR0ciI6IndlaWdodCgpIiwiZGVzYyI6dHJ1ZSwidmFsdWUiOjEyODEsInR5cGUiOiJpbnQifSx7ImF0dHIiOiJpZCIsImRlc2MiOmZhbHNlLCJ2YWx1ZSI6MiwidHlwZSI6ImludCJ9XX0= |
+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
1 row in set (0.00 sec)使用 scroll 的分页查询
要获取下一页结果,在后续查询中将滚动令牌作为选项包含。当提供 scroll 选项时,指定排序条件是可选的。
记得在此查询后再次调用 SHOW SCROLL,以获取下一页所需的新令牌。
SELECT ... [ORDER BY [... ,] id {ASC|DESC}] OPTION scroll='<base64 encoded scroll token>'[, ...];
这确保分页无缝继续,保持初始查询中建立的排序上下文。
- Paginated Query Example
SELECT weight(), id FROM test WHERE match('hello') limit 2
OPTION scroll='eyJvcmRlcl9ieV9zdHIiOiJ3ZWlnaHQoKSBkZXNjLCBpZCBhc2MiLCJvcmRlcl9ieSI6W3siYXR0ciI6IndlaWdodCgpIiwiZGVzYyI6dHJ1ZSwidmFsdWUiOjEyODEsInR5cGUiOiJpbnQifSx7ImF0dHIiOiJpZCIsImRlc2MiOmZhbHNlLCJ2YWx1ZSI6MiwidHlwZSI6ImludCJ9XX0=';+----------+------+
| weight() | id |
+----------+------+
| 1281 | 3 |
| 1281 | 4 |
+----------+------+
2 rows in set (0.00 sec)初始请求
在初始请求中,在选项中指定 "scroll": true 和所需的排序条件。注意,id 必须出现在 sort 数组中。响应将包含滚动令牌,可用于后续请求的分页。
POST /search
{
"table": "<table_names>",
"options": {
"scroll": true
},
...
"sort": [
...
{ "id":{ "order":"{asc|desc}"} }
]
}
示例输出:
{
"timed_out": false,
"hits": {
...
},
"scroll": "<base64 encoded scroll token>"
}
- Initial Request Example
POST /search
{
"table": "test",
"options":
{
"scroll": true
},
"query":
{
"query_string":"hello"
},
"sort":
[
{ "_score":{ "order":"desc"} },
{ "id":{ "order":"asc"} }
],
"track_scores": true,
"limit":2
}{
"took": 0,
"timed_out": false,
"hits":
{
"total": 10,
"total_relation": "eq",
"hits":
[
{
"_id": 1,
"_score": 1281,
"_source":
{
"title": "hello world1"
}
},
{
"_id": 2,
"_score": 1281,
"_source":
{
"title": "hello world2"
}
}
]
},
"scroll": "eyJvcmRlcl9ieV9zdHIiOiJAd2VpZ2h0IGRlc2MsIGlkIGFzYyIsIm9yZGVyX2J5IjpbeyJhdHRyIjoid2VpZ2h0KCkiLCJkZXNjIjp0cnVlLCJ2YWx1ZSI6MTI4MSwidHlwZSI6ImludCJ9LHsiYXR0ciI6ImlkIiwiZGVzYyI6ZmFsc2UsInZhbHVlIjoyLCJ0eXBlIjoiaW50In1dfQ=="
}使用 scroll 的分页请求
要继续分页,在下一次请求的选项对象中包含从上一次响应获得的滚动令牌。指定排序条件是可选的。
POST /search
{
"table": "<table_names>",
"options": {
"scroll": "<base64 encoded scroll token>"
},
...
}
- Paginated Request Example
POST /search
{
"table": "test",
"options":
{
"scroll": "eyJvcmRlcl9ieV9zdHIiOiJAd2VpZ2h0IGRlc2MsIGlkIGFzYyIsIm9yZGVyX2J5IjpbeyJhdHRyIjoid2VpZ2h0KCkiLCJkZXNjIjp0cnVlLCJ2YWx1ZSI6MTI4MSwidHlwZSI6ImludCJ9LHsiYXR0ciI6ImlkIiwiZGVzYyI6ZmFsc2UsInZhbHVlIjoyLCJ0eXBlIjoiaW50In1dfQ=="
},
"query":
{
"query_string":"hello"
},
"track_scores": true,
"limit":2
}{
"took": 0,
"timed_out": false,
"hits":
{
"total": 8,
"total_relation": "eq",
"hits":
[
{
"_id": 3,
"_score": 1281,
"_source":
{
"title": "hello world3"
}
},
{
"_id": 4,
"_score": 1281,
"_source":
{
"title": "hello world4"
}
}
]
},
"scroll": "eyJvcmRlcl9ieV9zdHIiOiJAd2VpZ2h0IGRlc2MsIGlkIGFzYyIsIm9yZGVyX2J5IjpbeyJhdHRyIjoid2VpZ2h0KCkiLCJkZXNjIjp0cnVlLCJ2YWx1ZSI6MTI4MSwidHlwZSI6ImludCJ9LHsiYXR0ciI6ImlkIiwiZGVzYyI6ZmFsc2UsInZhbHVlIjo0LCJ0eXBlIjoiaW50In1dfQ=="
}Manticore 设计为通过其分布式搜索功能实现有效扩展。分布式搜索有助于提高查询延迟(即搜索时间)和吞吐量(即最大查询数/秒),适用于多服务器、多CPU或多核环境。这对于需要搜索海量数据(即数十亿条记录和数TB文本)的应用至关重要。
其主要理念是将被搜索的数据水平分区到多个搜索节点,并行处理。
分区是手动完成的。设置步骤如下:
- 在不同服务器上设置多个 Manticore 实例
- 将数据集的不同部分分配给不同实例
- 在某些
searchd实例上配置特殊的分布式表 - 将查询路由到分布式表
这种类型的表仅包含对其他本地和远程表的引用,因此不能直接重新索引。相反,应重新索引它所引用的表。
当 Manticore 收到针对分布式表的查询时,会执行以下步骤:
- 连接到配置的远程代理
- 向它们发送查询
- 同时搜索配置的本地表(远程代理搜索的同时)
- 从远程代理检索搜索结果
- 合并所有结果,去除重复项
- 将合并后的结果发送给客户端
从应用程序的角度来看,通过常规表或分布式表搜索没有区别。换句话说,分布式表对应用程序完全透明,无法判断查询的表是分布式还是本地。
了解更多关于远程节点的信息。
多查询,或称查询批处理,允许您在一次网络请求中向 Manticore 发送多个搜索查询。
👍 为什么使用多查询?
主要原因是性能。通过批量发送请求给 Manticore,而不是逐个发送,您可以通过减少网络往返时间来节省时间。此外,批量发送查询允许 Manticore 执行某些内部优化。如果无法应用批量优化,查询将被单独处理。
⛔ 何时不使用多查询?
多查询要求批处理中的所有搜索查询相互独立,但情况并非总是如此。有时查询 B 依赖于查询 A 的结果,这意味着查询 B 只能在执行查询 A 后设置。例如,您可能只想在主表中未找到结果时显示来自辅助索引的结果,或者您可能想根据第一个结果集中的匹配数指定第二个结果集的偏移量。在这些情况下,您需要使用单独的查询(或单独的批处理)。
您可以通过用分号分隔多个搜索查询来运行多查询。当 Manticore 从客户端接收到这样格式的查询时,将应用所有语句间的优化。
多查询不支持带有 FACET 的查询。单个批处理中的多查询数量不应超过 max_batch_queries。
- SQL
SELECT id, price FROM products WHERE MATCH('remove hair') ORDER BY price DESC; SELECT id, price FROM products WHERE MATCH('remove hair') ORDER BY price ASC需要注意两种主要优化:公共查询优化和公共子树优化。
公共查询优化 意味着 searchd 会识别批处理中所有仅排序和分组设置不同的查询,并且只执行一次搜索。例如,如果一个批处理包含 3 个查询,它们都是针对“ipod nano”的,但第一个查询请求按价格排序的前 10 个结果,第二个查询按供应商 ID 分组并请求按评分排序的前 5 个供应商,第三个查询请求最大价格,则全文搜索“ipod nano”只会执行一次,其结果将被重用以构建 3 个不同的结果集。
分面搜索 是特别受益于此优化的一个重要案例。实际上,分面搜索可以通过运行多个查询来实现,一个用于检索搜索结果本身,其他几个使用相同的全文查询但不同的分组设置来检索所有所需的结果组(前 3 名作者,前 5 名供应商等)。只要全文查询和过滤设置保持不变,公共查询优化就会触发,并大大提高性能。
公共子树优化 更加有趣。它允许 searchd 利用批量全文查询之间的相似性。它识别所有查询中的公共全文查询部分(子树)并在查询之间缓存它们。例如,考虑以下查询批处理:
donald trump president
donald trump barack obama john mccain
donald trump speech
有一个公共的两词部分 donald trump,只需计算一次,然后缓存并在查询间共享。公共子树优化正是这样做的。每个查询的缓存大小由 subtree_docs_cache 和 subtree_hits_cache 指令严格控制(以防缓存所有匹配“i am”的十六亿亿文档耗尽内存并立即导致服务器崩溃)。
如何判断批处理中的查询是否真的被优化了?如果是,相关的查询日志将有一个“multiplier”字段,指定一起处理了多少个查询:
注意“x3”字段。这意味着该查询被优化并在一个包含 3 个查询的子批处理中处理。
- log
[Sun Jul 12 15:18:17.000 2009] 0.040 sec x3 [ext/0/rel 747541 (0,20)] [lj] the
[Sun Jul 12 15:18:17.000 2009] 0.040 sec x3 [ext/0/ext 747541 (0,20)] [lj] the
[Sun Jul 12 15:18:17.000 2009] 0.040 sec x3 [ext/0/ext 747541 (0,20)] [lj] the作为参考,如果查询未批处理,常规日志将如下所示:
- log
[Sun Jul 12 15:18:17.062 2009] 0.059 sec [ext/0/rel 747541 (0,20)] [lj] the
[Sun Jul 12 15:18:17.156 2009] 0.091 sec [ext/0/ext 747541 (0,20)] [lj] the
[Sun Jul 12 15:18:17.250 2009] 0.092 sec [ext/0/ext 747541 (0,20)] [lj] the注意多查询情况下每个查询的时间提升了 1.5 倍到 2.3 倍,具体取决于排序模式。