This is page 6 of 10. Use http://codebase.md/jakedismo/master-mcp-server?lines=true&page={x} to view the full context.
# Directory Structure
```
├── .env.example
├── .eslintignore
├── .eslintrc.cjs
├── .eslintrc.js
├── .gitignore
├── .prettierignore
├── .prettierrc
├── .prettierrc.json
├── CHANGELOG.md
├── config
│ ├── default.json
│ ├── development.json
│ ├── production.json
│ └── schema.json
├── config.json
├── CONTRIBUTING.md
├── debug-stdio.cjs
├── debug-stdio.js
├── deploy
│ ├── cloudflare
│ │ ├── .gitkeep
│ │ ├── README.md
│ │ └── wrangler.toml
│ ├── docker
│ │ ├── .gitkeep
│ │ ├── docker-compose.yml
│ │ ├── Dockerfile
│ │ └── entrypoint.sh
│ ├── koyeb
│ │ ├── .gitkeep
│ │ └── koyeb.yaml
│ └── README.md
├── docker-compose.yml
├── Dockerfile
├── docs
│ ├── .DS_Store
│ ├── .vitepress
│ │ ├── cache
│ │ │ └── deps
│ │ │ ├── _metadata.json
│ │ │ ├── chunk-HVR2FF6M.js
│ │ │ ├── chunk-HVR2FF6M.js.map
│ │ │ ├── chunk-P2XGSYO7.js
│ │ │ ├── chunk-P2XGSYO7.js.map
│ │ │ ├── package.json
│ │ │ ├── vitepress___@vue_devtools-api.js
│ │ │ ├── vitepress___@vue_devtools-api.js.map
│ │ │ ├── vitepress___@vueuse_core.js
│ │ │ ├── vitepress___@vueuse_core.js.map
│ │ │ ├── vitepress___@vueuse_integrations_useFocusTrap.js
│ │ │ ├── vitepress___@vueuse_integrations_useFocusTrap.js.map
│ │ │ ├── vitepress___mark__js_src_vanilla__js.js
│ │ │ ├── vitepress___mark__js_src_vanilla__js.js.map
│ │ │ ├── vitepress___minisearch.js
│ │ │ ├── vitepress___minisearch.js.map
│ │ │ ├── vue.js
│ │ │ └── vue.js.map
│ │ ├── config.ts
│ │ ├── dist
│ │ │ ├── 404.html
│ │ │ ├── advanced
│ │ │ │ ├── extensibility.html
│ │ │ │ ├── index.html
│ │ │ │ ├── monitoring.html
│ │ │ │ ├── performance.html
│ │ │ │ └── security.html
│ │ │ ├── api
│ │ │ │ ├── index.html
│ │ │ │ └── README.html
│ │ │ ├── assets
│ │ │ │ ├── advanced_extensibility.md.TrXUn5w5.js
│ │ │ │ ├── advanced_extensibility.md.TrXUn5w5.lean.js
│ │ │ │ ├── advanced_index.md.CPcpUlw_.js
│ │ │ │ ├── advanced_index.md.CPcpUlw_.lean.js
│ │ │ │ ├── advanced_monitoring.md.DTybdNg-.js
│ │ │ │ ├── advanced_monitoring.md.DTybdNg-.lean.js
│ │ │ │ ├── advanced_performance.md.DKmzK0ia.js
│ │ │ │ ├── advanced_performance.md.DKmzK0ia.lean.js
│ │ │ │ ├── advanced_security.md.B-oBD7IB.js
│ │ │ │ ├── advanced_security.md.B-oBD7IB.lean.js
│ │ │ │ ├── api_index.md.Dl1JB08_.js
│ │ │ │ ├── api_index.md.Dl1JB08_.lean.js
│ │ │ │ ├── chunks
│ │ │ │ │ └── framework.CHl2ywxc.js
│ │ │ │ ├── configuration_environment-variables.md.Ddy3P_Wz.js
│ │ │ │ ├── configuration_environment-variables.md.Ddy3P_Wz.lean.js
│ │ │ │ ├── configuration_environment.md.DxcTQ623.js
│ │ │ │ ├── configuration_environment.md.DxcTQ623.lean.js
│ │ │ │ ├── configuration_overview.md.DIkVDv7V.js
│ │ │ │ ├── configuration_overview.md.DIkVDv7V.lean.js
│ │ │ │ ├── configuration_performance.md.DbJdmLrW.js
│ │ │ │ ├── configuration_performance.md.DbJdmLrW.lean.js
│ │ │ │ ├── configuration_reference.md.27IKWqtk.js
│ │ │ │ ├── configuration_reference.md.27IKWqtk.lean.js
│ │ │ │ ├── configuration_security.md.-OOlkzN4.js
│ │ │ │ ├── configuration_security.md.-OOlkzN4.lean.js
│ │ │ │ ├── contributing_dev-setup.md.Ceqh4w-R.js
│ │ │ │ ├── contributing_dev-setup.md.Ceqh4w-R.lean.js
│ │ │ │ ├── contributing_guidelines.md.ZEAX2yVh.js
│ │ │ │ ├── contributing_guidelines.md.ZEAX2yVh.lean.js
│ │ │ │ ├── contributing_index.md.DYq9R6wr.js
│ │ │ │ ├── contributing_index.md.DYq9R6wr.lean.js
│ │ │ │ ├── contributing_maintenance.md.k2bR0IaR.js
│ │ │ │ ├── contributing_maintenance.md.k2bR0IaR.lean.js
│ │ │ │ ├── deployment_cicd.md.Ci2T0UYC.js
│ │ │ │ ├── deployment_cicd.md.Ci2T0UYC.lean.js
│ │ │ │ ├── deployment_cloudflare-workers.md.D2WHsfep.js
│ │ │ │ ├── deployment_cloudflare-workers.md.D2WHsfep.lean.js
│ │ │ │ ├── deployment_docker.md.B8bQDQTo.js
│ │ │ │ ├── deployment_docker.md.B8bQDQTo.lean.js
│ │ │ │ ├── deployment_index.md.ClYeOkpy.js
│ │ │ │ ├── deployment_index.md.ClYeOkpy.lean.js
│ │ │ │ ├── deployment_koyeb.md.B_wJhvF7.js
│ │ │ │ ├── deployment_koyeb.md.B_wJhvF7.lean.js
│ │ │ │ ├── examples_advanced-routing.md.B3CqhLZ7.js
│ │ │ │ ├── examples_advanced-routing.md.B3CqhLZ7.lean.js
│ │ │ │ ├── examples_basic-node.md.CaDZzGlO.js
│ │ │ │ ├── examples_basic-node.md.CaDZzGlO.lean.js
│ │ │ │ ├── examples_cloudflare-worker.md.DwVSz-c7.js
│ │ │ │ ├── examples_cloudflare-worker.md.DwVSz-c7.lean.js
│ │ │ │ ├── examples_index.md.CBF_BLkl.js
│ │ │ │ ├── examples_index.md.CBF_BLkl.lean.js
│ │ │ │ ├── examples_oauth-delegation.md.1hZxoqDl.js
│ │ │ │ ├── examples_oauth-delegation.md.1hZxoqDl.lean.js
│ │ │ │ ├── examples_overview.md.CZN0JbZ7.js
│ │ │ │ ├── examples_overview.md.CZN0JbZ7.lean.js
│ │ │ │ ├── examples_testing.md.Dek4GpNs.js
│ │ │ │ ├── examples_testing.md.Dek4GpNs.lean.js
│ │ │ │ ├── getting-started_concepts.md.D7ON9iGB.js
│ │ │ │ ├── getting-started_concepts.md.D7ON9iGB.lean.js
│ │ │ │ ├── getting-started_installation.md.BKnVqAGg.js
│ │ │ │ ├── getting-started_installation.md.BKnVqAGg.lean.js
│ │ │ │ ├── getting-started_overview.md.DvJDFL2N.js
│ │ │ │ ├── getting-started_overview.md.DvJDFL2N.lean.js
│ │ │ │ ├── getting-started_quickstart-node.md.GOO4aGas.js
│ │ │ │ ├── getting-started_quickstart-node.md.GOO4aGas.lean.js
│ │ │ │ ├── getting-started_quickstart-workers.md.Cpofh8Mj.js
│ │ │ │ ├── getting-started_quickstart-workers.md.Cpofh8Mj.lean.js
│ │ │ │ ├── getting-started.md.DG9ndneo.js
│ │ │ │ ├── getting-started.md.DG9ndneo.lean.js
│ │ │ │ ├── guides_configuration-management.md.B-jwYMbA.js
│ │ │ │ ├── guides_configuration-management.md.B-jwYMbA.lean.js
│ │ │ │ ├── guides_configuration.md.Ci3zYDFA.js
│ │ │ │ ├── guides_configuration.md.Ci3zYDFA.lean.js
│ │ │ │ ├── guides_index.md.CIlq2fmx.js
│ │ │ │ ├── guides_index.md.CIlq2fmx.lean.js
│ │ │ │ ├── guides_module-loading.md.BkJvuRnQ.js
│ │ │ │ ├── guides_module-loading.md.BkJvuRnQ.lean.js
│ │ │ │ ├── guides_oauth-delegation.md.DEOZ-_G0.js
│ │ │ │ ├── guides_oauth-delegation.md.DEOZ-_G0.lean.js
│ │ │ │ ├── guides_request-routing.md.Bdzf0VLg.js
│ │ │ │ ├── guides_request-routing.md.Bdzf0VLg.lean.js
│ │ │ │ ├── guides_testing.md.kYfHqJLu.js
│ │ │ │ ├── guides_testing.md.kYfHqJLu.lean.js
│ │ │ │ ├── inter-italic-cyrillic-ext.r48I6akx.woff2
│ │ │ │ ├── inter-italic-cyrillic.By2_1cv3.woff2
│ │ │ │ ├── inter-italic-greek-ext.1u6EdAuj.woff2
│ │ │ │ ├── inter-italic-greek.DJ8dCoTZ.woff2
│ │ │ │ ├── inter-italic-latin-ext.CN1xVJS-.woff2
│ │ │ │ ├── inter-italic-latin.C2AdPX0b.woff2
│ │ │ │ ├── inter-italic-vietnamese.BSbpV94h.woff2
│ │ │ │ ├── inter-roman-cyrillic-ext.BBPuwvHQ.woff2
│ │ │ │ ├── inter-roman-cyrillic.C5lxZ8CY.woff2
│ │ │ │ ├── inter-roman-greek-ext.CqjqNYQ-.woff2
│ │ │ │ ├── inter-roman-greek.BBVDIX6e.woff2
│ │ │ │ ├── inter-roman-latin-ext.4ZJIpNVo.woff2
│ │ │ │ ├── inter-roman-latin.Di8DUHzh.woff2
│ │ │ │ ├── inter-roman-vietnamese.BjW4sHH5.woff2
│ │ │ │ ├── README.md.BO5r5M9u.js
│ │ │ │ ├── README.md.BO5r5M9u.lean.js
│ │ │ │ ├── style.BQrfSMzK.css
│ │ │ │ ├── troubleshooting_common-issues.md.CScvzWM1.js
│ │ │ │ ├── troubleshooting_common-issues.md.CScvzWM1.lean.js
│ │ │ │ ├── troubleshooting_deployment.md.DUhpqnLE.js
│ │ │ │ ├── troubleshooting_deployment.md.DUhpqnLE.lean.js
│ │ │ │ ├── troubleshooting_errors.md.BSCsEmGc.js
│ │ │ │ ├── troubleshooting_errors.md.BSCsEmGc.lean.js
│ │ │ │ ├── troubleshooting_oauth.md.Cw60Eka3.js
│ │ │ │ ├── troubleshooting_oauth.md.Cw60Eka3.lean.js
│ │ │ │ ├── troubleshooting_performance.md.DxY6LJcT.js
│ │ │ │ ├── troubleshooting_performance.md.DxY6LJcT.lean.js
│ │ │ │ ├── troubleshooting_routing.md.BHN-MDhs.js
│ │ │ │ ├── troubleshooting_routing.md.BHN-MDhs.lean.js
│ │ │ │ ├── troubleshooting_security-best-practices.md.Yiu8E-zt.js
│ │ │ │ ├── troubleshooting_security-best-practices.md.Yiu8E-zt.lean.js
│ │ │ │ ├── tutorials_beginner-getting-started.md.BXObgobW.js
│ │ │ │ ├── tutorials_beginner-getting-started.md.BXObgobW.lean.js
│ │ │ │ ├── tutorials_cloudflare-workers-tutorial.md.MPHsc0aT.js
│ │ │ │ ├── tutorials_cloudflare-workers-tutorial.md.MPHsc0aT.lean.js
│ │ │ │ ├── tutorials_load-balancing-and-resilience.md.Dv9r9jyW.js
│ │ │ │ ├── tutorials_load-balancing-and-resilience.md.Dv9r9jyW.lean.js
│ │ │ │ ├── tutorials_oauth-delegation-github.md.Nq4glqCe.js
│ │ │ │ └── tutorials_oauth-delegation-github.md.Nq4glqCe.lean.js
│ │ │ ├── configuration
│ │ │ │ ├── environment-variables.html
│ │ │ │ ├── environment.html
│ │ │ │ ├── examples.html
│ │ │ │ ├── overview.html
│ │ │ │ ├── performance.html
│ │ │ │ ├── reference.html
│ │ │ │ └── security.html
│ │ │ ├── contributing
│ │ │ │ ├── dev-setup.html
│ │ │ │ ├── guidelines.html
│ │ │ │ ├── index.html
│ │ │ │ └── maintenance.html
│ │ │ ├── deployment
│ │ │ │ ├── cicd.html
│ │ │ │ ├── cloudflare-workers.html
│ │ │ │ ├── docker.html
│ │ │ │ ├── index.html
│ │ │ │ └── koyeb.html
│ │ │ ├── diagrams
│ │ │ │ └── architecture.svg
│ │ │ ├── examples
│ │ │ │ ├── advanced-routing.html
│ │ │ │ ├── basic-node.html
│ │ │ │ ├── cloudflare-worker.html
│ │ │ │ ├── index.html
│ │ │ │ ├── oauth-delegation.html
│ │ │ │ ├── overview.html
│ │ │ │ └── testing.html
│ │ │ ├── getting-started
│ │ │ │ ├── concepts.html
│ │ │ │ ├── installation.html
│ │ │ │ ├── overview.html
│ │ │ │ ├── quick-start.html
│ │ │ │ ├── quickstart-node.html
│ │ │ │ └── quickstart-workers.html
│ │ │ ├── getting-started.html
│ │ │ ├── guides
│ │ │ │ ├── authentication.html
│ │ │ │ ├── client-integration.html
│ │ │ │ ├── configuration-management.html
│ │ │ │ ├── configuration.html
│ │ │ │ ├── index.html
│ │ │ │ ├── module-loading.html
│ │ │ │ ├── oauth-delegation.html
│ │ │ │ ├── request-routing.html
│ │ │ │ ├── server-management.html
│ │ │ │ ├── server-sharing.html
│ │ │ │ └── testing.html
│ │ │ ├── hashmap.json
│ │ │ ├── index.html
│ │ │ ├── logo.svg
│ │ │ ├── README.html
│ │ │ ├── reports
│ │ │ │ └── mcp-compliance-audit.html
│ │ │ ├── troubleshooting
│ │ │ │ ├── common-issues.html
│ │ │ │ ├── deployment.html
│ │ │ │ ├── errors.html
│ │ │ │ ├── index.html
│ │ │ │ ├── oauth.html
│ │ │ │ ├── performance.html
│ │ │ │ ├── routing.html
│ │ │ │ └── security-best-practices.html
│ │ │ ├── tutorials
│ │ │ │ ├── beginner-getting-started.html
│ │ │ │ ├── cloudflare-workers-tutorial.html
│ │ │ │ ├── load-balancing-and-resilience.html
│ │ │ │ └── oauth-delegation-github.html
│ │ │ └── vp-icons.css
│ │ └── theme
│ │ ├── components
│ │ │ ├── ApiPlayground.vue
│ │ │ ├── AuthFlowDemo.vue
│ │ │ ├── CodeTabs.vue
│ │ │ └── ConfigGenerator.vue
│ │ ├── index.ts
│ │ └── style.css
│ ├── advanced
│ │ ├── extensibility.md
│ │ ├── index.md
│ │ ├── monitoring.md
│ │ ├── performance.md
│ │ └── security.md
│ ├── api
│ │ ├── functions
│ │ │ └── createServer.md
│ │ ├── index.md
│ │ ├── interfaces
│ │ │ └── RunningServer.md
│ │ └── README.md
│ ├── architecture
│ │ └── images
│ │ └── mcp_master_architecture.svg
│ ├── configuration
│ │ ├── environment-variables.md
│ │ ├── environment.md
│ │ ├── examples.md
│ │ ├── overview.md
│ │ ├── performance.md
│ │ ├── reference.md
│ │ └── security.md
│ ├── contributing
│ │ ├── dev-setup.md
│ │ ├── guidelines.md
│ │ ├── index.md
│ │ └── maintenance.md
│ ├── deployment
│ │ ├── cicd.md
│ │ ├── cloudflare-workers.md
│ │ ├── docker.md
│ │ ├── docs-site.md
│ │ ├── index.md
│ │ └── koyeb.md
│ ├── examples
│ │ ├── advanced-routing.md
│ │ ├── basic-node.md
│ │ ├── cloudflare-worker.md
│ │ ├── index.md
│ │ ├── oauth-delegation.md
│ │ ├── overview.md
│ │ └── testing.md
│ ├── getting-started
│ │ ├── concepts.md
│ │ ├── installation.md
│ │ ├── overview.md
│ │ ├── quick-start.md
│ │ ├── quickstart-node.md
│ │ └── quickstart-workers.md
│ ├── getting-started.md
│ ├── guides
│ │ ├── authentication.md
│ │ ├── client-integration.md
│ │ ├── configuration-management.md
│ │ ├── configuration.md
│ │ ├── index.md
│ │ ├── module-loading.md
│ │ ├── oauth-delegation.md
│ │ ├── request-routing.md
│ │ ├── server-management.md
│ │ ├── server-sharing.md
│ │ └── testing.md
│ ├── index.html
│ ├── public
│ │ ├── diagrams
│ │ │ └── architecture.svg
│ │ ├── github-social.png
│ │ │ └── image.png
│ │ ├── logo.png
│ │ └── logo.svg
│ ├── README.md
│ ├── stdio-servers.md
│ ├── testing
│ │ └── phase-9-testing-architecture.md
│ ├── troubleshooting
│ │ ├── common-issues.md
│ │ ├── deployment.md
│ │ ├── errors.md
│ │ ├── index.md
│ │ ├── oauth.md
│ │ ├── performance.md
│ │ ├── routing.md
│ │ └── security-best-practices.md
│ └── tutorials
│ ├── beginner-getting-started.md
│ ├── cloudflare-workers-tutorial.md
│ ├── load-balancing-and-resilience.md
│ └── oauth-delegation-github.md
├── examples
│ ├── advanced-routing
│ │ ├── config.yaml
│ │ └── README.md
│ ├── basic-node
│ │ ├── config.yaml
│ │ ├── README.md
│ │ └── server.ts
│ ├── cloudflare-worker
│ │ ├── README.md
│ │ └── worker.ts
│ ├── custom-auth
│ │ ├── config.yaml
│ │ ├── index.ts
│ │ └── README.md
│ ├── multi-server
│ │ ├── config.yaml
│ │ └── README.md
│ ├── oauth-delegation
│ │ └── README.md
│ ├── oauth-node
│ │ ├── config.yaml
│ │ └── README.md
│ ├── performance
│ │ ├── config.yaml
│ │ └── README.md
│ ├── sample-configs
│ │ ├── basic.yaml
│ │ └── simple-setup.yaml
│ ├── security-hardening
│ │ └── README.md
│ ├── stdio-mcp-server.cjs
│ ├── test-mcp-server.js
│ └── test-stdio-server.js
├── LICENSE
├── master-mcp-definition.md
├── package-lock.json
├── package.json
├── README.md
├── reports
│ └── claude_report_20250815_222153.html
├── scripts
│ └── generate-config-docs.ts
├── src
│ ├── auth
│ │ ├── multi-auth-manager.ts
│ │ ├── oauth-providers.ts
│ │ └── token-manager.ts
│ ├── config
│ │ ├── config-loader.ts
│ │ ├── environment-manager.ts
│ │ ├── schema-validator.ts
│ │ └── secret-manager.ts
│ ├── index.ts
│ ├── mcp-server.ts
│ ├── modules
│ │ ├── capability-aggregator.ts
│ │ ├── module-loader.ts
│ │ ├── request-router.ts
│ │ ├── stdio-capability-discovery.ts
│ │ └── stdio-manager.ts
│ ├── oauth
│ │ ├── callback-handler.ts
│ │ ├── flow-controller.ts
│ │ ├── flow-validator.ts
│ │ ├── pkce-manager.ts
│ │ ├── state-manager.ts
│ │ └── web-interface.ts
│ ├── routing
│ │ ├── circuit-breaker.ts
│ │ ├── load-balancer.ts
│ │ ├── retry-handler.ts
│ │ └── route-registry.ts
│ ├── runtime
│ │ ├── node.ts
│ │ └── worker.ts
│ ├── server
│ │ ├── config-manager.ts
│ │ ├── dependency-container.ts
│ │ ├── master-server.ts
│ │ └── protocol-handler.ts
│ ├── types
│ │ ├── auth.ts
│ │ ├── config.ts
│ │ ├── jose-shim.d.ts
│ │ ├── mcp.ts
│ │ └── server.ts
│ └── utils
│ ├── cache.ts
│ ├── crypto.ts
│ ├── dev.ts
│ ├── errors.ts
│ ├── http.ts
│ ├── logger.ts
│ ├── monitoring.ts
│ ├── string.ts
│ ├── time.ts
│ ├── validation.ts
│ └── validators.ts
├── static
│ └── oauth
│ ├── consent.html
│ ├── error.html
│ ├── script.js
│ ├── style.css
│ └── success.html
├── tests
│ ├── _setup
│ │ ├── miniflare.setup.ts
│ │ └── vitest.setup.ts
│ ├── _utils
│ │ ├── log-capture.ts
│ │ ├── mock-fetch.ts
│ │ └── test-server.ts
│ ├── .gitkeep
│ ├── e2e
│ │ ├── flow-controller.express.test.ts
│ │ └── flow-controller.worker.test.ts
│ ├── factories
│ │ ├── configFactory.ts
│ │ ├── mcpFactory.ts
│ │ └── oauthFactory.ts
│ ├── fixtures
│ │ ├── capabilities.json
│ │ └── stdio-server.js
│ ├── integration
│ │ ├── modules.capability-aggregator.test.ts
│ │ ├── modules.module-loader-health.test.ts
│ │ ├── oauth.callback-handler.test.ts
│ │ └── request-router.test.ts
│ ├── mocks
│ │ ├── mcp
│ │ │ └── fake-backend.ts
│ │ └── oauth
│ │ └── mock-oidc-provider.ts
│ ├── perf
│ │ ├── artillery
│ │ │ └── auth-routing.yaml
│ │ └── perf.auth-and-routing.test.ts
│ ├── security
│ │ └── security.oauth-and-input.test.ts
│ ├── servers
│ │ ├── test-auth-simple.js
│ │ ├── test-debug.js
│ │ ├── test-master-mcp.js
│ │ ├── test-mcp-client.js
│ │ ├── test-streaming-both-complete.js
│ │ ├── test-streaming-both-full.js
│ │ ├── test-streaming-both-simple.js
│ │ ├── test-streaming-both.js
│ │ └── test-streaming.js
│ ├── setup
│ │ └── test-setup.ts
│ ├── unit
│ │ ├── auth.multi-auth-manager.test.ts
│ │ ├── auth.token-manager.test.ts
│ │ ├── config.environment-manager.test.ts
│ │ ├── config.schema-validator.test.ts
│ │ ├── config.secret-manager.test.ts
│ │ ├── modules
│ │ │ ├── stdio-capability-discovery.test.ts
│ │ │ └── stdio-manager.test.ts
│ │ ├── modules.route-registry.test.ts
│ │ ├── oauth.pkce-state.test.ts
│ │ ├── routing
│ │ │ └── circuit-breaker.test.ts
│ │ ├── routing.core.test.ts
│ │ ├── stdio-capability-discovery.test.ts
│ │ ├── utils.crypto.test.ts
│ │ ├── utils.logger.test.ts
│ │ └── utils.monitoring.test.ts
│ └── utils
│ ├── fake-express.ts
│ ├── mock-http.ts
│ ├── oauth-mocks.ts
│ └── token-storages.ts
├── tsconfig.base.json
├── tsconfig.json
├── tsconfig.node.json
├── tsconfig.worker.json
├── typedoc.json
├── vitest.config.ts
└── vitest.worker.config.ts
```
# Files
--------------------------------------------------------------------------------
/docs/.vitepress/cache/deps/vitepress___minisearch.js:
--------------------------------------------------------------------------------
```javascript
1 | // node_modules/minisearch/dist/es/index.js
2 | var ENTRIES = "ENTRIES";
3 | var KEYS = "KEYS";
4 | var VALUES = "VALUES";
5 | var LEAF = "";
6 | var TreeIterator = class {
7 | constructor(set, type) {
8 | const node = set._tree;
9 | const keys = Array.from(node.keys());
10 | this.set = set;
11 | this._type = type;
12 | this._path = keys.length > 0 ? [{ node, keys }] : [];
13 | }
14 | next() {
15 | const value = this.dive();
16 | this.backtrack();
17 | return value;
18 | }
19 | dive() {
20 | if (this._path.length === 0) {
21 | return { done: true, value: void 0 };
22 | }
23 | const { node, keys } = last$1(this._path);
24 | if (last$1(keys) === LEAF) {
25 | return { done: false, value: this.result() };
26 | }
27 | const child = node.get(last$1(keys));
28 | this._path.push({ node: child, keys: Array.from(child.keys()) });
29 | return this.dive();
30 | }
31 | backtrack() {
32 | if (this._path.length === 0) {
33 | return;
34 | }
35 | const keys = last$1(this._path).keys;
36 | keys.pop();
37 | if (keys.length > 0) {
38 | return;
39 | }
40 | this._path.pop();
41 | this.backtrack();
42 | }
43 | key() {
44 | return this.set._prefix + this._path.map(({ keys }) => last$1(keys)).filter((key) => key !== LEAF).join("");
45 | }
46 | value() {
47 | return last$1(this._path).node.get(LEAF);
48 | }
49 | result() {
50 | switch (this._type) {
51 | case VALUES:
52 | return this.value();
53 | case KEYS:
54 | return this.key();
55 | default:
56 | return [this.key(), this.value()];
57 | }
58 | }
59 | [Symbol.iterator]() {
60 | return this;
61 | }
62 | };
63 | var last$1 = (array) => {
64 | return array[array.length - 1];
65 | };
66 | var fuzzySearch = (node, query, maxDistance) => {
67 | const results = /* @__PURE__ */ new Map();
68 | if (query === void 0)
69 | return results;
70 | const n = query.length + 1;
71 | const m = n + maxDistance;
72 | const matrix = new Uint8Array(m * n).fill(maxDistance + 1);
73 | for (let j = 0; j < n; ++j)
74 | matrix[j] = j;
75 | for (let i = 1; i < m; ++i)
76 | matrix[i * n] = i;
77 | recurse(node, query, maxDistance, results, matrix, 1, n, "");
78 | return results;
79 | };
80 | var recurse = (node, query, maxDistance, results, matrix, m, n, prefix) => {
81 | const offset = m * n;
82 | key: for (const key of node.keys()) {
83 | if (key === LEAF) {
84 | const distance = matrix[offset - 1];
85 | if (distance <= maxDistance) {
86 | results.set(prefix, [node.get(key), distance]);
87 | }
88 | } else {
89 | let i = m;
90 | for (let pos = 0; pos < key.length; ++pos, ++i) {
91 | const char = key[pos];
92 | const thisRowOffset = n * i;
93 | const prevRowOffset = thisRowOffset - n;
94 | let minDistance = matrix[thisRowOffset];
95 | const jmin = Math.max(0, i - maxDistance - 1);
96 | const jmax = Math.min(n - 1, i + maxDistance);
97 | for (let j = jmin; j < jmax; ++j) {
98 | const different = char !== query[j];
99 | const rpl = matrix[prevRowOffset + j] + +different;
100 | const del = matrix[prevRowOffset + j + 1] + 1;
101 | const ins = matrix[thisRowOffset + j] + 1;
102 | const dist = matrix[thisRowOffset + j + 1] = Math.min(rpl, del, ins);
103 | if (dist < minDistance)
104 | minDistance = dist;
105 | }
106 | if (minDistance > maxDistance) {
107 | continue key;
108 | }
109 | }
110 | recurse(node.get(key), query, maxDistance, results, matrix, i, n, prefix + key);
111 | }
112 | }
113 | };
114 | var SearchableMap = class _SearchableMap {
115 | /**
116 | * The constructor is normally called without arguments, creating an empty
117 | * map. In order to create a {@link SearchableMap} from an iterable or from an
118 | * object, check {@link SearchableMap.from} and {@link
119 | * SearchableMap.fromObject}.
120 | *
121 | * The constructor arguments are for internal use, when creating derived
122 | * mutable views of a map at a prefix.
123 | */
124 | constructor(tree = /* @__PURE__ */ new Map(), prefix = "") {
125 | this._size = void 0;
126 | this._tree = tree;
127 | this._prefix = prefix;
128 | }
129 | /**
130 | * Creates and returns a mutable view of this {@link SearchableMap},
131 | * containing only entries that share the given prefix.
132 | *
133 | * ### Usage:
134 | *
135 | * ```javascript
136 | * let map = new SearchableMap()
137 | * map.set("unicorn", 1)
138 | * map.set("universe", 2)
139 | * map.set("university", 3)
140 | * map.set("unique", 4)
141 | * map.set("hello", 5)
142 | *
143 | * let uni = map.atPrefix("uni")
144 | * uni.get("unique") // => 4
145 | * uni.get("unicorn") // => 1
146 | * uni.get("hello") // => undefined
147 | *
148 | * let univer = map.atPrefix("univer")
149 | * univer.get("unique") // => undefined
150 | * univer.get("universe") // => 2
151 | * univer.get("university") // => 3
152 | * ```
153 | *
154 | * @param prefix The prefix
155 | * @return A {@link SearchableMap} representing a mutable view of the original
156 | * Map at the given prefix
157 | */
158 | atPrefix(prefix) {
159 | if (!prefix.startsWith(this._prefix)) {
160 | throw new Error("Mismatched prefix");
161 | }
162 | const [node, path] = trackDown(this._tree, prefix.slice(this._prefix.length));
163 | if (node === void 0) {
164 | const [parentNode, key] = last(path);
165 | for (const k of parentNode.keys()) {
166 | if (k !== LEAF && k.startsWith(key)) {
167 | const node2 = /* @__PURE__ */ new Map();
168 | node2.set(k.slice(key.length), parentNode.get(k));
169 | return new _SearchableMap(node2, prefix);
170 | }
171 | }
172 | }
173 | return new _SearchableMap(node, prefix);
174 | }
175 | /**
176 | * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/clear
177 | */
178 | clear() {
179 | this._size = void 0;
180 | this._tree.clear();
181 | }
182 | /**
183 | * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/delete
184 | * @param key Key to delete
185 | */
186 | delete(key) {
187 | this._size = void 0;
188 | return remove(this._tree, key);
189 | }
190 | /**
191 | * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/entries
192 | * @return An iterator iterating through `[key, value]` entries.
193 | */
194 | entries() {
195 | return new TreeIterator(this, ENTRIES);
196 | }
197 | /**
198 | * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/forEach
199 | * @param fn Iteration function
200 | */
201 | forEach(fn) {
202 | for (const [key, value] of this) {
203 | fn(key, value, this);
204 | }
205 | }
206 | /**
207 | * Returns a Map of all the entries that have a key within the given edit
208 | * distance from the search key. The keys of the returned Map are the matching
209 | * keys, while the values are two-element arrays where the first element is
210 | * the value associated to the key, and the second is the edit distance of the
211 | * key to the search key.
212 | *
213 | * ### Usage:
214 | *
215 | * ```javascript
216 | * let map = new SearchableMap()
217 | * map.set('hello', 'world')
218 | * map.set('hell', 'yeah')
219 | * map.set('ciao', 'mondo')
220 | *
221 | * // Get all entries that match the key 'hallo' with a maximum edit distance of 2
222 | * map.fuzzyGet('hallo', 2)
223 | * // => Map(2) { 'hello' => ['world', 1], 'hell' => ['yeah', 2] }
224 | *
225 | * // In the example, the "hello" key has value "world" and edit distance of 1
226 | * // (change "e" to "a"), the key "hell" has value "yeah" and edit distance of 2
227 | * // (change "e" to "a", delete "o")
228 | * ```
229 | *
230 | * @param key The search key
231 | * @param maxEditDistance The maximum edit distance (Levenshtein)
232 | * @return A Map of the matching keys to their value and edit distance
233 | */
234 | fuzzyGet(key, maxEditDistance) {
235 | return fuzzySearch(this._tree, key, maxEditDistance);
236 | }
237 | /**
238 | * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/get
239 | * @param key Key to get
240 | * @return Value associated to the key, or `undefined` if the key is not
241 | * found.
242 | */
243 | get(key) {
244 | const node = lookup(this._tree, key);
245 | return node !== void 0 ? node.get(LEAF) : void 0;
246 | }
247 | /**
248 | * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/has
249 | * @param key Key
250 | * @return True if the key is in the map, false otherwise
251 | */
252 | has(key) {
253 | const node = lookup(this._tree, key);
254 | return node !== void 0 && node.has(LEAF);
255 | }
256 | /**
257 | * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/keys
258 | * @return An `Iterable` iterating through keys
259 | */
260 | keys() {
261 | return new TreeIterator(this, KEYS);
262 | }
263 | /**
264 | * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/set
265 | * @param key Key to set
266 | * @param value Value to associate to the key
267 | * @return The {@link SearchableMap} itself, to allow chaining
268 | */
269 | set(key, value) {
270 | if (typeof key !== "string") {
271 | throw new Error("key must be a string");
272 | }
273 | this._size = void 0;
274 | const node = createPath(this._tree, key);
275 | node.set(LEAF, value);
276 | return this;
277 | }
278 | /**
279 | * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/size
280 | */
281 | get size() {
282 | if (this._size) {
283 | return this._size;
284 | }
285 | this._size = 0;
286 | const iter = this.entries();
287 | while (!iter.next().done)
288 | this._size += 1;
289 | return this._size;
290 | }
291 | /**
292 | * Updates the value at the given key using the provided function. The function
293 | * is called with the current value at the key, and its return value is used as
294 | * the new value to be set.
295 | *
296 | * ### Example:
297 | *
298 | * ```javascript
299 | * // Increment the current value by one
300 | * searchableMap.update('somekey', (currentValue) => currentValue == null ? 0 : currentValue + 1)
301 | * ```
302 | *
303 | * If the value at the given key is or will be an object, it might not require
304 | * re-assignment. In that case it is better to use `fetch()`, because it is
305 | * faster.
306 | *
307 | * @param key The key to update
308 | * @param fn The function used to compute the new value from the current one
309 | * @return The {@link SearchableMap} itself, to allow chaining
310 | */
311 | update(key, fn) {
312 | if (typeof key !== "string") {
313 | throw new Error("key must be a string");
314 | }
315 | this._size = void 0;
316 | const node = createPath(this._tree, key);
317 | node.set(LEAF, fn(node.get(LEAF)));
318 | return this;
319 | }
320 | /**
321 | * Fetches the value of the given key. If the value does not exist, calls the
322 | * given function to create a new value, which is inserted at the given key
323 | * and subsequently returned.
324 | *
325 | * ### Example:
326 | *
327 | * ```javascript
328 | * const map = searchableMap.fetch('somekey', () => new Map())
329 | * map.set('foo', 'bar')
330 | * ```
331 | *
332 | * @param key The key to update
333 | * @param initial A function that creates a new value if the key does not exist
334 | * @return The existing or new value at the given key
335 | */
336 | fetch(key, initial) {
337 | if (typeof key !== "string") {
338 | throw new Error("key must be a string");
339 | }
340 | this._size = void 0;
341 | const node = createPath(this._tree, key);
342 | let value = node.get(LEAF);
343 | if (value === void 0) {
344 | node.set(LEAF, value = initial());
345 | }
346 | return value;
347 | }
348 | /**
349 | * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/values
350 | * @return An `Iterable` iterating through values.
351 | */
352 | values() {
353 | return new TreeIterator(this, VALUES);
354 | }
355 | /**
356 | * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/@@iterator
357 | */
358 | [Symbol.iterator]() {
359 | return this.entries();
360 | }
361 | /**
362 | * Creates a {@link SearchableMap} from an `Iterable` of entries
363 | *
364 | * @param entries Entries to be inserted in the {@link SearchableMap}
365 | * @return A new {@link SearchableMap} with the given entries
366 | */
367 | static from(entries) {
368 | const tree = new _SearchableMap();
369 | for (const [key, value] of entries) {
370 | tree.set(key, value);
371 | }
372 | return tree;
373 | }
374 | /**
375 | * Creates a {@link SearchableMap} from the iterable properties of a JavaScript object
376 | *
377 | * @param object Object of entries for the {@link SearchableMap}
378 | * @return A new {@link SearchableMap} with the given entries
379 | */
380 | static fromObject(object) {
381 | return _SearchableMap.from(Object.entries(object));
382 | }
383 | };
384 | var trackDown = (tree, key, path = []) => {
385 | if (key.length === 0 || tree == null) {
386 | return [tree, path];
387 | }
388 | for (const k of tree.keys()) {
389 | if (k !== LEAF && key.startsWith(k)) {
390 | path.push([tree, k]);
391 | return trackDown(tree.get(k), key.slice(k.length), path);
392 | }
393 | }
394 | path.push([tree, key]);
395 | return trackDown(void 0, "", path);
396 | };
397 | var lookup = (tree, key) => {
398 | if (key.length === 0 || tree == null) {
399 | return tree;
400 | }
401 | for (const k of tree.keys()) {
402 | if (k !== LEAF && key.startsWith(k)) {
403 | return lookup(tree.get(k), key.slice(k.length));
404 | }
405 | }
406 | };
407 | var createPath = (node, key) => {
408 | const keyLength = key.length;
409 | outer: for (let pos = 0; node && pos < keyLength; ) {
410 | for (const k of node.keys()) {
411 | if (k !== LEAF && key[pos] === k[0]) {
412 | const len = Math.min(keyLength - pos, k.length);
413 | let offset = 1;
414 | while (offset < len && key[pos + offset] === k[offset])
415 | ++offset;
416 | const child2 = node.get(k);
417 | if (offset === k.length) {
418 | node = child2;
419 | } else {
420 | const intermediate = /* @__PURE__ */ new Map();
421 | intermediate.set(k.slice(offset), child2);
422 | node.set(key.slice(pos, pos + offset), intermediate);
423 | node.delete(k);
424 | node = intermediate;
425 | }
426 | pos += offset;
427 | continue outer;
428 | }
429 | }
430 | const child = /* @__PURE__ */ new Map();
431 | node.set(key.slice(pos), child);
432 | return child;
433 | }
434 | return node;
435 | };
436 | var remove = (tree, key) => {
437 | const [node, path] = trackDown(tree, key);
438 | if (node === void 0) {
439 | return;
440 | }
441 | node.delete(LEAF);
442 | if (node.size === 0) {
443 | cleanup(path);
444 | } else if (node.size === 1) {
445 | const [key2, value] = node.entries().next().value;
446 | merge(path, key2, value);
447 | }
448 | };
449 | var cleanup = (path) => {
450 | if (path.length === 0) {
451 | return;
452 | }
453 | const [node, key] = last(path);
454 | node.delete(key);
455 | if (node.size === 0) {
456 | cleanup(path.slice(0, -1));
457 | } else if (node.size === 1) {
458 | const [key2, value] = node.entries().next().value;
459 | if (key2 !== LEAF) {
460 | merge(path.slice(0, -1), key2, value);
461 | }
462 | }
463 | };
464 | var merge = (path, key, value) => {
465 | if (path.length === 0) {
466 | return;
467 | }
468 | const [node, nodeKey] = last(path);
469 | node.set(nodeKey + key, value);
470 | node.delete(nodeKey);
471 | };
472 | var last = (array) => {
473 | return array[array.length - 1];
474 | };
475 | var OR = "or";
476 | var AND = "and";
477 | var AND_NOT = "and_not";
478 | var MiniSearch = class _MiniSearch {
479 | /**
480 | * @param options Configuration options
481 | *
482 | * ### Examples:
483 | *
484 | * ```javascript
485 | * // Create a search engine that indexes the 'title' and 'text' fields of your
486 | * // documents:
487 | * const miniSearch = new MiniSearch({ fields: ['title', 'text'] })
488 | * ```
489 | *
490 | * ### ID Field:
491 | *
492 | * ```javascript
493 | * // Your documents are assumed to include a unique 'id' field, but if you want
494 | * // to use a different field for document identification, you can set the
495 | * // 'idField' option:
496 | * const miniSearch = new MiniSearch({ idField: 'key', fields: ['title', 'text'] })
497 | * ```
498 | *
499 | * ### Options and defaults:
500 | *
501 | * ```javascript
502 | * // The full set of options (here with their default value) is:
503 | * const miniSearch = new MiniSearch({
504 | * // idField: field that uniquely identifies a document
505 | * idField: 'id',
506 | *
507 | * // extractField: function used to get the value of a field in a document.
508 | * // By default, it assumes the document is a flat object with field names as
509 | * // property keys and field values as string property values, but custom logic
510 | * // can be implemented by setting this option to a custom extractor function.
511 | * extractField: (document, fieldName) => document[fieldName],
512 | *
513 | * // tokenize: function used to split fields into individual terms. By
514 | * // default, it is also used to tokenize search queries, unless a specific
515 | * // `tokenize` search option is supplied. When tokenizing an indexed field,
516 | * // the field name is passed as the second argument.
517 | * tokenize: (string, _fieldName) => string.split(SPACE_OR_PUNCTUATION),
518 | *
519 | * // processTerm: function used to process each tokenized term before
520 | * // indexing. It can be used for stemming and normalization. Return a falsy
521 | * // value in order to discard a term. By default, it is also used to process
522 | * // search queries, unless a specific `processTerm` option is supplied as a
523 | * // search option. When processing a term from a indexed field, the field
524 | * // name is passed as the second argument.
525 | * processTerm: (term, _fieldName) => term.toLowerCase(),
526 | *
527 | * // searchOptions: default search options, see the `search` method for
528 | * // details
529 | * searchOptions: undefined,
530 | *
531 | * // fields: document fields to be indexed. Mandatory, but not set by default
532 | * fields: undefined
533 | *
534 | * // storeFields: document fields to be stored and returned as part of the
535 | * // search results.
536 | * storeFields: []
537 | * })
538 | * ```
539 | */
540 | constructor(options) {
541 | if ((options === null || options === void 0 ? void 0 : options.fields) == null) {
542 | throw new Error('MiniSearch: option "fields" must be provided');
543 | }
544 | const autoVacuum = options.autoVacuum == null || options.autoVacuum === true ? defaultAutoVacuumOptions : options.autoVacuum;
545 | this._options = {
546 | ...defaultOptions,
547 | ...options,
548 | autoVacuum,
549 | searchOptions: { ...defaultSearchOptions, ...options.searchOptions || {} },
550 | autoSuggestOptions: { ...defaultAutoSuggestOptions, ...options.autoSuggestOptions || {} }
551 | };
552 | this._index = new SearchableMap();
553 | this._documentCount = 0;
554 | this._documentIds = /* @__PURE__ */ new Map();
555 | this._idToShortId = /* @__PURE__ */ new Map();
556 | this._fieldIds = {};
557 | this._fieldLength = /* @__PURE__ */ new Map();
558 | this._avgFieldLength = [];
559 | this._nextId = 0;
560 | this._storedFields = /* @__PURE__ */ new Map();
561 | this._dirtCount = 0;
562 | this._currentVacuum = null;
563 | this._enqueuedVacuum = null;
564 | this._enqueuedVacuumConditions = defaultVacuumConditions;
565 | this.addFields(this._options.fields);
566 | }
567 | /**
568 | * Adds a document to the index
569 | *
570 | * @param document The document to be indexed
571 | */
572 | add(document) {
573 | const { extractField, tokenize, processTerm, fields, idField } = this._options;
574 | const id = extractField(document, idField);
575 | if (id == null) {
576 | throw new Error(`MiniSearch: document does not have ID field "${idField}"`);
577 | }
578 | if (this._idToShortId.has(id)) {
579 | throw new Error(`MiniSearch: duplicate ID ${id}`);
580 | }
581 | const shortDocumentId = this.addDocumentId(id);
582 | this.saveStoredFields(shortDocumentId, document);
583 | for (const field of fields) {
584 | const fieldValue = extractField(document, field);
585 | if (fieldValue == null)
586 | continue;
587 | const tokens = tokenize(fieldValue.toString(), field);
588 | const fieldId = this._fieldIds[field];
589 | const uniqueTerms = new Set(tokens).size;
590 | this.addFieldLength(shortDocumentId, fieldId, this._documentCount - 1, uniqueTerms);
591 | for (const term of tokens) {
592 | const processedTerm = processTerm(term, field);
593 | if (Array.isArray(processedTerm)) {
594 | for (const t of processedTerm) {
595 | this.addTerm(fieldId, shortDocumentId, t);
596 | }
597 | } else if (processedTerm) {
598 | this.addTerm(fieldId, shortDocumentId, processedTerm);
599 | }
600 | }
601 | }
602 | }
603 | /**
604 | * Adds all the given documents to the index
605 | *
606 | * @param documents An array of documents to be indexed
607 | */
608 | addAll(documents) {
609 | for (const document of documents)
610 | this.add(document);
611 | }
612 | /**
613 | * Adds all the given documents to the index asynchronously.
614 | *
615 | * Returns a promise that resolves (to `undefined`) when the indexing is done.
616 | * This method is useful when index many documents, to avoid blocking the main
617 | * thread. The indexing is performed asynchronously and in chunks.
618 | *
619 | * @param documents An array of documents to be indexed
620 | * @param options Configuration options
621 | * @return A promise resolving to `undefined` when the indexing is done
622 | */
623 | addAllAsync(documents, options = {}) {
624 | const { chunkSize = 10 } = options;
625 | const acc = { chunk: [], promise: Promise.resolve() };
626 | const { chunk, promise } = documents.reduce(({ chunk: chunk2, promise: promise2 }, document, i) => {
627 | chunk2.push(document);
628 | if ((i + 1) % chunkSize === 0) {
629 | return {
630 | chunk: [],
631 | promise: promise2.then(() => new Promise((resolve) => setTimeout(resolve, 0))).then(() => this.addAll(chunk2))
632 | };
633 | } else {
634 | return { chunk: chunk2, promise: promise2 };
635 | }
636 | }, acc);
637 | return promise.then(() => this.addAll(chunk));
638 | }
639 | /**
640 | * Removes the given document from the index.
641 | *
642 | * The document to remove must NOT have changed between indexing and removal,
643 | * otherwise the index will be corrupted.
644 | *
645 | * This method requires passing the full document to be removed (not just the
646 | * ID), and immediately removes the document from the inverted index, allowing
647 | * memory to be released. A convenient alternative is {@link
648 | * MiniSearch#discard}, which needs only the document ID, and has the same
649 | * visible effect, but delays cleaning up the index until the next vacuuming.
650 | *
651 | * @param document The document to be removed
652 | */
653 | remove(document) {
654 | const { tokenize, processTerm, extractField, fields, idField } = this._options;
655 | const id = extractField(document, idField);
656 | if (id == null) {
657 | throw new Error(`MiniSearch: document does not have ID field "${idField}"`);
658 | }
659 | const shortId = this._idToShortId.get(id);
660 | if (shortId == null) {
661 | throw new Error(`MiniSearch: cannot remove document with ID ${id}: it is not in the index`);
662 | }
663 | for (const field of fields) {
664 | const fieldValue = extractField(document, field);
665 | if (fieldValue == null)
666 | continue;
667 | const tokens = tokenize(fieldValue.toString(), field);
668 | const fieldId = this._fieldIds[field];
669 | const uniqueTerms = new Set(tokens).size;
670 | this.removeFieldLength(shortId, fieldId, this._documentCount, uniqueTerms);
671 | for (const term of tokens) {
672 | const processedTerm = processTerm(term, field);
673 | if (Array.isArray(processedTerm)) {
674 | for (const t of processedTerm) {
675 | this.removeTerm(fieldId, shortId, t);
676 | }
677 | } else if (processedTerm) {
678 | this.removeTerm(fieldId, shortId, processedTerm);
679 | }
680 | }
681 | }
682 | this._storedFields.delete(shortId);
683 | this._documentIds.delete(shortId);
684 | this._idToShortId.delete(id);
685 | this._fieldLength.delete(shortId);
686 | this._documentCount -= 1;
687 | }
688 | /**
689 | * Removes all the given documents from the index. If called with no arguments,
690 | * it removes _all_ documents from the index.
691 | *
692 | * @param documents The documents to be removed. If this argument is omitted,
693 | * all documents are removed. Note that, for removing all documents, it is
694 | * more efficient to call this method with no arguments than to pass all
695 | * documents.
696 | */
697 | removeAll(documents) {
698 | if (documents) {
699 | for (const document of documents)
700 | this.remove(document);
701 | } else if (arguments.length > 0) {
702 | throw new Error("Expected documents to be present. Omit the argument to remove all documents.");
703 | } else {
704 | this._index = new SearchableMap();
705 | this._documentCount = 0;
706 | this._documentIds = /* @__PURE__ */ new Map();
707 | this._idToShortId = /* @__PURE__ */ new Map();
708 | this._fieldLength = /* @__PURE__ */ new Map();
709 | this._avgFieldLength = [];
710 | this._storedFields = /* @__PURE__ */ new Map();
711 | this._nextId = 0;
712 | }
713 | }
714 | /**
715 | * Discards the document with the given ID, so it won't appear in search results
716 | *
717 | * It has the same visible effect of {@link MiniSearch.remove} (both cause the
718 | * document to stop appearing in searches), but a different effect on the
719 | * internal data structures:
720 | *
721 | * - {@link MiniSearch#remove} requires passing the full document to be
722 | * removed as argument, and removes it from the inverted index immediately.
723 | *
724 | * - {@link MiniSearch#discard} instead only needs the document ID, and
725 | * works by marking the current version of the document as discarded, so it
726 | * is immediately ignored by searches. This is faster and more convenient
727 | * than {@link MiniSearch#remove}, but the index is not immediately
728 | * modified. To take care of that, vacuuming is performed after a certain
729 | * number of documents are discarded, cleaning up the index and allowing
730 | * memory to be released.
731 | *
732 | * After discarding a document, it is possible to re-add a new version, and
733 | * only the new version will appear in searches. In other words, discarding
734 | * and re-adding a document works exactly like removing and re-adding it. The
735 | * {@link MiniSearch.replace} method can also be used to replace a document
736 | * with a new version.
737 | *
738 | * #### Details about vacuuming
739 | *
740 | * Repetite calls to this method would leave obsolete document references in
741 | * the index, invisible to searches. Two mechanisms take care of cleaning up:
742 | * clean up during search, and vacuuming.
743 | *
744 | * - Upon search, whenever a discarded ID is found (and ignored for the
745 | * results), references to the discarded document are removed from the
746 | * inverted index entries for the search terms. This ensures that subsequent
747 | * searches for the same terms do not need to skip these obsolete references
748 | * again.
749 | *
750 | * - In addition, vacuuming is performed automatically by default (see the
751 | * `autoVacuum` field in {@link Options}) after a certain number of
752 | * documents are discarded. Vacuuming traverses all terms in the index,
753 | * cleaning up all references to discarded documents. Vacuuming can also be
754 | * triggered manually by calling {@link MiniSearch#vacuum}.
755 | *
756 | * @param id The ID of the document to be discarded
757 | */
758 | discard(id) {
759 | const shortId = this._idToShortId.get(id);
760 | if (shortId == null) {
761 | throw new Error(`MiniSearch: cannot discard document with ID ${id}: it is not in the index`);
762 | }
763 | this._idToShortId.delete(id);
764 | this._documentIds.delete(shortId);
765 | this._storedFields.delete(shortId);
766 | (this._fieldLength.get(shortId) || []).forEach((fieldLength, fieldId) => {
767 | this.removeFieldLength(shortId, fieldId, this._documentCount, fieldLength);
768 | });
769 | this._fieldLength.delete(shortId);
770 | this._documentCount -= 1;
771 | this._dirtCount += 1;
772 | this.maybeAutoVacuum();
773 | }
774 | maybeAutoVacuum() {
775 | if (this._options.autoVacuum === false) {
776 | return;
777 | }
778 | const { minDirtFactor, minDirtCount, batchSize, batchWait } = this._options.autoVacuum;
779 | this.conditionalVacuum({ batchSize, batchWait }, { minDirtCount, minDirtFactor });
780 | }
781 | /**
782 | * Discards the documents with the given IDs, so they won't appear in search
783 | * results
784 | *
785 | * It is equivalent to calling {@link MiniSearch#discard} for all the given
786 | * IDs, but with the optimization of triggering at most one automatic
787 | * vacuuming at the end.
788 | *
789 | * Note: to remove all documents from the index, it is faster and more
790 | * convenient to call {@link MiniSearch.removeAll} with no argument, instead
791 | * of passing all IDs to this method.
792 | */
793 | discardAll(ids) {
794 | const autoVacuum = this._options.autoVacuum;
795 | try {
796 | this._options.autoVacuum = false;
797 | for (const id of ids) {
798 | this.discard(id);
799 | }
800 | } finally {
801 | this._options.autoVacuum = autoVacuum;
802 | }
803 | this.maybeAutoVacuum();
804 | }
805 | /**
806 | * It replaces an existing document with the given updated version
807 | *
808 | * It works by discarding the current version and adding the updated one, so
809 | * it is functionally equivalent to calling {@link MiniSearch#discard}
810 | * followed by {@link MiniSearch#add}. The ID of the updated document should
811 | * be the same as the original one.
812 | *
813 | * Since it uses {@link MiniSearch#discard} internally, this method relies on
814 | * vacuuming to clean up obsolete document references from the index, allowing
815 | * memory to be released (see {@link MiniSearch#discard}).
816 | *
817 | * @param updatedDocument The updated document to replace the old version
818 | * with
819 | */
820 | replace(updatedDocument) {
821 | const { idField, extractField } = this._options;
822 | const id = extractField(updatedDocument, idField);
823 | this.discard(id);
824 | this.add(updatedDocument);
825 | }
826 | /**
827 | * Triggers a manual vacuuming, cleaning up references to discarded documents
828 | * from the inverted index
829 | *
830 | * Vacuuming is only useful for applications that use the {@link
831 | * MiniSearch#discard} or {@link MiniSearch#replace} methods.
832 | *
833 | * By default, vacuuming is performed automatically when needed (controlled by
834 | * the `autoVacuum` field in {@link Options}), so there is usually no need to
835 | * call this method, unless one wants to make sure to perform vacuuming at a
836 | * specific moment.
837 | *
838 | * Vacuuming traverses all terms in the inverted index in batches, and cleans
839 | * up references to discarded documents from the posting list, allowing memory
840 | * to be released.
841 | *
842 | * The method takes an optional object as argument with the following keys:
843 | *
844 | * - `batchSize`: the size of each batch (1000 by default)
845 | *
846 | * - `batchWait`: the number of milliseconds to wait between batches (10 by
847 | * default)
848 | *
849 | * On large indexes, vacuuming could have a non-negligible cost: batching
850 | * avoids blocking the thread for long, diluting this cost so that it is not
851 | * negatively affecting the application. Nonetheless, this method should only
852 | * be called when necessary, and relying on automatic vacuuming is usually
853 | * better.
854 | *
855 | * It returns a promise that resolves (to undefined) when the clean up is
856 | * completed. If vacuuming is already ongoing at the time this method is
857 | * called, a new one is enqueued immediately after the ongoing one, and a
858 | * corresponding promise is returned. However, no more than one vacuuming is
859 | * enqueued on top of the ongoing one, even if this method is called more
860 | * times (enqueuing multiple ones would be useless).
861 | *
862 | * @param options Configuration options for the batch size and delay. See
863 | * {@link VacuumOptions}.
864 | */
865 | vacuum(options = {}) {
866 | return this.conditionalVacuum(options);
867 | }
868 | conditionalVacuum(options, conditions) {
869 | if (this._currentVacuum) {
870 | this._enqueuedVacuumConditions = this._enqueuedVacuumConditions && conditions;
871 | if (this._enqueuedVacuum != null) {
872 | return this._enqueuedVacuum;
873 | }
874 | this._enqueuedVacuum = this._currentVacuum.then(() => {
875 | const conditions2 = this._enqueuedVacuumConditions;
876 | this._enqueuedVacuumConditions = defaultVacuumConditions;
877 | return this.performVacuuming(options, conditions2);
878 | });
879 | return this._enqueuedVacuum;
880 | }
881 | if (this.vacuumConditionsMet(conditions) === false) {
882 | return Promise.resolve();
883 | }
884 | this._currentVacuum = this.performVacuuming(options);
885 | return this._currentVacuum;
886 | }
887 | async performVacuuming(options, conditions) {
888 | const initialDirtCount = this._dirtCount;
889 | if (this.vacuumConditionsMet(conditions)) {
890 | const batchSize = options.batchSize || defaultVacuumOptions.batchSize;
891 | const batchWait = options.batchWait || defaultVacuumOptions.batchWait;
892 | let i = 1;
893 | for (const [term, fieldsData] of this._index) {
894 | for (const [fieldId, fieldIndex] of fieldsData) {
895 | for (const [shortId] of fieldIndex) {
896 | if (this._documentIds.has(shortId)) {
897 | continue;
898 | }
899 | if (fieldIndex.size <= 1) {
900 | fieldsData.delete(fieldId);
901 | } else {
902 | fieldIndex.delete(shortId);
903 | }
904 | }
905 | }
906 | if (this._index.get(term).size === 0) {
907 | this._index.delete(term);
908 | }
909 | if (i % batchSize === 0) {
910 | await new Promise((resolve) => setTimeout(resolve, batchWait));
911 | }
912 | i += 1;
913 | }
914 | this._dirtCount -= initialDirtCount;
915 | }
916 | await null;
917 | this._currentVacuum = this._enqueuedVacuum;
918 | this._enqueuedVacuum = null;
919 | }
920 | vacuumConditionsMet(conditions) {
921 | if (conditions == null) {
922 | return true;
923 | }
924 | let { minDirtCount, minDirtFactor } = conditions;
925 | minDirtCount = minDirtCount || defaultAutoVacuumOptions.minDirtCount;
926 | minDirtFactor = minDirtFactor || defaultAutoVacuumOptions.minDirtFactor;
927 | return this.dirtCount >= minDirtCount && this.dirtFactor >= minDirtFactor;
928 | }
929 | /**
930 | * Is `true` if a vacuuming operation is ongoing, `false` otherwise
931 | */
932 | get isVacuuming() {
933 | return this._currentVacuum != null;
934 | }
935 | /**
936 | * The number of documents discarded since the most recent vacuuming
937 | */
938 | get dirtCount() {
939 | return this._dirtCount;
940 | }
941 | /**
942 | * A number between 0 and 1 giving an indication about the proportion of
943 | * documents that are discarded, and can therefore be cleaned up by vacuuming.
944 | * A value close to 0 means that the index is relatively clean, while a higher
945 | * value means that the index is relatively dirty, and vacuuming could release
946 | * memory.
947 | */
948 | get dirtFactor() {
949 | return this._dirtCount / (1 + this._documentCount + this._dirtCount);
950 | }
951 | /**
952 | * Returns `true` if a document with the given ID is present in the index and
953 | * available for search, `false` otherwise
954 | *
955 | * @param id The document ID
956 | */
957 | has(id) {
958 | return this._idToShortId.has(id);
959 | }
960 | /**
961 | * Returns the stored fields (as configured in the `storeFields` constructor
962 | * option) for the given document ID. Returns `undefined` if the document is
963 | * not present in the index.
964 | *
965 | * @param id The document ID
966 | */
967 | getStoredFields(id) {
968 | const shortId = this._idToShortId.get(id);
969 | if (shortId == null) {
970 | return void 0;
971 | }
972 | return this._storedFields.get(shortId);
973 | }
974 | /**
975 | * Search for documents matching the given search query.
976 | *
977 | * The result is a list of scored document IDs matching the query, sorted by
978 | * descending score, and each including data about which terms were matched and
979 | * in which fields.
980 | *
981 | * ### Basic usage:
982 | *
983 | * ```javascript
984 | * // Search for "zen art motorcycle" with default options: terms have to match
985 | * // exactly, and individual terms are joined with OR
986 | * miniSearch.search('zen art motorcycle')
987 | * // => [ { id: 2, score: 2.77258, match: { ... } }, { id: 4, score: 1.38629, match: { ... } } ]
988 | * ```
989 | *
990 | * ### Restrict search to specific fields:
991 | *
992 | * ```javascript
993 | * // Search only in the 'title' field
994 | * miniSearch.search('zen', { fields: ['title'] })
995 | * ```
996 | *
997 | * ### Field boosting:
998 | *
999 | * ```javascript
1000 | * // Boost a field
1001 | * miniSearch.search('zen', { boost: { title: 2 } })
1002 | * ```
1003 | *
1004 | * ### Prefix search:
1005 | *
1006 | * ```javascript
1007 | * // Search for "moto" with prefix search (it will match documents
1008 | * // containing terms that start with "moto" or "neuro")
1009 | * miniSearch.search('moto neuro', { prefix: true })
1010 | * ```
1011 | *
1012 | * ### Fuzzy search:
1013 | *
1014 | * ```javascript
1015 | * // Search for "ismael" with fuzzy search (it will match documents containing
1016 | * // terms similar to "ismael", with a maximum edit distance of 0.2 term.length
1017 | * // (rounded to nearest integer)
1018 | * miniSearch.search('ismael', { fuzzy: 0.2 })
1019 | * ```
1020 | *
1021 | * ### Combining strategies:
1022 | *
1023 | * ```javascript
1024 | * // Mix of exact match, prefix search, and fuzzy search
1025 | * miniSearch.search('ismael mob', {
1026 | * prefix: true,
1027 | * fuzzy: 0.2
1028 | * })
1029 | * ```
1030 | *
1031 | * ### Advanced prefix and fuzzy search:
1032 | *
1033 | * ```javascript
1034 | * // Perform fuzzy and prefix search depending on the search term. Here
1035 | * // performing prefix and fuzzy search only on terms longer than 3 characters
1036 | * miniSearch.search('ismael mob', {
1037 | * prefix: term => term.length > 3
1038 | * fuzzy: term => term.length > 3 ? 0.2 : null
1039 | * })
1040 | * ```
1041 | *
1042 | * ### Combine with AND:
1043 | *
1044 | * ```javascript
1045 | * // Combine search terms with AND (to match only documents that contain both
1046 | * // "motorcycle" and "art")
1047 | * miniSearch.search('motorcycle art', { combineWith: 'AND' })
1048 | * ```
1049 | *
1050 | * ### Combine with AND_NOT:
1051 | *
1052 | * There is also an AND_NOT combinator, that finds documents that match the
1053 | * first term, but do not match any of the other terms. This combinator is
1054 | * rarely useful with simple queries, and is meant to be used with advanced
1055 | * query combinations (see later for more details).
1056 | *
1057 | * ### Filtering results:
1058 | *
1059 | * ```javascript
1060 | * // Filter only results in the 'fiction' category (assuming that 'category'
1061 | * // is a stored field)
1062 | * miniSearch.search('motorcycle art', {
1063 | * filter: (result) => result.category === 'fiction'
1064 | * })
1065 | * ```
1066 | *
1067 | * ### Wildcard query
1068 | *
1069 | * Searching for an empty string (assuming the default tokenizer) returns no
1070 | * results. Sometimes though, one needs to match all documents, like in a
1071 | * "wildcard" search. This is possible by passing the special value
1072 | * {@link MiniSearch.wildcard} as the query:
1073 | *
1074 | * ```javascript
1075 | * // Return search results for all documents
1076 | * miniSearch.search(MiniSearch.wildcard)
1077 | * ```
1078 | *
1079 | * Note that search options such as `filter` and `boostDocument` are still
1080 | * applied, influencing which results are returned, and their order:
1081 | *
1082 | * ```javascript
1083 | * // Return search results for all documents in the 'fiction' category
1084 | * miniSearch.search(MiniSearch.wildcard, {
1085 | * filter: (result) => result.category === 'fiction'
1086 | * })
1087 | * ```
1088 | *
1089 | * ### Advanced combination of queries:
1090 | *
1091 | * It is possible to combine different subqueries with OR, AND, and AND_NOT,
1092 | * and even with different search options, by passing a query expression
1093 | * tree object as the first argument, instead of a string.
1094 | *
1095 | * ```javascript
1096 | * // Search for documents that contain "zen" and ("motorcycle" or "archery")
1097 | * miniSearch.search({
1098 | * combineWith: 'AND',
1099 | * queries: [
1100 | * 'zen',
1101 | * {
1102 | * combineWith: 'OR',
1103 | * queries: ['motorcycle', 'archery']
1104 | * }
1105 | * ]
1106 | * })
1107 | *
1108 | * // Search for documents that contain ("apple" or "pear") but not "juice" and
1109 | * // not "tree"
1110 | * miniSearch.search({
1111 | * combineWith: 'AND_NOT',
1112 | * queries: [
1113 | * {
1114 | * combineWith: 'OR',
1115 | * queries: ['apple', 'pear']
1116 | * },
1117 | * 'juice',
1118 | * 'tree'
1119 | * ]
1120 | * })
1121 | * ```
1122 | *
1123 | * Each node in the expression tree can be either a string, or an object that
1124 | * supports all {@link SearchOptions} fields, plus a `queries` array field for
1125 | * subqueries.
1126 | *
1127 | * Note that, while this can become complicated to do by hand for complex or
1128 | * deeply nested queries, it provides a formalized expression tree API for
1129 | * external libraries that implement a parser for custom query languages.
1130 | *
1131 | * @param query Search query
1132 | * @param searchOptions Search options. Each option, if not given, defaults to the corresponding value of `searchOptions` given to the constructor, or to the library default.
1133 | */
1134 | search(query, searchOptions = {}) {
1135 | const { searchOptions: globalSearchOptions } = this._options;
1136 | const searchOptionsWithDefaults = { ...globalSearchOptions, ...searchOptions };
1137 | const rawResults = this.executeQuery(query, searchOptions);
1138 | const results = [];
1139 | for (const [docId, { score, terms, match }] of rawResults) {
1140 | const quality = terms.length || 1;
1141 | const result = {
1142 | id: this._documentIds.get(docId),
1143 | score: score * quality,
1144 | terms: Object.keys(match),
1145 | queryTerms: terms,
1146 | match
1147 | };
1148 | Object.assign(result, this._storedFields.get(docId));
1149 | if (searchOptionsWithDefaults.filter == null || searchOptionsWithDefaults.filter(result)) {
1150 | results.push(result);
1151 | }
1152 | }
1153 | if (query === _MiniSearch.wildcard && searchOptionsWithDefaults.boostDocument == null) {
1154 | return results;
1155 | }
1156 | results.sort(byScore);
1157 | return results;
1158 | }
1159 | /**
1160 | * Provide suggestions for the given search query
1161 | *
1162 | * The result is a list of suggested modified search queries, derived from the
1163 | * given search query, each with a relevance score, sorted by descending score.
1164 | *
1165 | * By default, it uses the same options used for search, except that by
1166 | * default it performs prefix search on the last term of the query, and
1167 | * combine terms with `'AND'` (requiring all query terms to match). Custom
1168 | * options can be passed as a second argument. Defaults can be changed upon
1169 | * calling the {@link MiniSearch} constructor, by passing a
1170 | * `autoSuggestOptions` option.
1171 | *
1172 | * ### Basic usage:
1173 | *
1174 | * ```javascript
1175 | * // Get suggestions for 'neuro':
1176 | * miniSearch.autoSuggest('neuro')
1177 | * // => [ { suggestion: 'neuromancer', terms: [ 'neuromancer' ], score: 0.46240 } ]
1178 | * ```
1179 | *
1180 | * ### Multiple words:
1181 | *
1182 | * ```javascript
1183 | * // Get suggestions for 'zen ar':
1184 | * miniSearch.autoSuggest('zen ar')
1185 | * // => [
1186 | * // { suggestion: 'zen archery art', terms: [ 'zen', 'archery', 'art' ], score: 1.73332 },
1187 | * // { suggestion: 'zen art', terms: [ 'zen', 'art' ], score: 1.21313 }
1188 | * // ]
1189 | * ```
1190 | *
1191 | * ### Fuzzy suggestions:
1192 | *
1193 | * ```javascript
1194 | * // Correct spelling mistakes using fuzzy search:
1195 | * miniSearch.autoSuggest('neromancer', { fuzzy: 0.2 })
1196 | * // => [ { suggestion: 'neuromancer', terms: [ 'neuromancer' ], score: 1.03998 } ]
1197 | * ```
1198 | *
1199 | * ### Filtering:
1200 | *
1201 | * ```javascript
1202 | * // Get suggestions for 'zen ar', but only within the 'fiction' category
1203 | * // (assuming that 'category' is a stored field):
1204 | * miniSearch.autoSuggest('zen ar', {
1205 | * filter: (result) => result.category === 'fiction'
1206 | * })
1207 | * // => [
1208 | * // { suggestion: 'zen archery art', terms: [ 'zen', 'archery', 'art' ], score: 1.73332 },
1209 | * // { suggestion: 'zen art', terms: [ 'zen', 'art' ], score: 1.21313 }
1210 | * // ]
1211 | * ```
1212 | *
1213 | * @param queryString Query string to be expanded into suggestions
1214 | * @param options Search options. The supported options and default values
1215 | * are the same as for the {@link MiniSearch#search} method, except that by
1216 | * default prefix search is performed on the last term in the query, and terms
1217 | * are combined with `'AND'`.
1218 | * @return A sorted array of suggestions sorted by relevance score.
1219 | */
1220 | autoSuggest(queryString, options = {}) {
1221 | options = { ...this._options.autoSuggestOptions, ...options };
1222 | const suggestions = /* @__PURE__ */ new Map();
1223 | for (const { score, terms } of this.search(queryString, options)) {
1224 | const phrase = terms.join(" ");
1225 | const suggestion = suggestions.get(phrase);
1226 | if (suggestion != null) {
1227 | suggestion.score += score;
1228 | suggestion.count += 1;
1229 | } else {
1230 | suggestions.set(phrase, { score, terms, count: 1 });
1231 | }
1232 | }
1233 | const results = [];
1234 | for (const [suggestion, { score, terms, count }] of suggestions) {
1235 | results.push({ suggestion, terms, score: score / count });
1236 | }
1237 | results.sort(byScore);
1238 | return results;
1239 | }
1240 | /**
1241 | * Total number of documents available to search
1242 | */
1243 | get documentCount() {
1244 | return this._documentCount;
1245 | }
1246 | /**
1247 | * Number of terms in the index
1248 | */
1249 | get termCount() {
1250 | return this._index.size;
1251 | }
1252 | /**
1253 | * Deserializes a JSON index (serialized with `JSON.stringify(miniSearch)`)
1254 | * and instantiates a MiniSearch instance. It should be given the same options
1255 | * originally used when serializing the index.
1256 | *
1257 | * ### Usage:
1258 | *
1259 | * ```javascript
1260 | * // If the index was serialized with:
1261 | * let miniSearch = new MiniSearch({ fields: ['title', 'text'] })
1262 | * miniSearch.addAll(documents)
1263 | *
1264 | * const json = JSON.stringify(miniSearch)
1265 | * // It can later be deserialized like this:
1266 | * miniSearch = MiniSearch.loadJSON(json, { fields: ['title', 'text'] })
1267 | * ```
1268 | *
1269 | * @param json JSON-serialized index
1270 | * @param options configuration options, same as the constructor
1271 | * @return An instance of MiniSearch deserialized from the given JSON.
1272 | */
1273 | static loadJSON(json, options) {
1274 | if (options == null) {
1275 | throw new Error("MiniSearch: loadJSON should be given the same options used when serializing the index");
1276 | }
1277 | return this.loadJS(JSON.parse(json), options);
1278 | }
1279 | /**
1280 | * Async equivalent of {@link MiniSearch.loadJSON}
1281 | *
1282 | * This function is an alternative to {@link MiniSearch.loadJSON} that returns
1283 | * a promise, and loads the index in batches, leaving pauses between them to avoid
1284 | * blocking the main thread. It tends to be slower than the synchronous
1285 | * version, but does not block the main thread, so it can be a better choice
1286 | * when deserializing very large indexes.
1287 | *
1288 | * @param json JSON-serialized index
1289 | * @param options configuration options, same as the constructor
1290 | * @return A Promise that will resolve to an instance of MiniSearch deserialized from the given JSON.
1291 | */
1292 | static async loadJSONAsync(json, options) {
1293 | if (options == null) {
1294 | throw new Error("MiniSearch: loadJSON should be given the same options used when serializing the index");
1295 | }
1296 | return this.loadJSAsync(JSON.parse(json), options);
1297 | }
1298 | /**
1299 | * Returns the default value of an option. It will throw an error if no option
1300 | * with the given name exists.
1301 | *
1302 | * @param optionName Name of the option
1303 | * @return The default value of the given option
1304 | *
1305 | * ### Usage:
1306 | *
1307 | * ```javascript
1308 | * // Get default tokenizer
1309 | * MiniSearch.getDefault('tokenize')
1310 | *
1311 | * // Get default term processor
1312 | * MiniSearch.getDefault('processTerm')
1313 | *
1314 | * // Unknown options will throw an error
1315 | * MiniSearch.getDefault('notExisting')
1316 | * // => throws 'MiniSearch: unknown option "notExisting"'
1317 | * ```
1318 | */
1319 | static getDefault(optionName) {
1320 | if (defaultOptions.hasOwnProperty(optionName)) {
1321 | return getOwnProperty(defaultOptions, optionName);
1322 | } else {
1323 | throw new Error(`MiniSearch: unknown option "${optionName}"`);
1324 | }
1325 | }
1326 | /**
1327 | * @ignore
1328 | */
1329 | static loadJS(js, options) {
1330 | const { index, documentIds, fieldLength, storedFields, serializationVersion } = js;
1331 | const miniSearch = this.instantiateMiniSearch(js, options);
1332 | miniSearch._documentIds = objectToNumericMap(documentIds);
1333 | miniSearch._fieldLength = objectToNumericMap(fieldLength);
1334 | miniSearch._storedFields = objectToNumericMap(storedFields);
1335 | for (const [shortId, id] of miniSearch._documentIds) {
1336 | miniSearch._idToShortId.set(id, shortId);
1337 | }
1338 | for (const [term, data] of index) {
1339 | const dataMap = /* @__PURE__ */ new Map();
1340 | for (const fieldId of Object.keys(data)) {
1341 | let indexEntry = data[fieldId];
1342 | if (serializationVersion === 1) {
1343 | indexEntry = indexEntry.ds;
1344 | }
1345 | dataMap.set(parseInt(fieldId, 10), objectToNumericMap(indexEntry));
1346 | }
1347 | miniSearch._index.set(term, dataMap);
1348 | }
1349 | return miniSearch;
1350 | }
1351 | /**
1352 | * @ignore
1353 | */
1354 | static async loadJSAsync(js, options) {
1355 | const { index, documentIds, fieldLength, storedFields, serializationVersion } = js;
1356 | const miniSearch = this.instantiateMiniSearch(js, options);
1357 | miniSearch._documentIds = await objectToNumericMapAsync(documentIds);
1358 | miniSearch._fieldLength = await objectToNumericMapAsync(fieldLength);
1359 | miniSearch._storedFields = await objectToNumericMapAsync(storedFields);
1360 | for (const [shortId, id] of miniSearch._documentIds) {
1361 | miniSearch._idToShortId.set(id, shortId);
1362 | }
1363 | let count = 0;
1364 | for (const [term, data] of index) {
1365 | const dataMap = /* @__PURE__ */ new Map();
1366 | for (const fieldId of Object.keys(data)) {
1367 | let indexEntry = data[fieldId];
1368 | if (serializationVersion === 1) {
1369 | indexEntry = indexEntry.ds;
1370 | }
1371 | dataMap.set(parseInt(fieldId, 10), await objectToNumericMapAsync(indexEntry));
1372 | }
1373 | if (++count % 1e3 === 0)
1374 | await wait(0);
1375 | miniSearch._index.set(term, dataMap);
1376 | }
1377 | return miniSearch;
1378 | }
1379 | /**
1380 | * @ignore
1381 | */
1382 | static instantiateMiniSearch(js, options) {
1383 | const { documentCount, nextId, fieldIds, averageFieldLength, dirtCount, serializationVersion } = js;
1384 | if (serializationVersion !== 1 && serializationVersion !== 2) {
1385 | throw new Error("MiniSearch: cannot deserialize an index created with an incompatible version");
1386 | }
1387 | const miniSearch = new _MiniSearch(options);
1388 | miniSearch._documentCount = documentCount;
1389 | miniSearch._nextId = nextId;
1390 | miniSearch._idToShortId = /* @__PURE__ */ new Map();
1391 | miniSearch._fieldIds = fieldIds;
1392 | miniSearch._avgFieldLength = averageFieldLength;
1393 | miniSearch._dirtCount = dirtCount || 0;
1394 | miniSearch._index = new SearchableMap();
1395 | return miniSearch;
1396 | }
1397 | /**
1398 | * @ignore
1399 | */
1400 | executeQuery(query, searchOptions = {}) {
1401 | if (query === _MiniSearch.wildcard) {
1402 | return this.executeWildcardQuery(searchOptions);
1403 | }
1404 | if (typeof query !== "string") {
1405 | const options2 = { ...searchOptions, ...query, queries: void 0 };
1406 | const results2 = query.queries.map((subquery) => this.executeQuery(subquery, options2));
1407 | return this.combineResults(results2, options2.combineWith);
1408 | }
1409 | const { tokenize, processTerm, searchOptions: globalSearchOptions } = this._options;
1410 | const options = { tokenize, processTerm, ...globalSearchOptions, ...searchOptions };
1411 | const { tokenize: searchTokenize, processTerm: searchProcessTerm } = options;
1412 | const terms = searchTokenize(query).flatMap((term) => searchProcessTerm(term)).filter((term) => !!term);
1413 | const queries = terms.map(termToQuerySpec(options));
1414 | const results = queries.map((query2) => this.executeQuerySpec(query2, options));
1415 | return this.combineResults(results, options.combineWith);
1416 | }
1417 | /**
1418 | * @ignore
1419 | */
1420 | executeQuerySpec(query, searchOptions) {
1421 | const options = { ...this._options.searchOptions, ...searchOptions };
1422 | const boosts = (options.fields || this._options.fields).reduce((boosts2, field) => ({ ...boosts2, [field]: getOwnProperty(options.boost, field) || 1 }), {});
1423 | const { boostDocument, weights, maxFuzzy, bm25: bm25params } = options;
1424 | const { fuzzy: fuzzyWeight, prefix: prefixWeight } = { ...defaultSearchOptions.weights, ...weights };
1425 | const data = this._index.get(query.term);
1426 | const results = this.termResults(query.term, query.term, 1, query.termBoost, data, boosts, boostDocument, bm25params);
1427 | let prefixMatches;
1428 | let fuzzyMatches;
1429 | if (query.prefix) {
1430 | prefixMatches = this._index.atPrefix(query.term);
1431 | }
1432 | if (query.fuzzy) {
1433 | const fuzzy = query.fuzzy === true ? 0.2 : query.fuzzy;
1434 | const maxDistance = fuzzy < 1 ? Math.min(maxFuzzy, Math.round(query.term.length * fuzzy)) : fuzzy;
1435 | if (maxDistance)
1436 | fuzzyMatches = this._index.fuzzyGet(query.term, maxDistance);
1437 | }
1438 | if (prefixMatches) {
1439 | for (const [term, data2] of prefixMatches) {
1440 | const distance = term.length - query.term.length;
1441 | if (!distance) {
1442 | continue;
1443 | }
1444 | fuzzyMatches === null || fuzzyMatches === void 0 ? void 0 : fuzzyMatches.delete(term);
1445 | const weight = prefixWeight * term.length / (term.length + 0.3 * distance);
1446 | this.termResults(query.term, term, weight, query.termBoost, data2, boosts, boostDocument, bm25params, results);
1447 | }
1448 | }
1449 | if (fuzzyMatches) {
1450 | for (const term of fuzzyMatches.keys()) {
1451 | const [data2, distance] = fuzzyMatches.get(term);
1452 | if (!distance) {
1453 | continue;
1454 | }
1455 | const weight = fuzzyWeight * term.length / (term.length + distance);
1456 | this.termResults(query.term, term, weight, query.termBoost, data2, boosts, boostDocument, bm25params, results);
1457 | }
1458 | }
1459 | return results;
1460 | }
1461 | /**
1462 | * @ignore
1463 | */
1464 | executeWildcardQuery(searchOptions) {
1465 | const results = /* @__PURE__ */ new Map();
1466 | const options = { ...this._options.searchOptions, ...searchOptions };
1467 | for (const [shortId, id] of this._documentIds) {
1468 | const score = options.boostDocument ? options.boostDocument(id, "", this._storedFields.get(shortId)) : 1;
1469 | results.set(shortId, {
1470 | score,
1471 | terms: [],
1472 | match: {}
1473 | });
1474 | }
1475 | return results;
1476 | }
1477 | /**
1478 | * @ignore
1479 | */
1480 | combineResults(results, combineWith = OR) {
1481 | if (results.length === 0) {
1482 | return /* @__PURE__ */ new Map();
1483 | }
1484 | const operator = combineWith.toLowerCase();
1485 | const combinator = combinators[operator];
1486 | if (!combinator) {
1487 | throw new Error(`Invalid combination operator: ${combineWith}`);
1488 | }
1489 | return results.reduce(combinator) || /* @__PURE__ */ new Map();
1490 | }
1491 | /**
1492 | * Allows serialization of the index to JSON, to possibly store it and later
1493 | * deserialize it with {@link MiniSearch.loadJSON}.
1494 | *
1495 | * Normally one does not directly call this method, but rather call the
1496 | * standard JavaScript `JSON.stringify()` passing the {@link MiniSearch}
1497 | * instance, and JavaScript will internally call this method. Upon
1498 | * deserialization, one must pass to {@link MiniSearch.loadJSON} the same
1499 | * options used to create the original instance that was serialized.
1500 | *
1501 | * ### Usage:
1502 | *
1503 | * ```javascript
1504 | * // Serialize the index:
1505 | * let miniSearch = new MiniSearch({ fields: ['title', 'text'] })
1506 | * miniSearch.addAll(documents)
1507 | * const json = JSON.stringify(miniSearch)
1508 | *
1509 | * // Later, to deserialize it:
1510 | * miniSearch = MiniSearch.loadJSON(json, { fields: ['title', 'text'] })
1511 | * ```
1512 | *
1513 | * @return A plain-object serializable representation of the search index.
1514 | */
1515 | toJSON() {
1516 | const index = [];
1517 | for (const [term, fieldIndex] of this._index) {
1518 | const data = {};
1519 | for (const [fieldId, freqs] of fieldIndex) {
1520 | data[fieldId] = Object.fromEntries(freqs);
1521 | }
1522 | index.push([term, data]);
1523 | }
1524 | return {
1525 | documentCount: this._documentCount,
1526 | nextId: this._nextId,
1527 | documentIds: Object.fromEntries(this._documentIds),
1528 | fieldIds: this._fieldIds,
1529 | fieldLength: Object.fromEntries(this._fieldLength),
1530 | averageFieldLength: this._avgFieldLength,
1531 | storedFields: Object.fromEntries(this._storedFields),
1532 | dirtCount: this._dirtCount,
1533 | index,
1534 | serializationVersion: 2
1535 | };
1536 | }
1537 | /**
1538 | * @ignore
1539 | */
1540 | termResults(sourceTerm, derivedTerm, termWeight, termBoost, fieldTermData, fieldBoosts, boostDocumentFn, bm25params, results = /* @__PURE__ */ new Map()) {
1541 | if (fieldTermData == null)
1542 | return results;
1543 | for (const field of Object.keys(fieldBoosts)) {
1544 | const fieldBoost = fieldBoosts[field];
1545 | const fieldId = this._fieldIds[field];
1546 | const fieldTermFreqs = fieldTermData.get(fieldId);
1547 | if (fieldTermFreqs == null)
1548 | continue;
1549 | let matchingFields = fieldTermFreqs.size;
1550 | const avgFieldLength = this._avgFieldLength[fieldId];
1551 | for (const docId of fieldTermFreqs.keys()) {
1552 | if (!this._documentIds.has(docId)) {
1553 | this.removeTerm(fieldId, docId, derivedTerm);
1554 | matchingFields -= 1;
1555 | continue;
1556 | }
1557 | const docBoost = boostDocumentFn ? boostDocumentFn(this._documentIds.get(docId), derivedTerm, this._storedFields.get(docId)) : 1;
1558 | if (!docBoost)
1559 | continue;
1560 | const termFreq = fieldTermFreqs.get(docId);
1561 | const fieldLength = this._fieldLength.get(docId)[fieldId];
1562 | const rawScore = calcBM25Score(termFreq, matchingFields, this._documentCount, fieldLength, avgFieldLength, bm25params);
1563 | const weightedScore = termWeight * termBoost * fieldBoost * docBoost * rawScore;
1564 | const result = results.get(docId);
1565 | if (result) {
1566 | result.score += weightedScore;
1567 | assignUniqueTerm(result.terms, sourceTerm);
1568 | const match = getOwnProperty(result.match, derivedTerm);
1569 | if (match) {
1570 | match.push(field);
1571 | } else {
1572 | result.match[derivedTerm] = [field];
1573 | }
1574 | } else {
1575 | results.set(docId, {
1576 | score: weightedScore,
1577 | terms: [sourceTerm],
1578 | match: { [derivedTerm]: [field] }
1579 | });
1580 | }
1581 | }
1582 | }
1583 | return results;
1584 | }
1585 | /**
1586 | * @ignore
1587 | */
1588 | addTerm(fieldId, documentId, term) {
1589 | const indexData = this._index.fetch(term, createMap);
1590 | let fieldIndex = indexData.get(fieldId);
1591 | if (fieldIndex == null) {
1592 | fieldIndex = /* @__PURE__ */ new Map();
1593 | fieldIndex.set(documentId, 1);
1594 | indexData.set(fieldId, fieldIndex);
1595 | } else {
1596 | const docs = fieldIndex.get(documentId);
1597 | fieldIndex.set(documentId, (docs || 0) + 1);
1598 | }
1599 | }
1600 | /**
1601 | * @ignore
1602 | */
1603 | removeTerm(fieldId, documentId, term) {
1604 | if (!this._index.has(term)) {
1605 | this.warnDocumentChanged(documentId, fieldId, term);
1606 | return;
1607 | }
1608 | const indexData = this._index.fetch(term, createMap);
1609 | const fieldIndex = indexData.get(fieldId);
1610 | if (fieldIndex == null || fieldIndex.get(documentId) == null) {
1611 | this.warnDocumentChanged(documentId, fieldId, term);
1612 | } else if (fieldIndex.get(documentId) <= 1) {
1613 | if (fieldIndex.size <= 1) {
1614 | indexData.delete(fieldId);
1615 | } else {
1616 | fieldIndex.delete(documentId);
1617 | }
1618 | } else {
1619 | fieldIndex.set(documentId, fieldIndex.get(documentId) - 1);
1620 | }
1621 | if (this._index.get(term).size === 0) {
1622 | this._index.delete(term);
1623 | }
1624 | }
1625 | /**
1626 | * @ignore
1627 | */
1628 | warnDocumentChanged(shortDocumentId, fieldId, term) {
1629 | for (const fieldName of Object.keys(this._fieldIds)) {
1630 | if (this._fieldIds[fieldName] === fieldId) {
1631 | this._options.logger("warn", `MiniSearch: document with ID ${this._documentIds.get(shortDocumentId)} has changed before removal: term "${term}" was not present in field "${fieldName}". Removing a document after it has changed can corrupt the index!`, "version_conflict");
1632 | return;
1633 | }
1634 | }
1635 | }
1636 | /**
1637 | * @ignore
1638 | */
1639 | addDocumentId(documentId) {
1640 | const shortDocumentId = this._nextId;
1641 | this._idToShortId.set(documentId, shortDocumentId);
1642 | this._documentIds.set(shortDocumentId, documentId);
1643 | this._documentCount += 1;
1644 | this._nextId += 1;
1645 | return shortDocumentId;
1646 | }
1647 | /**
1648 | * @ignore
1649 | */
1650 | addFields(fields) {
1651 | for (let i = 0; i < fields.length; i++) {
1652 | this._fieldIds[fields[i]] = i;
1653 | }
1654 | }
1655 | /**
1656 | * @ignore
1657 | */
1658 | addFieldLength(documentId, fieldId, count, length) {
1659 | let fieldLengths = this._fieldLength.get(documentId);
1660 | if (fieldLengths == null)
1661 | this._fieldLength.set(documentId, fieldLengths = []);
1662 | fieldLengths[fieldId] = length;
1663 | const averageFieldLength = this._avgFieldLength[fieldId] || 0;
1664 | const totalFieldLength = averageFieldLength * count + length;
1665 | this._avgFieldLength[fieldId] = totalFieldLength / (count + 1);
1666 | }
1667 | /**
1668 | * @ignore
1669 | */
1670 | removeFieldLength(documentId, fieldId, count, length) {
1671 | if (count === 1) {
1672 | this._avgFieldLength[fieldId] = 0;
1673 | return;
1674 | }
1675 | const totalFieldLength = this._avgFieldLength[fieldId] * count - length;
1676 | this._avgFieldLength[fieldId] = totalFieldLength / (count - 1);
1677 | }
1678 | /**
1679 | * @ignore
1680 | */
1681 | saveStoredFields(documentId, doc) {
1682 | const { storeFields, extractField } = this._options;
1683 | if (storeFields == null || storeFields.length === 0) {
1684 | return;
1685 | }
1686 | let documentFields = this._storedFields.get(documentId);
1687 | if (documentFields == null)
1688 | this._storedFields.set(documentId, documentFields = {});
1689 | for (const fieldName of storeFields) {
1690 | const fieldValue = extractField(doc, fieldName);
1691 | if (fieldValue !== void 0)
1692 | documentFields[fieldName] = fieldValue;
1693 | }
1694 | }
1695 | };
1696 | MiniSearch.wildcard = Symbol("*");
1697 | var getOwnProperty = (object, property) => Object.prototype.hasOwnProperty.call(object, property) ? object[property] : void 0;
1698 | var combinators = {
1699 | [OR]: (a, b) => {
1700 | for (const docId of b.keys()) {
1701 | const existing = a.get(docId);
1702 | if (existing == null) {
1703 | a.set(docId, b.get(docId));
1704 | } else {
1705 | const { score, terms, match } = b.get(docId);
1706 | existing.score = existing.score + score;
1707 | existing.match = Object.assign(existing.match, match);
1708 | assignUniqueTerms(existing.terms, terms);
1709 | }
1710 | }
1711 | return a;
1712 | },
1713 | [AND]: (a, b) => {
1714 | const combined = /* @__PURE__ */ new Map();
1715 | for (const docId of b.keys()) {
1716 | const existing = a.get(docId);
1717 | if (existing == null)
1718 | continue;
1719 | const { score, terms, match } = b.get(docId);
1720 | assignUniqueTerms(existing.terms, terms);
1721 | combined.set(docId, {
1722 | score: existing.score + score,
1723 | terms: existing.terms,
1724 | match: Object.assign(existing.match, match)
1725 | });
1726 | }
1727 | return combined;
1728 | },
1729 | [AND_NOT]: (a, b) => {
1730 | for (const docId of b.keys())
1731 | a.delete(docId);
1732 | return a;
1733 | }
1734 | };
1735 | var defaultBM25params = { k: 1.2, b: 0.7, d: 0.5 };
1736 | var calcBM25Score = (termFreq, matchingCount, totalCount, fieldLength, avgFieldLength, bm25params) => {
1737 | const { k, b, d } = bm25params;
1738 | const invDocFreq = Math.log(1 + (totalCount - matchingCount + 0.5) / (matchingCount + 0.5));
1739 | return invDocFreq * (d + termFreq * (k + 1) / (termFreq + k * (1 - b + b * fieldLength / avgFieldLength)));
1740 | };
1741 | var termToQuerySpec = (options) => (term, i, terms) => {
1742 | const fuzzy = typeof options.fuzzy === "function" ? options.fuzzy(term, i, terms) : options.fuzzy || false;
1743 | const prefix = typeof options.prefix === "function" ? options.prefix(term, i, terms) : options.prefix === true;
1744 | const termBoost = typeof options.boostTerm === "function" ? options.boostTerm(term, i, terms) : 1;
1745 | return { term, fuzzy, prefix, termBoost };
1746 | };
1747 | var defaultOptions = {
1748 | idField: "id",
1749 | extractField: (document, fieldName) => document[fieldName],
1750 | tokenize: (text) => text.split(SPACE_OR_PUNCTUATION),
1751 | processTerm: (term) => term.toLowerCase(),
1752 | fields: void 0,
1753 | searchOptions: void 0,
1754 | storeFields: [],
1755 | logger: (level, message) => {
1756 | if (typeof (console === null || console === void 0 ? void 0 : console[level]) === "function")
1757 | console[level](message);
1758 | },
1759 | autoVacuum: true
1760 | };
1761 | var defaultSearchOptions = {
1762 | combineWith: OR,
1763 | prefix: false,
1764 | fuzzy: false,
1765 | maxFuzzy: 6,
1766 | boost: {},
1767 | weights: { fuzzy: 0.45, prefix: 0.375 },
1768 | bm25: defaultBM25params
1769 | };
1770 | var defaultAutoSuggestOptions = {
1771 | combineWith: AND,
1772 | prefix: (term, i, terms) => i === terms.length - 1
1773 | };
1774 | var defaultVacuumOptions = { batchSize: 1e3, batchWait: 10 };
1775 | var defaultVacuumConditions = { minDirtFactor: 0.1, minDirtCount: 20 };
1776 | var defaultAutoVacuumOptions = { ...defaultVacuumOptions, ...defaultVacuumConditions };
1777 | var assignUniqueTerm = (target, term) => {
1778 | if (!target.includes(term))
1779 | target.push(term);
1780 | };
1781 | var assignUniqueTerms = (target, source) => {
1782 | for (const term of source) {
1783 | if (!target.includes(term))
1784 | target.push(term);
1785 | }
1786 | };
1787 | var byScore = ({ score: a }, { score: b }) => b - a;
1788 | var createMap = () => /* @__PURE__ */ new Map();
1789 | var objectToNumericMap = (object) => {
1790 | const map = /* @__PURE__ */ new Map();
1791 | for (const key of Object.keys(object)) {
1792 | map.set(parseInt(key, 10), object[key]);
1793 | }
1794 | return map;
1795 | };
1796 | var objectToNumericMapAsync = async (object) => {
1797 | const map = /* @__PURE__ */ new Map();
1798 | let count = 0;
1799 | for (const key of Object.keys(object)) {
1800 | map.set(parseInt(key, 10), object[key]);
1801 | if (++count % 1e3 === 0) {
1802 | await wait(0);
1803 | }
1804 | }
1805 | return map;
1806 | };
1807 | var wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
1808 | var SPACE_OR_PUNCTUATION = /[\n\r\p{Z}\p{P}]+/u;
1809 | export {
1810 | MiniSearch as default
1811 | };
1812 | //# sourceMappingURL=vitepress___minisearch.js.map
1813 |
```