news 2026/8/5 21:14:30

一库多模实战:用 KingbaseES 同时搞定 JSON、中文全文检索与标签位图

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
一库多模实战:用 KingbaseES 同时搞定 JSON、中文全文检索与标签位图

一、引言:四类数据需求,难道真要上四个组件?

我先说一个情况吧。很多团队其实都踩过这个坑。比如我们要做一个电商后台。那么我们来盘一盘它到底有哪些数据需求。

订单还有库存、用户这些,是很规矩的结构化数据。关系型数据库处理这个是本来就擅长的。再看商品属性。这个就非常杂了。平板要记屏幕尺寸,手表要记防水等级,耳机要记降噪情况。字段根本对不上。这种情况其实属于半结构化文档数据。接着是商品描述。这些都是很长的中文文本。做运营的人有个要求,就是输入「搜续航」或者「搜降噪」的时候得能查出来。这就是全文检索需求。最后看用户这边。每个用户身上都挂了一大堆画像标签。比如运动啊、高消费啊、数码控之类的。每天还要去做那种圈选,就是「同时满足标签 A 和标签 B」的这种情况。这属于标签集合运算

如果按照以前的那种老思路。也就是一种需求就单独上一个专门的组件。那么架构往往就会变成下面这样:

  • 关系数据(订单/用户)→ 关系型数据库
  • 灵活商品属性 → MongoDB 之类的文档库
  • 中文全文检索 → Elasticsearch
  • 用户标签圈选 → 自研标签系统 / 专门的 OLAP 引擎

这四个组件一搭上去。问题马上就跟着来了。首先是数据要多头同步。商品在一个地方更新了。你还得把它同步到文档库里面去。接着还要推给 ES 去建索引。这个链路随便哪里断一下,数据对不上的情况就出现了。然后是运维成本翻几番。四套系统啊。每套系统都有自己的高可用方案。备份的方式也不一样。监控和扩容也都是分开搞的。最后开发也累。比如来了一个复合需求,要求是「带运动标签、描述提到续航、品牌国产」。为了这个,你要跨三个系统去查。要写三段不同的代码。最后还得在应用层把这些数据给拼装起来。

金仓给的方案是什么呢。是一库多模。也就是说,上面说的这些能力,你在一个 KingbaseES 库里面,就能全部给做掉。这其实正好呼应了这次征文的那个主题,叫「不止于替代」。也就是说,金仓替代的往往仅仅只是那个关系库吗?不是的。它还能把周边这一堆专用组件要干的活儿,顺手就给接过来了。

那么这篇文章的话,我就会用电商这个场景一直贯穿下去。接着我会依次去拆解三件套:JSON、中文全文检索、Roaring Bitmap。每一块我都会讲清楚,原理为什么这么设计,具体怎么动手操作,还有跑出来的结果说明了什么。最后会把它们融合成一条 SQL。

先说一下范围。本文主要是聚焦在 JSON、中文全文检索、还有 Roaring Bitmap 这三种能力上。这三种是最有代表性的。也最能体现「一库多模」的价值。至于地理和向量检索的话,就不在本文展开了。

二、环境准备:建一个干净的演示库

为了不污染其它数据,单独建一个演示库mm_demo

CREATEDATABASEmm_demo;\c mm_demo

后续所有扩展和表都建在这个库里。

三、模块一:JSON 文档——用一列把商品属性存起来

为什么用 JSONB,不直接拆成单独的列呢

不同品类的商品,属性差异其实是非常大的。如果用传统关系库里面的建表方式来搞。你要么给每个品类单独建一张表。品类一旦多起来,这个维护工作根本做不过来。要么就在一张大宽表里面塞几百个可以空着的列。但是呢,大部分行里面大部分列其实都是 NULL。看着很稀疏,也不好管理。这种表结构不固定、字段跟着品类变来变去的情况,用 JSON 来存就比较合适了。

金仓里面的 JSONB 是内置的能力。你不用去装什么扩展。它跟普通的 JSON 文本有什么区别呢。关键点在于,JSONB 在数据写进去的时候,就直接把文档解析成二进制结构化的格式存起来了。等你读的时候,就不需要再去解析一遍了。而且键会自动去重,也可以建索引。代价是什么呢。就是写进去的时候要多做一步解析的工作。但是对于商品属性这种读得多写得少的情况,其实是很划算的。

建表并写入几条不同品类的商品:

CREATETABLEproduct(idserialPRIMARYKEY,namevarchar(128),attrs jsonb-- 灵活的商品属性);INSERTINTOproduct(name,attrs)VALUES('平板电脑T10','{"品牌":"国产","屏幕":"11寸","内存":"8GB","标签":["办公","轻薄"]}'),('智能手表Pro','{"品牌":"国产","防水":"50米","续航":"14天","标签":["运动","健康"]}'),('无线耳机X1','{"品牌":"国产","降噪":true,"续航":"30小时","标签":["音乐","通勤"]}');

三个经常用的操作符:取值、过滤还有包含判断

JSONB 里面其实最常用的就是三个操作符。你把这几个记住,平时基本就够用了:

  • ->这个用来取出子元素。取出来之后还是 JSON 类型的。也就是说你还可以继续往下取;
  • ->>这个也是取子元素。不过它取出来之后会转成普通的文本。平时要比较或者展示的话,就用这个;
  • @>这个是用来判断左边的 JSON 是不是包含了右边的 JSON。平时做条件过滤经常用它。
-- 取品牌字段(->> 返回文本,可直接参与比较和展示)SELECTname,attrs->>'品牌'ASbrandFROMproduct;-- 找出带「运动」标签的商品(-> 先取出数组,@> 判断是否包含该元素)SELECTnameFROMproductWHEREattrs->'标签'@>'"运动"';

看一下结果。第一条语句把每个商品的品牌单独抽出来了,变成了一列。也就是说,这种半结构化的数据,也能跟普通字段一样用来查。第二条语句呢,正好找出了标签里面有“运动”这两个字的商品。这就说明@>这个操作符是能深入到数组元素里面去匹配的。它往往仅仅只是匹配最外层的键。

给 JSONB 建一个 GIN 索引,让包含查询能走索引

数据量如果上去了的话,@>要是走全表扫描就会很慢。JSONB 的包含查询想要走索引的话,用的是GIN(Generalized Inverted Index,通用倒排索引)

CREATEINDEXidx_product_attrsONproductUSINGgin(attrs);

GIN 索引是怎么做的呢。它把 JSONB 里面的每一个键、每一个值全都拆成一个个的“词条”。然后去建一个倒排表。等你查@>的时候,它就直接去定位包含目标词条的那些行。这样就不用一行一行去解析了。

这里有一个很关键的点。你的 GIN 索引是建在整个attrs这一列上的。那么你想要让包含查询用到这个索引,你的查询条件也得写成对这一列顶层的@>。前面演示操作符的时候,我们写的那个attrs -> '标签' @> '"运动"'。它是先取出了子元素,然后再去判断的。这个逻辑当然没问题,结果也是对的。但是呢,它实际上是作用在attrs -> '标签'这个子表达式上面的。它用不到attrs这一列上面的索引。那怎么写才能吃到索引呢。就是把条件整体写成顶层的包含关系:

-- 顶层包含写法:判断标签数组含「运动」,可走 idx_product_attrsSELECTnameFROMproductWHEREattrs @>'{"标签": ["运动"]}';

还有一点要说一下。索引这东西,得在有足够多数据的时候才能体现出价值来。如果你表里只有那么几行数据的话,优化器通常会去选择全表扫描。它觉得这样更快。所以在演示之前,我们先往product表里面灌个几万行商品数据进去:

-- 灌入 5 万行商品数据(标签统一设为「办公」,让「运动」保持高选择性)INSERTINTOproduct(name,attrs)SELECT'商品'||g,jsonb_build_object('品牌','国产','标签',to_jsonb(ARRAY['办公']))FROMgenerate_series(1,50000)g;ANALYZEproduct;

接着我们用EXPLAIN ANALYZE来对比一下。同一条顶层包含查询,在建索引之前和建索引之后,它的执行计划到底有什么差别:

-- 建索引前:全表 Seq ScanEXPLAINANALYZESELECTnameFROMproductWHEREattrs @>'{"标签": ["运动"]}';CREATEINDEXidx_product_attrsONproductUSINGgin(attrs);ANALYZEproduct;-- 建索引后:Bitmap Index Scan on idx_product_attrsEXPLAINANALYZESELECTnameFROMproductWHEREattrs @>'{"标签": ["运动"]}';

你看建索引之前,它走的是全表Seq Scan。5 万行数据,它得一行一行去做包含判断。你看那个Rows Removed by Filter: 50002。实际测出来的 Execution Time 是16.032 ms。等建好idx_product_attrs这个索引之后呢。执行计划就变成了Bitmap Index Scan on idx_product_attrs。它通过倒排索引直接去定位了。Heap Blocks 那里只需要精确命中 1 块就行。这个时候 Execution Time 就降到了0.077 ms。这中间差了差不多200 倍。这就是 GIN 倒排索引的作用。它把“一行一行去解析”变成了“按词条直接去定位”。而且随着你表里面的数据量越来越多,这个性能差距其实还会继续拉大的。

四、模块二:中文全文检索——让商品描述真正「能被搜到」

中文为什么必须先分词

我先讲一个大家平时容易忽略的原理。其实全文检索到底是怎么回事呢?说白了,就是把一段文本给切开。切成一个个词条。切完之后去建一个倒排索引。这个索引是干嘛的呢?也就是记录一下「每个词到底出现在了哪些文档里」。等你要查东西的时候,就按词去命中。然后再算一下相关度,排个序。

英文那就好办了。人家天生就带着空格。the quick brown fox放进去,一拆就是四个词,很清楚。中文没有天然分隔符。这是一个问题。比如「智能手表续航长」这句话。机器其实并不知道该怎么切。到底是切成「智能/手表/续航/长」呢,还是别的什么切法。那如果不去分词,直接按单个字去切会怎么样呢?「续航」这个词就会被拆成「续」和「航」两个字。这个时候你去搜「续航」,那么含有「航班」或者「继续」的无关节档也会被找出来。这个精度就非常差了。那为什么会这样呢?原因在于你没法靠单字去理解词意。所以啊,做中文全文检索的第一道坎,就是必须有一个懂中文的分词器。得靠它把连在一起的汉字切成有意义的词才行。

金仓这边给了两个中文分词器的扩展。一个是zhparser,它底层是基于 SCWS 的。另一个是sys_jieba,也就是结巴分词。这两个你选一个就行。只要能跑通就可以。那在这篇文章里的话,我就拿zhparser来做例子了。

第一步:装分词器、建全文检索配置

-- 1) 安装中文分词器扩展CREATEEXTENSIONIFNOTEXISTSzhparser;-- 2) 基于分词器建一个名为 chinese 的全文检索配置CREATETEXTSEARCH CONFIGURATION chinese(PARSER=zhparser);-- 3) 关键一步:配置哪些词性的分词结果纳入索引ALTERTEXTSEARCH CONFIGURATION chineseADDMAPPINGFORn,v,a,i,e,lWITHsimple;

这里面我要特别说一下第 3 步。也就是那个ADD MAPPING。这一步往往是最容易被忽略的。而且特别容易在这里翻车。为什么呢?因为zhparser在分词的时候,会给每个词打上词性的标签。比如n代表名词,v代表动词,a代表形容词。后面还有i是成语,e是感叹词,l是习惯用语等等。那么上面那条 SQL 语句的意思其实就是:只有这些词性的词,才会被收进 tsvector 里面去

映射要点ADD MAPPING决定了哪些词性的词会进入tsvector。那如果你只映射了n(也就是名词)的话,动词和形容词就会被排除在索引之外。这会有什么问题呢?像「降噪」这类词,它分词的时候往往会被切成动词性的成分,那你就检索不到它了。在咱们演示的这种情况里,我建议直接把n,v,a,i,e,l都给加上。名词、动词、形容词、成语全都纳进去,这样召回才是最全的。

第二步:把描述转成 tsvector

CREATETABLEgoods(idserialPRIMARYKEY,titlevarchar(128),bodytext);INSERTINTOgoods(title,body)VALUES('平板电脑T10','国产轻薄办公平板,11寸高清屏幕,适合移动办公和在线学习'),('智能手表Pro','专业运动健康监测,支持心率血氧,50米防水,续航长达14天'),('无线耳机X1','主动降噪无线耳机,通勤音乐两不误,单次续航30小时');-- 先验证分词效果,再决定要不要调映射SELECTto_tsvector('chinese','专业运动健康监测,续航长达14天');

你看这个to_tsvector('chinese', ...)跑出来的结果。它输出的是一串「词:位置」的列表。比如'健康':3 '运动':2 '续航':5 ...这样的形式。这其实就是倒排索引要用的原材料。它是怎么做的呢?也就是每个词都记下它在原来那段文本里的位置。这个位置信息有什么用呢?在后面我们要算「相邻度」或者做「短语匹配」的时候,就用得上它了。

第三步:GIN 还是 RUM?这里要做个选择

给 tsvector 建索引的话,这里其实有两个选择。一个是GIN,另一个是RUM。这是在这个模块里我觉得最值得讲透的一个点。

  • GIN:这个是经典的倒排索引。它往往仅仅只是记下「词 → 文档」的这么个映射关系。它能快速告诉你「哪些文档里面含有这个词」。但是呢,它不存词的位置和频率信息。那么这就带来一个问题了。当你用 GIN 的时候,你要想用ts_rank去做相关度排序,它就得回表。也就是回到原文档里面去重新算一遍。文档数量一多的话,这个排序就会变成一个很大的瓶颈。
  • RUM:这个你可以把它当成是「GIN 加了料之后的版本」。它在倒排索引里面额外存了词的位置信息,还有专门用来打分的数据。这么做有什么好处呢?两个好处。第一,ts_rank排序可以直接在索引里面就搞定了。不需要再去回表。所以带排序的全文检索明显会更快。第二,它支持短语查询,也就是相邻度查询。比如你要查「运动」这个词紧紧跟着「健康」这个词。当然,有好处就有代价。它的索引体积会更大,而且写入的时候也会更慢。

那么怎么选呢?我通常这么说:如果你只是判断「有没有」这个词,那用 GIN 就够了。但如果你要按相关度去排序,或者要做短语匹配的话,那就选 RUM。回到咱们的场景,商品搜索肯定是要按相关度排序的。所以这里我们选 RUM。

-- 安装 RUM 索引扩展CREATEEXTENSIONIFNOTEXISTSrum;-- 给 goods 加一列 tsvector,并填充(title 权重更高可另配,这里从简)ALTERTABLEgoodsADDCOLUMNtsv tsvector;UPDATEgoodsSETtsv=to_tsvector('chinese',title||' '||body);-- 建 RUM 索引CREATEINDEXidx_goods_tsvONgoodsUSINGrum(tsv);

第四步:按相关度搜索

-- 搜「同时提到 运动 和 续航」的商品,按相关度从高到低排SELECTtitle,ts_rank(tsv,q)ASrankFROMgoods,to_tsquery('chinese','运动 & 续航')qWHEREtsv @@ qORDERBYrankDESC;

这里面的逻辑其实不难。to_tsquery把你要查的词也解析成了词条。里面的&代表的是「与」的关系。|代表的是「或」的关系。接着看@@,这个符号是用来判断 tsvector 有没有命中 tsquery 的。最后那个ts_rank就是算出一个相关度的分值。这个分值越大,说明越相关。

结果解读:你看这个查询,我们用的是运动 & 续航来做「与」检索。也就是说这两个词得同时存在。跑出来的结果,只有「智能手表Pro」的描述里面同时含有这两个词。所以最后命中了 1 条数据。并且它带出了一个相关度的分值,rank ≈ 0.0517。这说明了什么呢?说明全文检索这东西,它不光能判断你「有没有包含」这个词。它还能量化出「到底有多相关」。然后根据这个去给你排序。这其实也就是 RUM 相比 GIN,在「带排序的检索」这种场景下,显得更合适的一个很直接的体现。

五、模块三:Roaring Bitmap——标签人群圈选用这个

传统标签圈选为什么慢的情况

用户画像里面其实是有很多标签的。比如运动、高消费、数码控、宝妈这些。运营平时经常提的需求是什么呢。就是圈出同时满足 A 和 B 的人群。或者是 A 或者 B。再或者是 A 但是不能包含 C。这些东西说白了其实都是集合运算

传统关系库里面怎么搞呢。通常是建一张user_tag(user_id, tag)的关联表。如果要圈“运动且高消费”的人,那就得这么写:

-- 传统写法:自连接 + 去重,标签一多、用户上亿就吃力SELECTa.user_idFROMuser_tag aJOINuser_tag bONa.user_id=b.user_idWHEREa.tag='运动'ANDb.tag='高消费';

问题出在哪里呢。当你的用户量上了亿,标签有几百个的时候。这张表可能就有几十亿行了。JOIN 加上 GROUP BY 去重,要扫非常多的行。内存也吃得很多。你做的交并集越多,它就越慢。存这些东西的话,空间也占得很大。

Roaring Bitmap 的做法:用压缩位图来表示人群

我们换一种方式来想。把拥有某个标签的用户集合,表示成一张位图。什么意思呢。就是用户的 id 当作下标。第 i 位如果是 1,就代表用户 i 有这个标签。那么“运动且高消费”这种情况怎么办。其实就是把两张位图做一下按位与(AND)。“或”的话就是做按位或(OR)。这些都是 CPU 算得很快的位运算。跑起来速度很快。

普通位图在数据比较稀疏的时候,其实挺浪费空间的。一亿用户的话就要一亿位。Roaring Bitmap它做了一件事情,就是分块压缩。它把整个 id 的空间切成一个个 64K 大小的桶。每个桶里面会看数据的稀疏或者密集程度。然后自动去选“数组”、“位图”或者“行程编码”里面最省空间的那种方式来存。这样的话,它就有了位图的运算速度。而且存储空间也省了很多。这就是它在大规模标签场景下,比“关联表 JOIN”好用的原因了。

动手写一下:建标签位图、做交并集

CREATEEXTENSIONIFNOTEXISTSroaringbitmap;-- 每个标签对应一张用户位图(把用户 id 灌进 bitmap)CREATETABLEtag_users(tagvarchar(32)PRIMARYKEY,users roaringbitmap);-- 用 rb_build 从用户 id 数组构造位图INSERTINTOtag_usersVALUES('运动',rb_build(ARRAY[1,2,3,5,8,13])),('高消费',rb_build(ARRAY[2,3,5,7,11]));
-- 同时是「运动」且「高消费」的用户数:rb_and 求交、rb_cardinality 计数SELECTrb_cardinality(rb_and((SELECTusersFROMtag_usersWHEREtag='运动'),(SELECTusersFROMtag_usersWHEREtag='高消费')))AS交集人数;-- 具体是哪些用户:rb_to_array 把位图还原成 id 数组SELECTrb_to_array(rb_and((SELECTusersFROMtag_usersWHEREtag='运动'),(SELECTusersFROMtag_usersWHEREtag='高消费')))AS用户列表;

看一下结果:交集的结果是{2,3,5}。也就是说,既在运动位图里,又在高消费位图里的就是这三个 id。你看整个过程,没有用 JOIN,也没有用 GROUP BY 去重。其实就是两张位图做了一次按位与的操作。当你的用户规模从几个变成几千万的时候,这个性能差距就非常明显了。

用法上的几个点rb_build这个函数里面要传的参数是整型数组。你用ARRAY[...]这种格式传进去就行。另外要注意,Roaring Bitmap 的 id 用的是无符号整数的意思。所以你在把业务主键映射到位图下标的时候,要保持是非负的,并且落在合理的范围里面就行。常用的函数其实记一套就够用了。构造的话用rb_build。交、并、差的话分别用rb_andrb_orrb_andnot。想数个数的话用rb_cardinality。要把位图变回数组的话用rb_to_array

六、融合起来的场景:一条 SQL 把三种能力串起来

单独拿出来的话,这几种能力其实也没什么特别的。JSON 的话别的库也有。全文检索的话可能 ES 更专业一些。但是呢,它们这几个东西都在同一个库里面,一条 SQL 就能配合着用。这才是关键的地方。

我们来想一个平时经常会碰到的运营需求。

找出“品牌是国产、带运动标签、并且描述里面提到了续航”的商品。

这三个条件其实对应的是不同的东西。分别是 JSON 属性过滤、JSON 标签包含,还有中文全文检索。在同一个库支持多模的情况下面,它其实就是一条 SQL 的事(按你实际的表结构改一改就行):

SELECTg.titleFROMgoods gJOINproduct pONp.name=g.titleWHEREg.tsv @@ to_tsquery('chinese','续航')-- 全文检索:描述里提到续航ANDp.attrs->>'品牌'='国产'-- JSON 过滤:品牌国产ANDp.attrs->'标签'@>'"运动"';-- JSON 标签包含:带运动标签

看一下结果:在一次查询里面,全文检索、JSON 字段的比较、JSON 数组的包含,这三种能力被AND连在了一起。然后交给同一个优化器去规划。执行一次就直接把结果返回来了。你不需要跨三个系统去分别查,然后再回到应用层去拼装数据。这就是“一个库支持多模”很好用的地方了。三种能力都在同一个库里面,同一条 SQL,同一个事务里面。配合起来非常自然。

你把它跟那种用很多组件搭起来的架构比一下的话,差别就很明显了。

七、总结一下:一个库,省掉三个组件

这篇文章用一个电商的场景,把金仓的三种“模”都过了一遍。

  • JSON/JSONB:这是它内置的能力。用->/->>/@>这些操作符去存那些半结构化的商品属性。配上 GIN 倒排索引的话,过滤起来速度也是很快的;
  • 中文全文检索:用分词器(zhparser/sys_jieba)加上tsvector倒排索引,再加上 RUM 索引。这样中文文本就能被搜到了,还能按相关度来排序;
  • Roaring Bitmap:用压缩位图来表示有标签的人群。做交并差集的话其实就是做位运算。在标签量很大的时候,比“关联表 JOIN 加上去重”要快很多,也省空间。

它们都有一个共同点。那就是不需要你再去额外装别的独立组件了。少弄一个组件,你就少做一份数据同步的工作。运维的事情也少了一点。成本也降了一点。做国产化适配的活儿也少了。对于现在正在做信创替代的团队来说,“一个库支持多模”不光是把功能补上了。它其实是在架构上把东西变简单了。以前那种“关系库加上 ES,再加上文档库,再加上标签系统”这一堆东西要干的事,现在一个 KingbaseES 就能接住了。

如果看完这篇文章你只能记住一句话,那记住这个就行:一个库,省掉三个组件。这其实也是它除了做替代之外,很有用的一个地方。金仓替代的并不只是原来那个关系数据库。它还把旁边那一堆专用组件要干的活儿,也都一起干了。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/5 21:13:53

Amyloid β-protein (1-42)

一、基本信息英文全称:Amyloid β-protein (1-42)中文全称:β 淀粉样蛋白 1-42(人源全长致病型 Aβ)三字母序列:Asp-Ala-Glu-Phe-Arg-His-Asp-Ser-Gly-Tyr-Glu-Val-His-His-Gln-Lys-Leu-Val-Phe-Phe-Ala-Glu-Asp-Val-G…

作者头像 李华
网站建设 2026/8/5 21:12:36

MetaGPT | 第十七章:外部环境与研究扩展:MGX、AFlow、SPO

预计阅读时间:60 分钟 难度等级:高级 本章导读 前面几章我们已经围绕 MetaGPT 的主干能力做了较完整的拆解: 软件公司 SOP-> Team / Environment / Role / Action / Message-> ProjectRepo-> LLM Provider-> Tool System-> Data Interpreter-> RAG / M…

作者头像 李华
网站建设 2026/8/5 21:12:23

北京3D动画制作公司推荐与选型指南

北京作为全国文化创意产业中心,汇聚了大量3D动画制作公司,但行业已高度分化——房地产建筑动画、工业设备演示、影视番剧、游戏CG等不同赛道的技术要求和核心能力完全不同。选型的关键在于根据项目类型匹配对应赛道的公司。以下按核心赛道分类推荐&#…

作者头像 李华
网站建设 2026/8/5 21:11:22

最短路题目:网络延迟时间

文章目录题目标题和出处难度题目描述要求示例数据范围前言解法一思路和算法代码复杂度分析解法二思路和算法代码复杂度分析题目 标题和出处 标题:网络延迟时间 出处:743. 网络延迟时间 难度 5 级 题目描述 要求 给定一个由 n\texttt{n}n 个结点组…

作者头像 李华
网站建设 2026/8/5 21:10:57

洛雪音乐音源:重新定义免费高品质音乐体验

洛雪音乐音源:重新定义免费高品质音乐体验 【免费下载链接】lxmusic- lxmusic(洛雪音乐)全网最新最全音源 项目地址: https://gitcode.com/gh_mirrors/lx/lxmusic- 在数字音乐付费订阅成为主流的今天,你是否还在为寻找免费又高质量的音乐资源而烦…

作者头像 李华
网站建设 2026/8/5 21:10:47

哈工大近世代数期末复习

近世代数是抽象代数的一个分支,是计算机科学和人工智能大数据的基础. 本文内容有点长,大家可以通过index来跳转到想要看的章节,第十章的总结在我的主页里下载 1.代数系 半群:满足结合律的代数系 交换半群:满足交换律的半群 群:判定方法有两种 method1 有单…

作者头像 李华