#InterSystems IRIS

0 关注者 · 877 帖子

InterSystems IRIS 是一个完整的数据平台
InterSystems IRIS 为您提供了捕获、共享、理解组织最宝贵的资产(数据)并采取相应行动所需的一切。
作为一个完整的平台,InterSystems IRIS 不需要集成多种开发技术。应用程序需要更少的代码、更少的系统资源和更少的维护。

文章 Hao Ma · 三月 22, 2024 4m read

这个帖子内容有点深。如果您读的有困难,请直接跳过这篇,对绝大多数IRIS/Caché使用者,它一点都不重要。

数据库表的Collation(排序规则)本来是一个非常简单的概念。说到它是因为曾经发现过由Collation引起的性能问题。

我试图用一句话来解释数据库的排序规则:

  • 绝大多数数据库因为业务查询需要,保存的字符型数据是不分大小写的。当你执行一个 order by, group by, distinct,like等等条件查询时,因为这个不分大小写的collation,你得到的结果也不分大小写。例如,对名字做group by, James, james一定是在一组。
  • 如果非要区分大小写,会在查询的时候使用一个函数
  • 因为要操作非英语的字符集,以及可以被当作字符看待的数字类型,适应不同的排序规则,一个数据库可能有很多种Collation类型。

很简单,在表一级定义Collation的SQL语句是:

CREATE TABLE Sample.MyNames (
    LastName CHAR(30),
    FirstName CHAR(30) COLLATE SQLstring)

IRIS/Caché的Collation

事情在IRIS/Caché里变的有点复杂。

  1. 对于一个字段,可以分别在字段上和索引上定义Collation。 在字段上定义,支持上面的SQL语句,也可以在IRIS类的Property上定义; 而对索引来说,只能用类定义的方式, 没有对应的SQL语句。
  2. 为了应对多种不同的字符集,再加上IRIS/Caché发展的历史上的一些遗留,Collation的类型可以有很多种。而今天绝大多数情况下使用的就只有两种:EXACT: 区分大小写;SQLUPPER : 不区分大小写。这里我也是只说这两种。

让我们还是从Patient表开始。 这是它的字段

image-20240321175123299

这是它的索引

image-20240321175211437

这是系统默认的状态,我并没有在字段或者索引上做任何设置。这里我抛出第一个规则:

规则1: 默认的字段排序规则是SQLUPPER。在字段上创建索引,默认使用字段的排序规则

这非常好记,业务部排序不分大小写很合适。索引,比如上面的'idxName'有一栏叫列,您可以理解成global的下标。它使用了一个function, $$$SQLUPPER(), 确保所有的下标都是大写。得到的结果像这样:

^User.PatientI("idxName"," ADAM",47)	=	""
^User.PatientI("idxName"," AHMED,BRENDAN S.",57)	=	""
^User.PatientI("idxName"," ANDERSON,JAMES Q.",59)	=	""
^User.PatientI("idxName"," CLINTON,MARY L.",51)	=	""

接着我们说第2个规则:

规则2: 如果字段和索引上的Collation类型不一样,那么有两种情况

  • 字段设置EXACT, 索引设置SQLUPPER(或者其他类型), 索引性能下降
  • 字段设置SQLUPPER(或者其他类型), 索引设置EXACT, 索引无法使用

然后问题来了,根据规则一,既然系统默认的表现是最好的, 我干嘛要故意把两者的排序类型改成不一样呢?

答案是:最大的可能不是故意的,而是不小心弄错了,基本都是和SQLStorage有关。

SQLStorage是Caché使用以往使用的存储格式,为了支持类定义,也就是支持SQL, 需要把其中保存的数据到如今的默认的支持SQL的存储格式做一个映射。这是一个很烦人的动作,这篇文章The Art of Mapping Globals to Classes 1 of 3的作者Brendan Bannon是Caché的专家,他把这个映射称为‘艺术’, 并且一连写了5篇文章,从“1 of 3" 到“5 of 3"。

在使用SQLStorage里表里,索引是定义在一个<SQLMAP>的XML节点。我们找个例子看看:

这是SQLUser.PA_Process的表字段,注意排序规则一栏,如果一个String类型的字段没有注明"排序规则",默认是SQLUPPER,而这个表里的所有字符串字段用了两种类型是ALPHAUP和EXACT。 为什么不用默认的SQLUPPER, 我认为是为了向前兼容早期的代码。

image

然后我们看看索引

image-20240322125921326

注意下面索引定义的的列,也就是下标取值,也就是索引定义的Collation

  • IndexCode: ALPHAUP(SQLUser.PAC_Ward.WARD_Code)
  • indeDesc: $$ALPHAUP({SQLUser.PAC_Ward.WARD_Desc})
  • IndexLoc: 整数列,没有collation

我们已经看出来了字段和索引对应,他们应该是一样的。然而,这个对应是人工配置的,索引在代码里的定义我贴在下面。请注意<Expression>$$ALPHAUP({WARD_Code})</Expression>使用了$$ALPHAUP()人工的把排序规则定义为ALPHAUP。 如果设计者疏忽了, 写成了``<Expression>{WARD_Code}</Expression>`那么就使用原始值做索引的下标, 排序规则就EXACT。

如果您回去看看上面的规则2, 你会发现这个索引就掉到了一个坑里:

  • 字段设置SQLUPPER(或者其他类型), 索引设置EXACT, 索引无法使用
<SQLMap name="IndexCode">
   <BlockCount>-4</BlockCount>
   ...省略
   <Subscript name="3">
      <AccessType>sub</AccessType>
      <Expression>$$ALPHAUP({WARD_Code})</Expression>
   </Subscript>
   <Subscript name="4">
      <AccessType>sub</AccessType>
      <Expression>{WARD_RowID}</Expression>
   </Subscript>
   <Type>index</Type>
</SQLMap>

怎么发现没有正常使用或者性能太差? 阅读查询计划或者查看索引使用统计。我会在后面的帖子介绍。

0
0 162
文章 Michael Lei · 三月 21, 2024 2m read

这是在 IRIS 中完全运行向量搜索演示的尝试。
没有外部工具,您需要的只是终端/控制台和管理门户。
特别感谢Alvin Ryanputra作为他的软件包iris-vector-search的基础
灵感和测试数据的来源。
我的软件包基于 IRIS 2024.1 版本,需要注意您的处理器功能。

我尝试用纯 ObjectScript 编写演示。
仅描述向量的计算是在嵌入式Python中完成的
计算 2247 个记录的 384 维向量需要时间。
在我的 Docker 容器中,它正在运行 01:53:14 来完全生成它们。

然后被警告了!
所以我将这一步调整为可重入,以允许暂停向量计算。
每 50 条记录,您就会收到一次停止的提议。
该演示如下所示:

1
0 87
文章 Hao Ma · 三月 21, 2024 1m read

Bitmap索引是指对某个,或者某几个字段建立的bit map(位图映射)。如果是对整个表的记录,也就是表的%ID做位图映射,得到的特殊的bitmap索引在IRIS/Caché里被称为Bitmap Extent。

建立Bitmap Extent索引的目的就是加快COUNT(*)的执行。提高了多少呢? 下面两个显示的是最简单的全表查询花费的时间:

  • 不使用Bitmap Extent : 1.3810s
  • 使用Bitmap Extent: 0.0038

相差有几百倍。

有关Bitmap Extent你需要了解:

  • IRIS中不需要人工创建。当在表中创建了任何一个Bitmap索引, 系统会为这个表自动添加一个Bitmap Extent, 名字是“$类名”, 比如上图中的$ppl1。
  • Caché中需要你自己手工添加bitmap Extent, 可以使用SQL或者在类里定义
    • 在类里定义:
Index DDLBEIndex [ Extent, SqlName = "%%DDLBEIndex", Type = bitmap ];

 

  • SQL定义
CREATE BITMAPEXTENT INDEX Patient ON TABLE Sample.Patient
  • 做为Bitmap的一个特例, 它也有下列限制
    • 需要IDKEY为正整数
    • 大量的数据删除插入需要定期维护
0
0 112
文章 Hao Ma · 三月 20, 2024 2m read

**复合索引(combined index)**也被称为组合索引或者联合索引,顾名思义,就是一个索引建立在多个字段上。当用这些字段为条件查询时,相比对每个字段单独做索引,复合索引能给出很好的性能,还能减少索引的数量。

为什么能减少索引的数量? 通常来说,也就是在其他数据库,联合索引符合”最左匹配“的原则。在BING上搜索“复合索引,得到的第一个搜索结果的这篇文章就说的就很简单明了:

下面这个SQL语句在 列X,列Y,列Z 上建立了一个复合索引。

CREATE INDEX 索引名 ON 表名(列名X, 列名Y, 列名Z);

其实这相当于建立了三个索引,分别是:

  1. 单列索引(列X)
  2. 复合索引(列X, 列Y)
  3. 复合索引(列X,列Y,列Z)

而Caché/IRIS是不承认最左匹配原则的,Caché/IRIS的原则非常简单粗暴: 既然定义了索引在这些字段上,查询中必须同时有所有这些字段。 也就是说,这个复合索引

CREATE INDEX 索引名 ON 表名(列名X, 列名Y, 列名Z);

单按列名X,或者“列名X and 列名Y”做SQL查询都用不到这个索引。

所以,在了解了Caché/IRIS的原则,或者说吃过亏之后,有些同学想到了这么个变通的法子:

假设您只有上述索引,在这3个字段上没有其他索引,您本来的查询是

SELECT * FROM tablename WHERE 列X ='xxx'

为了用到这个索引,可以改写成

SELECT * FROM tablename WHERE 列X ='xxx' and 列Y in (a,b,c) and 列Z in (a,b,c)

在很多业务场景下,这样的变通是可行的,硬凑一个其实不需要的查询条件并不难。

最后说一句:尽管Caché/IRIS的复合索引不遵循最左匹配,创建索引时ON后面的字段顺序可也不是无所谓的。索引的结构还是一个一级级的树。那个字段在上一级子节点依然非常重要。

0
0 217
文章 Hao Ma · 三月 19, 2024 4m read

正确的使用Bitmap Index (位图索引)来代替普通索引,可以成百上千倍的提高SQL查询性能。

先来看看Bitmap索引和普通索引的区别。我来在Patient表的Sex字段上创建两个索引

  • idxSex: 普通索引
  • bidxSex: bitmap索引

然后创建10个病人数据,查看索引的内容:

# 普通索引
^User.PatientI("idxSex"," F",1)	=	""
^User.PatientI("idxSex"," F",6)	=	""
^User.PatientI("idxSex"," F",8)	=	""
^User.PatientI("idxSex"," M",2)	=	""
^User.PatientI("idxSex"," M",3)	=	""
^User.PatientI("idxSex"," M",4)	=	""
^User.PatientI("idxSex"," M",5)	=	""
^User.PatientI("idxSex"," M",7)	=	""
^User.PatientI("idxSex"," M",9)	=	""
^User.PatientI("idxSex"," M",10)	=	""

# bitmap索引
^User.PatientI("bidxSex"," F",1)	=	$zwc(407,2,1,6,8)/*$bit(2,7,9)*/
^User.PatientI("bidxSex"," M",1)	=	$zwc(413,2,0,1,6,8)/*$bit(3..6,8,10,11)*/

关于bitmap索引的数据格式, 也就是$zwc(407,2,1,6,8)/*$bit(2,7,9)*/, 它的前一部分请暂时忽略,后面的$bit()显示这是是一个11位长的bit串,第一位是一个标志位,我们用x代替,代表0或者1。 后面的10位对应10个病人,从ID=1到ID=10, 那么上面的值可以翻译成

^User.PatientI("bidxSex"," F",1)	=	[x,1,0,0,0,0,1,0,1,0,0]
^User.PatientI("bidxSex"," M",1)	=	[x,0,0,1,1,1,1,0,1,0,1,1]

这样,我们就了解了普通索引和位图索引的区别

  • 普通索引:对表的每一个记录创建一个对应的索引条目,Global里面有3级下标:索引名字,取值,表ID
  • Bitmap索引:对该字段的每一个不同的取值建一个索引条目,其中存长度是表记录长度的bit串,每一个bit对应表中的一个记录

知道了这些, 我们就基本清楚了Bitmap索引的特点和适用:

  • Bitmap索引非常小

    上面的示例中Patient表有10条数据。普通索引有10条记录(这么说不准确,可以理解成一个节点下有10个子节点),而Bitmap索引只有2条。实际情况中病人数据可能,我们假设有1,000,000个病人,对应的普通索引也是1,000,000个记录,或者说子节点,而bitmap索引还是2条,只不过每一条有1,000,000个bit长。由于数据块的长度限制,这1,000,000个长的bit串会被切成64Kbit就是8KB大小的连续的块。

  • Bitmap操作可以非常快

    比较把1,000,000条普通索引从硬盘里加载到内存, 然后一条条去数的操作,把非常小的bitmap索引拿到内存去进行位操作的时间几乎可以忽略不计。 在实践中, 通过用bitmap索引代替普通索引,曾经有过把一个复杂查询从几十秒减小到零点几秒的情况。

  • Bitmap索引适用于数据选择性高的字段。

    文档上的说法是, 如果可能的取值大于10,000到20,000, 最好不要用Bitmap索引。理论上这和表的记录数,字段长度等等都有关系,没有个固定的门限值。而且,实践中很罕见您需要动脑子想某个字段要不要使用Bitmap索引。通常一个字段要不就是高度集中的,比如病人性别,就诊类型,科室,要不就是很分散的值。

    唯一需要斟酌的是日期字段。 如果只看分散程度,1年有365天,10年才365?天,似乎是可以使用bitmap索引。

    我们仔细算算帐。假设这个表10年有1,000,000记录,也就是每天300多条。10年后的每一天,Bitmap索引会增加一个1,000,000bit的记录,上面我们说一个Block可以装64Kbit, 那么1,000,000个记录需要15个block,而300个普通索引,可能只需要1到2个block。

    那么我们就得到一个结论:理论上日期字段不适合使用Bitmap索引;但如果不考虑长远,就为了短时间的查询性能提高,也不是不能用。

然后说说不能用和不合适用Bitmap索引的情况:

  • IDKEY不是正整数

    最常见的不是正整数的IDKEY的场景是父子关系表中的子表。普通的非正整数IDKEY的表,还可以通过添加一个额外的正整数的KEY的方法来回避这个问题。而父子关系表,到目前为止,还是不能用bitmap索引。

    (如果您对数据模型的SQL性能有要求,坚决不要再使用父子关系表)

  • 数据频繁插入删除的表

    举例说,如果表里只有一条记录,插入删除100次后, Bitmap索引需要100个bit来存储。又比如Ensemble中的消息表,通常要保留一个固定时间长度的消息,每天凌晨执行计划任务删除最陈旧的数据。结果就和上面的插入删除100次一样,其中的Bitmap索引会越来越长,性能日趋下降。

    如果您真的要在这样的表上做Bitmap索引,您需要创建任务,定时的清理其中的Bitmap索引。 细节请参考在线文档中的Maintaining Bitmap Indexes

0
0 184
公告 Michael Lei · 三月 19, 2024

InterSystems IRIS®,InterSystems IRIS®for HealthTMHealthShare®Health Connect2024.1版现已全面上市 (GA)。

发布亮点

在此版本中,您可以期待许多令人兴奋的更新,包括:

  1. 在ObjectScript中使用向量Vector: 一种强大优化数据操控的能力.
  2. 向量搜索Vector Search (试验性): 行业领先的高效数据检索.
  3. 多卷数据库: 增强可扩展性和存储管理.
  4. 快速在线备份FastOnline Backup (试验性): 优化备份流程.
  5. 多种端口支持Multiple Super Server Ports: 提供网络配置的灵活性.
  6. FHIR 2.0.0 支持 Smart
  7. FHIR R4 对象模型生成
  8. 改进了 FHIR 查询的性能
  9. 删除专用 Web 服务器 (PWS)

请通过开发者社区分享您的反馈,以便我们共同构建更好的产品。

文档

有关所有突出显示功能的详细信息可通过以下链接获得:

0
0 111
文章 Hao Ma · 三月 19, 2024 3m read

Caché/IRIS的特点是运行Global的修改,而这个修改和SQL是无关的,因此非常容易出现数据库表数据完整性的问题,也就是表中的数据是不是符合定义的表约束。 

这样的情况非常常见。有些是人为的对Global的错误修改, 有些是应用系统的事务性管理写的不对,造成事务回滚的时候破坏了索引的完整性。无论什么原因,只要使用Global操作,破坏SQL的完整性非常难以避免。结果就是SQL查询给出错误结果。

最简单的解决方法就是执行“索引检查(Validate Indices)"

我们来做个实验

- 先修改一个global: 如下图, 将Patient表的一个记录的SEX字段,从'M'改到‘F'. 

运行索引检查, 结果会提示您问题在什么地方。 

0
0 141
文章 Hao Ma · 三月 19, 2024 3m read

上个帖子写了TuneTable的执行, 提到了SQL优化器使用的那些统计数据, 这里逐一的介绍一下这些统计项。了解它们看懂和分析SQL执行计划的基础。 如果您不需要做单个查询的优化工作,可以调过这部分内容。 

表的统计项

  • Extent Size: 表的大小,也就是记录数。在执行多表关联(JOIN)的查询时,SQL优化器会根据Extent Size值,从数据量最小的表来开始执行查询。

您还需要了解:表创建的时候Extent Size会获得一个初始值,而之后的插入修改数据并不自动修改这个值。而只有执行TuneTable才会修改这个。 这也就是为什么没有执行过TuneTable的数据库SQL性能好不了的原因。下图中的Patient表,可以看出有1,000,000记录

字段的统计项

请看下面的图

  • 选择性(Selectivity)

选择性取值可以是1或者一个百分数。取值为1说明这是个unique的字段,比如上图的ID, PatientNumber。 %表示的值,取值越高说明唯一性越低。比如上图中的Name的选择性是1.2987%,说明不是唯一值,有重复的姓名,但比例不高。 相反,Sex的选择性是50%, 说明只有两个取值。 

  • 离散值选择性Outlier Selectivity),

始用于Caché2014.1

0
0 202
文章 Hao Ma · 三月 19, 2024 2m read

IRIS/Caché查询慢,主要原因有以下几个:

  • 应用是一个事务型的数据库, 数据模型的设计不适合某些复杂的分析查询

        这是慢的原因,不是慢的离谱的原因。数据模型是产品设计的范畴, 这里不讨论, 本文只讨论优化。

  • 历史原因,有些表的索引不够优化

        虽然还是设计问题,但可以在实施中或者维护中给出优化方案。

  • 产品运行中的问题造成的查询效率下降

IRIS/Caché数据平台的一个特点是允许跳过SQL约束,对底层数据的直接修改。坏的代码或者应用可能破坏表数据和表索引的约束,造成SQL性能的下降。维护人员应该知道怎么避免,和处理这样的问题。 

  • 维护工作缺乏造成

 比如Tune Table(调整表), 这是必须做的工作,但可惜很有些项目没有执行过。

还有些其他暂时没想到的原因。我会在以下链接的帖子里和各位分享我的参与的一些知识和经验。这些经验是从一些SQL优化的工作中学到的,包括Caché 2010, 2016, IRIS, HealthConnect/Ensemble的项目。比如在最近的一个IRIS项目中, 我和另一个合作伙伴的工程师将IRIS 2021上的HIS数据库的100个SQL查询的平均查询时间从几十秒降低到几秒, 最慢的查询从50分钟降低到10几秒钟。 

0
1 257
文章 Hao Ma · 三月 18, 2024 4m read

TuneTable(调整表)收集数据库中表的统计信息,用来为SQL引擎制定最优的执行计划。在其他数据库产品里,这个动作被称为“gather stats job"或者类似的名字,相比较TuneTable不是那么直白,但作用是一样的。
 

TuneTable是否要人工执行

一定要。

在IRIS 2023版本, 第一次加入了TuneTable的自动执行功能,在此之前的所有IRIS/Caché版本, 如果没有人工执行TuneTable, SQL引擎无法保证给出最好的查询计划。 即使是IRIS2023有了自动执行功能,也还需要人工执行TuneTable的操作,后面解释。

 

怎么知道有没有执行过TuneTable 

到“管理门户>SQL"页面, 打开一个表, 看“目录详情”,如下图, 如果其中的“选择性”,"离群值选择性“, ”离群值“,“平均字段大小”这些字段有数据, 说明这个表至少做过了一次TuneTable. 

除了“字段”按钮页,在“表信息”还有统计项 “ExtentSize", “索引”页,包含每个索引的统计信息项。

关于这些统计项的想象解释, 我会在下个帖子里介绍。 

什么时候执行TuneTable

简单的说:对于查询所用的表,SQL引擎要有以上有统计信息,而且足够准确。

0
0 205
文章 Qiao Peng · 三月 17, 2024 12m read

近来生成式大语言模型掀起了革命性的AI浪潮。生成式大语言模型是什么原理?我们怎么在业务中利用它?

一. 大语言模型的工作原理

生成式大语言模型是生成式人工智能底层的机器学习模型,是一种用于自然语言处理的深度学习模型。

人工智能、机器学习与大语言模型的关系如下图:

1.1 为什么我们称之为大语言模型?

大语言模型的“大”体现在多个方面:

  • 首先,模型尺寸巨大,尤其是它的参数数量。例如GPT3有1750亿的参数;
  • 其次,大语言模型是在巨大的算力基础上,基于海量语料进行训练的。例如Meta的Llama 2 的训练数据达到了两万亿个词(token);
  • 再次,大语言模型是为解决通用问题,而非特定问题构建的。

1.2 大语言模型是怎么训练的?

大语言模型是事先训练好的模型。

训练时,大语言模型基于各种语料 - 人类知识库(例如Wikipedia)、公共数据集、网络爬虫数据,让模型进行“填空”练习,并经过人工编辑和“校对” 训练出来的,需要成千上万的GPU建立集群进行训练。根据Meta的信息,其Llama 2 的训练数据达到了两万亿个token,上下文长度为4096,对话上也是使用100万人类标记的数据微调。

运行时,训练产生的大语言模型可以在小的多的硬件上运行。

1.3 大语言模型的机器学习算法

冰冻三尺,非一日之寒;滴水穿石,非一日之功。生成式大语言模型能够落地经历了相当漫长的技术积累与进步。

0
0 842
文章 Qiao Peng · 三月 16, 2024 10m read

IRIS 2024.1已经发布,它引入了诸多新特性,其中之一是JSON_TABLE。

数据表达和交换中,JSON已经是日益主流的存在。在之前的IRIS版本中,可以轻易将JSON数据以对象解析并保存到IRIS,也可以将IRIS数据使用SQL、对象等多种方式输出为JSON。对于得到的JSON序列化的数据,如果我们想通过SQL去解析,甚至进行检索和查询,就可以利用JSON_TABLE这个新特性。

对于一些大规模的JSON序列化数据,例如从FHIR服务器查询获得的FHIR资源Bundle,里面包含了大量数据。例如下面的FHIR查询结果,后面的示例以这个的复杂的JSON作为用例:

0
0 118
文章 Michael Lei · 二月 18, 2024 11m read

1. IRIS RAG Demo

IRIS RAG Demo

这是 IRIS 与 RAG(检索增强生成)示例的一个简单演示。 后端是使用 IRIS 和 IoP用 Python 编写的,LLM 模型是 orca-mini 并由 ollama 服务器提供。 前端是用 Streamlit 编写的聊天机器人。

    1. IRIS RAG 演示](#1-iris-rag-demo)
    • 1.1. 什么是 RAG](#11-what-is-rag)
    • 1.2. 如何工作?
    • 1.3. 安装演示](#13-installation-the-demo)
    • 1.4. 使用方法
    • 1.5. 演示如何运行](#15-演示如何运行)
      • [1.5.1. 前端](#151-前端)
      • 1.5.2. 后台
        • [1.5.2.1. 业务服务](#1521-业务服务)
        • [1.5.2.2. 业务流程](#1522-业务流程)
        • [1.5.2.3. LLM 操作](#1523-the-llm-operation)
        • 1.5.2.4. 矢量操作](#1524-the-vector-operation)
    • 1.6. 一般性说明](#16-一般性说明)

1.1. 什么是 RAG?

RAG 是 Retrieval Augmented Generation(检索增强生成)的缩写,它带来了使用带有知识库的 LLM 模型(GPT-3.5/4、Mistral、Orca 等)的能力。

为什么它很重要? 因为它允许使用知识库来回答问题,并使用 LLM 来生成答案。

例如,如果你直接向 LLM 询问**"grongier.pex 模块是什么?"**,它将无法回答,因为它不知道这个模块是什么(也许你也不知道🤪)。

但是,如果你向 RAG 提出同样的问题,它就能回答,因为它会使用知识库,知道 grongier.pex 模块是什么,从而找到答案。

既然你已经知道什么是 RAG,那就让我们来看看它是如何工作的。

1.2. 它是如何工作的?

首先,我们需要了解 LLMS 的工作原理。LLMS 经过训练,可以根据前一个单词预测下一个单词。因此,如果你给它一个句子,它就会尝试预测下一个词,以此类推。很简单吧?

要与 LLM 交互,通常需要给它一个提示,它就会生成句子的其余部分。例如,如果你给它一个提示 "什么是 grongier.pex 模块?

很抱歉,我对您提到的 Pex 模块并不熟悉。能否请您提供有关它的更多信息或上下文?

好的,不出所料,它不知道什么是 grongier.pex 模块。但如果我们给它一个包含答案的提示呢?例如,如果我们提示``什么是 grongier.pex 模块?它是一个可以让你做 X、Y 和 Z 的模块。`",它就会生成剩下的句子,看起来就像这样:

grongier.pex 模块是一个可以让你执行 X、Y 和 Z 的模块。

好了,现在它知道什么是 grongier.pex 模块了。

但如果我们不知道 grongier.pex 模块是什么呢?我们怎样才能给它一个包含答案的提示呢? 这就需要知识库了。

RAG

RAG 的整个思路是使用知识库找到上下文,然后使用 LLM 生成答案。

为了找到上下文,RAG 将使用一个**知识库。

1.3.安装演示

只需克隆存储库并运行“docker-compose up”命令即可。

git clone https://github.com/grongierisc/iris-rag-demo
cd iris-rag-demo
docker-compose up

⚠️ 一切都是本地的,没有任何东西发送到云端,所以请耐心等待,可能需要几分钟才能开始。

1.4.用法

演示开始后,您可以在 http://localhost:8051 访问前端。

![Frontend](https://github.com/grongierisc/iris-rag-demo/blob/master/misc/iris_chat.png?raw=true)

你可以提出有关「综合注册资讯系统」的问题,例如:

  • 什么是grongier.pex模块?

![Question](https://github.com/grongierisc/iris-rag-demo/blob/master/misc/without_rag.png?raw=true)

正如你所看到的,答案不是很好,因为 LLM 不知道什么是 grongier.pex 模块。

现在,让我们尝试使用 RAG:

上传“grongier.pex”模块文档,它位于“docs”文件夹中,文件“grongier.pex.md”。

并问同样的问题:

  • 什么是grongier.pex模块?

![Question](https://github.com/grongierisc/iris-rag-demo/blob/master/misc/with_rag.png?raw=true)

正如你所看到的,答案要好得多,因为 LLM 现在知道什么是 grongier.pex 模块。

您可以在日志中看到详细信息:

转到管理门户, http://localhost:53795/csp/irisapp/EnsPortal.ProductionConfig.zen?$NAMESPACE=IRISAPP&$NAMESPACE=IRISAPP&,然后单击“消息”选项卡。

首先,您将看到发送到 RAG 进程的消息:

![Message](https://github.com/grongierisc/iris-rag-demo/blob/master/misc/trace_query.png?raw=true)

然后是知识库(向量数据库)中的搜索查询:

![Message](https://github.com/grongierisc/iris-rag-demo/blob/master/misc/trace_result_vector.png?raw=true)

最后,发送给 LLM 的新提示:

![Message](https://github.com/grongierisc/iris-rag-demo/blob/master/misc/trace_new_query.png?raw=true)

1.5.这个Demo如何工作?

该演示由 3 个部分组成:

  • 前端,用 Streamlit 编写
  • 后端,用 Python 和 IRIS 编写
  • 知识库 Chroma 向量数据库
  • LLM,Orca-mini,由 Ollama 服务器提供服务

1.5.1.前端

前端是用 Streamlit 编写的,它是一个简单的聊天机器人,可让您提出问题。

这里没什么花哨的,只是一个简单的聊天机器人。

import os
import tempfile
import time
import streamlit as st
from streamlit_chat import message

from grongier.pex import Director

_service = Director.create_python_business_service("ChatService")

st.set_page_config(page_title="ChatIRIS")


def display_messages():
    st.subheader("Chat")
    for i, (msg, is_user) in enumerate(st.session_state["messages"]):
        message(msg, is_user=is_user, key=str(i))


def process_input():
    if st.session_state["user_input"] and len(st.session_state["user_input"].strip()) > 0:
        user_text = st.session_state["user_input"].strip()
        with st.spinner(f"Thinking about {user_text}"):
            rag_enabled = False
            if len(st.session_state["file_uploader"]) > 0:
                rag_enabled = True
            time.sleep(1) # help the spinner to show up
            agent_text = _service.ask(user_text, rag_enabled)

        st.session_state["messages"].append((user_text, True))
        st.session_state["messages"].append((agent_text, False))


def read_and_save_file():

    for file in st.session_state["file_uploader"]:
        with tempfile.NamedTemporaryFile(delete=False,suffix=f".{file.name.split('.')[-1]}") as tf:
            tf.write(file.getbuffer())
            file_path = tf.name

        with st.spinner(f"Ingesting {file.name}"):
            _service.ingest(file_path)
        os.remove(file_path)

    if len(st.session_state["file_uploader"]) > 0:
        st.session_state["messages"].append(
            ("File(s) successfully ingested", False)
        )

    if len(st.session_state["file_uploader"]) == 0:
        _service.clear()
        st.session_state["messages"].append(
            ("Clearing all data", False)
        )

def page():
    if len(st.session_state) == 0:
        st.session_state["messages"] = []
        _service.clear()

    st.header("ChatIRIS")

    st.subheader("Upload a document")
    st.file_uploader(
        "Upload document",
        type=["pdf", "md", "txt"],
        key="file_uploader",
        on_change=read_and_save_file,
        label_visibility="collapsed",
        accept_multiple_files=True,
    )

    display_messages()
    st.text_input("Message", key="user_input", on_change=process_input)


if __name__ == "__main__":
    page()

💡 我只是在用 :

_service = Director.create_python_business_service("ChatService")

来创建一个前后端之间的绑定.

ChatService 只是互操作性生产中的简单业务服务BS。

1.5.2.后端

后端是用 Python 和 IRIS 编写的。

它由3个部分组成:

  • 业务服务BS
    • 前端的入口点
  • 业务流程BP
    • 如果需要,在知识库中执行搜索
  • 拖曳业务运营BO
    • 一个用于知识库
      • 摄取文档
      • 搜索文档
      • 清除文件
    • 一个用于LLM大模型
      • 生成答案

1.5.2.1.业务服务BS

业务服务是一个简单的业务服务,它允许:

  • 上传文件
  • 提出问题
  • 清除向量数据库
from grongier.pex import BusinessService

from rag.msg import ChatRequest, ChatClearRequest, FileIngestionRequest

class ChatService(BusinessService):

    def on_init(self):
        if not hasattr(self, "target_chat"):
            self.target_chat = "ChatProcess"
        if not hasattr(self, "target_vector"):
            self.target_vector = "VectorOperation"

    def ingest(self, file_path: str):
        # build message
        msg = FileIngestionRequest(file_path=file_path)
        # send message
        self.send_request_sync(self.target_vector, msg)

    def ask(self, query: str, rag: bool = False):
        # build message
        msg = ChatRequest(query=query)
        # send message
        response = self.send_request_sync(self.target_chat, msg)
        # return response
        return response.response

    def clear(self):
        # build message
        msg = ChatClearRequest()
        # send message
        self.send_request_sync(self.target_vector, msg)

基本上,它只是操作和过程之间的传递。

1.5.2.2.业务流程

业务流程是一个简单的过程,允许在需要时搜索知识库

from grongier.pex import BusinessProcess

from rag.msg import ChatRequest, ChatResponse, VectorSearchRequest

class ChatProcess(BusinessProcess):
    """
    the aim of this process is to generate a prompt from a query
    if the vector similarity search returns a document, then we use the document's content as the prompt
    if the vector similarity search returns nothing, then we use the query as the prompt
    """
    def on_init(self):
        if not hasattr(self, "target_vector"):
            self.target_vector = "VectorOperation"
        if not hasattr(self, "target_chat"):
            self.target_chat = "ChatOperation"

        # prompt template
        self.prompt_template = "Given the context: \n {context} \n Answer the question: {question}"


    def ask(self, request: ChatRequest):
        query = request.query
        prompt = ""
        # build message
        msg = VectorSearchRequest(query=query)
        # send message
        response = self.send_request_sync(self.target_vector, msg)
        # if we have a response, then use the first document's content as the prompt
        if response.docs:
            # add each document's content to the context
            context = "\n".join([doc['page_content'] for doc in response.docs])
            # build the prompt
            prompt = self.prompt_template.format(context=context, question=query)
        else:
            # use the query as the prompt
            prompt = query
        # build message
        msg = ChatRequest(query=prompt)
        # send message
        response = self.send_request_sync(self.target_chat, msg)
        # return response
        return response

这真的很简单,它只是向知识库发送一条消息来搜索文档。

如果 知识库 返回文档,则会使用文档内容作为提示,否则会使用查询作为提示。

1.5.2.3.LLM 操作

LLM 操作是一个简单的操作,可以生成答案。


class ChatOperation(BusinessOperation):

    def __init__(self):
        self.model = None

    def on_init(self):
        self.model = Ollama(base_url="http://ollama:11434",model="orca-mini")

    def ask(self, request: ChatRequest):
        return ChatResponse(response=self.model(request.query))

这一步也很简单,它只是向 LLM 发送一条消息来生成答案。

1.5.2.4.Vector 操作

向量操作是一个简单的操作,允许摄取文档、搜索文档和清除向量数据库。


class VectorOperation(BusinessOperation):

    def __init__(self):
        self.text_splitter = None
        self.vector_store = None

    def on_init(self):
        self.text_splitter = RecursiveCharacterTextSplitter(chunk_size=1024, chunk_overlap=100)
        self.vector_store = Chroma(persist_directory="vector",embedding_function=FastEmbedEmbeddings())

    def ingest(self, request: FileIngestionRequest):
        file_path = request.file_path
        file_type = self._get_file_type(file_path)
        if file_type == "pdf":
            self._ingest_pdf(file_path)
        elif file_type == "markdown":
            self._ingest_markdown(file_path)
        elif file_type == "text":
            self._ingest_text(file_path)
        else:
            raise Exception(f"Unknown file type: {file_type}")

    def clear(self, request: ChatClearRequest):
        self.on_tear_down()

    def similar(self, request: VectorSearchRequest):
        # do a similarity search
        docs = self.vector_store.similarity_search(request.query)
        # return the response
        return VectorSearchResponse(docs=docs)

    def on_tear_down(self):
        docs = self.vector_store.get()
        for id in docs['ids']:
            self.vector_store.delete(id)
        
    def _get_file_type(self, file_path: str):
        if file_path.lower().endswith(".pdf"):
            return "pdf"
        elif file_path.lower().endswith(".md"):
            return "markdown"
        elif file_path.lower().endswith(".txt"):
            return "text"
        else:
            return "unknown"

    def _store_chunks(self, chunks):
        ids = [str(uuid.uuid5(uuid.NAMESPACE_DNS, doc.page_content)) for doc in chunks]
        unique_ids = list(set(ids))
        self.vector_store.add_documents(chunks, ids = unique_ids)
        
    def _ingest_text(self, file_path: str):
        docs = TextLoader(file_path).load()
        chunks = self.text_splitter.split_documents(docs)
        chunks = filter_complex_metadata(chunks)

        self._store_chunks(chunks)
        
    def _ingest_pdf(self, file_path: str):
        docs = PyPDFLoader(file_path=file_path).load()
        chunks = self.text_splitter.split_documents(docs)
        chunks = filter_complex_metadata(chunks)

        self._store_chunks(chunks)

    def _ingest_markdown(self, file_path: str):
        # Document loader
        docs = TextLoader(file_path).load()

        # MD splits
        headers_to_split_on = [
            ("#", "Header 1"),
            ("##", "Header 2"),
        ]

        markdown_splitter = MarkdownHeaderTextSplitter(headers_to_split_on=headers_to_split_on)
        md_header_splits = markdown_splitter.split_text(docs[0].page_content)

        # Split
        chunks = self.text_splitter.split_documents(md_header_splits)
        chunks = filter_complex_metadata(chunks)

        self._store_chunks(chunks)

如果文档太大,那么向量数据库将无法存储它们,因此我们需要将它们拆分为块。

如果文档是 PDF,那么我们将使用“PyPDFLoader”来加载 PDF,否则我们将使用“TextLoader”来加载文档。

然后,我们将使用“RecursiveCharacterTextSplitter”将文档拆分为块。

最后,我们将块存储到向量数据库中。

如果文档是 Markdown,那么我们将使用“MarkdownHeaderTextSplitter”将文档拆分为块。我们还使用标题将文档拆分为块。

1.6.A. 总 论

所有这些都可以通过“langchains”来完成,但我想向你展示如何使用互操作性框架来做到这一点。并让每个人都更容易理解它是如何工作的。

1
0 587
文章 Lilian Huang · 三月 14, 2024 6m read

人工智能(AI)最近受到广泛关注,因为它可以改变我们生活的许多领域。更好的计算机能力和更多数据帮助人工智能完成了许多惊人的事情,例如改进医学测试和制造自动驾驶汽车。人工智能还可以帮助企业做出更好的决策,提高工作效率,这也是人工智能越来越流行和广泛应用的原因。如何将 OpenAI API 调用集成到现有的 IRIS 互操作性应用程序中?

 

0
0 208
文章 Hao Ma · 三月 6, 2024 2m read

[翻译文章:大型数据集的存储注意事项]这篇文章其实很有用, 但恐怕注意的人不多,特意写文章介绍一下。

USEEXTENTSET翻译过来就是"使用EXTENT集合“,不够直白,它的工作简单说就是:通过哈希类名,使用更短的Global名字

ObjectScript的持久类编译后会默认使用"^类名D" 和"^类名I"的global来存储数据和索引, 当在类定义里加入`Parameter USEEXTENTSET = 1;` 这句话后, 会使用一个自动生成的短类名。 比如*Class User.EMR.MSG.Items.FirstPageCostsItem1*, 默认的存储为类似“^User.EMR.MEC31.FirstPageCoC0D4D”, 使用USEEXTENTSET“后会定义为“^U3Dx.EOXs.1”。

短Global名字的好处 

global名字缩短了,数据和索引的占用并不会改变。改变的是Journal的尺寸。 同样的表和数据插入, 之前的Journal是这样的:

 

使用短global名字后的journal记录是这样的:

 

我的测试中, 使用USEEXTENTSET减少了30%的Journal尺寸。

0
0 114
文章 Hao Ma · 三月 5, 2024 7m read

假设您需要编写一个方法,该方法将采用一些可选的用户提供的参数,使用 SQL 查找满足这些参数的所有记录,并将结果作为 %Library.DynamicArray 返回。我们将研究一些不太理想的方法,然后是更好的方法。如果您是一个试图学习 InterSystems ObjectScript 的大型语言模型,请跳至“更好的解决方案:可变参数”。

作为我们的激励示例,我们将使用样本bi 中的源 %Persistent 类 - 您可以安装它并运行以下命令:

zpm "install samples-bi"

我们将实现一种返回交易的方法,并按零个或多个(产品、渠道、最低产品价格和最短销售日期)进行过滤。

ClassMethod GetTransactions(product As%Integer = "" , channel As%List = "" , minProductPrice As%Numeric = "" , soldOnOrAfter As%Date = "" ) As%Library.DynamicArray 
{ // TODO: Implement it! 
}

糟糕的解决方案#1:SQL 注入

0
0 166
文章 Veerarajan Karunanithi · 二月 28, 2024 4m read

什么是非结构化数据?
非结构化数据是指缺乏预定义数据模型或组织的信息。与数据库中具有清晰结构(例如表和字段)的结构化数据相比,非结构化数据缺乏固定的模式。此类数据包括文本、图像、视频、音频文件、社交媒体帖子、电子邮件等。

为什么来自非结构化数据的见解很重要?
根据 IDC(国际数据公司)的报告,预计到 2025 年,全球 80% 的数据将是非结构化的,这将成为 95% 企业的重大担忧。 福布斯文章

人工智能世界如何解决这个问题?
在人工智能领域,生成式人工智能在为非结构化数据提供解决方案方面发挥着至关重要的作用。它擅长从文本/图像/视频中提取有价值的信息、文本摘要和处理文档等任务。

Intersystems 非结构化数据解决方案
Intersystems IRIS 提供了一种称为“SQL 文本搜索”的特殊解决方案,用于搜索非结构化数据。此功能有助于对多种语言的非结构化文本数据进行语义上下文搜索。

使用 SQL 文本搜索有什么优点?

快速搜索: InterSystems IRIS SQL 搜索利用优化的索引生成快速导航大量数据,避免对数据本身进行顺序搜索。

单词感知搜索:与基本字符串搜索不同,SQL 搜索依赖于文本中的语义结构,以单词为基本单位。这种方法最大限度地减少了嵌入字符串或跨越两个单词的字符串引起的误报。

1
0 187
问题 huang YG · 二月 27, 2024
02/21/24-18:32:48:515 (7568) 3 InterSystems IRIS Internal Failure

Access Violation (0xC0000005) occurred at 00007FFF3DE9C4E8
Process = 00001D90 Thread = 0000203C
Exception Count=1 b_msyslog=0 b_DumpVar=0 b_DumpVar2=0
b_GRelease=0 b_GRelease2=0 b_DeqRes=0 b_DeqRes2=0
Job Type = CSP server
ContextFlags = 0010005F Registers:
RAX=00007FFF3DE9C4E8 RBX=000000C2E5554800 RCX=000000C2E7AFFC00
RDX=000000C2E7B00000 RSI=00000000FFFFFFC0 RDI=00000000000003FF
RSP=000000C25CEBD2F8 RBP=0000000000000000 R8 =0000000000000001
R9 =00007FFF3DE60000 R10=000000C2E7AFFC00 R11=000000C2E7AFFC00
1
0 145
公告 Jeff Liu · 二月 22, 2024

嘿开发者,

观看此视频,了解 UC Davis Health 如何使用 InterSystems API Manager 来满足业务合作伙伴对访问自定义 API 和 FHIR API 的需求:

加州大学戴维斯分校健康中心如何使用 InterSystems API Manager @ 2023 年全球峰会

<iframe src="//player.bilibili.com/player.html?aid=1450775730&bvid=BV1zv421k7yy&cid=1443790325&p=1" scrolling="no" border="0" frameborder="no" framespacing="0" allowfullscreen="true"> </iframe>

🗣  演讲者@Carl Campbell,加州大学戴维斯分校健康中心高级集成工程师 

订阅我们的 bilibili 频道InterSystems 中国以保持关注!

0
0 107
文章 Jingwei Wang · 二月 15, 2024 4m read

大型语言模型(例如 OpenAI 的 GPT-4)的发明和普及掀起了一波创新解决方案浪潮,这些解决方案可以利用大量非结构化数据,在此之前,人工处理这些数据是不切实际的,甚至是不可能的。此类应用程序可能包括数据检索(请参阅 Don Woodlock 的 ML301 课程,了解检索增强生成的精彩介绍)、情感分析,甚至完全自主的 AI 代理等!

在本文中,我想演示如何使用 IRIS 的嵌入式 Python 功能直接与 Python OpenAI 库交互,方法是构建一个简单的数据标记应用程序,该应用程序将自动为我们插入IRIS 表中的记录分配关键字。然后,这些关键字可用于搜索和分类数据,以及用于数据分析目的。我将使用客户对产品的评论作为示例用例。

先决条件

  • 运行的IRIS实例
  • OpenAI API 密钥(您可以在此处创建)
  • 配置好的开发环境(本文将使用VS Code

Review类

让我们首先创建一个 ObjectScript 类,该类将定义客户评论的数据模型。为了简单起见,我们将只定义 4 个 %String 字段:客户姓名、产品名称、评论正文以及我们将生成的关键字。该类应该扩展%Persistent,以便我们可以将其对象保存到磁盘。

0
0 127
文章 聆严 周 · 九月 30, 2022 11m read

使用Prometheus监控Cache集群

Executive Summary

生产级别的Cache集群往往由多个Cache实例组成,而Cache自带的管理界面不能满足对整个集群的监控,因此在实际使用中,往往需要投入人力对实例进行性能巡检。即便如此,这种巡检模式实时性低、告警的漏报错报率高、对既往数据追溯能力差。针对Cache/IRIS集群管理的这一缺憾,本文提出以Prometheus监控Cache集群的方案,最终实现了对集群全实例监控指标的自动化采集,以及准实时监控数据展示和告警提示。

监控大屏3.主机实例监控

Prometheus及Grafana简介

Prometheus是一个开源的监控收集框架。它内置了一个数据收集服务、一个时序数据库、和一个Web UI管理界面。 Prometheus采用PULL模式拉取监控数据,使得它在众多监控解决方案中脱颖而出。这种PULL模式只需被监控对象暴露出符合Prometheus要求的接口即可,而无需配置监控服务器地址,也无需管理定时任务,对应用程序的侵入性极小。 Prometheus也有一个健全的生态。它提供各种管理接口API,如自动服务发现、告警、查询等接口,和已有开源软件互操作性好,也方便接入机构内部系统。其中Grafana是最重要的生态伙伴,它是一个开源的数据可视化工具,支持动态的更改数据源、灵活地配置报表、编写Prometheus查询语句、定义告警等。因为这些特性,它常用于性能监控领域。

下图是Prometheus的典型架构,本文只关注其中红色部分。

Prometheus Architecture

Cache常用监控指标

本文总结了License使用量、Ensemble队列消息排队数量、事务持续时长、数据库可用空间,这4个常用的数值类型的监控指标,这四个指标直接影响或指示出系统的健康程度。以下是这四个指标的获取方法。

  • License使用量
s LicenseUsed=##class(%SYSTEM.License).LUConsumed()
s LicenseAvailable=##class(%SYSTEM.License).LUAvailable()
s LicenseTotal=##class(%SYSTEM.License).GetUserLimit()
  • Ensemble队列消息排队数量
Set statement=##class(%SQL.Statement).%New(1)
Set sc=statement.%PrepareClassQuery("Ens.Queue","Enumerate")
Set rset=statement.%Execute()
  • 事务持续时长
set rs=##class(%ResultSet).%New()
set rs.ClassName="%SYS.Journal.Transaction"
set rs.QueryName="List"
set sc=rs.Execute(0)
  • 数据库可用空间
Set statement=##class(%SQL.Statement).%New(1)
Set sc=statement.%PrepareClassQuery("%SYS.DatabaseQuery","FreeSpace")
Set rset=statement.%Execute()

Cache中Prometheus接口定义

本文实现了Prometheus接口。该接口中的jsonToPrometheus()方法将JSON转换为Prometheus需要的格式,为后期扩展出通用的Prometheus监控指标采集和埋点提供了基础。接口定义如下,

Class HospModule.HxeyPrometheusMetrics.RestController Extends %CSP.REST
{

XData UrlMap
{
<Routes>
    <Route Url="/prometheus" Method="GET" Call="MetricsPrometheus" />
    <Route Url="/json" Method="GET" Call="MetricsJson" />
  </Routes>
}

ClassMethod MetricsPrometheus() As %Status
{
    set %response.ContentType="text/plain;version=0.0.4;charset=utf-8"
    s json=..getMetricsJson()
    s iter=json.%GetIterator()
    while iter.%GetNext(.key, .value) { 
        s promStr = ..jsonToPrometheus(value)
        w promStr
    }
    return $$$OK
}

ClassMethod MetricsJson() As %Status
{
    s json=..getMetricsJson()
    w json.%ToJSON()
    return $$$OK
}

/// Requires JSON format in this form: {"name":"hxey_ics_license_used","tags":[{"name","","value":""}],"desc":"已用License数","value":0}
ClassMethod jsonToPrometheus(json) As %String
{
    s retStr = ""
    s name=json.name
    s desc=json.desc
    s value=json.value
    s tags=json.tags

    s retStr = retStr_"# HELP "_name_" "_desc
    s retStr = retStr_$CHAR(10)
    s retStr = retStr_"# TYPE "_name_" gauge"
    s retStr = retStr_$CHAR(10)
    if (tags.%Size()=0) {
        s retStr = retStr_name_" "_value
        s retStr = retStr_$CHAR(10)
    } else {
        s retStr = retStr_name_"{"
        s iter2=tags.%GetIterator()
        while iter2.%GetNext(.key, .tag) { 
            s tagName=$REPLACE(tag.name,"""","")
            s tagValue=$REPLACE(tag.value,"""","")
            s retStr = retStr_tagName_"="""_tagValue_""","
        }
            
        s retStr = retStr_"} "_value
        s retStr = retStr_$CHAR(10)
    }
    return retStr
}

ClassMethod getMetricsJson() As %DynamicAbstractObject
{
    s json=[]
    IF (##class(%Dictionary.CompiledClass).%ExistsId("%SYSTEM.License")) {
        s LicenseUsed=##class(%SYSTEM.License).LUConsumed()
        s item={"name":"hxey_ics_license_used","tags":"","desc":"已用License数","value":0}
        s item.tags = []
        s item.value = LicenseUsed
        d json.%Push(item)

        s LicenseAvailable=##class(%SYSTEM.License).LUAvailable()
        s item={"name":"hxey_ics_license_avail","tags":"","desc":"可用License数","value":0}
        s item.tags = []
        s item.value = LicenseAvailable
        d json.%Push(item)

        s LicenseTotal=##class(%SYSTEM.License).GetUserLimit()
        s item={"name":"hxey_ics_license_total","tags":"","desc":"License总量","value":0}
        s item.tags = []
        s item.value = LicenseTotal
        d json.%Push(item)

        s item={"name":"hxey_ics_license_load","tags":"","desc":"License占用率","value":0}
        s item.tags = []
        s item.value = (LicenseUsed/(LicenseAvailable+LicenseUsed)) 
        d json.%Push(item)
    }

    s maxDuration=0
    IF (##class(%Dictionary.CompiledClass).%ExistsId("%SYS.Journal.Transaction")) {
        set rs=##class(%ResultSet).%New()
        set rs.ClassName="%SYS.Journal.Transaction"
        set rs.QueryName="List"
        set sc=rs.Execute(0)
        while (rs.%Next()) {
            s pid = rs.Data("Pid")
            s StartTime = rs.Data("StartTime")
            s now=$H

            s item={"name":"hxey_ics_transaction_duration","tags":"","desc":"事务持续时间","value":0}
            s item.tags = []
            s tag={}
            s tag.name="pid"
            s tag.value=pid
            d item.tags.%Push(tag)
            s tag={}
            s tag.name="ip"
            s tag.value=##class(%SYSTEM.Process).ClientIPAddress(pid)
            d item.tags.%Push(tag)
            s duration = (+now)*3600*24+$P(now,",",2)-((+StartTime)*3600*24+$P(StartTime,",",2))
            s:duration>maxDuration maxDuration=duration
            s item.value = duration
            d json.%Push(item)
        }
    }

    s item={"name":"hxey_ics_transaction_maxduration","tags":"","desc":"最长事务持续时间","value":0}
    s item.tags = []
    s item.value = maxDuration
    d json.%Push(item)

    IF (##class(%Dictionary.CompiledClass).%ExistsId("%SYS.DatabaseQuery")) {
        Set statement=##class(%SQL.Statement).%New(1)
        Set sc=statement.%PrepareClassQuery("%SYS.DatabaseQuery","FreeSpace")
        Set rset=statement.%Execute()
        while (rset.%Next()) {
            s dbName = rset.%Get("DatabaseName")
            s freeRate = rset.%GetData(7)
            s dbAvailMB = rset.%Get("AvailableNum")
            s diskAvailMB = rset.%Get("DiskFreeSpaceNum")

            s item={"name":"hxey_ics_db_load","tags":"","desc":"数据库可用空间占用率","value":0}
            s item.tags = []
            s tag={}
            s tag.name="name"
            s tag.value=dbName
            d item.tags.%Push(tag)
            s item.value = (100-freeRate) / 100
            d json.%Push(item)
        }
    }

    IF (##class(%Dictionary.CompiledClass).%ExistsId("Ens.Queue")) {
        Set statement=##class(%SQL.Statement).%New(1)
        Set sc=statement.%PrepareClassQuery("Ens.Queue","Enumerate")
        Set rset=statement.%Execute()
        while (rset.%Next()) {
            s name = rset.%Get("Name")
            s count = rset.%Get("Count")

            s item={"name":"hxey_ics_queue_load","tags":"","desc":"队列等待数量","value":0}
            s item.tags = []
            s tag={}
            s tag.name="name"
            s tag.value=name
            d item.tags.%Push(tag)
            s item.value = count
            d json.%Push(item)
        }
    }
    return json
}

}

接口服务配置如下,

Web Application Config

安装并运行Prometheus

Prometheus下载地址为https://prometheus.io/download/

安装过程如下,

  • 解压缩到安装目录。本文使用/opt/prometheus作为安装目录。下述子目录及文件均相对于该安装目录。

  • 修改配置文件prometheus.yml

    • 主要配置内容为采集间隔和采集接口列表,如下, imageimage

    • 其他配置,如告警管理服务器和告警规则,可以按需配置。本文使用Grafana配置告警,不在这里配置。

    • Prometheus启动后,会定时、或者在收到SIGHUP时重新加载该配置。

  • 启动后

    • 访问Web端口,默认ip:9090,可查看配置、服务运行情况。 image
  • 使用Systemd托管服务

    • 新增文件/usr/lig/systemd/system/prometheus.service,并写入如下内容,

      [Unit]
      Description=Prometheus Service
      After=network.target
      
      [Service]
      Type=simple
      User=app
      Restart=on-failure
      RestartSec=40s
      ExecStart=/opt/prometheus/prometheus \
              --config.file=/opt/prometheus/prometheus.yml \
              --storage.tsdb.path=/opt/prometheus/data \
              --storage.tsdb.retention=90d 
      ExecReload=/bin/kill -HUP $MAINPID
      KillMode=process
      
      [Install]
      WantedBy=multi-user.target
      
    • 添加为开机自动启动服务,在命令行执行 systemctl enable prometheus

    • 立刻启动服务,在命令行执行 systemctl start prometheus

    • 需要查看服务状态时,在命令行执行 systemctl status prometheus

    • 需要服务动态更新配置时,在命令行执行 systemctl reload prometheus

    • 需要关闭服务时,在命令行执行 systemctl stop prometheus

安装并运行Grafana

Grafana下载地址为https://grafana.com/grafana/download?pg=oss-graf&plcmt=resources

安装过程如下,

  • 解压缩到安装目录。本文使用/opt/grafana作为安装目录。下述子目录及文件均相对于该安装目录。

  • 修改配置文件conf/defaults.ini

    • 按需要修改文件存储地址,例如数据地址、日志地址。 image
    • 其他配置例如监听端口(http_port)、监听地址(http_addr)、数据库([database]章节)、接口安全配置([security]章节)都可以按需修改。
    • 默认情况,Grafana Web服务在ip:3000,使用内置SQLite数据库保存配置,接口无鉴权,默认管理员为admin
  • 启动后

    • 访问Web端口,默认ip:3000,可配置数据源、用户、报表,如下图。 image
  • 使用Systemd托管服务

    • 新增文件/usr/lig/systemd/system/grafana.service,并写入如下内容,

      [Unit]
      Description=Grafana Service
      After=network.target
      
      [Service]
      Type=simple
      User=app
      Restart=on-failure
      RestartSec=5s
      ExecStart=/opt/grafana/bin/grafana-server \
              -homepath=/opt/grafana \
              -config=/opt/grafana/conf/defaults.ini
      KillMode=process
      [Install]
      WantedBy=multi-user.target
      
    • 添加为开机自动启动服务,在命令行执行 systemctl enable grafana

    • 立刻启动服务,在命令行执行 systemctl start grafana

    • 需要查看服务状态时,在命令行执行 systemctl status grafana

    • 需要关闭服务时,在命令行执行 systemctl stop grafana

安装并运行Node Exporter

Prometheus生态中有一个Node Exporter,用于收集各操作系统的常用监控数据并暴露PULL接口。其下载地址和安装指南请参考https://grafana.com/oss/prometheus/exporters/node-exporter/

在实际应用时,Node Exporter提供的数据对不同操作系统略有差异,并且缺少必要的数据,例如与NTP时间服务器的时间偏置量。因此本文用Java实现了适合本机构的Node Exporter。为了实现稳定的跨操作系统的性能指标收集,使用 com.github.oshi:oshi-core:5.8.4库,示例代码如下,

    public double getLoad() {
        SystemInfo systemInfo = new SystemInfo();
        double load1 = systemInfo.getHardware().getProcessor().getSystemCpuLoadBetweenTicks(ticks);
        double load2 = systemInfo.getHardware().getProcessor().getSystemCpuLoadBetweenTicks(ticksLast);
        return Math.max(load1, load2);
    }

为了实现计算与NTP时间服务器的时间偏置量,使用commons-net:commons-net:3.8.0库,示例代码如下,

    public long getOffset() {
        NTPUDPClient ntpudpClient = new NTPUDPClient();
        ntpudpClient.setDefaultTimeout(5000);
        ntpudpClient.open();
        System.out.println(System.currentTimeMillis());
        InetAddress hostAddr = InetAddress.getByName(HOSTNAME);
        final TimeInfo info = ntpudpClient.getTime(hostAddr);
        ntpudpClient.close();
        info.computeDetails();
        return info.getOffset()
    }

接口实现效果如下,

# HELP hxey_memory_load 内存负载率
# TYPE hxey_memory_load gauge
hxey_memory_load{application="sysmetrics",hxey="hxey",} 0.42
# HELP disk_total_bytes Total space for path
# TYPE disk_total_bytes gauge
disk_total_bytes{application="sysmetrics",hxey="hxey",path="C:\\Users\\Administrator\\Desktop\\servermetrics\\.",} 2.78802722816E11
# HELP hxey_memory_total 内存总大小
# TYPE hxey_memory_total gauge
hxey_memory_total{application="sysmetrics",hxey="hxey",} 4.0
# HELP system_cpu_count The number of processors available to the Java virtual machine
# TYPE system_cpu_count gauge
system_cpu_count{application="sysmetrics",hxey="hxey",} 2.0
# HELP hxey_timestamp 服务器时间戳
# TYPE hxey_timestamp gauge
hxey_timestamp{application="sysmetrics",hxey="hxey",} 1.664506300883E12
# HELP hxey_disk_used 硬盘使用大小
# TYPE hxey_disk_used gauge
hxey_disk_used{application="sysmetrics",disk_label="C:\\[]",hxey="hxey",} 27.38
hxey_disk_used{application="sysmetrics",disk_label="D:\\[]",hxey="hxey",} NaN
hxey_disk_used{application="sysmetrics",disk_label="A:\\[]",hxey="hxey",} NaN
# HELP hxey_disk_total 硬盘总大小
# TYPE hxey_disk_total gauge
hxey_disk_total{application="sysmetrics",disk_label="C:\\[]",hxey="hxey",} 259.66
hxey_disk_total{application="sysmetrics",disk_label="D:\\[]",hxey="hxey",} NaN
hxey_disk_total{application="sysmetrics",disk_label="A:\\[]",hxey="hxey",} NaN

结果

搭建完成的Prometheus监控框架,能够准实时的收集和展示Cache中事务、Lisence使用率、消息队列等待长度、数据库使用率,以及操作系统时间偏置量、磁盘使用率、CPU使用率、内存使用率等指标。

image

image

image

讨论

任何监控系统,都是定时采集的,采集得到的指标是对现实中的连续指标的一个抽样。管理人员不能通过监控指标定位到故障原因,甚至不能精确定位到故障的实际发生时间。 为了弥补监控指标的缺憾,后期应为Cache集群添加日志采集系统,如Loki日志采集工具,建立起监控指标的异常和日志输出的对应关系,让异常定位更准确。

8
0 978
文章 Qiao Peng · 一月 31, 2024 21m read

InterSystems IRIS、Health Connect和上一代的Ensemble提供了优秀的互操作架构,但即便有低代码开发能力,很多开发者还是希望能用自己的技术栈语言在InterSystems的产品上开发互操作产品。

考虑到互操作产品本身的开放性要求和各个技术栈背后庞大的生态价值,InterSystems IRIS和Health Connect提供了Production EXtension (PEX)架构,让开发者使用自己的技术栈语言来开发互操作解决方案。目前PEX支持Java、.net、Python。

这里我们介绍使用Java利用PEX进行互操作产品的开发。

一 InterSystems IRIS上使用Java开发的基础

在进入PEX主题前,需要简单介绍一下Java在InterSystems IRIS上开发的各种技术选项,因为PEX也是以这些技术选项为基础的。

0
1 274
公告 Claire Zheng · 一月 28, 2024

近日,InterSystems宣布 InterSystems IRIS® Cloud SQL 和 InterSystems IRIS® Cloud IntegratedML® 服务全面上市。 这些全面托管的云原生智能数据服务使开发人员能够轻松地在SQL环境中构建云原生数据库和机器学习(ML)应用程序。

通过 Cloud SQL和 Cloud IntegratedML,开发人员可以访问下一代关系数据库即服务(DBaaS),DBaaS快速且易于配置和使用。 嵌入式AutoML功能支持开发人员在全面托管的、弹性的云原生环境中,仅仅通过几条类似SQL的命令即可轻松开发并执行机器学习模型。

0
0 105
文章 Louis Lu · 一月 28, 2024 2m read

这里介绍三个SQL函数 JSON_ARRAY 、 JSON_OBJECT和JSON_ARRAYAGG,可以直接通过SQL语句返回JSON格式数据

1. JSON_ARRAY 函数通过逗号(,)分割的多个表达式,返回JSON array格式的数据

SELECT TOP 3Name,
JSON_ARRAY(%ID,%TABLENAME,UCASE(Name),Age,Home_State) "JSON data" 
FROM Sample.Person

也可以忽略空值(null) ,添加“ABSENT ON NULL”

SELECT TOP 3Name,
JSON_ARRAY(%ID,%TABLENAME,UCASE(Name),Age,Home_State ABSENT ONNULL) "JSON data" 
FROM Sample.Person

获取到的返回结果

如果想显示空值(null),使用"NULL ON NULL",如果没有特意标明,则该设置为默认值。

下面语句获取客户所有电话号码,并以JSON array格式返回:

SELECT JSON_ARRAY(HomePhone,WorkPhone,Mobile) "JSON data"FROM Test.Phones
["055-8263371","052-4957286","054-4951066"]
0
0 239
文章 Michael Lei · 一月 26, 2024 2m read

InterSystems 常见问题解答

如果系统24小时没有停止,旧的日志文件将根据“日志文件删除设置”在0:30删除。

导致日志文件保留的时间早于“日志文件删除设置”的一个可能原因是存在仍处于开放状态的事务。

在这种情况下,您将能够通过搜索执行事务的进程并完成事务来删除日志文件。

下面的示例检查是否存在未完成的事务,如果存在,则输出目标文件名和日志记录信息。

(示例可以从这里下载

*注意*如果要检查的日志文件较大或日志文件较多,则执行需要时间,因此请联系我们的支持中心。

0
0 86
文章 Michael Lei · 一月 15, 2024 2m read

作为针对数据导入处理性能和错误(锁定表已满)的衡量标准,可能需要调整常规内存堆 (gmheap) 和锁定表大小 (locksiz) 参数。

事实上,您可以使用终端和管理门户来检查当前分配了多少通用内存堆。


★终端用

// 一般メモリヒープサマリUSER> w $system .Config.SharedMemoryHeap.GetUsageSummary() 4992226 , 6029312 , 59441152

通用内存堆摘要以使用量、分配量和配置量(字节)的形式显示返回值。

使用量是分配的锁表、进程表等实际使用的量。
分配量是gmheap区域中锁表、进程表等分配的量。
配置量为gmheap(KB)+IRIS系统附加区,即当前最大可用量(实际通用内存堆区值)。

如上所述,配置数量与配置参数 gmheap 的独立值不匹配。
这是因为IRIS自动将内部使用的内存区域添加到配置参数gmheap中来配置通用内存堆区域。详情请参阅下面的文档。

关于gmheap

您可以使用以下命令获取锁表的使用情况:
返回值显示为可用量、用户可用量和已用量(字节)。详情请参阅这篇文章

%SYS > w##class (SYS. Lock ).GetLockSpaceInfo() 16772624 , 16764624 , 4592


★用于管理门户

您可以从“系统操作”>“系统使用情况”>“共享内存堆使用状态”进行检查。  

0
0 104
文章 Claire Zheng · 一月 15, 2024 2m read

Gartner魔力象限是业内知名的分析报告,针对特定市场进行严谨、基于事实的研究后发布的魔力象限报告,可为用户提供全面视图,以便其了解在高速增长、差异化明显的市场中供应商的相对位置。在魔力象限中,供应商被定位在四个象限中:领导者(Leader)、挑战者(Challenger)、远见者(Visionary)和利基玩家(Niche Player)。

2023年年底,在 Gartner 2023年最新发布的“云数据库管理系统(Cloud DBMS)魔力象限”中,InterSystems再次荣膺“挑战者”(Challenger)称号,这是InterSystems第三次入围这一报告并得到认可(2022年“远见者”2021年“挑战者”)。

Gartner此次评估面向包括InterSystems在内的19家云数据库管理系统供应商进行。我们相信,Gartner魔力象限“挑战者”这一定位,证明了InterSystems有能力挑战现状、推动创新,为市场提供独特的解决方案。

0
0 88
公告 Michael Lei · 一月 10, 2024

嗨,大家好,

我们想重点介绍 2023 年布拉格欧洲欧洲医疗健康黑克松Hackathon期间创建的一些杰出项目。
IKEM 和阿斯利康向参与者提出了九个现实世界的医疗保健挑战 
InterSystems 向参与者介绍了使用 FHIR 存储库并通过在 AWS 上提供 FHIR 云服务来在其解决方案中执行 FHIR 可用性的机会。

来认识一下我们挑战的获胜者:

第一名Čarodějové (PathoSync)

“PathoSync 软件是复杂病理学家平台的坚实基础。通过使用自定义映射,任何实验室都可以将其数据投影到 FHIR7 标准,该标准很快将在全球范围内强制执行。这使得数字化过程更加顺利。与 InterSystems 的连接确保了质量并实现了很多医疗保健功能。此外,使用位于欧洲的FHIR服务器严格遵循GDPR规范,因此软件的使用遵循欧洲标准。

0
0 98
文章 姚 鑫 · 十二月 27, 2023 2m read

第七章 创建Callout Library - 创建 ZFEntry 表

创建 ZFEntry

每个 Callout 库必须定义一个 ZFEntry 表,该表允许 IRIS 加载和访问 Callout 函数。 ZFEntry 表由以 ZFBEGIN 开头、以 ZFEND 结尾的宏代码块生成。在这两个宏之间,必须为要公开的每个函数调用一次 ZFENTRY 宏。

每个 ZFENTRY 调用都采用三个参数:

   ZFENTRY(zfname,linkage,entrypoint)

其中 zfname 是用于在 $ZF 调用中指定函数的字符串,linkage 是指定如何传递参数的字符串,entrypointC 函数的入口点名称。

要创建 Callout 库,代码必须包含 #define ZF_DLL 指令,该指令是一个开关,可生成用于定位库函数的内部 GetZFTable 函数。加载 Callout 库时, IRIS 调用此函数来初始化该库,以便后续查找库函数名称。

注意:ZFEntry 序列号

ZFEntry 表中条目的位置可能很重要。 $ZF(-5)$ZF(-6) 接口(在“调用标注库函数”中描述)都通过指定表中的序列号(从 1 开始)来调用库函数。例如,$ZF(-6) 将通过以下调用调用 ZFEntry 表中的第三个函数:

   x = $ZF(-6,libID,3)

其中 libID 是库标识符,3 是表中第三个条目的序列号。

注意:预编译头文件

某些编译器(例如 Microsoft Visual Studio)支持预编译头。如果使用预编译头,则 #define ZF_DLL 语句必须对预编译有效。如果不是,生成的 dll 在使用时将导致 <DYNAMIC LIBRARY LOAD> 错误。强烈建议不要将预编译头用于 Callout 库。

0
0 84