千家信息网

Lucene查询原理是什么

发表于:2025-11-08 作者:千家信息网编辑
千家信息网最后更新 2025年11月08日,本篇内容介绍了"Lucene查询原理是什么"的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!Lucene
千家信息网最后更新 2025年11月08日Lucene查询原理是什么

本篇内容介绍了"Lucene查询原理是什么"的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!

Lucene查询原理

本节主要是一些Lucene的背景知识,了解这些知识的同学可以略过。

Lucene的数据结构和查询原理

Elasticsearch的底层是Lucene,可以说Lucene的查询性能就决定了Elasticsearch的查询性能。

Lucene查询原理

Lucene中最重要的就是它的几种数据结构,这决定了数据是如何被检索的,本文再简单描述一下几种数据结构:

  1. FST:保存term字典,可以在FST上实现单Term、Term范围、Term前缀和通配符查询等。

  2. 倒排链:保存了每个term对应的docId的列表,采用skipList的结构保存,用于快速跳跃。

  3. BKD-Tree:BKD-Tree是一种保存多维空间点的数据结构,用于数值类型(包括空间点)的快速查找。

  4. DocValues:基于docId的列式存储,由于列式存储的特点,可以有效提升排序聚合的性能。

组合条件的结果合并

了解了Lucene的数据结构和基本查询原理,我们知道:

  1. 对单个词条进行查询,Lucene会读取该词条的倒排链,倒排链中是一个有序的docId列表。

  2. 对字符串范围/前缀/通配符查询,Lucene会从FST中获取到符合条件的所有Term,然后就可以根据这些Term再查找倒排链,找到符合条件的doc。

  3. 对数字类型进行范围查找,Lucene会通过BKD-Tree找到符合条件的docId集合,但这个集合中的docId并非有序的。

现在的问题是,如果给一个组合查询条件,Lucene怎么对各个单条件的结果进行组合,得到最终结果。简化的问题就是如何求两个集合的交集和并集。

1. 对N个倒排链求交集

上面Lucene原理分析的文章中讲过,N个倒排链求交集,可以采用skipList,有效的跳过无效的doc。

2. 对N个倒排链求并集

处理方式一:仍然保留多个有序列表,多个有序列表的队首构成一个优先队列(最小堆),这样后续可以对整个并集进行iterator(堆顶的队首出堆,队列里下一个docID入堆),也可以通过skipList的方式向后跳跃(各个子列表分别通过skipList跳)。这种方式适合倒排链数量比较少(N比较小)的场景。

处理方式二:倒排链如果比较多(N比较大),采用方式一就不够划算,这时候可以直接把结果合并成一个有序的docID数组。

处理方式三:方式二中,直接保存原始的docID,如果docID非常多,很消耗内存,所以当doc数量超过一定值时(32位docID在BitSet中只需要一个bit,BitSet的大小取决于segments里的doc总数,所以可以根据doc总数和当前doc数估算是否BitSet更加划算),会采用构造BitSet的方式,非常节约内存,而且BitSet可以非常高效的取交/并集。

3. BKD-Tree的结果怎么跟其他结果合并

通过BKD-Tree查找到的docID是无序的,所以要么先转成有序的docID数组,或者构造BitSet,然后再与其他结果合并。

查询顺序优化

如果采用多个条件进行查询,那么先查询代价比较小的,再从小结果集上进行迭代,会更优一些。Lucene中做了很多这方面的优化,在查询前会先估算每个查询的代价,再决定查询顺序。

结果排序

默认情况下,Lucene会按照Score排序,即算分后的分数值,如果指定了其他的Sort字段,就会按照指定的字段排序。那么,排序会非常影响性能吗?首先,排序并不会对所有命中的doc进行排序,而是构造一个堆,保证前(Offset+Size)个数的doc是有序的,所以排序的性能取决于(Size+Offset)和命中的文档数,另外就是读取docValues的开销。因为(Size+Offset)并不会太大,而且docValues的读取性能很高,所以排序并不会非常的影响性能。

各场景查询性能分析

上一节讲了一些查询相关的理论知识,那么本节就是理论结合实践,通过具体的一些测试数字来分析一下各个场景的性能。测试采用单机单Shard、64核机器、SSD磁盘,主要分析各个场景的计算开销,不考虑操作系统Cache的影响,测试结果仅供参考。

单Term查询

ES中建立一个Index,一个shard,无replica。有1000万行数据,每行只有几个标签和一个唯一ID,现在将这些数据写入这个Index中。其中Tag1这个标签只有a和b两个值,现在要从1000万行中找到一条Tag1=a的数据(约500万)。给出以下查询,那么它耗时如何呢:请求:{  "query": {    "constant_score": {      "filter": {        "term": {          "Tag1": "a"        }      }    }  },  "size": 1}'响应:{"took":233,"timed_out":false,"_shards":{"total":1,"successful":1,"skipped":0,"failed":0},"hits":{"total":5184867,"max_score":1.0,"hits":...}

这个请求耗费了233ms,并且返回了符合条件的数据总数:5184867条。

对于Tag1="a"这个查询条件,我们知道是查询Tag1="a"的倒排链,这个倒排链的长度是5184867,是非常长的,主要时间就花在扫描这个倒排链上。其实对这个例子来说,扫描倒排链带来的收益就是拿到了符合条件的记录总数,因为条件中设置了constant_score,所以不需要算分,随便返回一条符合条件的记录即可。对于要算分的场景,Lucene会根据词条在doc中出现的频率来计算分值,并取分值排序返回。

目前我们得到一个结论,233ms时间至少可以扫描500万的倒排链,另外考虑到单个请求是单线程执行的,可以粗略估算,一个CPU核在一秒内扫描倒排链内doc的速度是千万级的。

我们再换一个小一点的倒排链,长度为1万,总共耗时3ms。

{"took":3,"timed_out":false,"_shards":{"total":1,"successful":1,"skipped":0,"failed":0},"hits":{"total":10478,"max_score":1.0,"hits":...}

Term组合查询

首先考虑两个Term查询求交集:

对于一个Term的组合查询,两个倒排链分别为1万和500万,合并后符合条件的数据为5000,查询性能如何呢?请求:{  "size": 1,  "query": {    "constant_score": {      "filter": {        "bool": {          "must": [            {              "term": {                "Tag1": "a"  // 倒排链长度500万              }            },            {              "term": {                "Tag2": "0" // 倒排链长度1万              }            }          ]        }      }    }  }}响应:{"took":21,"timed_out":false,"_shards":{"total":1,"successful":1,"skipped":0,"failed":0},"hits":{"total":5266,"max_score":2.0,"hits":...}

这个请求耗时21ms,主要是做两个倒排链的求交操作,因此我们主要分析skipList的性能。

这个例子中,倒排链长度是1万、500万,合并后仍有5000多个doc符合条件。对于1万的倒排链,基本上不进行skip,因为一半的doc都是符合条件的,对于500万的倒排链,平均每次skip1000个doc。因为倒排链在存储时最小的单位是BLOCK,一个BLOCK一般是128个docID,BLOCK内不会进行skip操作。所以即使能够skip到某个BLOCK,BLOCK内的docID还是要顺序扫描的。所以这个例子中,实际扫描的docID数粗略估计也有几十万,所以总时间花费了20多ms也符合预期。

对于Term查询求并集呢,将上面的bool查询的must改成should,查询结果为:

{"took":393,"timed_out":false,"_shards":{"total":1,"successful":1,"skipped":0,"failed":0},"hits":{"total":5190079,"max_score":1.0,"hits":...}

花费时间393ms,所以求并集的时间是多于其中单个条件查询的时间。

字符串范围查询

RecordID是一个UUID,1000万条数据,每个doc都有一个唯一的uuid,从中查找0~7开头的uuid,大概结果有500多万个,性能如何呢?请求:{  "query": {    "constant_score": {      "filter": {        "range": {          "RecordID": {            "gte": "0",            "lte": "8"          }        }      }    }  },  "size": 1}响应:{"took":3001,"timed_out":false,"_shards":{"total":1,"successful":1,"skipped":0,"failed":0},"hits":{"total":5185663,"max_score":1.0,"hits":...}查询a开头的uuid,结果大概有60多万,性能如何呢?请求:{  "query": {    "constant_score": {      "filter": {        "range": {          "RecordID": {            "gte": "a",            "lte": "b"          }        }      }    }  },  "size": 1}响应:{"took":379,"timed_out":false,"_shards":{"total":1,"successful":1,"skipped":0,"failed":0},"hits":{"total":648556,"max_score":1.0,"hits":...}

这个查询我们主要分析FST的查询性能,从上面的结果中我们可以看到,FST的查询性能相比扫描倒排链要差许多,同样扫描500万的数据,倒排链扫描只需要不到300ms,而FST上的扫描花费了3秒,基本上是慢十倍的。对于UUID长度的字符串来说,FST范围扫描的性能大概是每秒百万级。

字符串范围查询加Term查询

字符串范围查询(符合条件500万),加上两个Term查询(符合条件5000),最终符合条件数目2600,性能如何?请求:{  "query": {    "constant_score": {      "filter": {        "bool": {          "must": [            {              "range": {                "RecordID": {                  "gte": "0",                  "lte": "8"                }              }            },            {              "term": {                "Tag1": "a"              }            },            {              "term": {                "Tag2": "0"              }            }          ]        }      }    }  },  "size": 1}结果:{"took":2849,"timed_out":false,"_shards":{"total":1,"successful":1,"skipped":0,"failed":0},"hits":{"total":2638,"max_score":1.0,"hits":...}

这个例子中,查询消耗时间的大头还是在扫描FST的部分,通过FST扫描出符合条件的Term,然后读取每个Term对应的docID列表,构造一个BitSet,再与两个TermQuery的倒排链求交集。

数字Range查询

对于数字类型,我们同样从1000万数据中查找500万呢?请求:{  "query": {    "constant_score": {      "filter": {        "range": {          "Number": {            "gte": 100000000,            "lte": 150000000          }        }      }    }  },  "size": 1}响应:{"took":567,"timed_out":false,"_shards":{"total":1,"successful":1,"skipped":0,"failed":0},"hits":{"total":5183183,"max_score":1.0,"hits":...}

这个场景我们主要测试BKD-Tree的性能,可以看到BKD-Tree查询的性能还是不错的,查找500万个doc花费了500多ms,只比扫描倒排链差一倍,相比FST的性能有了很大的提升。地理位置相关的查询也是通过BKD-Tree实现的,性能很高。

数字Range查询加Term查询

这里我们构造一个复杂的查询场景,数字Range范围数据500万,再加两个Term条件,最终符合条件数据2600多条,性能如何?请求:{  "query": {    "constant_score": {      "filter": {        "bool": {          "must": [            {              "range": {                "Number": {                  "gte": 100000000,                  "lte": 150000000                }              }            },            {              "term": {                "Tag1": "a"              }            },            {              "term": {                "Tag2": "0"              }            }          ]        }      }    }  },  "size": 1}响应:{"took":27,"timed_out":false,"_shards":{"total":1,"successful":1,"skipped":0,"failed":0},"hits":{"total":2638,"max_score":1.0,"hits":...}

这个结果出乎我们的意料,竟然只需要27ms!因为在上一个例子中,数字Range查询耗时500多ms,而我们增加两个Term条件后,时间竟然变为27ms,这是为何呢?

实际上,Lucene在这里做了一个优化,底层有一个查询叫做IndexOrDocValuesQuery,会自动判断是查询Index(BKD-Tree)还是DocValues。在这个例子中,查询顺序是先对两个TermQuery求交集,得到5000多个docID,然后读取这个5000多个docID对应的docValues,从中筛选符合数字Range条件的数据。因为只需要读5000多个doc的docValues,所以花费时间很少。

"Lucene查询原理是什么"的内容就介绍到这里了,感谢大家的阅读。如果想了解更多行业相关的知识可以关注网站,小编将为大家输出更多高质量的实用文章!

查询 条件 性能 数据 结果 两个 排序 时间 数字 方式 范围 原理 有序 场景 多个 交集 结构 长度 分析 例子 数据库的安全要保护哪些东西 数据库安全各自的含义是什么 生产安全数据库录入 数据库的安全性及管理 数据库安全策略包含哪些 海淀数据库安全审计系统 建立农村房屋安全信息数据库 易用的数据库客户端支持安全管理 连接数据库失败ssl安全错误 数据库的锁怎样保障安全 典型网络安全威胁映射 立健 软件开发 中电28所软件开发提供住宿 网络安全专业考公务员有哪些岗位 数据库没有启动请查找原因 国家网络安全法第29条 英国软件开发兼职 软件开发的工资是多少钱 上海机慧网络技术有限公司 网络安全兼职的正规平台 教育软件网络技术发展 囯家网络安全工程师认证 游戏服务器被关闭怎么解决 局域网qq 服务器 大庆逐鹿中原服务器属于什么网络 黑客倾向于网络安全 兴义软件开发app 舟山学软件开发需要学什么 软件开发怎么越学压力越大 北京创新网络技术有限公司 网络技术发展路径 信息化应用网络安全 江西赣州宏图软件开发 网络安全专业考公务员有哪些岗位 江苏太仓dns服务器云主机 金税盘安全接入服务器地址是哪个 北京学软件开发那家好知乎 华岩数据库安装 关于网络安全的感想标语 系统服务器记录操作ip吗
0