关于mongodb的索引

往期回顾:

小改ARL (zgbsm.online)

大改小改ARL (zgbsm.online)

关于攻防场景下资产探测扫描的加速策略 – zgbsm’s note

大伙可能还记得我之前做的小改ARL,其实这个项目能坚持下来,有一部分原因就是目前开源产品里面还没有一个让我感觉很喜欢的资产收集平台,前几天看了绿盟ez,那玩意没有截图,而且需要开license才能用,实名渗透了属于是

这几天看了一个叫ScopeSentry的扫描器,作为一款新兴扫描器,个人感觉他的稳定性其实算是不错的了,就是内存吃的有点多,而且没有站点截图,相同内存占用下的速度跟原版ARL不相上下吧,不过ScopeSentry支持类似fofa那样的资产检索、支持网页内容监控、子域接管检测,感觉还是有点意思的,但是攻防用起来可能还差点意思

然后之前看过的nemo_go扫描器,感觉这个扫描器的作者安排的扫描逻辑有问题,而且遇到泛解析域名的时候会爆炸(相当于被客户ddos),很抽象

还有个rengine,这个好像是外国人写的,看介绍感觉很强,概念神,但是之前测的时候发现他不能扫IP,现在好像加上了扫IP的功能,不知道效果怎么样

结合我每天干的事情,我这个”大改小改ARL“未来也是要支持类fofa检索的,不仅如此,我还要做资产变化记录,将每次任务的扫描结果进行比对,然后整理出一份新增资产的表格;后续也要支持定时任务

既然确定了发展方向,那就有几个问题

刚刚把子域名扫描写完了,也想好了资产变化记录那个东西该怎么做,接下来就需要建索引了;我对mongodb的索引功能一无所知,所以现在学习一下

不过今天这里只是简单了解一下mongodb索引的用法,不涉及实现原理之类的,因为我也不懂实现原理,现在我看mongodb索引的适用场景和限制之类的东西感觉很有意思,像看规则怪谈一样

参考资料:Indexes - MongoDB Manual v7.0

关于Shard Keys和Hashed Indexes的事先不提,这俩好像跟分布式有关的

single field index

mongodb可以为某个单独的field创建索引,比如说我们存了篇文章,文章包括标题title、作者author、正文content、编辑时间update_date。那么我们就可以在update_date列创建一个single field index,这样在sort的时候就可以加速

1719051101455.png

上图是官方文档画的一个关于索引排序的示意图,因为mongo的索引在创建的时候需要指定排序方向

不过官方文档又说这个方向是无所谓的,mongodb在跑索引之前会自适应方向

compound index

这种索引支持多个fied创建到同一个索引里,示意图如下

1719051371881.png

上面示意图创建了两个field的compound index,这两个field分别是userid和score,因为userid在创建的时候写在前面,所以索引会优先按照userid排序,再按照score排序

compound index支持多个field,但是它用起来有很多限制:

multikey index

这个索引是给数组用的,示意图如下

1719107217375.png

这个索引可以加快mongodb对数组的检索速度,大伙也可以在compound index里面使用multikey index,但是每个compund index里面只能有一个multikey index,也就是说,在创建compound index的时候,只能有1个field是数组

排序限制

multikey index在排序的时候也有限制,如果不遵守这些限制,mongodb在排序的时候会将数据读取到内存再排序,从而影响效率:

可能第二个限制不太容易理解,我也不太理解,因为我对Mongodb的使用还比较有限,以后用的多了可能会遇到这种情况

覆盖查询

覆盖查询指的是我们查询的数据直接就在索引上,不需要再去取文档数据

当compound index中包含multikey index时,取出来的数据不能包含不同索引的内容,否则覆盖查询无效

例如我们创建这样的查询:

db.matches.insertMany( [  
   { name: "joe", event: ["open", "tournament"] },  
   { name: "bill", event: ["match", "championship"] }  
] )  
  
db.matches.createIndex( { name: 1, event: 1 } )  
  
db.matches.find( { name: "joe" },{_id:0,name:1} ).explain()

可以看到mongo的查询顺序,直接IXSCAN(扫索引)就输出了,没有FETCH,说明mongo只扫了索引,没有去FETCH数据

{  
  "namespace": "admin.matches",  
  "indexFilterSet": false,  
  "parsedQuery": {  
    "name": {  
      "$eq": "joe"  
    }  
  },  
  "queryHash": "039857FA",  
  "planCacheKey": "C56E2D60",  
  "maxIndexedOrSolutionsReached": false,  
  "maxIndexedAndSolutionsReached": false,  
  "maxScansToExplodeReached": false,  
  "winningPlan": {  
    "stage": "PROJECTION_COVERED",  
    "transformBy": {  
      "_id": new NumberInt("0"),  
      "name": new NumberInt("1")  
    },  
    "inputStage": {  
      "stage": "IXSCAN",  
      "keyPattern": {  
        "name": new NumberInt("1"),  
        "event": new NumberInt("1")  
      },  
      "indexName": "name_1_event_1",  
      "isMultiKey": true,  
      "multiKeyPaths": {  
        "name": [  
        ],  
        "event": [  
          "event"  
        ]  
      },  
      "isUnique": false,  
      "isSparse": false,  
      "isPartial": false,  
      "indexVersion": new NumberInt("2"),  
      "direction": "forward",  
      "indexBounds": {  
        "name": [  
          "[\"joe\", \"joe\"]"  
        ],  
        "event": [  
          "[MinKey, MaxKey]"  
        ]  
      }  
    }  
  },  
  "rejectedPlans": [  
  ]  
}

把查询输出结果改为输出多个字段:

db.matches.find( { name: "joe" },{_id:0,name:1, event:1} ).explain()

mongo的结果如下:

{  
  "namespace": "admin.matches",  
  "indexFilterSet": false,  
  "parsedQuery": {  
    "name": {  
      "$eq": "joe"  
    }  
  },  
  "queryHash": "D6970C41",  
  "planCacheKey": "F868C008",  
  "maxIndexedOrSolutionsReached": false,  
  "maxIndexedAndSolutionsReached": false,  
  "maxScansToExplodeReached": false,  
  "winningPlan": {  
    "stage": "PROJECTION_SIMPLE",  
    "transformBy": {  
      "_id": new NumberInt("0"),  
      "name": new NumberInt("1"),  
      "event": new NumberInt("1")  
    },  
    "inputStage": {  
      "stage": "FETCH",  
      "inputStage": {  
        "stage": "IXSCAN",  
        "keyPattern": {  
          "name": new NumberInt("1"),  
          "event": new NumberInt("1")  
        },  
        "indexName": "name_1_event_1",  
        "isMultiKey": true,  
        "multiKeyPaths": {  
          "name": [  
          ],  
          "event": [  
            "event"  
          ]  
        },  
        "isUnique": false,  
        "isSparse": false,  
        "isPartial": false,  
        "indexVersion": new NumberInt("2"),  
        "direction": "forward",  
        "indexBounds": {  
          "name": [  
            "[\"joe\", \"joe\"]"  
          ],  
          "event": [  
            "[MinKey, MaxKey]"  
          ]  
        }  
      }  
    }  
  },  
  "rejectedPlans": [  
  ]  
}

可以看到mongo先进行IXSCAN,再FETCH,最后才输出

在我们输出的内容中,name不是multikey index,event是multikey index,当单独输出name时,可触发覆盖查询,name和event一起输出时,不能覆盖查询,因为它们是不同类型的索引

数组作为查询条件

有时用户的查询条件输入一个数组,这时multikey index只会在比对第一个元素的时候起作用,其它元素比对的时候无效

实际运作的时候,mongo会将用户输入的数组第一个元素放到索引上查,再在查出来的结果中查询剩下的元素

text index

全文检索索引,别看了,不支持中文

这是mongodb全文索引支持的语言列表:

Language Name ISO 639-1 (Two letter codes)
danish da
dutch nl
english en
finnish fi
french fr
german de
hungarian hu
italian it
norwegian nb
portuguese pt
romanian ro
russian ru
spanish es
swedish sv
turkish tr

wildcard index

我个人认为这是mongodb最有意思的一个索引

mongo官方也知道这样一个事实,他们的用户——开发者们,有时候也不知道mongodb里面会出现什么样的数据,因为mongo强就强在这点:无论是什么样的数据类型,你只管塞,我基本全收

wildcard index大伙可以把它理解为“给整个table的每一个column都建一个索引”。如果一个field的类型是object,那对这个field建立wildcard index则会使这个object里面的每一个成员都拥有索引;也可以使用wildcard index对整个collection建立索引

覆盖查询条件

自动递归索引

一般情况下,wildcard index会递归索引每一个field里面的成员,如果field是个数组,那就会把所有数组元素都索引;但是数组嵌套数组时,第二层数组的元素不会索引,只会把第二层数组当作一个整体索引

查询数组时,如果当前查询位置下有8个以内的数组,那mongodb会在wildcard index上面查数据,如果超过8个数组,那mongodb就会看看有没有别的索引,没有的话就直接不用索引了

限制

虽然wildcard index限制很多,但是对于类FOFA的场景还是比较好用的,因为在这种场景下,用户查找的条件是不可能确定的,wildcard index也比较能覆盖到这个需求;虽然wildcard index很多时候性能不如之前那几个索引,但是总比没有强

geospatial index和hashed index

这俩我都不用的,不说了吧,大伙可以自己看官方文档