表旋转是一种过程,其中 searchd 服务器查找配置中定义的表的新版本。旋转仅支持 Plain 模式的操作。
有两种情况:
- 对于已经加载的 Plain 表
- 在配置中添加但尚未加载的表
在第一种情况下,indexer 无法将表的新版本投入使用,因为运行中的副本已被 searchd 锁定和加载。此时需要以带有 --rotate 参数调用 indexer。如果使用 rotate,indexer 会创建名称中带有 .new. 的新表文件,并向 searchd 发送 HUP 信号,通知其有新版本。searchd 将执行查找操作,将新版本的表替换旧版本并丢弃旧版本。在某些情况下,可能希望先创建新版本表但不立即执行旋转。比如,可能先希望检查新表版本的健康状态。此时,indexer 可以接受 --nohup 参数,禁止向服务器发送 HUP 信号。
新表可以通过旋转加载;但是,常规处理 HUP 信号的方式是只有在服务器启动后配置发生更改时才检查新表。如果表已在配置中定义,应首先运行不带旋转的 indexer 创建表,然后执行 RELOAD TABLES 语句。
还有两个专用语句可以用于对表执行旋转:
RELOAD TABLE tbl [ FROM '/path/to/table_files' [ OPTION switchover=1 ] ];
RELOAD TABLE 命令通过 SQL 实现表旋转。
此命令有三种模式。第一种模式是不指定路径,Manticore 服务器会检查由 path 指定的目录中新表文件。新表文件必须命名为 tbl.new.sp?。
如果指定路径,服务器会在该目录搜索表文件,将它们移动到表的 path,将文件名从 tbl.sp? 重命名为 tbl.new.sp?,并旋转它们。
第三种模式由 OPTION switchover=1 激活,将索引切换到新的路径。此时,守护进程尝试直接从新路径加载表而不移动文件。如果加载成功,这个新索引将取代旧索引。
此外,守护进程会在由 path 指定的目录中写入唯一链接文件(tbl.link),保持持久重定向。
如果将已重定向的索引恢复到配置中指定的路径,守护进程会检测到这一点并删除链接文件。
一旦重定向,守护进程从新的链接路径获取表。旋转时,它会在新的重定向路径查找新表版本。需要注意的是,守护进程会检查配置中的常见错误,比如不同表的路径重复,但不会识别多个表通过重定向指向同一路径的情况。正常操作下,表通过 .spl 文件锁定,但禁用锁定可能会导致问题。如果出现错误(例如路径因某种原因无法访问),需要手动修正(或简单删除)链接文件。
indextool 会跟随链接文件,但其他工具(indexer、index_converter 等)不识别链接文件,始终使用配置文件中定义的路径,忽略任何重定向。因此,可以用 indextool 检查索引,它会从新位置读取;但更复杂的操作如合并将不识别任何链接文件。
mysql> RELOAD TABLE plain_table;
mysql> RELOAD TABLE plain_table FROM '/home/mighty/new_table_files';
mysql> RELOAD TABLE plain_table FROM '/home/mighty/new/place/for/table/table_files' OPTION switchover=1;
RELOAD TABLES;
此命令的功能类似于 HUP 系统信号,触发表的旋转。但它并不完全等同于典型的 HUP 信号(例如通过 kill -HUP 命令或 indexer --rotate 触发)。该命令会主动查找需要旋转的表,并能重新读取配置。比如,若以配置文件启动的 Manticore 处于 Plain 模式,但配置指向一个不存在的 Plain 表,之后执行 indexer --rotate 不会让服务器识别新表,需执行 RELOAD TABLES 或重启服务器。
根据 seamless_rotate 设置的值,新查询可能会被短暂挂起,客户端会收到临时错误。
mysql> RELOAD TABLES;
Query OK, 0 rows affected (0.01 sec)
旋转假设旧表版本被丢弃,新表版本被加载并替换现有表。在交换期间,服务器还需处理对即将更新表的入站查询。为了避免查询阻塞,服务器默认实现了表的无缝旋转,如下所述。
表可能包含需要预缓存到内存的数据。目前,.spa、.spb、.spi 和 .spm 文件是完全预缓存的(它们分别包含属性数据、二进制大对象属性数据、关键词表和已删除行映射)。如果不使用无缝旋转,旋转表尽可能少占用内存,工作流程如下:
- 临时拒绝新查询(返回“重试”错误码)。
searchd等待所有正在运行的查询完成。- 释放旧表并重命名其文件。
- 重命名新表文件并分配所需内存。
- 将新表属性和字典数据预加载到内存。
searchd继续从新表处理查询。
不过,如果属性或字典数据量较大,则预加载步骤可能需要明显时间——在预加载 1-5+ GB 文件时可能达数分钟。
开启无缝旋转后,旋转过程如下:
- 分配新的表的RAM存储。
- 异步预加载新表的属性和字典数据到RAM。
- 成功后,释放旧表并重命名两个表的文件。
- 失败时,释放新表。
- 在任何时刻,查询要么由旧表副本提供,要么由新表副本提供。
无缝旋转的代价是在旋转期间内存使用的峰值更高(因为预加载新副本时,旧副本和新副本的.spa/.spb/.spi/.spm数据都需要在RAM中)。不过平均使用量保持不变。
示例:
seamless_rotate = 1
≫ 更新文档
您可以通过更新或替换来修改 RT 或 PQ 表中的现有数据。
UPDATE 以行式方式用新值替换现有文档的属性值。全文字段和列式属性不能被更新。如果需要更改全文字段或列式属性的内容,请使用 REPLACE。
REPLACE 的工作方式类似于 INSERT,但如果旧文档与新文档的 ID 相同,旧文档会在插入新文档之前被标记为已删除。请注意,旧文档不会从表中物理删除。删除只能在表块合并时发生,例如,作为 OPTIMIZE 的结果。
UPDATE 和部分 REPLACE 都可以更改字段的值,但它们的操作方式不同:
UPDATE只能更改非列式且非全文的字段。此过程是在原地完成的,通常比REPLACE更快。- 部分
REPLACE可以更改文档中的任何字段,但要求表中的所有字段都设置为“存储”(尽管这是默认设置)。使用UPDATE则不需要此要求。
REPLACE 的工作方式类似于 INSERT,但它会在插入新文档之前,将具有相同 ID 的前一个文档标记为已删除。
如果您想进行原地更新,请参阅 本节。
SQL REPLACE 语句的语法如下:
替换整个文档:
REPLACE INTO table [(column1, column2, ...)]
VALUES (value1, value2, ...)
[, (...)]
未在 SQL 语句中明确包括的列将被设置为其默认值,如 0 或空字符串,具体取决于它们的数据类型。
仅替换选定字段:
REPLACE INTO table
SET field1=value1[, ..., fieldN=valueN]
WHERE id = <id>
注意,此模式下只能通过 id 进行过滤。
注意:部分替换需要 Manticore Buddy。如果无法使用,请确保 Buddy 已安装。
从 SELECT 替换:
REPLACE INTO table
SELECT ... FROM source
REPLACE INTO table (column1, column2, column3)
SELECT ... FROM source
注意:此语法需要 Manticore Buddy。如果不起作用,请确保已安装 Buddy。
更多关于 UPDATE 与部分 REPLACE 的内容,请参见 这里。
详细信息请参见示例。
-
/replace:POST /replace { "table": "<table name>", "id": <document id>, "doc": { "<field1>": <value1>, ... "<fieldN>": <valueN> } }/index是同义端点,功能相同。 -
类似 Elasticsearch 的端点
<table>/_doc/<id>:PUT/POST /<table name>/_doc/<id> { "<field1>": <value1>, ... "<fieldN>": <valueN> }注意:类似 Elasticsearch 的替换需要 Manticore Buddy。如果无法使用,请确保 Buddy 已安装。
-
部分替换:
POST /<{table | cluster:table}>/_update/<id> { "<field1>": <value1>, ... "<fieldN>": <valueN> }<table name>可以仅是表名,也可以采用cluster:table格式。如果需要,可以跨特定集群进行更新。注意:部分替换需要 Manticore Buddy。如果无法使用,请确保 Buddy 已安装。
详细信息请参见示例。
- SQL
- REPLACE SET
- REPLACE ... SELECT
- JSON
- Elasticsearch-like
- Elasticsearch-like partial
- Elasticsearch-like partial in cluster
- PHP
- Python
- Python-asyncio
- javascript
- Java
- C#
- Rust
- TypeScript
- Go
REPLACE INTO products VALUES(1, "document one", 10);REPLACE INTO products SET description='HUAWEI Matebook 15', price=10 WHERE id = 55;CREATE TABLE products_src (id int, title text, price float, category_id int);
CREATE TABLE products (id int, title text, price float, category_id int);
INSERT INTO products_src VALUES
(1, 'Notebook Stand', 45.00, 10),
(2, 'USB-C Hub', 79.90, 12),
(3, 'Wireless Mouse', 129.00, 10);
REPLACE INTO products_a (id, price)
SELECT id, price FROM products_src;
REPLACE INTO products_b
SELECT * FROM products_src;
REPLACE INTO products_c (id, title, category_id)
SELECT id, title, category_id
FROM products_src
WHERE price >= 100;POST /replace
-H "Content-Type: application/x-ndjson" -d '
{
"table":"products",
"id":1,
"doc":
{
"title":"product one",
"price":10
}
}
'注意:类似 Elasticsearch 的替换需要 Manticore Buddy。如果无法使用,请确保 Buddy 已安装。
PUT /products/_doc/2
{
"title": "product two",
"price": 20
}
POST /products/_doc/3
{
"title": "product three",
"price": 10
}注意:部分替换需要 Manticore Buddy。如果无法使用,请确保 Buddy 已安装。
POST /products/_update/55
{
"doc": {
"description": "HUAWEI Matebook 15",
"price": 10
}
}POST /cluster_name:products/_update/55
{
"doc": {
"description": "HUAWEI Matebook 15",
"price": 10
}
}$index->replaceDocument([
'title' => 'document one',
'price' => 10
],1);indexApi.replace({"table" : "products", "id" : 1, "doc" : {"title" : "document one","price":10}})await indexApi.replace({"table" : "products", "id" : 1, "doc" : {"title" : "document one","price":10}})res = await indexApi.replace({"table" : "products", "id" : 1, "doc" : {"title" : "document one","price":10}});docRequest = new InsertDocumentRequest();
HashMap<String,Object> doc = new HashMap<String,Object>(){{
put("title","document one");
put("price",10);
}};
docRequest.index("products").id(1L).setDoc(doc);
sqlresult = indexApi.replace(docRequest);Dictionary<string, Object> doc = new Dictionary<string, Object>();
doc.Add("title", "document one");
doc.Add("price", 10);
InsertDocumentRequest docRequest = new InsertDocumentRequest(index: "products", id: 1, doc: doc);
var sqlresult = indexApi.replace(docRequest);let mut doc = HashMap::new();
doc.insert("title".to_string(), serde_json::json!("document one"));
doc.insert("price".to_string(), serde_json::json!(10));
let insert_req = InsertDocumentRequest::new("products".to_string(), serde_json::json!(doc));
let insert_res = index_api.replace(insert_req).await;res = await indexApi.replace({
index: 'test',
id: 1,
doc: { content: 'Text 11', name: 'Doc 11', cat: 3 },
});replaceDoc := map[string]interface{} {"content": "Text 11", "name": "Doc 11", "cat": 3}
replaceRequest := manticoreclient.NewInsertDocumentRequest("test", replaceDoc)
replaceRequest.SetId(1)
res, _, _ := apiClient.IndexAPI.Replace(context.Background()).InsertDocumentRequest(*replaceRequest).Execute()Query OK, 1 row affected (0.00 sec)Query OK, 1 row affected (0.00 sec)Query OK, 3 rows affected (0.00 sec){
"table":"products",
"_id":1,
"created":false,
"result":"updated",
"status":200
}{
"_id":2,
"table":"products",
"_primary_term":1,
"_seq_no":0,
"_shards":{
"failed":0,
"successful":1,
"total":1
},
"_type":"_doc",
"_version":1,
"result":"updated"
}
{
"_id":3,
"table":"products",
"_primary_term":1,
"_seq_no":0,
"_shards":{
"failed":0,
"successful":1,
"total":1
},
"_type":"_doc",
"_version":1,
"result":"updated"
}{
"table":"products",
"updated":1
}{
"table":"products",
"updated":1
}Array(
[_index] => products
[_id] => 1
[created] => false
[result] => updated
[status] => 200
){'created': False,
'found': None,
'id': 1,
'table': 'products',
'result': 'updated'}{'created': False,
'found': None,
'id': 1,
'table': 'products',
'result': 'updated'}{"table":"products","_id":1,"result":"updated"}class SuccessResponse {
index: products
id: 1
created: false
result: updated
found: null
}class SuccessResponse {
index: products
id: 1
created: false
result: updated
found: null
}class SuccessResponse {
index: products
id: 1
created: false
result: updated
found: null
}{
"table":"test",
"_id":1,
"created":false
"result":"updated"
"status":200
}{
"table":"test",
"_id":1,
"created":false
"result":"updated"
"status":200
}REPLACE 适用于实时表和感知表。不能替换普通表中的数据。
执行 REPLACE 时,前一个文档不会被移除,但会被标记为已删除,因此表的大小会增长,直到发生块合并。若要强制块合并,请使用 OPTIMIZE 语句。
您可以一次替换多个文档。更多信息请参考 批量添加文档。
- SQL
- JSON
- PHP
- Python
- Python-asyncio
- javascript
- Java
- C#
- Rust
- TypeScript
- Go
REPLACE INTO products(id,title,tag) VALUES (1, 'doc one', 10), (2,' doc two', 20);POST /bulk
-H "Content-Type: application/x-ndjson" -d '
{ "replace" : { "table" : "products", "id":1, "doc": { "title": "doc one", "tag" : 10 } } }
{ "replace" : { "table" : "products", "id":2, "doc": { "title": "doc two", "tag" : 20 } } }
'$index->replaceDocuments([
[
'id' => 1,
'title' => 'document one',
'tag' => 10
],
[
'id' => 2,
'title' => 'document one',
'tag' => 20
]
);indexApi = manticoresearch.IndexApi(client)
docs = [ \
{"replace": {"table" : "products", "id" : 1, "doc" : {"title" : "document one"}}}, \
{"replace": {"table" : "products", "id" : 2, "doc" : {"title" : "document two"}}} ]
api_resp = indexApi.bulk('\n'.join(map(json.dumps,docs)))indexApi = manticoresearch.IndexApi(client)
docs = [ \
{"replace": {"table" : "products", "id" : 1, "doc" : {"title" : "document one"}}}, \
{"replace": {"table" : "products", "id" : 2, "doc" : {"title" : "document two"}}} ]
api_resp = await indexApi.bulk('\n'.join(map(json.dumps,docs)))docs = [
{"replace": {"table" : "products", "id" : 1, "doc" : {"title" : "document one"}}},
{"replace": {"table" : "products", "id" : 2, "doc" : {"title" : "document two"}}} ];
res = await indexApi.bulk(docs.map(e=>JSON.stringify(e)).join('\n'));body = "{\"replace\": {\"index\" : \"products\", \"id\" : 1, \"doc\" : {\"title\" : \"document one\"}}}" +"\n"+
"{\"replace\": {\"index\" : \"products\", \"id\" : 2, \"doc\" : {\"title\" : \"document two\"}}}"+"\n" ;
indexApi.bulk(body);string body = "{\"replace\": {\"index\" : \"products\", \"id\" : 1, \"doc\" : {\"title\" : \"document one\"}}}" +"\n"+
"{\"replace\": {\"index\" : \"products\", \"id\" : 2, \"doc\" : {\"title\" : \"document two\"}}}"+"\n" ;
indexApi.Bulk(body);string body = r#"{"replace": {"index" : "products", "id" : 1, "doc" : {"title" : "document one"}}}
{"replace": {"index" : "products", "id" : 2, "doc" : {"title" : "document two"}}}
"#;
index_api.bulk(body).await;replaceDocs = [
{
replace: {
index: 'test',
id: 1,
doc: { content: 'Text 11', cat: 1, name: 'Doc 11' },
},
},
{
replace: {
index: 'test',
id: 2,
doc: { content: 'Text 22', cat: 9, name: 'Doc 22' },
},
},
];
res = await indexApi.bulk(
replaceDocs.map((e) => JSON.stringify(e)).join("\n")
);body := "{\"replace\": {\"index\": \"test\", \"id\": 1, \"doc\": {\"content\": \"Text 11\", \"name\": \"Doc 11\", \"cat\": 1 }}}" + "\n" +
"{\"replace\": {\"index\": \"test\", \"id\": 2, \"doc\": {\"content\": \"Text 22\", \"name\": \"Doc 22\", \"cat\": 9 }}}" +"\n";
res, _, _ := apiClient.IndexAPI.Bulk(context.Background()).Body(body).Execute()Query OK, 2 rows affected (0.00 sec){
"items":
[
{
"replace":
{
"table":"products",
"_id":1,
"created":false,
"result":"updated",
"status":200
}
},
{
"replace":
{
"table":"products",
"_id":2,
"created":false,
"result":"updated",
"status":200
}
}
],
"errors":false
}Array(
[items] =>
Array(
Array(
[_index] => products
[_id] => 2
[created] => false
[result] => updated
[status] => 200
)
Array(
[_index] => products
[_id] => 2
[created] => false
[result] => updated
[status] => 200
)
)
[errors => false
){'error': None,
'items': [{u'replace': {u'_id': 1,
u'table': u'products',
u'created': False,
u'result': u'updated',
u'status': 200}},
{u'replace': {u'_id': 2,
u'table': u'products',
u'created': False,
u'result': u'updated',
u'status': 200}}]}{'error': None,
'items': [{u'replace': {u'_id': 1,
u'table': u'products',
u'created': False,
u'result': u'updated',
u'status': 200}},
{u'replace': {u'_id': 2,
u'table': u'products',
u'created': False,
u'result': u'updated',
u'status': 200}}]}{"items":[{"replace":{"table":"products","_id":1,"created":false,"result":"updated","status":200}},{"replace":{"table":"products","_id":2,"created":false,"result":"updated","status":200}}],"errors":false}class BulkResponse {
items: [{replace={_index=products, _id=1, created=false, result=updated, status=200}}, {replace={_index=products, _id=2, created=false, result=updated, status=200}}]
error: null
additionalProperties: {errors=false}
}class BulkResponse {
items: [{replace={_index=products, _id=1, created=false, result=updated, status=200}}, {replace={_index=products, _id=2, created=false, result=updated, status=200}}]
error: null
additionalProperties: {errors=false}
}class BulkResponse {
items: [{replace={_index=products, _id=1, created=false, result=updated, status=200}}, {replace={_index=products, _id=2, created=false, result=updated, status=200}}]
error: null
additionalProperties: {errors=false}
}{
"items":
[
{
"replace":
{
"table":"test",
"_id":1,
"created":false,
"result":"updated",
"status":200
}
},
{
"replace":
{
"table":"test",
"_id":2,
"created":false,
"result":"updated",
"status":200
}
}
],
"errors":false
}{
"items":
[
{
"replace":
{
"table":"test",
"_id":1,
"created":false,
"result":"updated",
"status":200
}
},
{
"replace":
{
"table":"test",
"_id":2,
"created":false,
"result":"updated",
"status":200
}
}
],
"errors":false
}