在Neo4j中使用Java插件运行情感分析
在 [《SingleStore Cookbook》](https://singlestore-cookbook.github.io/part-ml/running-sentiment-analysis-inside-the-database-with-webassembly.html) 的一章中,有一个完整的情感分析流水线:使用 Rust 编译为 WebAssembly,并通过 SingleStore 的 Code Engine 直接加载到数据库中。最终效果非常干净:一行 CLI 命令即可部署,情感评分在数据库引擎内部与数据一同运行,并且在其上构建了完整的“股价 + 头条新闻”分析流水线。
我们能否在 Neo4j 中实现同样的效果?[Neo4j](https://dzone.com/articles/leveraging-neo4j-for-identity-access-management) 拥有一个文档完善且官方支持的扩展模型,允许我们用 Java 编写自定义函数和存储过程,并直接注册到数据库引擎中。Java 也有 **Valence Aware Dictionary and sEntiment Reasoner**(VADER)的移植版本,也就是 SingleStore Rust 实现中使用的同款基于词典的情感分析器。所有零件都齐备了。问题是它们能否很好地组合在一起,以及最终的流水线与 SingleStore 的 Wasm 方案相比会是什么样。本文记录了一个从始至终的完整实验:UDF 实现、图模式、完整的数据加载与评分流水线,以及一组完整的分析查询。在此过程中,我们还发现 Neo4j 还有第二条情感分析路径——通过 NLP 存储过程实现;而在这两者之间做选择,本身就是一个有趣的工程决策。
本文的目标并不是提出新的情感分析技术,而是探索 Neo4j 的扩展模型能实现什么,以及其结果与 SingleStore 的等价实现相比如何。完整源代码可在 [GitHub](https://github.com/VeryFatBoy/neo4j/tree/main/sentiment-udf) 上获取。
我们要构建什么
图 1 展示了数据如何在流水线中流动。CSV 文件通过 LOAD CSV 或 Python 加载器导入 Neo4j。每创建一个 Headline 节点时,会在同一条 Cypher 语句中内联调用 sentiment.score()——评分发生在数据库内部、数据摄入阶段,而不是单独的应用程序步骤中。生成的图随后可供本文后面介绍的分析查询使用。
[LOADING...]
图 1:流水线数据流
该流水线镜像了 SingleStore 书籍章节中的方案:
- 一个基于 VADER 的情感函数,注册到系统中,可从查询中调用
- 一个包含合成股价 tick 和新闻头条的图
- 一组分析查询:逐条头条评分、日聚合、情感与股价关联、最积极/最消极排名,以及实时一致性检查
对于本文中的示例,我们需要本地安装的 Neo4j、Docker 容器,或者一台可以放置文件并重启进程的服务器。
Neo4j 扩展机制的工作原理
Neo4j 允许我们通过打包为 .jar 文件的自定义 Java 代码扩展 Cypher。这是一条有完整文档且受官方支持的扩展路径。Neo4j 发布了关于[设置插件项目](https://neo4j.com/docs/java-reference/current/extending-neo4j/project-setup/\)的官方指南,并在 GitHub 上维护了一个[Neo4j Procedure Template](https://github.com/neo4j-examples/neo4j-procedure-template\)。
Neo4j 提供这种扩展模型,用于构建自定义扩展。有几种扩展类型:
- **用户自定义函数(UDF)**——接收输入,返回单个值,像内置函数一样在查询中内联调用
- **用户自定义聚合函数(UDA)**——分组级聚合,类似于
SUM或COLLECT - **存储过程(Procedure)**——更灵活,可以返回多行并产生副作用,使用
CALL调用
对于我们的情感分析用例,UDF 是最合适的。我们传入一个字符串,得到一张极性分数映射。在 SingleStore 中,等价物是返回行集的表值函数(TVF)。Neo4j UDF 返回 Map 是在结构上最接近的等价物。
关于命名的一个实用提示:Neo4j 维护了一个保留和废弃的存储过程命名空间列表,例如 db.*、dbms.*、graph.* 等。这些是不能使用的。sentiment.* 命名空间既不是保留的,也不是废弃的,因此是一个安全的选择。在为任何新插件选择命名空间之前,请查阅[用户自定义存储过程](https://neo4j.com/docs/java-reference/current/extending-neo4j/procedures/#reserved-and-deprecated-namespaces\),确认它不会与内置命名空间冲突。
构建前需要了解的事项
由于 Neo4j UDF 运行在与数据库引擎相同的 JVM 中,因此在深入之前,有必要了解一些实际注意事项。这些注意事项同样适用于任何对运行中的 JVM 进程的扩展——Neo4j 自己的插件作者也需要处理这些问题——提前意识到这些,可以让构建体验更顺畅。
**内存**。如果插件分配的内存超过 JVM 可用内存——例如加载非常大的模型文件或在多次调用之间累积状态——可能触发 OutOfMemoryError。我们在这里构建的 VADER UDF 会加载一个紧凑的词典,并且不持有任何状态,因此实际上无需担心。对于需要分配大量堆内存的更复杂插件,Neo4j 提供了预览版的 ProcedureMemory API,我们可以将分配注册到配置的事务内存限制中,从而防止无上限的内存增长导致数据库重启。
**未捕获异常**。UDF 中未处理的 RuntimeException 会向上传播,穿过 Neo4j 查询执行引擎。在 UDF 代码中做好错误处理可以避免这个问题。
**无限循环和线程饥饿**。一个挂起的 UDF——比如等待网络调用、死锁或陷入循环——会占用 Neo4j 共享线程池中的一个 JVM 线程。VADER UDF 不进行任何网络调用、不持有状态,并且每次调用只执行相对较少的计算,因此在这里无需担心;但对于更复杂的插件来说,这一点很重要。
**依赖冲突**。由于插件 jar 与数据库引擎共享类路径,打包到 fat jar 中的任何库都不能与 Neo4j 已经自带的库冲突。开发过程中我们就遇到了这个问题,构建部分会详细说明,以及一个简单的修复方法。
**启动失败**。无法加载的 jar 会导致系统无法启动。解决方法始终是先在开发环境中测试,例如 Neo4j Desktop 或本地 Docker 容器,然后再部署到更关键的环境中。
**安全性**。Java 插件可以完全访问 JVM、文件系统和网络。这与 Neo4j 自己插件的信任模型相同,并且适用于我们自己编写和审查过的代码。对于来自不可信来源的第三方插件,应像对待任何在关键进程中运行的第三方代码一样保持警惕。
**AuraDB**。[AuraDB](https://dzone.com/articles/kafka-neo4j-event-streaming\) 支持由 Neo4j 提供和认证的插件,例如 APOC、GDS 和 GenAI,但不支持任意的第三方或自定义 jar。本文中的 Java UDF 方案要求自管理的 Neo4j,例如 Desktop、Docker 或服务器安装。如果目标是 AuraDB,那么这里描述的 Java UDF 方案不可用;GenAI 插件或外部服务是替代方案。
这些都不应妨碍我们构建 Java UDF。我们在这里构建的 VADER UDF 很小、只做一件事、不进行网络调用、不持有状态,并且使用了一个经过良好测试的库。合理的方法是——这适用于任何插件开发——先在本地开发实例上构建和测试,然后再放心部署。
在 Neo4j 中,部署 UDF 的步骤如下:
- 构建 fat jar
- 停止服务器
- 将 jar 文件复制到服务器的
plugins目录 - 在
neo4j.conf中添加白名单条目 - 重启服务器
这种部署模型与 Wasm 方案不同——更多内容见下文“构建与部署”部分。
搭建项目
前置条件
在开始之前,我们需要以下内容:
- **Java 21**——使用
java -version检查。Java 21 是官方 Neo4j 插件模板以及本文使用的版本 - **Maven 3.8+ **——使用
mvn -version检查 - **Neo4j 2026.06.0**——本文使用的版本,按下面描述的方式之一运行
选择 Neo4j 安装方式
对于这个实验,我们使用 Neo4j Desktop 或 Docker。Neo4j 也支持在 Linux 和 Windows 上服务器安装——插件机制相同——但我们没有测试该路径,因此这里不提供相关说明。
**Neo4j Desktop** 是最简单的起点。从[Neo4j for Desktop](https://neo4j.com/download/\)下载,创建一个新项目并启动本地数据库服务器。要找到 plugins 目录的准确路径,请点击 **Open folder > plugins**。
**Docker** 适合干净、一次性的环境。下面的命令会启动 Neo4j 2026.06.0,并将一个 plugins 卷挂载到本地目录,我们将 jar 放到这个目录中:
使用 Docker 时,我们通过环境变量传递白名单,而不是直接编辑 neo4j.conf。jar 文件放到主机上的 ~/neo4j/plugins/ 中。
创建项目结构
创建一个新的 Maven 项目目录:
完成后的完整目录树应如下所示:
下面的部分将逐一介绍每个部分。接下来,我们创建两个源码目录:
Maven 依赖
我们将在项目根目录创建一个 pom.xml 文件。其结构遵循官方[Neo4j Procedure Template](https://github.com/neo4j-examples/neo4j-procedure-template\),并针对本项目做了三处调整,下面会解释。
与官方模板相比,三处调整已在上面代码块中以注释形式标出。其余内容——groupId 约定、Neo4j 依赖的 provided 作用域、shade 插件结构以及测试依赖模式——均遵循官方指南。
编写 UDF
创建文件 src/main/java/sentiment/Sentimentable.java 并粘贴以下内容:
以下实现细节值得强调。
v1.1.1 API 使用的是静态方法——SentimentAnalyzer.getScoresFor(text)——而不是可变实例。这意味着调用之间没有共享状态,这正是 Neo4j UDF 所需要的:多个 Cypher 查询可能并发调用该函数。VADER 词典库会在首次调用时在内部加载,并在后续调用中缓存。
@UserFunction("sentiment.score") 注解将该方法注册为在 Cypher 中可用该名称调用。参数上的 @Name 注解为 Neo4j 的函数元数据和文档提供了参数名——在 Cypher 中,UDF 总是以位置参数方式调用,正如本文通篇所示:sentiment.score(row.headline)。
返回类型是 Map。在 Cypher 中,这会表现为一个字面量映射,因此调用者可以用点号语法解构它:sc.compound、sc.positive 等等。
在 SingleStore 版本中,TVF 返回一个行集,并在 FROM 子句中使用。而在这里,UDF 则是在 WITH 或 RETURN 子句中内联调用的。
编写测试
按照官方 Neo4j 过程模板的模式,我们将使用 neo4j-harness 在 JUnit 中启动一个轻量级嵌入式 Neo4j 实例,注册我们的 UDF,并对其运行 Cypher 查询——整个过程无需部署到运行中的数据库。这是 Neo4j 官方文档推荐的测试方法。
创建文件 src/test/java/sentiment/SentimentableTest.java 并粘贴以下内容:
这四个测试与稍后在 Neo4j Browser 中手动运行的测试相对应,但现在它们会在构建过程中自动执行。Neo4jBuilders.newInProcessBuilder() 会启动一个轻量级嵌入式实例,并注册 Sentimentable 函数;.withDisabledServer() 会跳过 HTTP 服务器,因为我们只需要 Bolt 连接。其结构遵循官方 JoinTest.java 模式。
构建与部署
第 1 步:安装 Maven Wrapper 并构建
官方 Neo4j 过程模板使用 Maven Wrapper(mvnw),这意味着我们只需要安装 Java,而不需要单独安装 Maven。要为项目添加 wrapper,请运行:
然后构建并运行测试:
或者在开发期间跳过测试:
如果想直接使用全局安装的 Maven,mvn clean package -DskipTests 同样可行——wrapper 只是方便,不是必需。
Maven 会编译 Java 源码、运行 Shade 插件,并在 target/ 下生成两个 jar 文件。我们需要的是 sentimentable-1.0.0-SNAPSHOT.jar——即内嵌了 VADER 的 fat jar。original-sentimentable-1.0.0-SNAPSHOT.jar 是不含依赖的普通 jar,因此可以忽略。
如果构建失败并出现 package org.neo4j.procedure does not exist 错误,请检查 pom.xml 中的 Neo4j 依赖是否设置为 provided,以及版本是否与运行的 Neo4j 实例匹配。
第 2 步:将 Jar 复制到插件目录
**Neo4j Desktop**:
- 停止服务器
- **Open folder > plugins**,将
sentimentable-1.0.0-SNAPSHOT.jar复制到该目录 - **Open folder > conf > neo4j.conf**,找到
dbms.security.procedures.allowlist=,如果该行被注释则取消注释 - 在行尾添加
sentiment.*
**Docker**:将其复制到挂载为 /plugins 的主机目录:
第 3 步:将函数命名空间加入白名单
Neo4j 默认的 dbms.security.procedures.allowlist 是 *,会加载所有插件。如果配置了特定的白名单条目,则必须包含任何自定义命名空间,否则函数会静默不可用——启动时不会报错,只是该函数不存在。按照最小权限原则配置显式白名单是一个好习惯。
我们的 UDF 只使用公开的 Neo4j 过程 API,因此不需要单独的 dbms.security.procedures.unrestricted 设置——只有访问内部 API 的扩展才需要该设置。
第 4 步:重启 Neo4j
**Neo4j Desktop**:使用 Desktop UI 中的按钮重启服务器。如果 Desktop 在启动后立即显示“stopped”,请直接打开 http://localhost:7474——服务器可能已经在运行,只是 UI 还没有反映出来。
**Docker**:如果是首次启动,则无需重启——前面“选择 Neo4j 安装方式”中的 docker run 命令已经在启动 Neo4j 时,通过挂载的 plugins 目录将 jar 放在合适位置。如果容器已经运行,需要更新 jar,则停止容器,替换 ~/neo4j/plugins/ 中的 jar,然后重启:
确认插件正确加载的最清晰方式是运行下面第 5 步中的验证查询——如果 sentiment.score() 可见并返回结果,说明 jar 已被成功加载。
验证函数
我们可以通过在浏览器中输入 http://localhost:7474 来与 Neo4j 交互。
第 5 步:确认函数已加载
首先,我们检查 Neo4j 是否能看到该函数:
预期输出:
如果返回零行,说明 jar 不在 plugins 目录中、白名单条目缺失或拼写错误,或者 Neo4j 没有完全重启。
第 6 步:运行测试
运行以下测试:
预期输出:
接下来,我们测试 VADER 对大小写的感知是否生效:
预期输出:
大写 GREAT! 使 compound 分数上升,这与 Wasm 版本完全一致。在我们测试的示例中,Java 移植版产生的分数与书籍章节中使用的 Rust crate 一致。
现在测试空值保护。传入空字符串应返回中性结果,而不是抛出异常:
预期输出:
如果三个查询都返回预期值,说明 UDF 工作正常,我们可以开始构建图模式并加载数据了。
设计图模式
该流水线的图模型有三种节点标签,如图 2 所示。中心 Stock 节点通过 HAS_TICK 关系连接到 Tick 节点,并通过 HAS_HEADLINE 关系连接到 Headline 节点。VADER 极性分数在摄入时直接存储在每个 Headline 节点上,因此任何 Cypher 查询都可以直接使用,无需重新计算。
[LOADING...]
图 2:图数据模型
Stock 节点充当连接键。在 SingleStore 中,查询在 (symbol, DATE(ts)) 上连接 tick 和 stock_sentiment;而在 Neo4j 中,同样的共同引用通过从共享的 Stock 节点遍历到 Tick 和 Headline 节点,并加上日期谓词来表达。关系取代了外键。
现在运行以下命令来创建约束和索引:
加载数据并对头条新闻进行评分
获取数据集
原始 SingleStore 书籍章节的数据集、Notebook 和 SQL 文件都可以在书籍的 [GitHub 仓库](https://github.com/singlestore-cookbook/singlestore-cookbook.github.io/tree/main/code/part-ml/running-sentiment-analysis-inside-the-database-with-webassembly\) 中公开获取。我们需要的两个 CSV 文件位于 datasets 子目录中:
fictitious_stocks.csv——合成的每日 OHLCV 股价(随机游走模型,虚构代码)raw_fictitious_headlines.csv——程序生成的新闻头条(模板 + 股票代码 + 金融事件)
我们将把这两个文件下载到本地工作目录。### 数据集格式
fictitious_stocks.csv 有七列。其中 date 和 Name 列分别重命名为 ts 和 symbol,以匹配图模式:
raw_fictitious_headlines.csv 有五列,直接映射到 Headline 节点属性:
除了加载器已经完成的处理(例如丢弃空值、过滤一个极端成交量离群值、按日期排序)之外,不需要额外预处理。
Python 加载器
下面的 data_loader.py 读取两个 CSV 文件,并通过 Python 驱动写入 Neo4j。如果尚未安装依赖,请先执行:
然后运行加载器,并将实际下载的 CSV 文件路径替换到脚本中。同时,请将 your_password_here 替换为实际密码。
运行 Python 程序:
关键一行是 Cypher 中的 sentiment.score(row.headline) AS sc。这等同于 SingleStore INSERT ... SELECT 中 sentimentable(i.headline) TVF 调用的作用——在数据库层面、写入记录的同一操作中计算分数,无需往返应用层。
需要注意:如果需要重新运行加载器,脚本对 Tick 和 Headline 节点使用的是 CREATE,因此在未清空数据库的情况下再次运行会产生重复数据,而不是覆盖写入。请先用以下 Cypher 清空数据库(使用 Query 选项卡):
100 的批次大小是刻意为之的——更大的值可能超出默认事务内存限制并导致失败。清空后,在再次运行加载器之前,请重新创建 schema 约束和索引。
备选方案:使用 LOAD CSV 直接从 GitHub 加载
为了完全使用 Cypher 并避免 Python,Neo4j 的 LOAD CSV 命令可以通过 HTTPS 直接从 GitHub 获取文件。不需要复制文件、不需要 import 目录、也不需要 Python 依赖。请按顺序使用 Query 选项卡运行以下两个查询——先加载 tick 数据,再加载标题数据,因为标题查询会对 tick 查询创建的 Stock 节点执行 MATCH:
LOAD CSV WITH HEADERS 将第一行读取为列名,因此原始名称(row.Name、row.date)会被直接内联映射到图属性名——这与 Python 加载器中使用 rename() 完成的列重命名相同。IN TRANSACTIONS OF 1000 ROWS 的分批设置对于约 60 万行的 tick 文件是必需的,以避免事务内存限制。
同样的“先删除再加载”规则在这里也适用:在未清空数据库的情况下重新运行任一查询都会产生重复数据。唯一的要求是 Neo4j 具备访问 GitHub 的出站 HTTPS 能力,Neo4j Desktop 和本地 Docker 都满足。在网络受限的服务器环境中,使用本地文件的 Python 加载器是更安全的备选方案。
接下来,可以使用 Query 选项卡运行一些示例查询进行测试。
标题级情感分析
按股票和日期聚合情感
将情感与收盘价关联
在 Cypher 中,共享的 Stock 节点使 symbol 连接变为隐式,我们只需要一个日期谓词。
最正面标题
最负面标题
在 SingleStore 书中,CEO 丑闻标题在多个股票的负面排名中占主导地位。这里我们看到相同的模式,因为底层使用的 VADER 词表完全相同。
验证存储分数与实时 UDF 调用的一致性
这对应 SingleStore 书中的一致性检查:将存储的 stock_sentiment 值与新的 JOIN LATERAL sentimentable(...) 调用进行比较,以确认摄入管道是确定性的。
每日平均情感 vs. 收盘价
书中的 CTE 风格聚合可以自然地转换为 Cypher 的 WITH 链式写法。
经验总结
这次实验取得了明确成功。VADER 可以在 Neo4j 内部运行,通过一个简单的 Cypher 调用在摄入时对标题进行情感评分,SingleStore 书中的所有分析查询在 Cypher 中都有直接对应实现。在我们测试的示例中,Java 移植版产生的分数与 SingleStore 书中使用的 Rust crate 一致——不过由于分词或浮点处理的差异,独立的语言移植版本可能在边界情况下有所不同。
图模型能够自然地处理“股票 tick 数据 + 新闻标题”这一领域,并且在多个方面,Cypher 查询比对应的 SQL 更具表达力——通过共享 Stock 节点的关系遍历取代了基于键的 SQL 连接,这种方式反映了领域的真实结构,而不仅仅是实现细节。
图模型在连接查询上具有真正的优势。用通过共享 Stock 节点的图遍历取代 JOIN tick ON (symbol, DATE(ts)),不仅仅是语法偏好——它反映了领域的真实结构。股票代码作为图实体,自然地连接 tick 数据和新闻标题,Cypher 比基于键的 SQL 连接表达得更直接。
数据库内评分确实可行。在 Cypher CREATE 语句中调用 sentiment.score(row.headline),意味着评分和摄入在同一操作中完成,无需往返应用层。这与 SingleStore Wasm 管道的目标相同,而 Java UDF 干净地实现了这一点。
依赖冲突是一次性修复。开发过程中我们遇到了 commons-lang3 版本冲突,导致服务器无法启动。修复方法——使用 Maven Shade 插件将捆绑的类重定位到私有命名空间——只要知道查找方向就很简单,解决方案也已固化在本文的 pom.xml 中。
与 SingleStore Wasm 方式也存在一些客观差异。
部署需要重启。SingleStore 使用的工具可以将函数加载到运行中的数据库,无需停机。Neo4j 则需要构建 jar、复制文件、修改配置并重启。对于初始 Docker 启动,jar 会被自动加载——但之后任何 jar 的更新都需要重启容器。Maven Wrapper 和本文中清晰的部署步骤使该过程可重复。
没有执行沙箱。SingleStore 将每个 Wasm 函数实例运行在独立的隔离进程中,并具有硬性内存边界。Neo4j UDF 则与服务器运行在同一个 JVM 中。对于像 VADER UDF 这样小型且行为良好的插件,这没有实际区别,但对于更复杂或重量级的插件,这是一个有意义的结构性差异。
语言基于 JVM。Wasm 方式接受任何可编译为 Wasm 核心规范的语言。Neo4j 的扩展模型仅限 JVM。对于希望将现有 Python 或 Rust 模型引入数据库的团队,这一点值得提前了解。
其他备选方案
Java UDF 是本文的重点,但它并不是将情感评分引入 Neo4j 数据附近的唯一方式。在实验过程中,我们考虑了几种替代方案。有些在特定用例下很有吸引力,另一些则不然。了解这些选项有助于我们针对自身情况选择合适的工具。
在数据库外部预先评分。在加载前对所有标题进行评分。将极性分数作为 CSV 的列加入,然后用 LOAD CSV 加载所有内容。Neo4j 内部不需要运行任何自定义代码。对于这类批量管道(数据加载一次、多次查询),这种方法完全可行,且不需要 Java 知识。唯一放弃的是在查询时于 Cypher 中内联调用 sentiment.score() 的能力。对许多团队来说,这可能是正确的答案,也是通往可用管道的最简单路径。
外部微服务。部署一个运行 VADER 并提供 HTTP 端点的小型 Python 或 Rust 服务。外部微服务可以通过 HTTP API 暴露 VADER,应用层在摄入前或摄入期间调用该服务。这提供了完全的进程隔离——情感服务崩溃不会影响数据库——并且适用于 AuraDB。代价是每次调用的网络延迟以及运行独立服务的运维开销。对于低吞吐量或交互式用例,这是一种干净、灵活的模式。
Neo4j GenAI 插件。Neo4j 的 GenAI 插件支持直接从 Cypher 调用 embedding 和 LLM API——包括 OpenAI、Azure OpenAI 及兼容端点。它由 Neo4j 完全管理,适用于 AuraDB,且不需要 Java。使用云 LLM 进行情感分类,而不是 VADER 词表,是一条支持良好、阻力较低的路径。代价是 API 成本,以及相比 VADER 完全透明、可检查的词表,大语言模型的不透明性——这在需要解释分数来源的受监管领域尤为重要。
GraalVM 原生编译。GraalVM 可以将 Java UDF 提前编译为原生二进制文件,从而减少 JVM 启动开销和内存占用。这是一种性能优化而非架构变更——代码仍运行在 Neo4j 进程中——并且在这个用例中只会增加显著的构建复杂度,收益有限。对于更大、更重量级的插件值得了解,但不是这里的正确选择。
在 Java UDF 内嵌 Wasm 运行时。理论上,我们可以将 wasmtime 等 Wasm 运行时嵌入 Java UDF,并在 Neo4j 内部执行 VADER Wasm 模块,从而在 Neo4j 的插件模型中获得 Wasm 的沙箱保证。这在技术上是可行的,但目前似乎没有公开可用的工作示例,而且与替代方案相比,复杂度成本很高。这是一个值得关注的有趣想法,但当下并不实用。
下表比较了上述方案在最重要维度上的差异。
Java UDF 位于此表的中间位置——它独有能力是从任意 Cypher 查询中内联调用 sentiment.score(),无需应用层介入,并且完全在系统内部运行,没有外部 API 调用或网络延迟。这种内联、自包含的能力是否正是我们用例所需要的,是关键问题。对于开发、实验以及数据和团队都足够清晰的管道,这是一种有吸引力且实用的方法。对于其他情况,上面的替代方案提供了不同但同样有效的权衡。
另一条路径:APOC NLP 过程
两种方法的主要区别在于计算发生的位置,如图 3 所示。使用 Java UDF 时,VADER 词表打包在 jar 中,评分在 Neo4j JVM 内部运行——没有网络调用、没有外部依赖、也没有单次调用成本。使用 APOC NLP 时,Neo4j 编排对外部云 API 的调用,并通过网络接收分数。这一架构差异驱动了本节讨论的大部分权衡。
[LOADING...]
图 3:Java UDF 与 APOC NLP
Neo4j 本身已经具备情感分析能力——只是工作方式非常不同,而且它不在 GDS 中,而是在 APOC Extended 中,这是与 APOC Core 分离的一个组件。APOC 的 NLP 过程充当基于云的自然语言 API 的包装器。支持的提供商包括 AWS Comprehend、Azure Cognitive Services 和 Google Cloud Natural Language。调用模式很直接。例如使用 AWS:
使用 Azure:
graph 变体会更进一步,通过在配置映射中设置 write: true,自动将情感结果写回节点属性。
如何在两者之间选择
当评分量很大、API 成本很重要、文本属于 VADER 所擅长的短社交媒体风格内容,或者需要离线/隔离环境时,Java UDF 是更强的选择。VADER 词表完全透明——我们可以检查某个字符串为何得到特定分数,这在受监管领域非常重要。
当 Java 知识有限、文本需要 VADER 词表之外的细微语言处理(否定、讽刺、领域特定词汇),或者云 NLP API 已经用于其他工作负载时,APOC NLP 是更强的选择。
一个适用于两者的重要限制:APOC NLP 属于 APOC Extended,而不是 APOC Core。AuraDB 默认包含 APOC Core,但 APOC Extended 在 AuraDB 中不可用——因此 Java UDF 和 APOC NLP 都无法在那里使用。GenAI 插件或外部微服务才是实用的 AuraDB 路径。
GDS(Neo4j 的图数据科学库)不包含文本级情感分析——它面向图算法。Neo4j 中的文本评分要么通过 Java UDF 在数据库内完成,要么通过 APOC 委托给云 NLP 服务。
总结
实验证实,Neo4j 的 Java 扩展模型是支持数据库内计算的可靠平台。VADER UDF 可以正常工作,图模型天然适配“股票 tick 数据 + 新闻标题”领域,分析查询也能干净地从 SQL 转换为 Cypher——在某些情况下表达力更强,因为价格与标题之间的关系在图模式中是显式的,而不是在查询时通过连接谓词推断出来的。
更有趣的工程问题是什么时候使用 Java UDF,什么时候使用替代方案。答案主要取决于四个因素:
- 部署模型(UDF 仅适用于自托管的 Neo4j)
- 延迟与网络需求(UDF 没有;APOC NLP 和外部微服务都会引入)
- 模型复杂度(VADER 词表透明、快速但有局限;云 NLP API 提供更好的语言覆盖)
- 运维约束(Java 知识、插件管理,以及更新后需重启的要求都有成本)
没有普遍正确的选择——APOC NLP 一节中的表格列出了各种权衡,理性的团队会根据优先级做出不同选择。本文确实证明的是:该方法可行,并且受到官方支持。构建插件的流程有文档和模板可循。对于开发、实验以及理解到位的生产管道,这是一条实用且有趣的路径。
若想进一步探索,官方的 Neo4j Procedure Template 是一个很好的起点,neo4j-harness 可以在无需运行数据库实例的情况下轻松对 UDF 进行单元测试,完整的 Neo4j Java Reference 则深入介绍了过程、聚合函数以及完整的扩展 API。
完整源代码可在 GitHub 上获取。
本文表达的观点仅代表 DZone 贡献者个人意见。