查询结果可以按照全文排名权重、一个或多个属性或表达式进行排序。
全文查询返回匹配项默认按顺序排序。如果没有指定排序方式,则默认按相关性排序,这等同于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;
| Name | Level | Type | Summary |
|---|---|---|---|
| max_lcs | query | int | 当前查询的最大可能LCS值 |
| bm25 | document | int | BM25(1.2, 0) 的快速估算值 |
| bm25a(k1, b) | document | int | 精确的BM25()值,支持配置K1、B常数和语法 |
| 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 | field | float | 匹配关键字的tf*idf之和 == 所有出现的idf之和 |
| min_hit_pos | field | int | 首次匹配出现位置,按词数,1起始 |
| min_best_span_pos | field | int | 首个最大LCS跨度位置,按词数,1起始 |
| exact_hit | field | bool | 查询是否与字段完全相等 |
| min_idf | field | float | 匹配关键字中最小的idf |
| max_idf | field | float | 匹配关键字中最大的idf |
| sum_idf | field | float | 匹配关键字的idf之和 |
| exact_order | field | bool | 是否所有查询关键字a)均匹配且b)顺序与查询中一致 |
| min_gaps | field | int | 匹配跨度中匹配关键字之间的最小间隙数 |
| lccs | field | int | 查询与文档之间的最长连续公共子序列,按词数计 |
| wlccs | field | float | 加权最长连续公共子序列,连续关键字跨度上的idf之和 |
| atc | field | float | 聚合词项接近度,log(1+匹配关键字最佳对的 idf1idf2pow(distance, -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(整数),用户指定的每字段权重(参考 SQL 中的 OPTION field_weights)。若未显式指定,权重默认为 1。 -
hit_count(整数),字段中匹配到的关键词出现次数。注意,单个关键词可出现多次。例如,若字段中 'hello' 出现了 3 次,'world' 出现了 5 次,hit_count为 8。 -
word_count(整数),字段中匹配到的不同关键词数量。例如,若字段中出现 'hello' 和 'world',word_count为 2,不论它们出现次数多少。 -
tf_idf(浮点数),字段中所有匹配关键词的 TF/IDF 之和。IDF 表示逆文档频率,是 0 到 1 之间的浮点值,描述关键词的频率(基本上,出现于每个索引文档的关键词 IDF 为 0,而只出现于单一文档的关键词 IDF 为 1)。TF 是术语频率,字段中匹配关键词出现次数。顺带一提,tf_idf实际是通过对所有匹配出现累加 IDF 计算的。按照构造方法,这等价于对所有匹配关键词累加 TF*IDF。 -
min_hit_pos(整数),第一个匹配关键词出现的位置,按单词计数。因此,这是一个相对低层次的“原始”因子,通常你会想在使用它进行排名之前对它进行调整。具体调整高度依赖你的数据和最终公式,但这里先给几个思路:(a)当
word_count<2时,可以简单忽略任何基于min_gaps的提升;(b)当
word_count>=2且min_gaps非平凡时,可以用某个“最坏情况”的常量限制它,而平凡值(即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(浮点数),聚合术语接近度(Aggregate Term Closeness)。一种基于邻近的度量,当文档包含更多且更紧密且更重要(稀有)的查询关键词组合时,该值增加。警告: 你应该在 OPTION
idf='plain,tfidf_unnormalized'下使用 ATC(参见下文);否则可能得到意外的结果。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 的原因,因为如果没有它,对数内部的表达式可能是负数。
更接近的关键词出现对ATC的贡献 远多 于更频繁的关键词。事实上,当关键词紧挨着时,距离=1且k=1;当它们之间只有一个词时,距离=2且k=0.297,两个词之间时,距离=3且k=0.146,依此类推。同时,IDF的衰减速度较慢。例如,在一个100万文档集合中,匹配10、100和1000个文档的关键词的IDF值分别为0.833、0.667和0.500。因此,一对两个相对罕见的关键词,每个出现10次,但中间有2个词,其pair_tc = 0.101,几乎无法超过一对一个出现100次和一个出现1000次的关键词,中间只有一个词,其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 可以避免这种情况。
Plain IDF在 [0, log(N)] 范围内变化,关键词永远不会被惩罚;而normalized 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 之后才能设置。例如,您可能只想在主表中未找到结果时显示次级索引的结果,或者您可能需要根据第一个结果集中的匹配数量指定第二个结果集中的偏移量。在这种情况下,您将需要使用单独的查询(或单独的批处理)。
当使用连接器库时,例如 PHP 中的 mysqli,您可以添加多个查询,然后将它们作为一个批处理运行。这将作为一个单个多查询批处理工作。
注意:如果您使用控制台 MySQL 客户端,默认情况下它会将分号(;)解释为分隔符本身,并逐个将每个查询发送到服务器;这不是一个多查询批处理。要覆盖此行为,可以在客户端侧使用内部命令 delimiter 重新定义分隔符。在做出此更改后,客户端将发送整个包含分号的字符串,允许“多查询魔法”生效。
控制台客户端的这种行为有时会令人困惑,因为您可能会注意到相同的命令序列在 MySQL 客户端控制台中与 SQL-over-HTTP 等其他协议的行为有所不同。这是因为 MySQL 控制台客户端本身使用分号来划分查询,但其他协议可能会将整个序列作为一个批处理发送。
您可以使用 SQL 通过分号分隔多个搜索查询。当 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从控制台 MySQL/MariaDB 客户端:
DELIMITER _
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 指令严格控制(以确保缓存所有 160 亿个匹配“i am”的文档不会耗尽内存并立即杀死您的服务器)。
如何知道批处理中的查询是否实际进行了优化?如果进行了优化,相应的查询日志将有一个“倍数”字段,指定了一起处理了多少个查询:
注意“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 倍,具体取决于特定的排序模式。
多查询主要用于批处理查询并接收此类批处理的元数据。由于这一限制,批处理中只允许一小部分语句。在一个批处理中,您可以组合 SELECT、SHOW 和 SET 语句。
您可以像平常一样使用 SELECT;然而,请注意,所有查询将在一次通过中运行。如果查询之间没有关联,多查询就没有好处。守护进程会检测到这一点,并逐个运行查询。
你可以使用 SHOW 来处理 警告, 状态, 代理状态, 元数据, 配置文件 和 计划。所有其他在批次中的 SHOW 语句将被静默忽略,没有任何输出。例如,你不能执行 SHOW TABLES, SHOW THREADS, 或 SHOW VARIABLES,或任何其他未提及的语句进行批次处理。
你可以仅使用 SET 来设置 SET PROFILING。所有其他 SET ... 命令将被静默忽略。
执行的顺序也不同。守护进程在两轮中处理批次。
首先,它收集所有 SELECT 语句,并同时运行它看到的所有 SET PROFILING 语句。作为副作用,只有最后一个 SET PROFILING 语句有效。如果你执行一个类似的多查询语句,如 SET PROFILING=1; SELECT...; SHOW META; SHOW PROFILE; SET PROFILING=0,你将看不到任何配置文件,因为在第一轮中,守护进程执行了 SET PROFILING=1,然后立即执行了 SET PROFILING=0。
第二轮,守护进程尝试使用收集的所有 SELECT 语句执行单个批次查询。如果语句不相关,它将依次执行它们。
最后,它遍历初始批次序列,并返回结果集中的每个 SELECT 和 SHOW 的子结果数据和元数据。由于所有 SET PROFILING 语句在第一轮中已执行,因此在第二轮中将被跳过。
each SELECT and SHOW. Since all SET PROFILING statements were executed in the first pass, they are skipped on this second pass.