Percolate запросы также известны как Persistent queries, Prospective search, document routing, search in reverse и inverse search.
Традиционный способ проведения поиска предполагает хранение документов и выполнение поисковых запросов по ним. Однако бывают случаи, когда мы хотим применить запрос к вновь поступающему документу, чтобы сигнализировать о совпадении. Некоторые сценарии, где это желательно, включают системы мониторинга, которые собирают данные и уведомляют пользователей о конкретных событиях, таких как достижение определенного порога для метрики или появление конкретного значения в отслеживаемых данных. Другой пример — агрегация новостей, где пользователи могут захотеть получать уведомления только об определенных категориях или темах, или даже конкретных "ключевых словах".
В этих ситуациях традиционный поиск не является лучшим решением, так как он предполагает выполнение желаемого поиска по всей коллекции. Этот процесс умножается на количество пользователей, что приводит к выполнению множества запросов по всей коллекции, что может вызвать значительную дополнительную нагрузку. Альтернативный подход, описанный в этом разделе, предполагает хранение самих запросов и их проверку на соответствие входящему новому документу или пакету документов.
Google Alerts, AlertHN, Bloomberg Terminal и другие системы, позволяющие пользователям подписываться на определенный контент, используют аналогичную технологию.
- См. percolate для получения информации о создании PQ таблицы.
- См. Adding rules to a percolate table, чтобы узнать, как добавлять percolate правила (также известные как PQ правила). Вот краткий пример:
Ключевой момент, который нужно помнить о percolate запросах, это то, что ваши поисковые запросы уже находятся в таблице. Что вам нужно предоставить — это документы для проверки, соответствуют ли какие-либо из них каким-либо сохраненным правилам.
Вы можете выполнить percolate запрос через SQL или JSON интерфейсы, а также с помощью клиентов на языках программирования. SQL подход предлагает большую гибкость, в то время как HTTP метод проще и предоставляет большую часть необходимого. Следующая таблица может помочь вам понять различия.
| Желаемое поведение | SQL | HTTP |
|---|---|---|
| Предоставить один документ | CALL PQ('tbl', '{doc1}') |
query.percolate.document{doc1} |
| Предоставить один документ (альтернатива) | CALL PQ('tbl', 'doc1', 0 as docs_json) |
- |
| Предоставить несколько документов | CALL PQ('tbl', ('doc1', 'doc2'), 0 as docs_json) |
- |
| Предоставить несколько документов (альтернатива) | CALL PQ('tbl', ('{doc1}', '{doc2}')) |
- |
| Предоставить несколько документов (альтернатива) | CALL PQ('tbl', '[{doc1}, {doc2}]') |
- |
| Возвращать идентификаторы совпадающих документов | 0/1 as docs (отключено по умолчанию) | Включено по умолчанию |
| Использовать собственный id документа для отображения в результате | 'id field' as docs_id (отключено по умолчанию) | Недоступно |
| Считать входные документы JSON | 1 as docs_json (1 по умолчанию) | Включено по умолчанию |
| Считать входные документы простым текстом | 0 as docs_json (1 по умолчанию) | Недоступно |
| Режим разреженного распределения | default | default |
| Режим шардированного распределения | sharded as mode | Недоступно |
| Возвращать всю информацию о совпадающем запросе | 1 as query (0 по умолчанию) | Включено по умолчанию |
| Пропускать невалидный JSON | 1 as skip_bad_json (0 по умолчанию) | Недоступно |
| Расширенная информация в SHOW META | 1 as verbose (0 по умолчанию) | Недоступно |
| Определить число, которое будет добавлено к идентификаторам документов, если не предоставлены поля docs_id (в основном актуально в режимах распределенной PQ) | 1 as shift (0 по умолчанию) | Недоступно |
Чтобы продемонстрировать, как это работает, вот несколько примеров. Давайте создадим PQ таблицу с двумя полями:
- title (текст)
- color (строка)
и тремя правилами в ней:
- Просто полнотекстовый поиск. Запрос:
@title bag - Полнотекстовый поиск и фильтрация. Запрос:
@title shoes. Фильтры:color='red' - Полнотекстовый поиск и более сложная фильтрация. Запрос:
@title shoes. Фильтры:color IN('blue', 'green')
- SQL
- JSON
- PHP
- Python
- Python-asyncio
- javascript
- Java
- C#
- Rust
- TypeScript
- Go
CREATE TABLE products(title text, color string) type='pq';
INSERT INTO products(query) values('@title bag');
INSERT INTO products(query,filters) values('@title shoes', 'color=\'red\'');
INSERT INTO products(query,filters) values('@title shoes', 'color in (\'blue\', \'green\')');
select * from products;PUT /pq/products/doc/
{
"query": {
"match": {
"title": "bag"
}
},
"filters": ""
}
PUT /pq/products/doc/
{
"query": {
"match": {
"title": "shoes"
}
},
"filters": "color='red'"
}
PUT /pq/products/doc/
{
"query": {
"match": {
"title": "shoes"
}
},
"filters": "color IN ('blue', 'green')"
}$index = [
'table' => 'products',
'body' => [
'columns' => [
'title' => ['type' => 'text'],
'color' => ['type' => 'string']
],
'settings' => [
'type' => 'pq'
]
]
];
$client->indices()->create($index);
$query = [
'table' => 'products',
'body' => [ 'query'=>['match'=>['title'=>'bag']]]
];
$client->pq()->doc($query);
$query = [
'table' => 'products',
'body' => [ 'query'=>['match'=>['title'=>'shoes']],'filters'=>"color='red'"]
];
$client->pq()->doc($query);
$query = [
'table' => 'products',
'body' => [ 'query'=>['match'=>['title'=>'shoes']],'filters'=>"color IN ('blue', 'green')"]
];
$client->pq()->doc($query);utilsApi.sql('create table products(title text, color string) type=\'pq\'')
indexApi.insert({"table" : "products", "doc" : {"query" : "@title bag" }})
indexApi.insert({"table" : "products", "doc" : {"query" : "@title shoes", "filters": "color='red'" }})
indexApi.insert({"table" : "products", "doc" : {"query" : "@title shoes","filters": "color IN ('blue', 'green')" }})await utilsApi.sql('create table products(title text, color string) type=\'pq\'')
await indexApi.insert({"table" : "products", "doc" : {"query" : "@title bag" }})
await indexApi.insert({"table" : "products", "doc" : {"query" : "@title shoes", "filters": "color='red'" }})
await indexApi.insert({"table" : "products", "doc" : {"query" : "@title shoes","filters": "color IN ('blue', 'green')" }})res = await utilsApi.sql('create table products(title text, color string) type=\'pq\'');
res = indexApi.insert({"table" : "products", "doc" : {"query" : "@title bag" }});
res = indexApi.insert({"table" : "products", "doc" : {"query" : "@title shoes", "filters": "color='red'" }});
res = indexApi.insert({"table" : "products", "doc" : {"query" : "@title shoes","filters": "color IN ('blue', 'green')" }});utilsApi.sql("create table products(title text, color string) type='pq'", true);
doc = new HashMap<String,Object>(){{
put("query", "@title bag");
}};
newdoc = new InsertDocumentRequest();
newdoc.index("products").setDoc(doc);
indexApi.insert(newdoc);
doc = new HashMap<String,Object>(){{
put("query", "@title shoes");
put("filters", "color='red'");
}};
newdoc = new InsertDocumentRequest();
newdoc.index("products").setDoc(doc);
indexApi.insert(newdoc);
doc = new HashMap<String,Object>(){{
put("query", "@title shoes");
put("filters", "color IN ('blue', 'green')");
}};
newdoc = new InsertDocumentRequest();
newdoc.index("products").setDoc(doc);
indexApi.insert(newdoc);utilsApi.Sql("create table products(title text, color string) type='pq'", true);
Dictionary<string, Object> doc = new Dictionary<string, Object>();
doc.Add("query", "@title bag");
InsertDocumentRequest newdoc = new InsertDocumentRequest(index: "products", doc: doc);
indexApi.Insert(newdoc);
doc = new Dictionary<string, Object>();
doc.Add("query", "@title shoes");
doc.Add("filters", "color='red'");
newdoc = new InsertDocumentRequest(index: "products", doc: doc);
indexApi.Insert(newdoc);
doc = new Dictionary<string, Object>();
doc.Add("query", "@title bag");
doc.Add("filters", "color IN ('blue', 'green')");
newdoc = new InsertDocumentRequest(index: "products", doc: doc);
indexApi.Insert(newdoc);utils_api.sql("create table products(title text, color string) type='pq'", Some(true)).await;
let mut doc1 = HashMap::new();
doc1.insert("query".to_string(), serde_json::json!("@title bag"));
let insert_req1 = InsertDocumentRequest::new("products".to_string(), serde_json::json!(doc1));
index_api.insert(insert_req1).await;
let mut doc2 = HashMap::new();
doc2.insert("query".to_string(), serde_json::json!("@title shoes"));
doc2.insert("filters".to_string(), serde_json::json!("color='red'"));
let insert_req2 = InsertDocumentRequest::new("products".to_string(), serde_json::json!(doc2));
index_api.insert(insert_req2).await;
let mut doc3 = HashMap::new();
doc3.insert("query".to_string(), serde_json::json!("@title bag"));
doc3.insert("filters".to_string(), serde_json::json!("color IN ('blue', 'green')"));
let insert_req3 = InsertDocumentRequest::new("products".to_string(), serde_json::json!(doc3));
index_api.insert(insert_req3).await;res = await utilsApi.sql("create table test_pq(title text, color string) type='pq'");
res = indexApi.insert({
index: 'test_pq',
doc: { query : '@title bag' }
});
res = indexApi.insert(
index: 'test_pq',
doc: { query: '@title shoes', filters: "color='red'" }
});
res = indexApi.insert({
index: 'test_pq',
doc: { query : '@title shoes', filters: "color IN ('blue', 'green')" }
});apiClient.UtilsAPI.Sql(context.Background()).Body("create table test_pq(title text, color string) type='pq'").Execute()
indexDoc := map[string]interface{} {"query": "@title bag"}
indexReq := manticoreclient.NewInsertDocumentRequest("test_pq", indexDoc)
apiClient.IndexAPI.Insert(context.Background()).InsertDocumentRequest(*indexReq).Execute();
indexDoc = map[string]interface{} {"query": "@title shoes", "filters": "color='red'"}
indexReq = manticoreclient.NewInsertDocumentRequest("test_pq", indexDoc)
apiClient.IndexAPI.Insert(context.Background()).InsertDocumentRequest(*indexReq).Execute();
indexDoc = map[string]interface{} {"query": "@title shoes", "filters": "color IN ('blue', 'green')"}
indexReq = manticoreclient.NewInsertDocumentRequest("test_pq", indexDoc)
apiClient.IndexAPI.Insert(context.Background()).InsertDocumentRequest(*indexReq).Execute();+---------------------+--------------+------+---------------------------+
| id | query | tags | filters |
+---------------------+--------------+------+---------------------------+
| 1657852401006149635 | @title shoes | | color IN ('blue, 'green') |
| 1657852401006149636 | @title shoes | | color='red' |
| 1657852401006149637 | @title bag | | |
+---------------------+--------------+------+---------------------------+{
"table": "products",
"type": "doc",
"_id": 1657852401006149661,
"result": "created"
}
{
"table": "products",
"type": "doc",
"_id": 1657852401006149662,
"result": "created"
}
{
"table": "products",
"type": "doc",
"_id": 1657852401006149663,
"result": "created"
}Array(
[table] => products
[type] => doc
[_id] => 1657852401006149661
[result] => created
)
Array(
[table] => products
[type] => doc
[_id] => 1657852401006149662
[result] => created
)
Array(
[table] => products
[type] => doc
[_id] => 1657852401006149663
[result] => created
){'created': True,
'found': None,
'id': 0,
'table': 'products',
'result': 'created'}
{'created': True,
'found': None,
'id': 0,
'table': 'products',
'result': 'created'}
{'created': True,
'found': None,
'id': 0,
'table': 'products',
'result': 'created'}{'created': True,
'found': None,
'id': 0,
'table': 'products',
'result': 'created'}
{'created': True,
'found': None,
'id': 0,
'table': 'products',
'result': 'created'}
{'created': True,
'found': None,
'id': 0,
'table': 'products',
'result': 'created'}"table":"products","_id":0,"created":true,"result":"created"}
{"table":"products","_id":0,"created":true,"result":"created"}
{"table":"products","_id":0,"created":true,"result":"created"}{total=0, error=, warning=}
class SuccessResponse {
index: products
id: 0
created: true
result: created
found: null
}
class SuccessResponse {
index: products
id: 0
created: true
result: created
found: null
}
class SuccessResponse {
index: products
id: 0
created: true
result: created
found: null
}{total=0, error="", warning=""}
class SuccessResponse {
index: products
id: 0
created: true
result: created
found: null
}
class SuccessResponse {
index: products
id: 0
created: true
result: created
found: null
}
class SuccessResponse {
index: products
id: 0
created: true
result: created
found: null
}{total=0, error="", warning=""}
class SuccessResponse {
index: products
id: 0
created: true
result: created
found: null
}
class SuccessResponse {
index: products
id: 0
created: true
result: created
found: null
}
class SuccessResponse {
index: products
id: 0
created: true
result: created
found: null
}{
"table":"test_pq",
"_id":1657852401006149661,
"created":true,
"result":"created"
}
{
"table":"test_pq",
"_id":1657852401006149662,
"created":true,
"result":"created"
}
{
"table":"test_pq",
"_id":1657852401006149663,
"created":true,
"result":"created"
}{
"table":"test_pq",
"_id":1657852401006149661,
"created":true,
"result":"created"
}
{
"table":"test_pq",
"_id":1657852401006149662,
"created":true,
"result":"created"
}
{
"table":"test_pq",
"_id":1657852401006149663,
"created":true,
"result":"created"
}Первый документ не соответствует ни одному правилу. Он мог бы соответствовать первым двум, но они требуют дополнительных фильтров.
Второй документ соответствует одному правилу. Обратите внимание, что CALL PQ по умолчанию ожидает, что документ будет JSON, но если вы используете 0 as docs_json, вы можете передать простую строку.
- SQL
- JSON
- PHP
- Python
- Python-asyncio
- javascript
- Java
- C#
- Rust
- TypeScript
- Go
CALL PQ('products', 'Beautiful shoes', 0 as docs_json);
CALL PQ('products', 'What a nice bag', 0 as docs_json);
CALL PQ('products', '{"title": "What a nice bag"}');POST /pq/products/search
{
"query": {
"percolate": {
"document": {
"title": "What a nice bag"
}
}
}
}$percolate = [
'table' => 'products',
'body' => [
'query' => [
'percolate' => [
'document' => [
'title' => 'What a nice bag'
]
]
]
]
];
$client->pq()->search($percolate);searchApi.percolate('products',{"query":{"percolate":{"document":{"title":"What a nice bag"}}}})await searchApi.percolate('products',{"query":{"percolate":{"document":{"title":"What a nice bag"}}}})res = await searchApi.percolate('products',{"query":{"percolate":{"document":{"title":"What a nice bag"}}}});PercolateRequest percolateRequest = new PercolateRequest();
query = new HashMap<String,Object>(){{
put("percolate",new HashMap<String,Object >(){{
put("document", new HashMap<String,Object >(){{
put("title","what a nice bag");
}});
}});
}};
percolateRequest.query(query);
searchApi.percolate("test_pq",percolateRequest);Dictionary<string, Object> percolateDoc = new Dictionary<string, Object>();
percolateDoc.Add("document", new Dictionary<string, Object> {{ "title", "what a nice bag" }});
Dictionary<string, Object> query = new Dictionary<string, Object> {{ "percolate", percolateDoc }};
PercolateRequest percolateRequest = new PercolateRequest(query=query);
searchApi.Percolate("test_pq",percolateRequest);let mut percolate_doc_fields = HashMap::new();
percolate_doc_fileds.insert("title".to_string(), "what a nice bag");
let mut percolate_doc = HashMap::new();
percolate_doc.insert("document".to_string(), percolate_doc_fields);
let percolate_query = PercolateRequestQuery::new(serde_json::json!(percolate_doc));
let percolate_req = PercolateRequest::new(percolate_query);
search_api.percolate("test_pq", percolate_req).await;res = await searchApi.percolate('test_pq', { query: { percolate: { document : { title : 'What a nice bag' } } } } );query := map[string]interface{} {"title": "what a nice bag"}
percolateRequestQuery := manticoreclient.NewPercolateQuery(query)
percolateRequest := manticoreclient.NewPercolateRequest(percolateRequestQuery)
res, _, _ := apiClient.SearchAPI.Percolate(context.Background(), "test_pq").PercolateRequest(*percolateRequest).Execute()+---------------------+
| id |
+---------------------+
| 1657852401006149637 |
+---------------------+
+---------------------+
| id |
+---------------------+
| 1657852401006149637 |
+---------------------+{
"took": 0,
"timed_out": false,
"hits": {
"total": 1,
"max_score": 1,
"hits": [
{
"table": "products",
"_type": "doc",
"_id": 1657852401006149644,
"_score": "1",
"_source": {
"query": {
"ql": "@title bag"
}
},
"fields": {
"_percolator_document_slot": [
1
]
}
}
]
}
}Array
(
[took] => 0
[timed_out] =>
[hits] => Array
(
[total] => 1
[max_score] => 1
[hits] => Array
(
[0] => Array
(
[_index] => products
[_type] => doc
[_id] => 1657852401006149644
[_score] => 1
[_source] => Array
(
[query] => Array
(
[match] => Array
(
[title] => bag
)
)
)
[fields] => Array
(
[_percolator_document_slot] => Array
(
[0] => 1
)
)
)
)
)
){'hits': {'hits': [{u'_id': u'2811025403043381480',
u'table': u'products',
u'_score': u'1',
u'_source': {u'query': {u'ql': u'@title bag'}},
u'_type': u'doc',
u'fields': {u'_percolator_document_slot': [1]}}],
'total': 1},
'profile': None,
'timed_out': False,
'took': 0}{'hits': {'hits': [{u'_id': u'2811025403043381480',
u'table': u'products',
u'_score': u'1',
u'_source': {u'query': {u'ql': u'@title bag'}},
u'_type': u'doc',
u'fields': {u'_percolator_document_slot': [1]}}],
'total': 1},
'profile': None,
'timed_out': False,
'took': 0}{
"took": 0,
"timed_out": false,
"hits": {
"total": 1,
"hits": [
{
"table": "products",
"_type": "doc",
"_id": 2811045522851233808,
"_score": "1",
"_source": {
"query": {
"ql": "@title bag"
}
},
"fields": {
"_percolator_document_slot": [
1
]
}
}
]
}
}class SearchResponse {
took: 0
timedOut: false
hits: class SearchResponseHits {
total: 1
maxScore: 1
hits: [{_index=products, _type=doc, _id=2811045522851234109, _score=1, _source={query={ql=@title bag}}, fields={_percolator_document_slot=[1]}}]
aggregations: null
}
profile: null
}class SearchResponse {
took: 0
timedOut: false
hits: class SearchResponseHits {
total: 1
maxScore: 1
hits: [{_index=products, _type=doc, _id=2811045522851234109, _score=1, _source={query={ql=@title bag}}, fields={_percolator_document_slot=[1]}}]
aggregations: null
}
profile: null
}class SearchResponse {
took: 0
timedOut: false
hits: class SearchResponseHits {
total: 1
maxScore: 1
hits: [{_index=products, _type=doc, _id=2811045522851234109, _score=1, _source={query={ql=@title bag}}, fields={_percolator_document_slot=[1]}}]
aggregations: null
}
profile: null
}{
"took": 0,
"timed_out": false,
"hits": {
"total": 1,
"hits": [
{
"table": "test_pq",
"_type": "doc",
"_id": 1657852401006149661,
"_score": "1",
"_source": {
"query": {
"ql": "@title bag"
}
},
"fields": {
"_percolator_document_slot": [
1
]
}
}
]
}
}{
"took": 0,
"timed_out": false,
"hits": {
"total": 1,
"hits": [
{
"table": "test_pq",
"_type": "doc",
"_id": 1657852401006149661,
"_score": "1",
"_source": {
"query": {
"ql": "@title bag"
}
},
"fields": {
"_percolator_document_slot": [
1
]
}
}
]
}
}- SQL
- JSON
- PHP
- Python
- Python-asyncio
- javascript
- Java
- C#
- Rust
- TypeScript
- Go
CALL PQ('products', '{"title": "What a nice bag"}', 1 as query);POST /pq/products/search
{
"query": {
"percolate": {
"document": {
"title": "What a nice bag"
}
}
}
}$percolate = [
'table' => 'products',
'body' => [
'query' => [
'percolate' => [
'document' => [
'title' => 'What a nice bag'
]
]
]
]
];
$client->pq()->search($percolate);searchApi.percolate('products',{"query":{"percolate":{"document":{"title":"What a nice bag"}}}})await searchApi.percolate('products',{"query":{"percolate":{"document":{"title":"What a nice bag"}}}})res = await searchApi.percolate('products',{"query":{"percolate":{"document":{"title":"What a nice bag"}}}});PercolateRequest percolateRequest = new PercolateRequest();
query = new HashMap<String,Object>(){{
put("percolate",new HashMap<String,Object >(){{
put("document", new HashMap<String,Object >(){{
put("title","what a nice bag");
}});
}});
}};
percolateRequest.query(query);
searchApi.percolate("test_pq",percolateRequest);Dictionary<string, Object> percolateDoc = new Dictionary<string, Object>();
percolateDoc.Add("document", new Dictionary<string, Object> {{ "title", "what a nice bag" }});
Dictionary<string, Object> query = new Dictionary<string, Object> {{ "percolate", percolateDoc }};
PercolateRequest percolateRequest = new PercolateRequest(query=query);
searchApi.Percolate("test_pq",percolateRequest);let mut percolate_doc_fields = HashMap::new();
percolate_doc_fileds.insert("title".to_string(), "what a nice bag");
let mut percolate_doc = HashMap::new();
percolate_doc.insert("document".to_string(), percolate_doc_fields);
let percolate_query = PercolateRequestQuery::new(serde_json::json!(percolate_doc));
let percolate_req = PercolateRequest::new(percolate_query);
search_api.percolate("test_pq", percolate_req).await;res = await searchApi.percolate('test_pq', { query: { percolate: { document : { title : 'What a nice bag' } } } } );query := map[string]interface{} {"title": "what a nice bag"}
percolateRequestQuery := manticoreclient.NewPercolateQuery(query)
percolateRequest := manticoreclient.NewPercolateRequest(percolateRequestQuery)
res, _, _ := apiClient.SearchAPI.Percolate(context.Background(), "test_pq").PercolateRequest(*percolateRequest).Execute()+---------------------+------------+------+---------+
| id | query | tags | filters |
+---------------------+------------+------+---------+
| 1657852401006149637 | @title bag | | |
+---------------------+------------+------+---------+{
"took": 0,
"timed_out": false,
"hits": {
"total": 1,
"max_score": 1,
"hits": [
{
"table": "products",
"_type": "doc",
"_id": 1657852401006149644,
"_score": "1",
"_source": {
"query": {
"ql": "@title bag"
}
},
"fields": {
"_percolator_document_slot": [
1
]
}
}
]
}
}Array
(
[took] => 0
[timed_out] =>
[hits] => Array
(
[total] => 1
[max_score] => 1
[hits] => Array
(
[0] => Array
(
[_index] => products
[_type] => doc
[_id] => 1657852401006149644
[_score] => 1
[_source] => Array
(
[query] => Array
(
[match] => Array
(
[title] => bag
)
)
)
[fields] => Array
(
[_percolator_document_slot] => Array
(
[0] => 1
)
)
)
)
)
){'hits': {'hits': [{u'_id': u'2811025403043381480',
u'table': u'products',
u'_score': u'1',
u'_source': {u'query': {u'ql': u'@title bag'}},
u'_type': u'doc',
u'fields': {u'_percolator_document_slot': [1]}}],
'total': 1},
'profile': None,
'timed_out': False,
'took': 0}{'hits': {'hits': [{u'_id': u'2811025403043381480',
u'table': u'products',
u'_score': u'1',
u'_source': {u'query': {u'ql': u'@title bag'}},
u'_type': u'doc',
u'fields': {u'_percolator_document_slot': [1]}}],
'total': 1},
'profile': None,
'timed_out': False,
'took': 0}{
"took": 0,
"timed_out": false,
"hits": {
"total": 1,
"hits": [
{
"table": "products",
"_type": "doc",
"_id": 2811045522851233808,
"_score": "1",
"_source": {
"query": {
"ql": "@title bag"
}
},
"fields": {
"_percolator_document_slot": [
1
]
}
}
]
}
}class SearchResponse {
took: 0
timedOut: false
hits: class SearchResponseHits {
total: 1
maxScore: 1
hits: [{_index=products, _type=doc, _id=2811045522851234109, _score=1, _source={query={ql=@title bag}}, fields={_percolator_document_slot=[1]}}]
aggregations: null
}
profile: null
}class SearchResponse {
took: 0
timedOut: false
hits: class SearchResponseHits {
total: 1
maxScore: 1
hits: [{_index=products, _type=doc, _id=2811045522851234109, _score=1, _source={query={ql=@title bag}}, fields={_percolator_document_slot=[1]}}]
aggregations: null
}
profile: null
}class SearchResponse {
took: 0
timedOut: false
hits: class SearchResponseHits {
total: 1
maxScore: 1
hits: [{_index=products, _type=doc, _id=2811045522851234109, _score=1, _source={query={ql=@title bag}}, fields={_percolator_document_slot=[1]}}]
aggregations: null
}
profile: null
}{
"took": 0,
"timed_out": false,
"hits": {
"total": 1,
"hits": [
{
"table": "test_pq",
"_type": "doc",
"_id": 1657852401006149661,
"_score": "1",
"_source": {
"query": {
"ql": "@title bag"
}
},
"fields": {
"_percolator_document_slot": [
1
]
}
}
]
}
}{
"took": 0,
"timed_out": false,
"hits": {
"total": 1,
"hits": [
{
"table": "test_pq",
"_type": "doc",
"_id": 1657852401006149661,
"_score": "1",
"_source": {
"query": {
"ql": "@title bag"
}
},
"fields": {
"_percolator_document_slot": [
1
]
}
}
]
}
}Обратите внимание, что с помощью CALL PQ вы можете предоставить несколько документов разными способами:
- как массив простых документов в круглых скобках
('doc1', 'doc2'). Это требует0 as docs_json - как массив JSON в круглых скобках
('{doc1}', '{doc2}') - или как стандартный JSON-массив
'[{doc1}, {doc2}]'
- SQL
- JSON
- PHP
- Python
- Python-asyncio
- javascript
- Java
- C#
- Rust
- TypeScript
- Go
CALL PQ('products', ('nice pair of shoes', 'beautiful bag'), 1 as query, 0 as docs_json);
CALL PQ('products', ('{"title": "nice pair of shoes", "color": "red"}', '{"title": "beautiful bag"}'), 1 as query);
CALL PQ('products', '[{"title": "nice pair of shoes", "color": "blue"}, {"title": "beautiful bag"}]', 1 as query);POST /pq/products/search
{
"query": {
"percolate": {
"documents": [
{"title": "nice pair of shoes", "color": "blue"},
{"title": "beautiful bag"}
]
}
}
}$percolate = [
'table' => 'products',
'body' => [
'query' => [
'percolate' => [
'documents' => [
['title' => 'nice pair of shoes','color'=>'blue'],
['title' => 'beautiful bag']
]
]
]
]
];
$client->pq()->search($percolate);searchApi.percolate('products',{"query":{"percolate":{"documents":[{"title":"nice pair of shoes","color":"blue"},{"title":"beautiful bag"}]}}})await searchApi.percolate('products',{"query":{"percolate":{"documents":[{"title":"nice pair of shoes","color":"blue"},{"title":"beautiful bag"}]}}})res = await searchApi.percolate('products',{"query":{"percolate":{"documents":[{"title":"nice pair of shoes","color":"blue"},{"title":"beautiful bag"}]}}});percolateRequest = new PercolateRequest();
query = new HashMap<String,Object>(){{
put("percolate",new HashMap<String,Object >(){{
put("documents", new ArrayList<Object>(){{
add(new HashMap<String,Object >(){{
put("title","nice pair of shoes");
put("color","blue");
}});
add(new HashMap<String,Object >(){{
put("title","beautiful bag");
}});
}});
}});
}};
percolateRequest.query(query);
searchApi.percolate("products",percolateRequest);var doc1 = new Dictionary<string, Object>();
doc1.Add("title","nice pair of shoes");
doc1.Add("color","blue");
var doc2 = new Dictionary<string, Object>();
doc2.Add("title","beautiful bag");
var docs = new List<Object> {doc1, doc2};
Dictionary<string, Object> percolateDoc = new Dictionary<string, Object> {{ "documents", docs }};
Dictionary<string, Object> query = new Dictionary<string, Object> {{ "percolate", percolateDoc }};
PercolateRequest percolateRequest = new PercolateRequest(query=query);
searchApi.Percolate("products",percolateRequest);let mut percolate_doc_fields1 = HashMap::new();
percolate_doc_fields1.insert("title".to_string(), "nice pair of shoes");
percolate_doc_fields1.insert("color".to_string(), "blue");
let mut percolate_doc_fields2 = HashMap::new();
percolate_doc_fields2.insert("title".to_string(), "beautiful bag");
let mut percolate_doc_fields_list: [HashMap; 2] = [percolate_doc_fields1, percolate_doc_fields2];
let mut percolate_doc = HashMap::new();
percolate_doc.insert("documents".to_string(), percolate_doc_fields_list);
let percolate_query = PercolateRequestQuery::new(serde_json::json!(percolate_doc));
let percolate_req = PercolateRequest::new(percolate_query);
search_api.percolate("products", percolate_req).await;docs = [ {title : 'What a nice bag'}, {title : 'Really nice shoes'} ];
res = await searchApi.percolate('test_pq', { query: { percolate: { documents : docs } } } );doc1 := map[string]interface{} {"title": "What a nice bag"}
doc2 := map[string]interface{} {"title": "Really nice shoes"}
query := []interface{} {doc1, doc2}
percolateRequestQuery := manticoreclient.NewPercolateQuery(query)
percolateRequest := manticoreclient.NewPercolateRequest(percolateRequestQuery)
res, _, _ := apiClient.SearchAPI.Percolate(context.Background(), "test_pq").PercolateRequest(*percolateRequest).Execute()+---------------------+------------+------+---------+
| id | query | tags | filters |
+---------------------+------------+------+---------+
| 1657852401006149637 | @title bag | | |
+---------------------+------------+------+---------+
+---------------------+--------------+------+-------------+
| id | query | tags | filters |
+---------------------+--------------+------+-------------+
| 1657852401006149636 | @title shoes | | color='red' |
| 1657852401006149637 | @title bag | | |
+---------------------+--------------+------+-------------+
+---------------------+--------------+------+---------------------------+
| id | query | tags | filters |
+---------------------+--------------+------+---------------------------+
| 1657852401006149635 | @title shoes | | color IN ('blue, 'green') |
| 1657852401006149637 | @title bag | | |
+---------------------+--------------+------+---------------------------+{
"took": 0,
"timed_out": false,
"hits": {
"total": 2,
"max_score": 1,
"hits": [
{
"table": "products",
"_type": "doc",
"_id": 1657852401006149644,
"_score": "1",
"_source": {
"query": {
"ql": "@title bag"
}
},
"fields": {
"_percolator_document_slot": [
2
]
}
},
{
"table": "products",
"_type": "doc",
"_id": 1657852401006149646,
"_score": "1",
"_source": {
"query": {
"ql": "@title shoes"
}
},
"fields": {
"_percolator_document_slot": [
1
]
}
}
]
}
}Array
(
[took] => 23
[timed_out] =>
[hits] => Array
(
[total] => 2
[max_score] => 1
[hits] => Array
(
[0] => Array
(
[_index] => products
[_type] => doc
[_id] => 2810781492890828819
[_score] => 1
[_source] => Array
(
[query] => Array
(
[match] => Array
(
[title] => bag
)
)
)
[fields] => Array
(
[_percolator_document_slot] => Array
(
[0] => 2
)
)
)
[1] => Array
(
[_index] => products
[_type] => doc
[_id] => 2810781492890828821
[_score] => 1
[_source] => Array
(
[query] => Array
(
[match] => Array
(
[title] => shoes
)
)
)
[fields] => Array
(
[_percolator_document_slot] => Array
(
[0] => 1
)
)
)
)
)
){'hits': {'hits': [{u'_id': u'2811025403043381494',
u'table': u'products',
u'_score': u'1',
u'_source': {u'query': {u'ql': u'@title bag'}},
u'_type': u'doc',
u'fields': {u'_percolator_document_slot': [2]}},
{u'_id': u'2811025403043381496',
u'table': u'products',
u'_score': u'1',
u'_source': {u'query': {u'ql': u'@title shoes'}},
u'_type': u'doc',
u'fields': {u'_percolator_document_slot': [1]}}],
'total': 2},
'profile': None,
'timed_out': False,
'took': 0}{'hits': {'hits': [{u'_id': u'2811025403043381494',
u'table': u'products',
u'_score': u'1',
u'_source': {u'query': {u'ql': u'@title bag'}},
u'_type': u'doc',
u'fields': {u'_percolator_document_slot': [2]}},
{u'_id': u'2811025403043381496',
u'table': u'products',
u'_score': u'1',
u'_source': {u'query': {u'ql': u'@title shoes'}},
u'_type': u'doc',
u'fields': {u'_percolator_document_slot': [1]}}],
'total': 2},
'profile': None,
'timed_out': False,
'took': 0}{
"took": 6,
"timed_out": false,
"hits": {
"total": 2,
"hits": [
{
"table": "products",
"_type": "doc",
"_id": 2811045522851233808,
"_score": "1",
"_source": {
"query": {
"ql": "@title bag"
}
},
"fields": {
"_percolator_document_slot": [
2
]
}
},
{
"table": "products",
"_type": "doc",
"_id": 2811045522851233810,
"_score": "1",
"_source": {
"query": {
"ql": "@title shoes"
}
},
"fields": {
"_percolator_document_slot": [
1
]
}
}
]
}
}class SearchResponse {
took: 0
timedOut: false
hits: class SearchResponseHits {
total: 2
maxScore: 1
hits: [{_index=products, _type=doc, _id=2811045522851234133, _score=1, _source={query={ql=@title bag}}, fields={_percolator_document_slot=[2]}}, {_index=products, _type=doc, _id=2811045522851234135, _score=1, _source={query={ql=@title shoes}}, fields={_percolator_document_slot=[1]}}]
aggregations: null
}
profile: null
}class SearchResponse {
took: 0
timedOut: false
hits: class SearchResponseHits {
total: 2
maxScore: 1
hits: [{_index=products, _type=doc, _id=2811045522851234133, _score=1, _source={query={ql=@title bag}}, fields={_percolator_document_slot=[2]}}, {_index=products, _type=doc, _id=2811045522851234135, _score=1, _source={query={ql=@title shoes}}, fields={_percolator_document_slot=[1]}}]
aggregations: null
}
profile: null
}class SearchResponse {
took: 0
timedOut: false
hits: class SearchResponseHits {
total: 2
maxScore: 1
hits: [{_index=products, _type=doc, _id=2811045522851234133, _score=1, _source={query={ql=@title bag}}, fields={_percolator_document_slot=[2]}}, {_index=products, _type=doc, _id=2811045522851234135, _score=1, _source={query={ql=@title shoes}}, fields={_percolator_document_slot=[1]}}]
aggregations: null
}
profile: null
}{
"took": 0,
"timed_out": false,
"hits": {
"total": 2,
"hits": [
{
"table": "test_pq",
"_type": "doc",
"_id": 1657852401006149661,
"_score": "1",
"_source": {
"query": {
"ql": "@title bag"
}
},
"fields": {
"_percolator_document_slot": [
1
]
}
},
{
"table": "test_pq",
"_type": "doc",
"_id": 1657852401006149662,
"_score": "1",
"_source": {
"query": {
"ql": "@title shoes"
}
},
"fields": {
"_percolator_document_slot": [
1
]
}
}
]
}
}{
"took": 0,
"timed_out": false,
"hits": {
"total": 2,
"hits": [
{
"table": "test_pq",
"_type": "doc",
"_id": 1657852401006149661,
"_score": "1",
"_source": {
"query": {
"ql": "@title bag"
}
},
"fields": {
"_percolator_document_slot": [
1
]
}
},
{
"table": "test_pq",
"_type": "doc",
"_id": 1657852401006149662,
"_score": "1",
"_source": {
"query": {
"ql": "@title shoes"
}
},
"fields": {
"_percolator_document_slot": [
1
]
}
}
]
}
}Использование опции 1 as docs позволяет вам увидеть, какие из предоставленных документов соответствуют каким правилам.
- SQL
- JSON
- PHP
- Python
- Python-asyncio
- javascript
- Java
- C#
- Rust
- TypeScript
- Go
CALL PQ('products', '[{"title": "nice pair of shoes", "color": "blue"}, {"title": "beautiful bag"}]', 1 as query, 1 as docs);POST /pq/products/search
{
"query": {
"percolate": {
"documents": [
{"title": "nice pair of shoes", "color": "blue"},
{"title": "beautiful bag"}
]
}
}
}$percolate = [
'table' => 'products',
'body' => [
'query' => [
'percolate' => [
'documents' => [
['title' => 'nice pair of shoes','color'=>'blue'],
['title' => 'beautiful bag']
]
]
]
]
];
$client->pq()->search($percolate);searchApi.percolate('products',{"query":{"percolate":{"documents":[{"title":"nice pair of shoes","color":"blue"},{"title":"beautiful bag"}]}}})await searchApi.percolate('products',{"query":{"percolate":{"documents":[{"title":"nice pair of shoes","color":"blue"},{"title":"beautiful bag"}]}}})res = await searchApi.percolate('products',{"query":{"percolate":{"documents":[{"title":"nice pair of shoes","color":"blue"},{"title":"beautiful bag"}]}}});percolateRequest = new PercolateRequest();
query = new HashMap<String,Object>(){{
put("percolate",new HashMap<String,Object >(){{
put("documents", new ArrayList<Object>(){{
add(new HashMap<String,Object >(){{
put("title","nice pair of shoes");
put("color","blue");
}});
add(new HashMap<String,Object >(){{
put("title","beautiful bag");
}});
}});
}});
}};
percolateRequest.query(query);
searchApi.percolate("products",percolateRequest);var doc1 = new Dictionary<string, Object>();
doc1.Add("title","nice pair of shoes");
doc1.Add("color","blue");
var doc2 = new Dictionary<string, Object>();
doc2.Add("title","beautiful bag");
var docs = new List<Object> {doc1, doc2};
Dictionary<string, Object> percolateDoc = new Dictionary<string, Object> {{ "documents", docs }};
Dictionary<string, Object> query = new Dictionary<string, Object> {{ "percolate", percolateDoc }};
PercolateRequest percolateRequest = new PercolateRequest(query=query);
searchApi.Percolate("products",percolateRequest);let mut percolate_doc_fields1 = HashMap::new();
percolate_doc_fields1.insert("title".to_string(), "nice pair of shoes");
percolate_doc_fields1.insert("color".to_string(), "blue");
let mut percolate_doc_fields2 = HashMap::new();
percolate_doc_fields2.insert("title".to_string(), "beautiful bag");
let mut percolate_doc_fields_list: [HashMap; 2] = [percolate_doc_fields1, percolate_doc_fields2];
let mut percolate_doc = HashMap::new();
percolate_doc.insert("documents".to_string(), percolate_doc_fields_list);
let percolate_query = PercolateRequestQuery::new(serde_json::json!(percolate_doc));
let percolate_req = PercolateRequest::new(percolate_query);
search_api.percolate("products", percolate_req).await;docs = [ {title : 'What a nice bag'}, {title : 'Really nice shoes'} ];
res = await searchApi.percolate('test_pq', { query: { percolate: { documents : docs } } } );doc1 := map[string]interface{} {"title": "What a nice bag"}
doc2 := map[string]interface{} {"title": "Really nice shoes"}
query := []interface{} {doc1, doc2}
percolateRequestQuery := manticoreclient.NewPercolateQuery(query)
percolateRequest := manticoreclient.NewPercolateRequest(percolateRequestQuery)
res, _, _ := apiClient.SearchAPI.Percolate(context.Background(), "test_pq").PercolateRequest(*percolateRequest).Execute()+---------------------+-----------+--------------+------+---------------------------+
| id | documents | query | tags | filters |
+---------------------+-----------+--------------+------+---------------------------+
| 1657852401006149635 | 1 | @title shoes | | color IN ('blue, 'green') |
| 1657852401006149637 | 2 | @title bag | | |
+---------------------+-----------+--------------+------+---------------------------+{
"took": 0,
"timed_out": false,
"hits": {
"total": 2,
"max_score": 1,
"hits": [
{
"table": "products",
"_type": "doc",
"_id": 1657852401006149644,
"_score": "1",
"_source": {
"query": {
"ql": "@title bag"
}
},
"fields": {
"_percolator_document_slot": [
2
]
}
},
{
"table": "products",
"_type": "doc",
"_id": 1657852401006149646,
"_score": "1",
"_source": {
"query": {
"ql": "@title shoes"
}
},
"fields": {
"_percolator_document_slot": [
1
]
}
}
]
}
}Array
(
[took] => 23
[timed_out] =>
[hits] => Array
(
[total] => 2
[max_score] => 1
[hits] => Array
(
[0] => Array
(
[_index] => products
[_type] => doc
[_id] => 2810781492890828819
[_score] => 1
[_source] => Array
(
[query] => Array
(
[match] => Array
(
[title] => bag
)
)
)
[fields] => Array
(
[_percolator_document_slot] => Array
(
[0] => 2
)
)
)
[1] => Array
(
[_index] => products
[_type] => doc
[_id] => 2810781492890828821
[_score] => 1
[_source] => Array
(
[query] => Array
(
[match] => Array
(
[title] => shoes
)
)
)
[fields] => Array
(
[_percolator_document_slot] => Array
(
[0] => 1
)
)
)
)
)
){'hits': {'hits': [{u'_id': u'2811025403043381494',
u'table': u'products',
u'_score': u'1',
u'_source': {u'query': {u'ql': u'@title bag'}},
u'_type': u'doc',
u'fields': {u'_percolator_document_slot': [2]}},
{u'_id': u'2811025403043381496',
u'table': u'products',
u'_score': u'1',
u'_source': {u'query': {u'ql': u'@title shoes'}},
u'_type': u'doc',
u'fields': {u'_percolator_document_slot': [1]}}],
'total': 2},
'profile': None,
'timed_out': False,
'took': 0}{'hits': {'hits': [{u'_id': u'2811025403043381494',
u'table': u'products',
u'_score': u'1',
u'_source': {u'query': {u'ql': u'@title bag'}},
u'_type': u'doc',
u'fields': {u'_percolator_document_slot': [2]}},
{u'_id': u'2811025403043381496',
u'table': u'products',
u'_score': u'1',
u'_source': {u'query': {u'ql': u'@title shoes'}},
u'_type': u'doc',
u'fields': {u'_percolator_document_slot': [1]}}],
'total': 2},
'profile': None,
'timed_out': False,
'took': 0}{
"took": 6,
"timed_out": false,
"hits": {
"total": 2,
"hits": [
{
"table": "products",
"_type": "doc",
"_id": 2811045522851233808,
"_score": "1",
"_source": {
"query": {
"ql": "@title bag"
}
},
"fields": {
"_percolator_document_slot": [
2
]
}
},
{
"table": "products",
"_type": "doc",
"_id": 2811045522851233810,
"_score": "1",
"_source": {
"query": {
"ql": "@title shoes"
}
},
"fields": {
"_percolator_document_slot": [
1
]
}
}
]
}
}class SearchResponse {
took: 0
timedOut: false
hits: class SearchResponseHits {
total: 2
maxScore: 1
hits: [{_index=products, _type=doc, _id=2811045522851234133, _score=1, _source={query={ql=@title bag}}, fields={_percolator_document_slot=[2]}}, {_index=products, _type=doc, _id=2811045522851234135, _score=1, _source={query={ql=@title shoes}}, fields={_percolator_document_slot=[1]}}]
aggregations: null
}
profile: null
}class SearchResponse {
took: 0
timedOut: false
hits: class SearchResponseHits {
total: 2
maxScore: 1
hits: [{_index=products, _type=doc, _id=2811045522851234133, _score=1, _source={query={ql=@title bag}}, fields={_percolator_document_slot=[2]}}, {_index=products, _type=doc, _id=2811045522851234135, _score=1, _source={query={ql=@title shoes}}, fields={_percolator_document_slot=[1]}}]
aggregations: null
}
profile: null
}class SearchResponse {
took: 0
timedOut: false
hits: class SearchResponseHits {
total: 2
maxScore: 1
hits: [{_index=products, _type=doc, _id=2811045522851234133, _score=1, _source={query={ql=@title bag}}, fields={_percolator_document_slot=[2]}}, {_index=products, _type=doc, _id=2811045522851234135, _score=1, _source={query={ql=@title shoes}}, fields={_percolator_document_slot=[1]}}]
aggregations: null
}
profile: null
}{
"took": 0,
"timed_out": false,
"hits": {
"total": 2,
"hits": [
{
"table": "test_pq",
"_type": "doc",
"_id": 1657852401006149661,
"_score": "1",
"_source": {
"query": {
"ql": "@title bag"
}
},
"fields": {
"_percolator_document_slot": [
1
]
}
},
{
"table": "test_pq",
"_type": "doc",
"_id": 1657852401006149662,
"_score": "1",
"_source": {
"query": {
"ql": "@title shoes"
}
},
"fields": {
"_percolator_document_slot": [
1
]
}
}
]
}
}{
"took": 0,
"timed_out": false,
"hits": {
"total": 2,
"hits": [
{
"table": "test_pq",
"_type": "doc",
"_id": 1657852401006149661,
"_score": "1",
"_source": {
"query": {
"ql": "@title bag"
}
},
"fields": {
"_percolator_document_slot": [
1
]
}
},
{
"table": "test_pq",
"_type": "doc",
"_id": 1657852401006149662,
"_score": "1",
"_source": {
"query": {
"ql": "@title shoes"
}
},
"fields": {
"_percolator_document_slot": [
1
]
}
}
]
}
}По умолчанию идентификаторы совпадающих документов соответствуют их относительным номерам в предоставленном вами списке. Однако в некоторых случаях у каждого документа уже есть свой собственный идентификатор. Для этого случая существует опция 'id field name' as docs_id для CALL PQ.
Обратите внимание, что если идентификатор не может быть найден по указанному имени поля, правило PQ не будет отображено в результатах.
Эта опция доступна только для CALL PQ через SQL.
- SQL
CALL PQ('products', '[{"id": 123, "title": "nice pair of shoes", "color": "blue"}, {"id": 456, "title": "beautiful bag"}]', 1 as query, 'id' as docs_id, 1 as docs);+---------------------+-----------+--------------+------+---------------------------+
| id | documents | query | tags | filters |
+---------------------+-----------+--------------+------+---------------------------+
| 1657852401006149664 | 456 | @title bag | | |
| 1657852401006149666 | 123 | @title shoes | | color IN ('blue, 'green') |
+---------------------+-----------+--------------+------+---------------------------+При использовании CALL PQ с отдельными JSON можно использовать опцию 1 as skip_bad_json, чтобы пропускать любые неверные JSON в вводе. В приведенном ниже примере 2-й запрос не выполняется из-за неверного JSON, но 3-й запрос избегает ошибки, используя 1 as skip_bad_json. Учтите, что эта опция недоступна при отправке JSON-запросов через HTTP, так как в этом случае весь JSON-запрос должен быть валиден.
- SQL
CALL PQ('products', ('{"title": "nice pair of shoes", "color": "blue"}', '{"title": "beautiful bag"}'));
CALL PQ('products', ('{"title": "nice pair of shoes", "color": "blue"}', '{"title": "beautiful bag}'));
CALL PQ('products', ('{"title": "nice pair of shoes", "color": "blue"}', '{"title": "beautiful bag}'), 1 as skip_bad_json);+---------------------+
| id |
+---------------------+
| 1657852401006149635 |
| 1657852401006149637 |
+---------------------+
ERROR 1064 (42000): Bad JSON objects in strings: 2
+---------------------+
| id |
+---------------------+
| 1657852401006149635 |
+---------------------+Percolate-запросы разработаны с учетом высокой пропускной способности и больших объемов данных. Для оптимизации производительности с целью снижения задержки и увеличения пропускной способности рассмотрите следующее.
Существует два режима распределения для percolate-таблицы и того, как percolate-запрос может работать с ней:
- Разреженный (по умолчанию). Идеален для: множества документов, зеркальных PQ-таблиц. Когда ваш набор документов велик, но набор запросов, хранящихся в PQ-таблице, мал, разреженный режим выгоден. В этом режиме пакет документов, который вы передаете, будет разделен между количеством агентов, так что каждый узел обрабатывает только часть документов из вашего запроса. Manticore разделяет ваш набор документов и распределяет фрагменты между зеркалами. Как только агенты завершат обработку запросов, Manticore собирает и объединяет результаты, возвращая итоговый набор запросов, как если бы он поступил из одной таблицы. Используйте репликацию, чтобы помочь процессу.
- Сегментированный. Идеален для: множества PQ-правил, правил, разделенных между PQ-таблицами. В этом режиме весь набор документов рассылается на все таблицы распределенной PQ-таблицы без первоначального разделения документов. Это полезно, когда передается относительно небольшой набор документов, но количество хранимых запросов велико. В этом случае более уместно хранить только часть PQ-правил на каждом узле, а затем объединять результаты, возвращенные от узлов, которые обрабатывают один и тот же набор документов с разными наборами PQ-правил. Этот режим должен быть установлен явно, так как он подразумевает увеличение сетевой нагрузки и ожидает таблицы с разными PQ, что репликация не может сделать из коробки.
Предположим, у вас есть таблица pq_d2, определенная как:
table pq_d2
{
type = distributed
agent = 127.0.0.1:6712:pq
agent = 127.0.0.1:6712:ptitle
}
Каждая из 'pq' и 'ptitle' содержит:
- SQL
- JSON
- PHP
- Python
- Python-asyncio
- javascript
- Java
- C#
- Rust
- TypeScript
- Go
SELECT * FROM pq;POST /pq/pq/search$params = [
'table' => 'pq',
'body' => [
]
];
$response = $client->pq()->search($params);searchApi.search({"table":"pq","query":{"match_all":{}}})await searchApi.search({"table":"pq","query":{"match_all":{}}})res = await searchApi.search({"table":"pq","query":{"match_all":{}}});Map<String,Object> query = new HashMap<String,Object>();
query.put("match_all",null);
SearchRequest searchRequest = new SearchRequest();
searchRequest.setIndex("pq");
searchRequest.setQuery(query);
SearchResponse searchResponse = searchApi.search(searchRequest);object query = new { match_all=null };
SearchRequest searchRequest = new SearchRequest("pq", query);
SearchResponse searchResponse = searchApi.Search(searchRequest);let query = SearchQuery::new();
let search_req = SearchRequest {
table: "pq".to_string(),
query: Some(Box::new(query)),
..Default::default(),
};
let search_res = search_api.search(search_req).await;res = await searchApi.search({"table":"test_pq","query":{"match_all":{}}});query := map[string]interface{} {}
percolateRequestQuery := manticoreclient.NewPercolateRequestQuery(query)
percolateRequest := manticoreclient.NewPercolateRequest(percolateRequestQuery)
res, _, _ := apiClient.SearchAPI.Percolate(context.Background(), "test_pq").PercolateRequest(*percolateRequest).Execute()+------+-------------+------+-------------------+
| id | query | tags | filters |
+------+-------------+------+-------------------+
| 1 | filter test | | gid>=10 |
| 2 | angry | | gid>=10 OR gid<=3 |
+------+-------------+------+-------------------+
2 rows in set (0.01 sec){
"took":0,
"timed_out":false,
"hits":{
"total":2,
"hits":[
{
"_id": 1,
"_score":1,
"_source":{
"query":{ "ql":"filter test" },
"tags":"",
"filters":"gid>=10"
}
},
{
"_id": 2,
"_score":1,
"_source":{
"query":{"ql":"angry"},
"tags":"",
"filters":"gid>=10 OR gid<=3"
}
}
]
}
}(
[took] => 0
[timed_out] =>
[hits] =>
(
[total] => 2
[hits] =>
(
[0] =>
(
[_id] => 1
[_score] => 1
[_source] =>
(
[query] =>
(
[ql] => filter test
)
[tags] =>
[filters] => gid>=10
)
),
[1] =>
(
[_id] => 1
[_score] => 1
[_source] =>
(
[query] =>
(
[ql] => angry
)
[tags] =>
[filters] => gid>=10 OR gid<=3
)
)
)
)
){'hits': {'hits': [{u'_id': u'2811025403043381501',
u'_score': 1,
u'_source': {u'filters': u"gid>=10",
u'query': u'filter test',
u'tags': u''}},
{u'_id': u'2811025403043381502',
u'_score': 1,
u'_source': {u'filters': u"gid>=10 OR gid<=3",
u'query': u'angry',
u'tags': u''}}],
'total': 2},
'profile': None,
'timed_out': False,
'took': 0}{'hits': {'hits': [{u'_id': u'2811025403043381501',
u'_score': 1,
u'_source': {u'filters': u"gid>=10",
u'query': u'filter test',
u'tags': u''}},
{u'_id': u'2811025403043381502',
u'_score': 1,
u'_source': {u'filters': u"gid>=10 OR gid<=3",
u'query': u'angry',
u'tags': u''}}],
'total': 2},
'profile': None,
'timed_out': False,
'took': 0}{"hits": {"hits": [{"_id": 2811025403043381501,
"_score": 1,
"_source": {"filters": u"gid>=10",
"query": "filter test",
"tags": ""}},
{"_id": 2811025403043381502,
"_score": 1,
"_source": {"filters": u"gid>=10 OR gid<=3",
"query": "angry",
"tags": ""}}],
"total": 2},
"timed_out": false,
"took": 0}class SearchResponse {
took: 0
timedOut: false
hits: class SearchResponseHits {
total: 2
maxScore: null
hits: [{_id=2811045522851233962, _score=1, _source={filters=gid>=10, query=filter test, tags=}}, {_id=2811045522851233951, _score=1, _source={filters=gid>=10 OR gid<=3, query=angry,tags=}}]
aggregations: null
}
profile: null
}class SearchResponse {
took: 0
timedOut: false
hits: class SearchResponseHits {
total: 2
maxScore: null
hits: [{_id=2811045522851233962, _score=1, _source={filters=gid>=10, query=filter test, tags=}}, {_id=2811045522851233951, _score=1, _source={filters=gid>=10 OR gid<=3, query=angry,tags=}}]
aggregations: null
}
profile: null
}class SearchResponse {
took: 0
timedOut: false
hits: class SearchResponseHits {
total: 2
maxScore: null
hits: [{_id=2811045522851233962, _score=1, _source={filters=gid>=10, query=filter test, tags=}}, {_id=2811045522851233951, _score=1, _source={filters=gid>=10 OR gid<=3, query=angry,tags=}}]
aggregations: null
}
profile: null
}{
'hits':
{
'hits':
[{
'_id': '2811025403043381501',
'_score': 1,
'_source':
{
'filters': "gid>=10",
'query': 'filter test',
'tags': ''
}
},
{
'_id':
'2811025403043381502',
'_score': 1,
'_source':
{
'filters': "gid>=10 OR gid<=3",
'query': 'angry',
'tags': ''
}
}],
'total': 2
},
'profile': None,
'timed_out': False,
'took': 0
}{
'hits':
{
'hits':
[{
'_id': '2811025403043381501',
'_score': 1,
'_source':
{
'filters': "gid>=10",
'query': 'filter test',
'tags': ''
}
},
{
'_id':
'2811025403043381502',
'_score': 1,
'_source':
{
'filters': "gid>=10 OR gid<=3",
'query': 'angry',
'tags': ''
}
}],
'total': 2
},
'profile': None,
'timed_out': False,
'took': 0
}И вы выполняете CALL PQ на распределенной таблице с парой документов.
- SQL
- JSON
- PHP
- Python
- Python-asyncio
- javascript
- Java
- C#
- Rust
- TypeScript
- Go
CALL PQ ('pq_d2', ('{"title":"angry test", "gid":3 }', '{"title":"filter test doc2", "gid":13}'), 1 AS docs);POST /pq/pq/search -d '
"query":
{
"percolate":
{
"documents" : [
{ "title": "angry test", "gid": 3 },
{ "title": "filter test doc2", "gid": 13 }
]
}
}
'$params = [
'table' => 'pq',
'body' => [
'query' => [
'percolate' => [
'documents' => [
[
'title'=>'angry test',
'gid' => 3
],
[
'title'=>'filter test doc2',
'gid' => 13
],
]
]
]
]
];
$response = $client->pq()->search($params);searchApi.percolate('pq',{"percolate":{"documents":[{"title":"angry test","gid":3},{"title":"filter test doc2","gid":13}]}})await searchApi.percolate('pq',{"percolate":{"documents":[{"title":"angry test","gid":3},{"title":"filter test doc2","gid":13}]}})res = await searchApi.percolate('pq',{"percolate":{"documents":[{"title":"angry test","gid":3},{"title":"filter test doc2","gid":13}]}});percolateRequest = new PercolateRequest();
query = new HashMap<String,Object>(){{
put("percolate",new HashMap<String,Object >(){{
put("documents", new ArrayList<Object>(){{
add(new HashMap<String,Object >(){{
put("title","angry test");
put("gid",3);
}});
add(new HashMap<String,Object >(){{
put("title","filter test doc2");
put("gid",13);
}});
}});
}});
}};
percolateRequest.query(query);
searchApi.percolate("pq",percolateRequest);var doc1 = new Dictionary<string, Object>();
doc1.Add("title","angry test");
doc1.Add("gid",3);
var doc2 = new Dictionary<string, Object>();
doc2.Add("title","filter test doc2");
doc2.Add("gid",13);
var docs = new List<Object> {doc1, doc2};
Dictionary<string, Object> percolateDoc = new Dictionary<string, Object> {{ "documents", docs }};
Dictionary<string, Object> query = new Dictionary<string, Object> {{ "percolate", percolateDoc }};
PercolateRequest percolateRequest = new PercolateRequest(query=query);
searchApi.Percolate("pq",percolateRequest);let mut percolate_doc_fields1 = HashMap::new();
percolate_doc_fields1.insert("title".to_string(), "angry test");
percolate_doc_fields1.insert("gid".to_string(), 3);
let mut percolate_doc_fields2 = HashMap::new();
percolate_doc_fields2.insert("title".to_string(), "filter test doc2");
percolate_doc_fields2.insert("gid".to_string(), 13);
let mut percolate_doc_fields_list: [HashMap; 2] = [percolate_doc_fields1, percolate_doc_fields2];
let mut percolate_doc = HashMap::new();
percolate_doc.insert("documents".to_string(), percolate_doc_fields_list);
let percolate_query = PercolateRequestQuery::new(serde_json::json!(percolate_doc));
let percolate_req = PercolateRequest::new(percolate_query);
search_api.percolate("pq", percolate_req).await;docs = [ {title : 'What a nice bag'}, {title : 'Really nice shoes'} ];
res = await searchApi.percolate('test_pq', { query: { percolate: { documents : docs } } } );doc1 := map[string]interface{} {"title": "What a nice bag"}
doc2 := map[string]interface{} {"title": "Really nice shoes"}
query := []interface{} {doc1, doc2}
percolateRequestQuery := manticoreclient.NewPercolateQuery(query)
percolateRequest := manticoreclient.NewPercolateRequest(percolateRequestQuery)
res, _, _ := apiClient.SearchAPI.Percolate(context.Background(), "test_pq").PercolateRequest(*percolateRequest).Execute()+------+-----------+
| id | documents |
+------+-----------+
| 1 | 2 |
| 2 | 1 |
+------+-----------+{
"took":0,
"timed_out":false,
"hits":{
"total":2,"hits":[
{
"_id": 2,
"_score":1,
"_source":{
"query":{"title":"angry"},
"tags":"",
"filters":"gid>=10 OR gid<=3"
}
}
{
"_id": 1,
"_score":1,
"_source":{
"query":{"ql":"filter test"},
"tags":"",
"filters":"gid>=10"
}
},
]
}
}(
[took] => 0
[timed_out] =>
[hits] =>
(
[total] => 2
[hits] =>
(
[0] =>
(
[_index] => pq
[_type] => doc
[_id] => 2
[_score] => 1
[_source] =>
(
[query] =>
(
[ql] => angry
)
[tags] =>
[filters] => gid>=10 OR gid<=3
),
[fields] =>
(
[_percolator_document_slot] =>
(
[0] => 1
)
)
),
[1] =>
(
[_index] => pq
[_id] => 1
[_score] => 1
[_source] =>
(
[query] =>
(
[ql] => filter test
)
[tags] =>
[filters] => gid>=10
)
[fields] =>
(
[_percolator_document_slot] =>
(
[0] => 0
)
)
)
)
)
){'hits': {'hits': [{u'_id': u'2811025403043381480',
u'table': u'pq',
u'_score': u'1',
u'_source': {u'query': {u'ql': u'angry'},u'tags':u'',u'filters':u"gid>=10 OR gid<=3"},
u'_type': u'doc',
u'fields': {u'_percolator_document_slot': [1]}},
{u'_id': u'2811025403043381501',
u'table': u'pq',
u'_score': u'1',
u'_source': {u'query': {u'ql': u'filter test'},u'tags':u'',u'filters':u"gid>=10"},
u'_type': u'doc',
u'fields': {u'_percolator_document_slot': [1]}}],
'total': 2},
'profile': None,
'timed_out': False,
'took': 0}{'hits': {'hits': [{u'_id': u'2811025403043381480',
u'table': u'pq',
u'_score': u'1',
u'_source': {u'query': {u'ql': u'angry'},u'tags':u'',u'filters':u"gid>=10 OR gid<=3"},
u'_type': u'doc',
u'fields': {u'_percolator_document_slot': [1]}},
{u'_id': u'2811025403043381501',
u'table': u'pq',
u'_score': u'1',
u'_source': {u'query': {u'ql': u'filter test'},u'tags':u'',u'filters':u"gid>=10"},
u'_type': u'doc',
u'fields': {u'_percolator_document_slot': [1]}}],
'total': 2},
'profile': None,
'timed_out': False,
'took': 0}{'hits': {'hits': [{u'_id': u'2811025403043381480',
u'table': u'pq',
u'_score': u'1',
u'_source': {u'query': {u'ql': u'angry'},u'tags':u'',u'filters':u"gid>=10 OR gid<=3"},
u'_type': u'doc',
u'fields': {u'_percolator_document_slot': [1]}},
{u'_id': u'2811025403043381501',
u'table': u'pq',
u'_score': u'1',
u'_source': {u'query': {u'ql': u'filter test'},u'tags':u'',u'filters':u"gid>=10"},
u'_type': u'doc',
u'fields': {u'_percolator_document_slot': [1]}}],
'total': 2},
'profile': None,
'timed_out': False,
'took': 0}class SearchResponse {
took: 10
timedOut: false
hits: class SearchResponseHits {
total: 2
maxScore: 1
hits: [{_index=pq, _type=doc, _id=2811045522851234165, _score=1, _source={query={ql=@title angry}}, fields={_percolator_document_slot=[1]}}, {_index=pq, _type=doc, _id=2811045522851234166, _score=1, _source={query={ql=@title filter test doc2}}, fields={_percolator_document_slot=[2]}}]
aggregations: null
}
profile: null
}class SearchResponse {
took: 10
timedOut: false
hits: class SearchResponseHits {
total: 2
maxScore: 1
hits: [{_index=pq, _type=doc, _id=2811045522851234165, _score=1, _source={query={ql=@title angry}}, fields={_percolator_document_slot=[1]}}, {_index=pq, _type=doc, _id=2811045522851234166, _score=1, _source={query={ql=@title filter test doc2}}, fields={_percolator_document_slot=[2]}}]
aggregations: null
}
profile: null
}class SearchResponse {
took: 10
timedOut: false
hits: class SearchResponseHits {
total: 2
maxScore: 1
hits: [{_index=pq, _type=doc, _id=2811045522851234165, _score=1, _source={query={ql=@title angry}}, fields={_percolator_document_slot=[1]}}, {_index=pq, _type=doc, _id=2811045522851234166, _score=1, _source={query={ql=@title filter test doc2}}, fields={_percolator_document_slot=[2]}}]
aggregations: null
}
profile: null
}{
"took": 0,
"timed_out": false,
"hits": {
"total": 2,
"hits": [
{
"table": "test_pq",
"_type": "doc",
"_id": 1657852401006149661,
"_score": "1",
"_source": {
"query": {
"ql": "@title bag"
}
},
"fields": {
"_percolator_document_slot": [
1
]
}
},
{
"table": "test_pq",
"_type": "doc",
"_id": 1657852401006149662,
"_score": "1",
"_source": {
"query": {
"ql": "@title shoes"
}
},
"fields": {
"_percolator_document_slot": [
1
]
}
}
]
}
}{
"took": 0,
"timed_out": false,
"hits": {
"total": 2,
"hits": [
{
"table": "test_pq",
"_type": "doc",
"_id": 1657852401006149661,
"_score": "1",
"_source": {
"query": {
"ql": "@title bag"
}
},
"fields": {
"_percolator_document_slot": [
1
]
}
},
{
"table": "test_pq",
"_type": "doc",
"_id": 1657852401006149662,
"_score": "1",
"_source": {
"query": {
"ql": "@title shoes"
}
},
"fields": {
"_percolator_document_slot": [
1
]
}
}
]
}
}В предыдущем примере мы использовали режим по умолчанию разреженный. Чтобы продемонстрировать сегментированный режим, давайте создадим распределенную PQ-таблицу, состоящую из 2 локальных PQ-таблиц, и добавим 2 документа в "products1" и 1 документ в "products2":
create table products1(title text, color string) type='pq';
create table products2(title text, color string) type='pq';
create table products_distributed type='distributed' local='products1' local='products2';
INSERT INTO products1(query) values('@title bag');
INSERT INTO products1(query,filters) values('@title shoes', 'color=\'red\'');
INSERT INTO products2(query,filters) values('@title shoes', 'color in (\'blue\', \'green\')');
Теперь, если вы добавите 'sharded' as mode в CALL PQ, он отправит документы во все таблицы агентов (в данном случае, просто локальные таблицы, но они могут быть удаленными, чтобы использовать внешнее оборудование). Этот режим недоступен через JSON-интерфейс.
- SQL
CALL PQ('products_distributed', ('{"title": "nice pair of shoes", "color": "blue"}', '{"title": "beautiful bag"}'), 'sharded' as mode, 1 as query);+---------------------+--------------+------+---------------------------+
| id | query | tags | filters |
+---------------------+--------------+------+---------------------------+
| 1657852401006149639 | @title bag | | |
| 1657852401006149643 | @title shoes | | color IN ('blue, 'green') |
+---------------------+--------------+------+---------------------------+Обратите внимание, что синтаксис зеркал агентов в конфигурации (когда несколько хостов назначаются одной строке agent, разделенных |) не имеет ничего общего с режимом запроса CALL PQ. Каждый agent всегда представляет один узел, независимо от количества зеркал высокой доступности, указанных для этого агента.
В некоторых случаях вам может понадобиться получить более подробную информацию о производительности percolate-запроса. Для этой цели существует опция 1 as verbose, которая доступна только через SQL и позволяет сохранить больше метрик производительности. Вы можете увидеть их с помощью запроса SHOW META, который можно выполнить после CALL PQ. Подробнее см. в SHOW META.
- 1 as verbose
- 0 as verbose
CALL PQ('products', ('{"title": "nice pair of shoes", "color": "blue"}', '{"title": "beautiful bag"}'), 1 as verbose); show meta;CALL PQ('products', ('{"title": "nice pair of shoes", "color": "blue"}', '{"title": "beautiful bag"}'), 0 as verbose); show meta;+---------------------+
| id |
+---------------------+
| 1657852401006149644 |
| 1657852401006149646 |
+---------------------+
+-------------------------+-----------+
| Variable name | Value |
+-------------------------+-----------+
| total | 0.000 sec |
| setup | 0.000 sec |
| queries_matched | 2 |
| queries_failed | 0 |
| document_matched | 2 |
| total_queries_stored | 3 |
| term_only_queries | 3 |
| fast_rejected_queries | 0 |
| time_per_query | 27, 10 |
| time_of_matched_queries | 37 |
+-------------------------+-----------++---------------------+
| id |
+---------------------+
| 1657852401006149644 |
| 1657852401006149646 |
+---------------------+
+-----------------------+-----------+
| Variable name | Value |
+-----------------------+-----------+
| total | 0.000 sec |
| queries_matched | 2 |
| queries_failed | 0 |
| document_matched | 2 |
| total_queries_stored | 3 |
| term_only_queries | 3 |
| fast_rejected_queries | 0 |
+-----------------------+-----------+Автозаполнение, или дополнение слов, предсказывает и предлагает окончание слова или фразы во время набора текста. Его обычно используют в:
- Поисковых строках на сайтах
- Подсказках в поисковых системах
- Текстовых полях в приложениях
Manticore предлагает продвинутую функцию автозаполнения, которая предлагает варианты во время ввода, подобно известным поисковым системам. Это помогает ускорить поиск и позволяет пользователям быстрее находить нужное.
Кроме базовой функции автозаполнения, Manticore включает расширенные возможности для улучшения пользовательского опыта:
- Коррекция орфографии (Fuzziness): Автозаполнение Manticore помогает исправлять ошибки использования, используя алгоритмы, распознающие и исправляющие распространённые ошибки. Это значит, что даже если вы ввели что-то неправильно, вы всё равно сможете найти нужное.
- Автоматическое определение раскладки клавиатуры: Manticore может определить, какую раскладку клавиатуры вы используете. Это особенно полезно в регионах с множеством языков или если вы случайно вводите текст на неправильном языке. Например, если вы ошибочно набрали «ghbdtn», Manticore понимает, что вы хотели написать «привет» и предлагает правильное слово.
Автозаполнение Manticore можно настроить под разные нужды и параметры, что делает его гибким инструментом для множества приложений.

ПРИМЕЧАНИЕ:
CALL AUTOCOMPLETEи/autocompleteтребуют Manticore Buddy. Если не работает, убедитесь, что Buddy установлен.
Для использования автозаполнения в Manticore используйте SQL-запрос CALL AUTOCOMPLETE или его JSON-эквивалент /autocomplete. Эта функция предлагает варианты дополнения слов на основе ваших индексированных данных.
Перед использованием убедитесь, что в таблице, которую вы планируете использовать для автозаполнения, включены инфиксы.
Важно: В настройках таблицы есть автоматическая проверка параметра min_infix_len, которая использует кэш на 30 секунд для повышения производительности CALL AUTOCOMPLETE. После изменения таблицы может быть небольшая задержка при первом вызове CALL AUTOCOMPLETE (обычно незаметная). В кэш сохраняются только успешные результаты, поэтому если вы удалите таблицу или отключите min_infix_len, CALL AUTOCOMPLETE временно может возвращать неверные результаты, пока не начнёт показывать ошибку, связанную с min_infix_len.
CALL AUTOCOMPLETE('query_beginning', 'table', [...options]);
POST /autocomplete
{
"table":"table_name",
"query":"query_beginning"
[,"options": {<autocomplete options>}]
}
layouts: Строка, состоящая из кодов раскладок клавиатуры через запятую, для определения ошибок ввода, вызванных неправильной раскладкой (например, «ghbdtn» вместо «привет»). Manticore сравнивает позиции символов в разных раскладках, чтобы предлагать исправления. Для эффективного определения несоответствий нужны как минимум 2 раскладки. Доступные варианты: us, ru, ua, se, pt, no, it, gr, uk, fr, es, dk, de, ch, br, bg, be (подробнее здесь). По умолчанию: nonefuzziness:0,1или2(по умолчанию:2). Максимальное расстояние Левенштейна для поиска опечаток. Установите0для отключения нечёткого поискаpreserve:0или1(по умолчанию:0). При значении1в результаты сохраняются слова без успешных нечётких совпадений (например, «hello wrld» вернёт «hello wrld» и «hello world»). При значении0возвращаются только слова с успешными нечёткими совпадениями (например, «hello wrld» вернёт только «hello world»). Особенно полезно для сохранения коротких слов или собственных имён, которые могут отсутствовать в Manticore Searchprepend: Логическое (0/1 в SQL). Если true(1), добавляет звёздочку перед последним словом для префиксного расширения (например,*word)append: Логическое (0/1 в SQL). Если true(1), добавляет звёздочку после последнего слова для суффиксного расширения (например,word*)expansion_len: Количество символов для расширения последнего слова. По умолчанию:10force_bigrams: Логическое (0/1 в SQL). Принудительно использует биграммы (2-символьные n-граммы) вместо триграмм для всех длин слов, что может улучшить поиск для слов с ошибками перестановки символов. По умолчанию:0(используются триграммы для слов длиной ≥6 символов)
- SQL
- SQL with no fuzzy search
- JSON
- SQL with preserve option
- JSON with preserve option
mysql> CALL AUTOCOMPLETE('hello', 'comment');
+------------+
| query |
+------------+
| hello |
| helio |
| hell |
| shell |
| nushell |
| powershell |
| well |
| help |
+------------+mysql> CALL AUTOCOMPLETE('hello', 'comment', 0 as fuzziness);
+-------+
| query |
+-------+
| hello |
+-------+POST /autocomplete
{
"table":"comment",
"query":"hello"
}mysql> CALL AUTOCOMPLETE('hello wrld', 'comment', 1 as preserve);
+------------+
| query |
+------------+
| hello wrld |
| hello world|
+------------+POST /autocomplete
{
"table":"comment",
"query":"hello wrld",
"options": {
"preserve": 1
}
}[
{
"total": 8,
"error": "",
"warning": "",
"columns": [
{
"query": {
"type": "string"
}
}
],
"data": [
{
"query": "hello"
},
{
"query": "helio"
},
{
"query": "hell"
},
{
"query": "shell"
},
{
"query": "nushell"
},
{
"query": "powershell"
},
{
"query": "well"
},
{
"query": "help"
}
]
}
][
{
"total": 2,
"error": "",
"warning": "",
"columns": [
{
"query": {
"type": "string"
}
}
],
"data": [
{
"query": "hello wrld"
},
{
"query": "hello world"
}
]
}
]Опция force_bigrams помогает с словами, в которых есть ошибки перестановки символов, такими как «ipohne» вместо «iphone». Используя биграммы вместо триграмм, алгоритм лучше справляется с перестановками символов.
- SQL
- JSON
mysql> CALL AUTOCOMPLETE('ipohne', 'products', 1 as force_bigrams);POST /autocomplete
{
"table":"products",
"query":"ipohne",
"options": {
"force_bigrams": 1
}
}+--------+
| query |
+--------+
| iphone |
+--------+[
{
"total": 1,
"error": "",
"warning": "",
"columns": [
{
"query": {
"type": "string"
}
}
],
"data": [
{
"query": "iphone"
}
]
}
]- Демонстрация демонстрирующая функциональность автозаполнения:

- Блог о нечетком поиске и автозаполнении - https://manticoresearch.com/blog/new-fuzzy-search-and-autocomplete/
Хотя CALL AUTOCOMPLETE является рекомендуемым методом для большинства случаев, Manticore также поддерживает другие управляемые и настраиваемые подходы для реализации функции автозаполнения:
Чтобы автозаполнить предложение, можно использовать инфиксный поиск. Можно найти конец поля документа, указав его начало и:
- используя оператор подстановки full-text
*для совпадения с любыми символами - при желании используя
^для начала с начала поля - при желании используя
""для поиска фраз - и используя подсветку результатов
В нашем блоге есть статья об этом и интерактивный курс. Быстрый пример:
- Предположим, у вас есть документ:
Мой кот любит мою собаку. Кот (Felis catus) - это домашний вид небольшого плотоядного млекопитающего. - Тогда вы можете использовать
^,""и*, так что, пока пользователь печатает, вы делаете запросы, такие как:^"м*",^"мой *",^"мой к*",^"мой ко*"и так далее - Это найдет документ, и если вы также сделаете выделение, вы получите что-то вроде:
<b>Мой кот</b> любит мою собаку. Кот ( ...
В некоторых случаях все, что вам нужно, это автозаполнить одно слово или пару слов. В этом случае вы можете использовать CALL KEYWORDS.
CALL KEYWORDS доступен через SQL интерфейс и предлагает способ изучить, как ключевые слова токенизируются или получить токенизированные формы конкретных ключевых слов. Если таблица позволяет инфиксы, это позволяет вам быстро находить возможные окончания для заданных ключевых слов, что делает его подходящим для функции автозаполнения.
Это отличная альтернатива общему инфиксному поиску, так как она обеспечивает более высокую производительность, поскольку ей нужен только словарь таблицы, а не сами документы.
CALL KEYWORDS(text, table [, options])
Оператор CALL KEYWORDS делит текст на ключевые слова. Он возвращает токенизированные и нормализованные формы ключевых слов, а также, если это необходимо, статистику ключевых слов. Кроме того, он предоставляет позицию каждого ключевого слова в запросе и все формы токенизированных ключевых слов, когда таблица позволяет лемматизаторы.
| Параметр | Описание |
|---|---|
| text | Текст для разбивки на ключевые слова |
| table | Имя таблицы, из которой брать настройки обработки текста |
| 0/1 как stats | Показать статистику ключевых слов, по умолчанию 0 |
| 0/1 как fold_wildcards | Сложить подстановочные знаки, по умолчанию 0 |
| 0/1 как fold_lemmas | Сложить морфологические леммы, по умолчанию 0 |
| 0/1 как fold_blended | Сложить смешанные слова, по умолчанию 0 |
| N как expansion_limit | Переопределить expansion_limit, определенный в конфигурации сервера, по умолчанию 0 (использовать значение из конфигурации) |
| docs/hits как sort_mode | Сортирует выходные результаты по 'docs' или 'hits'. По умолчанию сортировка не применяется. |
| jieba_mode | Режим сегментации Jieba для запроса. См. jieba_mode для получения дополнительных сведений |
Примеры показывают, как это работает, если предположить, что пользователь пытается получить автозаполнение для "мой кот ...". Таким образом, на стороне приложения все, что вам нужно сделать, это предложить пользователю окончания из столбца "нормализованный" для каждого нового слова. Часто имеет смысл сортировать по hits или docs, используя 'hits' как sort_mode или 'docs' как sort_mode.
- Examples
MySQL [(none)]> CALL KEYWORDS('m*', 't', 1 as stats);
+------+-----------+------------+------+------+
| qpos | tokenized | normalized | docs | hits |
+------+-----------+------------+------+------+
| 1 | m* | my | 1 | 2 |
| 1 | m* | mammal | 1 | 1 |
+------+-----------+------------+------+------+
MySQL [(none)]> CALL KEYWORDS('my*', 't', 1 as stats);
+------+-----------+------------+------+------+
| qpos | tokenized | normalized | docs | hits |
+------+-----------+------------+------+------+
| 1 | my* | my | 1 | 2 |
+------+-----------+------------+------+------+
MySQL [(none)]> CALL KEYWORDS('c*', 't', 1 as stats, 'hits' as sort_mode);
+------+-----------+-------------+------+------+
| qpos | tokenized | normalized | docs | hits |
+------+-----------+-------------+------+------+
| 1 | c* | cat | 1 | 2 |
| 1 | c* | carnivorous | 1 | 1 |
| 1 | c* | catus | 1 | 1 |
+------+-----------+-------------+------+------+
MySQL [(none)]> CALL KEYWORDS('ca*', 't', 1 as stats, 'hits' as sort_mode);
+------+-----------+-------------+------+------+
| qpos | tokenized | normalized | docs | hits |
+------+-----------+-------------+------+------+
| 1 | ca* | cat | 1 | 2 |
| 1 | ca* | carnivorous | 1 | 1 |
| 1 | ca* | catus | 1 | 1 |
+------+-----------+-------------+------+------+
MySQL [(none)]> CALL KEYWORDS('cat*', 't', 1 as stats, 'hits' as sort_mode);
+------+-----------+------------+------+------+
| qpos | tokenized | normalized | docs | hits |
+------+-----------+------------+------+------+
| 1 | cat* | cat | 1 | 2 |
| 1 | cat* | catus | 1 | 1 |
+------+-----------+------------+------+------+Есть хороший трюк, как вы можете улучшить вышеуказанный алгоритм - используйте bigram_index. Когда вы включаете его для таблицы, то получаете не просто одно слово, а каждую пару слов, стоящих друг за другом, индексируемую как отдельный токен.
Это позволяет предсказывать не только окончание текущего слова, но и следующее слово, что особенно полезно для автозаполнения.
- Examples
MySQL [(none)]> CALL KEYWORDS('m*', 't', 1 as stats, 'hits' as sort_mode);
+------+-----------+------------+------+------+
| qpos | tokenized | normalized | docs | hits |
+------+-----------+------------+------+------+
| 1 | m* | my | 1 | 2 |
| 1 | m* | mammal | 1 | 1 |
| 1 | m* | my cat | 1 | 1 |
| 1 | m* | my dog | 1 | 1 |
+------+-----------+------------+------+------+
MySQL [(none)]> CALL KEYWORDS('my*', 't', 1 as stats, 'hits' as sort_mode);
+------+-----------+------------+------+------+
| qpos | tokenized | normalized | docs | hits |
+------+-----------+------------+------+------+
| 1 | my* | my | 1 | 2 |
| 1 | my* | my cat | 1 | 1 |
| 1 | my* | my dog | 1 | 1 |
+------+-----------+------------+------+------+
MySQL [(none)]> CALL KEYWORDS('c*', 't', 1 as stats, 'hits' as sort_mode);
+------+-----------+--------------------+------+------+
| qpos | tokenized | normalized | docs | hits |
+------+-----------+--------------------+------+------+
| 1 | c* | cat | 1 | 2 |
| 1 | c* | carnivorous | 1 | 1 |
| 1 | c* | carnivorous mammal | 1 | 1 |
| 1 | c* | cat felis | 1 | 1 |
| 1 | c* | cat loves | 1 | 1 |
| 1 | c* | catus | 1 | 1 |
| 1 | c* | catus is | 1 | 1 |
+------+-----------+--------------------+------+------+
MySQL [(none)]> CALL KEYWORDS('ca*', 't', 1 as stats, 'hits' as sort_mode);
+------+-----------+--------------------+------+------+
| qpos | tokenized | normalized | docs | hits |
+------+-----------+--------------------+------+------+
| 1 | ca* | cat | 1 | 2 |
| 1 | ca* | carnivorous | 1 | 1 |
| 1 | ca* | carnivorous mammal | 1 | 1 |
| 1 | ca* | cat felis | 1 | 1 |
| 1 | ca* | cat loves | 1 | 1 |
| 1 | ca* | catus | 1 | 1 |
| 1 | ca* | catus is | 1 | 1 |
+------+-----------+--------------------+------+------+
MySQL [(none)]> CALL KEYWORDS('cat*', 't', 1 as stats, 'hits' as sort_mode);
+------+-----------+------------+------+------+
| qpos | tokenized | normalized | docs | hits |
+------+-----------+------------+------+------+
| 1 | cat* | cat | 1 | 2 |
| 1 | cat* | cat felis | 1 | 1 |
| 1 | cat* | cat loves | 1 | 1 |
| 1 | cat* | catus | 1 | 1 |
| 1 | cat* | catus is | 1 | 1 |
+------+-----------+------------+------+------+CALL KEYWORDS поддерживает распределенные таблицы, так что независимо от того, насколько велик ваш набор данных, вы можете извлечь выгоду от его использования.
Исправление орфографии, также известное как:
- Автокоррекция
- Коррекция текста
- Исправление орфографических ошибок
- Допуск опечаток
- "Возможно, вы имели в виду?"
и так далее, это функциональность программного обеспечения, которая предлагает альтернативы или автоматически исправляет введённый вами текст. Концепция исправления набранного текста восходит к 1960-м годам, когда учёный-компьютерщик Уоррен Тейтельман, который также изобрёл команду "отменить", представил философию вычислений под названием D.W.I.M., или "Делай Что Я Имею в Виду". Вместо того чтобы программировать компьютеры на приём только идеально отформатированных инструкций, Тейтельман утверждал, что их следует программировать на распознавание очевидных ошибок.
Первым известным продуктом, предоставившим функциональность исправления орфографии, был Microsoft Word 6.0, выпущенный в 1993 году.
Есть несколько способов реализации исправления орфографии, но важно отметить, что не существует чисто программного способа с достойным качеством преобразовать вашу опечатку "ipone" в "iphone". В основном, необходима система, основанная на наборе данных. Набор данных может быть:
- Словарём правильно написанных слов, который, в свою очередь, может быть:
- Основан на ваших реальных данных. Идея здесь в том, что по большей части орфография в словаре, составленном из ваших данных, правильная, и система пытается найти слово, наиболее похожее на введённое (мы скоро обсудим, как это можно сделать с помощью Manticore).
- Или он может быть основан на внешнем словаре, не связанном с вашими данными. Проблема, которая может здесь возникнуть, заключается в том, что ваши данные и внешний словарь могут слишком сильно отличаться: некоторые слова могут отсутствовать в словаре, в то время как другие могут отсутствовать в ваших данных.
- Не только основанным на словаре, но и учитывающим контекст, например, "white ber" будет исправлено на "white bear", а "dark ber" — на "dark beer". Контекстом может быть не только соседнее слово в вашем запросе, но и ваше местоположение, время суток, грамматика текущего предложения (чтобы изменить "there" на "their" или нет), история ваших поисков и практически любые другие факторы, которые могут повлиять на ваше намерение.
- Ещё один классический подход — использовать предыдущие поисковые запросы в качестве набора данных для исправления орфографии. Это ещё более активно используется в функциональности автодополнения, но также имеет смысл и для автокоррекции. Идея в том, что пользователи в основном правы в орфографии, поэтому мы можем использовать слова из истории их поиска в качестве источника истины, даже если у нас нет этих слов в наших документах или мы не используем внешний словарь. Учёт контекста также возможен здесь.
Manticore предоставляет опцию нечёткого поиска и команды CALL QSUGGEST и CALL SUGGEST, которые могут быть использованы для целей автоматического исправления орфографии.
Функция нечёткого поиска позволяет осуществлять более гибкое сопоставление, учитывая небольшие вариации или опечатки в поисковом запросе. Она работает аналогично обычному оператору SQL SELECT или JSON-запросу /search, но предоставляет дополнительные параметры для управления поведением нечёткого сопоставления.
ПРИМЕЧАНИЕ: Опция
fuzzyтребует наличия Manticore Buddy. Если она не работает, убедитесь, что Buddy установлен.
ПРИМЕЧАНИЕ: Опция
fuzzyнедоступна для многозапросов.
SELECT
...
MATCH('...')
...
OPTION fuzzy={0|1}
[, distance=N]
[, preserve={0|1}]
[, layouts='{be,bg,br,ch,de,dk,es,fr,uk,gr,it,no,pt,ru,se,ua,us}']
}
Примечание: При проведении нечёткого поиска через SQL, в выражении MATCH не должно быть никаких полнотекстовых операторов, кроме оператора поиска по фразе, и оно должно содержать только слова, которые вы намерены сопоставить.
- SQL
- SQL with additional filters
- JSON
- SQL with preserve option
- JSON with preserve option
SELECT * FROM mytable WHERE MATCH('someting') OPTION fuzzy=1, layouts='us,ua', distance=2;Пример более сложного запроса нечёткого поиска с дополнительными фильтрами:
SELECT * FROM mytable WHERE MATCH('someting') OPTION fuzzy=1 AND (category='books' AND price < 20);POST /search
{
"table": "test",
"query": {
"bool": {
"must": [
{
"match": {
"*": "ghbdtn"
}
}
]
}
},
"options": {
"fuzzy": true,
"layouts": ["us", "ru"],
"distance": 2
}
}SELECT * FROM mytable WHERE MATCH('hello wrld') OPTION fuzzy=1, preserve=1;POST /search
{
"table": "test",
"query": {
"bool": {
"must": [
{
"match": {
"*": "hello wrld"
}
}
]
}
},
"options": {
"fuzzy": true,
"preserve": 1
}
}+------+-------------+
| id | content |
+------+-------------+
| 1 | something |
| 2 | some thing |
+------+-------------+
2 rows in set (0.00 sec)+------+-------------+
| id | content |
+------+-------------+
| 1 | hello wrld |
| 2 | hello world |
+------+-------------+
2 rows in set (0.00 sec)POST /search
{
"table": "table_name",
"query": {
<full-text query>
},
"options": {
"fuzzy": {true|false}
[,"layouts": ["be","bg","br","ch","de","dk","es","fr","uk","gr","it","no","pt","ru","se","ua","us"]]
[,"distance": N]
[,"preserve": {0|1}]
}
}
Примечание: Если вы используете query_string, имейте в виду, что он не поддерживает полнотекстовые операторы, кроме оператора поиска по фразе. Строка запроса должна состоять исключительно из слов, которые вы хотите сопоставить.
fuzzy: Включает или выключает нечёткий поиск.distance: Устанавливает расстояние Левенштейна для сопоставления. По умолчанию2.preserve:0или1(по умолчанию:0). При установке в1сохраняет слова, не имеющие нечётких совпадений, в результатах поиска (например, "hello wrld" возвращает и "hello wrld", и "hello world"). При установке в0возвращает только слова с успешными нечёткими совпадениями (например, "hello wrld" возвращает только "hello world"). Особенно полезно для сохранения коротких слов или имён собственных, которых может не быть в Manticore Search.layouts: Раскладки клавиатуры для обнаружения ошибок ввода, вызванных несоответствием раскладки клавиатуры (например, ввод "ghbdtn" вместо "привет" при использовании неправильной раскладки). Manticore сравнивает позиции символов в разных раскладках, чтобы предложить исправления. Требуется как минимум 2 раскладки для эффективного обнаружения несоответствий. По умолчанию раскладки не используются. Используйте пустую строку''(SQL) или массив[](JSON), чтобы отключить это. Поддерживаемые раскладки включают:be- Бельгийская раскладка AZERTYbg- Стандартная болгарская раскладкаbr- Бразильская раскладка QWERTYch- Швейцарская раскладка QWERTZde- Немецкая раскладка QWERTZdk- Датская раскладка QWERTYes- Испанская раскладка QWERTYfr- Французская раскладка AZERTYuk- Британская раскладка QWERTYgr- Греческая раскладка QWERTYit- Итальянская раскладка QWERTYno- Норвежская раскладка QWERTYpt- Португальская раскладка QWERTYru- Русская раскладка JCUKENse- Шведская раскладка QWERTYua- Украинская раскладка JCUKENus- Американская раскладка QWERTY
- Это демо демонстрирует функциональность нечёткого поиска:

- Пост в блоге о Нечётком поиске и автодополнении - https://manticoresearch.com/blog/new-fuzzy-search-and-autocomplete/
Обе команды доступны через SQL и поддерживают запросы к локальным (plain и real-time) и распределённым таблицам. Синтаксис следующий:
CALL QSUGGEST(<word or words>, <table name> [,options])
CALL SUGGEST(<word or words>, <table name> [,options])
options: N as option_name[, M as another_option, ...]
Эти команды предоставляют все предложения из словаря для заданного слова. Они работают только с таблицами, у которых включено инфиксирование и используется dict=keywords. Они возвращают предлагаемые ключевые слова, расстояние Левенштейна между предложенным и исходным ключевым словом, а также статистику документов по предложенному ключевому слову.
Если первый параметр содержит несколько слов, то:
CALL QSUGGESTвернёт предложения только для последнего слова, игнорируя остальные.CALL SUGGESTвернёт предложения только для первого слова.
Это единственное различие между ними. Поддерживается несколько опций для настройки:
| Опция | Описание | По умолчанию |
|---|---|---|
| limit | Возвращает N лучших совпадений | 5 |
| max_edits | Оставляет только слова из словаря с расстоянием Левенштейна меньше или равным N | 4 |
| result_stats | Предоставляет расстояние Левенштейна и количество документов для найденных слов | 1 (включено) |
| delta_len | Оставляет только слова из словаря с разницей в длине меньше N | 3 |
| max_matches | Количество совпадений для сохранения | 25 |
| reject | Отклонённые слова — это совпадения, которые не лучше уже находящихся в очереди совпадений. Они помещаются в очередь отклонённых, которая сбрасывается, если слово всё же может попасть в очередь совпадений. Этот параметр определяет размер очереди отклонённых (как reject*max(max_matched,limit)). Если очередь отклонённых заполнена, движок прекращает поиск потенциальных совпадений | 4 |
| result_line | альтернативный режим отображения данных, возвращающий все предложения, расстояния и количество документов каждое в отдельной строке | 0 |
| non_char | не пропускать слова из словаря с неалфавитными символами | 0 (пропускать такие слова) |
| sentence | Возвращает исходное предложение с заменой последнего слова на подобранное. | 0 (не возвращать полное предложение) |
| force_bigrams | Принудительно использовать биграммы (n-граммы из 2 символов) вместо триграмм для всех длин слов, что может улучшить сопоставление для слов с ошибками транспозиции | 0 (использовать триграммы для слов ≥6 символов) |
| search_mode | Уточняет предложения, выполняя поиск по индексу. Принимает 'phrase' для точного соответствия фразы или 'words' для соответствия по модели "мешок слов". При включении добавляет столбец found_docs, показывающий количество документов, и пересортировывает результаты по убыванию found_docs, затем по возрастанию distance. |
N/A (отключено по умолчанию) |
Чтобы показать, как это работает, создадим таблицу и добавим в неё несколько документов.
create table products(title text) min_infix_len='2';
insert into products values (0,'Crossbody Bag with Tassel'), (0,'microfiber sheet set'), (0,'Pet Hair Remover Glove');
Как видно, слово с опечаткой "crossbUdy" исправляется на "crossbody". По умолчанию CALL SUGGEST/QSUGGEST возвращают:
distance— расстояние Левенштейна, означающее, сколько правок потребовалось, чтобы преобразовать заданное слово в предложенноеdocs— количество документов, содержащих предложенное слово
Чтобы отключить отображение этой статистики, можно использовать опцию 0 as result_stats.
- Example
call suggest('crossbudy', 'products');+-----------+----------+------+
| suggest | distance | docs |
+-----------+----------+------+
| crossbody | 1 | 1 |
+-----------+----------+------+Если первый параметр — не одно слово, а несколько, то CALL SUGGEST вернёт предложения только для первого слова.
- Example
call suggest('bagg with tasel', 'products');+---------+----------+------+
| suggest | distance | docs |
+---------+----------+------+
| bag | 1 | 1 |
+---------+----------+------+Если первый параметр — не одно слово, а несколько, то CALL SUGGEST вернёт предложения только для последнего слова.
- Example
CALL QSUGGEST('bagg with tasel', 'products');+---------+----------+------+
| suggest | distance | docs |
+---------+----------+------+
| tassel | 1 | 1 |
+---------+----------+------+Добавление 1 as sentence заставляет CALL QSUGGEST возвращать всё предложение с исправленным последним словом.
- Example
CALL QSUGGEST('bag with tasel', 'products', 1 as sentence);+-------------------+----------+------+
| suggest | distance | docs |
+-------------------+----------+------+
| bag with tassel | 1 | 1 |
+-------------------+----------+------+Опция 1 as result_line меняет способ отображения предложений в выводе. Вместо показа каждого предложения в отдельной строке, она отображает все предложения, расстояния и количество документов в одной строке. Вот пример, демонстрирующий это:
CALL QSUGGEST('bagg with tasel', 'products', 1 as result_line);
+----------+--------+
| name | value |
+----------+--------+
| suggests | tassel |
| distance | 1 |
| docs | 1 |
+----------+--------+
Опция force_bigrams может помочь со словами, содержащими ошибки транспозиции, например, "ipohne" и "iphone". Используя биграммы вместо триграмм, алгоритм может лучше обрабатывать перестановки символов.
CALL SUGGEST('ipohne', 'products', 1 as force_bigrams);
+--------+----------+------+
| suggest| distance | docs |
+--------+----------+------+
| iphone | 2 | 1 |
+--------+----------+------+
Опция search_mode улучшает предложения, выполняя фактические поисковые запросы по индексу для подсчёта количества документов, содержащих каждую предложенную фразу или комбинацию слов. Это помогает ранжировать предложения на основе реальной релевантности документов, а не только статистики словаря.
Опция принимает два значения:
'phrase'- Выполняет поиск точных фраз. Например, при предложении "bag with tassel" выполняется поиск точной фразы"bag with tassel"и подсчитываются документы, содержащие эти слова как смежную фразу.'words'- Выполняет поиск по модели "мешок слов". Например, при предложении "bag with tassel" выполняется поискbag with tassel(без кавычек) и подсчитываются документы, содержащие все эти слова, независимо от порядка или наличия других слов между ними.
ПРИМЕЧАНИЕ: Опция
search_modeработает только тогда, когда включен режимsentence(т.е. когда ввод содержит несколько слов). Для запросов из одного словаsearch_modeигнорируется.
ПРИМЕЧАНИЕ: Соображения производительности: Каждый кандидат на предложение запускает отдельный поисковый запрос к индексу. Если вам нужно оценить много кандидатов, рассмотрите использование меньшего значения
limit, чтобы сократить количество выполняемых поисков.
Когда search_mode включен, результаты включают столбец found_docs, показывающий количество документов для каждого предложения, и результаты пересортировываются по убыванию found_docs, а затем по возрастанию distance.
CALL QSUGGEST('bag with tasel', 'products', 1 as sentence, 'phrase' as search_mode);
+-------------------+----------+------+-------------+
| suggest | distance | docs | found_docs |
+-------------------+----------+------+-------------+
| bag with tassel | 1 | 13 | 10 |
| bag with tazer | 2 | 27 | 3 |
+-------------------+----------+------+-------------+
-- With phrase matching: finds exact phrases only
CALL QSUGGEST('test carp', 'products', 1 as sentence, 'phrase' as search_mode);
-- With words matching: finds documents with all words regardless of order
CALL QSUGGEST('test carp', 'products', 1 as sentence, 'words' as search_mode);
-- Phrase mode results:
+----------------+----------+------+-------------+
| suggest | distance | docs | found_docs |
+----------------+----------+------+-------------+
| test car | 1 | 17 | 5 |
| test carpet | 2 | 19 | 4 |
+----------------+----------+------+-------------+
-- Words mode results (more matches for "test carpet" due to word separation):
+----------------+----------+------+-------------+
| suggest | distance | docs | found_docs |
+----------------+----------+------+-------------+
| test carpet | 2 | 19 | 19 |
| test car | 1 | 17 | 5 |
+----------------+----------+------+-------------+
Понимание разницы:
- Фразовое соответствие (
'phrase'): Ищет точные последовательности. Запрос"test carpet"соответствует только документам, где эти слова встречаются вместе в точном порядке (например, "test carpet cleaning" соответствует, а "test the carpet" или "carpet test" - нет). - Соответствие по модели "мешок слов" (
'words'): Ищет наличие всех слов в документе, порядок не имеет значения. Запросtest carpetсоответствует любому документу, содержащему оба слова "test" и "carpet" где угодно (например, "test the carpet", "test red carpet", "carpet test" - все соответствуют).
- Этот интерактивный курс показывает, как работает
CALL SUGGESTв небольшом веб-приложении.

Исправление орфографии, также известное как:
- Автокоррекция
- Коррекция текста
- Исправление орфографических ошибок
- Допуск опечаток
- "Возможно, вы имели в виду?"
и так далее, это функциональность программного обеспечения, которая предлагает альтернативы или автоматически исправляет введённый вами текст. Концепция исправления набранного текста восходит к 1960-м годам, когда учёный-компьютерщик Уоррен Тейтельман, который также изобрёл команду "отменить", представил философию вычислений под названием D.W.I.M., или "Делай Что Я Имею в Виду". Вместо того чтобы программировать компьютеры на приём только идеально отформатированных инструкций, Тейтельман утверждал, что их следует программировать на распознавание очевидных ошибок.
Первым известным продуктом, предоставившим функциональность исправления орфографии, был Microsoft Word 6.0, выпущенный в 1993 году.
Есть несколько способов реализации исправления орфографии, но важно отметить, что не существует чисто программного способа с достойным качеством преобразовать вашу опечатку "ipone" в "iphone". В основном, необходима система, основанная на наборе данных. Набор данных может быть:
- Словарём правильно написанных слов, который, в свою очередь, может быть:
- Основан на ваших реальных данных. Идея здесь в том, что по большей части орфография в словаре, составленном из ваших данных, правильная, и система пытается найти слово, наиболее похожее на введённое (мы скоро обсудим, как это можно сделать с помощью Manticore).
- Или он может быть основан на внешнем словаре, не связанном с вашими данными. Проблема, которая может здесь возникнуть, заключается в том, что ваши данные и внешний словарь могут слишком сильно отличаться: некоторые слова могут отсутствовать в словаре, в то время как другие могут отсутствовать в ваших данных.
- Не только основанным на словаре, но и учитывающим контекст, например, "white ber" будет исправлено на "white bear", а "dark ber" — на "dark beer". Контекстом может быть не только соседнее слово в вашем запросе, но и ваше местоположение, время суток, грамматика текущего предложения (чтобы изменить "there" на "their" или нет), история ваших поисков и практически любые другие факторы, которые могут повлиять на ваше намерение.
- Ещё один классический подход — использовать предыдущие поисковые запросы в качестве набора данных для исправления орфографии. Это ещё более активно используется в функциональности автодополнения, но также имеет смысл и для автокоррекции. Идея в том, что пользователи в основном правы в орфографии, поэтому мы можем использовать слова из истории их поиска в качестве источника истины, даже если у нас нет этих слов в наших документах или мы не используем внешний словарь. Учёт контекста также возможен здесь.
Manticore предоставляет опцию нечёткого поиска и команды CALL QSUGGEST и CALL SUGGEST, которые могут быть использованы для целей автоматического исправления орфографии.
Функция нечёткого поиска позволяет осуществлять более гибкое сопоставление, учитывая небольшие вариации или опечатки в поисковом запросе. Она работает аналогично обычному оператору SQL SELECT или JSON-запросу /search, но предоставляет дополнительные параметры для управления поведением нечёткого сопоставления.
ПРИМЕЧАНИЕ: Опция
fuzzyтребует наличия Manticore Buddy. Если она не работает, убедитесь, что Buddy установлен.
ПРИМЕЧАНИЕ: Опция
fuzzyнедоступна для многозапросов.
SELECT
...
MATCH('...')
...
OPTION fuzzy={0|1}
[, distance=N]
[, preserve={0|1}]
[, layouts='{be,bg,br,ch,de,dk,es,fr,uk,gr,it,no,pt,ru,se,ua,us}']
}
Примечание: При проведении нечёткого поиска через SQL, в выражении MATCH не должно быть никаких полнотекстовых операторов, кроме оператора поиска по фразе, и оно должно содержать только слова, которые вы намерены сопоставить.
- SQL
- SQL with additional filters
- JSON
- SQL with preserve option
- JSON with preserve option
SELECT * FROM mytable WHERE MATCH('someting') OPTION fuzzy=1, layouts='us,ua', distance=2;Пример более сложного запроса нечёткого поиска с дополнительными фильтрами:
SELECT * FROM mytable WHERE MATCH('someting') OPTION fuzzy=1 AND (category='books' AND price < 20);POST /search
{
"table": "test",
"query": {
"bool": {
"must": [
{
"match": {
"*": "ghbdtn"
}
}
]
}
},
"options": {
"fuzzy": true,
"layouts": ["us", "ru"],
"distance": 2
}
}SELECT * FROM mytable WHERE MATCH('hello wrld') OPTION fuzzy=1, preserve=1;POST /search
{
"table": "test",
"query": {
"bool": {
"must": [
{
"match": {
"*": "hello wrld"
}
}
]
}
},
"options": {
"fuzzy": true,
"preserve": 1
}
}+------+-------------+
| id | content |
+------+-------------+
| 1 | something |
| 2 | some thing |
+------+-------------+
2 rows in set (0.00 sec)+------+-------------+
| id | content |
+------+-------------+
| 1 | hello wrld |
| 2 | hello world |
+------+-------------+
2 rows in set (0.00 sec)POST /search
{
"table": "table_name",
"query": {
<full-text query>
},
"options": {
"fuzzy": {true|false}
[,"layouts": ["be","bg","br","ch","de","dk","es","fr","uk","gr","it","no","pt","ru","se","ua","us"]]
[,"distance": N]
[,"preserve": {0|1}]
}
}
Примечание: Если вы используете query_string, имейте в виду, что он не поддерживает полнотекстовые операторы, кроме оператора поиска по фразе. Строка запроса должна состоять исключительно из слов, которые вы хотите сопоставить.
fuzzy: Включает или выключает нечёткий поиск.distance: Устанавливает расстояние Левенштейна для сопоставления. По умолчанию2.preserve:0или1(по умолчанию:0). При установке в1сохраняет слова, не имеющие нечётких совпадений, в результатах поиска (например, "hello wrld" возвращает и "hello wrld", и "hello world"). При установке в0возвращает только слова с успешными нечёткими совпадениями (например, "hello wrld" возвращает только "hello world"). Особенно полезно для сохранения коротких слов или имён собственных, которых может не быть в Manticore Search.layouts: Раскладки клавиатуры для обнаружения ошибок ввода, вызванных несоответствием раскладки клавиатуры (например, ввод "ghbdtn" вместо "привет" при использовании неправильной раскладки). Manticore сравнивает позиции символов в разных раскладках, чтобы предложить исправления. Требуется как минимум 2 раскладки для эффективного обнаружения несоответствий. По умолчанию раскладки не используются. Используйте пустую строку''(SQL) или массив[](JSON), чтобы отключить это. Поддерживаемые раскладки включают:be- Бельгийская раскладка AZERTYbg- Стандартная болгарская раскладкаbr- Бразильская раскладка QWERTYch- Швейцарская раскладка QWERTZde- Немецкая раскладка QWERTZdk- Датская раскладка QWERTYes- Испанская раскладка QWERTYfr- Французская раскладка AZERTYuk- Британская раскладка QWERTYgr- Греческая раскладка QWERTYit- Итальянская раскладка QWERTYno- Норвежская раскладка QWERTYpt- Португальская раскладка QWERTYru- Русская раскладка JCUKENse- Шведская раскладка QWERTYua- Украинская раскладка JCUKENus- Американская раскладка QWERTY
- Это демо демонстрирует функциональность нечёткого поиска:

- Пост в блоге о Нечётком поиске и автодополнении - https://manticoresearch.com/blog/new-fuzzy-search-and-autocomplete/
Обе команды доступны через SQL и поддерживают запросы к локальным (plain и real-time) и распределённым таблицам. Синтаксис следующий:
CALL QSUGGEST(<word or words>, <table name> [,options])
CALL SUGGEST(<word or words>, <table name> [,options])
options: N as option_name[, M as another_option, ...]
Эти команды предоставляют все предложения из словаря для заданного слова. Они работают только с таблицами, у которых включено инфиксирование и используется dict=keywords. Они возвращают предлагаемые ключевые слова, расстояние Левенштейна между предложенным и исходным ключевым словом, а также статистику документов по предложенному ключевому слову.
Если первый параметр содержит несколько слов, то:
CALL QSUGGESTвернёт предложения только для последнего слова, игнорируя остальные.CALL SUGGESTвернёт предложения только для первого слова.
Это единственное различие между ними. Поддерживается несколько опций для настройки:
| Опция | Описание | По умолчанию |
|---|---|---|
| limit | Возвращает N лучших совпадений | 5 |
| max_edits | Оставляет только слова из словаря с расстоянием Левенштейна меньше или равным N | 4 |
| result_stats | Предоставляет расстояние Левенштейна и количество документов для найденных слов | 1 (включено) |
| delta_len | Оставляет только слова из словаря с разницей в длине меньше N | 3 |
| max_matches | Количество совпадений для сохранения | 25 |
| reject | Отклонённые слова — это совпадения, которые не лучше уже находящихся в очереди совпадений. Они помещаются в очередь отклонённых, которая сбрасывается, если слово всё же может попасть в очередь совпадений. Этот параметр определяет размер очереди отклонённых (как reject*max(max_matched,limit)). Если очередь отклонённых заполнена, движок прекращает поиск потенциальных совпадений | 4 |
| result_line | альтернативный режим отображения данных, возвращающий все предложения, расстояния и количество документов каждое в отдельной строке | 0 |
| non_char | не пропускать слова из словаря с неалфавитными символами | 0 (пропускать такие слова) |
| sentence | Возвращает исходное предложение с заменой последнего слова на подобранное. | 0 (не возвращать полное предложение) |
| force_bigrams | Принудительно использовать биграммы (n-граммы из 2 символов) вместо триграмм для всех длин слов, что может улучшить сопоставление для слов с ошибками транспозиции | 0 (использовать триграммы для слов ≥6 символов) |
| search_mode | Уточняет предложения, выполняя поиск по индексу. Принимает 'phrase' для точного соответствия фразы или 'words' для соответствия по модели "мешок слов". При включении добавляет столбец found_docs, показывающий количество документов, и пересортировывает результаты по убыванию found_docs, затем по возрастанию distance. |
N/A (отключено по умолчанию) |
Чтобы показать, как это работает, создадим таблицу и добавим в неё несколько документов.
create table products(title text) min_infix_len='2';
insert into products values (0,'Crossbody Bag with Tassel'), (0,'microfiber sheet set'), (0,'Pet Hair Remover Glove');
Как видно, слово с опечаткой "crossbUdy" исправляется на "crossbody". По умолчанию CALL SUGGEST/QSUGGEST возвращают:
distance— расстояние Левенштейна, означающее, сколько правок потребовалось, чтобы преобразовать заданное слово в предложенноеdocs— количество документов, содержащих предложенное слово
Чтобы отключить отображение этой статистики, можно использовать опцию 0 as result_stats.
- Example
call suggest('crossbudy', 'products');+-----------+----------+------+
| suggest | distance | docs |
+-----------+----------+------+
| crossbody | 1 | 1 |
+-----------+----------+------+Если первый параметр — не одно слово, а несколько, то CALL SUGGEST вернёт предложения только для первого слова.
- Example
call suggest('bagg with tasel', 'products');+---------+----------+------+
| suggest | distance | docs |
+---------+----------+------+
| bag | 1 | 1 |
+---------+----------+------+Если первый параметр — не одно слово, а несколько, то CALL SUGGEST вернёт предложения только для последнего слова.
- Example
CALL QSUGGEST('bagg with tasel', 'products');+---------+----------+------+
| suggest | distance | docs |
+---------+----------+------+
| tassel | 1 | 1 |
+---------+----------+------+Добавление 1 as sentence заставляет CALL QSUGGEST возвращать всё предложение с исправленным последним словом.
- Example
CALL QSUGGEST('bag with tasel', 'products', 1 as sentence);+-------------------+----------+------+
| suggest | distance | docs |
+-------------------+----------+------+
| bag with tassel | 1 | 1 |
+-------------------+----------+------+Опция 1 as result_line меняет способ отображения предложений в выводе. Вместо показа каждого предложения в отдельной строке, она отображает все предложения, расстояния и количество документов в одной строке. Вот пример, демонстрирующий это:
CALL QSUGGEST('bagg with tasel', 'products', 1 as result_line);
+----------+--------+
| name | value |
+----------+--------+
| suggests | tassel |
| distance | 1 |
| docs | 1 |
+----------+--------+
Опция force_bigrams может помочь со словами, содержащими ошибки транспозиции, например, "ipohne" и "iphone". Используя биграммы вместо триграмм, алгоритм может лучше обрабатывать перестановки символов.
CALL SUGGEST('ipohne', 'products', 1 as force_bigrams);
+--------+----------+------+
| suggest| distance | docs |
+--------+----------+------+
| iphone | 2 | 1 |
+--------+----------+------+
Опция search_mode улучшает предложения, выполняя фактические поисковые запросы по индексу для подсчёта количества документов, содержащих каждую предложенную фразу или комбинацию слов. Это помогает ранжировать предложения на основе реальной релевантности документов, а не только статистики словаря.
Опция принимает два значения:
'phrase'- Выполняет поиск точных фраз. Например, при предложении "bag with tassel" выполняется поиск точной фразы"bag with tassel"и подсчитываются документы, содержащие эти слова как смежную фразу.'words'- Выполняет поиск по модели "мешок слов". Например, при предложении "bag with tassel" выполняется поискbag with tassel(без кавычек) и подсчитываются документы, содержащие все эти слова, независимо от порядка или наличия других слов между ними.
ПРИМЕЧАНИЕ: Опция
search_modeработает только тогда, когда включен режимsentence(т.е. когда ввод содержит несколько слов). Для запросов из одного словаsearch_modeигнорируется.
ПРИМЕЧАНИЕ: Соображения производительности: Каждый кандидат на предложение запускает отдельный поисковый запрос к индексу. Если вам нужно оценить много кандидатов, рассмотрите использование меньшего значения
limit, чтобы сократить количество выполняемых поисков.
Когда search_mode включен, результаты включают столбец found_docs, показывающий количество документов для каждого предложения, и результаты пересортировываются по убыванию found_docs, а затем по возрастанию distance.
CALL QSUGGEST('bag with tasel', 'products', 1 as sentence, 'phrase' as search_mode);
+-------------------+----------+------+-------------+
| suggest | distance | docs | found_docs |
+-------------------+----------+------+-------------+
| bag with tassel | 1 | 13 | 10 |
| bag with tazer | 2 | 27 | 3 |
+-------------------+----------+------+-------------+
-- With phrase matching: finds exact phrases only
CALL QSUGGEST('test carp', 'products', 1 as sentence, 'phrase' as search_mode);
-- With words matching: finds documents with all words regardless of order
CALL QSUGGEST('test carp', 'products', 1 as sentence, 'words' as search_mode);
-- Phrase mode results:
+----------------+----------+------+-------------+
| suggest | distance | docs | found_docs |
+----------------+----------+------+-------------+
| test car | 1 | 17 | 5 |
| test carpet | 2 | 19 | 4 |
+----------------+----------+------+-------------+
-- Words mode results (more matches for "test carpet" due to word separation):
+----------------+----------+------+-------------+
| suggest | distance | docs | found_docs |
+----------------+----------+------+-------------+
| test carpet | 2 | 19 | 19 |
| test car | 1 | 17 | 5 |
+----------------+----------+------+-------------+
Понимание разницы:
- Фразовое соответствие (
'phrase'): Ищет точные последовательности. Запрос"test carpet"соответствует только документам, где эти слова встречаются вместе в точном порядке (например, "test carpet cleaning" соответствует, а "test the carpet" или "carpet test" - нет). - Соответствие по модели "мешок слов" (
'words'): Ищет наличие всех слов в документе, порядок не имеет значения. Запросtest carpetсоответствует любому документу, содержащему оба слова "test" и "carpet" где угодно (например, "test the carpet", "test red carpet", "carpet test" - все соответствуют).
- Этот интерактивный курс показывает, как работает
CALL SUGGESTв небольшом веб-приложении.
