bookmark_borderMongoDB index with multiple filters

Recently, my current project has faced some loading issue with mongodb, to be specific, documentdb from AWS. We have mid-level instance and a lot of collection for data seperation purpose. For some of collections, it store more than millions data.

Our data structure like this:

type Course struct {
    ClassType int
    Prerequirement []*Course
    Teacher string // should be a Teach but for simplicity using string here.
    BuildNumber int
    
}

type Student struct {
    Grade int
    AdmittedAt time.Time
    StartedAt time.Time
    GraduationTime time.Time
    CoursesJoined []*Course
}

The data structure is just a mock, to clarify the function of indexes, we will consider some scenario later.

Before we jump into the index, we should know the basic index concept and when/where/how to create an index which is an index strategy.

I will skip when and where here, but want to emphasize ‘How to create an effective index’

The ESR (Equality, Sort, Range) Rule

Equality

“Equality” refers to an exact match on a single value. The following exact match queries scan the cars collection for documents whose model field exactly matches Cordoba.

db.cars.find( { model: "Cordoba" } )db.cars.find( { model: { $eq: "Cordoba" } } )

Index searches make efficient use of exact matches to limit the number of documents that need to be examined to satisfy a query. Place fields that require exact matches first in your index.

An index may have multiple keys for queries with exact matches. The index keys for equality matches can appear in any order. However, to satisfy an equality match with the index, all of the index keys for exact matches must come before any other index fields. MongoDB’s search algorithm eliminates any need to arrange the exact match fields in a particular order.

Exact matches should be selective. To reduce the number of index keys scanned, ensure equality tests eliminate at least 90% of possible document matches.

Sort

“Sort” determines the order for results. Sort follows equality matches because the equality matches reduce the number of documents that need to be sorted. Sorting after the equality matches also allows MongoDB to do a non-blocking sort.

An index can support sort operations when the query fields are a subset of the index keys. Sort operations on a subset of the index keys are only supported if the query includes equality conditions for all of the prefix keys that precede the sort keys. For more information see: Sort and Non-prefix Subset of an Index.

The following example queries the cars collection. The output is sorted by model:

db.cars.find( { manufacturer: "GM" } ).sort( { model: 1 } )

To improve query performance, create an index on the manufacturer and model fields:

db.cars.createIndex( { manufacturer: 1, model: 1 } )
  • manufacturer is the first key because it is an equality match.
  • model is indexed in the same order ( 1 ) as the query.

Range

“Range” filters scan fields. The scan doesn’t require an exact match, which means range filters are loosely bound to index keys. To improve query efficiency, make the range bounds as tight as possible and use equality matches to limit the number of documents that must be scanned.

Range filters resemble the following:

db.cars.find( { price: { $gte: 15000} } )db.cars.find( { age: { $lt: 10 } } )db.cars.find( { priorAccidents: { $ne: null } } )

MongoDB cannot do an index sort on the results of a range filter. Place the range filter after the sort predicate so MongoDB can use a non-blocking index sort. For more information on blocking sorts, see cursor.allowDiskUse().

Please keep these three words in your mind until you find a job that completely is irrelated to development, of course, until you win the power ball as well.

So we can start to think about several cases:

We want to find all students that are in Grade 1 and admitted in past 6 weeks, then sorted by start date descending.

at this case, we know Equality is grade, and Sort is the start time, the time range is based on admitted time, so we can easily create an index

{grade:1, admitted_at: -1, started_at:-1}

But the requirement changed a lot. For some reason, the principal wants to know all school students instead of Grade 1, because of the index prefix mechanism in MongoDB (https://www.mongodb.com/docs/manual/core/index-compound/#prefixes) without grade, this index won’t be hit when searching only by admitted time and start time.

In this case, we may want to change the index to

 {admitted_at: -1, started_at:-1, grade:1}. 

But for the same reason, it will request every query need to contain a prefix to take advantage of the index.

We want to find all students that have Type A course and That course’s teacher is Weihao and admitted in past 6 weeks, then sorted by start date descending.

In this case, cause we have a subdocument in our data structure, we need to create an index for the subdocument as well. So the index will looks like

{courses_joined.class_type:1, courses_joined.teacher:1, admited_at:-1, started_at:-1}

We still try to use a compound index to fulfill the requirement, which is good for extensibility but also bad for extensibility. Imagine one more scenario, what if someday the product manager asks you when the department of education has issued a new law that doesn’t allow searching students much specific. So you have to change the AND relation between course_joined.class_type and course_joined.teacher to OR relation. Then you find out the index is not hitting anymore as OR aggregation requires both fields have an independent index.

In this case, we should create multiple indexes and use MongoDB index intersection

{admited_at:-1, started_at:-1}
{courses_joined.class_type:1}
{courses_joined.teacher:1}

Other MongoDB article:

bookmark_borderCaltrain的终点站是下元

从SFO 飞到 JFK的航班要5个半小时,我记得在上次回国的飞机上,我洋洋洒洒写了几千字,只为探究我是在哪一个时区跨过了2019年和2020年。现在想想那时是天真的可爱,转念一想当时的记录也算弥足珍贵了。可惜的是后来我发现找不到那篇文章写在哪了。原来2019真的回不去了。

网络上有人在一年前把当时称作“后疫情时代”,罗列了20年和21年漫长的疫情时间线,并盖棺定论般地说:当时处在“后疫情时代”的我们,会永远铭记这漫长历史上沾满冠状病毒的两年。或许是结论下的太早的原因,后来又将“后疫情时代”改成了“中疫情时代”。

说来惭愧,竟是不知不觉滑到了中年人的行列,倒真不是年龄的累加在身后催促,反而是因为在不知不觉中发现自己开始享受晚上8点窝在沙发上看电视,同时不用担心错过提交的作业。起初会咬牙切齿恨每天的日子会不经意被各种事情充斥,甩也甩不掉,做了也不会有成就感,后来不论是说释然了还是妥协了,但总算搞明白了的是:成就感这个东西,远比不上赌博和情爱的快感。与其说成年人是无聊无趣的,究其根本是比赛刚进入了白热化,作为选手的你却发现自己没报名而已。会看着远处的骄子们痛心疾首,也会和给你加油的朋友打气说重在参与。

回到这架目的是约翰肯尼迪机场的飞机上,如果这世上有人总结过,每一次出行的路上都是最好的内省时机。我必定跨过时间和空间的限制,去和他/她拥抱一下,然后问问,后半句是不是:在路上的内省深刻且准确,但是到达目的地后就会置之脑后。

因为工作的变动,之前和人打交道的生活变成了和摄像头打交道,当然不是指无休止的居家办公和Zoom meeting,而是挂在你背后天花板角落的那个黑色监工。工作的变动无可厚非,毕竟没有哪个工地能搬一辈子的砖,在哪里敲代码不是敲代码,写bug亦同,debug亦同。真正让人唏嘘的是看到自己慢慢拼凑起来的产品有一天再和自己毫无瓜葛,颇有一种霸王别姬的气质,唯一区别可能是楚霸王自刎于吴江边,我则是搭着江上的小舟去找下一个姬了。反倒是我的虞姬活了下来,成了那个独自面对这个喧哗世界的人了。坦白来讲,能够做出一个被数十万人使用的产品,可以和赌博和情爱并列了。

飞机总会遇到气流,上上下下晃来晃去,没想到我在离开母亲肚子二十多年后,还是会因为飞机的晃动产生困意。说起母亲,大概算来也有约834天没有见面了,最近视频的次数愈来愈多了,不知是她开始想我了,还是我开始想她了。该死的疫情!竟成了最好的借口,竟能压过无时无刻不在的思念,不知母亲有没有后悔数年前在海底捞排位时,轻易答应我出国的请求。在茂业百货的顶楼,我不知道那家海底捞现在还在不在,只记得他们的等位区竟是连美国的都不如,厚重的帘子还是挡不住太原的冷风,没有吹醒本就愚钝的我和一时失神的母亲,可没想到连头脑一向冷静的爸爸也一并同意了。该死的海底捞!

如果生活像起飞的飞机一样就好了,在起飞的时候,唯一的目的就是降落在目的地。时常会辗转反侧,原因也无他,挣扎于目的地而已,别人都在爬升的时候,还在盘旋的人却已经在考虑降落了,不知道是谨慎还是他们的苍穹太低了。

记得小时候去二姨家,要先穿过院子,再横跨12车道的迎泽大街,在省人大坐上1路公交车,投币一元,刷学生卡只要两毛五,约莫十站后,终点站下元到了,转乘摩的,我被夹在妈妈和摩的师傅之间,摩托在下元的人流里穿梭,几分钟之后就会把下元远远的甩在身后。与此同时,1路公交调转车头,始发站下元,终点站太原火车站,投币一元,刷卡七毛五。

bookmark_border嘿!妈妈

嘿妈妈

明天我就要出发了

出发去远方冒险

在那个太阳初升的清晨六点


嘿妈妈

今天到了新的城市

这里路很窄房子却很大

听说年轻时你也来过这里是吗?


嘿妈妈

昨天遇到了一个姑娘

她笑起来很甜

我们谈了很多,包括未来和理想


嘿妈妈

最近我又换了住所

往西搬了三个街区

和佳还是差十二个时区


嘿妈妈

故事还在继续

像微风清徐

在月下轻语

bookmark_border随笔

写一些不知所以的故事吧,在自己还未搞清世界和人生的时候。

出发的时候最忌讳回头,即使你知道背后还有人在看着你。

因为一些东西太容易被抹去了,所以才劝你一定要留下些什么。

写些不算道理的道理,都是前人所想,换个说法罢了

bookmark_borderAWS Lightsail WordPress SSL 证书一键renew

sudo /opt/bitnami/bncert-tool

AWS Lightsail WordPress的instance已经提前安装好了 bncert-tool, 如果之前已经配置过一次SSL的DNS records 和开启了HTTPS的话,遇到证书过期,只需要执行上面的command。然后他自动跑完之后就会签发证书了

2021-09-23 update

这两天发现之前的认证又失效了,于是准备继续去renew certificate,但是在跑的时候发现会报错说certificate expire,查了很久,刚开始觉得这个是一个悖论,因为expire所以需要renew,但是因为expire又不能renew。 实在头大,后来发现bitnami其实有自己的letencrypt的证书存储地址,所以在/etc/letencrypt/下面的证书并不会生效

最终的解决办法就是把 /opt/bitnami/letencrypt/certificate 下的证书删掉,重新跑就好了

这里我第二次授权的时候使用的是lego,但其实应该certbot也是可以的,只要确保过期的证书被删掉就好

lego在生产证书的时候,会自动生成在./.lego/certificate,所以还需要拷贝到对应的bitnami放证书的地方

doc:

https://go-acme.github.io/lego/usage/cli/examples/

https://docs.bitnami.com/general/how-to/generate-install-lets-encrypt-ssl/

2022-03-04 update
又踩了一个坑,因为renew报错显示需要先revoke 生效的certification,然后再重新申请,所以就直接删除了/opt/bitnami/letenscript/*, 然后发现apache直接down掉了,后来只能重新给apache生成了一个dummy的certificates,然后更改了bitnami的config,再重新生成就好了

https://docs.bitnami.com/aws/apps/wordpress/administration/create-ssl-certificate-apache/

bookmark_borderSwift-聊天记录平滑下拉加载(多种类 cell)

当我们在设计聊天界面的时候,由于聊天记录可能存放在服务端,又或者为了考虑内存的因素,我们偏向于在一开始仅仅展示一部分聊天记录,这时候我们就需要有一个方法来实现加载之前的聊天记录。通常是通过下拉来实现的(例如微信)

和下拉刷新的思想很接近,我们可以选择在tableview上加一个refresh control来实现下拉加载,但这里并不推荐使用这个方法(原因后面会提到)但是无论我们使用哪个方法,当我们获取到新的data的时候,需要把他添加到tableView的最上面,这样才符合逻辑。

Screen Shot 2018 12 30 at 6 17 23 PM
如图所示 来源:https://bluelemonbits.com/2018/12/30/reloading-inserting-dynamic-height-cells-and-keeping-scroll-position/

由于TableView的特性,在reload的data的时候,会自动跳转到新加入的cell的顶端,也就是上图所示绿色的最顶端,这样就会导致每次加载之后整个tableView都会产生突兀的跳转。

这时候我们需要的解决方法是像参考微信一样,在加入新的Cell的同时,保持我们当时的contentOffset。具体实现这个的代码如下:

let oldContentSizeHeight = ListView.contentSize.height
ListView.reloadData() 
ListView.layoutIfNeeded()
let newContentSizeHeight = ListView.contentSize.height
                ListView.setContentOffset(CGPoint(x:ListView.contentOffset.x, y:newContentSizeHeight - oldContentSizeHeight), animated: false)

简而言之,就是在reload data之前记录此时的content height,待reload结束之后再记录之后的height,通过相减来设置contentOffset

这时候我们需要了解tableView在计算content height的原理,如果只是简单的单一的cell,那么cell的height会是相同的,并且是在

func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { }

这个delegate方法里设定的,但是如果我们的cell是多种类的呢,这时候iOS提供了一个estimate height 对于cell

func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
return UITableView.automaticDimension
}

通过同时实现这个delegate和上一个delegate的方法,iOS会自动帮我们计算出合适的content height,这样就不会产生有问题height,从而导致跳转出现问题(换句话说,如果你不实现这个方法,那么计算你content height的方法,就是简单的cell number * [cell height])

通过以上方式,我们可以实现固定位置的上拉刷新,回到之前提到的问题,为什么不推荐使用refresh control,原因其实很简答,因为refresh control是通过加载一个新的uiView在tableView上面,然后设定一个阈值,当我们下拉到一定程度会调用。那对于用户的体验来讲,需要一直下拉刷新的话,并不让人愉悦。通常快速向上滑动的时候并不会触发这个调用,也就会导致我们需要很多次“两次操作”才能到达我们想要的聊天记录的位置。那么我这里用到的替代方法是:

func scrollViewDidScroll(_ scrollView: UIScrollView) {
        if scrollView == ListView {
            if ListView.contentOffset.y < 0 {
                if !isLoading {
                    loadPreviousMessage()
                }
            }
        }
    }

因为TableView的实现是在scrollView上的,所以我们可以检测是否scrollView的contentOffset < 0 来检测用户是否想要加载之前的聊天记录,同时因为scrollView的特性是支持bounce的,所以向上快速滑动的同时,contentOffset也会出现 < 0的情况,这里为了避免多次调用,使用了一个变量 “isLoading”来控制。

这样设定之后,我们就可以简单的实现一个向上滑动来加载之前的聊天记录的功能