首先为大家推荐这个 OceanBase 开源负责人老纪的公众号 “老纪的技术唠嗑局”,会持续更新和 OceanBase 相关的各种技术内容。欢迎感兴趣的朋友们关注!

背景(What is OB FTS)

随着 4.3.5 GA 发布,OB 全文索引从功能和性能方面带来了诸多增强与提升。与之前版本局限于协助业务验证选型不同,最新版本的全文索引能够帮助用户解决实际生产中的问题。例如在系统日志分析、用户行为和画像分析等场景里,全文索引能够对数据做高效率过滤筛选、或是高质量相关性评估。最近火热的 AI 方面,将稀疏稠密向量和全文结合在一起的多路召回架构,能在具有特殊知识领域的 RAG 系统中取得更高更好的召回效果。

本文将通过一些小 demo 和测试结果对比,展示新版 OB 全文在功能、性能和易用性方面的提升。

原理简述 (How it works)

数据库中全文索引要解决的基本问题就是如何通过查询里的关键词快速有效地找到对应的文档。在 OB 存储引擎内部,用户的文档(doc)会被分词器(parser)拆分成若干关键词(word/token)。这些关键词连同文档的统计信息特征被存储在内部的辅助表(tablet)上,用于信息检索阶段的相关性评估算法(ranking)。OB 采用能够更好评估信息关联性的 BM25 算法,对用户查询语句中的关键词和存储的文档计算相关性分数,并最终输出有关联的文档和其评分。

结合 OB 已有的高性能查询引擎能力,在全文索引查询流程内,我们针对性地做了 TAAT/DAAT 流程优化、对标 ORACLE 的 functional lookup 功能以及多索引间的 index merge 等。这使得全文能结合更多复杂的查询特性,完成用户想要的数据检索。

实验 (What can I do)

介绍了那么多,我们动手体验下 OB 全文索引,同时会展示一些常用的视图和查询技巧。

集群部署与数据导入

首先我们用最新版 OB 4.3.5 BP1 搭建了两副本,一个 2c4g 的 MySQL 模式租户。

OB 内置有支持中文语言的 IK 分词器,以及比传统自然语言模式更好用的布尔模式。所以实验的数据集使用中文足球体育新闻(https://github.com/ej0cl6/SportsSum)。在 OB 内创建一张无主键分区表,包含三列变长字符串(event,date,news)。对 news 字段使用了 IK 中文分词器,并指定 max_word 模式。IK 分词器的另一种 smart 模式,和 max_word 的区别是,其在匹配到最长词语后就停止匹配更短的词语。

OB 内置的分词器还包括适合英语的 space 和 beng。以及按照字符长度分割的 ngram。

CREATE TABLE sport_data_whole(
event varchar(64),
date varchar(16),
news varchar(65535),
fulltext INDEX (news) WITH parser ik PARSER_PROPERTIES =(ik_mode = "max_word")
);

通过客户端本地文件的方式,将新闻数据集导入到表格内,时间大概是在十五秒左右。

obclient [test]>

load data /*+ parallel(8) */
local infile "/home/xiaofeng.lby/sports_data_whole.csv"
into table sport_data_whole
fields terminated by ',' lines terminated by '\n'; Query OK, 5268 rows affected (15.33 sec)

导入后共 5268 条新闻,平均文档长度在 2700 个中文字。原始数据是 57MB 左右。实际存储的总空间大小,在经过存储引擎的压缩后,连同索引不到 30MB。可以看到其中比较大的是全文索引中倒排和正排辅助表,内部存储了比较多的分词记录。

select
avg(length(news)),
count(*)
from
sport_data_whole;
+-------------------+----------+
| avg(length(news)) | count(*) |
+-------------------+----------+
| 2781.6900 | 5268 |
+-------------------+----------+
1 row in set (0.03 sec) select
*
from
oceanbase.DBA_OB_TABLE_SPACE_USAGE\G
*************************** 1. row ***************************
TABLE_ID: 500007
DATABASE_NAME: test
TABLE_NAME: sport_data_whole
OCCUPY_SIZE: 8349796
REQUIRED_SIZE: 10489856
*************************** 2. row ***************************
TABLE_ID: 500008
DATABASE_NAME: test
TABLE_NAME: __idx_500007_news
OCCUPY_SIZE: 30247553
REQUIRED_SIZE: 31461376
*************************** 3. row ***************************
TABLE_ID: 500009
DATABASE_NAME: test
TABLE_NAME: __idx_500007_fts_rowkey_doc
OCCUPY_SIZE: 70125
REQUIRED_SIZE: 77824
*************************** 4. row ***************************
TABLE_ID: 500010
DATABASE_NAME: test
TABLE_NAME: __idx_500007_fts_doc_rowkey
OCCUPY_SIZE: 73171
REQUIRED_SIZE: 77824
*************************** 5. row ***************************
TABLE_ID: 500011
DATABASE_NAME: test
TABLE_NAME: __idx_500007_news_fts_doc_word
OCCUPY_SIZE: 28302737
REQUIRED_SIZE: 29364224

利用全文索引查询

利用存储进数据库中的新闻数据集和索引,我们可以做多条件自由组合,达到高过滤性信息检索的目的。例如作为球迷的我想搜索包含有 “拜仁” 和 “乌龙球” 的新闻,推荐使用布尔模式。相较于没有索引的字符串 like 匹配,布尔模式语法上更简洁易懂,查询速度也会更快。

select
count(*)
from
sport_data_whole
where
match (news) against ('+乌龙球 +拜仁' in boolean mode);
+----------+
| count(*) |
+----------+
| 2 |
+----------+
1 row in set (0.03 sec) select
count(*)
from
sport_data_whole
where
news like '%乌龙球%'
and news like '%拜仁%';
+----------+
| count(*) |
+----------+
| 2 |
+----------+
1 row in set (0.08 sec)

对于返回的多条新闻,在输出结果中增加分值,能用来帮助我们判断那条新闻更有关联。OB 的全文支持经过 BM25 算法计算得到的相关性分数。下面可以看到 date 是 0278 的新闻,和我们查询的目的更具关联性。

select
event,
date,
match (news) against ('乌龙球 拜仁') as score
from
sport_data_whole
where
match (news) against ('+乌龙球 +拜仁' in boolean mode);
+-------+------+---------------------+
| event | date | score |
+-------+------+---------------------+
| ucl | 0278 | 0.4657063867776557 |
| ucl | 0201 | 0.41760566608994765 |
+-------+------+---------------------+
2 rows in set (0.04 sec)

布尔模式相较于自然语言,还能反向剔除一些关键词。例如每场足球比赛中几乎都有犯规行为,如果我想知道哪些比赛很激烈,但是没有红黄牌甚至没有犯规,则可以用到布尔模式里的“-”运算符。

select
count(*)
from
sport_data_whole
where
match (news) against ('+激烈 -黄牌 -红牌 -犯规' in boolean mode);
+----------+
| count(*) |
+----------+
| 31 |
+----------+
1 row in set (0.04 sec)

一个调试的小技巧,当发现全文索引的查询结果不符合预期时,通常是因为分词结果不理想。OB 提供了一个快速的 TOKENIZE 函数来辅助测试分词结果。函数支持所有分词器和对应属性。例如下面手动的分词结果,反应了词典中对于国外体育明星人名的支持还不是很好(博阿滕、格策),因此用这些人名去检索新闻的效果可能达不到预期。

select
tokenize(
'博阿滕右路反击人球分过传中,格策后点停球转身闪开角度,在门前8米处低射从皮亚托夫裆下钻进门内',
'ik',
'[{"additional_args": [{"ik_mode": "smart"}]}]'
);
+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| tokenize('博阿滕右路反击人球分过传中,格策后点停球转身闪开角度,在门前8米处低射从皮亚托夫裆下钻进门内', 'ik', '[{"additional_args": [{"ik_mode": "smart"}]}]') |
+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| ["亚", "格", "夫", "阿", "门内", "从", "下钻", "后点", "右路", "分过", "传中", "低", "转身", "球", "射", "闪开", "博", "进", "反击", "门前", "停", "人", "皮", "裆", "策", "滕", "8米处", "托", "在", "角度"] |
+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
1 row in set (0.03 sec) select
count(*)
from
sport_data_whole
where
match (news) against ('+格策 +博阿滕' in boolean mode);
+----------+
| count(*) |
+----------+
| 0 |
+----------+
1 row in set (0.04 sec)

如果想提升分词器的精准性,OB 支持修改系统词典表。当我们将上述中文人名插入到系统词典表后,重新分词的效果立竿见影。

注:词典修改后,原索引分词效果不变,需要重建索引生效。

select
tokenize(
'博阿滕右路反击人球分过传中,格策后点停球转身闪开角度,在门前8米处低射从皮亚托夫裆下钻进门内',
'ik',
'[{"additional_args": [{"ik_mode": "smart"}]}]'
);
+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| tokenize('博阿滕右路反击人球分过传中,格策后点停球转身闪开角度,在门前8米处低射从皮亚托夫裆下钻进门内', 'ik', '[{"additional_args": [{"ik_mode": "smart"}]}]') |
+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| ["门内", "从", "下钻", "后点", "右路", "分过", "传中", "低", "转身", "球", "皮亚托夫", "射", "闪开", "进", "反击", "门前", "停", "人", "裆", "8米处", "在", "角度", "格策", "博阿滕"] |
+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
1 row in set (0.04 sec) select
count(*)
from
sport_data_whole
where
match (news) against ('+格策 +博阿滕' in boolean mode);
+----------+
| count(*) |
+----------+
| 79 |
+----------+
1 row in set (0.05 sec)

实验的最后,我们来对比下全文索引与普通索引混合查询下,union merge 带来的性能提升。我们对 sport_data_whole 表的 date 列再建立一个普通局部索引。可以通过 show index 观察索引生效情况。

alter table sport_data_whole add index (date);

show index from sport_data_whole\G
*************************** 1. row ***************************
Table: sport_data_whole
Non_unique: 1
Key_name: news
Seq_in_index: 1
Column_name: news
Collation: A
Cardinality: NULL
Sub_part: NULL
Packed: NULL
Null: YES
Index_type: FULLTEXT
Comment: available
Index_comment:
Visible: YES
Expression: NULL
*************************** 2. row ***************************
Table: sport_data_whole
Non_unique: 1
Key_name: date
Seq_in_index: 1
Column_name: date
Collation: A
Cardinality: NULL
Sub_part: NULL
Packed: NULL
Null: YES
Index_type: BTREE
Comment: available
Index_comment:
Visible: YES
Expression: NULL
2 rows in set (0.00 sec)

当两个索引条件使用 OR 连接时,过滤性好的情况下,union merge 带来的收益会比扫描普通索引后再过滤(计划中有 has_functional_lookup=true)更快。从两种计划最后预估的时间上可以看到有数量级提升。

explain
select
/*+UNION_MERGE(sport_data_whole date news)*/
*
from
sport_data_whole
where
date = '0322'
or (match (news) against ('+乌龙球' in boolean mode));
+-------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| Query Plan |
+-------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| =================================================================================== |
| |ID|OPERATOR |NAME |EST.ROWS|EST.TIME(us)| |
| ----------------------------------------------------------------------------------- |
| |0 |DISTRIBUTED INDEX MERGE SCAN|sport_data_whole(date,news)|45 |9102 | |
| =================================================================================== |
| Outputs & filters: |
| ------------------------------------- |
| 0 - output([sport_data_whole.event], [sport_data_whole.date], [sport_data_whole.news]), filter([sport_data_whole.date = '0322' OR MATCH(sport_data_whole.news) |
| AGAINST('+乌龙球' IN BOOLEAN MODE)]), rowset=256 |
| access([sport_data_whole.__pk_increment], [sport_data_whole.date], [sport_data_whole.news], [sport_data_whole.event]), partitions(p0) |
| is_index_back=true, is_global_index=false, keep_ordering=true, use_index_merge=true, filter_before_indexback[false], |
| index_name: date, range_cond([sport_data_whole.date = '0322']), filter(nil) |
| index_name: news, range_cond(nil), filter(nil) |
| lookup_filter([sport_data_whole.date = '0322' OR MATCH(sport_data_whole.news) AGAINST('+乌龙球' IN BOOLEAN MODE)]) |
+-------------------------------------------------------------------------------------------------------------------------------------------------------------------+
14 rows in set (0.03 sec) explain
select
*
from
sport_data_whole
where
date = '0322'
or (match (news) against ('+乌龙球' in boolean mode));
+-------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| Query Plan |
+-------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| =========================================================== |
| |ID|OPERATOR |NAME |EST.ROWS|EST.TIME(us)| |
| ----------------------------------------------------------- |
| |0 |TABLE FULL SCAN|sport_data_whole|79 |526939 | |
| =========================================================== |
| Outputs & filters: |
| ------------------------------------- |
| 0 - output([sport_data_whole.event], [sport_data_whole.date], [sport_data_whole.news]), filter([sport_data_whole.date = '0322' OR MATCH(sport_data_whole.news) |
| AGAINST('+乌龙球' IN BOOLEAN MODE)]), rowset=256 |
| access([sport_data_whole.__pk_increment], [sport_data_whole.date], [sport_data_whole.news], [sport_data_whole.event]), partitions(p0) |
| is_index_back=false, is_global_index=false, filter_before_indexback[false], |
| range_key([sport_data_whole.__pk_increment]), range(MIN ; MAX)always true, has_functional_lookup=true |
+-------------------------------------------------------------------------------------------------------------------------------------------------------------------+
12 rows in set (0.04 sec)

MySQL 性能对比(How about the performance)

OB 的全文索引性能横向比较又是如何的呢?我们以 MySQL 的全文为例。MySQL 的中文分词能力不是很好,因此对比数据集选定在英文数据集 wikir1k(369721行,平均每行100词)上。以下分别是自然语言模式,以及布尔模式下多种场景的对比结果。

结果上:

  • OB 在需要大量分词或是返回结果的场景中,都远优于 MySQL。
  • 小结果集上,因为计算量占比不高,查询引擎的优势不明显,两者十分接近。

测试环境

OB 租户规格 8c 16g

MySQL Ver 8.0.36 for Linux on x86_64 (MySQL Community Server - GPL)

自然语言模式

# q1
select * from wikir1k where match (document) against ('and'); # q2
select * from wikir1k where match (document) against ('and') limit 10; # q3
select * from wikir1k where match (document) against ('librettists'); # q4
select * from wikir1k where match (document) against ('librettists') limit 10; # q5
select * from wikir1k where match (document) against ('alleviating librettists'); # q6
select * from wikir1k where match (document) against ('black spotted white yellow'); # q7
select * from wikir1k where match (document) against ('black spotted white yellow') limit 10; # q8
select * from wikir1k where match (document) against ('between up and down'); # q9
select * from wikir1k where match (document) against ('between up and down') limit 10; # q10
select * from wikir1k where match (document) against ('alleviating librettists modifications retelling intangible hydrographic administratively berwickshire strathaven dumfriesshire lesmahagow transhumanist musselburgh prestwick cardiganshire montgomeryshire'); # q11
select * from wikir1k where match (document) against ('alleviating librettists modifications retelling intangible hydrographic administratively berwickshire strathaven dumfriesshire lesmahagow transhumanist musselburgh prestwick cardiganshire montgomeryshire and'); # q12
select * from wikir1k where match (document) against ('alleviating librettists modifications retelling intangible hydrographic administratively berwickshire strathaven dumfriesshire lesmahagow transhumanist musselburgh prestwick cardiganshire montgomeryshire and') limit 10;
场景 OB MySQL
q1 单 token 高频词 3820458us 5718430us
q2 单 token 高频词 limit 231861us 503772us
q3 单 token 低频词 879us 672us
q4 单 token 低频词 limit 720us 700us
q5 多 token 小结果集 1591us 1100us
q6 多 token 中结果集 259700us 602221us
q7 多 token 中结果集 limit 25502us 42620us
q8 多 token 大结果集 3842391us 6846847us
q9 多 token 大结果集 limit 301362us 784024us
q10 很多 token 小结果集 22143us 10161us
q11 很多 token 大结果集 3905829us 5929343us
q12 很多 token 大结果集 limit 345968us 769970us

布尔模式

# q1: +高频词 -中频词
select * from wikir1k where match (document) against ('+and -which -his' IN BOOLEAN MODE); # q2: +高频词 -低频词
select * from wikir1k where match (document) against ('+which (+and -his)' IN BOOLEAN MODE); # q3: +中频词 (+高频词 -中频词)
select * from wikir1k where match (document) against ('+and -carabantes -bufera' IN BOOLEAN MODE); # q4: +高频词 +低频词
select * from wikir1k where match (document) against ('+and +librettists' IN BOOLEAN MODE);
场景 OB MySQL
q1: +高频词 -中频词 1586657us 2440798us
q2: +高频词 -低频词 3726508us 7974832us
q3: +中频词 (+高频词 -中频词) 3080644us 5612041us
q4: +高频词 +低频词 230284us 357580us

更上一层楼(One more thing)

综上,最新版本的全文索引,在以下方面帮助用户解决更多搜索使用上的痛点:

  1. 支持分区表,索引数据和分区数据就近存储,提高性能。
  2. 支持主表上建立多种混合索引(普通二级、全文、多值、向量等),一套数据应对不同查询目标和加速场景。
  3. 支持 IK 中文分词器和词典修改,在一些需要中文专业术语的业务里,字典维护和匹配更加易用和精准。
  4. 支持常用的自然语言和布尔模式,性能优于 MySQL,功能与性能两方面支撑业务做平替。

OB 全文索引的能力还远不止于此,结合新的技术趋势和新的数据检索场景,在后续版本,我们还会推出更多易用性功能。例如:

  1. 能支持交集能力的全面的 index merge;
  2. 以插件方式支持更丰富流行的多语言分词器;
  3. 更灵活的用户自定义词典和停词;
  4. 更常用的 term query、phrase query,compound query 功能;
  5. 在检索方面,结合 OB 的多模稀疏向量,进一步增强文档的语义理解力;
  6. 使用多路召回、动态剪裁以及底层例如 WAND 的加速算法,在质量和速度两个方面提升检索体验等等。

敬请大家期待!

老纪的技术唠嗑局 不仅希望能持续给大家带来有价值的技术分享,也希望能和大家一起为开源社区贡献力量。如果你对 OceanBase 开源社区认可,点亮一颗小星星 吧!你的每一个Star,都是我们努力的动力~

https://github.com/oceanbase/oceanbase

千呼万唤始出来 —— OB 全文索引能力史诗级增强的更多相关文章

  1. 云HBase发布全文索引服务,轻松应对复杂查询

    云HBase发布了“全文索引服务”功能,自2019年01月25日后创建的云HBase实例,可以在控制台免费开启此“全文索引服务”功能.使用此功能可以让用户在HBase之上构建功能更丰富的搜索业务,不再 ...

  2. Neo4j 3.5发布,在索引方面大幅增强

    Neo4j 3.5版本已正式发布,这也是Neo4j宣布企业版闭源以来发布的第一个版本. 这个版本在性能.资源使用率以及安全方面均有增强,我们可以先快速浏览一下这个版本: 全文索引 基于Index的快速 ...

  3. 增强学习 | AlphaGo背后的秘密

    "敢于尝试,才有突破" 2017年5月27日,当今世界排名第一的中国棋手柯洁与AlphaGo 2.0的三局对战落败.该事件标志着最新的人工智能技术在围棋竞技领域超越了人类智能,借此 ...

  4. 机器学习中模型泛化能力和过拟合现象(overfitting)的矛盾、以及其主要缓解方法正则化技术原理初探

    1. 偏差与方差 - 机器学习算法泛化性能分析 在一个项目中,我们通过设计和训练得到了一个model,该model的泛化可能很好,也可能不尽如人意,其背后的决定因素是什么呢?或者说我们可以从哪些方面去 ...

  5. 小程序升级实时音视频录制及播放能力,开放 Wi-Fi、NFC(HCE) 等硬件连接功能

    “ 小程序升级实时音视频录制及播放能力,开放 Wi-Fi.NFC(HCE) 等硬件连接功能.同时提供按需加载.自定义组件和更多访问层级等新特性,增强了第三方平台的能力,以满足日趋丰富的业务需求.” 0 ...

  6. 关于51单片机IO引脚的驱动能力与上拉电阻

    单片机的引脚,可以用程序来控制,输出高.低电平,这些可算是单片机的输出电压.但是,程序控制不了单片机的输出电流. 单片机的输出电流,很大程度上是取决于引脚上的外接器件. 单片机输出低电平时,将允许外部 ...

  7. 关于51单片机IO引脚的驱动能力与上拉电阻设计方案

    转载自:http://bbs.dianyuan.com/article/20312-2 单片机的引脚,可以用程序来控制,输出高.低电平,这些可算是单片机的输出电压.但是,程序控制不了单片机的输出电流. ...

  8. MySQL v5.1.72 + v5.6.19

    MYSQL是一个多线程的,结构化查询语言(SQL)数据库服务器.SQL 在世界上是最流行的数据库语言.MySQL 的执行性能非常高,运行速度非常快,并非常容易使用.是一个非常捧的数据库. MySQL ...

  9. CSS预编译与PostCSS以及Webpack构建CSS综合方案

    CSS全称Cascading Style Sheets(层叠样式表),用来为HTML添加样式,本质上是一种标记类语言.CSS前期发展非常迅速,1994年哈肯·维姆·莱首次提出CSS,1996年12月W ...

  10. MySQL数据库各个版本的区别

    MySQL数据库各个版本的区别 MySQL数据库 MySQL是一种开放源代码的关系型数据库管理系统(RDBMS),MySQL数据库系统使用最常用的数据库管理语言--结构化查询语言(SQL)进行数据库管 ...

随机推荐

  1. go 遍历修改切片数据

    package main import "fmt" type good struct { id int64 sum int64 } func main() { good1 := g ...

  2. msvcp110.dll丢失修复 按我的方法来,保证修复!

    方法很简单,msvcp110.dll丢失,安装Microsoft Visual C++ 2012 Redistributable Package就可以,我把修复程序的链接放下面.链接地址: 链接:ht ...

  3. 探秘Transformer系列之(16)--- 资源占用

    探秘Transformer系列之(16)--- 资源占用 目录 探秘Transformer系列之(16)--- 资源占用 文章总表 0x00 概述 0x01 背景知识 1.1 数据类型 1.2 进制& ...

  4. 移动应用APP购物车(店铺系列二)

    今天还是说移动app开发,店铺系列文章,我们经常去超市使用购物车,即一个临时的储物空间,用完清空回收.我大兄弟说, 平时很忙,录入订单的目录很多,临时有事回来要可以继续填写,提交订单后才算结束,这就是 ...

  5. 在 VS Code 中,一键安装 MCP Server!

    大家好!我是韩老师. 本文是 MCP 系列文章的第三篇.之前的两篇文章是: Code Runner MCP Server,来了! 从零开始开发一个 MCP Server! 经过之前两篇文章的介绍,相信 ...

  6. HTTP 分段下载

    GET /user_crc.bin HTTP/1.1 Host: mnif.cn Range: bytes=0-1000

  7. Lua 的os.date()

    Lua os.date() os.date## 原型:os.date ([format [, time]]) 解释:返回一个按format格式化日期.时间的字串或表. usage## 参数格式: 由原 ...

  8. Rocketmq 如何保证消息的可用性/可靠性/不丢失呢 ?

    如何保证消息的可用性/可靠性/不丢失呢 ? 消息可能在哪些阶段丢失呢?可能会在这三个阶段发生丢失:生产阶段.存储阶段.消费阶段 生产阶段 在生产阶段,主要通过请求确认机制,来保证消息的可靠传递 1.同 ...

  9. Asp.net mvc基础(十三)集合常用的扩展方法和Linq语句

    详情参考:C#之集合常用扩展方法与Linq - 冯继强fjq - 博客园 (cnblogs.com)

  10. kettle安装文件下载(含多版本)

    kettle是一款基于java开发的洗数工具,可以通过图像化的操作界面,拖拉拽的操作方式,实现数据导入导出清洗等功能,还支持编写脚本进行数据处理,功能十分强大. 本文主要记录一下kettle各版本下载 ...