backfill_library_references_search.mjs 3.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111
  1. // @ts-check
  2. import minimist from 'minimist'
  3. import logger from '@overleaf/logger'
  4. import { db } from '../app/src/infrastructure/mongodb.mjs'
  5. import {
  6. buildSearchTokens,
  7. docSchema,
  8. } from '../modules/library/app/src/LibraryReferenceRepository.mts'
  9. import { tokenize } from '../modules/library/app/src/bibtex-search-tokens.mts'
  10. import { scriptRunner } from './lib/ScriptRunner.mjs'
  11. /** @typedef {import('mongodb').AnyBulkWriteOperation} AnyBulkWriteOperation */
  12. const argv = minimist(process.argv.slice(2), {
  13. boolean: ['commit', 'all', 'help'],
  14. default: { 'batch-size': 1000 },
  15. })
  16. function usage() {
  17. logger.info(
  18. {},
  19. `Usage: node backfill_library_references_search.mjs [options]
  20. Populates searchKey and searchTokens on libraryReferences so the
  21. account-level library search can index them. Also unsets the obsolete
  22. fields.$[].searchValue. Safe to rerun; picks up only un-indexed rows
  23. by default.
  24. Options:
  25. --commit Apply changes. Without this, runs as a dry run.
  26. --all Re-index every row, not just rows where searchTokens
  27. is null. Use when the tokenization format has changed.
  28. --batch-size <n> bulkWrite batch size (default 1000).
  29. `
  30. )
  31. }
  32. if (argv.help) {
  33. usage()
  34. process.exit(0)
  35. }
  36. const BATCH_SIZE = Number(argv['batch-size'])
  37. /** @param {(message: string) => Promise<void>} trackProgress */
  38. async function backfill(trackProgress) {
  39. const filter = argv.all ? {} : { searchTokens: null }
  40. const cursor = db.libraryReferences
  41. .find(filter)
  42. .project({ key: 1, type: 1, fields: 1, updatedAt: 1 })
  43. let processed = 0
  44. /** @type {AnyBulkWriteOperation[]} */
  45. let ops = []
  46. const flush = async () => {
  47. if (ops.length === 0) return
  48. if (argv.commit) {
  49. await db.libraryReferences.bulkWrite(ops, { ordered: false })
  50. }
  51. processed += ops.length
  52. await trackProgress(
  53. `${argv.commit ? 'wrote' : '[dry-run]'} ${processed} docs`
  54. )
  55. ops = []
  56. }
  57. for await (const doc of cursor) {
  58. const entry = docSchema.parse({
  59. ...doc,
  60. type: doc.type ?? 'misc',
  61. updatedAt: doc.updatedAt ?? new Date(0),
  62. })
  63. const searchKey = tokenize(doc.key)
  64. const searchTokens = buildSearchTokens(entry)
  65. ops.push({
  66. updateOne: {
  67. filter: { _id: doc._id },
  68. update: {
  69. $set: { searchKey, searchTokens },
  70. $unset: { 'fields.$[].searchValue': 1 },
  71. },
  72. },
  73. })
  74. if (ops.length >= BATCH_SIZE) {
  75. await flush()
  76. }
  77. }
  78. await flush()
  79. await trackProgress(`done; processed ${processed} docs`)
  80. }
  81. /** @param {(message: string) => Promise<void>} trackProgress */
  82. async function main(trackProgress) {
  83. if (!argv.commit) {
  84. await trackProgress('DRY RUN. Pass --commit to apply changes.')
  85. }
  86. await backfill(trackProgress)
  87. }
  88. try {
  89. await scriptRunner(main, {
  90. commit: Boolean(argv.commit),
  91. all: Boolean(argv.all),
  92. batchSize: BATCH_SIZE,
  93. })
  94. process.exit(0)
  95. } catch (err) {
  96. logger.error({ err }, 'backfill failed')
  97. process.exit(1)
  98. }