在数据处理领域,业务常常同时需要强大的实时分析(OLAP)能力和高效的文本检索能力。这通常导致您在关系型数据库(如PolarDB MySQL版)和专用搜索引擎(如Elasticsearch)之间做出选择,或维护一个“数据库+搜索引擎”的复杂双系统架构。列存索引(IMCI)通过在PolarDB内集成全文索引功能,提供了一种新的解决方案。它旨在将复杂的SQL分析与毫秒级的文本模糊查询能力融合,从而简化技术栈、保证数据一致性。Elasticsearch作为一个成熟的分布式搜索引擎,在全文检索、日志分析等领域拥有强大的功能和丰富的生态,但其非关系型特性在处理复杂关联查询和保证事务一致性方面存在天然的限制。本文档旨在客观对比列存索引(IMCI)和Elasticsearch在技术架构、核心功能、性能表现及适用场景上的差异,为您在不同业务需求下做出合理的技术选型提供决策支持。
对比分析
技术架构
两种产品的设计根源差异决定了它们在弹性、一致性和数据模型上的不同取舍。
对比维度 | 列存索引(IMCI) | 社区版Elasticsearch |
核心架构 | 计算存储分离。计算节点与存储节点可独立扩展,支持规格升降和节点增删。这种架构为处理高并发、低延迟的HTAP负载提供了秒级弹性伸缩。 | 计算存储一体。通过增加节点来同时扩展计算和存储能力。虽然Elasticsearch近年来通过冷热分层和可搜索快照(Searchable Snapshots)等功能优化了存储架构,但其核心的弹性伸缩和资源隔离能力仍受限于一体化设计。 |
资源隔离 | 强隔离。基于计算存储分离架构,可以实现严格的读写分离和多业务负载隔离,确保分析查询(AP)不会影响在线事务(TP)的稳定性。 | 有限隔离。主要通过节点角色和线程池模型提供计算隔离,但在高负载下,不同任务(如索引、搜索、聚合)之间仍可能发生资源争抢,海量数据下的复杂查询可能因OOM(Out of Memory)影响服务稳定性。 |
数据一致性 | 事务级一致性。作为PolarDB的一部分,列存索引(IMCI)的全文索引与主库数据通过事务紧密耦合。数据写入即对索引可见,不存在同步延迟,保证了数据与索引的强一致性。 | 近实时(Near Real-time)。写入的数据需要经过一个可配置的刷新间隔( |
数据模型 | 关系型模型+列式存储。支持标准SQL,能够高效处理复杂的多表关联(JOIN)、子查询和窗口函数。数据以高压缩率的列式格式存储,尤其适合聚合分析。 | 文档模型+倒排索引。数据以JSON文档形式存储。这种反范式设计非常适合全文检索和简单聚合,但原生不支持多表关联和复杂子查询。 |
核心功能
下表详细对比了两者在SQL分析、全文检索和存储成本等方面的核心功能差异。
功能分类 | 特性 | 列存索引(IMCI) | 社区版Elasticsearch |
SQL分析能力 | 查询语言 | 标准SQL,100%兼容MySQL | 专用JSON格式的DSL,学习成本较高。也提供SQL接口,但功能受限。 |
多表关联/子查询 | 支持 | 不支持。需要通过应用层逻辑、嵌套文档或宽表等方式绕过。 | |
复杂聚合/窗口函数 | 支持 | 仅支持有限的聚合分析,不支持窗口函数。 | |
物化视图 | 支持。可预计算并存储复杂查询结果,加速报表和分析场景。 | 不支持。 | |
全文检索能力 | 关键词检索 | 支持。兼容 MySQL的 | 支持。功能丰富,支持 |
短语查询 | 暂不支持。 | 支持。通过 | |
正则/通配符查询 | 暂不支持。 | 支持。通过 | |
相似度评分 | 灰度中。 | 支持。通过TEXT支持的索引功能,可以包含词频和位置信息。利用 | |
分词器 | 支持 | 支持 | |
自定义词典 | 支持对 | 支持 | |
停用词 | 支持 | 支持 | |
同义词 | 暂不支持。 | 支持。 | |
拼写纠错 | 暂不支持。 | 支持,提供 | |
=加速 | 支持。利用倒排索引加速字符串精确匹配。 | 支持,通过 | |
IN加速 | 支持。利用倒排索引加速多值精确匹配。 | 支持,通过 | |
LIKE查询加速 | 支持。无需修改SQL,通过 | 支持。通过 | |
JSON查询加速 | 支持。可通过 | 不支持。 | |
存储与成本 | 数据压缩率 | 高。列式存储的整体压缩率通常超过5:1。 | 中等。整体压缩率通常约为1.5:1。 |
索引空间 | 小。倒排索引空间占总列存存储通常不超过5%。 | 较大。为支持丰富的检索功能,倒排索引会占用可观的存储空间。 | |
表结构变更 | 灵活。支持DDL动态增删列或索引,对在线业务无感。 | 不灵活。索引一旦建立后难以变更,修改字段类型通常需要重建索引(Reindex)。 |
性能测试
为量化两者在全文检索场景下的性能差异,我们在相同的数据集和硬件环境下进行了一项特定的基准测试。
本测试仅针对单线程下,对不同词频的关键词进行全表计数这一特定场景,旨在衡量基础的倒排索引检索效率。测试结果不代表两款产品在所有场景下的综合性能表现。真实的业务性能取决于查询类型、并发量、数据模式和硬件配置等多种因素。
测试环境与数据集
数据集: Elasticsearch Rally Hub的
http_logs数据集,包含约2.47亿条日志记录。核心配置:单线程执行并关闭查询缓存,以确保测试测量的是实际的计算开销。
列存索引(IMCI):
# 设置单线程 SET imci_max_dop = 1; # 开启 like 加速 SET imci_convert_like_to_match = 2; # smid like 需要开启 SET imci_enable_like_searcher = ON;Doris:
# 设置单线程 SET parallel_pipeline_task_num = 1;Elasticsearch:
# 配置为单线程 thread_pool.search.size: 1 # 关闭 query cache curl -H "Content-Type: application/json" -X PUT "http://localhost:9200/http_logs/_settings?reopen=true" --data '{"index.queries.cache.enabled": false}'
测试方法
测试执行SELECT COUNT(*)操作,分别使用LIKE和MATCH语法在request字段中查找四种不同频率的词元。Elasticsearch使用match查询。
Elasticsearch计数方式的说明:在测试中,Elasticsearch采用了aggs聚合脚本进行计数,而非使用count方式。这是因为aggs方式与PolarDB的执行方式一致,即遍历命中行并累积出数量;而count方式则是直接返回倒排表的数量。搜索业务通常较少直接或仅求得数量的场景,通常是检索出命中行并读取相应数据。
查询语句
高频词元:基本全部命中,约2.47亿。
SELECT COUNT(*) FROM http_logs WHERE request LIKE "%http%"; SELECT COUNT(*) FROM http_logs WHERE MATCH(request) AGAINST("http"); SELECT COUNT(*) FROM http_logs WHERE request MATCH_ANY 'http'; curl -H 'Content-Type: application/json' -X POST 'http://127.0.0.1:9200/http_logs/_search?size=0' --data-binary '{"query":{"match":{"request":"http"}},"aggs":{"doc_count":{"value_count":{"script":{"source":"1"}}}}}'较高频词元:命中1千500万行左右。
SELECT COUNT(*) FROM http_logs WHERE request LIKE "%french%"; SELECT COUNT(*) FROM http_logs WHERE MATCH(request) AGAINST("french"); SELECT COUNT(*) FROM http_logs WHERE request MATCH_ANY "french"; curl -X POST 'http://127.0.0.1:9200/http_logs/_search?size=0' -H 'Content-Type: application/json' --data-binary '{"query":{"match":{"request":"french"}},"aggs":{"doc_count":{"value_count":{"script":{"source":"1"}}}}}'较低频词元:命中8万行左右。
SELECT COUNT(*) FROM http_logs WHERE request LIKE "%post%"; SELECT COUNT(*) FROM http_logs WHERE MATCH(request) AGAINST("post"); SELECT COUNT(*) FROM http_logs WHERE request MATCH_ANY "post"; curl -X POST 'http://127.0.0.1:9200/http_logs/_search?size=0' -H 'Content-Type: application/json' --data-binary '{"query":{"match":{"request":"POST"}},"aggs":{"doc_count":{"value_count":{"script":{"source":"1"}}}}}'低频词元:命中100行左右。
SELECT COUNT(*) FROM http_logs WHERE request LIKE "%Mozilla%"; SELECT COUNT(*) FROM http_logs WHERE MATCH(request) AGAINST("Mozilla"); SELECT COUNT(*) FROM http_logs WHERE request MATCH_ANY "Mozilla"; curl -X POST 'http://127.0.0.1:9200/http_logs/_search?size=0' -H 'Content-Type: application/json' --data-binary '{"query":{"match":{"request":"Mozilla"}},"aggs":{"doc_count":{"value_count":{"script":{"source":"1"}}}}}'
查询结果与分析
查询结果
对比项 | 高频词元 | 较高频词元 | 较低频词元 | 低频词元 | |
PolarDB | LIKE | 1 min 21.96 sec | 1 min 18.44 sec | 1 min 24.59 sec | 1 min 31.19 sec |
SMID LIKE | 25.46 sec | 22.80 sec | 21.98 sec | 21.60 sec | |
MATCH(自研FTS库) | 2.44 sec | 0.33 sec | 0.01 sec | 0.00 sec | |
MATCH(自研FTS库) 说明
| 2.33 sec | 0.15 sec | 0.01 sec | 0.00 sec | |
Doris(CLucene库) 说明
| 3.49 sec | 0.24 sec | 0.03 sec | 0.03 sec | |
Elasticsearch(Lucene库) | 6.457 sec | 0.482 sec | 0.022 sec | 0.018 sec | |

查询性能分析
MATCH性能优越:在本次单线程、单关键词计数测试中,列存索引(IMCI)的MATCH函数在各个词频下均表现出最低的查询延迟,尤其在处理低频词时达到毫秒级响应。LIKE加速效果显著:列存索引(IMCI)通过SMID加速将全表扫描的LIKE查询(耗时超过1分钟)加速至20秒级别,通过倒排索引加速至毫秒级别,证明了其对现有业务SQL透明加速的能力。
索引空间大小与分析
列存索引(IMCI)的全文索引(倒排索引)具备较高的存储效率。通过优化数据排序,其空间占用可远低于Elasticsearch等专用搜索引擎,有效控制存储成本。
空间大小
索引空间占用主要受分词方式和数据物理有序性的影响。以下是在http_logs测试数据集(总行数2.47亿)下,不同产品或配置的倒排索引空间占用对比:
对比项 | 索引空间占用 |
列存索引(IMCI)默认排序 | 1.3 GB |
列存索引(IMCI)按 | 37 MB |
Doris按 | 49 MB |
Elasticsearch | 2.3 GB |
空间分析
当数据在物理存储上依据被索引的列(例如,本例中的request列)进行排序时,倒排索引中记录的行号(RowID)将呈现连续或准连续递增。这种有序性使得行程长度编码(RLE)等压缩算法能够实现极高的压缩效率,从而将索引大小从GB级别降低至MB级别。总体而言,倒排索引的空间占比通常不会超过该列存储空间的30%。一般情况下,需创建倒排索引的列数远少于表的总列数,因此在大多数情况下,倒排索引的空间占总列存储的比例不超过5%。
总结
Elasticsearch常应用于实时分析、全文检索和向量搜索等。
实时分析主要包括点查、多维度精准匹配与简单聚合。为支持快速查找和过滤,对每一列均构建了倒排索引,而这正是列存索引(IMCI)的强项,完全可以迁移至列存索引(IMCI)的。
全文检索是Elasticsearch的强项,但其优势主要体现在功能丰富性上,而非性能。如果业务未充分利用高级检索功能,则可以考虑采用列存索引(IMCI)提供的全文索引功能,特别适用于数据库/数据仓库与Elasticsearch独立搜索的双引擎架构。
列存索引(IMCI)同样具备强大的向量搜索功能。
综上所述,在绝大多数情况下,列存索引(IMCI)能够完全替代Elasticsearch,唯独在需要使用Elasticsearch高级全文检索功能时,才能体现后者的优势。
附录:测试流程
本附录提供了一个详细的操作指南,旨在帮助您复现PolarDB 列存索引(IMCI)、Elasticsearch及Doris在http_logs数据集上的全文检索性能测试。整个流程包括准备数据集、为各产品部署和导入数据,以及执行查询。
准备测试数据集
本次测试使用由ESRally(Elastic官方基准测试工具)提供的http_logs数据集。
首先,克隆ESRally的数据轨道仓库,并下载所需的数据集。
# 克隆仓库
git clone https://github.com/elastic/rally-tracks.git
# 进入目录并下载 http_logs 数据集
cd rally-tracks
./download.sh http_logs执行完毕后,您将获得一个约1.7 GB的压缩包rally-track-data-http_logs.tar。解压后,数据文件大小约32 GB,包含2.47亿行JSON格式的日志数据。
导入数据
部署与导入数据至PolarDB列存索引(IMCI)
以下步骤将指导您在PolarDB中准备数据并创建用于全文检索的列存索引(IMCI)。
创建初始数据表:由于数据集中存在少量与MySQL
JSON类型不兼容的记录,首先使用VARCHAR类型来存储原始日志。CREATE TABLE http_logs( logs VARCHAR(4096) );导入原始数据:解压下载的数据文件,然后使用
LOAD DATA命令将数据导入到http_logs表中。# 解压所有 .bz2 文件 bunzip2 *.json.bz2-- 登录到 PolarDB 数据库后执行 LOAD DATA LOCAL INFILE '/path/to/your/documents-xxx.json' INTO TABLE http_logs COLUMNS TERMINATED BY '\n';创建列存索引与倒排索引:为实现高性能分析,需要添加列存索引(IMCI)并为
request字段创建倒排索引。添加虚拟列以提取
request字段:通过虚拟列,在不改变原始数据的情况下,动态提取每条日志中的request字段。ALTER TABLE http_logs ADD COLUMN request VARCHAR(1024) AS ( CASE WHEN JSON_VALID(logs) THEN (JSON_UNQUOTE(JSON_EXTRACT(logs, '$.request'))) ELSE NULL END );为表添加列存索引并创建倒排索引:通过修改表和列的
COMMENT,您可以为表开启IMCI并为request列创建倒排索引。-- 步骤一:为表添加列存索引 ALTER TABLE http_logs COMMENT 'columnar=1'; -- 步骤二:为 request 列创建倒排索引 ALTER TABLE http_logs MODIFY COLUMN request VARCHAR(1024) AS ( CASE WHEN json_valid(logs) THEN (json_unquote(json_extract(logs, '$.request'))) ELSE NULL END ) COMMENT 'imci_fts(type=2 mode=1 pack_cnt_max=2048)';
验证索引状态:您可以通过以下命令查看列存数据块的构建进度和倒排索引的构建进度。当
NEXT_PACK_ID的值接近NUM_PACKS时,表示索引构建已基本完成。-- 查看列存数据块的构建状态 SHOW IMCI INDEXES; -- 查看倒排索引的构建状态 SHOW IMCI INDEXES FULLTEXT;
部署与导入数据至Elasticsearch
在进行操作时,除了下述手动创建索引和导入数据的步骤外,您还可以通过esrally实现自动化操作。
pip3 install esrally
esrally --offline --pipeline=benchmark-only --track=http_logs --challenge=append-no-conflicts-index-only --target-hosts=127.0.0.1:9200 --track-params="number_of_shards:1,number_of_replicas:0"创建索引:首先,在Elasticsearch中创建一个名为
http_logs的索引,并定义字段映射。这里将request字段类型设置为match_only_text以优化纯文本搜索。重要在创建新索引之前,会先删除所有可能存在的旧索引。如果您的业务中存在同名索引,请务必手动更换索引名称。
# 删除可能存在的旧索引(请注意,这并非您的业务索引) curl -X DELETE 'http://127.0.0.1:9200/http_logs' # 创建新索引并定义 Mapping curl -X PUT 'http://127.0.0.1:9200/http_logs' -H 'Content-Type: application/json' --data-binary ' { "settings": { "number_of_shards": 1, "number_of_replicas": 0 }, "mappings": { "properties": { "@timestamp": { "type": "date" }, "message": { "type": "keyword", "index": false, "doc_values": false }, "clientip": { "type": "ip" }, "request": { "type": "match_only_text" }, "status": { "type": "integer" }, "size": { "type": "integer" }, "geoip" : { "properties" : { "country_name": { "type": "keyword" }, "city_name": { "type": "keyword" }, "location" : { "type" : "geo_point" } } } } } }'准备数据并导入:由于Elasticsearch的Bulk API有单次请求的大小限制,需要先将原始JSON文件处理成NDJSON格式,并分割成多个小文件,然后分批导入。
合并与分割文件:将所有解压后的JSON文件合并为一个大文件,然后按每1000万行分割成多个部分。
# 合并所有 JSON 文件 cat documents-*.json > documents-merged.json # 按 1000 万行分割 split -l 10000000 -d documents-merged.json documents-merged-part-转换为Bulk格式 (NDJSON):为每个分割后的文件添加 Bulk API 所需的元数据头部。
for f in documents-merged-part-*; do awk '{ print "{ \"index\" : {\"_index\":\"http_logs\"} }"; print }' $f > $f.ndjson done
执行批量导入:循环调用Bulk API,将所有
*.ndjson文件导入Elasticsearch。for f in documents-merged-part-*.ndjson; do curl -s -o /dev/null -w "%{http_code}\n" -H 'Content-Type: application/x-ndjson' -X POST 'http://127.0.0.1:9200/http_logs/_bulk' --data-binary @$f done刷新并验证数据:导入完成后,手动刷新索引并检查数据总数。
curl 'http://127.0.0.1:9200/http_logs/_refresh' curl 'http://127.0.0.1:9200/http_logs/_count'
部署与导入数据至Doris
创建表与倒排索:在Doris中创建表,并使用
USING INVERTED语法为request列创建倒排索引。CREATE TABLE http_logs( logs VARCHAR(4096), request VARCHAR(1024), INDEX request_idx (request) USING INVERTED PROPERTIES("parser" = "chinese") ) DISTRIBUTED BY HASH(request) BUCKETS 1 PROPERTIES ("replication_num" = "1");导入数据:您可以选择以下任一方式将PolarDB中的数据导入Doris。
方法一:通过SQL文件中转
先将PolarDB的数据导出为SQL文件,然后在Doris中执行该文件。# 从 PolarDB 导出数据 mysqldump --compact --skip-set-charset --host=<polardb_host> --port=<polardb_port> -u<user> -p<password> <database_name> http_logs > dbname_http_logs.sql # 登录 Doris 后导入数据 use <database_name> source dbname_http_logs.sql方法二:通过
ENGINE=MYSQL表(若支持)
如果您的Doris版本支持MYSQL表引擎,可以创建一个外部表直接映射到PolarDB的表,然后通过INSERT INTO SELECT导入数据。-- 创建 MySQL 外部表 CREATE TABLE mysql_http_logs ( logs varchar(4096), request varchar(1024) ) ENGINE=MYSQL PROPERTIES ( "host" = "<polardb_host>", "port" = "<polardb_port>", "user" = "<user>", "password" = "<password>", "database" = "<database_name>", "table" = "http_logs" ); -- 从外部表导入数据 INSERT INTO http_logs SELECT * FROM mysql_http_logs;