Compare commits

...

236 Commits

Author SHA1 Message Date
opengauss_bot 4c3fe10640
!99 merge master into master
更新2025申报表格

Created-by: difohyjoyjo
Commit-by: eugenecheng
Merged-by: opengauss_bot
Description: 【标题】(请简要描述下实现的内容)

【实现内容】:

【根因分析】:

【实现方案】:

【关联需求或issue】:

【开发自验报告】:
1. 请附上自验结果(内容或者截图)
2. 是否可以添加fastcheck测试用例,如是,请补充fastcheck用例
3. 是否涉及资料修改,如是,在docs仓库补充资料
4. 是否考虑升级场景(系统表修改、日志持久化以及修改执行态数据格式)
5. 是否考虑在线扩容等扩展场景
6. 是否考虑异常场景/并发场景/前向兼容/性能场景
7. 是否对其他模块产生影响
8. 是否需要回合补丁版本(5.0、6.0),涉及(**结果错误|coredump|性能劣化|内存泄露|集群异常**)相关修复请排查回合补丁分支

【其他说明】:

See merge request: opengauss/examples!99
2025-11-21 17:59:06 +08:00
eugenecheng f40cc4b745 更新2025申报表格 2025-11-21 16:16:48 +08:00
opengauss_bot 8e5c3b8555
!98 merge feature/er-model into master
开源之夏2025 openGauss DataKit支持ER图设计和建模插件

Created-by: soul_seeker
Commit-by: wangziqi
Merged-by: opengauss_bot
Description: 🧩 新增功能模块:ER-Model 插件(ER Model Plugin)

为 openGauss DataKit 新增了独立的可插拔模块 ER-Model,
提供面向数据库开发者的 Web 端实体关系(ER)建模与可视化设计能力。
该模块基于 DBML(Database Markup Language)
 标准,
实现数据建模的 定义、解析、转换与可视化全流程管理,
支持模型与数据库结构的 双向转换与交互执行。

🌟 功能概述
1️⃣ DBML ↔ SQL 双向转换

遵循 DBML 官方规范(https://www.dbml.org/home/
);

支持将 DBML 模型文件 自动解析为 openGauss / PostgreSQL SQL 建表语句;

支持从 数据库实例结构 反向生成 DBML 模型,实现模型-数据库同步;

内置 openGauss 方言类型适配与主外键关系映射。

2️⃣ 数据库连接与 Schema 导入

提供统一的数据库连接管理面板,支持配置多个 openGauss 实例;

一键导入指定 Schema 的表结构信息(表名、字段、索引、外键等);

支持通过连接选择目标数据库实例执行建表 SQL。

3️⃣ ER 图可视化设计与编辑

基于 DBML 解析生成可视化 ER 图,支持拖拽、编辑与自动布局;

图形编辑实时同步至 DBML 文本;

支持 关系高亮、字段类型展示、层次布局优化。

4️⃣ 模型导出与共享

支持导出为多种格式:

SQL 文件(建表语句)

PlantUML 文件(自动生成架构图)

PNG / SVG 图片(文档展示)

一键复制或下载,方便团队协作与版本控制。

5️⃣ 一键执行建表 SQL

支持在 Web 界面直接执行生成的建表 SQL 至指定 openGauss 数据库;

执行过程支持日志输出与错误捕获。

⚙️ 技术实现要点

前端:Vue3 + Quasar + DBML-Graph

后端:Spring Boot + openGauss JDBC + Redis

工具类:SqlGeneratorUtil 负责 DBML → openGauss SQL 精准生成;

接口路径:/ER/export-sql 支持 SQL 导出与执行;

支持反向解析数据库结构为 DBML 模型(Schema Import)。

📚 官方参考链接

📖 DBML 官方网站: https://www.dbml.org

💾 DBML GitHub 仓库: https://github.com/holistics/dbml

🌐 DBML 在线编辑器(官方示例环境): https://dbdiagram.io/home

See merge request: opengauss/examples!98
2025-10-30 14:31:24 +08:00
wangziqi 430cc3908e docs(ER-Modeling): 更新开发环境运行步骤
- 修改数据库连接配置说明
- 更新SSL密钥文件拷贝步骤
- 补充密码复杂度要求(包含数字、大小写字母、特殊字符)- 调整步骤编号以匹配最新安装流程
- 添加openGauss-workbench安装链接参考
2025-10-30 03:00:51 +08:00
wangziqi e394551679 track jdbc jar with LFS 2025-10-30 01:26:42 +08:00
wangziqi e32b196684 chore(deps): 更新依赖版本并调整构建配置- 升级 @babel/eslint-parser 和 @quasar/app-webpack 版本- 添加 overrides 配置以锁定关键 babel 依赖版本
- 调整 sass-loader 配置使用指定的 sass 实现
- 更新 README 构建步骤说明,增加 SSL 密码配置指引
- 补充 Maven 项目导入与环境变量设置说明- 优化文档结构与命令展示格式
2025-10-30 01:24:04 +08:00
wangziqi d7b45df997 新增 ER-Modeling 项目:
- 基于 Quasar + Vue 的 ER 图可视化插件
- 支持 DBML 转 SQL、反向解析数据库结构
- 与 openGauss DataKit 后端集成,实现数据库建模与可视化
2025-10-29 21:05:54 +08:00
wangziqi dbfbf64096 新增 er-model 项目:
- 基于 Quasar + Vue 的 ER 图可视化插件
- 支持 DBML 转 SQL、反向解析数据库结构
- 与 openGauss DataKit 后端集成,实现数据库建模与可视化
2025-10-29 20:44:15 +08:00
opengauss_bot d07eb9a8ec
!97 merge feature/er-model into master
开源之夏2025-DataKit支持ER图设计和建模

Created-by: soul_seeker
Commit-by: wangziqi
Merged-by: opengauss_bot
Description: 新增了 ER-Model 插件项目,为 openGauss DataKit 提供基于 Web 的 ER 模型设计与可视化功能。
主要实现以下能力:

- 支持 DBML ↔ SQL 双向转换

- 实例 Schema 导入与连接管理

- ER 图可视化编辑、自动布局与关系高亮

- SQL/PlantUML/图片导出

- 一键执行建表 SQL 至指定数据库

See merge request: opengauss/examples!97
2025-10-29 17:31:02 +08:00
wangziqi 4de6e57be6 Add ERModel project: ER-Model plugin for openGauss DataKit 2025-10-29 13:26:12 +08:00
opengauss_bot 96ceea0ac7
!96 merge master into master
OSPP: whyhow_opengauss

Created-by: paradox
Commit-by: paradox
Merged-by: opengauss_bot
Description: 【标题】openGauss向量数据库对接知识工程最佳实践

【实现内容】: WhyHow 与openGauss向量数据库适配,包括简单知识文档导入、对接大模型完成RAG端到端验证。

See merge request: opengauss/examples!96
2025-10-28 11:00:03 +08:00
paradox d3d1218950 docs 2025-10-27 21:55:44 +08:00
paradox 2ba3746346 Adapting the code to openGauss 2025-10-27 21:52:17 +08:00
paradox c248aea963 Import original WhyHow project source code 2025-10-27 21:50:59 +08:00
opengauss_bot 9da950ec10
!90 merge master into master
开源之夏2025 - 添加 openGauss 向量数据库集成 PrivateGPT 项目

Created-by: gcw_qP1tCuFs
Commit-by: gcw_qP1tCuFs
Merged-by: opengauss_bot
Description: 【标题】(请简要描述下实现的内容)

【实现内容】: 添加 openGauss 向量数据库集成 PrivateGPT 项目

【根因分析】: 现有 openGauss 社区缺少与 RAG/LLM 大语言模型的集成示例, 特别是和PrivateGPT项目的集成实例

【实现方案】: 通过修改PrivateGPT库代码 **(Fork到https://github.com/Suchun-sv/private-gpt)** 和 供应链LLamaIndex 代码 **(Fork 到 https://github.com/Suchun-sv/llama_index)** 成功将OpenGauss作为PrivateGPT的VectorStore,从而使得OpenGauss和PrivateGPT兼容

【关联需求或issue】: 开源之夏2025 《openGauss 向量数据库集成PrivateGPT》 项目

【开发自验报告】:
1. 请附上自验结果(内容或者截图)![image.png](https://raw.gitcode.com/user-images/assets/5089494/f4224523-d83e-4f99-947d-474f3ec9e8e9/image.png 'image.png')
2. 是否可以添加fastcheck测试用例,如是,请补充fastcheck用例
3. 是否涉及资料修改,如是,在docs仓库补充资料
4. 是否考虑升级场景(系统表修改、日志持久化以及修改执行态数据格式)
5. 是否考虑在线扩容等扩展场景
6. 是否考虑异常场景/并发场景/前向兼容/性能场景
7. 是否对其他模块产生影响
8. 是否需要回合补丁版本(5.0、6.0),涉及(**结果错误|coredump|性能劣化|内存泄露|集群异常**)相关修复请排查回合补丁分支

【其他说明】:

See merge request: opengauss/examples!90
2025-10-23 09:18:55 +08:00
opengauss_bot 527f97ee72
!80 merge master into master
开源之夏2025-openGauss向量数据库对接airbyte

Created-by: huliyan
Commit-by: huliyan
Merged-by: opengauss_bot
Description: 【标题】openGauss向量数据库对接airbyte。

【实现内容】: 
1.开发destination为opengauss向量数据库的airbyte目标连接器(destination connector);
2.输出实践案例,和详细使用文档。

【开发自验报告】:
请查看提交的 Airbyte_Destination_openGaussDataVec/Practical-Case-Tutorial/实践案例教程.md

See merge request: opengauss/examples!80
2025-10-22 14:43:39 +08:00
opengauss_bot 78193dc38f
!83 merge master into master
开源之夏2025-SUSE操作系统集成openGauss 6.0.0 LTS

Created-by: JaleShaw
Commit-by: JaleShaw
Merged-by: opengauss_bot
Description: 【标题】SUSE操作系统集成openGauss 6.0.0 LTS

【实现内容】:

针对SUSE系列操作系统的编译打包脚本、补丁和源码

【开发自验报告】:

更多内容请参阅项目Gitcode仓库:https://gitcode.com/JaleShaw/suse-rpm

See merge request: opengauss/examples!83
2025-10-22 14:43:00 +08:00
JaleShaw 1b1ec87c87
开源之夏2025-SUSE操作系统集成openGauss 6.0.0 LTS
Co-authored-by: JaleShaw<enjoy-success@qq.com>
2025-10-22 14:42:59 +08:00
opengauss_bot 7d3be6e208
!84 merge master into master
开源之夏2025-openGauss向量数据库对接数据来源最佳实践

Created-by: wyswyz
Commit-by: Yongshun Wang
Merged-by: opengauss_bot
Description: 【标题】开源之夏2025-openGauss向量数据库对接数据来源最佳实践

【实现内容】: 
		1. 基于openGauss 容器部署指导文档、Apify部署指导文档,基于docker完成服务部署。编写应用代码,完成Apify对接openGauss功能适配。
        2. 输出实践案例,和详细使用文档。 


See merge request: opengauss/examples!84
2025-10-22 14:38:39 +08:00
opengauss_bot 324f667371
!86 merge whyhow into master
openGauss向量数据库对接知识工程最佳实践

Created-by: paradox
Commit-by: paradox
Merged-by: opengauss_bot
Description: 【标题】openGauss向量数据库对接知识工程最佳实践

【实现内容】: WhyHow 与openGauss向量数据库适配,包括简单知识文档导入、对接大模型完成RAG端到端验证。


See merge request: opengauss/examples!86
2025-10-22 14:37:46 +08:00
opengauss_bot e867f6de71
!88 merge ospp into master
2025开源之夏 - openGauss 向量数据库 AI 评估框架对接最佳实践输出

Created-by: kfjdgnf123
Commit-by: kfjdgnf123;zlh123123
Merged-by: opengauss_bot
Description: 2025开源之夏 - openGauss 向量数据库 AI 评估框架对接最佳实践输出

See merge request: opengauss/examples!88
2025-10-22 14:36:58 +08:00
opengauss_bot 7c06a09938
!89 merge feature/django-opengauss into master
2025开源之夏 - django支持opengauss

Created-by: wangxianyi
Commit-by: wangxianyi
Merged-by: opengauss_bot
Description: ### 2025开源之夏 - django支持opengauss

【实现内容】:
  - 提供基于 Django 的 openGauss 数据库后端实现,采用 `src/django_opengauss/` 包结构
  - 附带功能测试、覆盖率测试与多数据库性能对比脚本,可直接运行于 `tests/` 目录
  - README 说明安装配置、测试执行与打包命令,便于在 openGauss 场景中快速集成 Django


See merge request: opengauss/examples!89
2025-10-22 14:36:10 +08:00
opengauss_bot cad81718e4
!91 merge master into master
开源之夏2025-openGauss向量数据库对接Embedding模型最佳实践

Created-by: Seasonmay
Commit-by: SeasonMay
Merged-by: opengauss_bot
Description: 【实现内容】:基于openGauss 容器部署指导文档、分别完成 HuggingFace、Cohere 、BentoML部署指导文档,基于docker完成服务部署。编写应用代码,HuggingFace、Cohere 、BentoML嵌入模型对接openGauss。

See merge request: opengauss/examples!91
2025-10-22 14:35:18 +08:00
opengauss_bot 60fbadcdae
!92 merge etl_to_openGauss into master
2025开源之夏-openGauss 向量数据库 AI 数据 ETL 框架最佳实践输出

Created-by: fancy3058
Commit-by: 曾繁曦
Merged-by: opengauss_bot
Description: 【标题】
	openGauss 向量数据库 AI 数据 ETL 框架最佳实践输出
【实现内容】:
1. 基于 openGauss 部署指导文档,完成openGauss Docker 部署
2. 参考 Milvus weaviate 等文档,完成 Apache Kafka、Firecrawl、VectorETL 对接 openGauss 最佳实践教程输出
3. 将文档提交至 openGauss 社区

See merge request: opengauss/examples!92
2025-10-22 14:33:11 +08:00
opengauss_bot 40df37412e
!81 merge master into master
开源之夏2025-openGauss 对接 AI 编排工具 Kotaemon 实现智能小助手

Created-by: xiexudong
Commit-by: XudongXie
Merged-by: opengauss_bot
Description: 【标题】openGauss 对接 AI 编排工具 Kotaemon 实现智能小助手
【实现内容】:基于 Kotaemon 构建 RAG 平台与 openGauss 向量数据库的标准化对接通道,实现:
  1.向量引擎协议兼容:开发 openGauss 向量空间管理中间件;
  2.配置嵌入模型、LLMs 等完成可视化 RAG 信息检索;
  3.输出 openGauss 适配脚本及 RAG 流程效果文档。
【实现方案】:Kotaemon 是一个基于LlamaIndex的RAG框架,支持多种向量数据库。项目根目录下的flowsettings.py是全局配置文件,决定了使用哪个具体的向量数据库。在 libs/kotaemon/kotaemon/storages/vectorstores/opengauss.py 中新增了完整的OpenGauss向量存储实现:继承自 BaseVectorStore 抽象基类;实现了所有必需的抽象方法:遵循 kotaemon 框架的向量存储接口规范。
【开发自验报告】:请查看libs\kotaemon\kotaemon\storages\vectorstores\README.md,每个实现的功能均写了单元测试,所有单元测试均通过,最后的系统整体测试也通过。


See merge request: opengauss/examples!81
2025-10-22 14:20:10 +08:00
opengauss_bot 516fc2cadc
!78 merge feature/add-llama-stack-example-ospp into master
添加openGauss对LlamaStack项目向量数据库支持案例(2025开源之夏)

Created-by: cbxgll
Commit-by: qifengleqifengle
Merged-by: opengauss_bot
Description: 本次提交为 LlamaStack 项目新增了 openGauss 向量数据库的支持(2025开源之夏项目)。

主要内容包括:

新增 Provider: 实现了 OpenGaussVectorIOAdapter,用于连接和操作 openGauss 数据库。
完整示例: 提供了一个完整的示例项目 (openGaussLlamaStack_ospp),包含客户端、配置、启动脚本和单元测试,展示了如何使用该功能。
文档: 添加了集成说明文档。
仓库维护: 新增了 .gitignore 文件,以忽略 IDE 配置文件和日志等无关文件,保持仓库整洁。

See merge request: opengauss/examples!78
2025-10-22 14:19:11 +08:00
opengauss_bot adcffa89b2
!82 merge master into master
开源之夏2025-openGauss 向量数据库集成MemGPT

Created-by: william4s
Commit-by: william4s
Merged-by: opengauss_bot
Description: 【标题】 openGauss 向量数据库集成MemGPT

【实现内容】 集成OpenGauss向量数据库到Letta(MemGPT),构建生产级RAG系统。实现自动数据库初始化、Memory Block智能分块存储、BGE-M3中文优化向量化、完整审计系统和Web可视化仪表板。

【实现方案】

开发OpenGauss存储模块,支持1024维向量增删改查
实现Alembic迁移兼容性,解决版本解析、UUID生成等问题
构建金融领域RAG应用,提供PDF文档智能问答
内置SQLite审计日志,支持风险检测和可视化报告
Docker容器化部署,环境变量灵活配置

【开发自验报告】: OPENGAUSS_INTEGRATION_SUCCESS.md   repot.pdf

See merge request: opengauss/examples!82
2025-10-22 14:17:49 +08:00
william4s 9627ee8d5b
开源之夏2025-openGauss 向量数据库集成MemGPT
Co-authored-by: william4s<william4s@126.com>
2025-10-22 14:17:49 +08:00
opengauss_bot 5f312099b7
!87 merge OSPP2025-Milvus2openGauss into master
开源之夏2025-openGauss向量数据库迁移Milvus的迁移测试

Created-by: TisaneYang
Commit-by: TisaneYang;Tianyu Yang
Merged-by: opengauss_bot
Description: 【标题】(请简要描述下实现的内容)

【实现内容】:

【根因分析】:

【实现方案】:

【关联需求或issue】:

【开发自验报告】:
1. 请附上自验结果(内容或者截图)
2. 是否可以添加fastcheck测试用例,如是,请补充fastcheck用例
3. 是否涉及资料修改,如是,在docs仓库补充资料
4. 是否考虑升级场景(系统表修改、日志持久化以及修改执行态数据格式)
5. 是否考虑在线扩容等扩展场景
6. 是否考虑异常场景/并发场景/前向兼容/性能场景
7. 是否对其他模块产生影响
8. 是否需要回合补丁版本(5.0、6.0),涉及(**结果错误|coredump|性能劣化|内存泄露|集群异常**)相关修复请排查回合补丁分支

【其他说明】:

See merge request: opengauss/examples!87
2025-10-22 14:17:32 +08:00
SeasonMay 421788ad8d 开源之夏2025-openGauss向量数据库对接Embedding模型最佳实践-v1 2025-09-30 17:20:47 +08:00
曾繁曦 414ab0cd6d 2025开源之夏-openGauss 向量数据库 AI 数据 ETL 框架最佳实践输出 2025-09-30 15:52:33 +08:00
wangxianyi e1d22f1cf3 chore: 删除 django-opengauss 相关的 egg-info 文件 和 pycache 文件 2025-09-30 15:41:08 +08:00
SeasonMay 13a7967eef 开源之夏2025-openGauss向量数据库对接Embedding模型最佳实践 2025-09-30 15:04:39 +08:00
gcw_qP1tCuFs 561873e824 feat: OSPP2025 - privateGPT 添加finRAG example 2025-09-29 11:35:43 +01:00
gcw_qP1tCuFs 0e57c05d39 feat: OSPP2025 - 添加 openGauss 向量数据库集成 PrivateGPT 项目 2025-09-29 02:35:29 +01:00
paradox e584b24d9a
update: 更新文件 .env.sample
Signed-off-by: paradox <jiatongli@stu.xidian.edu.cn>
2025-09-28 20:20:22 +08:00
TisaneYang aa55b5f6e9 modified: "Milvus2openGauss/doc/Milvus2OpenGauss \350\277\201\347\247\273\345\267\245\345\205\267\346\265\213\350\257\225\346\212\245\345\221\212.md" 2025-09-28 14:58:41 +08:00
TisaneYang 46fe76be9c new file: Milvus2openGauss/images/image-20250925175315991.png
new file:   Milvus2openGauss/images/image-20250925180445053.png
	new file:   Milvus2openGauss/images/image-20250925182514209.png
	new file:   Milvus2openGauss/images/image-20250925182915643.png
	new file:   Milvus2openGauss/images/image-20250925182934797.png
	new file:   Milvus2openGauss/images/image-20250925183016228.png
	new file:   Milvus2openGauss/images/image-20250925183416690.png
	new file:   Milvus2openGauss/images/image-20250925183448026.png
2025-09-28 14:20:32 +08:00
wangxianyi daebdcb819 feat: add django-opengauss driver example with src layout 2025-09-27 11:35:38 +08:00
kfjdgnf123 66472e843a
delete: 删除文件 openGauss-AIEval/结项报告.md
Signed-off-by: kfjdgnf123 <zhanglinghao@sjtu.edu.cn>
2025-09-26 22:08:19 +08:00
kfjdgnf123 85897c9f1a
delete: 删除文件 openGauss-AIEval/1
Signed-off-by: kfjdgnf123 <zhanglinghao@sjtu.edu.cn>
2025-09-26 22:08:02 +08:00
zlh123123 c266b0aad5 ospp 2025-09-26 22:02:28 +08:00
kfjdgnf123 41061c1e52
new: 新建文件 1
Signed-off-by: kfjdgnf123 <zhanglinghao@sjtu.edu.cn>
2025-09-26 21:49:00 +08:00
kfjdgnf123 a44e0e7fa1
delete: 删除文件夹 openGauss-AIEval
Signed-off-by: kfjdgnf123 <zhanglinghao@sjtu.edu.cn>
2025-09-26 21:42:41 +08:00
kfjdgnf123 bebcf435e5
new: 新建文件 1
Signed-off-by: kfjdgnf123 <zhanglinghao@sjtu.edu.cn>
2025-09-26 21:38:44 +08:00
Tianyu Yang d27e830f10 Milvus2OpenGauss 2025-09-26 12:06:33 +08:00
paradox 40df17e990 OSPP project 2025-09-23 19:12:29 +08:00
paradox e2b03d9865 OSPP project 2025-09-23 18:54:28 +08:00
Yongshun Wang b1e92f55f7 apify对接opengauss向量数据库 2025-09-23 16:18:29 +08:00
XudongXie 3d5fb74b3e fix some issues 2025-09-20 00:43:35 +08:00
huliyan 4b0f4abfcc 文档图片补充 2025-09-19 16:51:30 +08:00
huliyan 220f34fc9e airbyte对接opengauss向量数据库 2025-09-19 16:08:06 +08:00
qifengleqifengle 26b24f76a5 添加openGauss对LlamaStack项目向量数据库支持案例(2025开源之夏) 2025-09-16 11:09:02 +08:00
XudongXie ffebfc5ac6 add opengauss vector store, test suite and readme file 2025-09-05 10:44:43 +08:00
XudongXie 0500771b74 init 2025-08-13 16:49:11 +08:00
opengauss_bot 3790d3f3c8
!76 添加 2024 标杆案例申报表
Merge pull request !76 from 看我72遍/dev
2024-11-11 06:47:05 +00:00
mpb159753 854ef978de 更新应用申报表 2024-11-11 14:42:14 +08:00
opengauss_bot b0c184be57
!75 添加 2024 标杆案例申报表
Merge pull request !75 from 看我72遍/dev
2024-11-07 06:37:52 +00:00
mpb 2c4d19677f 添加 2024 标杆案例申报表 2024-11-07 14:25:38 +08:00
opengauss_bot 99585fd85e
add oGUG registration form
Signed-off-by: opengauss_bot <meixiangru@public.opengauss.org>
2024-11-05 02:18:22 +00:00
opengauss_bot 112d6e1ac2
!72 开源之夏2024-openGauss对hikari框架的兼容性测试
Merge pull request !72 from 郑雅丹/master
2024-10-31 06:27:06 +00:00
adamzheng 3ba8992164 开源之夏2024-openGauss对hikari框架的兼容性测试
开源之夏2024-openGauss对hikari框架的兼容性测试

开源之夏2024-openGauss对hikari框架的兼容性测试

开源之夏2024-openGauss对hikari框架的兼容性测试

补充结项报告

修改结项报告

修改结项报告

Signed-off-by: 郑雅丹 <1981820466@qq.com>

修复pom文件错误
2024-10-31 12:17:07 +08:00
opengauss_bot b9dafecd97
!73 开源之夏2024-openGauss对C3P0框架的兼容性测试
Merge pull request !73 from hydrogenair/master
2024-10-25 01:03:35 +00:00
lvxun a48e7dc340 删除文件 2024-10-24 20:35:13 +08:00
lvxun 31689f60d5 2024开源之夏-基于C3P0框架的openGauss测试修改 2024-10-18 08:24:11 +08:00
lvxun 718c2b7359 开源之夏2024-基于C3P0框架的openGauss测试 2024-10-17 20:39:37 +08:00
opengauss_bot fc780469f5
!70 开源之夏2024-openGauss Hibernate 方言包
Merge pull request !70 from SolisAmicus/master
2024-10-12 02:41:36 +00:00
opengauss_bot d66eb9e4f7
!69 测试opengauss常用数据类型对hiberate框架的兼容性
Merge pull request !69 from wzhao510/master
2024-10-11 08:44:08 +00:00
opengauss_bot f9c17d11fe
!71 开源之夏2024-SQL语法转译工具
Merge pull request !71 from XudongXie/master
2024-10-10 12:19:05 +00:00
lvxun 11c8aa51eb 开源之夏-基于C3P0框架的openGauss测试报告 2024-09-28 01:29:55 +08:00
lvxun 89d80a4a97 开源之夏-基于C3P0框架的openGauss、MySQL数据库测试代码 2024-09-28 01:23:18 +08:00
lvxun 6e0cd184f4 开源之夏-基于C3P0框架的openGauss数据库测试设计报告 2024-09-28 01:14:58 +08:00
XudongXie 54bef7c864 Add recursive parsing function 2024-09-26 22:30:07 +08:00
XudongXie 98a3b240a1 refactor the package name 2024-09-26 19:55:20 +08:00
XudongXie f833e13c94 refactor 2024-09-24 09:34:29 +08:00
XudongXie 07f747903c refactor 2024-09-24 09:33:52 +08:00
SolisAmicus e06f2610cc chore: Remove .idea directory from tracking 2024-09-23 01:00:49 +08:00
SolisAmicus 1a16fc6713 docs: Add design document, test document, and Hibernate OpenGauss dialect development manual
- Added 设计文档.md
- Added 测试文档.md
- Added Hibernate 关于 OpenGauss 方言包_开发手册.pdf
2024-09-23 00:50:06 +08:00
SolisAmicus d22dc3892d test: Add unit and integration tests for miscellaneous features in OpenGaussDialect 2024-09-23 00:41:07 +08:00
SolisAmicus 8f95aa5bec test: Add unit and integration tests for callableStatement and current timestamp support in OpenGaussDialect 2024-09-23 00:41:07 +08:00
SolisAmicus 0981ea829b test: Add unit and integration tests for locking support in OpenGaussDialect 2024-09-23 00:40:55 +08:00
SolisAmicus 185a372217 feat: Implement OpenGaussDialect with locking support 2024-09-23 00:39:05 +08:00
SolisAmicus da234f1f20 chore: Remove target directory from tracking 2024-09-23 00:39:04 +08:00
SolisAmicus 9ef10f1bd6 test: Add unit and integration tests for DDL-related features in OpenGaussDialect 2024-09-23 00:39:04 +08:00
SolisAmicus e7f4d77b7c test: Add unit and integration tests for sequence and identifier generation in OpenGaussDialect 2024-09-23 00:39:04 +08:00
SolisAmicus 092d34510f test: Add unit and integration tests for limit/offset support in OpenGaussDialect
- Implemented unit tests for the `OpenGaussDialect` limit/offset support using `LimitHandler`.
- Tested SQL generation for queries with and without offset.
  - `testProcessSqlWithOffset`: Validates SQL query with limit and offset values.
  - `testProcessSqlWithoutOffset`: Validates SQL query with only limit value.
  - `testSupportsLimit`: Ensures `supportsLimit()` returns true.
- Structured tests in `OpenGaussDialectLimitHandlerTest.java`.
- Verified the proper handling of limit and offset logic in OpenGaussDialect.
2024-09-23 00:38:12 +08:00
XudongXie 66bf74d25b revise trigger 2024-09-22 12:19:03 +08:00
SolisAmicus 18ba24a730 test: Add unit tests for OpenGaussDialect
- Added unit tests for column type mappings.
- Verified correct SQL function registration for OpenGauss functions.
2024-09-21 12:09:30 +08:00
SolisAmicus e084952ff6 feat: Implement OpenGaussDialect for Hibernate
This commit introduces the OpenGaussDialect class to support the OpenGauss database in Hibernate. Key features include:

- Column type mappings for OpenGauss-specific data types (bit, bool, int, etc.).
- Registration of OpenGauss-specific SQL functions such as abs, cbrt, concat, etc.
- Support for sequence generation, identity columns, and limit/offset pagination.
- Support for callable statements, current timestamp, and schema DDL commands.

Includes database type mapping, identity, sequence, and limit handling strategies. This implementation follows the PostgreSQL dialect structure with adjustments for OpenGauss.
2024-09-21 11:48:21 +08:00
XudongXie e10c31a36c revise trigger 2024-09-20 23:20:05 +08:00
XudongXie 7cc485ab95 revise trigger 2024-09-20 23:07:21 +08:00
wzhao510 1235a461ca
删除文件 .gitmodules 2024-09-20 10:04:56 +00:00
wzhao510 b206ac054b
删除文件 VerifyJDBCDataType/target/classes 2024-09-20 10:01:24 +00:00
wzhao510 48a7e798b5
删除文件 hibernateTest 2024-09-20 09:52:06 +00:00
wzhao510 0bfd2be29e hibernateTest 2024-09-20 17:49:05 +08:00
wzhao510 dc74b35b5c 删除子模块 hibernateTest/src/main/java/org/example/.keep 2024-09-20 09:22:53 +00:00
wzhao510 bd2c85d395 add hibernateTest/src/main/java/org/example/.keep submodule.
Signed-off-by: wzhao510 <510273941@qq.com>
2024-09-20 09:19:56 +00:00
wzhao510 0c1a107544
删除文件 hibernateTest/.gitignore 2024-09-20 09:12:04 +00:00
wzhao510 52e3b69b07
a
Signed-off-by: wzhao510 <510273941@qq.com>
2024-09-20 09:02:18 +00:00
wzhao510 ba986c872d
新建 test 2024-09-20 09:01:58 +00:00
wzhao510 c21af54463
新建 main 2024-09-20 09:01:47 +00:00
wzhao510 acb5e0b90e
a
Signed-off-by: wzhao510 <510273941@qq.com>
2024-09-20 09:01:11 +00:00
wzhao510 fa9afd073c
新建 blobs 2024-09-20 09:00:18 +00:00
wzhao510 ab611b2913
a
Signed-off-by: wzhao510 <510273941@qq.com>
2024-09-20 08:59:55 +00:00
wzhao510 af5224a3be
新建 chatSessions 2024-09-20 08:59:25 +00:00
wzhao510 93df8fb8e9
新建 copilot 2024-09-20 08:58:54 +00:00
wzhao510 f0aa7837d3
a
Signed-off-by: wzhao510 <510273941@qq.com>
2024-09-20 08:58:22 +00:00
wzhao510 08f524a37d
新建 .idea 2024-09-20 08:57:55 +00:00
wzhao510 f58ab0f29c
新建 src 2024-09-20 08:57:35 +00:00
wzhao510 f893a4298a
a
Signed-off-by: wzhao510 <510273941@qq.com>
2024-09-20 08:57:18 +00:00
wzhao510 9978eb5014
新建 hibernateTest 2024-09-20 08:56:13 +00:00
wzhao510 1f9846f8b4
删除文件 hibernateTest 2024-09-20 07:50:32 +00:00
wzhao510 a2c4fe6c0b 删除子模块 hibernateTest/.idea/copilot/chatSessions/blobs 2024-09-20 07:49:17 +00:00
wzhao510 bcfa8c3b37 add hibernateTest/.idea/copilot/chatSessions/blobs submodule.
Signed-off-by: wzhao510 <510273941@qq.com>
2024-09-20 07:48:39 +00:00
wzhao510 2818991f6f
删除文件 hibernateTest/hiberateTest(1).zip 2024-09-20 07:46:01 +00:00
wzhao510 c3c9f3d2cd
a
Signed-off-by: wzhao510 <510273941@qq.com>
2024-09-20 07:43:10 +00:00
wzhao510 b601b66577
a
Signed-off-by: wzhao510 <510273941@qq.com>
2024-09-20 07:42:20 +00:00
wzhao510 9d8976f947
新建 .idea 2024-09-20 07:41:44 +00:00
wzhao510 2b2d694066
新建 src 2024-09-20 07:35:46 +00:00
wzhao510 53ec04cc95
删除文件 hibernateTest/hiberateTest(1).zip 2024-09-20 07:34:50 +00:00
wzhao510 8d1a41b424
a
Signed-off-by: wzhao510 <510273941@qq.com>
2024-09-20 07:34:39 +00:00
wzhao510 fdc592e0d7
aa
Signed-off-by: wzhao510 <510273941@qq.com>
2024-09-20 07:31:17 +00:00
wzhao510 40e5a6ee2c
新建 hibernateTest 2024-09-20 07:29:57 +00:00
XudongXie b04c67710e revise 2024-09-20 15:18:52 +08:00
XudongXie 4a25a62b72 fix some bugs 2024-09-20 15:08:51 +08:00
XudongXie bfd142e34f fix some bugs 2024-09-20 14:58:23 +08:00
XudongXie e2ba57e485 add unit test 2024-09-20 14:27:19 +08:00
XudongXie acaf7f1214 add unit test 2024-09-20 12:57:18 +08:00
XudongXie 9cd1066f07 add unit test 2024-09-20 12:30:52 +08:00
XudongXie db2701ae44 add unit test 2024-09-20 09:57:33 +08:00
XudongXie 97091da1d1 add unit test 2024-09-20 09:46:27 +08:00
XudongXie 8deb614144 revise 2024-09-19 15:38:32 +08:00
XieXudong ac7e35195c revise 2024-09-19 15:34:02 +08:00
XudongXie 6dfaf16ff2 revise 2024-09-19 14:17:06 +08:00
XudongXie 15ec96c822 add pl/sql 2024-09-18 12:28:21 +08:00
XudongXie ac03ad7130 revise 2024-09-18 11:50:49 +08:00
XudongXie 146795c22d add PL ast classes 2024-09-18 11:23:38 +08:00
XudongXie 2d58893d57 add PL document 2024-09-18 11:14:10 +08:00
XudongXie 810e2d7608 add trigger 2024-09-18 11:01:52 +08:00
XudongXie 8a8b13c793 add trigger 2024-09-18 00:53:17 +08:00
XudongXie fe805b9721 add trigger ast classes 2024-09-18 00:21:54 +08:00
XudongXie 106f9e2ada add trigger document 2024-09-17 23:58:58 +08:00
XudongXie 6afc2b1085 add function 2024-09-17 23:36:18 +08:00
XudongXie d61e9d8460 add ast classes of function 2024-09-17 22:29:50 +08:00
XudongXie 9d91b73238 add document of function 2024-09-17 16:21:38 +08:00
XudongXie 1b04f51349 add document of rollback, commot, execute 2024-09-17 14:38:43 +08:00
XudongXie 9a54d05f12 add procedure 2024-09-17 14:28:30 +08:00
XudongXie f0a909f288 add ast classes of procedure 2024-09-16 22:23:59 +08:00
XudongXie 81458e5ef7 add document of Procedure 2024-09-16 21:06:37 +08:00
XudongXie a4c7aa2acf add exception 2024-09-16 16:19:26 +08:00
XudongXie 565851cb6c revise 2024-09-16 15:38:40 +08:00
XudongXie 82c337f52d add exception classes 2024-09-16 15:23:01 +08:00
XudongXie 2566ca965b revise 2024-09-16 15:17:37 +08:00
XudongXie 9a06b2e574 add Exception document 2024-09-15 23:11:07 +08:00
XudongXie f871f595d6 add Exception document 2024-09-15 23:10:55 +08:00
XudongXie f82a2acf79 add Loop 2024-09-14 23:12:23 +08:00
XudongXie d15d4cf123 add Loop ast classes 2024-09-14 20:16:17 +08:00
XudongXie 477e484385 add Loop keywords 2024-09-14 20:00:17 +08:00
XudongXie 405dcf6569 add Loop document 2024-09-14 19:56:46 +08:00
XudongXie 86b3eee863 add IF ELSIF 2024-09-14 15:26:01 +08:00
XudongXie 3f81250c9c add IF ELSIF 2024-09-14 11:36:08 +08:00
XudongXie 5d49c5bb3d add IF ELSIF 2024-09-13 23:35:47 +08:00
XudongXie dad5afc2fd add if elseif else endif ast classes 2024-09-13 22:49:32 +08:00
XudongXie 86e2716d53 add if elseif else endif document 2024-09-13 22:14:27 +08:00
XudongXie beb88c7510 revise 2024-09-13 21:55:02 +08:00
XudongXie aaa152a922 add view generate method 2024-09-13 21:48:56 +08:00
XudongXie c4cec7795c add view 2024-09-13 21:30:41 +08:00
XudongXie c10d92fae6 add ast class of view 2024-09-13 16:20:55 +08:00
XudongXie 8724f488c2 add keywords of view 2024-09-13 16:14:05 +08:00
XudongXie 3e95820c1f add view document 2024-09-13 16:09:24 +08:00
XudongXie 449b54cf6c fix some bugs 2024-09-13 16:04:56 +08:00
XudongXie 744b277675 add alter 2024-09-13 15:35:12 +08:00
XudongXie d1a2e1647a add alter 2024-09-13 14:51:19 +08:00
XudongXie 3254457fb0 revise 2024-09-13 14:37:09 +08:00
XudongXie aa5c0f2343 revise 2024-09-13 11:27:32 +08:00
XudongXie baf06a4d43 revise 2024-09-13 11:21:36 +08:00
XudongXie 3fc49a2963 revise create table document 2024-09-13 09:51:25 +08:00
XudongXie c379bd2e85 revise create table document 2024-09-13 09:50:51 +08:00
XudongXie 189d3db242 add alter table parser 2024-09-12 23:21:17 +08:00
XudongXie 57cfd48b73 add alter table keywords 2024-09-12 23:17:22 +08:00
XudongXie 4e3eae83af add alter table ast class 2024-09-12 23:12:56 +08:00
XudongXie a4127524c9 add alter table grammar document 2024-09-12 22:59:32 +08:00
XudongXie 824b2927d0 add Delete 2024-09-12 22:56:47 +08:00
XudongXie 84b694de4c add Delete ast class 2024-09-12 22:40:29 +08:00
XudongXie 382ac95ee0 add update convert rules 2024-09-12 22:32:12 +08:00
XudongXie efd9320ff6 add update ast class 2024-09-12 21:50:13 +08:00
XudongXie 66b46f52a7 add select convert rules 2024-09-12 21:34:51 +08:00
XudongXie 3db285b4cf refractor 2024-09-12 20:25:05 +08:00
XudongXie 3c0cbcdad8 refractor 2024-09-12 20:21:52 +08:00
XudongXie d4dca20bb9 add join convert rule 2024-09-12 16:18:41 +08:00
XudongXie 6fcb0541d0 add join 2024-09-12 15:55:26 +08:00
XudongXie 34023bc8e8 add join class 2024-09-12 11:35:56 +08:00
XudongXie bd1cec8fbd add join pattern 2024-09-11 23:34:42 +08:00
XudongXie 5623109565 revise 2024-09-11 23:00:05 +08:00
XudongXie f1e06e3650 revise 2024-09-11 22:37:08 +08:00
XudongXie 0998a3aa29 revise 2024-09-11 20:59:05 +08:00
XudongXie e6cc96a21f add parseSelect 2024-09-11 15:55:54 +08:00
XudongXie 995516a90d add parseSelect 2024-09-10 23:26:09 +08:00
XudongXie f009be079a add some comments 2024-09-10 23:18:30 +08:00
XudongXie 1155fd3b42 add pattern of functions in select 2024-09-10 23:11:01 +08:00
XudongXie b4c48ae085 revise 2024-09-10 22:29:41 +08:00
XudongXie 54a4bf3db0 revise 2024-09-10 16:48:55 +08:00
XudongXie 15ef28ad4f add case when 2024-09-10 16:20:14 +08:00
XudongXie e9475c0507 add select 2024-09-10 16:09:24 +08:00
XudongXie ddb4978a32 add select 2024-09-10 11:02:30 +08:00
XudongXie 0eea1eef1b add todolist 2024-09-09 23:44:45 +08:00
XudongXie 18b4711a83 add todolist 2024-09-09 22:07:05 +08:00
XudongXie 12ff94828e add insert sql 2024-09-09 20:31:54 +08:00
opengauss_bot c57b0e8ab7
openGauss案例集
Signed-off-by: opengauss_bot <meixiangru@public.opengauss.org>
2024-09-05 06:17:47 +00:00
XudongXie 9b7a3bb85f add insert sql 2024-08-31 23:11:10 +08:00
XudongXie 62dfa6da61 finish the framework of sqltranslate (createTab sql is now being supported!) 2024-08-24 23:00:19 +08:00
XudongXie d3779f8b3c coding 2024-08-24 22:34:53 +08:00
XudongXie e0a1c67c1b coding 2024-08-24 21:47:06 +08:00
XudongXie c704d436aa coding 2024-08-24 21:38:20 +08:00
XudongXie 0245c1d0ed add function 2024-08-24 16:18:05 +08:00
XudongXie 1d40557152 add function 2024-08-24 12:01:19 +08:00
XudongXie fe03e497a9 add function 2024-08-23 23:07:06 +08:00
XudongXie 7eb3119503 add function 2024-08-23 22:36:37 +08:00
XudongXie bae72996a4 add function 2024-08-23 21:19:37 +08:00
XudongXie ff0b115ecc add function 2024-08-22 22:51:40 +08:00
XudongXie 0bcc3c21b7 add parser function 2024-08-20 23:00:46 +08:00
XudongXie 9791f9a1dc revise 2024-08-20 22:44:06 +08:00
XudongXie 9187a6e135 revise 2024-08-20 22:43:47 +08:00
XudongXie f49954ec42 revise 2024-08-20 22:43:27 +08:00
XudongXie 18ded7e870 revise 2024-08-20 22:38:16 +08:00
XudongXie 33978043c6 add functions 2024-08-20 17:12:04 +08:00
XudongXie 7beef2b80f add AST node 2024-08-18 22:42:41 +08:00
XudongXie d4aa1c1ffd AST node 2024-08-17 13:15:41 +08:00
XudongXie db166f7b5c lexer 2024-08-16 22:35:28 +08:00
XudongXie d123cbfa82 init 2024-08-08 16:48:08 +08:00
opengauss_bot b8ffe4ca84
openGauss联合方案模板
Signed-off-by: opengauss_bot <meixiangru@public.opengauss.org>
2024-06-05 07:04:34 +00:00
opengauss_bot 6d7cfe49ac
!65 替换fastjson jar包为1.2.83
Merge pull request !65 from 蒋宏博/master
2024-03-12 07:00:15 +00:00
蒋宏博 982459f60f 替换fastjson版本为1.2.83 2024-03-09 11:29:03 +08:00
opengauss_bot e29cbc6f40
!64 2023年鲲鹏应用创新大赛全国总决赛项目代码提交
Merge pull request !64 from lujunfeng/master
2023-12-27 11:28:43 +00:00
richardlujunfeng 560e1a8c83 提交文件夹下的所有文件 2023-12-27 19:00:18 +08:00
opengauss_bot bfa04e3207
Application practice case application form
Signed-off-by: opengauss_bot <meixiangru@public.opengauss.org>
2023-11-21 06:14:52 +00:00
6599 changed files with 888944 additions and 0 deletions

1
.gitattributes vendored Normal file
View File

@ -0,0 +1 @@
*.jar filter=lfs diff=lfs merge=lfs -text

28
.gitignore vendored Normal file
View File

@ -0,0 +1,28 @@
# Build artifacts
target/
*.jar
*.war
*.ear
# IDE configuration files
.project
.classpath
.settings/
.idea/
*.iml
*.ipr
*.iws
# Log files
*.log
# Maven files
pom.xml.tag
pom.xml.releaseBackup
pom.xml.versionsBackup
pom.xml.next
release.properties
dependency-reduced-pom.xml
buildNumber.properties
.mvn/timing.properties

3
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"cmake.sourceDirectory": "D:/examples/openGaussSharedTableParallelHashJoin"
}

View File

@ -0,0 +1,64 @@
import ollama
import psycopg2
table_name = "item_collection_website_content_crawler"
def embedding(text):
vector = ollama.embeddings(model="nomic-embed-text", prompt=text)
return vector["embedding"]
conn = psycopg2.connect(
database="postgres",
user="hly",
password="Hly@1234",
host="192.168.219.134",
port="5432"
)
cur = conn.cursor()
# 设置搜索路径
cur.execute("SET search_path TO public;")
# 创建索引
cur.execute("CREATE INDEX IF NOT EXISTS idx_{}_embedding_hnsw ON {} USING hnsw (embedding vector_l2_ops);".format(table_name, table_name))
conn.commit()
question = "介绍一下openGauss DataVec"
emb_data = embedding(question)
dimensions = len(emb_data)
cur = conn.cursor()
cur.execute("select document_content from {} order by embedding <-> '{}' limit 3;".format(table_name, emb_data))
conn.commit()
rows = cur.fetchall()
print(rows)
cur.close()
conn.close()
# 指定容器所在主机或容器名、端口(如果需要的话), 或通过设置环境变量
# client = ollama.Client(host="http://<宿主机 IP>:11434")
context = rows
# 测试
# context = ""
SYSTEM_PROMPT = "你作为一个对话 AI 助手,结合上下文信息简练高效的回答用户提出的问题"
USER_PROMPT = f"请结合{context}信息来回答{question}的问题,不需要额外的无用回答"
response = ollama.chat(
model="deepseek-r1:1.5b",
stream=True,
messages=[
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": USER_PROMPT}
]
)
print("\n\n")
# 实时打印每一段内容
for chunk in response:
print(chunk["message"]["content"], end="", flush=True)
# print(response["message"]["content"])

View File

@ -0,0 +1,395 @@
# This file is automatically @generated by Poetry 2.2.0 and should not be changed by hand.
[[package]]
name = "annotated-types"
version = "0.7.0"
description = "Reusable constraint types to use with typing.Annotated"
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"},
{file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"},
]
[[package]]
name = "anyio"
version = "4.10.0"
description = "High-level concurrency and networking framework on top of asyncio or Trio"
optional = false
python-versions = ">=3.9"
groups = ["main"]
files = [
{file = "anyio-4.10.0-py3-none-any.whl", hash = "sha256:60e474ac86736bbfd6f210f7a61218939c318f43f9972497381f1c5e930ed3d1"},
{file = "anyio-4.10.0.tar.gz", hash = "sha256:3f3fae35c96039744587aa5b8371e7e8e603c0702999535961dd336026973ba6"},
]
[package.dependencies]
idna = ">=2.8"
sniffio = ">=1.1"
typing_extensions = {version = ">=4.5", markers = "python_version < \"3.13\""}
[package.extras]
trio = ["trio (>=0.26.1)"]
[[package]]
name = "certifi"
version = "2025.8.3"
description = "Python package for providing Mozilla's CA Bundle."
optional = false
python-versions = ">=3.7"
groups = ["main"]
files = [
{file = "certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5"},
{file = "certifi-2025.8.3.tar.gz", hash = "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407"},
]
[[package]]
name = "h11"
version = "0.16.0"
description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1"
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86"},
{file = "h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1"},
]
[[package]]
name = "httpcore"
version = "1.0.9"
description = "A minimal low-level HTTP client."
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55"},
{file = "httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8"},
]
[package.dependencies]
certifi = "*"
h11 = ">=0.16"
[package.extras]
asyncio = ["anyio (>=4.0,<5.0)"]
http2 = ["h2 (>=3,<5)"]
socks = ["socksio (==1.*)"]
trio = ["trio (>=0.22.0,<1.0)"]
[[package]]
name = "httpx"
version = "0.28.1"
description = "The next generation HTTP client."
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad"},
{file = "httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc"},
]
[package.dependencies]
anyio = "*"
certifi = "*"
httpcore = "==1.*"
idna = "*"
[package.extras]
brotli = ["brotli ; platform_python_implementation == \"CPython\"", "brotlicffi ; platform_python_implementation != \"CPython\""]
cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"]
http2 = ["h2 (>=3,<5)"]
socks = ["socksio (==1.*)"]
zstd = ["zstandard (>=0.18.0)"]
[[package]]
name = "idna"
version = "3.10"
description = "Internationalized Domain Names in Applications (IDNA)"
optional = false
python-versions = ">=3.6"
groups = ["main"]
files = [
{file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"},
{file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"},
]
[package.extras]
all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"]
[[package]]
name = "ollama"
version = "0.5.4"
description = "The official Python client for Ollama."
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "ollama-0.5.4-py3-none-any.whl", hash = "sha256:6374c9bb4f2a371b3583c09786112ba85b006516745689c172a7e28af4d4d1a2"},
{file = "ollama-0.5.4.tar.gz", hash = "sha256:75857505a5d42e5e58114a1b78cc8c24596d8866863359d8a2329946a9b6d6f3"},
]
[package.dependencies]
httpx = ">=0.27"
pydantic = ">=2.9"
[[package]]
name = "psycopg2-binary"
version = "2.9.9"
description = "psycopg2 - Python-PostgreSQL Database Adapter"
optional = false
python-versions = ">=3.7"
groups = ["main"]
files = [
{file = "psycopg2-binary-2.9.9.tar.gz", hash = "sha256:7f01846810177d829c7692f1f5ada8096762d9172af1b1a28d4ab5b77c923c1c"},
{file = "psycopg2_binary-2.9.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c2470da5418b76232f02a2fcd2229537bb2d5a7096674ce61859c3229f2eb202"},
{file = "psycopg2_binary-2.9.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c6af2a6d4b7ee9615cbb162b0738f6e1fd1f5c3eda7e5da17861eacf4c717ea7"},
{file = "psycopg2_binary-2.9.9-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:75723c3c0fbbf34350b46a3199eb50638ab22a0228f93fb472ef4d9becc2382b"},
{file = "psycopg2_binary-2.9.9-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:83791a65b51ad6ee6cf0845634859d69a038ea9b03d7b26e703f94c7e93dbcf9"},
{file = "psycopg2_binary-2.9.9-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0ef4854e82c09e84cc63084a9e4ccd6d9b154f1dbdd283efb92ecd0b5e2b8c84"},
{file = "psycopg2_binary-2.9.9-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed1184ab8f113e8d660ce49a56390ca181f2981066acc27cf637d5c1e10ce46e"},
{file = "psycopg2_binary-2.9.9-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d2997c458c690ec2bc6b0b7ecbafd02b029b7b4283078d3b32a852a7ce3ddd98"},
{file = "psycopg2_binary-2.9.9-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:b58b4710c7f4161b5e9dcbe73bb7c62d65670a87df7bcce9e1faaad43e715245"},
{file = "psycopg2_binary-2.9.9-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:0c009475ee389757e6e34611d75f6e4f05f0cf5ebb76c6037508318e1a1e0d7e"},
{file = "psycopg2_binary-2.9.9-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8dbf6d1bc73f1d04ec1734bae3b4fb0ee3cb2a493d35ede9badbeb901fb40f6f"},
{file = "psycopg2_binary-2.9.9-cp310-cp310-win32.whl", hash = "sha256:3f78fd71c4f43a13d342be74ebbc0666fe1f555b8837eb113cb7416856c79682"},
{file = "psycopg2_binary-2.9.9-cp310-cp310-win_amd64.whl", hash = "sha256:876801744b0dee379e4e3c38b76fc89f88834bb15bf92ee07d94acd06ec890a0"},
{file = "psycopg2_binary-2.9.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ee825e70b1a209475622f7f7b776785bd68f34af6e7a46e2e42f27b659b5bc26"},
{file = "psycopg2_binary-2.9.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1ea665f8ce695bcc37a90ee52de7a7980be5161375d42a0b6c6abedbf0d81f0f"},
{file = "psycopg2_binary-2.9.9-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:143072318f793f53819048fdfe30c321890af0c3ec7cb1dfc9cc87aa88241de2"},
{file = "psycopg2_binary-2.9.9-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c332c8d69fb64979ebf76613c66b985414927a40f8defa16cf1bc028b7b0a7b0"},
{file = "psycopg2_binary-2.9.9-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7fc5a5acafb7d6ccca13bfa8c90f8c51f13d8fb87d95656d3950f0158d3ce53"},
{file = "psycopg2_binary-2.9.9-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:977646e05232579d2e7b9c59e21dbe5261f403a88417f6a6512e70d3f8a046be"},
{file = "psycopg2_binary-2.9.9-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b6356793b84728d9d50ead16ab43c187673831e9d4019013f1402c41b1db9b27"},
{file = "psycopg2_binary-2.9.9-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:bc7bb56d04601d443f24094e9e31ae6deec9ccb23581f75343feebaf30423359"},
{file = "psycopg2_binary-2.9.9-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:77853062a2c45be16fd6b8d6de2a99278ee1d985a7bd8b103e97e41c034006d2"},
{file = "psycopg2_binary-2.9.9-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:78151aa3ec21dccd5cdef6c74c3e73386dcdfaf19bced944169697d7ac7482fc"},
{file = "psycopg2_binary-2.9.9-cp311-cp311-win32.whl", hash = "sha256:dc4926288b2a3e9fd7b50dc6a1909a13bbdadfc67d93f3374d984e56f885579d"},
{file = "psycopg2_binary-2.9.9-cp311-cp311-win_amd64.whl", hash = "sha256:b76bedd166805480ab069612119ea636f5ab8f8771e640ae103e05a4aae3e417"},
{file = "psycopg2_binary-2.9.9-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:8532fd6e6e2dc57bcb3bc90b079c60de896d2128c5d9d6f24a63875a95a088cf"},
{file = "psycopg2_binary-2.9.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b0605eaed3eb239e87df0d5e3c6489daae3f7388d455d0c0b4df899519c6a38d"},
{file = "psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f8544b092a29a6ddd72f3556a9fcf249ec412e10ad28be6a0c0d948924f2212"},
{file = "psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2d423c8d8a3c82d08fe8af900ad5b613ce3632a1249fd6a223941d0735fce493"},
{file = "psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2e5afae772c00980525f6d6ecf7cbca55676296b580c0e6abb407f15f3706996"},
{file = "psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e6f98446430fdf41bd36d4faa6cb409f5140c1c2cf58ce0bbdaf16af7d3f119"},
{file = "psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c77e3d1862452565875eb31bdb45ac62502feabbd53429fdc39a1cc341d681ba"},
{file = "psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:cb16c65dcb648d0a43a2521f2f0a2300f40639f6f8c1ecbc662141e4e3e1ee07"},
{file = "psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:911dda9c487075abd54e644ccdf5e5c16773470a6a5d3826fda76699410066fb"},
{file = "psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:57fede879f08d23c85140a360c6a77709113efd1c993923c59fde17aa27599fe"},
{file = "psycopg2_binary-2.9.9-cp312-cp312-win32.whl", hash = "sha256:64cf30263844fa208851ebb13b0732ce674d8ec6a0c86a4e160495d299ba3c93"},
{file = "psycopg2_binary-2.9.9-cp312-cp312-win_amd64.whl", hash = "sha256:81ff62668af011f9a48787564ab7eded4e9fb17a4a6a74af5ffa6a457400d2ab"},
{file = "psycopg2_binary-2.9.9-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:2293b001e319ab0d869d660a704942c9e2cce19745262a8aba2115ef41a0a42a"},
{file = "psycopg2_binary-2.9.9-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:03ef7df18daf2c4c07e2695e8cfd5ee7f748a1d54d802330985a78d2a5a6dca9"},
{file = "psycopg2_binary-2.9.9-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a602ea5aff39bb9fac6308e9c9d82b9a35c2bf288e184a816002c9fae930b77"},
{file = "psycopg2_binary-2.9.9-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8359bf4791968c5a78c56103702000105501adb557f3cf772b2c207284273984"},
{file = "psycopg2_binary-2.9.9-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:275ff571376626195ab95a746e6a04c7df8ea34638b99fc11160de91f2fef503"},
{file = "psycopg2_binary-2.9.9-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:f9b5571d33660d5009a8b3c25dc1db560206e2d2f89d3df1cb32d72c0d117d52"},
{file = "psycopg2_binary-2.9.9-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:420f9bbf47a02616e8554e825208cb947969451978dceb77f95ad09c37791dae"},
{file = "psycopg2_binary-2.9.9-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:4154ad09dac630a0f13f37b583eae260c6aa885d67dfbccb5b02c33f31a6d420"},
{file = "psycopg2_binary-2.9.9-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:a148c5d507bb9b4f2030a2025c545fccb0e1ef317393eaba42e7eabd28eb6041"},
{file = "psycopg2_binary-2.9.9-cp37-cp37m-win32.whl", hash = "sha256:68fc1f1ba168724771e38bee37d940d2865cb0f562380a1fb1ffb428b75cb692"},
{file = "psycopg2_binary-2.9.9-cp37-cp37m-win_amd64.whl", hash = "sha256:281309265596e388ef483250db3640e5f414168c5a67e9c665cafce9492eda2f"},
{file = "psycopg2_binary-2.9.9-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:60989127da422b74a04345096c10d416c2b41bd7bf2a380eb541059e4e999980"},
{file = "psycopg2_binary-2.9.9-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:246b123cc54bb5361588acc54218c8c9fb73068bf227a4a531d8ed56fa3ca7d6"},
{file = "psycopg2_binary-2.9.9-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:34eccd14566f8fe14b2b95bb13b11572f7c7d5c36da61caf414d23b91fcc5d94"},
{file = "psycopg2_binary-2.9.9-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:18d0ef97766055fec15b5de2c06dd8e7654705ce3e5e5eed3b6651a1d2a9a152"},
{file = "psycopg2_binary-2.9.9-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d3f82c171b4ccd83bbaf35aa05e44e690113bd4f3b7b6cc54d2219b132f3ae55"},
{file = "psycopg2_binary-2.9.9-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ead20f7913a9c1e894aebe47cccf9dc834e1618b7aa96155d2091a626e59c972"},
{file = "psycopg2_binary-2.9.9-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:ca49a8119c6cbd77375ae303b0cfd8c11f011abbbd64601167ecca18a87e7cdd"},
{file = "psycopg2_binary-2.9.9-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:323ba25b92454adb36fa425dc5cf6f8f19f78948cbad2e7bc6cdf7b0d7982e59"},
{file = "psycopg2_binary-2.9.9-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:1236ed0952fbd919c100bc839eaa4a39ebc397ed1c08a97fc45fee2a595aa1b3"},
{file = "psycopg2_binary-2.9.9-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:729177eaf0aefca0994ce4cffe96ad3c75e377c7b6f4efa59ebf003b6d398716"},
{file = "psycopg2_binary-2.9.9-cp38-cp38-win32.whl", hash = "sha256:804d99b24ad523a1fe18cc707bf741670332f7c7412e9d49cb5eab67e886b9b5"},
{file = "psycopg2_binary-2.9.9-cp38-cp38-win_amd64.whl", hash = "sha256:a6cdcc3ede532f4a4b96000b6362099591ab4a3e913d70bcbac2b56c872446f7"},
{file = "psycopg2_binary-2.9.9-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:72dffbd8b4194858d0941062a9766f8297e8868e1dd07a7b36212aaa90f49472"},
{file = "psycopg2_binary-2.9.9-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:30dcc86377618a4c8f3b72418df92e77be4254d8f89f14b8e8f57d6d43603c0f"},
{file = "psycopg2_binary-2.9.9-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:31a34c508c003a4347d389a9e6fcc2307cc2150eb516462a7a17512130de109e"},
{file = "psycopg2_binary-2.9.9-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:15208be1c50b99203fe88d15695f22a5bed95ab3f84354c494bcb1d08557df67"},
{file = "psycopg2_binary-2.9.9-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1873aade94b74715be2246321c8650cabf5a0d098a95bab81145ffffa4c13876"},
{file = "psycopg2_binary-2.9.9-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a58c98a7e9c021f357348867f537017057c2ed7f77337fd914d0bedb35dace7"},
{file = "psycopg2_binary-2.9.9-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4686818798f9194d03c9129a4d9a702d9e113a89cb03bffe08c6cf799e053291"},
{file = "psycopg2_binary-2.9.9-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:ebdc36bea43063116f0486869652cb2ed7032dbc59fbcb4445c4862b5c1ecf7f"},
{file = "psycopg2_binary-2.9.9-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:ca08decd2697fdea0aea364b370b1249d47336aec935f87b8bbfd7da5b2ee9c1"},
{file = "psycopg2_binary-2.9.9-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ac05fb791acf5e1a3e39402641827780fe44d27e72567a000412c648a85ba860"},
{file = "psycopg2_binary-2.9.9-cp39-cp39-win32.whl", hash = "sha256:9dba73be7305b399924709b91682299794887cbbd88e38226ed9f6712eabee90"},
{file = "psycopg2_binary-2.9.9-cp39-cp39-win_amd64.whl", hash = "sha256:f7ae5d65ccfbebdfa761585228eb4d0df3a8b15cfb53bd953e713e09fbb12957"},
]
[[package]]
name = "pydantic"
version = "2.11.9"
description = "Data validation using Python type hints"
optional = false
python-versions = ">=3.9"
groups = ["main"]
files = [
{file = "pydantic-2.11.9-py3-none-any.whl", hash = "sha256:c42dd626f5cfc1c6950ce6205ea58c93efa406da65f479dcb4029d5934857da2"},
{file = "pydantic-2.11.9.tar.gz", hash = "sha256:6b8ffda597a14812a7975c90b82a8a2e777d9257aba3453f973acd3c032a18e2"},
]
[package.dependencies]
annotated-types = ">=0.6.0"
pydantic-core = "2.33.2"
typing-extensions = ">=4.12.2"
typing-inspection = ">=0.4.0"
[package.extras]
email = ["email-validator (>=2.0.0)"]
timezone = ["tzdata ; python_version >= \"3.9\" and platform_system == \"Windows\""]
[[package]]
name = "pydantic-core"
version = "2.33.2"
description = "Core functionality for Pydantic validation and serialization"
optional = false
python-versions = ">=3.9"
groups = ["main"]
files = [
{file = "pydantic_core-2.33.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2b3d326aaef0c0399d9afffeb6367d5e26ddc24d351dbc9c636840ac355dc5d8"},
{file = "pydantic_core-2.33.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e5b2671f05ba48b94cb90ce55d8bdcaaedb8ba00cc5359f6810fc918713983d"},
{file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0069c9acc3f3981b9ff4cdfaf088e98d83440a4c7ea1bc07460af3d4dc22e72d"},
{file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d53b22f2032c42eaaf025f7c40c2e3b94568ae077a606f006d206a463bc69572"},
{file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0405262705a123b7ce9f0b92f123334d67b70fd1f20a9372b907ce1080c7ba02"},
{file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4b25d91e288e2c4e0662b8038a28c6a07eaac3e196cfc4ff69de4ea3db992a1b"},
{file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6bdfe4b3789761f3bcb4b1ddf33355a71079858958e3a552f16d5af19768fef2"},
{file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:efec8db3266b76ef9607c2c4c419bdb06bf335ae433b80816089ea7585816f6a"},
{file = "pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:031c57d67ca86902726e0fae2214ce6770bbe2f710dc33063187a68744a5ecac"},
{file = "pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:f8de619080e944347f5f20de29a975c2d815d9ddd8be9b9b7268e2e3ef68605a"},
{file = "pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:73662edf539e72a9440129f231ed3757faab89630d291b784ca99237fb94db2b"},
{file = "pydantic_core-2.33.2-cp310-cp310-win32.whl", hash = "sha256:0a39979dcbb70998b0e505fb1556a1d550a0781463ce84ebf915ba293ccb7e22"},
{file = "pydantic_core-2.33.2-cp310-cp310-win_amd64.whl", hash = "sha256:b0379a2b24882fef529ec3b4987cb5d003b9cda32256024e6fe1586ac45fc640"},
{file = "pydantic_core-2.33.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:4c5b0a576fb381edd6d27f0a85915c6daf2f8138dc5c267a57c08a62900758c7"},
{file = "pydantic_core-2.33.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e799c050df38a639db758c617ec771fd8fb7a5f8eaaa4b27b101f266b216a246"},
{file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc46a01bf8d62f227d5ecee74178ffc448ff4e5197c756331f71efcc66dc980f"},
{file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a144d4f717285c6d9234a66778059f33a89096dfb9b39117663fd8413d582dcc"},
{file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:73cf6373c21bc80b2e0dc88444f41ae60b2f070ed02095754eb5a01df12256de"},
{file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3dc625f4aa79713512d1976fe9f0bc99f706a9dee21dfd1810b4bbbf228d0e8a"},
{file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:881b21b5549499972441da4758d662aeea93f1923f953e9cbaff14b8b9565aef"},
{file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bdc25f3681f7b78572699569514036afe3c243bc3059d3942624e936ec93450e"},
{file = "pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:fe5b32187cbc0c862ee201ad66c30cf218e5ed468ec8dc1cf49dec66e160cc4d"},
{file = "pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:bc7aee6f634a6f4a95676fcb5d6559a2c2a390330098dba5e5a5f28a2e4ada30"},
{file = "pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:235f45e5dbcccf6bd99f9f472858849f73d11120d76ea8707115415f8e5ebebf"},
{file = "pydantic_core-2.33.2-cp311-cp311-win32.whl", hash = "sha256:6368900c2d3ef09b69cb0b913f9f8263b03786e5b2a387706c5afb66800efd51"},
{file = "pydantic_core-2.33.2-cp311-cp311-win_amd64.whl", hash = "sha256:1e063337ef9e9820c77acc768546325ebe04ee38b08703244c1309cccc4f1bab"},
{file = "pydantic_core-2.33.2-cp311-cp311-win_arm64.whl", hash = "sha256:6b99022f1d19bc32a4c2a0d544fc9a76e3be90f0b3f4af413f87d38749300e65"},
{file = "pydantic_core-2.33.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a7ec89dc587667f22b6a0b6579c249fca9026ce7c333fc142ba42411fa243cdc"},
{file = "pydantic_core-2.33.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3c6db6e52c6d70aa0d00d45cdb9b40f0433b96380071ea80b09277dba021ddf7"},
{file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e61206137cbc65e6d5256e1166f88331d3b6238e082d9f74613b9b765fb9025"},
{file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb8c529b2819c37140eb51b914153063d27ed88e3bdc31b71198a198e921e011"},
{file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c52b02ad8b4e2cf14ca7b3d918f3eb0ee91e63b3167c32591e57c4317e134f8f"},
{file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96081f1605125ba0855dfda83f6f3df5ec90c61195421ba72223de35ccfb2f88"},
{file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f57a69461af2a5fa6e6bbd7a5f60d3b7e6cebb687f55106933188e79ad155c1"},
{file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:572c7e6c8bb4774d2ac88929e3d1f12bc45714ae5ee6d9a788a9fb35e60bb04b"},
{file = "pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:db4b41f9bd95fbe5acd76d89920336ba96f03e149097365afe1cb092fceb89a1"},
{file = "pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:fa854f5cf7e33842a892e5c73f45327760bc7bc516339fda888c75ae60edaeb6"},
{file = "pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5f483cfb75ff703095c59e365360cb73e00185e01aaea067cd19acffd2ab20ea"},
{file = "pydantic_core-2.33.2-cp312-cp312-win32.whl", hash = "sha256:9cb1da0f5a471435a7bc7e439b8a728e8b61e59784b2af70d7c169f8dd8ae290"},
{file = "pydantic_core-2.33.2-cp312-cp312-win_amd64.whl", hash = "sha256:f941635f2a3d96b2973e867144fde513665c87f13fe0e193c158ac51bfaaa7b2"},
{file = "pydantic_core-2.33.2-cp312-cp312-win_arm64.whl", hash = "sha256:cca3868ddfaccfbc4bfb1d608e2ccaaebe0ae628e1416aeb9c4d88c001bb45ab"},
{file = "pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f"},
{file = "pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6"},
{file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef"},
{file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a"},
{file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916"},
{file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a"},
{file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d"},
{file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56"},
{file = "pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5"},
{file = "pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e"},
{file = "pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162"},
{file = "pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849"},
{file = "pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9"},
{file = "pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9"},
{file = "pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac"},
{file = "pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5"},
{file = "pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9"},
{file = "pydantic_core-2.33.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:a2b911a5b90e0374d03813674bf0a5fbbb7741570dcd4b4e85a2e48d17def29d"},
{file = "pydantic_core-2.33.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6fa6dfc3e4d1f734a34710f391ae822e0a8eb8559a85c6979e14e65ee6ba2954"},
{file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c54c939ee22dc8e2d545da79fc5381f1c020d6d3141d3bd747eab59164dc89fb"},
{file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:53a57d2ed685940a504248187d5685e49eb5eef0f696853647bf37c418c538f7"},
{file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:09fb9dd6571aacd023fe6aaca316bd01cf60ab27240d7eb39ebd66a3a15293b4"},
{file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0e6116757f7959a712db11f3e9c0a99ade00a5bbedae83cb801985aa154f071b"},
{file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d55ab81c57b8ff8548c3e4947f119551253f4e3787a7bbc0b6b3ca47498a9d3"},
{file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c20c462aa4434b33a2661701b861604913f912254e441ab8d78d30485736115a"},
{file = "pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:44857c3227d3fb5e753d5fe4a3420d6376fa594b07b621e220cd93703fe21782"},
{file = "pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:eb9b459ca4df0e5c87deb59d37377461a538852765293f9e6ee834f0435a93b9"},
{file = "pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9fcd347d2cc5c23b06de6d3b7b8275be558a0c90549495c699e379a80bf8379e"},
{file = "pydantic_core-2.33.2-cp39-cp39-win32.whl", hash = "sha256:83aa99b1285bc8f038941ddf598501a86f1536789740991d7d8756e34f1e74d9"},
{file = "pydantic_core-2.33.2-cp39-cp39-win_amd64.whl", hash = "sha256:f481959862f57f29601ccced557cc2e817bce7533ab8e01a797a48b49c9692b3"},
{file = "pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5c4aa4e82353f65e548c476b37e64189783aa5384903bfea4f41580f255fddfa"},
{file = "pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d946c8bf0d5c24bf4fe333af284c59a19358aa3ec18cb3dc4370080da1e8ad29"},
{file = "pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87b31b6846e361ef83fedb187bb5b4372d0da3f7e28d85415efa92d6125d6e6d"},
{file = "pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa9d91b338f2df0508606f7009fde642391425189bba6d8c653afd80fd6bb64e"},
{file = "pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2058a32994f1fde4ca0480ab9d1e75a0e8c87c22b53a3ae66554f9af78f2fe8c"},
{file = "pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:0e03262ab796d986f978f79c943fc5f620381be7287148b8010b4097f79a39ec"},
{file = "pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:1a8695a8d00c73e50bff9dfda4d540b7dee29ff9b8053e38380426a85ef10052"},
{file = "pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:fa754d1850735a0b0e03bcffd9d4b4343eb417e47196e4485d9cca326073a42c"},
{file = "pydantic_core-2.33.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:a11c8d26a50bfab49002947d3d237abe4d9e4b5bdc8846a63537b6488e197808"},
{file = "pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:dd14041875d09cc0f9308e37a6f8b65f5585cf2598a53aa0123df8b129d481f8"},
{file = "pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d87c561733f66531dced0da6e864f44ebf89a8fba55f31407b00c2f7f9449593"},
{file = "pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f82865531efd18d6e07a04a17331af02cb7a651583c418df8266f17a63c6612"},
{file = "pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bfb5112df54209d820d7bf9317c7a6c9025ea52e49f46b6a2060104bba37de7"},
{file = "pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:64632ff9d614e5eecfb495796ad51b0ed98c453e447a76bcbeeb69615079fc7e"},
{file = "pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:f889f7a40498cc077332c7ab6b4608d296d852182211787d4f3ee377aaae66e8"},
{file = "pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:de4b83bb311557e439b9e186f733f6c645b9417c84e2eb8203f3f820a4b988bf"},
{file = "pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:82f68293f055f51b51ea42fafc74b6aad03e70e191799430b90c13d643059ebb"},
{file = "pydantic_core-2.33.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:329467cecfb529c925cf2bbd4d60d2c509bc2fb52a20c1045bf09bb70971a9c1"},
{file = "pydantic_core-2.33.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:87acbfcf8e90ca885206e98359d7dca4bcbb35abdc0ff66672a293e1d7a19101"},
{file = "pydantic_core-2.33.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:7f92c15cd1e97d4b12acd1cc9004fa092578acfa57b67ad5e43a197175d01a64"},
{file = "pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d3f26877a748dc4251cfcfda9dfb5f13fcb034f5308388066bcfe9031b63ae7d"},
{file = "pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dac89aea9af8cd672fa7b510e7b8c33b0bba9a43186680550ccf23020f32d535"},
{file = "pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:970919794d126ba8645f3837ab6046fb4e72bbc057b3709144066204c19a455d"},
{file = "pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:3eb3fe62804e8f859c49ed20a8451342de53ed764150cb14ca71357c765dc2a6"},
{file = "pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:3abcd9392a36025e3bd55f9bd38d908bd17962cc49bc6da8e7e96285336e2bca"},
{file = "pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:3a1c81334778f9e3af2f8aeb7a960736e5cab1dfebfb26aabca09afd2906c039"},
{file = "pydantic_core-2.33.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2807668ba86cb38c6817ad9bc66215ab8584d1d304030ce4f0887336f28a5e27"},
{file = "pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc"},
]
[package.dependencies]
typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0"
[[package]]
name = "sniffio"
version = "1.3.1"
description = "Sniff out which async library your code is running under"
optional = false
python-versions = ">=3.7"
groups = ["main"]
files = [
{file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"},
{file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"},
]
[[package]]
name = "typing-extensions"
version = "4.15.0"
description = "Backported and Experimental Type Hints for Python 3.9+"
optional = false
python-versions = ">=3.9"
groups = ["main"]
files = [
{file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"},
{file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"},
]
[[package]]
name = "typing-inspection"
version = "0.4.1"
description = "Runtime typing introspection tools"
optional = false
python-versions = ">=3.9"
groups = ["main"]
files = [
{file = "typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51"},
{file = "typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28"},
]
[package.dependencies]
typing-extensions = ">=4.12.0"
[metadata]
lock-version = "2.1"
python-versions = ">=3.11"
content-hash = "132dfa86c9b3a4e9221cb6ce4a3fea6bb11593b0c0b9f168615adc2aaf34db3a"

View File

@ -0,0 +1,16 @@
[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"
[tool.poetry]
name = "chatbot-ollama"
version = "0.1.0"
description = "A chatbot implementation using Ollama."
authors = ["huliyan <liyanhu03@163.com>"]
license = "MIT"
readme = "README.md"
[tool.poetry.dependencies]
python = ">=3.11"
ollama = ">=0.2.0"
psycopg2-binary = "2.9.9"

View File

@ -0,0 +1,312 @@
# Airbyte实践案例: 从Apify Dataset到openGauss DataVec
Airbyte目前有300+ Source可选本文选择使用频率很高、非常流行的Apify Dataset作为sourcedestination则为openGauss DataVec。
具体而言本教程将使用Apify的Website Content Crawler Actor抓取openGauss DataVec的官方文档存入Apify Dataset之后通过airbyte平台写入openGauss DataVec向量数据库。再本地部署deepseek-r1大模型和nomic-embed-text嵌入模型搭建本地AI助手。
## 前置准备
- 本地部署的airbyte
- apify账号
- docker
本文实践所采用的操作系统为 **openEuler 22.03 LTS**。为了确保各个组件之间能够实现无缝兼容并顺利运行,我们选择使用 **python3.11**
## Apify抓取openGauss DataVec官方文档
登录Apify控制台网站 [Apify Console](https://console.apify.com/)
选择Apify Store -> Website Content Crawler
![image-20250919104414489](../images/image-20250919104414489.png)
点击后进行简单配置这里选择抓取一页openGauss DataVec官方文档
- `Start URLs``https://docs.opengauss.org/zh/docs/latest/docs/DataVec/DataVec-Overview.html`
- `Crawler type`选择第三个`Raw HTTP client(Cheerio)-Fastest, but doesn't render JavaScript content.`
- `HTML Processing` -> `Keep HTML elements (CSS selector)` 填入 `article`
其他保留默认值即可。
![image-20250919111343004](../images/image-20250919111343004.png)
![image-20250919111507359](../images/image-20250919111507359.png)
点击start获取到结果如下。
![image-20250919111750445](../images/image-20250919111750445.png)
点击左边导航栏Storage查看最新捕获内容的`Dataset ID`并修改name为`openGauss DataVec介绍`
![8b91c87878b915c17cf8a9c65e68524f](../images/8b91c87878b915c17cf8a9c65e68524f.png)
复制`Dataset ID`等下airbyte需要用到。
点击左边栏`Settings` -> `API& Integrations`复制 `Personal API tokens`等下airbyte也需要用到。
![image-20250919112723433](../images/image-20250919112723433.png)
## 配置Airbyte-Apify Dataset
进入airbyte界面点击左边栏`Source` ,选择`Apify Dataset`,填充我们之前复制的`Dataset ID`和 `Personal API tokens`,然后`set up source`
![image-20250919113536026](../images/image-20250919113536026.png)
---
## 部署openGauss DataVec
命令行拉取openGauss镜像并运行
```bash
# 拉取镜像
docker pull opengauss/opengauss:latest
# 运行容器
docker run --name opengauss --privileged=true -d -e GS_PASSWORD=Test@123 -v /opengauss:/var/lib/opengauss -p 5432:5432 opengauss/opengauss:latest
```
创建新用户(因为openGauss不支持初始用户通过远程连接登录)
```bash
# 进入opengauss容器
docker exec -it opengauss bash
# 切换用户
su omm
# 连接opengauss
gsql -d postgres
# 创建新用户
CREATE USER airbyte WITH PASSWORD 'Airbyte@123';
# 分配权限
GRANT ALL PRIVILEGES TO airbyte;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO airbyte;
GRANT INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO airbyte;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO airbyte;
```
## 部署deepseek-r1大模型和nomic-embed-text嵌入模型
命令行拉取ollama镜像并运行
```bash
# 拉取镜像
docker pull ollama/ollama
# 运行镜像
docker run -d -p 11434:11434 --name ollama ollama/ollama:latest
# 进入容器
docker exec -it ollama bash
# 拉取模型
ollama pull nomic-embed-text
ollama pull deepseek-r1:1.5b # 根据电脑配置选择适当参数的模型
```
## 配置airbyte-openGauss DataVec
### 加载openGauss DataVec destination connector镜像
进入airbyte主界面点击`Setting` -> `Destinations` -> `+New connector`
![image-20250918222315397](../images/image-20250918222315397_1.png)
填写表单如下
- `Connector display name``openGauss DataVec`
- `Docker repository name`: `lucashu123/airbyte-connector-opengauss-datavec`
- `Docker image tag``latest`
![image-20250918222423289](../images/image-20250918222423289_1.png)
之后便可在Destination -> Custom中选择openGauss DataVec作为destination。
![image-20250918223149758](../images/image-20250918223149758_1.png)
### 填写配置
配置openGauss DataVec有3个部分。
- `Processing`—决定将source中获取的内容的哪些字段作为元数据哪些字段作为文本字段分别由`Fields to store as metadata`和`Text fields to embed`控制(若不填,默认选择所有字段)。以及将所有文本字段拼接成一个总文本后,如何进行分块,由`Chunk size`和`Text splitter`等控制。
- `Embedding`—选择嵌入向量模型API将每个chunk分块文本嵌入向量。可以选OpenAI API自部署的OpenAI-compatible API等。这里我们选择自部署的`nomic-embed-text嵌入模型`。
- `Indexing`—填写openGauss向量数据库的连接配置。
配置如下:
- `Processing`部分
- `chunk size`: 100
- `Fields to store as metadata`: `metadata`
- `Text fields to embed`: `markdown`
- `Text splitter`选择`By Markdown header``Split level`填充`2`
- `Embedding`部分,选择`OpenAI-compatible`
- `Base URL`: http://localhost:11434/v1 (替换为上面部署ollama容器所在机器的ip地址)
- `Embedding dimensions`: 768
- `Model name` : `nomic-embed-text`
- `Indexing`部分
- `Host`: `localhost` (替换为上面部署openGauss容器所在机器的ip地址)
- `Database`: `postgres`
- `Username`: `airbyte` (上面创建的新用户)
- `Password`: `Airbyte@123` (上面创建的新用户密码)
![image-20250919152143510](../images/image-20250919152143510.png)
## 建立连接
进入airbyte主界面点击左边栏`connections` -> `+ New connection`
`Define source`选择上面配置好的`Apify Dataset``Define destination`选择上面配置好的`openGauss DataVec`
`Select streams`中有4个stream我们只选择`item_collection_website_content_crawler`流,其中只勾选`markdown`和`metadata`字段
![image-20250919143542532](../images/image-20250919143542532.png)
`Configure connection`中的`Schedule type`我们选择调度方式为`Manual`因为我们选择的Apify Dataset的内容是固定的手动同步一次即可无需定时按频率同步。
## 开始同步
建立好连接后,即可点击`Sync now`开始同步大概花费5min等待同步结束。
![image-20250919144750751](../images/image-20250919144750751.png)
所有流将被存储到同名的表中。如果表不存在将创建表。表将具有以下列:
- `document_id`(字符串)- 文档的唯一标识符通过stream名 + stream的主键构成若没有主键则uuid随机生成`document_id`
- `chunk_id`(字符串)- 块的唯一标识符,通过将块编号附加到 document_id 创建
- `metadata`(变体)- 文档的元数据,以键值对形式存储
- `document_content`(字符串)- 块的文本内容
- `embedding`(向量)- 块的嵌入,以浮点数列表形式存储
## 搭建AI助手进行测试
源代码如下利用本地部署的deepseek-r1作大模型
```python
import ollama
import psycopg2
table_name = "item_collection_website_content_crawler"
def embedding(text):
vector = ollama.embeddings(model="nomic-embed-text", prompt=text)
return vector["embedding"]
# 替换为自己的配置
conn = psycopg2.connect(
database="postgres",
user="airbyte",
password="Airbyte@123",
host="192.168.219.134",
port="5432"
)
cur = conn.cursor()
# 设置搜索路径
cur.execute("SET search_path TO public;")
# 创建索引
cur.execute("CREATE INDEX IF NOT EXISTS idx_{}_embedding_hnsw ON {} USING hnsw (embedding vector_l2_ops);".format(table_name, table_name))
conn.commit()
question = "介绍一下openGauss DataVec"
emb_data = embedding(question)
dimensions = len(emb_data)
cur = conn.cursor()
cur.execute("select document_content from {} order by embedding <-> '{}' limit 3;".format(table_name, emb_data))
conn.commit()
rows = cur.fetchall()
print(rows)
cur.close()
conn.close()
# 指定容器所在主机或容器名、端口(如果需要的话), 或通过设置环境变量
# client = ollama.Client(host="http://<宿主机 IP>:11434")
context = rows
# 测试
# context = ""
SYSTEM_PROMPT = "你作为一个对话 AI 助手,结合上下文信息简练高效的回答用户提出的问题"
USER_PROMPT = f"请结合{context}信息来回答{question}的问题,不需要额外的无用回答"
response = ollama.chat(
model="deepseek-r1:1.5b",
stream=True,
messages=[
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": USER_PROMPT}
]
)
print("\n\n")
# 实时打印每一段内容
for chunk in response:
print(chunk["message"]["content"], end="", flush=True)
```
问题为`介绍一下openGauss DataVec`。当没有为大模型提供DataVec查询结果作为上下文时的回答如下
```
<think>
我现在需要帮用户介绍一下openGauss DataVec的问题。用户要求答案要简洁高效不需要额外的信息。
首先我得了解DataVec的基本概念和结构。DataVec是openGauss的一个数据结构用于存储数据向量类似于Java中的List或Python的List。
接下来我要考虑用户可能对DataVec的使用情况感兴趣。也许他们是在开发一个需要高扩展性的数据库系统或者在进行数据管理和查询 优化的工作中。因此在回答时可以提到DataVec的优势比如快速查找和插入记录以及支持聚合操作这样能帮助用户更好地理解其价 值。
另外用户可能关心如何高效存储大量数据。DataVec的随机访问特性可能能满足他们的需求尤其是在处理高并发或大表的情况下。
最后总结一下DataVec适合什么样的场景比如在需要动态查询、快速插入和数据管理中的应用。
</think>
openGauss DataVec是一个高效的数据库数据结构专为存储动态数据向量设计。与传统列表类型数据结构相比DataVec的优势在于
1. **快速查找**:支持从随机位置获取记录信息,适用于动态查询场景。
2. **快速插入**:无需重新初始化数据结构,可以快速添加新记录。
3. **支持聚合操作**:适合进行统计、汇总等操作。
例如在大数据分析或实时系统中DataVec能够高效管理大量数据向量满足高扩展性和灵活性的需求。
```
幻觉比较严重当我们添加DataVec查询结果作为大模型补充上下文时的回答如下
```
<think>
好的我现在需要帮助用户解决一个关于openGauss DataVec的问题。根据用户提供的上下文我看到他们提到了DataVec向量数据库和它的多语言生态对接并且提供了详细的说明。
首先我要理解用户的需求是什么。他们想知道详细介绍openGauss DataVec的内容但不需要额外的信息所以我只需要提取关键点。
接下来我会回顾一下提供的信息DataVec是一个基于openGauss的向量引擎支持存储、检索大规模高维向量数据适用于智能知识检索和RAG应用。它还提到了支持的各种功能如最近邻搜索、L2距离、余弦距离、内积等并且用熟悉的SQL语法操作。
然后我会考虑用户可能的身份和使用场景。他们可能是开发人员或者研究人员需要在大数据环境中高效地处理向量数据。因此我应该强调DataVec的性能优势以及其对多种语言的支持。
最后,我会整理这些信息,确保回答简明扼要,涵盖主要功能、支持的语言生态和技术特点。
</think>
openGauss DataVec是一个基于openGauss的向量引擎提供向量数据类型的存储和检索。在处理大规模高维向量数据时能够快速、准确地检索结果并适用于智能知识检索和RAGRetrieval-Augmented Generation等复杂应用场景的智能应用。
DataVec支持的各种功能包括
1. **精确和近似的最近邻搜索**:快速找到最接近的向量。
2. **L2距离&余弦距离&内积**:多种距离计算方式,满足不同场景的需求。
3. **向量索引**:高效的索引结构,提升查询性能。
4. **向量操作函数和操作符**:灵活的操作符组合,增强功能扩展。
此外DataVec提供了多语言生态对接支持通过Python、Java、Node.js等编程语言的API调用使用户能够快速集成向量数据库能力。另 外它还采用开源组件优化了RAG场景下的兼容性提供了多样化的选择。
总体而言openGauss DataVec在处理向量数据时表现出色结合了高效的性能和丰富的功能支持满足复杂智能应用的需求。
```
可见AI助手回答效果更加精确和可靠。

View File

@ -0,0 +1,31 @@
## openGauss向量数据库对接airbyte
本项目为开源之夏2025的项目——openGauss向量数据库对接airbyte。
主要内容为
1. 开发destination为openGauss DataVec向量数据库的airbyte连接器(connector)
2. 输出实践案例教程和详细使用文档。
### 目录结构
```text
Airbyte_Destination_openGaussDataVec/
├─ destination-opengauss-datavec/
├─ Practical-Case-Tutorial/
├─ images/
├─ airbyte对接openGauss DataVec.md
├─ openGauss_DataVec_Connector_Document.md
├─ openGauss_DataVec_Connector_Document.zh.md
└─ README.md
```
#### 目录与文件说明
- **destination-opengauss-datavec/**: openGauss DataVec目标连接器(destination connector)源代码
- **Practical-Case-Tutorial/**: 实践案例与教程,包含演示与依赖配置。
- **images/**: 文档中使用的图片资源文件夹存储所有Markdown文件引用的图片。
- **airbyte对接openGauss DataVec.md**: airbyte本地部署和openGauss DataVec连接器镜像加载教程。
- **openGauss_DataVec_Connector_Document.md**: openGauss DataVec连接器配置说明的英文文档。
- **openGauss_DataVec_Connector_Document.zh.md**: openGauss DataVec连接器配置说明的中文文档。
- **README.md**: 总说明。

View File

@ -0,0 +1,96 @@
# Airbyte对接openGauss DataVec
本教程讲述如何将openGauss DataVec作为airbyte的destination连接器。
## 环境准备
- 安装较高版本的Docker 或 Docker Desktop
- 配置代理
## 本地部署airbyte
参考官方教程:[Quickstart | Airbyte Docs](https://docs.airbyte.com/platform/using-airbyte/getting-started/oss-quickstart)
本教程以Linux系统为例进行部署。
### 1.安装abctl
abctl是airbyte部署和管理的命令行工具。
```bash
curl -LsfS https://get.airbyte.com | bash -
```
成功后可以看见`abctl install succeeded.`
### 2.运行airbyte
运行命令
```bash
abctl local install --insecure-cookies --low-resource-mode
```
- `--insecure-cookies` 表示通过http进行连接针对不想配置ssl证书使用安全https连接时适用可以不用
- `--low-resource-mode`表示在低资源环境下(少于4个CPUs)运行不能使用Connector Builder工具。可以不用
可通过`abctl local install --help`查看更多参数详情。上述2个参数可根据自身情况选择是否使用。
整个过程大概需要花费30min。安装完成后可在浏览器中输入 [http://localhost:8000](http://localhost:8000/) 进入登录界面。第一次登录会要求填写邮箱,公司等信息。
> 温馨提示:若安装过程中遇见报错,优先考虑重启机器和更换稳定代理
### 3.查看凭证
查看默认密码命令
```bash
abctl local credentials
```
输出大致为
```bash
Credentials:
Email: user@example.com
Password: a-random-password
Client-Id: 03ef466c-5558-4ca5-856b-4960ba7c161b
Client-Secret: m2UjnDO4iyBQ3IsRiy5GG3LaZWP6xs9I
```
若要修改密码,采用下面命令。
```bash
abctl local credentials --password YourStrongPasswordExample
```
## 加载openGauss DataVec connector镜像
访问 [http://localhost:8000](http://localhost:8000/) (部署airbyte所在机器的ip)登录后点击Setting -> Destinations -> +New connector
![image-20250918222315397](images/image-20250918222315397.png)
填写表单如下
- `Connector display name``openGauss DataVec`
- `Docker repository name`: `lucashu123/airbyte-connector-opengauss-datavec`
- `Docker image tag``latest`
![image-20250918222423289](images/image-20250918222423289.png)
> Connector documentation URL选项有点问题填充URL后也不会显示文档
点击`Add`等待2-3min后成功加载。若超过5min大概率是因为代理网络问题。
之后便可在Destination -> Custom中选择openGauss DataVec作为destination。
![image-20250918223149758](images/image-20250918223149758.png)

View File

@ -0,0 +1,162 @@
# OpenGauss DataVec Destination
This is the repository for the OpenGauss DataVec destination connector, written in Python.
For information about how to use this connector within Airbyte, see [the documentation](https://docs.airbyte.com/integrations/destinations/opengauss-datavec).(todo)
## Overview
This destination allows you to sync data to OpenGauss database with vector storage capabilities using the DataVec extension. It supports:
- **Vector Embeddings**: Store text data as vector embeddings using various embedding providers (OpenAI, Cohere, Azure OpenAI, etc.)
- **Multiple Sync Modes**: Full refresh, incremental append, and append with deduplication
- **Flexible Text Processing**: Configurable text chunking and field mapping
- **OpenGauss DataVec Integration**: Native support for OpenGauss vector operations and distance functions
## Prerequisites
- OpenGauss database with DataVec extension installed
- Database user with appropriate permissions
- API key for chosen embedding provider (or use fake embeddings for testing)
## Local development
### Prerequisites
**To iterate on this connector, make sure to complete this prerequisites section.**
#### Minimum Python version required `= 3.10.0`
### Installing the connector
From this connector directory, run:
```bash
poetry install --with dev.
```
#### Create credentials
**If you are a community contributor**, follow the instructions in the [documentation](https://docs.airbyte.com/integrations/destinations/opengauss-datavec)(todo)
to generate the necessary credentials. Then create a file `secrets/config.json` conforming to the `destination_opengauss_datavec/spec.json` file.
Note that the `secrets` directory is gitignored by default, so there is no danger of accidentally checking in sensitive information.
See `integration_tests/sample_config.json` for a sample config file.
**If you are an Airbyte core member**, copy the credentials in Lastpass under the secret name `destination opengauss-datavec test creds`
and place them into `secrets/config.json`.
### Locally running the connector
```
poetry run python main.py spec
poetry run python main.py check --config secrets/config.json
cat examples/messages.jsonl | poetry run python main.py write --config secrets/config.json --catalog examples/configured_catalog.json
```
### Locally running the connector docker image
#### Use `airbyte-ci` to build your connector
The Airbyte way of building this connector is to use our `airbyte-ci` tool.
You can follow install instructions [here](https://github.com/airbytehq/airbyte/blob/master/airbyte-ci/connectors/pipelines/README.md#L1).
Then running the following command will build your connector:
```bash
airbyte-ci connectors --name destination-opengauss-datavec build
```
Once the command is done, you will find your connector image in your local docker registry: `airbyte/destination-opengauss-datavec:dev`.
##### Customizing our build process
When contributing on our connector you might need to customize the build process to add a system dependency or set an env var.
You can customize our build process by adding a `build_customization.py` module to your connector.
This module should contain a `pre_connector_install` and `post_connector_install` async function that will mutate the base image and the connector container respectively.
It will be imported at runtime by our build process and the functions will be called if they exist.
Here is an example of a `build_customization.py` module:
```python
from __future__ import annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING:
# Feel free to check the dagger documentation for more information on the Container object and its methods.
# https://dagger-io.readthedocs.io/en/sdk-python-v0.6.4/
from dagger import Container
async def pre_connector_install(base_image_container: Container) -> Container:
return await base_image_container.with_env_variable("MY_PRE_BUILD_ENV_VAR", "my_pre_build_env_var_value")
async def post_connector_install(connector_container: Container) -> Container:
return await connector_container.with_env_variable("MY_POST_BUILD_ENV_VAR", "my_post_build_env_var_value")
```
#### Build your own connector image
This connector is built using our dynamic built process in `airbyte-ci`.
The base image used to build it is defined within the metadata.yaml file under the `connectorBuildOptions`.
The build logic is defined using [Dagger](https://dagger.io/) [here](https://github.com/airbytehq/airbyte/blob/master/airbyte-ci/connectors/pipelines/pipelines/builds/python_connectors.py).
It does not rely on a Dockerfile.
If you would like to patch our connector and build your own a simple approach would be to:
1. Create your own Dockerfile based on the latest version of the connector image.
```Dockerfile
# todo
FROM airbyte/destination-opengauss-datavec:latest
COPY . ./airbyte/integration_code
RUN pip install ./airbyte/integration_code
# The entrypoint and default env vars are already set in the base image
# ENV AIRBYTE_ENTRYPOINT "python /airbyte/integration_code/main.py"
# ENTRYPOINT ["python", "/airbyte/integration_code/main.py"]
```
Please use this as an example. This is not optimized.
2. Build your image:
```bash
docker build -t airbyte/destination-opengauss-datavec:dev .
# Running the spec command against your patched connector
docker run airbyte/destination-opengauss-datavec:dev spec
```
#### Run
Then run any of the connector commands as follows:
```
docker run --rm airbyte/destination-opengauss-datavec:dev spec
docker run --rm -v $(pwd)/secrets:/secrets airbyte/destination-opengauss-datavec:dev check --config /secrets/config.json
# messages.jsonl is a file containing line-separated JSON representing AirbyteMessages
cat messages.jsonl | docker run --rm -v $(pwd)/secrets:/secrets -v $(pwd)/integration_tests:/integration_tests airbyte/destination-opengauss-datavec:dev write --config /secrets/config.json --catalog /integration_tests/configured_catalog.json
```
## Testing
You can run our full test suite locally using [`airbyte-ci`](https://github.com/airbytehq/airbyte/blob/master/airbyte-ci/connectors/pipelines/README.md):
```bash
airbyte-ci connectors --name=destination-opengauss-datavec test
```
### Unit Tests
To run unit tests locally, from the connector directory run:
```
poetry run pytest -s unit_tests
```
### Integration Tests
There are two types of integration tests: Acceptance Tests (Airbyte's test suite for all destination connectors) and custom integration tests (which are specific to this connector).
To run integration tests locally, make sure you have a secrets/config.json as explained above, and then run:
```
poetry run pytest -s integration_tests
```
### Customizing acceptance Tests
Customize `acceptance-test-config.yml` file to configure tests. See [Connector Acceptance Tests](https://docs.airbyte.com/connector-development/testing-connectors/connector-acceptance-tests-reference) for more information.
If your connector requires to create or destroy resources for use during acceptance tests create fixtures for it and place them inside integration_tests/acceptance.py.
### Using `airbyte-ci` to run tests
See [airbyte-ci documentation](https://github.com/airbytehq/airbyte/blob/master/airbyte-ci/connectors/pipelines/README.md#connectors-test-command)
## Dependency Management
All of your dependencies should go in `pyproject.toml`
* required for your connector to work need to go to `[tool.poetry.dependencies]` list.
* required for the testing need to go to `[tool.poetry.group.dev.dependencies]` list
### Publishing a new version of the connector
You've checked out the repo, implemented a million dollar feature, and you're ready to share your changes with the world. Now what?
1. Make sure your changes are passing unit and integration tests.
1. Bump the connector version in `pyproject.toml` -- increment the value appropriately (we use [SemVer](https://semver.org/)).
1. Update the `metadata.yaml` file with the new version.
1. Create a Pull Request.
1. Pat yourself on the back for being an awesome contributor.
1. Someone from Airbyte will take a look at your PR and iterate with you to merge it into master.

View File

@ -0,0 +1,7 @@
acceptance_tests:
spec:
tests:
- spec_path: integration_tests/spec.json
backward_compatibility_tests_config:
disable_for_version: "0.0.1"
connector_image: airbyte/destination-opengauss-datavec:dev

View File

@ -0,0 +1,13 @@
# openGauss DataVec Destination Connector Bootstrap
This destination does three things:
- Split records into chunks and separates metadata from text data
- Embeds text data into an embedding vector
- Stores the metadata and embedding vector in a vector database
The record processing is using the text split components from https://python.langchain.com/docs/modules/data_connection/document_transformers/.
There are various possible providers for generating embeddings, delivered as part of the CDK (`airbyte_cdk.destinations.vector_db_based`).
Embedded documents are stored in the openGauss DataVec vector database.

View File

@ -0,0 +1,4 @@
cd /home/hly/airbyte/airbyte-ci/connectors/pipelines/
eval "$(poetry env activate)"
airbyte-ci connectors --name destination-opengauss-datavec build -t dev

View File

@ -0,0 +1,8 @@
#
# Copyright (c) 2023 Airbyte, Inc., all rights reserved.
#
from .destination import DestinationOpenGaussDataVec
__all__ = ["DestinationOpenGaussDataVec"]

View File

@ -0,0 +1,104 @@
# Copyright (c) 2023 Airbyte, Inc., all rights reserved.
"""Catalog provider implementation.
A catalog provider wraps a configured catalog and configured streams. This class is responsible for
providing information about the catalog and streams. A catalog provider can also be updated with new
streams as they are discovered, providing a thin layer of abstraction over the configured catalog.
"""
from __future__ import annotations
from typing import TYPE_CHECKING, Any, final
from airbyte import exceptions as exc
from airbyte_cdk.models import DestinationSyncMode
if TYPE_CHECKING:
from airbyte_cdk.models import ConfiguredAirbyteCatalog, ConfiguredAirbyteStream
class CatalogProvider:
"""A catalog provider wraps a configured catalog and configured streams.
This class is responsible for providing information about the catalog and streams.
Note:
- The catalog provider is not responsible for managing the catalog or streams but it may
be updated with new streams as they are discovered.
"""
def __init__(
self,
configured_catalog: ConfiguredAirbyteCatalog,
) -> None:
"""Initialize the catalog manager with a catalog object reference.
Since the catalog is passed by reference, the catalog manager may be updated with new
streams as they are discovered.
"""
self._catalog: ConfiguredAirbyteCatalog = configured_catalog
@property
def configured_catalog(self) -> ConfiguredAirbyteCatalog:
return self._catalog
@property
def stream_names(self) -> list[str]:
return list({stream.stream.name for stream in self.configured_catalog.streams})
def get_configured_stream_info(
self,
stream_name: str,
) -> ConfiguredAirbyteStream:
"""Return the column definitions for the given stream."""
if not self.configured_catalog:
raise exc.PyAirbyteInternalError(
message="Cannot get stream JSON schema without a catalog.",
)
matching_streams: list[ConfiguredAirbyteStream] = [
stream
for stream in self.configured_catalog.streams
if stream.stream.name == stream_name
]
if not matching_streams:
raise exc.AirbyteStreamNotFoundError(
stream_name=stream_name,
context={
"available_streams": [
stream.stream.name for stream in self.configured_catalog.streams
],
},
)
if len(matching_streams) > 1:
raise exc.PyAirbyteInternalError(
message="Multiple streams found with same name.",
context={
"stream_name": stream_name,
},
)
return matching_streams[0]
@final
def get_stream_json_schema(
self,
stream_name: str,
) -> dict[str, Any]:
"""Return the column definitions for the given stream."""
return self.get_configured_stream_info(stream_name).stream.json_schema
def get_stream_properties(
self,
stream_name: str,
) -> dict[str, dict]:
"""Return the names of the top-level properties for the given stream."""
return self.get_stream_json_schema(stream_name)["properties"]
def get_destination_sync_mode(
self,
stream_name: str,
) -> DestinationSyncMode:
"""Return the destination sync mode for the given stream."""
return self.get_configured_stream_info(stream_name).destination_sync_mode

View File

@ -0,0 +1,313 @@
# Copyright (c) 2023 Airbyte, Inc., all rights reserved.
"""Abstract base class for Processors, including SQL processors.
Processors accept Airbyte messages as input from STDIN or from another input stream.
"""
from __future__ import annotations
import abc
import io
import queue
import sys
import warnings
from collections import defaultdict
from typing import TYPE_CHECKING, Union, cast, final
from airbyte import exceptions as exc
from airbyte.strategies import WriteStrategy
from airbyte_cdk.models import (
AirbyteMessage,
AirbyteRecordMessage,
AirbyteStateMessage,
AirbyteStateType,
AirbyteStreamState,
Type,
)
from destination_opengauss_datavec.common.state.state_writers import StateWriterBase, StdOutStateWriter
if TYPE_CHECKING:
from collections.abc import Iterable, Iterator
from airbyte._batch_handles import BatchHandle
from destination_opengauss_datavec.common.catalog.catalog_providers import CatalogProvider
from destination_opengauss_datavec.common.state.state_writers import StateWriterBase
class AirbyteMessageParsingError(Exception):
"""Raised when an Airbyte message is invalid or cannot be parsed."""
class RecordProcessorBase(abc.ABC):
"""Abstract base class for classes which can process Airbyte messages from a source.
This class is responsible for all aspects of handling Airbyte protocol.
The class should be passed a catalog manager and stream manager class to handle the
catalog and state aspects of the protocol.
"""
def __init__(
self,
*,
catalog_provider: CatalogProvider,
state_writer: Union[StateWriterBase, None] = None,
) -> None:
"""Initialize the processor.
If a state writer is not provided, the processor will use the default (STDOUT) state writer.
"""
self._catalog_provider: Union[CatalogProvider, None] = catalog_provider
self._state_writer: Union[StateWriterBase, None] = state_writer or StdOutStateWriter()
self._pending_state_messages: dict[str, list[AirbyteStateMessage]] = defaultdict(list, {})
self._finalized_state_messages: dict[
str,
list[AirbyteStateMessage],
] = defaultdict(list, {})
self._setup()
@property
def expected_streams(self) -> set[str]:
"""Return the expected stream names."""
return set(self.catalog_provider.stream_names)
@property
def catalog_provider(
self,
) -> CatalogProvider:
"""Return the catalog manager.
Subclasses should set this property to a valid catalog manager instance if one
is not explicitly passed to the constructor.
Raises:
PyAirbyteInternalError: If the catalog manager is not set.
"""
if not self._catalog_provider:
raise exc.PyAirbyteInternalError(
message="Catalog manager should exist but does not.",
)
return self._catalog_provider
@property
def state_writer(
self,
) -> StateWriterBase:
"""Return the state writer instance.
Subclasses should set this property to a valid state manager instance if one
is not explicitly passed to the constructor.
Raises:
PyAirbyteInternalError: If the state manager is not set.
"""
if not self._state_writer:
raise exc.PyAirbyteInternalError(
message="State manager should exist but does not.",
)
return self._state_writer
@final
def process_stdin(
self,
*,
write_strategy: WriteStrategy = WriteStrategy.AUTO,
) -> None:
"""Process the input stream from stdin.
Return a list of summaries for testing.
"""
input_stream = io.TextIOWrapper(sys.stdin.buffer, encoding="utf-8")
self.process_input_stream(
input_stream,
write_strategy=write_strategy,
)
@final
def _airbyte_messages_from_buffer(
self,
buffer: io.TextIOBase,
) -> Iterator[AirbyteMessage]:
"""Yield messages from a buffer."""
yield from (AirbyteMessage.parse_raw(line) for line in buffer)
@final
def process_input_stream(
self,
input_stream: io.TextIOBase,
*,
write_strategy: WriteStrategy = WriteStrategy.AUTO,
) -> None:
"""Parse the input stream and process data in batches.
Return a list of summaries for testing.
"""
messages = self._airbyte_messages_from_buffer(input_stream)
self.process_airbyte_messages(
messages,
write_strategy=write_strategy,
)
@abc.abstractmethod
def process_record_message(
self,
record_msg: AirbyteRecordMessage,
stream_schema: dict,
) -> None:
"""Write a record.
This method is called for each record message.
In most cases, the SQL processor will not perform any action, but will pass this along to to
the file processor.
"""
def process_airbyte_messages_as_generator(
self,
messages: Iterable[AirbyteMessage],
*,
write_strategy: WriteStrategy,
) -> Iterable[AirbyteMessage]:
"""
This is copied from PyAirbyte's RecordProcessorBase class.
This version will _also_ yield `AirbyteMessage` objects which would otherwise only
be pushed to STDOUT or sent to the state writer.
TODO:
- In the future, we should optimize this to emit STATE messages as soon as their records
are committed, rather than waiting until the end of the input stream.
"""
# Create a queue to store output messages
output_queue: queue.Queue[AirbyteMessage] = queue.Queue()
class WrappedStateWriter(StateWriterBase):
def __init__(self, wrapped: Union[StateWriterBase, None]) -> None:
self.wrapped = wrapped
super().__init__()
def write_state(self, state_message: AirbyteStateMessage) -> None:
"""First add state message to output queue, then call the wrapped state writer."""
output_queue.put(state_message)
if self.wrapped:
return self.wrapped.write_state(state_message)
self._state_writer = WrappedStateWriter(self._state_writer)
self.process_airbyte_messages(
messages=messages,
write_strategy=write_strategy,
)
# Restore the original state writer
self._state_writer = self._state_writer.wrapped
# Yield all messages from the output queue
yield from output_queue.queue
@final
def process_airbyte_messages(
self,
messages: Iterable[AirbyteMessage],
*,
write_strategy: WriteStrategy,
) -> None:
"""Process a stream of Airbyte messages."""
if not isinstance(write_strategy, WriteStrategy):
raise exc.AirbyteInternalError(
message="Invalid `write_strategy` argument. Expected instance of WriteStrategy.",
context={"write_strategy": write_strategy},
)
stream_schemas: dict[str, dict] = {}
# Process messages, writing to batches as we go
for message in messages:
if message.type is Type.RECORD:
record_msg = cast(AirbyteRecordMessage, message.record)
stream_name = record_msg.stream
if stream_name not in stream_schemas:
stream_schemas[stream_name] = self.catalog_provider.get_stream_json_schema(
stream_name=stream_name
)
self.process_record_message(
record_msg,
stream_schema=stream_schemas[stream_name],
)
elif message.type is Type.STATE:
state_msg = cast(AirbyteStateMessage, message.state)
if state_msg.type in {AirbyteStateType.GLOBAL, AirbyteStateType.LEGACY}:
self._pending_state_messages[f"_{state_msg.type}"].append(state_msg)
elif state_msg.type is AirbyteStateType.STREAM:
stream_state = cast(AirbyteStreamState, state_msg.stream)
stream_name = stream_state.stream_descriptor.name
self._pending_state_messages[stream_name].append(state_msg)
else:
warnings.warn(
f"Unexpected state message type. State message was: {state_msg}",
stacklevel=2,
)
self._pending_state_messages[f"_{state_msg.type}"].append(state_msg)
else:
# Ignore unexpected or unhandled message types:
# Type.LOG, Type.TRACE, Type.CONTROL, etc.
pass
# We've finished processing input data.
# Finalize all received records and state messages:
self.write_all_stream_data(
write_strategy=write_strategy,
)
self.cleanup_all()
def write_all_stream_data(self, write_strategy: WriteStrategy) -> None:
"""Finalize any pending writes."""
for stream_name in self.catalog_provider.stream_names:
self.write_stream_data(
stream_name,
write_strategy=write_strategy,
)
@abc.abstractmethod
def write_stream_data(
self,
stream_name: str,
write_strategy: WriteStrategy,
) -> list[BatchHandle]:
"""Write pending stream data to the cache."""
...
def _finalize_state_messages(
self,
state_messages: list[AirbyteStateMessage],
) -> None:
"""Handle state messages by passing them to the catalog manager."""
if state_messages:
self.state_writer.write_state(
state_message=state_messages[-1],
)
def _setup(self) -> None: # noqa: B027 # Intentionally empty, not abstract
"""Create the database.
By default this is a no-op but subclasses can override this method to prepare
any necessary resources.
"""
pass
def cleanup_all(self) -> None: # noqa: B027 # Intentionally empty, not abstract
"""Clean up all resources.
The default implementation is a no-op.
"""
pass

View File

@ -0,0 +1,993 @@
# Copyright (c) 2023 Airbyte, Inc., all rights reserved.
"""The base SQL Cache implementation."""
from __future__ import annotations
import abc
import contextlib
import enum
import hashlib
import logging
from contextlib import contextmanager
from functools import cached_property
from pathlib import Path
from typing import TYPE_CHECKING, Optional, Union, cast, final
import pandas as pd
import sqlalchemy
import ulid
from airbyte import exceptions as exc
from airbyte._util.name_normalizers import LowerCaseNormalizer
from airbyte.constants import AB_EXTRACTED_AT_COLUMN, AB_META_COLUMN, AB_RAW_ID_COLUMN, DEBUG_MODE
from airbyte.progress import progress
from airbyte.strategies import WriteStrategy
from airbyte.types import SQLTypeConverter
from airbyte_cdk.models.airbyte_protocol import DestinationSyncMode
from pandas import Index
from pydantic import BaseModel
from sqlalchemy import Column, Table, and_, create_engine, insert, null, select, text, update
from sqlalchemy.sql.elements import TextClause
logger = logging.getLogger(__name__)
from destination_opengauss_datavec.common.destinations.record_processor import RecordProcessorBase
from destination_opengauss_datavec.common.state.state_writers import StdOutStateWriter
if TYPE_CHECKING:
from collections.abc import Generator
from airbyte._batch_handles import BatchHandle
from airbyte._processors.file.base import FileWriterBase
from airbyte.secrets.base import SecretString
from airbyte_cdk.models import AirbyteRecordMessage, AirbyteStateMessage
from sqlalchemy.engine import Connection, Engine
from sqlalchemy.engine.cursor import CursorResult
from sqlalchemy.engine.reflection import Inspector
from sqlalchemy.sql.base import Executable
from sqlalchemy.sql.type_api import TypeEngine
from destination_opengauss_datavec.common.catalog.catalog_providers import CatalogProvider
from destination_opengauss_datavec.common.state.state_writers import StateWriterBase
class RecordDedupeMode(enum.Enum):
APPEND = "append"
REPLACE = "replace"
class SQLRuntimeError(Exception):
"""Raised when an SQL operation fails."""
class SqlConfig(BaseModel, abc.ABC):
"""Common configuration for SQL connections."""
schema_name: str
"""The name of the schema to write to."""
table_prefix: Optional[str] = ""
"""A prefix to add to created table names."""
@abc.abstractmethod
def get_sql_alchemy_url(self) -> SecretString:
"""Returns a SQL Alchemy URL."""
...
@abc.abstractmethod
def get_database_name(self) -> str:
"""Return the name of the database."""
...
def connect(self) -> None:
"""Attempt to connect, and raise `AirbyteConnectionError` if the connection fails."""
engine = self.get_sql_engine()
try:
connection = engine.connect()
connection.close()
except Exception as ex:
raise exc.AirbyteConnectionError(
message="Could not connect to the database.",
guidance="Check the connection settings and try again.",
) from ex
def get_sql_engine(self) -> Engine:
"""Return a new SQL engine to use."""
return create_engine(
url=self.get_sql_alchemy_url(),
echo=DEBUG_MODE,
execution_options={
"schema_translate_map": {None: self.schema_name},
},
)
def get_vendor_client(self) -> object:
"""Return the vendor-specific client object.
This is used for vendor-specific operations.
Raises `NotImplementedError` if a custom vendor client is not defined.
"""
raise NotImplementedError(
f"The type '{type(self).__name__}' does not define a custom client."
)
class SqlProcessorBase(RecordProcessorBase):
"""A base class to be used for SQL Caches."""
type_converter_class: type[SQLTypeConverter] = SQLTypeConverter
"""The type converter class to use for converting JSON schema types to SQL types."""
normalizer = LowerCaseNormalizer
"""The name normalizer to user for table and column name normalization."""
file_writer_class: type[FileWriterBase]
"""The file writer class to use for writing files to the cache."""
supports_merge_insert = False
"""True if the database supports the MERGE INTO syntax."""
# Constructor:
def __init__(
self,
*,
sql_config: SqlConfig,
catalog_provider: CatalogProvider,
state_writer: Union[StateWriterBase, None] = None,
file_writer: Union[FileWriterBase, None] = None,
temp_dir: Union[Path, None] = None,
temp_file_cleanup: bool,
) -> None:
if not temp_dir and not file_writer:
raise exc.PyAirbyteInternalError(
message="Either `temp_dir` or `file_writer` must be provided.",
)
state_writer = state_writer or StdOutStateWriter()
self._sql_config: SqlConfig = sql_config
super().__init__(
state_writer=state_writer,
catalog_provider=catalog_provider,
)
self.file_writer = file_writer or self.file_writer_class(
cache_dir=cast(Path, temp_dir),
cleanup=temp_file_cleanup,
)
self.type_converter = self.type_converter_class()
self._cached_table_definitions: dict[str, sqlalchemy.Table] = {}
self._ensure_schema_exists()
# Public interface:
@property
def sql_config(self) -> SqlConfig:
return self._sql_config
def get_sql_alchemy_url(self) -> SecretString:
"""Return the SQLAlchemy URL to use."""
return self.sql_config.get_sql_alchemy_url()
@final
@cached_property
def database_name(self) -> str:
"""Return the name of the database."""
return self.sql_config.get_database_name()
@final
def get_sql_engine(self) -> Engine:
"""Return a new SQL engine to use."""
return self.sql_config.get_sql_engine()
@contextmanager
def get_sql_connection(self) -> Generator[sqlalchemy.engine.Connection, None, None]:
"""A context manager which returns a new SQL connection for running queries.
If the connection needs to close, it will be closed automatically.
"""
with self.get_sql_engine().begin() as connection:
self._init_connection_settings(connection)
yield connection
connection.close()
del connection
def get_sql_table_name(
self,
stream_name: str,
) -> str:
"""Return the name of the SQL table for the given stream."""
table_prefix = self.sql_config.table_prefix
# TODO: Add default prefix based on the source name.
long_name = f"{table_prefix}{stream_name}"
normalized_name = self.normalizer.normalize(long_name)
if len(normalized_name) <= 63:
return normalized_name
# The max length for an identifier in postgres is 63 characters.
# We use a part of the stream name for readability, and a hash for uniqueness to avoid collisions.
hash_suffix = hashlib.md5(normalized_name.encode("utf-8")).hexdigest()
# 63 characters max, 1 for the underscore, 32 for the hash
truncate_limit = 63 - 1 - 32
truncated_name = normalized_name[:truncate_limit]
short_name = f"{truncated_name}_{hash_suffix}"
logger.warning(
f"Table name '{normalized_name}' is too long ({len(normalized_name)} > 63). "
f"Truncating to '{short_name}'."
)
return short_name
@final
def get_sql_table(
self,
stream_name: str,
) -> sqlalchemy.Table:
"""Return the main table object for the stream."""
return self._get_table_by_name(
self.get_sql_table_name(stream_name),
)
# Record processing:
def process_record_message(
self,
record_msg: AirbyteRecordMessage,
stream_schema: dict,
) -> None:
"""Write a record to the cache.
This method is called for each record message, before the batch is written.
In most cases, the SQL processor will not perform any action, but will pass this along to to
the file processor.
"""
self.file_writer.process_record_message(
record_msg,
stream_schema=stream_schema,
)
# Protected members (non-public interface):
def _init_connection_settings(self, connection: Connection) -> None:
"""This is called automatically whenever a new connection is created.
By default this is a no-op. Subclasses can use this to set connection settings, such as
timezone, case-sensitivity settings, and other session-level variables.
"""
pass
def _invalidate_table_cache(
self,
table_name: str,
) -> None:
"""Invalidate the the named table cache.
This should be called whenever the table schema is known to have changed.
"""
if table_name in self._cached_table_definitions:
del self._cached_table_definitions[table_name]
def _get_table_by_name(
self,
table_name: str,
*,
force_refresh: bool = False,
shallow_okay: bool = False,
) -> sqlalchemy.Table:
"""Return a table object from a table name.
If 'shallow_okay' is True, the table will be returned without requiring properties to
be read from the database.
To prevent unnecessary round-trips to the database, the table is cached after the first
query. To ignore the cache and force a refresh, set 'force_refresh' to True.
"""
if force_refresh and shallow_okay:
raise exc.PyAirbyteInternalError(
message="Cannot force refresh and use shallow query at the same time."
)
if force_refresh and table_name in self._cached_table_definitions:
self._invalidate_table_cache(table_name)
if table_name not in self._cached_table_definitions:
if shallow_okay:
# Return a shallow instance, without column declarations. Do not cache
# the table definition in this case.
return sqlalchemy.Table(
table_name,
sqlalchemy.MetaData(schema=self.sql_config.schema_name),
)
self._cached_table_definitions[table_name] = sqlalchemy.Table(
table_name,
sqlalchemy.MetaData(schema=self.sql_config.schema_name),
autoload_with=self.get_sql_engine(),
)
return self._cached_table_definitions[table_name]
def _ensure_schema_exists(
self,
) -> None:
"""Return a new (unique) temporary table name."""
schema_name = self.sql_config.schema_name
if schema_name in self._get_schemas_list():
return
sql = f"CREATE SCHEMA IF NOT EXISTS {schema_name}"
try:
self._execute_sql(sql)
except Exception as ex:
# Ignore schema exists errors.
if "already exists" not in str(ex):
raise
if DEBUG_MODE:
found_schemas = self._get_schemas_list()
assert schema_name in found_schemas, (
f"Schema {schema_name} was not created. Found: {found_schemas}"
)
def _quote_identifier(self, identifier: str) -> str:
"""Return the given identifier, quoted."""
return f'"{identifier}"'
@final
def _get_temp_table_name(
self,
stream_name: str,
batch_id: Union[str, None] = None, # ULID of the batch
) -> str:
"""Return a new (unique) temporary table name."""
batch_id = batch_id or str(ulid.ULID())
long_name = f"{stream_name}_{batch_id}"
normalized_name = self.normalizer.normalize(long_name)
if len(normalized_name) <= 63:
return normalized_name
# The max length for an identifier in postgres is 63 characters.
# We use a part of the stream name for readability, and a hash for uniqueness to avoid collisions.
hash_suffix = hashlib.md5(normalized_name.encode("utf-8")).hexdigest()
# 63 characters max, 1 for the underscore, 32 for the hash
truncate_limit = 63 - 1 - 32
truncated_name = normalized_name[:truncate_limit]
return f"{truncated_name}_{hash_suffix}"
def _fully_qualified(
self,
table_name: str,
) -> str:
"""Return the fully qualified name of the given table."""
return f"{self.sql_config.schema_name}.{self._quote_identifier(table_name)}"
@final
def _create_table_for_loading(
self,
/,
stream_name: str,
batch_id: str,
) -> str:
"""Create a new table for loading data."""
temp_table_name = self._get_temp_table_name(stream_name, batch_id)
column_definition_str = ",\n ".join(
f"{self._quote_identifier(column_name)} {sql_type}"
for column_name, sql_type in self._get_sql_column_definitions(stream_name).items()
)
self._create_table(temp_table_name, column_definition_str)
return temp_table_name
def _get_tables_list(
self,
) -> list[str]:
"""Return a list of all tables in the database."""
with self.get_sql_connection() as conn:
inspector: Inspector = sqlalchemy.inspect(conn)
return inspector.get_table_names(schema=self.sql_config.schema_name)
def _get_schemas_list(
self,
database_name: Union[str, None] = None,
) -> list[str]:
"""Return a list of all tables in the database."""
inspector: Inspector = sqlalchemy.inspect(self.get_sql_engine())
database_name = database_name or self.database_name
found_schemas = inspector.get_schema_names()
return [
found_schema.split(".")[-1].strip('"')
for found_schema in found_schemas
if "." not in found_schema
or (found_schema.split(".")[0].lower().strip('"') == database_name.lower())
]
def _ensure_final_table_exists(
self,
stream_name: str,
*,
create_if_missing: bool = True,
) -> str:
"""Create the final table if it doesn't already exist.
Return the table name.
"""
table_name = self.get_sql_table_name(stream_name)
did_exist = self._table_exists(table_name)
if not did_exist and create_if_missing:
column_definition_str = ",\n ".join(
f"{self._quote_identifier(column_name)} {sql_type}"
for column_name, sql_type in self._get_sql_column_definitions(
stream_name,
).items()
)
self._create_table(table_name, column_definition_str)
return table_name
def _ensure_compatible_table_schema(
self,
stream_name: str,
table_name: str,
) -> None:
"""Return true if the given table is compatible with the stream's schema.
Raises an exception if the table schema is not compatible with the schema of the
input stream.
TODO:
- Expand this to check for column types and sizes.
"""
self._add_missing_columns_to_table(
stream_name=stream_name,
table_name=table_name,
)
@final
def _create_table(
self,
table_name: str,
column_definition_str: str,
primary_keys: Union[list[str], None] = None,
) -> None:
if primary_keys:
pk_str = ", ".join(primary_keys)
column_definition_str += f",\n PRIMARY KEY ({pk_str})"
cmd = f"""
CREATE TABLE {self._fully_qualified(table_name)} (
{column_definition_str}
)
"""
_ = self._execute_sql(cmd)
def _get_sql_column_definitions(
self,
stream_name: str,
) -> dict[str, sqlalchemy.types.TypeEngine]:
"""Return the column definitions for the given stream."""
columns: dict[str, sqlalchemy.types.TypeEngine] = {}
properties = self.catalog_provider.get_stream_properties(stream_name)
for property_name, json_schema_property_def in properties.items():
clean_prop_name = self.normalizer.normalize(property_name)
columns[clean_prop_name] = self.type_converter.to_sql_type(
json_schema_property_def,
)
columns[AB_RAW_ID_COLUMN] = self.type_converter_class.get_string_type()
columns[AB_EXTRACTED_AT_COLUMN] = sqlalchemy.TIMESTAMP()
columns[AB_META_COLUMN] = self.type_converter_class.get_json_type()
return columns
@final
def write_stream_data(
self,
stream_name: str,
write_strategy: WriteStrategy,
) -> list[BatchHandle]:
"""Finalize all uncommitted batches.
This is a generic 'final' SQL implementation, which should not be overridden.
Returns a mapping of batch IDs to batch handles, for those processed batches.
TODO: Add a dedupe step here to remove duplicates from the temp table.
Some sources will send us duplicate records within the same stream,
although this is a fairly rare edge case we can ignore in V1.
"""
# Flush any pending writes
self.file_writer.flush_active_batches()
with self.finalizing_batches(stream_name) as batches_to_finalize:
# Make sure the target schema and target table exist.
self._ensure_schema_exists()
final_table_name = self._ensure_final_table_exists(
stream_name,
create_if_missing=True,
)
if not batches_to_finalize:
# If there are no batches to finalize, return after ensuring the table exists.
return []
files: list[Path] = []
# Get a list of all files to finalize from all pending batches.
for batch_handle in batches_to_finalize:
files += batch_handle.files
# Use the max batch ID as the batch ID for table names.
max_batch_id = max(batch.batch_id for batch in batches_to_finalize)
temp_table_name = self._write_files_to_new_table(
files=files,
stream_name=stream_name,
batch_id=max_batch_id,
)
try:
self._write_temp_table_to_final_table(
stream_name=stream_name,
temp_table_name=temp_table_name,
final_table_name=final_table_name,
write_strategy=write_strategy,
)
finally:
self._drop_temp_table(temp_table_name, if_exists=True)
progress.log_stream_finalized(stream_name)
# Return the batch handles as measure of work completed.
return batches_to_finalize
@final
def cleanup_all(self) -> None:
"""Clean resources."""
self.file_writer.cleanup_all()
# Finalizing context manager
@final
@contextlib.contextmanager
def finalizing_batches(
self,
stream_name: str,
) -> Generator[list[BatchHandle], str, None]:
"""Context manager to use for finalizing batches, if applicable.
Returns a mapping of batch IDs to batch handles, for those processed batches.
"""
batches_to_finalize: list[BatchHandle] = self.file_writer.get_pending_batches(stream_name)
state_messages_to_finalize: list[AirbyteStateMessage] = self._pending_state_messages[
stream_name
].copy()
self._pending_state_messages[stream_name].clear()
progress.log_batches_finalizing(stream_name, len(batches_to_finalize))
yield batches_to_finalize
self._finalize_state_messages(state_messages_to_finalize)
progress.log_batches_finalized(stream_name, len(batches_to_finalize))
for batch_handle in batches_to_finalize:
batch_handle.finalized = True
self._finalized_state_messages[stream_name] += state_messages_to_finalize
def _execute_sql(self, sql: Union[str, TextClause, Executable]) -> CursorResult:
"""Execute the given SQL statement."""
if isinstance(sql, str):
sql = text(sql)
if isinstance(sql, TextClause):
sql = sql.execution_options(
autocommit=True,
)
with self.get_sql_connection() as conn:
try:
result = conn.execute(sql)
except (
sqlalchemy.exc.ProgrammingError,
sqlalchemy.exc.SQLAlchemyError,
) as ex:
msg = f"Error when executing SQL:\n{sql}\n{type(ex).__name__}{ex!s}"
raise SQLRuntimeError(msg) from None # from ex
return result
def _drop_temp_table(
self,
table_name: str,
*,
if_exists: bool = True,
) -> None:
"""Drop the given table."""
exists_str = "IF EXISTS" if if_exists else ""
self._execute_sql(f"DROP TABLE {exists_str} {self._fully_qualified(table_name)}")
def _write_files_to_new_table(
self,
files: list[Path],
stream_name: str,
batch_id: str,
) -> str:
"""Write a file(s) to a new table.
This is a generic implementation, which can be overridden by subclasses
to improve performance.
"""
temp_table_name = self._create_table_for_loading(stream_name, batch_id)
for file_path in files:
dataframe = pd.read_json(file_path, lines=True)
sql_column_definitions: dict[str, TypeEngine] = self._get_sql_column_definitions(
stream_name
)
# Remove fields that are not in the schema
for col_name in dataframe.columns:
if col_name not in sql_column_definitions:
dataframe = dataframe.drop(columns=col_name)
# Pandas will auto-create the table if it doesn't exist, which we don't want.
if not self._table_exists(temp_table_name):
raise exc.PyAirbyteInternalError(
message="Table does not exist after creation.",
context={
"temp_table_name": temp_table_name,
},
)
# Normalize all column names to lower case.
dataframe.columns = Index([self.normalizer.normalize(col) for col in dataframe.columns])
# Write the data to the table.
dataframe.to_sql(
temp_table_name,
self.get_sql_alchemy_url(),
schema=self.sql_config.schema_name,
if_exists="append",
index=False,
dtype=sql_column_definitions,
)
return temp_table_name
def _add_column_to_table(
self,
table: Table,
column_name: str,
column_type: sqlalchemy.types.TypeEngine,
) -> None:
"""Add a column to the given table."""
self._execute_sql(
text(
f"ALTER TABLE {self._fully_qualified(table.name)} "
f"ADD COLUMN {column_name} {column_type}"
),
)
def _add_missing_columns_to_table(
self,
stream_name: str,
table_name: str,
) -> None:
"""Add missing columns to the table.
This is a no-op if all columns are already present.
"""
columns = self._get_sql_column_definitions(stream_name)
# First check without forcing a refresh of the cache (faster). If nothing is missing,
# then we're done.
table = self._get_table_by_name(
table_name,
force_refresh=False,
)
missing_columns: bool = any(column_name not in table.columns for column_name in columns)
if missing_columns:
# If we found missing columns, refresh the cache and then take action on anything
# that's still confirmed missing.
columns_added = False
table = self._get_table_by_name(
table_name,
force_refresh=True,
)
for column_name, column_type in columns.items():
if column_name not in table.columns:
self._add_column_to_table(table, column_name, column_type)
columns_added = True
if columns_added:
# We've added columns, so invalidate the cache.
self._invalidate_table_cache(table_name)
@final
def _write_temp_table_to_final_table(
self,
stream_name: str,
temp_table_name: str,
final_table_name: str,
write_strategy: WriteStrategy,
) -> None:
"""Write the temp table into the final table using the provided write strategy."""
has_pks: bool = bool(self._get_primary_keys(stream_name))
has_incremental_key: bool = bool(self._get_incremental_key(stream_name))
if write_strategy == WriteStrategy.MERGE and not has_pks:
raise exc.PyAirbyteInputError(
message="Cannot use merge strategy on a stream with no primary keys.",
context={
"stream_name": stream_name,
},
)
if write_strategy == WriteStrategy.AUTO:
configured_destination_sync_mode: DestinationSyncMode = (
self.catalog_provider.get_destination_sync_mode(stream_name)
)
if configured_destination_sync_mode == DestinationSyncMode.overwrite:
write_strategy = WriteStrategy.REPLACE
elif configured_destination_sync_mode == DestinationSyncMode.append:
write_strategy = WriteStrategy.APPEND
elif configured_destination_sync_mode == DestinationSyncMode.append_dedup:
write_strategy = WriteStrategy.MERGE
# TODO: Consider removing the rest of these cases if they are dead code.
elif has_pks:
write_strategy = WriteStrategy.MERGE
elif has_incremental_key:
write_strategy = WriteStrategy.APPEND
else:
write_strategy = WriteStrategy.REPLACE
if write_strategy == WriteStrategy.REPLACE:
# Note: No need to check for schema compatibility
# here, because we are fully replacing the table.
self._swap_temp_table_with_final_table(
stream_name=stream_name,
temp_table_name=temp_table_name,
final_table_name=final_table_name,
)
return
if write_strategy == WriteStrategy.APPEND:
self._ensure_compatible_table_schema(
stream_name=stream_name,
table_name=final_table_name,
)
self._append_temp_table_to_final_table(
stream_name=stream_name,
temp_table_name=temp_table_name,
final_table_name=final_table_name,
)
return
if write_strategy == WriteStrategy.MERGE:
self._ensure_compatible_table_schema(
stream_name=stream_name,
table_name=final_table_name,
)
if not self.supports_merge_insert:
# Fallback to emulated merge if the database does not support merge natively.
self._emulated_merge_temp_table_to_final_table(
stream_name=stream_name,
temp_table_name=temp_table_name,
final_table_name=final_table_name,
)
return
self._merge_temp_table_to_final_table(
stream_name=stream_name,
temp_table_name=temp_table_name,
final_table_name=final_table_name,
)
return
raise exc.PyAirbyteInternalError(
message="Write strategy is not supported.",
context={
"write_strategy": write_strategy,
},
)
def _append_temp_table_to_final_table(
self,
temp_table_name: str,
final_table_name: str,
stream_name: str,
) -> None:
nl = "\n"
columns = [self._quote_identifier(c) for c in self._get_sql_column_definitions(stream_name)]
self._execute_sql(
f"""
INSERT INTO {self._fully_qualified(final_table_name)} (
{f",{nl} ".join(columns)}
)
SELECT
{f",{nl} ".join(columns)}
FROM {self._fully_qualified(temp_table_name)}
""",
)
def _get_primary_keys(
self,
stream_name: str,
) -> list[str]:
pks = self.catalog_provider.get_configured_stream_info(stream_name).primary_key
if not pks:
return []
joined_pks = [".".join(pk) for pk in pks]
for pk in joined_pks:
if "." in pk:
msg = f"Nested primary keys are not yet supported. Found: {pk}"
raise NotImplementedError(msg)
return joined_pks
def _get_incremental_key(
self,
stream_name: str,
) -> Union[str, None]:
return self.catalog_provider.get_configured_stream_info(stream_name).cursor_field
def _swap_temp_table_with_final_table(
self,
stream_name: str,
temp_table_name: str,
final_table_name: str,
) -> None:
"""Merge the temp table into the main one.
This implementation requires MERGE support in the SQL DB.
Databases that do not support this syntax can override this method.
"""
if final_table_name is None:
raise exc.PyAirbyteInternalError(message="Arg 'final_table_name' cannot be None.")
if temp_table_name is None:
raise exc.PyAirbyteInternalError(message="Arg 'temp_table_name' cannot be None.")
_ = stream_name
deletion_name = f"{final_table_name}_deleteme"
commands = "\n".join([
f"ALTER TABLE {self._fully_qualified(final_table_name)} RENAME TO {deletion_name};",
f"ALTER TABLE {self._fully_qualified(temp_table_name)} RENAME TO {final_table_name};",
f"DROP TABLE {self._fully_qualified(deletion_name)};",
])
self._execute_sql(commands)
def _merge_temp_table_to_final_table(
self,
stream_name: str,
temp_table_name: str,
final_table_name: str,
) -> None:
"""Merge the temp table into the main one.
This implementation requires MERGE support in the SQL DB.
Databases that do not support this syntax can override this method.
"""
nl = "\n"
columns = {self._quote_identifier(c) for c in self._get_sql_column_definitions(stream_name)}
pk_columns = {self._quote_identifier(c) for c in self._get_primary_keys(stream_name)}
non_pk_columns = columns - pk_columns
join_clause = f"{nl} AND ".join(f"tmp.{pk_col} = final.{pk_col}" for pk_col in pk_columns)
set_clause = f"{nl} , ".join(f"{col} = tmp.{col}" for col in non_pk_columns)
self._execute_sql(
f"""
MERGE INTO {self._fully_qualified(final_table_name)} final
USING (
SELECT *
FROM {self._fully_qualified(temp_table_name)}
) AS tmp
ON {join_clause}
WHEN MATCHED THEN UPDATE
SET
{set_clause}
WHEN NOT MATCHED THEN INSERT
(
{f",{nl} ".join(columns)}
)
VALUES (
tmp.{f",{nl} tmp.".join(columns)}
);
""",
)
def _get_column_by_name(self, table: Union[str, Table], column_name: str) -> Column:
"""Return the column object for the given column name.
This method is case-insensitive.
"""
if isinstance(table, str):
table = self._get_table_by_name(table)
try:
# Try to get the column in a case-insensitive manner
return next(col for col in table.c if col.name.lower() == column_name.lower())
except StopIteration:
raise exc.PyAirbyteInternalError(
message="Could not find matching column.",
context={
"table": table,
"column_name": column_name,
},
) from None
def _emulated_merge_temp_table_to_final_table(
self,
stream_name: str,
temp_table_name: str,
final_table_name: str,
) -> None:
"""Emulate the merge operation using a series of SQL commands.
This is a fallback implementation for databases that do not support MERGE.
"""
final_table = self._get_table_by_name(final_table_name)
temp_table = self._get_table_by_name(temp_table_name)
pk_columns = self._get_primary_keys(stream_name)
columns_to_update: set[str] = self._get_sql_column_definitions(
stream_name=stream_name
).keys() - set(pk_columns)
# Create a dictionary mapping columns in users_final to users_stage for updating
update_values = {
self._get_column_by_name(final_table, column): (
self._get_column_by_name(temp_table, column)
)
for column in columns_to_update
}
# Craft the WHERE clause for composite primary keys
join_conditions = [
self._get_column_by_name(final_table, pk_column)
== self._get_column_by_name(temp_table, pk_column)
for pk_column in pk_columns
]
join_clause = and_(*join_conditions)
# Craft the UPDATE statement
update_stmt = update(final_table).values(update_values).where(join_clause)
# Define a join between temp_table and final_table
joined_table = temp_table.outerjoin(final_table, join_clause)
# Define a condition that checks for records in temp_table that do not have a corresponding
# record in final_table
where_not_exists_clause = self._get_column_by_name(final_table, pk_columns[0]) == null()
# Select records from temp_table that are not in final_table
select_new_records_stmt = (
select([temp_table]).select_from(joined_table).where(where_not_exists_clause)
)
# Craft the INSERT statement using the select statement
insert_new_records_stmt = insert(final_table).from_select(
names=[column.name for column in temp_table.columns], select=select_new_records_stmt
)
if DEBUG_MODE:
print(str(update_stmt))
print(str(insert_new_records_stmt))
with self.get_sql_connection() as conn:
conn.execute(update_stmt)
conn.execute(insert_new_records_stmt)
def _table_exists(
self,
table_name: str,
) -> bool:
"""Return true if the given table exists.
Subclasses may override this method to provide a more efficient implementation.
"""
return table_name in self._get_tables_list()

View File

@ -0,0 +1,38 @@
# Copyright (c) 2023 Airbyte, Inc., all rights reserved.
"""State writer implementation."""
from __future__ import annotations
import abc
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from airbyte_cdk.models.airbyte_protocol import AirbyteStateMessage
class StateWriterBase(abc.ABC):
"""A class to write state artifacts."""
@abc.abstractmethod
def write_state(
self,
state_message: AirbyteStateMessage,
) -> None:
"""Save or 'write' a state artifact."""
...
class StdOutStateWriter(StateWriterBase):
"""A state writer that writes state artifacts to stdout.
This is required when we are functioning as a "Destination" in the Airbyte protocol, and
an orchestrator is responsible for saving those state artifacts.
"""
def write_state(
self,
state_message: AirbyteStateMessage,
) -> None:
"""Save or 'write' a state artifact."""
print(state_message.json())

View File

@ -0,0 +1,61 @@
#
# Copyright (c) 2023 Airbyte, Inc., all rights reserved.
#
from airbyte_cdk.destinations.vector_db_based.config import VectorDBConfigModel
from pydantic import BaseModel, Field
class OpenGaussDatavecIndexingModel(BaseModel):
host: str = Field(
...,
title="Host",
order=1,
description="Hostname of the database.",
)
database: str = Field(
...,
title="Database",
order=2,
description="Enter the name of the database that you want to sync data into",
examples=["AIRBYTE_DATABASE"],
)
username: str = Field(
...,
title="Username",
order=3,
description="Enter the name of the user you want to use to access the database",
examples=["root"],
)
password: str = Field(
...,
title="Password",
order=4,
description="Enter the password of the user you want to use to access the database",
)
port: int = Field(
default=5432,
title="Port",
description="Enter the port you want to use to access the database",
examples=["5432"],
)
default_schema: str = Field(
default="public",
title="Default Schema",
order=6,
description="Enter the name of the default schema",
examples=["AIRBYTE_SCHEMA"],
)
class Config:
title = "Indexing"
schema_extra = {
"description": "Indexing configuration",
"group": "indexing",
}
class ConfigModel(VectorDBConfigModel):
indexing: OpenGaussDatavecIndexingModel

View File

@ -0,0 +1,84 @@
#
# Copyright (c) 2023 Airbyte, Inc., all rights reserved.
#
import tempfile
from logging import Logger
from pathlib import Path
from typing import Any, Iterable, Mapping, Optional
from airbyte.secrets import SecretString
from airbyte.strategies import WriteStrategy
from airbyte_cdk.destinations import Destination
from airbyte_cdk.models import (
AirbyteConnectionStatus,
AirbyteMessage,
ConfiguredAirbyteCatalog,
ConnectorSpecification,
DestinationSyncMode,
Status,
)
from destination_opengauss_datavec import opengauss_processor
from destination_opengauss_datavec.config import ConfigModel
from destination_opengauss_datavec.common.catalog.catalog_providers import CatalogProvider
BATCH_SIZE = 150
class DestinationOpenGaussDataVec(Destination):
sql_processor: opengauss_processor.OpenGaussDataVecProcessor
def _init_sql_processor(
self, config: ConfigModel, configured_catalog: Optional[ConfiguredAirbyteCatalog] = None
) -> None:
self.sql_processor = opengauss_processor.OpenGaussDataVecProcessor(
sql_config=opengauss_processor.OpenGaussConfig(
host=config.indexing.host,
port=config.indexing.port,
database=config.indexing.database,
schema_name=config.indexing.default_schema,
username=config.indexing.username,
password=SecretString(config.indexing.password),
),
splitter_config=config.processing,
embedder_config=config.embedding, # type: ignore [arg-type]
catalog_provider=CatalogProvider(configured_catalog),
temp_dir=Path(tempfile.mkdtemp()),
temp_file_cleanup=True,
)
def write(
self,
config: Mapping[str, Any],
configured_catalog: ConfiguredAirbyteCatalog,
input_messages: Iterable[AirbyteMessage],
) -> Iterable[AirbyteMessage]:
parsed_config = ConfigModel.parse_obj(config)
self._init_sql_processor(config=parsed_config, configured_catalog=configured_catalog)
yield from self.sql_processor.process_airbyte_messages_as_generator(
messages=input_messages,
write_strategy=WriteStrategy.AUTO,
)
def check(self, logger: Logger, config: Mapping[str, Any]) -> AirbyteConnectionStatus:
_ = logger # Unused
try:
parsed_config = ConfigModel.parse_obj(config)
self._init_sql_processor(config=parsed_config)
self.sql_processor.sql_config.get_sql_engine().connect()
return AirbyteConnectionStatus(status=Status.SUCCEEDED)
except Exception as e:
return AirbyteConnectionStatus(
status=Status.FAILED, message=f"An exception occurred: {repr(e)}"
)
def spec(self, *args: Any, **kwargs: Any) -> ConnectorSpecification:
return ConnectorSpecification(
documentationUrl="https://docs.airbyte.com/integrations/destinations/opengauss-datavec",
supportsIncremental=True,
supported_destination_sync_modes=[DestinationSyncMode.overwrite, DestinationSyncMode.append, DestinationSyncMode.append_dedup],
connectionSpecification=ConfigModel.schema(), # type: ignore[attr-defined]
)

View File

@ -0,0 +1,10 @@
# Copyright (c) 2024 Airbyte, Inc., all rights reserved.
"""Globals and constants for the destination."""
from __future__ import annotations
DOCUMENT_ID_COLUMN = "document_id"
CHUNK_ID_COLUMN = "chunk_id"
METADATA_COLUMN = "metadata"
DOCUMENT_CONTENT_COLUMN = "document_content"
EMBEDDING_COLUMN = "embedding"

View File

@ -0,0 +1,275 @@
# Copyright (c) 2024 Airbyte, Inc., all rights reserved.
"""An OpenGauss DataVec implementation of the SQL processor."""
from __future__ import annotations
import uuid
from pathlib import Path
from typing import Any, Union
import dpath
import sqlalchemy
from airbyte.secrets import SecretString
from airbyte._processors.file.jsonl import JsonlWriter
from airbyte_cdk.destinations.vector_db_based import embedder
from airbyte_cdk.destinations.vector_db_based.document_processor import (
DocumentProcessor as DocumentSplitter,
)
from airbyte_cdk.destinations.vector_db_based.document_processor import (
ProcessingConfigModel as DocumentSplitterConfig,
)
from airbyte_cdk.models import AirbyteRecordMessage
from sqlalchemy import (
insert,
select,
delete,
)
from overrides import overrides
from typing_extensions import Protocol
from destination_opengauss_datavec.common.catalog.catalog_providers import CatalogProvider
from destination_opengauss_datavec.globals import (
CHUNK_ID_COLUMN,
DOCUMENT_CONTENT_COLUMN,
DOCUMENT_ID_COLUMN,
EMBEDDING_COLUMN,
METADATA_COLUMN,
)
# OpenGauss DataVec vector type definition
from opengauss_sqlalchemy.usertype import Vector
# Import the SQL config base class
from destination_opengauss_datavec.common.sql.sql_processor import SqlConfig, SqlProcessorBase
class OpenGaussConfig(SqlConfig):
"""Configuration for the OpenGauss DataVec cache.
Also inherits config from the JsonlWriter, which is responsible for writing files to disk.
"""
host: str
port: int
database: str
username: str
password: Union[SecretString, str]
def __init__(
self,
host: str,
port: int,
database: str,
username: str,
password: Union[SecretString, str],
schema_name: str = "public",
):
# Initialize parent SqlConfig
super().__init__(
schema_name=schema_name,
host=host,
port=port,
database=database,
username=username,
password=password,
)
@overrides
def get_sql_alchemy_url(self) -> SecretString:
"""Return the SQLAlchemy URL to use."""
url = sqlalchemy.engine.URL.create(
drivername="opengauss+psycopg2",
username=str(self.username),
password=str(self.password),
host=self.host,
port=self.port,
database=self.database,
)
return SecretString(str(url))
@overrides
def get_database_name(self) -> str:
"""Return the name of the database."""
return self.database
class EmbeddingConfig(Protocol):
"""A protocol for embedding configuration."""
mode: str
class OpenGaussDataVecProcessor(SqlProcessorBase):
"""An OpenGauss DataVec implementation of the SQL Processor."""
supports_merge_insert = False
"""We use the emulated merge code path because each primary key has multiple rows (chunks)."""
sql_config: OpenGaussConfig
"""The configuration for the OpenGauss DataVec processor, including the vector length."""
splitter_config: DocumentSplitterConfig
"""The configuration for the document splitter."""
file_writer_class = JsonlWriter
def __init__(
self,
sql_config: OpenGaussConfig,
splitter_config: DocumentSplitterConfig,
embedder_config: EmbeddingConfig,
catalog_provider: CatalogProvider,
temp_dir: Path,
temp_file_cleanup: bool = True,
) -> None:
"""Initialize the OpenGauss DataVec processor."""
self.splitter_config = splitter_config
self.embedder_config = embedder_config
super().__init__(
sql_config=sql_config,
catalog_provider=catalog_provider,
temp_dir=temp_dir,
temp_file_cleanup=temp_file_cleanup,
)
def _get_sql_column_definitions(
self,
stream_name: str,
) -> dict[str, sqlalchemy.types.TypeEngine]:
"""Return the column definitions for the given stream."""
_ = stream_name # unused
return {
DOCUMENT_ID_COLUMN: sqlalchemy.String(),
CHUNK_ID_COLUMN: sqlalchemy.String(),
METADATA_COLUMN: sqlalchemy.JSON(),
DOCUMENT_CONTENT_COLUMN: sqlalchemy.Text(),
EMBEDDING_COLUMN: Vector(self.embedding_dimensions),
}
def _emulated_merge_temp_table_to_final_table(
self,
stream_name: str,
temp_table_name: str,
final_table_name: str,
) -> None:
"""Emulate the merge operation using a series of SQL commands."""
final_table = self._get_table_by_name(final_table_name)
temp_table = self._get_table_by_name(temp_table_name)
columns_list: list[str] = list(
self._get_sql_column_definitions(stream_name=stream_name).keys()
)
subquery = select(temp_table.c[DOCUMENT_ID_COLUMN])
delete_statement = delete(final_table).where(
final_table.c[DOCUMENT_ID_COLUMN].in_(subquery)
)
select_all_from_temp = select(temp_table)
insert_statement = insert(final_table).from_select(
columns_list, select_all_from_temp
)
with self.get_sql_connection() as conn:
# This is a transactional operation to avoid "outages"
conn.execute(delete_statement)
conn.execute(insert_statement)
def process_record_message(
self,
record_msg: AirbyteRecordMessage,
stream_schema: dict,
) -> None:
"""Write a record to the cache."""
document_chunks, id_to_delete = self.splitter.process(record_msg)
_ = id_to_delete # unused
embeddings = self.embedder.embed_documents(
documents=document_chunks,
)
for i, chunk in enumerate(document_chunks, start=0):
new_data: dict[str, Any] = {
DOCUMENT_ID_COLUMN: self._create_document_id(record_msg),
CHUNK_ID_COLUMN: str(uuid.uuid4().int),
METADATA_COLUMN: chunk.metadata,
DOCUMENT_CONTENT_COLUMN: chunk.page_content,
EMBEDDING_COLUMN: embeddings[i],
}
self.file_writer.process_record_message(
record_msg=AirbyteRecordMessage(
namespace=record_msg.namespace,
stream=record_msg.stream,
data=new_data,
emitted_at=record_msg.emitted_at,
),
stream_schema={
"type": "object",
"properties": {
DOCUMENT_ID_COLUMN: {"type": "string"},
CHUNK_ID_COLUMN: {"type": "string"},
METADATA_COLUMN: {"type": "object"},
DOCUMENT_CONTENT_COLUMN: {"type": "string"},
EMBEDDING_COLUMN: {
"type": "array",
"items": {"type": "float"},
},
},
},
)
def _add_missing_columns_to_table(
self,
stream_name: str,
table_name: str,
) -> None:
"""Add missing columns to the table.
This is a no-op because metadata scans do not work with the `VECTOR` data type.
"""
pass
@property
def embedder(self) -> embedder.Embedder:
return embedder.create_from_config(
embedding_config=self.embedder_config, # type: ignore [arg-type]
processing_config=self.splitter_config,
)
@property
def embedding_dimensions(self) -> int:
"""Return the number of dimensions for the embeddings."""
return self.embedder.embedding_dimensions
@property
def splitter(self) -> DocumentSplitter:
return DocumentSplitter(
config=self.splitter_config,
catalog=self.catalog_provider.configured_catalog,
)
def _create_document_id(self, record_msg: AirbyteRecordMessage) -> str:
"""Create document id based on the primary key values."""
stream_name = record_msg.stream
primary_key = self._get_record_primary_key(record_msg=record_msg)
if primary_key is not None:
return f"Stream_{stream_name}_Key_{primary_key}"
return str(uuid.uuid4().int)
def _get_record_primary_key(self, record_msg: AirbyteRecordMessage) -> Union[str, None]:
"""Create primary key for the record by appending the primary keys."""
stream_name = record_msg.stream
primary_keys = self._get_primary_keys(stream_name)
if not primary_keys:
return None
primary_key = []
for key in primary_keys:
try:
primary_key.append(str(dpath.get(record_msg.data, key)))
except KeyError:
primary_key.append("__not_found__")
# return a stringified version of all primary keys
stringified_primary_key = "_".join(primary_key)
return stringified_primary_key

View File

@ -0,0 +1,19 @@
{
"streams": [
{
"stream": {
"name": "example_stream_3",
"json_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {}
},
"supported_sync_modes": ["full_refresh", "incremental"],
"source_defined_cursor": false,
"default_cursor_field": ["column_name"]
},
"sync_mode": "full_refresh",
"destination_sync_mode": "overwrite"
}
]
}

View File

@ -0,0 +1,19 @@
{
"streams": [
{
"stream": {
"name": "example_stream_3",
"json_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {}
},
"supported_sync_modes": ["full_refresh", "incremental"],
"source_defined_cursor": false,
"default_cursor_field": ["column_name"]
},
"sync_mode": "full_refresh",
"destination_sync_mode": "append_dedup"
}
]
}

View File

@ -0,0 +1,19 @@
{
"streams": [
{
"stream": {
"name": "example_stream_3",
"json_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {}
},
"supported_sync_modes": ["full_refresh", "incremental"],
"source_defined_cursor": false,
"default_cursor_field": ["column_name"]
},
"sync_mode": "full_refresh",
"destination_sync_mode": "append"
}
]
}

View File

@ -0,0 +1,19 @@
{
"streams": [
{
"stream": {
"name": "this_is_a_very_long_stream_name_that_is_designed_to_exceed_the_sixty_three_character_limit_for_postgres",
"json_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {}
},
"supported_sync_modes": ["full_refresh", "incremental"],
"source_defined_cursor": false,
"default_cursor_field": ["column_name"]
},
"sync_mode": "full_refresh",
"destination_sync_mode": "append"
}
]
}

View File

@ -0,0 +1,2 @@
{"type": "RECORD", "record": {"stream": "example_stream_3", "data": { "title": "value1", "field2": "value2", "str_col":"text1", "int_col":1 }, "emitted_at": 1625383200000}}
{"type": "RECORD", "record": {"stream": "example_stream_3", "data": { "title": "value2", "field2": "value2", "str_col":"text2", "int_col":2 }, "emitted_at": 1625383200000}}

View File

@ -0,0 +1,3 @@
{"type": "RECORD", "record": {"stream": "example_stream_3", "data": { "title": "value1", "field2": "value2", "str_col":"text1", "int_col":1 }, "emitted_at": 1625383200000}}
{"type": "STATE", "state": {"type": "STREAM", "stream": {"stream_descriptor": {"name": "example_stream_3", "namespace": null}, "stream_state": {"__ab_no_cursor_state_message": true}}, "global_": null, "data": null, "sourceStats": {"recordCount": 1.0, "rejectedRecordCount": null}, "destinationStats": null, "id": 2}}
{"type": "RECORD", "record": {"stream": "example_stream_3", "data": { "title": "value2", "field2": "value2", "str_col":"text2", "int_col":2 }, "emitted_at": 1625383200000}}

View File

@ -0,0 +1,3 @@
{"type": "RECORD", "record": {"stream": "this_is_a_very_long_stream_name_that_is_designed_to_exceed_the_sixty_three_character_limit_for_postgres", "data": { "title": "value1", "field2": "value2", "str_col":"text1", "int_col":1 }, "emitted_at": 1625383200000}}
{"type": "STATE", "state": {"type": "STREAM", "stream": {"stream_descriptor": {"name": "this_is_a_very_long_stream_name_that_is_designed_to_exceed_the_sixty_three_character_limit_for_postgres", "namespace": null}, "stream_state": {"__ab_no_cursor_state_message": true}}, "global_": null, "data": null, "sourceStats": {"recordCount": 1.0, "rejectedRecordCount": null}, "destinationStats": null, "id": 2}}
{"type": "RECORD", "record": {"stream": "this_is_a_very_long_stream_name_that_is_designed_to_exceed_the_sixty_three_character_limit_for_postgres", "data": { "title": "value2", "field2": "value2", "str_col":"text2", "int_col":2 }, "emitted_at": 1625383200000}}

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.4 KiB

View File

@ -0,0 +1,363 @@
#
# Copyright (c) 2023 Airbyte, Inc., all rights reserved.
#
from __future__ import annotations
import json
import logging
from typing import Any
from airbyte_cdk.models.airbyte_protocol import AirbyteStateType
import psycopg2
from airbyte_cdk.destinations.vector_db_based.test_utils import BaseIntegrationTest
from airbyte_cdk.models import DestinationSyncMode, Status
from destination_opengauss_datavec.destination import DestinationOpenGaussDataVec
class OpenGaussDataVecIntegrationTest(BaseIntegrationTest):
def setUp(self):
with open("secrets/config.json", "r") as f:
self.config = json.loads(f.read())
def tearDown(self):
pass
def test_check_valid_config(self):
outcome = DestinationOpenGaussDataVec().check(logging.getLogger("airbyte"), self.config)
assert outcome.status == Status.SUCCEEDED
def test_check_invalid_config(self):
outcome = DestinationOpenGaussDataVec().check(
logging.getLogger("airbyte"),
{
"processing": {
"text_fields": ["str_col"],
"chunk_size": 1000,
"metadata_fields": ["int_col"],
},
"embedding": {"mode": "openai", "openai_key": "mykey"},
"indexing": {
"host": "invalid_host",
"port": 9999,
"database": "invalid_database",
"default_schema": "invalid_schema",
"username": "invalid_user",
"password": "invalid_password",
},
},
)
assert outcome.status == Status.FAILED
def _get_db_connection(self):
return psycopg2.connect(
dbname=self.config["indexing"]["database"],
user=self.config["indexing"]["username"],
password=self.config["indexing"]["password"],
host=self.config["indexing"]["host"],
port=self.config["indexing"]["port"],
)
def _get_record_count(self, table_name):
"""Return the number of records in the table."""
conn = self._get_db_connection()
cursor = conn.cursor()
cursor.execute(f"SELECT COUNT(*) FROM {table_name};")
result = cursor.fetchone()
cursor.close()
conn.close()
return result[0]
def _get_all_records(self, table_name) -> list[dict[str, Any]]:
"""Return all records from the table as a list of dictionaries."""
conn = self._get_db_connection()
cursor = conn.cursor()
cursor.execute(f"SELECT * FROM {table_name};")
column_names = [desc[0] for desc in cursor.description]
result: list[dict[str, Any]] = []
for row in cursor.fetchall():
result.append(dict(zip(column_names, row)))
cursor.close()
conn.close()
return result
def _delete_table(self, table_name):
conn = self._get_db_connection()
cursor = conn.cursor()
cursor.execute(f"DROP TABLE IF EXISTS {table_name};")
conn.commit()
conn.close()
def _run_cosine_similarity(self, query_vector, table_name):
conn = self._get_db_connection()
cursor = conn.cursor()
query = f"""
SELECT DOCUMENT_CONTENT
FROM {table_name}
ORDER BY embedding <-> CAST({query_vector} AS VECTOR(1536))
LIMIT 1
"""
cursor.execute(query)
result = cursor.fetchone()
cursor.close()
conn.close()
return result
def test_write(self):
self._delete_table("mystream")
catalog = self._get_configured_catalog(DestinationSyncMode.overwrite)
first_state_message = self._state({"state": "1"})
first_state_message.state.type = AirbyteStateType.GLOBAL
first_record = [
self._record(
stream="mystream",
str_value=f"Dogs are number {i}",
int_value=i,
)
for i in range(5)
]
# initial sync with replace
destination = DestinationOpenGaussDataVec()
_ = list(
destination.write(
config=self.config,
configured_catalog=catalog,
input_messages=[*first_record, first_state_message],
)
)
assert self._get_record_count("mystream") == 5
# subsequent sync with append
append_catalog = self._get_configured_catalog(DestinationSyncMode.append)
list(
destination.write(
config=self.config,
configured_catalog=append_catalog,
input_messages=[self._record("mystream", "Cats are nice", 6), first_state_message],
)
)
assert self._get_record_count("mystream") == 6
def test_write_and_replace(self):
self._delete_table("mystream")
catalog = self._get_configured_catalog(DestinationSyncMode.overwrite)
first_state_message = self._state({"state": "1"})
first_state_message.state.type = AirbyteStateType.GLOBAL
first_five_records = [
self._record(
stream="mystream",
str_value=f"Dogs are number {i}",
int_value=i,
)
for i in range(5)
]
# initial sync with replace
destination = DestinationOpenGaussDataVec()
list(
destination.write(
config=self.config,
configured_catalog=catalog,
input_messages=[*first_five_records, first_state_message],
)
)
assert self._get_record_count("mystream") == 5
# subsequent sync with append
append_catalog = self._get_configured_catalog(DestinationSyncMode.append)
list(
destination.write(
config=self.config,
configured_catalog=append_catalog,
input_messages=[self._record("mystream", "Cats are nice", 6), first_state_message],
)
)
assert self._get_record_count("mystream") == 6
# subsequent sync with append_dedup
append_dedup_catalog = self._get_configured_catalog(DestinationSyncMode.append_dedup)
list(
destination.write(
config=self.config,
configured_catalog=append_dedup_catalog,
input_messages=[
self._record("mystream", "Cats are nice too", 4),
first_state_message,
],
)
)
# TODO: FIXME: This should be 6, but it's 7 because the deduplication is not working
assert self._get_record_count("mystream") == 6
# comment the following so we can use fake for testing
# embeddings = OpenAIEmbeddings(openai_api_key=self.config["embedding"]["openai_key"])
# result = self._run_cosine_similarity(embeddings.embed_query("feline animals"), "mystream")
# assert(len(result) == 1)
# result[0] == "str_col: Cats are nice"
def test_overwrite_mode_deletes_records(self):
self._delete_table("mystream")
catalog = self._get_configured_catalog(DestinationSyncMode.overwrite)
first_state_message = self._state({"state": "1"})
first_state_message.state.type = AirbyteStateType.GLOBAL
first_four_records = [
self._record(
stream="mystream",
str_value=f"Dogs are number {i}",
int_value=i,
)
for i in range(4)
]
# initial sync with replace
destination = DestinationOpenGaussDataVec()
list(destination.write(self.config, catalog, [*first_four_records, first_state_message]))
assert self._get_record_count("mystream") == 4
# following should replace existing records
append_catalog = self._get_configured_catalog(DestinationSyncMode.overwrite)
list(
destination.write(
config=self.config,
configured_catalog=append_catalog,
input_messages=[self._record("mystream", "Cats are nice", 6), first_state_message],
)
)
assert self._get_record_count("mystream") == 1
def test_record_write_fidelity(self):
self._delete_table("mystream")
catalog = self._get_configured_catalog(DestinationSyncMode.overwrite)
first_state_message = self._state({"state": "1"})
first_state_message.state.type = AirbyteStateType.GLOBAL
records = [
self._record(
stream="mystream",
str_value=f"Dogs are number {i}",
int_value=i,
)
for i in range(1)
]
# initial sync with replace
destination = DestinationOpenGaussDataVec()
list(destination.write(self.config, catalog, [*records, first_state_message]))
assert self._get_record_count("mystream") == 1
first_written_record = self._get_all_records("mystream")[0]
assert list(first_written_record.keys()) == [
"document_id",
"chunk_id",
"metadata",
"document_content",
"embedding",
]
assert first_written_record.pop("embedding")
assert first_written_record.pop("chunk_id")
metadata = first_written_record.pop("metadata")
_ = metadata
# TODO: Fix the data type issue here (currently stringified):
# assert isinstance(metadata, dict), f"METADATA should be a dict: {metadata}"
# assert metadata["int_col"] == 0
assert first_written_record == {
"document_id": "Stream_mystream_Key_0",
"document_content": "str_col: Dogs are number 0",
}
def test_write_with_chunk_size_5(self):
self._delete_table("mystream")
self.config["processing"]["chunk_size"] = 5
catalog = self._get_configured_catalog(DestinationSyncMode.overwrite)
first_state_message = self._state({"state": "1"})
first_state_message.state.type = AirbyteStateType.GLOBAL
first_record = [
self._record(
stream="mystream",
str_value=f"Dogs are number {i}",
int_value=i,
)
for i in range(5)
]
# initial sync with replace
destination = DestinationOpenGaussDataVec()
_ = list(
destination.write(
config=self.config,
configured_catalog=catalog,
input_messages=[*first_record, first_state_message],
)
)
assert self._get_record_count("mystream") == 15
# subsequent sync with append
append_catalog = self._get_configured_catalog(DestinationSyncMode.append)
list(
destination.write(
config=self.config,
configured_catalog=append_catalog,
input_messages=[self._record("mystream", "Cats are nice", 6), first_state_message],
)
)
assert self._get_record_count("mystream") == 18
# subsequent sync with append_dedup
append_dedup_catalog = self._get_configured_catalog(DestinationSyncMode.append_dedup)
list(
destination.write(
config=self.config,
configured_catalog=append_dedup_catalog,
input_messages=[
self._record("mystream", "Cats are nice too", 4),
first_state_message,
],
)
)
assert self._get_record_count("mystream") == 18
def test_write_fidelity_with_chunk_size_5(self):
self._delete_table("mystream")
self.config["processing"]["chunk_size"] = 5
catalog = self._get_configured_catalog(DestinationSyncMode.overwrite)
first_state_message = self._state({"state": "1"})
first_state_message.state.type = AirbyteStateType.GLOBAL
records = [
self._record(
stream="mystream",
str_value=f"Dogs are number {i}",
int_value=i,
)
for i in range(1)
]
# initial sync with replace
destination = DestinationOpenGaussDataVec()
list(destination.write(self.config, catalog, [*records, first_state_message]))
assert self._get_record_count("mystream") == 3
first_written_record = self._get_all_records("mystream")[0]
second_written_record = self._get_all_records("mystream")[1]
third_written_record = self._get_all_records("mystream")[2]
assert list(first_written_record.keys()) == [
"document_id",
"chunk_id",
"metadata",
"document_content",
"embedding",
]
assert first_written_record.pop("embedding")
assert first_written_record.pop("chunk_id")
metadata = first_written_record.pop("metadata")
_ = metadata
assert first_written_record == {
"document_id": "Stream_mystream_Key_0",
"document_content": "str_col:",
}
assert second_written_record["document_id"] == "Stream_mystream_Key_0"
assert second_written_record["document_content"] == "Dogs are"
assert third_written_record["document_id"] == "Stream_mystream_Key_0"
assert third_written_record["document_content"] == "number 0"

View File

@ -0,0 +1,19 @@
{
"processing": {
"text_fields": ["str_col"],
"metadata_fields": ["int_col"],
"chunk_size": 1000
},
"embedding": {
"mode": "openai",
"openai_key": "mykey"
},
"indexing": {
"host": "MYACCOUNT",
"port": 5432,
"database": "MYDATABASE",
"default_schema": "MYSCHEMA",
"username": "MYUSERNAME",
"password": "xxxxxxx"
}
}

View File

@ -0,0 +1,389 @@
{
"documentationUrl": "https://docs.airbyte.com/integrations/destinations/opengauss-datavec",
"connectionSpecification": {
"title": "Destination Config",
"description": "The configuration model for the Vector DB based destinations. This model is used to generate the UI for the destination configuration,\nas well as to provide type safety for the configuration passed to the destination.\n\nThe configuration model is composed of four parts:\n* Processing configuration\n* Embedding configuration\n* Indexing configuration\n* Advanced configuration\n\nProcessing, embedding and advanced configuration are provided by this base class, while the indexing configuration is provided by the destination connector in the sub class.",
"type": "object",
"properties": {
"embedding": {
"title": "Embedding",
"description": "Embedding configuration",
"group": "embedding",
"type": "object",
"oneOf": [
{
"title": "OpenAI",
"type": "object",
"properties": {
"mode": {
"title": "Mode",
"default": "openai",
"const": "openai",
"enum": ["openai"],
"type": "string"
},
"openai_key": {
"title": "OpenAI API key",
"airbyte_secret": true,
"type": "string"
}
},
"required": ["openai_key", "mode"],
"description": "Use the OpenAI API to embed text. This option is using the text-embedding-ada-002 model with 1536 embedding dimensions."
},
{
"title": "Cohere",
"type": "object",
"properties": {
"mode": {
"title": "Mode",
"default": "cohere",
"const": "cohere",
"enum": ["cohere"],
"type": "string"
},
"cohere_key": {
"title": "Cohere API key",
"airbyte_secret": true,
"type": "string"
}
},
"required": ["cohere_key", "mode"],
"description": "Use the Cohere API to embed text."
},
{
"title": "Fake",
"type": "object",
"properties": {
"mode": {
"title": "Mode",
"default": "fake",
"const": "fake",
"enum": ["fake"],
"type": "string"
}
},
"description": "Use a fake embedding made out of random vectors with 1536 embedding dimensions. This is useful for testing the data pipeline without incurring any costs.",
"required": ["mode"]
},
{
"title": "Azure OpenAI",
"type": "object",
"properties": {
"mode": {
"title": "Mode",
"default": "azure_openai",
"const": "azure_openai",
"enum": ["azure_openai"],
"type": "string"
},
"openai_key": {
"title": "Azure OpenAI API key",
"description": "The API key for your Azure OpenAI resource. You can find this in the Azure portal under your Azure OpenAI resource",
"airbyte_secret": true,
"type": "string"
},
"api_base": {
"title": "Resource base URL",
"description": "The base URL for your Azure OpenAI resource. You can find this in the Azure portal under your Azure OpenAI resource",
"examples": ["https://your-resource-name.openai.azure.com"],
"type": "string"
},
"deployment": {
"title": "Deployment",
"description": "The deployment for your Azure OpenAI resource. You can find this in the Azure portal under your Azure OpenAI resource",
"examples": ["your-resource-name"],
"type": "string"
}
},
"required": ["openai_key", "api_base", "deployment", "mode"],
"description": "Use the Azure-hosted OpenAI API to embed text. This option is using the text-embedding-ada-002 model with 1536 embedding dimensions."
},
{
"title": "OpenAI-compatible",
"type": "object",
"properties": {
"mode": {
"title": "Mode",
"default": "openai_compatible",
"const": "openai_compatible",
"enum": ["openai_compatible"],
"type": "string"
},
"api_key": {
"title": "API key",
"default": "",
"airbyte_secret": true,
"type": "string"
},
"base_url": {
"title": "Base URL",
"description": "The base URL for your OpenAI-compatible service",
"examples": ["https://your-service-name.com"],
"type": "string"
},
"model_name": {
"title": "Model name",
"description": "The name of the model to use for embedding",
"default": "text-embedding-ada-002",
"examples": ["text-embedding-ada-002"],
"type": "string"
},
"dimensions": {
"title": "Embedding dimensions",
"description": "The number of dimensions the embedding model is generating",
"examples": [1536, 384],
"type": "integer"
}
},
"required": ["base_url", "dimensions", "mode"],
"description": "Use a service that's compatible with the OpenAI API to embed text."
}
]
},
"processing": {
"title": "ProcessingConfigModel",
"type": "object",
"properties": {
"chunk_size": {
"title": "Chunk size",
"description": "Size of chunks in tokens to store in vector store (make sure it is not too big for the context if your LLM)",
"maximum": 8191,
"minimum": 1,
"type": "integer"
},
"chunk_overlap": {
"title": "Chunk overlap",
"description": "Size of overlap between chunks in tokens to store in vector store to better capture relevant context",
"default": 0,
"type": "integer"
},
"text_fields": {
"title": "Text fields to embed",
"description": "List of fields in the record that should be used to calculate the embedding. The field list is applied to all streams in the same way and non-existing fields are ignored. If none are defined, all fields are considered text fields. When specifying text fields, you can access nested fields in the record by using dot notation, e.g. `user.name` will access the `name` field in the `user` object. It's also possible to use wildcards to access all fields in an object, e.g. `users.*.name` will access all `names` fields in all entries of the `users` array.",
"default": [],
"always_show": true,
"examples": ["text", "user.name", "users.*.name"],
"type": "array",
"items": {
"type": "string"
}
},
"metadata_fields": {
"title": "Fields to store as metadata",
"description": "List of fields in the record that should be stored as metadata. The field list is applied to all streams in the same way and non-existing fields are ignored. If none are defined, all fields are considered metadata fields. When specifying text fields, you can access nested fields in the record by using dot notation, e.g. `user.name` will access the `name` field in the `user` object. It's also possible to use wildcards to access all fields in an object, e.g. `users.*.name` will access all `names` fields in all entries of the `users` array. When specifying nested paths, all matching values are flattened into an array set to a field named by the path.",
"default": [],
"always_show": true,
"examples": ["age", "user", "user.name"],
"type": "array",
"items": {
"type": "string"
}
},
"text_splitter": {
"title": "Text splitter",
"description": "Split text fields into chunks based on the specified method.",
"type": "object",
"oneOf": [
{
"title": "By Separator",
"type": "object",
"properties": {
"mode": {
"title": "Mode",
"default": "separator",
"const": "separator",
"enum": ["separator"],
"type": "string"
},
"separators": {
"title": "Separators",
"description": "List of separator strings to split text fields by. The separator itself needs to be wrapped in double quotes, e.g. to split by the dot character, use \".\". To split by a newline, use \"\\n\".",
"default": ["\"\\n\\n\"", "\"\\n\"", "\" \"", "\"\""],
"type": "array",
"items": {
"type": "string"
}
},
"keep_separator": {
"title": "Keep separator",
"description": "Whether to keep the separator in the resulting chunks",
"default": false,
"type": "boolean"
}
},
"description": "Split the text by the list of separators until the chunk size is reached, using the earlier mentioned separators where possible. This is useful for splitting text fields by paragraphs, sentences, words, etc.",
"required": ["mode"]
},
{
"title": "By Markdown header",
"type": "object",
"properties": {
"mode": {
"title": "Mode",
"default": "markdown",
"const": "markdown",
"enum": ["markdown"],
"type": "string"
},
"split_level": {
"title": "Split level",
"description": "Level of markdown headers to split text fields by. Headings down to the specified level will be used as split points",
"default": 1,
"minimum": 1,
"maximum": 6,
"type": "integer"
}
},
"description": "Split the text by Markdown headers down to the specified header level. If the chunk size fits multiple sections, they will be combined into a single chunk.",
"required": ["mode"]
},
{
"title": "By Programming Language",
"type": "object",
"properties": {
"mode": {
"title": "Mode",
"default": "code",
"const": "code",
"enum": ["code"],
"type": "string"
},
"language": {
"title": "Language",
"description": "Split code in suitable places based on the programming language",
"enum": [
"cpp",
"go",
"java",
"js",
"php",
"proto",
"python",
"rst",
"ruby",
"rust",
"scala",
"swift",
"markdown",
"latex",
"html",
"sol"
],
"type": "string"
}
},
"required": ["language", "mode"],
"description": "Split the text by suitable delimiters based on the programming language. This is useful for splitting code into chunks."
}
]
},
"field_name_mappings": {
"title": "Field name mappings",
"description": "List of fields to rename. Not applicable for nested fields, but can be used to rename fields already flattened via dot notation.",
"default": [],
"type": "array",
"items": {
"title": "FieldNameMappingConfigModel",
"type": "object",
"properties": {
"from_field": {
"title": "From field name",
"description": "The field name in the source",
"type": "string"
},
"to_field": {
"title": "To field name",
"description": "The field name to use in the destination",
"type": "string"
}
},
"required": ["from_field", "to_field"]
}
}
},
"required": ["chunk_size"],
"group": "processing"
},
"omit_raw_text": {
"title": "Do not store raw text",
"description": "Do not store the text that gets embedded along with the vector and the metadata in the destination. If set to true, only the vector and the metadata will be stored - in this case raw text for LLM use cases needs to be retrieved from another source.",
"default": false,
"group": "advanced",
"type": "boolean"
},
"indexing": {
"title": "Indexing",
"type": "object",
"properties": {
"host": {
"title": "Host",
"description": "Hostname of the database.",
"order": 1,
"type": "string"
},
"database": {
"title": "Database",
"description": "Enter the name of the database that you want to sync data into",
"order": 2,
"examples": ["AIRBYTE_DATABASE", "postgres"],
"type": "string"
},
"username": {
"title": "Username",
"description": "Enter the name of the user you want to use to access the database",
"order": 3,
"examples": ["root"],
"type": "string"
},
"password": {
"title": "Password",
"description": "Enter the password of the user you want to use to access the database",
"order": 4,
"airbyte_secret": true,
"type": "string"
},
"port": {
"title": "Port",
"description": "Enter the port you want to use to access the database",
"default": 5432,
"examples": ["5432"],
"type": "integer"
},
"default_schema": {
"title": "Default Schema",
"description": "Enter the name of the default schema",
"default": "public",
"order": 6,
"examples": ["AIRBYTE_SCHEMA"],
"type": "string"
}
},
"required": ["host", "database", "username", "password"],
"description": "Indexing configuration",
"group": "indexing"
}
},
"required": ["embedding", "processing", "indexing"],
"groups": [
{
"id": "processing",
"title": "Processing"
},
{
"id": "embedding",
"title": "Embedding"
},
{
"id": "indexing",
"title": "Indexing"
},
{
"id": "advanced",
"title": "Advanced"
}
]
},
"supportsIncremental": true,
"supported_destination_sync_modes": ["overwrite", "append", "append_dedup"]
}

View File

@ -0,0 +1,12 @@
#
# Copyright (c) 2023 Airbyte, Inc., all rights reserved.
#
import sys
from destination_opengauss_datavec import DestinationOpenGaussDataVec
if __name__ == "__main__":
DestinationOpenGaussDataVec().run(sys.argv[1:])

View File

@ -0,0 +1,61 @@
data:
ab_internal:
ql: 300
sl: 200
allowedHosts:
hosts:
- api.openai.com
- api.cohere.ai
- ${embedding.api_base}
connectorBuildOptions:
baseImage: docker.io/airbyte/python-connector-base:4.0.0@sha256:d9894b6895923b379f3006fa251147806919c62b7d9021b5cd125bb67d7bbe22
connectorSubtype: vectorstore
connectorType: destination
definitionId: 3ea1b85a-564c-46f9-9eae-632b845fcebe
dockerImageTag: dev
dockerRepository: airbyte/destination-opengauss-datavec
documentationUrl: https://docs.airbyte.com/integrations/destinations/opengauss-datavec
githubIssueLabel: destination-opengauss-datavec
icon: icon.svg
license: MIT
name: openGauss Datavec
# 必须有否则不能通过qa_checks
remoteRegistries:
pypi:
enabled: true
packageName: airbyte-destination-opengauss-datavec
registryOverrides:
cloud:
enabled: false
oss:
enabled: true
releaseDate: 2025-08-30
releaseStage: alpha
resourceRequirements:
jobSpecific:
- jobType: sync
resourceRequirements:
memory_limit: 2Gi
memory_request: 2Gi
supportLevel: community
tags:
- language:python
- cdk:python
connectorTestSuitesOptions:
# used for Airbyte's official CI/CD processes during pr and airbyte-ci.
- suite: unitTests
- suite: integrationTests
testSecrets:
- name: SECRET_DESTINATION-OPENGAUSS-DATAVEC_CREDS
fileName: config.json
secretStore:
type: GSM
alias: airbyte-connector-testing-secret-store
- suite: acceptanceTests
testSecrets:
- name: SECRET_DESTINATION-OPENGAUSS-DATAVEC_CREDS
fileName: config.json
secretStore:
type: GSM
alias: airbyte-connector-testing-secret-store
metadataSpecVersion: "1.0"

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,80 @@
[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"
[tool.poetry]
name = "airbyte-destination-opengauss-datavec"
version = "0.1.0"
description = "Airbyte destination implementation for OpenGauss with vector support"
authors = ["hly <liyanhu03@163.com>"]
license = "MIT"
readme = "README.md"
[[tool.poetry.packages]]
include = "destination_opengauss_datavec"
[tool.poetry.dependencies.airbyte-cdk]
# Version constrained by PyAirbyte (`airbyte`) version
version = ">=1.0"
extras = ["vector-db-based"]
[tool.poetry.dependencies]
python = ">=3.10,<3.13"
airbyte = "^0.12.0"
sqlalchemy = "<2.0"
opengauss-sqlalchemy = { git = "https://gitcode.com/opengauss/openGauss-sqlalchemy.git", rev = "da80fc05ebc105968bdebfd4d0614776ad83d5fe" }
psycopg2-binary = "2.9.9"
jaraco-functools = "^4.1.0"
[tool.poetry.group.dev.dependencies]
pytest = "^8.0.0"
ruff = "^0.3.2"
mypy = "^1.9.0"
coverage = "^7.5.3"
[tool.poetry.scripts]
destination-opengauss-datavec = "destination_opengauss_datavec.run:run"
[tool.ruff]
target-version = "py310"
preview = true
line-length = 100
[[tool.mypy.overrides]]
module = [
"airbyte_protocol",
"airbyte_protocol.*",
"sqlalchemy",
"sqlalchemy.*",
"opengauss_sqlalchemy",
"opengauss_sqlalchemy.*",
"dpath",
"dpath.util",
]
ignore_missing_imports = true # No stubs yet (😢)
[tool.coverage.run]
# branch = true
# parallel = true
omit = [
"unit_tests/*",
"integration_tests/*",
"destination_opengauss_datavec/common/*",
]
[tool.coverage.report]
precision = 1
fail_under = 90.0
exclude_also = [
# # TYPE_CHECKING block is only executed while running mypy
"if TYPE_CHECKING:",
"\\.\\.\\."
]
[tool.poe]
include = [
# Shared tasks definition file(s) can be imported here.
# Run `poe` or `poe --help` to see the list of available tasks.
"${POE_GIT_DIR}/poe-tasks/poetry-connector-tasks.toml",
]

View File

@ -0,0 +1,82 @@
#
# Copyright (c) 2023 Airbyte, Inc., all rights reserved.
#
import logging
import unittest
from unittest.mock import MagicMock, Mock, patch
from airbyte.strategies import WriteStrategy
from airbyte_cdk.models import ConnectorSpecification, Status
from destination_opengauss_datavec.config import ConfigModel
from destination_opengauss_datavec.destination import DestinationOpenGaussDataVec
class TestDestinationOpenGaussDataVec(unittest.TestCase):
def setUp(self):
self.config = {
"processing": {"text_fields": ["str_col"], "metadata_fields": [], "chunk_size": 1000},
"embedding": {"mode": "fake"},
"indexing": {
"host": "localhost",
"database": "postgres",
"username": "hly",
"password": "Hly12345",
"port": 8888,
"schema": "public",
"default_schema": "public",
},
}
self.config_model = ConfigModel.parse_obj(self.config)
self.logger = logging.getLogger("airbyte")
def test_spec(self):
destination = DestinationOpenGaussDataVec()
result = destination.spec()
self.assertIsInstance(result, ConnectorSpecification)
@patch("destination_opengauss_datavec.opengauss_processor.OpenGaussDataVecProcessor")
def test_check(self, MockedOpenGaussDataVecProcessor):
mock_processor = Mock()
MockedOpenGaussDataVecProcessor.return_value = mock_processor
destination = DestinationOpenGaussDataVec()
result = destination.check(self.logger, self.config)
self.assertEqual(result.status, Status.SUCCEEDED)
mock_processor.sql_config.get_sql_engine().connect.assert_called_once()
@patch("destination_opengauss_datavec.opengauss_processor.OpenGaussDataVecProcessor")
def test_check_with_errors(self, MockedOpenGaussDataVecProcessor):
mock_processor = Mock()
MockedOpenGaussDataVecProcessor.return_value = mock_processor
indexer_error_message = "Indexer Error"
mock_processor.sql_config.get_sql_engine().connect.side_effect = Exception(indexer_error_message)
destination = DestinationOpenGaussDataVec()
result = destination.check(self.logger, self.config)
self.assertEqual(result.status, Status.FAILED)
mock_processor.sql_config.get_sql_engine().connect.assert_called_once()
@patch("destination_opengauss_datavec.opengauss_processor.OpenGaussDataVecProcessor")
def test_write(
self,
MockedOpenGaussDataVecProcessor,
):
mock_processor = Mock()
MockedOpenGaussDataVecProcessor.return_value = mock_processor
mock_processor.process_airbyte_messages_as_generator.return_value = []
configured_catalog = MagicMock()
input_messages = []
destination = DestinationOpenGaussDataVec()
list(destination.write(self.config, configured_catalog, input_messages))
mock_processor.process_airbyte_messages_as_generator.assert_called_once_with(
messages=input_messages,
write_strategy=WriteStrategy.AUTO,
)

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 177 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 177 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 149 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 149 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 326 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 131 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 213 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 131 KiB

View File

@ -0,0 +1,221 @@
# OpenGauss DataVec Destination
<div align="center">
[![English](https://img.shields.io/badge/Language-English-blue)](openGauss_DataVec_Connector_Document.md)
[![中文](https://img.shields.io/badge/语言-中文-red)](openGauss_DataVec_Connector_Document.zh.md)
**English** | [中文](openGauss_DataVec_Connector_Document.zh.md)
</div>
---
## Table of Contents
- [Overview](#overview)
- [Prerequisites](#prerequisites)
- [Step 1: Set up OpenGauss](#step-1-set-up-opengauss)
- [Step 2: Set up the OpenGauss DataVec connector in Airbyte](#step-2-set-up-the-opengauss-datavec-connector-in-airbyte)
- [Naming Conventions](#naming-conventions)
- [Features](#features)
- [Data type mapping](#data-type-mapping)
- [Configuration](#configuration)
- [Changelog](#changelog)
## Overview
This page guides you through the process of setting up the OpenGauss DataVec destination connector.
There are three parts to this:
* Processing - split up individual records in chunks so they will fit the context window and decide which fields to use as context and which are supplementary metadata.
* Embedding - convert the text into a vector representation using a pre-trained model. Currently supported:
* OpenAI's `text-embedding-ada-002`
* Cohere's `embed-english-light-v2.0`
* Azure OpenAI
* Fake `random vectors with 1536 embedding dimensions`
* OpenAI-compatible
* Coming soon: Hugging Face's `e5-base-v2`.
* OpenGauss Connection - where to store the vectors. This configures a vector store using OpenGauss tables having the `VECTOR` data type which is achieved by installing the DataVec extension.
## Prerequisites
To use the OpenGauss DataVec destination, you'll need:
- An account with API access depending on which embedding method you want to use.
- An openGauss database with vector engine support. See the [OpenGauss DataVec documentation](https://docs.opengauss.org/zh/docs/latest/docs/DataVec/DataVec-Overview.html) for installation instructions.
You'll need the following information to configure the destination:
- **Embedding service API Key** - The API key for your embedding account and other params depending on your model.
- **Port** - The port number the server is listening on. Defaults to the standard port
number (5432).
- **Username**
- **Password**
- **Default Schema Name** - Specify the schema (or several schemas separated by commas) to be set in
the search-path. These schemas will be used to resolve unqualified object names used in statements
executed over this connection.
- **Database** - The database name. The default is to connect to a database with the same name as
the user name.
### Configure Network Access
Make sure your OpenGauss database can be accessed by Airbyte. If your database is within a VPC, you
may need to allow access from the IP you're using to expose Airbyte.
## Step 1: Set up OpenGauss
### **Permissions**
You need an OpenGauss user with the following permissions:
- can create tables and write rows.
- can create schemas e.g:
You can create such a user by running:
```sql
CREATE USER airbyte_user WITH PASSWORD '<password>';
GRANT CREATE, TEMPORARY ON DATABASE <database> TO airbyte_user;
```
You can also use a pre-existing user but we highly recommend creating a dedicated user for Airbyte.
## Step 2: Set up the OpenGauss DataVec connector in Airbyte
### Target Database
You will need to choose an existing database or create a new database that will be used to store
synced data from Airbyte.
## Naming Conventions
From
[Postgres SQL Identifiers syntax](https://www.postgresql.org/docs/9.0/sql-syntax-lexical.html#SQL-SYNTAX-IDENTIFIERS):
- SQL identifiers and key words must begin with a letter \(a-z, but also letters with diacritical
marks and non-Latin letters\) or an underscore \(\_\).
- Subsequent characters in an identifier or key word can be letters, underscores, digits \(0-9\), or
dollar signs \($\).
Note that dollar signs are not allowed in identifiers according to the SQL standard, so their use
might render applications less portable. The SQL standard will not define a key word that contains
digits or starts or ends with an underscore, so identifiers of this form are safe against possible
conflict with future extensions of the standard.
- The system uses no more than NAMEDATALEN-1 bytes of an identifier; longer names can be written in
commands, but they will be truncated. By default, NAMEDATALEN is 64 so the maximum identifier
length is 63 bytes
- Quoted identifiers can contain any character, except the character with code zero. \(To include a
double quote, write two double quotes.\) This allows constructing table or column names that would
otherwise not be possible, such as ones containing spaces or ampersands. The length limitation
still applies.
- Quoting an identifier also makes it case-sensitive, whereas unquoted names are always folded to
lower case.
- In order to make your applications portable and less error-prone, use consistent quoting with each
name (either always quote it or never quote it).
:::info
Airbyte OpenGauss DataVec destination will create raw tables and schemas using the Unquoted identifiers by
replacing any special characters with an underscore. All final tables and their corresponding
columns are created using Quoted identifiers preserving the case sensitivity. Special characters in final
tables are replaced with underscores.
:::
1. [Log into your Airbyte Cloud](https://cloud.airbyte.com/workspaces) account.
2. In the left navigation bar, click **Destinations**. In the top-right corner, click **new
destination**.
3. On the Set up the destination page, enter the name for the OpenGauss DataVec connector and select
**OpenGauss DataVec** from the Destination type dropdown.
4. Enter a name for your source.
5. Enter processing information.
6. Enter embedding information.
7. For the **Host**, **Port**, and **DB Name**, enter the hostname, port number, and name for your
OpenGauss database.
8. Enter the **Default Schemas**.
:::note
The schema names are case sensitive. The 'public' schema is set by default.
:::
7. For **User** and **Password**, enter the username and password you created in
[Step 1](#step-1-set-up-opengauss).
## Features
| Feature | Supported? | Notes |
| :----------------------------- | :------------------- | :---- |
| Full Refresh Sync | Yes | |
| Incremental - Append Sync | Yes | |
| Incremental - Append + Deduped | Yes | |
## Data type mapping
All fields specified as metadata fields will be stored in the metadata object of the document and can be used for filtering. The following data types are allowed for metadata fields:
* String
* Number (integer or floating point, gets converted to a 64 bit floating point)
* Booleans (true, false)
* List of String
All other fields are ignored.
## Configuration
### Processing
Each record will be split into text fields and meta fields as configured in the "Processing" section. All text fields are concatenated into a single string and then split into chunks of configured length. If specified, the metadata fields are stored as-is along with the embedded text chunks. Please note that meta data fields can only be used for filtering and not for retrieval and have to be of type string, number, boolean (all other values are ignored). Please note that there's a 40kb limit on the _total_ size of the metadata saved for each entry. Options around configuring the chunking process use the [Langchain Python library](https://python.langchain.com/docs/get_started/introduction).
When specifying text fields, you can access nested fields in the record by using dot notation, e.g. `user.name` will access the `name` field in the `user` object. It's also possible to use wildcards to access all fields in an object, e.g. `users.*.name` will access all `names` fields in all entries of the `users` array.
The chunk length is measured in tokens produced by the `tiktoken` library. The maximum is 8191 tokens, which is the maximum length supported by the `text-embedding-ada-002` model.
The stream name gets added as a metadata field `_ab_stream` to each document. If available, the primary key of the record is used to identify the document to avoid duplications when updated versions of records are indexed. It is added as the `_ab_record_id` metadata field.
### Embedding
The connector can use one of the following embedding methods:
1. OpenAI - using [OpenAI API](https://beta.openai.com/docs/api-reference/text-embedding) , the connector will produce embeddings using the `text-embedding-ada-002` model with **1536 dimensions**. This integration will be constrained by the [speed of the OpenAI embedding API](https://platform.openai.com/docs/guides/rate-limits/overview).
2. Cohere - using the [Cohere API](https://docs.cohere.com/reference/embed), the connector will produce embeddings using the `embed-english-light-v2.0` model with **1024 dimensions**.
For testing purposes, it's also possible to use the [Fake embeddings](https://python.langchain.com/docs/modules/data_connection/text_embedding/integrations/fake) integration. It will generate random embeddings and is suitable to test a data pipeline without incurring embedding costs.
### Indexing/Data Storage
- For the **Host**, **Port**, and **DB Name**, enter the hostname, port number, and name for your
OpenGauss database.
- List the **Default Schemas**.
All streams will be indexed/stored into a table with the same name. The table will be created if it doesn't exist. The table will have the following columns:
- document_id (string) - the unique identifier of the document, creating from appending the primary keys in the stream schema
- chunk_id (string) - the unique identifier of the chunk, created by appending the chunk number to the document_id
- metadata (variant) - the metadata of the document, stored as key-value pairs
- document_content (string) - the text content of the chunk
- embedding (vector) - the embedding of the chunk, stored as a list of floats
> Note: If the table name exceeds the maximum identifier limit of 63 bytes, it will be hashed and truncated.
## Changelog
<details>
<summary>Expand to review</summary>
| Version | Date | Pull Request | Subject |
|:--------| :--------- |:--------------------------------------------------------------|:-----------------------------------------------------------------------------------------------------------------------------------------------------|
| 0.1.0 | 2025-09-01 | TBD | Add support for openGauss DateVec as a Vector Destination. |
</details>
---
<div align="center">
[🔝 Back to Top](#opengauss-datavec-destination) | [🇨🇳 中文版本](openGauss_DataVec_Connector_Document.zh.md)
</div>

View File

@ -0,0 +1,195 @@
# OpenGauss DataVec 目标连接器
<div align="center">
[![English](https://img.shields.io/badge/Language-English-blue)](openGauss_DataVec_Connector_Document.md)
[![中文](https://img.shields.io/badge/语言-中文-red)](openGauss_DataVec_Connector_Document.zh.md)
[English](openGauss_DataVec_Connector_Document.md) | **中文**
</div>
---
## 目录
- [概述](#概述)
- [前提条件](#前提条件)
- [步骤 1设置 OpenGauss](#步骤-1设置-opengauss)
- [步骤 2在 Airbyte 中设置 OpenGauss DataVec 连接器](#步骤-2在-airbyte-中设置-opengauss-datavec-连接器)
- [命名约定](#命名约定)
- [功能特性](#功能特性)
- [数据类型映射](#数据类型映射)
- [配置](#配置)
- [更新日志](#更新日志)
## 概述
本页面指导您完成 OpenGauss DataVec 目标连接器的设置过程。
此过程包含三个部分:
* **处理** - 将单个记录拆分为块,以适应上下文窗口,并决定哪些字段用作上下文,哪些是补充元数据。
* **嵌入** - 使用预训练模型将文本转换为向量表示。目前支持:
* OpenAI 的 `text-embedding-ada-002`
* Cohere 的 `embed-english-light-v2.0`
* Azure OpenAI
* 虚拟 `1536 维随机向量`
* OpenAI 兼容接口
* 即将支持Hugging Face 的 `e5-base-v2`
* **OpenGauss 连接** - 存储向量的位置。通过安装 DataVec 扩展,使用具有 `VECTOR` 数据类型的 OpenGauss 表配置向量存储。
## 前提条件
要使用 OpenGauss DataVec 目标连接器,您需要:
- 根据您要使用的嵌入方法拥有 API 访问权限的账户。
- 支持向量引擎的 openGauss 数据库。请参阅 [OpenGauss DataVec 文档](https://docs.opengauss.org/zh/docs/latest/docs/DataVec/DataVec-Overview.html) 获取安装说明。
您需要以下信息来配置目标连接器:
- **嵌入服务 API 密钥** - 您的嵌入账户的 API 密钥和其他参数,具体取决于您的模型。
- **端口** - 服务器正在监听的端口号。默认为标准端口号 (5432)。
- **用户名**
- **密码**
- **默认模式名称** - 指定要在搜索路径中设置的模式(或用逗号分隔的多个模式)。这些模式将用于解析在此连接上执行的语句中使用的非限定对象名称。
- **数据库** - 数据库名称。默认是连接到与用户名同名的数据库。
### 配置网络访问
确保 Airbyte 可以访问您的 OpenGauss 数据库。如果您的数据库在 VPC 内,您可能需要允许从用于暴露 Airbyte 的 IP 进行访问。
## 步骤 1设置 OpenGauss
### **权限**
您需要一个具有以下权限的 OpenGauss 用户:
- 可以创建表和写入行
- 可以创建模式,例如:
您可以通过运行以下命令创建这样的用户:
```sql
CREATE USER airbyte_user WITH PASSWORD '<password>';
GRANT CREATE, TEMPORARY ON DATABASE <database> TO airbyte_user;
```
您也可以使用现有用户,但我们强烈建议为 Airbyte 创建专用用户。
## 步骤 2在 Airbyte 中设置 OpenGauss DataVec 连接器
### 目标数据库
您需要选择现有数据库或创建新数据库,用于存储来自 Airbyte 的同步数据。
## 命名约定
来自 [Postgres SQL 标识符语法](https://www.postgresql.org/docs/9.0/sql-syntax-lexical.html#SQL-SYNTAX-IDENTIFIERS)
- SQL 标识符和关键字必须以字母a-z还包括带变音符号的字母和非拉丁字母或下划线_开头。
- 标识符或关键字中的后续字符可以是字母、下划线、数字0-9或美元符号$)。
请注意,根据 SQL 标准标识符中不允许使用美元符号因此使用它们可能会降低应用程序的可移植性。SQL 标准不会定义包含数字或以下划线开头或结尾的关键字,因此这种形式的标识符可以安全地避免与标准的未来扩展产生可能的冲突。
- 系统使用标识符的不超过 NAMEDATALEN-1 字节可以在命令中写入更长的名称但它们将被截断。默认情况下NAMEDATALEN 是 64所以最大标识符长度是 63 字节。
- 引用的标识符可以包含任何字符,除了代码为零的字符。(要包含双引号,请写两个双引号。)这允许构造原本不可能的表或列名,例如包含空格或 & 符号的名称。长度限制仍然适用。
- 引用标识符也使其区分大小写,而未引用的名称总是折叠为小写。
- 为了使您的应用程序具有可移植性且不易出错,请对每个名称使用一致的引用(要么总是引用它,要么从不引用它)。
:::info
Airbyte OpenGauss DataVec 目标连接器将通过用下划线替换任何特殊字符来创建原始表和模式,使用未引用的标识符。所有最终表及其对应的列都使用引用的标识符创建,保持大小写敏感性。最终表中的特殊字符被替换为下划线。
:::
1. [登录您的 Airbyte Cloud](https://cloud.airbyte.com/workspaces) 账户。
2. 在左侧导航栏中,点击 **目标连接器**。在右上角,点击 **新建目标连接器**
3. 在设置目标连接器页面上,输入 OpenGauss DataVec 连接器的名称,并从目标连接器类型下拉菜单中选择 **OpenGauss DataVec**
4. 输入您的源的名称。
5. 输入处理信息。
6. 输入嵌入信息。
7. 对于 **主机**、**端口** 和 **数据库名称**,输入您的 OpenGauss 数据库的主机名、端口号和名称。
8. 输入 **默认模式**
:::note
模式名称区分大小写。默认设置 'public' 模式。
:::
7. 对于 **用户****密码**,输入您在 [步骤 1](#步骤-1设置-opengauss) 中创建的用户名和密码。
## 功能特性
| 功能 | 是否支持? | 备注 |
| :----------------------------- | :------------------- | :---- |
| 完全刷新同步 | 是 | |
| 增量 - 追加同步 | 是 | |
| 增量 - 追加 + 去重 | 是 | |
## 数据类型映射
所有指定为元数据字段的字段将存储在文档的元数据对象中,可用于过滤。元数据字段允许以下数据类型:
* 字符串
* 数字(整数或浮点数,转换为 64 位浮点数)
* 布尔值true、false
* 字符串列表
所有其他字段将被忽略。
## 配置
### 处理
每个记录将根据"处理"部分中的配置分为文本字段和元字段。所有文本字段连接成单个字符串,然后分割成配置长度的块。如果指定,元数据字段将与嵌入的文本块一起按原样存储。请注意,元数据字段只能用于过滤而不能用于检索,并且必须是字符串、数字、布尔值类型(忽略所有其他值)。请注意,为每个条目保存的元数据总大小有 40kb 的限制。有关配置分块过程的选项使用 [Langchain Python 库](https://python.langchain.com/docs/get_started/introduction)。
在指定文本字段时,您可以使用点符号访问记录中的嵌套字段,例如 `user.name` 将访问 `user` 对象中的 `name` 字段。还可以使用通配符访问对象中的所有字段,例如 `users.*.name` 将访问 `users` 数组所有条目中的所有 `names` 字段。
块长度以 `tiktoken` 库产生的令牌来衡量。最大值为 8191 个令牌,这是 `text-embedding-ada-002` 模型支持的最大长度。
流名称作为元数据字段 `_ab_stream` 添加到每个文档中。如果可用,记录的主键用于标识文档,以避免在索引记录的更新版本时出现重复。它作为 `_ab_record_id` 元数据字段添加。
### 嵌入
连接器可以使用以下嵌入方法之一:
1. **OpenAI** - 使用 [OpenAI API](https://beta.openai.com/docs/api-reference/text-embedding),连接器将使用具有 **1536 维**`text-embedding-ada-002` 模型生成嵌入。此集成将受到 [OpenAI 嵌入 API 速度](https://platform.openai.com/docs/guides/rate-limits/overview) 的限制。
2. **Cohere** - 使用 [Cohere API](https://docs.cohere.com/reference/embed),连接器将使用具有 **1024 维**`embed-english-light-v2.0` 模型生成嵌入。
为了测试目的,也可以使用 [虚拟嵌入](https://python.langchain.com/docs/modules/data_connection/text_embedding/integrations/fake) 集成。它将生成随机嵌入,适用于测试数据管道而不产生嵌入成本。
### 索引/数据存储
- 对于 **主机**、**端口** 和 **数据库名称**,输入您的 OpenGauss 数据库的主机名、端口号和名称。
- 列出 **默认模式**
所有流将被索引/存储到同名的表中。如果表不存在将创建表。表将具有以下列:
- `document_id`(字符串)- 文档的唯一标识符,通过附加流模式中的主键创建
- `chunk_id`(字符串)- 块的唯一标识符,通过将块编号附加到 document_id 创建
- `metadata`(变体)- 文档的元数据,以键值对形式存储
- `document_content`(字符串)- 块的文本内容
- `embedding`(向量)- 块的嵌入,以浮点数列表形式存储
> 注意若表名超过63字节的最大标识符限制会hash+截断处理
## 更新日志
<details>
<summary>展开查看</summary>
| 版本 | 日期 | Pull Request | 主题 |
|:--------| :--------- |:--------------------------------------------------------------|:-----------------------------------------------------------------------------------------------------------------------------------------------------|
| 0.1.0 | 2025-09-01 | TBD | 添加对 openGauss DateVec 作为向量目标连接器的支持。 |
</details>
---
<div align="center">
[🔝 返回顶部](#opengauss-datavec-目标连接器) | [🇺🇸 English Version](openGauss_DataVec_Connector_Document.md)
</div>

View File

@ -0,0 +1,101 @@
# type: ignore
import os
from apify_client import ApifyClient
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import PromptTemplate
from langchain_core.runnables import RunnablePassthrough
from langchain_opengauss import OpenGauss, OpenGaussSettings
from langchain_openai import ChatOpenAI, OpenAIEmbeddings
APIFY_API_TOKEN = os.getenv("APIFY_API_TOKEN") or "YOUR-APIFY-TOKEN"
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY") or "YOUR-OPENAI-API-KEY"
OPENGAUSS_HOST = os.getenv("OPENGAUSS_HOST")
OPENGAUSS_PORT = os.getenv("OPENGAUSS_PORT")
OPENGAUSS_USER = os.getenv("OPENGAUSS_USER")
OPENGAUSS_PASSWORD = os.getenv("OPENGAUSS_PASSWORD")
OPENGAUSS_DBNAME = os.getenv("OPENGAUSS_DBNAME")
OPENGAUSS_TABLE_NAME = os.getenv("OPENGAUSS_TABLE_NAME")
client = ApifyClient(APIFY_API_TOKEN)
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
print("Starting Apify's Website Content Crawler")
print("Crawling will take some time ... you can check the progress in the Apify console")
actor_call = client.actor(actor_id="apify/website-content-crawler").call(
run_input={"maxCrawlPages": 10, "startUrls": [{"url": "https://opengauss.org/"}]}
)
print("Actor website content crawler has finished")
print(actor_call)
opengauss_integration_inputs = {
"opengaussHost": OPENGAUSS_HOST,
"opengaussPort": OPENGAUSS_PORT,
"opengaussUser": OPENGAUSS_USER,
"opengaussPassword": OPENGAUSS_PASSWORD,
"opengaussDBname": OPENGAUSS_DBNAME,
"opengaussTableName": OPENGAUSS_TABLE_NAME,
"datasetFields": ["text"],
"datasetId": actor_call["defaultDatasetId"],
"deltaUpdatesPrimaryDatasetFields": ["url"],
"expiredObjectDeletionPeriodDays": 7,
"embeddingsApiKey": OPENAI_API_KEY,
"embeddingsConfig": {
"model": "text-embedding-3-small",
},
"embeddingsProvider": "OpenAI",
"performChunking": True,
"chunkSize": 2000,
"chunkOverlap": 200
}
print("Starting Apify's OpenGauss Integration")
actor_call = client.actor("wyswyz/opengauss-integration").call(run_input=opengauss_integration_inputs)
print("Apify's OpenGauss Integration has finished")
print(actor_call)
print("Question answering using OpenGauss database")
config = OpenGaussSettings(
host=OPENGAUSS_HOST,
port=OPENGAUSS_PORT,
user=OPENGAUSS_USER,
password=OPENGAUSS_PASSWORD,
database=OPENGAUSS_DBNAME,
table_name=OPENGAUSS_TABLE_NAME,
embedding_dimension=384,
index_type="HNSW",
distance_strategy="cosine",
)
vector_store = OpenGauss(embedding=embeddings, config=config)
prompt = PromptTemplate(
input_variables=["context", "question"],
template="Use the following pieces of retrieved context to answer the question. If you don't know the answer, "
"just say that you don't know. \nQuestion: {question} \nContext: {context} \nAnswer:",
)
def format_docs(docs):
return "\n\n".join(doc.page_content for doc in docs)
rag_chain = (
{
"context": vector_store.as_retriever() | format_docs,
"question": RunnablePassthrough(),
}
| prompt
| ChatOpenAI(model="gpt-5-mini", temperature=1)
| StrOutputParser()
)
question = "什么是openGauss"
print("Question:", question)
print("Answer:", rag_chain.invoke(question))

View File

@ -0,0 +1,219 @@
# 使用 Apify 和 openGauss 构建企业级 RAG 问答系统:实战教程
本文将通过一个完整的实例,向您展示如何利用 Apify 强大的云端数据抓取与集成能力,结合 openGauss 数据库的向量存储功能,快速构建一个针对 openGauss 官方文档的 RAG 问答机器人。
**Apify 是一个云端 Web 抓取和自动化平台。** 它允许开发者轻松地从任何网站提取数据,或将任何网站的工作流程自动化。其核心是 **"Actors"**——可以执行任意任务的无服务器云程序。
Apify 平台的优势在于:
* **简化数据采集**您无需关心基础设施、IP 轮换、浏览器指纹等复杂问题Apify 会为您处理好一切。
* **丰富的 Actor 市场**Apify Store 中有大量预构建好的 Actors例如通用的网站内容爬虫Website Content Crawler、搜索引擎爬虫等可以开箱即用。
* **强大的集成能力**Apify 可以轻松地将采集到的数据推送到各种数据库、API 或云存储中,实现了从数据源到目的地的无缝衔接。
在本教程中,我们将使用两个关键的 Apify Actors
1. **`apify/website-content-crawler`**: 用于抓取 openGauss 官网的网页内容。
2. **`wyswyz/opengauss-integration`**: 用于将抓取到的内容进行处理(分块、生成向量),并自动存入 openGauss 数据库。
## **前置准备工作**
1. **Apify 账户**:注册一个 [Apify 账户](https://apify.com/) 并获取您的 API Token。
2. **OpenAI 账户**:获取您的 [OpenAI API Key](https://platform.openai.com/api-keys),用于内容向量化和问答生成。
3. **openGauss 数据库**:您需要一个的 openGauss 数据库实例,并且支持向量数据库功能
### 使用docker部署openGauss
**获取镜像**
```bash
$ docker pull opengauss/opengauss-server:latest
```
**查看镜像状态**
```bash
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
opengauss/opengauss-server latest 9763e8b26794 6 months ago 1.68GB
```
**运行容器**
```bash
$ docker run --name opengauss --privileged=true -d -e GS_PASSWORD=YourPassoword -p 8888:5432 opengauss/opengauss-server:latest
```
**验证容器运行状态**
```bash
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
c5f0b44adf9a opengauss/opengauss-server:latest "entrypoint.sh gauss…" 7 weeks ago Up 7 weeks 0.0.0.0:8888->5432/tcp opengauss
```
至此已经成功用docker部署openGauss数据库
**配置环境变量**:为了安全和方便,建议将密钥和数据库连接信息配置为环境变量。
```bash
export APIFY_API_TOKEN="YOUR-APIFY-TOKEN"
export OPENAI_API_KEY="YOUR-OPENAI-API-KEY"
export OPENGAUSS_HOST="your-db-host"
export OPENGAUSS_PORT="your-db-port"
export OPENGAUSS_USER="your-db-user"
export OPENGAUSS_PASSWORD="your-db-password"
export OPENGAUSS_DBNAME="your-db-name"
export OPENGAUSS_TABLE_NAME="opengauss_docs"
```
**Python 环境**:安装必要的库。
```bash
pip install apify-client langchain-core langchain-opengauss langchain-openai
```
## **数据采集 - 使用 Apify 网站内容爬虫**
这是 RAG 流程的第一步:获取知识。我们使用 Apify 预构建的 `website-content-crawler` Actor 来抓取 openGauss 官网的内容。
```python
import os
from apify_client import ApifyClient
# ... [环境变量加载] ...
client = ApifyClient(APIFY_API_TOKEN)
print("Starting Apify's Website Content Crawler")
print("Crawling will take some time")
# 调用 Actor 开始抓取
actor_call = client.actor(actor_id="apify/website-content-crawler").call(
run_input={
"maxCrawlPages": 10,
"startUrls": [{"url": "https://opengauss.org/"}]
}
)
print("Actor website content crawler has finished")
print(actor_call)
```
website-content-crawler的爬取行为可以通过run_input进行定制我们选择爬取openGauss的主页。为了方便演示设置最大爬取页数为10。等待actor爬取完成。
```bash
2025-09-22T13:16:41.429Z INFO PlaywrightCrawler: Finished! Total 9 requests: 9 succeeded, 0 failed.
```
## **数据处理与入库 - 使用 Apify openGauss 集成 Actor**
现在我们有了原始的网页数据,下一步是将其处理并存入 openGauss 向量数据库。这正是 `opengauss-integration` Actor 的用武之地。它会自动完成**分块 (Chunking)、向量化 (Embedding) 和存储 (Storing)** 这一系列繁琐的工作。
首先配置 openGauss 数据库的参数,并设置我们需要的字段以及分块、向量化等参数,所有参数信息可以参考[actor详情页](https://apify.com/wyswyz/opengauss-integration)
```python
# 准备 openGauss 集成 Actor 的输入参数
opengauss_integration_inputs = {
# 数据库连接信息
"opengaussHost": OPENGAUSS_HOST,
"opengaussPort": OPENGAUSS_PORT,
"opengaussUser": OPENGAUSS_USER,
"opengaussPassword": OPENGAUSS_PASSWORD,
"opengaussDBname": OPENGAUSS_DBNAME,
"opengaussTableName": OPENGAUSS_TABLE_NAME,
# 数据源和处理配置
"datasetId": actor_call["defaultDatasetId"],
"datasetFields": ["text"],
"performChunking": True,
"chunkSize": 2000,
"chunkOverlap": 200,
# 向量化配置
"embeddingsProvider": "OpenAI",
"embeddingsApiKey": OPENAI_API_KEY,
"embeddingsConfig": {
"model": "text-embedding-3-small",
},
# 其他可选配置
"deltaUpdatesPrimaryDatasetFields": ["url"],
"expiredObjectDeletionPeriodDays": 7,
}
```
接下来我们调用`wyswyz/opengauss-integration`将数据存储到openGauss数据库中
```python
print("Starting Apify's OpenGauss Integration")
# 调用 openGauss 集成 Actor
actor_call = client.actor("wyswyz/opengauss-integration").call(run_input=opengauss_integration_inputs)
print("Apify's OpenGauss Integration has finished")
print(actor_call)
```
当这个 Actor 运行完毕后,您的 openGauss 数据库中指定的表(`opengauss_docs`)就已经包含了处理好的知识数据和对应的向量。
## **构建 RAG 应用 - 使用 LangChain 进行问答**
现在我们的知识库已经准备就绪,可以使用 LangChain 来构建一个问答应用了。
```python
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import PromptTemplate
from langchain_core.runnables import RunnablePassthrough
from langchain_opengauss import OpenGauss, OpenGaussSettings
from langchain_openai import ChatOpenAI, OpenAIEmbeddings
# ... [接上文] ...
print("Question answering using OpenGauss database")
# 1. 配置 openGauss 连接
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
config = OpenGaussSettings(
host=OPENGAUSS_HOST,
port=OPENGAUSS_PORT,
user=OPENGAUSS_USER,
password=OPENGAUSS_PASSWORD,
database=OPENGAUSS_DBNAME,
table_name=OPENGAUSS_TABLE_NAME,
embedding_dimension=384,
index_type="HNSW",
distance_strategy="cosine",
)
# 初始化 LangChain 的 openGauss 向量存储
vector_store = OpenGauss(embedding=embeddings, config=config)
# 2. 定义 Prompt 模板
prompt = PromptTemplate(
input_variables=["context", "question"],
template="Use the following pieces of retrieved context to answer the question. If you don't know the answer, "
"just say that you don't know. \nQuestion: {question} \nContext: {context} \nAnswer:",
)
# 3. 定义文档格式化函数
def format_docs(docs):
return "\n\n".join(doc.page_content for doc in docs)
# 4. 构建 RAG 链 (Chain)
rag_chain = (
{
"context": vector_store.as_retriever() | format_docs,
"question": RunnablePassthrough(),
}
| prompt
| ChatOpenAI(model="gpt-5-mini", temperature=1)
| StrOutputParser()
)
# 5. 提问与回答
question = "什么是openGauss"
print("Question:", question)
print("Answer:", rag_chain.invoke(question))
```
结果如下
```bash
Question: 什么是openGauss
Answer: openGauss 是一个开源数据库及其社区(即 openGauss 社区)。该项目面向企业级应用,倡导开源开放、社区协作,提供完善的技术文档和实践案例(如资源池化、告警机制、容灾集群、一主两备部署、线程池与 RDMA 指导、在 openEuler 上安装、与 OpenStack 集成等),并通过 meetup 等线下/线上活动推动生态繁荣。
```
# 总结
通过本教程,我们利用 Apify 和 openGauss 成功构建了一个从数据采集到智能问答的端到端 RAG 应用。
此集成支持增量更新,仅更新已更改的数据。这种方法减少了不必要的嵌入计算和存储操作,使其适用于搜索和检索增强生成。
更多有关 Apify-openGauss 集成的信息,请参考集成[README文件](https://apify.com/wyswyz/opengauss-integration)。

View File

@ -0,0 +1,18 @@
# Files and folders to ignore during Docker build
.env/
.env.example
# Git
.git
.gitignore
# Docker
.docker
# Python
.pytest_cache
.idea
.mypy_cache
# mono-repo
code/.venv

View File

@ -0,0 +1,207 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[codz]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py.cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# UV
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
#uv.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
#poetry.toml
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
# https://pdm-project.org/en/latest/usage/project/#working-with-version-control
#pdm.lock
#pdm.toml
.pdm-python
.pdm-build/
# pixi
# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
#pixi.lock
# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
# in the .venv directory. It is recommended not to include this directory in version control.
.pixi
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.envrc
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
# Abstra
# Abstra is an AI-powered process automation framework.
# Ignore directories containing user credentials, local state, and settings.
# Learn more at https://abstra.io/docs
.abstra/
# Visual Studio Code
# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
# and can be added to the global gitignore or merged into this file. However, if you prefer,
# you could uncomment the following to ignore the entire vscode folder
# .vscode/
# Ruff stuff:
.ruff_cache/
# PyPI configuration file
.pypirc
# Cursor
# Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to
# exclude from AI features like autocomplete and code analysis. Recommended for sensitive data
# refer to https://docs.cursor.com/context/ignore-files
.cursorignore
.cursorindexingignore
# Marimo
marimo/_static/
marimo/_lsp/
__marimo__/

View File

@ -0,0 +1,14 @@
repos:
- repo: local
hooks:
- id: lint
name: Lint codebase
entry: make lint
language: system
pass_filenames: false
- id: type-check
name: Type-check codebase
entry: make type-check
language: system
pass_filenames: false

View File

@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@ -0,0 +1,37 @@
.PHONY: clean install-dev lint type-check check-code format
DIRS_WITH_CODE = code
DIRS_WITH_ACTORS = actors
clean:
rm -rf .venv .mypy_cache .pytest_cache .ruff_cache __pycache__
install-dev:
cd $(DIRS_WITH_CODE) && pip install --upgrade pip poetry && poetry install --with main,dev,opengauss && poetry run pre-commit install && cd ..
lint:
poetry run -C $(DIRS_WITH_CODE) ruff check
type-check:
poetry run -C $(DIRS_WITH_CODE) mypy
check-code: lint type-check
format:
poetry run -C $(DIRS_WITH_CODE) ruff check --fix
poetry run -C $(DIRS_WITH_CODE) ruff format
pydantic-model:
datamodel-codegen --input $(DIRS_WITH_ACTORS)/opengauss/.actor/input_schema.json --output $(DIRS_WITH_CODE)/src/models/opengauss_input_model.py --input-file-type jsonschema --field-constraints --enum-field-as-literal all
# Integration tests are marked with @pytest.mark.integration_test
# You will require all databased running to run these tests.
# Check docker-compose.yml for the list of databases.
test-integration:
poetry run -C $(DIRS_WITH_CODE) pytest --with-integration
test-unit:
poetry run -C $(DIRS_WITH_CODE) pytest
test: test-unit test-integration

View File

@ -0,0 +1,28 @@
# Apify Actor for OpenGauss Integrations
This project was inspired by and derived from the official `apify/actor-vector-database-integrations` repository. Special thanks to the Apify team for their foundational work.
| Actor | Actor badge |
|-----------------------------|---------------------|
| [OpenGauss](https://opengauss.org/) | [![Opengauss integration](https://apify.com/actor-badge?actor=wyswyz/opengauss-integration)](https://apify.com/wyswyz/opengauss-integration) |
#### Vector database integrations (Actors)
The Apify Vector Database Integrations facilitate the transfer of data from Apify Actors to openGauss vector database.
This process includes data processing, optional splitting into chunks, embedding computation, and data storage
These integrations support incremental updates, ensuring that only changed data is updated.
This reduces unnecessary embedding computation and storage operations, making it ideal for search and retrieval augmented generation (RAG) use cases.
## How does it work?
1. Retrieve a dataset as output from an Actor.
2. _[Optional]_ Split text data into chunks using [langchain](https://python.langchain.com).
3. _[Optional]_ Update only changed data.
4. Compute embeddings, e.g. using [OpenAI](https://platform.openai.com/docs/guides/embeddings) or [Cohere](https://cohere.com/embeddings).
5. Save data into the database.
## Supported Vector Embeddings
- [OpenAI](https://platform.openai.com/docs/guides/embeddings)
- [Cohere](https://cohere.com/embeddings)

View File

@ -0,0 +1,15 @@
{
"actorSpecification": 1,
"name": "opengauss-integration",
"title": "openGauss Integration",
"description": "Upload a dataset to openGauss",
"version": "0.0",
"input": "./input_schema.json",
"dockerfile": "../../../shared/Dockerfile",
"readme": "./README.md",
"changelog":"../../../shared/CHANGELOG.md",
"storages": {
"dataset": "../../../shared/dataset_schema.json"
},
"dockerContextDir": "../../.."
}

View File

@ -0,0 +1,180 @@
{
"title": "openGauss-integration",
"type": "object",
"schemaVersion": 1,
"properties": {
"opengaussHost": {
"title": "openGauss Host",
"type": "string",
"description": "The Host of openGauss",
"editor": "textfield"
},
"opengaussPort": {
"title": "openGauss Port",
"type": "string",
"description": "The Port of openGauss",
"editor": "textfield"
},
"opengaussUser": {
"title": "openGauss User",
"type": "string",
"description": "The User of openGauss",
"editor": "textfield"
},
"opengaussPassword": {
"title": "openGauss Password",
"type": "string",
"description": "The Password of openGauss",
"editor": "textfield"
},
"opengaussDBname": {
"title": "openGauss DBname",
"type": "string",
"description": "The DBname of openGauss",
"editor": "textfield"
},
"opengaussTableName": {
"title": "openGauss SQL table name",
"type": "string",
"description": "The name of the table to use",
"editor": "textfield"
},
"embeddingsProvider": {
"title": "Embeddings provider (as defined in the langchain API)",
"description": "Choose the embeddings provider to use for generating embeddings",
"type": "string",
"editor": "select",
"enum": ["OpenAI", "Cohere"],
"default": "OpenAI",
"sectionCaption": "Embeddings settings"
},
"embeddingsConfig": {
"title": "Configuration for embeddings provider",
"description": "Configure the parameters for the LangChain embedding class. Key points to consider:\n\n1. Typically, you only need to specify the model name. For example, for OpenAI, set the model name as {\"model\": \"text-embedding-3-small\"}.\n\n2. It's required to ensure that the vector size of your embeddings matches the size of embeddings in the database.\n\n3. Here are examples of embedding models:\n - [OpenAI](https://platform.openai.com/docs/guides/embeddings): `text-embedding-3-small`, `text-embedding-3-large`, etc.\n - [Cohere](https://docs.cohere.com/docs/cohere-embed): `embed-english-v3.0`, `embed-multilingual-light-v3.0`, etc.\n\n4. For more details about other parameters, refer to the [LangChain documentation](https://python.langchain.com/docs/integrations/text_embedding/).",
"type": "object",
"editor": "json"
},
"embeddingsApiKey": {
"title": "Embeddings API KEY (whenever applicable, depends on provider)",
"description": "Value of the API KEY for the embeddings provider (if required).\n\n For example for OpenAI it is OPENAI_API_KEY, for Cohere it is COHERE_API_KEY)",
"type": "string",
"editor": "textfield",
"isSecret": true
},
"datasetFields": {
"title": "Dataset fields to select from the dataset results and store in the database",
"type": "array",
"description": "This array specifies the dataset fields to be selected and stored in the vector store. Only the fields listed here will be included in the vector store.\n\nFor instance, when using the Website Content Crawler, you might choose to include fields such as `text`, `url`, and `metadata.title` in the vector store.",
"default": ["text"],
"prefill": ["text"],
"editor": "stringList",
"sectionCaption": "Dataset settings"
},
"metadataDatasetFields": {
"title": "Dataset fields to select from the dataset and store as metadata in the database",
"type": "object",
"description": "A list of dataset fields which should be selected from the dataset and stored as metadata in the vector stores.\n\nFor example, when using the Website Content Crawler, you might want to store `url` in metadata. In this case, use `metadataDatasetFields parameter as follows {\"url\": \"url\"}`",
"editor": "json"
},
"metadataObject": {
"title": "Custom object to be stored as metadata in the vector store database",
"type": "object",
"description": "This object allows you to store custom metadata for every item in the vector store.\n\nFor example, if you want to store the `domain` as metadata, use the `metadataObject` like this: {\"domain\": \"apify.com\"}.",
"editor": "json"
},
"datasetId": {
"title": "Dataset ID",
"type": "string",
"description": "Dataset ID (when running standalone without integration)",
"editor": "textfield"
},
"dataUpdatesStrategy": {
"title": "Update strategy (add, upsert, deltaUpdates (default))",
"description": "Choose the update strategy for the integration. The update strategy determines how the integration updates the data in the database.\n\nThe available options are:\n\n- **Add data** (`add`):\n - Always adds new records to the database.\n - No checks for existing records or updates are performed.\n - Useful when appending data without concern for duplicates.\n\n- **Upsert data** (`upsert`):\n - Updates existing records if they match a key or identifier.\n - Inserts new records into the database if they don't already exist.\n - Ideal for ensuring the database contains the most up-to-date data, avoiding duplicates.\n\n- **Update changed data based on deltas** (`deltaUpdates`):\n - Performs incremental updates by identifying differences (deltas) between the new dataset and the existing records.\n - Only adds new records and updates those that have changed.\n - Unchanged records are left untouched.\n - Maximizes efficiency by reducing unnecessary updates.\n\nSelect the strategy that best fits your use case.",
"type": "string",
"editor": "select",
"enum": ["add", "upsert", "deltaUpdates"],
"default": "deltaUpdates",
"prefill": "deltaUpdates",
"sectionCaption": "Data updates settings"
},
"dataUpdatesPrimaryDatasetFields": {
"title": "Dataset fields to uniquely identify dataset items (only relevant when dataUpdatesStrategy is `upsert` or `deltaUpdates`)",
"type": "array",
"description": "This array contains fields that are used to uniquely identify dataset items, which helps to handle content changes across different runs.\n\nFor instance, in a web content crawling scenario, the `url` field could serve as a unique identifier for each item.",
"editor": "stringList",
"default": [
"url"
],
"prefill": [
"url"
]
},
"enableDeltaUpdates": {
"title": "Enable incremental updates for objects based on deltas (deprecated)",
"type": "boolean",
"description": "When set to true, this setting enables incremental updates for objects in the database by comparing the changes (deltas) between the crawled dataset items and the existing objects, uniquely identified by the `datasetKeysToItemId` field.\n\n The integration will only add new objects and update those that have changed, reducing unnecessary updates. The `datasetFields`, `metadataDatasetFields`, and `metadataObject` fields are used to determine the changes.",
"default": true,
"editor": "hidden"
},
"deltaUpdatesPrimaryDatasetFields": {
"title": "Dataset fields to uniquely identify dataset items (only relevant when `enableDeltaUpdates` is enabled) (deprecated)",
"type": "array",
"description": "This array contains fields that are used to uniquely identify dataset items, which helps to handle content changes across different runs.\n\nFor instance, in a web content crawling scenario, the `url` field could serve as a unique identifier for each item.",
"editor": "hidden",
"default": [
"url"
],
"prefill": [
"url"
]
},
"deleteExpiredObjects": {
"title": "Delete expired objects from the database",
"type": "boolean",
"description": "When set to true, delete objects from the database that have not been crawled for a specified period.",
"default": true
},
"expiredObjectDeletionPeriodDays": {
"title": "Delete expired objects from the database after a specified number of days",
"type": "integer",
"description": "This setting allows the integration to manage the deletion of objects from the database that have not been crawled for a specified period. It is typically used in subsequent runs after the initial crawl.\n\nWhen the value is greater than 0, the integration checks if objects have been seen within the last X days (determined by the expiration period). If the objects are expired, they are deleted from the database. The specific value for `deletedExpiredObjectsDays` depends on your use case and how frequently you crawl data.\n\nFor example, if you crawl data daily, you can set `deletedExpiredObjectsDays` to 7 days. If you crawl data weekly, you can set `deletedExpiredObjectsDays` to 30 days.",
"default": 30,
"minimum": 0,
"unit": "days",
"editor": "number"
},
"performChunking": {
"title": "Enable text chunking",
"description": "When set to true, the text will be divided into smaller chunks based on the settings provided below. Proper chunking helps optimize retrieval and ensures accurate and efficient responses.",
"default": true,
"type": "boolean",
"sectionCaption": "Text chunking settings"
},
"chunkSize": {
"title": "Maximum chunk size",
"type": "integer",
"description": "Defines the maximum number of characters in each text chunk. Choosing the right size balances between detailed context and system performance. Optimal sizes ensure high relevancy and minimal response time.",
"default": 2000,
"minimum": 1
},
"chunkOverlap": {
"title": "Chunk overlap",
"type": "integer",
"description": "Specifies the number of overlapping characters between consecutive text chunks. Adjusting this helps maintain context across chunks, which is crucial for accuracy in retrieval-augmented generation systems.",
"default": 0,
"minimum": 0
}
},
"required": [
"opengaussHost",
"opengaussPort",
"opengaussUser",
"opengaussPassword",
"opengaussDBname",
"opengaussTableName",
"embeddingsProvider",
"embeddingsApiKey",
"datasetFields"
]
}

View File

@ -0,0 +1,248 @@
# openGauss integration
The Apify openGauss integration transfers selected data from Apify Actors to a [openGauss](https://opengauss.org/) database.
It processes the data, optionally splits it into chunks, computes embeddings, and saves them to openGauss.
This integration supports incremental updates, updating only the data that has changed.
This approach reduces unnecessary embedding computation and storage operations, making it suitable for search and retrieval augmented generation (RAG) use cases.
💡 **Note**: This Actor is meant to be used together with other Actors' integration sections.
For instance, if you are using the [Website Content Crawler](https://apify.com/apify/website-content-crawler), you can activate openGauss integration to save web data as vectors to openGauss.
## 📋 How does Apify-openGauss integration work?
Apify openGauss integration computes text embeddings and store them in openGauss.
It uses [LangChain](https://www.langchain.com/) to compute embeddings and interact with [openGauss](https://opengauss.org/).
1. Retrieve a dataset as output from an Actor
2. _[Optional]_ Split text data into chunks using `langchain`'s `RecursiveCharacterTextSplitter`
(enable/disable using `performChunking` and specify `chunkSize`, `chunkOverlap`)
3. _[Optional]_ Update only changed data (select `dataUpdatesStrategy`)
4. Compute embeddings, e.g. using `OpenAI` or `Cohere` (specify `embeddings` and `embeddingsConfig`)
5. Save data into the database
## ✅ Before you start
To utilize this integration, ensure you have:
- Created or existing `openGauss` database. You need to know `opengaussHost`, `opengaussPort`, `opengaussUser`, `opengaussPassword`, `opengaussDBName`and `opengaussTableName`.
- An account to compute embeddings using one of the providers, e.g., [OpenAI](https://platform.openai.com/docs/guides/embeddings) or [Cohere](https://docs.cohere.com/docs/cohere-embed).
## 👉 Examples
The configuration consists of three parts: openGauss, embeddings provider, and data.
Ensure that the vector size of your embeddings aligns with the configuration of your openGauss.
For instance, if you're using the `text-embedding-3-small` model from `OpenAI`, it generates vectors of size `1536`.
This means your openGauss vector should also be configured to accommodate vectors of the same size, `1536` in this case.
For detailed input information refer to the [Input page]().
#### Database: openGauss
```json
{
"opengaussHost": "YOUR-opengaussHost",
"opengaussPort": "YOUR-opengaussPort",
"opengaussUser": "YOUR-opengaussUser",
"opengaussPassword": "YOUR-opengaussPassword",
"opengaussDBname": "YOUR-opengaussDBname",
"opengaussTableName": "apif_collection"
}
```
#### Embeddings provider: OpenAI
```json
{
"embeddingsProvider": "OpenAIEmbeddings",
"embeddingsApiKey": "YOUR-OPENAI-API-KEY",
"embeddingsConfig": {"model": "text-embedding-3-large"}
}
```
### Save data from Website Content Crawler to openGauss
Data is transferred in the form of a dataset from [Website Content Crawler](https://apify.com/apify/website-content-crawler), which provides a dataset with the following output fields (truncated for brevity):
```json
{
"url": "https://www.apify.com",
"text": "Apify is a platform that enables developers to build, run, and share automation tasks.",
"metadata": {"title": "Apify"}
}
```
This dataset is then processed by the openGauss integration.
In the integration settings you need to specify which fields you want to save to openGauss, e.g., `["text"]` and which of them should be used as metadata, e.g., `{"title": "metadata.title"}`.
Without any other configuration, the data is saved to openGauss as is.
```json
{
"datasetFields": ["text"],
"metadataDatasetFields": {"title": "metadata.title"}
}
```
### Create chunks from Website Content Crawler data and save them to the database
Assume that the text data from the [Website Content Crawler](https://apify.com/apify/website-content-crawler) is too long to compute embeddings.
Therefore, we need to divide the data into smaller pieces called chunks.
We can leverage LangChain's `RecursiveCharacterTextSplitter` to split the text into chunks and save them into a database.
The parameters `chunkSize` and `chunkOverlap` are important.
The settings depend on your use case where a proper chunking helps optimize retrieval and ensures accurate responses.
```json
{
"datasetFields": ["text"],
"metadataDatasetFields": {"title": "metadata.title"},
"performChunking": true,
"chunkSize": 1000,
"chunkOverlap": 0
}
```
### Configure update strategy
To control how the integration updates data in the database, use the `dataUpdatesStrategy` parameter. This parameter allows you to choose between different update strategies based on your use case, such as adding new data, upserting records, or incrementally updating records based on changes (deltas). Below are the available strategies and explanations for when to use each:
- **Add data (`add`)**:
- Appends new data to the database without checking for duplicates or updating existing records.
- Suitable for cases where deduplication or updates are unnecessary, and the data simply needs to be added.
- For example, you might use this strategy to continually append data from independent crawls without regard for overlaps.
- **Upsert data (`upsert`)**:
- Delete existing records in the database if they match a key or identifier and inserts new records.
- Ideal when you want to maintain accurate and up-to-date data while avoiding duplication.
- For instance, this is useful in cases where unique items (such as user profiles or documents) need to be managed, ensuring the database reflects the latest changes.
- Check the `dataUpdatesPrimaryDatasetFields` parameter to specify which fields are used to uniquely identify each dataset item.
- **Delta updates (`deltaUpdates`)**:
- Incrementally updates records by identifying differences (deltas) between the new dataset and the existing database records.
- Ensures only new or modified records are processed, leaving unchanged records untouched. This minimizes unnecessary database operations and improves efficiency.
- This is the most efficient strategy when integrating data that evolves over time, such as website content or recurring crawls.
- Check the `dataUpdatesPrimaryDatasetFields` parameter to specify which fields are used to uniquely identify each dataset item.
### Incrementally update database from the Website Content Crawler
To incrementally update data from the [Website Content Crawler](https://apify.com/apify/website-content-crawler) to database, configure the integration to update only the changed or new data.
This is controlled by the `dataUpdatesStrategy` setting.
This way, the integration minimizes unnecessary updates and ensures that only new or modified data is processed.
A checksum is computed for each dataset item (together with all metadata) and stored in the database alongside the vectors.
When the data is re-crawled, the checksum is recomputed and compared with the stored checksum.
If the checksum is different, the old data (including vectors) is deleted and new data is saved.
Otherwise, only the `last_seen_at` metadata field is updated to indicate when the data was last seen.
#### Provide unique identifier for each dataset item
To incrementally update the data, you need to be able to uniquely identify each dataset item.
The variable `dataUpdatesPrimaryDatasetFields` specifies which fields are used to uniquely identify each dataset item and helps track content changes across different crawls.
For instance, when working with the Website Content Crawler, you can use the URL as a unique identifier.
```json
{
"dataUpdatesStrategy": "deltaUpdates",
"dataUpdatePrimaryDatasetFields": ["url"]
}
```
To fully maximize the potential of incremental data updates, it is recommended to start with an empty database.
While it is possible to use this feature with an existing database, records that were not originally saved using a prefix or metadata will not be updated.
### Delete outdated (expired) data
The integration can delete data from the database that hasn't been crawled for a specified period, which is useful when data becomes outdated, such as when a page is removed from a website.
The deletion feature can be enabled or disabled using the `deleteExpiredObjects` setting.
For each crawl, the `last_seen_at` metadata field is created or updated.
This field records the most recent time the data object was crawled.
The `expiredObjectDeletionPeriodDays` setting is used to control number of days since the last crawl, after which the data object is considered expired.
If a database object has not been seen for more than the `expiredObjectDeletionPeriodDays`, it will be deleted automatically.
The specific value of `expiredObjectDeletionPeriodDays` depends on your use case.
- If a website is crawled daily, `expiredObjectDeletionPeriodDays` can be set to 7.
- If you crawl weekly, it can be set to 30.
To disable this feature, set `deleteExpiredObjects` to `false`.
```json
{
"deleteExpiredObjects": true,
"expiredObjectDeletionPeriodDays": 30
}
```
💡 If you are using multiple Actors to update the same database, ensure that all Actors crawl the data at the same frequency.
Otherwise, data crawled by one Actor might expire due to inconsistent crawling schedules.
## 💾 Outputs
This integration will save the selected fields from your Actor to openGauss.
## 🔢 Example configuration
#### Full Input Example for Website Content Crawler Actor with openGauss integration
```json
{
"opengaussHost": "YOUR-opengaussHost",
"opengaussPort": "YOUR-opengaussPort",
"opengaussUser": "YOUR-opengaussUser",
"opengaussPassword": "YOUR-opengaussPassword",
"opengaussDBname": "YOUR-opengaussDBname",
"opengaussTableName": "apif_collection",
"embeddingsApiKey": "YOUR-OPENAI-API-KEY",
"embeddingsConfig": {
"model": "text-embedding-3-small"
},
"embeddingsProvider": "OpenAI",
"datasetFields": [
"text"
],
"dataUpdatesStrategy": "deltaUpdates",
"dataUpdatePrimaryDatasetFields": ["url"],
"expiredObjectDeletionPeriodDays": 7,
"performChunking": true,
"chunkSize": 2000,
"chunkOverlap": 200
}
```
#### openGauss
```json
{
"opengaussHost": "YOUR-opengaussHost",
"opengaussPort": "YOUR-opengaussPort",
"opengaussUser": "YOUR-opengaussUser",
"opengaussPassword": "YOUR-opengaussPassword",
"opengaussDBname": "YOUR-opengaussDBname",
"opengaussTableName": "apif_collection"
}
```
#### OpenAI embeddings
```json
{
"embeddingsApiKey": "YOUR-OPENAI-API-KEY",
"embeddings": "OpenAI",
"embeddingsConfig": {"model": "text-embedding-3-large"}
}
```
#### Cohere embeddings
```json
{
"embeddingsApiKey": "YOUR-COHERE-API-KEY",
"embeddings": "Cohere",
"embeddingsConfig": {"model": "embed-multilingual-v3.0"}
}
```

View File

@ -0,0 +1,18 @@
# IDE configurations
.idea
.vscode
# crawlee and apify storage folders
apify_storage
crawlee_storage
storage
# python related
__pycache__
.mypy_cache
.pytest_cache
.coverage
.venv
# git folder
.git

View File

@ -0,0 +1,15 @@
root = true
[*]
indent_style = space
indent_size = 4
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
end_of_line = lf
[Makefile]
indent_style = tab
[{*.yaml, *.yml, *.toml, *.ini, *.cfg}]
indent_size = 2

View File

@ -0,0 +1,9 @@
APIFY_API_TOKEN=
OPENAI_API_KEY=
# openGauss
OPENGAUSS_HOST=
OPENGAUSS_PORT=
OPENGAUSS_USER=
OPENGAUSS_PASSWORD=
OPENGAUSS_DBNAME=

View File

@ -0,0 +1,32 @@
# This file tells Git which files shouldn't be added to source control
.idea
.DS_Store
.vscode
apify_storage
storage
.venv/
.env/
__pypackages__
dist/
build/
*.egg-info/
*.egg
__pycache__
.mypy_cache
.dmypy.json
dmypy.json
.pytest_cache
.ruff_cache
.scrapy
*.log
# Added by Apify CLI
node_modules
.venv
.env

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,4 @@
cache-dir = "/tmp/.poetry-cache"
[virtualenvs]
in-project = true

View File

@ -0,0 +1,129 @@
[tool.poetry]
# These fields are not used by Apify, fill configs in .actor/ instead
authors = ["jiri.spilka@apify.com"]
description = ""
name = "store-vector-db"
readme = "README.md"
version = "0.1.5"
package-mode = false
[tool.poetry.dependencies]
apify = "^2.7.3"
apify-client = "^1.12.2"
backoff = "^2.2.1"
langchain-cohere = "^0.3.0"
langchain-community = "^0.3.0"
langchain-core = "0.3.70"
langchain-openai = "^0.2.0"
openai = "^1.17.0"
python = ">=3.11,<3.12"
python-dotenv = "^1.0.1"
langchain-apify = "^0.1.4"
[tool.poetry.group.dev.dependencies]
coverage = "^7.5.4"
datamodel-code-generator = "^0.25.5"
ipython = "^8.23.0"
mypy = "^1.9.0"
pandas = "^2.2.2"
pre-commit = "^3.7.0"
pytest = "^8.2.0"
pytest-asyncio = "^0.23.6"
pytest-integration-mark = "^0.2.0"
ruff = "^0.3.5"
[tool.poetry.group.opengauss]
optional = true
[tool.poetry.group.opengauss.dependencies]
langchain-opengauss = "^0.1.4"
[tool.ruff]
line-length = 150
exclude = ["src/models/**", "src/examples/**"]
[tool.ruff.lint]
select = ["ALL"]
ignore = [
"ANN101", # Missing type annotation for `{name}` in method
"ANN102", # Missing type annotation for `{name}` in classmethod
"ANN401", # Dynamically typed expressions (typing.Any) are disallowed in {filename}
"BLE001", # Do not catch blind exception
"C901", # `{name}` is too complex
"COM812", # This rule may cause conflicts when used with the formatter
"D100", # Missing docstring in public module
"D104", # Missing docstring in public package
"D107", # Missing docstring in `__init__`
"EM", # flake8-errmsg
"G004", # Logging statement uses f-string
"ISC001", # This rule may cause conflicts when used with the formatter
"FIX", # flake8-fixme
"PGH003", # Use specific rule codes when ignoring type issues
"PLR0911", # Too many return statements
"PLR0913", # Too many arguments in function definition
"PLR0915", # Too many statements
"PTH", # flake8-use-pathlib
"PYI034", # `__aenter__` methods in classes like `{name}` usually return `self` at runtime
"PYI036", # The second argument in `__aexit__` should be annotated with `object` or `BaseException | None`
"S102", # Use of `exec` detected
"S105", # Possible hardcoded password assigned to
"S106", # Possible hardcoded password assigned to argument: "{name}"
"S301", # `pickle` and modules that wrap it can be unsafe when used to deserialize untrusted data, possible security issue
"S303", # Use of insecure MD2, MD4, MD5, or SHA1 hash function
"S311", # Standard pseudo-random generators are not suitable for cryptographic purposes
"TD002", # Missing author in TODO; try: `# TODO(<author_name>): ...` or `# TODO @<author_name>: ...
"TID252", # Prefer absolute imports over relative imports from parent modules
"TRY003", # Avoid specifying long messages outside the exception class
#
"D",
]
[tool.ruff.format]
quote-style = "double"
indent-style = "space"
[tool.ruff.lint.per-file-ignores]
"**/__init__.py" = [
"F401", # Unused imports
]
"**/{tests}/*" = [
"D", # Everything from the pydocstyle
"INP001", # File {filename} is part of an implicit namespace package, add an __init__.py
"PLR2004", # Magic value used in comparison, consider replacing {value} with a constant variable
"S101", # Use of assert detected
"SLF001", # Private member accessed: `{name}`
"T20", # flake8-print
"TRY301", # Abstract `raise` to an inner function
]
[tool.ruff.lint.flake8-quotes]
docstring-quotes = "double"
inline-quotes = "double"
[tool.ruff.lint.pydocstyle]
convention = "google"
#[tool.ruff.lint.isort]
#known-first-party = ["apify"]
[tool.pytest.ini_options]
addopts = "-ra"
asyncio_mode = "auto"
timeout = 1200
[tool.mypy]
python_version = "3.11"
files = ["src", "tests"]
check_untyped_defs = true
disallow_incomplete_defs = true
disallow_untyped_calls = true
disallow_untyped_decorators = true
disallow_untyped_defs = true
no_implicit_optional = true
warn_redundant_casts = true
warn_return_any = true
warn_unreachable = true
warn_unused_ignores = true
[tool.mypy-sortedcollections]
ignore_missing_imports = true

View File

@ -0,0 +1,19 @@
import asyncio
import logging
from apify.log import ActorLogFormatter
from .entrypoint import main
handler = logging.StreamHandler()
handler.setFormatter(ActorLogFormatter())
apify_client_logger = logging.getLogger("apify_client")
apify_client_logger.setLevel(logging.INFO)
apify_client_logger.addHandler(handler)
apify_logger = logging.getLogger("apify")
apify_logger.setLevel(logging.DEBUG)
apify_logger.addHandler(handler)
asyncio.run(main())

View File

@ -0,0 +1,10 @@
from __future__ import annotations
from typing import TYPE_CHECKING, TypeAlias
if TYPE_CHECKING:
from .models import OpengaussIntegration
from .vector_stores import OpenGaussDatabase
ActorInputsDb: TypeAlias = OpengaussIntegration
VectorDb: TypeAlias = OpenGaussDatabase

View File

@ -0,0 +1,15 @@
import enum
VCR_HEADERS_EXCLUDE = ["Authorization", "Api-Key"]
DAY_IN_SECONDS = 24 * 3600
class SupportedVectorStores(str, enum.Enum):
opengauss = "opengauss"
class SupportedEmbeddings(str, enum.Enum):
openai = "OpenAI"
cohere = "Cohere"
fake = "Fake"

View File

@ -0,0 +1,37 @@
from __future__ import annotations
from typing import TYPE_CHECKING
from apify import Actor
from .constants import SupportedEmbeddings
if TYPE_CHECKING:
from langchain_core.embeddings import Embeddings
async def get_embedding_provider(embeddings_name: str, api_key: str | None = None, config: dict | None = None) -> Embeddings:
"""Return the embeddings based on the user preference."""
if embeddings_name == SupportedEmbeddings.openai:
from langchain_openai.embeddings import OpenAIEmbeddings
config = config or {}
config["openai_api_key"] = api_key
return config and OpenAIEmbeddings(**config) or OpenAIEmbeddings()
if embeddings_name == SupportedEmbeddings.cohere:
from langchain_cohere import CohereEmbeddings
config = config or {}
config["cohere_api_key"] = api_key
return CohereEmbeddings(**config)
if embeddings_name == SupportedEmbeddings.fake:
from langchain_core.embeddings import FakeEmbeddings
config = config or {}
return FakeEmbeddings(**config)
await Actor.fail(status_message=f"Failed to get embeddings for embeddings: {embeddings_name} and config: {config}")
raise ValueError("Failed to get embeddings")

View File

@ -0,0 +1,64 @@
import os
from apify import Actor
from .constants import SupportedVectorStores
from .main import run_actor
from .models import OpengaussIntegration
async def main() -> None:
async with Actor:
Actor.log.info("Starting the Vector Store Actor")
if not (actor_input := await Actor.get_input() or {}):
await Actor.fail(status_message="No input provided", exit_code=1)
# Set the Apify API token if it is available in the environment variables.
if apify_api_token := os.getenv("APIFY_TOKEN"):
os.environ["APIFY_API_TOKEN"] = apify_api_token
if not (arg := os.getenv("ACTOR_PATH_IN_DOCKER_CONTEXT")):
if Actor.is_at_home():
await Actor.exit(
exit_code=100,
status_message="This Actor was built incorrectly; no environment variable specifies which Actor "
"to start. If you encounter this issue, please contact the Actor developer.",
)
arg = f"actors/{SupportedVectorStores.opengauss.value}"
Actor.log.warning(
f"The environment variable ACTOR_PATH_IN_DOCKER_CONTEXT was not specified. " f"Using default for local development: {arg}"
)
actor_type = arg.split("/")[-1]
Actor.log.info("Received start argument (vector database name): %s", actor_type)
actor_input_ensure_backward_compatibility(actor_input)
if actor_type == SupportedVectorStores.opengauss.value:
await run_actor(OpengaussIntegration(**actor_input), actor_input)
else:
await Actor.exit(
exit_code=10,
status_message=f"This Actor was built incorrectly; an unknown Actor was selected "
f"to start ({actor_type}). If you encounter this issue, please contact the Actor developer.",
)
def actor_input_ensure_backward_compatibility(actor_input: dict) -> None:
"""Ensure backward compatibility for the actor input."""
if not actor_input.get("dataUpdatesStrategy"):
# legacy update mechanism
if actor_input.get("enableDeltaUpdates") is False:
actor_input["dataUpdatesStrategy"] = "add"
else:
actor_input["dataUpdatesStrategy"] = "deltaUpdates"
else:
# for integrations that do not have updateStrategy implemented
actor_input["enableDeltaUpdates"] = actor_input["dataUpdatesStrategy"] == "deltaUpdates"
if not actor_input.get("dataUpdatesPrimaryDatasetFields"):
actor_input["dataUpdatesPrimaryDatasetFields"] = actor_input.get("deltaUpdatesPrimaryDatasetFields", [])
else:
# for integrations that do not have updateStrategy implemented
actor_input["deltaUpdatesPrimaryDatasetFields"] = actor_input.get("dataUpdatesPrimaryDatasetFields")

View File

@ -0,0 +1,129 @@
# type: ignore
import os
import time
from datetime import datetime, timezone
from dotenv import load_dotenv
from langchain_openai.embeddings import OpenAIEmbeddings
from ..models import OpengaussIntegration
from .data_examples_uuid import (
ID1, ID3, ID4A, ID4B, ID4C, ID5A, ID5B, ID5C, ID6,
crawl_1, crawl_2, expected_results,
)
from ..vcs import compare_crawled_data_with_db
from ..vector_stores.opengauss import OpenGaussDatabase
load_dotenv()
OPENGAUSS_TABLE_NAME = os.getenv("OPENGAUSS_TABLE_NAME", "apify")
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
DROP_AND_INSERT = True
db = OpenGaussDatabase(
actor_input=OpengaussIntegration(
opengaussHost=os.getenv("OPENGAUSS_HOST"),
opengaussPort=os.getenv("OPENGAUSS_PORT"),
opengaussUser=os.getenv("OPENGAUSS_USER"),
opengaussPassword=os.getenv("OPENGAUSS_PASSWORD"),
opengaussDBname=os.getenv("OPENGAUSS_DBNAME"),
opengaussTableName=OPENGAUSS_TABLE_NAME,
embeddingsProvider="OpenAI",
embeddingsApiKey=os.getenv("OPENAI_API_KEY"),
datasetFields=["text"],
),
embeddings=embeddings,
)
def wait_for_index(sec: float = 1.0):
time.sleep(sec)
if DROP_AND_INSERT:
db.delete_all()
r = db.similarity_search("text", k=100)
print("Initial results count:", len(r))
inserted = db.add_documents(documents=crawl_1, ids=[d.metadata["chunk_id"] for d in crawl_1])
print("Inserted ids:", inserted)
wait_for_index()
r = db.similarity_search("text", k=100)
print("Search results:", r)
print("Search results count:", len(r))
res = db.search_by_vector(db.dummy_vector, k=10)
print("Objects in the database:", len(res), res)
assert len(res) == 6, "Expected 6 objects in the database"
data_add, ids_update_last_seen, ids_del = compare_crawled_data_with_db(db, crawl_2)
print("Data to add", data_add)
print("Ids to update", ids_update_last_seen)
print("Ids to delete", ids_del)
assert len(data_add) == 4, "Expected 4 objects to add"
assert data_add[0].metadata["chunk_id"] == ID4C
assert data_add[1].metadata["chunk_id"] == ID5B
assert data_add[2].metadata["chunk_id"] == ID5C
assert data_add[3].metadata["chunk_id"] == ID6
assert len(ids_update_last_seen) == 1, "Expected 1 object to update"
assert ID3 in ids_update_last_seen, f"Expected {ID3} to be updated"
assert len(ids_del) == 3, "Expected 3 objects to delete"
assert ID4A in ids_del, f"Expected {ID4A} to be deleted"
assert ID4B in ids_del, f"Expected {ID4B} to be deleted"
assert ID5A in ids_del, f"Expected {ID5A} to be deleted"
# Delete data that were removed
db.delete(ids_del)
wait_for_index()
res = db.search_by_vector(db.dummy_vector, k=10)
print("Database objects after delete: ", len(res), res)
assert len(res) == 3, "Expected 3 objects in the database after deletion"
# Add new data
r = db.add_documents(data_add, ids=[d.metadata["chunk_id"] for d in data_add])
wait_for_index()
res = db.search_by_vector(db.dummy_vector, k=10)
print("Database objects after adding new", len(res), res)
ids = [r.metadata["chunk_id"] for r in res]
assert len(res) == 7, "Expected 7 objects in the database after addition"
assert ID4C in ids and ID5B in ids and ID5C in ids, "Expected new chunk_ids to be present"
# Update metadata (last_seen_at)
ts = int(datetime.now(timezone.utc).timestamp())
res = db.search_by_vector(db.dummy_vector, k=10)
# precondition in examples: ID3 initially has last_seen_at == 1
assert next(r for r in res if r.metadata["chunk_id"] == ID3).metadata["last_seen_at"] == 1
db.update_last_seen_at(ids_update_last_seen)
wait_for_index()
res = db.search_by_vector(db.dummy_vector, k=10)
assert len(res) == 7, "Expected 7 objects after metadata update"
assert next(r for r in res if r.metadata["chunk_id"] == ID3).metadata["last_seen_at"] >= ts, f"Expected {ID3} to be updated"
# delete expired objects
db.delete_expired(expired_ts=1)
wait_for_index()
res = db.search_by_vector(db.dummy_vector, k=10)
res = [r for r in res]
print("Database objects after all updates", len(res), res)
assert len(res) == 6, "Expected 6 objects after all updates"
assert next((r for r in res if r.metadata["chunk_id"] == ID1), None) is None, f"Expected {ID1} to be deleted"
# compare results with expected results
for r in expected_results:
d = db.get_by_id(r.metadata["chunk_id"])
assert d is not None, f"Expected document {r.metadata['chunk_id']} to exist"
metadata = d.metadata
assert metadata["item_id"] == r.metadata["item_id"], f"Expected item_id {r.metadata['item_id']}"
assert metadata["checksum"] == r.metadata["checksum"], f"Expected checksum {r.metadata['checksum']}"
print("DONE")

View File

@ -0,0 +1,19 @@
"""
Define crawled data for the database playground files
"""
from langchain_core.documents import Document
d1 = Document(page_content="Expired->del", metadata={"item_id": "id1", "id": "id1#1", "checksum": "1", "last_seen_at": 0})
d2 = Document(page_content="Old->not-del", metadata={"item_id": "id2", "id": "id2#2", "checksum": "2", "last_seen_at": 1})
d3a = Document(page_content="Unchanged->upt-meta", metadata={"item_id": "id3", "id": "id3#3", "checksum": "3", "last_seen_at": 1})
d3b = Document(page_content="Unchanged->upt-meta", metadata={"item_id": "id3", "id": "id3#3", "checksum": "3", "last_seen_at": 2})
d4a = Document(page_content="Changed->del", metadata={"item_id": "id4", "id": "id4#4a", "checksum": "4", "last_seen_at": 1})
d4b = Document(page_content="Changed->del", metadata={"item_id": "id4", "id": "id4#4b", "checksum": "4", "last_seen_at": 1})
d4c = Document(page_content="Changed->add-new", metadata={"item_id": "id4", "id": "id4#4c", "checksum": "0", "last_seen_at": 2})
d5 = Document(page_content="New->add", metadata={"item_id": "id5", "id": "id5#5", "checksum": "5", "last_seen_at": 2})
crawl_1 = [d1, d2, d3a, d4a, d4b]
crawl_2 = [d3b, d4c, d5]
expected_results = [d2, d3b, d4c, d5]

View File

@ -0,0 +1,33 @@
"""
Define crawled data for the database playground files
"""
from langchain_core.documents import Document
UUID = "00000000-0000-0000-0000-0000000000"
ID1 = f"{UUID}10"
ID2 = f"{UUID}20"
ID3 = f"{UUID}30"
ID4A, ID4B, ID4C = f"{UUID}4a", f"{UUID}4b", f"{UUID}4c"
ID5A, ID5B, ID5C = f"{UUID}5a", f"{UUID}5b", f"{UUID}5c"
ID6 = f"{UUID}60"
ITEM_ID1 = "id1"
ITEM_ID4 = "id4"
d1 = Document(page_content="Expired->del", metadata={"item_id": ITEM_ID1, "chunk_id": ID1, "checksum": "1", "last_seen_at": 0})
d2 = Document(page_content="Old->not-del", metadata={"item_id": "id2", "chunk_id": ID2, "checksum": "2", "last_seen_at": 1})
d3a = Document(page_content="Unchanged->upt-meta", metadata={"item_id": "id3", "chunk_id": ID3, "checksum": "3", "last_seen_at": 1})
d3b = Document(page_content="Unchanged->upt-meta", metadata={"item_id": "id3", "chunk_id": ID3, "checksum": "3", "last_seen_at": 2})
d4a = Document(page_content="Changed->del", metadata={"item_id": ITEM_ID4, "chunk_id": ID4A, "checksum": "4", "last_seen_at": 1})
d4b = Document(page_content="Changed->del", metadata={"item_id": ITEM_ID4, "chunk_id": ID4B, "checksum": "4", "last_seen_at": 1})
d4c = Document(page_content="Changed->add-new", metadata={"item_id": ITEM_ID4, "chunk_id": ID4C, "checksum": "4c", "last_seen_at": 2})
d5a = Document(page_content="Changed->del", metadata={"item_id": "id5", "chunk_id": ID5A, "checksum": "5", "last_seen_at": 1})
d5b = Document(page_content="Changed->add-new", metadata={"item_id": "id5", "chunk_id": ID5B, "checksum": "5bc", "last_seen_at": 2})
d5c = Document(page_content="Changed->add-new", metadata={"item_id": "id5", "chunk_id": ID5C, "checksum": "5bc", "last_seen_at": 2})
d6 = Document(page_content="New->add", metadata={"item_id": "id5", "chunk_id": ID6, "checksum": "6", "last_seen_at": 2})
crawl_1 = [d1, d2, d3a, d4a, d4b, d5a]
crawl_2 = [d3b, d4c, d5b, d5c, d6]
expected_results = [d2, d3b, d4c, d5b, d5c, d6]

View File

@ -0,0 +1,2 @@
class FailedToConnectToDatabaseError(Exception):
"""Failed to connect to a vector database."""

View File

@ -0,0 +1,154 @@
from __future__ import annotations
from datetime import datetime, timezone
from typing import TYPE_CHECKING
from apify import Actor
from langchain_text_splitters import RecursiveCharacterTextSplitter
from .constants import DAY_IN_SECONDS
from .emb import get_embedding_provider
from .utils import add_chunk_id, add_item_checksum, get_dataset_loader
from .vcs import delete_expired_objects, get_vector_database, update_db_with_crawled_data, upsert_db_with_crawled_data
if TYPE_CHECKING:
from langchain_core.documents import Document
from langchain_core.embeddings import Embeddings
from ._types import ActorInputsDb, VectorDb
async def run_actor(actor_input: ActorInputsDb, payload: dict) -> None:
"""Main function to run the actor.
It loads the dataset, chunks the documents if necessary and updates the vector store with the new documents while removing the old ones.
"""
payload = payload.get("payload", {})
resource = payload.get("resource", {})
if not (dataset_id := resource.get("defaultDatasetId") or actor_input.datasetId):
msg = (
"The `datasetId` is not provided. There are two ways to specify the datasetId:"
"1. Automatic Input: If this integration is used with other Actors, such as the Website Content Crawler, the datasetId should be "
"automatically passed in the 'payload'. Please check the `Input` payload to ensure the datasetId is included."
"2. Manual Input: If you are running this Actor independently, you need to manually specify the 'datasetId'. "
"You can do this by entering the dataset ID in the 'Dataset Settings' section of the Actor's input screen."
"Please verify that one of these options is correctly configured to provide the datasetId."
)
Actor.log.error(msg)
await Actor.fail(status_message=msg)
return
embeddings = await get_embeddings(actor_input)
documents = await load_dataset(actor_input, dataset_id)
documents = add_item_checksum(documents, actor_input.dataUpdatesPrimaryDatasetFields) # type: ignore[arg-type]
if actor_input.performChunking:
text_splitter = RecursiveCharacterTextSplitter(chunk_size=actor_input.chunkSize, chunk_overlap=actor_input.chunkOverlap)
documents = text_splitter.split_documents(documents)
Actor.log.info("Documents chunked to %s chunks", len(documents))
documents = add_chunk_id(documents)
try:
vcs_: VectorDb = await get_vector_database(actor_input, embeddings)
except Exception as e:
Actor.log.exception(e)
await Actor.fail(
status_message="Failed to connect/get database. Please ensure the following: "
"1. Database credentials are correct and the database is configure properly. "
"2. The vector dimension of your embedding model in the Actor input (Embedding settings -> model) matches the one set up in the database."
f" Database error message: {e}"
)
return
try:
data_update_strategy = hasattr(actor_input, "dataUpdatesStrategy") and actor_input.dataUpdatesStrategy
if data_update_strategy == "deltaUpdates":
Actor.log.info("Update database with crawled data. Delta updates enabled")
update_db_with_crawled_data(vcs_, documents)
elif data_update_strategy == "add":
vcs_.add_documents(documents)
Actor.log.info("Added %s new objects to the vector store", len(documents))
elif data_update_strategy == "upsert":
upsert_db_with_crawled_data(vcs_, documents)
else:
await Actor.fail(
status_message=f"Invalid dataUpdatesStrategy: {data_update_strategy}. "
f"Please ensure that the configuration in the Database Settings is correct."
)
if actor_input.deleteExpiredObjects:
expired_days = actor_input.expiredObjectDeletionPeriodDays or 0
ts_expired = expired_days and int(datetime.now(timezone.utc).timestamp() - expired_days * DAY_IN_SECONDS) or 0
Actor.log.info("Delete expired objects in the database: expired_days: %s", expired_days)
delete_expired_objects(vcs_, ts_expired)
await Actor.push_data([doc.dict() for doc in documents])
if hasattr(vcs_, "close"):
vcs_.close()
except Exception as e:
Actor.log.error(e)
# I had to create a msg variable to avoid a ruff lint error S608 (SQL Injection)
msg = (
"Failed to update database. Please ensure the following:"
"1. Database is configured properly."
"2. The vector dimension of your embedding model in the Actor input (Embedding settings -> model) matches the one set up in the database."
"Error message:"
)
await Actor.fail(status_message=f"{msg} {e}", exception=e)
async def get_embeddings(actor_input: ActorInputsDb) -> Embeddings: # type: ignore[return]
try:
embed_provider_name = str(actor_input.embeddingsProvider)
Actor.log.info("Get embeddings class: %s", embed_provider_name)
embeddings = await get_embedding_provider(
embed_provider_name,
actor_input.embeddingsApiKey,
actor_input.embeddingsConfig,
)
except Exception as e:
Actor.log.error(e)
await Actor.fail(status_message=f"Failed to get embeddings: {e}. Ensure that the configuration in the Embeddings Settings is correct.")
else:
return embeddings
async def load_dataset(actor_input: ActorInputsDb, dataset_id: str) -> list[Document]: # type: ignore[return]
"""Load dataset from the datasetId and extract fields from the dataset."""
# Add parameters related to chunking to every dataset item to be able to update DB when chunkSize, chunkOverlap or performChunking changes
meta_object = actor_input.metadataObject or {}
meta_object.update({"chunkSize": actor_input.chunkSize, "chunkOverlap": actor_input.chunkOverlap, "performChunking": actor_input.performChunking})
# Required for checksum calculation
# Update metadata fields with datasetFieldsToItemId for dataset loading
meta_fields = actor_input.metadataDatasetFields or {}
meta_fields.update({k: k for k in actor_input.dataUpdatesPrimaryDatasetFields or []})
Actor.log.info("Load Dataset ID %s and extract fields %s", dataset_id, actor_input.datasetFields)
try:
dataset_loader = get_dataset_loader(
str(dataset_id),
fields=actor_input.datasetFields,
meta_object=meta_object,
meta_fields=meta_fields,
)
documents = dataset_loader.load()
documents = [doc for doc in documents if doc.page_content]
Actor.log.info("Dataset loaded, number of documents: %s", len(documents))
except Exception as e:
Actor.log.error(e)
await Actor.fail(
status_message=f"Failed to load datasetId {dataset_id} due to error: {e}. Ensure the following: "
f"1. If running this Actor standalone, the dataset should exist. "
f"2. If this Actor is configured with another Actor (in the integration section), the `datasetId` should be correctly passed. "
f"3. If the problem persists, consider creating an issue."
)
else:
return documents

View File

@ -0,0 +1,2 @@
# __init__.py
from .opengauss_input_model import OpengaussIntegration

View File

@ -0,0 +1,116 @@
# generated by datamodel-codegen:
# filename: input_schema.json
# timestamp: 2025-09-19T03:51:44+00:00
from __future__ import annotations
from typing import Any, Dict, List, Optional
from pydantic import BaseModel, Field
from typing_extensions import Literal
class OpengaussIntegration(BaseModel):
opengaussHost: str = Field(
..., description='The Host of openGauss', title='openGauss Host'
)
opengaussPort: str = Field(
..., description='The Port of openGauss', title='openGauss Port'
)
opengaussUser: str = Field(
..., description='The User of openGauss', title='openGauss User'
)
opengaussPassword: str = Field(
..., description='The Password of openGauss', title='openGauss Password'
)
opengaussDBname: str = Field(
..., description='The DBname of openGauss', title='openGauss DBname'
)
opengaussTableName: str = Field(
...,
description='The name of the table to use',
title='openGauss SQL table name',
)
embeddingsProvider: Literal['OpenAI', 'Cohere'] = Field(
...,
description='Choose the embeddings provider to use for generating embeddings',
title='Embeddings provider (as defined in the langchain API)',
)
embeddingsConfig: Optional[Dict[str, Any]] = Field(
None,
description='Configure the parameters for the LangChain embedding class. Key points to consider:\n\n1. Typically, you only need to specify the model name. For example, for OpenAI, set the model name as {"model": "text-embedding-3-small"}.\n\n2. It\'s required to ensure that the vector size of your embeddings matches the size of embeddings in the database.\n\n3. Here are examples of embedding models:\n - [OpenAI](https://platform.openai.com/docs/guides/embeddings): `text-embedding-3-small`, `text-embedding-3-large`, etc.\n - [Cohere](https://docs.cohere.com/docs/cohere-embed): `embed-english-v3.0`, `embed-multilingual-light-v3.0`, etc.\n\n4. For more details about other parameters, refer to the [LangChain documentation](https://python.langchain.com/docs/integrations/text_embedding/).',
title='Configuration for embeddings provider',
)
embeddingsApiKey: str = Field(
...,
description='Value of the API KEY for the embeddings provider (if required).\n\n For example for OpenAI it is OPENAI_API_KEY, for Cohere it is COHERE_API_KEY)',
title='Embeddings API KEY (whenever applicable, depends on provider)',
)
datasetFields: List = Field(
...,
description='This array specifies the dataset fields to be selected and stored in the vector store. Only the fields listed here will be included in the vector store.\n\nFor instance, when using the Website Content Crawler, you might choose to include fields such as `text`, `url`, and `metadata.title` in the vector store.',
title='Dataset fields to select from the dataset results and store in the database',
)
metadataDatasetFields: Optional[Dict[str, Any]] = Field(
None,
description='A list of dataset fields which should be selected from the dataset and stored as metadata in the vector stores.\n\nFor example, when using the Website Content Crawler, you might want to store `url` in metadata. In this case, use `metadataDatasetFields parameter as follows {"url": "url"}`',
title='Dataset fields to select from the dataset and store as metadata in the database',
)
metadataObject: Optional[Dict[str, Any]] = Field(
None,
description='This object allows you to store custom metadata for every item in the vector store.\n\nFor example, if you want to store the `domain` as metadata, use the `metadataObject` like this: {"domain": "apify.com"}.',
title='Custom object to be stored as metadata in the vector store database',
)
datasetId: Optional[str] = Field(
None,
description='Dataset ID (when running standalone without integration)',
title='Dataset ID',
)
dataUpdatesStrategy: Optional[Literal['add', 'upsert', 'deltaUpdates']] = Field(
'deltaUpdates',
description="Choose the update strategy for the integration. The update strategy determines how the integration updates the data in the database.\n\nThe available options are:\n\n- **Add data** (`add`):\n - Always adds new records to the database.\n - No checks for existing records or updates are performed.\n - Useful when appending data without concern for duplicates.\n\n- **Upsert data** (`upsert`):\n - Updates existing records if they match a key or identifier.\n - Inserts new records into the database if they don't already exist.\n - Ideal for ensuring the database contains the most up-to-date data, avoiding duplicates.\n\n- **Update changed data based on deltas** (`deltaUpdates`):\n - Performs incremental updates by identifying differences (deltas) between the new dataset and the existing records.\n - Only adds new records and updates those that have changed.\n - Unchanged records are left untouched.\n - Maximizes efficiency by reducing unnecessary updates.\n\nSelect the strategy that best fits your use case.",
title='Update strategy (add, upsert, deltaUpdates (default))',
)
dataUpdatesPrimaryDatasetFields: Optional[List] = Field(
['url'],
description='This array contains fields that are used to uniquely identify dataset items, which helps to handle content changes across different runs.\n\nFor instance, in a web content crawling scenario, the `url` field could serve as a unique identifier for each item.',
title='Dataset fields to uniquely identify dataset items (only relevant when dataUpdatesStrategy is `upsert` or `deltaUpdates`)',
)
enableDeltaUpdates: Optional[bool] = Field(
True,
description='When set to true, this setting enables incremental updates for objects in the database by comparing the changes (deltas) between the crawled dataset items and the existing objects, uniquely identified by the `datasetKeysToItemId` field.\n\n The integration will only add new objects and update those that have changed, reducing unnecessary updates. The `datasetFields`, `metadataDatasetFields`, and `metadataObject` fields are used to determine the changes.',
title='Enable incremental updates for objects based on deltas (deprecated)',
)
deltaUpdatesPrimaryDatasetFields: Optional[List] = Field(
['url'],
description='This array contains fields that are used to uniquely identify dataset items, which helps to handle content changes across different runs.\n\nFor instance, in a web content crawling scenario, the `url` field could serve as a unique identifier for each item.',
title='Dataset fields to uniquely identify dataset items (only relevant when `enableDeltaUpdates` is enabled) (deprecated)',
)
deleteExpiredObjects: Optional[bool] = Field(
True,
description='When set to true, delete objects from the database that have not been crawled for a specified period.',
title='Delete expired objects from the database',
)
expiredObjectDeletionPeriodDays: Optional[int] = Field(
30,
description='This setting allows the integration to manage the deletion of objects from the database that have not been crawled for a specified period. It is typically used in subsequent runs after the initial crawl.\n\nWhen the value is greater than 0, the integration checks if objects have been seen within the last X days (determined by the expiration period). If the objects are expired, they are deleted from the database. The specific value for `deletedExpiredObjectsDays` depends on your use case and how frequently you crawl data.\n\nFor example, if you crawl data daily, you can set `deletedExpiredObjectsDays` to 7 days. If you crawl data weekly, you can set `deletedExpiredObjectsDays` to 30 days.',
ge=0,
title='Delete expired objects from the database after a specified number of days',
)
performChunking: Optional[bool] = Field(
True,
description='When set to true, the text will be divided into smaller chunks based on the settings provided below. Proper chunking helps optimize retrieval and ensures accurate and efficient responses.',
title='Enable text chunking',
)
chunkSize: Optional[int] = Field(
2000,
description='Defines the maximum number of characters in each text chunk. Choosing the right size balances between detailed context and system performance. Optimal sizes ensure high relevancy and minimal response time.',
ge=1,
title='Maximum chunk size',
)
chunkOverlap: Optional[int] = Field(
0,
description='Specifies the number of overlapping characters between consecutive text chunks. Adjusting this helps maintain context across chunks, which is crucial for accuracy in retrieval-augmented generation systems.',
ge=0,
title='Chunk overlap',
)

View File

@ -0,0 +1,159 @@
from __future__ import annotations
import copy
import hashlib
import logging
from collections import defaultdict
from datetime import datetime, timezone
from typing import Any
from uuid import uuid4
from langchain_apify import ApifyDatasetLoader
from langchain_core.documents import Document
EXCLUDE_KEYS_FROM_CHECKSUM = {"metadata": {"chunk_id", "id", "checksum", "last_seen_at", "item_id"}}
DAY_IN_SECONDS = 24 * 3600
logger = logging.getLogger("apify")
def get_nested_value(d: dict, keys: str) -> Any:
"""
Extract nested value from dict.
Example:
>>> get_nested_value({"a": "v1", "c1": {"c2": "v2"}}, "c1.c2")
'v2'
"""
d = copy.deepcopy(d)
for key in keys.split("."):
if d and isinstance(d, dict) and d.get(key):
d = d[key]
else:
return ""
return d
def stringify_dict(d: dict, keys: list[str]) -> str:
"""Stringify all values in a dictionary.
Example:
>>> d_ = {"a": {"text": "Apify is cool"}, "description": "Apify platform"}
>>> stringify_dict(d_, ["a.text", "description"])
'a.text: Apify is cool\\ndescription: Apify platform'
"""
return "\n".join([f"{key}: {value}" for key in keys if (value := get_nested_value(d, key))])
def get_dataset_loader(dataset_id: str, fields: list[str], meta_object: dict, meta_fields: dict) -> ApifyDatasetLoader:
"""Load dataset by dataset_id using ApifyDatasetLoader.
The dataset_mapping_function is used to map the dataset item to a Document object.
Stringify dict using the fields.
"""
return ApifyDatasetLoader(
dataset_id,
dataset_mapping_function=lambda dataset_item: Document(
page_content=stringify_dict(dataset_item, fields) or "",
metadata={
**meta_object,
**{key: get_nested_value(dataset_item, value) for key, value in meta_fields.items()},
},
),
)
def compute_hash(text: str) -> str:
"""Compute hash of the text."""
return hashlib.sha256(text.encode()).hexdigest()
def get_chunks_to_delete(chunks_prev: list[Document], chunks_current: list[Document], expired_days: float) -> tuple[list[Document], list[Document]]:
"""
Identifies chunks to be deleted based on their last seen timestamp and presence in the current run.
Compare the chunks from the previous and current runs and identify chunks that are not present
in the current run and have not been updated within the specified 'expired_days'. These chunks are marked for deletion.
"""
ids_current = {d.metadata["item_id"] for d in chunks_current}
ts_expired = int(datetime.now(timezone.utc).timestamp() - expired_days * DAY_IN_SECONDS)
chunks_expired_delete, chunks_old_keep = [], []
# chunks that have been crawled in the current run and are older than ts_expired => to delete
for d in chunks_prev:
if d.metadata["item_id"] not in ids_current:
if d.metadata["last_seen_at"] < ts_expired:
chunks_expired_delete.append(d)
else:
chunks_old_keep.append(d)
return chunks_expired_delete, chunks_old_keep
def get_chunks_to_update(chunks_prev: list[Document], chunks_current: list[Document]) -> tuple[list[Document], list[Document]]:
"""
Identifies chunks that need to be updated or added based on their unique identifiers and checksums.
Compare the chunks from the previous and current runs and identify chunks that are new or have
undergone content changes by comparing their checksums. These chunks are marked for addition. chunks that are
present in both runs but have not undergone content changes are marked for metadata update.
"""
prev_id_checksum = defaultdict(list)
for chunk in chunks_prev:
prev_id_checksum[chunk.metadata["item_id"]].append(chunk.metadata["checksum"])
chunks_add = []
chunks_update_metadata = []
for chunk in chunks_current:
if chunk.metadata["item_id"] in prev_id_checksum:
if chunk.metadata["checksum"] in prev_id_checksum[chunk.metadata["item_id"]]:
chunks_update_metadata.append(chunk)
else:
chunks_add.append(chunk)
else:
chunks_add.append(chunk)
return chunks_add, chunks_update_metadata
def add_item_last_seen_at(items: list[Document]) -> list[Document]:
"""Add last_seen_at timestamp to the metadata of each dataset item."""
for item in items:
item.metadata["last_seen_at"] = int(datetime.now(timezone.utc).timestamp())
return items
def add_item_checksum(items: list[Document], dataset_fields_to_item_id: list[str]) -> list[Document]:
"""
Adds a checksum and unique item_id to the metadata of each dataset item.
This function computes a checksum for each item based on its content and metadata, excluding certain keys.
The checksum is then added to the document's metadata. Additionally, a unique item ID is generated based on
specified keys in the document's metadata and added to the metadata as well.
"""
for item in items:
item.metadata["checksum"] = compute_hash(item.json(exclude=EXCLUDE_KEYS_FROM_CHECKSUM))
hash_str = "".join([str(item.metadata[key]) for key in dataset_fields_to_item_id])
item.metadata["item_id"] = compute_hash(hash_str)
if not hash_str:
logger.warning(
"Item_id %s was generated with an empty hash. This typically means that `dataUpdatesPrimaryDatasetFields` "
"are empty or non-existent.",
item.metadata["item_id"],
)
return add_item_last_seen_at(items)
def add_chunk_id(chunks: list[Document]) -> list[Document]:
"""For every chunk (document stored in vector db) add chunk_id to metadata.
The chunk_id is a unique identifier for each chunk and is not required, but it is better to keep it in metadata.
"""
for d in chunks:
d.metadata["chunk_id"] = d.metadata.get("chunk_id", str(uuid4()))
return chunks

View File

@ -0,0 +1,177 @@
from __future__ import annotations
import concurrent.futures
import datetime
from collections import defaultdict
from typing import TYPE_CHECKING
from apify import Actor
from langchain_core.documents import Document
from langchain_core.vectorstores import VectorStore
from .models import OpengaussIntegration
from .utils import get_chunks_to_delete, get_chunks_to_update
if TYPE_CHECKING:
from langchain.vectorstores import VectorStore
from langchain_core.embeddings import Embeddings
from ._types import ActorInputsDb, VectorDb
async def get_vector_database(actor_input: ActorInputsDb | None, embeddings: Embeddings) -> VectorDb:
"""Get database based on the integration type."""
if isinstance(actor_input, OpengaussIntegration):
from .vector_stores.opengauss import OpenGaussDatabase
return OpenGaussDatabase(actor_input, embeddings)
raise ValueError("Unknown integration type")
def update_db_with_crawled_data(vector_store: VectorDb, documents: list[Document]) -> None:
"""Update the database with new crawled data."""
Actor.log.info("Comparing crawled data with the database ...")
data_add, ids_update_last_seen, ids_del = compare_crawled_data_with_db(vector_store, documents)
Actor.log.info("Objects: to add: %s, to update last_seen_at: %s, to delete: %s", len(data_add), len(ids_update_last_seen), len(ids_del))
# Delete data that were updated
if ids_del:
vector_store.delete(ids_del)
Actor.log.info("Deleted %s objects from the vector store where the content has changed since the last update", len(ids_del))
# Add new data
if data_add:
Actor.log.info("Adding %s new objects to the vector store", len(data_add))
vector_store.add_documents(data_add, ids=[d.metadata["chunk_id"] for d in data_add])
Actor.log.info("Added %s new objects to the vector store", len(data_add))
# Update metadata data
if ids_update_last_seen:
vector_store.update_last_seen_at(ids_update_last_seen)
Actor.log.info("Updated last_seen_at metadata for %s objects", len(ids_update_last_seen))
def upsert_db_with_crawled_data(vector_store: VectorDb, documents: list[Document]) -> None:
"""Upsert crawled data into the database by first deleting all documents and then adding all the documents."""
Actor.log.info("Upsert crawled data into database")
Actor.log.info("Delete documents by item_id. This might take a while as documents are deleted one by one.")
for d in documents:
vector_store.delete_by_item_id(d.metadata["item_id"])
Actor.log.info("Delete documents by item_id. Done")
Actor.log.info("Add documents")
vector_store.add_documents(documents, ids=[d.metadata["chunk_id"] for d in documents])
Actor.log.info("Added %s new objects to the vector store", len(documents))
def delete_expired_objects(vector_store: VectorDb, timestamp_expired: int) -> None:
"""Delete expired objects from the database."""
if timestamp_expired:
dt = datetime.datetime.fromtimestamp(timestamp_expired, tz=datetime.timezone.utc)
Actor.log.info("About to delete objects from the database that were not seen since %s (timestamp: %s)", dt, timestamp_expired)
vector_store.delete_expired(timestamp_expired)
def get_items_ids_from_db(vector_store: VectorDb, data: list[Document]) -> dict[str, list[Document]]:
"""Get documents from the database by item_id."""
items_ids = {d.metadata["item_id"] for d in data}
def _get_item_id(item_id: str) -> tuple[str, list[Document]]:
return item_id, vector_store.get_by_item_id(item_id)
crawled_db = defaultdict(list)
with concurrent.futures.ThreadPoolExecutor() as executor:
future_to_item_id = {executor.submit(_get_item_id, item_id): item_id for item_id in items_ids}
for k, future in enumerate(concurrent.futures.as_completed(future_to_item_id)):
item_id = future_to_item_id[future]
if k % 1000 == 0:
Actor.log.info("Processing item_id %s (%d/%d) to compare crawled data with the database", item_id, k, len(items_ids))
try:
item_id, documents = future.result()
crawled_db[item_id].extend(documents)
except Exception as e:
Actor.log.error("Item_id %s generated an error", item_id, e)
return dict(crawled_db)
def compare_crawled_data_with_db(vector_store: VectorDb, data: list[Document]) -> tuple[list[Document], list[str], list[str]]:
"""Compare current crawled data with the data in the database. Return data to add, delete and update.
New data is added
Data that was not changed -> update metadata last_seen_at
Data that was changed -> delete and add new
"""
data_add = []
ids_delete: set[str] = set()
ids_update_last_seen: set[str] = set()
if hasattr(vector_store, "count") and vector_store.count() == 0:
return data, [], []
crawled_db = get_items_ids_from_db(vector_store, data)
for d in data:
if res := crawled_db.get(d.metadata["item_id"]):
if d.metadata["checksum"] in {r.metadata["checksum"] for r in res}:
# Because of weaviate database, we need to use chunk_id instead of id
ids_update_last_seen.update({r.metadata.get("id") or r.metadata.get("chunk_id", ""): r for r in res})
else:
ids_delete.update({r.metadata.get("id") or r.metadata.get("chunk_id", ""): r for r in res})
data_add.append(d)
else:
data_add.append(d)
return data_add, list(ids_update_last_seen), list(ids_delete)
async def update_db_with_crawled_data_using_internal_cache(
vector_store: VectorStore, documents: list[Document], cache_key_name: str, cache_kv_store_name: str, expired_days: float
) -> None:
"""
Updates the vector store with new documents and removes outdated ones.
This function uses Apify's key-value store to handle documents. Each document, along with its metadata,
is hashed and stored as a key in the key-value store.
The function performs a comparison between the current set of documents and the set from the previous runs.
It identifies new documents and those no longer present. New documents are added to the vector store, while
documents older than the specified 'expired_days' are removed.
"""
Actor.log.info("Load previous cache %s from the key-value store: %s", cache_key_name, cache_kv_store_name)
kv_store = await Actor.open_key_value_store(name=cache_kv_store_name)
previous_runs = await kv_store.get_value(cache_key_name) or {}
previous_runs = [Document.parse_obj(doc) for doc in previous_runs]
Actor.log.info("Previous runs contains: %s records", len(previous_runs))
chunks_to_add, chunks_to_update = get_chunks_to_update(previous_runs, documents)
chunks_to_delete, chunks_old_keep = get_chunks_to_delete(previous_runs, documents, expired_days=expired_days)
Actor.log.info("Chunks to add: %s, chunks to update last_seen metadata: %s", len(chunks_to_add), len(chunks_to_update))
Actor.log.info("Chunks to delete: %s", len(chunks_to_delete))
if chunks_to_delete:
vector_store.delete(ids=[x.metadata["id"] for x in chunks_to_delete if x.metadata["id"]])
Actor.log.info("Deleted %s from database", len(chunks_to_delete))
if chunks_to_add:
inserted = vector_store.add_documents(chunks_to_add, ids=[x.metadata["id"] for x in chunks_to_add if x.metadata["id"]])
Actor.log.info("Added %s documents to the vector store", len(inserted))
else:
Actor.log.info("No new documents to add")
# update cache
if current_cache := chunks_to_add + chunks_to_update + chunks_old_keep:
await kv_store.set_value(cache_key_name, current_cache)
Actor.log.info("Updated cache: %s in the key-value store with %s entries", cache_key_name, len(current_cache))
Actor.log.info("Push chunked data to the unnamed output dataset")
await Actor.push_data([doc.dict() for doc in current_cache])

View File

@ -0,0 +1,4 @@
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from .opengauss import OpenGaussDatabase

Some files were not shown because too many files have changed in this diff Show More