Результаты запросов могут быть отсортированы по весу полнотекстового ранжирования, одному или нескольким атрибутам или выражениям.
Полнотекстовые запросы возвращают совпадения отсортированными по умолчанию. Если ничего не указано, они сортируются по релевантности, что эквивалентно ORDER BY weight() DESC в формате SQL.
Не полнотекстовые запросы по умолчанию не выполняют никакой сортировки.
Расширенный режим автоматически включается, когда вы явно задаете правила сортировки, добавляя предложение ORDER BY в формате SQL или используя опцию sort через HTTP JSON.
Общий синтаксис:
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"
}
}
]
}
}Сортировка по MVA-атрибутам также поддерживается в JSON-запросах. Режим сортировки можно задать через свойство 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 настраиваемо. В нем есть понятие так называемого ранкера. Ранкер формально можно определить как функцию, которая принимает на вход документ и запрос и производит на выходе значение релевантности. Проще говоря, ранкер контролирует именно то, как (с помощью какого конкретного алгоритма) 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 | совпадают ли все ключевые слова запроса а) были найдены и б) расположены в порядке запроса |
| min_gaps | field | int | минимальное количество пропусков между совпавшими ключевыми словами в пределах совпадающих интервалов |
| lccs | field | int | самая длинная общая непрерывная подпоследовательность между запросом и документом, в словах |
| wlccs | field | float | взвешенная самая длинная общая непрерывная подпоследовательность, сумма(idf) по непрерывным интервалам ключевых слов |
| atc | field | float | агрегированная близость терминов, log(1+sum(idf1idf2pow(distance, -1.75)) по лучшим парам ключевых слов |
Примечание: Для запросов с использованием операторов Phrase, Proximity или 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 означает Longest Common Subsequence (или Подпоследовательность). Принимает минимальное значение 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(целое число), позиция первого совпавшего ключевого слова, считаемая в словахСледовательно, это относительно низкоуровневый, «сырой» фактор, который, скорее всего, потребуется подкорректировать перед использованием в ранжировании. Конкретные корректировки сильно зависят от ваших данных и итоговой формулы, но вот несколько идей для начала: (а) любые бусты на основе min_gaps можно просто игнорировать, если word_count<2;
(b) не тривиальные значения min_gaps (т.е. когда word_count>=2) могут быть ограничены определённой константой "худшего случая", а тривиальные значения (т.е. когда min_gaps=0 и word_count<2) могут быть заменены этой константой;
(c) можно применить функцию преобразования типа 1/(1+min_gaps) (так, чтобы лучшие, меньшие min_gaps максимизировали значение, а худшие, большие min_gaps медленно уменьшали его); и так далее.
-
lccs(целое число). Longest Common Contiguous Subsequence — Длина самой длинной общей непрерывной подпоследовательности между запросом и документом, измеряемой по ключевым словам.Фактор LCCS отчасти похож на LCS, но более строгий. В то время как LCS может быть больше 1, даже если никакие два слова из запроса не стоят рядом, LCCS будет больше 1 только если в документе есть точные, непрерывные подфразы из запроса. Например, запрос (one two three four five) и документ (one hundred three hundred five hundred) дадут lcs=3, но lccs=1, поскольку совпадают позиции трёх ключевых слов (one, three, five), но никакие 2 соседних совпадения не расположены подряд.
Обратите внимание, что LCCS по-прежнему не учитывает частоту слов; для этого смотрите WLCCS.
-
wlccs(число с плавающей точкой). Weighted Longest Common Contiguous Subsequence — Взвешенная длина самой длинной общей непрерывной подпоследовательности, сумма IDF ключевых слов самой длинной общей подфразы между запросом и документом.WLCCS вычисляется аналогично LCCS, но каждое «подходящее» совпавшее слово увеличивает значение на IDF данного слова, а не просто на 1 (как в LCS и LCCS). Это позволяет ранжировать последовательности из более редких и важных ключевых слов выше, чем последовательности частых ключевых слов, даже если последние длиннее. Например, запрос
(Zanzibar bed and breakfast)даёт lccs=1 для документа(hotels of Zanzibar), но lccs=3 для(London bed and breakfast), хотя «Zanzibar» на самом деле несколько реже, чем вся фраза «bed and breakfast». Фактор WLCCS решает эту проблему, учитывая частоты ключевых слов. -
atc(число с плавающей точкой). Aggregate Term Closeness — агрегированная близость терминов. Мера близости, которая растёт, если документ содержит больше групп более тесно расположенных и более важных (редких) ключевых слов из запроса.ВНИМАНИЕ: следует использовать 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 затухает несколько медленнее. Например, в коллекции из 1 миллиона документов значения IDF для ключевых слов, которые встречаются в 10, 100 и 1000 документах, будут соответственно 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
Исторически используемый по умолчанию 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=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'.
По умолчанию 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>
}
}Опция прокрутки (scroll search) предоставляет эффективный и надежный способ пагинации по большим наборам результатов. В отличие от традиционной пагинации на основе смещения, прокрутка обеспечивает лучшую производительность для глубокой пагинации и предлагает более простой способ её реализации.
Хотя она использует то же окно 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 разработан для эффективного масштабирования за счёт возможностей распределённого поиска. Распределённый поиск полезен для улучшения задержки выполнения запросов (т.е. времени поиска) и пропускной способности (т.е. максимального количества запросов в секунду) в многосерверных, многоядерных или многоядерных средах. Это критично для приложений, которым нужно искать в огромных объёмах данных (т.е. миллиарды записей и терабайты текста).
Основная идея заключается в горизонтальном разделении данных для поиска между узлами поиска и их параллельной обработке.
Разбиение выполняется вручную. Для настройки необходимо:
- Развернуть несколько экземпляров Manticore на разных серверах
- Распределить разные части вашего набора данных по разным экземплярам
- Настроить специальную распределённую таблицу на некоторых из
searchdэкземпляров - Направить ваши запросы к распределённой таблице
Этот тип таблицы содержит только ссылки на другие локальные и удалённые таблицы — поэтому её нельзя индексировать напрямую. Вместо этого необходимо переиндексировать таблицы, на которые она ссылается.
Когда Manticore получает запрос к распределённой таблице, он выполняет следующие шаги:
- Подключается к настроенным удалённым агентам
- Отправляет им запрос
- Одновременно выполняет поиск по настроенным локальным таблицам (в то время как удалённые агенты ищут)
- Получает результаты поиска от удалённых агентов
- Объединяет все результаты, удаляя дубликаты
- Отправляет объединённые результаты клиенту
С точки зрения приложения, нет различий между поиском в обычной таблице или в распределённой таблице. Другими словами, распределённые таблицы полностью прозрачны для приложения, и невозможно определить, была ли запрошенная таблица распределённой или локальной.
Узнайте больше о удалённых узлах.
Мультизапросы, или пакетные запросы, позволяют отправлять несколько поисковых запросов в Manticore в рамках одного сетевого запроса.
👍 Зачем использовать мультизапросы?
Основная причина — производительность. Отправляя запросы в Manticore пакетом, а не по одному, вы экономите время за счет сокращения сетевых обходов. Кроме того, отправка запросов пакетом позволяет Manticore выполнять определенные внутренние оптимизации. Если оптимизации пакета неприменимы, запросы будут обрабатываться по отдельности.
⛔ Когда не следует использовать мультизапросы?
Мультизапросы требуют, чтобы все поисковые запросы в пакете были независимыми, что не всегда так. Иногда запрос B зависит от результатов запроса A, то есть запрос B можно настроить только после выполнения запроса A. Например, вы можете захотеть отобразить результаты из вторичного индекса только в том случае, если в основной таблице не найдено результатов, или вы можете указать смещение во втором наборе результатов на основе количества совпадений в первом наборе результатов. В этих случаях вам потребуется использовать отдельные запросы (или отдельные пакеты).
При использовании библиотек-коннекторов, таких как mysqli в PHP, вы можете добавить несколько запросов, а затем выполнить их все как единый пакет. Это будет работать как один пакет мультизапросов.
Примечание: Если вы используете консольный клиент 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", но 1-й запрос запрашивает топ-10 результатов, отсортированных по цене, 2-й запрос группирует по ID поставщика и запрашивает топ-5 поставщиков, отсортированных по рейтингу, а 3-й запрос запрашивает максимальную цену, полнотекстовый поиск по "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 раза в зависимости от конкретного режима сортировки.
Мультизапросы в основном поддерживаются для пакетной обработки запросов и получения метаинформации после таких пакетов. Из-за этого ограничения в пакетах разрешено только небольшое подмножество операторов. В одном пакете можно комбинировать только операторы 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.