The script will be invoked for every matching document (e.g. the docs that match the query and filters). So if all 2m docs match, then yes... the script will be invoked 2m times. Painless is pretty fast so I wouldn't be toooo concerned, but it is something to keep in mind. It's the price that has to be paid for extreme flexibility unfortunately.
This only applies to unique compilations. If you parameterize the script correctly so that all dynamically changing parts are provided in the
params object, the script will only be compiled once. You can invoke it as many times as you like... it's just the compilation that is rate-limited.
There may be a way to approximate your algorithm if you index the size of the category array alongside the array itself. Then you could try to work out a sort value from the query score and the size. Not sure, haven't thought about it too deeply but thought I'd mention. I still think script is likely your best option