知识库

将显式 Lucene 索引迁移到原生模式索引

鉴于仍有一些客户使用利用传统/显式索引的较旧 Neo4j 版本,本文将讨论在升级到 Neo4j 4.x 版本时如何将这些索引转换为原生模式索引的一些要点,因为传统/显式索引已在 3.5.x 版本中被弃用,并在 4.x 版本中完全移除。

关于背景,在 Neo4j 3.2 版本(发布日期 2017 年)之前,传统/显式索引是唯一可用的索引类型。这些早期版本索引在底层通过 Lucene 实现,通常会导致显著的写入性能下降,因此需要在 3.3+ 版本中使用可用的原生模式索引来替代它们。

除了性能方面,同样重要的是,显式索引在通过 Java API(以及/或从 3.3.x 开始的存储过程)添加/删除/更新节点/关系/属性时,还必须显式地/手动地保持最新,这就是它们被称为显式索引的原因。

相比之下,原生索引会自动维护,从而无需任何此类手动步骤和/或额外代码来使其保持最新。

从实现的角度来看,这些传统索引没有列出很多模式详细信息,除了原始作者在创建索引时选择的命名约定外,没有太多有用的线索可以帮助以脚本化的方式实现将这些索引迁移到 Neo4j 原生模式索引的自动化过程。

因此,任何索引迁移都必须逐个索引进行,并且可能涉及一定程度的猜测。这意味着您需要查看 API 代码并评估哪些节点或关系属性正在被索引,并相应地在这些属性上实现一个补充模式索引,或者查看 Cypher 查询并提出支持性索引来优化其执行。

总体而言,从宏观角度来看,推荐的方法如下:

1) 获取当前版本中所有索引的列表

  • 示例:在 3.x 中,您可以使用 "call db.index.explicit.list();",它将显示索引名称及其类型(可以是“精确”或“全文”),以及它是节点索引还是关系索引,这将帮助您确定要构建哪种类型的模式索引。

2) 接下来查看您的 Cypher 和/或 Java API 代码,并相应地将其转换为 4.x+ 格式。

  • 例如,将 "start" 语句转换为等效的 Match 语句。示例:

    • START n=node:myExplicitIndexYear("myid:1234567") RETURN n;

    • 上述查询将更改为以下内容:

    • MATCH (n:Person{myid:1234567}) RETURN n;

3) 实现您需要支持和优化步骤 2 中相关/转换后的查询执行的等效模式索引。

  • 示例:CREATE INDEX index_name FOR (n:Person) ON (n.myid);

  • 更具体地说:

    • 检查所有 Cypher 查询实际上写入该索引的内容,并推导出创建索引(用于整数、字符串、日期、地理空间等)和/或调用 db.index.fulltext.createIndexForNodes (FTS) 语句。

    • 检查对该索引的所有读取,并调整语句以使用新索引(这意味着启用查询日志记录以捕获生成的 Cypher 查询及其性能属性,例如执行时间、I/O、内存和 CPU,以便逐个标记运行时间最长的查询进行故障排除——您的查询模式很可能彼此相似,因此,一旦修复了一个查询,它将同样帮助其他类似的查询)。

4) 完成所有显式索引后,关闭数据库并移动/删除相关的索引子文件夹(例如 /data/databases/graph.db/index)。

请注意,如果您将传统索引转换为等效的模式索引后,使用传统索引的搜索查询返回了意外的行数(与模式索引相比),那么这很可能表明模式索引可能定义不正确(在错误的标签:属性上),或者传统索引未及时更新(请记住,传统索引必须手动保持最新,因此举例来说,如果您的 Java API 代码中没有适当的错误处理,那么在遇到回滚时,例如由于服务器重启等任何原因,事务完全有可能没有正确地重试并应用提交)。这里有一个 3.x 示例说明了这种情况:

// Create New Nodes
//
CREATE (p:Person {name:'Steve_1',year:1990});
CREATE (p:Person {name:'Steve_2',year:1990});
CREATE (p:Person {name:'Steve_3',year:1990});


// Create Two Explicit Indexes(also called Legacy/Manual/Lucene Indexes)
//
CALL db.index.explicit.forNodes('myExplicitIndexFTS', {type: 'fulltext', provider: 'lucene'});
CALL db.index.explicit.forNodes('myExplicitIndexYear', {type: 'exact', provider: 'lucene'});

// List Explicit Indexes
//
call db.index.explicit.list;

// Index nodes with name property
//
MATCH (p:Person{name:'Steve_1'}) call db.index.explicit.addNode('myExplicitIndexFTS', p, 'name', 'Steve_1') yield success return count(*);
MATCH (p:Person{name:'Steve_2'}) call db.index.explicit.addNode('myExplicitIndexFTS', p, 'name', 'Steve_2') yield success return count(*);

// Index nodes with "birth year" property
//
match (p:Person{name:'Steve_1'})
with p
call db.index.explicit.addNode("myExplicitIndexYear",p,"year",p.year) yield success return count(*);

// Search for persons with matching name VE, which will only return Steve_1, and Steve_2
//
CALL db.index.explicit.searchNodes('myExplicitIndexFTS','name:*VE*');

// Search for persons with year=1990 which will only return Steve_1
//
CALL db.index.explicit.searchNodes('myExplicitIndexYear','year:1990');

// Search for persons with year=1990 using explicit index which will only return Steve_1
//
start n=node:myExplicitIndexYear("year:1990") return n;

// Convert "start" to equivalent Match statement, and this statement returns all 3 rows corresponding to year=1990 (and of course ideally, you would want to create an index on :Person(
year) or :Person(name) for best performance when creating equivalent native schema indexes on these two properties.
match (n:Person{year:1990}) return n;

附录